重构动物模型和路由系统,优化查询逻辑并新增商户和促销活动功能
This commit is contained in:
420
backend/docs/商户管理API接口文档.md
Normal file
420
backend/docs/商户管理API接口文档.md
Normal file
@@ -0,0 +1,420 @@
|
||||
# 商户管理API接口文档
|
||||
|
||||
## 概述
|
||||
|
||||
商户管理模块提供了完整的商户信息管理功能,包括商户的增删改查、统计信息等操作。所有接口均遵循RESTful API设计规范。
|
||||
|
||||
## 基础信息
|
||||
|
||||
- **基础URL**: `/api/v1/merchants`
|
||||
- **认证方式**: Bearer Token(部分接口需要管理员权限)
|
||||
- **数据格式**: JSON
|
||||
- **字符编码**: UTF-8
|
||||
|
||||
## 数据模型
|
||||
|
||||
### 商户信息 (Merchant)
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"name": "示例商户",
|
||||
"type": "company",
|
||||
"contact_person": "张三",
|
||||
"contact_phone": "13800138000",
|
||||
"email": "merchant@example.com",
|
||||
"address": "北京市朝阳区示例街道123号",
|
||||
"description": "这是一个示例商户",
|
||||
"status": "active",
|
||||
"created_at": "2024-01-01T00:00:00.000Z",
|
||||
"updated_at": "2024-01-01T00:00:00.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
### 字段说明
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| id | integer | - | 商户ID(系统自动生成) |
|
||||
| name | string | ✓ | 商户名称 |
|
||||
| type | string | ✓ | 商户类型:`individual`(个人)、`company`(企业) |
|
||||
| contact_person | string | ✓ | 联系人姓名 |
|
||||
| contact_phone | string | ✓ | 联系电话 |
|
||||
| email | string | - | 邮箱地址 |
|
||||
| address | string | - | 地址 |
|
||||
| description | string | - | 商户描述 |
|
||||
| status | string | - | 状态:`active`(活跃)、`inactive`(非活跃)、`banned`(禁用) |
|
||||
| created_at | datetime | - | 创建时间 |
|
||||
| updated_at | datetime | - | 更新时间 |
|
||||
|
||||
## 接口列表
|
||||
|
||||
### 1. 获取商户列表
|
||||
|
||||
**接口地址**: `GET /api/v1/merchants`
|
||||
|
||||
**接口描述**: 获取商户列表,支持分页、搜索和筛选
|
||||
|
||||
**请求参数**:
|
||||
|
||||
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|
||||
|------|------|------|--------|------|
|
||||
| page | integer | - | 1 | 页码(最小值:1) |
|
||||
| limit | integer | - | 20 | 每页数量(范围:1-100) |
|
||||
| keyword | string | - | - | 搜索关键词(匹配商户名称、联系人、电话) |
|
||||
| status | string | - | - | 状态筛选:`active`、`inactive`、`banned` |
|
||||
| type | string | - | - | 类型筛选:`individual`、`company` |
|
||||
|
||||
**请求示例**:
|
||||
```http
|
||||
GET /api/v1/merchants?page=1&limit=20&keyword=示例&status=active&type=company
|
||||
```
|
||||
|
||||
**响应示例**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "示例商户",
|
||||
"type": "company",
|
||||
"contact_person": "张三",
|
||||
"contact_phone": "13800138000",
|
||||
"email": "merchant@example.com",
|
||||
"address": "北京市朝阳区示例街道123号",
|
||||
"description": "这是一个示例商户",
|
||||
"status": "active",
|
||||
"created_at": "2024-01-01T00:00:00.000Z",
|
||||
"updated_at": "2024-01-01T00:00:00.000Z"
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"page": 1,
|
||||
"limit": 20,
|
||||
"total": 1,
|
||||
"totalPages": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**错误响应**:
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "请求参数错误",
|
||||
"errors": [
|
||||
{
|
||||
"field": "page",
|
||||
"message": "页码必须是正整数"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 获取商户详情
|
||||
|
||||
**接口地址**: `GET /api/v1/merchants/{merchantId}`
|
||||
|
||||
**接口描述**: 根据商户ID获取商户详细信息
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| merchantId | integer | ✓ | 商户ID |
|
||||
|
||||
**请求示例**:
|
||||
```http
|
||||
GET /api/v1/merchants/1
|
||||
```
|
||||
|
||||
**响应示例**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": 1,
|
||||
"name": "示例商户",
|
||||
"type": "company",
|
||||
"contact_person": "张三",
|
||||
"contact_phone": "13800138000",
|
||||
"email": "merchant@example.com",
|
||||
"address": "北京市朝阳区示例街道123号",
|
||||
"description": "这是一个示例商户",
|
||||
"status": "active",
|
||||
"created_at": "2024-01-01T00:00:00.000Z",
|
||||
"updated_at": "2024-01-01T00:00:00.000Z",
|
||||
"animal_count": 15,
|
||||
"order_count": 128
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**错误响应**:
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "商户不存在"
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 创建商户
|
||||
|
||||
**接口地址**: `POST /api/v1/merchants`
|
||||
|
||||
**接口描述**: 创建新商户(需要管理员权限)
|
||||
|
||||
**认证要求**: Bearer Token + 管理员权限
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"name": "新商户",
|
||||
"type": "company",
|
||||
"contact_person": "李四",
|
||||
"contact_phone": "13900139000",
|
||||
"email": "newmerchant@example.com",
|
||||
"address": "上海市浦东新区示例路456号",
|
||||
"description": "这是一个新商户"
|
||||
}
|
||||
```
|
||||
|
||||
**请求参数**:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| name | string | ✓ | 商户名称 |
|
||||
| type | string | ✓ | 商户类型:`individual`、`company` |
|
||||
| contact_person | string | ✓ | 联系人姓名 |
|
||||
| contact_phone | string | ✓ | 联系电话 |
|
||||
| email | string | - | 邮箱地址(需符合邮箱格式) |
|
||||
| address | string | - | 地址 |
|
||||
| description | string | - | 商户描述 |
|
||||
|
||||
**响应示例**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": 2,
|
||||
"name": "新商户",
|
||||
"type": "company",
|
||||
"contact_person": "李四",
|
||||
"contact_phone": "13900139000",
|
||||
"email": "newmerchant@example.com",
|
||||
"address": "上海市浦东新区示例路456号",
|
||||
"description": "这是一个新商户",
|
||||
"status": "active",
|
||||
"created_at": "2024-01-01T12:00:00.000Z",
|
||||
"updated_at": "2024-01-01T12:00:00.000Z"
|
||||
},
|
||||
"message": "商户创建成功"
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 更新商户信息
|
||||
|
||||
**接口地址**: `PUT /api/v1/merchants/{merchantId}`
|
||||
|
||||
**接口描述**: 更新商户信息(需要管理员权限)
|
||||
|
||||
**认证要求**: Bearer Token + 管理员权限
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| merchantId | integer | ✓ | 商户ID |
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"name": "更新后的商户名称",
|
||||
"contact_person": "王五",
|
||||
"contact_phone": "13700137000",
|
||||
"status": "inactive"
|
||||
}
|
||||
```
|
||||
|
||||
**请求参数**:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| name | string | - | 商户名称 |
|
||||
| type | string | - | 商户类型:`individual`、`company` |
|
||||
| contact_person | string | - | 联系人姓名 |
|
||||
| contact_phone | string | - | 联系电话 |
|
||||
| email | string | - | 邮箱地址 |
|
||||
| address | string | - | 地址 |
|
||||
| description | string | - | 商户描述 |
|
||||
| status | string | - | 状态:`active`、`inactive`、`banned` |
|
||||
|
||||
**响应示例**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": 1,
|
||||
"name": "更新后的商户名称",
|
||||
"type": "company",
|
||||
"contact_person": "王五",
|
||||
"contact_phone": "13700137000",
|
||||
"email": "merchant@example.com",
|
||||
"address": "北京市朝阳区示例街道123号",
|
||||
"description": "这是一个示例商户",
|
||||
"status": "inactive",
|
||||
"created_at": "2024-01-01T00:00:00.000Z",
|
||||
"updated_at": "2024-01-01T15:30:00.000Z"
|
||||
},
|
||||
"message": "商户信息更新成功"
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 删除商户
|
||||
|
||||
**接口地址**: `DELETE /api/v1/merchants/{merchantId}`
|
||||
|
||||
**接口描述**: 删除商户(需要管理员权限)
|
||||
|
||||
**认证要求**: Bearer Token + 管理员权限
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| merchantId | integer | ✓ | 商户ID |
|
||||
|
||||
**请求示例**:
|
||||
```http
|
||||
DELETE /api/v1/merchants/1
|
||||
```
|
||||
|
||||
**响应示例**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "商户删除成功"
|
||||
}
|
||||
```
|
||||
|
||||
### 6. 获取商户统计信息
|
||||
|
||||
**接口地址**: `GET /api/v1/merchants/statistics`
|
||||
|
||||
**接口描述**: 获取商户统计信息(需要管理员权限)
|
||||
|
||||
**认证要求**: Bearer Token + 管理员权限
|
||||
|
||||
**请求示例**:
|
||||
```http
|
||||
GET /api/v1/merchants/statistics
|
||||
```
|
||||
|
||||
**响应示例**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"total": 150,
|
||||
"active": 120,
|
||||
"inactive": 25,
|
||||
"banned": 5,
|
||||
"individual": 80,
|
||||
"company": 70
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 错误码说明
|
||||
|
||||
| HTTP状态码 | 错误码 | 说明 |
|
||||
|------------|--------|------|
|
||||
| 400 | BAD_REQUEST | 请求参数错误 |
|
||||
| 401 | UNAUTHORIZED | 未授权,需要登录 |
|
||||
| 403 | FORBIDDEN | 权限不足,需要管理员权限 |
|
||||
| 404 | NOT_FOUND | 商户不存在 |
|
||||
| 409 | CONFLICT | 商户信息冲突(如名称重复) |
|
||||
| 500 | INTERNAL_ERROR | 服务器内部错误 |
|
||||
|
||||
## 通用错误响应格式
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "错误描述",
|
||||
"code": "ERROR_CODE",
|
||||
"timestamp": "2024-01-01T12:00:00.000Z",
|
||||
"errors": [
|
||||
{
|
||||
"field": "字段名",
|
||||
"message": "字段错误描述"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
|
||||
### JavaScript (Axios)
|
||||
|
||||
```javascript
|
||||
// 获取商户列表
|
||||
const getMerchants = async (params = {}) => {
|
||||
try {
|
||||
const response = await axios.get('/api/v1/merchants', { params });
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('获取商户列表失败:', error.response.data);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// 创建商户
|
||||
const createMerchant = async (merchantData) => {
|
||||
try {
|
||||
const response = await axios.post('/api/v1/merchants', merchantData, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('创建商户失败:', error.response.data);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### cURL
|
||||
|
||||
```bash
|
||||
# 获取商户列表
|
||||
curl -X GET "http://localhost:3200/api/v1/merchants?page=1&limit=20" \
|
||||
-H "Content-Type: application/json"
|
||||
|
||||
# 创建商户
|
||||
curl -X POST "http://localhost:3200/api/v1/merchants" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-d '{
|
||||
"name": "测试商户",
|
||||
"type": "company",
|
||||
"contact_person": "测试联系人",
|
||||
"contact_phone": "13800138000"
|
||||
}'
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **权限控制**: 创建、更新、删除商户以及获取统计信息需要管理员权限
|
||||
2. **数据验证**: 所有输入数据都会进行严格验证,确保数据完整性
|
||||
3. **分页限制**: 列表接口每页最多返回100条记录
|
||||
4. **搜索功能**: 关键词搜索支持模糊匹配商户名称、联系人和电话
|
||||
5. **状态管理**: 商户状态变更会影响相关业务功能的可用性
|
||||
6. **数据关联**: 删除商户前请确保没有关联的动物或订单数据
|
||||
|
||||
## 更新日志
|
||||
|
||||
- **v1.0.0** (2024-01-01): 初始版本,包含基础的商户管理功能
|
||||
@@ -1,4 +1,4 @@
|
||||
-- 解班客数据库完整结构创建脚本
|
||||
-- 结伴客数据库完整结构创建脚本
|
||||
-- 创建时间: 2024年
|
||||
-- 数据库: jbkdata
|
||||
|
||||
|
||||
@@ -46,6 +46,44 @@ CREATE TABLE IF NOT EXISTS orders (
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- 创建促销活动表
|
||||
CREATE TABLE IF NOT EXISTS promotion_activities (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
description TEXT,
|
||||
type ENUM('signup', 'invitation', 'purchase', 'custom') NOT NULL,
|
||||
status ENUM('active', 'inactive', 'expired') DEFAULT 'active',
|
||||
start_date DATE NOT NULL,
|
||||
end_date DATE NOT NULL,
|
||||
reward_type ENUM('cash', 'points', 'coupon') NOT NULL,
|
||||
reward_amount DECIMAL(15,2) NOT NULL,
|
||||
participation_limit INT DEFAULT 0 COMMENT '0表示无限制',
|
||||
current_participants INT DEFAULT 0,
|
||||
rules JSON COMMENT '活动规则配置',
|
||||
created_by INT NOT NULL COMMENT '创建人ID',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (created_by) REFERENCES admins(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- 创建奖励记录表
|
||||
CREATE TABLE IF NOT EXISTS promotion_rewards (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
user_name VARCHAR(100) NOT NULL,
|
||||
user_phone VARCHAR(20),
|
||||
activity_id INT NOT NULL,
|
||||
activity_name VARCHAR(100) NOT NULL,
|
||||
reward_type ENUM('cash', 'points', 'coupon') NOT NULL,
|
||||
reward_amount DECIMAL(15,2) NOT NULL,
|
||||
status ENUM('pending', 'issued', 'failed') DEFAULT 'pending',
|
||||
issued_at TIMESTAMP NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (activity_id) REFERENCES promotion_activities(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- 插入默认管理员账号
|
||||
INSERT INTO admins (username, password, email, role) VALUES
|
||||
('admin', '$2b$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'admin@jiebanke.com', 'super_admin'),
|
||||
@@ -64,4 +102,11 @@ CREATE INDEX idx_users_email ON users(email);
|
||||
CREATE INDEX idx_users_phone ON users(phone);
|
||||
CREATE INDEX idx_orders_user_id ON orders(user_id);
|
||||
CREATE INDEX idx_orders_order_no ON orders(order_no);
|
||||
CREATE INDEX idx_orders_status ON orders(status);
|
||||
CREATE INDEX idx_orders_status ON orders(status);
|
||||
CREATE INDEX idx_promotion_activities_status ON promotion_activities(status);
|
||||
CREATE INDEX idx_promotion_activities_type ON promotion_activities(type);
|
||||
CREATE INDEX idx_promotion_activities_dates ON promotion_activities(start_date, end_date);
|
||||
CREATE INDEX idx_promotion_rewards_user_id ON promotion_rewards(user_id);
|
||||
CREATE INDEX idx_promotion_rewards_activity_id ON promotion_rewards(activity_id);
|
||||
CREATE INDEX idx_promotion_rewards_status ON promotion_rewards(status);
|
||||
CREATE INDEX idx_promotion_rewards_created_at ON promotion_rewards(created_at);
|
||||
@@ -8,8 +8,9 @@
|
||||
const mysql = require('mysql2/promise');
|
||||
const config = require('../config/env');
|
||||
|
||||
// 引入database.js配置
|
||||
const dbConfig = require('../src/config/database').pool.config;
|
||||
// 引入环境配置
|
||||
const envConfig = require('../config/env');
|
||||
const dbConfig = envConfig.mysql;
|
||||
|
||||
// 数据库配置已从database.js导入
|
||||
|
||||
|
||||
@@ -15,13 +15,13 @@ const { globalErrorHandler, notFound } = require('./utils/errors');
|
||||
// 检查是否为无数据库模式
|
||||
const NO_DB_MODE = process.env.NO_DB_MODE === 'true';
|
||||
|
||||
let authRoutes, userRoutes, travelRoutes, animalRoutes, orderRoutes, adminRoutes, travelRegistrationRoutes;
|
||||
let authRoutes, userRoutes, travelRoutes, animalRoutes, orderRoutes, adminRoutes, travelRegistrationRoutes, promotionRoutes, merchantRoutes;
|
||||
|
||||
// 路由导入 - 根据是否为无数据库模式决定是否导入实际路由
|
||||
// 路由导入
|
||||
if (NO_DB_MODE) {
|
||||
console.log('⚠️ 无数据库模式:将使用模拟路由');
|
||||
} else {
|
||||
// 路由导入
|
||||
console.log('✅ 数据库模式:加载实际路由');
|
||||
authRoutes = require('./routes/auth');
|
||||
userRoutes = require('./routes/user');
|
||||
travelRoutes = require('./routes/travel');
|
||||
@@ -31,6 +31,8 @@ if (NO_DB_MODE) {
|
||||
travelRegistrationRoutes = require('./routes/travelRegistration'); // 旅行报名路由
|
||||
paymentRoutes = require('./routes/payment-simple');
|
||||
animalClaimRoutes = require('./routes/animalClaim-simple'); // 动物认领路由(简化版)
|
||||
promotionRoutes = require('./routes/promotion'); // 促销活动路由
|
||||
merchantRoutes = require('./routes/merchant'); // 商户路由
|
||||
}
|
||||
|
||||
const app = express();
|
||||
@@ -50,8 +52,10 @@ app.use(cors({
|
||||
'https://webapi.jiebanke.com',
|
||||
'http://localhost:3150', // 管理后台本地开发地址
|
||||
'http://localhost:3000', // 备用端口
|
||||
'http://localhost:3200', // 备用端口
|
||||
'http://127.0.0.1:3150', // 备用地址
|
||||
'http://127.0.0.1:3000' // 备用地址
|
||||
'http://127.0.0.1:3000', // 备用地址
|
||||
'http://127.0.0.1:3200' // 备用地址
|
||||
],
|
||||
credentials: true,
|
||||
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
||||
@@ -132,7 +136,8 @@ app.get('/api/v1', (req, res) => {
|
||||
payments: '/api/v1/payments',
|
||||
animalClaims: '/api/v1/animal-claims',
|
||||
admin: '/api/v1/admin',
|
||||
travelRegistration: '/api/v1/travel-registration'
|
||||
travelRegistration: '/api/v1/travel-registration',
|
||||
promotion: '/api/v1/promotion'
|
||||
},
|
||||
documentation: 'https://webapi.jiebanke.com/api-docs'
|
||||
});
|
||||
@@ -239,6 +244,20 @@ if (NO_DB_MODE) {
|
||||
message: '当前为无数据库模式,管理员功能不可用'
|
||||
});
|
||||
});
|
||||
|
||||
app.use('/api/v1/promotion', (req, res) => {
|
||||
res.status(503).json({
|
||||
success: false,
|
||||
message: '当前为无数据库模式,促销活动功能不可用'
|
||||
});
|
||||
});
|
||||
|
||||
app.use('/api/v1/merchants', (req, res) => {
|
||||
res.status(503).json({
|
||||
success: false,
|
||||
message: '当前为无数据库模式,商户功能不可用'
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// API路由
|
||||
app.use('/api/v1/auth', authRoutes);
|
||||
@@ -253,6 +272,10 @@ if (NO_DB_MODE) {
|
||||
app.use('/api/v1/admin', adminRoutes);
|
||||
// 旅行报名路由
|
||||
app.use('/api/v1/travel-registration', travelRegistrationRoutes);
|
||||
// 促销活动路由
|
||||
app.use('/api/v1/promotion', promotionRoutes);
|
||||
// 商户路由
|
||||
app.use('/api/v1/merchants', merchantRoutes);
|
||||
}
|
||||
|
||||
// 404处理
|
||||
|
||||
@@ -48,6 +48,12 @@ const query = async (sql, params = []) => {
|
||||
let connection;
|
||||
try {
|
||||
connection = await pool.getConnection();
|
||||
|
||||
// 添加调试信息
|
||||
console.log('执行SQL:', sql);
|
||||
console.log('参数:', params);
|
||||
console.log('参数类型:', params.map(p => typeof p));
|
||||
|
||||
const [results] = await connection.execute(sql, params);
|
||||
connection.release();
|
||||
return results;
|
||||
@@ -55,6 +61,9 @@ const query = async (sql, params = []) => {
|
||||
if (connection) {
|
||||
connection.release();
|
||||
}
|
||||
console.error('SQL执行错误:', error);
|
||||
console.error('SQL语句:', sql);
|
||||
console.error('参数:', params);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -249,45 +249,45 @@ const getDashboardStatistics = async () => {
|
||||
const todayEnd = new Date(todayStart.getTime() + 24 * 60 * 60 * 1000);
|
||||
|
||||
// 总用户数
|
||||
const [totalUsersResult] = await query('SELECT COUNT(*) as count FROM users WHERE deleted_at IS NULL');
|
||||
const [totalUsersResult] = await query('SELECT COUNT(*) as count FROM users');
|
||||
const totalUsers = totalUsersResult.count;
|
||||
|
||||
// 今日新增用户
|
||||
const [todayUsersResult] = await query(
|
||||
'SELECT COUNT(*) as count FROM users WHERE created_at >= ? AND created_at < ? AND deleted_at IS NULL',
|
||||
'SELECT COUNT(*) as count FROM users WHERE created_at >= ? AND created_at < ?',
|
||||
[todayStart, todayEnd]
|
||||
);
|
||||
const todayNewUsers = todayUsersResult.count;
|
||||
|
||||
// 总动物数
|
||||
const [totalAnimalsResult] = await query('SELECT COUNT(*) as count FROM animals WHERE deleted_at IS NULL');
|
||||
const [totalAnimalsResult] = await query('SELECT COUNT(*) as count FROM animals');
|
||||
const totalAnimals = totalAnimalsResult.count;
|
||||
|
||||
// 今日新增动物
|
||||
const [todayAnimalsResult] = await query(
|
||||
'SELECT COUNT(*) as count FROM animals WHERE created_at >= ? AND created_at < ? AND deleted_at IS NULL',
|
||||
'SELECT COUNT(*) as count FROM animals WHERE created_at >= ? AND created_at < ?',
|
||||
[todayStart, todayEnd]
|
||||
);
|
||||
const todayNewAnimals = todayAnimalsResult.count;
|
||||
|
||||
// 总旅行数
|
||||
const [totalTravelsResult] = await query('SELECT COUNT(*) as count FROM travels WHERE deleted_at IS NULL');
|
||||
const [totalTravelsResult] = await query('SELECT COUNT(*) as count FROM travels');
|
||||
const totalTravels = totalTravelsResult.count;
|
||||
|
||||
// 今日新增旅行
|
||||
const [todayTravelsResult] = await query(
|
||||
'SELECT COUNT(*) as count FROM travels WHERE created_at >= ? AND created_at < ? AND deleted_at IS NULL',
|
||||
'SELECT COUNT(*) as count FROM travels WHERE created_at >= ? AND created_at < ?',
|
||||
[todayStart, todayEnd]
|
||||
);
|
||||
const todayNewTravels = todayTravelsResult.count;
|
||||
|
||||
// 总认领数
|
||||
const [totalClaimsResult] = await query('SELECT COUNT(*) as count FROM animal_claims WHERE deleted_at IS NULL');
|
||||
const [totalClaimsResult] = await query('SELECT COUNT(*) as count FROM animal_claims');
|
||||
const totalClaims = totalClaimsResult.count;
|
||||
|
||||
// 今日新增认领
|
||||
const [todayClaimsResult] = await query(
|
||||
'SELECT COUNT(*) as count FROM animal_claims WHERE created_at >= ? AND created_at < ? AND deleted_at IS NULL',
|
||||
'SELECT COUNT(*) as count FROM animal_claims WHERE created_at >= ? AND created_at < ?',
|
||||
[todayStart, todayEnd]
|
||||
);
|
||||
const todayNewClaims = todayClaimsResult.count;
|
||||
@@ -327,7 +327,7 @@ const getRecentActivities = async () => {
|
||||
const recentUsers = await query(`
|
||||
SELECT id, nickname, created_at
|
||||
FROM users
|
||||
WHERE deleted_at IS NULL
|
||||
WHERE status != 'banned'
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 5
|
||||
`);
|
||||
@@ -348,8 +348,7 @@ const getRecentActivities = async () => {
|
||||
const recentAnimals = await query(`
|
||||
SELECT a.id, a.name, a.created_at, u.id as user_id, u.nickname as user_nickname
|
||||
FROM animals a
|
||||
LEFT JOIN users u ON a.user_id = u.id
|
||||
WHERE a.deleted_at IS NULL
|
||||
LEFT JOIN users u ON a.farmer_id = u.id
|
||||
ORDER BY a.created_at DESC
|
||||
LIMIT 5
|
||||
`);
|
||||
|
||||
@@ -6,13 +6,13 @@ class AnimalController {
|
||||
// 获取动物列表
|
||||
static async getAnimals(req, res, next) {
|
||||
try {
|
||||
const { page, pageSize, species, status } = req.query;
|
||||
const { page, pageSize, type, status } = req.query;
|
||||
|
||||
const result = await AnimalService.getAnimals({
|
||||
merchantId: req.userId,
|
||||
page: parseInt(page) || 1,
|
||||
pageSize: parseInt(pageSize) || 10,
|
||||
species,
|
||||
type,
|
||||
status
|
||||
});
|
||||
|
||||
@@ -43,7 +43,7 @@ class AnimalController {
|
||||
try {
|
||||
const {
|
||||
name,
|
||||
species,
|
||||
type,
|
||||
breed,
|
||||
age,
|
||||
gender,
|
||||
@@ -51,26 +51,30 @@ class AnimalController {
|
||||
description,
|
||||
images,
|
||||
health_status,
|
||||
vaccination_status
|
||||
vaccination_records,
|
||||
farm_location,
|
||||
contact_info
|
||||
} = req.body;
|
||||
|
||||
// 验证必要字段
|
||||
if (!name || !species || !price) {
|
||||
throw new AppError('缺少必要字段: name, species, price', 400);
|
||||
if (!name || !type || !price) {
|
||||
throw new AppError('缺少必要字段: name, type, price', 400);
|
||||
}
|
||||
|
||||
const animalData = {
|
||||
merchant_id: req.userId,
|
||||
name,
|
||||
species,
|
||||
type,
|
||||
breed: breed || null,
|
||||
age: age || null,
|
||||
gender: gender || null,
|
||||
price: parseFloat(price),
|
||||
description: description || null,
|
||||
images: images || null,
|
||||
images: images || [],
|
||||
health_status: health_status || null,
|
||||
vaccination_status: vaccination_status || null,
|
||||
vaccination_records: vaccination_records || [],
|
||||
farm_location: farm_location || null,
|
||||
contact_info: contact_info || {},
|
||||
status: 'available'
|
||||
};
|
||||
|
||||
|
||||
276
backend/src/controllers/merchant.js
Normal file
276
backend/src/controllers/merchant.js
Normal file
@@ -0,0 +1,276 @@
|
||||
const Merchant = require('../models/Merchant');
|
||||
|
||||
/**
|
||||
* 获取商户列表
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
*/
|
||||
async function getMerchantList(req, res, next) {
|
||||
try {
|
||||
const {
|
||||
page = 1,
|
||||
limit = 20,
|
||||
keyword = '',
|
||||
status = '',
|
||||
type = ''
|
||||
} = req.query;
|
||||
|
||||
// 参数验证
|
||||
const pageNum = parseInt(page);
|
||||
const limitNum = parseInt(limit);
|
||||
|
||||
if (pageNum < 1) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '页码必须大于0'
|
||||
});
|
||||
}
|
||||
|
||||
if (limitNum < 1 || limitNum > 100) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '每页数量必须在1-100之间'
|
||||
});
|
||||
}
|
||||
|
||||
const options = {
|
||||
page: pageNum,
|
||||
limit: limitNum,
|
||||
keyword: keyword.trim(),
|
||||
status,
|
||||
type
|
||||
};
|
||||
|
||||
const result = await Merchant.getMerchantList(options);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result.merchants,
|
||||
pagination: result.pagination
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取商户列表控制器错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || '获取商户列表失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取商户详情
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
*/
|
||||
async function getMerchantDetail(req, res, next) {
|
||||
try {
|
||||
const { merchantId } = req.params;
|
||||
|
||||
if (!merchantId || isNaN(merchantId)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '商户ID无效'
|
||||
});
|
||||
}
|
||||
|
||||
const merchant = await Merchant.getMerchantDetail(parseInt(merchantId));
|
||||
|
||||
if (!merchant) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '商户不存在'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: merchant
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取商户详情控制器错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || '获取商户详情失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建商户
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
*/
|
||||
async function createMerchant(req, res, next) {
|
||||
try {
|
||||
const merchantData = req.body;
|
||||
|
||||
// 验证必要字段
|
||||
const requiredFields = ['name', 'type', 'contact_person', 'contact_phone'];
|
||||
for (const field of requiredFields) {
|
||||
if (!merchantData[field]) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: `缺少必要字段: ${field}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 验证商户类型
|
||||
if (!['individual', 'company'].includes(merchantData.type)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '商户类型必须是 individual 或 company'
|
||||
});
|
||||
}
|
||||
|
||||
const merchant = await Merchant.create(merchantData);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: merchant,
|
||||
message: '商户创建成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('创建商户控制器错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || '创建商户失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新商户信息
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
*/
|
||||
async function updateMerchant(req, res, next) {
|
||||
try {
|
||||
const { merchantId } = req.params;
|
||||
const merchantData = req.body;
|
||||
|
||||
if (!merchantId || isNaN(merchantId)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '商户ID无效'
|
||||
});
|
||||
}
|
||||
|
||||
// 检查商户是否存在
|
||||
const existingMerchant = await Merchant.findById(parseInt(merchantId));
|
||||
if (!existingMerchant) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '商户不存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 验证商户类型(如果提供)
|
||||
if (merchantData.type && !['individual', 'company'].includes(merchantData.type)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '商户类型必须是 individual 或 company'
|
||||
});
|
||||
}
|
||||
|
||||
// 验证状态(如果提供)
|
||||
if (merchantData.status && !['active', 'inactive', 'banned'].includes(merchantData.status)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '商户状态必须是 active、inactive 或 banned'
|
||||
});
|
||||
}
|
||||
|
||||
const updatedMerchant = await Merchant.update(parseInt(merchantId), merchantData);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: updatedMerchant,
|
||||
message: '商户信息更新成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('更新商户控制器错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || '更新商户失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除商户
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
*/
|
||||
async function deleteMerchant(req, res, next) {
|
||||
try {
|
||||
const { merchantId } = req.params;
|
||||
|
||||
if (!merchantId || isNaN(merchantId)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '商户ID无效'
|
||||
});
|
||||
}
|
||||
|
||||
// 检查商户是否存在
|
||||
const existingMerchant = await Merchant.findById(parseInt(merchantId));
|
||||
if (!existingMerchant) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '商户不存在'
|
||||
});
|
||||
}
|
||||
|
||||
const deleted = await Merchant.delete(parseInt(merchantId));
|
||||
|
||||
if (deleted) {
|
||||
res.json({
|
||||
success: true,
|
||||
message: '商户删除成功'
|
||||
});
|
||||
} else {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '删除商户失败'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除商户控制器错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || '删除商户失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取商户统计信息
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
*/
|
||||
async function getMerchantStatistics(req, res, next) {
|
||||
try {
|
||||
const statistics = await Merchant.getStatistics();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: statistics
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取商户统计控制器错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || '获取商户统计失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getMerchantList,
|
||||
getMerchantDetail,
|
||||
createMerchant,
|
||||
updateMerchant,
|
||||
deleteMerchant,
|
||||
getMerchantStatistics
|
||||
};
|
||||
461
backend/src/controllers/promotion/activityController.js
Normal file
461
backend/src/controllers/promotion/activityController.js
Normal file
@@ -0,0 +1,461 @@
|
||||
/**
|
||||
* 推广活动控制器
|
||||
* @module controllers/promotion/activityController
|
||||
*/
|
||||
|
||||
const db = require('../../config/database');
|
||||
|
||||
/**
|
||||
* 获取推广活动列表
|
||||
* @function getActivities
|
||||
* @param {Object} req - Express请求对象
|
||||
* @param {Object} res - Express响应对象
|
||||
* @param {Function} next - Express中间件next函数
|
||||
*/
|
||||
exports.getActivities = async (req, res, next) => {
|
||||
try {
|
||||
const {
|
||||
page = 1,
|
||||
pageSize = 20,
|
||||
name = '',
|
||||
status = ''
|
||||
} = req.query;
|
||||
|
||||
const offset = (page - 1) * pageSize;
|
||||
const limit = parseInt(pageSize);
|
||||
|
||||
let whereConditions = [];
|
||||
let queryParams = [];
|
||||
|
||||
if (name) {
|
||||
whereConditions.push('name LIKE ?');
|
||||
queryParams.push(`%${name}%`);
|
||||
}
|
||||
|
||||
if (status) {
|
||||
whereConditions.push('status = ?');
|
||||
queryParams.push(status);
|
||||
}
|
||||
|
||||
const whereClause = whereConditions.length > 0
|
||||
? `WHERE ${whereConditions.join(' AND ')}`
|
||||
: '';
|
||||
|
||||
// 获取总数
|
||||
const countSql = `SELECT COUNT(*) as total FROM promotion_activities ${whereClause}`;
|
||||
const countResult = await db.query(countSql, queryParams);
|
||||
const total = countResult[0].total;
|
||||
|
||||
// 获取数据
|
||||
const dataSql = `
|
||||
SELECT
|
||||
id, name, description, reward_type, reward_amount, status,
|
||||
start_time, end_time, max_participants, current_participants,
|
||||
created_at, updated_at
|
||||
FROM promotion_activities
|
||||
${whereClause}
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
`;
|
||||
|
||||
const dataParams = [...queryParams, limit, offset];
|
||||
const activities = await db.query(dataSql, dataParams);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
code: 200,
|
||||
message: '获取成功',
|
||||
data: activities,
|
||||
pagination: {
|
||||
current: parseInt(page),
|
||||
pageSize: limit,
|
||||
total,
|
||||
totalPages: Math.ceil(total / limit)
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取推广活动详情
|
||||
* @function getActivity
|
||||
* @param {Object} req - Express请求对象
|
||||
* @param {Object} res - Express响应对象
|
||||
* @param {Function} next - Express中间件next函数
|
||||
*/
|
||||
exports.getActivity = async (req, res, next) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const sql = `
|
||||
SELECT
|
||||
id, name, description, reward_type, reward_amount, status,
|
||||
start_time, end_time, max_participants, current_participants,
|
||||
created_at, updated_at
|
||||
FROM promotion_activities
|
||||
WHERE id = ?
|
||||
`;
|
||||
|
||||
const activities = await db.query(sql, [id]);
|
||||
|
||||
if (activities.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
code: 404,
|
||||
message: '推广活动不存在'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
code: 200,
|
||||
message: '获取成功',
|
||||
data: activities[0]
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 创建推广活动
|
||||
* @function createActivity
|
||||
* @param {Object} req - Express请求对象
|
||||
* @param {Object} res - Express响应对象
|
||||
* @param {Function} next - Express中间件next函数
|
||||
*/
|
||||
exports.createActivity = async (req, res, next) => {
|
||||
try {
|
||||
const {
|
||||
name,
|
||||
description,
|
||||
reward_type,
|
||||
reward_amount,
|
||||
status = 'upcoming',
|
||||
start_time,
|
||||
end_time,
|
||||
max_participants = 0
|
||||
} = req.body;
|
||||
|
||||
// 验证必填字段
|
||||
if (!name || !reward_type || !reward_amount || !start_time || !end_time) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
code: 400,
|
||||
message: '缺少必填字段'
|
||||
});
|
||||
}
|
||||
|
||||
const sql = `
|
||||
INSERT INTO promotion_activities
|
||||
(name, description, reward_type, reward_amount, status, start_time, end_time, max_participants, current_participants)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0)
|
||||
`;
|
||||
|
||||
const result = await db.query(sql, [
|
||||
name,
|
||||
description,
|
||||
reward_type,
|
||||
reward_amount,
|
||||
status,
|
||||
start_time,
|
||||
end_time,
|
||||
max_participants
|
||||
]);
|
||||
|
||||
// 获取新创建的活动
|
||||
const newActivity = await db.query(
|
||||
'SELECT * FROM promotion_activities WHERE id = ?',
|
||||
[result.insertId]
|
||||
);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
code: 201,
|
||||
message: '创建成功',
|
||||
data: newActivity[0]
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新推广活动
|
||||
* @function updateActivity
|
||||
* @param {Object} req - Express请求对象
|
||||
* @param {Object} res - Express响应对象
|
||||
* @param {Function} next - Express中间件next函数
|
||||
*/
|
||||
exports.updateActivity = async (req, res, next) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const updates = req.body;
|
||||
|
||||
// 检查活动是否存在
|
||||
const existingActivity = await db.query(
|
||||
'SELECT id FROM promotion_activities WHERE id = ?',
|
||||
[id]
|
||||
);
|
||||
|
||||
if (existingActivity.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
code: 404,
|
||||
message: '推广活动不存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 构建更新字段
|
||||
const updateFields = [];
|
||||
const updateValues = [];
|
||||
|
||||
const allowedFields = [
|
||||
'name', 'description', 'reward_type', 'reward_amount',
|
||||
'status', 'start_time', 'end_time', 'max_participants'
|
||||
];
|
||||
|
||||
Object.keys(updates).forEach(key => {
|
||||
if (allowedFields.includes(key) && updates[key] !== undefined) {
|
||||
updateFields.push(`${key} = ?`);
|
||||
updateValues.push(updates[key]);
|
||||
}
|
||||
});
|
||||
|
||||
if (updateFields.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
code: 400,
|
||||
message: '没有有效的更新字段'
|
||||
});
|
||||
}
|
||||
|
||||
updateValues.push(id);
|
||||
|
||||
const sql = `
|
||||
UPDATE promotion_activities
|
||||
SET ${updateFields.join(', ')}, updated_at = NOW()
|
||||
WHERE id = ?
|
||||
`;
|
||||
|
||||
await db.query(sql, updateValues);
|
||||
|
||||
// 获取更新后的活动
|
||||
const updatedActivity = await db.query(
|
||||
'SELECT * FROM promotion_activities WHERE id = ?',
|
||||
[id]
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
code: 200,
|
||||
message: '更新成功',
|
||||
data: updatedActivity[0]
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 删除推广活动
|
||||
* @function deleteActivity
|
||||
* @param {Object} req - Express请求对象
|
||||
* @param {Object} res - Express响应对象
|
||||
* @param {Function} next - Express中间件next函数
|
||||
*/
|
||||
exports.deleteActivity = async (req, res, next) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
// 检查活动是否存在
|
||||
const existingActivity = await db.query(
|
||||
'SELECT id FROM promotion_activities WHERE id = ?',
|
||||
[id]
|
||||
);
|
||||
|
||||
if (existingActivity.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
code: 404,
|
||||
message: '推广活动不存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 删除活动
|
||||
await db.query('DELETE FROM promotion_activities WHERE id = ?', [id]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
code: 200,
|
||||
message: '删除成功'
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 暂停推广活动
|
||||
* @function pauseActivity
|
||||
* @param {Object} req - Express请求对象
|
||||
* @param {Object} res - Express响应对象
|
||||
* @param {Function} next - Express中间件next函数
|
||||
*/
|
||||
exports.pauseActivity = async (req, res, next) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
// 检查活动是否存在
|
||||
const existingActivity = await db.query(
|
||||
'SELECT id, status FROM promotion_activities WHERE id = ?',
|
||||
[id]
|
||||
);
|
||||
|
||||
if (existingActivity.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
code: 404,
|
||||
message: '推广活动不存在'
|
||||
});
|
||||
}
|
||||
|
||||
const currentStatus = existingActivity[0].status;
|
||||
|
||||
if (currentStatus !== 'active') {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
code: 400,
|
||||
message: '只有活跃状态的活动可以暂停'
|
||||
});
|
||||
}
|
||||
|
||||
// 更新状态为暂停
|
||||
await db.query(
|
||||
'UPDATE promotion_activities SET status = "paused", updated_at = NOW() WHERE id = ?',
|
||||
[id]
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
code: 200,
|
||||
message: '暂停成功'
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 恢复推广活动
|
||||
* @function resumeActivity
|
||||
* @param {Object} req - Express请求对象
|
||||
* @param {Object} res - Express响应对象
|
||||
* @param {Function} next - Express中间件next函数
|
||||
*/
|
||||
exports.resumeActivity = async (req, res, next) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
// 检查活动是否存在
|
||||
const existingActivity = await db.query(
|
||||
'SELECT id, status FROM promotion_activities WHERE id = ?',
|
||||
[id]
|
||||
);
|
||||
|
||||
if (existingActivity.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
code: 404,
|
||||
message: '推广活动不存在'
|
||||
});
|
||||
}
|
||||
|
||||
const currentStatus = existingActivity[0].status;
|
||||
|
||||
if (currentStatus !== 'paused') {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
code: 400,
|
||||
message: '只有暂停状态的活动可以恢复'
|
||||
});
|
||||
}
|
||||
|
||||
// 更新状态为活跃
|
||||
await db.query(
|
||||
'UPDATE promotion_activities SET status = "active", updated_at = NOW() WHERE id = ?',
|
||||
[id]
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
code: 200,
|
||||
message: '恢复成功'
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取推广统计数据
|
||||
* @function getStatistics
|
||||
* @param {Object} req - Express请求对象
|
||||
* @param {Object} res - Express响应对象
|
||||
* @param {Function} next - Express中间件next函数
|
||||
*/
|
||||
exports.getStatistics = async (req, res, next) => {
|
||||
try {
|
||||
// 获取活动总数
|
||||
const totalActivities = await db.query(
|
||||
'SELECT COUNT(*) as count FROM promotion_activities'
|
||||
);
|
||||
|
||||
// 获取活跃活动数
|
||||
const activeActivities = await db.query(
|
||||
'SELECT COUNT(*) as count FROM promotion_activities WHERE status = "active"'
|
||||
);
|
||||
|
||||
// 获取奖励记录总数
|
||||
const totalRewards = await db.query(
|
||||
'SELECT COUNT(*) as count FROM promotion_rewards'
|
||||
);
|
||||
|
||||
// 获取已发放奖励数
|
||||
const issuedRewards = await db.query(
|
||||
'SELECT COUNT(*) as count FROM promotion_rewards WHERE status = "issued"'
|
||||
);
|
||||
|
||||
// 获取奖励总金额
|
||||
const totalAmount = await db.query(`
|
||||
SELECT COALESCE(SUM(
|
||||
CASE
|
||||
WHEN reward_type = 'cash' THEN reward_amount
|
||||
WHEN reward_type = 'points' THEN reward_amount * 0.01 -- 假设1积分=0.01元
|
||||
ELSE 0
|
||||
END
|
||||
), 0) as total_amount
|
||||
FROM promotion_rewards
|
||||
WHERE status = 'issued'
|
||||
`);
|
||||
|
||||
const statistics = {
|
||||
total_activities: totalActivities[0].count,
|
||||
active_activities: activeActivities[0].count,
|
||||
total_rewards: totalRewards[0].count,
|
||||
issued_rewards: issuedRewards[0].count,
|
||||
total_amount: parseFloat(totalAmount[0].total_amount)
|
||||
};
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
code: 200,
|
||||
message: '获取成功',
|
||||
data: statistics
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
175
backend/src/controllers/promotion/rewardController.js
Normal file
175
backend/src/controllers/promotion/rewardController.js
Normal file
@@ -0,0 +1,175 @@
|
||||
/**
|
||||
* 奖励记录控制器
|
||||
* @module controllers/promotion/rewardController
|
||||
*/
|
||||
|
||||
const db = require('../../config/database');
|
||||
|
||||
/**
|
||||
* 获取奖励记录列表
|
||||
* @function getRewards
|
||||
* @param {Object} req - Express请求对象
|
||||
* @param {Object} res - Express响应对象
|
||||
* @param {Function} next - Express中间件next函数
|
||||
*/
|
||||
exports.getRewards = async (req, res, next) => {
|
||||
try {
|
||||
const {
|
||||
page = 1,
|
||||
pageSize = 20,
|
||||
user = '',
|
||||
reward_type = '',
|
||||
status = ''
|
||||
} = req.query;
|
||||
|
||||
const offset = (page - 1) * pageSize;
|
||||
const limit = parseInt(pageSize);
|
||||
|
||||
let whereConditions = [];
|
||||
let queryParams = [];
|
||||
|
||||
if (user) {
|
||||
whereConditions.push('(user_name LIKE ? OR user_phone LIKE ?)');
|
||||
queryParams.push(`%${user}%`, `%${user}%`);
|
||||
}
|
||||
|
||||
if (reward_type) {
|
||||
whereConditions.push('reward_type = ?');
|
||||
queryParams.push(reward_type);
|
||||
}
|
||||
|
||||
if (status) {
|
||||
whereConditions.push('status = ?');
|
||||
queryParams.push(status);
|
||||
}
|
||||
|
||||
const whereClause = whereConditions.length > 0
|
||||
? `WHERE ${whereConditions.join(' AND ')}`
|
||||
: '';
|
||||
|
||||
// 获取总数
|
||||
const countSql = `SELECT COUNT(*) as total FROM promotion_rewards ${whereClause}`;
|
||||
const countResult = await db.query(countSql, queryParams);
|
||||
const total = countResult[0].total;
|
||||
|
||||
// 获取数据
|
||||
const dataSql = `
|
||||
SELECT
|
||||
id, user_id, user_name, user_phone, activity_id, activity_name,
|
||||
reward_type, reward_amount, status, issued_at, created_at
|
||||
FROM promotion_rewards
|
||||
${whereClause}
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
`;
|
||||
|
||||
const dataParams = [...queryParams, limit, offset];
|
||||
const rewards = await db.query(dataSql, dataParams);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
code: 200,
|
||||
message: '获取成功',
|
||||
data: rewards,
|
||||
pagination: {
|
||||
current: parseInt(page),
|
||||
pageSize: limit,
|
||||
total,
|
||||
totalPages: Math.ceil(total / limit)
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 发放奖励
|
||||
* @function issueReward
|
||||
* @param {Object} req - Express请求对象
|
||||
* @param {Object} res - Express响应对象
|
||||
* @param {Function} next - Express中间件next函数
|
||||
*/
|
||||
exports.issueReward = async (req, res, next) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
// 检查奖励记录是否存在
|
||||
const reward = await db.query(
|
||||
'SELECT * FROM promotion_rewards WHERE id = ?',
|
||||
[id]
|
||||
);
|
||||
|
||||
if (reward.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
code: 404,
|
||||
message: '奖励记录不存在'
|
||||
});
|
||||
}
|
||||
|
||||
const rewardData = reward[0];
|
||||
|
||||
if (rewardData.status !== 'pending') {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
code: 400,
|
||||
message: '只有待发放状态的奖励可以发放'
|
||||
});
|
||||
}
|
||||
|
||||
// 更新奖励状态为已发放
|
||||
await db.query(
|
||||
'UPDATE promotion_rewards SET status = "issued", issued_at = NOW() WHERE id = ?',
|
||||
[id]
|
||||
);
|
||||
|
||||
// 根据奖励类型执行相应的发放逻辑
|
||||
try {
|
||||
switch (rewardData.reward_type) {
|
||||
case 'cash':
|
||||
// 现金奖励发放逻辑
|
||||
// 这里可以集成支付系统或记录到用户账户
|
||||
console.log(`发放现金奖励: ${rewardData.reward_amount}元给用户 ${rewardData.user_name}`);
|
||||
break;
|
||||
|
||||
case 'points':
|
||||
// 积分奖励发放逻辑
|
||||
// 这里可以更新用户积分
|
||||
console.log(`发放积分奖励: ${rewardData.reward_amount}积分给用户 ${rewardData.user_name}`);
|
||||
break;
|
||||
|
||||
case 'coupon':
|
||||
// 优惠券发放逻辑
|
||||
// 这里可以生成优惠券并关联到用户
|
||||
console.log(`发放优惠券奖励给用户 ${rewardData.user_name}`);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log(`未知奖励类型: ${rewardData.reward_type}`);
|
||||
}
|
||||
} catch (distributionError) {
|
||||
// 如果发放失败,回滚奖励状态
|
||||
await db.query(
|
||||
'UPDATE promotion_rewards SET status = "failed" WHERE id = ?',
|
||||
[id]
|
||||
);
|
||||
|
||||
console.error('奖励发放失败:', distributionError);
|
||||
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
code: 500,
|
||||
message: '奖励发放失败'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
code: 200,
|
||||
message: '奖励发放成功'
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
@@ -41,33 +41,49 @@ class TravelController {
|
||||
static async createTravelPlan(req, res, next) {
|
||||
try {
|
||||
const {
|
||||
title,
|
||||
description,
|
||||
destination,
|
||||
start_date,
|
||||
end_date,
|
||||
budget,
|
||||
companions,
|
||||
transportation,
|
||||
accommodation,
|
||||
activities,
|
||||
notes
|
||||
max_participants,
|
||||
price_per_person,
|
||||
itinerary,
|
||||
requirements,
|
||||
includes,
|
||||
excludes,
|
||||
images
|
||||
} = req.body;
|
||||
|
||||
if (!destination || !start_date || !end_date) {
|
||||
throw new AppError('目的地、开始日期和结束日期不能为空', 400);
|
||||
if (!title || !destination || !start_date || !end_date || !price_per_person) {
|
||||
throw new AppError('标题、目的地、开始日期、结束日期和价格不能为空', 400);
|
||||
}
|
||||
|
||||
const planId = await TravelService.createTravelPlan(req.userId, {
|
||||
const planData = {
|
||||
title,
|
||||
description: description || null,
|
||||
destination,
|
||||
start_date,
|
||||
end_date,
|
||||
budget,
|
||||
companions,
|
||||
transportation,
|
||||
accommodation,
|
||||
activities,
|
||||
notes
|
||||
max_participants: max_participants || null,
|
||||
price_per_person: parseFloat(price_per_person),
|
||||
itinerary: itinerary || [],
|
||||
requirements: requirements || null,
|
||||
includes: includes || [],
|
||||
excludes: excludes || [],
|
||||
images: images || []
|
||||
};
|
||||
|
||||
// 调试:检查传递给服务层的数据
|
||||
console.log('Plan Data:', planData);
|
||||
Object.keys(planData).forEach(key => {
|
||||
if (planData[key] === undefined) {
|
||||
console.log(`Field ${key} is undefined`);
|
||||
}
|
||||
});
|
||||
|
||||
const planId = await TravelService.createTravelPlan(req.userId, planData);
|
||||
|
||||
const plan = await TravelService.getTravelPlanById(planId);
|
||||
|
||||
res.status(201).json(success({
|
||||
|
||||
@@ -42,12 +42,13 @@ class Animal {
|
||||
const query = `
|
||||
SELECT
|
||||
a.*,
|
||||
type,
|
||||
m.name as merchant_name,
|
||||
m.contact_phone as merchant_phone
|
||||
FROM animals a
|
||||
LEFT JOIN merchants m ON a.merchant_id = m.id
|
||||
WHERE 1=1 ${whereClause}
|
||||
ORDER BY a.${sortBy} ${sortOrder}
|
||||
WHERE ${whereClause}
|
||||
ORDER BY a.created_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
`;
|
||||
|
||||
@@ -92,6 +93,7 @@ class Animal {
|
||||
const query = `
|
||||
SELECT
|
||||
a.*,
|
||||
a.type,
|
||||
m.name as merchant_name,
|
||||
m.contact_phone as merchant_phone,
|
||||
m.address as merchant_address
|
||||
@@ -194,27 +196,26 @@ class Animal {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取按物种分类的统计
|
||||
* 获取动物统计信息(按类型分组)
|
||||
* @returns {Array} 统计信息
|
||||
*/
|
||||
static async getAnimalStatsBySpecies() {
|
||||
static async getAnimalStatsByType() {
|
||||
try {
|
||||
const query = `
|
||||
SELECT
|
||||
species,
|
||||
type,
|
||||
COUNT(*) as count,
|
||||
COUNT(CASE WHEN status = 'available' THEN 1 END) as available_count,
|
||||
COUNT(CASE WHEN status = 'claimed' THEN 1 END) as claimed_count,
|
||||
AVG(price) as avg_price
|
||||
FROM animals
|
||||
GROUP BY species
|
||||
WHERE status = 'available'
|
||||
GROUP BY type
|
||||
ORDER BY count DESC
|
||||
`;
|
||||
|
||||
const [rows] = await db.execute(query);
|
||||
return rows;
|
||||
} catch (error) {
|
||||
console.error('获取按物种分类的统计失败:', error);
|
||||
console.error('获取动物统计信息失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -341,28 +342,35 @@ class Animal {
|
||||
try {
|
||||
const {
|
||||
name,
|
||||
species,
|
||||
type,
|
||||
breed,
|
||||
age,
|
||||
gender,
|
||||
weight,
|
||||
price,
|
||||
description,
|
||||
image_url,
|
||||
health_status,
|
||||
vaccination_records,
|
||||
images,
|
||||
merchant_id,
|
||||
farm_location,
|
||||
contact_info,
|
||||
status = 'available'
|
||||
} = animalData;
|
||||
|
||||
const query = `
|
||||
INSERT INTO animals (
|
||||
name, species, breed, age, gender, weight, price,
|
||||
description, image_url, merchant_id, status, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
|
||||
name, type, breed, age, gender, weight, price,
|
||||
description, health_status, vaccination_records, images,
|
||||
merchant_id, farm_location, contact_info, status, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
|
||||
`;
|
||||
|
||||
const [result] = await db.execute(query, [
|
||||
name, species, breed, age, gender, weight, price,
|
||||
description, image_url, merchant_id, status
|
||||
name, type, breed, age, gender, weight, price,
|
||||
description, health_status, JSON.stringify(vaccination_records || []),
|
||||
JSON.stringify(images || []), merchant_id, farm_location,
|
||||
JSON.stringify(contact_info || {}), status
|
||||
]);
|
||||
|
||||
return { id: result.insertId, ...animalData };
|
||||
|
||||
217
backend/src/models/Merchant.js
Normal file
217
backend/src/models/Merchant.js
Normal file
@@ -0,0 +1,217 @@
|
||||
const { query } = require('../config/database');
|
||||
|
||||
/**
|
||||
* 商户模型类
|
||||
* 处理商户相关的数据库操作
|
||||
*/
|
||||
class Merchant {
|
||||
// 根据ID查找商户
|
||||
static async findById(id) {
|
||||
try {
|
||||
const sql = 'SELECT * FROM merchants WHERE id = ?';
|
||||
const [rows] = await query(sql, [id]);
|
||||
return rows.length > 0 ? rows[0] : null;
|
||||
} catch (error) {
|
||||
console.error('查找商户失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取商户列表(支持分页和筛选)
|
||||
static async getMerchantList(options = {}) {
|
||||
try {
|
||||
const {
|
||||
page = 1,
|
||||
limit = 20,
|
||||
keyword = '',
|
||||
status = '',
|
||||
type = ''
|
||||
} = options;
|
||||
|
||||
const offset = (page - 1) * limit;
|
||||
let whereConditions = [];
|
||||
let params = [];
|
||||
|
||||
// 构建查询条件
|
||||
if (keyword) {
|
||||
whereConditions.push('(name LIKE ? OR contact_person LIKE ? OR contact_phone LIKE ?)');
|
||||
const keywordPattern = `%${keyword}%`;
|
||||
params.push(keywordPattern, keywordPattern, keywordPattern);
|
||||
}
|
||||
|
||||
if (status) {
|
||||
whereConditions.push('status = ?');
|
||||
params.push(status);
|
||||
}
|
||||
|
||||
if (type) {
|
||||
whereConditions.push('type = ?');
|
||||
params.push(type);
|
||||
}
|
||||
|
||||
const whereClause = whereConditions.length > 0 ? `WHERE ${whereConditions.join(' AND ')}` : '';
|
||||
|
||||
// 查询总数
|
||||
const countSql = `SELECT COUNT(*) as total FROM merchants ${whereClause}`;
|
||||
const [countResult] = await query(countSql, params);
|
||||
const total = countResult[0].total;
|
||||
|
||||
// 查询数据
|
||||
const dataSql = `
|
||||
SELECT * FROM merchants
|
||||
${whereClause}
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
`;
|
||||
const [rows] = await query(dataSql, [...params, limit, offset]);
|
||||
|
||||
return {
|
||||
data: rows,
|
||||
pagination: {
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit),
|
||||
total,
|
||||
totalPages: Math.ceil(total / limit)
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('获取商户列表失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 创建商户
|
||||
static async create(merchantData) {
|
||||
try {
|
||||
const {
|
||||
name,
|
||||
type,
|
||||
contact_person,
|
||||
contact_phone,
|
||||
email = null,
|
||||
address = null,
|
||||
description = null
|
||||
} = merchantData;
|
||||
|
||||
const sql = `
|
||||
INSERT INTO merchants (name, type, contact_person, contact_phone, email, address, description, status, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, 'active', NOW(), NOW())
|
||||
`;
|
||||
|
||||
const params = [name, type, contact_person, contact_phone, email, address, description];
|
||||
const [result] = await query(sql, params);
|
||||
|
||||
// 返回创建的商户信息
|
||||
return await this.findById(result.insertId);
|
||||
} catch (error) {
|
||||
console.error('创建商户失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新商户信息
|
||||
static async update(id, merchantData) {
|
||||
try {
|
||||
const updateFields = [];
|
||||
const params = [];
|
||||
|
||||
// 动态构建更新字段
|
||||
const allowedFields = ['name', 'type', 'contact_person', 'contact_phone', 'email', 'address', 'description', 'status'];
|
||||
|
||||
for (const field of allowedFields) {
|
||||
if (merchantData[field] !== undefined) {
|
||||
updateFields.push(`${field} = ?`);
|
||||
params.push(merchantData[field]);
|
||||
}
|
||||
}
|
||||
|
||||
if (updateFields.length === 0) {
|
||||
throw new Error('没有提供要更新的字段');
|
||||
}
|
||||
|
||||
updateFields.push('updated_at = NOW()');
|
||||
params.push(id);
|
||||
|
||||
const sql = `UPDATE merchants SET ${updateFields.join(', ')} WHERE id = ?`;
|
||||
const [result] = await query(sql, params);
|
||||
|
||||
if (result.affectedRows === 0) {
|
||||
throw new Error('商户不存在或更新失败');
|
||||
}
|
||||
|
||||
// 返回更新后的商户信息
|
||||
return await this.findById(id);
|
||||
} catch (error) {
|
||||
console.error('更新商户失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 删除商户
|
||||
static async delete(id) {
|
||||
try {
|
||||
const sql = 'DELETE FROM merchants WHERE id = ?';
|
||||
const [result] = await query(sql, [id]);
|
||||
|
||||
if (result.affectedRows === 0) {
|
||||
throw new Error('商户不存在或删除失败');
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('删除商户失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取商户详情(包含统计信息)
|
||||
static async getDetailWithStats(id) {
|
||||
try {
|
||||
const merchant = await this.findById(id);
|
||||
if (!merchant) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 获取关联的动物数量
|
||||
const animalCountSql = 'SELECT COUNT(*) as count FROM animals WHERE merchant_id = ?';
|
||||
const [animalResult] = await query(animalCountSql, [id]);
|
||||
|
||||
// 获取关联的订单数量
|
||||
const orderCountSql = 'SELECT COUNT(*) as count FROM orders WHERE merchant_id = ?';
|
||||
const [orderResult] = await query(orderCountSql, [id]);
|
||||
|
||||
return {
|
||||
...merchant,
|
||||
animal_count: animalResult[0].count,
|
||||
order_count: orderResult[0].count
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('获取商户详情失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取商户统计信息
|
||||
static async getStatistics() {
|
||||
try {
|
||||
const sql = `
|
||||
SELECT
|
||||
COUNT(*) as total,
|
||||
SUM(CASE WHEN status = 'active' THEN 1 ELSE 0 END) as active,
|
||||
SUM(CASE WHEN status = 'inactive' THEN 1 ELSE 0 END) as inactive,
|
||||
SUM(CASE WHEN status = 'banned' THEN 1 ELSE 0 END) as banned,
|
||||
SUM(CASE WHEN type = 'individual' THEN 1 ELSE 0 END) as individual,
|
||||
SUM(CASE WHEN type = 'company' THEN 1 ELSE 0 END) as company
|
||||
FROM merchants
|
||||
`;
|
||||
|
||||
const [rows] = await query(sql);
|
||||
return rows[0];
|
||||
} catch (error) {
|
||||
console.error('获取商户统计信息失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Merchant;
|
||||
427
backend/src/models/Travel.js
Normal file
427
backend/src/models/Travel.js
Normal file
@@ -0,0 +1,427 @@
|
||||
const { query } = require('../config/database');
|
||||
|
||||
/**
|
||||
* 旅行计划数据模型
|
||||
* 处理旅行计划相关的数据库操作
|
||||
*/
|
||||
class Travel {
|
||||
/**
|
||||
* 创建旅行计划
|
||||
* @param {Object} travelData - 旅行计划数据
|
||||
* @returns {Promise<Object>} 创建的旅行计划
|
||||
*/
|
||||
static async create(travelData) {
|
||||
const {
|
||||
title,
|
||||
destination,
|
||||
description,
|
||||
start_date,
|
||||
end_date,
|
||||
max_participants,
|
||||
price_per_person,
|
||||
includes,
|
||||
excludes,
|
||||
itinerary,
|
||||
images,
|
||||
requirements,
|
||||
created_by
|
||||
} = travelData;
|
||||
|
||||
const query = `
|
||||
INSERT INTO travel_plans
|
||||
(title, destination, description, start_date, end_date, max_participants,
|
||||
current_participants, price_per_person, includes, excludes, itinerary,
|
||||
images, requirements, created_by, status, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?, ?, ?, ?, 'draft', NOW(), NOW())
|
||||
`;
|
||||
|
||||
const [result] = await query(query, [
|
||||
title,
|
||||
destination,
|
||||
description || null,
|
||||
start_date,
|
||||
end_date,
|
||||
max_participants || 10,
|
||||
price_per_person,
|
||||
JSON.stringify(includes || []),
|
||||
JSON.stringify(excludes || []),
|
||||
JSON.stringify(itinerary || []),
|
||||
JSON.stringify(images || []),
|
||||
requirements || null,
|
||||
created_by
|
||||
]);
|
||||
|
||||
return this.findById(result.insertId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID查找旅行计划
|
||||
* @param {number} id - 旅行计划ID
|
||||
* @returns {Promise<Object|null>} 旅行计划信息
|
||||
*/
|
||||
static async findById(id) {
|
||||
const query = `
|
||||
SELECT
|
||||
tp.*,
|
||||
u.username as creator_name,
|
||||
u.avatar as creator_avatar,
|
||||
u.phone as creator_phone
|
||||
FROM travel_plans tp
|
||||
LEFT JOIN users u ON tp.created_by = u.id
|
||||
WHERE tp.id = ?
|
||||
`;
|
||||
|
||||
const [rows] = await query(query, [id]);
|
||||
|
||||
if (rows.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const travel = rows[0];
|
||||
|
||||
// 解析JSON字段
|
||||
if (travel.includes) {
|
||||
travel.includes = JSON.parse(travel.includes);
|
||||
}
|
||||
if (travel.excludes) {
|
||||
travel.excludes = JSON.parse(travel.excludes);
|
||||
}
|
||||
if (travel.itinerary) {
|
||||
travel.itinerary = JSON.parse(travel.itinerary);
|
||||
}
|
||||
if (travel.images) {
|
||||
travel.images = JSON.parse(travel.images);
|
||||
}
|
||||
|
||||
return travel;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取旅行计划列表
|
||||
* @param {Object} options - 查询选项
|
||||
* @returns {Promise<Array>} 旅行计划列表
|
||||
*/
|
||||
static async findAll(options = {}) {
|
||||
const {
|
||||
page = 1,
|
||||
limit = 10,
|
||||
destination,
|
||||
status,
|
||||
created_by,
|
||||
start_date,
|
||||
end_date,
|
||||
sort_by = 'created_at',
|
||||
sort_order = 'DESC'
|
||||
} = options;
|
||||
|
||||
let whereClause = 'WHERE 1=1';
|
||||
const params = [];
|
||||
|
||||
if (destination) {
|
||||
whereClause += ' AND tp.destination LIKE ?';
|
||||
params.push(`%${destination}%`);
|
||||
}
|
||||
|
||||
if (status) {
|
||||
whereClause += ' AND tp.status = ?';
|
||||
params.push(status);
|
||||
}
|
||||
|
||||
if (created_by) {
|
||||
whereClause += ' AND tp.created_by = ?';
|
||||
params.push(created_by);
|
||||
}
|
||||
|
||||
if (start_date) {
|
||||
whereClause += ' AND tp.start_date >= ?';
|
||||
params.push(start_date);
|
||||
}
|
||||
|
||||
if (end_date) {
|
||||
whereClause += ' AND tp.end_date <= ?';
|
||||
params.push(end_date);
|
||||
}
|
||||
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
const query = `
|
||||
SELECT
|
||||
tp.*,
|
||||
u.username as creator_name,
|
||||
u.avatar as creator_avatar
|
||||
FROM travel_plans tp
|
||||
LEFT JOIN users u ON tp.created_by = u.id
|
||||
${whereClause}
|
||||
ORDER BY tp.${sort_by} ${sort_order}
|
||||
LIMIT ? OFFSET ?
|
||||
`;
|
||||
|
||||
const [rows] = await query(query, [...params, limit, offset]);
|
||||
|
||||
// 解析JSON字段
|
||||
return rows.map(travel => {
|
||||
if (travel.includes) {
|
||||
travel.includes = JSON.parse(travel.includes);
|
||||
}
|
||||
if (travel.excludes) {
|
||||
travel.excludes = JSON.parse(travel.excludes);
|
||||
}
|
||||
if (travel.itinerary) {
|
||||
travel.itinerary = JSON.parse(travel.itinerary);
|
||||
}
|
||||
if (travel.images) {
|
||||
travel.images = JSON.parse(travel.images);
|
||||
}
|
||||
return travel;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取旅行计划总数
|
||||
* @param {Object} options - 查询选项
|
||||
* @returns {Promise<number>} 总数
|
||||
*/
|
||||
static async count(options = {}) {
|
||||
const {
|
||||
destination,
|
||||
status,
|
||||
created_by,
|
||||
start_date,
|
||||
end_date
|
||||
} = options;
|
||||
|
||||
let whereClause = 'WHERE 1=1';
|
||||
const params = [];
|
||||
|
||||
if (destination) {
|
||||
whereClause += ' AND destination LIKE ?';
|
||||
params.push(`%${destination}%`);
|
||||
}
|
||||
|
||||
if (status) {
|
||||
whereClause += ' AND status = ?';
|
||||
params.push(status);
|
||||
}
|
||||
|
||||
if (created_by) {
|
||||
whereClause += ' AND created_by = ?';
|
||||
params.push(created_by);
|
||||
}
|
||||
|
||||
if (start_date) {
|
||||
whereClause += ' AND start_date >= ?';
|
||||
params.push(start_date);
|
||||
}
|
||||
|
||||
if (end_date) {
|
||||
whereClause += ' AND end_date <= ?';
|
||||
params.push(end_date);
|
||||
}
|
||||
|
||||
const query = `SELECT COUNT(*) as count FROM travel_plans ${whereClause}`;
|
||||
const [rows] = await query(query, params);
|
||||
return rows[0].count;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新旅行计划
|
||||
* @param {number} id - 旅行计划ID
|
||||
* @param {Object} updateData - 更新数据
|
||||
* @returns {Promise<Object|null>} 更新后的旅行计划
|
||||
*/
|
||||
static async update(id, updateData) {
|
||||
const {
|
||||
title,
|
||||
destination,
|
||||
description,
|
||||
start_date,
|
||||
end_date,
|
||||
max_participants,
|
||||
price_per_person,
|
||||
includes,
|
||||
excludes,
|
||||
itinerary,
|
||||
images,
|
||||
requirements,
|
||||
status
|
||||
} = updateData;
|
||||
|
||||
const fields = [];
|
||||
const params = [];
|
||||
|
||||
if (title !== undefined) {
|
||||
fields.push('title = ?');
|
||||
params.push(title);
|
||||
}
|
||||
if (destination !== undefined) {
|
||||
fields.push('destination = ?');
|
||||
params.push(destination);
|
||||
}
|
||||
if (description !== undefined) {
|
||||
fields.push('description = ?');
|
||||
params.push(description);
|
||||
}
|
||||
if (start_date !== undefined) {
|
||||
fields.push('start_date = ?');
|
||||
params.push(start_date);
|
||||
}
|
||||
if (end_date !== undefined) {
|
||||
fields.push('end_date = ?');
|
||||
params.push(end_date);
|
||||
}
|
||||
if (max_participants !== undefined) {
|
||||
fields.push('max_participants = ?');
|
||||
params.push(max_participants);
|
||||
}
|
||||
if (price_per_person !== undefined) {
|
||||
fields.push('price_per_person = ?');
|
||||
params.push(price_per_person);
|
||||
}
|
||||
if (includes !== undefined) {
|
||||
fields.push('includes = ?');
|
||||
params.push(JSON.stringify(includes));
|
||||
}
|
||||
if (excludes !== undefined) {
|
||||
fields.push('excludes = ?');
|
||||
params.push(JSON.stringify(excludes));
|
||||
}
|
||||
if (itinerary !== undefined) {
|
||||
fields.push('itinerary = ?');
|
||||
params.push(JSON.stringify(itinerary));
|
||||
}
|
||||
if (images !== undefined) {
|
||||
fields.push('images = ?');
|
||||
params.push(JSON.stringify(images));
|
||||
}
|
||||
if (requirements !== undefined) {
|
||||
fields.push('requirements = ?');
|
||||
params.push(requirements);
|
||||
}
|
||||
if (status !== undefined) {
|
||||
fields.push('status = ?');
|
||||
params.push(status);
|
||||
}
|
||||
|
||||
if (fields.length === 0) {
|
||||
return this.findById(id);
|
||||
}
|
||||
|
||||
fields.push('updated_at = NOW()');
|
||||
params.push(id);
|
||||
|
||||
const query = `UPDATE travel_plans SET ${fields.join(', ')} WHERE id = ?`;
|
||||
await query(query, params);
|
||||
|
||||
return this.findById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除旅行计划
|
||||
* @param {number} id - 旅行计划ID
|
||||
* @returns {Promise<boolean>} 是否删除成功
|
||||
*/
|
||||
static async delete(id) {
|
||||
const query = 'DELETE FROM travel_plans WHERE id = ?';
|
||||
const [result] = await query(query, [id]);
|
||||
return result.affectedRows > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 增加参与人数
|
||||
* @param {number} id - 旅行计划ID
|
||||
* @param {number} count - 增加的人数
|
||||
* @returns {Promise<boolean>} 是否更新成功
|
||||
*/
|
||||
static async incrementParticipants(id, count = 1) {
|
||||
const query = `
|
||||
UPDATE travel_plans
|
||||
SET current_participants = current_participants + ?, updated_at = NOW()
|
||||
WHERE id = ? AND current_participants + ? <= max_participants
|
||||
`;
|
||||
const [result] = await query(query, [count, id, count]);
|
||||
return result.affectedRows > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 减少参与人数
|
||||
* @param {number} id - 旅行计划ID
|
||||
* @param {number} count - 减少的人数
|
||||
* @returns {Promise<boolean>} 是否更新成功
|
||||
*/
|
||||
static async decrementParticipants(id, count = 1) {
|
||||
const query = `
|
||||
UPDATE travel_plans
|
||||
SET current_participants = GREATEST(0, current_participants - ?), updated_at = NOW()
|
||||
WHERE id = ?
|
||||
`;
|
||||
const [result] = await query(sql, [count, id]);
|
||||
return result.affectedRows > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否可以报名
|
||||
* @param {number} id - 旅行计划ID
|
||||
* @returns {Promise<boolean>} 是否可以报名
|
||||
*/
|
||||
static async canRegister(id) {
|
||||
const query = `
|
||||
SELECT
|
||||
current_participants < max_participants as can_register,
|
||||
status = 'published' as is_published,
|
||||
start_date > NOW() as not_started
|
||||
FROM travel_plans
|
||||
WHERE id = ?
|
||||
`;
|
||||
const [rows] = await query(sql, [id]);
|
||||
|
||||
if (rows.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { can_register, is_published, not_started } = rows[0];
|
||||
return can_register && is_published && not_started;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取热门旅行计划
|
||||
* @param {number} limit - 限制数量
|
||||
* @returns {Promise<Array>} 热门旅行计划列表
|
||||
*/
|
||||
static async getPopular(limit = 10) {
|
||||
const query = `
|
||||
SELECT
|
||||
tp.*,
|
||||
u.username as creator_name,
|
||||
u.avatar as creator_avatar,
|
||||
COUNT(tr.id) as registration_count
|
||||
FROM travel_plans tp
|
||||
LEFT JOIN users u ON tp.created_by = u.id
|
||||
LEFT JOIN travel_registrations tr ON tp.id = tr.travel_plan_id AND tr.status = 'approved'
|
||||
WHERE tp.status = 'published' AND tp.start_date > NOW()
|
||||
GROUP BY tp.id
|
||||
ORDER BY registration_count DESC, tp.created_at DESC
|
||||
LIMIT ?
|
||||
`;
|
||||
|
||||
const [rows] = await query(sql, [limit]);
|
||||
|
||||
// 解析JSON字段
|
||||
return rows.map(travel => {
|
||||
if (travel.includes) {
|
||||
travel.includes = JSON.parse(travel.includes);
|
||||
}
|
||||
if (travel.excludes) {
|
||||
travel.excludes = JSON.parse(travel.excludes);
|
||||
}
|
||||
if (travel.itinerary) {
|
||||
travel.itinerary = JSON.parse(travel.itinerary);
|
||||
}
|
||||
if (travel.images) {
|
||||
travel.images = JSON.parse(travel.images);
|
||||
}
|
||||
return travel;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Travel;
|
||||
@@ -36,6 +36,9 @@ class UserMySQL {
|
||||
|
||||
// 根据ID查找用户
|
||||
static async findById(id) {
|
||||
if (id === undefined || id === null) {
|
||||
throw new Error('User ID cannot be undefined or null');
|
||||
}
|
||||
const sql = 'SELECT * FROM users WHERE id = ?';
|
||||
const rows = await query(sql, [id]);
|
||||
return rows[0] || null;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const express = require('express')
|
||||
const { body } = require('express-validator')
|
||||
const authController = require('../controllers/authControllerMySQL')
|
||||
const { authenticateUser } = require('../middleware/auth')
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
@@ -169,7 +170,7 @@ router.post(
|
||||
* 500:
|
||||
* description: 服务器内部错误
|
||||
*/
|
||||
router.get('/me', authController.getCurrentUser)
|
||||
router.get('/me', authenticateUser, authController.getCurrentUser)
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
@@ -224,7 +225,7 @@ router.get('/me', authController.getCurrentUser)
|
||||
* 500:
|
||||
* description: 服务器内部错误
|
||||
*/
|
||||
router.put('/profile', authController.updateProfile)
|
||||
router.put('/profile', authenticateUser, authController.updateProfile)
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
@@ -271,6 +272,7 @@ router.put('/profile', authController.updateProfile)
|
||||
*/
|
||||
router.put(
|
||||
'/password',
|
||||
authenticateUser,
|
||||
[
|
||||
body('currentPassword').notEmpty().withMessage('当前密码不能为空'),
|
||||
body('newPassword').isLength({ min: 6 }).withMessage('新密码长度不能少于6位')
|
||||
|
||||
453
backend/src/routes/merchant.js
Normal file
453
backend/src/routes/merchant.js
Normal file
@@ -0,0 +1,453 @@
|
||||
const express = require('express');
|
||||
const { body, query, param } = require('express-validator');
|
||||
const MerchantController = require('../controllers/merchant');
|
||||
const { authenticateUser, requireRole } = require('../middleware/auth');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* tags:
|
||||
* name: Merchants
|
||||
* description: 商户管理相关接口
|
||||
*/
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* components:
|
||||
* schemas:
|
||||
* Merchant:
|
||||
* type: object
|
||||
* properties:
|
||||
* id:
|
||||
* type: integer
|
||||
* description: 商户ID
|
||||
* name:
|
||||
* type: string
|
||||
* description: 商户名称
|
||||
* type:
|
||||
* type: string
|
||||
* enum: [individual, company]
|
||||
* description: 商户类型
|
||||
* contact_person:
|
||||
* type: string
|
||||
* description: 联系人
|
||||
* contact_phone:
|
||||
* type: string
|
||||
* description: 联系电话
|
||||
* email:
|
||||
* type: string
|
||||
* description: 邮箱
|
||||
* address:
|
||||
* type: string
|
||||
* description: 地址
|
||||
* description:
|
||||
* type: string
|
||||
* description: 描述
|
||||
* status:
|
||||
* type: string
|
||||
* enum: [active, inactive, banned]
|
||||
* description: 状态
|
||||
* created_at:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: 创建时间
|
||||
* updated_at:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: 更新时间
|
||||
*/
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /merchants:
|
||||
* get:
|
||||
* summary: 获取商户列表
|
||||
* tags: [Merchants]
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: page
|
||||
* schema:
|
||||
* type: integer
|
||||
* minimum: 1
|
||||
* default: 1
|
||||
* description: 页码
|
||||
* - in: query
|
||||
* name: limit
|
||||
* schema:
|
||||
* type: integer
|
||||
* minimum: 1
|
||||
* maximum: 100
|
||||
* default: 20
|
||||
* description: 每页数量
|
||||
* - in: query
|
||||
* name: keyword
|
||||
* schema:
|
||||
* type: string
|
||||
* description: 搜索关键词(商户名称、联系人、电话)
|
||||
* - in: query
|
||||
* name: status
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [active, inactive, banned]
|
||||
* description: 状态筛选
|
||||
* - in: query
|
||||
* name: type
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [individual, company]
|
||||
* description: 类型筛选
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 获取成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* data:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/Merchant'
|
||||
* pagination:
|
||||
* type: object
|
||||
* properties:
|
||||
* page:
|
||||
* type: integer
|
||||
* limit:
|
||||
* type: integer
|
||||
* total:
|
||||
* type: integer
|
||||
* totalPages:
|
||||
* type: integer
|
||||
* 400:
|
||||
* description: 请求参数错误
|
||||
* 500:
|
||||
* description: 服务器内部错误
|
||||
*/
|
||||
router.get('/',
|
||||
[
|
||||
query('page').optional().isInt({ min: 1 }).withMessage('页码必须是正整数'),
|
||||
query('limit').optional().isInt({ min: 1, max: 100 }).withMessage('每页数量必须在1-100之间'),
|
||||
query('status').optional().isIn(['active', 'inactive', 'banned']).withMessage('状态值无效'),
|
||||
query('type').optional().isIn(['individual', 'company']).withMessage('类型值无效')
|
||||
],
|
||||
MerchantController.getMerchantList
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /merchants/{merchantId}:
|
||||
* get:
|
||||
* summary: 获取商户详情
|
||||
* tags: [Merchants]
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: merchantId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: 商户ID
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 获取成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* data:
|
||||
* allOf:
|
||||
* - $ref: '#/components/schemas/Merchant'
|
||||
* - type: object
|
||||
* properties:
|
||||
* animal_count:
|
||||
* type: integer
|
||||
* description: 动物数量
|
||||
* order_count:
|
||||
* type: integer
|
||||
* description: 订单数量
|
||||
* 400:
|
||||
* description: 商户ID无效
|
||||
* 404:
|
||||
* description: 商户不存在
|
||||
* 500:
|
||||
* description: 服务器内部错误
|
||||
*/
|
||||
router.get('/:merchantId',
|
||||
[
|
||||
param('merchantId').isInt({ min: 1 }).withMessage('商户ID必须是正整数')
|
||||
],
|
||||
MerchantController.getMerchantDetail
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /merchants:
|
||||
* post:
|
||||
* summary: 创建商户
|
||||
* tags: [Merchants]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - name
|
||||
* - type
|
||||
* - contact_person
|
||||
* - contact_phone
|
||||
* properties:
|
||||
* name:
|
||||
* type: string
|
||||
* description: 商户名称
|
||||
* type:
|
||||
* type: string
|
||||
* enum: [individual, company]
|
||||
* description: 商户类型
|
||||
* contact_person:
|
||||
* type: string
|
||||
* description: 联系人
|
||||
* contact_phone:
|
||||
* type: string
|
||||
* description: 联系电话
|
||||
* email:
|
||||
* type: string
|
||||
* description: 邮箱
|
||||
* address:
|
||||
* type: string
|
||||
* description: 地址
|
||||
* description:
|
||||
* type: string
|
||||
* description: 描述
|
||||
* responses:
|
||||
* 201:
|
||||
* description: 创建成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* data:
|
||||
* $ref: '#/components/schemas/Merchant'
|
||||
* message:
|
||||
* type: string
|
||||
* 400:
|
||||
* description: 请求参数错误
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 403:
|
||||
* description: 权限不足
|
||||
* 500:
|
||||
* description: 服务器内部错误
|
||||
*/
|
||||
router.post('/',
|
||||
authenticateUser,
|
||||
requireRole(['admin', 'super_admin']),
|
||||
[
|
||||
body('name').notEmpty().withMessage('商户名称不能为空'),
|
||||
body('type').isIn(['individual', 'company']).withMessage('商户类型必须是 individual 或 company'),
|
||||
body('contact_person').notEmpty().withMessage('联系人不能为空'),
|
||||
body('contact_phone').notEmpty().withMessage('联系电话不能为空'),
|
||||
body('email').optional().isEmail().withMessage('邮箱格式无效')
|
||||
],
|
||||
MerchantController.createMerchant
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /merchants/{merchantId}:
|
||||
* put:
|
||||
* summary: 更新商户信息
|
||||
* tags: [Merchants]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: merchantId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: 商户ID
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* name:
|
||||
* type: string
|
||||
* description: 商户名称
|
||||
* type:
|
||||
* type: string
|
||||
* enum: [individual, company]
|
||||
* description: 商户类型
|
||||
* contact_person:
|
||||
* type: string
|
||||
* description: 联系人
|
||||
* contact_phone:
|
||||
* type: string
|
||||
* description: 联系电话
|
||||
* email:
|
||||
* type: string
|
||||
* description: 邮箱
|
||||
* address:
|
||||
* type: string
|
||||
* description: 地址
|
||||
* description:
|
||||
* type: string
|
||||
* description: 描述
|
||||
* status:
|
||||
* type: string
|
||||
* enum: [active, inactive, banned]
|
||||
* description: 状态
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 更新成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* data:
|
||||
* $ref: '#/components/schemas/Merchant'
|
||||
* message:
|
||||
* type: string
|
||||
* 400:
|
||||
* description: 请求参数错误
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 403:
|
||||
* description: 权限不足
|
||||
* 404:
|
||||
* description: 商户不存在
|
||||
* 500:
|
||||
* description: 服务器内部错误
|
||||
*/
|
||||
router.put('/:merchantId',
|
||||
authenticateUser,
|
||||
requireRole(['admin', 'super_admin']),
|
||||
[
|
||||
param('merchantId').isInt({ min: 1 }).withMessage('商户ID必须是正整数'),
|
||||
body('name').optional().notEmpty().withMessage('商户名称不能为空'),
|
||||
body('type').optional().isIn(['individual', 'company']).withMessage('商户类型必须是 individual 或 company'),
|
||||
body('contact_person').optional().notEmpty().withMessage('联系人不能为空'),
|
||||
body('contact_phone').optional().notEmpty().withMessage('联系电话不能为空'),
|
||||
body('email').optional().isEmail().withMessage('邮箱格式无效'),
|
||||
body('status').optional().isIn(['active', 'inactive', 'banned']).withMessage('状态值无效')
|
||||
],
|
||||
MerchantController.updateMerchant
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /merchants/{merchantId}:
|
||||
* delete:
|
||||
* summary: 删除商户
|
||||
* tags: [Merchants]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: merchantId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: 商户ID
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 删除成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* message:
|
||||
* type: string
|
||||
* 400:
|
||||
* description: 商户ID无效
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 403:
|
||||
* description: 权限不足
|
||||
* 404:
|
||||
* description: 商户不存在
|
||||
* 500:
|
||||
* description: 服务器内部错误
|
||||
*/
|
||||
router.delete('/:merchantId',
|
||||
authenticateUser,
|
||||
requireRole(['admin', 'super_admin']),
|
||||
[
|
||||
param('merchantId').isInt({ min: 1 }).withMessage('商户ID必须是正整数')
|
||||
],
|
||||
MerchantController.deleteMerchant
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /merchants/statistics:
|
||||
* get:
|
||||
* summary: 获取商户统计信息
|
||||
* tags: [Merchants]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 获取成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* total:
|
||||
* type: integer
|
||||
* description: 总商户数
|
||||
* active:
|
||||
* type: integer
|
||||
* description: 活跃商户数
|
||||
* inactive:
|
||||
* type: integer
|
||||
* description: 非活跃商户数
|
||||
* banned:
|
||||
* type: integer
|
||||
* description: 被禁用商户数
|
||||
* individual:
|
||||
* type: integer
|
||||
* description: 个人商户数
|
||||
* company:
|
||||
* type: integer
|
||||
* description: 企业商户数
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 403:
|
||||
* description: 权限不足
|
||||
* 500:
|
||||
* description: 服务器内部错误
|
||||
*/
|
||||
router.get('/statistics',
|
||||
authenticateUser,
|
||||
requireRole(['admin', 'super_admin']),
|
||||
MerchantController.getMerchantStatistics
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
61
backend/src/routes/promotion.js
Normal file
61
backend/src/routes/promotion.js
Normal file
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* 推广活动路由
|
||||
* @module routes/promotion
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
|
||||
const activityController = require('../controllers/promotion/activityController');
|
||||
const rewardController = require('../controllers/promotion/rewardController');
|
||||
|
||||
/**
|
||||
* @route GET /api/v1/promotion/activities
|
||||
* @description 获取推广活动列表
|
||||
* @access Public
|
||||
*/
|
||||
router.get('/activities', activityController.getActivities);
|
||||
|
||||
/**
|
||||
* @route GET /api/v1/promotion/activities/:id
|
||||
* @description 获取推广活动详情
|
||||
* @access Public
|
||||
*/
|
||||
router.get('/activities/:id', activityController.getActivity);
|
||||
|
||||
/**
|
||||
* @route POST /api/v1/promotion/activities
|
||||
* @description 创建推广活动
|
||||
* @access Private
|
||||
*/
|
||||
router.post('/activities', activityController.createActivity);
|
||||
|
||||
/**
|
||||
* @route PUT /api/v1/promotion/activities/:id
|
||||
* @description 更新推广活动
|
||||
* @access Private
|
||||
*/
|
||||
router.put('/activities/:id', activityController.updateActivity);
|
||||
|
||||
/**
|
||||
* @route DELETE /api/v1/promotion/activities/:id
|
||||
* @description 删除推广活动
|
||||
* @access Private
|
||||
*/
|
||||
router.delete('/activities/:id', activityController.deleteActivity);
|
||||
|
||||
/**
|
||||
* @route GET /api/v1/promotion/rewards
|
||||
* @description 获取奖励记录列表
|
||||
* @access Private
|
||||
*/
|
||||
router.get('/rewards', rewardController.getRewards);
|
||||
|
||||
/**
|
||||
* @route POST /api/v1/promotion/rewards/:id/issue
|
||||
* @description 发放奖励
|
||||
* @access Private
|
||||
*/
|
||||
router.post('/rewards/:id/issue', rewardController.issueReward);
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,7 +1,7 @@
|
||||
const express = require('express');
|
||||
const { body, query } = require('express-validator');
|
||||
const UserController = require('../controllers/user');
|
||||
const { authenticateUser, requireRole: requireAdmin } = require('../middleware/auth');
|
||||
const { authenticateUser, authenticateAdmin, requireRole: requireAdmin } = require('../middleware/auth');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -179,7 +179,7 @@ router.put('/profile', authenticateUser, UserController.updateProfile);
|
||||
* description: 服务器内部错误
|
||||
*/
|
||||
router.get('/',
|
||||
authenticateUser,
|
||||
authenticateAdmin,
|
||||
requireAdmin(['admin', 'super_admin']),
|
||||
UserController.getUsers
|
||||
);
|
||||
@@ -223,7 +223,7 @@ router.get('/',
|
||||
* 500:
|
||||
* description: 服务器内部错误
|
||||
*/
|
||||
router.get('/:userId', authenticateUser, requireAdmin(['admin', 'super_admin']), UserController.getUserById);
|
||||
router.get('/:userId', authenticateAdmin, requireAdmin(['admin', 'super_admin']), UserController.getUserById);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
@@ -273,7 +273,7 @@ router.get('/:userId', authenticateUser, requireAdmin(['admin', 'super_admin']),
|
||||
* 500:
|
||||
* description: 服务器内部错误
|
||||
*/
|
||||
router.get('/statistics', authenticateUser, requireAdmin(['admin', 'super_admin']), UserController.getUserStatistics);
|
||||
router.get('/statistics', authenticateAdmin, requireAdmin(['admin', 'super_admin']), UserController.getUserStatistics);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
@@ -329,7 +329,7 @@ router.get('/statistics', authenticateUser, requireAdmin(['admin', 'super_admin'
|
||||
* description: 服务器内部错误
|
||||
*/
|
||||
router.post('/batch-status',
|
||||
authenticateUser,
|
||||
authenticateAdmin,
|
||||
requireAdmin(['admin', 'super_admin']),
|
||||
[
|
||||
body('userIds').isArray().withMessage('userIds必须是数组'),
|
||||
@@ -379,6 +379,6 @@ router.post('/batch-status',
|
||||
* 500:
|
||||
* description: 服务器内部错误
|
||||
*/
|
||||
router.delete('/:userId', authenticateUser, requireAdmin(['admin', 'super_admin']), UserController.deleteUser);
|
||||
router.delete('/:userId', authenticateAdmin, requireAdmin(['admin', 'super_admin']), UserController.deleteUser);
|
||||
|
||||
module.exports = router;
|
||||
@@ -5,7 +5,7 @@ class AnimalService {
|
||||
// 获取动物列表
|
||||
static async getAnimals(searchParams) {
|
||||
try {
|
||||
const { merchantId, species, status, page = 1, pageSize = 10 } = searchParams;
|
||||
const { merchantId, type, status, page = 1, pageSize = 10 } = searchParams;
|
||||
const offset = (page - 1) * pageSize;
|
||||
|
||||
let sql = `
|
||||
@@ -22,9 +22,9 @@ class AnimalService {
|
||||
params.push(merchantId);
|
||||
}
|
||||
|
||||
if (species) {
|
||||
sql += ' AND a.species = ?';
|
||||
params.push(species);
|
||||
if (type) {
|
||||
sql += ' AND a.type = ?';
|
||||
params.push(type);
|
||||
}
|
||||
|
||||
if (status) {
|
||||
@@ -85,18 +85,26 @@ class AnimalService {
|
||||
try {
|
||||
const sql = `
|
||||
INSERT INTO animals (
|
||||
merchant_id, name, species, breed, birth_date, personality, farm_location, price, status
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
merchant_id, name, type, breed, age, gender, weight, price,
|
||||
description, health_status, vaccination_records, images,
|
||||
farm_location, contact_info, status, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
|
||||
`;
|
||||
const params = [
|
||||
animalData.merchant_id,
|
||||
animalData.name,
|
||||
animalData.species,
|
||||
animalData.type,
|
||||
animalData.breed,
|
||||
animalData.birth_date,
|
||||
animalData.personality,
|
||||
animalData.farm_location,
|
||||
animalData.age,
|
||||
animalData.gender,
|
||||
animalData.weight,
|
||||
animalData.price,
|
||||
animalData.description,
|
||||
animalData.health_status,
|
||||
JSON.stringify(animalData.vaccination_records || []),
|
||||
JSON.stringify(animalData.images || []),
|
||||
animalData.farm_location,
|
||||
JSON.stringify(animalData.contact_info || {}),
|
||||
animalData.status || 'available'
|
||||
];
|
||||
|
||||
|
||||
@@ -6,36 +6,41 @@ class TravelService {
|
||||
static async getTravelPlans(searchParams) {
|
||||
try {
|
||||
const { userId, page = 1, pageSize = 10, status } = searchParams;
|
||||
const offset = (page - 1) * pageSize;
|
||||
const offset = (parseInt(page) - 1) * parseInt(pageSize);
|
||||
|
||||
let sql = `
|
||||
SELECT tp.*, u.username, u.real_name, u.avatar_url
|
||||
FROM travel_plans tp
|
||||
INNER JOIN users u ON tp.user_id = u.id
|
||||
WHERE 1=1
|
||||
`;
|
||||
// 构建基础查询条件
|
||||
let whereClause = 'WHERE 1=1';
|
||||
const params = [];
|
||||
|
||||
if (userId) {
|
||||
sql += ' AND tp.user_id = ?';
|
||||
whereClause += ' AND tp.created_by = ?';
|
||||
params.push(userId);
|
||||
}
|
||||
|
||||
if (status) {
|
||||
sql += ' AND tp.status = ?';
|
||||
whereClause += ' AND tp.status = ?';
|
||||
params.push(status);
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
const countSql = `SELECT COUNT(*) as total FROM (${sql}) as count_query`;
|
||||
const countSql = `SELECT COUNT(*) as total FROM travel_plans tp ${whereClause}`;
|
||||
const countResult = await query(countSql, params);
|
||||
const total = countResult[0].total;
|
||||
|
||||
// 添加分页和排序
|
||||
sql += ' ORDER BY tp.created_at DESC LIMIT ? OFFSET ?';
|
||||
params.push(pageSize, offset);
|
||||
|
||||
const plans = await query(sql, params);
|
||||
// 获取数据 - 使用简单的查询方式
|
||||
const dataSql = `
|
||||
SELECT tp.*, u.username, u.real_name, u.avatar_url
|
||||
FROM travel_plans tp
|
||||
INNER JOIN users u ON tp.created_by = u.id
|
||||
${whereClause}
|
||||
ORDER BY tp.created_at DESC
|
||||
LIMIT ${parseInt(pageSize)} OFFSET ${parseInt(offset)}
|
||||
`;
|
||||
|
||||
console.log('执行SQL:', dataSql);
|
||||
console.log('参数:', params);
|
||||
|
||||
const plans = await query(dataSql, params);
|
||||
|
||||
return {
|
||||
plans: plans.map(plan => this.sanitizePlan(plan)),
|
||||
@@ -57,7 +62,7 @@ class TravelService {
|
||||
const sql = `
|
||||
SELECT tp.*, u.username, u.real_name, u.avatar_url
|
||||
FROM travel_plans tp
|
||||
INNER JOIN users u ON tp.user_id = u.id
|
||||
INNER JOIN users u ON tp.created_by = u.id
|
||||
WHERE tp.id = ?
|
||||
`;
|
||||
|
||||
@@ -76,37 +81,53 @@ class TravelService {
|
||||
static async createTravelPlan(userId, planData) {
|
||||
try {
|
||||
const {
|
||||
title,
|
||||
description,
|
||||
destination,
|
||||
start_date,
|
||||
end_date,
|
||||
budget,
|
||||
companions,
|
||||
transportation,
|
||||
accommodation,
|
||||
activities,
|
||||
notes
|
||||
max_participants,
|
||||
price_per_person,
|
||||
itinerary,
|
||||
requirements,
|
||||
includes,
|
||||
excludes,
|
||||
images
|
||||
} = planData;
|
||||
|
||||
const sql = `
|
||||
INSERT INTO travel_plans (
|
||||
user_id, destination, start_date, end_date, budget, companions,
|
||||
transportation, accommodation, activities, notes, status, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'planning', NOW(), NOW())
|
||||
created_by, title, description, destination, start_date, end_date,
|
||||
max_participants, price_per_person, itinerary,
|
||||
requirements, includes, excludes, images,
|
||||
status, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'published', NOW(), NOW())
|
||||
`;
|
||||
|
||||
const params = [
|
||||
userId,
|
||||
title,
|
||||
description || null,
|
||||
destination,
|
||||
start_date,
|
||||
end_date,
|
||||
budget,
|
||||
companions,
|
||||
transportation,
|
||||
accommodation,
|
||||
activities,
|
||||
notes
|
||||
max_participants || 20,
|
||||
price_per_person,
|
||||
JSON.stringify(itinerary || []),
|
||||
requirements || null,
|
||||
JSON.stringify(includes || []),
|
||||
JSON.stringify(excludes || []),
|
||||
JSON.stringify(images || [])
|
||||
];
|
||||
|
||||
// 调试:检查参数中是否有undefined
|
||||
console.log('SQL Parameters:', params);
|
||||
params.forEach((param, index) => {
|
||||
if (param === undefined) {
|
||||
console.log(`Parameter at index ${index} is undefined`);
|
||||
}
|
||||
});
|
||||
|
||||
const result = await query(sql, params);
|
||||
return result.insertId;
|
||||
} catch (error) {
|
||||
@@ -118,8 +139,9 @@ class TravelService {
|
||||
static async updateTravelPlan(planId, userId, updateData) {
|
||||
try {
|
||||
const allowedFields = [
|
||||
'destination', 'start_date', 'end_date', 'budget', 'companions',
|
||||
'transportation', 'accommodation', 'activities', 'notes', 'status'
|
||||
'title', 'description', 'destination', 'start_date', 'end_date',
|
||||
'max_participants', 'price_per_person', 'includes', 'excludes',
|
||||
'itinerary', 'images', 'requirements', 'status'
|
||||
];
|
||||
|
||||
const setClauses = [];
|
||||
@@ -127,8 +149,13 @@ class TravelService {
|
||||
|
||||
for (const [key, value] of Object.entries(updateData)) {
|
||||
if (allowedFields.includes(key) && value !== undefined) {
|
||||
setClauses.push(`${key} = ?`);
|
||||
params.push(value);
|
||||
if (Array.isArray(value)) {
|
||||
setClauses.push(`${key} = ?`);
|
||||
params.push(JSON.stringify(value));
|
||||
} else {
|
||||
setClauses.push(`${key} = ?`);
|
||||
params.push(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,7 +169,7 @@ class TravelService {
|
||||
const sql = `
|
||||
UPDATE travel_plans
|
||||
SET ${setClauses.join(', ')}
|
||||
WHERE id = ? AND user_id = ?
|
||||
WHERE id = ? AND created_by = ?
|
||||
`;
|
||||
|
||||
const result = await query(sql, params);
|
||||
@@ -159,7 +186,7 @@ class TravelService {
|
||||
// 删除旅行计划
|
||||
static async deleteTravelPlan(planId, userId) {
|
||||
try {
|
||||
const sql = 'DELETE FROM travel_plans WHERE id = ? AND user_id = ?';
|
||||
const sql = 'DELETE FROM travel_plans WHERE id = ? AND created_by = ?';
|
||||
const result = await query(sql, [planId, userId]);
|
||||
|
||||
if (result.affectedRows === 0) {
|
||||
@@ -181,9 +208,9 @@ class TravelService {
|
||||
COUNT(CASE WHEN status = 'completed' THEN 1 END) as completed_plans,
|
||||
COUNT(CASE WHEN status = 'planning' THEN 1 END) as planning_plans,
|
||||
COUNT(CASE WHEN status = 'cancelled' THEN 1 END) as cancelled_plans,
|
||||
SUM(budget) as total_budget
|
||||
SUM(price_per_person * max_participants) as total_budget
|
||||
FROM travel_plans
|
||||
WHERE user_id = ?
|
||||
WHERE created_by = ?
|
||||
`;
|
||||
|
||||
const stats = await query(sql, [userId]);
|
||||
|
||||
@@ -96,8 +96,7 @@ class UserService {
|
||||
const total = countResult[0].total;
|
||||
|
||||
// 添加分页和排序
|
||||
sql += ' ORDER BY created_at DESC LIMIT ? OFFSET ?';
|
||||
params.push(pageSize, offset);
|
||||
sql += ` ORDER BY created_at DESC LIMIT ${parseInt(pageSize)} OFFSET ${parseInt(offset)}`;
|
||||
|
||||
const users = await UserMySQL.query(sql, params);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user