commit 658ff89c6aab15b8eb6088b03d6d68638ee56828 Author: dzl <786316265@qq.com> Date: Wed Sep 24 10:02:03 2025 +0800 商城后端模板 diff --git a/.env b/.env new file mode 100644 index 0000000..067bfb4 --- /dev/null +++ b/.env @@ -0,0 +1,68 @@ +# 数据库配置 +DB_HOST=114.55.111.44 +DB_USER=maov2 +DB_PASSWORD=5fYhw8z6T62b7heS +DB_NAME=maov2 + +# JWT密钥 +JWT_SECRET=your_jwt_secret_key + +# 阿里云短信服务配置 +# 请在阿里云控制台获取以下配置信息: +# 1. AccessKey ID 和 AccessKey Secret:在阿里云控制台 -> AccessKey管理中创建 +# 2. 短信签名:在阿里云短信服务控制台中申请并审核通过的签名 +# 3. 短信模板CODE:在阿里云短信服务控制台中申请并审核通过的模板CODE +ALIYUN_ACCESS_KEY_ID=LTAI5tBHymRUu1vvo5tgYpaa +ALIYUN_ACCESS_KEY_SECRET=lNsDZvpUVX2b3pfBQCBawOEyr3dNB9 +ALIYUN_SMS_SIGN_NAME=宁波炬融歆创科技 +ALIYUN_SMS_TEMPLATE_CODE=SMS_324470054 + +# 环境配置 +NODE_ENV=development +PORT=3000 + +# 前端地址配置 +FRONTEND_URL=https://www.zrbjr.com/frontend +# FRONTEND_URL=http://114.55.111.44:3001/frontend + +# MinIO 对象存储配置 +# MinIO服务器地址(不包含协议) +MINIO_ENDPOINT=114.55.111.44 +# MinIO服务器端口 +MINIO_PORT=9000 +# 是否使用SSL(true/false) +MINIO_USE_SSL=false +# MinIO访问密钥 +MINIO_ACCESS_KEY=minio +# MinIO秘密密钥 +MINIO_SECRET_KEY=CNy6fMCfyfeaEjbE +# MinIO公开访问地址(用于生成文件URL) +MINIO_PUBLIC_URL=https://minio.zrbjr.com + +# MinIO存储桶配置 +MINIO_BUCKET_UPLOADS=jurongquan +MINIO_BUCKET_AVATARS=jurongquan +MINIO_BUCKET_PRODUCTS=jurongquan +MINIO_BUCKET_DOCUMENTS=jurongquan + +#支付配置 +WECHAT_APP_ID=wx3a702dbe13fd2217 +WECHAT_MCH_ID=1726377336 +WECHAT_API_KEY=NINGBOJURONGkejiyouxiangongsi202 +WECHAT_API_V3_KEY=NINGBOJURONGkejiyouxiangongsi202 +WECHAT_CERT_PATH=./cert/apiclient_cert.pem +WECHAT_KEY_PATH=./cert/apiclient_key.pem +WECHAT_NOTIFY_URL=https://www.zrbjr.com/api/wechat-pay/notify + +# 支付宝配置 +# 请在支付宝开放平台获取以下配置信息: +# 1. 应用ID:在支付宝开放平台创建应用后获得 +# 2. 应用私钥和支付宝公钥现在从文件读取 +ALIPAY_APP_ID=2021005188682022 +ALIPAY_NOTIFY_URL=https://www.zrbjr.com/api/payment/alipay/notify +ALIPAY_RETURN_URL=https://www.zrbjr.com/payment +ALIPAY_QUIT_URL=https://www.zrbjr.com/payment/ +#ALIPAY_APP_ID=9021000151699946 +#ALIPAY_NOTIFY_URL=https://test.zrbjr.com/api/payment/alipay/notify +#ALIPAY_RETURN_URL=http://192.168.1.124:5173/frontend/payment +#ALIPAY_QUIT_URL=http://192.168.1.124:5173/frontend/payment diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..eba2d37 --- /dev/null +++ b/.env.example @@ -0,0 +1,64 @@ +# 数据库配置 +DB_HOST=114.55.111.44 +DB_USER=maov2 +DB_PASSWORD=5fYhw8z6T62b7heS +DB_NAME=maov2 + +# JWT密钥 +JWT_SECRET=your_jwt_secret_key + +# 阿里云短信服务配置 +# 请在阿里云控制台获取以下配置信息: +# 1. AccessKey ID 和 AccessKey Secret:在阿里云控制台 -> AccessKey管理中创建 +# 2. 短信签名:在阿里云短信服务控制台中申请并审核通过的签名 +# 3. 短信模板CODE:在阿里云短信服务控制台中申请并审核通过的模板CODE +ALIYUN_ACCESS_KEY_ID=LTAI5tBHymRUu1vvo5tgYpaa +ALIYUN_ACCESS_KEY_SECRET=lNsDZvpUVX2b3pfBQCBawOEyr3dNB9 +ALIYUN_SMS_SIGN_NAME=宁波炬融歆创科技 +ALIYUN_SMS_TEMPLATE_CODE=SMS_324470054 + +# 环境配置 +NODE_ENV=development +PORT=3000 + +# 前端地址配置 +FRONTEND_URL=https://www.zrbjr.com/frontend +# FRONTEND_URL=http://114.55.111.44:3001/frontend + +# MinIO 对象存储配置 +# MinIO服务器地址(不包含协议) +MINIO_ENDPOINT=114.55.111.44 +# MinIO服务器端口 +MINIO_PORT=9000 +# 是否使用SSL(true/false) +MINIO_USE_SSL=false +# MinIO访问密钥 +MINIO_ACCESS_KEY=minio +# MinIO秘密密钥 +MINIO_SECRET_KEY=CNy6fMCfyfeaEjbE +# MinIO公开访问地址(用于生成文件URL) +MINIO_PUBLIC_URL=https://minio.zrbjr.com + +# MinIO存储桶配置 +MINIO_BUCKET_UPLOADS=jurongquan +MINIO_BUCKET_AVATARS=jurongquan +MINIO_BUCKET_PRODUCTS=jurongquan +MINIO_BUCKET_DOCUMENTS=jurongquan + +#支付配置 +WECHAT_APP_ID=wx3a702dbe13fd2217 +WECHAT_MCH_ID=1726377336 +WECHAT_API_KEY=NINGBOJURONGkejiyouxiangongsi202 +WECHAT_API_V3_KEY=NINGBOJURONGkejiyouxiangongsi202 +WECHAT_CERT_PATH=./cert/apiclient_cert.pem +WECHAT_KEY_PATH=./cert/apiclient_key.pem +WECHAT_NOTIFY_URL=https://www.zrbjr.com/api/wechat-pay/notify + +# 支付宝配置 +# 请在支付宝开放平台获取以下配置信息: +# 1. 应用ID:在支付宝开放平台创建应用后获得 +# 2. 应用私钥和支付宝公钥现在从文件读取 +ALIPAY_APP_ID=2021005188682022 +ALIPAY_NOTIFY_URL=https://www.zrbjr.com/api/payment/alipay/notify +ALIPAY_RETURN_URL=https://www.zrbjr.com/payment/success +ALIPAY_QUIT_URL=https://www.zrbjr.com/payment/cancel \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0839afc --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/node_modules +/logs +/uploads +/frontend +/admin diff --git a/.trae/TODO.md b/.trae/TODO.md new file mode 100644 index 0000000..8ad817f --- /dev/null +++ b/.trae/TODO.md @@ -0,0 +1,7 @@ +# TODO: + +- [x] backend-api: 在 routes/transfers.js 中创建 GET /api/transfers/pending-allocations 接口 (priority: High) +- [x] frontend-tabs: 在 admin/src/views/Transfers.vue 中添加待处理匹配订单标签页 (priority: High) +- [x] frontend-display: 实现待处理匹配订单的数据显示和表格 (priority: High) +- [x] frontend-operations: 添加待处理匹配订单的操作功能(确认、拒绝等) (priority: Medium) +- [x] testing: 测试新功能的完整性和正确性 (priority: Medium) diff --git a/.vercel/project.json b/.vercel/project.json new file mode 100644 index 0000000..d931e9c --- /dev/null +++ b/.vercel/project.json @@ -0,0 +1 @@ +{"projectName":"trae_code_bv1k"} \ No newline at end of file diff --git a/API-DOCS-README.md b/API-DOCS-README.md new file mode 100644 index 0000000..a47efc1 --- /dev/null +++ b/API-DOCS-README.md @@ -0,0 +1,123 @@ +# API文档生成与Apifox同步指南 + +本项目已集成Swagger用于API文档生成,并提供了与Apifox同步的工具脚本。 + +## 1. 查看API文档 + +启动服务器后,可以通过以下URL访问Swagger UI界面查看API文档: + +``` +http://localhost:3000/api-docs +``` + +## 2. 手动导出并导入Apifox + +### 2.1 导出Swagger文档 + +运行以下命令导出Swagger文档: + +```bash +node export-swagger.js +``` + +这将在`api-docs`目录下生成`swagger.json`文件。 + +### 2.2 手动导入Apifox + +1. 打开Apifox应用 +2. 选择您的项目 +3. 点击"导入"按钮 +4. 选择"导入OpenAPI(Swagger)" +5. 选择刚才导出的swagger.json文件 +6. 按照Apifox的导入向导完成导入 + +## 3. 自动同步到Apifox + +### 3.1 安装Apifox CLI + +```bash +npm install -g apifox-cli +``` + +### 3.2 登录Apifox CLI + +```bash +apifox-cli login +``` + +### 3.3 配置项目ID + +编辑`apifox-sync.js`文件,将`YOUR_APIFOX_PROJECT_ID`替换为您的Apifox项目ID。 + +> 获取项目ID:在Apifox网页版中打开项目,URL中包含项目ID + +### 3.4 运行同步脚本 + +```bash +node apifox-sync.js +``` + +## 4. 为API添加Swagger注释 + +为了使API文档保持最新,在添加新的API接口时,请按照以下格式添加Swagger注释: + +```javascript +/** + * @swagger + * /api/resource: + * get: + * summary: 接口描述 + * tags: [资源分类] + * parameters: + * - name: param + * in: query + * description: 参数描述 + * required: true + * schema: + * type: string + * responses: + * 200: + * description: 成功响应 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * data: + * type: object + */ +router.get('/resource', (req, res) => { + // 实现代码 +}); +``` + +## 5. 定义数据模型 + +在路由文件顶部定义数据模型,例如: + +```javascript +/** + * @swagger + * components: + * schemas: + * ModelName: + * type: object + * properties: + * id: + * type: integer + * name: + * type: string + * required: + * - name + */ +``` + +## 6. 自动化集成 + +可以将API文档生成和同步集成到CI/CD流程中,在每次部署前自动更新API文档。 + +--- + +如有任何问题,请参考[Swagger官方文档](https://swagger.io/docs/)和[Apifox官方文档](https://www.apifox.cn/help/)。 \ No newline at end of file diff --git a/MIGRATION-GUIDE.md b/MIGRATION-GUIDE.md new file mode 100644 index 0000000..9fbf739 --- /dev/null +++ b/MIGRATION-GUIDE.md @@ -0,0 +1,186 @@ +# 文件迁移到 MinIO 指南 + +本指南将帮助您将现有的本地静态文件迁移到 MinIO 对象存储。 + +## 迁移前准备 + +### 1. 确保 MinIO 配置正确 + +确保 `.env` 文件中的 MinIO 配置正确: + +```env +MINIO_ENDPOINT=your-minio-server +MINIO_PORT=9000 +MINIO_ACCESS_KEY=your-access-key +MINIO_SECRET_KEY=your-secret-key +MINIO_USE_SSL=false +MINIO_PUBLIC_URL=https://minio.yourdomain.com +``` + +### 2. 确保存储桶已创建 + +确保以下存储桶在 MinIO 中已存在: +- `avatars` - 用户头像 +- `products` - 产品图片 +- `documents` - 文档文件 + +### 3. 备份数据库 + +**重要:在开始迁移前,请务必备份数据库!** + +```bash +mysqldump -u username -p database_name > backup_before_migration.sql +``` + +## 执行迁移 + +### 1. 运行迁移脚本 + +```bash +cd jurong_circle_black +node migrate-to-minio.js +``` + +### 2. 监控迁移过程 + +脚本会显示详细的迁移进度: +- 扫描本地文件 +- 逐个上传到 MinIO +- 更新数据库中的文件路径引用 +- 生成迁移报告 + +## 迁移后验证 + +### 1. 检查迁移报告 + +查看生成的 `migration-report.json` 文件: + +```json +{ + "migrationDate": "2024-01-15T10:30:00.000Z", + "totalFiles": 150, + "successCount": 148, + "failedCount": 2, + "migratedFiles": [...], + "failedFiles": [...] +} +``` + +### 2. 验证文件访问 + +- 检查用户头像是否正常显示 +- 检查产品图片是否正常显示 +- 检查文档下载是否正常 + +### 3. 验证数据库更新 + +检查数据库中的文件路径是否已更新: + +```sql +-- 检查用户头像路径 +SELECT id, username, avatar FROM users WHERE avatar LIKE 'https://minio%' LIMIT 10; + +-- 检查产品图片路径 +SELECT id, name, image_url FROM products WHERE image_url LIKE 'https://minio%' LIMIT 10; +``` + +## 文件组织结构 + +迁移后,文件将按以下结构组织: + +``` +MinIO 存储桶/ +├── avatars/ +│ ├── 2024/ +│ │ ├── 01/ +│ │ │ ├── 15/ +│ │ │ │ ├── 1640995200000_a1b2c3d4.jpg +│ │ │ │ └── 1640995300000_b2c3d4e5.png +│ │ │ └── 16/ +│ │ └── 02/ +│ └── 2023/ +├── products/ +│ ├── 2024/ +│ │ ├── 01/ +│ │ │ ├── 15/ +│ │ │ └── 16/ +│ │ └── 02/ +│ └── 2023/ +└── documents/ + ├── 2024/ + └── 2023/ +``` + +## 故障排除 + +### 常见问题 + +1. **连接 MinIO 失败** + - 检查 MinIO 服务是否运行 + - 验证网络连接 + - 确认访问密钥正确 + +2. **存储桶不存在** + - 在 MinIO 控制台创建所需的存储桶 + - 确保存储桶名称与配置一致 + +3. **权限问题** + - 确保 MinIO 用户有读写权限 + - 检查存储桶策略设置 + +4. **部分文件迁移失败** + - 查看迁移报告中的失败文件列表 + - 检查文件是否损坏或被占用 + - 手动重新上传失败的文件 + +### 回滚方案 + +如果迁移出现问题,可以通过以下步骤回滚: + +1. **恢复数据库备份** + ```bash + mysql -u username -p database_name < backup_before_migration.sql + ``` + +2. **重新配置文件上传路径** + - 修改 `routes/upload.js` 使用本地存储 + - 确保 `uploads` 目录存在且有正确权限 + +## 迁移完成后的清理 + +### 1. 删除本地文件(可选) + +**警告:只有在确认迁移成功且系统运行正常后才执行此操作!** + +```bash +# 备份 uploads 目录 +mv uploads uploads_backup_$(date +%Y%m%d) + +# 或者直接删除(谨慎操作) +# rm -rf uploads +``` + +### 2. 更新部署脚本 + +更新生产环境的部署脚本,移除对 `uploads` 目录的依赖。 + +### 3. 更新备份策略 + +确保备份策略包含 MinIO 数据的备份。 + +## 注意事项 + +1. **迁移时间**:根据文件数量和大小,迁移可能需要较长时间 +2. **网络稳定性**:确保网络连接稳定,避免迁移中断 +3. **存储空间**:确保 MinIO 有足够的存储空间 +4. **并发限制**:脚本已添加延迟避免过快请求,如需调整可修改代码 +5. **文件路径**:迁移后的文件路径将包含日期文件夹结构 + +## 技术支持 + +如果在迁移过程中遇到问题,请: + +1. 查看控制台输出的错误信息 +2. 检查 `migration-report.json` 中的详细信息 +3. 确保 MinIO 服务正常运行 +4. 验证网络连接和权限设置 \ No newline at end of file diff --git a/api-docs/swagger.json b/api-docs/swagger.json new file mode 100644 index 0000000..066f91c --- /dev/null +++ b/api-docs/swagger.json @@ -0,0 +1,8639 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "融豆商城 API", + "version": "1.0.0", + "description": "融豆商城后端API文档", + "contact": { + "name": "技术支持", + "email": "support@example.com" + } + }, + "servers": [ + { + "url": "/api", + "description": "API服务器" + } + ], + "components": { + "securitySchemes": { + "bearerAuth": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT" + } + }, + "schemas": { + "Announcement": { + "type": "object", + "required": [ + "title", + "content", + "type", + "priority" + ], + "properties": { + "id": { + "type": "integer", + "description": "公告ID", + "example": 1 + }, + "title": { + "type": "string", + "description": "公告标题", + "example": "系统维护通知" + }, + "content": { + "type": "string", + "description": "公告内容", + "example": "系统将于今晚进行维护,预计维护时间2小时,期间可能影响部分功能使用。" + }, + "type": { + "type": "string", + "description": "公告类型", + "enum": [ + "system", + "maintenance", + "promotion", + "warning" + ], + "example": "maintenance" + }, + "priority": { + "type": "string", + "description": "优先级", + "enum": [ + "low", + "medium", + "high", + "urgent" + ], + "example": "high" + }, + "status": { + "type": "string", + "description": "状态", + "enum": [ + "draft", + "published", + "archived" + ], + "example": "published" + }, + "isTop": { + "type": "boolean", + "description": "是否置顶", + "example": false + }, + "publishTime": { + "type": "string", + "format": "date-time", + "description": "发布时间", + "example": "2024-01-15T10:00:00Z" + }, + "expireTime": { + "type": "string", + "format": "date-time", + "description": "过期时间", + "example": "2024-01-20T10:00:00Z" + }, + "createdBy": { + "type": "integer", + "description": "创建者用户ID", + "example": 1 + }, + "createdAt": { + "type": "string", + "format": "date-time", + "description": "创建时间", + "example": "2024-01-15T09:00:00Z" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "description": "更新时间", + "example": "2024-01-15T09:30:00Z" + }, + "creator": { + "type": "object", + "description": "创建者信息", + "properties": { + "id": { + "type": "integer", + "example": 1 + }, + "username": { + "type": "string", + "example": "admin" + }, + "email": { + "type": "string", + "example": "admin@example.com" + } + } + }, + "is_pinned": { + "type": "boolean", + "description": "是否置顶" + }, + "publish_time": { + "type": "string", + "format": "date-time", + "description": "发布时间" + }, + "expire_time": { + "type": "string", + "format": "date-time", + "description": "过期时间" + }, + "created_by": { + "type": "integer", + "description": "创建者ID" + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "创建时间" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "description": "更新时间" + } + } + }, + "AnnouncementCreate": { + "type": "object", + "required": [ + "title", + "content", + "type", + "priority" + ], + "properties": { + "title": { + "type": "string", + "description": "公告标题", + "example": "系统维护通知" + }, + "content": { + "type": "string", + "description": "公告内容", + "example": "系统将于今晚进行维护,预计维护时间2小时。" + }, + "type": { + "type": "string", + "description": "公告类型", + "enum": [ + "system", + "activity", + "maintenance", + "urgent" + ], + "example": "maintenance" + }, + "priority": { + "type": "string", + "description": "优先级", + "enum": [ + "high", + "medium", + "low" + ], + "example": "high" + }, + "status": { + "type": "string", + "description": "公告状态", + "enum": [ + "draft", + "published" + ], + "default": "draft", + "example": "draft" + }, + "isTop": { + "type": "boolean", + "description": "是否置顶", + "default": false, + "example": false + }, + "publishTime": { + "type": "string", + "format": "date-time", + "description": "发布时间", + "example": "2024-01-15T10:00:00Z" + }, + "expireTime": { + "type": "string", + "format": "date-time", + "description": "过期时间", + "example": "2024-01-20T10:00:00Z" + } + } + }, + "AnnouncementUpdate": { + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "公告标题", + "example": "系统维护通知(更新)" + }, + "content": { + "type": "string", + "description": "公告内容", + "example": "系统维护时间调整为明晚进行。" + }, + "type": { + "type": "string", + "description": "公告类型", + "enum": [ + "system", + "activity", + "maintenance", + "urgent" + ], + "example": "maintenance" + }, + "priority": { + "type": "string", + "description": "优先级", + "enum": [ + "high", + "medium", + "low" + ], + "example": "medium" + }, + "status": { + "type": "string", + "description": "公告状态", + "enum": [ + "draft", + "published", + "expired" + ], + "example": "published" + }, + "isTop": { + "type": "boolean", + "description": "是否置顶", + "example": true + }, + "publishTime": { + "type": "string", + "format": "date-time", + "description": "发布时间", + "example": "2024-01-16T10:00:00Z" + }, + "expireTime": { + "type": "string", + "format": "date-time", + "description": "过期时间", + "example": "2024-01-21T10:00:00Z" + } + } + }, + "AnnouncementList": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "data": { + "type": "object", + "properties": { + "announcements": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Announcement" + } + }, + "total": { + "type": "integer", + "description": "总记录数", + "example": 50 + }, + "page": { + "type": "integer", + "description": "当前页码", + "example": 1 + }, + "limit": { + "type": "integer", + "description": "每页数量", + "example": 10 + }, + "totalPages": { + "type": "integer", + "description": "总页数", + "example": 5 + } + } + } + } + }, + "AnnouncementResponse": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "操作成功" + }, + "data": { + "$ref": "#/components/schemas/Announcement" + } + } + }, + "AnnouncementError": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": false + }, + "message": { + "type": "string", + "example": "操作失败" + }, + "error": { + "type": "string", + "example": "公告不存在" + } + } + }, + "CartItem": { + "type": "object", + "required": [ + "user_id", + "product_id", + "quantity" + ], + "properties": { + "id": { + "type": "integer", + "description": "购物车项ID" + }, + "user_id": { + "type": "integer", + "description": "用户ID" + }, + "product_id": { + "type": "integer", + "description": "商品ID" + }, + "quantity": { + "type": "integer", + "description": "商品数量" + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "创建时间" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "description": "更新时间" + }, + "spec_combination_id": { + "type": "integer", + "description": "商品规格组合ID" + }, + "product": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "price": { + "type": "integer" + }, + "points_price": { + "type": "integer" + }, + "rongdou_price": { + "type": "integer" + }, + "image_url": { + "type": "string" + }, + "stock": { + "type": "integer" + }, + "status": { + "type": "string" + } + } + } + } + }, + "CartItemWithProduct": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "购物车商品ID" + }, + "product_id": { + "type": "integer", + "description": "商品ID" + }, + "product_name": { + "type": "string", + "description": "商品名称" + }, + "quantity": { + "type": "integer", + "description": "商品数量" + }, + "points_price": { + "type": "integer", + "description": "积分价格" + }, + "rongdou_price": { + "type": "number", + "description": "融豆价格" + }, + "image_url": { + "type": "string", + "description": "商品图片URL" + }, + "stock": { + "type": "integer", + "description": "库存数量" + }, + "payment_methods": { + "type": "array", + "items": { + "type": "string" + }, + "description": "支付方式列表" + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "创建时间" + } + } + }, + "AddToCartRequest": { + "type": "object", + "required": [ + "product_id", + "quantity" + ], + "properties": { + "product_id": { + "type": "integer", + "description": "商品ID" + }, + "quantity": { + "type": "integer", + "minimum": 1, + "description": "商品数量" + } + } + }, + "UpdateCartRequest": { + "type": "object", + "required": [ + "quantity" + ], + "properties": { + "quantity": { + "type": "integer", + "minimum": 1, + "description": "商品数量" + } + } + }, + "Order": { + "type": "object", + "required": [ + "user_id", + "total_amount", + "status" + ], + "properties": { + "id": { + "type": "integer", + "description": "订单ID" + }, + "order_number": { + "type": "string", + "description": "订单号" + }, + "user_id": { + "type": "integer", + "description": "用户ID" + }, + "total_amount": { + "type": "number", + "description": "订单总金额" + }, + "total_points": { + "type": "integer", + "description": "订单总积分" + }, + "total_rongdou": { + "type": "number", + "description": "订单总融豆" + }, + "status": { + "type": "string", + "description": "订单状态", + "enum": [ + "pending", + "confirmed", + "shipped", + "delivered", + "cancelled" + ] + }, + "payment_status": { + "type": "string", + "description": "支付状态", + "enum": [ + "pending", + "paid", + "failed", + "refunded" + ] + }, + "shipping_address": { + "type": "string", + "description": "收货地址" + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "创建时间" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "description": "更新时间" + } + } + }, + "OrderItem": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "订单商品ID" + }, + "order_id": { + "type": "integer", + "description": "订单ID" + }, + "product_id": { + "type": "integer", + "description": "商品ID" + }, + "quantity": { + "type": "integer", + "description": "商品数量" + }, + "price": { + "type": "number", + "description": "商品价格" + }, + "points_price": { + "type": "integer", + "description": "积分价格" + }, + "rongdou_price": { + "type": "number", + "description": "融豆价格" + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "创建时间" + } + } + }, + "PreOrder": { + "type": "object", + "properties": { + "preOrderId": { + "type": "integer", + "description": "预订单ID" + }, + "orderNumber": { + "type": "string", + "description": "订单号" + }, + "totalAmount": { + "type": "number", + "description": "总金额" + }, + "totalPoints": { + "type": "integer", + "description": "所需积分总数" + }, + "totalRongdou": { + "type": "number", + "description": "所需融豆总数" + }, + "paymentMethods": { + "type": "array", + "items": { + "type": "string" + }, + "description": "去重后的支付方式列表" + } + } + }, + "Product": { + "type": "object", + "required": [ + "name", + "points_price", + "stock" + ], + "properties": { + "id": { + "type": "integer", + "description": "商品ID" + }, + "name": { + "type": "string", + "description": "商品名称" + }, + "category": { + "type": "string", + "description": "商品分类" + }, + "points_price": { + "type": "integer", + "description": "积分价格" + }, + "rongdou_price": { + "type": "number", + "description": "融豆价格" + }, + "stock": { + "type": "integer", + "description": "库存数量" + }, + "image_url": { + "type": "string", + "description": "商品图片URL" + }, + "description": { + "type": "string", + "description": "商品描述" + }, + "status": { + "type": "string", + "description": "商品状态", + "enum": [ + "active", + "inactive" + ] + }, + "payment_methods": { + "type": "array", + "items": { + "type": "string" + }, + "description": "支付方式列表" + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "创建时间" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "description": "更新时间" + } + } + }, + "User": { + "type": "object", + "required": [ + "username", + "password", + "real_name", + "id_card" + ], + "properties": { + "id": { + "type": "integer", + "description": "用户ID" + }, + "username": { + "type": "string", + "description": "用户名" + }, + "email": { + "type": "string", + "format": "email", + "description": "邮箱地址" + }, + "phone": { + "type": "string", + "description": "手机号" + }, + "points": { + "type": "integer", + "description": "用户积分" + }, + "rongdou": { + "type": "number", + "description": "融豆余额" + }, + "avatar": { + "type": "string", + "description": "用户头像URL" + }, + "status": { + "type": "string", + "description": "用户状态", + "enum": [ + "active", + "inactive", + "banned" + ] + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "创建时间" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "description": "更新时间" + }, + "role": { + "type": "string", + "description": "用户角色", + "enum": [ + "user", + "admin", + "merchant" + ] + }, + "real_name": { + "type": "string", + "description": "真实姓名" + }, + "id_card": { + "type": "string", + "description": "身份证号" + }, + "is_system_account": { + "type": "boolean", + "description": "是否为系统账户" + }, + "is_distribute": { + "type": "boolean", + "description": "是否为分发账户" + } + } + }, + "UserProfile": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "用户ID" + }, + "username": { + "type": "string", + "description": "用户名" + }, + "email": { + "type": "string", + "description": "邮箱地址" + }, + "phone": { + "type": "string", + "description": "手机号码" + }, + "points": { + "type": "integer", + "description": "积分余额" + }, + "rongdou": { + "type": "number", + "description": "融豆余额" + }, + "avatar": { + "type": "string", + "description": "头像URL" + } + } + }, + "LoginRequest": { + "type": "object", + "required": [ + "username", + "password" + ], + "properties": { + "username": { + "type": "string", + "description": "用户名或邮箱" + }, + "password": { + "type": "string", + "description": "密码" + } + } + }, + "RegisterRequest": { + "type": "object", + "required": [ + "username", + "phone", + "password", + "registrationCode", + "city", + "district_id", + "captchaId", + "captchaText", + "smsCode" + ], + "properties": { + "username": { + "type": "string", + "description": "用户名" + }, + "email": { + "type": "string", + "format": "email", + "description": "邮箱地址" + }, + "password": { + "type": "string", + "description": "密码" + }, + "phone": { + "type": "string", + "description": "手机号" + }, + "registrationCode": { + "type": "string", + "description": "注册激活码" + }, + "city": { + "type": "string", + "description": "城市" + }, + "district_id": { + "type": "string", + "description": "区域ID" + }, + "captchaId": { + "type": "string", + "description": "图形验证码ID" + }, + "captchaText": { + "type": "string", + "description": "图形验证码文本" + }, + "smsCode": { + "type": "string", + "description": "短信验证码" + }, + "role": { + "type": "string", + "description": "用户角色", + "default": "user" + } + } + }, + "Address": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "地址ID" + }, + "user_id": { + "type": "integer", + "description": "用户ID" + }, + "recipient_name": { + "type": "string", + "description": "收件人姓名" + }, + "phone": { + "type": "string", + "description": "联系电话" + }, + "province_code": { + "type": "string", + "description": "省份编码" + }, + "province_name": { + "type": "string", + "description": "省份名称" + }, + "city_code": { + "type": "string", + "description": "城市编码" + }, + "city_name": { + "type": "string", + "description": "城市名称" + }, + "district_code": { + "type": "string", + "description": "区县编码" + }, + "district_name": { + "type": "string", + "description": "区县名称" + }, + "detailed_address": { + "type": "string", + "description": "详细地址" + }, + "postal_code": { + "type": "string", + "description": "邮政编码" + }, + "label_id": { + "type": "integer", + "description": "地址标签ID" + }, + "is_default": { + "type": "boolean", + "description": "是否为默认地址" + }, + "label_name": { + "type": "string", + "description": "标签名称" + }, + "label_color": { + "type": "string", + "description": "标签颜色" + } + }, + "required": [ + "recipient_name", + "phone", + "province_code", + "province_name", + "city_code", + "city_name", + "district_code", + "district_name", + "detailed_address" + ] + }, + "LoginCredentials": { + "type": "object", + "required": [ + "username", + "password" + ], + "properties": { + "username": { + "type": "string", + "description": "用户名或手机号" + }, + "password": { + "type": "string", + "description": "密码" + } + } + }, + "MatchingOrder": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "匹配订单ID" + }, + "initiator_id": { + "type": "integer", + "description": "发起人ID" + }, + "matching_type": { + "type": "string", + "enum": [ + "small", + "large" + ], + "description": "匹配类型(小额或大额)" + }, + "amount": { + "type": "number", + "description": "匹配总金额" + }, + "status": { + "type": "string", + "enum": [ + "pending", + "matching", + "completed", + "failed" + ], + "description": "订单状态" + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "创建时间" + } + } + }, + "Allocation": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "分配ID" + }, + "from_user_id": { + "type": "integer", + "description": "发送方用户ID" + }, + "to_user_id": { + "type": "integer", + "description": "接收方用户ID" + }, + "amount": { + "type": "number", + "description": "分配金额" + }, + "cycle_number": { + "type": "integer", + "description": "轮次编号" + }, + "status": { + "type": "string", + "enum": [ + "pending", + "confirmed", + "rejected", + "cancelled" + ], + "description": "分配状态" + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "创建时间" + } + } + }, + "UnreasonableMatch": { + "type": "object", + "properties": { + "allocation_id": { + "type": "integer", + "description": "分配ID" + }, + "from_user_id": { + "type": "integer", + "description": "发送方用户ID" + }, + "to_user_id": { + "type": "integer", + "description": "接收方用户ID" + }, + "amount": { + "type": "number", + "description": "分配金额" + }, + "status": { + "type": "string", + "enum": [ + "pending", + "confirmed", + "rejected", + "cancelled" + ], + "description": "分配状态" + }, + "to_username": { + "type": "string", + "description": "接收方用户名" + }, + "to_user_balance": { + "type": "number", + "description": "接收方用户余额" + }, + "from_username": { + "type": "string", + "description": "发送方用户名" + }, + "from_user_balance": { + "type": "number", + "description": "发送方用户余额" + } + } + }, + "PointsHistory": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "积分历史记录ID" + }, + "points_change": { + "type": "integer", + "description": "积分变动数量" + }, + "type": { + "type": "string", + "description": "积分变动类型(earn-获得, spend-消费, admin_adjust-管理员调整)" + }, + "description": { + "type": "string", + "description": "积分变动描述" + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "创建时间" + } + } + }, + "Region": { + "type": "object", + "properties": { + "code": { + "type": "string", + "description": "地区编码" + }, + "name": { + "type": "string", + "description": "地区名称" + } + } + }, + "ZhejiangRegion": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "地区ID" + }, + "city_name": { + "type": "string", + "description": "城市名称" + }, + "district_name": { + "type": "string", + "description": "区县名称" + }, + "region_code": { + "type": "string", + "description": "地区编码" + }, + "is_available": { + "type": "integer", + "description": "是否可用(1:可用 0:不可用)" + } + } + }, + "SMSVerification": { + "type": "object", + "properties": { + "phone": { + "type": "string", + "description": "手机号码" + }, + "code": { + "type": "string", + "description": "验证码" + } + } + }, + "SpecName": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string", + "description": "规格名称(如:颜色、尺寸)" + }, + "display_name": { + "type": "string", + "description": "显示名称" + }, + "sort_order": { + "type": "integer", + "description": "排序" + }, + "status": { + "type": "string", + "enum": [ + "active", + "inactive" + ] + } + } + }, + "SpecValue": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "spec_name_id": { + "type": "integer" + }, + "value": { + "type": "string", + "description": "规格值(如:红色、XL)" + }, + "display_value": { + "type": "string" + }, + "color_code": { + "type": "string", + "description": "颜色代码" + }, + "image_url": { + "type": "string" + }, + "sort_order": { + "type": "integer" + }, + "status": { + "type": "string", + "enum": [ + "active", + "inactive" + ] + } + } + }, + "Transfer": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "转账记录ID" + }, + "user_id": { + "type": "integer", + "description": "用户ID" + }, + "recipient_id": { + "type": "integer", + "description": "接收方用户ID" + }, + "amount": { + "type": "number", + "format": "float", + "description": "转账金额" + }, + "status": { + "type": "string", + "enum": [ + "pending", + "completed", + "failed", + "cancelled" + ], + "description": "转账状态" + }, + "transfer_type": { + "type": "string", + "enum": [ + "user_to_user", + "user_to_system", + "system_to_user" + ], + "description": "转账类型" + }, + "voucher_image": { + "type": "string", + "description": "转账凭证图片路径" + }, + "remark": { + "type": "string", + "description": "转账备注" + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "创建时间" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "description": "更新时间" + } + } + }, + "Pagination": { + "type": "object", + "properties": { + "total": { + "type": "integer", + "description": "总记录数" + }, + "page": { + "type": "integer", + "description": "当前页码" + }, + "limit": { + "type": "integer", + "description": "每页记录数" + }, + "total_pages": { + "type": "integer", + "description": "总页数" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "paths": { + "/api/announcements/{id}": { + "get": { + "summary": "获取单个公告详情", + "tags": [ + "Announcements" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + }, + "description": "公告ID" + } + ], + "responses": { + "200": { + "description": "成功获取公告详情", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "data": { + "$ref": "#/components/schemas/Announcement" + } + } + } + } + } + }, + "401": { + "description": "未授权" + }, + "404": { + "description": "公告不存在" + }, + "500": { + "description": "服务器错误" + } + } + }, + "put": { + "summary": "更新通知公告", + "tags": [ + "Announcements" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + }, + "description": "公告ID" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "公告标题" + }, + "content": { + "type": "string", + "description": "公告内容" + }, + "type": { + "type": "string", + "enum": [ + "system", + "activity", + "maintenance", + "urgent" + ], + "description": "公告类型" + }, + "priority": { + "type": "string", + "enum": [ + "high", + "medium", + "low", + "urgent" + ], + "description": "优先级" + }, + "status": { + "type": "string", + "enum": [ + "draft", + "published", + "expired" + ], + "description": "状态" + }, + "is_pinned": { + "type": "boolean" + }, + "publish_time": { + "type": "string", + "format": "date-time" + }, + "expire_time": { + "type": "string", + "format": "date-time" + }, + "isTop": { + "type": "boolean", + "description": "是否置顶" + }, + "publishTime": { + "type": "string", + "format": "date-time", + "description": "发布时间" + }, + "expireTime": { + "type": "string", + "format": "date-time", + "description": "过期时间" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "公告更新成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "公告更新成功" + }, + "data": { + "$ref": "#/components/schemas/Announcement" + } + } + } + } + } + }, + "400": { + "description": "请求参数错误" + }, + "401": { + "description": "未授权" + }, + "404": { + "description": "公告不存在" + }, + "500": { + "description": "服务器错误" + } + } + }, + "delete": { + "summary": "删除通知公告", + "tags": [ + "Announcements" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + }, + "description": "公告ID" + } + ], + "responses": { + "200": { + "description": "公告删除成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "公告删除成功" + } + } + } + } + } + }, + "401": { + "description": "未授权" + }, + "404": { + "description": "公告不存在" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/api/announcements/{id}/read": { + "post": { + "summary": "标记公告为已读", + "tags": [ + "Announcements" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + }, + "description": "公告ID" + } + ], + "responses": { + "200": { + "description": "标记已读成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "已标记为已读" + } + } + } + } + } + }, + "401": { + "description": "未授权" + }, + "404": { + "description": "公告不存在" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/api/announcements/unread/count": { + "get": { + "summary": "获取用户未读公告数量", + "tags": [ + "Announcements" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "获取未读数量成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "data": { + "type": "object", + "properties": { + "unread_count": { + "type": "integer", + "example": 5, + "description": "未读公告数量" + } + } + } + } + } + } + } + }, + "401": { + "description": "未授权" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/api/announcements/batch/read": { + "post": { + "summary": "批量标记公告为已读", + "tags": [ + "Announcements" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "announcement_ids" + ], + "properties": { + "announcement_ids": { + "type": "array", + "items": { + "type": "integer" + }, + "example": [ + 1, + 2, + 3 + ], + "description": "公告ID列表" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "批量标记已读成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "批量标记已读成功" + } + } + } + } + } + }, + "400": { + "description": "请求参数错误" + }, + "401": { + "description": "未授权" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/api/announcements": { + "post": { + "summary": "创建新的通知公告", + "tags": [ + "Announcements" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "title", + "content", + "type", + "priority" + ], + "properties": { + "title": { + "type": "string", + "description": "公告标题", + "example": "系统维护通知" + }, + "content": { + "type": "string", + "description": "公告内容", + "example": "系统将于今晚进行维护,预计维护时间2小时" + }, + "type": { + "type": "string", + "enum": [ + "system", + "activity", + "maintenance", + "urgent" + ], + "default": "system", + "description": "公告类型", + "example": "maintenance" + }, + "priority": { + "type": "string", + "enum": [ + "high", + "medium", + "low", + "urgent" + ], + "default": "medium", + "description": "优先级", + "example": "high" + }, + "status": { + "type": "string", + "enum": [ + "draft", + "published" + ], + "default": "draft", + "description": "状态" + }, + "is_pinned": { + "type": "boolean", + "default": false + }, + "publish_time": { + "type": "string", + "format": "date-time" + }, + "expire_time": { + "type": "string", + "format": "date-time" + }, + "isTop": { + "type": "boolean", + "default": false, + "description": "是否置顶" + }, + "publishTime": { + "type": "string", + "format": "date-time", + "description": "发布时间" + }, + "expireTime": { + "type": "string", + "format": "date-time", + "description": "过期时间" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "公告创建成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "公告创建成功" + }, + "data": { + "type": "object", + "properties": { + "id": { + "type": "integer" + } + }, + "$ref": "#/components/schemas/Announcement" + } + } + } + } + } + }, + "400": { + "description": "请求参数错误" + }, + "401": { + "description": "未授权" + }, + "500": { + "description": "服务器错误" + } + } + }, + "get": { + "summary": "获取通知公告列表", + "tags": [ + "Announcements" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "in": "query", + "name": "page", + "schema": { + "type": "integer", + "default": 1 + }, + "description": "页码" + }, + { + "in": "query", + "name": "limit", + "schema": { + "type": "integer", + "default": 10 + }, + "description": "每页数量" + }, + { + "in": "query", + "name": "search", + "schema": { + "type": "string" + }, + "description": "搜索关键词(标题或内容)" + }, + { + "in": "query", + "name": "type", + "schema": { + "type": "string", + "enum": [ + "system", + "activity", + "maintenance", + "urgent" + ] + }, + "description": "公告类型" + }, + { + "in": "query", + "name": "priority", + "schema": { + "type": "string", + "enum": [ + "high", + "medium", + "low" + ] + }, + "description": "优先级" + }, + { + "in": "query", + "name": "status", + "schema": { + "type": "string", + "enum": [ + "draft", + "published", + "expired" + ] + }, + "description": "状态" + }, + { + "in": "query", + "name": "isTop", + "schema": { + "type": "boolean" + }, + "description": "是否置顶" + }, + { + "in": "query", + "name": "sortBy", + "schema": { + "type": "string", + "enum": [ + "created_at", + "updated_at", + "publish_time", + "priority" + ], + "default": "created_at" + }, + "description": "排序字段" + }, + { + "in": "query", + "name": "sortOrder", + "schema": { + "type": "string", + "enum": [ + "ASC", + "DESC" + ], + "default": "DESC" + }, + "description": "排序方向" + } + ], + "responses": { + "200": { + "description": "成功获取公告列表", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "data": { + "type": "object", + "properties": { + "announcements": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Announcement" + } + }, + "total": { + "type": "integer", + "example": 50 + }, + "page": { + "type": "integer", + "example": 1 + }, + "limit": { + "type": "integer", + "example": 10 + }, + "totalPages": { + "type": "integer", + "example": 5 + } + } + } + } + } + } + } + }, + "401": { + "description": "未授权" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/api/announcements/public/list": { + "get": { + "summary": "获取公开发布的公告列表(无需认证)", + "tags": [ + "Announcements" + ], + "parameters": [ + { + "in": "query", + "name": "limit", + "schema": { + "type": "integer", + "default": 5 + }, + "description": "获取数量" + } + ], + "responses": { + "200": { + "description": "成功获取公开公告列表", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Announcement" + } + } + } + } + } + } + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/api/announcements/{id}/toggle-top": { + "put": { + "summary": "切换公告置顶状态", + "tags": [ + "Announcements" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + }, + "description": "公告ID" + } + ], + "responses": { + "200": { + "description": "置顶状态切换成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "置顶状态更新成功" + }, + "data": { + "type": "object", + "properties": { + "isTop": { + "type": "boolean", + "example": true + } + } + } + } + } + } + } + }, + "401": { + "description": "未授权" + }, + "404": { + "description": "公告不存在" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/api/orders": { + "get": { + "summary": "获取订单列表", + "tags": [ + "Orders" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "in": "query", + "name": "page", + "schema": { + "type": "integer", + "default": 1 + }, + "description": "页码" + }, + { + "in": "query", + "name": "limit", + "schema": { + "type": "integer", + "default": 10 + }, + "description": "每页数量" + }, + { + "in": "query", + "name": "search", + "schema": { + "type": "string" + }, + "description": "搜索关键词" + }, + { + "in": "query", + "name": "orderNumber", + "schema": { + "type": "string" + }, + "description": "订单号" + }, + { + "in": "query", + "name": "username", + "schema": { + "type": "string" + }, + "description": "用户名" + }, + { + "in": "query", + "name": "status", + "schema": { + "type": "string" + }, + "description": "订单状态" + }, + { + "in": "query", + "name": "startDate", + "schema": { + "type": "string", + "format": "date" + }, + "description": "开始日期" + }, + { + "in": "query", + "name": "endDate", + "schema": { + "type": "string", + "format": "date" + }, + "description": "结束日期" + } + ], + "responses": { + "200": { + "description": "成功获取订单列表", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "data": { + "type": "object", + "properties": { + "orders": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Order" + } + }, + "pagination": { + "type": "object", + "properties": { + "page": { + "type": "integer" + }, + "limit": { + "type": "integer" + }, + "total": { + "type": "integer" + }, + "pages": { + "type": "integer" + } + } + } + } + } + } + } + } + } + }, + "401": { + "description": "未授权" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/api/orders/confirm": { + "post": { + "summary": "确认下单", + "tags": [ + "Orders" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "pre_order_id", + "address" + ], + "properties": { + "pre_order_id": { + "type": "integer", + "description": "预订单ID" + }, + "address": { + "type": "object", + "properties": { + "recipient_name": { + "type": "string", + "description": "收货人姓名" + }, + "phone": { + "type": "string", + "description": "收货人电话" + }, + "province": { + "type": "string", + "description": "省份" + }, + "city": { + "type": "string", + "description": "城市" + }, + "district": { + "type": "string", + "description": "区县" + }, + "detail_address": { + "type": "string", + "description": "详细地址" + } + } + } + } + } + } + } + }, + "responses": { + "200": { + "description": "确认下单成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "message": { + "type": "string" + }, + "data": { + "type": "object", + "properties": { + "order_id": { + "type": "integer" + }, + "order_no": { + "type": "string" + } + } + } + } + } + } + } + }, + "400": { + "description": "请求参数错误" + }, + "401": { + "description": "未授权" + }, + "404": { + "description": "预订单不存在" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/api/orders/pre-order/{id}": { + "get": { + "summary": "获取预订单详情", + "tags": [ + "Orders" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + }, + "description": "预订单ID" + } + ], + "responses": { + "200": { + "description": "获取预订单详情成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "data": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "order_no": { + "type": "string" + }, + "total_amount": { + "type": "integer" + }, + "total_points": { + "type": "integer" + }, + "total_rongdou": { + "type": "integer" + }, + "status": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "product_id": { + "type": "integer" + }, + "product_name": { + "type": "string" + }, + "quantity": { + "type": "integer" + }, + "price": { + "type": "integer" + }, + "points_price": { + "type": "integer" + }, + "rongdou_price": { + "type": "integer" + }, + "spec_info": { + "type": "object" + } + } + } + } + } + } + } + } + } + } + }, + "401": { + "description": "未授权" + }, + "404": { + "description": "预订单不存在" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/api/orders/{id}": { + "get": { + "summary": "获取单个订单详情", + "tags": [ + "Orders" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + }, + "description": "订单ID" + } + ], + "responses": { + "200": { + "description": "成功获取订单详情", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "data": { + "type": "object", + "properties": { + "order": { + "$ref": "#/components/schemas/Order" + } + } + } + } + } + } + } + }, + "401": { + "description": "未授权" + }, + "404": { + "description": "订单不存在" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/products": { + "get": { + "summary": "获取商品列表", + "tags": [ + "Products" + ], + "parameters": [ + { + "in": "query", + "name": "page", + "schema": { + "type": "integer", + "default": 1 + }, + "description": "页码" + }, + { + "in": "query", + "name": "limit", + "schema": { + "type": "integer", + "default": 10 + }, + "description": "每页数量" + }, + { + "in": "query", + "name": "search", + "schema": { + "type": "string" + }, + "description": "搜索关键词" + }, + { + "in": "query", + "name": "category", + "schema": { + "type": "string" + }, + "description": "商品分类" + }, + { + "in": "query", + "name": "status", + "schema": { + "type": "string", + "enum": [ + "active", + "inactive" + ] + }, + "description": "商品状态" + } + ], + "responses": { + "200": { + "description": "成功获取商品列表", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "data": { + "type": "object", + "properties": { + "products": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Product" + } + }, + "pagination": { + "type": "object", + "properties": { + "page": { + "type": "integer" + }, + "limit": { + "type": "integer" + }, + "total": { + "type": "integer" + }, + "pages": { + "type": "integer" + } + } + } + } + } + } + } + } + } + } + } + } + }, + "/products/categories": { + "get": { + "summary": "获取商品分类列表", + "tags": [ + "Products" + ], + "responses": { + "200": { + "description": "成功获取分类列表", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "data": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "/products/hot": { + "get": { + "summary": "获取热门商品", + "tags": [ + "Products" + ], + "parameters": [ + { + "in": "query", + "name": "limit", + "schema": { + "type": "integer", + "default": 10 + }, + "description": "返回数量" + } + ], + "responses": { + "200": { + "description": "成功获取热门商品", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "data": { + "type": "object", + "properties": { + "products": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Product" + } + } + } + } + } + } + } + } + } + } + } + }, + "/products/{id}": { + "get": { + "summary": "获取单个商品详情(包含增强规格信息)", + "tags": [ + "Products" + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + }, + "description": "商品ID" + } + ], + "responses": { + "200": { + "description": "成功获取商品详情,包含完整的规格信息", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "data": { + "$ref": "#/components/schemas/Product", + "type": "object", + "properties": { + "product": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "category": { + "type": "string" + }, + "price": { + "type": "number" + }, + "points_price": { + "type": "number" + }, + "rongdou_price": { + "type": "number" + }, + "stock": { + "type": "integer" + }, + "specifications": { + "type": "array", + "description": "商品规格组合列表(笛卡尔积规格系统)", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "规格组合ID" + }, + "combination_key": { + "type": "string", + "description": "规格组合键(如:1-3-5)" + }, + "spec_display": { + "type": "string", + "description": "规格显示文本(如:颜色:红色 | 尺寸:XL)" + }, + "spec_details": { + "type": "array", + "description": "规格详细信息", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "spec_name": { + "type": "string", + "description": "规格名称" + }, + "spec_display_name": { + "type": "string", + "description": "规格显示名称" + }, + "value": { + "type": "string", + "description": "规格值" + }, + "display_value": { + "type": "string", + "description": "规格显示值" + }, + "color_code": { + "type": "string", + "description": "颜色代码" + }, + "image_url": { + "type": "string", + "description": "规格图片" + } + } + } + }, + "price_adjustment": { + "type": "number", + "description": "价格调整" + }, + "points_adjustment": { + "type": "number", + "description": "积分调整" + }, + "rongdou_adjustment": { + "type": "number", + "description": "融豆调整" + }, + "stock": { + "type": "integer", + "description": "规格库存" + }, + "sku_code": { + "type": "string", + "description": "SKU编码" + }, + "barcode": { + "type": "string", + "description": "条形码" + }, + "weight": { + "type": "number", + "description": "重量" + }, + "volume": { + "type": "number", + "description": "体积" + }, + "actual_price": { + "type": "number", + "description": "实际价格(基础价格+调整)" + }, + "actual_points_price": { + "type": "number", + "description": "实际积分价格" + }, + "actual_rongdou_price": { + "type": "number", + "description": "实际融豆价格" + }, + "is_available": { + "type": "boolean", + "description": "是否有库存" + } + } + } + }, + "specification_count": { + "type": "integer", + "description": "规格总数" + }, + "available_specifications": { + "type": "integer", + "description": "有库存的规格数量" + }, + "attributes": { + "type": "array", + "description": "商品属性" + }, + "isFavorited": { + "type": "boolean", + "description": "是否已收藏" + } + } + } + } + } + } + } + } + } + }, + "404": { + "description": "商品不存在" + } + } + } + }, + "/addresses": { + "get": { + "summary": "获取用户收货地址列表", + "tags": [ + "Addresses" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "成功获取地址列表", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Address" + } + } + } + } + } + } + }, + "401": { + "description": "未授权" + }, + "500": { + "description": "服务器错误" + } + } + }, + "post": { + "summary": "创建收货地址", + "tags": [ + "Addresses" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "recipient_name": { + "type": "string", + "description": "收件人姓名" + }, + "phone": { + "type": "string", + "description": "联系电话" + }, + "province_code": { + "type": "string", + "description": "省份编码" + }, + "province_name": { + "type": "string", + "description": "省份名称" + }, + "city_code": { + "type": "string", + "description": "城市编码" + }, + "city_name": { + "type": "string", + "description": "城市名称" + }, + "district_code": { + "type": "string", + "description": "区县编码" + }, + "district_name": { + "type": "string", + "description": "区县名称" + }, + "detailed_address": { + "type": "string", + "description": "详细地址" + }, + "postal_code": { + "type": "string", + "description": "邮政编码" + }, + "label_id": { + "type": "integer", + "description": "地址标签ID" + }, + "is_default": { + "type": "boolean", + "description": "是否为默认地址" + } + }, + "required": [ + "recipient_name", + "phone", + "province_code", + "province_name", + "city_code", + "city_name", + "district_code", + "district_name", + "detailed_address" + ] + } + } + } + }, + "responses": { + "201": { + "description": "地址创建成功" + }, + "400": { + "description": "请求参数错误" + }, + "401": { + "description": "未授权" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/addresses/{id}": { + "get": { + "summary": "获取单个收货地址详情", + "tags": [ + "Addresses" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + }, + "description": "地址ID" + } + ], + "responses": { + "200": { + "description": "成功获取地址详情", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "data": { + "$ref": "#/components/schemas/Address" + } + } + } + } + } + }, + "401": { + "description": "未授权" + }, + "404": { + "description": "地址不存在" + }, + "500": { + "description": "服务器错误" + } + } + }, + "put": { + "summary": "更新收货地址", + "tags": [ + "Addresses" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + }, + "description": "地址ID" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "recipient_name": { + "type": "string", + "description": "收件人姓名" + }, + "phone": { + "type": "string", + "description": "联系电话" + }, + "province_code": { + "type": "string", + "description": "省份编码" + }, + "province_name": { + "type": "string", + "description": "省份名称" + }, + "city_code": { + "type": "string", + "description": "城市编码" + }, + "city_name": { + "type": "string", + "description": "城市名称" + }, + "district_code": { + "type": "string", + "description": "区县编码" + }, + "district_name": { + "type": "string", + "description": "区县名称" + }, + "detailed_address": { + "type": "string", + "description": "详细地址" + }, + "postal_code": { + "type": "string", + "description": "邮政编码" + }, + "label_id": { + "type": "integer", + "description": "地址标签ID" + }, + "is_default": { + "type": "boolean", + "description": "是否为默认地址" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "地址更新成功" + }, + "400": { + "description": "请求参数错误" + }, + "401": { + "description": "未授权" + }, + "404": { + "description": "地址不存在" + }, + "500": { + "description": "服务器错误" + } + } + }, + "delete": { + "summary": "删除收货地址", + "tags": [ + "Addresses" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + }, + "description": "地址ID" + } + ], + "responses": { + "200": { + "description": "地址删除成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "收货地址删除成功" + } + } + } + } + } + }, + "401": { + "description": "未授权" + }, + "404": { + "description": "地址不存在" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/addresses/{id}/default": { + "put": { + "summary": "设置默认地址", + "tags": [ + "Addresses" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + }, + "description": "地址ID" + } + ], + "responses": { + "200": { + "description": "默认地址设置成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "默认地址设置成功" + } + } + } + } + } + }, + "401": { + "description": "未授权" + }, + "404": { + "description": "地址不存在" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/auth/register": { + "post": { + "summary": "用户注册", + "description": "需要提供有效的激活码才能注册", + "tags": [ + "Authentication" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RegisterRequest" + } + } + } + }, + "responses": { + "201": { + "description": "用户注册成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "message": { + "type": "string" + }, + "token": { + "type": "string", + "description": "JWT认证令牌" + }, + "user": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "username": { + "type": "string" + }, + "role": { + "type": "string" + } + } + } + } + } + } + } + }, + "400": { + "description": "请求参数错误" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/auth/login": { + "post": { + "summary": "用户登录", + "tags": [ + "Authentication" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LoginCredentials" + } + } + } + }, + "responses": { + "200": { + "description": "登录成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "message": { + "type": "string" + }, + "token": { + "type": "string", + "description": "JWT认证令牌" + }, + "user": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "username": { + "type": "string" + }, + "role": { + "type": "string" + }, + "avatar": { + "type": "string" + }, + "points": { + "type": "integer" + } + } + } + } + } + } + } + }, + "400": { + "description": "请求参数错误" + }, + "401": { + "description": "用户名或密码错误" + }, + "403": { + "description": "账户审核未通过" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/captcha/generate": { + "get": { + "summary": "生成图形验证码", + "tags": [ + "Captcha" + ], + "responses": { + "200": { + "description": "成功生成验证码", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "data": { + "type": "object", + "properties": { + "captchaId": { + "type": "string", + "description": "验证码唯一ID" + }, + "image": { + "type": "string", + "description": "Base64编码的SVG验证码图片" + } + } + } + } + } + } + } + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/captcha/verify": { + "post": { + "summary": "验证用户输入的验证码", + "tags": [ + "Captcha" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "captchaId", + "captchaText" + ], + "properties": { + "captchaId": { + "type": "string", + "description": "验证码唯一ID" + }, + "captchaText": { + "type": "string", + "description": "用户输入的验证码" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "验证码验证成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "验证码验证成功" + } + } + } + } + } + }, + "400": { + "description": "验证码错误或已过期", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": false + }, + "message": { + "type": "string", + "example": "验证码错误" + } + } + } + } + } + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/api/cart": { + "get": { + "summary": "获取购物车列表", + "tags": [ + "Cart" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "获取购物车成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "data": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CartItem" + } + }, + "total_count": { + "type": "integer", + "description": "购物车商品总数量" + }, + "total_amount": { + "type": "integer", + "description": "购物车总金额" + }, + "total_points": { + "type": "integer", + "description": "购物车总积分" + }, + "total_rongdou": { + "type": "integer", + "description": "购物车总融豆" + } + } + } + } + } + } + } + }, + "401": { + "description": "未授权" + }, + "500": { + "description": "服务器错误" + } + } + }, + "post": { + "summary": "添加商品到购物车", + "tags": [ + "Cart" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "product_id": { + "type": "integer", + "description": "商品ID" + }, + "quantity": { + "type": "integer", + "description": "商品数量", + "minimum": 1 + }, + "spec_combination_id": { + "type": "integer", + "description": "商品规格组合ID(可选)" + } + }, + "required": [ + "product_id", + "quantity" + ] + } + } + } + }, + "responses": { + "201": { + "description": "添加到购物车成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "message": { + "type": "string" + }, + "data": { + "type": "object", + "properties": { + "cart_item_id": { + "type": "integer" + } + } + } + } + } + } + } + }, + "400": { + "description": "参数错误或库存不足" + }, + "401": { + "description": "未授权" + }, + "404": { + "description": "商品不存在或已下架" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/api/cart/{id}": { + "put": { + "summary": "更新购物车商品数量", + "tags": [ + "Cart" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + }, + "description": "购物车项ID" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "quantity": { + "type": "integer", + "description": "新的商品数量", + "minimum": 1 + } + }, + "required": [ + "quantity" + ] + } + } + } + }, + "responses": { + "200": { + "description": "更新购物车成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "message": { + "type": "string" + } + } + } + } + } + }, + "400": { + "description": "参数错误或库存不足" + }, + "401": { + "description": "未授权" + }, + "404": { + "description": "购物车项不存在" + }, + "500": { + "description": "服务器错误" + } + } + }, + "delete": { + "summary": "删除购物车商品", + "tags": [ + "Cart" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + }, + "description": "购物车项ID" + } + ], + "responses": { + "200": { + "description": "删除购物车商品成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "message": { + "type": "string" + } + } + } + } + } + }, + "401": { + "description": "未授权" + }, + "404": { + "description": "购物车项不存在" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/api/cart/batch": { + "delete": { + "summary": "批量删除购物车商品", + "tags": [ + "Cart" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "cart_item_ids": { + "type": "array", + "items": { + "type": "integer" + }, + "description": "购物车项ID数组" + } + }, + "required": [ + "cart_item_ids" + ] + } + } + } + }, + "responses": { + "200": { + "description": "批量删除购物车商品成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "message": { + "type": "string" + }, + "data": { + "type": "object", + "properties": { + "deleted_count": { + "type": "integer", + "description": "删除的商品数量" + } + } + } + } + } + } + } + }, + "400": { + "description": "参数错误" + }, + "401": { + "description": "未授权" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/api/cart/clear": { + "delete": { + "summary": "清空购物车", + "tags": [ + "Cart" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "清空购物车成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "message": { + "type": "string" + } + } + } + } + } + }, + "401": { + "description": "未授权" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/api/cart/count": { + "get": { + "summary": "获取购物车商品数量", + "tags": [ + "Cart" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "获取购物车商品数量成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "data": { + "type": "object", + "properties": { + "count": { + "type": "integer", + "description": "购物车商品总数量" + } + } + } + } + } + } + } + }, + "401": { + "description": "未授权" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/api/cart/checkout": { + "post": { + "summary": "购物车结账", + "tags": [ + "Cart" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "cart_item_ids": { + "type": "array", + "items": { + "type": "integer" + }, + "description": "要结账的购物车项ID数组" + }, + "shipping_address": { + "type": "string", + "description": "收货地址" + } + }, + "required": [ + "cart_item_ids", + "shipping_address" + ] + } + } + } + }, + "responses": { + "201": { + "description": "结账成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "message": { + "type": "string" + }, + "data": { + "type": "object", + "properties": { + "order_id": { + "type": "integer" + }, + "order_no": { + "type": "string" + }, + "total_amount": { + "type": "integer" + }, + "total_points": { + "type": "integer" + }, + "total_rongdou": { + "type": "integer" + } + } + } + } + } + } + } + }, + "400": { + "description": "参数错误或库存不足" + }, + "401": { + "description": "未授权" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/api/matching/create": { + "post": { + "summary": "创建匹配订单", + "tags": [ + "Matching" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "matchingType": { + "type": "string", + "enum": [ + "small", + "large" + ], + "default": "small", + "description": "匹配类型(小额或大额)" + }, + "customAmount": { + "type": "number", + "description": "大额匹配时的自定义金额(5000-50000之间)" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "匹配订单创建成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "message": { + "type": "string" + }, + "data": { + "type": "object", + "properties": { + "matchingOrderId": { + "type": "integer" + }, + "amounts": { + "type": "array", + "items": { + "type": "number" + } + }, + "matchingType": { + "type": "string" + }, + "totalAmount": { + "type": "number" + } + } + } + } + } + } + } + }, + "400": { + "description": "参数错误或用户未满足匹配条件" + }, + "401": { + "description": "未授权" + }, + "404": { + "description": "用户不存在" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/api/matching/my-orders": { + "get": { + "summary": "获取用户的匹配订单列表", + "tags": [ + "Matching" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "in": "query", + "name": "page", + "schema": { + "type": "integer", + "default": 1 + }, + "description": "页码" + }, + { + "in": "query", + "name": "limit", + "schema": { + "type": "integer", + "default": 10 + }, + "description": "每页数量" + } + ], + "responses": { + "200": { + "description": "成功获取匹配订单列表", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MatchingOrder" + } + } + } + } + } + } + }, + "401": { + "description": "未授权" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/api/matching/pending-allocations": { + "get": { + "summary": "获取用户待处理的分配", + "tags": [ + "Matching" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "成功获取待处理分配", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Allocation" + } + } + } + } + } + } + }, + "401": { + "description": "未授权" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/api/matching/allocation/{id}": { + "get": { + "summary": "获取分配详情", + "tags": [ + "Matching" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + }, + "description": "分配ID" + } + ], + "responses": { + "200": { + "description": "成功获取分配详情", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "data": { + "$ref": "#/components/schemas/Allocation" + } + } + } + } + } + }, + "401": { + "description": "未授权" + }, + "403": { + "description": "无权限访问" + }, + "404": { + "description": "分配不存在" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/api/matching/confirm-allocation/{allocationId}": { + "post": { + "summary": "确认分配(创建转账)", + "tags": [ + "Matching" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "allocationId", + "required": true, + "schema": { + "type": "integer" + }, + "description": "分配ID" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "transferAmount": { + "type": "number", + "description": "转账金额" + }, + "description": { + "type": "string", + "description": "转账描述" + }, + "voucher": { + "type": "string", + "description": "转账凭证(图片URL)" + } + }, + "required": [ + "voucher" + ] + } + } + } + }, + "responses": { + "200": { + "description": "转账凭证提交成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "message": { + "type": "string" + }, + "data": { + "type": "object", + "properties": { + "transferId": { + "type": "integer" + } + } + } + } + } + } + } + }, + "400": { + "description": "参数错误" + }, + "401": { + "description": "未授权" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/api/matching/reject-allocation/{allocationId}": { + "post": { + "summary": "拒绝分配", + "tags": [ + "Matching" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "allocationId", + "required": true, + "schema": { + "type": "integer" + }, + "description": "分配ID" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "reason": { + "type": "string", + "description": "拒绝原因" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "拒绝分配成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "message": { + "type": "string" + } + } + } + } + } + }, + "401": { + "description": "未授权" + }, + "404": { + "description": "分配不存在或无权限" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/api/matching/order/{orderId}": { + "get": { + "summary": "获取匹配订单详情", + "tags": [ + "Matching" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "orderId", + "required": true, + "schema": { + "type": "integer" + }, + "description": "订单ID" + } + ], + "responses": { + "200": { + "description": "成功获取订单详情", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "data": { + "type": "object", + "properties": { + "order": { + "$ref": "#/components/schemas/MatchingOrder" + }, + "allocations": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Allocation" + } + }, + "records": { + "type": "array", + "items": { + "type": "object" + } + } + } + } + } + } + } + } + }, + "401": { + "description": "未授权" + }, + "403": { + "description": "无权限查看" + }, + "404": { + "description": "订单不存在" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/api/matching/stats": { + "get": { + "summary": "获取匹配统计信息", + "tags": [ + "Matching" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "成功获取统计信息", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "data": { + "type": "object", + "properties": { + "userStats": { + "type": "object", + "properties": { + "initiated_orders": { + "type": "integer" + }, + "participated_allocations": { + "type": "integer" + }, + "total_initiated_amount": { + "type": "number" + }, + "total_participated_amount": { + "type": "number" + } + } + } + } + } + } + } + } + } + }, + "401": { + "description": "未授权" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/api/matching-admin/unreasonable-matches": { + "get": { + "summary": "获取不合理的匹配记录(正余额用户被匹配的情况)", + "tags": [ + "MatchingAdmin" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "in": "query", + "name": "page", + "schema": { + "type": "integer", + "default": 1 + }, + "description": "页码" + }, + { + "in": "query", + "name": "limit", + "schema": { + "type": "integer", + "default": 20 + }, + "description": "每页数量" + } + ], + "responses": { + "200": { + "description": "成功获取不合理匹配记录", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "data": { + "type": "object", + "properties": { + "matches": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UnreasonableMatch" + } + }, + "pagination": { + "type": "object", + "properties": { + "page": { + "type": "integer" + }, + "limit": { + "type": "integer" + }, + "total": { + "type": "integer" + }, + "totalPages": { + "type": "integer" + } + } + } + } + } + } + } + } + } + }, + "401": { + "description": "未授权" + }, + "403": { + "description": "无管理员权限" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/api/matching-admin/fix-unreasonable-match/{allocationId}": { + "post": { + "summary": "修复不合理的匹配记录", + "tags": [ + "MatchingAdmin" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "allocationId", + "required": true, + "schema": { + "type": "integer" + }, + "description": "分配ID" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "action": { + "type": "string", + "enum": [ + "cancel", + "reassign" + ], + "description": "修复操作类型(取消或重新分配)" + } + }, + "required": [ + "action" + ] + } + } + } + }, + "responses": { + "200": { + "description": "修复成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "message": { + "type": "string" + } + } + } + } + } + }, + "400": { + "description": "参数错误或无需修复" + }, + "401": { + "description": "未授权" + }, + "403": { + "description": "无管理员权限" + }, + "404": { + "description": "分配记录不存在" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/api/matching-admin/matching-stats": { + "get": { + "summary": "获取匹配统计信息", + "tags": [ + "MatchingAdmin" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "成功获取匹配统计信息", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "data": { + "type": "object", + "properties": { + "currentStats": { + "type": "object", + "properties": { + "unreasonable_matches": { + "type": "integer" + }, + "reasonable_matches": { + "type": "integer" + }, + "system_matches": { + "type": "integer" + }, + "unreasonable_amount": { + "type": "number" + }, + "reasonable_amount": { + "type": "number" + } + } + }, + "yesterdayStats": { + "type": "object", + "properties": { + "total_outbound": { + "type": "number" + }, + "unique_amounts": { + "type": "integer" + } + } + } + } + } + } + } + } + } + }, + "401": { + "description": "未授权" + }, + "403": { + "description": "无管理员权限" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/api/matching-admin/fix-all-unreasonable": { + "post": { + "summary": "批量修复所有不合理匹配", + "tags": [ + "MatchingAdmin" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "批量修复完成", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "message": { + "type": "string" + }, + "data": { + "type": "object", + "properties": { + "fixedCount": { + "type": "integer", + "description": "成功修复的记录数" + }, + "errorCount": { + "type": "integer", + "description": "修复失败的记录数" + }, + "errors": { + "type": "array", + "items": { + "type": "string" + }, + "description": "错误信息列表(最多10条)" + } + } + } + } + } + } + } + }, + "401": { + "description": "未授权" + }, + "403": { + "description": "无管理员权限" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/api/matching-admin/confirm-allocation/{allocationId}": { + "post": { + "summary": "管理员确认分配", + "tags": [ + "MatchingAdmin" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "allocationId", + "required": true, + "schema": { + "type": "integer" + }, + "description": "分配ID" + } + ], + "responses": { + "200": { + "description": "分配确认成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "message": { + "type": "string" + } + } + } + } + } + }, + "401": { + "description": "未授权" + }, + "403": { + "description": "无管理员权限" + }, + "404": { + "description": "分配不存在或状态不是待处理" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/api/matching-admin/cancel-allocation/{allocationId}": { + "post": { + "summary": "管理员取消分配", + "tags": [ + "MatchingAdmin" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "allocationId", + "required": true, + "schema": { + "type": "integer" + }, + "description": "分配ID" + } + ], + "responses": { + "200": { + "description": "分配取消成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "message": { + "type": "string" + } + } + } + } + } + }, + "401": { + "description": "未授权" + }, + "403": { + "description": "无管理员权限" + }, + "404": { + "description": "分配不存在或状态不是待处理" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/api/orders/{id}/cancel": { + "put": { + "summary": "用户取消订单", + "tags": [ + "Orders" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "id", + "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": "只能取消待处理的订单" + }, + "401": { + "description": "未授权" + }, + "404": { + "description": "订单不存在" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/api/orders/{id}/confirm": { + "put": { + "summary": "确认收货", + "tags": [ + "Orders" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "id", + "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": "只能确认已发货的订单" + }, + "401": { + "description": "未授权" + }, + "404": { + "description": "订单不存在" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/api/orders/{id}/status": { + "put": { + "summary": "更新订单状态(管理员)", + "tags": [ + "Orders" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + }, + "description": "订单ID" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "pending", + "shipped", + "completed", + "cancelled" + ], + "description": "订单状态" + } + }, + "required": [ + "status" + ] + } + } + } + }, + "responses": { + "200": { + "description": "订单状态更新成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "message": { + "type": "string" + } + } + } + } + } + }, + "400": { + "description": "无效的订单状态" + }, + "401": { + "description": "未授权" + }, + "403": { + "description": "无管理员权限" + }, + "404": { + "description": "订单不存在" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/api/orders/pending-payment/{id}": { + "get": { + "summary": "获取待支付预订单详情", + "tags": [ + "Orders" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + }, + "description": "预订单ID" + } + ], + "responses": { + "200": { + "description": "获取预订单详情成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "data": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "order_no": { + "type": "string" + }, + "total_amount": { + "type": "integer" + }, + "total_points": { + "type": "integer" + }, + "total_rongdou": { + "type": "integer" + }, + "status": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "product_id": { + "type": "integer" + }, + "product_name": { + "type": "string" + }, + "quantity": { + "type": "integer" + }, + "price": { + "type": "integer" + }, + "points_price": { + "type": "integer" + }, + "rongdou_price": { + "type": "integer" + }, + "spec_info": { + "type": "object" + } + } + } + } + } + } + } + } + } + } + }, + "401": { + "description": "未授权" + }, + "404": { + "description": "预订单不存在" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/api/orders/confirm-payment": { + "post": { + "summary": "确认支付订单", + "description": "根据商品支付方式确认订单支付:\n- 仅积分支付:按10000积分=1融豆的比例扣除积分\n- 仅融豆支付:直接扣除融豆\n- 组合支付:优先扣除积分(按10000:1转换),不足部分扣除融豆\n", + "tags": [ + "Orders" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "order_id", + "address_id" + ], + "properties": { + "order_id": { + "type": "integer", + "description": "订单ID", + "example": 123 + }, + "address_id": { + "type": "integer", + "description": "收货地址ID", + "example": 456 + } + } + } + } + } + }, + "responses": { + "200": { + "description": "确认支付成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "订单支付成功" + }, + "data": { + "type": "object", + "properties": { + "order_id": { + "type": "integer", + "example": 123 + }, + "order_no": { + "type": "string", + "example": "ORD20240101123456" + } + } + } + } + } + } + } + }, + "400": { + "description": "请求参数错误或余额不足", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": false + }, + "message": { + "type": "string", + "enum": [ + "订单ID和收货地址ID为必填项", + "积分不足", + "融豆不足", + "积分和融豆余额不足", + "商品支付方式配置错误" + ] + } + } + } + } + } + }, + "401": { + "description": "未授权" + }, + "404": { + "description": "订单或地址不存在", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": false + }, + "message": { + "type": "string", + "enum": [ + "订单不存在或已处理", + "收货地址不存在", + "用户不存在" + ] + } + } + } + } + } + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/api/orders/stats": { + "get": { + "summary": "获取订单统计信息(管理员权限)", + "tags": [ + "Orders" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "成功获取订单统计信息", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "data": { + "type": "object", + "properties": { + "totalOrders": { + "type": "integer", + "description": "总订单数" + }, + "pendingOrders": { + "type": "integer", + "description": "待发货订单数" + }, + "completedOrders": { + "type": "integer", + "description": "已完成订单数" + }, + "monthOrders": { + "type": "integer", + "description": "本月新增订单数" + }, + "monthGrowthRate": { + "type": "number", + "description": "月增长率" + }, + "totalPointsConsumed": { + "type": "number", + "description": "总积分消费" + } + } + } + } + } + } + } + }, + "401": { + "description": "未授权" + }, + "403": { + "description": "无管理员权限" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/api/points/balance": { + "get": { + "summary": "获取用户当前积分余额", + "tags": [ + "Points" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "成功获取积分余额", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "data": { + "type": "object", + "properties": { + "points": { + "type": "integer", + "description": "用户当前积分" + } + } + } + } + } + } + } + }, + "401": { + "description": "未授权,需要登录" + }, + "404": { + "description": "用户不存在" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/api/points/history": { + "get": { + "summary": "获取用户积分历史记录", + "tags": [ + "Points" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "in": "query", + "name": "page", + "schema": { + "type": "integer", + "default": 1 + }, + "description": "页码" + }, + { + "in": "query", + "name": "limit", + "schema": { + "type": "integer", + "default": 10 + }, + "description": "每页记录数" + }, + { + "in": "query", + "name": "type", + "schema": { + "type": "string", + "enum": [ + "earn", + "spend", + "admin_adjust" + ] + }, + "description": "积分变动类型" + }, + { + "in": "query", + "name": "username", + "schema": { + "type": "string" + }, + "description": "用户名(仅管理员可用)" + }, + { + "in": "query", + "name": "change", + "schema": { + "type": "string", + "enum": [ + "positive", + "negative" + ] + }, + "description": "积分变动方向(仅管理员可用)" + }, + { + "in": "query", + "name": "startDate", + "schema": { + "type": "string", + "format": "date" + }, + "description": "开始日期(仅管理员可用)" + }, + { + "in": "query", + "name": "endDate", + "schema": { + "type": "string", + "format": "date" + }, + "description": "结束日期(仅管理员可用)" + } + ], + "responses": { + "200": { + "description": "成功获取积分历史", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "data": { + "type": "object", + "properties": { + "records": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PointsHistory" + } + }, + "pagination": { + "type": "object", + "properties": { + "page": { + "type": "integer" + }, + "limit": { + "type": "integer" + }, + "total": { + "type": "integer" + }, + "totalPages": { + "type": "integer" + } + } + } + } + } + } + } + } + } + }, + "401": { + "description": "未授权,需要登录" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/api/points/adjust": { + "post": { + "summary": "管理员调整用户积分", + "tags": [ + "Points" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "userId", + "points", + "reason" + ], + "properties": { + "userId": { + "type": "integer", + "description": "用户ID" + }, + "points": { + "type": "integer", + "description": "调整的积分数量(正数为增加,负数为减少)" + }, + "reason": { + "type": "string", + "description": "调整原因" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "积分调整成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "积分调整成功" + }, + "data": { + "type": "object", + "properties": { + "userId": { + "type": "integer" + }, + "pointsChanged": { + "type": "integer" + }, + "newBalance": { + "type": "integer" + } + } + } + } + } + } + } + }, + "400": { + "description": "参数错误或积分不足" + }, + "401": { + "description": "未授权,需要管理员权限" + }, + "404": { + "description": "用户不存在" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/api/points/recharge": { + "post": { + "summary": "管理员给用户充值积分", + "tags": [ + "Points" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "user_id", + "points" + ], + "properties": { + "user_id": { + "type": "integer", + "description": "用户ID" + }, + "points": { + "type": "integer", + "description": "充值的积分数量(必须为正数)" + }, + "description": { + "type": "string", + "description": "充值描述", + "default": "管理员充值" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "积分充值成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "积分充值成功" + }, + "data": { + "type": "object", + "properties": { + "userId": { + "type": "integer" + }, + "pointsAdded": { + "type": "integer" + } + } + } + } + } + } + } + }, + "400": { + "description": "参数错误" + }, + "401": { + "description": "未授权,需要管理员权限" + }, + "404": { + "description": "用户不存在" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/api/points/leaderboard": { + "get": { + "summary": "获取积分排行榜", + "tags": [ + "Points" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "in": "query", + "name": "limit", + "schema": { + "type": "integer", + "default": 10 + }, + "description": "返回的排行榜数量" + } + ], + "responses": { + "200": { + "description": "成功获取积分排行榜", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "data": { + "type": "object", + "properties": { + "leaderboard": { + "type": "array", + "items": { + "type": "object", + "properties": { + "rank": { + "type": "integer" + }, + "userId": { + "type": "integer" + }, + "username": { + "type": "string" + }, + "points": { + "type": "integer" + } + } + } + } + } + } + } + } + } + } + }, + "401": { + "description": "未授权,需要登录" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/api/points/stats": { + "get": { + "summary": "获取积分统计信息(管理员权限)", + "tags": [ + "Points" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "成功获取积分统计信息", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "data": { + "type": "object", + "properties": { + "stats": { + "type": "object", + "properties": { + "totalPoints": { + "type": "integer", + "description": "系统中总积分数量" + }, + "totalEarned": { + "type": "integer", + "description": "总积分发放量" + }, + "totalSpent": { + "type": "integer", + "description": "总积分消费量" + }, + "activeUsers": { + "type": "integer", + "description": "活跃用户数" + } + } + } + } + } + } + } + } + } + }, + "401": { + "description": "未授权,需要管理员权限" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/products/flash-sale": { + "get": { + "summary": "获取秒杀商品", + "tags": [ + "Products" + ], + "responses": { + "200": { + "description": "成功获取秒杀商品", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "data": { + "type": "object", + "properties": { + "products": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Product" + } + } + } + } + } + } + } + } + } + } + } + }, + "/regions/zhejiang": { + "get": { + "summary": "获取浙江省所有地区数据", + "tags": [ + "Regions" + ], + "responses": { + "200": { + "description": "成功获取浙江省地区数据", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ZhejiangRegion" + } + }, + "message": { + "type": "string", + "example": "获取地区数据成功" + } + } + } + } + } + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/regions/provinces": { + "get": { + "summary": "获取所有省份", + "tags": [ + "Regions" + ], + "responses": { + "200": { + "description": "成功获取省份列表", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Region" + } + } + } + } + } + } + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/regions/cities/{provinceCode}": { + "get": { + "summary": "根据省份代码获取城市列表", + "tags": [ + "Regions" + ], + "parameters": [ + { + "in": "path", + "name": "provinceCode", + "required": true, + "schema": { + "type": "string" + }, + "description": "省份代码" + } + ], + "responses": { + "200": { + "description": "成功获取城市列表", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Region" + } + } + } + } + } + } + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/regions/districts/{cityCode}": { + "get": { + "summary": "根据城市代码获取区县列表", + "tags": [ + "Regions" + ], + "parameters": [ + { + "in": "path", + "name": "cityCode", + "required": true, + "schema": { + "type": "string" + }, + "description": "城市代码" + } + ], + "responses": { + "200": { + "description": "成功获取区县列表", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Region" + } + } + } + } + } + } + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/regions/path/{regionCode}": { + "get": { + "summary": "根据区域代码获取完整路径(省-市-区)", + "tags": [ + "Regions" + ], + "parameters": [ + { + "in": "path", + "name": "regionCode", + "required": true, + "schema": { + "type": "string" + }, + "description": "区域代码" + } + ], + "responses": { + "200": { + "description": "成功获取区域完整路径", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "data": { + "type": "object", + "properties": { + "province": { + "$ref": "#/components/schemas/Region" + }, + "city": { + "$ref": "#/components/schemas/Region" + }, + "district": { + "$ref": "#/components/schemas/Region" + } + } + } + } + } + } + } + }, + "404": { + "description": "区域不存在" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/risk-management/users": { + "get": { + "summary": "获取风险用户列表", + "tags": [ + "RiskManagement" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "in": "query", + "name": "page", + "schema": { + "type": "integer", + "default": 1 + }, + "description": "页码" + }, + { + "in": "query", + "name": "limit", + "schema": { + "type": "integer", + "default": 10 + }, + "description": "每页数量" + }, + { + "in": "query", + "name": "is_blacklisted", + "schema": { + "type": "integer", + "enum": [ + 0, + 1 + ] + }, + "description": "是否被拉黑" + }, + { + "in": "query", + "name": "username", + "schema": { + "type": "string" + }, + "description": "用户名" + } + ], + "responses": { + "200": { + "description": "成功获取风险用户列表", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "data": { + "type": "object", + "properties": { + "users": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "username": { + "type": "string" + }, + "real_name": { + "type": "string" + }, + "is_blacklisted": { + "type": "boolean" + }, + "blacklist_reason": { + "type": "string" + }, + "blacklisted_at": { + "type": "string", + "format": "date-time" + } + } + } + }, + "pagination": { + "type": "object", + "properties": { + "total": { + "type": "integer" + }, + "page": { + "type": "integer" + }, + "limit": { + "type": "integer" + }, + "pages": { + "type": "integer" + } + } + } + } + } + } + } + } + } + }, + "401": { + "description": "未授权" + }, + "403": { + "description": "权限不足" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/risk-management/blacklist/{userId}": { + "post": { + "summary": "拉黑用户", + "tags": [ + "RiskManagement" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "userId", + "schema": { + "type": "integer" + }, + "required": true, + "description": "用户ID" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "reason" + ], + "properties": { + "reason": { + "type": "string", + "description": "拉黑原因" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "用户已被拉黑", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "用户已被拉黑" + } + } + } + } + } + }, + "400": { + "description": "请求参数错误" + }, + "401": { + "description": "未授权" + }, + "403": { + "description": "权限不足" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/risk-management/unblacklist/{userId}": { + "post": { + "summary": "解除拉黑", + "tags": [ + "RiskManagement" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "userId", + "schema": { + "type": "integer" + }, + "required": true, + "description": "用户ID" + } + ], + "responses": { + "200": { + "description": "已解除拉黑", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "已解除拉黑" + } + } + } + } + } + }, + "401": { + "description": "未授权" + }, + "403": { + "description": "权限不足" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/risk-management/overdue-transfers": { + "get": { + "summary": "获取超时转账列表", + "tags": [ + "RiskManagement" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "in": "query", + "name": "page", + "schema": { + "type": "integer", + "default": 1 + }, + "description": "页码" + }, + { + "in": "query", + "name": "limit", + "schema": { + "type": "integer", + "default": 10 + }, + "description": "每页数量" + } + ], + "responses": { + "200": { + "description": "成功获取超时转账列表", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "data": { + "type": "object", + "properties": { + "transfers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "user_id": { + "type": "integer" + }, + "recipient_id": { + "type": "integer" + }, + "amount": { + "type": "number" + }, + "status": { + "type": "string" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "username": { + "type": "string" + }, + "recipient_name": { + "type": "string" + }, + "overdue_hours": { + "type": "number" + } + } + } + }, + "pagination": { + "type": "object", + "properties": { + "total": { + "type": "integer" + }, + "page": { + "type": "integer" + }, + "limit": { + "type": "integer" + }, + "pages": { + "type": "integer" + } + } + } + } + } + } + } + } + } + }, + "401": { + "description": "未授权" + }, + "403": { + "description": "权限不足" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/api/sms/send": { + "post": { + "summary": "发送短信验证码", + "tags": [ + "SMS" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "phone" + ], + "properties": { + "phone": { + "type": "string", + "description": "手机号码" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "验证码发送成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "验证码发送成功" + } + } + } + } + } + }, + "400": { + "description": "参数错误或发送频率限制" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/api/sms/verify": { + "post": { + "summary": "验证短信验证码", + "tags": [ + "SMS" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "phone", + "code" + ], + "properties": { + "phone": { + "type": "string", + "description": "手机号码" + }, + "code": { + "type": "string", + "description": "验证码" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "验证码验证成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "手机号验证成功" + }, + "data": { + "type": "object", + "properties": { + "phone": { + "type": "string" + }, + "verified": { + "type": "boolean" + } + } + } + } + } + } + } + }, + "400": { + "description": "参数错误或验证码错误" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/specifications/names": { + "get": { + "summary": "获取所有规格名称", + "tags": [ + "Specifications" + ], + "parameters": [ + { + "in": "query", + "name": "status", + "schema": { + "type": "string", + "enum": [ + "active", + "inactive" + ] + }, + "description": "状态筛选" + } + ], + "responses": { + "200": { + "description": "成功获取规格名称列表", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SpecName" + } + } + } + } + } + } + } + } + }, + "post": { + "summary": "创建规格名称", + "tags": [ + "Specifications" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "name", + "display_name" + ], + "properties": { + "name": { + "type": "string", + "description": "规格名称" + }, + "display_name": { + "type": "string", + "description": "显示名称" + }, + "sort_order": { + "type": "integer", + "default": 0 + } + } + } + } + } + }, + "responses": { + "201": { + "description": "规格名称创建成功" + } + } + } + }, + "/specifications/names/{id}": { + "delete": { + "summary": "删除规格名称", + "tags": [ + "Specifications" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + }, + "description": "规格名称ID" + } + ], + "responses": { + "200": { + "description": "删除成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "message": { + "type": "string", + "example": "规格名称删除成功" + } + } + } + } + } + }, + "400": { + "description": "该规格名称下还有规格值,无法删除" + }, + "404": { + "description": "规格名称不存在" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/specifications/values": { + "get": { + "summary": "获取规格值列表", + "tags": [ + "Specifications" + ], + "parameters": [ + { + "in": "query", + "name": "spec_name_id", + "schema": { + "type": "integer" + }, + "description": "规格名称ID" + }, + { + "in": "query", + "name": "status", + "schema": { + "type": "string", + "enum": [ + "active", + "inactive" + ] + }, + "description": "状态筛选" + } + ], + "responses": { + "200": { + "description": "成功获取规格值列表" + } + } + }, + "post": { + "summary": "创建规格值", + "tags": [ + "Specifications" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "spec_name_id", + "value", + "display_value" + ], + "properties": { + "spec_name_id": { + "type": "integer" + }, + "value": { + "type": "string" + }, + "display_value": { + "type": "string" + }, + "color_code": { + "type": "string" + }, + "image_url": { + "type": "string" + }, + "sort_order": { + "type": "integer", + "default": 0 + } + } + } + } + } + }, + "responses": { + "201": { + "description": "规格值创建成功" + } + } + } + }, + "/specifications/combinations/{productId}": { + "get": { + "summary": "获取商品的规格组合", + "tags": [ + "Specifications" + ], + "parameters": [ + { + "in": "path", + "name": "productId", + "required": true, + "schema": { + "type": "integer" + }, + "description": "商品ID" + }, + { + "in": "query", + "name": "status", + "schema": { + "type": "string", + "enum": [ + "active", + "inactive" + ] + }, + "description": "状态筛选" + } + ], + "responses": { + "200": { + "description": "成功获取规格组合" + } + } + } + }, + "/specifications/combinations/{id}": { + "get": { + "summary": "获取单个规格组合详情", + "tags": [ + "Specifications" + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + }, + "description": "规格组合ID" + } + ], + "responses": { + "200": { + "description": "成功获取规格组合详情", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "data": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "product_id": { + "type": "integer" + }, + "combination_key": { + "type": "string" + }, + "spec_values": { + "type": "array", + "items": { + "type": "integer" + } + }, + "price_adjustment": { + "type": "integer" + }, + "points_adjustment": { + "type": "integer" + }, + "rongdou_adjustment": { + "type": "integer" + }, + "stock": { + "type": "integer" + }, + "sku_code": { + "type": "string" + }, + "barcode": { + "type": "string" + }, + "weight": { + "type": "number" + }, + "volume": { + "type": "number" + }, + "status": { + "type": "string" + }, + "spec_details": { + "type": "array", + "items": { + "type": "object" + } + }, + "actual_price": { + "type": "number" + }, + "actual_points_price": { + "type": "number" + }, + "actual_rongdou_price": { + "type": "number" + }, + "is_available": { + "type": "boolean" + } + } + } + } + } + } + } + }, + "404": { + "description": "规格组合不存在" + }, + "500": { + "description": "服务器错误" + } + } + }, + "delete": { + "summary": "删除规格组合", + "tags": [ + "Specifications" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + }, + "description": "规格组合ID" + } + ], + "responses": { + "200": { + "description": "删除成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "message": { + "type": "string", + "example": "规格组合删除成功" + } + } + } + } + } + }, + "404": { + "description": "规格组合不存在" + }, + "500": { + "description": "服务器错误" + } + } + }, + "put": { + "summary": "更新规格组合", + "tags": [ + "Specifications" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + }, + "description": "规格组合ID" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "price_adjustment": { + "type": "integer" + }, + "points_adjustment": { + "type": "integer" + }, + "rongdou_adjustment": { + "type": "integer" + }, + "stock": { + "type": "integer" + }, + "sku_code": { + "type": "string" + }, + "barcode": { + "type": "string" + }, + "weight": { + "type": "number" + }, + "volume": { + "type": "number" + }, + "status": { + "type": "string", + "enum": [ + "active", + "inactive" + ] + } + } + } + } + } + }, + "responses": { + "200": { + "description": "规格组合更新成功" + }, + "404": { + "description": "规格组合不存在" + } + } + } + }, + "/specifications/combinations": { + "post": { + "summary": "创建商品规格组合", + "tags": [ + "Specifications" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "product_id", + "spec_values" + ], + "properties": { + "product_id": { + "type": "integer" + }, + "spec_values": { + "type": "array", + "items": { + "type": "integer" + }, + "description": "规格值ID数组" + }, + "price_adjustment": { + "type": "integer", + "default": 0 + }, + "points_adjustment": { + "type": "integer", + "default": 0 + }, + "rongdou_adjustment": { + "type": "integer", + "default": 0 + }, + "stock": { + "type": "integer", + "default": 0 + }, + "sku_code": { + "type": "string" + }, + "barcode": { + "type": "string" + }, + "weight": { + "type": "number" + }, + "volume": { + "type": "number" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "规格组合创建成功" + } + } + } + }, + "/specifications/generate-combinations": { + "post": { + "summary": "为商品生成笛卡尔积规格组合", + "tags": [ + "Specifications" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "product_id", + "spec_name_ids" + ], + "properties": { + "product_id": { + "type": "integer" + }, + "spec_name_ids": { + "type": "array", + "items": { + "type": "integer" + }, + "description": "规格名称ID数组" + }, + "default_stock": { + "type": "integer", + "default": 0, + "description": "默认库存" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "规格组合生成成功" + } + } + } + }, + "/transfers": { + "get": { + "summary": "获取转账列表", + "tags": [ + "Transfers" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "in": "query", + "name": "status", + "schema": { + "type": "string" + }, + "description": "转账状态过滤" + }, + { + "in": "query", + "name": "transfer_type", + "schema": { + "type": "string" + }, + "description": "转账类型过滤" + }, + { + "in": "query", + "name": "start_date", + "schema": { + "type": "string", + "format": "date" + }, + "description": "开始日期过滤" + }, + { + "in": "query", + "name": "end_date", + "schema": { + "type": "string", + "format": "date" + }, + "description": "结束日期过滤" + }, + { + "in": "query", + "name": "search", + "schema": { + "type": "string" + }, + "description": "搜索关键词(用户名或真实姓名)" + }, + { + "in": "query", + "name": "page", + "schema": { + "type": "integer", + "default": 1 + }, + "description": "页码" + }, + { + "in": "query", + "name": "limit", + "schema": { + "type": "integer", + "default": 10 + }, + "description": "每页数量" + }, + { + "in": "query", + "name": "sort", + "schema": { + "type": "string" + }, + "description": "排序字段" + }, + { + "in": "query", + "name": "order", + "schema": { + "type": "string", + "enum": [ + "asc", + "desc" + ] + }, + "description": "排序方向" + } + ], + "responses": { + "200": { + "description": "成功获取转账列表", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "data": { + "type": "object", + "properties": { + "transfers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Transfer" + } + }, + "pagination": { + "$ref": "#/components/schemas/Pagination" + } + } + } + } + } + } + } + }, + "401": { + "description": "未授权" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/transfers/list": { + "get": { + "summary": "获取转账记录列表", + "tags": [ + "Transfers" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "in": "query", + "name": "status", + "schema": { + "type": "string" + }, + "description": "转账状态过滤" + }, + { + "in": "query", + "name": "transfer_type", + "schema": { + "type": "string" + }, + "description": "转账类型过滤" + }, + { + "in": "query", + "name": "start_date", + "schema": { + "type": "string", + "format": "date" + }, + "description": "开始日期过滤" + }, + { + "in": "query", + "name": "end_date", + "schema": { + "type": "string", + "format": "date" + }, + "description": "结束日期过滤" + }, + { + "in": "query", + "name": "page", + "schema": { + "type": "integer", + "default": 1 + }, + "description": "页码" + }, + { + "in": "query", + "name": "limit", + "schema": { + "type": "integer", + "default": 10 + }, + "description": "每页数量" + }, + { + "in": "query", + "name": "sort", + "schema": { + "type": "string" + }, + "description": "排序字段" + }, + { + "in": "query", + "name": "order", + "schema": { + "type": "string", + "enum": [ + "asc", + "desc" + ] + }, + "description": "排序方向" + } + ], + "responses": { + "200": { + "description": "成功获取转账记录列表", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "data": { + "type": "object", + "properties": { + "transfers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Transfer" + } + }, + "pagination": { + "$ref": "#/components/schemas/Pagination" + } + } + } + } + } + } + } + }, + "401": { + "description": "未授权" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/transfers/public-account": { + "get": { + "summary": "获取公户信息", + "tags": [ + "Transfers" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "成功获取公户信息", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "data": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "公户ID" + }, + "username": { + "type": "string", + "description": "公户用户名", + "example": "public_account" + }, + "real_name": { + "type": "string", + "description": "公户名称" + }, + "balance": { + "type": "number", + "format": "float", + "description": "公户余额" + } + } + } + } + } + } + } + }, + "401": { + "description": "未授权" + }, + "404": { + "description": "公户不存在" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/transfers/create": { + "post": { + "summary": "创建转账记录", + "tags": [ + "Transfers" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "to_user_id", + "amount", + "transfer_type" + ], + "properties": { + "to_user_id": { + "type": "integer", + "description": "接收方用户ID" + }, + "amount": { + "type": "number", + "format": "float", + "description": "转账金额" + }, + "transfer_type": { + "type": "string", + "enum": [ + "user_to_user", + "user_to_system", + "system_to_user" + ], + "description": "转账类型" + }, + "remark": { + "type": "string", + "description": "转账备注" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "转账记录创建成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "转账记录创建成功,等待确认" + }, + "data": { + "type": "object", + "properties": { + "transfer_id": { + "type": "integer", + "description": "转账记录ID" + } + } + } + } + } + } + } + }, + "400": { + "description": "请求参数错误" + }, + "401": { + "description": "未授权" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/transfers/admin/create": { + "post": { + "summary": "管理员创建转账记录", + "tags": [ + "Transfers" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "from_user_id", + "to_user_id", + "amount", + "transfer_type" + ], + "properties": { + "from_user_id": { + "type": "integer", + "description": "发送方用户ID" + }, + "to_user_id": { + "type": "integer", + "description": "接收方用户ID" + }, + "amount": { + "type": "number", + "format": "float", + "description": "转账金额" + }, + "transfer_type": { + "type": "string", + "enum": [ + "user_to_user", + "user_to_system", + "system_to_user" + ], + "description": "转账类型" + }, + "description": { + "type": "string", + "description": "转账描述" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "转账记录创建成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "转账记录创建成功" + }, + "data": { + "type": "object", + "properties": { + "transfer_id": { + "type": "integer", + "description": "转账记录ID" + } + } + } + } + } + } + } + }, + "400": { + "description": "请求参数错误" + }, + "401": { + "description": "未授权" + }, + "403": { + "description": "权限不足" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/upload/image": { + "post": { + "summary": "上传图片", + "tags": [ + "Upload" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "file": { + "type": "string", + "format": "binary", + "description": "要上传的图片文件" + }, + "type": { + "type": "string", + "enum": [ + "avatar", + "product", + "document" + ], + "default": "document", + "description": "上传文件类型" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "图片上传成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "url": { + "type": "string", + "description": "上传后的文件URL" + }, + "filename": { + "type": "string", + "description": "上传后的文件名" + } + } + } + } + } + }, + "400": { + "description": "请求参数错误" + }, + "401": { + "description": "未授权" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/upload": { + "post": { + "summary": "多文件上传接口 (支持MediaUpload组件)", + "tags": [ + "Upload" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "files": { + "type": "array", + "items": { + "type": "string", + "format": "binary" + }, + "description": "要上传的文件列表" + }, + "type": { + "type": "string", + "enum": [ + "avatar", + "product", + "document" + ], + "default": "document", + "description": "上传文件类型" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "文件上传成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "文件上传成功" + }, + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "filename": { + "type": "string" + }, + "originalname": { + "type": "string" + }, + "mimetype": { + "type": "string" + }, + "size": { + "type": "integer" + }, + "path": { + "type": "string" + }, + "url": { + "type": "string" + } + } + } + } + } + } + } + } + }, + "400": { + "description": "请求参数错误" + }, + "401": { + "description": "未授权" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/upload/single": { + "post": { + "summary": "单文件上传接口(兼容性接口)", + "tags": [ + "Upload" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "file": { + "type": "string", + "format": "binary", + "description": "要上传的文件" + }, + "type": { + "type": "string", + "enum": [ + "avatar", + "product", + "document" + ], + "default": "document", + "description": "上传文件类型" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "文件上传成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "文件上传成功" + }, + "url": { + "type": "string", + "description": "上传后的文件URL" + }, + "filename": { + "type": "string", + "description": "上传后的文件名" + }, + "originalname": { + "type": "string", + "description": "原始文件名" + }, + "size": { + "type": "integer", + "description": "文件大小" + } + } + } + } + } + }, + "400": { + "description": "请求参数错误" + }, + "401": { + "description": "未授权" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/users": { + "post": { + "summary": "创建用户(管理员权限)", + "tags": [ + "Users" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "username", + "password", + "real_name", + "id_card" + ], + "properties": { + "username": { + "type": "string" + }, + "password": { + "type": "string" + }, + "role": { + "type": "string", + "enum": [ + "user", + "admin", + "merchant" + ], + "default": "user" + }, + "is_system_account": { + "type": "boolean", + "default": false + }, + "real_name": { + "type": "string" + }, + "id_card": { + "type": "string" + }, + "wechat_qr": { + "type": "string" + }, + "alipay_qr": { + "type": "string" + }, + "bank_card": { + "type": "string" + }, + "unionpay_qr": { + "type": "string" + }, + "phone": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "用户创建成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "message": { + "type": "string" + }, + "user": { + "$ref": "#/components/schemas/User" + } + } + } + } + } + }, + "400": { + "description": "请求参数错误" + }, + "401": { + "description": "未授权" + }, + "403": { + "description": "权限不足" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/users/pending-audit": { + "get": { + "summary": "获取待审核用户列表(管理员权限)", + "tags": [ + "Users" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "in": "query", + "name": "page", + "schema": { + "type": "integer", + "default": 1 + }, + "description": "页码" + }, + { + "in": "query", + "name": "limit", + "schema": { + "type": "integer", + "default": 10 + }, + "description": "每页数量" + } + ], + "responses": { + "200": { + "description": "成功获取待审核用户列表", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "data": { + "type": "object", + "properties": { + "users": { + "type": "array", + "items": { + "$ref": "#/components/schemas/User" + } + }, + "pagination": { + "type": "object", + "properties": { + "page": { + "type": "integer" + }, + "limit": { + "type": "integer" + }, + "total": { + "type": "integer" + }, + "pages": { + "type": "integer" + } + } + } + } + } + } + } + } + } + }, + "401": { + "description": "未授权" + }, + "403": { + "description": "权限不足" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/api/users/{id}/distribute": { + "put": { + "summary": "设置用户分发状态", + "description": "更新指定用户的分发状态", + "tags": [ + "Users" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + }, + "description": "用户ID" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "is_distribute" + ], + "properties": { + "is_distribute": { + "type": "boolean", + "description": "分发状态,true为启用分发,false为禁用分发", + "example": true + } + } + } + } + } + }, + "responses": { + "200": { + "description": "分发状态更新成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "分发状态更新成功" + }, + "is_distribute": { + "type": "boolean", + "description": "更新后的分发状态", + "example": true + } + } + } + } + } + }, + "400": { + "description": "请求参数错误", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": false + }, + "message": { + "type": "string", + "example": "分发状态无效" + } + } + } + } + } + }, + "404": { + "description": "用户不存在", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": false + }, + "message": { + "type": "string", + "example": "用户不存在" + } + } + } + } + } + }, + "500": { + "description": "服务器内部错误", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": false + }, + "message": { + "type": "string", + "example": "服务器内部错误" + } + } + } + } + } + } + } + } + } + }, + "tags": [ + { + "name": "Announcements", + "description": "通知公告管理API" + }, + { + "name": "Orders", + "description": "订单管理API" + }, + { + "name": "Products", + "description": "商品管理API" + }, + { + "name": "Authentication", + "description": "用户认证API" + }, + { + "name": "Captcha", + "description": "验证码API" + }, + { + "name": "Cart", + "description": "购物车管理相关接口" + }, + { + "name": "Matching", + "description": "匹配订单相关接口" + }, + { + "name": "MatchingAdmin", + "description": "匹配订单管理员相关接口" + }, + { + "name": "Points", + "description": "积分管理相关接口" + }, + { + "name": "Regions", + "description": "地区数据API" + }, + { + "name": "RiskManagement", + "description": "风险管理API" + }, + { + "name": "SMS", + "description": "短信验证码相关接口" + }, + { + "name": "Upload", + "description": "文件上传API" + }, + { + "name": "Users", + "description": "用户管理API" + } + ] +} \ No newline at end of file diff --git a/apifox-sync.js b/apifox-sync.js new file mode 100644 index 0000000..9c93f17 --- /dev/null +++ b/apifox-sync.js @@ -0,0 +1,27 @@ +/** + * 自动导出Swagger文档到本地文件 + * 使用方法: + * 1. 运行此脚本: node apifox-sync.js + * 2. 手动将生成的swagger.json文件导入到Apifox + */ +const fs = require('fs'); +const path = require('path'); +const swaggerSpecs = require('./swagger'); + +// 确保输出目录存在 +const outputDir = path.join(__dirname, 'api-docs'); +if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }); +} + +// 导出Swagger文档到JSON文件 +const outputPath = path.join(outputDir, 'swagger.json'); +fs.writeFileSync(outputPath, JSON.stringify(swaggerSpecs, null, 2)); +console.log(`Swagger文档已导出到: ${outputPath}`); +console.log('请手动将此文件导入到Apifox:'); +console.log('1. 登录Apifox网页版'); +console.log('2. 打开您的项目'); +console.log('3. 点击"导入"按钮'); +console.log('4. 选择"导入OpenAPI(Swagger)"'); +console.log('5. 上传刚才生成的swagger.json文件'); +console.log('6. 选择导入方式(合并或覆盖)并完成导入'); \ No newline at end of file diff --git a/cert/apiclient_cert.p12 b/cert/apiclient_cert.p12 new file mode 100644 index 0000000..425cddd Binary files /dev/null and b/cert/apiclient_cert.p12 differ diff --git a/cert/apiclient_cert.pem b/cert/apiclient_cert.pem new file mode 100644 index 0000000..ee924ac --- /dev/null +++ b/cert/apiclient_cert.pem @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIEKDCCAxCgAwIBAgIUXysc+VIXyKce5hqbA2vg9d9uH7kwDQYJKoZIhvcNAQEL +BQAwXjELMAkGA1UEBhMCQ04xEzARBgNVBAoTClRlbnBheS5jb20xHTAbBgNVBAsT +FFRlbnBheS5jb20gQ0EgQ2VudGVyMRswGQYDVQQDExJUZW5wYXkuY29tIFJvb3Qg +Q0EwHhcNMjUwOTAyMDg0NDA0WhcNMzAwOTAxMDg0NDA0WjCBgTETMBEGA1UEAwwK +MTcyNjM3NzMzNjEbMBkGA1UECgwS5b6u5L+h5ZWG5oi357O757ufMS0wKwYDVQQL +DCTlroHms6Lngqzono3mrYbliJvnp5HmioDmnInpmZDlhazlj7gxCzAJBgNVBAYT +AkNOMREwDwYDVQQHDAhTaGVuWmhlbjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBAKncf/y0dT12nyCmnK5w3ECQgtoHrU32LqQH/N5SUbiJVbkys99Qq41D +MO0539Ki2MBuOuDHnWOrgNbRhH4WMXS8qodTcWu39cgVlnmw/KtQJdyInrPynFYH +lWO6boc1jfM4UEbEk90iaLM49uFo75+bCNGZVHOt6UMmTNFB9zHDfnhm+UaxNJOf +p1z7sntmD3H98c4ghzcE5L51KL6A7JtW+cp0zTpOunj0T/drvmRoyQVzqZ4IlXga +9pJ/6Un2fVNn8pZL804ldKumAf5KVKmqEmz64ydh3896MoAshT/UJIHDuduN+Jbk +gROt9QiM68jR6CuqeRsV7EZ1OA/BLI8CAwEAAaOBuTCBtjAJBgNVHRMEAjAAMAsG +A1UdDwQEAwID+DCBmwYDVR0fBIGTMIGQMIGNoIGKoIGHhoGEaHR0cDovL2V2Y2Eu +aXRydXMuY29tLmNuL3B1YmxpYy9pdHJ1c2NybD9DQT0xQkQ0MjIwRTUwREJDMDRC +MDZBRDM5NzU0OTg0NkMwMUMzRThFQkQyJnNnPUhBQ0M0NzFCNjU0MjJFMTJCMjdB +OUQzM0E4N0FEMUNERjU5MjZFMTQwMzcxMA0GCSqGSIb3DQEBCwUAA4IBAQCzyJZ/ ++rnvUd1kJ74KCb6kxAwM/bX8w5lhkdUkeyQxdmbUCXCrkOGJ8uRQMfiK93eeET4h +KMrZywQHvL1E5WXQpUmQZVYj6eAxaUO+RoW8wPeWb5x/LbSXqQCrCNF6U+AvC6wj +6haW8TK8egCLsjxPBXL9NjkxvcsIOIM8F6JKhMSAAjT7F1nkXthyxC50o/Mbox6l +YfvVZ70gWuUR2e6o9Sob1tTq6YwSwDr5OMwJT8QpDEdjAbWLLtv7a+ApzfqWeTHP +dmVawsaQnOzFaUPDtEGYSh23/eaTrJ9DHpgtubtU/CQsGos6QHD6huRYMmvw3Wml +mFAVWux4DJ8lZXGZ +-----END CERTIFICATE----- diff --git a/cert/apiclient_key.pem b/cert/apiclient_key.pem new file mode 100644 index 0000000..e87a2d3 --- /dev/null +++ b/cert/apiclient_key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCp3H/8tHU9dp8g +ppyucNxAkILaB61N9i6kB/zeUlG4iVW5MrPfUKuNQzDtOd/SotjAbjrgx51jq4DW +0YR+FjF0vKqHU3Frt/XIFZZ5sPyrUCXciJ6z8pxWB5Vjum6HNY3zOFBGxJPdImiz +OPbhaO+fmwjRmVRzrelDJkzRQfcxw354ZvlGsTSTn6dc+7J7Zg9x/fHOIIc3BOS+ +dSi+gOybVvnKdM06Trp49E/3a75kaMkFc6meCJV4GvaSf+lJ9n1TZ/KWS/NOJXSr +pgH+SlSpqhJs+uMnYd/PejKALIU/1CSBw7nbjfiW5IETrfUIjOvI0egrqnkbFexG +dTgPwSyPAgMBAAECggEAF5dGN0Sg285zv0ckj52hGV54retPCHrec22gkwf/zY9V +VolSLfu4N8BTNT9KdKilTeSBTOKsW0FgfXVP32sZp0rkrDLMl9dOzWEiKviHvws8 +lupqkDdruw8Gknk8DI9FjbgOfiWjG51ByVJqB1hZn2Ma0HFpJz/KG8df99gfisuP +3s4s/N8gChKRobNJzoFTVlEexVwM8vPZl7foxm9SEc0tbXyU2yB+pglVv/BkkLo5 +aZFNIC2TVnGX1WJlTTfuhng+MMeckSIPjJGH9c7RpgAChKGOBFOAK87/1+3tep47 +qp3i2WnceIxy2Y/wIT4cGWUAd0Ghi/kkIo8KqpagEQKBgQDfiwhBitymWc5QRr+R +LM6+Yu/EWlpd4/ZrcIgX9S5Z/La4gs/fqyWyKp9r/KjyYKG5sEfArim3NenvQBac +Vp99kNpjAl6SCSWFNeA7aY+zLDJBRHSaG4ovxWXk1ZYD2hBLKahzp7PCdcNvPICU +60AapDMiGlfbcJDot1uP+9i16wKBgQDChiW9qroJGfZcpe5nkh38orPOp7or09mG +perNOAXGghJMV7tc1Se+ttjcvlfK6hrQcxPCMccH/x0bmeZ/MaG1RZjZS2zAXqH5 +9huZtgIHc2idVS+j1t4HsszuIBp84c6ykCU+6NltW1LQFRZ9Y2HuA5HW/fgyhLMG +RHpRdgfG7QKBgDd9P5NlcNgqOrhal3rl8Hv5+yJ2ezALQkPxLxcjWVolDQZIEmmn +BjhvtBsOILHporuBMo51rQ05aNRmyDYOmpCEwHELSYZelt22Pe8BiRYkxmTFJVyL +sYWiLmTbT92s55aAxLvQySJgMR8Pmatdqg/y6m5ws5ZZHt9lhGj9TxH5AoGAR8Rc +WjyJxF/av9XMPlPvUkzoz76b9h2D7KR8G1im8NT+UUIw8xAFSNyG5/Ily8xRNkSu +rn/U8YNSxuMh4h16jrltqgWkythfJCyDhFNdLkiK+Tj7iZP1eJuj9drMSvS4YLLD +uxEHXsxJolGVaY9oCvswLESo9GJ29kH/atyEBAUCgYB+xm2NI9OpF+dSq5O8vMiW +NtlD744WfsiBDzatQz1A9PAB6cyLi03zjW10+HCNZRxBSfI8au/sn9lOdcCjvSGk +8tjZXMzL71jDhEYs4qTUaiKJ2YpbLNcLFeTy48hWeRpew7t7bunksx8zb+0W14M2 +zDmIHi+CSve59dOzoiYuNg== +-----END PRIVATE KEY----- diff --git a/cert/证书使用说明.txt b/cert/证书使用说明.txt new file mode 100644 index 0000000..9a0aab1 --- /dev/null +++ b/cert/证书使用说明.txt @@ -0,0 +1,18 @@ +欢迎使用微信支付! +附件中的三份文件(证书pkcs12格式、证书pem格式、证书密钥pem格式),为接口中强制要求时需携带的证书文件。 +证书属于敏感信息,请妥善保管不要泄露和被他人复制。 +不同开发语言下的证书格式不同,以下为说明指引: + 证书pkcs12格式(apiclient_cert.p12) + 包含了私钥信息的证书文件,为p12(pfx)格式,由微信支付签发给您用来标识和界定您的身份 + 部分安全性要求较高的API需要使用该证书来确认您的调用身份 + windows上可以直接双击导入系统,导入过程中会提示输入证书密码,证书密码默认为您的商户号(如:1900006031) + 证书pem格式(apiclient_cert.pem) + 从apiclient_cert.p12中导出证书部分的文件,为pem格式,请妥善保管不要泄漏和被他人复制 + 部分开发语言和环境,不能直接使用p12文件,而需要使用pem,所以为了方便您使用,已为您直接提供 + 您也可以使用openssl命令来自己导出:openssl pkcs12 -clcerts -nokeys -in apiclient_cert.p12 -out apiclient_cert.pem + 证书密钥pem格式(apiclient_key.pem) + 从apiclient_cert.p12中导出密钥部分的文件,为pem格式 + 部分开发语言和环境,不能直接使用p12文件,而需要使用pem,所以为了方便您使用,已为您直接提供 + 您也可以使用openssl命令来自己导出:openssl pkcs12 -nocerts -in apiclient_cert.p12 -out apiclient_key.pem +备注说明: + 由于绝大部分操作系统已内置了微信支付服务器证书的根CA证书, 2018年3月6日后, 不再提供CA证书文件(rootca.pem)下载 \ No newline at end of file diff --git a/certs/README.md b/certs/README.md new file mode 100644 index 0000000..bef908a --- /dev/null +++ b/certs/README.md @@ -0,0 +1,34 @@ +# 支付宝支付证书配置指南 + +## 证书文件说明 + +本目录需要包含以下证书文件,用于支付宝支付V3版本API的证书模式: + +1. `appCertPublicKey.crt` - 应用公钥证书(已存在) +2. `alipayRootCert.crt` - 支付宝根证书(需下载) +3. `alipayCertPublicKey_RSA2.crt` - 支付宝公钥证书(需下载) + +## 如何获取证书 + +### 已有证书 +- `appCertPublicKey.crt` - 应用公钥证书已存在于本目录 + +### 需要下载的证书 +请登录支付宝开放平台,从您的应用详情页面下载以下证书: + +1. **支付宝根证书**:下载后重命名为 `alipayRootCert.crt` 并放入本目录 +2. **支付宝公钥证书**:下载后重命名为 `alipayCertPublicKey_RSA2.crt` 并放入本目录 + +## 证书下载步骤 + +1. 登录[支付宝开放平台](https://open.alipay.com/) +2. 进入「开发者中心」->「我的应用」 +3. 选择对应的应用 +4. 点击「开发设置」 +5. 在「接口加签方式」区域,选择「公钥证书」 +6. 下载「支付宝根证书」和「支付宝公钥证书」 +7. 将下载的证书放入本目录,并按上述名称重命名 + +## 配置验证 + +证书配置完成后,系统将使用证书模式进行支付宝API调用,支持V3版本的接口。 \ No newline at end of file diff --git a/certs/alipay-private-key-test.pem b/certs/alipay-private-key-test.pem new file mode 100644 index 0000000..8385c1e --- /dev/null +++ b/certs/alipay-private-key-test.pem @@ -0,0 +1 @@ +MIIEowIBAAKCAQEAk3HgYh7dK97PBX4UhlsusYGI4bLL7EQak2JfOZUeoVJK144ENg6yF0t3eohiryiK/vpyuyqX3hP5jEQMQHM92Mor9mf/KPj6Dw5AAvNLEdEaDCLwKUByk8sN2M6Xibdh6X0nQ67wOe6kaDkOG5Efkr4rHJCBfwbINXuaxXnJUr1mLdzwp1XN09IBUcKt+EXOKfpwbIuU6JoxTMiz7hnRHDuVKTYqnvx9CdV5Ng1eqYv5OI4r9abiFttRUJ37+RXbvXzffJGa/HFjpyPp4MOuK8n24ZW4vdjSBlflzzRHTAuo3tSo56gy9UouY6XtT0C9sXE7CxHkHeupOO3Wzl6t9wIDAQABAoIBAFvPRNDUNxPiITZiU5V1oZWV+w3Or3vmzEWJs5G/cNsyyrd+DtE6RVqL+1GpKwm2TRaIDHjPBNGbzn+wv5BCMfiTqtw71X5Fyi8lyGYN6Gins3hrKPAG2VF91plxyppOPgGNlK1oeN0Z4/Wh0U8JBofxMhcPRBM8vd3PoYflVZ7WpeHaApdL4n0S0Z+HcaCtnhfbJDh2fTXNkl01e2nI1Rqatkqc/Giwc8Pg5SrTKBO2g6GhPbk9LUUPqat9+3jmbT1CI56HS4OVTWFbqCMFqQE/KSXNpuFYRRTCgWDQSPbmJEKyDQMinG+DdJj2IIMhD60qYut+OBb6XQVqtdlBnoECgYEA/fbCiFIZPZ6IU7hnn75Jj2JDc6XIqFAVrbofViL3HUyeZ1KSYkXtDdh42Cl8fBCiT0xyagA/fdHmFTTJBueKfoW3+sqFMm3vKe4spJCNLmSAP8gu60zTAslmiQ5zyq+UVd74BE1Y7HSLoAv3lZnwIJkhOgrUq2mH4+r73SPN+C8CgYEAlKB+s7RZW+h4NX0YGZFdQ1AQJI0uSYOFt6YVqhTIRhUSqM++Yy/giE8X0TRckqhhAsBD6SN05rB54o6CdSacVriALtD+vrVZBDt+abTmeh1BhRUkrYeC7wZK5/+5pni9Nr1GfNKe+U2a8rzKCUJg6BWW6qGUrHOiU+9lYhQ17LkCgYBudtiipuMntD8j+z/HceNZJKqmMOQYoczsJdrfgpHuApeb5YSajkPQE+psS49D/5A54cyaYsU7GwNzEeSmxiutYMhno5NQHhU7LcfpRJ7EIR7Pn2kZG+9kdOnOj6S58qkYuMU0Sdh18TOSR+JHBhA2faTANFnQvTRIZLtsstgyLQKBgQCBl8/icYbZFMJ8IS86W/2uC8mHlXKetweJMlABlU1rjkRO3ZVsdvqY4B4sVDPDzP2JoIuWZUwxOf+NBCXMcHYxR369U45MS2PqxNVc5ldwcsIGgIESre4E7L+zus7t0KlraW5kuGHVj01kCilAGZjVxL1qqKkyFUGdXkhQVL8QQQKBgCO6GkpCR5yBs181iPh+AdI3EZh70SEBHfZNNJFs5a4FvDhxZhvkwIG77FY9IVyBn/aZLrVHJXUVB/HGA1QeNhHSXvIkWGwsNQ04OZJu0uio+4D+yGxY5XQ4F6Ti9KPI9dbu9S7OZKfZCsj5nhm/kKB3MlExN2MKqoQIMGHDcI0k \ No newline at end of file diff --git a/certs/alipay-private-key.pem b/certs/alipay-private-key.pem new file mode 100644 index 0000000..a908ace --- /dev/null +++ b/certs/alipay-private-key.pem @@ -0,0 +1 @@ +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCV9dEIPimkwxhJXXy2cntNZqeZdV4JAdx7ZpnvbjV6OjeEZczksKFa6lgsYR6Wq6G/khEdVRyNW4RsepBRwVGDBxxUN0nB9uTtPti220PtXZIJycLY585nL/iaUjUt+Cyh2qsyIGQ0lGx9VFWU+x5stGCLgfZbHOaMo0S+49upEsX5p4MyRbc4zA/vf+xgmGm5f1YB1Nwm32JBhVRourmHhi82MLX3Hxwo6R/SIOrDIcUrELYC9XRC2LUWYJIsQ1QMtrE/0XVFmIB68gU9JqRTNXhLCyL19K3WUGvRXG9N5mTaZSBIYco7ZrtbTFHtIxSOYxqYTOtkPKVYxGn1I2WHAgMBAAECggEAEeaA6Dn7YJaFPKSzMVgjDc82LGRNMEgPmI9byq/eJFP0spIwThAjgqW8lreVHikoqqR19IlnWhxVh1luBsRLxZdAs3DSFhwxoXxCBDnSNvBXcWGrJ5csFTcttsYfYPhh44QlsVsaewhIlwFNIfaD4Df72ktOK/wcLSeEGkE6xEixfMcLawg6a8rkHbvJtppFdTSL2YBB4X+dnt3cYCxFYch7Q5+RF+Wqr++860B5ztBRSX8xmqEIDS/95tf09qDyhyrnE2a2wh/jT+BWzvILKNc8goNo29kACo8bE9p+rWCnG8YsL+4emlrxk6/OYzR5lG6xZNvXAmnY0LDKzQsqeQKBgQDTm/uQP82o0ez9YHP/sAvTVq7pIJlEnaZqI2jBvyGxf6fDTU5b0yfJF9VO5EoPVT9CHRJ7A0T4RUDhIAzo9fjrz3VjkXO/RZyOIHsptjvOFeKCiFGrHO8sKxC5i4IZIRwIB7A84DU7rVzQ5Kh3XRqi8srKl/DBXo1BloWU0HJetQKBgQC1axqVH/eIwZrYjSbXgSHwGYMhH0YmOIJEYavD6TmOp9QL5N1ax6B0r/BIVt7XAIRck8kueKY4LeRulq3azXIhBy5Qx6sp2Sb/SX3vrPrvA/ImoDQKR4CeL5zXQJkqbVgbbS//UTEWilKyQXNz13D4qbCqNBbLJaDirXqBT0ycywKBgCMvT2/XvAlzBlXHAOKl0gGM6z5mJjXrhK0nQBbfAeoykKF/rCTGgloEdXpNqSbNhNwoW1dK3t/tG/GS07K0m3QSJbGtkLJgD7zuF6yC2YTVzLjpk7LA99+/NWO0l6g4AiIvrRUiLpfCpqkxK/XU7EXl2uQ+yVBNuW0LayCoXCv1AoGAU/CJbSRMUO9baQTuStoJzODRBltFBtwwkdkrM0tPAU1v1E0BikZBXJwnLiFbm9k2ZOtQM3tJVUcOoYiASnOyccuzx1aLQKKj44yqg2Hi/QIzYWHQkk0BGq/m/sV52OKc2JvNkHGNp+M6XhXgiGHPeI5zGl1dioMPjLI9s2Twir8CgYEAn5wAbpx2imCPDtKgMxb0zxbKB5kq37QGuIRRAoorPkFr0Z7xSZs1M0qkhZcaX/kfnNgigPVwpUeUDQmETBRaXrCuXxRkOXGo1st4v5gn9kgQYBCv0dVbV1QNnnXp2fAtQ/3Jw6l9w4T0PgPBhu9TPnSTVZA0YldNo7TEEL5kEbA= \ No newline at end of file diff --git a/certs/alipay-public-key-test.pem b/certs/alipay-public-key-test.pem new file mode 100644 index 0000000..29aa871 --- /dev/null +++ b/certs/alipay-public-key-test.pem @@ -0,0 +1 @@ +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAhptRt5n0KVACDX69KfZogfeLULSuZGkH0m3wVDI32a4eBH3TrzKjSShdO2WBF6glfvdCruuMuLmYJrIxa09koNcAF5CNc/2iHE8O8pUbrsLKgX+dpXoNZt6XVo/jHqsxmYDFjZogL3xFVW0z0pGcxaIWnVEhQ0hQ+ji2RxJ0Bb45mmOZOOVihpLf9hEFW0rHamf2Tfu+Hd4NWTb/CVZwgzchJ/cwLTqP1Ar3GeQdmB2tmaCTu1h8wyt6lkSUTOYTEf8xCdmv8xfS12dXAeh11t1a3SuqPT2b4UxuLJHHMmiKndD7BnPZIENxi7e6N5JKz1zahyuh23GeBCHs1wHtQQIDAQAB diff --git a/certs/alipay-public-key-y.pem b/certs/alipay-public-key-y.pem new file mode 100644 index 0000000..89738c0 --- /dev/null +++ b/certs/alipay-public-key-y.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlfXRCD4ppMMYSV18tnJ7 +TWanmXVeCQHce2aZ7241ejo3hGXM5LChWupYLGEelquhv5IRHVUcjVuEbHqQUcFR +gwccVDdJwfbk7T7YtttD7V2SCcnC2OfOZy/4mlI1LfgsodqrMiBkNJRsfVRVlPse +bLRgi4H2WxzmjKNEvuPbqRLF+aeDMkW3OMwP73/sYJhpuX9WAdTcJt9iQYVUaLq5 +h4YvNjC19x8cKOkf0iDqwyHFKxC2AvV0Qti1FmCSLENUDLaxP9F1RZiAevIFPSak +UzV4Swsi9fSt1lBr0VxvTeZk2mUgSGHKO2a7W0xR7SMUjmMamEzrZDylWMRp9SNl +hwIDAQAB +-----END PUBLIC KEY----- \ No newline at end of file diff --git a/certs/alipay-public-key.pem b/certs/alipay-public-key.pem new file mode 100644 index 0000000..8c927a9 --- /dev/null +++ b/certs/alipay-public-key.pem @@ -0,0 +1 @@ +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5swLKPSzOMucRC52c9kKJZI9cYWDFd+s3UuE+aDtWodGrGV8g3szmp7hUWlaWY/didKc9vQNq93y67eEyw6QsMn26WwlzDbgP0xTcHEt+qDCeAltSqf6MX3KPmlz0f/DNneR9DR9ZGwaW1ATY3kg8gj+kIWngrqgjOv37UJWEpQOxUfWDGTBC1zzhC0PTXY7lX3GUZmDEtDtBs1BsFUdk995TbTD1cTiyDFuea49br0dovmU1ROOg6vK3G9xDd4Mke/opDunLTHe63+fBCnB7FyZ9F8zWg4LYND1QPmIX2m5gwICBHhNm8WqIfp9T64vpAxlM74BEsMlv3hNy0INQQIDAQAB \ No newline at end of file diff --git a/certs/alipayCertPublicKey_RSA2.crt b/certs/alipayCertPublicKey_RSA2.crt new file mode 100644 index 0000000..60f26b0 --- /dev/null +++ b/certs/alipayCertPublicKey_RSA2.crt @@ -0,0 +1,3 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5swLKPSzOMucRC52c9kKJZI9cYWDFd+s3UuE+aDtWodGrGV8g3szmp7hUWlaWY/didKc9vQNq93y67eEyw6QsMn26WwlzDbgP0xTcHEt+qDCeAltSqf6MX3KPmlz0f/DNneR9DR9ZGwaW1ATY3kg8gj+kIWngrqgjOv37UJWEpQOxUfWDGTBC1zzhC0PTXY7lX3GUZmDEtDtBs1BsFUdk995TbTD1cTiyDFuea49br0dovmU1ROOg6vK3G9xDd4Mke/opDunLTHe63+fBCnB7FyZ9F8zWg4LYND1QPmIX2m5gwICBHhNm8WqIfp9T64vpAxlM74BEsMlv3hNy0INQQIDAQAB +-----END PUBLIC KEY----- \ No newline at end of file diff --git a/certs/alipayPublicKey_RSA2.crt b/certs/alipayPublicKey_RSA2.crt new file mode 100644 index 0000000..60f26b0 --- /dev/null +++ b/certs/alipayPublicKey_RSA2.crt @@ -0,0 +1,3 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5swLKPSzOMucRC52c9kKJZI9cYWDFd+s3UuE+aDtWodGrGV8g3szmp7hUWlaWY/didKc9vQNq93y67eEyw6QsMn26WwlzDbgP0xTcHEt+qDCeAltSqf6MX3KPmlz0f/DNneR9DR9ZGwaW1ATY3kg8gj+kIWngrqgjOv37UJWEpQOxUfWDGTBC1zzhC0PTXY7lX3GUZmDEtDtBs1BsFUdk995TbTD1cTiyDFuea49br0dovmU1ROOg6vK3G9xDd4Mke/opDunLTHe63+fBCnB7FyZ9F8zWg4LYND1QPmIX2m5gwICBHhNm8WqIfp9T64vpAxlM74BEsMlv3hNy0INQQIDAQAB +-----END PUBLIC KEY----- \ No newline at end of file diff --git a/certs/appCertPublicKey.crt b/certs/appCertPublicKey.crt new file mode 100644 index 0000000..d13ac61 --- /dev/null +++ b/certs/appCertPublicKey.crt @@ -0,0 +1,3 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlfXRCD4ppMMYSV18tnJ7TWanmXVeCQHce2aZ7241ejo3hGXM5LChWupYLGEelquhv5IRHVUcjVuEbHqQUcFRgwccVDdJwfbk7T7YtttD7V2SCcnC2OfOZy/4mlI1LfgsodqrMiBkNJRsfVRVlPsebLRgi4H2WxzmjKNEvuPbqRLF+aeDMkW3OMwP73/sYJhpuX9WAdTcJt9iQYVUaLq5h4YvNjC19x8cKOkf0iDqwyHFKxC2AvV0Qti1FmCSLENUDLaxP9F1RZiAevIFPSakUzV4Swsi9fSt1lBr0VxvTeZk2mUgSGHKO2a7W0xR7SMUjmMamEzrZDylWMRp9SNlhwIDAQAB +-----END PUBLIC KEY----- \ No newline at end of file diff --git a/config/config.js b/config/config.js new file mode 100644 index 0000000..3099ef2 --- /dev/null +++ b/config/config.js @@ -0,0 +1,17 @@ +const mysql = require('mysql2') + +const sql = { + createConnection() { + return mysql.createPool({ + connectionLimit: 10, + host: '114.55.111.44', + user: 'test_mao', + password: 'nK2mPbWriBp25BRd', + database: 'test_mao', + charset: 'utf8mb4', + multipleStatements: true + + }) + } +} +module.exports = sql diff --git a/config/constants.js b/config/constants.js new file mode 100644 index 0000000..3272b44 --- /dev/null +++ b/config/constants.js @@ -0,0 +1,70 @@ +// 系统常量配置 +module.exports = { + // 转账类型 + TRANSFER_TYPES: { + USER_TO_USER: 'user_to_user', + SYSTEM_TO_USER: 'system_to_user', + USER_TO_SYSTEM: 'user_to_system' + }, + + // 转账状态 + TRANSFER_STATUS: { + PENDING: 'pending', + CONFIRMED: 'confirmed', + RECEIVED: 'received', + REJECTED: 'rejected', + CANCELLED: 'cancelled', + NOT_RECEIVED: 'not_received', + FAILED: 'failed' + }, + + // 用户角色 + USER_ROLES: { + ADMIN: 'admin', + USER: 'user' + }, + + // 订单状态 + ORDER_STATUS: { + PENDING: 'pending', + PAID: 'paid', + SHIPPED: 'shipped', + DELIVERED: 'delivered', + CANCELLED: 'cancelled' + }, + + // 错误代码 + ERROR_CODES: { + VALIDATION_ERROR: 'VALIDATION_ERROR', + AUTHENTICATION_ERROR: 'AUTHENTICATION_ERROR', + AUTHORIZATION_ERROR: 'AUTHORIZATION_ERROR', + NOT_FOUND: 'NOT_FOUND', + DUPLICATE_ENTRY: 'DUPLICATE_ENTRY', + DATABASE_ERROR: 'DATABASE_ERROR', + INTERNAL_ERROR: 'INTERNAL_ERROR' + }, + + // HTTP状态码 + HTTP_STATUS: { + OK: 200, + CREATED: 201, + BAD_REQUEST: 400, + UNAUTHORIZED: 401, + FORBIDDEN: 403, + NOT_FOUND: 404, + CONFLICT: 409, + INTERNAL_SERVER_ERROR: 500 + }, + + // 分页默认值 + PAGINATION: { + DEFAULT_PAGE: 1, + DEFAULT_LIMIT: 10, + MAX_LIMIT: 100 + }, + + // JWT配置 + JWT: { + EXPIRES_IN: '24h' + } +}; \ No newline at end of file diff --git a/config/database-init.js b/config/database-init.js new file mode 100644 index 0000000..a0481bf --- /dev/null +++ b/config/database-init.js @@ -0,0 +1,38 @@ +const mysql = require('mysql2/promise'); +const bcrypt = require('bcryptjs'); +const { initDB, getDB, dbConfig } = require('../database'); + +/** + * 数据库初始化函数 + * 创建所有必要的表结构和初始数据 + */ +async function initDatabase() { + try { + + + // 初始化数据库连接池 + await initDB(); + console.log('数据库连接池初始化成功'); + + // 创建所有表 + // await createTables(); + + // 添加字段(处理表结构升级) + // await addMissingFields(); + + // 创建默认数据 + // await createDefaultData(); + + console.log('数据库初始化完成'); + } catch (error) { + console.error('数据库初始化失败:', error); + throw error; + } +} + + + + +module.exports = { + initDatabase +}; \ No newline at end of file diff --git a/config/dbv2.js b/config/dbv2.js new file mode 100644 index 0000000..e7e253b --- /dev/null +++ b/config/dbv2.js @@ -0,0 +1,363 @@ +class QueryBuilder { + constructor() { + this.conditions = {}; + this.limit = null; + this.offset = null; + this.groupBy = null; + } + + where(condition, ...params) { + this.conditions[condition] = params; + return this; + } + + setLimit(limit) { + this.limit = limit; + return this; + } + + setOffset(offset) { + this.offset = offset; + return this; + } + + setGroupBy(groupBy) { + this.groupBy = groupBy; + return this; + } + + sqdata(sql, params) { + return new Promise((resolve, reject) => { + global.sqlReq.query(sql, params, (err, result) => { + if (err) { + reject(err); + } + resolve(result); + }); + }); + } + + getParams() { + return Object.values(this.conditions).flat(); + } + + buildConditions() { + return Object.keys(this.conditions).map(condition => `${condition}`).join(' AND '); + } +} + +class SelectBuilder extends QueryBuilder { + constructor() { + super(); + this.selectFields = []; + this.tables = []; + this.orderByField = ''; + this.orderByDirection = 'ASC'; + this.subQueries = []; // 用于存储子查询 + this.unions = []; // 存储UNION查询 + } + // 添加UNION查询 + union(queryBuilder, type = 'UNION') { + this.unions.push({queryBuilder, type}); + return this; + } + + // 添加UNION ALL查询 + unionAll(queryBuilder) { + this.union(queryBuilder, 'UNION ALL'); + return this; + } + + // 构建主查询部分(不含ORDER BY/LIMIT/OFFSET) + buildMainQuery() { + const subQuerySQL = this.subQueries.map(({alias, subQuery}) => `(${subQuery}) AS ${alias}`); + const selectClause = this.selectFields.concat(subQuerySQL).join(', '); + + let sql = `SELECT ${selectClause} + FROM ${this.tables.join(' ')}`; + + const conditionClauses = this.buildConditions(); + if (conditionClauses) { + sql += ` WHERE ${conditionClauses}`; + } + + if (this.groupBy) { + sql += ` GROUP BY ${this.groupBy}`; + } + + const params = this.getParams(); + return {sql, params}; + } + + // 供UNION查询调用的构建方法 + buildForUnion() { + return this.buildMainQuery(); + } + + select(fields) { + this.selectFields = fields.split(',').map(field => field.trim()); + return this; + } + + // 添加子查询 + addSubQuery(alias, subQuery) { + this.subQueries.push({alias, subQuery}); + return this; + } + + whereLike(fields, keyword) { + const likeConditions = fields.map(field => `${field} LIKE ?`).join(' OR '); + this.conditions[likeConditions] = fields.map(() => `%${keyword}%`); + return this; + } + + from(table) { + this.tables.push(table); + return this; + } + + leftJoin(table, condition) { + this.tables.push(`LEFT JOIN ${table} ON ${condition}`); + return this; + } + + orderBy(field, direction = 'ASC') { + this.orderByField = field; + this.orderByDirection = direction.toUpperCase(); + return this; + } + + paginate(page, pageSize) { + if (page <= 0 || pageSize <= 0) { + throw new Error('分页参数必须大于0'); + } + this.limit = pageSize; + this.offset = (page - 1) * pageSize; + return this; + } + + async chidBuild() { + + let sql = `SELECT ${this.selectFields.join(', ')} + FROM ${this.tables.join(' ')}`; + let conditionClauses = this.buildConditions(); + if (conditionClauses) { + sql += ` WHERE ${conditionClauses}`; + } + if (this.orderByField) { + sql += ` ORDER BY ${this.orderByField} ${this.orderByDirection}`; + } + if (this.limit !== null) { + sql += ` LIMIT ${this.limit}`; + } + if (this.offset !== null) { + sql += ` OFFSET ${this.offset}`; + } + return sql; + } + + async build() { + const main = this.buildMainQuery(); + let fullSql = `(${main.sql})`; + const allParams = [...main.params]; + + // 处理UNION部分 + for (const union of this.unions) { + const unionBuilder = union.queryBuilder; + if (!(unionBuilder instanceof SelectBuilder)) { + throw new Error('UNION query must be a SelectBuilder instance'); + } + const unionResult = unionBuilder.buildForUnion(); + fullSql += ` ${union.type} (${unionResult.sql})`; + allParams.push(...unionResult.params); + } + + // 添加ORDER BY、LIMIT、OFFSET + if (this.orderByField) { + fullSql += ` ORDER BY ${this.orderByField} ${this.orderByDirection}`; + } + if (this.limit !== null) { + fullSql += ` LIMIT ${this.limit}`; + } + if (this.offset !== null) { + fullSql += ` OFFSET ${this.offset}`; + } + console.log(fullSql,allParams); + return await this.sqdata(fullSql, allParams); + } +} + + +class UpdateBuilder extends QueryBuilder { + constructor() { + super(); + this.table = ''; + this.updateFields = {}; + } + + update(table) { + this.table = table; + return this; + } + + set(field, value) { + if (value && value.increment && typeof value === 'object' ) { + this.updateFields[field] = {increment: value.increment}; + } else { + this.updateFields[field] = value; + } + return this; + } + + async build() { + let sql = `UPDATE ${this.table} + SET `; + let updateClauses = Object.keys(this.updateFields).map(field => { + const value = this.updateFields[field]; + if (value && value.increment && typeof value === 'object' ) { + return `${field} = ${field} + ?`; + } + return `${field} = ?`; + }).join(', '); + + sql += updateClauses; + + let conditionClauses = this.buildConditions(); + if (conditionClauses) { + sql += ` WHERE ${conditionClauses}`; + } + // 处理参数,确保自增字段也传入增量值 + const params = [ + ...Object.values(this.updateFields).map(value => + (value && value.increment && typeof value === 'object' ) ? value.increment : value + ), + ...this.getParams() + ]; + return await this.sqdata(sql, params); + } +} + +class InsertBuilder extends QueryBuilder { + constructor() { + super(); + this.table = ''; + this.insertValues = []; + this.updateValues = {}; + } + + insertInto(table) { + this.table = table; + return this; + } + + // 仍然保留单条记录的插入 + values(values) { + if (Array.isArray(values)) { + this.insertValues = values; + } else { + this.insertValues = [values]; // 将单条记录包装成数组 + } + return this; + } + + // 新增方法,支持一次插入多条记录 + valuesMultiple(records) { + if (!Array.isArray(records) || records.length === 0) { + throw new Error('Values must be a non-empty array'); + } + + // 确保每一条记录都是对象 + records.forEach(record => { + if (typeof record !== 'object') { + throw new Error('Each record must be an object'); + } + }); + + this.insertValues = records; + return this; + } + + // 新增 upsert 方法,支持更新或插入 + upsert(values, updateFields) { + // values: 要插入的记录 + // updateFields: 如果记录存在时,需要更新的字段 + if (!Array.isArray(values) || values.length === 0) { + throw new Error('Values must be a non-empty array'); + } + + // 检查每条记录是否是对象 + values.forEach(record => { + if (typeof record !== 'object') { + throw new Error('Each record must be an object'); + } + }); + + this.insertValues = values; + this.updateValues = updateFields || {}; + return this; + } + + async build() { + if (this.insertValues.length === 0) { + throw new Error("No values to insert"); + } + + // 获取表单列名,假设所有记录有相同的字段 + const columns = Object.keys(this.insertValues[0]); + + // 构建 VALUES 子句,支持批量插入 + const valuePlaceholders = this.insertValues.map(() => + `(${columns.map(() => '?').join(', ')})` + ).join(', '); + + // 展平所有的插入值 + const params = this.insertValues.flatMap(record => + columns.map(column => record[column]) + ); + + // 如果有 updateFields,构建 ON DUPLICATE KEY UPDATE 子句 + let updateClause = ''; + if (Object.keys(this.updateValues).length > 0) { + updateClause = ' ON DUPLICATE KEY UPDATE ' + + Object.keys(this.updateValues).map(field => { + return `${field} = VALUES(${field})`; + }).join(', '); + } + + // 生成 SQL 语句 + const sql = `INSERT INTO ${this.table} (${columns.join(', ')}) + VALUES ${valuePlaceholders} ${updateClause}`; + // 执行查询 + return await this.sqdata(sql, params); + } +} + + +class DeleteBuilder extends QueryBuilder { + constructor() { + super(); + this.table = ''; + } + + deleteFrom(table) { + this.table = table; + return this; + } + + async build() { + let sql = `DELETE + FROM ${this.table}`; + let conditionClauses = this.buildConditions(); + if (conditionClauses) { + sql += ` WHERE ${conditionClauses}`; + } + return await this.sqdata(sql, this.getParams()); + } +} + +module.exports = { + SelectBuilder, + UpdateBuilder, + InsertBuilder, + DeleteBuilder, +}; diff --git a/config/logger.js b/config/logger.js new file mode 100644 index 0000000..45cda51 --- /dev/null +++ b/config/logger.js @@ -0,0 +1,73 @@ +const winston = require('winston'); +const path = require('path'); + +// 创建日志目录 +const logDir = path.join(__dirname, '../logs'); + +// 日志格式配置 +const logFormat = winston.format.combine( + winston.format.timestamp({ + format: 'YYYY-MM-DD HH:mm:ss' + }), + winston.format.errors({ stack: true }), + winston.format.json() +); + +// 控制台日志格式 +const consoleFormat = winston.format.combine( + winston.format.colorize(), + winston.format.timestamp({ + format: 'YYYY-MM-DD HH:mm:ss' + }), + winston.format.printf(({ timestamp, level, message, ...meta }) => { + return `${timestamp} [${level}]: ${message} ${Object.keys(meta).length ? JSON.stringify(meta, null, 2) : ''}`; + }) +); + +// 创建logger实例 +const logger = winston.createLogger({ + level: process.env.LOG_LEVEL || 'info', + format: logFormat, + defaultMeta: { service: 'integrated-system' }, + transports: [ + // 错误日志文件 + new winston.transports.File({ + filename: path.join(logDir, 'error.log'), + level: 'error', + maxsize: 5242880, // 5MB + maxFiles: 5 + }), + // 所有日志文件 + new winston.transports.File({ + filename: path.join(logDir, 'combined.log'), + maxsize: 5242880, // 5MB + maxFiles: 5 + }) + ] +}); + +// 开发环境添加控制台输出 +if (process.env.NODE_ENV !== 'production') { + logger.add(new winston.transports.Console({ + format: consoleFormat + })); +} + +// 审计日志记录器 +const auditLogger = winston.createLogger({ + level: 'info', + format: logFormat, + defaultMeta: { service: 'audit' }, + transports: [ + new winston.transports.File({ + filename: path.join(logDir, 'audit.log'), + maxsize: 5242880, // 5MB + maxFiles: 10 + }) + ] +}); + +module.exports = { + logger, + auditLogger +}; \ No newline at end of file diff --git a/config/minio.js b/config/minio.js new file mode 100644 index 0000000..f479bea --- /dev/null +++ b/config/minio.js @@ -0,0 +1,97 @@ +const Minio = require('minio'); +require('dotenv').config(); + +/** + * MinIO 配置 + * 用于对象存储服务配置 + */ +const minioConfig = { + // MinIO 服务器配置 + endPoint: process.env.MINIO_ENDPOINT || 'localhost', + port: parseInt(process.env.MINIO_PORT) || 9000, + useSSL: process.env.MINIO_USE_SSL === 'true' || false, + accessKey: process.env.MINIO_ACCESS_KEY || 'minioadmin', + secretKey: process.env.MINIO_SECRET_KEY || 'minioadmin', + + // 存储桶配置 + buckets: { + uploads: process.env.MINIO_BUCKET_UPLOADS || 'uploads', + avatars: process.env.MINIO_BUCKET_AVATARS || 'avatars', + products: process.env.MINIO_BUCKET_PRODUCTS || 'products', + documents: process.env.MINIO_BUCKET_DOCUMENTS || 'documents' + }, + + // 文件访问配置 + publicUrl: process.env.MINIO_PUBLIC_URL || `http://localhost:9000` +}; + +/** + * 创建 MinIO 客户端实例 + */ +const createMinioClient = () => { + return new Minio.Client({ + endPoint: minioConfig.endPoint, + port: minioConfig.port, + useSSL: minioConfig.useSSL, + accessKey: minioConfig.accessKey, + secretKey: minioConfig.secretKey + }); +}; + +/** + * 初始化存储桶 + * 确保所有需要的存储桶都存在 + */ +const initializeBuckets = async () => { + const minioClient = createMinioClient(); + + try { + // 检查并创建存储桶 + for (const [key, bucketName] of Object.entries(minioConfig.buckets)) { + const exists = await minioClient.bucketExists(bucketName); + if (!exists) { + await minioClient.makeBucket(bucketName, 'us-east-1'); + console.log(`✅ 存储桶 '${bucketName}' 创建成功`); + + // 设置存储桶策略为公开读取(可选) + const policy = { + Version: '2012-10-17', + Statement: [ + { + Effect: 'Allow', + Principal: { AWS: ['*'] }, + Action: ['s3:GetObject'], + Resource: [`arn:aws:s3:::${bucketName}/*`] + } + ] + }; + + try { + await minioClient.setBucketPolicy(bucketName, JSON.stringify(policy)); + console.log(`✅ 存储桶 '${bucketName}' 策略设置成功`); + } catch (policyError) { + console.warn(`⚠️ 存储桶 '${bucketName}' 策略设置失败:`, policyError.message); + } + } else { + console.log(`✅ 存储桶 '${bucketName}' 已存在`); + } + } + } catch (error) { + console.error('❌ 初始化存储桶失败:', error); + throw error; + } +}; + +/** + * 获取文件的公开访问URL + */ +const getPublicUrl = (bucketName, objectName) => { + return `${minioConfig.publicUrl}/${bucketName}/${objectName}`; +}; + +module.exports = { + minioConfig, + createMinioClient, + initializeBuckets, + getPublicUrl +}; \ No newline at end of file diff --git a/config/wechatPay.js b/config/wechatPay.js new file mode 100644 index 0000000..16b61f9 --- /dev/null +++ b/config/wechatPay.js @@ -0,0 +1,24 @@ +// 微信支付配置 +module.exports = { + // 微信支付配置 + wechatPay: { + appId: process.env.WECHAT_APP_ID || '', // 微信公众号AppID + mchId: process.env.WECHAT_MCH_ID || '', // 商户号 + apiKey: process.env.WECHAT_API_KEY || '', // API密钥 + apiV3Key: process.env.WECHAT_API_V3_KEY || '', // APIv3密钥 + notifyUrl: process.env.WECHAT_NOTIFY_URL || 'https://your-domain.com/api/wechat/notify', // 支付回调地址 + + // 证书路径(生产环境需要配置) + certPath: process.env.WECHAT_CERT_PATH || '', + keyPath: process.env.WECHAT_KEY_PATH || '', + + // 支付相关配置 + tradeType: { + h5: 'MWEB', // H5支付 + jsapi: 'JSAPI' // 公众号支付 + }, + + // 注册费用配置(单位:分) + registrationFee: 100 // 1元注册费 + } +}; \ No newline at end of file diff --git a/config/withdrawal-init.sql b/config/withdrawal-init.sql new file mode 100644 index 0000000..a42c3fe --- /dev/null +++ b/config/withdrawal-init.sql @@ -0,0 +1,34 @@ +-- 创建代理提现记录表 +CREATE TABLE IF NOT EXISTS agent_withdrawals ( + id INT AUTO_INCREMENT PRIMARY KEY, + agent_id INT NOT NULL, + amount DECIMAL(10,2) NOT NULL, + payment_type ENUM('bank', 'wechat', 'alipay', 'unionpay') DEFAULT 'bank' COMMENT '收款方式类型', + bank_name VARCHAR(100) COMMENT '银行名称', + account_number VARCHAR(50) COMMENT '账号/银行账号', + account_holder VARCHAR(100) COMMENT '持有人姓名', + qr_code_url VARCHAR(255) COMMENT '收款码图片URL', + status ENUM('pending', 'approved', 'rejected', 'completed') DEFAULT 'pending', + apply_note TEXT, + admin_note TEXT, + processed_by INT NULL, + processed_at TIMESTAMP NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (agent_id) REFERENCES regional_agents(id) ON DELETE CASCADE, + FOREIGN KEY (processed_by) REFERENCES users(id) ON DELETE SET NULL, + -- 兼容旧字段 + bank_account VARCHAR(50) COMMENT '银行账号(兼容旧版本)' +); + +-- 为regional_agents表添加提现相关字段 +ALTER TABLE regional_agents ADD COLUMN withdrawn_amount DECIMAL(10,2) DEFAULT 0.00 COMMENT '已提现金额'; +ALTER TABLE regional_agents ADD COLUMN pending_withdrawal DECIMAL(10,2) DEFAULT 0.00 COMMENT '待审核提现金额'; +ALTER TABLE regional_agents ADD COLUMN payment_type ENUM('bank', 'wechat', 'alipay', 'unionpay') DEFAULT 'bank' COMMENT '收款方式类型'; +ALTER TABLE regional_agents ADD COLUMN bank_name VARCHAR(100) COMMENT '银行名称'; +ALTER TABLE regional_agents ADD COLUMN account_number VARCHAR(50) COMMENT '账号/银行账号'; +ALTER TABLE regional_agents ADD COLUMN account_holder VARCHAR(100) COMMENT '持有人姓名'; +ALTER TABLE regional_agents ADD COLUMN qr_code_url VARCHAR(255) COMMENT '收款码图片URL'; + +-- 兼容旧字段(可选,用于数据迁移) +ALTER TABLE regional_agents ADD COLUMN bank_account VARCHAR(50) COMMENT '银行账号(兼容旧版本)'; \ No newline at end of file diff --git a/database.js b/database.js new file mode 100644 index 0000000..949a3af --- /dev/null +++ b/database.js @@ -0,0 +1,159 @@ +const mysql = require('mysql2/promise'); + +// 数据库配置 +const dbConfig = { + // host: process.env.DB_HOST || '114.55.111.44', + // user: process.env.DB_USER || 'maov2', + // password: process.env.DB_PASSWORD || '5fYhw8z6T62b7heS', + // database: process.env.DB_NAME || 'maov2', + host: '114.55.111.44', + user: 'test_mao', + password: 'nK2mPbWriBp25BRd', + database: 'test_mao', + charset: 'utf8mb4', + dateStrings: true, + // 连接池配置 + connectionLimit: 20, // 连接池最大连接数 + queueLimit: 0, // 排队等待连接的最大数量,0表示无限制 + // 连接超时配置 + // acquireTimeout: 60000, // 获取连接超时时间 60秒 + // timeout: 60000, // 查询超时时间 60秒 + // reconnect: true, // 自动重连 + // 连接保活配置 + // multipleStatements: true, + // 空闲连接超时配置 + // idleTimeout: 300000, // 5分钟空闲超时 + // maxLifetime: 1800000, // 30分钟最大生命周期 + // 连接保活设置 + // keepAliveInitialDelay: 0, // 开始保活探测前的延迟时间 + // enableKeepAlive: true, // 启用TCP保活 + // 添加类型转换配置 + typeCast: function (field, next) { + if (field.type === 'TINY' && field.length === 1) { + return (field.string() === '1'); // 1 = true, 0 = false + } + return next(); + }, + // 确保参数正确处理 + supportBigNumbers: true, + bigNumberStrings: false +}; + +// 创建数据库连接池 +let pool; + +/** + * 初始化数据库连接池 + * @returns {Promise} 数据库连接池 + */ +async function initDB() { + if (!pool) { + try { + pool = mysql.createPool(dbConfig); + + // 添加连接池事件监听 + pool.on('connection', function (connection) { + console.log('新的数据库连接建立:', connection.threadId); + }); + + // 注释掉频繁的连接获取和释放日志,避免日志过多 + // pool.on('acquire', function (connection) { + // console.log('连接池获取连接:', connection.threadId); + // }); + + // pool.on('release', function (connection) { + // console.log('连接池释放连接:', connection.threadId); + // }); + + pool.on('error', function (err) { + console.error('数据库连接池错误:', err); + if (err.code === 'PROTOCOL_CONNECTION_LOST') { + console.log('数据库连接丢失,尝试重新连接...'); + } else if (err.code === 'ECONNRESET') { + console.log('数据库连接被重置,尝试重新连接...'); + } else if (err.code === 'ETIMEDOUT') { + console.log('数据库连接超时,尝试重新连接...'); + } + }); + + // 测试连接 + const connection = await pool.getConnection(); + console.log('数据库连接池初始化成功'); + connection.release(); + + } catch (error) { + console.error('数据库连接池初始化失败:', error); + throw error; + } + } + return pool; +} + +/** + * 获取数据库连接池 + * @returns {mysql.Pool} 数据库连接池 + */ +function getDB() { + if (!pool) { + throw new Error('数据库连接池未初始化,请先调用 initDB()'); + } + return pool; +} + +/** + * 执行数据库查询(带重试机制) + * @param {string} sql SQL查询语句 + * @param {Array} params 查询参数 + * @param {number} retries 重试次数 + * @returns {Promise} 查询结果 + */ +async function executeQuery(sql, params = [], retries = 3) { + for (let i = 0; i < retries; i++) { + try { + const connection = await pool.getConnection(); + try { + const [results] = await connection.execute(sql, params); + connection.release(); + return results; + } catch (error) { + connection.release(); + throw error; + } + } catch (error) { + console.error(`数据库查询失败 (尝试 ${i + 1}/${retries}):`, error.message); + + if (i === retries - 1) { + throw error; + } + + // 如果是连接相关错误,等待后重试 + if (error.code === 'PROTOCOL_CONNECTION_LOST' || + error.code === 'ECONNRESET' || + error.code === 'ETIMEDOUT') { + console.log(`等待 ${(i + 1) * 1000}ms 后重试...`); + await new Promise(resolve => setTimeout(resolve, (i + 1) * 1000)); + } else { + throw error; + } + } + } +} + +/** + * 关闭数据库连接池 + */ +async function closeDB() { + if (pool) { + await pool.end(); + pool = null; + console.log('数据库连接池已关闭'); + } +} + +module.exports = { + initDB, + getDB, + closeDB, + executeQuery, + dbConfig +}; \ No newline at end of file diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..73d8db2 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,81 @@ +# API 文档结构说明 + +本项目已将 Swagger API 文档从路由文件中分离出来,采用模块化的文档管理方式。 + +## 文件夹结构 + +``` +docs/ +├── README.md # 本说明文件 +├── schemas/ # 数据模型定义 +│ ├── product.js # 商品相关数据模型 +│ ├── order.js # 订单相关数据模型 +│ ├── user.js # 用户相关数据模型 +│ ├── cart.js # 购物车相关数据模型 +│ └── announcement.js # 通知公告相关数据模型 +└── apis/ # API 接口定义 + ├── products.js # 商品相关 API + ├── orders.js # 订单相关 API + └── announcements.js # 通知公告相关 API +``` + +## 优势 + +1. **模块化管理**: 按功能模块分离文档,便于维护和查找 +2. **代码分离**: 路由文件专注于业务逻辑,文档定义独立管理 +3. **复用性**: Schema 定义可以在多个 API 中复用 +4. **可维护性**: 文档修改不会影响业务代码,降低出错风险 + +## 使用方法 + +### 添加新的 Schema + +在 `schemas/` 文件夹中创建新的 `.js` 文件,使用 `@swagger` 注释定义数据模型: + +```javascript +/** + * @swagger + * components: + * schemas: + * YourModel: + * type: object + * properties: + * id: + * type: integer + * description: ID + */ +``` + +### 添加新的 API 文档 + +在 `apis/` 文件夹中创建新的 `.js` 文件,使用 `@swagger` 注释定义 API 接口: + +```javascript +/** + * @swagger + * tags: + * name: YourModule + * description: 模块描述 + */ + +/** + * @swagger + * /your-endpoint: + * get: + * summary: 接口描述 + * tags: [YourModule] + * responses: + * 200: + * description: 成功响应 + */ +``` + +## 配置 + +Swagger 配置文件 `swagger.js` 已更新扫描路径: + +```javascript +apis: ['./docs/schemas/*.js', './docs/apis/*.js', './routes/*.js', './admin/routes/*.js'] +``` + +这样既保持了对现有路由文件中文档的兼容性,又支持新的模块化文档结构。 \ No newline at end of file diff --git a/docs/apis/announcements.js b/docs/apis/announcements.js new file mode 100644 index 0000000..12c5eea --- /dev/null +++ b/docs/apis/announcements.js @@ -0,0 +1,736 @@ +/** + * @swagger + * tags: + * name: Announcements + * description: 通知公告管理API + */ + +/** + * @swagger + * /api/announcements/{id}: + * get: + * summary: 获取单个公告详情 + * tags: [Announcements] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: 公告ID + * responses: + * 200: + * description: 成功获取公告详情 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * data: + * $ref: '#/components/schemas/Announcement' + * 401: + * description: 未授权 + * 404: + * description: 公告不存在 + * 500: + * description: 服务器错误 + */ + +/** + * @swagger + * /api/announcements/{id}/read: + * post: + * summary: 标记公告为已读 + * tags: [Announcements] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: 公告ID + * responses: + * 200: + * description: 标记已读成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: "已标记为已读" + * 401: + * description: 未授权 + * 404: + * description: 公告不存在 + * 500: + * description: 服务器错误 + */ + +/** + * @swagger + * /api/announcements/unread/count: + * get: + * summary: 获取用户未读公告数量 + * tags: [Announcements] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: 获取未读数量成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * data: + * type: object + * properties: + * unread_count: + * type: integer + * example: 5 + * description: 未读公告数量 + * 401: + * description: 未授权 + * 500: + * description: 服务器错误 + */ + +/** + * @swagger + * /api/announcements/batch/read: + * post: + * summary: 批量标记公告为已读 + * tags: [Announcements] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - announcement_ids + * properties: + * announcement_ids: + * type: array + * items: + * type: integer + * example: [1, 2, 3] + * description: 公告ID列表 + * responses: + * 200: + * description: 批量标记已读成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: "批量标记已读成功" + * 400: + * description: 请求参数错误 + * 401: + * description: 未授权 + * 500: + * description: 服务器错误 + */ + +/** + * @swagger + * /api/announcements: + * post: + * summary: 创建新公告 + * tags: [Announcements] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - title + * - content + * properties: + * title: + * type: string + * description: 公告标题 + * content: + * type: string + * description: 公告内容 + * type: + * type: string + * enum: [system, maintenance, promotion, warning] + * default: system + * priority: + * type: string + * enum: [low, medium, high, urgent] + * default: medium + * status: + * type: string + * enum: [draft, published] + * default: draft + * is_pinned: + * type: boolean + * default: false + * publish_time: + * type: string + * format: date-time + * expire_time: + * type: string + * format: date-time + * responses: + * 201: + * description: 公告创建成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * message: + * type: string + * data: + * type: object + * properties: + * id: + * type: integer + * 400: + * description: 请求参数错误 + * 401: + * description: 未授权 + * 500: + * description: 服务器错误 + */ + +/** + * @swagger + * /api/announcements/{id}: + * put: + * summary: 更新公告 + * tags: [Announcements] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: 公告ID + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * title: + * type: string + * content: + * type: string + * type: + * type: string + * enum: [system, maintenance, promotion, warning] + * priority: + * type: string + * enum: [low, medium, high, urgent] + * status: + * type: string + * enum: [draft, published, archived] + * is_pinned: + * type: boolean + * publish_time: + * type: string + * format: date-time + * expire_time: + * type: string + * format: date-time + * responses: + * 200: + * description: 公告更新成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * message: + * type: string + * data: + * $ref: '#/components/schemas/Announcement' + * 400: + * description: 请求参数错误 + * 401: + * description: 未授权 + * 404: + * description: 公告不存在 + * 500: + * description: 服务器错误 + * + * delete: + * summary: 删除公告 + * tags: [Announcements] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: 公告ID + * responses: + * 200: + * description: 公告删除成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * message: + * type: string + * 401: + * description: 未授权 + * 404: + * description: 公告不存在 + * 500: + * description: 服务器错误 + */ + +/** + * @swagger + * /api/announcements/public/list: + * get: + * summary: 获取公开发布的公告列表(无需认证) + * tags: [Announcements] + * parameters: + * - in: query + * name: limit + * schema: + * type: integer + * default: 5 + * description: 获取数量 + * responses: + * 200: + * description: 成功获取公开公告列表 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * data: + * type: array + * items: + * $ref: '#/components/schemas/Announcement' + * 500: + * description: 服务器错误 + */ + +/** + * @swagger + * components: + * schemas: + * Announcement: + * type: object + * required: + * - title + * - content + * properties: + * id: + * type: integer + * description: 公告ID + * title: + * type: string + * description: 公告标题 + * content: + * type: string + * description: 公告内容 + * type: + * type: string + * description: 公告类型 + * enum: [system, maintenance, promotion, warning] + * priority: + * type: string + * description: 优先级 + * enum: [low, medium, high, urgent] + * status: + * type: string + * description: 状态 + * enum: [draft, published, archived] + * is_pinned: + * type: boolean + * description: 是否置顶 + * publish_time: + * type: string + * format: date-time + * description: 发布时间 + * expire_time: + * type: string + * format: date-time + * description: 过期时间 + * created_by: + * type: integer + * description: 创建者ID + * created_at: + * type: string + * format: date-time + * description: 创建时间 + * updated_at: + * type: string + * format: date-time + * description: 更新时间 + */ + +/** + * @swagger + * /api/announcements: + * get: + * summary: 获取通知公告列表 + * tags: [Announcements] + * security: + * - bearerAuth: [] + * parameters: + * - in: query + * name: page + * schema: + * type: integer + * default: 1 + * description: 页码 + * - in: query + * name: limit + * schema: + * type: integer + * default: 10 + * description: 每页数量 + * - in: query + * name: search + * schema: + * type: string + * description: 搜索关键词(标题或内容) + * - in: query + * name: type + * schema: + * type: string + * enum: [system, activity, maintenance, urgent] + * description: 公告类型 + * - in: query + * name: priority + * schema: + * type: string + * enum: [high, medium, low] + * description: 优先级 + * - in: query + * name: status + * schema: + * type: string + * enum: [draft, published, expired] + * description: 状态 + * - in: query + * name: isTop + * schema: + * type: boolean + * description: 是否置顶 + * - in: query + * name: sortBy + * schema: + * type: string + * enum: [created_at, updated_at, publish_time, priority] + * default: created_at + * description: 排序字段 + * - in: query + * name: sortOrder + * schema: + * type: string + * enum: [ASC, DESC] + * default: DESC + * description: 排序方向 + * responses: + * 200: + * description: 成功获取公告列表 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * data: + * type: object + * properties: + * announcements: + * type: array + * items: + * $ref: '#/components/schemas/Announcement' + * total: + * type: integer + * example: 50 + * page: + * type: integer + * example: 1 + * limit: + * type: integer + * example: 10 + * totalPages: + * type: integer + * example: 5 + * 401: + * description: 未授权 + * 500: + * description: 服务器错误 + * + * post: + * summary: 创建新的通知公告 + * tags: [Announcements] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - title + * - content + * - type + * - priority + * properties: + * title: + * type: string + * description: 公告标题 + * example: "系统维护通知" + * content: + * type: string + * description: 公告内容 + * example: "系统将于今晚进行维护,预计维护时间2小时" + * type: + * type: string + * enum: [system, activity, maintenance, urgent] + * description: 公告类型 + * example: "maintenance" + * priority: + * type: string + * enum: [high, medium, low] + * description: 优先级 + * example: "high" + * status: + * type: string + * enum: [draft, published] + * default: draft + * description: 状态 + * isTop: + * type: boolean + * default: false + * description: 是否置顶 + * publishTime: + * type: string + * format: date-time + * description: 发布时间 + * expireTime: + * type: string + * format: date-time + * description: 过期时间 + * responses: + * 201: + * description: 公告创建成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: "公告创建成功" + * data: + * $ref: '#/components/schemas/Announcement' + * 400: + * description: 请求参数错误 + * 401: + * description: 未授权 + * 500: + * description: 服务器错误 + */ + +/** + * @swagger + * /api/announcements/{id}: + * put: + * summary: 更新通知公告 + * tags: [Announcements] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: 公告ID + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * title: + * type: string + * description: 公告标题 + * content: + * type: string + * description: 公告内容 + * type: + * type: string + * enum: [system, activity, maintenance, urgent] + * description: 公告类型 + * priority: + * type: string + * enum: [high, medium, low] + * description: 优先级 + * status: + * type: string + * enum: [draft, published, expired] + * description: 状态 + * isTop: + * type: boolean + * description: 是否置顶 + * publishTime: + * type: string + * format: date-time + * description: 发布时间 + * expireTime: + * type: string + * format: date-time + * description: 过期时间 + * responses: + * 200: + * description: 公告更新成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: "公告更新成功" + * data: + * $ref: '#/components/schemas/Announcement' + * 400: + * description: 请求参数错误 + * 401: + * description: 未授权 + * 404: + * description: 公告不存在 + * 500: + * description: 服务器错误 + * + * delete: + * summary: 删除通知公告 + * tags: [Announcements] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: 公告ID + * responses: + * 200: + * description: 公告删除成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: "公告删除成功" + * 401: + * description: 未授权 + * 404: + * description: 公告不存在 + * 500: + * description: 服务器错误 + */ + +/** + * @swagger + * /api/announcements/{id}/toggle-top: + * put: + * summary: 切换公告置顶状态 + * tags: [Announcements] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: 公告ID + * responses: + * 200: + * description: 置顶状态切换成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: "置顶状态更新成功" + * data: + * type: object + * properties: + * isTop: + * type: boolean + * example: true + * 401: + * description: 未授权 + * 404: + * description: 公告不存在 + * 500: + * description: 服务器错误 + */ \ No newline at end of file diff --git a/docs/apis/captcha.js b/docs/apis/captcha.js new file mode 100644 index 0000000..3a46213 --- /dev/null +++ b/docs/apis/captcha.js @@ -0,0 +1,87 @@ +/** + * @swagger + * tags: + * name: Captcha + * description: 验证码API + */ +/** + * @swagger + * /captcha/generate: + * get: + * summary: 生成图形验证码 + * tags: [Captcha] + * responses: + * 200: + * description: 成功生成验证码 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * data: + * type: object + * properties: + * captchaId: + * type: string + * description: 验证码唯一ID + * image: + * type: string + * description: Base64编码的SVG验证码图片 + * 500: + * description: 服务器错误 + */ +/** + * @swagger + * /captcha/verify: + * post: + * summary: 验证用户输入的验证码 + * tags: [Captcha] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - captchaId + * - captchaText + * properties: + * captchaId: + * type: string + * description: 验证码唯一ID + * captchaText: + * type: string + * description: 用户输入的验证码 + * responses: + * 200: + * description: 验证码验证成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: 验证码验证成功 + * 400: + * description: 验证码错误或已过期 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: 验证码错误 + * 500: + * description: 服务器错误 + */ \ No newline at end of file diff --git a/docs/apis/matching.js b/docs/apis/matching.js new file mode 100644 index 0000000..7e7a72b --- /dev/null +++ b/docs/apis/matching.js @@ -0,0 +1,159 @@ +/** + * @swagger + * tags: + * name: Matching + * description: 匹配订单相关接口 + */ +/** + * @swagger + * /api/matching/my-orders: + * get: + * summary: 获取用户的匹配订单列表 + * tags: [Matching] + * security: + * - bearerAuth: [] + * parameters: + * - in: query + * name: page + * schema: + * type: integer + * default: 1 + * description: 页码 + * - in: query + * name: limit + * schema: + * type: integer + * default: 10 + * description: 每页数量 + * responses: + * 200: + * description: 成功获取匹配订单列表 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * data: + * type: array + * items: + * $ref: '#/components/schemas/MatchingOrder' + * 401: + * description: 未授权 + * 500: + * description: 服务器错误 + */ +/** + * @swagger + * components: + * schemas: + * MatchingOrder: + * type: object + * properties: + * id: + * type: integer + * description: 匹配订单ID + * initiator_id: + * type: integer + * description: 发起人ID + * matching_type: + * type: string + * enum: [small, large] + * description: 匹配类型(小额或大额) + * amount: + * type: number + * description: 匹配总金额 + * status: + * type: string + * enum: [pending, matching, completed, failed] + * description: 订单状态 + * created_at: + * type: string + * format: date-time + * description: 创建时间 + * Allocation: + * type: object + * properties: + * id: + * type: integer + * description: 分配ID + * from_user_id: + * type: integer + * description: 发送方用户ID + * to_user_id: + * type: integer + * description: 接收方用户ID + * amount: + * type: number + * description: 分配金额 + * cycle_number: + * type: integer + * description: 轮次编号 + * status: + * type: string + * enum: [pending, confirmed, rejected, cancelled] + * description: 分配状态 + * created_at: + * type: string + * format: date-time + * description: 创建时间 + */ + +/** + * @swagger + * /api/matching/create: + * post: + * summary: 创建匹配订单 + * tags: [Matching] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * matchingType: + * type: string + * enum: [small, large] + * default: small + * description: 匹配类型(小额或大额) + * customAmount: + * type: number + * description: 大额匹配时的自定义金额(5000-50000之间) + * responses: + * 200: + * description: 匹配订单创建成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * message: + * type: string + * data: + * type: object + * properties: + * matchingOrderId: + * type: integer + * amounts: + * type: array + * items: + * type: number + * matchingType: + * type: string + * totalAmount: + * type: number + * 400: + * description: 参数错误或用户未满足匹配条件 + * 401: + * description: 未授权 + * 404: + * description: 用户不存在 + * 500: + * description: 服务器错误 + */ \ No newline at end of file diff --git a/docs/apis/orders.js b/docs/apis/orders.js new file mode 100644 index 0000000..8342c27 --- /dev/null +++ b/docs/apis/orders.js @@ -0,0 +1,273 @@ +/** + * @swagger + * tags: + * name: Orders + * description: 订单管理API + */ + +/** + * @swagger + * /api/orders: + * get: + * summary: 获取订单列表 + * tags: [Orders] + * security: + * - bearerAuth: [] + * parameters: + * - in: query + * name: page + * schema: + * type: integer + * default: 1 + * description: 页码 + * - in: query + * name: limit + * schema: + * type: integer + * default: 10 + * description: 每页数量 + * - in: query + * name: search + * schema: + * type: string + * description: 搜索关键词 + * - in: query + * name: orderNumber + * schema: + * type: string + * description: 订单号 + * - in: query + * name: username + * schema: + * type: string + * description: 用户名 + * - in: query + * name: status + * schema: + * type: string + * description: 订单状态 + * - in: query + * name: startDate + * schema: + * type: string + * format: date + * description: 开始日期 + * - in: query + * name: endDate + * schema: + * type: string + * format: date + * description: 结束日期 + * responses: + * 200: + * description: 成功获取订单列表 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * data: + * type: object + * properties: + * orders: + * type: array + * items: + * $ref: '#/components/schemas/Order' + * pagination: + * type: object + * properties: + * page: + * type: integer + * limit: + * type: integer + * total: + * type: integer + * pages: + * type: integer + * 401: + * description: 未授权 + * 500: + * description: 服务器错误 + */ + +/** + * @swagger + * /api/orders/confirm: + * post: + * summary: 确认下单 + * tags: [Orders] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - pre_order_id + * - address + * properties: + * pre_order_id: + * type: integer + * description: 预订单ID + * address: + * type: object + * properties: + * recipient_name: + * type: string + * description: 收货人姓名 + * phone: + * type: string + * description: 收货人电话 + * province: + * type: string + * description: 省份 + * city: + * type: string + * description: 城市 + * district: + * type: string + * description: 区县 + * detail_address: + * type: string + * description: 详细地址 + * responses: + * 200: + * description: 确认下单成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * message: + * type: string + * data: + * type: object + * properties: + * order_id: + * type: integer + * order_no: + * type: string + * 400: + * description: 请求参数错误 + * 401: + * description: 未授权 + * 404: + * description: 预订单不存在 + * 500: + * description: 服务器错误 + */ + +/** + * @swagger + * /api/orders/pre-order/{id}: + * get: + * summary: 获取预订单详情 + * tags: [Orders] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: 预订单ID + * responses: + * 200: + * description: 获取预订单详情成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * data: + * type: object + * properties: + * id: + * type: integer + * order_no: + * type: string + * total_amount: + * type: integer + * total_points: + * type: integer + * total_rongdou: + * type: integer + * status: + * type: string + * created_at: + * type: string + * items: + * type: array + * items: + * type: object + * properties: + * id: + * type: integer + * product_id: + * type: integer + * product_name: + * type: string + * quantity: + * type: integer + * price: + * type: integer + * points_price: + * type: integer + * rongdou_price: + * type: integer + * spec_info: + * type: object + * 401: + * description: 未授权 + * 404: + * description: 预订单不存在 + * 500: + * description: 服务器错误 + */ + +/** + * @swagger + * /api/orders/{id}: + * get: + * summary: 获取单个订单详情 + * tags: [Orders] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: 订单ID + * responses: + * 200: + * description: 成功获取订单详情 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * data: + * type: object + * properties: + * order: + * $ref: '#/components/schemas/Order' + * 401: + * description: 未授权 + * 404: + * description: 订单不存在 + * 500: + * description: 服务器错误 + */ \ No newline at end of file diff --git a/docs/apis/products.js b/docs/apis/products.js new file mode 100644 index 0000000..da8520c --- /dev/null +++ b/docs/apis/products.js @@ -0,0 +1,154 @@ +/** + * @swagger + * tags: + * name: Products + * description: 商品管理API + */ + +/** + * @swagger + * /products: + * get: + * summary: 获取商品列表 + * tags: [Products] + * parameters: + * - in: query + * name: page + * schema: + * type: integer + * default: 1 + * description: 页码 + * - in: query + * name: limit + * schema: + * type: integer + * default: 10 + * description: 每页数量 + * - in: query + * name: search + * schema: + * type: string + * description: 搜索关键词 + * - in: query + * name: category + * schema: + * type: string + * description: 商品分类 + * - in: query + * name: status + * schema: + * type: string + * enum: [active, inactive] + * description: 商品状态 + * responses: + * 200: + * description: 成功获取商品列表 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * data: + * type: object + * properties: + * products: + * type: array + * items: + * $ref: '#/components/schemas/Product' + * pagination: + * type: object + * properties: + * page: + * type: integer + * limit: + * type: integer + * total: + * type: integer + * pages: + * type: integer + */ + +/** + * @swagger + * /products/categories: + * get: + * summary: 获取商品分类列表 + * tags: [Products] + * responses: + * 200: + * description: 成功获取分类列表 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * data: + * type: array + * items: + * type: string + */ + +/** + * @swagger + * /products/hot: + * get: + * summary: 获取热门商品 + * tags: [Products] + * parameters: + * - in: query + * name: limit + * schema: + * type: integer + * default: 10 + * description: 返回数量 + * responses: + * 200: + * description: 成功获取热门商品 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * data: + * type: object + * properties: + * products: + * type: array + * items: + * $ref: '#/components/schemas/Product' + */ + +/** + * @swagger + * /products/{id}: + * get: + * summary: 获取商品详情 + * tags: [Products] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: 商品ID + * responses: + * 200: + * description: 成功获取商品详情 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * data: + * $ref: '#/components/schemas/Product' + * 404: + * description: 商品不存在 + */ \ No newline at end of file diff --git a/docs/apis/transfers.js b/docs/apis/transfers.js new file mode 100644 index 0000000..1db9f85 --- /dev/null +++ b/docs/apis/transfers.js @@ -0,0 +1,388 @@ +/** + * @swagger + * components: + * schemas: + * Transfer: + * type: object + * properties: + * id: + * type: integer + * description: 转账记录ID + * user_id: + * type: integer + * description: 用户ID + * recipient_id: + * type: integer + * description: 接收方用户ID + * amount: + * type: number + * format: float + * description: 转账金额 + * status: + * type: string + * enum: [pending, completed, failed, cancelled] + * description: 转账状态 + * transfer_type: + * type: string + * enum: [user_to_user, user_to_system, system_to_user] + * description: 转账类型 + * voucher_image: + * type: string + * description: 转账凭证图片路径 + * remark: + * type: string + * description: 转账备注 + * created_at: + * type: string + * format: date-time + * description: 创建时间 + * updated_at: + * type: string + * format: date-time + * description: 更新时间 + * Pagination: + * type: object + * properties: + * total: + * type: integer + * description: 总记录数 + * page: + * type: integer + * description: 当前页码 + * limit: + * type: integer + * description: 每页记录数 + * total_pages: + * type: integer + * description: 总页数 + */ +/** + * @swagger + * /transfers: + * get: + * summary: 获取转账列表 + * tags: [Transfers] + * security: + * - bearerAuth: [] + * parameters: + * - in: query + * name: status + * schema: + * type: string + * description: 转账状态过滤 + * - in: query + * name: transfer_type + * schema: + * type: string + * description: 转账类型过滤 + * - in: query + * name: start_date + * schema: + * type: string + * format: date + * description: 开始日期过滤 + * - in: query + * name: end_date + * schema: + * type: string + * format: date + * description: 结束日期过滤 + * - in: query + * name: search + * schema: + * type: string + * description: 搜索关键词(用户名或真实姓名) + * - in: query + * name: page + * schema: + * type: integer + * default: 1 + * description: 页码 + * - in: query + * name: limit + * schema: + * type: integer + * default: 10 + * description: 每页数量 + * - in: query + * name: sort + * schema: + * type: string + * description: 排序字段 + * - in: query + * name: order + * schema: + * type: string + * enum: [asc, desc] + * description: 排序方向 + * responses: + * 200: + * description: 成功获取转账列表 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * data: + * type: object + * properties: + * transfers: + * type: array + * items: + * $ref: '#/components/schemas/Transfer' + * pagination: + * $ref: '#/components/schemas/Pagination' + * 401: + * description: 未授权 + * 500: + * description: 服务器错误 + */ +/** + * @swagger + * /transfers/list: + * get: + * summary: 获取转账记录列表 + * tags: [Transfers] + * security: + * - bearerAuth: [] + * parameters: + * - in: query + * name: status + * schema: + * type: string + * description: 转账状态过滤 + * - in: query + * name: transfer_type + * schema: + * type: string + * description: 转账类型过滤 + * - in: query + * name: start_date + * schema: + * type: string + * format: date + * description: 开始日期过滤 + * - in: query + * name: end_date + * schema: + * type: string + * format: date + * description: 结束日期过滤 + * - in: query + * name: page + * schema: + * type: integer + * default: 1 + * description: 页码 + * - in: query + * name: limit + * schema: + * type: integer + * default: 10 + * description: 每页数量 + * - in: query + * name: sort + * schema: + * type: string + * description: 排序字段 + * - in: query + * name: order + * schema: + * type: string + * enum: [asc, desc] + * description: 排序方向 + * responses: + * 200: + * description: 成功获取转账记录列表 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * data: + * type: object + * properties: + * transfers: + * type: array + * items: + * $ref: '#/components/schemas/Transfer' + * pagination: + * $ref: '#/components/schemas/Pagination' + * 401: + * description: 未授权 + * 500: + * description: 服务器错误 + */ +/** + * @swagger + * /transfers/public-account: + * get: + * summary: 获取公户信息 + * tags: [Transfers] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: 成功获取公户信息 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * data: + * type: object + * properties: + * id: + * type: integer + * description: 公户ID + * username: + * type: string + * description: 公户用户名 + * example: public_account + * real_name: + * type: string + * description: 公户名称 + * balance: + * type: number + * format: float + * description: 公户余额 + * 401: + * description: 未授权 + * 404: + * description: 公户不存在 + * 500: + * description: 服务器错误 + */ +/** + * @swagger + * /transfers/create: + * post: + * summary: 创建转账记录 + * tags: [Transfers] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - to_user_id + * - amount + * - transfer_type + * properties: + * to_user_id: + * type: integer + * description: 接收方用户ID + * amount: + * type: number + * format: float + * description: 转账金额 + * transfer_type: + * type: string + * enum: [user_to_user, user_to_system, system_to_user] + * description: 转账类型 + * remark: + * type: string + * description: 转账备注 + * responses: + * 201: + * description: 转账记录创建成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: 转账记录创建成功,等待确认 + * data: + * type: object + * properties: + * transfer_id: + * type: integer + * description: 转账记录ID + * 400: + * description: 请求参数错误 + * 401: + * description: 未授权 + * 500: + * description: 服务器错误 + */ +/** + * @swagger + * /transfers/admin/create: + * post: + * summary: 管理员创建转账记录 + * tags: [Transfers] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - from_user_id + * - to_user_id + * - amount + * - transfer_type + * properties: + * from_user_id: + * type: integer + * description: 发送方用户ID + * to_user_id: + * type: integer + * description: 接收方用户ID + * amount: + * type: number + * format: float + * description: 转账金额 + * transfer_type: + * type: string + * enum: [user_to_user, user_to_system, system_to_user] + * description: 转账类型 + * description: + * type: string + * description: 转账描述 + * responses: + * 201: + * description: 转账记录创建成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: 转账记录创建成功 + * data: + * type: object + * properties: + * transfer_id: + * type: integer + * description: 转账记录ID + * 400: + * description: 请求参数错误 + * 401: + * description: 未授权 + * 403: + * description: 权限不足 + * 500: + * description: 服务器错误 + */ \ No newline at end of file diff --git a/docs/apis/user.js b/docs/apis/user.js new file mode 100644 index 0000000..c437846 --- /dev/null +++ b/docs/apis/user.js @@ -0,0 +1,423 @@ +/** + * @swagger + * tags: + * name: Authentication + * description: 用户认证API + */ +/** + * @swagger + * components: + * schemas: + * LoginCredentials: + * type: object + * required: + * - username + * - password + * properties: + * username: + * type: string + * description: 用户名或手机号 + * password: + * type: string + * description: 密码 + * RegisterRequest: + * type: object + * required: + * - username + * - phone + * - password + * - registrationCode + * - city + * - district_id + * - captchaId + * - captchaText + * - smsCode + * properties: + * username: + * type: string + * description: 用户名 + * phone: + * type: string + * description: 手机号 + * password: + * type: string + * description: 密码 + * registrationCode: + * type: string + * description: 注册激活码 + * city: + * type: string + * description: 城市 + * district_id: + * type: string + * description: 区域ID + * captchaId: + * type: string + * description: 图形验证码ID + * captchaText: + * type: string + * description: 图形验证码文本 + * smsCode: + * type: string + * description: 短信验证码 + * role: + * type: string + * description: 用户角色 + * default: user + */ +/** + * @swagger + * /users/pending-audit: + * get: + * summary: 获取待审核用户列表(管理员权限) + * tags: [Users] + * security: + * - bearerAuth: [] + * parameters: + * - in: query + * name: page + * schema: + * type: integer + * default: 1 + * description: 页码 + * - in: query + * name: limit + * schema: + * type: integer + * default: 10 + * description: 每页数量 + * responses: + * 200: + * description: 成功获取待审核用户列表 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * data: + * type: object + * properties: + * users: + * type: array + * items: + * $ref: '#/components/schemas/User' + * pagination: + * type: object + * properties: + * page: + * type: integer + * limit: + * type: integer + * total: + * type: integer + * pages: + * type: integer + * 401: + * description: 未授权 + * 403: + * description: 权限不足 + * 500: + * description: 服务器错误 + */ +/** + * @swagger + * /auth/register: + * post: + * summary: 用户注册 + * description: 需要提供有效的激活码才能注册 + * tags: [Authentication] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/RegisterRequest' + * responses: + * 201: + * description: 用户注册成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * message: + * type: string + * token: + * type: string + * description: JWT认证令牌 + * user: + * type: object + * properties: + * id: + * type: integer + * username: + * type: string + * role: + * type: string + * 400: + * description: 请求参数错误 + * 500: + * description: 服务器错误 + */ + +/** + * @swagger + * /auth/login: + * post: + * summary: 用户登录 + * tags: [Authentication] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/LoginCredentials' + * responses: + * 200: + * description: 登录成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * message: + * type: string + * token: + * type: string + * description: JWT认证令牌 + * user: + * type: object + * properties: + * id: + * type: integer + * username: + * type: string + * role: + * type: string + * avatar: + * type: string + * points: + * type: integer + * 400: + * description: 请求参数错误 + * 401: + * description: 用户名或密码错误 + * 403: + * description: 账户审核未通过 + * 500: + * description: 服务器错误 + */ +/** + * @swagger + * /api/users/{id}/distribute: + * put: + * summary: 设置用户分发状态 + * description: 更新指定用户的分发状态 + * tags: [Users] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: 用户ID + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - is_distribute + * properties: + * is_distribute: + * type: boolean + * description: 分发状态,true为启用分发,false为禁用分发 + * example: true + * responses: + * 200: + * description: 分发状态更新成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: "分发状态更新成功" + * is_distribute: + * type: boolean + * description: 更新后的分发状态 + * example: true + * 400: + * description: 请求参数错误 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: "分发状态无效" + * 404: + * description: 用户不存在 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: "用户不存在" + * 500: + * description: 服务器内部错误 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: "服务器内部错误" + */ +/** + * @swagger + * components: + * schemas: + * User: + * type: object + * required: + * - username + * - password + * - real_name + * - id_card + * properties: + * id: + * type: integer + * description: 用户ID + * username: + * type: string + * description: 用户名 + * role: + * type: string + * description: 用户角色 + * enum: [user, admin, merchant] + * avatar: + * type: string + * description: 用户头像URL + * points: + * type: integer + * description: 用户积分 + * real_name: + * type: string + * description: 真实姓名 + * id_card: + * type: string + * description: 身份证号 + * phone: + * type: string + * description: 手机号 + * is_system_account: + * type: boolean + * description: 是否为系统账户 + * is_distribute: + * type: boolean + * description: 是否为分发账户 + * created_at: + * type: string + * format: date-time + * description: 创建时间 + * updated_at: + * type: string + * format: date-time + * description: 更新时间 + */ + +/** + * @swagger + * /users: + * post: + * summary: 创建用户(管理员权限) + * tags: [Users] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - username + * - password + * - real_name + * - id_card + * properties: + * username: + * type: string + * password: + * type: string + * role: + * type: string + * enum: [user, admin, merchant] + * default: user + * is_system_account: + * type: boolean + * default: false + * real_name: + * type: string + * id_card: + * type: string + * wechat_qr: + * type: string + * alipay_qr: + * type: string + * bank_card: + * type: string + * unionpay_qr: + * type: string + * phone: + * type: string + * responses: + * 201: + * description: 用户创建成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * message: + * type: string + * user: + * $ref: '#/components/schemas/User' + * 400: + * description: 请求参数错误 + * 401: + * description: 未授权 + * 403: + * description: 权限不足 + * 500: + * description: 服务器错误 + */ \ No newline at end of file diff --git a/docs/schemas/announcement.js b/docs/schemas/announcement.js new file mode 100644 index 0000000..ddb6e92 --- /dev/null +++ b/docs/schemas/announcement.js @@ -0,0 +1,225 @@ +/** + * @swagger + * components: + * schemas: + * Announcement: + * type: object + * required: + * - title + * - content + * - type + * - priority + * properties: + * id: + * type: integer + * description: 公告ID + * example: 1 + * title: + * type: string + * description: 公告标题 + * example: "系统维护通知" + * content: + * type: string + * description: 公告内容 + * example: "系统将于今晚进行维护,预计维护时间2小时,期间可能影响部分功能使用。" + * type: + * type: string + * description: 公告类型 + * enum: [system, activity, maintenance, urgent] + * example: "maintenance" + * priority: + * type: string + * description: 优先级 + * enum: [high, medium, low] + * example: "high" + * status: + * type: string + * description: 公告状态 + * enum: [draft, published, expired] + * example: "published" + * isTop: + * type: boolean + * description: 是否置顶 + * example: false + * publishTime: + * type: string + * format: date-time + * description: 发布时间 + * example: "2024-01-15T10:00:00Z" + * expireTime: + * type: string + * format: date-time + * description: 过期时间 + * example: "2024-01-20T10:00:00Z" + * createdBy: + * type: integer + * description: 创建者用户ID + * example: 1 + * createdAt: + * type: string + * format: date-time + * description: 创建时间 + * example: "2024-01-15T09:00:00Z" + * updatedAt: + * type: string + * format: date-time + * description: 更新时间 + * example: "2024-01-15T09:30:00Z" + * creator: + * type: object + * description: 创建者信息 + * properties: + * id: + * type: integer + * example: 1 + * username: + * type: string + * example: "admin" + * email: + * type: string + * example: "admin@example.com" + * + * AnnouncementCreate: + * type: object + * required: + * - title + * - content + * - type + * - priority + * properties: + * title: + * type: string + * description: 公告标题 + * example: "系统维护通知" + * content: + * type: string + * description: 公告内容 + * example: "系统将于今晚进行维护,预计维护时间2小时。" + * type: + * type: string + * description: 公告类型 + * enum: [system, activity, maintenance, urgent] + * example: "maintenance" + * priority: + * type: string + * description: 优先级 + * enum: [high, medium, low] + * example: "high" + * status: + * type: string + * description: 公告状态 + * enum: [draft, published] + * default: draft + * example: "draft" + * isTop: + * type: boolean + * description: 是否置顶 + * default: false + * example: false + * publishTime: + * type: string + * format: date-time + * description: 发布时间 + * example: "2024-01-15T10:00:00Z" + * expireTime: + * type: string + * format: date-time + * description: 过期时间 + * example: "2024-01-20T10:00:00Z" + * + * AnnouncementUpdate: + * type: object + * properties: + * title: + * type: string + * description: 公告标题 + * example: "系统维护通知(更新)" + * content: + * type: string + * description: 公告内容 + * example: "系统维护时间调整为明晚进行。" + * type: + * type: string + * description: 公告类型 + * enum: [system, activity, maintenance, urgent] + * example: "maintenance" + * priority: + * type: string + * description: 优先级 + * enum: [high, medium, low] + * example: "medium" + * status: + * type: string + * description: 公告状态 + * enum: [draft, published, expired] + * example: "published" + * isTop: + * type: boolean + * description: 是否置顶 + * example: true + * publishTime: + * type: string + * format: date-time + * description: 发布时间 + * example: "2024-01-16T10:00:00Z" + * expireTime: + * type: string + * format: date-time + * description: 过期时间 + * example: "2024-01-21T10:00:00Z" + * + * AnnouncementList: + * type: object + * properties: + * success: + * type: boolean + * example: true + * data: + * type: object + * properties: + * announcements: + * type: array + * items: + * $ref: '#/components/schemas/Announcement' + * total: + * type: integer + * description: 总记录数 + * example: 50 + * page: + * type: integer + * description: 当前页码 + * example: 1 + * limit: + * type: integer + * description: 每页数量 + * example: 10 + * totalPages: + * type: integer + * description: 总页数 + * example: 5 + * + * AnnouncementResponse: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: "操作成功" + * data: + * $ref: '#/components/schemas/Announcement' + * + * AnnouncementError: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: "操作失败" + * error: + * type: string + * example: "公告不存在" + */ \ No newline at end of file diff --git a/docs/schemas/cart.js b/docs/schemas/cart.js new file mode 100644 index 0000000..71ffbe9 --- /dev/null +++ b/docs/schemas/cart.js @@ -0,0 +1,93 @@ +/** + * @swagger + * components: + * schemas: + * CartItem: + * type: object + * required: + * - user_id + * - product_id + * - quantity + * properties: + * id: + * type: integer + * description: 购物车商品ID + * user_id: + * type: integer + * description: 用户ID + * product_id: + * type: integer + * description: 商品ID + * quantity: + * type: integer + * description: 商品数量 + * created_at: + * type: string + * format: date-time + * description: 创建时间 + * updated_at: + * type: string + * format: date-time + * description: 更新时间 + * + * CartItemWithProduct: + * type: object + * properties: + * id: + * type: integer + * description: 购物车商品ID + * product_id: + * type: integer + * description: 商品ID + * product_name: + * type: string + * description: 商品名称 + * quantity: + * type: integer + * description: 商品数量 + * points_price: + * type: integer + * description: 积分价格 + * rongdou_price: + * type: number + * description: 融豆价格 + * image_url: + * type: string + * description: 商品图片URL + * stock: + * type: integer + * description: 库存数量 + * payment_methods: + * type: array + * items: + * type: string + * description: 支付方式列表 + * created_at: + * type: string + * format: date-time + * description: 创建时间 + * + * AddToCartRequest: + * type: object + * required: + * - product_id + * - quantity + * properties: + * product_id: + * type: integer + * description: 商品ID + * quantity: + * type: integer + * minimum: 1 + * description: 商品数量 + * + * UpdateCartRequest: + * type: object + * required: + * - quantity + * properties: + * quantity: + * type: integer + * minimum: 1 + * description: 商品数量 + */ \ No newline at end of file diff --git a/docs/schemas/order.js b/docs/schemas/order.js new file mode 100644 index 0000000..9151207 --- /dev/null +++ b/docs/schemas/order.js @@ -0,0 +1,102 @@ +/** + * @swagger + * components: + * schemas: + * Order: + * type: object + * required: + * - user_id + * - total_amount + * - status + * properties: + * id: + * type: integer + * description: 订单ID + * order_number: + * type: string + * description: 订单号 + * user_id: + * type: integer + * description: 用户ID + * total_amount: + * type: number + * description: 订单总金额 + * total_points: + * type: integer + * description: 订单总积分 + * total_rongdou: + * type: number + * description: 订单总融豆 + * status: + * type: string + * description: 订单状态 + * enum: [pending, confirmed, shipped, delivered, cancelled] + * payment_status: + * type: string + * description: 支付状态 + * enum: [pending, paid, failed, refunded] + * shipping_address: + * type: string + * description: 收货地址 + * created_at: + * type: string + * format: date-time + * description: 创建时间 + * updated_at: + * type: string + * format: date-time + * description: 更新时间 + * + * OrderItem: + * type: object + * properties: + * id: + * type: integer + * description: 订单商品ID + * order_id: + * type: integer + * description: 订单ID + * product_id: + * type: integer + * description: 商品ID + * quantity: + * type: integer + * description: 商品数量 + * price: + * type: number + * description: 商品价格 + * points_price: + * type: integer + * description: 积分价格 + * rongdou_price: + * type: number + * description: 融豆价格 + * created_at: + * type: string + * format: date-time + * description: 创建时间 + * + * PreOrder: + * type: object + * properties: + * preOrderId: + * type: integer + * description: 预订单ID + * orderNumber: + * type: string + * description: 订单号 + * totalAmount: + * type: number + * description: 总金额 + * totalPoints: + * type: integer + * description: 所需积分总数 + * totalRongdou: + * type: number + * description: 所需融豆总数 + * paymentMethods: + * type: array + * items: + * type: string + * description: 去重后的支付方式列表 + */ \ No newline at end of file diff --git a/docs/schemas/product.js b/docs/schemas/product.js new file mode 100644 index 0000000..314372d --- /dev/null +++ b/docs/schemas/product.js @@ -0,0 +1,53 @@ +/** + * @swagger + * components: + * schemas: + * Product: + * type: object + * required: + * - name + * - points_price + * - stock + * properties: + * id: + * type: integer + * description: 商品ID + * name: + * type: string + * description: 商品名称 + * category: + * type: string + * description: 商品分类 + * points_price: + * type: integer + * description: 积分价格 + * rongdou_price: + * type: number + * description: 融豆价格 + * stock: + * type: integer + * description: 库存数量 + * image_url: + * type: string + * description: 商品图片URL + * description: + * type: string + * description: 商品描述 + * status: + * type: string + * description: 商品状态 + * enum: [active, inactive] + * payment_methods: + * type: array + * items: + * type: string + * description: 支付方式列表 + * created_at: + * type: string + * format: date-time + * description: 创建时间 + * updated_at: + * type: string + * format: date-time + * description: 更新时间 + */ \ No newline at end of file diff --git a/docs/schemas/user.js b/docs/schemas/user.js new file mode 100644 index 0000000..08288a1 --- /dev/null +++ b/docs/schemas/user.js @@ -0,0 +1,104 @@ +/** + * @swagger + * components: + * schemas: + * User: + * type: object + * required: + * - username + * - email + * properties: + * id: + * type: integer + * description: 用户ID + * username: + * type: string + * description: 用户名 + * email: + * type: string + * format: email + * description: 邮箱地址 + * phone: + * type: string + * description: 手机号码 + * points: + * type: integer + * description: 积分余额 + * rongdou: + * type: number + * description: 融豆余额 + * avatar: + * type: string + * description: 头像URL + * status: + * type: string + * description: 用户状态 + * enum: [active, inactive, banned] + * created_at: + * type: string + * format: date-time + * description: 创建时间 + * updated_at: + * type: string + * format: date-time + * description: 更新时间 + * + * UserProfile: + * type: object + * properties: + * id: + * type: integer + * description: 用户ID + * username: + * type: string + * description: 用户名 + * email: + * type: string + * description: 邮箱地址 + * phone: + * type: string + * description: 手机号码 + * points: + * type: integer + * description: 积分余额 + * rongdou: + * type: number + * description: 融豆余额 + * avatar: + * type: string + * description: 头像URL + * + * LoginRequest: + * type: object + * required: + * - username + * - password + * properties: + * username: + * type: string + * description: 用户名或邮箱 + * password: + * type: string + * description: 密码 + * + * RegisterRequest: + * type: object + * required: + * - username + * - email + * - password + * properties: + * username: + * type: string + * description: 用户名 + * email: + * type: string + * format: email + * description: 邮箱地址 + * password: + * type: string + * description: 密码 + * phone: + * type: string + * description: 手机号码 + */ \ No newline at end of file diff --git a/export-swagger.js b/export-swagger.js new file mode 100644 index 0000000..e086c50 --- /dev/null +++ b/export-swagger.js @@ -0,0 +1,25 @@ +/** + * 导出Swagger文档到JSON文件,用于导入Apifox + */ +const fs = require('fs'); +const path = require('path'); +const swaggerSpecs = require('./swagger'); + +// 确保输出目录存在 +const outputDir = path.join(__dirname, 'api-docs'); +if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }); +} + +// 导出Swagger文档到JSON文件 +const outputPath = path.join(outputDir, 'swagger.json'); +fs.writeFileSync(outputPath, JSON.stringify(swaggerSpecs, null, 2)); + +console.log(`Swagger文档已导出到: ${outputPath}`); +console.log('现在您可以将此文件导入到Apifox中:'); +console.log('1. 打开Apifox应用'); +console.log('2. 选择您的项目'); +console.log('3. 点击"导入"按钮'); +console.log('4. 选择"导入OpenAPI(Swagger)"'); +console.log('5. 选择刚才导出的swagger.json文件'); +console.log('6. 按照Apifox的导入向导完成导入'); \ No newline at end of file diff --git a/middleware/auth.js b/middleware/auth.js new file mode 100644 index 0000000..ddb8180 --- /dev/null +++ b/middleware/auth.js @@ -0,0 +1,110 @@ +const jwt = require('jsonwebtoken'); +const { getDB } = require('../database'); + +const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key'; // 在生产环境中应该使用环境变量 + +/** + * 用户认证中间件 + * 验证JWT令牌并检查用户状态(包括是否被拉黑) + */ +const auth = async (req, res, next) => { + try { + const token = req.header('Authorization')?.replace('Bearer ', ''); + + if (!token) { + return res.status(401).json({ success: false, message: '未提供认证令牌' }); + } + + const decoded = jwt.verify(token, JWT_SECRET); + const db = getDB(); + const [users] = await db.execute('SELECT * FROM users WHERE id = ?', [decoded.userId]); + + if (users.length === 0) { + return res.status(401).json({ success: false, message: '用户不存在' }); + } + + const user = users[0]; + + // 检查用户是否被拉黑 + if (user.is_blacklisted) { + return res.status(403).json({ + success: false, + message: '账户已被拉黑,请联系管理员', + code: 'USER_BLACKLISTED' + }); + } + + // 检查支付状态(管理员除外) + if (user.role !== 'admin' && user.payment_status === 'unpaid') { + return res.status(403).json({ + success: false, + message: '您的账户尚未激活,请完成支付后再使用', + code: 'PAYMENT_REQUIRED', + needPayment: true, + userId: user.id + }); + } + + req.user = user; + next(); + } catch (error) { + res.status(401).json({ success: false, message: '无效的认证令牌' }); + } +}; + +// 管理员认证中间件 +const adminAuth = (req, res, next) => { + if (req.user.role !== 'admin') { + return res.status(403).json({ success: false, message: '需要管理员权限' }); + } + next(); +}; + +/** + * 支付认证中间件 + * 只验证JWT令牌和用户状态,不检查支付状态 + * 用于支付相关接口,允许未支付用户创建支付订单 + */ +const paymentAuth = async (req, res, next) => { + try { + const token = req.header('Authorization')?.replace('Bearer ', ''); + + if (!token) { + return res.status(401).json({ success: false, message: '未提供认证令牌' }); + } + + const decoded = jwt.verify(token, JWT_SECRET); + const db = getDB(); + const [users] = await db.execute('SELECT * FROM users WHERE id = ?', [decoded.userId]); + + if (users.length === 0) { + return res.status(401).json({ success: false, message: '用户不存在' }); + } + + const user = users[0]; + + // 检查用户是否被拉黑 + if (user.is_blacklisted) { + return res.status(403).json({ + success: false, + message: '账户已被拉黑,请联系管理员', + code: 'USER_BLACKLISTED' + }); + } + + // 注意:这里不检查支付状态,允许未支付用户创建支付订单 + req.user = user; + next(); + } catch (error) { + console.error('支付认证失败:', error); + if (error.name === 'JsonWebTokenError') { + return res.status(401).json({ success: false, message: '无效的认证令牌' }); + } + if (error.name === 'TokenExpiredError') { + return res.status(401).json({ success: false, message: '认证令牌已过期' }); + } + return res.status(500).json({ success: false, message: '认证失败' }); + } +}; + +module.exports = { auth, adminAuth, paymentAuth, JWT_SECRET }; \ No newline at end of file diff --git a/middleware/errorHandler.js b/middleware/errorHandler.js new file mode 100644 index 0000000..eb26f22 --- /dev/null +++ b/middleware/errorHandler.js @@ -0,0 +1,129 @@ +const { logger } = require('../config/logger'); +const { ERROR_CODES, HTTP_STATUS } = require('../config/constants'); + +// 全局错误处理中间件 +const errorHandler = (err, req, res, next) => { + let error = { ...err }; + error.message = err.message; + + // 记录错误日志 + logger.error('Error occurred:', { + message: err.message, + stack: err.stack, + url: req.originalUrl, + method: req.method, + ip: req.ip, + userAgent: req.get('User-Agent'), + userId: req.user?.id + }); + + // MySQL错误处理 + if (err.code) { + switch (err.code) { + case 'ER_DUP_ENTRY': + error.message = '数据已存在'; + error.statusCode = HTTP_STATUS.CONFLICT; + error.errorCode = ERROR_CODES.DUPLICATE_ENTRY; + break; + case 'ER_NO_REFERENCED_ROW_2': + error.message = '关联数据不存在'; + error.statusCode = HTTP_STATUS.BAD_REQUEST; + error.errorCode = ERROR_CODES.VALIDATION_ERROR; + break; + case 'ER_ROW_IS_REFERENCED_2': + error.message = '数据正在被使用,无法删除'; + error.statusCode = HTTP_STATUS.CONFLICT; + error.errorCode = ERROR_CODES.VALIDATION_ERROR; + break; + case 'ECONNREFUSED': + error.message = '数据库连接失败'; + error.statusCode = HTTP_STATUS.INTERNAL_SERVER_ERROR; + error.errorCode = ERROR_CODES.DATABASE_ERROR; + break; + default: + error.message = '数据库操作失败'; + error.statusCode = HTTP_STATUS.INTERNAL_SERVER_ERROR; + error.errorCode = ERROR_CODES.DATABASE_ERROR; + } + } + + // JWT错误处理 + if (err.name === 'JsonWebTokenError') { + error.message = '无效的访问令牌'; + error.statusCode = HTTP_STATUS.UNAUTHORIZED; + error.errorCode = ERROR_CODES.AUTHENTICATION_ERROR; + } + + if (err.name === 'TokenExpiredError') { + error.message = '访问令牌已过期'; + error.statusCode = HTTP_STATUS.UNAUTHORIZED; + error.errorCode = ERROR_CODES.AUTHENTICATION_ERROR; + } + + // 参数验证错误 + if (err.name === 'ValidationError' || err.isJoi) { + const message = err.details ? err.details.map(detail => detail.message).join(', ') : err.message; + error.message = `参数验证失败: ${message}`; + error.statusCode = HTTP_STATUS.BAD_REQUEST; + error.errorCode = ERROR_CODES.VALIDATION_ERROR; + } + + // 业务逻辑错误处理 + if (err.message === '余额不足') { + error.message = '用户积分余额不足,无法完成转账操作。请先为用户充值积分或选择其他用户。'; + error.statusCode = HTTP_STATUS.BAD_REQUEST; + error.errorCode = ERROR_CODES.VALIDATION_ERROR; + } + + if (err.message === '用户不存在') { + error.message = '指定的用户不存在,请检查用户信息后重试。'; + error.statusCode = HTTP_STATUS.BAD_REQUEST; + error.errorCode = ERROR_CODES.VALIDATION_ERROR; + } + + // 自定义错误 + if (err.statusCode) { + error.statusCode = err.statusCode; + error.errorCode = err.errorCode || ERROR_CODES.INTERNAL_ERROR; + } + + // 默认错误 + const statusCode = error.statusCode || HTTP_STATUS.INTERNAL_SERVER_ERROR; + const errorCode = error.errorCode || ERROR_CODES.INTERNAL_ERROR; + const message = error.message || '服务器内部错误'; + + res.status(statusCode).json({ + success: false, + error: { + code: errorCode, + message: message + }, + ...(process.env.NODE_ENV === 'development' && { stack: err.stack }) + }); +}; + +// 404错误处理 +const notFound = (req, res, next) => { + const error = new Error(`路径 ${req.originalUrl} 未找到`); + error.statusCode = HTTP_STATUS.NOT_FOUND; + error.errorCode = ERROR_CODES.NOT_FOUND; + next(error); +}; + +// 自定义错误类 +class AppError extends Error { + constructor(message, statusCode, errorCode) { + super(message); + this.statusCode = statusCode; + this.errorCode = errorCode; + this.isOperational = true; + + Error.captureStackTrace(this, this.constructor); + } +} + +module.exports = { + errorHandler, + notFound, + AppError +}; \ No newline at end of file diff --git a/middleware/validation.js b/middleware/validation.js new file mode 100644 index 0000000..41e97f1 --- /dev/null +++ b/middleware/validation.js @@ -0,0 +1,230 @@ +const Joi = require('joi'); +const { AppError } = require('./errorHandler'); +const { ERROR_CODES, HTTP_STATUS } = require('../config/constants'); + +// 验证中间件工厂函数 +const validate = (schema) => { + return (req, res, next) => { + const { error } = schema.validate(req.body, { abortEarly: false }); + if (error) { + const errorMessage = error.details.map(detail => detail.message).join(', '); + return next(new AppError(errorMessage, HTTP_STATUS.BAD_REQUEST, ERROR_CODES.VALIDATION_ERROR)); + } + next(); + }; +}; + +// 查询参数验证中间件 +const validateQuery = (schema) => { + return (req, res, next) => { + const { error } = schema.validate(req.query, { abortEarly: false }); + if (error) { + const errorMessage = error.details.map(detail => detail.message).join(', '); + return next(new AppError(errorMessage, HTTP_STATUS.BAD_REQUEST, ERROR_CODES.VALIDATION_ERROR)); + } + next(); + }; +}; + +// 路径参数验证中间件 +const validateParams = (schema) => { + return (req, res, next) => { + const { error } = schema.validate(req.params, { abortEarly: false }); + if (error) { + const errorMessage = error.details.map(detail => detail.message).join(', '); + return next(new AppError(errorMessage, HTTP_STATUS.BAD_REQUEST, ERROR_CODES.VALIDATION_ERROR)); + } + next(); + }; +}; + +// 通用验证规则 +const commonSchemas = { + // ID验证 + id: Joi.number().integer().positive().required().messages({ + 'number.base': 'ID必须是数字', + 'number.integer': 'ID必须是整数', + 'number.positive': 'ID必须是正数', + 'any.required': 'ID是必需的' + }), + + // 分页验证 + pagination: Joi.object({ + page: Joi.number().integer().min(1).default(1).messages({ + 'number.base': '页码必须是数字', + 'number.integer': '页码必须是整数', + 'number.min': '页码必须大于0' + }), + limit: Joi.number().integer().min(1).max(100).default(10).messages({ + 'number.base': '每页数量必须是数字', + 'number.integer': '每页数量必须是整数', + 'number.min': '每页数量必须大于0', + 'number.max': '每页数量不能超过100' + }) + }) +}; + +// 用户相关验证规则 +const userSchemas = { + // 用户注册 + register: Joi.object({ + username: Joi.string().alphanum().min(3).max(30).required().messages({ + 'string.base': '用户名必须是字符串', + 'string.alphanum': '用户名只能包含字母和数字', + 'string.min': '用户名至少3个字符', + 'string.max': '用户名最多30个字符', + 'any.required': '用户名是必需的' + }), + password: Joi.string().min(6).max(128).required().messages({ + 'string.base': '密码必须是字符串', + 'string.min': '密码至少6个字符', + 'string.max': '密码最多128个字符', + 'any.required': '密码是必需的' + }), + phone: Joi.string().pattern(/^1[3-9]\d{9}$/).required().messages({ + 'string.pattern.base': '手机号格式不正确', + 'any.required': '手机号是必需的' + }), + // 可选字段,注册时不需要填写 + real_name: Joi.string().max(50).allow('').optional().messages({ + 'string.max': '真实姓名最多50个字符' + }), + role: Joi.string().valid('admin', 'user').default('user').messages({ + 'any.only': '角色只能是admin或user' + }) + }), + + // 用户登录 + login: Joi.object({ + username: Joi.string().required().messages({ + 'any.required': '用户名是必需的' + }), + password: Joi.string().required().messages({ + 'any.required': '密码是必需的' + }) + }) +}; + +// 转账相关验证规则 +const transferSchemas = { + // 转账查询参数 + query: Joi.object({ + page: Joi.number().integer().min(1).default(1).messages({ + 'number.base': '页码必须是数字', + 'number.integer': '页码必须是整数', + 'number.min': '页码必须大于0' + }), + limit: Joi.number().integer().min(1).max(100).default(10).messages({ + 'number.base': '每页数量必须是数字', + 'number.integer': '每页数量必须是整数', + 'number.min': '每页数量必须大于0', + 'number.max': '每页数量不能超过100' + }), + status: Joi.string().valid('pending', 'confirmed', 'rejected', 'cancelled').allow('').messages({ + 'any.only': '状态值无效' + }), + type: Joi.string().valid('user_to_user', 'system_to_user', 'user_to_system').allow('').messages({ + 'any.only': '转账类型无效' + }), + search: Joi.string().allow('').max(100).messages({ + 'string.max': '搜索关键词最多100个字符' + }), + transfer_type: Joi.string().valid('user_to_user', 'system_to_user', 'user_to_system').allow('').messages({ + 'any.only': '转账类型无效' + }), + start_date: Joi.date().iso().allow('').messages({ + 'date.format': '开始日期格式不正确' + }), + end_date: Joi.date().iso().allow('').messages({ + 'date.format': '结束日期格式不正确' + }), + sort: Joi.string().valid('id', 'amount', 'created_at', 'updated_at', 'status').allow('').messages({ + 'any.only': '排序字段无效,只支持: id, amount, created_at, updated_at, status' + }), + order: Joi.string().valid('asc', 'desc').allow('').messages({ + 'any.only': '排序方向无效,只支持: asc, desc' + }), + // 优先显示待处理转账参数 + show_pending: Joi.alternatives().try( + Joi.boolean(), + Joi.string().valid('true', 'false', '') + ).allow('').messages({ + 'alternatives.match': 'show_pending参数只能是布尔值或字符串true/false' + }) + }), + + // 创建转账 + create: Joi.object({ + to_user_id: Joi.number().integer().positive().required().messages({ + 'number.base': '收款用户ID必须是数字', + 'number.integer': '收款用户ID必须是整数', + 'number.positive': '收款用户ID必须是正数', + 'any.required': '收款用户ID是必需的' + }), + amount: Joi.number().positive().precision(2).required().messages({ + 'number.base': '金额必须是数字', + 'number.positive': '金额必须是正数', + 'any.required': '金额是必需的' + }), + transfer_type: Joi.string().valid('user_to_user', 'system_to_user', 'user_to_system').required().messages({ + 'any.only': '转账类型无效', + 'any.required': '转账类型是必需的' + }), + description: Joi.string().max(500).allow('').messages({ + 'string.max': '描述最多500个字符' + }), + voucher_url: Joi.string().uri().allow('').messages({ + 'string.uri': '凭证URL格式不正确' + }) + }), + + // 确认转账 + confirm: Joi.object({ + transfer_id: Joi.number().integer().positive().required().messages({ + 'number.base': '转账ID必须是数字', + 'number.integer': '转账ID必须是整数', + 'number.positive': '转账ID必须是正数', + 'any.required': '转账ID是必需的' + }), + note: Joi.string().max(500).allow('').messages({ + 'string.max': '备注最多500个字符' + }) + }), + + // 拒绝转账 + reject: Joi.object({ + transfer_id: Joi.number().integer().positive().required().messages({ + 'number.base': '转账ID必须是数字', + 'number.integer': '转账ID必须是整数', + 'number.positive': '转账ID必须是正数', + 'any.required': '转账ID是必需的' + }), + note: Joi.string().max(500).allow('').messages({ + 'string.max': '备注最多500个字符' + }) + }) +}; +// 系统设置相关验证规则 +const systemSchemas = { + updateSettings: Joi.object({ + site_name: Joi.string().max(100).optional(), + site_description: Joi.string().max(500).optional(), + + contact_phone: Joi.string().max(20).optional(), + maintenance_mode: Joi.boolean().optional(), + max_transfer_amount: Joi.number().positive().optional(), + min_transfer_amount: Joi.number().positive().optional(), + transfer_fee_rate: Joi.number().min(0).max(1).optional() + }) +}; + +// 导出所有验证规则 +module.exports = { + validate, + validateQuery, + validateParams, + commonSchemas, + userSchemas, + transferSchemas, + systemSchemas +}; \ No newline at end of file diff --git a/migrations/add_alipay_support.sql b/migrations/add_alipay_support.sql new file mode 100644 index 0000000..36d716d --- /dev/null +++ b/migrations/add_alipay_support.sql @@ -0,0 +1,17 @@ +-- 为支付订单表添加支付宝支持 +-- 更新交易类型枚举,添加支付宝相关类型 +ALTER TABLE `payment_orders` MODIFY COLUMN `trade_type` varchar(32) NOT NULL COMMENT '交易类型(H5/JSAPI/ALIPAY_WAP/ALIPAY_APP等)'; + +-- 更新transaction_id字段注释,支持多种支付方式 +ALTER TABLE `payment_orders` MODIFY COLUMN `transaction_id` varchar(64) DEFAULT NULL COMMENT '第三方支付订单号(微信/支付宝)'; + +-- 添加支付方式字段 +ALTER TABLE `payment_orders` ADD COLUMN `payment_method` enum('wechat','alipay') DEFAULT 'wechat' COMMENT '支付方式' AFTER `trade_type`; + +-- 添加支付宝特有字段 +ALTER TABLE `payment_orders` ADD COLUMN `buyer_user_id` varchar(32) DEFAULT NULL COMMENT '支付宝买家用户ID' AFTER `transaction_id`; +ALTER TABLE `payment_orders` ADD COLUMN `trade_status` varchar(32) DEFAULT NULL COMMENT '支付宝交易状态' AFTER `buyer_user_id`; + +-- 添加索引 +ALTER TABLE `payment_orders` ADD KEY `idx_payment_method` (`payment_method`); +ALTER TABLE `payment_orders` ADD KEY `idx_trade_status` (`trade_status`); \ No newline at end of file diff --git a/migrations/create_payment_orders_table.sql b/migrations/create_payment_orders_table.sql new file mode 100644 index 0000000..fb4ea53 --- /dev/null +++ b/migrations/create_payment_orders_table.sql @@ -0,0 +1,27 @@ +-- 创建支付订单表 +CREATE TABLE IF NOT EXISTS `payment_orders` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '订单ID', + `user_id` int(11) NOT NULL COMMENT '用户ID', + `out_trade_no` varchar(64) NOT NULL COMMENT '商户订单号', + `transaction_id` varchar(64) DEFAULT NULL COMMENT '微信支付订单号', + `total_fee` int(11) NOT NULL COMMENT '订单金额(分)', + `body` varchar(128) NOT NULL COMMENT '商品描述', + `trade_type` varchar(16) NOT NULL COMMENT '交易类型', + `prepay_id` varchar(64) DEFAULT NULL COMMENT '预支付交易会话标识', + `mweb_url` text DEFAULT NULL COMMENT 'H5支付跳转链接', + `status` enum('pending','paid','failed','cancelled') NOT NULL DEFAULT 'pending' COMMENT '支付状态', + `paid_at` datetime DEFAULT NULL COMMENT '支付完成时间', + `created_at` datetime NOT NULL COMMENT '创建时间', + `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_out_trade_no` (`out_trade_no`), + KEY `idx_user_id` (`user_id`), + KEY `idx_status` (`status`), + KEY `idx_created_at` (`created_at`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='支付订单表'; + +-- 为用户表添加支付状态字段 +ALTER TABLE `users` ADD COLUMN `payment_status` enum('unpaid','paid') NOT NULL DEFAULT 'unpaid' COMMENT '支付状态' AFTER `status`; + +-- 添加索引 +ALTER TABLE `users` ADD KEY `idx_payment_status` (`payment_status`); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..5b7f5c8 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,4153 @@ +{ + "name": "integrated-system", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "integrated-system", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@alicloud/dysmsapi20170525": "^4.1.2", + "@alicloud/openapi-client": "^0.4.15", + "alipay-sdk": "^4.14.0", + "axios": "^1.11.0", + "bcryptjs": "^2.4.3", + "body-parser": "^1.20.2", + "cors": "^2.8.5", + "crypto": "^1.0.1", + "dayjs": "^1.11.13", + "dotenv": "^17.2.1", + "express": "^4.18.2", + "express-rate-limit": "^7.1.5", + "express-validator": "^7.2.1", + "helmet": "^8.1.0", + "joi": "^17.13.3", + "jsonwebtoken": "^9.0.2", + "minio": "^8.0.5", + "multer": "^1.4.5-lts.1", + "mysql2": "^3.14.3", + "node-cron": "^4.2.1", + "node-rsa": "^1.1.1", + "qrcode": "^1.5.4", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.1", + "winston": "^3.17.0", + "xml2js": "^0.6.2" + }, + "devDependencies": { + "concurrently": "^8.2.2", + "nodemon": "^3.0.2" + } + }, + "node_modules/@alicloud/credentials": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@alicloud/credentials/-/credentials-2.4.4.tgz", + "integrity": "sha512-/eRAGSKcniLIFQ1UCpDhB/IrHUZisQ1sc65ws/c2avxUMpXwH1rWAohb76SVAUJhiF4mwvLzLJM1Mn1XL4Xe/Q==", + "license": "MIT", + "dependencies": { + "@alicloud/tea-typescript": "^1.8.0", + "httpx": "^2.3.3", + "ini": "^1.3.5", + "kitx": "^2.0.0" + } + }, + "node_modules/@alicloud/darabonba-array": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@alicloud/darabonba-array/-/darabonba-array-0.1.1.tgz", + "integrity": "sha512-UPP7p9//jywqM8EN6BjSbw1ovl/BzqreXdi5FmxT6m3PmFxsxabe+yamjeopyf2Gi0p3WqwJTBCeNji5eYUsJw==", + "license": "ISC", + "dependencies": { + "@alicloud/tea-typescript": "^1.7.1" + } + }, + "node_modules/@alicloud/darabonba-encode-util": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@alicloud/darabonba-encode-util/-/darabonba-encode-util-0.0.2.tgz", + "integrity": "sha512-mlsNctkeqmR0RtgE1Rngyeadi5snLOAHBCWEtYf68d7tyKskosXDTNeZ6VCD/UfrUu4N51ItO8zlpfXiOgeg3A==", + "license": "ISC", + "dependencies": { + "moment": "^2.29.1" + } + }, + "node_modules/@alicloud/darabonba-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@alicloud/darabonba-map/-/darabonba-map-0.0.1.tgz", + "integrity": "sha512-2ep+G3YDvuI+dRYVlmER1LVUQDhf9kEItmVB/bbEu1pgKzelcocCwAc79XZQjTcQGFgjDycf3vH87WLDGLFMlw==", + "license": "ISC", + "dependencies": { + "@alicloud/tea-typescript": "^1.7.1" + } + }, + "node_modules/@alicloud/darabonba-signature-util": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@alicloud/darabonba-signature-util/-/darabonba-signature-util-0.0.4.tgz", + "integrity": "sha512-I1TtwtAnzLamgqnAaOkN0IGjwkiti//0a7/auyVThdqiC/3kyafSAn6znysWOmzub4mrzac2WiqblZKFcN5NWg==", + "license": "ISC", + "dependencies": { + "@alicloud/darabonba-encode-util": "^0.0.1" + } + }, + "node_modules/@alicloud/darabonba-signature-util/node_modules/@alicloud/darabonba-encode-util": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@alicloud/darabonba-encode-util/-/darabonba-encode-util-0.0.1.tgz", + "integrity": "sha512-Sl5vCRVAYMqwmvXpJLM9hYoCHOMsQlGxaWSGhGWulpKk/NaUBArtoO1B0yHruJf1C5uHhEJIaylYcM48icFHgw==", + "license": "ISC", + "dependencies": { + "@alicloud/tea-typescript": "^1.7.1", + "moment": "^2.29.1" + } + }, + "node_modules/@alicloud/darabonba-string": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@alicloud/darabonba-string/-/darabonba-string-1.0.3.tgz", + "integrity": "sha512-NyWwrU8cAIesWk3uHL1Q7pTDTqLkCI/0PmJXC4/4A0MFNAZ9Ouq0iFBsRqvfyUujSSM+WhYLuTfakQXiVLkTMA==", + "license": "Apache-2.0", + "dependencies": { + "@alicloud/tea-typescript": "^1.5.1" + } + }, + "node_modules/@alicloud/dysmsapi20170525": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@alicloud/dysmsapi20170525/-/dysmsapi20170525-4.1.2.tgz", + "integrity": "sha512-Wxg+wQjpBGmXCvmIf9QE0mBv9dcGI0q13NxzF48akLYjSf/Mpk7jbnYttqEzNZPpRMShi1wViANwo8q+WkvYfQ==", + "license": "Apache-2.0", + "dependencies": { + "@alicloud/openapi-core": "^1.0.0", + "@darabonba/typescript": "^1.0.0" + } + }, + "node_modules/@alicloud/endpoint-util": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@alicloud/endpoint-util/-/endpoint-util-0.0.1.tgz", + "integrity": "sha512-+pH7/KEXup84cHzIL6UJAaPqETvln4yXlD9JzlrqioyCSaWxbug5FUobsiI6fuUOpw5WwoB3fWAtGbFnJ1K3Yg==", + "license": "Apache-2.0", + "dependencies": { + "@alicloud/tea-typescript": "^1.5.1", + "kitx": "^2.0.0" + } + }, + "node_modules/@alicloud/gateway-pop": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@alicloud/gateway-pop/-/gateway-pop-0.0.6.tgz", + "integrity": "sha512-KF4I+JvfYuLKc3fWeWYIZ7lOVJ9jRW0sQXdXidZn1DKZ978ncfGf7i0LBfONGk4OxvNb/HD3/0yYhkgZgPbKtA==", + "license": "ISC", + "dependencies": { + "@alicloud/credentials": "^2", + "@alicloud/darabonba-array": "^0.1.0", + "@alicloud/darabonba-encode-util": "^0.0.2", + "@alicloud/darabonba-map": "^0.0.1", + "@alicloud/darabonba-signature-util": "^0.0.4", + "@alicloud/darabonba-string": "^1.0.2", + "@alicloud/endpoint-util": "^0.0.1", + "@alicloud/gateway-spi": "^0.0.8", + "@alicloud/openapi-util": "^0.3.2", + "@alicloud/tea-typescript": "^1.7.1", + "@alicloud/tea-util": "^1.4.8" + } + }, + "node_modules/@alicloud/gateway-spi": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/@alicloud/gateway-spi/-/gateway-spi-0.0.8.tgz", + "integrity": "sha512-KM7fu5asjxZPmrz9sJGHJeSU+cNQNOxW+SFmgmAIrITui5hXL2LB+KNRuzWmlwPjnuA2X3/keq9h6++S9jcV5g==", + "license": "ISC", + "dependencies": { + "@alicloud/credentials": "^2", + "@alicloud/tea-typescript": "^1.7.1" + } + }, + "node_modules/@alicloud/openapi-client": { + "version": "0.4.15", + "resolved": "https://registry.npmjs.org/@alicloud/openapi-client/-/openapi-client-0.4.15.tgz", + "integrity": "sha512-4VE0/k5ZdQbAhOSTqniVhuX1k5DUeUMZv74degn3wIWjLY6Bq+hxjaGsaHYlLZ2gA5wUrs8NcI5TE+lIQS3iiA==", + "license": "ISC", + "dependencies": { + "@alicloud/credentials": "^2.4.2", + "@alicloud/gateway-spi": "^0.0.8", + "@alicloud/openapi-util": "^0.3.2", + "@alicloud/tea-typescript": "^1.7.1", + "@alicloud/tea-util": "1.4.9", + "@alicloud/tea-xml": "0.0.3" + } + }, + "node_modules/@alicloud/openapi-core": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@alicloud/openapi-core/-/openapi-core-1.0.4.tgz", + "integrity": "sha512-e9WK1lKiMOOziuLgNaYWv7FL50FyrcpO+idoLhNmFR7k0Fax4lPht5suBpTBr1PSINg5R1W3eOCm5vaUTrY4lg==", + "hasInstallScript": true, + "license": "ISC", + "dependencies": { + "@alicloud/credentials": "latest", + "@alicloud/gateway-pop": "0.0.6", + "@alicloud/gateway-spi": "^0.0.8", + "@darabonba/typescript": "^1.0.2" + } + }, + "node_modules/@alicloud/openapi-util": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@alicloud/openapi-util/-/openapi-util-0.3.2.tgz", + "integrity": "sha512-EC2JvxdcOgMlBAEG0+joOh2IB1um8CPz9EdYuRfTfd1uP8Yc9D8QRUWVGjP6scnj6fWSOaHFlit9H6PrJSyFow==", + "license": "ISC", + "dependencies": { + "@alicloud/tea-typescript": "^1.7.1", + "@alicloud/tea-util": "^1.3.0", + "kitx": "^2.1.0", + "sm3": "^1.0.3" + } + }, + "node_modules/@alicloud/tea-typescript": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@alicloud/tea-typescript/-/tea-typescript-1.8.0.tgz", + "integrity": "sha512-CWXWaquauJf0sW30mgJRVu9aaXyBth5uMBCUc+5vKTK1zlgf3hIqRUjJZbjlwHwQ5y9anwcu18r48nOZb7l2QQ==", + "license": "ISC", + "dependencies": { + "@types/node": "^12.0.2", + "httpx": "^2.2.6" + } + }, + "node_modules/@alicloud/tea-util": { + "version": "1.4.9", + "resolved": "https://registry.npmjs.org/@alicloud/tea-util/-/tea-util-1.4.9.tgz", + "integrity": "sha512-S0wz76rGtoPKskQtRTGqeuqBHFj8BqUn0Vh+glXKun2/9UpaaaWmuJwcmtImk6bJZfLYEShDF/kxDmDJoNYiTw==", + "license": "Apache-2.0", + "dependencies": { + "@alicloud/tea-typescript": "^1.5.1", + "kitx": "^2.0.0" + } + }, + "node_modules/@alicloud/tea-xml": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@alicloud/tea-xml/-/tea-xml-0.0.3.tgz", + "integrity": "sha512-+/9GliugjrLglsXVrd1D80EqqKgGpyA0eQ6+1ZdUOYCaRguaSwz44trX3PaxPu/HhIPJg9PsGQQ3cSLXWZjbAA==", + "license": "Apache-2.0", + "dependencies": { + "@alicloud/tea-typescript": "^1", + "@types/xml2js": "^0.4.5", + "xml2js": "^0.6.0" + } + }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz", + "integrity": "sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==", + "license": "MIT", + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.6", + "call-me-maybe": "^1.0.1", + "js-yaml": "^4.1.0" + } + }, + "node_modules/@apidevtools/openapi-schemas": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz", + "integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/@apidevtools/swagger-methods": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz", + "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==", + "license": "MIT" + }, + "node_modules/@apidevtools/swagger-parser": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz", + "integrity": "sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==", + "license": "MIT", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^9.0.6", + "@apidevtools/openapi-schemas": "^2.0.4", + "@apidevtools/swagger-methods": "^3.0.2", + "@jsdevtools/ono": "^7.1.3", + "call-me-maybe": "^1.0.1", + "z-schema": "^5.0.1" + }, + "peerDependencies": { + "openapi-types": ">=7" + } + }, + "node_modules/@babel/runtime": { + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz", + "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "license": "MIT", + "dependencies": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, + "node_modules/@darabonba/typescript": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@darabonba/typescript/-/typescript-1.0.3.tgz", + "integrity": "sha512-/y2y6wf5TsxD7pCPIm0OvTC+5qV0Tk7HQYxwpIuWRLXQLB0CRDvr6qk4bR6rTLO/JglJa8z2uCGZsaLYpQNqFQ==", + "license": "Apache License 2.0", + "dependencies": { + "@alicloud/tea-typescript": "^1.5.1", + "httpx": "^2.3.2", + "lodash": "^4.17.21", + "moment": "^2.30.1", + "moment-timezone": "^0.5.45", + "xml2js": "^0.6.2" + } + }, + "node_modules/@fidm/asn1": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@fidm/asn1/-/asn1-1.0.4.tgz", + "integrity": "sha512-esd1jyNvRb2HVaQGq2Gg8Z0kbQPXzV9Tq5Z14KNIov6KfFD6PTaRIO8UpcsYiTNzOqJpmyzWgVTrUwFV3UF4TQ==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@fidm/x509": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@fidm/x509/-/x509-1.2.1.tgz", + "integrity": "sha512-nwc2iesjyc9hkuzcrMCBXQRn653XuAUKorfWM8PZyJawiy1QzLj4vahwzaI25+pfpwOLvMzbJ0uKpWLDNmo16w==", + "license": "MIT", + "dependencies": { + "@fidm/asn1": "^1.0.4", + "tweetnacl": "^1.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", + "license": "MIT" + }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, + "node_modules/@sideway/address": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", + "license": "BSD-3-Clause" + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "12.20.55", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", + "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", + "license": "MIT" + }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "license": "MIT" + }, + "node_modules/@types/xml2js": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.14.tgz", + "integrity": "sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@zxing/text-encoding": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz", + "integrity": "sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==", + "license": "(Unlicense OR Apache-2.0)", + "optional": true + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/alipay-sdk": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/alipay-sdk/-/alipay-sdk-4.14.0.tgz", + "integrity": "sha512-oiD/VP5Ei0RRacHHmE+N0uqgOj2xzce7c0fHrtyyh1P04O+o9I1r65LdGPzU8960J56xOxS/d3c+R/9lsPUH7g==", + "license": "MIT", + "dependencies": { + "@fidm/x509": "^1.2.1", + "bignumber.js": "^9.1.2", + "camelcase-keys": "^7.0.2", + "crypto-js": "^4.2.0", + "formstream": "^1.5.0", + "snakecase-keys": "^8.0.0", + "sse-decoder": "^1.0.0", + "urllib": "^4", + "utility": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/axios": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", + "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", + "license": "MIT" + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/block-stream2": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/block-stream2/-/block-stream2-2.1.0.tgz", + "integrity": "sha512-suhjmLI57Ewpmq00qaygS8UgEq2ly2PCItenIyhMqVjo4t4pGzqMvfgJuX8iWTeSDdfSSqS6j38fL4ToNL7Pfg==", + "license": "MIT", + "dependencies": { + "readable-stream": "^3.4.0" + } + }, + "node_modules/block-stream2/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-or-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/browser-or-node/-/browser-or-node-2.1.1.tgz", + "integrity": "sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg==", + "license": "MIT" + }, + "node_modules/buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-me-maybe": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", + "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", + "license": "MIT" + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-keys": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-7.0.2.tgz", + "integrity": "sha512-Rjs1H+A9R+Ig+4E/9oyB66UC5Mj9Xq3N//vcLf2WzgdTi/3gUu3Z9KoqmlrEG4VuuLK8wJHofxzdQXz/knhiYg==", + "license": "MIT", + "dependencies": { + "camelcase": "^6.3.0", + "map-obj": "^4.1.0", + "quick-lru": "^5.1.1", + "type-fest": "^1.2.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/camelcase-keys/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/color/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" + }, + "node_modules/colorspace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "license": "MIT", + "dependencies": { + "color": "^3.1.3", + "text-hex": "1.0.x" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz", + "integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/concurrently": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.2.tgz", + "integrity": "sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "date-fns": "^2.30.0", + "lodash": "^4.17.21", + "rxjs": "^7.8.1", + "shell-quote": "^1.8.1", + "spawn-command": "0.0.2", + "supports-color": "^8.1.1", + "tree-kill": "^1.2.2", + "yargs": "^17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": "^14.13.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/crypto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz", + "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==", + "deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in.", + "license": "ISC" + }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", + "license": "MIT" + }, + "node_modules/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decode-uri-component": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "license": "MIT" + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/dotenv": { + "version": "17.2.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.1.tgz", + "integrity": "sha512-kQhDYKZecqnM0fCnzI5eIv5L4cAe/iRI+HqMbO/hbRdTAeXDG+M9FjipUxNfbARuEg4iHIbhnhs78BCHNbSxEQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", + "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/express-validator": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.2.1.tgz", + "integrity": "sha512-CjNE6aakfpuwGaHQZ3m8ltCG2Qvivd7RHtVMS/6nVxOM7xVGqr4bhflsm4+N5FP5zI7Zxp+Hae+9RE+o8e3ZOQ==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21", + "validator": "~13.12.0" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-xml-parser": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz", + "integrity": "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^1.1.1" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "license": "MIT" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/filter-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", + "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "license": "MIT" + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/formstream": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/formstream/-/formstream-1.5.2.tgz", + "integrity": "sha512-NASf0lgxC1AyKNXQIrXTEYkiX99LhCEXTkiGObXAkpBui86a4u8FjH1o2bGb3PpqI3kafC+yw4zWeK6l6VHTgg==", + "license": "MIT", + "dependencies": { + "destroy": "^1.0.4", + "mime": "^2.5.2", + "node-hex": "^1.0.1", + "pause-stream": "~0.0.11" + } + }, + "node_modules/formstream/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/helmet": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz", + "integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/httpx": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/httpx/-/httpx-2.3.3.tgz", + "integrity": "sha512-k1qv94u1b6e+XKCxVbLgYlOypVP9MPGpnN5G/vxFf6tDO4V3xpz3d6FUOY/s8NtPgaq5RBVVgSB+7IHpVxMYzw==", + "license": "MIT", + "dependencies": { + "@types/node": "^20", + "debug": "^4.1.1" + } + }, + "node_modules/httpx/node_modules/@types/node": { + "version": "20.19.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.9.tgz", + "integrity": "sha512-cuVNgarYWZqxRJDQHEB58GEONhOK79QVR/qYx4S7kcUObQvUwvFnYxJuuHUKm2aieN9X3yZB4LZsuYNU1Qphsw==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/httpx/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/httpx/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "license": "MIT" + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/joi": { + "version": "17.13.3", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", + "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.3.0", + "@hapi/topo": "^5.1.0", + "@sideway/address": "^4.1.5", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/kitx": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/kitx/-/kitx-2.2.0.tgz", + "integrity": "sha512-tBMwe6AALTBQJb0woQDD40734NKzb0Kzi3k7wQj9ar3AbP9oqhoVrdXPh7rk2r00/glIgd0YbToIUJsnxWMiIg==", + "license": "MIT", + "dependencies": { + "@types/node": "^22.5.4" + } + }, + "node_modules/kitx/node_modules/@types/node": { + "version": "22.16.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.16.5.tgz", + "integrity": "sha512-bJFoMATwIGaxxx8VJPeM8TonI8t579oRvgAuT8zFugJsJZgzqv0Fu8Mhp68iecjzG7cnN3mO2dJQ5uUM2EFrgQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.mergewith": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", + "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "license": "MIT", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/logform/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/lru.min": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.2.tgz", + "integrity": "sha512-Nv9KddBcQSlQopmBHXSsZVY5xsdlZkdH/Iey0BlcBYggMd4two7cZnKOK9vmy3nY0O5RGH99z1PCeTpPqszUYg==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, + "node_modules/map-obj": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", + "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minio": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/minio/-/minio-8.0.5.tgz", + "integrity": "sha512-/vAze1uyrK2R/DSkVutE4cjVoAowvIQ18RAwn7HrqnLecLlMazFnY0oNBqfuoAWvu7mZIGX75AzpuV05TJeoHg==", + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.4", + "block-stream2": "^2.1.0", + "browser-or-node": "^2.1.1", + "buffer-crc32": "^1.0.0", + "eventemitter3": "^5.0.1", + "fast-xml-parser": "^4.4.1", + "ipaddr.js": "^2.0.1", + "lodash": "^4.17.21", + "mime-types": "^2.1.35", + "query-string": "^7.1.3", + "stream-json": "^1.8.0", + "through2": "^4.0.2", + "web-encoding": "^1.1.5", + "xml2js": "^0.5.0 || ^0.6.2" + }, + "engines": { + "node": "^16 || ^18 || >=20" + } + }, + "node_modules/minio/node_modules/ipaddr.js": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", + "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/moment-timezone": { + "version": "0.5.48", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz", + "integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==", + "license": "MIT", + "dependencies": { + "moment": "^2.29.4" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/multer": { + "version": "1.4.5-lts.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz", + "integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==", + "deprecated": "Multer 1.x is impacted by a number of vulnerabilities, which have been patched in 2.x. You should upgrade to the latest 2.x version.", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/mysql2": { + "version": "3.14.3", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.14.3.tgz", + "integrity": "sha512-fD6MLV8XJ1KiNFIF0bS7Msl8eZyhlTDCDl75ajU5SJtpdx9ZPEACulJcqJWr1Y8OYyxsFc4j3+nflpmhxCU5aQ==", + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.1", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.6.3", + "long": "^5.2.1", + "lru.min": "^1.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/mysql2/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz", + "integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==", + "license": "MIT", + "dependencies": { + "lru-cache": "^7.14.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "license": "MIT", + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/node-cron": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.2.1.tgz", + "integrity": "sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg==", + "license": "ISC", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/node-hex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/node-hex/-/node-hex-1.0.1.tgz", + "integrity": "sha512-iwpZdvW6Umz12ICmu9IYPRxg0tOLGmU3Tq2tKetejCj3oZd7b2nUXwP3a7QA5M9glWy8wlPS1G3RwM/CdsUbdQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/node-rsa": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/node-rsa/-/node-rsa-1.1.1.tgz", + "integrity": "sha512-Jd4cvbJMryN21r5HgxQOpMEqv+ooke/korixNNK3mGqfGJmy0M77WDDzo/05969+OkMy3XW1UuZsSmW9KQm7Fw==", + "license": "MIT", + "dependencies": { + "asn1": "^0.2.4" + } + }, + "node_modules/nodemon": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", + "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nodemon/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/nodemon/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nodemon/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "license": "MIT", + "dependencies": { + "fn.name": "1.x.x" + } + }, + "node_modules/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", + "license": "MIT", + "peer": true + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/pause-stream": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", + "integrity": "sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==", + "license": [ + "MIT", + "Apache2" + ], + "dependencies": { + "through": "~2.3" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "license": "MIT", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/qrcode/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/qrcode/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC" + }, + "node_modules/qrcode/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/query-string": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz", + "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==", + "license": "MIT", + "dependencies": { + "decode-uri-component": "^0.2.2", + "filter-obj": "^1.1.0", + "split-on-first": "^1.0.0", + "strict-uri-encode": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "license": "ISC" + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", + "license": "ISC" + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sm3": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sm3/-/sm3-1.0.3.tgz", + "integrity": "sha512-KyFkIfr8QBlFG3uc3NaljaXdYcsbRy1KrSfc4tsQV8jW68jAktGeOcifu530Vx/5LC+PULHT0Rv8LiI8Gw+c1g==", + "license": "MIT" + }, + "node_modules/snake-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", + "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", + "license": "MIT", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/snakecase-keys": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/snakecase-keys/-/snakecase-keys-8.1.0.tgz", + "integrity": "sha512-9/Eug2btrCiOi+9+vIXJnxUcKVfcbLy5Uwff4BrO6PQf3Oq/2iYQ/1zkmnrpIIjfel/DAasAlux7OvAmCa+Xnw==", + "license": "MIT", + "dependencies": { + "map-obj": "^4.2.0", + "snake-case": "^3.0.4", + "type-fest": "^4.15.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/snakecase-keys/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/spawn-command": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz", + "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==", + "dev": true + }, + "node_modules/split-on-first": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", + "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/sse-decoder": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/sse-decoder/-/sse-decoder-1.0.0.tgz", + "integrity": "sha512-JPopy3jfNmPcUz5Ru6skKhHNRJbsvcEW6Z4SirKkucLS8Jya1Bmf4FVX8giOkLm8xQJ7kK68P6GXoVSTkbedUA==", + "license": "MIT", + "engines": { + "node": ">= 14.19.3" + } + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stream-chain": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/stream-chain/-/stream-chain-2.2.5.tgz", + "integrity": "sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==", + "license": "BSD-3-Clause" + }, + "node_modules/stream-json": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/stream-json/-/stream-json-1.9.1.tgz", + "integrity": "sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==", + "license": "BSD-3-Clause", + "dependencies": { + "stream-chain": "^2.2.5" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/strict-uri-encode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", + "integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strnum": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz", + "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/swagger-jsdoc": { + "version": "6.2.8", + "resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz", + "integrity": "sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==", + "license": "MIT", + "dependencies": { + "commander": "6.2.0", + "doctrine": "3.0.0", + "glob": "7.1.6", + "lodash.mergewith": "^4.6.2", + "swagger-parser": "^10.0.3", + "yaml": "2.0.0-1" + }, + "bin": { + "swagger-jsdoc": "bin/swagger-jsdoc.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/swagger-parser": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.3.tgz", + "integrity": "sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==", + "license": "MIT", + "dependencies": { + "@apidevtools/swagger-parser": "10.0.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/swagger-ui-dist": { + "version": "5.27.1", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.27.1.tgz", + "integrity": "sha512-oGtpYO3lnoaqyGtlJalvryl7TwzgRuxpOVWqEHx8af0YXI+Kt+4jMpLdgMtMcmWmuQ0QTCHLKExwrBFMSxvAUA==", + "license": "Apache-2.0", + "dependencies": { + "@scarf/scarf": "=1.4.0" + } + }, + "node_modules/swagger-ui-express": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", + "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", + "license": "MIT", + "dependencies": { + "swagger-ui-dist": ">=5.0.0" + }, + "engines": { + "node": ">= v0.10.32" + }, + "peerDependencies": { + "express": ">=4.0.0 || >=5.0.0-beta" + } + }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "license": "MIT" + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "license": "MIT" + }, + "node_modules/through2": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", + "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", + "license": "MIT", + "dependencies": { + "readable-stream": "3" + } + }, + "node_modules/through2/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tweetnacl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", + "license": "Unlicense" + }, + "node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.15.0.tgz", + "integrity": "sha512-7oZJCPvvMvTd0OlqWsIxTuItTpJBpU1tcbVl24FMn3xt3+VSunwUasmfPJRE57oNO1KsZ4PgA1xTdAX4hq8NyQ==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/unescape": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/unescape/-/unescape-1.0.1.tgz", + "integrity": "sha512-O0+af1Gs50lyH1nUu3ZyYS1cRh01Q/kUKatTOkSs7jukXE6/NebucDVxyiDsA9AQ4JC1V1jUH9EO8JX2nMDgGQ==", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/urllib": { + "version": "4.8.2", + "resolved": "https://registry.npmjs.org/urllib/-/urllib-4.8.2.tgz", + "integrity": "sha512-V5oo9kzQfF9UQAC9KOVFmmmbYPJ9nksgO8HM89BZse96QcCyjrssPVxKzL/9sVPRC8D4Sd3nAdaMCXAZ3dqEYA==", + "license": "MIT", + "dependencies": { + "form-data": "^4.0.1", + "formstream": "^1.5.1", + "mime-types": "^2.1.35", + "qs": "^6.12.1", + "type-fest": "^4.20.1", + "undici": "^7.1.1", + "ylru": "^2.0.0" + }, + "engines": { + "node": ">= 18.19.0" + } + }, + "node_modules/urllib/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utility": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/utility/-/utility-2.5.0.tgz", + "integrity": "sha512-lDbOVde5UAKgtxrSyZNhqrTA7f7anba6DTqbsDWgUFk6PZlmr7djqPYw0FnL5a6TbJvRt38VmYqt07zVLzXG2A==", + "license": "MIT", + "dependencies": { + "escape-html": "^1.0.3", + "unescape": "^1.0.1", + "ylru": "^2.0.0" + }, + "engines": { + "node": ">= 16.0.0" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/validator": { + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", + "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/web-encoding": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/web-encoding/-/web-encoding-1.1.5.tgz", + "integrity": "sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==", + "license": "MIT", + "dependencies": { + "util": "^0.12.3" + }, + "optionalDependencies": { + "@zxing/text-encoding": "0.9.0" + } + }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "license": "ISC" + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/winston": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", + "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", + "license": "MIT", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.2", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "license": "MIT", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/winston/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yaml": { + "version": "2.0.0-1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz", + "integrity": "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/ylru": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ylru/-/ylru-2.0.0.tgz", + "integrity": "sha512-T6hTrKcr9lKeUG0MQ/tO72D3UGptWVohgzpHG8ljU1jeBt2RCjcWxvsTPD8ZzUq1t1FvwROAw1kxg2euvg/THg==", + "license": "MIT", + "engines": { + "node": ">= 18.19.0" + } + }, + "node_modules/z-schema": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz", + "integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==", + "license": "MIT", + "dependencies": { + "lodash.get": "^4.4.2", + "lodash.isequal": "^4.5.0", + "validator": "^13.7.0" + }, + "bin": { + "z-schema": "bin/z-schema" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "commander": "^9.4.1" + } + }, + "node_modules/z-schema/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": "^12.20.0 || >=14" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..404039a --- /dev/null +++ b/package.json @@ -0,0 +1,50 @@ +{ + "name": "integrated-system", + "version": "1.0.0", + "description": "Vue3 + Node.js 集成系统", + "main": "server.js", + "scripts": { + "dev": "nodemon server.js" + }, + "dependencies": { + "@alicloud/dysmsapi20170525": "^4.1.2", + "@alicloud/openapi-client": "^0.4.15", + "alipay-sdk": "^4.14.0", + "axios": "^1.11.0", + "bcryptjs": "^2.4.3", + "body-parser": "^1.20.2", + "cors": "^2.8.5", + "crypto": "^1.0.1", + "dayjs": "^1.11.13", + "dotenv": "^17.2.1", + "express": "^4.18.2", + "express-rate-limit": "^7.1.5", + "express-validator": "^7.2.1", + "helmet": "^8.1.0", + "joi": "^17.13.3", + "jsonwebtoken": "^9.0.2", + "minio": "^8.0.5", + "multer": "^1.4.5-lts.1", + "mysql2": "^3.14.3", + "node-cron": "^4.2.1", + "node-rsa": "^1.1.1", + "qrcode": "^1.5.4", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.1", + "winston": "^3.17.0", + "xml2js": "^0.6.2" + }, + "devDependencies": { + "concurrently": "^8.2.2", + "nodemon": "^3.0.2" + }, + "keywords": [ + "vue3", + "nodejs", + "express", + "mysql", + "element-plus" + ], + "author": "", + "license": "MIT" +} diff --git a/routes/address-labels.js b/routes/address-labels.js new file mode 100644 index 0000000..b304bce --- /dev/null +++ b/routes/address-labels.js @@ -0,0 +1,180 @@ +const express = require('express'); +const router = express.Router(); +const { getDB } = require('../database'); +const { auth } = require('../middleware/auth'); + +// 获取地址标签列表(包含系统默认和用户自定义) +router.get('/', auth, async (req, res) => { + try { + const userId = req.user.id; + + const [labels] = await getDB().execute( + `SELECT * FROM address_labels + WHERE user_id IS NULL OR user_id = ? + ORDER BY is_system DESC, created_at ASC`, + [userId] + ); + + res.json({ + success: true, + data: labels + }); + } catch (error) { + console.error('获取地址标签列表错误:', error); + res.status(500).json({ message: '获取地址标签列表失败' }); + } +}); + +// 创建自定义地址标签 +router.post('/', auth, async (req, res) => { + try { + const userId = req.user.id; + const { name, color = '#1890ff' } = req.body; + + if (!name || name.trim() === '') { + return res.status(400).json({ message: '标签名称不能为空' }); + } + + // 检查标签名称是否已存在(系统标签或用户自定义标签) + const [existing] = await getDB().execute( + `SELECT id FROM address_labels + WHERE name = ? AND (user_id IS NULL OR user_id = ?)`, + [name.trim(), userId] + ); + + if (existing.length > 0) { + return res.status(400).json({ message: '标签名称已存在' }); + } + + const [result] = await getDB().execute( + `INSERT INTO address_labels (name, color, user_id, is_system, created_at, updated_at) + VALUES (?, ?, ?, false, NOW(), NOW())`, + [name.trim(), color, userId] + ); + + res.status(201).json({ + success: true, + message: '地址标签创建成功', + data: { labelId: result.insertId } + }); + } catch (error) { + console.error('创建地址标签错误:', error); + res.status(500).json({ message: '创建地址标签失败' }); + } +}); + +// 更新自定义地址标签 +router.put('/:id', auth, async (req, res) => { + try { + const labelId = req.params.id; + const userId = req.user.id; + const { name, color } = req.body; + + // 检查标签是否存在且属于当前用户(不能修改系统标签) + const [existing] = await getDB().execute( + 'SELECT id, is_system FROM address_labels WHERE id = ? AND user_id = ?', + [labelId, userId] + ); + + if (existing.length === 0) { + return res.status(404).json({ message: '地址标签不存在或无权限修改' }); + } + + if (existing[0].is_system) { + return res.status(403).json({ message: '系统标签不能修改' }); + } + + if (name && name.trim() !== '') { + // 检查新名称是否已存在 + const [nameExists] = await getDB().execute( + `SELECT id FROM address_labels + WHERE name = ? AND id != ? AND (user_id IS NULL OR user_id = ?)`, + [name.trim(), labelId, userId] + ); + + if (nameExists.length > 0) { + return res.status(400).json({ message: '标签名称已存在' }); + } + } + + const updateFields = []; + const updateValues = []; + + if (name && name.trim() !== '') { + updateFields.push('name = ?'); + updateValues.push(name.trim()); + } + + if (color) { + updateFields.push('color = ?'); + updateValues.push(color); + } + + if (updateFields.length === 0) { + return res.status(400).json({ message: '没有需要更新的字段' }); + } + + updateFields.push('updated_at = NOW()'); + updateValues.push(labelId, userId); + + await getDB().execute( + `UPDATE address_labels SET ${updateFields.join(', ')} WHERE id = ? AND user_id = ?`, + updateValues + ); + + res.json({ + success: true, + message: '地址标签更新成功' + }); + } catch (error) { + console.error('更新地址标签错误:', error); + res.status(500).json({ message: '更新地址标签失败' }); + } +}); + +// 删除自定义地址标签 +router.delete('/:id', auth, async (req, res) => { + try { + const labelId = req.params.id; + const userId = req.user.id; + + // 检查标签是否存在且属于当前用户(不能删除系统标签) + const [existing] = await getDB().execute( + 'SELECT id, is_system FROM address_labels WHERE id = ? AND user_id = ?', + [labelId, userId] + ); + + if (existing.length === 0) { + return res.status(404).json({ message: '地址标签不存在或无权限删除' }); + } + + if (existing[0].is_system) { + return res.status(403).json({ message: '系统标签不能删除' }); + } + + // 检查是否有地址正在使用该标签 + const [addressesUsingLabel] = await getDB().execute( + 'SELECT COUNT(*) as count FROM user_addresses WHERE label_id = ? AND deleted_at IS NULL', + [labelId] + ); + + if (addressesUsingLabel[0].count > 0) { + return res.status(400).json({ message: '该标签正在被使用,无法删除' }); + } + + await getDB().execute( + 'DELETE FROM address_labels WHERE id = ? AND user_id = ?', + [labelId, userId] + ); + + res.json({ + success: true, + message: '地址标签删除成功' + }); + } catch (error) { + console.error('删除地址标签错误:', error); + res.status(500).json({ message: '删除地址标签失败' }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/routes/addresses.js b/routes/addresses.js new file mode 100644 index 0000000..101bf80 --- /dev/null +++ b/routes/addresses.js @@ -0,0 +1,567 @@ +const express = require('express'); +const router = express.Router(); +const { getDB } = require('../database'); +const { auth } = require('../middleware/auth'); + +/** + * @swagger + * components: + * schemas: + * Address: + * type: object + * properties: + * id: + * type: integer + * description: 地址ID + * user_id: + * type: integer + * description: 用户ID + * recipient_name: + * type: string + * description: 收件人姓名 + * phone: + * type: string + * description: 联系电话 + * province_code: + * type: string + * description: 省份编码 + * province_name: + * type: string + * description: 省份名称 + * city_code: + * type: string + * description: 城市编码 + * city_name: + * type: string + * description: 城市名称 + * district_code: + * type: string + * description: 区县编码 + * district_name: + * type: string + * description: 区县名称 + * detailed_address: + * type: string + * description: 详细地址 + * postal_code: + * type: string + * description: 邮政编码 + * label_id: + * type: integer + * description: 地址标签ID + * is_default: + * type: boolean + * description: 是否为默认地址 + * label_name: + * type: string + * description: 标签名称 + * label_color: + * type: string + * description: 标签颜色 + * required: + * - recipient_name + * - phone + * - province_code + * - province_name + * - city_code + * - city_name + * - district_code + * - district_name + * - detailed_address + */ + +/** + * @swagger + * /addresses: + * get: + * summary: 获取用户收货地址列表 + * tags: [Addresses] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: 成功获取地址列表 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * data: + * type: array + * items: + * $ref: '#/components/schemas/Address' + * 401: + * description: 未授权 + * 500: + * description: 服务器错误 + */ +router.get('/', auth, async (req, res) => { + try { + const userId = req.user.id; + + const [addresses] = await getDB().execute( + `SELECT ua.*, al.name as label_name, al.color as label_color, + p.name as province_name, c.name as city_name, d.name as district_name + FROM user_addresses ua + LEFT JOIN address_labels al ON ua.label = al.id + LEFT JOIN china_regions p ON ua.province = p.code + LEFT JOIN china_regions c ON ua.city = c.code + LEFT JOIN china_regions d ON ua.district = d.code + WHERE ua.user_id = ? + ORDER BY ua.is_default DESC, ua.created_at DESC`, + [userId] + ); + + res.json({ + success: true, + data: addresses + }); + } catch (error) { + console.error('获取收货地址列表错误:', error); + res.status(500).json({ message: '获取收货地址列表失败' }); + } +}); + +/** + * @swagger + * /addresses/{id}: + * get: + * summary: 获取单个收货地址详情 + * tags: [Addresses] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: 地址ID + * responses: + * 200: + * description: 成功获取地址详情 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * data: + * $ref: '#/components/schemas/Address' + * 401: + * description: 未授权 + * 404: + * description: 地址不存在 + * 500: + * description: 服务器错误 + */ +router.get('/:id', auth, async (req, res) => { + try { + const addressId = req.params.id; + const userId = req.user.id; + + const [addresses] = await getDB().execute( + `SELECT ua.*, al.name as label_name, al.color as label_color + FROM user_addresses ua + LEFT JOIN address_labels al ON ua.label_id = al.id + WHERE ua.id = ? AND ua.user_id = ? AND ua.deleted_at IS NULL`, + [addressId, userId] + ); + + if (addresses.length === 0) { + return res.status(404).json({ message: '收货地址不存在' }); + } + + res.json({ + success: true, + data: addresses[0] + }); + } catch (error) { + console.error('获取收货地址详情错误:', error); + res.status(500).json({ message: '获取收货地址详情失败' }); + } +}); + +/** + * @swagger + * /addresses: + * post: + * summary: 创建收货地址 + * tags: [Addresses] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * recipient_name: + * type: string + * description: 收件人姓名 + * phone: + * type: string + * description: 联系电话 + * province_code: + * type: string + * description: 省份编码 + * province_name: + * type: string + * description: 省份名称 + * city_code: + * type: string + * description: 城市编码 + * city_name: + * type: string + * description: 城市名称 + * district_code: + * type: string + * description: 区县编码 + * district_name: + * type: string + * description: 区县名称 + * detailed_address: + * type: string + * description: 详细地址 + * postal_code: + * type: string + * description: 邮政编码 + * label_id: + * type: integer + * description: 地址标签ID + * is_default: + * type: boolean + * description: 是否为默认地址 + * required: + * - recipient_name + * - phone + * - province_code + * - province_name + * - city_code + * - city_name + * - district_code + * - district_name + * - detailed_address + * responses: + * 201: + * description: 地址创建成功 + * 400: + * description: 请求参数错误 + * 401: + * description: 未授权 + * 500: + * description: 服务器错误 + */ +router.post('/', auth, async (req, res) => { + try { + const userId = req.user.id; + const { + recipient_name, + phone, + province_code, + city_code, + district_code, + detailed_address, + is_default = false + } = req.body; + + // 验证必填字段 + if (!recipient_name || !phone || !province_code || !city_code || !district_code || !detailed_address) { + return res.status(400).json({ message: '收件人姓名、电话、省市区和详细地址不能为空' }); + } + + // 如果设置为默认地址,先取消其他默认地址 + if (is_default) { + await getDB().execute( + 'UPDATE user_addresses SET is_default = false WHERE user_id = ? ', + [userId] + ); + } + + const [result] = await getDB().execute( + `INSERT INTO user_addresses ( + user_id, receiver_name, receiver_phone, province, city, + district, detailed_address, is_default, created_at, updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())`, + [ + userId, recipient_name, phone, province_code, city_code, + district_code, detailed_address, is_default + ] + ); + + res.status(201).json({ + success: true, + message: '收货地址创建成功', + data: { addressId: result.insertId } + }); + } catch (error) { + console.error('创建收货地址错误:', error); + res.status(500).json({ message: '创建收货地址失败' }); + } +}); + +/** + * @swagger + * /addresses/{id}: + * put: + * summary: 更新收货地址 + * tags: [Addresses] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: 地址ID + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * recipient_name: + * type: string + * description: 收件人姓名 + * phone: + * type: string + * description: 联系电话 + * province_code: + * type: string + * description: 省份编码 + * province_name: + * type: string + * description: 省份名称 + * city_code: + * type: string + * description: 城市编码 + * city_name: + * type: string + * description: 城市名称 + * district_code: + * type: string + * description: 区县编码 + * district_name: + * type: string + * description: 区县名称 + * detailed_address: + * type: string + * description: 详细地址 + * postal_code: + * type: string + * description: 邮政编码 + * label_id: + * type: integer + * description: 地址标签ID + * is_default: + * type: boolean + * description: 是否为默认地址 + * responses: + * 200: + * description: 地址更新成功 + * 400: + * description: 请求参数错误 + * 401: + * description: 未授权 + * 404: + * description: 地址不存在 + * 500: + * description: 服务器错误 + */ +router.put('/:id', auth, async (req, res) => { + try { + const addressId = req.params.id; + const userId = req.user.id; + const { + recipient_name, + phone, + province_code, + city_code, + district_code, + detailed_address, + is_default + } = req.body; + if (!recipient_name || !phone || !province_code || !city_code || !district_code || !detailed_address) { + return res.status(400).json({ message: '收件人姓名、电话、省市区和详细地址不能为空' }); + } + + // 检查地址是否存在且属于当前用户 + const [existing] = await getDB().execute( + 'SELECT id FROM user_addresses WHERE id = ? AND user_id = ? ', + [addressId, userId] + ); + + if (existing.length === 0) { + return res.status(404).json({ message: '收货地址不存在' }); + } + + // 如果设置为默认地址,先取消其他默认地址 + if (is_default) { + await getDB().execute( + 'UPDATE user_addresses SET is_default = false WHERE user_id = ? AND id != ? ', + [userId, addressId] + ); + } + + const [result] = await getDB().execute( + `UPDATE user_addresses SET + receiver_name = ?, receiver_phone = ?, province = ?, city = ?, + district = ?, detailed_address = ?, is_default = ?, updated_at = NOW() + WHERE id = ? AND user_id = ?`, + [ + recipient_name, phone, province_code, city_code, + district_code, detailed_address, is_default, + addressId, userId + ] + ); + + res.json({ + success: true, + message: '收货地址更新成功' + }); + } catch (error) { + console.error('更新收货地址错误:', error); + res.status(500).json({ message: '更新收货地址失败' }); + } +}); + +/** + * @swagger + * /addresses/{id}: + * delete: + * summary: 删除收货地址 + * tags: [Addresses] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: 地址ID + * responses: + * 200: + * description: 地址删除成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: 收货地址删除成功 + * 401: + * description: 未授权 + * 404: + * description: 地址不存在 + * 500: + * description: 服务器错误 + */ +router.delete('/:id', auth, async (req, res) => { + try { + const addressId = req.params.id; + const userId = req.user.id; + + const [result] = await getDB().execute( + 'DELETE FROM user_addresses WHERE id = ? AND user_id = ?', + [addressId, userId] + ); + + if (result.affectedRows === 0) { + return res.status(404).json({ message: '收货地址不存在' }); + } + + res.json({ + success: true, + message: '收货地址删除成功' + }); + } catch (error) { + console.error('删除收货地址错误:', error); + res.status(500).json({ message: '删除收货地址失败' }); + } +}); + +/** + * @swagger + * /addresses/{id}/default: + * put: + * summary: 设置默认地址 + * tags: [Addresses] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: 地址ID + * responses: + * 200: + * description: 默认地址设置成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: 默认地址设置成功 + * 401: + * description: 未授权 + * 404: + * description: 地址不存在 + * 500: + * description: 服务器错误 + */ +router.put('/:id/default', auth, async (req, res) => { + try { + const addressId = req.params.id; + const userId = req.user.id; + + // 检查地址是否存在且属于当前用户 + const [existing] = await getDB().execute( + 'SELECT id FROM user_addresses WHERE id = ? AND user_id = ? AND deleted_at IS NULL', + [addressId, userId] + ); + + if (existing.length === 0) { + return res.status(404).json({ message: '收货地址不存在' }); + } + + // 取消其他默认地址 + await getDB().execute( + 'UPDATE user_addresses SET is_default = false WHERE user_id = ? AND deleted_at IS NULL', + [userId] + ); + + // 设置当前地址为默认 + await getDB().execute( + 'UPDATE user_addresses SET is_default = true WHERE id = ? AND user_id = ?', + [addressId, userId] + ); + + res.json({ + success: true, + message: '默认地址设置成功' + }); + } catch (error) { + console.error('设置默认地址错误:', error); + res.status(500).json({ message: '设置默认地址失败' }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/routes/agent-withdrawals.js b/routes/agent-withdrawals.js new file mode 100644 index 0000000..7b4cd34 --- /dev/null +++ b/routes/agent-withdrawals.js @@ -0,0 +1,475 @@ +const express = require('express'); +const router = express.Router(); +const { getDB } = require('../database'); +const { auth } = require('../middleware/auth'); +const multer = require('multer'); +const path = require('path'); +const fs = require('fs'); + +// 配置multer用于文件上传 +const storage = multer.diskStorage({ + destination: function (req, file, cb) { + const uploadDir = 'uploads/qr-codes'; + // 确保上传目录存在 + if (!fs.existsSync(uploadDir)) { + fs.mkdirSync(uploadDir, { recursive: true }); + } + cb(null, uploadDir); + }, + filename: function (req, file, cb) { + // 生成唯一文件名 + const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9); + cb(null, 'qr-code-' + uniqueSuffix + path.extname(file.originalname)); + } +}); + +// 文件过滤器 +const fileFilter = (req, file, cb) => { + // 只允许图片文件 + if (file.mimetype.startsWith('image/')) { + cb(null, true); + } else { + cb(new Error('只允许上传图片文件'), false); + } +}; + +const upload = multer({ + storage: storage, + fileFilter: fileFilter, + limits: { + fileSize: 5 * 1024 * 1024 // 限制文件大小为5MB + } +}); + +// 获取数据库连接 +const db = { + query: async (sql, params = []) => { + const connection = getDB(); + const [rows] = await connection.execute(sql, params); + return rows; + } +}; + +/** + * 检查用户是否为代理商 + */ +const requireAgent = async (req, res, next) => { + try { + const userId = req.user.id; + + // 查询用户是否为代理商 + const agentResult = await db.query( + 'SELECT * FROM regional_agents WHERE user_id = ? AND status = "active"', + [userId] + ); + + if (!agentResult || agentResult.length === 0) { + return res.status(403).json({ success: false, message: '您不是活跃的代理商' }); + } + + req.agent = agentResult[0]; + next(); + } catch (error) { + console.error('检查代理商身份失败:', error); + res.status(500).json({ success: false, message: '检查代理商身份失败' }); + } +}; + +/** + * 获取代理商佣金统计信息 + */ +router.get('/stats', auth, requireAgent, async (req, res) => { + try { + const agentId = req.agent.id; + + // 查询佣金统计 + const statsQuery = ` + SELECT + CAST(COALESCE(commission_sum.total_commission, 0) AS DECIMAL(10,2)) as total_commission, + CAST(COALESCE(ra.withdrawn_amount, 0) AS DECIMAL(10,2)) as withdrawn_amount, + CAST(COALESCE(ra.pending_withdrawal, 0) AS DECIMAL(10,2)) as pending_withdrawal, + CAST(COALESCE(commission_sum.total_commission, 0) - COALESCE(ra.withdrawn_amount, 0) - COALESCE(ra.pending_withdrawal, 0) AS DECIMAL(10,2)) as available_amount + FROM regional_agents ra + LEFT JOIN ( + SELECT agent_id, SUM(commission_amount) as total_commission + FROM agent_commission_records + WHERE agent_id = ? + GROUP BY agent_id + ) commission_sum ON ra.id = commission_sum.agent_id + WHERE ra.id = ? + `; + + const statsResult = await db.query(statsQuery, [agentId, agentId]); + const stats = statsResult && statsResult.length > 0 ? statsResult[0] : { + total_commission: 0, + withdrawn_amount: 0, + pending_withdrawal: 0, + available_amount: 0 + }; + + // 查询代理商信息包括收款方式 + const agentInfo = await db.query( + 'SELECT payment_type, bank_name, account_number, account_holder, qr_code_url, bank_account FROM regional_agents WHERE id = ?', + [agentId] + ); + + const agent = agentInfo[0] || {}; + + // 构建收款方式信息,兼容旧数据 + const paymentInfo = { + payment_type: agent.payment_type || 'bank', + bank_name: agent.bank_name || '', + account_number: agent.account_number || agent.bank_account, // 兼容旧字段 + account_holder: agent.account_holder, + qr_code_url: agent.qr_code_url || '' + }; + + // 兼容旧的bankInfo字段 + const bankInfo = { + bank_name: agent.bank_name || '', + bank_account: agent.bank_account || agent.account_number, + account_holder: agent.account_holder + }; + + res.json({ + success: true, + data: { + ...stats, + paymentInfo: paymentInfo, + bank_info: bankInfo // 保持向后兼容 + } + }); + } catch (error) { + console.error('获取佣金统计失败:', error); + res.status(500).json({ success: false, message: '获取佣金统计失败' }); + } +}); + +/** + * 更新收款方式信息 + */ +router.put('/payment-info', auth, requireAgent, async (req, res) => { + try { + const { payment_type, bank_name, account_number, account_holder, qr_code_url } = req.body; + const agentId = req.agent.id; + + // 验证收款方式类型 + const validPaymentTypes = ['bank', 'wechat', 'alipay', 'unionpay']; + if (!validPaymentTypes.includes(payment_type)) { + return res.status(400).json({ success: false, message: '收款方式类型不正确' }); + } + + // 根据收款方式类型进行不同的验证 + if (payment_type === 'bank') { + // 银行卡验证 + if (!bank_name || !account_number || !account_holder) { + return res.status(400).json({ success: false, message: '银行信息不完整' }); + } + // 验证银行账号格式(简单验证) + if (!/^\d{10,25}$/.test(account_number.replace(/\s/g, ''))) { + return res.status(400).json({ success: false, message: '银行账号格式不正确' }); + } + } else { + // 收款码验证 + if (!account_holder || !qr_code_url) { + return res.status(400).json({ success: false, message: '收款码信息不完整' }); + } + } + + // 更新收款方式信息 + await db.query( + 'UPDATE regional_agents SET payment_type = ?, bank_name = ?, account_number = ?, account_holder = ?, qr_code_url = ? WHERE id = ?', + [payment_type, bank_name, account_number, account_holder, qr_code_url, agentId] + ); + + res.json({ + success: true, + message: '收款方式信息更新成功' + }); + } catch (error) { + console.error('更新收款方式信息失败:', error); + res.status(500).json({ success: false, message: '更新收款方式信息失败' }); + } +}); + +/** + * 上传收款码图片 + */ +router.post('/upload-qr-code', auth, requireAgent, upload.single('qrCode'), async (req, res) => { + try { + if (!req.file) { + return res.status(400).json({ success: false, message: '请选择要上传的图片' }); + } + + // 构建文件访问URL + const fileUrl = `/uploads/qr-codes/${req.file.filename}`; + + res.json({ + success: true, + message: '收款码上传成功', + data: { + url: fileUrl, + filename: req.file.filename + } + }); + } catch (error) { + console.error('上传收款码失败:', error); + res.status(500).json({ success: false, message: '上传收款码失败' }); + } +}); + +/** + * 兼容旧的银行信息接口 + */ +router.put('/bank-info', auth, requireAgent, async (req, res) => { + try { + const agentId = req.agent.id; + const { bank_name, bank_account, account_holder } = req.body; + + // 验证必填字段 + if (!bank_name || !bank_account || !account_holder) { + return res.status(400).json({ success: false, message: '银行信息不完整' }); + } + + // 验证银行账号格式(简单验证) + if (!/^\d{10,25}$/.test(bank_account)) { + return res.status(400).json({ success: false, message: '银行账号格式不正确' }); + } + + // 更新银行信息 + await db.query( + 'UPDATE regional_agents SET payment_type = "bank", bank_name = ?, account_number = ?, account_holder = ?, bank_account = ? WHERE id = ?', + [bank_name, bank_account, account_holder, bank_account, agentId] + ); + + res.json({ + success: true, + message: '银行信息更新成功' + }); + } catch (error) { + console.error('更新银行信息失败:', error); + res.status(500).json({ success: false, message: '更新银行信息失败' }); + } +}); + +/** + * 申请提现 + */ +router.post('/apply', auth, requireAgent, async (req, res) => { + try { + const agentId = req.agent.id; + const { amount, apply_note } = req.body; + + // 验证提现金额 + if (!amount || amount <= 0) { + return res.status(400).json({ success: false, message: '提现金额必须大于0' }); + } + + if (amount < 10) { + return res.status(400).json({ success: false, message: '最低提现金额为100元' }); + } + + // 查询代理商信息和可提现金额 + const agentQuery = ` + SELECT + ra.*, + CAST(COALESCE(SUM(acr.commission_amount), 0) - COALESCE(ra.withdrawn_amount, 0) - COALESCE(ra.pending_withdrawal, 0) AS DECIMAL(10,2)) as available_amount + FROM regional_agents ra + LEFT JOIN agent_commission_records acr ON ra.id = acr.agent_id + WHERE ra.id = ? + GROUP BY ra.id + `; + + const agentResult = await db.query(agentQuery, [agentId]); + + if (!agentResult || agentResult.length === 0) { + return res.status(404).json({ success: false, message: '代理商信息不存在' }); + } + + const agent = agentResult[0]; + + // 检查收款方式信息是否完整 + const paymentType = agent.payment_type || 'bank'; + + if (paymentType === 'bank') { + // 银行卡收款方式验证 + if (!agent.bank_name || !agent.account_number || !agent.account_holder) { + return res.status(400).json({ success: false, message: '请先完善银行信息' }); + } + } else { + // 收款码收款方式验证 + if (!agent.account_holder || !agent.qr_code_url) { + return res.status(400).json({ success: false, message: '请先完善收款码信息' }); + } + } + + // 检查可提现金额 + if (amount > agent.available_amount) { + return res.status(400).json({ + success: false, + message: `可提现金额不足,当前可提现:¥${agent.available_amount}` + }); + } + + // 开始事务 + const pool = getDB(); + const connection = await pool.getConnection(); + await connection.beginTransaction(); + + try { + // 创建提现申请 + await connection.execute( + 'INSERT INTO agent_withdrawals (agent_id, amount, payment_type, bank_name, account_number, account_holder, qr_code_url, apply_note, bank_account) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)', + [agentId, amount, paymentType, agent.bank_name || '', agent.account_number, agent.account_holder, agent.qr_code_url, apply_note || null, agent.account_number] + ); + + // 更新代理的待提现金额 + await connection.execute( + 'UPDATE regional_agents SET pending_withdrawal = pending_withdrawal + ? WHERE id = ?', + [amount, agentId] + ); + + await connection.commit(); + connection.release(); // 释放连接回连接池 + + res.json({ + success: true, + message: '提现申请提交成功,请等待审核', + data: { + paymentType: paymentType + } + }); + } catch (error) { + await connection.rollback(); + connection.release(); // 释放连接回连接池 + throw error; + } + } catch (error) { + console.error('申请提现失败:', error); + res.status(500).json({ success: false, message: '申请提现失败' }); + } +}); + +/** + * 获取提现记录 + */ +router.get('/records', auth, requireAgent, async (req, res) => { + try { + const agentId = req.agent.id; + const { page = 1, limit = 20, status } = req.query; + const pageNum = parseInt(page) || 1; + const limitNum = parseInt(limit) || 20; + const offset = (pageNum - 1) * limitNum; + + // 构建查询条件 + let whereConditions = ['agent_id = ?']; + let queryParams = [agentId]; + + if (status) { + whereConditions.push('status = ?'); + queryParams.push(status); + } + + const whereClause = whereConditions.join(' AND '); + + // 查询提现记录 + const recordsQuery = ` + SELECT + id, + amount, + payment_type, + bank_name, + account_number, + account_holder, + qr_code_url, + status, + apply_note, + admin_note, + created_at, + processed_at, + bank_account + FROM agent_withdrawals + WHERE ${whereClause} + ORDER BY created_at DESC + LIMIT ${limitNum} OFFSET ${offset} + `; + + const records = await db.query(recordsQuery, queryParams); + + // 处理记录数据,兼容旧格式 + const processedRecords = records.map(record => ({ + ...record, + payment_type: record.payment_type || 'bank', + account_number: record.account_number || record.bank_account, + qr_code_url: record.qr_code_url || '', + // 保持向后兼容 + bank_account: record.bank_account || record.account_number + })); + + // 查询总数 + const totalResult = await db.query( + `SELECT COUNT(*) as total FROM agent_withdrawals WHERE ${whereClause}`, + queryParams + ); + const total = totalResult && totalResult.length > 0 ? totalResult[0].total : 0; + + res.json({ + success: true, + data: { + records: processedRecords, + total: parseInt(total) + } + }); + } catch (error) { + console.error('获取提现记录失败:', error); + res.status(500).json({ success: false, message: '获取提现记录失败' }); + } +}); + +/** + * 获取佣金明细 + */ +router.get('/commissions', auth, requireAgent, async (req, res) => { + try { + const agentId = req.agent.id; + const { page = 1, limit = 20 } = req.query; + const pageNum = parseInt(page) || 1; + const limitNum = parseInt(limit) || 20; + const offset = (pageNum - 1) * limitNum; + + // 查询佣金记录 + const commissionsQuery = ` + SELECT + acr.*, + u.real_name as merchant_name, + CONCAT(SUBSTRING(u.phone, 1, 3), '****', SUBSTRING(u.phone, -4)) as merchant_phone_masked + FROM agent_commission_records acr + JOIN users u ON acr.merchant_id = u.id + WHERE acr.agent_id = ? + ORDER BY acr.created_at DESC + LIMIT ${limitNum} OFFSET ${offset} + `; + + const commissions = await db.query(commissionsQuery, [agentId]); + + // 查询总数 + const totalResult = await db.query( + 'SELECT COUNT(*) as total FROM agent_commission_records WHERE agent_id = ?', + [agentId] + ); + const total = totalResult && totalResult.length > 0 ? totalResult[0].total : 0; + + res.json({ + success: true, + data: { + commissions, + total: parseInt(total) + } + }); + } catch (error) { + console.error('获取佣金明细失败:', error); + res.status(500).json({ success: false, message: '获取佣金明细失败' }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/routes/agents.js b/routes/agents.js new file mode 100644 index 0000000..76147c7 --- /dev/null +++ b/routes/agents.js @@ -0,0 +1,777 @@ +const express = require('express'); +const router = express.Router(); +const {getDB} = require('../database'); +const QRCode = require('qrcode'); +const crypto = require('crypto'); +const bcrypt = require('bcryptjs'); +const {auth} = require('../middleware/auth'); +const dayjs = require('dayjs'); + +// 获取浙江省所有区域列表 +router.get('/regions', async (req, res) => { + try { + const [regions] = await getDB().execute( + 'SELECT * FROM zhejiang_regions ORDER BY city_name, district_name' + ); + res.json({success: true, data: regions}); + } catch (error) { + console.error('获取区域列表失败:', error); + res.status(500).json({success: false, message: '获取区域列表失败'}); + } +}); + +// 申请成为区域代理 +router.post('/apply', async (req, res) => { + try { + const {region_id, real_name, phone, id_card, contact_address} = req.body; + + if (!region_id || !real_name || !phone || !id_card) { + return res.status(400).json({success: false, message: '请填写完整信息'}); + } + + // 检查该区域是否已有代理(包括所有状态,不仅仅是active) + const [existingRegionAgent] = await getDB().execute( + 'SELECT id, status FROM regional_agents WHERE region_id = ? AND status IN ("pending", "active")', + [region_id] + ); + + if (existingRegionAgent.length > 0) { + const status = existingRegionAgent[0].status; + if (status === 'active') { + return res.status(400).json({ + success: false, + message: '该区域已有激活的代理,每个区域只能有一个代理账号' + }); + } else if (status === 'pending') { + return res.status(400).json({ + success: false, + message: '该区域已有待审核的代理申请,每个区域只能有一个代理账号' + }); + } + } + + // 检查手机号是否已存在用户 + const [existingUser] = await getDB().execute( + 'SELECT id FROM users WHERE phone = ?', + [phone] + ); + + let userId; + if (existingUser.length > 0) { + userId = existingUser[0].id; + + // 检查该用户是否已申请过代理(包括所有状态) + const [existingUserAgent] = await getDB().execute( + 'SELECT id, status, region_id FROM regional_agents WHERE user_id = ?', + [userId] + ); + + if (existingUserAgent.length > 0) { + const agentStatus = existingUserAgent[0].status; + if (agentStatus === 'active') { + return res.status(400).json({ + success: false, + message: '该用户已是其他区域的激活代理,一个用户只能申请一个区域的代理' + }); + } else if (agentStatus === 'pending') { + return res.status(400).json({ + success: false, + message: '该用户已有待审核的代理申请,一个用户只能申请一个区域的代理' + }); + } else if (agentStatus === 'suspended' || agentStatus === 'terminated') { + return res.status(400).json({ + success: false, + message: '该用户的代理资格已被暂停或终止,无法重新申请' + }); + } + } + } else { + // 创建新用户(为代理申请用户生成临时密码) + const bcrypt = require('bcryptjs'); + const tempPassword = Math.random().toString(36).slice(-8); // 生成8位临时密码 + const hashedPassword = await bcrypt.hash(tempPassword, 10); + + const [userResult] = await getDB().execute( + 'INSERT INTO users (username, password, phone, real_name, id_card, created_at) VALUES (?, ?, ?, ?, ?, NOW())', + [phone, hashedPassword, phone, real_name, id_card] + ); + userId = userResult.insertId; + } + + // 生成代理编码 + const agentCode = 'AG' + Date.now().toString().slice(-8); + + // 创建代理申请 + await getDB().execute( + 'INSERT INTO regional_agents (user_id, region_id, agent_code, status, created_at) VALUES (?, ?, ?, "pending", NOW())', + [userId, region_id, agentCode] + ); + + res.json({success: true, message: '申请提交成功,请等待审核'}); + } catch (error) { + console.error('申请代理失败:', error); + res.status(500).json({success: false, message: '申请失败'}); + } +}); + +// 代理登录 +router.post('/login', async (req, res) => { + try { + const {phone, password} = req.body; + + if (!phone || !password) { + return res.status(400).json({success: false, message: '请输入手机号和密码'}); + } + + // 先查询用户和代理信息(包含密码用于验证) + const [agents] = await getDB().execute( + `SELECT ra.*, + u.id as user_id, + u.username, + u.phone, + u.real_name, + u.password, + u.role, + zr.name as city_name, + d.name as district_name + FROM regional_agents ra + JOIN users u ON ra.user_id = u.id + JOIN china_regions d ON d.code = u.district_id + JOIN china_regions zr ON ra.region_id = zr.code + WHERE u.phone = ? + AND ra.status = "active"`, + [phone] + ); + + if (agents.length === 0) { + return res.status(401).json({success: false, message: '手机号或密码错误,或账户未激活'}); + } + + const agent = agents[0]; + + // 验证密码 + const isPasswordValid = await bcrypt.compare(password, agent.password); + if (!isPasswordValid) { + return res.status(401).json({success: false, message: '手机号或密码错误,或账户未激活'}); + } + + // 生成JWT token + const jwt = require('jsonwebtoken'); + const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key'; + const token = jwt.sign( + { + userId: agent.user_id, + username: agent.username || agent.phone, + role: agent.role || 'agent', + agentId: agent.id + }, + JWT_SECRET, + {expiresIn: '24h'} + ); + + delete agent.password; // 不返回密码 + + res.json({ + success: true, + data: { + ...agent, + token + }, + message: '登录成功' + }); + } catch (error) { + console.error('代理登录失败:', error); + res.status(500).json({success: false, message: '登录失败'}); + } +}); + +// 获取代理的商户列表(包含所有商户,标注早期商户状态) +router.get('/merchants/:agent_id',auth, async (req, res) => { + try { + const {agent_id} = req.params; + const {page = 1, limit = 10} = req.query; + const offset = (page - 1) * limit; + + // 首先获取代理的注册时间 + const [agentInfo] = await getDB().execute( + `SELECT ra.created_at as agent_created_at, ra.region_id, ra.user_id + FROM regional_agents ra + WHERE ra.id = ?`, + [parseInt(agent_id)] + ); + const regionId = agentInfo[0].region_id; + + const userId = agentInfo[0].user_id; + + if (!agentInfo || agentInfo.length === 0) { + return res.status(404).json({success: false, message: '代理不存在'}); + } + + const agentCreatedAt = agentInfo[0].agent_created_at; + + // 获取商户列表(包含所有商户,包括agent_merchants表中的和符合条件的早期商户) + const [merchants] = await getDB().execute( + `SELECT u.id, + u.username, + u.phone, + u.real_name, + u.created_at, + u.audit_status, + IFNULL(u.created_at, '未关联') as joined_at, + CASE + WHEN u.created_at < ? AND u.district_id = ? THEN 1 + ELSE 0 + END as is_early_merchant, + CASE + WHEN u.created_at < ? AND u.district_id = ? THEN '早期商户(不记录佣金)' + ELSE '正常商户' + END as merchant_status, + (SELECT COUNT(*) + FROM matching_orders + WHERE initiator_id = u.id + AND status = 'completed') as completed_matches + + FROM users u + WHERE (u.inviter = ? OR (u.created_at < ? AND u.district_id = ? AND u.role = 'user')) + ORDER BY u.created_at DESC + LIMIT ${parseInt(limit)} OFFSET ${parseInt(offset)}`, + [agentCreatedAt, parseInt(regionId), agentCreatedAt, parseInt(regionId), parseInt(userId), agentCreatedAt, parseInt(regionId)] + ); + + // 获取总数(包括代理关联的商户和符合条件的早期商户) + const [countResult] = await getDB().execute( + `SELECT COUNT(*) as total + FROM users u + WHERE (u.inviter = ? OR (u.created_at < ? AND u.district_id = ?))`, + [parseInt(userId), agentCreatedAt, parseInt(regionId)] + ); + + // 获取早期商户统计(从user表获取所有符合条件的早期商户) + // 早期商户的判断条件:1.早期商户注册时间比代理要早。2.代理商代理的区县与商户的区县一致 + const [earlyMerchantStats] = await getDB().execute( + `SELECT COUNT(*) as early_merchant_count + FROM users u + WHERE u.created_at < ? + AND u.district_id = ? + AND u.role = 'user'`, + [agentCreatedAt, parseInt(regionId)] + ); + + // 获取正常商户统计(包括代理关联的商户,排除符合条件的早期商户) + const [normalMerchantStats] = await getDB().execute( + `SELECT COUNT(*) as normal_merchant_count + FROM users u + WHERE (u.inviter = ? AND (u.created_at >= ? OR u.district_id != ?))`, + [parseInt(userId), agentCreatedAt, parseInt(regionId)] + ); + + res.json({ + success: true, + data: { + merchants, + total: parseInt(countResult[0].total), + page: parseInt(page), + limit: parseInt(limit), + stats: { + total_merchants: parseInt(countResult[0].total), + early_merchants: parseInt(earlyMerchantStats[0].early_merchant_count), + normal_merchants: parseInt(normalMerchantStats[0].normal_merchant_count) + } + } + }); + } catch (error) { + console.error('获取商户列表失败:', error); + res.status(500).json({success: false, message: '获取商户列表失败'}); + } +}); + +// 获取代理的佣金记录 +router.get('/commissions/:agent_id',auth, async (req, res) => { + try { + const {agent_id} = req.params; + const {id} = req.user + const {page = 1, limit = 10} = req.query; + const offset = (page - 1) * limit; + + // 获取佣金记录 + const [commissions] = await getDB().execute( + `SELECT acr.*, u.username, u.real_name + FROM transfers acr + JOIN users u ON acr.from_user_id = u.id + WHERE acr.to_user_id = ${parseInt(id)} AND source_type='agent' + ORDER BY acr.created_at DESC + LIMIT ${parseInt(limit)} OFFSET ${parseInt(offset)}` + ); + + // 获取总数和总佣金 + const [summary] = await getDB().execute( + `SELECT COUNT(*) as total_records, + COALESCE(SUM(amount), 0) as total_commission + FROM transfers + WHERE source_type = 'agent' AND to_user_id=${id}` + ); + + // 由于agent_commission_records表没有status字段,设置默认值 + summary[0].paid_commission = summary[0].total_commission; + summary[0].pending_commission = 0; + + res.json({ + success: true, + data: { + commissions, + summary: summary[0], + page: parseInt(page), + limit: parseInt(limit) + } + }); + } catch (error) { + console.error('获取佣金记录失败:', error); + res.status(500).json({success: false, message: '获取佣金记录失败'}); + } +}); + +// 获取代理统计信息 +router.get('/stats/:agent_id', auth,async (req, res) => { + try { + const {agent_id} = req.params; + + // 获取统计数据 + const [stats] = await getDB().execute( + `SELECT (SELECT COUNT(*) FROM agent_merchants WHERE agent_id = ${parseInt(agent_id)}) as total_merchants, + (SELECT COUNT(*) + FROM agent_merchants am + JOIN users u ON am.merchant_id = u.id + WHERE am.agent_id = ${parseInt(agent_id)} + AND u.audit_status = 'approved') as approved_merchants, + (SELECT COALESCE(SUM(commission_amount), 0) + FROM agent_commission_records + WHERE agent_id = ${parseInt(agent_id)}) as total_commission, + (SELECT COALESCE(SUM(commission_amount), 0) + FROM agent_commission_records + WHERE agent_id = ${parseInt(agent_id)}) as paid_commission, + (SELECT COUNT(*) + FROM registration_codes rc + JOIN regional_agents ra ON rc.agent_id = ra.user_id + WHERE ra.id = ${parseInt(agent_id)} + AND rc.is_used = 1) as used_codes, + (SELECT COUNT(*) + FROM registration_codes rc + JOIN regional_agents ra ON rc.agent_id = ra.user_id + WHERE ra.id = ${parseInt(agent_id)} + AND rc.is_used = 0 + AND rc.expires_at > NOW()) as active_codes` + ); + + res.json({success: true, data: stats[0]}); + } catch (error) { + console.error('获取统计信息失败:', error); + res.status(500).json({success: false, message: '获取统计信息失败'}); + } +}); + +// 获取代理列表 +router.get('/list', auth,async (req, res) => { + try { + const {page = 1, limit = 10, status, region_id} = req.query; + const offset = (page - 1) * limit; + + let whereClause = '1=1'; + let params = []; + + if (status) { + whereClause += ' AND ra.status = ?'; + params.push(status); + } + + if (region_id) { + whereClause += ' AND ra.region_id = ?'; + params.push(region_id); + } + + // 获取代理列表 + const [agents] = await getDB().execute( + `SELECT ra.*, + u.username, + u.phone, + u.real_name, + u.created_at as user_created_at, + zr.city_name, + zr.district_name, + zr.region_code + FROM regional_agents ra + JOIN users u ON ra.user_id = u.id + JOIN zhejiang_regions zr ON ra.region_id = zr.id + WHERE ${whereClause} + ORDER BY ra.created_at DESC + LIMIT ${limit} OFFSET ${offset}` + ); + + // 获取总数 + const [countResult] = await getDB().execute( + `SELECT COUNT(*) as total + FROM regional_agents ra + JOIN users u ON ra.user_id = u.id + JOIN zhejiang_regions zr ON ra.region_id = zr.id + WHERE ${whereClause}` + ); + + const total = countResult[0].total; + const totalPages = Math.ceil(total / limit); + + res.json({ + success: true, + data: { + agents, + pagination: { + page: parseInt(page), + limit: parseInt(limit), + total, + totalPages + } + } + }); + } catch (error) { + console.error('获取代理列表失败:', error); + res.status(500).json({success: false, message: '获取代理列表失败'}); + } +}); + +/** + * 获取代理佣金趋势数据 + * @route GET /agents/commission-trend/:agent_id + * @param {string} agent_id - 代理ID + * @param {string} period - 时间周期 (7d, 30d, 3m) + * @returns {Object} 佣金趋势数据 + */ +router.get('/commission-trend/:agent_id',auth, async (req, res) => { + try { + console.log(req.params, 'req.params') + const {agent_id} = req.params; + let [agentUserInfo] = await getDB().execute(` + SELECT u.* + FROM regional_agents as rg + LEFT JOIN users u ON rg.user_id = u.id + WHERE rg.id = ${agent_id} + `) + let userId = agentUserInfo[0].id + console.log(userId, 'userId') + const {period = '7d'} = req.query; + let days; + switch (period) { + case '7d': + days = 7; + break; + case '30d': + days = 30; + break; + case '3m': + days = 90; + break; + default: + days = 7; + } + + console.log(period, 'period') + + const [trendData] = await getDB().execute(` + SELECT DATE(created_at) as date, + CAST(COALESCE(SUM(amount), 0) AS DECIMAL(10, 2)) as amount + FROM transfers + WHERE to_user_id = ? + AND created_at >= DATE_SUB(CURDATE(), INTERVAL ? DAY) + AND source_type = 'user_to_agent' + GROUP BY DATE(created_at) + ORDER BY date ASC + `, [userId, parseInt(days)]); + + // 填充缺失的日期(佣金为0) + const result = []; + + for (let i = days - 1; i >= 0; i--) { + const date = dayjs().subtract(i, 'day'); + const dateStr = date.format('YYYY-MM-DD'); + + const existingData = trendData.find(item => { + const itemDateStr = dayjs(item.date).format('YYYY-MM-DD'); + return itemDateStr === dateStr; + }); + + result.push({ + date: date.format('MM-DD'), + amount: existingData ? parseFloat(existingData.amount) : 0 + }); + } + + res.json({ + success: true, + data: result + }); + + } catch (error) { + console.log(error); + + res.status(500).json({ + success: false, + message: '获取趋势数据失败' + }); + } +}); + +/** + * 获取代理商户状态分布数据 + * @route GET /agents/merchant-status/:agent_id + * @param {string} agent_id - 代理ID + * @returns {Object} 商户状态分布数据 + */ +router.get('/merchant-status/:agent_id',auth, async (req, res) => { + try { + const {agent_id} = req.params; + let [agent] = await getDB().execute(` + SELECT u.* + FROM users as u + LEFT JOIN regional_agents as rg ON rg.user_id = u.id + WHERE rg.id = ${agent_id} + `) + let userId = agent[0].id + // 获取商户状态分布 + const [statusData] = await getDB().execute( + `SELECT CASE + WHEN am.audit_status = 'approved' THEN '已审核' + WHEN am.audit_status = 'pending' THEN '待审核' + WHEN am.audit_status = 'rejected' THEN '已拒绝' + ELSE '未知状态' + END as status, + COUNT(*) as count + FROM users am + WHERE am.inviter = ? + GROUP BY am.audit_status + ORDER BY count DESC`, + [parseInt(userId)] + ); + + res.json({ + success: true, + data: statusData + }); + } catch (error) { + console.error('获取商户状态分布数据失败:', error); + res.status(500).json({success: false, message: '获取商户状态分布数据失败'}); + } +}); + +/** + * 获取代理详细统计数据(包含更多维度) + * @route GET /agents/detailed-stats/:agent_id + * @param {string} agent_id - 代理ID + * @returns {Object} 详细统计数据 + */ +router.get('/detailed-stats/:agent_id',auth, async (req, res) => { + try { + const {agent_id} = req.params; + let [agentUserInfo] = await getDB().execute(` + SELECT u.* + FROM users as u + LEFT JOIN regional_agents as ag ON ag.user_id = u.id + WHERE ag.id = ${agent_id} + `) + let agUserInfo = agentUserInfo[0]; + // 获取基础统计数据 + const [basicStats] = await getDB().execute( + `SELECT (SELECT COUNT(*) FROM users WHERE inviter = ?) as total_merchants, + (SELECT COUNT(*) + FROM users am + WHERE am.inviter = ? + AND am.audit_status = 'approved') as approved_merchants, + (SELECT COUNT(*) + FROM users am + WHERE am.inviter = ? + AND am.audit_status = 'pending') as pending_merchants, + (SELECT COUNT(*) + FROM users am + WHERE am.inviter = ? + AND am.audit_status = 'rejected') as rejected_merchants, + (SELECT COALESCE(SUM(amount), 0) + FROM transfers + WHERE transfer_type = 'user_to_agent' + AND to_user_id = ?) as total_commission + `, + [parseInt(agUserInfo.id), parseInt(agUserInfo.id), parseInt(agUserInfo.id), parseInt(agUserInfo.id), parseInt(agUserInfo.id)] + ); + + // 获取本月营收 + const [monthlyStats] = await getDB().execute( + `SELECT COALESCE(SUM(amount), 0) as monthly_commission, + COUNT(*) as monthly_commission_records + FROM transfers + WHERE to_user_id = ? + AND transfer_type = 'user_to_agent' + AND YEAR(created_at) = YEAR(CURDATE()) + AND MONTH(created_at) = MONTH(CURDATE())`, + [parseInt(agUserInfo.id)] + ); + + // 获取今日佣金 + const [dailyStats] = await getDB().execute( + `SELECT COALESCE(SUM(amount), 0) as daily_commission, + COUNT(*) as daily_commission_records + FROM transfers + WHERE to_user_id = ? + AND transfer_type = 'user_to_agent' + AND DATE(created_at) = CURDATE()`, + [parseInt(agUserInfo.id)] + ); + + // 获取最近7天新增商户数 + const [weeklyMerchants] = await getDB().execute( + `SELECT COUNT(*) as weekly_new_merchants + FROM users am + WHERE am.inviter = ? + AND am.created_at >= DATE_SUB(CURDATE(), INTERVAL 7 DAY)`, + [parseInt(agUserInfo.id)] + ); + + + // 合并所有统计数据 + const stats = { + ...basicStats[0], + ...monthlyStats[0], + ...dailyStats[0], + ...weeklyMerchants[0] + }; + + res.json({ + success: true, + data: stats + }); + } catch (error) { + console.error('获取详细统计数据失败:', error); + res.status(500).json({success: false, message: '获取详细统计数据失败'}); + } +}); + +/** + * 获取代理商户的转账记录 + * @route GET /agents/merchants/:agent_id/transfers + * @param {string} agent_id - 代理ID + * @param {string} page - 页码 + * @param {string} limit - 每页数量 + * @returns {Object} 转账记录列表 + */ +router.get('/merchants/:agent_id/transfers', auth,async (req, res) => { + try { + const {agent_id} = req.params; + const {page = 1, limit = 10} = req.query; + const pageNum = parseInt(page) || 1; + const limitNum = parseInt(limit) || 10; + const offset = (pageNum - 1) * limitNum; + + // 检查代理是否存在 + const [agentResult] = await getDB().execute( + 'SELECT * FROM regional_agents WHERE id = ?', + [parseInt(agent_id)] + ); + + if (agentResult.length === 0) { + return res.status(404).json({success: false, message: '代理不存在'}); + } + + // 查询商户转账记录 + const transferQuery = ` + SELECT t.id, + t.from_user_id, + t.to_user_id, + t.amount, + t.status, + t.transfer_type, + t.description, + t.created_at, + t.confirmed_at, + from_user.real_name as from_real_name, + from_user.phone as from_phone, + to_user.real_name as to_real_name, + to_user.phone as to_phone + FROM agent_merchants am + JOIN transfers t ON am.merchant_id = t.from_user_id + LEFT JOIN users from_user ON t.from_user_id = from_user.id + LEFT JOIN users to_user ON t.to_user_id = to_user.id + WHERE am.agent_id = ? + ORDER BY t.created_at DESC + LIMIT ${limitNum} OFFSET ${offset} + `; + const [transfers] = await getDB().execute(transferQuery, [parseInt(agent_id)]); + + // 查询总数 + const [totalResult] = await getDB().execute( + `SELECT COUNT(*) as total + FROM agent_merchants am + JOIN transfers t ON am.merchant_id = t.from_user_id + WHERE am.agent_id = ?`, + [parseInt(agent_id)] + ); + + const total = totalResult[0].total; + + res.json({ + success: true, + data: { + transfers, + pagination: { + page: pageNum, + limit: limitNum, + total, + pages: Math.ceil(total / limitNum) + } + } + }); + } catch (error) { + console.error('获取代理商户转账记录失败:', error); + res.status(500).json({success: false, message: '获取代理商户转账记录失败,请稍后再试'}); + } +}); +/** + * 获取分销列表 + * @route GET /agents/distribution + * @returns {Object} 分销列表 + */ +router.get('/distribution', auth, async (req, res) => { + try { + + const {page = 1, size = 10, user_id} = req.query; + const {id} = user_id || req.user; + const pageNum = parseInt(page) || 1; + const limitNum = parseInt(size) || 10; + const offset = (page - 1) * size; + const [result] = await getDB().execute( + `SELECT real_name, phone, username, avatar, created_at, id as user_id + FROM users + WHERE inviter = ? + ORDER BY created_at DESC + LIMIT ${size} OFFSET ${offset}`, + [parseInt(id)] + ); + const [totalResult] = await getDB().execute( + `SELECT COUNT(*) as total + FROM users + WHERE inviter = ? `, + [parseInt(id)] + ); + result.forEach(item => { + item.created_at = dayjs(item.created_at).format('YYYY-MM-DD HH:mm:ss'); + }) + + const total = totalResult[0].total; + res.json({ + success: true, data: result, pagination: { + page: pageNum, + limit: limitNum, + total, + pages: Math.ceil(total / limitNum) + } + }); + } catch (error) { + console.error('获取分销列表失败:', error); + res.status(500).json({success: false, message: '获取分销列表失败'}); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/routes/agents/agents.js b/routes/agents/agents.js new file mode 100644 index 0000000..921e094 --- /dev/null +++ b/routes/agents/agents.js @@ -0,0 +1,722 @@ +const express = require('express'); +const router = express.Router(); +const { getDB } = require('../../database'); +const bcrypt = require('bcryptjs'); +const { auth, adminAuth } = require('../../middleware/auth'); +const dayjs = require('dayjs'); + +// 创建管理员认证中间件组合 +const authenticateAdmin = [auth, adminAuth]; + +// 获取数据库连接 +const db = { + query: async (sql, params = []) => { + const connection = getDB(); + const [rows] = await connection.execute(sql, params); + return rows; + } +}; + +// 获取代理列表和统计信息 +router.get('/', authenticateAdmin, async (req, res) => { + try { + const { page = 1, limit = 20, status, city, search,district } = req.query; + const pageNum = parseInt(page) || 1; + const limitNum = parseInt(limit) || 20; + const offset = (pageNum - 1) * limitNum; + + // 构建查询条件 + let whereConditions = []; + let queryParams = []; + + if (status) { + whereConditions.push('ra.status = ?'); + queryParams.push(status); + } + + if (district) { + whereConditions.push('zr.name = ?'); + queryParams.push(district); + } + + if (search) { + whereConditions.push('(u.real_name LIKE ? OR u.phone LIKE ?)'); + queryParams.push(`%${search}%`, `%${search}%`); + } + + const whereClause = whereConditions.length > 0 ? `WHERE ${whereConditions.join(' AND ')}` : ''; + + // 查询代理列表 + const agentsQuery = ` + SELECT ra.*, + u.real_name, + u.phone, + u.id_card, + province.name AS province_name, + city.name AS city_name, + zr.name AS district_name, + (SELECT COUNT(DISTINCT merchant_id) + FROM agent_merchants + WHERE agent_id = ra.id) as merchant_count, + (SELECT CAST(COALESCE(SUM(commission_amount), 0) AS DECIMAL(10, 2)) + FROM agent_commission_records + WHERE agent_id = ra.id) as total_commission, + 0 as paid_commission, + (SELECT CAST(COALESCE(SUM(commission_amount), 0) AS DECIMAL(10, 2)) + FROM agent_commission_records + WHERE agent_id = ra.id) as pending_commission + FROM regional_agents ra + LEFT JOIN users u ON ra.user_id = u.id + LEFT JOIN china_regions zr ON ra.region_id = zr.code -- 区 + LEFT JOIN china_regions city ON zr.parent_code = city.code -- 市 + LEFT JOIN china_regions province ON city.parent_code = province.code -- 省 + ${whereClause} + ORDER BY ra.created_at + DESC + LIMIT ${limitNum} OFFSET ${offset};`; + console.log(agentsQuery,queryParams) + const agents = await db.query(agentsQuery, queryParams); + + // 查询总数 + const countQuery = ` + SELECT COUNT(DISTINCT ra.id) as total + FROM regional_agents ra + LEFT JOIN users u ON ra.user_id = u.id + LEFT JOIN china_regions zr ON ra.region_id = zr.code + ${whereClause} + `; + + const totalResult = await db.query(countQuery, queryParams); + const total = totalResult && totalResult.length > 0 ? totalResult[0].total : 0; + + // 查询统计信息 + const statsQuery = ` + SELECT + COUNT(*) as total_agents, + COUNT(CASE WHEN status = 'pending' THEN 1 END) as pending_agents, + COUNT(CASE WHEN status = 'active' THEN 1 END) as active_agents, + CAST(COALESCE(SUM(commission_stats.total_commission), 0) AS DECIMAL(10,2)) as total_commission + FROM regional_agents ra + LEFT JOIN ( + SELECT + agent_id, + SUM(commission_amount) as total_commission + FROM agent_commission_records + GROUP BY agent_id + ) commission_stats ON ra.id = commission_stats.agent_id + `; + + const statsResult = await db.query(statsQuery); + const stats = statsResult && statsResult.length > 0 ? statsResult[0] : { + total_agents: 0, + pending_agents: 0, + active_agents: 0, + total_commission: 0 + }; + + res.json({ + success: true, + data: { + agents, + total: parseInt(total), + stats + } + }); + } catch (error) { + console.error('获取代理列表失败:', error); + res.status(500).json({ success: false, message: '获取代理列表失败' }); + } +}); + +// 获取代理详情 +router.get('/:id', authenticateAdmin, async (req, res) => { + try { + const { id } = req.params; + + const agentQuery = ` + SELECT + ra.*, + u.real_name as name, + u.phone, + u.id_card, + CONCAT(u.city, ' ', zr.district_name) as address, + zr.city_name, + zr.district_name, + ( + SELECT COUNT(DISTINCT merchant_id) + FROM agent_merchants + WHERE agent_id = ra.id + ) as merchant_count, + ( + SELECT COALESCE(SUM(commission_amount), 0) + FROM agent_commission_records + WHERE agent_id = ra.id + ) as total_commission, + 0 as paid_commission, + ( + SELECT COALESCE(SUM(commission_amount), 0) + FROM agent_commission_records + WHERE agent_id = ra.id + ) as pending_commission + FROM regional_agents ra + LEFT JOIN users u ON ra.user_id = u.id + LEFT JOIN zhejiang_regions zr ON ra.region_id = zr.id + WHERE ra.id = ? + `; + + const agentResult = await db.query(agentQuery, [id]); + + if (!agentResult || agentResult.length === 0) { + return res.status(404).json({ success: false, message: '代理不存在' }); + } + + const agent = agentResult[0]; + + res.json({ + success: true, + data: agent + }); + } catch (error) { + console.error('获取代理详情失败:', error); + res.status(500).json({ success: false, message: '获取代理详情失败' }); + } +}); + +// 审核通过代理申请 +router.put('/:id/approve', authenticateAdmin, async (req, res) => { + try { + const { id } = req.params; + const { password } = req.body; + + if (!password || password.length < 6) { + return res.status(400).json({ success: false, message: '密码长度不能少于6位' }); + } + + // 检查代理是否存在且状态为待审核 + const agents = await db.query( + 'SELECT * FROM regional_agents WHERE id = ? AND status = "pending"', + [id] + ); + + if (!agents || agents.length === 0) { + return res.status(404).json({ success: false, message: '代理不存在或状态不正确' }); + } + + const agent = agents[0]; + + // 检查该区域是否已有其他激活的代理 + const existingActiveAgents = await db.query( + 'SELECT id FROM regional_agents WHERE region_id = ? AND status = "active" AND id != ?', + [agent.region_id, id] + ); + + if (existingActiveAgents && existingActiveAgents.length > 0) { + return res.status(400).json({ success: false, message: '该区域已有激活的代理,每个区域只能有一个代理账号' }); + } + + // 加密密码并更新用户表 + const hashedPassword = await bcrypt.hash(password, 10); + + // 更新用户密码 + await db.query( + `UPDATE users SET password = ? WHERE id = ( + SELECT user_id FROM regional_agents WHERE id = ? + )`, + [hashedPassword, id] + ); + + // 更新代理状态 + await db.query( + `UPDATE regional_agents + SET status = 'active', approved_at = NOW(), approved_by_admin_id = ? + WHERE id = ?`, + [req.user.id, id] + ); + + res.json({ + success: true, + message: '代理申请已通过' + }); + } catch (error) { + console.error('审核代理申请失败:', error); + res.status(500).json({ success: false, message: '审核代理申请失败' }); + } +}); + +// 拒绝代理申请 +router.put('/:id/reject', authenticateAdmin, async (req, res) => { + try { + const { id } = req.params; + const { reason } = req.body; + + if (!reason || reason.trim() === '') { + return res.status(400).json({ success: false, message: '请输入拒绝原因' }); + } + + // 检查代理是否存在且状态为待审核 + const agentResult = await db.query( + 'SELECT * FROM regional_agents WHERE id = ? AND status = "pending"', + [id] + ); + + if (!agentResult || agentResult.length === 0) { + return res.status(404).json({ success: false, message: '代理不存在或状态不正确' }); + } + + const agent = agentResult[0]; + + // 更新代理状态 + await db.query( + `UPDATE regional_agents + SET status = 'rejected', reject_reason = ?, rejected_at = NOW(), rejected_by_admin_id = ? + WHERE id = ?`, + [reason.trim(), req.user.id, id] + ); + + res.json({ + success: true, + message: '代理申请已拒绝' + }); + } catch (error) { + console.error('拒绝代理申请失败:', error); + res.status(500).json({ success: false, message: '拒绝代理申请失败' }); + } +}); + +// 禁用代理 +router.put('/:id/disable', authenticateAdmin, async (req, res) => { + try { + const { id } = req.params; + + // 检查代理是否存在且状态为激活 + const agentResult = await db.query( + 'SELECT * FROM regional_agents WHERE id = ? AND status = "active"', + [id] + ); + + if (!agentResult || agentResult.length === 0) { + return res.status(404).json({ success: false, message: '代理不存在或状态不正确' }); + } + + const agent = agentResult[0]; + + // 更新代理状态 + await db.query( + 'UPDATE regional_agents SET status = "disabled", disabled_at = NOW() WHERE id = ?', + [id] + ); + + res.json({ + success: true, + message: '代理已禁用' + }); + } catch (error) { + console.error('禁用代理失败:', error); + res.status(500).json({ success: false, message: '禁用代理失败' }); + } +}); + +// 启用代理 +router.put('/:id/enable', authenticateAdmin, async (req, res) => { + try { + const { id } = req.params; + + // 检查代理是否存在且状态为禁用 + const agentResult = await db.query( + 'SELECT * FROM regional_agents WHERE id = ? AND status = "disabled"', + [id] + ); + + if (!agentResult || agentResult.length === 0) { + return res.status(404).json({ success: false, message: '代理不存在或状态不正确' }); + } + + const agent = agentResult[0]; + + // 检查该区域是否已有其他激活的代理 + const existingActiveAgentResult = await db.query( + 'SELECT id FROM regional_agents WHERE region_id = ? AND status = "active" AND id != ?', + [agent.region_id, id] + ); + + if (existingActiveAgentResult && existingActiveAgentResult.length > 0) { + return res.status(400).json({ success: false, message: '该区域已有激活的代理,每个区域只能有一个代理账号' }); + } + + // 更新代理状态 + await db.query( + 'UPDATE regional_agents SET status = "active", disabled_at = NULL WHERE id = ?', + [id] + ); + + res.json({ + success: true, + message: '代理已启用' + }); + } catch (error) { + console.error('启用代理失败:', error); + res.status(500).json({ success: false, message: '启用代理失败' }); + } +}); + +// 获取代理商户列表 +router.get('/:id/merchants', authenticateAdmin, async (req, res) => { + try { + const { id } = req.params; + const { page = 1, limit = 20 } = req.query; + const pageNum = parseInt(page) || 1; + const limitNum = parseInt(limit) || 20; + const offset = (pageNum - 1) * limitNum; + + // 检查代理是否存在 + const agentResult = await db.query(`SELECT * FROM users WHERE id = ? AND (user_type='agent' OR user_type='agent_directly')`, [id]); + if (!agentResult || agentResult.length === 0) { + return res.status(404).json({ success: false, message: '代理不存在' }); + } + + // 查询代理的商户列表 + const merchantsQuery = ` + SELECT u.id, + u.real_name, + u.phone, + u.created_at, + u.created_at as joined_at + FROM users u + WHERE u.inviter = ${id} + GROUP BY u.id, u.created_at + ORDER BY u.created_at + DESC + LIMIT ${limitNum} OFFSET ${offset} + `; + + const merchants = await db.query(merchantsQuery, [id]); + + // 查询总数 + const totalResult = await db.query( + 'SELECT COUNT(*) as total FROM users WHERE inviter = ?', + [id] + ); + const total = totalResult && totalResult.length > 0 ? totalResult[0].total : 0; + + res.json({ + success: true, + data: { + merchants, + total: parseInt(total) + } + }); + } catch (error) { + console.error('获取代理商户列表失败:', error); + res.status(500).json({ success: false, message: '获取代理商户列表失败' }); + } +}); + +// 获取代理佣金记录 +router.get('/:id/commissions', authenticateAdmin, async (req, res) => { + try { + const { id } = req.params; + const { page = 1, limit = 20 } = req.query; + const pageNum = parseInt(page) || 1; + const limitNum = parseInt(limit) || 20; + const offset = (pageNum - 1) * limitNum; + + // 检查代理是否存在 + const agentResult = await db.query('SELECT * FROM regional_agents WHERE id = ?', [id]); + if (!agentResult || agentResult.length === 0) { + return res.status(404).json({ success: false, message: '代理不存在' }); + } + const agent = agentResult[0]; + + // 查询佣金记录 + const commissionsQuery = ` + SELECT + acr.*, + u.phone as merchant_phone + FROM agent_commission_records acr + JOIN users u ON acr.merchant_id = u.id + WHERE acr.agent_id = ? + ORDER BY acr.created_at DESC + LIMIT ${limitNum} OFFSET ${offset} + `; + + const commissions = await db.query(commissionsQuery, [id]); + + // 查询总数 + const totalResult = await db.query( + 'SELECT COUNT(*) as total FROM agent_commission_records WHERE agent_id = ?', + [id] + ); + const total = totalResult && totalResult.length > 0 ? totalResult[0].total : 0; + + res.json({ + success: true, + data: { + commissions, + total: parseInt(total) + } + }); + } catch (error) { + console.error('获取代理佣金记录失败:', error); + res.status(500).json({ success: false, message: '获取代理佣金记录失败' }); + } +}); + +// 获取代理商户的转账记录 +router.get('/:id/merchant-transfers', authenticateAdmin, async (req, res) => { + try { + const { id } = req.params; + const { page = 1, limit = 20, merchant_id } = req.query; + const pageNum = parseInt(page) || 1; + const limitNum = parseInt(limit) || 20; + const offset = (pageNum - 1) * limitNum; + + // 检查代理是否存在 + const agentResult = await db.query(`SELECT * FROM users WHERE id = ? AND (user_type='agent' OR user_type='agent_directly')`, [id]); + if (!agentResult || agentResult.length === 0) { + return res.status(404).json({ success: false, message: '代理不存在' }); + } + + // 构建查询条件 + let whereConditions = ['am.inviter = ?']; + let queryParams = [id]; + + if (merchant_id) { + whereConditions.push('t.from_user_id = ?'); + queryParams.push(merchant_id); + } + + const whereClause = whereConditions.join(' AND '); + + // 查询商户转账记录 + const transfersQuery = ` + SELECT t.id, + t.from_user_id, + t.to_user_id, + t.amount, + t.status, + t.transfer_type, + t.description, + t.created_at, + t.confirmed_at, + from_user.real_name as from_real_name, + CONCAT(SUBSTRING(from_user.phone, 1, 3), '****', SUBSTRING(from_user.phone, -4)) as from_phone_masked, + to_user.real_name as to_real_name, + CONCAT(SUBSTRING(to_user.phone, 1, 3), '****', SUBSTRING(to_user.phone, -4)) as to_phone_masked + FROM users as am + JOIN transfers t ON am.id = t.from_user_id + LEFT JOIN users from_user ON t.from_user_id = from_user.id + LEFT JOIN users to_user ON t.to_user_id = to_user.id + WHERE ${whereClause} + ORDER BY t.created_at DESC + LIMIT ${limitNum} OFFSET ${offset} + `; + console.log(transfersQuery,queryParams); + const transfers = await db.query(transfersQuery, queryParams); + + // 查询总数 + const totalQuery = ` + SELECT COUNT(*) as total + FROM users am + JOIN transfers t ON am.id = t.from_user_id + WHERE ${whereClause} + `; + const totalResult = await db.query(totalQuery, queryParams); + const total = totalResult && totalResult.length > 0 ? totalResult[0].total : 0; + + res.json({ + success: true, + data: { + transfers, + total: parseInt(total) + } + }); + } catch (error) { + console.error('1:', error); + res.status(500).json({ success: false, message: '1获取代理商户转账记录失败' }); + } +}); + +/** + * 修改代理密码 + */ +router.put('/:id/password', authenticateAdmin, async (req, res) => { + try { + const { id } = req.params; + const { password } = req.body; + + if (!password || password.length < 6) { + return res.status(400).json({ success: false, message: '密码长度不能少于6位' }); + } + + // 检查代理是否存在且状态为激活 + const agentResult = await db.query( + 'SELECT * FROM regional_agents WHERE id = ? AND status = "active"', + [id] + ); + + if (!agentResult || agentResult.length === 0) { + return res.status(404).json({ success: false, message: '代理不存在或状态不正确' }); + } + + // 加密新密码 + const hashedPassword = await bcrypt.hash(password, 10); + + // 更新用户表中的密码(与审核通过时的逻辑一致) + await db.query( + `UPDATE users SET password = ? WHERE id = ( + SELECT user_id FROM regional_agents WHERE id = ? + )`, + [hashedPassword, id] + ); + + res.json({ + success: true, + message: '代理密码修改成功' + }); + } catch (error) { + console.error('修改代理密码失败:', error); + res.status(500).json({ success: false, message: '修改代理密码失败' }); + } +}); + +/** + * 删除代理 + */ +router.delete('/:id', authenticateAdmin, async (req, res) => { + try { + const { id } = req.params; + const { force = 'false' } = req.query; // 是否强制删除 + const forceDelete = force === 'true' + + // 检查代理是否存在 + const agentResult = await db.query( + 'SELECT * FROM regional_agents WHERE id = ?', + [id] + ); + + if (!agentResult || agentResult.length === 0) { + return res.status(404).json({ success: false, message: '代理不存在' }); + } + + const agent = agentResult[0]; + + // 检查代理是否有关联的商户 + const merchantCount = await db.query( + 'SELECT COUNT(*) as count FROM agent_merchants WHERE agent_id = ?', + [id] + ); + + const hasMerchants = merchantCount && merchantCount.length > 0 && merchantCount[0].count > 0; + + // 检查代理是否有佣金记录 + const commissionCount = await db.query( + 'SELECT COUNT(*) as count FROM agent_commission_records WHERE agent_id = ?', + [id] + ); + + const hasCommissions = commissionCount && commissionCount.length > 0 && commissionCount[0].count > 0; + + // 如果有关联数据且不是强制删除,则提示用户 + // if ((hasMerchants || hasCommissions) && !forceDelete) { + // return res.status(400).json({ + // success: false, + // message: '该代理存在关联数据(商户或佣金记录),请确认是否强制删除', + // data: { + // has_merchants: hasMerchants, + // has_commissions: hasCommissions, + // merchant_count: hasMerchants ? merchantCount[0].count : 0, + // commission_count: hasCommissions ? commissionCount[0].count : 0 + // }, + // require_force: true + // }); + // } + + // 开始事务删除 + const pool = getDB(); + const connection = await pool.getConnection(); + await connection.beginTransaction(); + + try { + // 删除代理商户关系 + if (hasMerchants) { + await connection.execute('DELETE FROM agent_merchants WHERE agent_id = ?', [id]); + } + + // 删除佣金记录(根据业务需求,可能需要保留历史记录) + if (hasCommissions && forceDelete) { + await connection.execute('DELETE FROM agent_commission_records WHERE agent_id = ?', [id]); + } + + // 删除代理记录 + await connection.execute('DELETE FROM regional_agents WHERE id = ?', [id]); + + // 获取区域信息用于日志 + const [regionResult] = await connection.execute( + 'SELECT * FROM zhejiang_regions WHERE id = ?', + [agent.region_id] + ); + const region = regionResult && regionResult.length > 0 ? regionResult[0] : null; + + await connection.commit(); + + // 记录操作日志 + const logMessage = `删除代理: ${agent.user_id} (区域: ${region ? `${region.city_name} ${region.district_name}` : '未知区域'})`; + console.log(`管理员 ${req.user.id} 执行操作: ${logMessage}`); + + res.json({ + success: true, + message: '代理删除成功', + data: { + deleted_agent_id: id, + deleted_merchants: hasMerchants ? merchantCount[0].count : 0, + deleted_commissions: (hasCommissions && forceDelete) ? commissionCount[0].count : 0 + } + }); + } catch (error) { + await connection.rollback(); + throw error; + } finally { + connection.release(); + } + } catch (error) { + console.error('删除代理失败:', error); + res.status(500).json({ success: false, message: '删除代理失败' }); + } +}); + +/** + * 获取可用的城市区域列表(用于代理城市更换) + */ +router.get('/available-regions', authenticateAdmin, async (req, res) => { + try { + // 查询所有区域,并标记是否已有激活代理 + const regionsQuery = ` + SELECT + zr.id, + zr.city_name, + zr.district_name, + CASE + WHEN ra.id IS NOT NULL THEN 1 + ELSE 0 + END as has_active_agent, + ra.id as agent_id, + u.real_name as agent_name + FROM zhejiang_regions zr + LEFT JOIN regional_agents ra ON zr.id = ra.region_id AND ra.status = 'active' + LEFT JOIN users u ON ra.user_id = u.id + ORDER BY zr.city_name, zr.district_name + `; + + const regions = await db.query(regionsQuery); + + res.json({ + success: true, + data: regions + }); + } catch (error) { + console.error('获取可用区域列表失败:', error); + res.status(500).json({ success: false, message: '获取可用区域列表失败' }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/routes/agents/withdrawals.js b/routes/agents/withdrawals.js new file mode 100644 index 0000000..e67a63f --- /dev/null +++ b/routes/agents/withdrawals.js @@ -0,0 +1,274 @@ +const express = require('express'); +const router = express.Router(); +const { getDB } = require('../../database'); +const { auth, adminAuth } = require('../../middleware/auth'); + +// 创建管理员认证中间件组合 +const authenticateAdmin = [auth, adminAuth]; + +// 获取数据库连接 +const db = { + query: async (sql, params = []) => { + const connection = getDB(); + const [rows] = await connection.execute(sql, params); + return rows; + } +}; + +/** + * 获取提现申请列表 + */ +router.get('/', authenticateAdmin, async (req, res) => { + try { + const { page = 1, limit = 20, status, agent_id } = req.query; + const pageNum = parseInt(page) || 1; + const limitNum = parseInt(limit) || 20; + const offset = (pageNum - 1) * limitNum; + + // 构建查询条件 + let whereConditions = []; + let queryParams = []; + + if (status) { + whereConditions.push('aw.status = ?'); + queryParams.push(status); + } + + if (agent_id) { + whereConditions.push('aw.agent_id = ?'); + queryParams.push(agent_id); + } + + const whereClause = whereConditions.length > 0 ? `WHERE ${whereConditions.join(' AND ')}` : ''; + + // 查询提现申请列表 + const withdrawalsQuery = ` + SELECT + aw.*, + ra.agent_code, + u.real_name as agent_name, + u.phone as agent_phone, + zr.city_name, + zr.district_name, + admin.real_name as processed_by_name + FROM agent_withdrawals aw + JOIN regional_agents ra ON aw.agent_id = ra.id + JOIN users u ON ra.user_id = u.id + LEFT JOIN zhejiang_regions zr ON ra.region_id = zr.id + LEFT JOIN users admin ON aw.processed_by = admin.id + ${whereClause} + ORDER BY aw.created_at DESC + LIMIT ${limitNum} OFFSET ${offset} + `; + + const withdrawals = await db.query(withdrawalsQuery, queryParams); + + // 查询总数 + const countQuery = ` + SELECT COUNT(*) as total + FROM agent_withdrawals aw + ${whereClause} + `; + + const totalResult = await db.query(countQuery, queryParams); + const total = totalResult && totalResult.length > 0 ? totalResult[0].total : 0; + + // 查询统计信息 + const statsQuery = ` + SELECT + COUNT(*) as total_applications, + COUNT(CASE WHEN status = 'pending' THEN 1 END) as pending_count, + COUNT(CASE WHEN status = 'approved' THEN 1 END) as approved_count, + COUNT(CASE WHEN status = 'completed' THEN 1 END) as completed_count, + COUNT(CASE WHEN status = 'rejected' THEN 1 END) as rejected_count, + CAST(COALESCE(SUM(CASE WHEN status = 'pending' THEN amount END), 0) AS DECIMAL(10,2)) as pending_amount, + CAST(COALESCE(SUM(CASE WHEN status = 'completed' THEN amount END), 0) AS DECIMAL(10,2)) as completed_amount + FROM agent_withdrawals + `; + + const statsResult = await db.query(statsQuery); + const stats = statsResult && statsResult.length > 0 ? statsResult[0] : { + total_applications: 0, + pending_count: 0, + approved_count: 0, + completed_count: 0, + rejected_count: 0, + pending_amount: 0, + completed_amount: 0 + }; + + res.json({ + success: true, + data: { + withdrawals, + total: parseInt(total), + stats + } + }); + } catch (error) { + console.error('获取提现申请列表失败:', error); + res.status(500).json({ success: false, message: '获取提现申请列表失败' }); + } +}); + +/** + * 审核提现申请 + */ +router.put('/:id/review', authenticateAdmin, async (req, res) => { + try { + const { id } = req.params; + const { action, admin_note } = req.body; + const adminId = req.user.id; + + if (!['approve', 'reject'].includes(action)) { + return res.status(400).json({ success: false, message: '无效的审核操作' }); + } + + // 检查提现申请是否存在且状态为待审核 + const withdrawalResult = await db.query( + 'SELECT * FROM agent_withdrawals WHERE id = ? AND status = "pending"', + [id] + ); + + if (!withdrawalResult || withdrawalResult.length === 0) { + return res.status(404).json({ success: false, message: '提现申请不存在或已处理' }); + } + + const withdrawal = withdrawalResult[0]; + const newStatus = action === 'approve' ? 'approved' : 'rejected'; + + // 开始事务 + const pool = getDB(); + const connection = await pool.getConnection(); + await connection.beginTransaction(); + + try { + // 更新提现申请状态 + await connection.execute( + 'UPDATE agent_withdrawals SET status = ?, admin_note = ?, processed_by = ?, processed_at = NOW() WHERE id = ?', + [newStatus, admin_note || null, adminId, id] + ); + + // 如果是拒绝,需要恢复代理的待提现金额 + if (action === 'reject') { + await connection.execute( + 'UPDATE regional_agents SET pending_withdrawal = pending_withdrawal - ? WHERE id = ?', + [withdrawal.amount, withdrawal.agent_id] + ); + } + + await connection.commit(); + connection.release(); // 释放连接回连接池 + + res.json({ + success: true, + message: action === 'approve' ? '提现申请已通过审核' : '提现申请已拒绝' + }); + } catch (error) { + await connection.rollback(); + connection.release(); // 释放连接回连接池 + throw error; + } + } catch (error) { + console.error('审核提现申请失败:', error); + res.status(500).json({ success: false, message: '审核提现申请失败' }); + } +}); + +/** + * 标记提现完成 + */ +router.put('/:id/complete', authenticateAdmin, async (req, res) => { + try { + const { id } = req.params; + const adminId = req.user.id; + + // 检查提现申请是否存在且状态为已审核 + const withdrawalResult = await db.query( + 'SELECT * FROM agent_withdrawals WHERE id = ? AND status = "approved"', + [id] + ); + + if (!withdrawalResult || withdrawalResult.length === 0) { + return res.status(404).json({ success: false, message: '提现申请不存在或状态不正确' }); + } + + const withdrawal = withdrawalResult[0]; + + // 开始事务 + const pool = getDB(); + const connection = await pool.getConnection(); + await connection.beginTransaction(); + + try { + // 更新提现申请状态为已完成 + await connection.execute( + 'UPDATE agent_withdrawals SET status = "completed", processed_by = ?, processed_at = NOW() WHERE id = ?', + [adminId, id] + ); + + // 更新代理的已提现金额和待提现金额 + await connection.execute( + 'UPDATE regional_agents SET withdrawn_amount = withdrawn_amount + ?, pending_withdrawal = pending_withdrawal - ? WHERE id = ?', + [withdrawal.amount, withdrawal.amount, withdrawal.agent_id] + ); + + await connection.commit(); + connection.release(); // 释放连接回连接池 + + res.json({ + success: true, + message: '提现已标记为完成' + }); + } catch (error) { + await connection.rollback(); + connection.release(); // 释放连接回连接池 + throw error; + } + } catch (error) { + console.error('标记提现完成失败:', error); + res.status(500).json({ success: false, message: '标记提现完成失败' }); + } +}); + +/** + * 获取提现申请详情 + */ +router.get('/:id', authenticateAdmin, async (req, res) => { + try { + const { id } = req.params; + + const withdrawalQuery = ` + SELECT + aw.*, + ra.agent_code, + u.real_name as agent_name, + u.phone as agent_phone, + zr.city_name, + zr.district_name, + admin.real_name as processed_by_name + FROM agent_withdrawals aw + JOIN regional_agents ra ON aw.agent_id = ra.id + JOIN users u ON ra.user_id = u.id + LEFT JOIN zhejiang_regions zr ON ra.region_id = zr.id + LEFT JOIN users admin ON aw.processed_by = admin.id + WHERE aw.id = ? + `; + + const withdrawalResult = await db.query(withdrawalQuery, [id]); + + if (!withdrawalResult || withdrawalResult.length === 0) { + return res.status(404).json({ success: false, message: '提现申请不存在' }); + } + + res.json({ + success: true, + data: withdrawalResult[0] + }); + } catch (error) { + console.error('获取提现申请详情失败:', error); + res.status(500).json({ success: false, message: '获取提现申请详情失败' }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/routes/announcements.js b/routes/announcements.js new file mode 100644 index 0000000..ecbbb74 --- /dev/null +++ b/routes/announcements.js @@ -0,0 +1,397 @@ +const express = require('express'); +const { getDB } = require('../database'); +const { auth, adminAuth } = require('../middleware/auth'); +const dayjs = require('dayjs'); + +const router = express.Router(); + + + +router.get('/', auth, async (req, res) => { + try { + const db = getDB(); + const { page = 1, limit = 10, status, type, keyword } = req.query; + const offset = (page - 1) * limit; + + let whereClause = 'WHERE 1=1'; + const params = []; + + if (status) { + whereClause += ' AND status = ?'; + params.push(status); + } + + if (type) { + whereClause += ' AND type = ?'; + params.push(type); + } + + if (keyword) { + whereClause += ' AND (title LIKE ? OR content LIKE ?)'; + params.push(`%${keyword}%`, `%${keyword}%`); + } + + // 获取总数 + const countQuery = `SELECT COUNT(*) as total FROM announcements ${whereClause}`; + const [countResult] = await db.execute(countQuery, params); + const total = countResult[0].total; + + // 获取公告列表(包含用户阅读状态) + const limitValue = Math.max(1, Math.min(100, parseInt(limit))); + const offsetValue = Math.max(0, parseInt(offset)); + + const query = ` + SELECT a.*, u.username as creator_name, + uar.is_read, + uar.read_at, + CASE + WHEN a.expire_time IS NOT NULL AND a.expire_time < NOW() THEN 1 + ELSE 0 + END as is_expired + FROM announcements a + LEFT JOIN users u ON a.created_by = u.id + LEFT JOIN user_announcement_reads uar ON a.id = uar.announcement_id AND uar.user_id = ? + ${whereClause} + ORDER BY a.is_pinned DESC, a.created_at DESC + LIMIT ${limitValue} OFFSET ${offsetValue} + `; + + const [announcements] = await db.execute(query, [req.user.id, ...params]); + + + + const expiredUnreadAnnouncements = announcements.filter(a => a.is_expired && !a.is_read); + + if (expiredUnreadAnnouncements.length > 0) { + const expiredIds = expiredUnreadAnnouncements.map(a => a.id); + await db.execute(` + INSERT INTO user_announcement_reads (user_id, announcement_id, is_read, read_at) + VALUES ${expiredIds.map(() => '(?, ?, TRUE, NOW())').join(', ')} + ON DUPLICATE KEY UPDATE is_read = TRUE, read_at = NOW() + `, expiredIds.flatMap(id => [req.user.id, id])); + + // 更新返回数据中的阅读状态 + expiredUnreadAnnouncements.forEach(a => { + a.is_read = true; + a.read_at = new Date(); + }); + } + + res.json({ + success: true, + data: { + announcements, + total, + page: parseInt(page), + limit: parseInt(limit), + totalPages: Math.ceil(total / limit) + } + }); + } catch (error) { + console.error('获取公告列表失败:', error); + res.status(500).json({ success: false, message: '获取公告列表失败' }); + } +}); + + +router.get('/:id', auth, async (req, res) => { + try { + const db = getDB(); + const { id } = req.params; + + const query = ` + SELECT a.*, u.username as creator_name, + uar.is_read, + uar.read_at, + CASE + WHEN a.expire_time IS NOT NULL AND a.expire_time < NOW() THEN 1 + ELSE 0 + END as is_expired + FROM announcements a + LEFT JOIN users u ON a.created_by = u.id + LEFT JOIN user_announcement_reads uar ON a.id = uar.announcement_id AND uar.user_id = ? + WHERE a.id = ? + `; + + const [result] = await db.execute(query, [req.user.id, id]); + + if (result.length === 0) { + return res.status(404).json({ success: false, message: '公告不存在' }); + } + + const announcement = result[0]; + + // 如果公告未读或已过期但未标记为已读,则标记为已读 + if (!announcement.is_read || (announcement.is_expired && !announcement.is_read)) { + await db.execute(` + INSERT INTO user_announcement_reads (user_id, announcement_id, is_read, read_at) + VALUES (?, ?, TRUE, NOW()) + ON DUPLICATE KEY UPDATE is_read = TRUE, read_at = NOW() + `, [req.user.id, id]); + + announcement.is_read = true; + announcement.read_at = new Date(); + } + + res.json({ success: true, data: announcement }); + } catch (error) { + console.error('获取公告详情失败:', error); + res.status(500).json({ success: false, message: '获取公告详情失败' }); + } +}); + + +router.post('/', auth, adminAuth, async (req, res) => { + try { + const db = getDB(); + const { + title, + content, + type = 'system', + priority = 'medium', + status = 'draft', + is_pinned = false, + publish_time, + expire_time + } = req.body; + + if (!title || !content) { + return res.status(400).json({ success: false, message: '标题和内容不能为空' }); + } + + const query = ` + INSERT INTO announcements ( + title, content, type, priority, status, is_pinned, + publish_time, expire_time, created_by, created_at, updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW()) + `; + + const [result] = await db.execute(query, [ + title, + content, + type, + priority, + status, + is_pinned, + publish_time || null, + expire_time || null, + req.user.id + ]); + + res.status(201).json({ + success: true, + message: '公告创建成功', + data: { id: result.insertId } + }); + } catch (error) { + console.error('创建公告失败:', error); + res.status(500).json({ success: false, message: '创建公告失败' }); + } +}); + + +router.put('/:id', auth, adminAuth, async (req, res) => { + try { + const db = getDB(); + const { id } = req.params; + let { + title, + content, + type, + priority, + status, + is_pinned, + publish_time, + expire_time + } = req.body; + + // 检查公告是否存在 + const [existing] = await db.execute('SELECT id FROM announcements WHERE id = ?', [id]); + if (existing.length === 0) { + return res.status(404).json({ success: false, message: '公告不存在' }); + } + + const updates = []; + const params = []; + + if (title !== undefined) { + updates.push('title = ?'); + params.push(title); + } + if (content !== undefined) { + updates.push('content = ?'); + params.push(content); + } + if (type !== undefined) { + updates.push('type = ?'); + params.push(type); + } + if (priority !== undefined) { + updates.push('priority = ?'); + params.push(priority); + } + if (status !== undefined) { + updates.push('status = ?'); + params.push(status); + } + if (is_pinned !== undefined) { + updates.push('is_pinned = ?'); + params.push(is_pinned); + } + if (publish_time !== undefined) { + updates.push('publish_time = ?'); + publish_time = dayjs(publish_time).format('YYYY-MM-DD'); + params.push(publish_time); + } + if (expire_time !== undefined) { + updates.push('expire_time = ?'); + params.push(expire_time); + } + + if (updates.length === 0) { + return res.status(400).json({ success: false, message: '没有要更新的字段' }); + } + + updates.push('updated_at = NOW()'); + params.push(id); + + const query = `UPDATE announcements SET ${updates.join(', ')} WHERE id = ?`; + await db.execute(query, params); + + res.json({ success: true, message: '公告更新成功' }); + } catch (error) { + console.error('更新公告失败:', error); + res.status(500).json({ success: false, message: '更新公告失败' }); + } +}); + + +router.delete('/:id', auth, adminAuth, async (req, res) => { + try { + const db = getDB(); + const { id } = req.params; + + // 检查公告是否存在 + const [existing] = await db.execute('SELECT id FROM announcements WHERE id = ?', [id]); + if (existing.length === 0) { + return res.status(404).json({ success: false, message: '公告不存在' }); + } + + await db.execute('DELETE FROM announcements WHERE id = ?', [id]); + + res.json({ success: true, message: '公告删除成功' }); + } catch (error) { + console.error('删除公告失败:', error); + res.status(500).json({ success: false, message: '删除公告失败' }); + } +}); + + +router.get('/public/list', async (req, res) => { + try { + const db = getDB(); + const { limit = 5 } = req.query; + const limitValue = Math.max(1, Math.min(50, parseInt(limit))); + + const query = ` + SELECT id, title, content, type, priority, publish_time, created_at + FROM announcements + WHERE status = 'published' + AND (expire_time IS NULL OR expire_time > NOW()) + AND (publish_time IS NULL OR publish_time <= NOW()) + ORDER BY is_pinned DESC, created_at DESC + LIMIT ${limitValue} + `; + + const [announcements] = await db.execute(query, []); + + res.json({ success: true, data: announcements }); + } catch (error) { + console.error('获取公开公告失败:', error); + res.status(500).json({ success: false, message: '获取公开公告失败' }); + } +}); + +// 标记公告为已读 +router.post('/:id/read', auth, async (req, res) => { + try { + const db = getDB(); + const { id } = req.params; + + // 检查公告是否存在 + const [existing] = await db.execute('SELECT id FROM announcements WHERE id = ?', [id]); + if (existing.length === 0) { + return res.status(404).json({ success: false, message: '公告不存在' }); + } + + // 标记为已读 + await db.execute(` + INSERT INTO user_announcement_reads (user_id, announcement_id, is_read, read_at) + VALUES (?, ?, TRUE, NOW()) + ON DUPLICATE KEY UPDATE is_read = TRUE, read_at = NOW() + `, [req.user.id, id]); + + res.json({ success: true, message: '已标记为已读' }); + } catch (error) { + console.error('标记公告已读失败:', error); + res.status(500).json({ success: false, message: '标记公告已读失败' }); + } +}); + +// 获取用户未读公告数量 +router.get('/unread/count', auth, async (req, res) => { + try { + const db = getDB(); + + const query = ` + SELECT COUNT(*) as unread_count + FROM announcements a + LEFT JOIN user_announcement_reads uar ON a.id = uar.announcement_id AND uar.user_id = ? + WHERE a.status = 'published' + AND (a.publish_time IS NULL OR a.publish_time <= NOW()) + AND (a.expire_time IS NULL OR a.expire_time > NOW()) + AND (uar.is_read IS NULL OR uar.is_read = FALSE) + `; + + const [result] = await db.execute(query, [req.user.id]); + + res.json({ + success: true, + data: { + unread_count: result[0].unread_count + } + }); + } catch (error) { + console.error('获取未读公告数量失败:', error); + res.status(500).json({ success: false, message: '获取未读公告数量失败' }); + } +}); + +// 批量标记公告为已读 +router.post('/batch/read', auth, async (req, res) => { + try { + const db = getDB(); + const { announcement_ids } = req.body; + + if (!announcement_ids || !Array.isArray(announcement_ids) || announcement_ids.length === 0) { + return res.status(400).json({ success: false, message: '请提供有效的公告ID列表' }); + } + + // 批量标记为已读 + const values = announcement_ids.map(() => '(?, ?, TRUE, NOW())').join(', '); + const params = announcement_ids.flatMap(id => [req.user.id, id]); + + await db.execute(` + INSERT INTO user_announcement_reads (user_id, announcement_id, is_read, read_at) + VALUES ${values} + ON DUPLICATE KEY UPDATE is_read = TRUE, read_at = NOW() + `, params); + + res.json({ success: true, message: '批量标记已读成功' }); + } catch (error) { + console.error('批量标记公告已读失败:', error); + res.status(500).json({ success: false, message: '批量标记公告已读失败' }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/routes/auth.js b/routes/auth.js new file mode 100644 index 0000000..d65b95d --- /dev/null +++ b/routes/auth.js @@ -0,0 +1,345 @@ +const express = require('express'); +const bcrypt = require('bcryptjs'); +const jwt = require('jsonwebtoken'); +const {getDB} = require('../database'); + +const router = express.Router(); +const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key'; +router.post('/register', async (req, res) => { + try { + const db = getDB(); + await db.query('START TRANSACTION'); + + const { + username, + phone, + password, + city, + district_id: district, + province, + inviter = null, + captchaId, + captchaText, + smsCode, // 短信验证码 + role = 'user' + } = req.body; + + if (!username || !phone || !password || !city || !district || !province) { + return res.status(400).json({success: false, message: '用户名、手机号、密码、城市和区域不能为空'}); + } + + if (!captchaId || !captchaText) { + return res.status(400).json({success: false, message: '图形验证码不能为空'}); + } + const storedCaptcha = global.captchaStore.get(captchaId); + console.log(storedCaptcha); + + if (!storedCaptcha) { + return res.status(400).json({ + success: false, + message: '验证码不存在或已过期' + }); + } + + // 检查是否过期 + if (Date.now() > storedCaptcha.expires) { + global.captchaStore.delete(captchaId); + return res.status(400).json({ + success: false, + message: '验证码已过期' + }); + } + + // 验证验证码(不区分大小写) + const isValid = storedCaptcha.text === captchaText.toLowerCase(); + + // 删除已验证的验证码 + global.captchaStore.delete(captchaId); + + if (!isValid) { + return res.status(400).json({ + success: false, + message: '验证码错误' + }); + } + if (!smsCode) { + return res.status(400).json({success: false, message: '短信验证码不能为空'}); + } + // 验证短信验证码 + const smsAPI = require('./sms'); + const smsValid = smsAPI.verifySMSCode(phone, smsCode); + if (!smsValid) { + return res.status(400).json({success: false, message: '短信验证码错误或已过期'}); + } + + // 验证手机号格式 + const phoneRegex = /^1[3-9]\d{9}$/; + if (!phoneRegex.test(phone)) { + return res.status(400).json({success: false, message: '手机号格式不正确'}); + } + + + // 检查用户是否已存在 + const [existingUsers] = await db.execute( + 'SELECT id, payment_status FROM users WHERE username = ? OR phone = ?', + [username, phone] + ); + + if (existingUsers.length > 0) { + return res.status(400).json({success: false, message: '用户名或手机号已存在'}); + } + + // 加密密码 + const hashedPassword = await bcrypt.hash(password, 10); + + // 创建用户(初始状态为未支付) + const [result] = await db.execute( + 'INSERT INTO users (username, phone, password, role, points, audit_status, city, district_id, payment_status, province, inviter) VALUES (?, ?, ?, ?, ?, ?, ?, ?, "unpaid", ?, ?)', + [username, phone, hashedPassword, role, 0, 'pending', city, district, province, inviter] + ); + + const userId = result.insertId; + await db.query('COMMIT'); + + // 生成JWT token(用于支付流程) + const token = jwt.sign( + {userId: userId, username, role}, + JWT_SECRET, + {expiresIn: '24h'} + ); + + res.status(201).json({ + success: true, + message: '用户信息创建成功,请完成支付以激活账户', + token, + user: { + id: userId, + username, + phone, + role, + points: 0, + audit_status: 'pending', + city, + district, + paymentStatus: 'unpaid' + }, + needPayment: true + }); + } catch (error) { + try { + // await getDB().query('ROLLBACK'); + } catch (rollbackError) { + console.error('回滚错误:', rollbackError); + } + console.error('注册错误详情:', error); + console.error('错误堆栈:', error.stack); + res.status(500).json({ + success: false, + message: '注册失败', + error: process.env.NODE_ENV === 'development' ? error.message : undefined + }); + } +}); + + +router.post('/login', async (req, res) => { + try { + const db = getDB(); + const {username, password, captchaId, captchaText} = req.body; + + if (!username || !password) { + return res.status(400).json({success: false, message: '用户名和密码不能为空'}); + } + + if (!captchaId || !captchaText) { + return res.status(400).json({success: false, message: '验证码不能为空'}); + } + // 获取存储的验证码 + const storedCaptcha = global.captchaStore.get(captchaId); + console.log(storedCaptcha); + + if (!storedCaptcha) { + return res.status(400).json({ + success: false, + message: '验证码不存在或已过期' + }); + } + + // 检查是否过期 + if (Date.now() > storedCaptcha.expires) { + global.captchaStore.delete(captchaId); + return res.status(400).json({ + success: false, + message: '验证码已过期' + }); + } + + // 验证验证码(不区分大小写) + const isValid = storedCaptcha.text === captchaText.toLowerCase(); + + // 删除已验证的验证码 + global.captchaStore.delete(captchaId); + + if (!isValid) { + return res.status(400).json({ + success: false, + message: '验证码错误' + }); + } + + // 注意:验证码已在前端通过 /captcha/verify 接口验证过,这里不再重复验证 + + // 查找用户(包含支付状态) + console.log('登录尝试 - 用户名:', username); + const [users] = await db.execute( + 'SELECT * FROM users WHERE username = ?', + [username] + ); + + console.log('查找到的用户数量:', users.length); + if (users.length === 0) { + console.log('用户不存在:', username); + return res.status(401).json({success: false, message: '用户名或密码错误'}); + } + + const user = users[0]; + console.log('找到用户:', user.username, '密码长度:', user.password ? user.password.length : 'null'); + + // 验证密码 + console.log('验证密码 - 输入密码:', password, '数据库密码前10位:', user.password ? user.password.substring(0, 10) : 'null'); + const isValidPassword = await bcrypt.compare(password, user.password); + console.log('密码验证结果:', isValidPassword); + + if (!isValidPassword) { + console.log('密码验证失败'); + return res.status(401).json({success: false, message: '用户名或密码错误'}); + } + + // 检查支付状态(管理员除外) + if (user.role !== 'admin' && user.payment_status === 'unpaid') { + const token = jwt.sign( + {userId: user.id, username: user.username, role: user.role}, + JWT_SECRET, + {expiresIn: '5m'} + ); + return res.status(200).json({ + success: false, + message: '您的账户尚未激活,请完成支付后再登录', + needPayment: true, + user: user[0], + token + }); + } + + // 检查用户审核状态(管理员除外,只阻止被拒绝的用户) + if (user.role !== 'admin' && user.audit_status === 'rejected') { + return res.status(403).json({success: false, message: '您的账户审核未通过,请联系管理员'}); + } + // 待审核用户可以正常登录使用系统,但匹配功能会有限制 + + // 生成JWT token + const token = jwt.sign( + {userId: user.id, username: user.username, role: user.role}, + JWT_SECRET, + {expiresIn: '24h'} + ); + const [is_distribution] = await db.execute(` + SELECT * + FROM distribution + WHERE user_id = ?`, [user.id]); + user.distribution = is_distribution.length > 0 ? true : false; + res.json({ + success: true, + message: '登录成功', + token, + user + }); + } catch (error) { + console.error('登录错误:', error); + res.status(500).json({success: false, message: '登录失败'}); + } +}); + +// 验证token中间件 +const authenticateToken = (req, res, next) => { + const authHeader = req.headers['authorization']; + const token = authHeader && authHeader.split(' ')[1]; + + if (!token) { + return res.status(401).json({success: false, message: '访问令牌缺失'}); + } + + jwt.verify(token, JWT_SECRET, (err, user) => { + if (err) { + return res.status(403).json({success: false, message: '访问令牌无效'}); + } + req.user = user; + next(); + }); +}; + +// 获取当前用户信息 +router.get('/me', authenticateToken, async (req, res) => { + try { + const db = getDB(); + const [users] = await db.execute( + 'SELECT id, username, role, avatar, points, created_at FROM users WHERE id = ?', + [req.user.userId] + ); + + if (users.length === 0) { + return res.status(404).json({success: false, message: '用户不存在'}); + } + + res.json({success: true, user: users[0]}); + } catch (error) { + console.error('获取用户信息错误:', error); + res.status(500).json({success: false, message: '获取用户信息失败'}); + } +}); + +// 修改密码 +router.put('/change-password', authenticateToken, async (req, res) => { + try { + const db = getDB(); + const {currentPassword, newPassword} = req.body; + + if (!currentPassword || !newPassword) { + return res.status(400).json({success: false, message: '旧密码和新密码不能为空'}); + } + + // 获取用户当前密码 + const [users] = await db.execute( + 'SELECT password FROM users WHERE id = ?', + [req.user.userId] + ); + + if (users.length === 0) { + return res.status(404).json({success: false, message: '用户不存在'}); + } + + // 验证旧密码 + const isValidPassword = await bcrypt.compare(currentPassword, users[0].password); + + if (!isValidPassword) { + return res.status(400).json({success: false, message: '旧密码错误'}); + } + + // 加密新密码 + const hashedNewPassword = await bcrypt.hash(newPassword, 10); + + // 更新密码 + await db.execute( + 'UPDATE users SET password = ? WHERE id = ?', + [hashedNewPassword, req.user.userId] + ); + + res.json({success: true, message: '密码修改成功'}); + } catch (error) { + console.error('修改密码错误:', error); + res.status(500).json({success: false, message: '修改密码失败'}); + } +}); + +module.exports = router; +module.exports.authenticateToken = authenticateToken; \ No newline at end of file diff --git a/routes/captcha.js b/routes/captcha.js new file mode 100644 index 0000000..4fb4f11 --- /dev/null +++ b/routes/captcha.js @@ -0,0 +1,220 @@ +const express = require('express'); +const crypto = require('crypto'); +const router = express.Router(); + + + +// 内存存储验证码(生产环境建议使用Redis) + + +/** + * 生成随机验证码字符串 + * @param {number} length 验证码长度 + * @returns {string} 验证码字符串 + */ +function generateCaptchaText(length = 4) { + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + let result = ''; + for (let i = 0; i < length; i++) { + result += chars.charAt(Math.floor(Math.random() * chars.length)); + } + return result; +} + +/** + * 生成SVG验证码图片 + * @param {string} text 验证码文本 + * @returns {string} SVG字符串 + */ +function generateCaptchaSVG(text) { + const width = 120; + const height = 40; + const fontSize = 18; + + // 生成随机颜色 + const getRandomColor = () => { + const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', '#DDA0DD', '#98D8C8']; + return colors[Math.floor(Math.random() * colors.length)]; + }; + + // 生成干扰线 + const generateNoise = () => { + let noise = ''; + for (let i = 0; i < 3; i++) { + const x1 = Math.random() * width; + const y1 = Math.random() * height; + const x2 = Math.random() * width; + const y2 = Math.random() * height; + noise += ``; + } + return noise; + }; + + // 生成干扰点 + const generateDots = () => { + let dots = ''; + for (let i = 0; i < 20; i++) { + const x = Math.random() * width; + const y = Math.random() * height; + const r = Math.random() * 2 + 1; + dots += ``; + } + return dots; + }; + + // 生成文字 + let textElements = ''; + const charWidth = width / text.length; + + for (let i = 0; i < text.length; i++) { + const char = text[i]; + const x = charWidth * i + charWidth / 2; + const y = height / 2 + fontSize / 3; + const rotation = (Math.random() - 0.5) * 30; // 随机旋转角度 + const color = getRandomColor(); + + textElements += ` + + ${char} + `; + } + + const svg = ` + + + + + + + + + ${generateNoise()} + ${generateDots()} + ${textElements} + `; + + return svg; +} + + +router.get('/generate', (req, res) => { + try { + // 生成验证码文本 + const captchaText = generateCaptchaText(); + + // 生成唯一ID + const captchaId = crypto.randomUUID(); + + // 存储验证码(5分钟过期) + global.captchaStore.set(captchaId, { + text: captchaText.toLowerCase(), // 存储小写用于比较 + expires: Date.now() + 5 * 60 * 1000 // 5分钟过期 + }); + + // 生成SVG图片 + const svgImage = generateCaptchaSVG(captchaText); + res.json({ + success: true, + data: { + captchaId, + image: `data:image/svg+xml;base64,${Buffer.from(svgImage).toString('base64')}` + } + }); + } catch (error) { + console.error('生成验证码失败:', error); + res.status(500).json({ + success: false, + message: '生成验证码失败' + }); + } +}); + + +router.post('/verify', (req, res) => { + try { + const { captchaId, captchaText } = req.body; + + if (!captchaId || !captchaText) { + return res.status(400).json({ + success: false, + message: '验证码ID和验证码不能为空' + }); + } + + // 获取存储的验证码 + const storedCaptcha = global.captchaStore.get(captchaId); + + if (!storedCaptcha) { + return res.status(400).json({ + success: false, + message: '验证码不存在或已过期' + }); + } + + // 检查是否过期 + if (Date.now() > storedCaptcha.expires) { + global.captchaStore.delete(captchaId); + return res.status(400).json({ + success: false, + message: '验证码已过期' + }); + } + + // 验证验证码(不区分大小写) + const isValid = storedCaptcha.text === captchaText.toLowerCase(); + + // 验证后删除验证码(无论成功失败) + global.captchaStore.delete(captchaId); + + if (isValid) { + res.json({ + success: true, + message: '验证码验证成功' + }); + } else { + res.status(400).json({ + success: false, + message: '验证码错误' + }); + } + } catch (error) { + console.error('验证验证码失败:', error); + res.status(500).json({ + success: false, + message: '验证验证码失败' + }); + } +}); + +// 清理过期验证码的定时任务 +setInterval(() => { + const now = Date.now(); + for (const [id, captcha] of global.captchaStore.entries()) { + if (now > captcha.expires) { + global.captchaStore.delete(id); + } + } +}, 60 * 1000); // 每分钟清理一次 + +// 导出验证函数供其他模块使用 +module.exports = router; +module.exports.verifyCaptcha = (captchaId, captchaText) => { + const captcha = global.captchaStore.get(captchaId); + if (!captcha) { + return false; // 验证码不存在或已过期 + } + + if (captcha.text.toLowerCase() !== captchaText.toLowerCase()) { + return false; // 验证码错误 + } + + // 验证成功后删除验证码(一次性使用) + global.captchaStore.delete(captchaId); + return true; +}; \ No newline at end of file diff --git a/routes/cart.js b/routes/cart.js new file mode 100644 index 0000000..6cb109f --- /dev/null +++ b/routes/cart.js @@ -0,0 +1,935 @@ +const express = require('express'); +const { getDB } = require('../database'); +const { auth } = require('../middleware/auth'); + +const router = express.Router(); + +/** + * @swagger + * tags: + * name: Cart + * description: 购物车管理相关接口 + */ + +/** + * @swagger + * components: + * schemas: + * CartItem: + * type: object + * properties: + * id: + * type: integer + * description: 购物车项ID + * user_id: + * type: integer + * description: 用户ID + * product_id: + * type: integer + * description: 商品ID + * quantity: + * type: integer + * description: 商品数量 + * spec_combination_id: + * type: integer + * description: 商品规格组合ID + * created_at: + * type: string + * format: date-time + * description: 创建时间 + * updated_at: + * type: string + * format: date-time + * description: 更新时间 + * product: + * type: object + * properties: + * id: + * type: integer + * name: + * type: string + * price: + * type: integer + * points_price: + * type: integer + * rongdou_price: + * type: integer + * image_url: + * type: string + * stock: + * type: integer + * status: + * type: string + */ + +/** + * @swagger + * /api/cart: + * get: + * summary: 获取购物车列表 + * tags: [Cart] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: 获取购物车成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * data: + * type: object + * properties: + * items: + * type: array + * items: + * $ref: '#/components/schemas/CartItem' + * total_count: + * type: integer + * description: 购物车商品总数量 + * total_amount: + * type: integer + * description: 购物车总金额 + * total_points: + * type: integer + * description: 购物车总积分 + * total_rongdou: + * type: integer + * description: 购物车总融豆 + * 401: + * description: 未授权 + * 500: + * description: 服务器错误 + */ +router.get('/', auth, async (req, res) => { + try { + const userId = req.user.id; + + // 获取购物车商品列表 + const query = ` + SELECT + c.id, c.user_id, c.product_id, c.quantity, c.specification_id, + c.created_at, c.updated_at, + p.name, p.price, p.points_price, p.rongdou_price, p.image_url, + p.stock, p.status, p.shop_name, p.shop_avatar, + psc.combination_key, psc.price_adjustment, + psc.points_adjustment, psc.rongdou_adjustment, psc.stock as spec_stock, + GROUP_CONCAT(CONCAT(sn.display_name, ':', sv.display_value) ORDER BY sn.sort_order SEPARATOR ' | ') as spec_display + FROM cart_items c + LEFT JOIN products p ON c.product_id = p.id + LEFT JOIN product_spec_combinations psc ON c.specification_id = psc.id + LEFT JOIN JSON_TABLE(psc.spec_values, '$[*]' COLUMNS (spec_value_id INT PATH '$')) jt ON psc.id IS NOT NULL + LEFT JOIN spec_values sv ON jt.spec_value_id = sv.id + LEFT JOIN spec_names sn ON sv.spec_name_id = sn.id + WHERE c.user_id = ? AND p.status = 'active' + GROUP BY c.id + ORDER BY c.created_at DESC + `; + + const [cartItems] = await getDB().execute(query, [userId]); + + // 计算总计信息 + let totalCount = 0; + let totalAmount = 0; + let totalPoints = 0; + let totalRongdou = 0; + + const items = cartItems.map(item => { + const finalPrice = item.price + (item.price_adjustment || 0); + const finalPointsPrice = item.points_price + (item.points_adjustment || 0); + const finalRongdouPrice = item.rongdou_price + (item.rongdou_adjustment || 0); + + totalCount += item.quantity; + totalAmount += finalPrice * item.quantity; + totalPoints += finalPointsPrice * item.quantity; + totalRongdou += finalRongdouPrice * item.quantity; + + return { + id: item.id, + user_id: item.user_id, + product_id: item.product_id, + quantity: item.quantity, + spec_combination_id: item.spec_combination_id, + created_at: item.created_at, + updated_at: item.updated_at, + product: { + id: item.product_id, + name: item.name, + price: finalPrice, + points_price: finalPointsPrice, + rongdou_price: finalRongdouPrice, + image_url: item.image_url, + stock: item.spec_combination_id ? item.spec_stock : item.stock, + status: item.status, + shop_name: item.shop_name, + shop_avatar: item.shop_avatar + }, + specification: item.spec_combination_id ? { + id: item.spec_combination_id, + combination_key: item.combination_key, + spec_display: item.spec_display, + price_adjustment: item.price_adjustment, + points_adjustment: item.points_adjustment, + rongdou_adjustment: item.rongdou_adjustment + } : null + }; + }); + + res.json({ + success: true, + data: { + items, + total_count: totalCount, + total_amount: totalAmount, + total_points: totalPoints, + total_rongdou: totalRongdou + } + }); + } catch (error) { + console.error('获取购物车失败:', error); + res.status(500).json({ success: false, message: '获取购物车失败' }); + } +}); + +/** + * @swagger + * /api/cart: + * post: + * summary: 添加商品到购物车 + * tags: [Cart] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * product_id: + * type: integer + * description: 商品ID + * quantity: + * type: integer + * description: 商品数量 + * minimum: 1 + * spec_combination_id: + * type: integer + * description: 商品规格组合ID(可选) + * required: + * - product_id + * - quantity + * responses: + * 201: + * description: 添加到购物车成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * message: + * type: string + * data: + * type: object + * properties: + * cart_item_id: + * type: integer + * 400: + * description: 参数错误或库存不足 + * 401: + * description: 未授权 + * 404: + * description: 商品不存在或已下架 + * 500: + * description: 服务器错误 + */ +router.post('/add', auth, async (req, res) => { + const db = getDB(); + await db.query('START TRANSACTION'); + + try { + const { productId, quantity, specificationId } = req.body; + const userId = req.user.id; + + + // 验证必填字段 + if (!productId || !quantity || quantity < 1) { + await db.query('ROLLBACK'); + return res.status(400).json({ success: false, message: '请填写正确的商品信息和数量' }); + } + + // 检查商品是否存在且有效 + const [products] = await db.execute( + 'SELECT id, name, stock, status FROM products WHERE id = ?', + [productId] + ); + + if (products.length === 0 || products[0].status !== 'active') { + await db.query('ROLLBACK'); + return res.status(404).json({ success: false, message: '商品不存在或已下架' }); + } + + const product = products[0]; + let availableStock = product.stock; + + // 如果指定了规格组合,检查规格组合库存 + if (specificationId) { + const [specs] = await db.execute( + 'SELECT id, stock, status FROM product_spec_combinations WHERE id = ? AND product_id = ?', + [specificationId, productId] + ); + + if (specs.length === 0 || specs[0].status !== 'active') { + await db.query('ROLLBACK'); + return res.status(404).json({ success: false, message: '商品规格组合不存在或已下架' }); + } + + availableStock = specs[0].stock; + } + + // 检查购物车中是否已存在相同商品和规格组合 + const [existingItems] = await db.execute( + 'SELECT id, quantity FROM cart_items WHERE user_id = ? AND product_id = ? AND (specification_id = ? OR (specification_id IS NULL AND ? IS NULL))', + [userId, productId, specificationId, specificationId] + ); + + let finalQuantity = quantity; + if (existingItems.length > 0) { + finalQuantity += existingItems[0].quantity; + } + + // 检查库存是否足够 + if (availableStock < finalQuantity) { + await db.query('ROLLBACK'); + return res.status(400).json({ success: false, message: '库存不足' }); + } + + let cartItemId; + + if (existingItems.length > 0) { + // 更新现有购物车项的数量 + await db.execute( + 'UPDATE cart_items SET quantity = ?, updated_at = NOW() WHERE id = ?', + [finalQuantity, existingItems[0].id] + ); + cartItemId = existingItems[0].id; + } else { + // 添加新的购物车项 + const [result] = await db.execute( + 'INSERT INTO cart_items (user_id, product_id, quantity, specification_id, created_at, updated_at) VALUES (?, ?, ?, ?, NOW(), NOW())', + [userId, productId, quantity, specificationId] + ); + cartItemId = result.insertId; + } + + await db.query('COMMIT'); + + res.status(201).json({ + success: true, + message: '添加到购物车成功', + data: { cart_item_id: cartItemId } + }); + } catch (error) { + await db.query('ROLLBACK'); + console.error('添加到购物车失败:', error); + res.status(500).json({ success: false, message: '添加到购物车失败' }); + } +}); + +/** + * @swagger + * /api/cart/{id}: + * put: + * summary: 更新购物车商品数量 + * tags: [Cart] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: 购物车项ID + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * quantity: + * type: integer + * description: 新的商品数量 + * minimum: 1 + * required: + * - quantity + * responses: + * 200: + * description: 更新购物车成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * message: + * type: string + * 400: + * description: 参数错误或库存不足 + * 401: + * description: 未授权 + * 404: + * description: 购物车项不存在 + * 500: + * description: 服务器错误 + */ +router.put('/:id', auth, async (req, res) => { + const db = getDB(); + await db.query('START TRANSACTION'); + + try { + const cartItemId = req.params.id; + const { quantity } = req.body; + const userId = req.user.id; + + // 验证数量 + if (!quantity || quantity < 1) { + await db.query('ROLLBACK'); + return res.status(400).json({ success: false, message: '商品数量必须大于0' }); + } + + // 检查购物车项是否存在且属于当前用户 + const [cartItems] = await db.execute( + 'SELECT id, product_id, specification_id FROM cart_items WHERE id = ? AND user_id = ?', + [cartItemId, userId] + ); + console.log(cartItems,'cartItems'); + + + if (cartItems.length === 0) { + await db.query('ROLLBACK'); + return res.status(404).json({ success: false, message: '购物车项不存在' }); + } + + const cartItem = cartItems[0]; + + // 检查商品库存 + const [products] = await db.execute( + 'SELECT stock, status FROM products WHERE id = ?', + [cartItem.product_id] + ); + + if (products.length === 0 || products[0].status !== 'active') { + await db.query('ROLLBACK'); + return res.status(404).json({ success: false, message: '商品不存在或已下架' }); + } + + let availableStock = products[0].stock; + + // 如果有规格,检查规格库存 + if (cartItem.specification_id) { + const [specs] = await db.execute( + 'SELECT stock, status FROM product_spec_combinations WHERE id = ?', + [cartItem.specification_id] + ); + + if (specs.length === 0 || specs[0].status !== 'active') { + await db.query('ROLLBACK'); + return res.status(404).json({ success: false, message: '商品规格不存在或已下架' }); + } + + availableStock = specs[0].stock; + } + + // 检查库存是否足够 + if (availableStock < quantity) { + await db.query('ROLLBACK'); + return res.status(400).json({ success: false, message: '库存不足' }); + } + + // 更新购物车项数量 + await db.execute( + 'UPDATE cart_items SET quantity = ?, updated_at = NOW() WHERE id = ?', + [quantity, cartItemId] + ); + + await db.query('COMMIT'); + + res.json({ + success: true, + message: '更新购物车成功' + }); + } catch (error) { + await db.query('ROLLBACK'); + console.error('更新购物车失败:', error); + res.status(500).json({ success: false, message: '更新购物车失败' }); + } +}); + +/** + * @swagger + * /api/cart/{id}: + * delete: + * summary: 删除购物车商品 + * tags: [Cart] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: 购物车项ID + * responses: + * 200: + * description: 删除购物车商品成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * message: + * type: string + * 401: + * description: 未授权 + * 404: + * description: 购物车项不存在 + * 500: + * description: 服务器错误 + */ +router.delete('/:id', auth, async (req, res) => { + try { + const cartItemId = req.params.id; + const userId = req.user.id; + + // 检查购物车项是否存在且属于当前用户 + const [cartItems] = await getDB().execute( + 'SELECT id FROM cart_items WHERE id = ? AND user_id = ?', + [cartItemId, userId] + ); + + if (cartItems.length === 0) { + return res.status(404).json({ success: false, message: '购物车项不存在' }); + } + + // 删除购物车项 + await getDB().execute( + 'DELETE FROM cart_items WHERE id = ?', + [cartItemId] + ); + + res.json({ + success: true, + message: '删除购物车商品成功' + }); + } catch (error) { + console.error('删除购物车商品失败:', error); + res.status(500).json({ success: false, message: '删除购物车商品失败' }); + } +}); + +/** + * @swagger + * /api/cart/batch: + * delete: + * summary: 批量删除购物车商品 + * tags: [Cart] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * cart_item_ids: + * type: array + * items: + * type: integer + * description: 购物车项ID数组 + * required: + * - cart_item_ids + * responses: + * 200: + * description: 批量删除购物车商品成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * message: + * type: string + * data: + * type: object + * properties: + * deleted_count: + * type: integer + * description: 删除的商品数量 + * 400: + * description: 参数错误 + * 401: + * description: 未授权 + * 500: + * description: 服务器错误 + */ +router.delete('/batch', auth, async (req, res) => { + try { + const { cart_item_ids } = req.body; + const userId = req.user.id; + + // 验证参数 + if (!cart_item_ids || !Array.isArray(cart_item_ids) || cart_item_ids.length === 0) { + return res.status(400).json({ success: false, message: '请选择要删除的商品' }); + } + + // 构建删除条件 + const placeholders = cart_item_ids.map(() => '?').join(','); + const query = `DELETE FROM cart_items WHERE id IN (${placeholders}) AND user_id = ?`; + const params = [...cart_item_ids, userId]; + + const [result] = await getDB().execute(query, params); + + res.json({ + success: true, + message: '批量删除购物车商品成功', + data: { + deleted_count: result.affectedRows + } + }); + } catch (error) { + console.error('批量删除购物车商品失败:', error); + res.status(500).json({ success: false, message: '批量删除购物车商品失败' }); + } +}); + +/** + * @swagger + * /api/cart/clear: + * delete: + * summary: 清空购物车 + * tags: [Cart] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: 清空购物车成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * message: + * type: string + * 401: + * description: 未授权 + * 500: + * description: 服务器错误 + */ +router.delete('/clear', auth, async (req, res) => { + try { + const userId = req.user.id; + + // 清空用户购物车 + await getDB().execute( + 'DELETE FROM cart_items WHERE user_id = ?', + [userId] + ); + + res.json({ + success: true, + message: '清空购物车成功' + }); + } catch (error) { + console.error('清空购物车失败:', error); + res.status(500).json({ success: false, message: '清空购物车失败' }); + } +}); + +/** + * @swagger + * /api/cart/count: + * get: + * summary: 获取购物车商品数量 + * tags: [Cart] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: 获取购物车商品数量成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * data: + * type: object + * properties: + * count: + * type: integer + * description: 购物车商品总数量 + * 401: + * description: 未授权 + * 500: + * description: 服务器错误 + */ +router.get('/count', auth, async (req, res) => { + try { + const userId = req.user.id; + + // 获取购物车商品总数量 + const [result] = await getDB().execute( + 'SELECT SUM(quantity) as count FROM cart_items WHERE user_id = ?', + [userId] + ); + + const count = result[0].count || 0; + + res.json({ + success: true, + data: { count } + }); + } catch (error) { + console.error('获取购物车商品数量失败:', error); + res.status(500).json({ success: false, message: '获取购物车商品数量失败' }); + } +}); + +/** + * @swagger + * /api/cart/checkout: + * post: + * summary: 购物车结账 + * tags: [Cart] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * cart_item_ids: + * type: array + * items: + * type: integer + * description: 要结账的购物车项ID数组 + * shipping_address: + * type: string + * description: 收货地址 + * required: + * - cart_item_ids + * - shipping_address + * responses: + * 201: + * description: 结账成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * message: + * type: string + * data: + * type: object + * properties: + * order_id: + * type: integer + * order_no: + * type: string + * total_amount: + * type: integer + * total_points: + * type: integer + * total_rongdou: + * type: integer + * 400: + * description: 参数错误或库存不足 + * 401: + * description: 未授权 + * 500: + * description: 服务器错误 + */ +router.post('/checkout', auth, async (req, res) => { + const db = getDB(); + await db.query('START TRANSACTION'); + + try { + const { cart_item_ids, shipping_address } = req.body; + const userId = req.user.id; + + // 验证参数 + if (!cart_item_ids || !Array.isArray(cart_item_ids) || cart_item_ids.length === 0) { + await db.query('ROLLBACK'); + return res.status(400).json({ success: false, message: '请选择要结账的商品' }); + } + + if (!shipping_address) { + await db.query('ROLLBACK'); + return res.status(400).json({ success: false, message: '请填写收货地址' }); + } + + // 获取购物车商品信息 + const placeholders = cart_item_ids.map(() => '?').join(','); + const cartQuery = ` + SELECT + c.id, c.product_id, c.quantity, c.spec_combination_id, + p.name, p.price, p.points_price, p.rongdou_price, p.stock, p.status, + psc.price_adjustment, psc.points_adjustment, psc.rongdou_adjustment, psc.stock as spec_stock + FROM cart_items c + LEFT JOIN products p ON c.product_id = p.id + LEFT JOIN product_spec_combinations psc ON c.spec_combination_id = psc.id + WHERE c.id IN (${placeholders}) AND c.user_id = ? + `; + + const [cartItems] = await db.execute(cartQuery, [...cart_item_ids, userId]); + + if (cartItems.length === 0) { + await db.query('ROLLBACK'); + return res.status(400).json({ success: false, message: '购物车商品不存在' }); + } + + // 验证商品状态和库存 + let totalAmount = 0; + let totalPoints = 0; + let totalRongdou = 0; + + for (const item of cartItems) { + if (item.status !== 'active') { + await db.query('ROLLBACK'); + return res.status(400).json({ success: false, message: `商品 ${item.name} 已下架` }); + } + + const availableStock = item.spec_combination_id ? item.spec_stock : item.stock; + if (availableStock < item.quantity) { + await db.query('ROLLBACK'); + return res.status(400).json({ success: false, message: `商品 ${item.name} 库存不足` }); + } + + const finalPrice = item.price + (item.price_adjustment || 0); + const finalPointsPrice = item.points_price + (item.points_adjustment || 0); + const finalRongdouPrice = item.rongdou_price + (item.rongdou_adjustment || 0); + + totalAmount += finalPrice * item.quantity; + totalPoints += finalPointsPrice * item.quantity; + totalRongdou += finalRongdouPrice * item.quantity; + } + + // 检查用户积分和融豆是否足够 + const [users] = await db.execute( + 'SELECT points, rongdou FROM users WHERE id = ?', + [userId] + ); + + if (users.length === 0) { + await db.query('ROLLBACK'); + return res.status(404).json({ success: false, message: '用户不存在' }); + } + + const user = users[0]; + + if (user.points < totalPoints) { + await db.query('ROLLBACK'); + return res.status(400).json({ success: false, message: '积分不足' }); + } + + if (user.rongdou < totalRongdou) { + await db.query('ROLLBACK'); + return res.status(400).json({ success: false, message: '融豆不足' }); + } + + // 生成订单号 + const orderNo = 'ORD' + Date.now() + Math.random().toString(36).substr(2, 5).toUpperCase(); + + // 创建订单 + const [orderResult] = await db.execute( + `INSERT INTO orders (order_no, user_id, total_amount, total_points, total_rongdou, + status, shipping_address, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, 'pending', ?, NOW(), NOW())`, + [orderNo, userId, totalAmount, totalPoints, totalRongdou, shipping_address] + ); + + const orderId = orderResult.insertId; + + // 创建订单项 + for (const item of cartItems) { + const finalPrice = item.price + (item.price_adjustment || 0); + const finalPointsPrice = item.points_price + (item.points_adjustment || 0); + const finalRongdouPrice = item.rongdou_price + (item.rongdou_adjustment || 0); + + await db.execute( + `INSERT INTO order_items (order_id, product_id, spec_combination_id, quantity, + price, points_price, rongdou_price, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?, NOW())`, + [orderId, item.product_id, item.spec_combination_id, item.quantity, + finalPrice, finalPointsPrice, finalRongdouPrice] + ); + + // 更新库存 + if (item.spec_combination_id) { + await db.execute( + 'UPDATE product_spec_combinations SET stock = stock - ? WHERE id = ?', + [item.quantity, item.spec_combination_id] + ); + } else { + await db.execute( + 'UPDATE products SET stock = stock - ? WHERE id = ?', + [item.quantity, item.product_id] + ); + } + } + + // 扣除用户积分和融豆 + await db.execute( + 'UPDATE users SET points = points - ?, rongdou = rongdou - ? WHERE id = ?', + [totalPoints, totalRongdou, userId] + ); + + // 删除已结账的购物车项 + const deletePlaceholders = cart_item_ids.map(() => '?').join(','); + await db.execute( + `DELETE FROM cart_items WHERE id IN (${deletePlaceholders}) AND user_id = ?`, + [...cart_item_ids, userId] + ); + + await db.query('COMMIT'); + + res.status(201).json({ + success: true, + message: '结账成功', + data: { + order_id: orderId, + order_no: orderNo, + total_amount: totalAmount, + total_points: totalPoints, + total_rongdou: totalRongdou + } + }); + + } catch (error) { + await db.query('ROLLBACK'); + console.error('购物车结账失败:', error); + res.status(500).json({ success: false, message: '结账失败' }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/routes/matching.js b/routes/matching.js new file mode 100644 index 0000000..d9e1db5 --- /dev/null +++ b/routes/matching.js @@ -0,0 +1,626 @@ +const express = require('express'); +const router = express.Router(); +const { getDB } = require('../database'); +const matchingService = require('../services/matchingService'); +const { auth } = require('../middleware/auth'); +const { default: axios } = require('axios'); + + +router.post('/create', auth, async (req, res) => { + try { + console.log('匹配订单创建请求 - 用户ID:', req.user.id); + console.log('请求体:', req.body); + const userId = req.user.id; + const { matchingType = 'small', customAmount } = req.body; + const [user_type] = await getDB().query(`SELECT count(*) as total FROM users WHERE id=${userId} and user_type='directly_operated'`); + if(user_type[0].total > 0){ + return res.status(400).json({message: '平台暂不支持直营用户获得融豆'}) + } + // 验证匹配类型 + if (!['small', 'large'].includes(matchingType)) { + return res.status(400).json({ message: '无效的匹配类型' }); + } + + // 验证大额匹配的金额 + if (matchingType === 'large') { + if (!customAmount || typeof customAmount !== 'number') { + return res.status(400).json({ message: '大额匹配需要指定金额' }); + } + if (customAmount < 3000 || customAmount > 50000) { + return res.status(400).json({ message: '大额匹配金额必须在3000-50000之间' }); + } + } + + // 检查用户是否有未完成的匹配订单(排除已失败的订单) + const [existingOrders] = await getDB().execute( + 'SELECT COUNT(*) as count FROM matching_orders WHERE initiator_id = ? AND status IN ("pending", "matching")', + [userId] + ); + + if (existingOrders[0].count > 0) { + return res.status(400).json({ message: '您有未完成的匹配订单,请等待完成后再创建新订单' }); + } + + // 校验用户是否已上传必要的证件和收款码 + const [userInfo] = await getDB().execute( + 'SELECT business_license, id_card_front, id_card_back, wechat_qr, alipay_qr, bank_card, unionpay_qr FROM users WHERE id = ?', + [userId] + ); + + if (userInfo.length === 0) { + return res.status(404).json({ message: '用户不存在' }); + } + + const user = userInfo[0]; + + // 检查证件是否已上传 + if (!user.business_license || !user.id_card_front || !user.id_card_back) { + return res.status(400).json({ + message: '开始匹配前,请先在个人中心上传营业执照和身份证正反面', + code: 'MISSING_DOCUMENTS' + }); + } + + // 检查收款码是否已上传(至少需要一种收款方式) + if (!user.wechat_qr && !user.alipay_qr && !user.bank_card && !user.unionpay_qr) { + return res.status(400).json({ + message: '开始匹配前,请先在个人中心设置至少一种收款方式(微信、支付宝、银行卡或云闪付)', + code: 'MISSING_PAYMENT_METHODS' + }); + } + + // 创建匹配订单 + const result = await matchingService.createMatchingOrder(userId, matchingType, customAmount); + + const message = matchingType === 'small' + ? '小额匹配成功!已为您生成3笔转账分配' + : `大额匹配成功!已为您生成${result.totalAmount}笔转账分配`; + + res.json({ + success: true, + message, + data: { + matchingOrderId: result.orderId, + amounts: result.amounts, + matchingType: result.matchingType, + totalAmount: result.totalAmount + } + }); + + } catch (error) { + console.error('创建匹配订单失败:', error); + res.status(500).json({ message: error.message || '匹配失败,请稍后重试' }); + } +}); + + +router.get('/my-orders', auth, async (req, res) => { + try { + const userId = req.user.id; + const page = parseInt(req.query.page) || 1; + const limit = parseInt(req.query.limit) || 10; + + const orders = await matchingService.getUserMatchingOrders(userId, page, limit); + + res.json({ + success: true, + data: orders + }); + + } catch (error) { + console.error('获取匹配订单失败:', error); + res.status(500).json({ message: '获取匹配订单失败' }); + } +}); + +/** + * @swagger + * /api/matching/pending-allocations: + * get: + * summary: 获取用户待处理的分配 + * tags: [Matching] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: 成功获取待处理分配 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * data: + * type: array + * items: + * $ref: '#/components/schemas/Allocation' + * 401: + * description: 未授权 + * 500: + * description: 服务器错误 + */ +router.get('/pending-allocations', auth, async (req, res) => { + try { + const userId = req.user.id; + + const allocations = await matchingService.getUserPendingAllocations(userId); + + res.json({ + success: true, + data: allocations + }); + + } catch (error) { + console.error('获取待处理分配失败:', error); + res.status(500).json({ message: '获取待处理分配失败' }); + } +}); + +/** + * @swagger + * /api/matching/allocation/{id}: + * get: + * summary: 获取分配详情 + * tags: [Matching] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: 分配ID + * responses: + * 200: + * description: 成功获取分配详情 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * data: + * $ref: '#/components/schemas/Allocation' + * 401: + * description: 未授权 + * 403: + * description: 无权限访问 + * 404: + * description: 分配不存在 + * 500: + * description: 服务器错误 + */ +router.get('/allocation/:id', auth, async (req, res) => { + try { + const db = getDB(); + const allocationId = req.params.id; + const userId = req.user.id; + + // 首先获取分配信息 + const [allocations] = await db.execute(` + SELECT + oa.id, + oa.from_user_id, + oa.to_user_id, + oa.cycle_number, + oa.amount, + oa.status, + oa.created_at, + from_user.username as from_user_name, + to_user.username as to_user_name + FROM transfers oa + JOIN users from_user ON oa.from_user_id = from_user.id + JOIN users to_user ON oa.to_user_id = to_user.id + WHERE oa.id = ? + `, [allocationId]); + + if (allocations.length === 0) { + return res.status(404).json({ success: false, message: '分配不存在' }); + } + + const allocation = allocations[0]; + + // 检查权限:只有分配的发起人或接收人可以查看 + if (allocation.from_user_id !== userId && allocation.to_user_id !== userId) { + return res.status(403).json({ success: false, message: '无权限访问此分配' }); + } + + res.json({ + success: true, + data: allocation + }); + } catch (error) { + console.error('获取分配详情错误:', error); + res.status(500).json({ success: false, message: '获取分配详情失败' }); + } +}); + +/** + * @swagger + * /api/matching/confirm-allocation/{allocationId}: + * post: + * summary: 确认分配(创建转账) + * tags: [Matching] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: allocationId + * required: true + * schema: + * type: integer + * description: 分配ID + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * transferAmount: + * type: number + * description: 转账金额 + * description: + * type: string + * description: 转账描述 + * voucher: + * type: string + * description: 转账凭证(图片URL) + * required: + * - voucher + * responses: + * 200: + * description: 转账凭证提交成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * message: + * type: string + * data: + * type: object + * properties: + * transferId: + * type: integer + * 400: + * description: 参数错误 + * 401: + * description: 未授权 + * 500: + * description: 服务器错误 + */ +router.post('/confirm-allocation/:allocationId', auth, async (req, res) => { + try { + const { allocationId } = req.params; + const userId = req.user.id; + const { transferAmount, description, voucher } = req.body; // 获取转账信息 + + // 校验转账凭证是否存在 + if (!voucher) { + return res.status(400).json({ + success: false, + message: '请上传转账凭证' + }); + } + + // 调用服务层方法,传递完整的转账信息 + const transferId = await matchingService.confirmAllocation( + allocationId, + userId, + transferAmount, + description, + voucher + ); + + res.json({ + success: true, + message: '转账凭证已提交,转账记录已创建', + data: { transferId } + }); + + axios.post('http://localhost:5000/ocr',{ + id: allocationId + }).then(res => { + console.log(res.data) + }) + + } catch (error) { + console.error('确认分配失败:', error); + res.status(500).json({ message: error.message || '确认分配失败' }); + } +}); + +/** + * @swagger + * /api/matching/reject-allocation/{allocationId}: + * post: + * summary: 拒绝分配 + * tags: [Matching] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: allocationId + * required: true + * schema: + * type: integer + * description: 分配ID + * requestBody: + * content: + * application/json: + * schema: + * type: object + * properties: + * reason: + * type: string + * description: 拒绝原因 + * responses: + * 200: + * description: 拒绝分配成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * message: + * type: string + * 401: + * description: 未授权 + * 404: + * description: 分配不存在或无权限 + * 500: + * description: 服务器错误 + */ +router.post('/reject-allocation/:allocationId', auth, async (req, res) => { + try { + const { allocationId } = req.params; + const userId = req.user.id; + const { reason } = req.body; + + const db = getDB(); + + // 获取分配信息 + const [allocations] = await db.execute( + 'SELECT * FROM transfers WHERE id = ? AND from_user_id = ?', + [allocationId, userId] + ); + + if (allocations.length === 0) { + return res.status(404).json({ message: '分配不存在或无权限' }); + } + + const allocation = allocations[0]; + + // 更新分配状态 + await db.execute( + 'UPDATE transfers SET status = "rejected" WHERE id = ?', + [allocationId] + ); + + // 记录拒绝动作 + await db.execute( + 'INSERT INTO matching_records (matching_order_id, user_id, action, note) VALUES (?, ?, "reject", ?)', + [allocation.matching_order_id, userId, reason || '用户拒绝'] + ); + + // 检查订单状态是否需要更新 + const statusResult = await matchingService.checkOrderStatusAfterRejection( + allocation.matching_order_id, + allocation.cycle_number + ); + + let message = '已拒绝分配'; + if (statusResult === 'failed') { + message = '已拒绝分配,该轮次所有分配均被拒绝,匹配订单已失败'; + } + + res.json({ + success: true, + message + }); + + } catch (error) { + console.error('拒绝分配失败:', error); + res.status(500).json({ message: '拒绝分配失败' }); + } +}); + +/** + * @swagger + * /api/matching/order/{orderId}: + * get: + * summary: 获取匹配订单详情 + * tags: [Matching] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: orderId + * required: true + * schema: + * type: integer + * description: 订单ID + * responses: + * 200: + * description: 成功获取订单详情 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * data: + * type: object + * properties: + * order: + * $ref: '#/components/schemas/MatchingOrder' + * allocations: + * type: array + * items: + * $ref: '#/components/schemas/Allocation' + * records: + * type: array + * items: + * type: object + * 401: + * description: 未授权 + * 403: + * description: 无权限查看 + * 404: + * description: 订单不存在 + * 500: + * description: 服务器错误 + */ +router.get('/order/:orderId', auth, async (req, res) => { + try { + const { orderId } = req.params; + const userId = req.user.id; + + const db = getDB(); + + // 获取订单基本信息 + const [orders] = await db.execute( + `SELECT mo.*, u.username as initiator_name,u.real_name as initiator_real_name + FROM matching_orders mo + JOIN users u ON mo.initiator_id = u.id + WHERE mo.id = ?`, + [orderId] + ); + + if (orders.length === 0) { + return res.status(404).json({ message: '匹配订单不存在' }); + } + + const order = orders[0]; + + // 检查权限(订单发起人或参与者) + const [userCheck] = await db.execute( + `SELECT COUNT(*) as count FROM ( + SELECT initiator_id as user_id FROM matching_orders WHERE id = ? + UNION + SELECT from_user_id as user_id FROM transfers WHERE id = ? + ) as participants WHERE user_id = ?`, + [orderId, orderId, userId] + ); + + if (userCheck[0].count === 0) { + return res.status(403).json({ message: '无权限查看此订单' }); + } + + // 获取分配信息 + const [allocations] = await db.execute( + `SELECT oa.*, + uf.username as from_user_name, + ut.username as to_user_name + FROM transfers oa + JOIN users uf ON oa.from_user_id = uf.id + JOIN users ut ON oa.to_user_id = ut.id + WHERE oa.id = ? + ORDER BY oa.cycle_number, oa.created_at`, + [orderId] + ); + + // 获取匹配记录 + const [records] = await db.execute( + `SELECT mr.*, u.username + FROM matching_records mr + JOIN users u ON mr.user_id = u.id + WHERE mr.matching_order_id = ? + ORDER BY mr.created_at`, + [orderId] + ); + + res.json({ + success: true, + data: { + order, + allocations, + records + } + }); + + } catch (error) { + console.error('获取匹配订单详情失败:', error); + res.status(500).json({ message: '获取匹配订单详情失败' }); + } +}); + +/** + * @swagger + * /api/matching/stats: + * get: + * summary: 获取匹配统计信息 + * tags: [Matching] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: 成功获取统计信息 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * data: + * type: object + * properties: + * userStats: + * type: object + * properties: + * initiated_orders: + * type: integer + * participated_allocations: + * type: integer + * total_initiated_amount: + * type: number + * total_participated_amount: + * type: number + * 401: + * description: 未授权 + * 500: + * description: 服务器错误 + */ +router.get('/stats', auth, async (req, res) => { + try { + const userId = req.user.id; + const db = getDB(); + + // 获取用户统计 + const [userStats] = await db.execute( + `SELECT + COUNT(CASE WHEN mo.initiator_id = ? THEN 1 END) as initiated_orders, + COUNT(CASE WHEN oa.from_user_id = ? THEN 1 END) as participated_allocations, + SUM(CASE WHEN mo.initiator_id = ? AND mo.status = 'completed' THEN mo.amount ELSE 0 END) as total_initiated_amount, + SUM(CASE WHEN oa.from_user_id = ? AND oa.status = 'completed' THEN oa.amount ELSE 0 END) as total_participated_amount + FROM matching_orders mo + LEFT JOIN transfers oa ON mo.id = oa.id`, + [userId, userId, userId, userId] + ); + + res.json({ + success: true, + data: { + userStats: userStats[0] + } + }); + + } catch (error) { + console.error('获取匹配统计失败:', error); + res.status(500).json({ message: '获取匹配统计失败' }); + } +}); + + + +module.exports = router; \ No newline at end of file diff --git a/routes/matchingAdmin.js b/routes/matchingAdmin.js new file mode 100644 index 0000000..cd47b51 --- /dev/null +++ b/routes/matchingAdmin.js @@ -0,0 +1,621 @@ +const express = require('express'); +const router = express.Router(); +const db = require('../database'); +const { auth, adminAuth } = require('../middleware/auth'); +const logger = require('../config/logger'); +const matchingService = require('../services/matchingService'); +const dayjs = require('dayjs'); + +/** + * @swagger + * tags: + * name: MatchingAdmin + * description: 匹配订单管理员相关接口 + */ + +/** + * @swagger + * components: + * schemas: + * UnreasonableMatch: + * type: object + * properties: + * allocation_id: + * type: integer + * description: 分配ID + * from_user_id: + * type: integer + * description: 发送方用户ID + * to_user_id: + * type: integer + * description: 接收方用户ID + * amount: + * type: number + * description: 分配金额 + * status: + * type: string + * enum: [pending, confirmed, rejected, cancelled] + * description: 分配状态 + * to_username: + * type: string + * description: 接收方用户名 + * to_user_balance: + * type: number + * description: 接收方用户余额 + * from_username: + * type: string + * description: 发送方用户名 + * from_user_balance: + * type: number + * description: 发送方用户余额 + */ + +/** + * @swagger + * /api/matching-admin/unreasonable-matches: + * get: + * summary: 获取不合理的匹配记录(正余额用户被匹配的情况) + * tags: [MatchingAdmin] + * security: + * - bearerAuth: [] + * parameters: + * - in: query + * name: page + * schema: + * type: integer + * default: 1 + * description: 页码 + * - in: query + * name: limit + * schema: + * type: integer + * default: 20 + * description: 每页数量 + * responses: + * 200: + * description: 成功获取不合理匹配记录 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * data: + * type: object + * properties: + * matches: + * type: array + * items: + * $ref: '#/components/schemas/UnreasonableMatch' + * pagination: + * type: object + * properties: + * page: + * type: integer + * limit: + * type: integer + * total: + * type: integer + * totalPages: + * type: integer + * 401: + * description: 未授权 + * 403: + * description: 无管理员权限 + * 500: + * description: 服务器错误 + */ +router.get('/unreasonable-matches', auth, adminAuth, async (req, res) => { + try { + const page = parseInt(req.query.page) || 1; + const limit = parseInt(req.query.limit) || 20; + const offset = (page - 1) * limit; + + // 查找正余额用户被匹配的情况 + const query = `SELECT + oa.id as allocation_id, + oa.from_user_id, + oa.to_user_id, + oa.amount, + oa.status, + oa.outbound_date, + oa.created_at, + u_to.username as to_username, + u_to.balance as to_user_balance, + u_from.username as from_username, + u_from.balance as from_user_balance, + mo.amount as total_order_amount + FROM transfers oa + JOIN users u_to ON oa.to_user_id = u_to.id + JOIN users u_from ON oa.from_user_id = u_from.id + JOIN matching_orders mo ON oa.id = mo.id + WHERE oa.source_type = 'allocation' + AND u_to.balance > 0 + AND u_to.is_system_account = FALSE + AND oa.status IN ('pending', 'confirmed') + ORDER BY oa.created_at DESC + LIMIT ${offset}, ${limit}`; + + const countQuery = `SELECT COUNT(*) as total + FROM transfers oa + JOIN users u_to ON oa.to_user_id = u_to.id + WHERE oa.source_type = 'allocation' + AND u_to.balance > 0 + AND u_to.is_system_account = FALSE + AND oa.status IN ('pending', 'confirmed')`; + + const unreasonableMatches = await db.executeQuery(query); + + // 获取总数 + const countResult = await db.executeQuery(countQuery); + + const total = countResult[0].total; + + res.json({ + success: true, + data: { + matches: unreasonableMatches, + pagination: { + page, + limit, + total, + totalPages: Math.ceil(total / limit) + } + } + }); + + } catch (error) { + console.error('获取不合理匹配记录失败:', error); + res.status(500).json({ message: '获取不合理匹配记录失败' }); + } +}); + + +/** + * @swagger + * /api/matching-admin/matching-stats: + * get: + * summary: 获取匹配统计信息 + * tags: [MatchingAdmin] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: 成功获取匹配统计信息 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * data: + * type: object + * properties: + * currentStats: + * type: object + * properties: + * unreasonable_matches: + * type: integer + * reasonable_matches: + * type: integer + * system_matches: + * type: integer + * unreasonable_amount: + * type: number + * reasonable_amount: + * type: number + * yesterdayStats: + * type: object + * properties: + * total_outbound: + * type: number + * unique_amounts: + * type: integer + * 401: + * description: 未授权 + * 403: + * description: 无管理员权限 + * 500: + * description: 服务器错误 + */ +router.get('/matching-stats', auth, adminAuth, async (req, res) => { + try { + + // 获取各种统计数据 + const stats = await db.executeQuery( + `SELECT + COUNT(CASE WHEN u_to.balance > 0 AND u_to.is_system_account = FALSE AND oa.status IN ('pending', 'confirmed') THEN 1 END) as unreasonable_matches, + COUNT(CASE WHEN u_to.balance < 0 AND u_to.is_system_account = FALSE AND oa.status IN ('pending', 'confirmed') THEN 1 END) as reasonable_matches, + COUNT(CASE WHEN u_to.is_system_account = TRUE AND oa.status IN ('pending', 'confirmed') THEN 1 END) as system_matches, + SUM(CASE WHEN u_to.balance > 0 AND u_to.is_system_account = FALSE AND oa.status IN ('pending', 'confirmed') THEN oa.amount ELSE 0 END) as unreasonable_amount, + SUM(CASE WHEN u_to.balance < 0 AND u_to.is_system_account = FALSE AND oa.status IN ('pending', 'confirmed') THEN oa.amount ELSE 0 END) as reasonable_amount + FROM transfers oa + JOIN users u_to ON oa.to_user_id = u_to.id + WHERE oa.source_type = 'allocation'` + ); + + // 获取昨天的匹配验证统计 + const yesterdayStr = dayjs().subtract(1, 'day').format('YYYY-MM-DD'); + + const yesterdayStats = await db.executeQuery( + `SELECT + SUM(oa.amount) as total_outbound, + COUNT(DISTINCT oa.amount) as unique_amounts + FROM transfers oa + JOIN users u ON oa.from_user_id = u.id + WHERE oa.source_type = 'allocation' AND DATE(oa.outbound_date) = ? AND oa.status = 'confirmed' AND u.is_system_account = FALSE`, + [yesterdayStr] + ); + + res.json({ + success: true, + data: { + currentStats: stats[0], + yesterdayStats: yesterdayStats[0] + } + }); + + } catch (error) { + console.error('获取匹配统计失败:', error); + res.status(500).json({ message: '获取匹配统计失败' }); + } +}); + +/** + * @swagger + * /api/matching-admin/fix-all-unreasonable: + * post: + * summary: 批量修复所有不合理匹配 + * tags: [MatchingAdmin] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: 批量修复完成 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * message: + * type: string + * data: + * type: object + * properties: + * fixedCount: + * type: integer + * description: 成功修复的记录数 + * errorCount: + * type: integer + * description: 修复失败的记录数 + * errors: + * type: array + * items: + * type: string + * description: 错误信息列表(最多10条) + * 401: + * description: 未授权 + * 403: + * description: 无管理员权限 + * 500: + * description: 服务器错误 + */ +router.post('/fix-all-unreasonable', auth, adminAuth, async (req, res) => { + try { + let fixedCount = 0; + let errorCount = 0; + const errors = []; + + // 获取所有不合理的匹配记录 + const unreasonableMatches = await db.executeQuery( + `SELECT oa.id, oa.from_user_id, oa.to_user_id, oa.amount, u_to.username, u_to.balance + FROM transfers oa + JOIN users u_to ON oa.to_user_id = u_to.id + WHERE oa.source_type = 'allocation' + AND u_to.balance > 0 + AND u_to.is_system_account = FALSE + AND oa.status IN ('pending', 'confirmed') + ORDER BY u_to.balance DESC` + ); + + for (const match of unreasonableMatches) { + const connection = await db.getDB().getConnection(); + try { + await connection.query('START TRANSACTION'); + + // 尝试重新分配给负余额用户 + const usedTargetUsers = new Set([match.to_user_id]); + const newTargetUser = await matchingService.getMatchingTargetExcluding(match.from_user_id, usedTargetUsers); + + // 获取当前时间 + const currentTime = new Date(); + + if (newTargetUser) { + // 更新分配目标 + await connection.execute( + 'UPDATE transfers SET to_user_id = ?, updated_at = ? WHERE id = ?', + [newTargetUser, currentTime, match.id] + ); + + // 记录操作日志 + await connection.execute( + 'INSERT INTO admin_operation_logs (admin_id, operation_type, target_type, target_id, description, created_at) VALUES (?, "batch_fix_matching", "allocation", ?, ?, ?)', + [req.user.id, match.id, `批量修复:从正余额用户${match.username}(余额${match.balance}元)重新分配${match.amount}元给负余额用户`, currentTime] + ); + + fixedCount++; + } else { + // 如果没有可用的负余额用户,取消分配 + await connection.execute( + 'UPDATE transfers SET status = "cancelled", updated_at = ? WHERE id = ?', + [currentTime, match.id] + ); + + await connection.execute( + 'INSERT INTO admin_operation_logs (admin_id, operation_type, target_type, target_id, description, created_at) VALUES (?, "batch_fix_matching", "allocation", ?, ?, ?)', + [req.user.id, match.id, `批量修复:取消正余额用户${match.username}(余额${match.balance}元)的${match.amount}元分配`, currentTime] + ); + + fixedCount++; + } + + await connection.query('COMMIT'); + connection.release(); + + } catch (error) { + await connection.query('ROLLBACK'); + connection.release(); + errorCount++; + errors.push(`分配ID ${match.id}: ${error.message}`); + console.error(`修复分配${match.id}失败:`, error); + } + } + + res.json({ + success: true, + message: `批量修复完成:成功修复${fixedCount}条记录,失败${errorCount}条记录`, + data: { + fixedCount, + errorCount, + errors: errors.slice(0, 10) // 只返回前10个错误 + } + }); + + } catch (error) { + console.error('批量修复不合理匹配失败:', error); + res.status(500).json({ message: '批量修复不合理匹配失败' }); + } +}); + +/** + * @swagger + * /api/matching-admin/confirm-allocation/{allocationId}: + * post: + * summary: 管理员确认分配 + * tags: [MatchingAdmin] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: allocationId + * required: true + * schema: + * type: integer + * description: 分配ID + * responses: + * 200: + * description: 分配确认成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * message: + * type: string + * 401: + * description: 未授权 + * 403: + * description: 无管理员权限 + * 404: + * description: 分配不存在或状态不是待处理 + * 500: + * description: 服务器错误 + */ +router.post('/confirm-allocation/:allocationId', auth, adminAuth, async (req, res) => { + try { + const { allocationId } = req.params; + const adminId = req.user.id; + + const connection = await db.getDB().getConnection(); + + try { + await connection.query('START TRANSACTION'); + + // 检查分配是否存在且状态为pending + const [allocations] = await connection.execute( + `SELECT oa.*, u_from.username as from_username, u_to.username as to_username + FROM transfers oa + JOIN users u_from ON oa.from_user_id = u_from.id + JOIN users u_to ON oa.to_user_id = u_to.id + WHERE oa.source_type = 'allocation' AND oa.id = ? AND oa.status = 'pending'`, + [allocationId] + ); + + if (allocations.length === 0) { + await connection.query('ROLLBACK'); + connection.release(); + return res.status(404).json({ message: '分配不存在或状态不是待处理' }); + } + + const allocation = allocations[0]; + + // 获取当前时间 + const currentTime = new Date(); + + // 计算3小时后的截止时间 + const deadline = new Date(); + deadline.setHours(deadline.getHours() + 3); + + // 创建转账记录,直接设置为confirmed状态 + const transferDescription = `匹配订单 ${allocation.matching_order_id} 第 ${allocation.cycle_number} 轮转账(管理员确认)`; + const [transferResult] = await connection.execute( + `INSERT INTO transfers (from_user_id, to_user_id, amount, transfer_type, status, description, deadline_at, confirmed_at, source_type) VALUES (?, ?, ?, "user_to_user", "confirmed", ?, ?, ?, 'allocation')`, + [ + allocation.from_user_id, + allocation.to_user_id, + allocation.amount, + transferDescription, + deadline, + currentTime + ] + ); + + // 更新分配状态为已确认,并关联转账记录 + await connection.execute( + 'UPDATE transfers SET status = "confirmed", transfer_id = ?, confirmed_at = ?, updated_at = ? WHERE id = ?', + [transferResult.insertId, currentTime, currentTime, allocationId] + ); + + // 记录管理员操作日志 + await connection.execute( + 'INSERT INTO admin_operation_logs (admin_id, operation_type, target_type, target_id, description, created_at) VALUES (?, "confirm_allocation", "allocation", ?, ?, ?)', + [adminId, allocationId, `管理员确认分配:${allocation.from_username} -> ${allocation.to_username},金额:${allocation.amount}元`, currentTime] + ); + + // 记录确认动作到匹配记录 + await connection.execute( + 'INSERT INTO matching_records (matching_order_id, user_id, action, amount, note) VALUES (?, ?, "confirm", ?, ?)', + [ + allocation.matching_order_id, + adminId, + allocation.amount, + '管理员确认分配' + ] + ); + + await connection.query('COMMIT'); + connection.release(); + + res.json({ + success: true, + message: '分配已确认' + }); + + } catch (innerError) { + await connection.query('ROLLBACK'); + connection.release(); + throw innerError; + } + + } catch (error) { + console.error('确认分配失败:', error); + res.status(500).json({ message: error.message || '确认分配失败' }); + } + }); + + /** + * @swagger + * /api/matching-admin/cancel-allocation/{allocationId}: + * post: + * summary: 管理员取消分配 + * tags: [MatchingAdmin] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: allocationId + * required: true + * schema: + * type: integer + * description: 分配ID + * responses: + * 200: + * description: 分配取消成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * message: + * type: string + * 401: + * description: 未授权 + * 403: + * description: 无管理员权限 + * 404: + * description: 分配不存在或状态不是待处理 + * 500: + * description: 服务器错误 + */ +router.post('/cancel-allocation/:allocationId', auth, adminAuth, async (req, res) => { + try { + const { allocationId } = req.params; + const adminId = req.user.id; + + const connection = await db.getDB().getConnection(); + + try { + await connection.query('START TRANSACTION'); + + // 检查分配是否存在且状态为pending + const [allocations] = await connection.execute( + `SELECT oa.*, u_from.username as from_username, u_to.username as to_username + FROM transfers oa + JOIN users u_from ON oa.from_user_id = u_from.id + JOIN users u_to ON oa.to_user_id = u_to.id + WHERE oa.source_type = 'allocation' AND oa.id = ? AND oa.status = 'pending'`, + [allocationId] + ); + + if (allocations.length === 0) { + await connection.query('ROLLBACK'); + connection.release(); + return res.status(404).json({ message: '分配不存在或状态不是待处理' }); + } + + const allocation = allocations[0]; + + // 获取当前时间 + const currentTime = new Date(); + + // 更新分配状态为已取消 + await connection.execute( + 'UPDATE transfers SET status = "cancelled", updated_at = ? WHERE id = ?', + [currentTime, allocationId] + ); + + // 记录管理员操作日志 + await connection.execute( + 'INSERT INTO admin_operation_logs (admin_id, operation_type, target_type, target_id, description, created_at) VALUES (?, "cancel_allocation", "allocation", ?, ?, ?)', + [adminId, allocationId, `管理员取消分配:${allocation.from_username} -> ${allocation.to_username},金额:${allocation.amount}元`, currentTime] + ); + + await connection.query('COMMIT'); + connection.release(); + + res.json({ + success: true, + message: '分配已取消' + }); + + } catch (innerError) { + await connection.query('ROLLBACK'); + connection.release(); + throw innerError; + } + + } catch (error) { + console.error('取消分配失败:', error); + res.status(500).json({ message: error.message || '取消分配失败' }); + } + }); + + module.exports = router; \ No newline at end of file diff --git a/routes/orders.js b/routes/orders.js new file mode 100644 index 0000000..8f82071 --- /dev/null +++ b/routes/orders.js @@ -0,0 +1,1357 @@ +const express = require('express'); +const { getDB } = require('../database'); +const { auth, adminAuth } = require('../middleware/auth'); + +const router = express.Router(); + +// 订单管理路由 + + +// 获取订单列表 +router.get('/', auth, async (req, res) => { + try { + const { page = 1, limit = 10, search = '', orderNumber = '', username = '', status = '', startDate = '', endDate = '' } = req.query; + + + + // 确保参数为有效数字 + const pageNum = parseInt(page) || 1; + const limitNum = parseInt(limit) || 10; + const offset = (pageNum - 1) * limitNum; + const isAdmin = req.user.role === 'admin'; + + let whereClause = 'WHERE 1=1'; + const params = []; + + // 非管理员只能查看自己的订单 + if (!isAdmin) { + whereClause += ' AND o.user_id = ?'; + params.push(req.user.id); + } + + if (search) { + whereClause += ' AND (o.order_no LIKE ? OR u.username LIKE ?)'; + params.push(`%${search}%`, `%${search}%`); + } + + if (orderNumber) { + whereClause += ' AND o.order_no LIKE ?'; + params.push(`%${orderNumber}%`); + } + + if (username) { + whereClause += ' AND u.username LIKE ?'; + params.push(`%${username}%`); + } + + if (status && status.trim()) { + whereClause += ' AND o.status = ?'; + params.push(status); + } + + if (startDate && startDate.trim()) { + whereClause += ' AND DATE(o.created_at) >= ?'; + params.push(startDate); + } + + if (endDate && endDate.trim()) { + whereClause += ' AND DATE(o.created_at) <= ?'; + params.push(endDate); + } + + // 获取总数 + const countQuery = ` + SELECT COUNT(*) as total + FROM orders as o + LEFT JOIN users u ON o.user_id = u.id + ${whereClause} + `; + console.log(countQuery, params); + + const [countResult] = await getDB().execute(countQuery, params); + const total = countResult[0].total; + console.log(total, '数量'); + + // 获取订单列表 + const query = ` + SELECT + o.id, o.order_no, o.user_id, o.total_amount, o.total_points, + o.status, o.address, o.created_at, o.updated_at,o.total_rongdou, + u.username + FROM orders o + LEFT JOIN users u ON o.user_id = u.id + ${whereClause} + ORDER BY o.created_at DESC + LIMIT ${limitNum} OFFSET ${offset} + `; + + const [orders] = await getDB().execute(query, [...params]); + + // 为每个订单获取商品详情 + for (const order of orders) { + const [orderItems] = await getDB().execute( + `SELECT + oi.id, oi.product_id, oi.quantity, oi.price, oi.points_price, oi.rongdou_price, + oi.spec_combination_id, + p.name as product_name, p.image_url, p.description, + psc.spec_values as spec_info + FROM order_items oi + LEFT JOIN products p ON oi.product_id = p.id + LEFT JOIN product_spec_combinations psc ON oi.spec_combination_id = psc.id + WHERE oi.order_id = ?`, + [order.id] + ); + + // 处理规格信息 + for (const item of orderItems) { + if (item.spec_info) { + try { + item.spec_info = JSON.parse(item.spec_info); + } catch (e) { + item.spec_info = null; + } + } + } + + // 处理地址信息 + console.log(order.address,'order.address'); + + if (order.address) { + try { + order.address = order.address; + } catch (e) { + order.address = null; + } + } + + order.items = orderItems; + } + + res.json({ + success: true, + data: { + orders, + pagination: { + page: pageNum, + limit: limitNum, + total, + pages: Math.ceil(total / limitNum) + } + } + }); + + + router.post('/confirm', auth, async (req, res) => { + const connection = await getDB().getConnection(); + + try { + await connection.beginTransaction(); + + const { pre_order_id, address } = req.body; + const userId = req.user.id; + + // 验证必填字段 + if (!pre_order_id || !address) { + return res.status(400).json({ success: false, message: '预订单ID和收货地址为必填项' }); + } + + const { recipient_name, phone, province, city, district, detail_address } = address; + if (!recipient_name || !phone || !province || !city || !district || !detail_address) { + return res.status(400).json({ success: false, message: '收货地址信息不完整' }); + } + + // 获取预订单信息 + const [preOrders] = await connection.execute( + `SELECT id, order_no, user_id, total_amount, total_points, total_rongdou, status + FROM orders WHERE id = ? AND user_id = ? AND status = 'pre_order'`, + [pre_order_id, userId] + ); + + if (preOrders.length === 0) { + await connection.rollback(); + return res.status(404).json({ success: false, message: '预订单不存在或已处理' }); + } + + const preOrder = preOrders[0]; + + // 获取用户当前积分和融豆 + const [users] = await connection.execute( + 'SELECT points, rongdou FROM users WHERE id = ?', + [userId] + ); + + if (users.length === 0) { + await connection.rollback(); + return res.status(404).json({ success: false, message: '用户不存在' }); + } + + const user = users[0]; + + // 检查积分和融豆是否足够 + if (preOrder.total_points > 0 && user.points < preOrder.total_points) { + await connection.rollback(); + return res.status(400).json({ success: false, message: '积分不足' }); + } + + if (preOrder.total_rongdou > 0 && user.rongdou < preOrder.total_rongdou) { + await connection.rollback(); + return res.status(400).json({ success: false, message: '融豆不足' }); + } + + // 扣除积分 + if (preOrder.total_points > 0) { + await connection.execute( + 'UPDATE users SET points = points - ? WHERE id = ?', + [preOrder.total_points, userId] + ); + + // 记录积分变动历史 + await connection.execute( + `INSERT INTO points_history (user_id, type, amount, description, order_id) + VALUES (?, 'spend', ?, ?, ?)`, + [userId, preOrder.total_points, `订单消费 - ${preOrder.order_no}`, pre_order_id] + ); + } + + // 扣除融豆 + if (preOrder.total_rongdou > 0) { + await connection.execute( + 'UPDATE users SET rongdou = rongdou - ? WHERE id = ?', + [preOrder.total_rongdou, userId] + ); + + // 记录融豆变动历史 + await connection.execute( + `INSERT INTO rongdou_history (user_id, type, amount, description, order_id) + VALUES (?, 'spend', ?, ?, ?)`, + [userId, preOrder.total_rongdou, `订单消费 - ${preOrder.order_no}`, pre_order_id] + ); + } + + // 更新订单状态和收货地址 + const addressStr = JSON.stringify({ + recipient_name, + phone, + province, + city, + district, + detail_address + }); + + await connection.execute( + `UPDATE orders SET status = 'pending', address = ?, updated_at = NOW() + WHERE id = ?`, + [addressStr, pre_order_id] + ); + + await connection.commit(); + + res.json({ + success: true, + message: '订单确认成功', + data: { + order_id: pre_order_id, + order_no: preOrder.order_no + } + }); + + } catch (error) { + await connection.rollback(); + console.error('确认下单失败:', error); + res.status(500).json({ success: false, message: '确认下单失败' }); + } finally { + connection.release(); + } + }); + + + router.get('/pre-order/:id', auth, async (req, res) => { + try { + const preOrderId = req.params.id; + const userId = req.user.id; + + // 获取预订单基本信息 + const [orders] = await getDB().execute( + `SELECT id, order_no, user_id, total_amount, total_points, total_rongdou, + status, created_at FROM orders WHERE id = ? AND user_id = ? AND status = 'pre_order'`, + [preOrderId, userId] + ); + + if (orders.length === 0) { + return res.status(404).json({ success: false, message: '预订单不存在' }); + } + + const order = orders[0]; + + // 获取预订单商品详情 + const [orderItems] = await getDB().execute( + `SELECT + oi.id, oi.product_id, oi.quantity, oi.price, oi.points_price, oi.rongdou_price, + oi.spec_combination_id, + p.name as product_name, p.image_url, p.description, + psc.spec_values as spec_info + FROM order_items oi + LEFT JOIN products p ON oi.product_id = p.id + LEFT JOIN product_spec_combinations psc ON oi.spec_combination_id = psc.id + WHERE oi.order_id = ?`, + [preOrderId] + ); + + // 处理规格信息 + for (const item of orderItems) { + if (item.spec_info) { + try { + item.spec_info = JSON.parse(item.spec_info); + } catch (e) { + item.spec_info = null; + } + } + } + + res.json({ + success: true, + data: { + ...order, + items: orderItems + } + }); + } catch (error) { + console.error('获取预订单详情失败:', error); + res.status(500).json({ success: false, message: '获取预订单详情失败' }); + } + }); + } catch (error) { + console.error('获取订单列表失败:', error); + res.status(500).json({ success: false, message: '获取订单列表失败' }); + } +}); + + +router.get('/:id', auth, async (req, res) => { + try { + const { id } = req.params; + const isAdmin = req.user.role === 'admin'; + + let whereClause = 'WHERE o.id = ?'; + const params = [id]; + + // 非管理员只能查看自己的订单 + if (!isAdmin) { + whereClause += ' AND o.user_id = ?'; + params.push(req.user.id); + } + + const query = ` + SELECT + o.id, o.order_no, o.user_id, o.total_amount, o.total_points, + o.status, o.address, o.created_at, o.updated_at, + u.username, u.phone + FROM orders o + LEFT JOIN users u ON o.user_id = u.id + ${whereClause} + `; + + const [orders] = await getDB().execute(query, params); + + if (orders.length === 0) { + return res.status(404).json({ success: false, message: '订单不存在' }); + } + + const order = orders[0]; + + // 获取订单商品详情 + const [orderItems] = await getDB().execute( + `SELECT + oi.id, oi.product_id, oi.quantity, oi.price, oi.points_price, oi.rongdou_price, + oi.spec_combination_id, + p.name as product_name, p.image_url, p.description, + psc.spec_values as spec_info + FROM order_items oi + LEFT JOIN products p ON oi.product_id = p.id + LEFT JOIN product_spec_combinations psc ON oi.spec_combination_id = psc.id + WHERE oi.order_id = ?`, + [order.id] + ); + + // 处理规格信息 + for (const item of orderItems) { + if (item.spec_info) { + try { + item.spec_info = JSON.parse(item.spec_info); + } catch (e) { + item.spec_info = null; + } + } + } + + // 处理地址信息 + console.log(order.address,'order.address'); + + if (order.address) { + try { + order.address = order.address; + } catch (e) { + order.address = null; + } + } + + order.items = orderItems; + + res.json({ + success: true, + data: { order } + }); + } catch (error) { + console.error('获取订单详情失败:', error); + res.status(500).json({ success: false, message: '获取订单详情失败' }); + } +}); + +// 创建预订单 +router.post('/create-from-cart', auth, async (req, res) => { + const db = getDB(); + await db.query('START TRANSACTION'); + + try { + const { cart_item_ids } = req.body; + const user_id = req.user.id; + + // 验证必填字段 + if (!cart_item_ids || !Array.isArray(cart_item_ids) || cart_item_ids.length === 0) { + await db.query('ROLLBACK'); + return res.status(400).json({ success: false, message: '请选择要购买的商品' }); + } + + // 获取购物车商品信息和支付方式 + const placeholders = cart_item_ids.map(() => '?').join(','); + const cartQuery = ` + SELECT + c.id, c.product_id, c.quantity, c.specification_id, + p.name, p.price, p.points_price, p.rongdou_price, p.stock, p.status, p.payment_methods, + psc.price_adjustment, psc.points_adjustment, psc.rongdou_adjustment, psc.stock as spec_stock + FROM cart_items c + LEFT JOIN products p ON c.product_id = p.id + LEFT JOIN product_spec_combinations psc ON c.specification_id = psc.id + WHERE c.id IN (${placeholders}) AND c.user_id = ? + `; + + const [cartItems] = await db.execute(cartQuery, [...cart_item_ids, user_id]); + + if (cartItems.length === 0) { + await db.query('ROLLBACK'); + return res.status(400).json({ success: false, message: '购物车商品不存在' }); + } + + // 验证商品状态和库存,计算总价和支付方式 + let totalAmount = 0; + let totalPoints = 0; + let totalRongdou = 0; + let allPaymentMethods = []; + + for (const item of cartItems) { + if (item.status !== 'active') { + await db.query('ROLLBACK'); + return res.status(400).json({ success: false, message: `商品 ${item.name} 已下架` }); + } + + const availableStock = item.specification_id ? item.spec_stock : item.stock; + if (availableStock < item.quantity) { + await db.query('ROLLBACK'); + return res.status(400).json({ success: false, message: `商品 ${item.name} 库存不足` }); + } + + // 解析商品支付方式 + let productPaymentMethods = ['rongdou']; // 默认支付方式 + if (item.payment_methods) { + try { + productPaymentMethods = JSON.parse(item.payment_methods); + } catch (e) { + console.error('解析商品支付方式失败:', e); + } + } + console.log(productPaymentMethods,'productPaymentMethods'); + + allPaymentMethods = allPaymentMethods.concat(productPaymentMethods); + + const finalPrice = item.price + (item.price_adjustment || 0); + const finalRongdouPrice = item.rongdou_price + (item.rongdou_adjustment || 0); + + totalAmount += finalPrice * item.quantity; + + // 根据支付方式计算积分和融豆需求 + const hasPoints = productPaymentMethods.includes('points') || productPaymentMethods.includes('points_rongdou'); + const hasRongdou = productPaymentMethods.includes('rongdou') || productPaymentMethods.includes('points_rongdou'); + + if (hasPoints && !hasRongdou) { + // 仅积分支付:按10000积分=1融豆计算 + totalPoints += finalRongdouPrice * item.quantity * 10000; + totalRongdou += finalRongdouPrice * item.quantity; + } else if (!hasPoints && hasRongdou) { + // 仅融豆支付 + totalRongdou += finalRongdouPrice * item.quantity; + } else { + // 组合支付或默认:记录融豆价格,前端可选择支付方式 + totalRongdou += finalRongdouPrice * item.quantity; + } + } + + // 去重支付方式 + const uniquePaymentMethods = [...new Set(allPaymentMethods)]; + console.log('订单支付方式:', uniquePaymentMethods); + + // 生成预订单号 + const orderNumber = 'PRE' + Date.now() + Math.random().toString(36).substr(2, 5).toUpperCase(); + + // 创建预订单(状态为pre_order) + const [orderResult] = await db.execute( + `INSERT INTO orders (order_no, user_id, total_amount, total_points, total_rongdou, + status, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, 'pre_order', NOW(), NOW())`, + [orderNumber, user_id, totalAmount, totalPoints, totalRongdou] + ); + + const preOrderId = orderResult.insertId; + + // 创建预订单项 + for (const item of cartItems) { + const finalPrice = item.price + (item.price_adjustment || 0); + const finalPointsPrice = item.points_price + (item.points_adjustment || 0); + const finalRongdouPrice = item.rongdou_price + (item.rongdou_adjustment || 0); + + await db.execute( + `INSERT INTO order_items (order_id, product_id, spec_combination_id, quantity, + price, points, points_price, rongdou, rongdou_price, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())`, + [preOrderId, item.product_id, item.specification_id, item.quantity, + finalPrice, finalPointsPrice * item.quantity, finalPointsPrice, finalRongdouPrice * item.quantity, finalRongdouPrice] + ); + } + + // 删除购物车中的商品 + await db.execute( + `DELETE FROM cart_items WHERE id IN (${placeholders})`, + cart_item_ids + ); + + await db.query('COMMIT'); + + res.status(201).json({ + success: true, + message: '预订单创建成功', + data: { + preOrderId, + orderNumber, + totalAmount, + totalPoints, + totalRongdou, + paymentMethods: uniquePaymentMethods + } + }); + } catch (error) { + await db.query('ROLLBACK'); + console.error('创建预订单失败:', error); + res.status(500).json({ success: false, message: '创建预订单失败' }); + } +}); + +/** + * @swagger + * /api/orders/{id}/cancel: + * put: + * summary: 用户取消订单 + * tags: [Orders] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * 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: 只能取消待处理的订单 + * 401: + * description: 未授权 + * 404: + * description: 订单不存在 + * 500: + * description: 服务器错误 + */ +router.put('/:id/cancel', auth, async (req, res) => { + const db = getDB(); + await db.query('START TRANSACTION'); + + try { + + const orderId = req.params.id; + const userId = req.user.id; + + // 检查订单是否存在且属于当前用户 + const [orders] = await db.execute( + 'SELECT id, user_id, total_points, status FROM orders WHERE id = ? AND user_id = ?', + [orderId, userId] + ); + + if (orders.length === 0) { + await db.query('ROLLBACK'); + return res.status(404).json({ success: false, message: '订单不存在' }); + } + + const order = orders[0]; + + if (order.status !== 'pending') { + await db.query('ROLLBACK'); + return res.status(400).json({ success: false, message: '只能取消待处理的订单' }); + } + + // 退还用户积分 + await db.execute( + 'UPDATE users SET points = points + ? WHERE id = ?', + [order.total_points, userId] + ); + + // 记录积分历史 + await db.execute( + `INSERT INTO points_history (user_id, amount, type, description, created_at) + VALUES (?, ?, 'refund', '订单取消退还积分', NOW())`, + [userId, order.total_points] + ); + + // 更新订单状态 + await db.execute( + 'UPDATE orders SET status = "cancelled", updated_at = NOW() WHERE id = ?', + [orderId] + ); + + await db.query('COMMIT'); + + res.json({ success: true, message: '订单已取消' }); + } catch (error) { + await db.query('ROLLBACK'); + console.error('取消订单失败:', error); + res.status(500).json({ success: false, message: '取消订单失败' }); + } +}); + +/** + * @swagger + * /api/orders/{id}/confirm: + * put: + * summary: 确认收货 + * tags: [Orders] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * 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: 只能确认已发货的订单 + * 401: + * description: 未授权 + * 404: + * description: 订单不存在 + * 500: + * description: 服务器错误 + */ +router.put('/:id/confirm', auth, async (req, res) => { + try { + const orderId = req.params.id; + const userId = req.user.id; + + // 检查订单是否存在且属于当前用户 + const [orders] = await getDB().execute( + 'SELECT id, status FROM orders WHERE id = ? AND user_id = ?', + [orderId, userId] + ); + + if (orders.length === 0) { + return res.status(404).json({ success: false, message: '订单不存在' }); + } + + const order = orders[0]; + + if (order.status !== 'shipped') { + return res.status(400).json({ success: false, message: '只能确认已发货的订单' }); + } + + // 更新订单状态 + await getDB().execute( + 'UPDATE orders SET status = "completed", updated_at = NOW() WHERE id = ?', + [orderId] + ); + + res.json({ success: true, message: '确认收货成功' }); + } catch (error) { + console.error('确认收货失败:', error); + res.status(500).json({ success: false, message: '确认收货失败' }); + } +}); + +/** + * @swagger + * /api/orders/{id}/status: + * put: + * summary: 更新订单状态(管理员) + * tags: [Orders] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: 订单ID + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * enum: [pending, shipped, completed, cancelled] + * description: 订单状态 + * required: + * - status + * responses: + * 200: + * description: 订单状态更新成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * message: + * type: string + * 400: + * description: 无效的订单状态 + * 401: + * description: 未授权 + * 403: + * description: 无管理员权限 + * 404: + * description: 订单不存在 + * 500: + * description: 服务器错误 + */ +router.put('/:id/status', auth, adminAuth, async (req, res) => { + const db = getDB(); + await db.query('START TRANSACTION'); + + try { + + const orderId = req.params.id; + const { status } = req.body; + + const validStatuses = ['pending', 'shipped', 'completed', 'cancelled']; + if (!validStatuses.includes(status)) { + await db.query('ROLLBACK'); + return res.status(400).json({ success: false, message: '无效的订单状态' }); + } + + // 检查订单是否存在 + const [orders] = await db.execute( + 'SELECT id, user_id, total_points, total_rongdou, status FROM orders WHERE id = ?', + [orderId] + ); + + if (orders.length === 0) { + await db.query('ROLLBACK'); + return res.status(404).json({ success: false, message: '订单不存在' }); + } + + const order = orders[0]; + + // 如果是取消订单,需要退还积分和融豆 + if (status === 'cancelled' && order.status !== 'cancelled' && order.status !== 'pre_order') { + // 退还用户积分 + if (order.total_points > 0) { + await db.execute( + 'UPDATE users SET points = points + ? WHERE id = ?', + [order.total_points, order.user_id] + ); + + // 记录积分历史 + await db.execute( + `INSERT INTO points_history (user_id, amount, type, description, created_at) + VALUES (?, ?, 'earn', '订单取消退还积分', NOW())`, + [order.user_id, order.total_points] + ); + } + + // 退还用户融豆 + if (order.total_rongdou > 0) { + await db.execute( + 'UPDATE users SET balance = balance - ? WHERE id = ?', + [order.total_rongdou, order.user_id] + ); + + // 记录融豆历史 + await db.execute( + `INSERT INTO rongdou_history (user_id, amount, type, description, created_at) + VALUES (?, ?, 'earn', '订单取消退还融豆', NOW())`, + [order.user_id, order.total_rongdou] + ); + } + } + + // 更新订单状态 + await db.execute( + 'UPDATE orders SET status = ?, updated_at = NOW() WHERE id = ?', + [status, orderId] + ); + + await db.query('COMMIT'); + + res.json({ success: true, message: '订单状态已更新' }); + } catch (error) { + await db.query('ROLLBACK'); + console.error('更新订单状态失败:', error); + res.status(500).json({ success: false, message: '更新订单状态失败' }); + } +}); + +/** + * @swagger + * /api/orders/pending-payment/{id}: + * get: + * summary: 获取待支付预订单详情 + * tags: [Orders] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: 预订单ID + * responses: + * 200: + * description: 获取预订单详情成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * data: + * type: object + * properties: + * id: + * type: integer + * order_no: + * type: string + * total_amount: + * type: integer + * total_points: + * type: integer + * total_rongdou: + * type: integer + * status: + * type: string + * created_at: + * type: string + * items: + * type: array + * items: + * type: object + * properties: + * id: + * type: integer + * product_id: + * type: integer + * product_name: + * type: string + * quantity: + * type: integer + * price: + * type: integer + * points_price: + * type: integer + * rongdou_price: + * type: integer + * spec_info: + * type: object + * 401: + * description: 未授权 + * 404: + * description: 预订单不存在 + * 500: + * description: 服务器错误 + */ +router.get('/pending-payment/:id', auth, async (req, res) => { + try { + const preOrderId = req.params.id; + const userId = req.user.id; + + // 获取预订单基本信息 + const [orders] = await getDB().execute( + `SELECT id, order_no, user_id, total_amount, total_points, total_rongdou, + status, created_at FROM orders WHERE id = ? AND user_id = ? AND status = 'pre_order'`, + [preOrderId, userId] + ); + + if (orders.length === 0) { + return res.status(404).json({ success: false, message: '预订单不存在' }); + } + + const order = orders[0]; + + // 获取预订单商品详情 + const [orderItems] = await getDB().execute( + `SELECT + oi.id, oi.product_id, oi.quantity, oi.price, oi.points_price, oi.rongdou_price, + oi.spec_combination_id, + p.name as product_name, p.image_url, p.description, + psc.spec_values as spec_info + FROM order_items oi + LEFT JOIN products p ON oi.product_id = p.id + LEFT JOIN product_spec_combinations psc ON oi.spec_combination_id = psc.id + WHERE oi.order_id = ?`, + [preOrderId] + ); + + // 处理规格信息 + for (const item of orderItems) { + if (item.spec_info) { + try { + item.spec_info = JSON.parse(item.spec_info); + } catch (e) { + item.spec_info = null; + } + } + } + + res.json({ + success: true, + data: { + ...order, + items: orderItems + } + }); + } catch (error) { + console.error('获取预订单详情失败:', error); + res.status(500).json({ success: false, message: '获取预订单详情失败' }); + } +}); + +/** + * @swagger + * /api/orders/confirm-payment: + * post: + * summary: 确认支付订单 + * description: | + * 根据商品支付方式确认订单支付: + * - 仅积分支付:按10000积分=1融豆的比例扣除积分 + * - 仅融豆支付:直接扣除融豆 + * - 组合支付:优先扣除积分(按10000:1转换),不足部分扣除融豆 + * tags: [Orders] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - order_id + * - address_id + * properties: + * order_id: + * type: integer + * description: 订单ID + * example: 123 + * address_id: + * type: integer + * description: 收货地址ID + * example: 456 + * responses: + * 200: + * description: 确认支付成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: "订单支付成功" + * data: + * type: object + * properties: + * order_id: + * type: integer + * example: 123 + * order_no: + * type: string + * example: "ORD20240101123456" + * 400: + * description: 请求参数错误或余额不足 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * enum: ["订单ID和收货地址ID为必填项", "积分不足", "融豆不足", "积分和融豆余额不足", "商品支付方式配置错误"] + * 401: + * description: 未授权 + * 404: + * description: 订单或地址不存在 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * enum: ["订单不存在或已处理", "收货地址不存在", "用户不存在"] + * 500: + * description: 服务器错误 + */ +router.post('/confirm-payment', auth, async (req, res) => { + const connection = await getDB().getConnection(); + + try { + await connection.beginTransaction(); + + const { orderId: order_id, addressId: address_id } = req.body; + const userId = req.user.id; + + // 验证必填字段 + if (!order_id || !address_id) { + return res.status(400).json({ success: false, message: '订单ID和收货地址ID为必填项' }); + } + + // 获取订单信息和商品支付方式 + const [orders] = await connection.execute( + `SELECT o.id, o.order_no, o.user_id, o.total_amount, o.total_points, o.total_rongdou, o.status, + GROUP_CONCAT(DISTINCT p.payment_methods) as payment_methods_list + FROM orders o + JOIN order_items oi ON o.id = oi.order_id + JOIN products p ON oi.product_id = p.id + WHERE o.id = ? AND o.user_id = ? AND o.status = 'pre_order' + GROUP BY o.id`, + [order_id, userId] + ); + + if (orders.length === 0) { + await connection.rollback(); + return res.status(404).json({ success: false, message: '订单不存在或已处理' }); + } + + const order = orders[0]; + + // 解析支付方式 + let allPaymentMethods = []; + console.log(typeof order.payment_methods_list); + + if (order.payment_methods_list) { + try { + // 数据库中存储的是序列化的JSON字符串,直接解析 + + allPaymentMethods = JSON.parse(JSON.parse(order.payment_methods_list)); + } catch (e) { + console.error('解析支付方式失败:', e, 'raw data:', order.payment_methods_list); + allPaymentMethods = []; + } + } + + // 去重支付方式 + allPaymentMethods = [...new Set(allPaymentMethods)]; + + // 判断支付方式类型 + const hasPoints = allPaymentMethods.includes('points') || allPaymentMethods.includes('points_rongdou'); + const hasRongdou = allPaymentMethods.includes('rongdou') || allPaymentMethods.includes('points_rongdou'); + const isComboPayment = allPaymentMethods.includes('points_rongdou'); + + console.log('订单支付方式:', allPaymentMethods, { hasPoints, hasRongdou, isComboPayment }); + + // 获取收货地址信息 + const [addresses] = await connection.execute( + 'SELECT id, receiver_name, receiver_phone, province, city, district, detailed_address as detail_address FROM user_addresses WHERE id = ? AND user_id = ?', + [address_id, userId] + ); + + if (addresses.length === 0) { + await connection.rollback(); + return res.status(404).json({ success: false, message: '收货地址不存在' }); + } + + const address = addresses[0]; + + // 获取用户当前积分和融豆 + const [users] = await connection.execute( + 'SELECT points, balance FROM users WHERE id = ?', + [userId] + ); + + if (users.length === 0) { + await connection.rollback(); + return res.status(404).json({ success: false, message: '用户不存在' }); + } + + const user = users[0]; + if (user.balance > 0) { + return res.status(400).json({ success: false, message: '融豆不足' }); + } + user.balance = Math.abs(user.balance); + + // 根据支付方式处理扣费逻辑 + let totalRongdouNeeded = order.total_rongdou; // 需要的融豆总数 + let pointsToDeduct = 0; // 需要扣除的积分 + let rongdouToDeduct = 0; // 需要扣除的融豆 + + if (!hasRongdou && !hasPoints) { + await connection.rollback(); + return res.status(400).json({ success: false, message: '商品支付方式配置错误' }); + } + + if (hasPoints && !hasRongdou) { + // 只支持积分支付,按10000积分=1融豆转换 + const pointsNeeded = totalRongdouNeeded * 10000; + if (user.points < pointsNeeded) { + await connection.rollback(); + return res.status(400).json({ success: false, message: '积分不足' }); + } + pointsToDeduct = pointsNeeded; + rongdouToDeduct = 0; + } else if (!hasPoints && hasRongdou) { + // 只支持融豆支付 + if (user.balance < totalRongdouNeeded) { + await connection.rollback(); + return res.status(400).json({ success: false, message: '融豆不足' }); + } + pointsToDeduct = 0; + rongdouToDeduct = totalRongdouNeeded; + } else if (hasPoints && hasRongdou) { + // 组合支付:先扣积分,不足部分用融豆 + const availablePointsInRongdou = Math.floor(user.points / 10000); // 积分可转换的融豆数 + + if (availablePointsInRongdou >= totalRongdouNeeded) { + // 积分足够支付全部 + pointsToDeduct = totalRongdouNeeded * 10000; + rongdouToDeduct = 0; + } else { + // 积分不够,需要组合支付 + pointsToDeduct = availablePointsInRongdou * 10000; + rongdouToDeduct = totalRongdouNeeded - availablePointsInRongdou; + + if (user.balance < rongdouToDeduct) { + await connection.rollback(); + return res.status(400).json({ success: false, message: '积分和融豆余额不足' }); + } + } + } + + console.log('扣费计算:', { totalRongdouNeeded, pointsToDeduct, rongdouToDeduct, userPoints: user.points, userBalance: user.balance }); + + // 扣除积分 + if (pointsToDeduct > 0) { + await connection.execute( + 'UPDATE users SET points = points - ? WHERE id = ?', + [pointsToDeduct, userId] + ); + + // 记录积分变动历史 + await connection.execute( + `INSERT INTO points_history (user_id, type, amount, description, order_id) + VALUES (?, 'spend', ?, ?, ?)`, + [userId, pointsToDeduct, `订单支付 - ${order.order_no}`, order_id] + ); + } + + // 扣除融豆 + if (rongdouToDeduct > 0) { + await connection.execute( + 'UPDATE users SET balance = balance + ? WHERE id = ?', + [rongdouToDeduct, userId] + ); + + // 记录融豆变动历史 + await connection.execute( + `INSERT INTO rongdou_history (user_id, type, amount, description, order_id) + VALUES (?, 'spend', ?, ?, ?)`, + [userId, rongdouToDeduct, `订单支付 - ${order.order_no}`, order_id] + ); + } + + // 更新订单状态和收货地址 + const addressStr = JSON.stringify({ + recipient_name: address.receiver_name, + phone: address.receiver_phone, + province: address.province, + city: address.city, + district: address.district, + detail_address: address.detail_address + }); + + await connection.execute( + `UPDATE orders SET status = 'pending', address = ?, updated_at = NOW() + WHERE id = ?`, + [addressStr, order_id] + ); + + await connection.commit(); + + res.json({ + success: true, + message: '订单支付成功', + data: { + order_id: order_id, + order_no: order.order_no + } + }); + + } catch (error) { + await connection.rollback(); + console.error('确认支付失败:', error); + res.status(500).json({ success: false, message: '确认支付失败' }); + } finally { + connection.release(); + } +}); + +/** + * @swagger + * /api/orders/stats: + * get: + * summary: 获取订单统计信息(管理员权限) + * tags: [Orders] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: 成功获取订单统计信息 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * data: + * type: object + * properties: + * totalOrders: + * type: integer + * description: 总订单数 + * pendingOrders: + * type: integer + * description: 待发货订单数 + * completedOrders: + * type: integer + * description: 已完成订单数 + * monthOrders: + * type: integer + * description: 本月新增订单数 + * monthGrowthRate: + * type: number + * description: 月增长率 + * totalPointsConsumed: + * type: number + * description: 总积分消费 + * 401: + * description: 未授权 + * 403: + * description: 无管理员权限 + * 500: + * description: 服务器错误 + */ +router.get('/stats', auth, adminAuth, async (req, res) => { + try { + // 总订单数 + const [totalOrders] = await getDB().execute('SELECT COUNT(*) as count FROM orders'); + + // 待发货订单数 + const [pendingOrders] = await getDB().execute('SELECT COUNT(*) as count FROM orders WHERE status = "pending"'); + + // 已完成订单数 + const [completedOrders] = await getDB().execute('SELECT COUNT(*) as count FROM orders WHERE status = "completed"'); + + // 本月新增订单 + const [monthOrders] = await getDB().execute( + 'SELECT COUNT(*) as count FROM orders WHERE YEAR(created_at) = YEAR(NOW()) AND MONTH(created_at) = MONTH(NOW())' + ); + + // 上月订单数(用于计算增长率) + const [lastMonthOrders] = await getDB().execute( + 'SELECT COUNT(*) as count FROM orders WHERE YEAR(created_at) = YEAR(DATE_SUB(NOW(), INTERVAL 1 MONTH)) AND MONTH(created_at) = MONTH(DATE_SUB(NOW(), INTERVAL 1 MONTH))' + ); + + // 计算月增长率 + const lastMonthCount = lastMonthOrders[0].count; + const currentMonthCount = monthOrders[0].count; + let monthGrowthRate = 0; + if (lastMonthCount > 0) { + monthGrowthRate = ((currentMonthCount - lastMonthCount) / lastMonthCount * 100).toFixed(1); + } + + // 总积分消费 + const [totalPointsConsumed] = await getDB().execute('SELECT SUM(points_cost) as total FROM orders WHERE status != "cancelled"'); + + res.json({ + success: true, + data: { + totalOrders: totalOrders[0].count, + pendingOrders: pendingOrders[0].count, + completedOrders: completedOrders[0].count, + monthOrders: monthOrders[0].count, + monthGrowthRate: parseFloat(monthGrowthRate), + totalPointsConsumed: totalPointsConsumed[0].total || 0 + } + }); + } catch (error) { + console.error('获取订单统计失败:', error); + res.status(500).json({ success: false, message: '获取订单统计失败' }); + } +}); + + +module.exports = router; \ No newline at end of file diff --git a/routes/payment.js b/routes/payment.js new file mode 100644 index 0000000..72a82bd --- /dev/null +++ b/routes/payment.js @@ -0,0 +1,400 @@ +const express = require('express'); +const router = express.Router(); +const WechatPayService = require('../services/wechatPayService'); +const AlipayService = require('../services/alipayservice'); +const { getDB } = require('../database'); +const { auth, paymentAuth } = require('../middleware/auth'); + +// 创建支付服务实例 +const wechatPayService = new WechatPayService(); +const alipayService = new AlipayService(); + +/** + * 获取支持的支付方式 + * GET /api/payment/methods + */ +router.get('/methods', (req, res) => { + res.json({ + success: true, + data: { + methods: [ + { + code: 'wechat_h5', + name: '微信支付', + description: '微信H5支付', + icon: 'wechat', + enabled: true + }, + { + code: 'alipay_wap', + name: '支付宝支付', + description: '支付宝手机网站支付', + icon: 'alipay', + enabled: true + } + ] + } + }); +}); + +/** + * 创建统一支付订单 + * POST /api/payment/create-order + */ +router.post('/create-order', paymentAuth, async (req, res) => { + try { + const { paymentMethod } = req.body; + const userId = req.user.id; + const username = req.user.username; + const phone = req.user.phone; + + // 验证支付方式 + if (!paymentMethod || !['wechat_h5', 'alipay_wap'].includes(paymentMethod)) { + return res.status(400).json({ + success: false, + message: '不支持的支付方式' + }); + } + + // 检查用户是否已经支付过 + const db = getDB(); + const [existingOrders] = await db.execute( + 'SELECT id FROM payment_orders WHERE user_id = ? AND status = "paid"', + [userId] + ); + + if (existingOrders.length > 0) { + return res.status(400).json({ + success: false, + message: '用户已完成支付,无需重复支付' + }); + } + + let result; + + // 获取客户端IP + const clientIp = req.headers['x-forwarded-for'] || + req.headers['x-real-ip'] || + req.connection.remoteAddress || + req.socket.remoteAddress || + (req.connection.socket ? req.connection.socket.remoteAddress : null) || + '127.0.0.1'; + + // 根据支付方式创建订单 + if (paymentMethod === 'wechat_h5') { + // 创建微信支付订单 + result = await wechatPayService.createRegistrationPayOrder({ + userId, + username, + phone, + clientIp + }); + } else if (paymentMethod === 'alipay_wap') { + // 创建支付宝支付订单 + result = await alipayService.createRegistrationPayOrder({ + userId, + username, + phone, + clientIp + }); + } + + if (result && result.success) { + res.json({ + success: true, + data: { + outTradeNo: result.data.outTradeNo, + payUrl: result.data.h5Url || result.data.payUrl, + paymentType: result.data.paymentType, + paymentMethod + } + }); + } else { + res.status(500).json({ + success: false, + message: '创建支付订单失败' + }); + } + } catch (error) { + console.error('创建统一支付订单异常:', error); + res.status(500).json({ + success: false, + message: error.message || '服务器内部错误' + }); + } +}); + +/** + * 查询支付状态 + * GET /api/payment/query-status/:outTradeNo + */ +router.get('/query-status/:outTradeNo', paymentAuth, async (req, res) => { + try { + const { outTradeNo } = req.params; + const userId = req.user.id; + + // 验证订单是否属于当前用户 + const db = getDB(); + const [orders] = await db.execute( + 'SELECT id FROM payment_orders WHERE out_trade_no = ? AND user_id = ?', + [outTradeNo, userId] + ); + + if (orders.length === 0) { + return res.status(404).json({ + success: false, + message: '订单不存在或无权限访问' + }); + } + + // 获取订单详细信息,包括trade_type + const [orderDetails] = await db.execute( + 'SELECT id, trade_type FROM payment_orders WHERE id = ?', + [orders[0].id] + ); + + if (orderDetails.length === 0) { + return res.status(404).json({ + success: false, + message: '订单详情不存在' + }); + } + + let result; + const tradeType = orderDetails[0].trade_type; + + // 根据交易类型查询支付状态 + if (tradeType === 'WECHAT_H5') { + // 查询微信支付状态 + result = await wechatPayService.queryPaymentStatus(outTradeNo); + } else if (tradeType === 'ALIPAY_WAP') { + // 查询支付宝支付状态 + result = await alipayService.queryPaymentStatus(outTradeNo); + } else { + return res.status(400).json({ + success: false, + message: '不支持的支付方式' + }); + } + + res.json(result); + } catch (error) { + console.error('查询支付状态失败:', error); + res.status(500).json({ + success: false, + message: error.message || '查询支付状态失败' + }); + } +}); + +/** + * 获取用户支付记录 + * GET /api/payment/orders + */ +router.get('/orders', paymentAuth, async (req, res) => { + try { + const userId = req.user.id; + const { page = 1, limit = 10, status } = req.query; + + const offset = (page - 1) * limit; + const db = getDB(); + + let whereClause = 'WHERE user_id = ?'; + let params = [userId]; + + if (status) { + whereClause += ' AND status = ?'; + params.push(status); + } + + // 查询订单列表 + const [orders] = await db.execute( + `SELECT id, out_trade_no, transaction_id, total_fee, body, trade_type, + status, paid_at, created_at + FROM payment_orders + ${whereClause} + ORDER BY created_at DESC + LIMIT ? OFFSET ?`, + [...params, parseInt(limit), parseInt(offset)] + ); + + // 查询总数 + const [countResult] = await db.execute( + `SELECT COUNT(*) as total FROM payment_orders ${whereClause}`, + params + ); + + const total = countResult[0].total; + + res.json({ + success: true, + data: { + orders: orders.map(order => ({ + ...order, + total_fee: order.total_fee / 100, // 转换为元 + payment_method_name: order.trade_type && order.trade_type.startsWith('ALIPAY') ? '支付宝支付' : '微信支付' + })), + pagination: { + page: parseInt(page), + limit: parseInt(limit), + total, + pages: Math.ceil(total / limit) + } + } + }); + } catch (error) { + console.error('获取支付记录失败:', error); + res.status(500).json({ + success: false, + message: '获取支付记录失败' + }); + } +}); + +/** + * 检查用户支付状态 + * GET /api/payment/check-status + */ +router.get('/check-status', auth, async (req, res) => { + try { + const userId = req.user.id; + const db = getDB(); + + // 查询用户支付状态 + const [users] = await db.execute( + 'SELECT payment_status FROM users WHERE id = ?', + [userId] + ); + + if (users.length === 0) { + return res.status(404).json({ + success: false, + message: '用户不存在' + }); + } + + const paymentStatus = users[0].payment_status; + + // 查询最近的支付订单 + const [recentOrders] = await db.execute( + `SELECT out_trade_no, trade_type, status, total_fee, paid_at + FROM payment_orders + WHERE user_id = ? + ORDER BY created_at DESC + LIMIT 1`, + [userId] + ); + + res.json({ + success: true, + data: { + paymentStatus, + isPaid: paymentStatus === 'paid', + recentOrder: recentOrders.length > 0 ? { + ...recentOrders[0], + total_fee: recentOrders[0].total_fee / 100, + payment_method_name: recentOrders[0].trade_type.startsWith('ALIPAY') ? '支付宝支付' : '微信支付' + } : null + } + }); + } catch (error) { + console.error('检查用户支付状态失败:', error); + res.status(500).json({ + success: false, + message: '检查支付状态失败' + }); + } +}); + +/** + * 支付宝支付回调通知 + * POST /api/payment/alipay/notify + */ +router.post('/alipay/notify', async (req, res) => { + try { + console.log('收到支付宝支付回调:', req.body); + + // 验证签名 + const isValid = alipayService.verifyNotifySign(req.body); + if (!isValid) { + console.error('支付宝回调签名验证失败'); + return res.status(400).send('FAIL'); + } + + const { + out_trade_no: outTradeNo, + trade_no: transactionId, + trade_status: tradeStatus, + total_amount: totalAmount + } = req.body; + + // 只处理支付成功的回调 + if (tradeStatus === 'TRADE_SUCCESS') { + const db = getDB(); + + // 检查订单是否存在 + const [orders] = await db.execute( + 'SELECT id, user_id, status FROM payment_orders WHERE out_trade_no = ?', + [outTradeNo] + ); + + if (orders.length === 0) { + console.error('支付宝回调:订单不存在', outTradeNo); + return res.status(400).send('FAIL'); + } + + const order = orders[0]; + + // 如果订单已经处理过,直接返回成功 + if (order.status === 'paid') { + console.log('支付宝回调:订单已处理', outTradeNo); + return res.send('SUCCESS'); + } + + // 更新订单状态 + await alipayService.updatePaymentStatus(outTradeNo, { + status: 'paid', + transactionId, + paidAt: new Date() + }); + + console.log('支付宝支付成功处理完成:', { + outTradeNo, + transactionId, + userId: order.user_id + }); + } + + res.send('SUCCESS'); + } catch (error) { + console.error('处理支付宝支付回调失败:', error); + res.status(500).send('FAIL'); + } +}); + +/** + * 支付宝支付返回页面处理 + * GET /api/payment/alipay/return + */ +router.get('/alipay/return', async (req, res) => { + try { + console.log('支付宝支付返回:', req.query); + + // 验证签名 + const isValid = alipayService.verifyNotifySign(req.query); + if (!isValid) { + console.error('支付宝返回签名验证失败'); + return res.redirect('/payment/failed'); + } + + const { out_trade_no: outTradeNo } = req.query; + + // 重定向到支付成功页面 + res.redirect(`/payment/success?outTradeNo=${outTradeNo}`); + } catch (error) { + console.error('处理支付宝支付返回失败:', error); + res.redirect('/payment/failed'); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/routes/points.js b/routes/points.js new file mode 100644 index 0000000..99315d3 --- /dev/null +++ b/routes/points.js @@ -0,0 +1,697 @@ +const express = require('express'); +const router = express.Router(); +const { getDB } = require('../database'); +const { auth, adminAuth } = require('../middleware/auth'); + +/** + * @swagger + * tags: + * name: Points + * description: 积分管理相关接口 + */ + +/** + * @swagger + * components: + * schemas: + * PointsHistory: + * type: object + * properties: + * id: + * type: integer + * description: 积分历史记录ID + * points_change: + * type: integer + * description: 积分变动数量 + * type: + * type: string + * description: 积分变动类型(earn-获得, spend-消费, admin_adjust-管理员调整) + * description: + * type: string + * description: 积分变动描述 + * created_at: + * type: string + * format: date-time + * description: 创建时间 + */ + +/** + * @swagger + * /api/points/balance: + * get: + * summary: 获取用户当前积分余额 + * tags: [Points] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: 成功获取积分余额 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * data: + * type: object + * properties: + * points: + * type: integer + * description: 用户当前积分 + * 401: + * description: 未授权,需要登录 + * 404: + * description: 用户不存在 + * 500: + * description: 服务器错误 + */ +router.get('/balance', auth, async (req, res) => { + try { + const userId = req.user.id; + + const [users] = await getDB().execute( + 'SELECT points FROM users WHERE id = ?', + [userId] + ); + + if (users.length === 0) { + return res.status(404).json({ success: false, message: '用户不存在' }); + } + + res.json({ + success: true, + data: { + points: users[0].points + } + }); + } catch (error) { + console.error('获取积分余额失败:', error); + res.status(500).json({ success: false, message: '获取积分余额失败' }); + } +}); + +/** + * @swagger + * /api/points/history: + * get: + * summary: 获取用户积分历史记录 + * tags: [Points] + * security: + * - bearerAuth: [] + * parameters: + * - in: query + * name: page + * schema: + * type: integer + * default: 1 + * description: 页码 + * - in: query + * name: limit + * schema: + * type: integer + * default: 10 + * description: 每页记录数 + * - in: query + * name: type + * schema: + * type: string + * enum: [earn, spend, admin_adjust] + * description: 积分变动类型 + * - in: query + * name: username + * schema: + * type: string + * description: 用户名(仅管理员可用) + * - in: query + * name: change + * schema: + * type: string + * enum: [positive, negative] + * description: 积分变动方向(仅管理员可用) + * - in: query + * name: startDate + * schema: + * type: string + * format: date + * description: 开始日期(仅管理员可用) + * - in: query + * name: endDate + * schema: + * type: string + * format: date + * description: 结束日期(仅管理员可用) + * responses: + * 200: + * description: 成功获取积分历史 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * data: + * type: object + * properties: + * records: + * type: array + * items: + * $ref: '#/components/schemas/PointsHistory' + * pagination: + * type: object + * properties: + * page: + * type: integer + * limit: + * type: integer + * total: + * type: integer + * totalPages: + * type: integer + * 401: + * description: 未授权,需要登录 + * 500: + * description: 服务器错误 + */ +router.get('/history', auth, async (req, res) => { + try { + const { page = 1, limit = 10, type, username, change, startDate, endDate } = req.query; + + // 确保参数为有效数字 + const pageNum = parseInt(page) || 1; + const limitNum = parseInt(limit) || 10; + const offset = (pageNum - 1) * limitNum; + + let whereClause = ''; + let queryParams = []; + + // 如果是管理员,可以查看所有用户的积分历史 + if (req.user.role === 'admin') { + whereClause = 'WHERE 1=1'; + + // 按用户名筛选 + if (username) { + whereClause += ' AND u.username LIKE ?'; + queryParams.push(`%${username}%`); + } + + // 按类型筛选 + if (type) { + whereClause += ' AND ph.type = ?'; + queryParams.push(type); + } + + // 按积分变化筛选 + if (change === 'positive') { + whereClause += ' AND ph.amount > 0'; + } else if (change === 'negative') { + whereClause += ' AND ph.amount < 0'; + } + + // 按时间范围筛选 + if (startDate) { + whereClause += ' AND DATE(ph.created_at) >= ?'; + queryParams.push(startDate); + } + if (endDate) { + whereClause += ' AND DATE(ph.created_at) <= ?'; + queryParams.push(endDate); + } + } else { + // 普通用户只能查看自己的积分历史 + whereClause = 'WHERE ph.user_id = ?'; + queryParams.push(req.user.id); + + if (type && ['earn', 'spend'].includes(type)) { + whereClause += ' AND ph.type = ?'; + queryParams.push(type); + } + } + + // 获取总数 + const countQuery = req.user.role === 'admin' + ? `SELECT COUNT(*) as total FROM points_history ph JOIN users u ON ph.user_id = u.id ${whereClause}` + : `SELECT COUNT(*) as total FROM points_history ph ${whereClause}`; + + const [countResult] = await getDB().execute(countQuery, queryParams); + + // 获取历史记录 + const historyQuery = req.user.role === 'admin' + ? `SELECT ph.id, ph.amount as points, ph.type, ph.description, ph.created_at, + u.username, + (SELECT points FROM users WHERE id = ph.user_id) as balance_after + FROM points_history ph + JOIN users u ON ph.user_id = u.id + ${whereClause} + ORDER BY ph.created_at DESC + LIMIT ${limitNum} OFFSET ${offset}` + : `SELECT id, amount as points_change, type, description, created_at + FROM points_history ph + ${whereClause} + ORDER BY created_at DESC + LIMIT ${limitNum} OFFSET ${offset}`; + + const [records] = await getDB().execute(historyQuery, queryParams); + + const responseData = req.user.role === 'admin' + ? { + history: records, + total: countResult[0].total + } + : { + records, + pagination: { + page: pageNum, + limit: limitNum, + total: countResult[0].total, + totalPages: Math.ceil(countResult[0].total / limitNum) + } + }; + + res.json({ + success: true, + data: responseData + }); + } catch (error) { + console.error('获取积分历史失败:', error); + res.status(500).json({ success: false, message: '获取积分历史失败' }); + } +}); + +/** + * @swagger + * /api/points/adjust: + * post: + * summary: 管理员调整用户积分 + * tags: [Points] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - userId + * - points + * - reason + * properties: + * userId: + * type: integer + * description: 用户ID + * points: + * type: integer + * description: 调整的积分数量(正数为增加,负数为减少) + * reason: + * type: string + * description: 调整原因 + * responses: + * 200: + * description: 积分调整成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: 积分调整成功 + * data: + * type: object + * properties: + * userId: + * type: integer + * pointsChanged: + * type: integer + * newBalance: + * type: integer + * 400: + * description: 参数错误或积分不足 + * 401: + * description: 未授权,需要管理员权限 + * 404: + * description: 用户不存在 + * 500: + * description: 服务器错误 + */ +router.post('/adjust', auth, adminAuth, async (req, res) => { + const connection = await getDB().getConnection(); + + try { + await connection.beginTransaction(); + + const { userId, points, reason } = req.body; + + if (!userId || points === undefined || points === null || !reason) { + await connection.rollback(); + return res.status(400).json({ success: false, message: '请提供有效的用户ID、积分数量和调整原因' }); + } + + // 检查用户是否存在 + const [users] = await connection.execute( + 'SELECT id, username, points FROM users WHERE id = ?', + [userId] + ); + + if (users.length === 0) { + await connection.rollback(); + return res.status(404).json({ success: false, message: '用户不存在' }); + } + + const currentPoints = users[0].points; + const newPoints = currentPoints + points; + + // 检查积分是否会变为负数 + if (newPoints < 0) { + await connection.rollback(); + return res.status(400).json({ success: false, message: '用户积分不足,无法扣除' }); + } + + // 更新用户积分 + await connection.execute( + 'UPDATE users SET points = ? WHERE id = ?', + [newPoints, userId] + ); + + // 记录积分历史 + await connection.execute( + `INSERT INTO points_history (user_id, amount, type, description, created_at) + VALUES (?, ?, 'admin_adjust', ?, NOW())`, + [userId, points, reason] + ); + + await connection.commit(); + + res.json({ + success: true, + message: '积分调整成功', + data: { + userId: userId, + pointsChanged: points, + newBalance: newPoints + } + }); + } catch (error) { + await connection.rollback(); + console.error('积分调整失败:', error); + res.status(500).json({ success: false, message: '积分调整失败' }); + } finally { + connection.release(); + } +}); + +/** + * @swagger + * /api/points/recharge: + * post: + * summary: 管理员给用户充值积分 + * tags: [Points] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - user_id + * - points + * properties: + * user_id: + * type: integer + * description: 用户ID + * points: + * type: integer + * description: 充值的积分数量(必须为正数) + * description: + * type: string + * description: 充值描述 + * default: 管理员充值 + * responses: + * 200: + * description: 积分充值成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: 积分充值成功 + * data: + * type: object + * properties: + * userId: + * type: integer + * pointsAdded: + * type: integer + * 400: + * description: 参数错误 + * 401: + * description: 未授权,需要管理员权限 + * 404: + * description: 用户不存在 + * 500: + * description: 服务器错误 + */ +router.post('/recharge', auth, adminAuth, async (req, res) => { + const connection = await getDB().getConnection(); + + try { + await connection.beginTransaction(); + + const { user_id, points, description = '管理员充值' } = req.body; + + if (!user_id || !points || points <= 0) { + await connection.rollback(); + return res.status(400).json({ success: false, message: '请提供有效的用户ID和积分数量' }); + } + + // 检查用户是否存在 + const [users] = await connection.execute( + 'SELECT id, username FROM users WHERE id = ?', + [user_id] + ); + + if (users.length === 0) { + await connection.rollback(); + return res.status(404).json({ success: false, message: '用户不存在' }); + } + + // 增加用户积分 + await connection.execute( + 'UPDATE users SET points = points + ? WHERE id = ?', + [points, user_id] + ); + + // 记录积分历史 + await connection.execute( + `INSERT INTO points_history (user_id, amount, type, description, created_at) + VALUES (?, ?, 'earn', ?, NOW())`, + [user_id, points, description] + ); + + await connection.commit(); + + res.json({ + success: true, + message: '积分充值成功', + data: { + userId: user_id, + pointsAdded: points + } + }); + } catch (error) { + await connection.rollback(); + console.error('积分充值失败:', error); + res.status(500).json({ success: false, message: '积分充值失败' }); + } finally { + connection.release(); + } +}); + + + +/** + * @swagger + * /api/points/leaderboard: + * get: + * summary: 获取积分排行榜 + * tags: [Points] + * security: + * - bearerAuth: [] + * parameters: + * - in: query + * name: limit + * schema: + * type: integer + * default: 10 + * description: 返回的排行榜数量 + * responses: + * 200: + * description: 成功获取积分排行榜 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * data: + * type: object + * properties: + * leaderboard: + * type: array + * items: + * type: object + * properties: + * rank: + * type: integer + * userId: + * type: integer + * username: + * type: string + * points: + * type: integer + * 401: + * description: 未授权,需要登录 + * 500: + * description: 服务器错误 + */ +router.get('/leaderboard', auth, async (req, res) => { + try { + const { limit = 10 } = req.query; + + const [users] = await getDB().execute( + `SELECT id, username, points + FROM users + WHERE points > 0 + ORDER BY points DESC + LIMIT ?`, + [parseInt(limit)] + ); + + res.json({ + success: true, + data: { + leaderboard: users.map((user, index) => ({ + rank: index + 1, + userId: user.id, + username: user.username, + points: user.points + })) + } + }); + } catch (error) { + console.error('获取积分排行榜失败:', error); + res.status(500).json({ success: false, message: '获取积分排行榜失败' }); + } +}); + +/** + * @swagger + * /api/points/stats: + * get: + * summary: 获取积分统计信息(管理员权限) + * tags: [Points] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: 成功获取积分统计信息 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * data: + * type: object + * properties: + * stats: + * type: object + * properties: + * totalPoints: + * type: integer + * description: 系统中总积分数量 + * totalEarned: + * type: integer + * description: 总积分发放量 + * totalSpent: + * type: integer + * description: 总积分消费量 + * activeUsers: + * type: integer + * description: 活跃用户数 + * 401: + * description: 未授权,需要管理员权限 + * 500: + * description: 服务器错误 + */ +router.get('/stats', auth, adminAuth, async (req, res) => { + try { + // 总积分发放量 + const [totalEarned] = await getDB().execute( + 'SELECT SUM(amount) as total FROM points_history WHERE type = "earn"' + ); + + // 总积分消费量 + const [totalConsumed] = await getDB().execute( + 'SELECT SUM(ABS(amount)) as total FROM points_history WHERE type = "spend"' + ); + + // 本月积分发放 + const [monthEarned] = await getDB().execute( + 'SELECT SUM(amount) as total FROM points_history WHERE type = "earn" AND YEAR(created_at) = YEAR(NOW()) AND MONTH(created_at) = MONTH(NOW())' + ); + + // 上月积分发放(用于计算增长率) + const [lastMonthEarned] = await getDB().execute( + 'SELECT SUM(amount) as total FROM points_history WHERE type = "earn" AND YEAR(created_at) = YEAR(DATE_SUB(NOW(), INTERVAL 1 MONTH)) AND MONTH(created_at) = MONTH(DATE_SUB(NOW(), INTERVAL 1 MONTH))' + ); + + // 计算月增长率 + const lastMonthTotal = lastMonthEarned[0].total || 0; + const currentMonthTotal = monthEarned[0].total || 0; + let monthGrowthRate = 0; + if (lastMonthTotal > 0) { + monthGrowthRate = ((currentMonthTotal - lastMonthTotal) / lastMonthTotal * 100).toFixed(1); + } + + // 活跃用户数(有积分记录的用户) + const [activeUsers] = await getDB().execute( + 'SELECT COUNT(DISTINCT user_id) as count FROM points_history' + ); + + res.json({ + success: true, + data: { + stats: { + totalPoints: totalEarned[0].total || 0, + totalEarned: totalEarned[0].total || 0, + totalSpent: totalConsumed[0].total || 0, + activeUsers: activeUsers[0].count + } + } + }); + } catch (error) { + console.error('获取积分统计失败:', error); + res.status(500).json({ success: false, message: '获取积分统计失败' }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/routes/products.js b/routes/products.js new file mode 100644 index 0000000..a06b7d2 --- /dev/null +++ b/routes/products.js @@ -0,0 +1,1142 @@ +const express = require('express'); +const { getDB } = require('../database'); +const { auth, adminAuth } = require('../middleware/auth'); + +const router = express.Router(); + +// 商品管理路由 +router.get('/', async (req, res) => { + try { + const { page = 1, limit = 10, search = '', category = '', status = '' } = req.query; + + // 确保参数为有效数字 + const pageNum = Math.max(1, parseInt(page) || 1); + const limitNum = Math.max(1, Math.min(100, parseInt(limit) || 10)); // 限制最大100条 + const offset = Math.max(0, (pageNum - 1) * limitNum); + + console.log('分页参数:', { pageNum, limitNum, offset, search, category, status }); + + let whereClause = 'WHERE 1=1'; + const params = []; + + if (search) { + whereClause += ' AND name LIKE ?'; + params.push(`%${search}%`); + } + + if (category) { + whereClause += ' AND category = ?'; + params.push(category); + } + + if (status) { + whereClause += ' AND status = ?'; + params.push(status); + } else { + whereClause += ' AND status = "active"'; + } + + // 获取总数 + const countQuery = `SELECT COUNT(*) as total FROM products ${whereClause}`; + const [countResult] = await getDB().execute(countQuery, params); + const total = countResult[0].total; + + // 获取商品列表 + const query = ` + SELECT id, name, rongdou_price, category, points_price, stock, image_url as image, description, status, payment_methods, created_at, updated_at + FROM products + ${whereClause} + ORDER BY created_at DESC + LIMIT ${limitNum} OFFSET ${offset} + `; + + // 确保参数数组正确传递 + const queryParams = [...params]; + console.log('Query params:', queryParams, 'Query:', query); + const [products] = await getDB().execute(query, queryParams); + products.forEach(item=>{ + item.payment_methods = JSON.parse(item.payment_methods) + }) + res.json({ + success: true, + data: { + products, + pagination: { + page: pageNum, + limit: limitNum, + total, + pages: Math.ceil(total / limitNum) + } + } + }); + } catch (error) { + console.error('获取商品列表失败:', error); + res.status(500).json({ success: false, message: '获取商品列表失败' }); + } +}); + +// 获取商品分类列表 +router.get('/categories', async (req, res) => { + try { + const [categories] = await getDB().execute( + 'SELECT DISTINCT category FROM products WHERE status = "active" AND category IS NOT NULL' + ); + + res.json({ + success: true, + data: { + categories: categories.map(item => item.category) + } + }); + } catch (error) { + console.error('获取商品分类失败:', error); + res.status(500).json({ success: false, message: '获取商品分类失败' }); + } +}); + +// 获取热销商品 +router.get('/hot', async (req, res) => { + try { + // 从活跃商品中随机获取2个商品 + const [products] = await getDB().execute( + `SELECT id, name, category, price, points_price, rongdou_price, stock, + image_url, images, description, shop_name, shop_avatar, + payment_methods, sales, rating, status, created_at, updated_at + FROM products + WHERE status = 'active' AND stock > 0 + ORDER BY RAND() + LIMIT 2` + ); + + // 格式化商品数据 + const formattedProducts = products.map(product => ({ + ...product, + images: product.images ? JSON.parse(product.images) : (product.image_url ? [product.image_url] : []), + payment_methods: product.payment_methods ? JSON.parse(product.payment_methods) : ['points'], + // 保持向后兼容 + points: product.points_price, + image: product.image_url + })); + + res.json({ + success: true, + data: { + products: formattedProducts + } + }); + } catch (error) { + console.error('获取热销商品失败:', error); + res.status(500).json({ success: false, message: '获取热销商品失败' }); + } +}); + +/** + * @swagger + * /products/flash-sale: + * get: + * summary: 获取秒杀商品 + * tags: [Products] + * responses: + * 200: + * description: 成功获取秒杀商品 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * data: + * type: object + * properties: + * products: + * type: array + * items: + * $ref: '#/components/schemas/Product' + */ +router.get('/cheap', async (req, res) => { + try { + // 从活跃商品中随机获取2个商品作为秒杀商品 + const [products] = await getDB().execute( + `SELECT id, name, category, price, points_price, rongdou_price, stock, + image_url, images, description, shop_name, shop_avatar, + payment_methods, sales, rating, status, created_at, updated_at + FROM products + WHERE status = 'active' AND stock > 0 + ORDER BY RAND() + LIMIT 2` + ); + + // 格式化商品数据,为秒杀商品添加特殊标识 + const formattedProducts = products.map(product => ({ + ...product, + images: product.images ? JSON.parse(product.images) : (product.image_url ? [product.image_url] : []), + payment_methods: product.payment_methods ? JSON.parse(product.payment_methods) : ['points'], + // 秒杀商品特殊处理:价格打8折 + flash_sale_price: Math.floor(product.price * 0.8), + flash_sale_points: Math.floor(product.points_price * 0.8), + flash_sale_rongdou: Math.floor(product.rongdou_price * 0.8), + is_flash_sale: true, + // 保持向后兼容 + points: product.points_price, + image: product.image_url + })); + + res.json({ + success: true, + data: { + products: formattedProducts + } + }); + } catch (error) { + console.error('获取秒杀商品失败:', error); + res.status(500).json({ success: false, message: '获取秒杀商品失败' }); + } +}); + +/** + * @swagger + * /products/{id}: + * get: + * summary: 获取单个商品详情(包含增强规格信息) + * tags: [Products] + * parameters: + * - in: path + * name: id + * schema: + * type: integer + * required: true + * description: 商品ID + * responses: + * 200: + * description: 成功获取商品详情,包含完整的规格信息 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * data: + * type: object + * properties: + * product: + * type: object + * properties: + * id: + * type: integer + * name: + * type: string + * category: + * type: string + * price: + * type: number + * points_price: + * type: number + * rongdou_price: + * type: number + * stock: + * type: integer + * specifications: + * type: array + * description: 商品规格组合列表(笛卡尔积规格系统) + * items: + * type: object + * properties: + * id: + * type: integer + * description: 规格组合ID + * combination_key: + * type: string + * description: 规格组合键(如:1-3-5) + * spec_display: + * type: string + * description: 规格显示文本(如:颜色:红色 | 尺寸:XL) + * spec_details: + * type: array + * description: 规格详细信息 + * items: + * type: object + * properties: + * id: + * type: integer + * spec_name: + * type: string + * description: 规格名称 + * spec_display_name: + * type: string + * description: 规格显示名称 + * value: + * type: string + * description: 规格值 + * display_value: + * type: string + * description: 规格显示值 + * color_code: + * type: string + * description: 颜色代码 + * image_url: + * type: string + * description: 规格图片 + * price_adjustment: + * type: number + * description: 价格调整 + * points_adjustment: + * type: number + * description: 积分调整 + * rongdou_adjustment: + * type: number + * description: 融豆调整 + * stock: + * type: integer + * description: 规格库存 + * sku_code: + * type: string + * description: SKU编码 + * barcode: + * type: string + * description: 条形码 + * weight: + * type: number + * description: 重量 + * volume: + * type: number + * description: 体积 + * actual_price: + * type: number + * description: 实际价格(基础价格+调整) + * actual_points_price: + * type: number + * description: 实际积分价格 + * actual_rongdou_price: + * type: number + * description: 实际融豆价格 + * is_available: + * type: boolean + * description: 是否有库存 + * specification_count: + * type: integer + * description: 规格总数 + * available_specifications: + * type: integer + * description: 有库存的规格数量 + * attributes: + * type: array + * description: 商品属性 + * isFavorited: + * type: boolean + * description: 是否已收藏 + * 404: + * description: 商品不存在 + */ +router.get('/:id', async (req, res) => { + try { + const { id } = req.params; + const userId = req.user?.id; // 可选的用户ID,用于检查收藏状态 + + const query = ` + SELECT id, name, category, price, points_price, rongdou_price, stock, + image_url, images, videos, description, details, shop_name, shop_avatar, + payment_methods, sales, rating, status, created_at, updated_at + FROM products + WHERE id = ? AND status = 'active' + `; + + const [products] = await getDB().execute(query, [id]); + + if (products.length === 0) { + return res.status(404).json({ success: false, message: '商品不存在' }); + } + + const product = products[0]; + + // 获取商品的规格组合(新的笛卡尔积规格系统) + const [specCombinations] = await getDB().execute( + `SELECT psc.*, + GROUP_CONCAT(CONCAT(sn.display_name, ':', sv.display_value) ORDER BY sn.sort_order SEPARATOR ' | ') as spec_display + FROM product_spec_combinations psc + LEFT JOIN JSON_TABLE(psc.spec_values, '$[*]' COLUMNS (spec_value_id INT PATH '$')) jt ON TRUE + LEFT JOIN spec_values sv ON jt.spec_value_id = sv.id + LEFT JOIN spec_names sn ON sv.spec_name_id = sn.id + WHERE psc.product_id = ? AND psc.status = 'active' + GROUP BY psc.id + ORDER BY psc.combination_key`, + [id] + ); + + // 为每个规格组合获取详细的规格值信息 + const enhancedSpecifications = []; + for (const combination of specCombinations) { + // 智能解析 spec_values 字段,兼容多种数据格式 + let specValueIds = []; + try { + if (combination.spec_values) { + // 如果是 Buffer 对象,先转换为字符串 + let specValuesStr = combination.spec_values; + if (Buffer.isBuffer(specValuesStr)) { + specValuesStr = specValuesStr.toString('utf8'); + } + + // 尝试 JSON 解析 + if (typeof specValuesStr === 'string') { + specValuesStr = specValuesStr.trim(); + if (specValuesStr.startsWith('[') && specValuesStr.endsWith(']')) { + // JSON 数组格式 + specValueIds = JSON.parse(specValuesStr); + } else if (specValuesStr.includes(',')) { + // 逗号分隔的字符串格式 + specValueIds = specValuesStr.split(',').map(id => parseInt(id.trim())).filter(id => !isNaN(id)); + } else if (specValuesStr && !isNaN(parseInt(specValuesStr))) { + // 单个数字 + specValueIds = [parseInt(specValuesStr)]; + } + } else if (Array.isArray(specValuesStr)) { + // 已经是数组 + specValueIds = specValuesStr; + } + } + } catch (parseError) { + console.warn(`解析规格值失败 (combination_id: ${combination.id}):`, parseError.message); + specValueIds = []; + } + + // 获取规格值详情 + if (specValueIds && specValueIds.length > 0) { + const placeholders = specValueIds.map(() => '?').join(','); + const [specDetails] = await getDB().execute( + `SELECT sv.*, sn.name as spec_name, sn.display_name as spec_display_name + FROM spec_values sv + LEFT JOIN spec_names sn ON sv.spec_name_id = sn.id + WHERE sv.id IN (${placeholders}) + ORDER BY sn.sort_order, sv.sort_order`, + specValueIds + ); + + enhancedSpecifications.push({ + id: combination.id, + combination_key: combination.combination_key, + spec_display: combination.spec_display, + spec_details: specDetails, + price_adjustment: combination.price_adjustment || 0, + points_adjustment: combination.points_adjustment || 0, + rongdou_adjustment: combination.rongdou_adjustment || 0, + stock: combination.stock, + sku_code: combination.sku_code, + barcode: combination.barcode, + weight: combination.weight, + volume: combination.volume, + actual_price: product.price + (combination.price_adjustment || 0), + actual_points_price: product.points_price + (combination.points_adjustment || 0), + actual_rongdou_price: product.rongdou_price + (combination.rongdou_adjustment || 0), + is_available: combination.stock > 0, + status: combination.status, + created_at: combination.created_at, + updated_at: combination.updated_at + }); + } + } + + // 获取商品属性 + const [attributes] = await getDB().execute( + 'SELECT * FROM product_attributes WHERE product_id = ? ORDER BY sort_order, id', + [id] + ); + + // 检查用户是否收藏了该商品 + let isFavorited = false; + if (userId) { + const [favorites] = await getDB().execute( + 'SELECT id FROM product_favorites WHERE user_id = ? AND product_id = ?', + [userId, id] + ); + isFavorited = favorites.length > 0; + } + + // 构建增强的商品数据 + const enhancedProduct = { + ...product, + images: (() => { + try { + if (product.images) { + let imagesStr = product.images; + if (Buffer.isBuffer(imagesStr)) { + imagesStr = imagesStr.toString('utf8'); + } + if (typeof imagesStr === 'string') { + imagesStr = imagesStr.trim(); + if (imagesStr.startsWith('[') && imagesStr.endsWith(']')) { + return JSON.parse(imagesStr); + } + } + } + return product.image_url ? [product.image_url] : []; + } catch (e) { + console.warn('解析商品图片失败:', e.message); + return product.image_url ? [product.image_url] : []; + } + })(), + videos: (() => { + try { + if (product.videos) { + let videosStr = product.videos; + if (Buffer.isBuffer(videosStr)) { + videosStr = videosStr.toString('utf8'); + } + if (typeof videosStr === 'string') { + videosStr = videosStr.trim(); + if (videosStr.startsWith('[') && videosStr.endsWith(']')) { + return JSON.parse(videosStr); + } + } + } + return []; + } catch (e) { + console.warn('解析商品视频失败:', e.message); + return []; + } + })(), + payment_methods: (() => { + try { + if (product.payment_methods) { + let methodsStr = product.payment_methods; + if (Buffer.isBuffer(methodsStr)) { + methodsStr = methodsStr.toString('utf8'); + } + if (typeof methodsStr === 'string') { + methodsStr = methodsStr.trim(); + if (methodsStr.startsWith('[') && methodsStr.endsWith(']')) { + return JSON.parse(methodsStr); + } + } + } + return ['points']; + } catch (e) { + console.warn('解析支付方式失败:', e.message); + return ['points']; + } + })(), + specifications: enhancedSpecifications, + attributes, + isFavorited, + // 规格统计信息 + specification_count: enhancedSpecifications.length, + available_specifications: enhancedSpecifications.filter(spec => spec.is_available).length, + // 保持向后兼容 + points: product.points_price, + image: product.image_url, + tags: product.category ? [product.category] : [] + }; + + res.json({ + success: true, + data: { product: enhancedProduct } + }); + } catch (error) { + console.error('获取商品详情失败:', error); + res.status(500).json({ success: false, message: '获取商品详情失败' }); + } +}); + +// 创建商品(管理员权限) +router.post('/', auth, adminAuth, async (req, res) => { + try { + const { + name, description, price, points_price, rongdou_price = 0, stock, category, + image_url, images = [], videos = [], details, status = 'active', + shop_name, shop_avatar, payment_methods = ['points', 'rongdou', 'points_rongdou'], + specifications = [], attributes = [] + } = req.body; + + if (!name || !price || (!points_price && !rongdou_price) || stock === undefined) { + return res.status(400).json({ message: '商品名称、原价、积分价格或融豆价格、库存不能为空' }); + } + + const [result] = await getDB().execute( + `INSERT INTO products (name, description, price, points_price, rongdou_price, stock, category, + image_url, images, videos, details, shop_name, shop_avatar, payment_methods, status, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())`, + [name, description, price, points_price, rongdou_price, stock, category || null, + image_url, JSON.stringify(images), JSON.stringify(videos), details, + shop_name, shop_avatar, JSON.stringify(payment_methods), status] + ); + + const productId = result.insertId; + + + + // 添加商品属性 + if (attributes && attributes.length > 0) { + for (const attr of attributes) { + await getDB().execute( + `INSERT INTO product_attributes (product_id, attribute_key, attribute_value, sort_order) + VALUES (?, ?, ?, ?)`, + [productId, attr.key, attr.value, attr.sort_order || 0] + ); + } + } + + res.status(201).json({ + success: true, + message: '商品创建成功', + data: { productId } + }); + } catch (error) { + console.error('创建商品错误:', error); + res.status(500).json({ message: '创建商品失败' }); + } +}); + +// 更新商品(管理员权限) +router.put('/:id', auth, adminAuth, async (req, res) => { + try { + const productId = req.params.id; + const { + name, description, price, points_price, rongdou_price, stock, category, + image_url, images, videos, details, status, shop_name, shop_avatar, payment_methods, + specifications, attributes + } = req.body; + + // 检查商品是否存在 + const [products] = await getDB().execute( + 'SELECT id FROM products WHERE id = ?', + [productId] + ); + + if (products.length === 0) { + return res.status(404).json({ message: '商品不存在' }); + } + + // 构建更新字段 + const updateFields = []; + const updateValues = []; + + if (name) { + updateFields.push('name = ?'); + updateValues.push(name); + } + + if (description !== undefined) { + updateFields.push('description = ?'); + updateValues.push(description); + } + + if (price !== undefined) { + updateFields.push('price = ?'); + updateValues.push(price); + } + + if (points_price !== undefined) { + updateFields.push('points_price = ?'); + updateValues.push(points_price); + } + + if (rongdou_price !== undefined) { + updateFields.push('rongdou_price = ?'); + updateValues.push(rongdou_price); + } + + if (stock !== undefined) { + updateFields.push('stock = ?'); + updateValues.push(stock); + } + + if (category !== undefined) { + updateFields.push('category = ?'); + updateValues.push(category); + } + + if (image_url !== undefined) { + updateFields.push('image_url = ?'); + updateValues.push(image_url); + } + + if (images !== undefined) { + updateFields.push('images = ?'); + updateValues.push(JSON.stringify(images || [])); + } + + if (videos !== undefined) { + updateFields.push('videos = ?'); + updateValues.push(JSON.stringify(videos || [])); + } + + if (details !== undefined) { + updateFields.push('details = ?'); + updateValues.push(details); + } + + if (shop_name !== undefined) { + updateFields.push('shop_name = ?'); + updateValues.push(shop_name); + } + + if (shop_avatar !== undefined) { + updateFields.push('shop_avatar = ?'); + updateValues.push(shop_avatar); + } + + if (payment_methods !== undefined) { + updateFields.push('payment_methods = ?'); + updateValues.push(JSON.stringify(payment_methods || [])); + } + + if (status) { + updateFields.push('status = ?'); + updateValues.push(status); + } + + if (updateFields.length === 0) { + return res.status(400).json({ message: '没有要更新的字段' }); + } + + updateFields.push('updated_at = NOW()'); + updateValues.push(productId); + + await getDB().execute( + `UPDATE products SET ${updateFields.join(', ')} WHERE id = ?`, + updateValues + ); + + + + // 更新商品属性 + if (attributes !== undefined) { + // 删除原有属性 + await getDB().execute('DELETE FROM product_attributes WHERE product_id = ?', [productId]); + + // 添加新属性 + if (attributes && attributes.length > 0) { + for (const attr of attributes) { + await getDB().execute( + `INSERT INTO product_attributes (product_id, attribute_key, attribute_value, sort_order) + VALUES (?, ?, ?, ?)`, + [productId, attr.key, attr.value, attr.sort_order || 0] + ); + } + } + } + + res.json({ + success: true, + message: '商品更新成功' + }); + } catch (error) { + console.error('更新商品错误:', error); + res.status(500).json({ message: '更新商品失败' }); + } +}); + +// 删除商品(管理员权限) +router.delete('/:id', auth, adminAuth, async (req, res) => { + try { + const { id } = req.params; + + // 检查商品是否存在 + const checkQuery = 'SELECT id FROM products WHERE id = ?'; + const [existing] = await getDB().execute(checkQuery, [id]); + + if (existing.length === 0) { + return res.status(404).json({ success: false, message: '商品不存在' }); + } + + // 检查是否有相关订单 + const orderCheckQuery = 'SELECT id FROM orders WHERE product_id = ? LIMIT 1'; + const [orders] = await getDB().execute(orderCheckQuery, [id]); + + if (orders.length > 0) { + return res.status(400).json({ success: false, message: '该商品存在相关订单,无法删除' }); + } + + const query = 'DELETE FROM products WHERE id = ?'; + await getDB().execute(query, [id]); + + res.json({ + success: true, + message: '商品删除成功' + }); + } catch (error) { + console.error('删除商品失败:', error); + res.status(500).json({ success: false, message: '删除商品失败' }); + } +}); + +// 获取商品统计信息(管理员权限) +router.get('/stats', auth, adminAuth, async (req, res) => { + try { + // 获取商品总数 + const totalQuery = 'SELECT COUNT(*) as total FROM products'; + const [totalResult] = await getDB().execute(totalQuery); + const totalProducts = totalResult[0].total; + + // 获取活跃商品数 + const activeQuery = 'SELECT COUNT(*) as total FROM products WHERE status = "active"'; + const [activeResult] = await getDB().execute(activeQuery); + const activeProducts = activeResult[0].total; + + // 获取库存不足商品数(库存小于10) + const lowStockQuery = 'SELECT COUNT(*) as total FROM products WHERE stock < 10'; + const [lowStockResult] = await getDB().execute(lowStockQuery); + const lowStockProducts = lowStockResult[0].total; + + // 获取本月新增商品数 + const monthlyQuery = ` + SELECT COUNT(*) as total + FROM products + WHERE YEAR(created_at) = YEAR(CURDATE()) AND MONTH(created_at) = MONTH(CURDATE()) + `; + const [monthlyResult] = await getDB().execute(monthlyQuery); + const monthlyProducts = monthlyResult[0].total; + + // 计算月增长率 + const lastMonthQuery = ` + SELECT COUNT(*) as total + FROM products + WHERE YEAR(created_at) = YEAR(DATE_SUB(CURDATE(), INTERVAL 1 MONTH)) + AND MONTH(created_at) = MONTH(DATE_SUB(CURDATE(), INTERVAL 1 MONTH)) + `; + const [lastMonthResult] = await getDB().execute(lastMonthQuery); + const lastMonthProducts = lastMonthResult[0].total; + + const monthlyGrowth = lastMonthProducts > 0 + ? ((monthlyProducts - lastMonthProducts) / lastMonthProducts * 100).toFixed(1) + : 0; + + res.json({ + success: true, + data: { + stats: { + totalProducts, + activeProducts, + lowStockProducts, + monthlyProducts, + monthlyGrowth: parseFloat(monthlyGrowth) + } + } + }); + } catch (error) { + console.error('获取商品统计失败:', error); + res.status(500).json({ success: false, message: '获取商品统计失败' }); + } +}); + +// 获取商品评论 +router.get('/:id/reviews', async (req, res) => { + try { + const { id } = req.params; + const page = parseInt(req.query.page) || 1; + const limit = parseInt(req.query.limit) || 10; + const offset = (page - 1) * limit; + + // 获取评论列表 + const [reviews] = await getDB().execute( + `SELECT pr.id, pr.rating, pr.comment as content, pr.images, pr.created_at as createdAt, + u.username as user_name, u.avatar as user_avatar + FROM product_reviews pr + JOIN users u ON pr.user_id = u.id + WHERE pr.product_id = ? + ORDER BY pr.created_at DESC + LIMIT ${limit} OFFSET ${offset}`, + [id] + ); + + // 获取评论总数 + const [countResult] = await getDB().execute( + 'SELECT COUNT(*) as total FROM product_reviews WHERE product_id = ?', + [id] + ); + + // 计算平均评分 + const [avgResult] = await getDB().execute( + 'SELECT AVG(rating) as avg_rating FROM product_reviews WHERE product_id = ?', + [id] + ); + + // 格式化评论数据 + const formattedReviews = reviews.map(review => ({ + id: review.id, + user: { + name: review.user_name, + avatar: review.user_avatar + }, + rating: review.rating, + content: review.content, + createdAt: review.createdAt, + images: review.images ? JSON.parse(review.images) : null + })); + + res.json({ + success: true, + data: { + reviews: formattedReviews, + total: countResult[0].total, + averageRating: avgResult[0].avg_rating ? parseFloat(avgResult[0].avg_rating).toFixed(1) : 0, + pagination: { + page, + limit, + total: countResult[0].total, + totalPages: Math.ceil(countResult[0].total / limit) + } + } + }); + } catch (error) { + console.error('获取商品评论失败:', error); + res.status(500).json({ success: false, message: '获取商品评论失败' }); + } +}); + +// 获取推荐商品 +router.get('/:id/recommended', async (req, res) => { + try { + const id = parseInt(req.params.id); + + // 获取同类别的其他商品作为推荐 + const query = ` + SELECT p2.id, p2.name, p2.category, p2.price, p2.points_price as points, + p2.stock, p2.image_url as image, p2.description + FROM products p1 + JOIN products p2 ON p1.category = p2.category + WHERE p1.id = ? AND p2.id != ? AND p2.status = 'active' + ORDER BY RAND() + LIMIT 6 + `; + + const [recommendedProducts] = await getDB().execute(query, [id, id]); + + // 如果同类别商品不足,补充其他热门商品 + if (recommendedProducts.length < 6) { + const remainingCount = 6 - recommendedProducts.length; + if (remainingCount > 0) { + const additionalQuery = ` + SELECT id, name, category, price, points_price as points, + stock, image_url as image, description + FROM products + WHERE id != ? AND status = 'active' + ORDER BY RAND() + LIMIT ${remainingCount} + `; + + const [additionalProducts] = await getDB().execute( + additionalQuery, + [id] + ); + + recommendedProducts.push(...additionalProducts); + } + } + + res.json({ + success: true, + data: { + products: recommendedProducts + } + }); + } catch (error) { + console.error('获取推荐商品失败:', error); + res.status(500).json({ success: false, message: '获取推荐商品失败' }); + } +}); + +// 收藏商品 +router.post('/:id/favorite', auth, async (req, res) => { + try { + const productId = req.params.id; + const userId = req.user.id; + + // 检查商品是否存在 + const [products] = await getDB().execute('SELECT id FROM products WHERE id = ?', [productId]); + if (products.length === 0) { + return res.status(404).json({ message: '商品不存在' }); + } + + // 检查是否已收藏 + const [existing] = await getDB().execute( + 'SELECT id FROM product_favorites WHERE user_id = ? AND product_id = ?', + [userId, productId] + ); + + if (existing.length > 0) { + return res.status(400).json({ message: '商品已收藏' }); + } + + await getDB().execute( + 'INSERT INTO product_favorites (user_id, product_id, created_at) VALUES (?, ?, NOW())', + [userId, productId] + ); + + res.json({ + success: true, + message: '收藏成功' + }); + } catch (error) { + console.error('收藏商品错误:', error); + res.status(500).json({ message: '收藏失败' }); + } +}); + +// 取消收藏商品 +router.delete('/:id/favorite', auth, async (req, res) => { + try { + const productId = req.params.id; + const userId = req.user.id; + + const [result] = await getDB().execute( + 'DELETE FROM product_favorites WHERE user_id = ? AND product_id = ?', + [userId, productId] + ); + + if (result.affectedRows === 0) { + return res.status(404).json({ message: '未收藏该商品' }); + } + + res.json({ + success: true, + message: '取消收藏成功' + }); + } catch (error) { + console.error('取消收藏错误:', error); + res.status(500).json({ message: '取消收藏失败' }); + } +}); + +// 获取用户收藏的商品列表 +router.get('/favorites', auth, async (req, res) => { + try { + const userId = req.user.id; + const page = parseInt(req.query.page) || 1; + const limit = parseInt(req.query.limit) || 10; + const offset = (page - 1) * limit; + + const [favorites] = await getDB().execute( + `SELECT p.*, pf.created_at as favorite_time + FROM product_favorites pf + JOIN products p ON pf.product_id = p.id + WHERE pf.user_id = ? AND p.status = 'active' + ORDER BY pf.created_at DESC + LIMIT ${limit} OFFSET ${offset}`, + [userId] + ); + + const [countResult] = await getDB().execute( + `SELECT COUNT(*) as total + FROM product_favorites pf + JOIN products p ON pf.product_id = p.id + WHERE pf.user_id = ? AND p.status = 'active'`, + [userId] + ); + + res.json({ + success: true, + data: { + products: favorites.map(product => ({ + ...product, + images: product.images ? JSON.parse(product.images) : [], + videos: product.videos ? JSON.parse(product.videos) : [], + payment_methods: product.payment_methods ? JSON.parse(product.payment_methods) : [] + })), + pagination: { + page, + limit, + total: countResult[0].total, + totalPages: Math.ceil(countResult[0].total / limit) + } + } + }); + } catch (error) { + console.error('获取收藏列表错误:', error); + res.status(500).json({ message: '获取收藏列表失败' }); + } +}); + + + + + + + + + +// 获取商品属性 +router.get('/:id/attributes', async (req, res) => { + try { + const productId = req.params.id; + + const [attributes] = await getDB().execute( + 'SELECT * FROM product_attributes WHERE product_id = ? ORDER BY sort_order, id', + [productId] + ); + + res.json({ + success: true, + data: attributes + }); + } catch (error) { + console.error('获取商品属性错误:', error); + res.status(500).json({ message: '获取商品属性失败' }); + } +}); + +// 创建商品评论 +router.post('/:id/reviews', auth, async (req, res) => { + try { + const productId = req.params.id; + const userId = req.user.id; + const { orderId, rating, comment, images = [] } = req.body; + + // 验证必填字段 + if (!orderId || !rating || rating < 1 || rating > 5) { + return res.status(400).json({ message: '订单ID和评分(1-5)不能为空' }); + } + + // 检查订单是否存在且属于当前用户 + const [orders] = await getDB().execute( + `SELECT o.id FROM orders o + JOIN order_items oi ON o.id = oi.order_id + WHERE o.id = ? AND o.user_id = ? AND oi.product_id = ? AND o.status = 'delivered'`, + [orderId, userId, productId] + ); + + if (orders.length === 0) { + return res.status(400).json({ message: '只能评价已完成的订单商品' }); + } + + // 检查是否已经评价过 + const [existingReviews] = await getDB().execute( + 'SELECT id FROM product_reviews WHERE product_id = ? AND user_id = ? AND order_id = ?', + [productId, userId, orderId] + ); + + if (existingReviews.length > 0) { + return res.status(400).json({ message: '该商品已评价过' }); + } + + // 创建评论 + const [result] = await getDB().execute( + `INSERT INTO product_reviews (product_id, user_id, order_id, rating, comment, images, created_at) + VALUES (?, ?, ?, ?, ?, ?, NOW())`, + [productId, userId, orderId, rating, comment, JSON.stringify(images)] + ); + + // 更新商品平均评分 + const [avgResult] = await getDB().execute( + 'SELECT AVG(rating) as avg_rating FROM product_reviews WHERE product_id = ?', + [productId] + ); + + await getDB().execute( + 'UPDATE products SET rating = ? WHERE id = ?', + [parseFloat(avgResult[0].avg_rating).toFixed(2), productId] + ); + + res.status(201).json({ + success: true, + message: '评价成功', + data: { reviewId: result.insertId } + }); + } catch (error) { + console.error('创建商品评论错误:', error); + res.status(500).json({ message: '评价失败' }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/routes/regions.js b/routes/regions.js new file mode 100644 index 0000000..6bded1a --- /dev/null +++ b/routes/regions.js @@ -0,0 +1,436 @@ +const express = require('express') +const router = express.Router() +const {getDB} = require('../database') + +/** + * @swagger + * tags: + * name: Regions + * description: 地区数据API + */ + +/** + * @swagger + * components: + * schemas: + * Region: + * type: object + * properties: + * code: + * type: string + * description: 地区编码 + * name: + * type: string + * description: 地区名称 + * ZhejiangRegion: + * type: object + * properties: + * id: + * type: integer + * description: 地区ID + * city_name: + * type: string + * description: 城市名称 + * district_name: + * type: string + * description: 区县名称 + * region_code: + * type: string + * description: 地区编码 + * is_available: + * type: integer + * description: 是否可用(1:可用 0:不可用) + */ + +/** + * @swagger + * /regions/zhejiang: + * get: + * summary: 获取浙江省所有地区数据 + * tags: [Regions] + * responses: + * 200: + * description: 成功获取浙江省地区数据 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * data: + * type: array + * items: + * $ref: '#/components/schemas/ZhejiangRegion' + * message: + * type: string + * example: 获取地区数据成功 + * 500: + * description: 服务器错误 + */ +router.get('/zhejiang', async (req, res) => { + try { + const query = ` + SELECT id, city_name, district_name, region_code, is_available + FROM zhejiang_regions + WHERE is_available = 1 + ORDER BY city_name, district_name + ` + + const [rows] = await getDB().execute(query) + + res.json({ + success: true, + data: rows, + message: '获取地区数据成功' + }) + } catch (error) { + console.error('获取浙江省地区数据失败:', error) + res.status(500).json({ + success: false, + message: '获取地区数据失败' + }) + } +}) + +/** + * @swagger + * /regions/provinces: + * get: + * summary: 获取所有省份 + * tags: [Regions] + * responses: + * 200: + * description: 成功获取省份列表 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * data: + * type: array + * items: + * $ref: '#/components/schemas/Region' + * 500: + * description: 服务器错误 + */ +router.get('/provinces', async (req, res) => { + try { + // 按level分组数据 + const regionsByLevel = { + 1: [], // 省份 + 2: [], // 城市 + 3: [] // 区县 + }; + if (!global.provinces) { + // 一次性获取所有区域数据(省、市、区县) + const [allRegions] = await getDB().execute( + `SELECT code, name as label, level, parent_code + FROM china_regions + WHERE level <= 3 + ORDER BY level, code` + ); + + + // 创建code到region的映射,便于快速查找 + const regionMap = {}; + + // 分组并建立映射 + allRegions.forEach(region => { + region.children = []; // 初始化children数组 + regionsByLevel[region.level].push(region); + regionMap[region.code] = region; + }); + + // 构建层级关系:先处理区县到城市的关系 + regionsByLevel[3].forEach(district => { + const parentCity = regionMap[district.parent_code]; + if (parentCity) { + parentCity.children.push(district); + } + }); + + // 再处理城市到省份的关系 + regionsByLevel[2].forEach(city => { + const parentProvince = regionMap[city.parent_code]; + if (parentProvince) { + parentProvince.children.push(city); + } + }); + global.provinces = regionsByLevel[1]; + }else { + console.log('1111') + regionsByLevel[1] = global.provinces; + } + + + // 返回省份数据(已包含完整的层级结构) + res.json({ + success: true, + data: regionsByLevel[1] + }); + } catch (error) { + console.error('获取省份列表错误:', error); + res.status(500).json({message: '获取省份列表失败'}); + } +}); + +/** + * @swagger + * /regions/cities/{provinceCode}: + * get: + * summary: 根据省份代码获取城市列表 + * tags: [Regions] + * parameters: + * - in: path + * name: provinceCode + * required: true + * schema: + * type: string + * description: 省份代码 + * responses: + * 200: + * description: 成功获取城市列表 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * data: + * type: array + * items: + * $ref: '#/components/schemas/Region' + * 500: + * description: 服务器错误 + */ +router.get('/cities/:provinceCode', async (req, res) => { + try { + const provinceCode = req.params.provinceCode; + + const [cities] = await getDB().execute( + `SELECT code, name + FROM china_regions + WHERE level = 2 + AND parent_code = ? + ORDER BY code`, + [provinceCode] + ); + + res.json({ + success: true, + data: cities + }); + } catch (error) { + console.error('获取城市列表错误:', error); + res.status(500).json({message: '获取城市列表失败'}); + } +}); + +/** + * @swagger + * /regions/districts/{cityCode}: + * get: + * summary: 根据城市代码获取区县列表 + * tags: [Regions] + * parameters: + * - in: path + * name: cityCode + * required: true + * schema: + * type: string + * description: 城市代码 + * responses: + * 200: + * description: 成功获取区县列表 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * data: + * type: array + * items: + * $ref: '#/components/schemas/Region' + * 500: + * description: 服务器错误 + */ +router.get('/districts/:cityCode', async (req, res) => { + try { + const cityCode = req.params.cityCode; + + const [districts] = await getDB().execute( + `SELECT code, name + FROM china_regions + WHERE level = 3 + AND parent_code = ? + ORDER BY code`, + [cityCode] + ); + + res.json({ + success: true, + data: districts + }); + } catch (error) { + console.error('获取区县列表错误:', error); + res.status(500).json({message: '获取区县列表失败'}); + } +}); + +/** + * @swagger + * /regions/path/{regionCode}: + * get: + * summary: 根据区域代码获取完整路径(省-市-区) + * tags: [Regions] + * parameters: + * - in: path + * name: regionCode + * required: true + * schema: + * type: string + * description: 区域代码 + * responses: + * 200: + * description: 成功获取区域完整路径 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * data: + * type: object + * properties: + * province: + * $ref: '#/components/schemas/Region' + * city: + * $ref: '#/components/schemas/Region' + * district: + * $ref: '#/components/schemas/Region' + * 404: + * description: 区域不存在 + * 500: + * description: 服务器错误 + */ +router.get('/path/:regionCode', async (req, res) => { + try { + const regionCode = req.params.regionCode; + + // 获取当前区域信息 + const [currentRegion] = await getDB().execute( + 'SELECT code, name, level, parent_code FROM china_regions WHERE code = ?', + [regionCode] + ); + + if (currentRegion.length === 0) { + return res.status(404).json({message: '区域不存在'}); + } + + const region = currentRegion[0]; + const path = [region]; + + // 递归获取父级区域 + let parentCode = region.parent_code; + while (parentCode) { + const [parentRegion] = await getDB().execute( + 'SELECT code, name, level, parent_code FROM china_regions WHERE code = ? AND status = "active"', + [parentCode] + ); + + if (parentRegion.length > 0) { + path.unshift(parentRegion[0]); + parentCode = parentRegion[0].parent_code; + } else { + break; + } + } + + res.json({ + success: true, + data: { + path, + province: path.find(r => r.level === 1) || null, + city: path.find(r => r.level === 2) || null, + district: path.find(r => r.level === 3) || null + } + }); + } catch (error) { + console.error('获取区域路径错误:', error); + res.status(500).json({message: '获取区域路径失败'}); + } +}); + +// 搜索区域(支持模糊搜索) +router.get('/search', async (req, res) => { + try { + const {keyword, level} = req.query; + + if (!keyword || keyword.trim() === '') { + return res.status(400).json({message: '搜索关键词不能为空'}); + } + + let sql = `SELECT code, name, level, parent_code + FROM china_regions + WHERE name LIKE ?`; + const params = [`%${keyword.trim()}%`]; + + if (level) { + sql += ' AND level = ?'; + params.push(parseInt(level)); + } + + sql += ' ORDER BY level, code LIMIT 50'; + + const [regions] = await getDB().execute(sql, params); + + // 为每个搜索结果获取完整路径 + const results = []; + for (const region of regions) { + const path = [region]; + let parentCode = region.parent_code; + + while (parentCode) { + const [parentRegion] = await getDB().execute( + 'SELECT code, name, level, parent_code FROM china_regions WHERE code = ? AND status = "active"', + [parentCode] + ); + + if (parentRegion.length > 0) { + path.unshift(parentRegion[0]); + parentCode = parentRegion[0].parent_code; + } else { + break; + } + } + + results.push({ + ...region, + path, + fullName: path.map(r => r.name).join(' - ') + }); + } + + res.json({ + success: true, + data: results + }); + } catch (error) { + console.error('搜索区域错误:', error); + res.status(500).json({message: '搜索区域失败'}); + } +}); + +module.exports = router \ No newline at end of file diff --git a/routes/riskManagement.js b/routes/riskManagement.js new file mode 100644 index 0000000..967ecb6 --- /dev/null +++ b/routes/riskManagement.js @@ -0,0 +1,440 @@ +const express = require('express'); +router = express.Router(); + +/** + * @swagger + * tags: + * name: RiskManagement + * description: 风险管理API + */ +const { auth } = require('../middleware/auth'); +const timeoutService = require('../services/timeoutService'); +const { getDB } = require('../database'); + +/** + * 检查管理员权限 + */ +const requireAdmin = (req, res, next) => { + if (req.user.role !== 'admin') { + return res.status(403).json({ success: false, message: '需要管理员权限' }); + } + next(); +}; + +/** + * @swagger + * /risk-management/users: + * get: + * summary: 获取风险用户列表 + * tags: [RiskManagement] + * security: + * - bearerAuth: [] + * parameters: + * - in: query + * name: page + * schema: + * type: integer + * default: 1 + * description: 页码 + * - in: query + * name: limit + * schema: + * type: integer + * default: 10 + * description: 每页数量 + * - in: query + * name: is_blacklisted + * schema: + * type: integer + * enum: [0, 1] + * description: 是否被拉黑 + * - in: query + * name: username + * schema: + * type: string + * description: 用户名 + * responses: + * 200: + * description: 成功获取风险用户列表 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * data: + * type: object + * properties: + * users: + * type: array + * items: + * type: object + * properties: + * id: + * type: integer + * username: + * type: string + * real_name: + * type: string + * is_blacklisted: + * type: boolean + * blacklist_reason: + * type: string + * blacklisted_at: + * type: string + * format: date-time + * pagination: + * type: object + * properties: + * total: + * type: integer + * page: + * type: integer + * limit: + * type: integer + * pages: + * type: integer + * 401: + * description: 未授权 + * 403: + * description: 权限不足 + * 500: + * description: 服务器错误 + */ +router.get('/users', auth, requireAdmin, async (req, res) => { + try { + const { page = 1, limit = 10, is_blacklisted, username } = req.query; + + const filters = {}; + if (is_blacklisted !== undefined) { + filters.is_blacklisted = parseInt(is_blacklisted); + } + if (username) { + filters.username = username; + } + + const result = await timeoutService.getRiskUsers(filters, { page, limit }); + + res.json({ + success: true, + data: result + }); + } catch (error) { + console.error('获取风险用户列表失败:', error); + res.status(500).json({ success: false, message: '获取风险用户列表失败' }); + } +}); + +/** + * @swagger + * /risk-management/blacklist/{userId}: + * post: + * summary: 拉黑用户 + * tags: [RiskManagement] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: userId + * schema: + * type: integer + * required: true + * description: 用户ID + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - reason + * properties: + * reason: + * type: string + * description: 拉黑原因 + * responses: + * 200: + * description: 用户已被拉黑 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: 用户已被拉黑 + * 400: + * description: 请求参数错误 + * 401: + * description: 未授权 + * 403: + * description: 权限不足 + * 500: + * description: 服务器错误 + */ +router.post('/blacklist/:userId', auth, requireAdmin, async (req, res) => { + try { + const { userId } = req.params; + const { reason } = req.body; + const operatorId = req.user.id; + + if (!reason || reason.trim() === '') { + return res.status(400).json({ success: false, message: '请提供拉黑原因' }); + } + + await timeoutService.blacklistUser(parseInt(userId), reason.trim(), operatorId); + + res.json({ + success: true, + message: '用户已被拉黑' + }); + } catch (error) { + console.error('拉黑用户失败:', error); + res.status(500).json({ success: false, message: error.message || '拉黑用户失败' }); + } +}); + +/** + * @swagger + * /risk-management/unblacklist/{userId}: + * post: + * summary: 解除拉黑 + * tags: [RiskManagement] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: userId + * schema: + * type: integer + * required: true + * description: 用户ID + * responses: + * 200: + * description: 已解除拉黑 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: 已解除拉黑 + * 401: + * description: 未授权 + * 403: + * description: 权限不足 + * 500: + * description: 服务器错误 + */ +router.post('/unblacklist/:userId', auth, requireAdmin, async (req, res) => { + try { + const { userId } = req.params; + const operatorId = req.user.id; + + await timeoutService.unblacklistUser(parseInt(userId), operatorId); + + res.json({ + success: true, + message: '已解除拉黑' + }); + } catch (error) { + console.error('解除拉黑失败:', error); + res.status(500).json({ success: false, message: error.message || '解除拉黑失败' }); + } +}); + +/** + * @swagger + * /risk-management/overdue-transfers: + * get: + * summary: 获取超时转账列表 + * tags: [RiskManagement] + * security: + * - bearerAuth: [] + * parameters: + * - in: query + * name: page + * schema: + * type: integer + * default: 1 + * description: 页码 + * - in: query + * name: limit + * schema: + * type: integer + * default: 10 + * description: 每页数量 + * responses: + * 200: + * description: 成功获取超时转账列表 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * data: + * type: object + * properties: + * transfers: + * type: array + * items: + * type: object + * properties: + * id: + * type: integer + * user_id: + * type: integer + * recipient_id: + * type: integer + * amount: + * type: number + * status: + * type: string + * created_at: + * type: string + * format: date-time + * username: + * type: string + * recipient_name: + * type: string + * overdue_hours: + * type: number + * pagination: + * type: object + * properties: + * total: + * type: integer + * page: + * type: integer + * limit: + * type: integer + * pages: + * type: integer + * 401: + * description: 未授权 + * 403: + * description: 权限不足 + * 500: + * description: 服务器错误 + */ +router.get('/overdue-transfers', auth, requireAdmin, async (req, res) => { + try { + const { page = 1, limit = 10 } = req.query; + const pageNum = parseInt(page, 10) || 1; + const limitNum = parseInt(limit, 10) || 10; + const offset = (pageNum - 1) * limitNum; + + const db = getDB(); + + // 获取总数 + const [countResult] = await db.execute( + 'SELECT COUNT(*) as total FROM transfers WHERE is_overdue = 1' + ); + const total = countResult[0].total; + + // 获取数据 + const [transfers] = await db.execute( + `SELECT t.*, + fu.username as from_username, fu.real_name as from_real_name, + tu.username as to_username, tu.real_name as to_real_name + FROM transfers t + LEFT JOIN users fu ON t.from_user_id = fu.id + LEFT JOIN users tu ON t.to_user_id = tu.id + WHERE t.is_overdue = 1 + ORDER BY t.overdue_at DESC + LIMIT ${limitNum} OFFSET ${offset}` + ); + + res.json({ + success: true, + data: { + transfers, + pagination: { + page: pageNum, + limit: limitNum, + total, + pages: Math.ceil(total / limitNum) + } + } + }); + } catch (error) { + console.error('获取超时转账列表失败:', error); + res.status(500).json({ success: false, message: '获取超时转账列表失败' }); + } +}); + +/** + * 手动检查转账超时 + */ +router.post('/check-timeouts', auth, requireAdmin, async (req, res) => { + try { + await timeoutService.checkTransferTimeouts(); + + res.json({ + success: true, + message: '转账超时检查已完成' + }); + } catch (error) { + console.error('手动检查转账超时失败:', error); + res.status(500).json({ success: false, message: '检查转账超时失败' }); + } +}); + +/** + * 获取风险管理统计信息 + */ +router.get('/stats', auth, requireAdmin, async (req, res) => { + try { + const db = getDB(); + + // 获取统计数据 + const [stats] = await db.execute( + `SELECT + COUNT(CASE WHEN is_risk_user = 1 THEN 1 END) as risk_users_count, + COUNT(CASE WHEN is_blacklisted = 1 THEN 1 END) as blacklisted_users_count, + COUNT(CASE WHEN is_risk_user = 1 AND is_blacklisted = 0 THEN 1 END) as risk_not_blacklisted_count + FROM users` + ); + + const [overdueStats] = await db.execute( + `SELECT + COUNT(*) as overdue_transfers_count, + SUM(amount) as overdue_amount_total + FROM transfers + WHERE is_overdue = 1` + ); + + const [todayOverdue] = await db.execute( + `SELECT COUNT(*) as today_overdue_count + FROM transfers + WHERE is_overdue = 1 AND DATE(overdue_at) = CURDATE()` + ); + + res.json({ + success: true, + data: { + riskUsersCount: stats[0].risk_users_count, + blacklistedUsersCount: stats[0].blacklisted_users_count, + riskNotBlacklistedCount: stats[0].risk_not_blacklisted_count, + overdueTransfersCount: overdueStats[0].overdue_transfers_count, + overdueAmountTotal: overdueStats[0].overdue_amount_total || 0, + todayOverdueCount: todayOverdue[0].today_overdue_count + } + }); + } catch (error) { + console.error('获取风险管理统计失败:', error); + res.status(500).json({ success: false, message: '获取统计信息失败' }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/routes/sms.js b/routes/sms.js new file mode 100644 index 0000000..787bd56 --- /dev/null +++ b/routes/sms.js @@ -0,0 +1,340 @@ +const express = require('express') +const router = express.Router() +const { getDB } = require('../database') +const Dysmsapi20170525 = require('@alicloud/dysmsapi20170525') +const OpenApi = require('@alicloud/openapi-client') +const { Config } = require('@alicloud/openapi-client') + +/** + * @swagger + * tags: + * name: SMS + * description: 短信验证码相关接口 + */ + +/** + * @swagger + * components: + * schemas: + * SMSVerification: + * type: object + * properties: + * phone: + * type: string + * description: 手机号码 + * code: + * type: string + * description: 验证码 + */ + +// 阿里云短信配置 +const config = new Config({ + // 您的AccessKey ID + accessKeyId: process.env.ALIYUN_ACCESS_KEY_ID || 'your_access_key_id', + // 您的AccessKey Secret + accessKeySecret: process.env.ALIYUN_ACCESS_KEY_SECRET || 'your_access_key_secret', + // 访问的域名 + endpoint: 'dysmsapi.aliyuncs.com' +}) + +// 创建短信客户端 +const client = new Dysmsapi20170525.default(config) + +// 短信模板配置 +const SMS_CONFIG = { + signName: process.env.ALIYUN_SMS_SIGN_NAME || '您的签名', // 短信签名 + templateCode: process.env.ALIYUN_SMS_TEMPLATE_CODE || 'SMS_XXXXXX', // 短信模板CODE + // 开发环境标识 + isDevelopment: process.env.NODE_ENV !== 'production' +} + +// 存储验证码的内存对象(生产环境建议使用Redis) +const smsCodeStore = new Map() + +// 验证码有效期(5分钟) +const CODE_EXPIRE_TIME = 5 * 60 * 1000 +// 最大尝试次数 +const MAX_ATTEMPTS = 3 +// 发送频率限制(60秒) +const SEND_INTERVAL = 60 * 1000 + +/** + * 生成6位数字验证码 + * @returns {string} 验证码 + */ +function generateSMSCode() { + return Math.floor(100000 + Math.random() * 900000).toString(); +} + +/** + * @swagger + * /api/sms/send: + * post: + * summary: 发送短信验证码 + * tags: [SMS] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - phone + * properties: + * phone: + * type: string + * description: 手机号码 + * responses: + * 200: + * description: 验证码发送成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: 验证码发送成功 + * 400: + * description: 参数错误或发送频率限制 + * 500: + * description: 服务器错误 + */ +router.post('/send', async (req, res) => { + try { + const { phone } = req.body + console.log(phone) + // 验证手机号格式 + const phoneRegex = /^1[3-9]\d{9}$/ + if (!phoneRegex.test(phone)) { + return res.json({ + success: false, + message: '手机号格式不正确' + }) + } + + // 检查发送频率限制 + const lastSendTime = smsCodeStore.get(`last_send_${phone}`) + if (lastSendTime && Date.now() - lastSendTime < SEND_INTERVAL) { + const remainingTime = Math.ceil((SEND_INTERVAL - (Date.now() - lastSendTime)) / 1000) + return res.json({ + success: false, + message: `请等待${remainingTime}秒后再发送` + }) + } + + // 生成6位数字验证码 + const code = Math.random().toString().slice(-6) + + // 存储验证码信息 + smsCodeStore.set(phone, { + code, + timestamp: Date.now(), + attempts: 0 + }) + + // 记录发送时间 + smsCodeStore.set(`last_send_${phone}`, Date.now()) + // 生产环境发送真实短信 + try { + console.log(code); + res.json({ + success: true, + message: '验证码发送成功' + }) + return + const sendSmsRequest = new Dysmsapi20170525.SendSmsRequest({ + phoneNumbers: phone, + signName: SMS_CONFIG.signName, + templateCode: SMS_CONFIG.templateCode, + templateParam: JSON.stringify({ code }) + }) + + const response = await client.sendSms(sendSmsRequest) + console.log(response.body); + + if (response.body.code === 'OK') { + res.json({ + success: true, + message: '验证码发送成功' + }) + } else { + console.error('阿里云短信发送失败:', response.body) + res.json({ + success: false, + message: '发送失败,请稍后重试' + }) + } + } catch (smsError) { + console.error('阿里云短信API调用失败:', smsError) + res.json({ + success: false, + message: '发送失败,请稍后重试' + }) + } + + } catch (error) { + console.error('发送短信验证码失败:', error) + res.status(500).json({ + success: false, + message: '发送失败,请稍后重试' + }) + } +}); + +/** + * @swagger + * /api/sms/verify: + * post: + * summary: 验证短信验证码 + * tags: [SMS] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - phone + * - code + * properties: + * phone: + * type: string + * description: 手机号码 + * code: + * type: string + * description: 验证码 + * responses: + * 200: + * description: 验证码验证成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: 手机号验证成功 + * data: + * type: object + * properties: + * phone: + * type: string + * verified: + * type: boolean + * 400: + * description: 参数错误或验证码错误 + * 500: + * description: 服务器错误 + */ +router.post('/verify', async (req, res) => { + try { + const { phone, code } = req.body; + + if (!phone || !code) { + return res.status(400).json({ success: false, message: '手机号和验证码不能为空' }); + } + + const storedData = smsCodeStore.get(phone); + + if (!storedData) { + return res.status(400).json({ success: false, message: '验证码不存在或已过期' }); + } + + // 检查验证码是否过期(5分钟) + if (Date.now() - storedData.timestamp > 300000) { + smsCodeStore.delete(phone); + return res.status(400).json({ success: false, message: '验证码已过期' }); + } + + // 检查尝试次数(最多3次) + if (storedData.attempts >= 3) { + smsCodeStore.delete(phone); + return res.status(400).json({ success: false, message: '验证码错误次数过多,请重新获取' }); + } + + // 验证验证码 + if (storedData.code !== code) { + storedData.attempts++; + smsCodeStore.set(phone, storedData); + return res.status(400).json({ + success: false, + message: `验证码错误,还可尝试${3 - storedData.attempts}次` + }); + } + + // 验证成功,删除验证码 + smsCodeStore.delete(phone); + smsCodeStore.delete(`time_${phone}`); + + res.json({ + success: true, + message: '手机号验证成功', + data: { + phone: phone, + verified: true + } + }); + + } catch (error) { + console.error('验证短信验证码错误:', error); + res.status(500).json({ success: false, message: '验证失败' }); + } +}); + +/** + * 导出验证手机号的函数供其他模块使用 + * @param {string} phone 手机号 + * @param {string} code 验证码 + * @returns {boolean} 验证结果 + */ +function verifySMSCode(phone, code) { + const storedData = smsCodeStore.get(phone); + + if (!storedData) { + return false; + } + + // 检查是否过期 + if (Date.now() - storedData.timestamp > 300000) { + smsCodeStore.delete(phone); + return false; + } + + // 检查尝试次数 + if (storedData.attempts >= 3) { + smsCodeStore.delete(phone); + return false; + } + + // 验证验证码 + if (storedData.code === code) { + smsCodeStore.delete(phone); + smsCodeStore.delete(`time_${phone}`); + return true; + } + + return false; +} + +// 清理过期验证码的定时任务 +setInterval(() => { + const now = Date.now(); + for (const [key, value] of smsCodeStore.entries()) { + if (key.startsWith('time_')) continue; + + if (value.timestamp && now - value.timestamp > 300000) { + smsCodeStore.delete(key); + smsCodeStore.delete(`time_${key}`); + } + } +}, 60000); // 每分钟清理一次 + +module.exports = router; +module.exports.verifySMSCode = verifySMSCode; \ No newline at end of file diff --git a/routes/specifications.js b/routes/specifications.js new file mode 100644 index 0000000..3563d10 --- /dev/null +++ b/routes/specifications.js @@ -0,0 +1,1096 @@ +const express = require('express'); +const router = express.Router(); +const { getDB } = require('../database'); +const { auth, adminAuth } = require('../middleware/auth'); + +/** + * @swagger + * components: + * schemas: + * SpecName: + * type: object + * properties: + * id: + * type: integer + * name: + * type: string + * description: 规格名称(如:颜色、尺寸) + * display_name: + * type: string + * description: 显示名称 + * sort_order: + * type: integer + * description: 排序 + * status: + * type: string + * enum: [active, inactive] + * SpecValue: + * type: object + * properties: + * id: + * type: integer + * spec_name_id: + * type: integer + * value: + * type: string + * description: 规格值(如:红色、XL) + * display_value: + * type: string + * color_code: + * type: string + * description: 颜色代码 + * image_url: + * type: string + * sort_order: + * type: integer + * status: + * type: string + * enum: [active, inactive] + */ + +/** + * @swagger + * /specifications/names: + * get: + * summary: 获取所有规格名称 + * tags: [Specifications] + * parameters: + * - in: query + * name: status + * schema: + * type: string + * enum: [active, inactive] + * description: 状态筛选 + * responses: + * 200: + * description: 成功获取规格名称列表 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * data: + * type: array + * items: + * $ref: '#/components/schemas/SpecName' + */ +router.get('/names', async (req, res) => { + try { + const { status = 'active' } = req.query; + + let query = 'SELECT * FROM spec_names'; + const params = []; + + if (status) { + query += ' WHERE status = ?'; + params.push(status); + } + + query += ' ORDER BY sort_order, id'; + + const [specNames] = await getDB().execute(query, params); + + res.json({ + success: true, + data: specNames + }); + } catch (error) { + console.error('获取规格名称失败:', error); + res.status(500).json({ success: false, message: '获取规格名称失败' }); + } +}); + +/** + * @swagger + * /specifications/names: + * post: + * summary: 创建规格名称 + * tags: [Specifications] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - name + * - display_name + * properties: + * name: + * type: string + * description: 规格名称 + * display_name: + * type: string + * description: 显示名称 + * sort_order: + * type: integer + * default: 0 + * responses: + * 201: + * description: 规格名称创建成功 + */ +router.post('/names', auth, adminAuth, async (req, res) => { + try { + const { name, display_name, sort_order = 0 } = req.body; + + if (!name || !display_name) { + return res.status(400).json({ success: false, message: '规格名称和显示名称不能为空' }); + } + + const [result] = await getDB().execute( + 'INSERT INTO spec_names (name, display_name, sort_order) VALUES (?, ?, ?)', + [name, display_name, sort_order] + ); + + res.status(201).json({ + success: true, + message: '规格名称创建成功', + data: { id: result.insertId } + }); + } catch (error) { + if (error.code === 'ER_DUP_ENTRY') { + return res.status(400).json({ success: false, message: '规格名称已存在' }); + } + console.error('创建规格名称失败:', error); + res.status(500).json({ success: false, message: '创建规格名称失败' }); + } +}); + +/** + * @swagger + * /specifications/names/{id}: + * delete: + * summary: 删除规格名称 + * tags: [Specifications] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: 规格名称ID + * responses: + * 200: + * description: 删除成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * message: + * type: string + * example: 规格名称删除成功 + * 400: + * description: 该规格名称下还有规格值,无法删除 + * 404: + * description: 规格名称不存在 + * 500: + * description: 服务器错误 + */ +router.delete('/names/:id', auth, adminAuth, async (req, res) => { + try { + const { id } = req.params; + + // 检查规格名称是否存在 + const [existingName] = await getDB().execute( + 'SELECT id FROM spec_names WHERE id = ?', + [id] + ); + + if (existingName.length === 0) { + return res.status(404).json({ success: false, message: '规格名称不存在' }); + } + + // 检查该规格名称下是否还有规格值 + const [specValues] = await getDB().execute( + 'SELECT COUNT(*) as count FROM spec_values WHERE spec_name_id = ?', + [id] + ); + + if (specValues[0].count > 0) { + return res.status(400).json({ success: false, message: '该规格名称下还有规格值,请先删除所有规格值' }); + } + + // 删除规格名称 + await getDB().execute( + 'DELETE FROM spec_names WHERE id = ?', + [id] + ); + + res.json({ + success: true, + message: '规格名称删除成功' + }); + } catch (error) { + console.error('删除规格名称失败:', error); + res.status(500).json({ success: false, message: '删除规格名称失败' }); + } +}); + +/** + * @swagger + * /specifications/values: + * get: + * summary: 获取规格值列表 + * tags: [Specifications] + * parameters: + * - in: query + * name: spec_name_id + * schema: + * type: integer + * description: 规格名称ID + * - in: query + * name: status + * schema: + * type: string + * enum: [active, inactive] + * description: 状态筛选 + * responses: + * 200: + * description: 成功获取规格值列表 + */ +router.get('/values', async (req, res) => { + try { + const { spec_name_id, status = 'active' } = req.query; + + let query = ` + SELECT sv.*, sn.name as spec_name, sn.display_name as spec_display_name + FROM spec_values sv + LEFT JOIN spec_names sn ON sv.spec_name_id = sn.id + WHERE 1=1 + `; + const params = []; + + if (spec_name_id) { + query += ' AND sv.spec_name_id = ?'; + params.push(spec_name_id); + } + + if (status) { + query += ' AND sv.status = ?'; + params.push(status); + } + + query += ' ORDER BY sv.spec_name_id, sv.sort_order, sv.id'; + + const [specValues] = await getDB().execute(query, params); + + res.json({ + success: true, + data: specValues + }); + } catch (error) { + console.error('获取规格值失败:', error); + res.status(500).json({ success: false, message: '获取规格值失败' }); + } +}); + +/** + * @swagger + * /specifications/values: + * post: + * summary: 创建规格值 + * tags: [Specifications] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - spec_name_id + * - value + * - display_value + * properties: + * spec_name_id: + * type: integer + * value: + * type: string + * display_value: + * type: string + * color_code: + * type: string + * image_url: + * type: string + * sort_order: + * type: integer + * default: 0 + * responses: + * 201: + * description: 规格值创建成功 + */ +router.post('/values', auth, adminAuth, async (req, res) => { + try { + const { spec_name_id, value, display_value, color_code, image_url, sort_order = 0 } = req.body; + + if (!spec_name_id || !value || !display_value) { + return res.status(400).json({ success: false, message: '规格名称ID、规格值和显示值不能为空' }); + } + + const [result] = await getDB().execute( + `INSERT INTO spec_values (spec_name_id, value, display_value, color_code, image_url, sort_order) + VALUES (?, ?, ?, ?, ?, ?)`, + [spec_name_id, value, display_value, color_code || null, image_url || null, sort_order] + ); + + res.status(201).json({ + success: true, + message: '规格值创建成功', + data: { id: result.insertId } + }); + } catch (error) { + if (error.code === 'ER_DUP_ENTRY') { + return res.status(400).json({ success: false, message: '该规格名称下的规格值已存在' }); + } + console.error('创建规格值失败:', error); + res.status(500).json({ success: false, message: '创建规格值失败' }); + } +}); + +/** + * @swagger + * /specifications/combinations/{productId}: + * get: + * summary: 获取商品的规格组合 + * tags: [Specifications] + * parameters: + * - in: path + * name: productId + * required: true + * schema: + * type: integer + * description: 商品ID + * - in: query + * name: status + * schema: + * type: string + * enum: [active, inactive] + * description: 状态筛选 + * responses: + * 200: + * description: 成功获取规格组合 + */ +router.get('/combinations/:productId', async (req, res) => { + try { + const { productId } = req.params; + const { status = 'active' } = req.query; + + // 获取商品的规格组合 + let query = ` + SELECT psc.*, p.name as product_name, p.price as base_price, + p.points_price as base_points_price, p.rongdou_price as base_rongdou_price + FROM product_spec_combinations psc + LEFT JOIN products p ON psc.product_id = p.id + WHERE psc.product_id = ? + `; + const params = [productId]; + + if (status) { + query += ' AND psc.status = ?'; + params.push(status); + } + + query += ' ORDER BY psc.combination_key'; + + const [combinations] = await getDB().execute(query, params); + + // 为每个组合获取详细的规格值信息 + for (let combination of combinations) { + let specValueIds; + try { + // 处理不同的数据格式 + if (!combination.spec_values) { + specValueIds = []; + } else if (typeof combination.spec_values === 'string') { + // 如果是字符串,尝试JSON解析,失败则按逗号分隔处理 + try { + specValueIds = JSON.parse(combination.spec_values); + } catch { + // 按逗号分隔的字符串处理 + specValueIds = combination.spec_values.split(',').map(id => parseInt(id.trim())).filter(id => !isNaN(id)); + } + } else if (Buffer.isBuffer(combination.spec_values)) { + // 如果是Buffer,转换为字符串后处理 + const strValue = combination.spec_values.toString(); + try { + specValueIds = JSON.parse(strValue); + } catch { + specValueIds = strValue.split(',').map(id => parseInt(id.trim())).filter(id => !isNaN(id)); + } + } else { + // 其他情况,尝试直接使用 + specValueIds = Array.isArray(combination.spec_values) ? combination.spec_values : []; + } + } catch (error) { + console.error('解析规格值失败:', combination.spec_values, error); + specValueIds = []; + } + + if (specValueIds && specValueIds.length > 0) { + const placeholders = specValueIds.map(() => '?').join(','); + const [specDetails] = await getDB().execute( + `SELECT sv.*, sn.name as spec_name, sn.display_name as spec_display_name + FROM spec_values sv + LEFT JOIN spec_names sn ON sv.spec_name_id = sn.id + WHERE sv.id IN (${placeholders}) + ORDER BY sn.sort_order, sv.sort_order`, + specValueIds + ); + + combination.spec_details = specDetails; + } else { + combination.spec_details = []; + } + + // 计算实际价格 + combination.actual_price = combination.base_price + (combination.price_adjustment || 0); + combination.actual_points_price = combination.base_points_price + (combination.points_adjustment || 0); + combination.actual_rongdou_price = combination.base_rongdou_price + (combination.rongdou_adjustment || 0); + combination.is_available = combination.stock > 0; + } + + res.json({ + success: true, + data: combinations + }); + } catch (error) { + console.error('获取规格组合失败:', error); + res.status(500).json({ success: false, message: '获取规格组合失败' }); + } +}); + +/** + * @swagger + * /specifications/combinations/{id}: + * get: + * summary: 获取单个规格组合详情 + * tags: [Specifications] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: 规格组合ID + * responses: + * 200: + * description: 成功获取规格组合详情 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * data: + * type: object + * properties: + * id: + * type: integer + * product_id: + * type: integer + * combination_key: + * type: string + * spec_values: + * type: array + * items: + * type: integer + * price_adjustment: + * type: integer + * points_adjustment: + * type: integer + * rongdou_adjustment: + * type: integer + * stock: + * type: integer + * sku_code: + * type: string + * barcode: + * type: string + * weight: + * type: number + * volume: + * type: number + * status: + * type: string + * spec_details: + * type: array + * items: + * type: object + * actual_price: + * type: number + * actual_points_price: + * type: number + * actual_rongdou_price: + * type: number + * is_available: + * type: boolean + * 404: + * description: 规格组合不存在 + * 500: + * description: 服务器错误 + * delete: + * summary: 删除规格组合 + * tags: [Specifications] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: 规格组合ID + * responses: + * 200: + * description: 删除成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * message: + * type: string + * example: 规格组合删除成功 + * 404: + * description: 规格组合不存在 + * 500: + * description: 服务器错误 + */ +router.get('/combinations/:id', async (req, res) => { + try { + const { id } = req.params; + + // 获取规格组合详情 + const [combinations] = await getDB().execute( + `SELECT psc.*, p.name as product_name, p.price as base_price, + p.points_price as base_points_price, p.rongdou_price as base_rongdou_price + FROM product_spec_combinations psc + LEFT JOIN products p ON psc.product_id = p.id + WHERE psc.id = ?`, + [id] + ); + + if (combinations.length === 0) { + return res.status(404).json({ success: false, message: '规格组合不存在' }); + } + + const combination = combinations[0]; + + // 解析规格值并获取详细信息 + let specValueIds; + try { + if (!combination.spec_values) { + specValueIds = []; + } else if (typeof combination.spec_values === 'string') { + try { + specValueIds = JSON.parse(combination.spec_values); + } catch { + specValueIds = combination.spec_values.split(',').map(id => parseInt(id.trim())).filter(id => !isNaN(id)); + } + } else if (Buffer.isBuffer(combination.spec_values)) { + const strValue = combination.spec_values.toString(); + try { + specValueIds = JSON.parse(strValue); + } catch { + specValueIds = strValue.split(',').map(id => parseInt(id.trim())).filter(id => !isNaN(id)); + } + } else { + specValueIds = Array.isArray(combination.spec_values) ? combination.spec_values : []; + } + } catch (error) { + console.error('解析规格值失败:', combination.spec_values, error); + specValueIds = []; + } + + if (specValueIds && specValueIds.length > 0) { + const placeholders = specValueIds.map(() => '?').join(','); + const [specDetails] = await getDB().execute( + `SELECT sv.*, sn.name as spec_name, sn.display_name as spec_display_name + FROM spec_values sv + LEFT JOIN spec_names sn ON sv.spec_name_id = sn.id + WHERE sv.id IN (${placeholders}) + ORDER BY sn.sort_order, sv.sort_order`, + specValueIds + ); + + combination.spec_details = specDetails; + } else { + combination.spec_details = []; + } + + // 计算实际价格 + combination.actual_price = combination.base_price + (combination.price_adjustment || 0); + combination.actual_points_price = combination.base_points_price + (combination.points_adjustment || 0); + combination.actual_rongdou_price = combination.base_rongdou_price + (combination.rongdou_adjustment || 0); + combination.is_available = combination.stock > 0; + + res.json({ + success: true, + data: combination + }); + } catch (error) { + console.error('获取规格组合详情失败:', error); + res.status(500).json({ success: false, message: '获取规格组合详情失败' }); + } +}); + +/** + * @swagger + * /specifications/combinations/{id}: + * delete: + * summary: 删除规格组合 + * tags: [Specifications] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: 规格组合ID + * responses: + * 200: + * description: 删除成功 + * 404: + * description: 规格组合不存在 + */ +router.delete('/combinations/:id', auth, adminAuth, async (req, res) => { + try { + const { id } = req.params; + + // 检查规格组合是否存在 + const [existingCombination] = await getDB().execute( + 'SELECT id FROM product_spec_combinations WHERE id = ?', + [id] + ); + + if (existingCombination.length === 0) { + return res.status(404).json({ success: false, message: '规格组合不存在' }); + } + + // 删除规格组合 + await getDB().execute( + 'DELETE FROM product_spec_combinations WHERE id = ?', + [id] + ); + + res.json({ + success: true, + message: '规格组合删除成功' + }); + } catch (error) { + console.error('删除规格组合失败:', error); + res.status(500).json({ success: false, message: '删除规格组合失败' }); + } +}); + +/** + * @swagger + * /specifications/combinations/{id}: + * put: + * summary: 更新规格组合 + * tags: [Specifications] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: 规格组合ID + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * price_adjustment: + * type: integer + * points_adjustment: + * type: integer + * rongdou_adjustment: + * type: integer + * stock: + * type: integer + * sku_code: + * type: string + * barcode: + * type: string + * weight: + * type: number + * volume: + * type: number + * status: + * type: string + * enum: [active, inactive] + * responses: + * 200: + * description: 规格组合更新成功 + * 404: + * description: 规格组合不存在 + */ +router.put('/combinations/:id', auth, adminAuth, async (req, res) => { + try { + const { id } = req.params; + const { + price_adjustment, + points_adjustment, + rongdou_adjustment, + stock, + sku_code, + barcode, + weight, + volume, + status + } = req.body; + + // 检查规格组合是否存在 + const [existing] = await getDB().execute( + 'SELECT id FROM product_spec_combinations WHERE id = ?', + [id] + ); + + if (existing.length === 0) { + return res.status(404).json({ success: false, message: '规格组合不存在' }); + } + + // 构建更新字段 + const updateFields = []; + const updateValues = []; + + if (price_adjustment !== undefined) { + updateFields.push('price_adjustment = ?'); + updateValues.push(price_adjustment); + } + + if (points_adjustment !== undefined) { + updateFields.push('points_adjustment = ?'); + updateValues.push(points_adjustment); + } + + if (rongdou_adjustment !== undefined) { + updateFields.push('rongdou_adjustment = ?'); + updateValues.push(rongdou_adjustment); + } + + if (stock !== undefined) { + updateFields.push('stock = ?'); + updateValues.push(stock); + } + + if (sku_code !== undefined) { + updateFields.push('sku_code = ?'); + updateValues.push(sku_code); + } + + if (barcode !== undefined) { + updateFields.push('barcode = ?'); + updateValues.push(barcode); + } + + if (weight !== undefined) { + updateFields.push('weight = ?'); + updateValues.push(weight); + } + + if (volume !== undefined) { + updateFields.push('volume = ?'); + updateValues.push(volume); + } + + if (status !== undefined) { + updateFields.push('status = ?'); + updateValues.push(status); + } + + if (updateFields.length === 0) { + return res.status(400).json({ success: false, message: '没有提供要更新的字段' }); + } + + updateFields.push('updated_at = NOW()'); + updateValues.push(id); + + const updateQuery = `UPDATE product_spec_combinations SET ${updateFields.join(', ')} WHERE id = ?`; + + await getDB().execute(updateQuery, updateValues); + + res.json({ + success: true, + message: '规格组合更新成功' + }); + } catch (error) { + console.error('更新规格组合失败:', error); + res.status(500).json({ success: false, message: '更新规格组合失败' }); + } +}); + +/** + * @swagger + * /specifications/combinations: + * post: + * summary: 创建商品规格组合 + * tags: [Specifications] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - product_id + * - spec_values + * properties: + * product_id: + * type: integer + * spec_values: + * type: array + * items: + * type: integer + * description: 规格值ID数组 + * price_adjustment: + * type: integer + * default: 0 + * points_adjustment: + * type: integer + * default: 0 + * rongdou_adjustment: + * type: integer + * default: 0 + * stock: + * type: integer + * default: 0 + * sku_code: + * type: string + * barcode: + * type: string + * weight: + * type: number + * volume: + * type: number + * responses: + * 201: + * description: 规格组合创建成功 + */ +router.post('/combinations', auth, adminAuth, async (req, res) => { + try { + const { + product_id, + spec_values, + price_adjustment = 0, + points_adjustment = 0, + rongdou_adjustment = 0, + stock = 0, + sku_code, + barcode, + weight, + volume + } = req.body; + + if (!product_id || !spec_values || !Array.isArray(spec_values) || spec_values.length === 0) { + return res.status(400).json({ success: false, message: '商品ID和规格值数组不能为空' }); + } + + // 生成组合键 + const sortedSpecValues = [...spec_values].sort((a, b) => a - b); + const combinationKey = sortedSpecValues.join('-'); + + const [result] = await getDB().execute( + `INSERT INTO product_spec_combinations + (product_id, combination_key, spec_values, price_adjustment, points_adjustment, + rongdou_adjustment, stock, sku_code, barcode, weight, volume) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [ + product_id, + combinationKey, + JSON.stringify(sortedSpecValues), + price_adjustment, + points_adjustment, + rongdou_adjustment, + stock, + sku_code, + barcode, + weight, + volume + ] + ); + + res.status(201).json({ + success: true, + message: '规格组合创建成功', + data: { id: result.insertId, combination_key: combinationKey } + }); + } catch (error) { + if (error.code === 'ER_DUP_ENTRY') { + return res.status(400).json({ success: false, message: '该规格组合已存在' }); + } + console.error('创建规格组合失败:', error); + res.status(500).json({ success: false, message: '创建规格组合失败' }); + } +}); + +/** + * @swagger + * /specifications/generate-combinations: + * post: + * summary: 为商品生成笛卡尔积规格组合 + * tags: [Specifications] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - product_id + * - spec_name_ids + * properties: + * product_id: + * type: integer + * spec_name_ids: + * type: array + * items: + * type: integer + * description: 规格名称ID数组 + * default_stock: + * type: integer + * default: 0 + * description: 默认库存 + * responses: + * 201: + * description: 规格组合生成成功 + */ +router.post('/generate-combinations', auth, adminAuth, async (req, res) => { + try { + const { product_id, spec_name_ids, default_stock = 0 } = req.body; + + if (!product_id || !spec_name_ids || !Array.isArray(spec_name_ids) || spec_name_ids.length === 0) { + return res.status(400).json({ success: false, message: '商品ID和规格名称ID数组不能为空' }); + } + + // 获取每个规格名称下的所有活跃规格值 + const specValueGroups = []; + + for (const specNameId of spec_name_ids) { + const [specValues] = await getDB().execute( + 'SELECT id FROM spec_values WHERE spec_name_id = ? AND status = "active" ORDER BY sort_order, id', + [specNameId] + ); + + if (specValues.length === 0) { + return res.status(400).json({ + success: false, + message: `规格名称ID ${specNameId} 下没有活跃的规格值` + }); + } + + specValueGroups.push(specValues.map(sv => sv.id)); + } + + // 生成笛卡尔积 + function cartesianProduct(arrays) { + return arrays.reduce((acc, curr) => { + const result = []; + acc.forEach(a => { + curr.forEach(c => { + result.push([...a, c]); + }); + }); + return result; + }, [[]]); + } + + const combinations = cartesianProduct(specValueGroups); + + // 生成所有组合键 + const combinationData = combinations.map(combination => { + const sortedCombination = [...combination].sort((a, b) => a - b); + const combinationKey = sortedCombination.join('-'); + return { + combination: sortedCombination, + key: combinationKey + }; + }); + + // 批量检查已存在的组合 + const existingKeys = new Set(); + if (combinationData.length > 0) { + const keys = combinationData.map(item => item.key); + const placeholders = keys.map(() => '?').join(','); + + const [existingCombinations] = await getDB().execute( + `SELECT combination_key FROM product_spec_combinations + WHERE product_id = ? AND combination_key IN (${placeholders})`, + [product_id, ...keys] + ); + + existingCombinations.forEach(row => { + existingKeys.add(row.combination_key); + }); + } + + // 过滤出需要插入的新组合 + const newCombinations = combinationData.filter(item => !existingKeys.has(item.key)); + + // 批量插入新的规格组合 + let createdCount = 0; + const skippedCount = combinationData.length - newCombinations.length; + + if (newCombinations.length > 0) { + // 使用批量插入提高性能 + const values = []; + const placeholders = []; + + newCombinations.forEach(item => { + values.push( + product_id, + item.key, + JSON.stringify(item.combination), + default_stock + ); + placeholders.push('(?, ?, ?, ?)'); + }); + + const sql = `INSERT INTO product_spec_combinations + (product_id, combination_key, spec_values, stock) + VALUES ${placeholders.join(', ')}`; + + const [result] = await getDB().execute(sql, values); + createdCount = result.affectedRows; + } + + res.status(201).json({ + success: true, + message: '规格组合生成完成', + data: { + total_combinations: combinations.length, + created: createdCount, + skipped: skippedCount + } + }); + } catch (error) { + console.error('生成规格组合失败:', error); + res.status(500).json({ success: false, message: '生成规格组合失败' }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/routes/system.js b/routes/system.js new file mode 100644 index 0000000..ffdaea0 --- /dev/null +++ b/routes/system.js @@ -0,0 +1,321 @@ +const express = require('express'); +const { auth } = require('../middleware/auth'); +const { validateQuery, validate } = require('../middleware/validation'); +const { logger } = require('../config/logger'); +const { HTTP_STATUS } = require('../config/constants'); +const { getDB } = require('../database'); +const Joi = require('joi'); + +const router = express.Router(); + +/** + * 系统设置验证规则 + */ +const systemSchemas = { + // 更新系统设置 + updateSettings: Joi.object({ + basic: Joi.object({ + siteName: Joi.string().max(100).allow(''), + siteDescription: Joi.string().max(500).allow(''), + siteKeywords: Joi.string().max(200).allow(''), + siteLogo: Joi.string().allow(''), + siteFavicon: Joi.string().allow(''), + + icp: Joi.string().max(100).allow('') + }).optional(), + features: Joi.object({ + allowRegister: Joi.boolean(), + allowTransfer: Joi.boolean(), + allowExchange: Joi.boolean(), + allowReview: Joi.boolean(), + allowComment: Joi.boolean() + }).optional(), + + security: Joi.object({ + maxLoginAttempts: Joi.number().integer().min(1).max(100), + lockoutDuration: Joi.number().integer().min(1).max(86400), + ipWhitelist: Joi.string().allow('') + }).optional() + }) +}; + +/** + * 获取系统设置 (公开接口,不需要认证) + * GET /api/system/settings + */ +router.get('/settings', async (req, res, next) => { + try { + + const db = getDB(); + + // 获取系统设置 + const [settings] = await db.execute( + 'SELECT setting_key, setting_value FROM system_settings' + ); + + // 组织设置数据 + const settingsData = { + basic: { + siteName: '', + siteDescription: '', + siteKeywords: '', + siteLogo: '', + siteFavicon: '', + + icp: '' + }, + features: { + allowRegister: true, + allowTransfer: true, + allowExchange: true, + allowReview: true, + allowComment: true + }, + + security: { + maxLoginAttempts: 5, + lockoutDuration: 300, + ipWhitelist: '' + } + }; + + // 填充数据库中的设置 + settings.forEach(setting => { + const keys = setting.setting_key.split('.'); + if (keys.length === 2 && settingsData[keys[0]]) { + try { + // 尝试解析JSON值,如果失败则使用原始值 + settingsData[keys[0]][keys[1]] = JSON.parse(setting.setting_value); + } catch { + settingsData[keys[0]][keys[1]] = setting.setting_value; + } + } + }); + + logger.info('System settings retrieved', { + settingsCount: settings.length + }); + + res.json({ + success: true, + data: settingsData + }); + } catch (error) { + next(error); + } +}); + +/** + * 更新系统设置 + * PUT /api/system/settings + */ +router.put('/settings', + auth, + validate(systemSchemas.updateSettings), + async (req, res, next) => { + try { + // 检查管理员权限 + if (req.user.role !== 'admin') { + return res.status(HTTP_STATUS.FORBIDDEN).json({ + success: false, + message: '权限不足' + }); + } + + const db = getDB(); + const settings = req.body; + + // 开始事务 + await db.beginTransaction(); + + try { + // 更新设置 + for (const [category, categorySettings] of Object.entries(settings)) { + for (const [key, value] of Object.entries(categorySettings)) { + const settingKey = `${category}.${key}`; + const settingValue = JSON.stringify(value); + + await db.execute( + `INSERT INTO system_settings (setting_key, setting_value, updated_at) + VALUES (?, ?, NOW()) + ON DUPLICATE KEY UPDATE + setting_value = VALUES(setting_value), + updated_at = VALUES(updated_at)`, + [settingKey, settingValue] + ); + } + } + + await db.commit(); + + logger.info('System settings updated', { + userId: req.user.id, + categories: Object.keys(settings) + }); + + res.json({ + success: true, + message: '系统设置更新成功' + }); + } catch (error) { + await db.rollback(); + throw error; + } + } catch (error) { + next(error); + } + } +); + +/** + * 获取系统信息 + * GET /api/system/info + */ +router.get('/info', auth, async (req, res, next) => { + try { + // 检查管理员权限 + if (req.user.role !== 'admin') { + return res.status(HTTP_STATUS.FORBIDDEN).json({ + success: false, + message: '权限不足' + }); + } + + const systemInfo = { + version: '1.0.0', + nodeVersion: process.version, + platform: process.platform, + uptime: process.uptime(), + memoryUsage: process.memoryUsage(), + timestamp: new Date().toISOString() + }; + + res.json({ + success: true, + data: systemInfo + }); + } catch (error) { + next(error); + } +}); + +/** + * 获取维护模式状态(公开接口) + * GET /api/system/maintenance-status + */ +router.get('/maintenance-status', async (req, res, next) => { + try { + const db = getDB(); + + // 从系统设置表获取维护模式状态 + const [rows] = await db.execute( + 'SELECT setting_value FROM system_settings WHERE setting_key = ?', + ['maintenance_mode'] + ); + + const maintenanceMode = rows.length > 0 ? rows[0].setting_value === 'true' : false; + + res.json({ + success: true, + data: { + maintenance_mode: maintenanceMode + } + }); + + } catch (error) { + console.error('获取维护模式状态失败:', error); + next(error); + } +}); + +/** + * 获取维护模式状态(管理员接口) + * GET /api/system/admin/maintenance-status + */ +router.get('/admin/maintenance-status', auth, async (req, res, next) => { + try { + // 检查管理员权限 + if (req.user.role !== 'admin') { + return res.status(403).json({ + success: false, + error: { + code: 'FORBIDDEN', + message: '权限不足' + } + }); + } + + const db = getDB(); + + // 从系统设置表获取维护模式状态 + const [rows] = await db.execute( + 'SELECT setting_value FROM system_settings WHERE setting_key = ?', + ['maintenance_mode'] + ); + + const maintenanceMode = rows.length > 0 ? rows[0].setting_value === 'true' : false; + + res.json({ + success: true, + data: { + maintenance_mode: maintenanceMode + } + }); + + } catch (error) { + console.error('获取维护模式状态失败:', error); + next(error); + } +}); + +/** + * 切换维护模式 + * POST /api/system/toggle-maintenance + */ +router.post('/toggle-maintenance', auth, async (req, res, next) => { + try { + // 检查管理员权限 + if (req.user.role !== 'admin') { + return res.status(403).json({ + success: false, + error: { + code: 'FORBIDDEN', + message: '权限不足' + } + }); + } + + const { maintenance_mode } = req.body; + const db = getDB(); + + // 更新或插入维护模式设置 + await db.execute( + `INSERT INTO system_settings (setting_key, setting_value, updated_at) + VALUES ('maintenance_mode', ?, NOW()) + ON DUPLICATE KEY UPDATE + setting_value = VALUES(setting_value), + updated_at = NOW()`, + [maintenance_mode ? 'true' : 'false'] + ); + + logger.info('Maintenance mode toggled', { + userId: req.user.id, + username: req.user.username, + maintenanceMode: maintenance_mode + }); + + res.json({ + success: true, + data: { + maintenance_mode: maintenance_mode + }, + message: maintenance_mode ? '维护模式已开启' : '维护模式已关闭' + }); + + } catch (error) { + console.error('切换维护模式失败:', error); + next(error); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/routes/transfers.js b/routes/transfers.js new file mode 100644 index 0000000..91ff2fd --- /dev/null +++ b/routes/transfers.js @@ -0,0 +1,1208 @@ +const express = require('express'); +const transferService = require('../services/transferService'); +const {auth: authenticateToken} = require('../middleware/auth'); +const {validate, validateQuery, transferSchemas, commonSchemas} = require('../middleware/validation'); +const {logger} = require('../config/logger'); +const {HTTP_STATUS} = require('../config/constants'); +const {getDB} = require('../database'); +const multer = require('multer'); +const path = require('path'); +const dayjs = require('dayjs'); + +const router = express.Router(); + + +router.get('/', + authenticateToken, + validateQuery(transferSchemas.query), + async (req, res, next) => { + try { + const {page, limit, status, start_date, end_date, search, sort, order} = req.query; + + const filters = { + status, + start_date, + end_date, + search + }; + + // 非管理员只能查看自己相关的转账 + if (req.user.role !== 'admin') { + filters.user_id = req.user.id; + } + + const result = await transferService.getTransfers(filters, {page, limit, sort, order}); + + logger.info('Transfer list requested', { + userId: req.user.id, + filters, + resultCount: result.transfers.length + }); + + res.json({ + success: true, + data: result + }); + } catch (error) { + next(error); + } + } +); + +router.get('/history', authenticateToken, async (req, res, next) => { + try { + const {page, limit, start_date, end_date, search, sort, order} = req.query; + + const filters = { + start_date, + end_date, + search + }; + + // 非管理员只能查看自己相关的转账 + if (req.user.role !== 'admin') { + filters.user_id = req.user.id; + } + + const result = await transferService.getTransfersHistory(filters, {page, limit, sort, order}); + + res.json({ + success: true, + data: result + }); + } catch (error) { + next(error); + } +}) + +router.get('/list', + authenticateToken, + validateQuery(transferSchemas.query), + async (req, res, next) => { + try { + const {page, limit, status, transfer_type, start_date, end_date, sort, order} = req.query; + + const filters = { + status, + transfer_type, + start_date, + end_date + }; + + // 非管理员只能查看自己相关的转账 + if (req.user.role !== 'admin') { + filters.user_id = req.user.id; + } + + const result = await transferService.getTransfers(filters, {page, limit, sort, order}); + + logger.info('Transfer list requested', { + userId: req.user.id, + filters, + resultCount: result.transfers.length + }); + + res.json({ + success: true, + data: result + }); + } catch (error) { + next(error); + } + } +); + + +router.get('/public-account', authenticateToken, async (req, res) => { + try { + const db = getDB(); + const [publicUser] = await db.execute(` + SELECT id, username, real_name, balance + FROM users + WHERE username = 'public_account' + AND is_system_account = TRUE + `); + + if (publicUser.length === 0) { + return res.status(404).json({success: false, message: '公户不存在'}); + } + + res.json({success: true, data: publicUser[0]}); + } catch (error) { + console.error('获取公户信息失败:', error); + res.status(500).json({success: false, message: '服务器错误'}); + } +}); + + +router.post('/create', + authenticateToken, + validate(transferSchemas.create), + async (req, res, next) => { + try { + const result = await transferService.createTransfer(req.user.id, req.body); + + logger.info('Transfer creation requested', { + userId: req.user.id, + transferId: result.transfer_id, + amount: req.body.amount + }); + + res.status(HTTP_STATUS.CREATED).json({ + success: true, + message: '转账记录创建成功,等待确认', + data: result + }); + } catch (error) { + next(error); + } + } +); + + +router.post('/admin/create', + authenticateToken, + async (req, res, next) => { + try { + // 检查管理员权限 + if (req.user.role !== 'admin') { + return res.status(403).json({success: false, message: '权限不足'}); + } + + const {from_user_id, to_user_id, amount, transfer_type, description} = req.body; + + // 验证必填字段 + if (!from_user_id || !to_user_id || !amount || !transfer_type) { + return res.status(400).json({success: false, message: '缺少必填字段'}); + } + + const result = await transferService.createTransfer(from_user_id, { + to_user_id, + amount, + transfer_type, + description: description || '管理员分配转账' + }); + + logger.info('Admin transfer creation requested', { + adminId: req.user.id, + fromUserId: from_user_id, + toUserId: to_user_id, + transferId: result.transfer_id, + amount: amount + }); + + res.status(HTTP_STATUS.CREATED).json({ + success: true, + message: '转账分配成功', + data: result + }); + } catch (error) { + next(error); + } + } +); + +// 确认转账 +router.post('/confirm', + authenticateToken, + validate(transferSchemas.confirm), + async (req, res, next) => { + try { + const {transfer_id, note} = req.body; + + await transferService.confirmTransfer(transfer_id, note, req.user.id); + + logger.info('Transfer confirmed', { + transferId: transfer_id, + operatorId: req.user.id + }); + + res.json({ + success: true, + message: '转账确认成功' + }); + } catch (error) { + next(error); + } + } +); + +// 确认转账(路径参数形式) +router.post('/confirm/:id', + authenticateToken, + async (req, res, next) => { + try { + const transfer_id = req.params.id; + const {action, note} = req.body; + + // 验证action参数 + if (action !== 'confirm') { + return res.status(400).json({ + success: false, + message: 'action参数必须为confirm' + }); + } + + await transferService.confirmTransfer(transfer_id, note, req.user.id); + + logger.info('Transfer confirmed via path param', { + transferId: transfer_id, + operatorId: req.user.id + }); + + res.json({ + success: true, + message: '转账确认成功' + }); + } catch (error) { + next(error); + } + } +); + +// 拒绝转账 +router.post('/reject', + authenticateToken, + validate(transferSchemas.reject), + async (req, res, next) => { + try { + const {transfer_id, note = ''} = req.body; + + await transferService.rejectTransfer(transfer_id, note, req.user.id); + + logger.info('Transfer rejected', { + transferId: transfer_id, + operatorId: req.user.id + }); + + res.json({ + success: true, + message: '转账已拒绝' + }); + } catch (error) { + next(error); + } + } +); + +// 用户确认收到转账 +router.post('/confirm-received', + authenticateToken, + async (req, res, next) => { + try { + const {transfer_id} = req.body; + + if (!transfer_id) { + return res.status(400).json({success: false, message: '缺少转账ID'}); + } + + await transferService.confirmReceived(transfer_id, req.user.id); + + logger.info('Transfer received confirmed by user', { + transferId: transfer_id, + userId: req.user.id + }); + + res.json({ + success: true, + message: '已确认收到转账,余额已更新' + }); + } catch (error) { + next(error); + } + } +); + +// 用户确认未收到转账 +router.post('/confirm-not-received', + authenticateToken, + async (req, res, next) => { + try { + const {transfer_id} = req.body; + + if (!transfer_id) { + return res.status(400).json({success: false, message: '缺少转账ID'}); + } + + await transferService.confirmNotReceived(transfer_id, req.user.id); + + logger.info('Transfer not received confirmed by user', { + transferId: transfer_id, + userId: req.user.id + }); + + res.json({ + success: true, + message: '已确认未收到转账' + }); + } catch (error) { + next(error); + } + } +); + + + +// 获取用户转账记录 +router.get('/user/:userId', authenticateToken, async (req, res) => { + try { + const userId = req.params.userId; + const {page = 1, limit = 10, status} = req.query; + + // 检查权限(只能查看自己的记录或管理员查看所有) + // if (req.user.id != userId && req.user.role !== 'admin') { + // return res.status(403).json({ success: false, message: '权限不足' }); + // } + + const db = getDB(); + + // 确保参数为有效数字 + const pageNum = Math.max(1, parseInt(page) || 1); + const limitNum = Math.max(1, Math.min(100, parseInt(limit) || 10)); + const offset = Math.max(0, (pageNum - 1) * limitNum); + + let whereClause = `WHERE source_type='manual' AND (t.from_user_id = ? OR t.to_user_id = ?)`; + const userIdInt = parseInt(userId); + let listParams = [userIdInt, userIdInt]; + let countParams = [userIdInt, userIdInt]; + + if (status) { + whereClause += ' AND t.status = ?'; + listParams.push(status); + countParams.push(status); + } + + // 添加分页参数 + listParams.push(limitNum.toString(), offset.toString()); + + const [transfers] = await db.execute(` + SELECT t.*, + from_user.username as from_username, + from_user.real_name as from_real_name, + to_user.username as to_username, + to_user.real_name as to_real_name + FROM transfers t + LEFT JOIN users from_user ON t.from_user_id = from_user.id + LEFT JOIN users to_user ON t.to_user_id = to_user.id + ${whereClause} + ORDER BY t.created_at + DESC + LIMIT ${limitNum} OFFSET ${offset} + `, countParams); + + const [countResult] = await db.execute(` + SELECT COUNT(*) as total + FROM transfers t ${whereClause} + `, countParams); + + res.json({ + success: true, + data: { + transfers, + pagination: { + page: pageNum, + limit: limitNum, + total: countResult[0].total, + pages: Math.ceil(countResult[0].total / limitNum) + } + } + }); + } catch (error) { + console.error('获取转账记录失败:', error); + res.status(500).json({success: false, message: '服务器错误'}); + } +}); + +// 获取转账统计信息 +router.get('/stats', authenticateToken, async (req, res) => { + try { + const userId = req.user.id; + const isAdmin = req.user.role === 'admin'; + const db = getDB(); + + let stats = {}; + + if (isAdmin) { + // 管理员可以查看全局统计 + const [totalStats] = await db.execute(` + SELECT COUNT(*) as total_transfers, + SUM(amount) as total_flow_amount, + SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending_count, + SUM(CASE WHEN status = 'confirmed' THEN 1 ELSE 0 END) as confirmed_count, + SUM(CASE WHEN status = 'received' THEN 1 ELSE 0 END) as received_count, + SUM(CASE WHEN status = 'rejected' THEN 1 ELSE 0 END) as rejected_count, + SUM(CASE WHEN status = 'cancelled' THEN 1 ELSE 0 END) as cancelled_count, + SUM(CASE WHEN status = 'not_received' THEN 1 ELSE 0 END) as not_received_count, + SUM(CASE WHEN is_overdue = 1 THEN 1 ELSE 0 END) as overdue_count, + SUM(CASE WHEN is_bad_debt = 1 THEN 1 ELSE 0 END) as bad_debt_count, + SUM(CASE WHEN status = 'confirmed' THEN amount ELSE 0 END) as total_amount, + SUM(CASE WHEN is_bad_debt = 1 THEN amount ELSE 0 END) as bad_debt_amount, + SUM(CASE + WHEN transfer_type = 'initial' AND status = 'confirmed' THEN amount + ELSE 0 END) as initial_amount, + SUM(CASE + WHEN transfer_type = 'return' AND status = 'confirmed' THEN amount + ELSE 0 END) as return_amount, + SUM(CASE + WHEN transfer_type = 'user_to_user' AND status = 'confirmed' THEN amount + ELSE 0 END) as user_to_user_amount, + (SELECT SUM(balance) + FROM users + WHERE role = 'user' + AND is_system_account = 1) as total_merchant_balance, + (SELECT SUM(balance) + FROM users + WHERE role = 'user' + AND is_system_account != 1) as total_user_balance, + SUM(CASE WHEN source_type IN ('system') THEN amount END) as participated_transfers, + SUM(CASE WHEN source_type IN ('agent') THEN amount END) as agent_total, + SUM(CASE WHEN source_type IN ('operated_agent') THEN amount END) as operated_agent_total + FROM transfers + `); + + const todayStr = dayjs().format('YYYY-MM-DD'); + const currentYear = dayjs().year(); + const currentMonth = dayjs().month() + 1; + console.log(todayStr, 'todayStr'); + + const [todayStats] = await db.execute(` + SELECT COUNT(*) as today_transfers, + ( + COALESCE((SELECT SUM(amount) + FROM transfers + WHERE DATE(created_at) = ? + AND to_user_id IN (SELECT id FROM users WHERE is_system_account = 1) + AND status = 'received'), 0) - + COALESCE((SELECT SUM(amount) + FROM transfers + WHERE DATE(created_at) = ? + AND from_user_id IN (SELECT id FROM users WHERE is_system_account = 1) + AND status = 'received'), 0) + ) as today_amount + FROM transfers + WHERE DATE(created_at) = ? + `, [todayStr, todayStr, todayStr]); + + const [monthlyStats] = await db.execute(` + SELECT COUNT(*) as monthly_transfers, + SUM(CASE WHEN status = 'received' THEN amount ELSE 0 END) as monthly_amount, + SUM(CASE WHEN source_type IN ('system') THEN amount END) as monthly_participated_transfers + FROM transfers + WHERE YEAR(created_at) = ? + AND MONTH(created_at) = ? + `, [currentYear, currentMonth]); + + // 获取上月统计数据用于对比 + const lastMonth = currentMonth === 1 ? 12 : currentMonth - 1; + const lastMonthYear = currentMonth === 1 ? currentYear - 1 : currentYear; + + const [lastMonthStats] = await db.execute(` + SELECT COUNT(*) as last_monthly_transfers, + SUM(CASE WHEN status = 'confirmed' THEN amount ELSE 0 END) as last_monthly_amount, + SUM(CASE WHEN source_type IN ('system') THEN amount END) as last_monthly_participated_transfers + FROM transfers + WHERE YEAR(created_at) = ? + AND MONTH(created_at) = ? + `, [lastMonthYear, lastMonth]); + + stats = { + total: { + transfers: totalStats[0].total_transfers || 0, + pending: parseFloat(totalStats[0].total_flow_amount || 0), + pending_count: totalStats[0].pending_count || 0, + confirmed: totalStats[0].confirmed_count || 0, + received_count: totalStats[0].received_count || 0, + rejected: totalStats[0].rejected_count || 0, + cancelled_count: totalStats[0].cancelled_count || 0, + not_received_count: totalStats[0].not_received_count || 0, + overdue: totalStats[0].overdue_count || 0, + bad_debt: totalStats[0].bad_debt_count || 0, + amount: parseFloat(totalStats[0].total_amount || 0), + bad_debt_amount: parseFloat(totalStats[0].bad_debt_amount || 0), + total_merchant_balance: parseFloat(totalStats[0].total_merchant_balance || 0), + initial_amount: parseFloat(totalStats[0].initial_amount || 0), + return_amount: parseFloat(totalStats[0].return_amount || 0), + user_to_user_amount: parseFloat(totalStats[0].user_to_user_amount || 0), + participated_transfers: totalStats[0].participated_transfers || 0, + total_user_balance:totalStats[0].total_user_balance || 0, + agent_total:totalStats[0].agent_total || 0,//代理收入 + operated_agent_total:totalStats[0].operated_agent_total || 0,//直营代理收入 + }, + today: { + transfers: todayStats[0].today_transfers || 0, + amount: parseFloat(todayStats[0].today_amount || 0) + }, + monthly: { + transfers: monthlyStats[0].monthly_transfers || 0, + amount: parseFloat(monthlyStats[0].monthly_amount || 0), + participated_transfers: monthlyStats[0].monthly_participated_transfers || 0 + }, + lastMonth: { + transfers: lastMonthStats[0].last_monthly_transfers || 0, + amount: parseFloat(lastMonthStats[0].last_monthly_amount || 0), + participated_transfers: lastMonthStats[0].last_monthly_participated_transfers || 0 + } + }; + } else { + // 普通用户只能查看自己的统计 + const [userStats] = await db.execute(` + SELECT COUNT(*) as total_transfers, + SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending_count, + SUM(CASE WHEN status = 'confirmed' THEN 1 ELSE 0 END) as confirmed_count, + SUM(CASE WHEN status = 'rejected' THEN 1 ELSE 0 END) as rejected_count, + SUM(CASE WHEN status = 'confirmed' AND from_user_id = ? THEN amount ELSE 0 END) as sent_amount, + SUM(CASE WHEN status = 'confirmed' AND to_user_id = ? THEN amount ELSE 0 END) as received_amount + FROM transfers + WHERE from_user_id = ? + OR to_user_id = ? + `, [userId, userId, userId, userId]); + + const todayStr = dayjs().format('YYYY-MM-DD'); + + const [todayStats] = await db.execute(` + SELECT COUNT(*) as today_transfers, + SUM(CASE WHEN status = 'confirmed' AND from_user_id = ? THEN amount ELSE 0 END) as today_sent, + SUM(CASE WHEN status = 'confirmed' AND to_user_id = ? THEN amount ELSE 0 END) as today_received + FROM transfers + WHERE (from_user_id = ? OR to_user_id = ?) + AND DATE(created_at) = ? + `, [userId, userId, userId, userId, todayStr]); + + stats = { + total: { + transfers: userStats[0].total_transfers || 0, + pending: userStats[0].pending_count || 0, + confirmed: userStats[0].confirmed_count || 0, + rejected: userStats[0].rejected_count || 0, + sent_amount: parseFloat(userStats[0].sent_amount || 0), + received_amount: parseFloat(userStats[0].received_amount || 0) + }, + today: { + transfers: todayStats[0].today_transfers || 0, + sent_amount: parseFloat(todayStats[0].today_sent || 0), + received_amount: parseFloat(todayStats[0].today_received || 0) + } + }; + } + + res.json({success: true, data: stats}); + } catch (error) { + console.error('获取转账统计失败:', error); + res.status(500).json({success: false, message: '获取转账统计失败'}); + } +}); + +// 获取待确认的转账 +router.get('/pending', authenticateToken, async (req, res) => { + try { + const userId = parseInt(req.user.id); + const db = getDB(); + + const [transfers] = await db.execute(` + SELECT t.*, + from_user.username as from_username, + from_user.real_name as from_real_name + FROM transfers t + LEFT JOIN users from_user ON t.from_user_id = from_user.id + WHERE t.to_user_id = ? + AND t.status = 'pending' + ORDER BY t.created_at DESC + `, [userId]); + + res.json({success: true, data: transfers}); + } catch (error) { + console.error('获取待确认转账失败:', error); + res.status(500).json({success: false, message: '服务器错误'}); + } +}); + +// 获取当前用户账户信息(不需要传递用户ID) +router.get('/account', authenticateToken, async (req, res) => { + try { + const userId = req.user.id; + + const db = getDB(); + const [user] = await db.execute(` + SELECT id, username, real_name, balance, created_at, updated_at, points + FROM users + WHERE id = ? + `, [userId]); + + if (user.length === 0) { + return res.status(404).json({success: false, message: '用户不存在'}); + } + + // 返回用户账户信息,格式与原来的 accounts 表保持一致 + const accountData = { + id: user[0].id, + user_id: user[0].id, + account_type: 'user', + balance: user[0].balance, + username: user[0].username, + real_name: user[0].real_name, + created_at: user[0].created_at, + updated_at: user[0].updated_at, + points: user[0].points + }; + + res.json({success: true, data: accountData}); + } catch (error) { + console.error('获取账户信息失败:', error); + res.status(500).json({success: false, message: '服务器错误'}); + } +}); + +// 获取指定用户账户信息(管理员权限或用户本人) +router.get('/account/:userId', authenticateToken, async (req, res) => { + try { + const userId = req.params.userId; + + // 检查权限 + if (req.user.id != userId && req.user.role !== 'admin') { + return res.status(403).json({success: false, message: '权限不足'}); + } + + const db = getDB(); + const [user] = await db.execute(` + SELECT id, username, real_name, balance, created_at, updated_at + FROM users + WHERE id = ? + `, [userId]); + + if (user.length === 0) { + return res.status(404).json({success: false, message: '用户不存在'}); + } + + // 返回用户账户信息,格式与原来的 accounts 表保持一致 + const accountData = { + id: user[0].id, + user_id: user[0].id, + account_type: 'user', + balance: user[0].balance, + username: user[0].username, + real_name: user[0].real_name, + created_at: user[0].created_at, + updated_at: user[0].updated_at + }; + + res.json({success: true, data: accountData}); + } catch (error) { + console.error('获取账户信息失败:', error); + res.status(500).json({success: false, message: '服务器错误'}); + } +}); + +// 获取转账趋势数据(管理员权限) +router.get('/trend', authenticateToken, async (req, res) => { + try { + // 检查管理员权限 + if (req.user.role !== 'admin') { + return res.status(403).json({success: false, message: '权限不足'}); + } + + const db = getDB(); + const {days = 7} = req.query; + const daysNum = Math.min(30, Math.max(1, parseInt(days) || 7)); + + // 首先获取数据库中最早和最晚的转账日期 + const [dateRange] = await db.execute(` + SELECT MIN(DATE(created_at)) as min_date, + MAX(DATE(created_at)) as max_date, + COUNT(*) as total_count + FROM transfers + `); + + if (dateRange[0].total_count === 0) { + // 如果没有转账记录,返回空数据 + const result = []; + const now = new Date(); + + for (let i = daysNum - 1; i >= 0; i--) { + const date = dayjs().subtract(i, 'day'); + result.push({ + date: date.format('MM-DD'), + count: 0, + amount: 0 + }); + } + + return res.json({ + success: true, + data: result + }); + } + + // 获取最近的转账数据(基于实际数据的最大日期) + const maxDate = dayjs(dateRange[0].max_date); + + // 获取指定天数内的转账趋势(从最大日期往前推) + const [trendData] = await db.execute(` + SELECT DATE(created_at) as date, + COUNT(*) as count, + SUM(amount) as amount + FROM transfers + WHERE DATE(created_at) >= DATE_SUB(?, INTERVAL ? DAY) + AND status IN ('confirmed', 'received') + GROUP BY DATE(created_at) + ORDER BY date ASC + `, [dateRange[0].max_date, daysNum - 1]); + + // 填充缺失的日期(转账数为0) + const result = []; + + for (let i = daysNum - 1; i >= 0; i--) { + const date = maxDate.subtract(i, 'day'); + const dateStr = date.format('YYYY-MM-DD'); + + // 修复日期比较:将数据库返回的Date对象转换为字符串进行比较 + const existingData = trendData.find(item => { + const itemDateStr = dayjs(item.date).format('YYYY-MM-DD'); + return itemDateStr === dateStr; + }); + + result.push({ + date: date.format('MM-DD'), + count: existingData ? existingData.count : 0, + amount: existingData ? parseFloat(existingData.amount) : 0 + }); + } + + res.json({ + success: true, + data: result + }); + } catch (error) { + console.error('获取转账趋势错误:', error); + res.status(500).json({success: false, message: '获取转账趋势失败'}); + } +}); + +// 管理员解除坏账(管理员权限) +router.post('/remove-bad-debt/:transferId', authenticateToken, async (req, res) => { + try { + // 检查管理员权限 + if (req.user.role !== 'admin') { + return res.status(403).json({success: false, message: '权限不足'}); + } + + const {transferId} = req.params; + const {reason} = req.body; + const adminId = req.user.id; + + // 验证转账ID + if (!transferId || isNaN(transferId)) { + return res.status(400).json({success: false, message: '无效的转账ID'}); + } + + const result = await transferService.removeBadDebt(transferId, adminId, reason); + + res.json({ + success: true, + message: '坏账标记已解除', + data: result + }); + } catch (error) { + console.error('解除坏账失败:', error); + if (error.statusCode) { + return res.status(error.statusCode).json({ + success: false, + message: error.message + }); + } + res.status(500).json({success: false, message: '解除坏账失败'}); + } +}); + +// 强制变更转账状态(管理员权限) +router.post('/force-change-status/:transferId', authenticateToken, async (req, res) => { + try { + // 检查管理员权限 + if (req.user.role !== 'admin') { + return res.status(403).json({success: false, message: '权限不足'}); + } + + const {transferId} = req.params; + const {newStatus, status, reason, adjust_balance = false} = req.body; + console.log('newStatus:', newStatus); + console.log('status:', status); + console.log('reason:', reason); + console.log('adjust_balance:', adjust_balance); + + // 兼容两种参数名:newStatus 和 status + const actualNewStatus = newStatus || status; + const adminId = req.user.id; + + // 验证转账ID + if (!transferId || isNaN(transferId)) { + return res.status(400).json({success: false, message: '无效的转账ID'}); + } + + // 验证必填参数 + if (!actualNewStatus) { + return res.status(400).json({success: false, message: '新状态不能为空'}); + } + + // if (!reason) { + // return res.status(400).json({ success: false, message: '变更原因不能为空' }); + // } + + const result = await transferService.forceChangeTransferStatus( + transferId, + actualNewStatus, + reason, + adminId, + adjust_balance + ); + + res.json({ + success: true, + message: `转账状态已从 ${result.oldStatus} 变更为 ${result.newStatus}`, + data: result + }); + } catch (error) { + console.error('强制变更转账状态失败:', error); + if (error.statusCode) { + return res.status(error.statusCode).json({ + success: false, + message: error.message + }); + } + res.status(500).json({success: false, message: '变更转账状态失败'}); + } +}); + +// 管理员查看数据库连接状态 +router.get('/admin/database/status', authenticateToken, async (req, res) => { + try { + // 检查管理员权限 + if (req.user.role !== 'admin') { + return res.status(403).json({success: false, message: '权限不足'}); + } + + const dbMonitor = require('../db-monitor'); + const diagnosis = await dbMonitor.diagnose(); + + res.json({ + success: true, + data: diagnosis + }); + } catch (error) { + logger.error('Get database status failed', { + adminId: req.user.id, + error: error.message + }); + res.status(500).json({ + success: false, + message: '获取数据库状态失败: ' + error.message + }); + } +}); + +// 管理员获取数据库监控报告 +router.get('/admin/database/report', authenticateToken, async (req, res) => { + try { + // 检查管理员权限 + if (req.user.role !== 'admin') { + return res.status(403).json({success: false, message: '权限不足'}); + } + + const dbMonitor = require('../db-monitor'); + const report = await dbMonitor.generateReport(); + + res.json({ + success: true, + data: { + report, + timestamp: new Date().toISOString() + } + }); + } catch (error) { + logger.error('Get database report failed', { + adminId: req.user.id, + error: error.message + }); + res.status(500).json({ + success: false, + message: '获取数据库报告失败: ' + error.message + }); + } +}); + +/** + * 获取待处理的匹配转账订单 + * @param {number} page - 页码 + * @param {number} limit - 每页数量 + * @param {string} status - 状态过滤 + * @param {string} search - 搜索关键词(用户名或真实姓名) + * @param {string} sort - 排序字段 + * @param {string} order - 排序方向(asc/desc) + */ +router.get('/pending-allocations', + authenticateToken, + async (req, res, next) => { + try { + // 检查管理员权限 + if (req.user.role !== 'admin') { + return res.status(403).json({success: false, message: '权限不足'}); + } + + const { + page = 1, + limit = 20, + status = '', + search = '', + sort = 'created_at', + order = 'desc' + } = req.query; + + const db = getDB(); + const offset = (parseInt(page) - 1) * parseInt(limit); + + // 构建查询条件 + let whereConditions = []; + let queryParams = []; + + // 状态过滤 + if (status) { + whereConditions.push('oa.status = ?'); + queryParams.push(status); + } + + // 搜索过滤(用户名或真实姓名) + if (search) { + whereConditions.push('(uf.username LIKE ? OR uf.real_name LIKE ? OR ut.username LIKE ? OR ut.real_name LIKE ?)'); + const searchPattern = `%${search}%`; + queryParams.push(searchPattern, searchPattern, searchPattern, searchPattern); + } + + const whereClause = whereConditions.length > 0 ? 'WHERE ' + whereConditions.join(' AND ') : ''; + + // 验证排序字段 + const allowedSortFields = ['created_at', 'amount', 'status', 'cycle_number']; + const sortField = allowedSortFields.includes(sort) ? sort : 'created_at'; + const sortOrder = order.toLowerCase() === 'asc' ? 'ASC' : 'DESC'; + + // 获取总数 + const countQuery = ` + SELECT COUNT(*) as total + FROM transfers oa + JOIN users uf ON oa.from_user_id = uf.id + JOIN users ut ON oa.to_user_id = ut.id + JOIN matching_orders mo ON oa.id = mo.id + ${whereClause} + `; + + const [countResult] = await db.execute(countQuery, queryParams); + const total = countResult[0].total; + + // 使用 query 方法避免 LIMIT/OFFSET 参数问题 + const dataQuery = ` + SELECT oa.id, + oa.from_user_id, + oa.to_user_id, + oa.amount, + oa.cycle_number, + oa.status, + oa.outbound_date, + oa.return_date, + oa.can_return_after, + oa.confirmed_at, + oa.created_at, + oa.updated_at, + uf.username as from_username, + uf.real_name as from_real_name, + ut.username as to_username, + ut.real_name as to_real_name, + mo.amount as order_total_amount, + mo.status as order_status, + mo.matching_type, + t.status as transfer_status, + t.voucher_url + FROM transfers oa + JOIN users uf ON oa.from_user_id = uf.id + JOIN users ut ON oa.to_user_id = ut.id + JOIN matching_orders mo ON oa.id = mo.id + ${whereClause} + ORDER BY oa.${sortField} ${sortOrder} + LIMIT ${parseInt(limit)} OFFSET ${parseInt(offset)} + `; + + const [allocations] = queryParams.length > 0 + ? await db.execute(dataQuery, queryParams) + : await db.query(dataQuery); + + // 计算分页信息 + const totalPages = Math.ceil(total / parseInt(limit)); + + logger.info('Pending allocations list requested', { + userId: req.user.id, + page: parseInt(page), + limit: parseInt(limit), + total, + resultCount: allocations.length + }); + + res.json({ + success: true, + data: { + allocations, + pagination: { + page: parseInt(page), + limit: parseInt(limit), + total, + totalPages + } + } + }); + } catch (error) { + logger.error('Get pending allocations failed', { + userId: req.user.id, + error: error.message + }); + next(error); + } + } +); + +/** + * 获取待处理匹配订单的统计信息 + */ +router.get('/pending-allocations/stats', + authenticateToken, + async (req, res, next) => { + try { + // 检查管理员权限 + if (req.user.role !== 'admin') { + return res.status(403).json({success: false, message: '权限不足'}); + } + + const db = getDB(); + + // 获取统计数据 + const [stats] = await db.execute(` + SELECT COUNT(*) as total_allocations, + COUNT(CASE WHEN oa.status = 'pending' THEN 1 END) as pending_count, + COUNT(CASE WHEN oa.status = 'confirmed' THEN 1 END) as confirmed_count, + COUNT(CASE WHEN oa.status = 'completed' THEN 1 END) as completed_count, + SUM(oa.amount) as total_amount, + SUM(CASE WHEN oa.status = 'pending' THEN oa.amount ELSE 0 END) as pending_amount + FROM transfers oa + JOIN matching_orders mo ON oa.id = mo.id + WHERE mo.status != 'cancelled' + `); + + res.json({ + success: true, + data: stats[0] + }); + } catch (error) { + logger.error('Get pending allocations stats failed', { + userId: req.user.id, + error: error.message + }); + next(error); + } + } +); + +/** + * 获取昨日用户转账统计列表 + */ +router.get('/daily-stats', + authenticateToken, + async (req, res, next) => { + try { + // 检查管理员权限 + if (req.user.role !== 'admin') { + return res.status(403).json({success: false, message: '权限不足'}); + } + + const db = getDB(); + + // 获取昨日和今日的日期范围(从0点开始计算) + // 今日0点到23:59:59 + const todayStart = dayjs().startOf('day'); + const todayEnd = dayjs().endOf('day'); + + // 昨日0点到23:59:59 + const yesterdayStart = dayjs().subtract(1, 'day').startOf('day'); + const yesterdayEnd = dayjs().subtract(1, 'day').endOf('day'); + + // 转换为MySQL兼容的字符串格式 + const todayStartStr = todayStart.format('YYYY-MM-DD HH:mm:ss'); + const todayEndStr = todayEnd.format('YYYY-MM-DD HH:mm:ss'); + const yesterdayStartStr = yesterdayStart.format('YYYY-MM-DD HH:mm:ss'); + const yesterdayEndStr = yesterdayEnd.format('YYYY-MM-DD HH:mm:ss'); + + // 使用dayjs格式化日期字符串用于返回 + const todayStr = todayStart.format('YYYY-MM-DD'); + const yesterdayStr = yesterdayStart.format('YYYY-MM-DD'); + + // 获取所有用户的昨日转出和今日入账统计 + let [userStats] = await db.execute(` + SELECT u.id as user_id, + u.username, + u.real_name, + u.phone, + u.balance, + COALESCE(yesterday_out.amount, 0) as yesterday_out_amount, + COALESCE(today_in.amount, 0) as today_in_amount, + COALESCE(confirmed_from.confirmed_amount, 0) as confirmed_from_amount, + CASE + WHEN (COALESCE(u.balance, 0) + COALESCE(confirmed_from.confirmed_amount, 0)) > ABS(u.balance) + THEN ABS(u.balance) + ELSE (COALESCE(u.balance, 0) + COALESCE(confirmed_from.confirmed_amount, 0)) + END as balance_needed + FROM users u + LEFT JOIN (SELECT from_user_id, + SUM(amount) as amount + FROM transfers + WHERE created_at >= ? + AND created_at <= ? + AND status IN ('confirmed', 'received') + GROUP BY from_user_id) yesterday_out ON u.id = yesterday_out.from_user_id + LEFT JOIN (SELECT to_user_id, + SUM(amount) as amount + FROM transfers + WHERE created_at >= ? + AND created_at <= ? + AND status IN ('confirmed', 'received') + GROUP BY to_user_id) today_in ON u.id = today_in.to_user_id + left join (select from_user_id, + sum(amount) as confirmed_amount + from transfers + where status = 'received' + and created_at >= ? + and created_at <= ? + group by from_user_id) as confirmed_from on u.id = confirmed_from.from_user_id + WHERE u.role != 'admin' + AND u.is_system_account != 1 + AND yesterday_out.amount > 0 + AND u.balance < 0 + ORDER BY balance_needed DESC, yesterday_out_amount DESC + `, [yesterdayStartStr, yesterdayEndStr, todayStartStr, todayEndStr, todayStartStr, todayEndStr]); + // userStats = userStats.filter(item=>item.balance_needed >= 100) + userStats.forEach(item => { + item.balance_needed = Math.abs(item.balance_needed) + }) + res.json({ + success: true, + data: { + date: { + yesterday: yesterdayStr, + today: todayStr + }, + users: userStats + } + }); + } catch (error) { + logger.error('Get daily transfer stats failed', { + userId: req.user.id, + error: error.message + }); + next(error); + } + } +); + +module.exports = router; \ No newline at end of file diff --git a/routes/upload.js b/routes/upload.js new file mode 100644 index 0000000..2222e0b --- /dev/null +++ b/routes/upload.js @@ -0,0 +1,421 @@ +const express = require('express'); +const multer = require('multer'); +const path = require('path'); +const { auth } = require('../middleware/auth'); +const { authenticateToken } = require('./auth'); +const minioService = require('../services/minioService'); +const { initializeBuckets } = require('../config/minio'); + +const router = express.Router(); + +// 初始化MinIO存储桶 +// initializeBuckets().catch(console.error); + +/** + * @swagger + * tags: + * name: Upload + * description: 文件上传API + */ + +// 配置multer内存存储(用于MinIO上传) +const storage = multer.memoryStorage(); + +// 文件过滤器 - 支持图片和视频 +const fileFilter = (req, file, cb) => { + // 允许图片和视频文件 + if (file.mimetype.startsWith('image/') || file.mimetype.startsWith('video/')) { + cb(null, true); + } else { + cb(new Error('只能上传图片或视频文件'), false); + } +}; + +// 单文件上传配置 +const upload = multer({ + storage: storage, + fileFilter: fileFilter, + limits: { + fileSize: 5 * 1024 * 1024, // 5MB + files: 1 // 一次只能上传一个文件 + } +}); + +// 多文件上传配置 +const multiUpload = multer({ + storage: storage, + fileFilter: fileFilter, + limits: { + fileSize: 10 * 1024 * 1024, // 10MB (视频文件更大) + files: 10 // 最多10个文件 + } +}); + +/** + * @swagger + * /upload/image: + * post: + * summary: 上传图片 + * tags: [Upload] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * multipart/form-data: + * schema: + * type: object + * properties: + * file: + * type: string + * format: binary + * description: 要上传的图片文件 + * type: + * type: string + * enum: [avatar, product, document] + * default: document + * description: 上传文件类型 + * responses: + * 200: + * description: 图片上传成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * url: + * type: string + * description: 上传后的文件URL + * filename: + * type: string + * description: 上传后的文件名 + * 400: + * description: 请求参数错误 + * 401: + * description: 未授权 + * 500: + * description: 服务器错误 + */ +router.post('/image', authenticateToken, (req, res) => { + upload.single('file')(req, res, async (err) => { + if (err instanceof multer.MulterError) { + if (err.code === 'LIMIT_FILE_SIZE') { + return res.status(400).json({ + success: false, + message: '文件大小不能超过 5MB' + }); + } + if (err.code === 'LIMIT_FILE_COUNT') { + return res.status(400).json({ + success: false, + message: '一次只能上传一个文件' + }); + } + return res.status(400).json({ + success: false, + message: '文件上传失败:' + err.message + }); + } else if (err) { + return res.status(400).json({ + success: false, + message: err.message + }); + } + + if (!req.file) { + return res.status(400).json({ + success: false, + message: '请选择要上传的文件' + }); + } + + try { + // 使用MinIO服务上传文件 + const type = req.body.type || 'document'; + const result = await minioService.uploadFile( + req.file.buffer, + req.file.originalname, + req.file.mimetype, + type + ); + + res.json({ + success: true, + message: '文件上传成功', + data: result.data + }); + } catch (error) { + console.error('文件上传到MinIO失败:', error); + res.status(500).json({ + success: false, + message: error.message || '文件上传失败' + }); + } + }); +}); + +/** + * @swagger + * /upload: + * post: + * summary: 多文件上传接口 (支持MediaUpload组件) + * tags: [Upload] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * multipart/form-data: + * schema: + * type: object + * properties: + * files: + * type: array + * items: + * type: string + * format: binary + * description: 要上传的文件列表 + * type: + * type: string + * enum: [avatar, product, document] + * default: document + * description: 上传文件类型 + * responses: + * 200: + * description: 文件上传成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: 文件上传成功 + * data: + * type: array + * items: + * type: object + * properties: + * filename: + * type: string + * originalname: + * type: string + * mimetype: + * type: string + * size: + * type: integer + * path: + * type: string + * url: + * type: string + * 400: + * description: 请求参数错误 + * 401: + * description: 未授权 + * 500: + * description: 服务器错误 + * @access Private + */ +router.post('/', authenticateToken, (req, res) => { + multiUpload.array('file', 10)(req, res, async (err) => { + if (err instanceof multer.MulterError) { + if (err.code === 'LIMIT_FILE_SIZE') { + return res.status(400).json({ + success: false, + message: '文件大小不能超过 10MB' + }); + } + if (err.code === 'LIMIT_FILE_COUNT') { + return res.status(400).json({ + success: false, + message: '一次最多只能上传10个文件' + }); + } + return res.status(400).json({ + success: false, + message: '文件上传失败:' + err.message + }); + } else if (err) { + return res.status(400).json({ + success: false, + message: err.message + }); + } + + if (!req.files || req.files.length === 0) { + return res.status(400).json({ + success: false, + message: '请选择要上传的文件' + }); + } + + try { + // 使用MinIO服务上传多个文件 + const type = req.body.type || 'document'; + const files = req.files.map(file => ({ + buffer: file.buffer, + originalName: file.originalname, + mimeType: file.mimetype + })); + + const result = await minioService.uploadMultipleFiles(files, type); + + // 如果只上传了一个文件,返回单文件格式以保持兼容性 + if (result.data.files.length === 1) { + result.data.files.forEach(element => { + element.path = '/' + element.path + }); + res.json({ + success: true, + message: '文件上传成功', + data: { + ...result.data.files[0], + urls: result.data.urls // 同时提供urls数组格式 + } + }); + } else { + // 多文件返回数组格式 + res.json({ + success: true, + message: `成功上传${result.data.files.length}个文件`, + data: result.data + }); + } + } catch (error) { + console.error('文件上传到MinIO失败:', error); + res.status(500).json({ + success: false, + message: error.message || '文件上传失败' + }); + } + }); +}); + +/** + * @swagger + * /upload/single: + * post: + * summary: 单文件上传接口(兼容性接口) + * tags: [Upload] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * multipart/form-data: + * schema: + * type: object + * properties: + * file: + * type: string + * format: binary + * description: 要上传的文件 + * type: + * type: string + * enum: [avatar, product, document] + * default: document + * description: 上传文件类型 + * responses: + * 200: + * description: 文件上传成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: 文件上传成功 + * url: + * type: string + * description: 上传后的文件URL + * filename: + * type: string + * description: 上传后的文件名 + * originalname: + * type: string + * description: 原始文件名 + * size: + * type: integer + * description: 文件大小 + * 400: + * description: 请求参数错误 + * 401: + * description: 未授权 + * 500: + * description: 服务器错误 + */ +router.post('/single', auth, (req, res) => { + upload.single('file')(req, res, async (err) => { + if (err instanceof multer.MulterError) { + return res.status(400).json({ + success: false, + message: '文件上传失败:' + err.message + }); + } else if (err) { + return res.status(400).json({ + success: false, + message: err.message + }); + } + + if (!req.file) { + return res.status(400).json({ success: false, message: '没有上传文件' }); + } + + try { + // 使用MinIO服务上传文件 + const type = req.body.type || 'document'; + const result = await minioService.uploadFile( + req.file.buffer, + req.file.originalname, + req.file.mimetype, + type + ); + + res.json({ + success: true, + message: '文件上传成功', + url: result.data.url, + filename: result.data.filename, + originalname: result.data.originalname, + size: result.data.size + }); + } catch (error) { + console.error('文件上传到MinIO失败:', error); + res.status(500).json({ + success: false, + message: error.message || '文件上传失败' + }); + } + }); +}); + +// 错误处理中间件 +router.use((error, req, res, next) => { + if (error instanceof multer.MulterError) { + if (error.code === 'LIMIT_FILE_SIZE') { + return res.status(400).json({ success: false, message: '文件大小不能超过10MB' }); + } + if (error.code === 'LIMIT_FILE_COUNT') { + return res.status(400).json({ success: false, message: '一次最多只能上传10个文件' }); + } + } + + if (error.message === '只能上传图片或视频文件') { + return res.status(400).json({ success: false, message: error.message }); + } + + res.status(500).json({ success: false, message: '上传失败' }); +}); + +module.exports = router; \ No newline at end of file diff --git a/routes/users.js b/routes/users.js new file mode 100644 index 0000000..5b0afbc --- /dev/null +++ b/routes/users.js @@ -0,0 +1,1816 @@ +const express = require('express'); +const bcrypt = require('bcryptjs'); +const {getDB} = require('../database'); +const {auth, adminAuth} = require('../middleware/auth'); +const dayjs = require('dayjs'); + +const router = express.Router(); + +/** + * @swagger + * tags: + * name: Users + * description: 用户管理API + */ + +router.post('/', auth, adminAuth, async (req, res) => { + try { + const db = getDB(); + await db.query('START TRANSACTION'); + + const { + username, + password, + role = 'user', + isSystemAccount = false, // 是否为虚拟商户 + realName, + idCard, + wechatQr, + alipayQr, + bankCard, + unionpayQr, + province, + city, + districtId, + phone, + avatar, + user_type = 'directly_operated', + inviter = null + } = req.body; + + if (!username || !password) { + return res.status(400).json({success: false, message: '用户名和密码不能为空'}); + } + + if (!realName || !idCard) { + return res.status(400).json({success: false, message: '姓名和身份证号不能为空'}); + } + if (!city || !districtId || !province) { + return res.status(400).json({success: false, message: '请选择城市和区县'}); + } + + // 验证身份证号格式 + const idCardRegex = /^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/; + if (!idCardRegex.test(idCard)) { + return res.status(400).json({success: false, message: '身份证号格式不正确'}); + } + + // 检查用户是否已存在 + const [existingUsers] = await db.execute( + 'SELECT id FROM users WHERE username = ? OR id_card = ? OR (phone IS NOT NULL AND phone = ?)', + [username, idCard, phone || null] + ); + + if (existingUsers.length > 0) { + return res.status(400).json({success: false, message: '用户名、身份证号或手机号已存在'}); + } + + // 加密密码 + const hashedPassword = await bcrypt.hash(password, 10); + + // 创建用户 + console.log([username, hashedPassword, role, isSystemAccount, 0, realName, idCard, wechatQr, alipayQr, bankCard, unionpayQr, phone, province, city, districtId, user_type, inviter], 'info'); + + const [result] = await db.execute( + 'INSERT INTO users (username, password, role, is_system_account, points, real_name, id_card, wechat_qr, alipay_qr, bank_card, unionpay_qr, phone, province, city, district_id, user_type, inviter,avatar) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,?,?)', + [username, hashedPassword, role, isSystemAccount, 0, realName, idCard, wechatQr, alipayQr, bankCard, unionpayQr, phone, province, city, districtId, user_type, inviter, avatar] + ); + + const userId = result.insertId; + if (user_type === 'agent_directly') { + const agentCode = 'AG' + Date.now().toString().slice(-8); + await db.execute( + 'INSERT INTO regional_agents (user_id, region_id,status,agent_code) VALUES (?, ?,?,?)', + [userId, districtId, 'active', agentCode] + ); + await db.execute( + `UPDATE users + SET payment_status='paid' + WHERE id = ${userId}` + ) + await db.execute(` + INSERT INTO distribution (user_id, amount, is_offline, type) + VALUES (${userId}, 2980, 1, 'system') + `) + } + + // 用户余额已在创建用户时设置为默认值0.00,无需额外操作 + + await db.query('COMMIT'); + + // 返回创建的用户信息(不包含密码) + const [newUser] = await db.execute( + 'SELECT id, username, role, avatar, points, real_name, phone, created_at, updated_at FROM users WHERE id = ?', + [userId] + ); + + res.status(201).json({ + success: true, + message: '用户创建成功', + user: newUser[0] + }); + } catch (error) { + try { + await getDB().query('ROLLBACK'); + } catch (rollbackError) { + console.error('回滚错误:', rollbackError); + } + console.error('创建用户错误:', error); + res.status(500).json({success: false, message: '创建用户失败'}); + } +}); + + +router.get('/pending-audit', auth, adminAuth, async (req, res) => { + try { + const db = getDB(); + const {page = 1, limit = 10} = req.query; + const pageNum = parseInt(page) || 1; + const limitNum = parseInt(limit) || 10; + const offset = (pageNum - 1) * limitNum; + + // 获取待审核用户总数 + const [countResult] = await db.execute( + 'SELECT COUNT(*) as total FROM users WHERE audit_status = ?', + ['pending'] + ); + const total = countResult[0].total; + + // 获取待审核用户列表 + const [users] = await db.execute( + `SELECT id, + username, + phone, + real_name, + business_license, + id_card_front, + id_card_back, + wechat_qr, + alipay_qr, + unionpay_qr, + bank_card, + audit_status, + created_at + FROM users + WHERE audit_status = ? + ORDER BY created_at ASC + LIMIT ${limitNum} OFFSET ${offset}`, + ['pending'] + ); + + res.json({ + success: true, + data: { + users, + pagination: { + page: pageNum, + limit: limitNum, + total, + pages: Math.ceil(total / limitNum) + } + } + }); + } catch (error) { + console.error('获取待审核用户列表错误:', error); + res.status(500).json({success: false, message: '获取待审核用户列表失败'}); + } +}); + +// 获取用户列表用于转账(普通用户权限) +router.get('/for-transfer', auth, async (req, res) => { + try { + const db = getDB(); + + // 获取所有用户的基本信息(用于转账选择) + const [users] = await db.execute( + 'SELECT id, username, real_name FROM users WHERE id != ? ORDER BY username', + [req.user.id] + ); + + res.json({ + success: true, + data: users + }); + } catch (error) { + console.error('获取转账用户列表错误:', error); + res.status(500).json({success: false, message: '获取用户列表失败'}); + } +}); + +// 获取用户列表(管理员权限) +router.get('/', auth, adminAuth, async (req, res) => { + try { + const db = getDB(); + const { + page = 1, + limit = 10, + search = '', + role = '', + city = '', + district = '', + province = '', + sort = 'created_at', + order = 'desc' + } = req.query; + + // 确保参数为有效数字 + const pageNum = Math.max(1, parseInt(page) || 1); + const limitNum = Math.max(1, Math.min(100, parseInt(limit) || 10)); + const offset = Math.max(0, (pageNum - 1) * limitNum); + + let whereConditions = []; + let countParams = []; + let listParams = []; + + // 构建查询条件 + if (search) { + whereConditions.push('(u.username LIKE ? OR u.real_name LIKE ?)'); + countParams.push(`%${search}%`, `%${search}%`); + listParams.push(`%${search}%`, `%${search}%`); + } + + if (role && role !== 'all') { + whereConditions.push('u.role = ?'); + countParams.push(role); + listParams.push(role); + } + + if (city) { + whereConditions.push('u.city = ?'); + countParams.push(city); + listParams.push(city); + } + + if (province) { + whereConditions.push('u.province = ?'); + countParams.push(province); + listParams.push(province); + } + + if (district) { + whereConditions.push('u.district_id = ?'); + countParams.push(district); + listParams.push(district); + } + + const whereClause = whereConditions.length > 0 ? `WHERE ${whereConditions.join(' AND ')}` : ''; + + // 添加分页参数 + listParams.push(limitNum.toString(), offset.toString()); + + + // 获取总数 + const [countResult] = await db.execute( + `SELECT COUNT(*) as total + FROM users u + LEFT JOIN zhejiang_regions r ON u.district_id = r.id + ${whereClause}`, + countParams + ); + + // 验证排序字段,防止SQL注入 + const validSortFields = ['id', 'username', 'role', 'points', 'balance', 'created_at', 'updated_at']; + const sortField = validSortFields.includes(sort) ? sort : 'created_at'; + + // 验证排序方向 + const sortOrder = (order && (order.toUpperCase() === 'ASC' || order.toUpperCase() === 'DESC')) + ? order.toUpperCase() + : 'DESC'; + + // 获取用户列表,关联地区信息和转账统计 + const [users] = await db.execute( + `SELECT u.id, + u.username, + u.role, + u.avatar, + u.points, + u.balance, + u.real_name, + u.id_card, + u.phone, + u.wechat_qr, + u.alipay_qr, + u.bank_card, + u.unionpay_qr, + u.audit_status, + u.is_system_account, + u.created_at, + u.updated_at, + u.province, + u.city, + u.district_id, + u.id_card_front, + u.id_card_back, + u.business_license, + u.is_distribute, + u.user_type, + u.inviter, + p.name as province_name, + c.name as city_name, + d.name as district_name, + COALESCE(yesterday_out.amount, 0) as yesterday_transfer_amount, + COALESCE(today_in.amount, 0) as today_received_amount + FROM users u + LEFT JOIN china_regions p ON u.province = p.code + LEFT JOIN china_regions c ON u.city = c.code + LEFT JOIN china_regions d ON u.district_id = d.code + LEFT JOIN (SELECT from_user_id, SUM(amount) as amount + FROM transfers + WHERE created_at >= DATE(DATE_SUB(NOW(), INTERVAL 1 DAY)) + AND created_at < DATE(NOW()) + AND status IN ('confirmed', 'received') + GROUP BY from_user_id) yesterday_out ON u.id = yesterday_out.from_user_id + LEFT JOIN (SELECT to_user_id, SUM(amount) as amount + FROM transfers + WHERE created_at >= DATE(NOW()) + AND created_at < DATE(DATE_ADD(NOW(), INTERVAL 1 DAY)) + AND status IN ('confirmed', 'received') + GROUP BY to_user_id) today_in ON u.id = today_in.to_user_id + ${whereClause} + ORDER BY u.${sortField} ${sortOrder} + LIMIT ${limitNum} OFFSET ${offset}`, + listParams.slice(0, -2) + ); + users.forEach(user => { + user.region = [user.province, user.city, user.district_id] + }) + + res.json({ + success: true, + users, + total: countResult[0].total, + page: pageNum, + limit: limitNum, + totalPages: Math.ceil(countResult[0].total / limitNum) + }); + } catch (error) { + console.error('获取用户列表错误:', error); + res.status(500).json({success: false, message: '获取用户列表失败'}); + } +}); + +// 获取当前用户的个人资料 +router.get('/profile', auth, async (req, res) => { + try { + const db = getDB(); + const userId = req.user.id; + + const [users] = await db.execute( + 'SELECT * FROM users WHERE id = ?', + [userId] + ); + const [distribution] = await db.execute(` + SELECT count(*) as total + FROM distribution + WHERE user_id = ?`, [userId]) + users[0].distribution = distribution[0].total > 0 ? true : false; + if (users.length === 0) { + return res.status(404).json({success: false, message: '用户不存在'}); + } + const profile = { + ...users[0], + nickname: users[0].username, // 添加nickname字段,映射到username + realName: users[0].real_name, + idCard: users[0].id_card, + wechatQr: users[0].wechat_qr, + alipayQr: users[0].alipay_qr, + bankCard: users[0].bank_card, + unionpayQr: users[0].unionpay_qr, + businessLicense: users[0].business_license, + idCardFront: users[0].id_card_front, + idCardBack: users[0].id_card_back + }; + res.json({success: true, user: profile}); + } catch (error) { + console.error('获取用户资料错误:', error); + res.status(500).json({success: false, message: '获取用户资料失败'}); + } +}); + +/** + * 获取当前用户的收款码状态 + * 用于检查用户是否已上传微信、支付宝、云闪付收款码 + */ +router.get('/payment-codes-status', auth, async (req, res) => { + try { + const db = getDB(); + const userId = req.user.id; + + const [users] = await db.execute( + 'SELECT wechat_qr, alipay_qr, unionpay_qr FROM users WHERE id = ?', + [userId] + ); + + if (users.length === 0) { + return res.status(404).json({success: false, message: '用户不存在'}); + } + + const paymentCodes = users[0]; + + res.json({ + success: true, + data: { + wechat_qr: paymentCodes.wechat_qr || '', + alipay_qr: paymentCodes.alipay_qr || '', + unionpay_qr: paymentCodes.unionpay_qr || '' + } + }); + } catch (error) { + console.error('获取收款码状态错误:', error); + res.status(500).json({success: false, message: '获取收款码状态失败'}); + } +}); + +// 获取用户收款信息 +router.get('/payment-info/:userId', auth, async (req, res) => { + try { + const db = getDB(); + const targetUserId = req.params.userId; + + const [users] = await db.execute( + 'SELECT id, username, wechat_qr, alipay_qr, unionpay_qr, bank_card FROM users WHERE id = ?', + [targetUserId] + ); + + if (users.length === 0) { + return res.status(404).json({success: false, message: '用户不存在'}); + } + + const user = users[0]; + res.json({ + success: true, + data: { + id: user.id, + username: user.username, + wechat_qr: user.wechat_qr, + alipay_qr: user.alipay_qr, + unionpay_qr: user.unionpay_qr, + bank_card: user.bank_card + } + }); + } catch (error) { + console.error('获取用户收款信息错误:', error); + res.status(500).json({success: false, message: '获取用户收款信息失败'}); + } +}); + +// 获取当前用户的统计信息 +router.get('/stats', auth, async (req, res) => { + try { + const db = getDB(); + const userId = req.user.id; + + // 如果是管理员,返回全局统计 + if (req.user.role === 'admin') { + // 总用户数 + const [totalUsers] = await db.execute('SELECT COUNT(*) as count FROM users'); + + // 直营数量 + const [directly_operated] = await db.execute('SELECT COUNT(*) as count FROM users WHERE user_type = "directly_operated"'); + // 代理数量 + const [agent] = await db.execute('SELECT COUNT(*) as count FROM users WHERE user_type = "agent"'); + // 直营代理数量 + const [agent_directly] = await db.execute('SELECT COUNT(*) as count FROM users WHERE user_type = "agent_directly"'); + // 普通用户数量 + const [regularUsers] = await db.execute('SELECT COUNT(*) as count FROM users WHERE user_type = "user"'); + + // 本月新增用户 + const [monthUsers] = await db.execute( + 'SELECT COUNT(*) as count FROM users WHERE YEAR(created_at) = YEAR(NOW()) AND MONTH(created_at) = MONTH(NOW())' + ); + + // 上月新增用户 + const [lastMonthUsers] = await db.execute( + 'SELECT COUNT(*) as count FROM users WHERE YEAR(created_at) = YEAR(DATE_SUB(NOW(), INTERVAL 1 MONTH)) AND MONTH(created_at) = MONTH(DATE_SUB(NOW(), INTERVAL 1 MONTH))' + ); + + // 计算月增长率 + const monthGrowthRate = lastMonthUsers[0].count > 0 + ? ((monthUsers[0].count - lastMonthUsers[0].count) / lastMonthUsers[0].count * 100).toFixed(2) + : 0; + + // 用户总积分 + const [totalPoints] = await db.execute('SELECT COALESCE(SUM(points), 0) as total FROM users'); + + // 今日新增用户 + const [todayUsers] = await db.execute( + 'SELECT COUNT(*) as count FROM users WHERE DATE(created_at) = CURDATE()' + ); + + // 昨日新增用户 + const [yesterdayUsers] = await db.execute( + 'SELECT COUNT(*) as count FROM users WHERE DATE(created_at) = DATE_SUB(CURDATE(), INTERVAL 1 DAY)' + ); + + // 活跃用户数(有订单的用户) + const [activeUsers] = await db.execute( + 'SELECT COUNT(DISTINCT from_user_id) as count FROM transfers' + ); + const [weekUsers] = await db.execute(` + SELECT COUNT(DISTINCT from_user_id) as count + FROM transfers + WHERE DATE(created_at) = DATE_SUB(CURDATE(), INTERVAL 7 DAY) + `) + res.json({ + success: true, + stats: { + totalUsers: totalUsers[0].count, + directly_operated: directly_operated[0].count, + agent: agent[0].count, + agent_directly: agent_directly[0].count, + regularUsers: regularUsers[0].count, + monthNewUsers: monthUsers[0].count, + todayUsers: todayUsers[0].count, + yesterdayUsers: yesterdayUsers[0].count, + monthlyGrowth: parseFloat(monthGrowthRate), + totalPoints: totalPoints[0].total, + activeUsers: activeUsers[0].count, + activeRate: (weekUsers[0].count / totalUsers[0].count * 100).toFixed(2) + } + }); + } else { + // 普通用户返回个人统计 + // 用户订单数 + const [orderCount] = await db.execute( + 'SELECT COUNT(*) as count FROM orders WHERE user_id = ?', + [userId] + ); + + // 用户总消费 + const [totalSpent] = await db.execute( + 'SELECT COALESCE(SUM(total_amount), 0) as total FROM orders WHERE user_id = ? AND status = "completed"', + [userId] + ); + + // 用户积分历史 + const [pointsEarned] = await db.execute( + 'SELECT COALESCE(SUM(amount), 0) as total FROM points_history WHERE user_id = ? AND type = "earn"', + [userId] + ); + + const [pointsSpent] = await db.execute( + 'SELECT COALESCE(SUM(amount), 0) as total FROM points_history WHERE user_id = ? AND type = "spend"', + [userId] + ); + + res.json({ + success: true, + stats: { + orderCount: orderCount[0].count, + totalSpent: totalSpent[0].total, + pointsEarned: pointsEarned[0].total, + pointsSpent: pointsSpent[0].total, + currentPoints: req.user.points || 0 + } + }); + } + } catch (error) { + console.error('获取用户统计错误:', error); + res.status(500).json({success: false, message: '获取用户统计失败'}); + } +}); + +// 获取用户统计信息(管理员权限)- 必须在/:id路由之前定义 +router.get('/admin/stats', auth, adminAuth, async (req, res) => { + try { + const db = getDB(); + // 总用户数 + const [totalUsers] = await db.execute('SELECT COUNT(*) as count FROM users'); + + // 管理员数量 + const [adminUsers] = await db.execute('SELECT COUNT(*) as count FROM users WHERE role = "admin"'); + + // 普通用户数量 + const [regularUsers] = await db.execute('SELECT COUNT(*) as count FROM users WHERE role = "user"'); + + // 本月新增用户 + const [monthUsers] = await db.execute( + 'SELECT COUNT(*) as count FROM users WHERE YEAR(created_at) = YEAR(NOW()) AND MONTH(created_at) = MONTH(NOW())' + ); + + // 上月新增用户 + const [lastMonthUsers] = await db.execute( + 'SELECT COUNT(*) as count FROM users WHERE YEAR(created_at) = YEAR(DATE_SUB(NOW(), INTERVAL 1 MONTH)) AND MONTH(created_at) = MONTH(DATE_SUB(NOW(), INTERVAL 1 MONTH))' + ); + + // 计算月增长率 + const monthGrowthRate = lastMonthUsers[0].count > 0 + ? ((monthUsers[0].count - lastMonthUsers[0].count) / lastMonthUsers[0].count * 100).toFixed(2) + : 0; + + // 用户总积分 + const [totalPoints] = await db.execute('SELECT COALESCE(SUM(points), 0) as total FROM users'); + + // 今日新增用户 + const [todayUsers] = await db.execute( + 'SELECT COUNT(*) as count FROM users WHERE DATE(created_at) = CURDATE()' + ); + + // 昨日新增用户 + const [yesterdayUsers] = await db.execute( + 'SELECT COUNT(*) as count FROM users WHERE DATE(created_at) = DATE_SUB(CURDATE(), INTERVAL 1 DAY)' + ); + + // 活跃用户数(有订单的用户) + const [activeUsers] = await db.execute( + 'SELECT COUNT(DISTINCT user_id) as count FROM orders' + ); + + res.json({ + success: true, + stats: { + totalUsers: totalUsers[0].count, + adminUsers: adminUsers[0].count, + regularUsers: regularUsers[0].count, + monthNewUsers: monthUsers[0].count, + todayUsers: todayUsers[0].count, + yesterdayUsers: yesterdayUsers[0].count, + monthlyGrowth: parseFloat(monthGrowthRate), + totalPoints: totalPoints[0].total, + activeUsers: activeUsers[0].count + } + }); + } catch (error) { + console.error('获取用户统计错误:', error); + res.status(500).json({success: false, message: '获取用户统计失败'}); + } +}); + +// 获取当前用户积分 +router.get('/points', auth, async (req, res) => { + try { + const userId = req.user.id; + + const [users] = await getDB().execute( + 'SELECT points FROM users WHERE id = ?', + [userId] + ); + + if (users.length === 0) { + return res.status(404).json({success: false, message: '用户不存在'}); + } + + res.json({ + success: true, + points: users[0].points + }); + } catch (error) { + console.error('获取用户积分错误:', error); + res.status(500).json({success: false, message: '获取用户积分失败'}); + } +}); + +// 获取用户积分历史记录 +router.get('/points/history', auth, async (req, res) => { + try { + const userId = req.user.id; + const {page = 1, limit = 20, type} = req.query; + + // 确保参数为有效数字 + const pageNum = Math.max(1, parseInt(page) || 1); + const limitNum = Math.max(1, Math.min(100, parseInt(limit) || 20)); + const offset = Math.max(0, (pageNum - 1) * limitNum); + + let whereClause = 'WHERE user_id = ?'; + let queryParams = [userId]; + + if (type && ['earn', 'spend'].includes(type)) { + whereClause += ' AND type = ?'; + queryParams.push(type); + } + + // 获取总数 + const [countResult] = await getDB().execute( + `SELECT COUNT(*) as total + FROM points_history ${whereClause}`, + queryParams + ); + + // 获取历史记录 + const [records] = await getDB().execute( + `SELECT id, type, amount, description, order_id, created_at + FROM points_history ${whereClause} + ORDER BY created_at DESC + LIMIT ${limitNum} OFFSET ${offset}`, + queryParams + ); + + res.json({ + success: true, + data: { + records, + pagination: { + page: pageNum, + limit: limitNum, + total: countResult[0].total, + totalPages: Math.ceil(countResult[0].total / limitNum) + } + } + }); + } catch (error) { + console.error('获取积分历史失败:', error); + res.status(500).json({success: false, message: '获取积分历史失败'}); + } +}); + +// 获取用户增长趋势数据(管理员权限) +router.get('/growth-trend', auth, adminAuth, async (req, res) => { + try { + const db = getDB(); + const {days = 7} = req.query; + const daysNum = Math.min(90, Math.max(1, parseInt(days) || 7)); + + // 获取指定天数内的用户注册趋势 + const [trendData] = await db.execute(` + SELECT DATE(created_at) as date, + COUNT(*) as count + FROM users + WHERE created_at >= DATE_SUB(NOW(), INTERVAL ? DAY) + GROUP BY DATE(created_at) + ORDER BY date ASC + `, [daysNum]); + + // 填充缺失的日期(注册数为0) + const result = []; + + for (let i = daysNum - 1; i >= 0; i--) { + const date = dayjs().subtract(i, 'day'); + const dateStr = date.format('YYYY-MM-DD'); + + // 修复日期比较:将数据库返回的Date对象转换为字符串进行比较 + const existingData = trendData.find(item => { + const itemDateStr = dayjs(item.date).format('YYYY-MM-DD'); + return itemDateStr === dateStr; + }); + + result.push({ + date: date.format('MM-DD'), + count: existingData ? existingData.count : 0 + }); + } + + res.json({ + success: true, + data: result + }); + } catch (error) { + console.error('获取用户增长趋势错误:', error); + res.status(500).json({success: false, message: '获取用户增长趋势失败'}); + } +}); + +// 获取日收入统计数据(管理员权限) +router.get('/daily-revenue', auth, adminAuth, async (req, res) => { + try { + const db = getDB(); + const {days = 30} = req.query; + const daysNum = Math.min(90, Math.max(1, parseInt(days) || 30)); + + // 获取指定天数内的用户注册数据,按天统计 + const [dailyData] = await db.execute(` + SELECT DATE(created_at) as date, + COUNT(*) as user_count + FROM users + WHERE created_at >= DATE_SUB(NOW(), INTERVAL ? DAY) + GROUP BY DATE(created_at) + ORDER BY date ASC + `, [daysNum]); + const [dailyDataTransfers] = await db.execute(` + SELECT DATE(created_at) as date, + SUM(CASE WHEN source_type IN ('system') THEN amount END) as amount + FROM transfers + WHERE created_at >= DATE_SUB(NOW(), INTERVAL ? DAY) + GROUP BY DATE(created_at) + ORDER BY date ASC + `, [daysNum]); + // 填充缺失的日期(注册数为0) + const result = []; + for (let i = daysNum - 1; i >= 0; i--) { + const date = dayjs().subtract(i, 'day'); + const dateStr = date.format('YYYY-MM-DD'); // YYYY-MM-DD格式 + const dateDisplay = date.format('M/D'); // 显示格式 + + const existingData = dailyData.find(item => { + const itemDateStr = dayjs(item.date).format('YYYY-MM-DD'); + return itemDateStr === dateStr; + }); + const existingDataTransfers = dailyDataTransfers.find(item => { + const itemDateStr = dayjs(item.date).format('YYYY-MM-DD'); + return itemDateStr === dateStr; + }); + const userCount = existingData ? existingData.user_count : 0; + const revenue = existingDataTransfers && existingDataTransfers.amount !== null ? existingDataTransfers.amount : 0; // 每个用户398元收入 + + result.push({ + date: dateDisplay, + userCount: userCount, + amount: revenue + }); + } + + res.json({ + success: true, + data: result + }); + } catch (error) { + console.error('获取日收入统计错误:', error); + res.status(500).json({success: false, message: '获取日收入统计失败'}); + } +}); +// 获取当前用户个人资料 +router.get('/profile', auth, async (req, res) => { + try { + const db = getDB(); + const userId = req.user.id; + + const [users] = await db.execute( + 'SELECT id, username, role, avatar, points, real_name, id_card, phone, wechat_qr, alipay_qr, bank_card, unionpay_qr, business_license, id_card_front, id_card_back, audit_status, created_at, updated_at FROM users WHERE id = ?', + [userId] + ); + + if (users.length === 0) { + return res.status(404).json({success: false, message: '用户不存在'}); + } + + // 转换字段名以匹配前端 + const user = users[0]; + const profile = { + ...user, + nickname: user.username, // 添加nickname字段,映射到username + realName: user.real_name, + idCard: user.id_card, + wechatQr: user.wechat_qr, + alipayQr: user.alipay_qr, + bankCard: user.bank_card, + unionpayQr: user.unionpay_qr, + businessLicense: user.business_license, + idCardFront: user.id_card_front, + idCardBack: user.id_card_back, + auditStatus: user.audit_status + }; + + res.json({success: true, user: profile}); + } catch (error) { + console.error('获取用户个人资料错误:', error); + res.status(500).json({success: false, message: '获取用户个人资料失败'}); + } +}); + +// 更新当前用户个人资料 +router.put('/profile', auth, async (req, res) => { + try { + const db = getDB(); + const userId = req.user.id; + const { + username, + nickname, + avatar, + realName, + idCard, + phone, + wechatQr, + alipayQr, + bankCard, + unionpayQr, + businessLicense, + idCardFront, + idCardBack, + city, + districtId + } = req.body; + + // 处理nickname字段,如果提供了nickname,则使用nickname作为username + const finalUsername = nickname || username; + + // 检查用户名、身份证号和手机号是否已被其他用户使用 + if (finalUsername || idCard || phone) { + const conditions = []; + const checkValues = []; + + if (finalUsername) { + conditions.push('username = ?'); + checkValues.push(finalUsername); + } + if (idCard) { + conditions.push('id_card = ?'); + checkValues.push(idCard); + } + if (phone) { + conditions.push('phone = ?'); + checkValues.push(phone); + } + + if (conditions.length > 0) { + const [existingUsers] = await db.execute( + `SELECT id + FROM users + WHERE (${conditions.join(' OR ')}) + AND id != ?`, + [...checkValues, userId] + ); + + if (existingUsers.length > 0) { + return res.status(400).json({success: false, message: '用户名、身份证号或手机号已被使用'}); + } + } + } + + // 验证身份证号格式 + if (idCard) { + const idCardRegex = /^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/; + if (!idCardRegex.test(idCard)) { + return res.status(400).json({success: false, message: '身份证号格式不正确'}); + } + } + + // 构建更新字段 + const updateFields = []; + const updateValues = []; + + if (finalUsername !== undefined) { + updateFields.push('username = ?'); + updateValues.push(finalUsername); + } + + if (avatar !== undefined) { + updateFields.push('avatar = ?'); + updateValues.push(avatar); + } + + if (realName !== undefined) { + updateFields.push('real_name = ?'); + updateValues.push(realName); + } + + if (idCard !== undefined) { + updateFields.push('id_card = ?'); + updateValues.push(idCard); + } + + if (phone !== undefined) { + updateFields.push('phone = ?'); + updateValues.push(phone); + } + + // 添加城市和地区字段更新 + if (city !== undefined) { + updateFields.push('city = ?'); + updateValues.push(city); + } + + if (districtId !== undefined) { + updateFields.push('district_id = ?'); + updateValues.push(districtId); + } + + // 检查是否更新了需要重新审核的关键信息 + let needsReaudit = false; + + if (wechatQr !== undefined) { + updateFields.push('wechat_qr = ?'); + updateValues.push(wechatQr); + needsReaudit = true; + } + + if (alipayQr !== undefined) { + updateFields.push('alipay_qr = ?'); + updateValues.push(alipayQr); + needsReaudit = true; + } + + if (bankCard !== undefined) { + updateFields.push('bank_card = ?'); + updateValues.push(bankCard); + needsReaudit = true; + } + + if (unionpayQr !== undefined) { + updateFields.push('unionpay_qr = ?'); + updateValues.push(unionpayQr); + needsReaudit = true; + } + + if (city !== undefined) { + updateFields.push('city = ?'); + updateValues.push(city); + } + + if (districtId !== undefined) { + updateFields.push('district_id = ?'); + updateValues.push(districtId); + } + + if (businessLicense !== undefined) { + updateFields.push('business_license = ?'); + updateValues.push(businessLicense); + needsReaudit = true; + } + + if (idCardFront !== undefined) { + updateFields.push('id_card_front = ?'); + updateValues.push(idCardFront); + needsReaudit = true; + } + + if (idCardBack !== undefined) { + updateFields.push('id_card_back = ?'); + updateValues.push(idCardBack); + needsReaudit = true; + } + + // 如果更新了关键信息且用户不是管理员,则重置审核状态为待审核 + if (needsReaudit && req.user.role !== 'admin') { + updateFields.push('audit_status = ?'); + updateValues.push('pending'); + } + + if (updateFields.length === 0) { + return res.status(400).json({success: false, message: '没有要更新的字段'}); + } + + updateValues.push(userId); + + await db.execute( + `UPDATE users + SET ${updateFields.join(', ')} + WHERE id = ?`, + updateValues + ); + + // 返回更新后的用户信息 + const [updatedUsers] = await db.execute( + 'SELECT id, username, role, avatar, points, real_name, id_card, phone, wechat_qr, alipay_qr, bank_card, unionpay_qr, business_license, id_card_front, id_card_back, audit_status, is_system_account, created_at, updated_at FROM users WHERE id = ?', + [userId] + ); + + // 转换字段名以匹配前端 + const user = updatedUsers[0]; + const profile = { + ...user, + nickname: user.username, // 添加nickname字段,映射到username + realName: user.real_name, + idCard: user.id_card, + wechatQr: user.wechat_qr, + alipayQr: user.alipay_qr, + bankCard: user.bank_card, + unionpayQr: user.unionpay_qr, + businessLicense: user.business_license, + idCardFront: user.id_card_front, + idCardBack: user.id_card_back, + auditStatus: user.audit_status + }; + + res.json({ + success: true, + message: '个人资料更新成功', + data: profile + }); + } catch (error) { + console.error('更新个人资料错误:', error); + res.status(500).json({success: false, message: '更新个人资料失败'}); + } +}); +// 获取用户详情 +router.get('/:id', auth, async (req, res) => { + try { + const db = getDB(); + const userId = req.params.id; + + // 只有管理员或用户本人可以查看详情 + if (req.user.role !== 'admin' && req.user.id != userId) { + return res.status(403).json({success: false, message: '权限不足'}); + } + + const [users] = await db.execute( + 'SELECT id, username, role, avatar, points, real_name, id_card, phone, wechat_qr, alipay_qr, bank_card, unionpay_qr, created_at, updated_at FROM users WHERE id = ?', + [userId] + ); + + if (users.length === 0) { + return res.status(404).json({success: false, message: '用户不存在'}); + } + + res.json({success: true, user: users[0]}); + } catch (error) { + console.error('获取用户详情错误:', error); + res.status(500).json({success: false, message: '获取用户详情失败'}); + } +}); + +// 更新用户信息 +router.put('/:id', auth, async (req, res) => { + try { + const db = getDB(); + const userId = req.params.id; + const { + username, + password, + role, + isSystemAccount, + avatar, + realName, + idCard, + phone, + wechatQr, + alipayQr, + bankCard, + unionpayQr, + idCardFront, + idCardBack, + businessLicense, + province, + city, + districtId, + user_type, + inviter, + } = req.body; + + // 只有管理员或用户本人可以更新信息 + if (req.user.role !== 'admin' && req.user.id != userId) { + return res.status(403).json({success: false, message: '权限不足'}); + } + + // 非管理员不能修改角色 + if (req.user.role !== 'admin' && role) { + return res.status(403).json({success: false, message: '无权限修改用户角色'}); + } + + // 检查用户名、身份证号和手机号是否已被其他用户使用 + if (username || idCard || phone) { + const conditions = []; + const checkValues = []; + + if (username) { + conditions.push('username = ?'); + checkValues.push(username); + } + if (idCard) { + conditions.push('id_card = ?'); + checkValues.push(idCard); + } + if (phone) { + conditions.push('phone = ?'); + checkValues.push(phone); + } + + if (conditions.length > 0) { + const [existingUsers] = await db.execute( + `SELECT id + FROM users + WHERE (${conditions.join(' OR ')}) + AND id != ?`, + [...checkValues, userId] + ); + + if (existingUsers.length > 0) { + return res.status(400).json({success: false, message: '用户名、身份证号或手机号已被使用'}); + } + } + } + + // 验证身份证号格式 + if (idCard) { + const idCardRegex = /^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/; + if (!idCardRegex.test(idCard)) { + return res.status(400).json({success: false, message: '身份证号格式不正确'}); + } + } + + // 构建更新字段 + const updateFields = []; + const updateValues = []; + + if (username) { + updateFields.push('username = ?'); + updateValues.push(username); + } + + // 处理密码更新 + if (password && password.trim() !== '') { + const hashedPassword = await bcrypt.hash(password, 10); + updateFields.push('password = ?'); + updateValues.push(hashedPassword); + } + + if (role && req.user.role === 'admin') { + updateFields.push('role = ?'); + updateValues.push(role); + } + + // 只有管理员可以修改账户类型 + if (isSystemAccount !== undefined && req.user.role === 'admin') { + updateFields.push('is_system_account = ?'); + updateValues.push(isSystemAccount); + } + + if (avatar !== undefined) { + updateFields.push('avatar = ?'); + updateValues.push(avatar); + } + + if (realName !== undefined) { + updateFields.push('real_name = ?'); + updateValues.push(realName); + } + + if (idCard !== undefined) { + updateFields.push('id_card = ?'); + updateValues.push(idCard); + } + + if (phone !== undefined) { + updateFields.push('phone = ?'); + updateValues.push(phone); + } + if (city !== undefined) { + updateFields.push('city = ?'); + updateValues.push(city); + } + + if (districtId !== undefined) { + updateFields.push('district_id = ?'); + updateValues.push(districtId); + } + // 检查是否更新了需要重新审核的关键信息 + let needsReaudit = false; + + if (wechatQr !== undefined) { + updateFields.push('wechat_qr = ?'); + updateValues.push(wechatQr); + needsReaudit = true; + } + + if (alipayQr !== undefined) { + updateFields.push('alipay_qr = ?'); + updateValues.push(alipayQr); + needsReaudit = true; + } + + if (bankCard !== undefined) { + updateFields.push('bank_card = ?'); + updateValues.push(bankCard); + needsReaudit = true; + } + + if (unionpayQr !== undefined) { + updateFields.push('unionpay_qr = ?'); + updateValues.push(unionpayQr); + needsReaudit = true; + } + if (idCardFront !== undefined) { + updateFields.push('id_card_front = ?'); + updateValues.push(idCardFront); + needsReaudit = true; + } + if (idCardBack !== undefined) { + updateFields.push('id_card_back = ?'); + updateValues.push(idCardBack); + needsReaudit = true; + } + if (province !== undefined) { + updateFields.push('province = ?'); + updateValues.push(province); + needsReaudit = true; + } + if (businessLicense !== undefined) { + updateFields.push('business_license = ?'); + updateValues.push(businessLicense); + needsReaudit = true; + } + if (user_type !== undefined) { + updateFields.push('user_type = ?'); + updateValues.push(user_type); + needsReaudit = true; + } + if (inviter !== undefined) { + updateFields.push('inviter = ?'); + updateValues.push(inviter); + needsReaudit = true; + } + + // 如果更新了关键信息且用户不是管理员,则重置审核状态为待审核 + if (needsReaudit && req.user.role !== 'admin') { + updateFields.push('audit_status = ?'); + updateValues.push('pending'); + } + + if (updateFields.length === 0) { + return res.status(400).json({success: false, message: '没有要更新的字段'}); + } + + updateValues.push(userId); + + await db.execute( + `UPDATE users + SET ${updateFields.join(', ')} + WHERE id = ?`, + updateValues + ); + + // 返回更新后的用户信息 + const [updatedUsers] = await db.execute( + 'SELECT id, username, role, avatar, points, real_name, id_card, phone, wechat_qr, alipay_qr, bank_card, unionpay_qr, city, district_id, province, created_at, updated_at FROM users WHERE id = ?', + [userId] + ); + + res.json({ + success: true, + message: '用户信息更新成功', + user: updatedUsers[0] + }); + } catch (error) { + console.error('更新用户信息错误:', error); + res.status(500).json({success: false, message: '更新用户信息失败'}); + } +}); + + +// 删除用户(管理员权限) +router.delete('/:id', auth, adminAuth, async (req, res) => { + try { + const db = getDB(); + const userId = req.params.id; + + // 不能删除自己 + if (req.user.id == userId) { + return res.status(400).json({success: false, message: '不能删除自己的账户'}); + } + + // 检查用户是否存在 + const [users] = await db.execute( + 'SELECT id FROM users WHERE id = ?', + [userId] + ); + + if (users.length === 0) { + return res.status(404).json({success: false, message: '用户不存在'}); + } + + // 删除用户 + await db.execute('DELETE FROM users WHERE id = ?', [userId]); + + res.json({success: true, message: '用户删除成功'}); + } catch (error) { + console.error('删除用户错误:', error); + res.status(500).json({success: false, message: '删除用户失败'}); + } +}); + +/** + * 审核用户(管理员权限) + */ +router.put('/:id/audit', auth, adminAuth, async (req, res) => { + try { + const db = getDB(); + const userId = req.params.id; + const {action, note} = req.body; // action: 'approve' 或 'reject' + + if (!action || !['approve', 'reject'].includes(action)) { + return res.status(400).json({success: false, message: '审核操作无效'}); + } + + // 检查用户是否存在且为待审核状态 + const [users] = await db.execute( + 'SELECT id, username, audit_status FROM users WHERE id = ?', + [userId] + ); + + if (users.length === 0) { + return res.status(404).json({success: false, message: '用户不存在'}); + } + + const user = users[0]; + if (user.audit_status !== 'pending') { + return res.status(400).json({success: false, message: '该用户不是待审核状态'}); + } + + // 更新审核状态 + const auditStatus = action === 'approve' ? 'approved' : 'rejected'; + await db.execute( + `UPDATE users + SET audit_status = ?, + audit_note = ?, + audited_by = ?, + audited_at = NOW() + WHERE id = ?`, + [auditStatus, note || null, req.user.id, userId] + ); + + const message = action === 'approve' ? '用户审核通过' : '用户审核拒绝'; + res.json({success: true, message}); + } catch (error) { + console.error('审核用户错误:', error); + res.status(500).json({success: false, message: '审核用户失败'}); + } +}); + +/** + * 获取用户审核详情(管理员权限) + */ +router.get('/:id/audit-detail', auth, adminAuth, async (req, res) => { + try { + const db = getDB(); + const userId = req.params.id; + + // 获取用户详细信息 + const [users] = await db.execute( + `SELECT u.id, + u.username, + u.phone, + u.real_name, + u.business_license, + u.id_card_front, + u.id_card_back, + u.audit_status, + u.audit_note, + u.audited_at, + u.created_at, + auditor.username as auditor_name + FROM users u + LEFT JOIN users auditor ON u.audited_by = auditor.id + WHERE u.id = ?`, + [userId] + ); + + if (users.length === 0) { + return res.status(404).json({success: false, message: '用户不存在'}); + } + + res.json({ + success: true, + data: users[0] + }); + } catch (error) { + console.error('获取用户审核详情错误:', error); + res.status(500).json({success: false, message: '获取用户审核详情失败'}); + } +}); + +router.put('/:id/distribute', auth, async (req, res) => { + try { + const db = getDB(); + const userId = req.params.id; + const {is_distribute} = req.body; + + if (typeof is_distribute !== 'boolean') { + return res.status(400).json({success: false, message: '分发状态无效'}); + } + + + // 检查用户是否存在 + const [users] = await db.execute( + 'SELECT id,user_type FROM users WHERE id = ?', + [userId] + ); + + if (users.length === 0) { + return res.status(404).json({success: false, message: '用户不存在'}); + } + if (users[0].user_type === 'directly_operated') { + return res.status(400).json({success: false, message: '直营用户不允许开启委托出售'}); + } + let [isServiceFee] = await db.execute('SELECT COUNT(*) AS total FROM distribution WHERE created_at >= DATE_SUB(NOW(), INTERVAL 1 YEAR) AND user_id = ?', [userId]); + if (isServiceFee[0].total > 0) { + // 更新分发状态 + await db.execute( + 'UPDATE users SET is_distribute = ? WHERE id = ?', + [is_distribute, userId] + ); + res.json({ + success: true, + message: '分发状态更新成功', + is_distribute + }); + } else { + return res.json({success: false, message: '请缴纳2980融豆服务费'}); + } + } catch (error) { + + } +}) +/** + * 扣除2980融豆服务费 + */ +router.post('/:id/deduct-service-fee', auth, async (req, res) => { + const db = getDB(); + try { + const userId = req.params.id; + const serviceFee = 2980; // 服务费金额 + + // 开始事务 + await db.query('START TRANSACTION'); + + // 使用行级锁定查询用户信息,只锁定当前用户记录 + const [users] = await db.execute( + 'SELECT id, balance, username,district_id FROM users WHERE id = ? FOR UPDATE', + [userId] + ); + + if (users.length === 0) { + await db.query('ROLLBACK'); + return res.status(404).json({success: false, message: '用户不存在'}); + } + //判断今年是否已扣款 + let [isServiceFee] = await db.execute('SELECT COUNT(*) AS total FROM distribution WHERE created_at >= DATE_SUB(NOW(), INTERVAL 1 YEAR) AND user_id = ?', [userId]); + if (isServiceFee[0].total > 0) { + return res.status(400).json({success: false, message: '已缴纳2980融豆服务费'}); + } + const user = users[0]; + const currentBalance = Math.abs(user.balance); // 将负数转为正数处理 + + // 检查融豆余额是否足够 + if (currentBalance < serviceFee) { + await db.query('ROLLBACK'); + return res.status(400).json({ + success: false, + message: `融豆余额不足,当前余额:${currentBalance},需要:${serviceFee}` + }); + } + + // 扣除融豆(balance字段为负数,所以减去服务费实际是增加负数) + await db.execute( + 'UPDATE users SET balance = balance + ? WHERE id = ?', + [serviceFee, userId] + ); + //查找上级分销 + let [distribute] = await db.execute( + 'SELECT inviter FROM users WHERE id = ?', + [userId] + ); + distribute = distribute[0] + //如果有上级分销 + if (distribute.inviter) { + // 查找上级分销 + let [distributeUser] = await db.execute( + 'SELECT id, balance,user_type,inviter FROM users WHERE id = ?', + [distribute.inviter] + ); + distributeUser = distributeUser[0] + if (distributeUser.user_type == 'agent') { + //给代理添加2980融豆的70% 增加区域保护 + await db.execute( + 'UPDATE users SET balance = balance - ? WHERE id = ?', + [serviceFee * 0.7, distributeUser.id] + ); + //记录转账记录 + await db.execute( + 'INSERT INTO transfers (from_user_id, to_user_id, transfer_type,status,amount,description,source_type) VALUES (?, ?, ?,?,?,?,?)', + [userId, distributeUser.id, 'user_to_agent', 'received', serviceFee * 0.7, '用户服务费返现', 'agent'] + ); + //记录平台利润 + await db.execute( + 'INSERT INTO transfers (from_user_id, to_user_id, transfer_type,status,amount,description,source_type) VALUES (?, ?, ?,?,?,?,?)', + [userId, 3512, 'user_to_system', 'received', serviceFee * 0.3, '用户服务费返现', 'system'] + ); + //记录服务费 + await db.execute( + 'INSERT INTO distribution (user_id,agent_id, amount, type) VALUES (?, ?, ?,?)', + [userId, distributeUser.id, serviceFee, 'agent'] + ) + } + //如果不是代理,查看是否是直营代理 + if (distributeUser.user_type == 'agent_directly') { + //给直营代理50%融豆给平台50%融豆 + await db.execute( + 'UPDATE users SET balance = balance - ? WHERE id = ?', + [serviceFee * 0.5, distributeUser.id] + ); + //记录转账记录 + await db.execute( + 'INSERT INTO transfers (from_user_id, to_user_id, transfer_type,status,amount,description,source_type) VALUES (?, ?, ?,?,?,?,?)', + [userId, distributeUser.id, 'user_to_agent', 'received', serviceFee * 0.5, '用户服务费返现', 'agent_operated'] + ); + //记录平台利润 + await db.execute( + 'INSERT INTO transfers (from_user_id, to_user_id, transfer_type,status,amount,description,source_type) VALUES (?, ?, ?,?,?,?,?)', + [userId, 3512, 'user_to_system', 'received', serviceFee * 0.5, '用户服务费返现', 'system'] + ); + //记录服务费 + await db.execute( + 'INSERT INTO distribution (user_id,agent_id, amount, type) VALUES (?, ?, ?,?)', + [userId, distributeUser.id, serviceFee, 'direct_agent'] + ) + } + //是否是直营 + if (distributeUser.user_type == 'directly_operated') { + //查询这个月直营做了多少单 + let [orderCount] = await db.execute( + `SELECT COUNT(*) AS total + FROM distribution + WHERE agent_id = ? + AND created_at >= DATE_FORMAT(NOW(), '%Y-%m-01')`, + [distributeUser.id] + ); + orderCount = orderCount[0] + if (orderCount.total <= 5) { + //给直营代理20%融豆给平台50% 融豆给用户30% + await db.execute( + 'UPDATE users SET balance = balance - ? WHERE id = ?', + [serviceFee * 0.2, distributeUser.inviter] + ); + //给直营添加30%融豆 + await db.execute( + 'UPDATE users SET balance = balance - ? WHERE id = ?', + [serviceFee * 0.3, distributeUser.id] + ); + //记录转账记录 + await db.execute( + 'INSERT INTO transfers (from_user_id, to_user_id, transfer_type,status,amount,description,source_type) VALUES (?, ?, ?,?,?,?,?)', + [userId, distributeUser.inviter, 'user_to_agent', 'received', serviceFee * 0.2, '用户服务费返现', 'operated_agent'] + ); + //记录直营利润 + await db.execute( + 'INSERT INTO transfers (from_user_id, to_user_id, transfer_type,status,amount,description,source_type) VALUES (?, ?, ?,?,?,?,?)', + [userId, distributeUser.id, 'user_to_operated', 'received', serviceFee * 0.3, '用户服务费返现', 'directly_operated'] + ); + //记录平台利润 + await db.execute( + 'INSERT INTO transfers (from_user_id, to_user_id, transfer_type,status,amount,description,source_type) VALUES (?, ?, ?,?,?,?,?)', + [userId, 3512, 'user_to_system', 'received', serviceFee * 0.5, '用户服务费返现', 'system'] + ); + } + if (orderCount.total > 5 && orderCount.total <= 15) { + //给直营代理20%融豆给平台50%融豆给用户30% + await db.execute( + 'UPDATE users SET balance = balance - ? WHERE id = ?', + [serviceFee * 0.15, distributeUser.inviter] + ); + //给直营添加30%融豆 + await db.execute( + 'UPDATE users SET balance = balance - ? WHERE id = ?', + [serviceFee * 0.35, distributeUser.id] + ); + //记录转账记录 + await db.execute( + 'INSERT INTO transfers (from_user_id, to_user_id, transfer_type,status,amount,description,source_type) VALUES (?, ?, ?,?,?,?,?)', + [userId, distributeUser.inviter, 'user_to_agent', 'received', serviceFee * 0.15, '用户服务费返现', 'agent_operated'] + ); + //记录直营利润 + await db.execute( + 'INSERT INTO transfers (from_user_id, to_user_id, transfer_type,status,amount,description,source_type) VALUES (?, ?, ?,?,?,?,?)', + [userId, distributeUser.id, 'user_to_operated', 'received', serviceFee * 0.35, '用户服务费返现', 'directly_operated'] + ); + //记录平台利润 + await db.execute( + 'INSERT INTO transfers (from_user_id, to_user_id, transfer_type,status,amount,description,source_type) VALUES (?, ?, ?,?,?,?,?)', + [userId, 3512, 'user_to_system', 'received', serviceFee * 0.5, '用户服务费返现', 'system'] + ); + } + if (orderCount.total > 15) { + //给直营代理20%融豆给平台50%融豆给用户30% + await db.execute( + 'UPDATE users SET balance = balance - ? WHERE id = ?', + [serviceFee * 0.1, distributeUser.inviter] + ); + //给直营添加30%融豆 + await db.execute( + 'UPDATE users SET balance = balance - ? WHERE id = ?', + [serviceFee * 0.4, distributeUser.id] + ); + //记录转账记录 + await db.execute( + 'INSERT INTO transfers (from_user_id, to_user_id, transfer_type,status,amount,description,source_type) VALUES (?, ?, ?,?,?,?,?)', + [userId, distributeUser.inviter, 'user_to_agent', 'received', serviceFee * 0.1, '用户服务费返现', 'agent_operated'] + ); + //记录直营利润 + await db.execute( + 'INSERT INTO transfers (from_user_id, to_user_id, transfer_type,status,amount,description,source_type) VALUES (?, ?, ?,?,?,?,?)', + [userId, distributeUser.id, 'user_to_operated', 'received', serviceFee * 0.4, '用户服务费返现', 'directly_operated'] + ); + //记录平台利润 + await db.execute( + 'INSERT INTO transfers (from_user_id, to_user_id, transfer_type,status,amount,description,source_type) VALUES (?, ?, ?,?,?,?,?)', + [userId, 3512, 'user_to_system', 'received', serviceFee * 0.5, '用户服务费返现', 'system'] + ); + } + //记录服务费 + await db.execute( + 'INSERT INTO distribution (user_id,agent_id, amount, type) VALUES (?, ?, ?,?)', + [userId, distributeUser.id, serviceFee, 'direct_agent'] + ) + } + //要是用户之间分销 + if (distributeUser.user_type == 'user') { + //查询用户是否有上级 + let [userUpInfo] = await db.execute( + `SELECT * + FROM users + WHERE id = ?`, + [distributeUser.inviter] + ) + userUpInfo = userUpInfo[0] + //判断用户上级是否是代理 + if (userUpInfo && userUpInfo.user_type === 'agent') { + //给用户分配 + await db.execute( + 'UPDATE users SET balance = balance - ? WHERE id = ?', + [serviceFee * 0.2, distributeUser.id] + ); + //给代理分配 + await db.execute( + 'UPDATE users SET balance = balance - ? WHERE id = ?', + [serviceFee * 0.5, userUpInfo.id] + ); + //记录用户转账信息 + await db.execute( + 'INSERT INTO transfers (from_user_id, to_user_id, transfer_type,status,amount,description,source_type) VALUES (?, ?, ?,?,?,?,?)', + [userId, distributeUser.id, 'user_to_user', 'received', serviceFee * 0.2, '用户服务费返现', 'operated'] + ); + //记录代理转账信息 + await db.execute( + 'INSERT INTO transfers (from_user_id, to_user_id, transfer_type,status,amount,description,source_type) VALUES (?, ?, ?,?,?,?,?)', + [userId, userUpInfo.id, 'user_to_agent', 'received', serviceFee * 0.5, '用户服务费返现', 'agent'] + ); + //记录平台利润 + await db.execute( + 'INSERT INTO transfers (from_user_id, to_user_id, transfer_type,status,amount,description,source_type) VALUES (?, ?, ?,?,?,?,?)', + [userId, 3512, 'user_to_system', 'received', serviceFee * 0.3, '用户服务费返现', 'system'] + ); + } else { + //用户没有上级 + await db.execute( + 'UPDATE users SET balance = balance - ? WHERE id = ?', + [serviceFee * 0.2, distributeUser.id] + ); + //记录转账记录 + await db.execute( + 'INSERT INTO transfers (from_user_id, to_user_id, transfer_type,status,amount,description,source_type) VALUES (?, ?, ?,?,?,?,?)', + [userId, distributeUser.id, 'user_to_user', 'received', serviceFee * 0.2, '用户服务费返现', 'operated'] + ); + //记录平台利润 + await db.execute( + 'INSERT INTO transfers (from_user_id, to_user_id, transfer_type,status,amount,description,source_type) VALUES (?, ?, ?,?,?,?,?)', + [userId, 3512, 'user_to_system', 'received', serviceFee * 0.8, '用户服务费返现', 'system'] + ); + + } + //记录服务费 + await db.execute( + 'INSERT INTO distribution (user_id,agent_id, amount, type) VALUES (?, ?, ?,?)', + [userId, distributeUser.id, serviceFee, 'user'] + ) + } + + } else { + //判断用户此区域是否有代理 + let [agentUser] = await db.execute(` + SELECT user_id + FROM regional_agents + WHERE region_id = ? + `, [users[0].district_id]) + if (agentUser.length > 0) { + //给代理分成5% + await db.execute( + 'INSERT INTO transfers (from_user_id, to_user_id, transfer_type,status,amount,description,source_type) VALUES (?, ?, ?,?,?,?,?)', + [userId, agentUser[0].user_id, 'user_to_regional', 'received', serviceFee * 0.05, '区域保护服务费返现', 'agent'] + ) + //记录平台利润 + await db.execute( + 'INSERT INTO transfers (from_user_id, to_user_id, transfer_type,status,amount,description,source_type) VALUES (?, ?, ?,?,?,?,?)', + [userId, 3512, 'user_to_system', 'received', serviceFee * 0.95, '用户服务费返现', 'system'] + ); + } else { + //记录平台利润 + await db.execute( + 'INSERT INTO transfers (from_user_id, to_user_id, transfer_type,status,amount,description,source_type) VALUES (?, ?, ?,?,?,?,?)', + [userId, 3512, 'user_to_system', 'received', serviceFee, '用户服务费返现', 'system'] + ); + + } + await db.execute( + 'INSERT INTO distribution (user_id,agent_id, amount, type) VALUES (?, ?, ?,?)', + [userId, 3512, serviceFee, 'user'] + ) + + } + await db.execute( + 'UPDATE users SET is_distribute = ? WHERE id = ?', + [true, userId] + ); + // 提交事务 + await db.query('COMMIT'); + + res.json({ + success: true, + message: '服务费扣除成功', + data: { + user_id: userId, + username: user.username, + deducted_amount: serviceFee, + remaining_balance: currentBalance - serviceFee + } + }); + + } catch (error) { + try { + // 发生错误时回滚事务 + await db.query('ROLLBACK'); + } catch (rollbackError) { + console.error('回滚失败:', rollbackError); + } + console.error('扣除服务费失败:', error); + res.status(500).json({success: false, message: '扣除服务费失败'}); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/routes/wechatPay.js b/routes/wechatPay.js new file mode 100644 index 0000000..149e337 --- /dev/null +++ b/routes/wechatPay.js @@ -0,0 +1,202 @@ +const express = require('express'); +const router = express.Router(); +const WechatPayService = require('../services/wechatPayService'); +const { getDB } = require('../database'); +const { auth, paymentAuth } = require('../middleware/auth'); + +// 创建微信支付服务实例 +const wechatPayService = new WechatPayService(); + +/** + * 创建注册支付订单 (API v3 - H5支付) + * POST /api/wechat-pay/create-registration-order + */ +router.post('/create-registration-order', paymentAuth, async (req, res) => { + try { + const userId = req.user.id; + const username = req.user.username; + const phone = req.user.phone; + + // 获取客户端IP + const clientIp = req.headers['x-forwarded-for'] || + req.headers['x-real-ip'] || + req.connection.remoteAddress || + req.socket.remoteAddress || + (req.connection.socket ? req.connection.socket.remoteAddress : null) || + '127.0.0.1'; + + // 检查用户是否已经支付过 + const db = getDB(); + const [existingOrders] = await db.execute( + 'SELECT id FROM payment_orders WHERE user_id = ? AND status = "paid"', + [userId] + ); + + if (existingOrders.length > 0) { + return res.status(400).json({ + success: false, + message: '用户已完成支付,无需重复支付' + }); + } + + console.log('创建H5支付订单:', { + userId, + username, + phone, + clientIp + }); + + // 创建H5支付订单 + const result = await wechatPayService.createRegistrationPayOrder({ + userId, + username, + phone, + clientIp + }); + + if (result.success) { + res.json({ + success: true, + data: { + outTradeNo: result.data.outTradeNo, + h5Url: result.data.h5Url, + paymentType: result.data.paymentType + } + }); + } else { + res.status(500).json({ + success: false, + message: '创建支付订单失败' + }); + } + } catch (error) { + console.error('创建H5支付订单异常:', error); + res.status(500).json({ + success: false, + message: error.message || '服务器内部错误' + }); + } +}); + +// H5支付不需要获取openid,移除相关接口 + +/** + * 微信支付回调接口 (API v3) + * POST /api/wechat-pay/notify + */ +router.post('/notify', async (req, res) => { + try { + // API v3 回调是JSON格式 + const notifyData = req.body; + + // 获取请求头中的签名信息 + const signature = req.headers['wechatpay-signature']; + const timestamp = req.headers['wechatpay-timestamp']; + const nonce = req.headers['wechatpay-nonce']; + const serial = req.headers['wechatpay-serial']; + + console.log('收到API v3支付回调:', { + signature, + timestamp, + nonce, + serial, + body: notifyData + }); + + // 验证签名和处理回调 + const result = await wechatPayService.handleV3PaymentNotify({ + signature, + timestamp, + nonce, + serial, + body: JSON.stringify(notifyData) + }); + + if (result.success) { + // API v3 成功响应 + res.status(200).json({ code: 'SUCCESS', message: '成功' }); + } else { + // API v3 失败响应 + res.status(400).json({ code: 'FAIL', message: result.message || '处理失败' }); + } + } catch (error) { + console.error('支付回调处理异常:', error); + res.status(500).json({ code: 'ERROR', message: '服务器内部错误' }); + } +}); + +/** + * 查询支付状态 + * GET /api/wechat-pay/query-status/:outTradeNo + */ +router.get('/query-status/:outTradeNo', paymentAuth, async (req, res) => { + try { + const { outTradeNo } = req.params; + const userId = req.user.id; + + // 验证订单是否属于当前用户 + const db = getDB(); + const [orders] = await db.execute( + 'SELECT id FROM payment_orders WHERE out_trade_no = ? AND user_id = ?', + [outTradeNo, userId] + ); + + if (orders.length === 0) { + return res.status(404).json({ + success: false, + message: '订单不存在或无权限访问' + }); + } + + const result = await wechatPayService.queryPaymentStatus(outTradeNo); + res.json(result); + } catch (error) { + console.error('查询支付状态失败:', error); + res.status(500).json({ + success: false, + message: error.message || '查询支付状态失败' + }); + } +}); + +/** + * 检查用户支付状态 + * GET /api/wechat-pay/check-user-payment + */ +router.get('/check-user-payment', auth, async (req, res) => { + try { + const userId = req.user.id; + const db = getDB(); + + // 查询用户支付状态 + const [users] = await db.execute( + 'SELECT payment_status FROM users WHERE id = ?', + [userId] + ); + + if (users.length === 0) { + return res.status(404).json({ + success: false, + message: '用户不存在' + }); + } + + const paymentStatus = users[0].payment_status; + + res.json({ + success: true, + data: { + paymentStatus, + isPaid: paymentStatus === 'paid' + } + }); + } catch (error) { + console.error('检查用户支付状态失败:', error); + res.status(500).json({ + success: false, + message: '检查支付状态失败' + }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/scripts/README_MERGE_TABLES.md b/scripts/README_MERGE_TABLES.md new file mode 100644 index 0000000..4b5a980 --- /dev/null +++ b/scripts/README_MERGE_TABLES.md @@ -0,0 +1,56 @@ +# 表合并说明文档 + +## 概述 + +本文档说明如何使用 `merge_tables.js` 脚本将 `order_allocations` 表数据合并到 `transfers` 表中,以实现数据库结构优化。 + +## 背景 + +原系统中,`order_allocations` 表和 `transfers` 表存在功能重叠,为了简化数据库结构和提高查询效率,我们决定将 `order_allocations` 表的数据合并到 `transfers` 表中,并通过 `source_type` 字段区分不同来源的转账记录。 + +## 合并策略 + +1. 为 `transfers` 表添加必要的字段,包括 `source_type`、`matching_order_id`、`cycle_number` 等 +2. 将 `order_allocations` 表中的数据迁移到 `transfers` 表 +3. 更新相关的外键引用 + +## 使用方法 + +### 前置条件 + +1. 确保已对数据库进行备份 +2. 确保系统处于维护状态,没有用户正在使用 + +### 执行步骤 + +1. 进入项目根目录 +2. 执行以下命令运行合并脚本: + +```bash +node scripts/merge_tables.js +``` + +3. 脚本执行完成后,检查控制台输出,确认迁移是否成功 + +### 验证步骤 + +1. 检查 `transfers` 表中是否包含所有 `order_allocations` 表的数据 +2. 验证系统功能是否正常,特别是与匹配订单相关的功能 +3. 确认无误后,可以考虑删除 `order_allocations` 表(可选) + +## 回滚方案 + +如果合并过程中出现问题,或者合并后系统功能异常,可以通过以下步骤回滚: + +1. 使用之前的数据库备份恢复数据 +2. 如果没有备份,可以手动将 `transfers` 表中 `source_type='allocation'` 的记录删除,并重新运行原有的系统 + +## 注意事项 + +1. 合并过程会自动处理已有关联的记录,避免重复迁移 +2. 合并脚本使用事务进行操作,确保数据一致性 +3. 建议在测试环境验证成功后再在生产环境执行 + +## 技术支持 + +如有问题,请联系技术支持团队。 \ No newline at end of file diff --git a/scripts/import_china_regions.js b/scripts/import_china_regions.js new file mode 100644 index 0000000..99af78d --- /dev/null +++ b/scripts/import_china_regions.js @@ -0,0 +1,144 @@ +const fs = require('fs'); +const path = require('path'); +const mysql = require('mysql2/promise'); +require('dotenv').config(); + +// 数据库配置 +const dbConfig = { + host: process.env.DB_HOST || '114.55.111.44', + user: process.env.DB_USER || 'maov2', + password: process.env.DB_PASSWORD || '5fYhw8z6T62b7heS', + database: process.env.DB_NAME || 'maov2', +}; + +// 初始化数据库连接 +async function initDB() { + try { + const connection = await mysql.createConnection(dbConfig); + console.log('数据库连接成功'); + return connection; + } catch (error) { + console.error('数据库连接失败:', error); + throw error; + } +} + +// 递归解析省市区数据 +function parseRegionData(regions, parentCode = null, level = 1) { + const result = []; + let sortOrder = 1; + + for (const region of regions) { + // 添加当前区域 + result.push({ + code: region.code, + name: region.name, + parent_code: parentCode, + level: level, + sort_order: sortOrder++ + }); + + // 递归处理子区域 + if (region.children && region.children.length > 0) { + const childrenData = parseRegionData(region.children, region.code, level + 1); + result.push(...childrenData); + } + } + + return result; +} + +// 导入省市区数据 +async function importChinaRegions() { + let connection; + + try { + // 读取 JSON 数据文件 + const jsonFilePath = path.join(__dirname, 'pca-code.json'); + const jsonData = fs.readFileSync(jsonFilePath, 'utf8'); + const regionsData = JSON.parse(jsonData); + + console.log('成功读取省市区数据文件'); + + // 解析数据 + const parsedData = parseRegionData(regionsData); + console.log(`解析完成,共 ${parsedData.length} 条记录`); + + // 连接数据库 + connection = await initDB(); + + // 清空现有数据 + await connection.execute('DELETE FROM china_regions'); + console.log('已清空现有数据'); + + // 批量插入数据 + const batchSize = 100; + let insertedCount = 0; + + for (let i = 0; i < parsedData.length; i += batchSize) { + const batch = parsedData.slice(i, i + batchSize); + const values = batch.map(item => [ + item.code, + item.name, + item.parent_code, + item.level, + item.sort_order + ]); + + const placeholders = values.map(() => '(?, ?, ?, ?, ?)').join(', '); + const flatValues = values.flat(); + + await connection.execute( + `INSERT INTO china_regions (code, name, parent_code, level, sort_order) VALUES ${placeholders}`, + flatValues + ); + + insertedCount += batch.length; + console.log(`已插入 ${insertedCount}/${parsedData.length} 条记录`); + } + + // 统计导入结果 + const [provinceResult] = await connection.execute( + 'SELECT COUNT(*) as count FROM china_regions WHERE level = 1' + ); + const [cityResult] = await connection.execute( + 'SELECT COUNT(*) as count FROM china_regions WHERE level = 2' + ); + const [districtResult] = await connection.execute( + 'SELECT COUNT(*) as count FROM china_regions WHERE level = 3' + ); + const [totalResult] = await connection.execute( + 'SELECT COUNT(*) as count FROM china_regions' + ); + + console.log('\n=== 导入完成 ==='); + console.log(`省份数量: ${provinceResult[0].count}`); + console.log(`城市数量: ${cityResult[0].count}`); + console.log(`区县数量: ${districtResult[0].count}`); + console.log(`总记录数: ${totalResult[0].count}`); + + } catch (error) { + console.error('导入失败:', error); + throw error; + } finally { + if (connection) { + await connection.end(); + console.log('数据库连接已关闭'); + } + } +} + +// 如果直接运行此脚本 +if (require.main === module) { + importChinaRegions() + .then(() => { + console.log('省市区数据导入成功!'); + process.exit(0); + }) + .catch((error) => { + console.error('导入过程中发生错误:', error); + process.exit(1); + }); +} + +module.exports = { importChinaRegions }; \ No newline at end of file diff --git a/scripts/pca-code.json b/scripts/pca-code.json new file mode 100644 index 0000000..aaca902 --- /dev/null +++ b/scripts/pca-code.json @@ -0,0 +1,14625 @@ +[ + { + "code": "11", + "name": "北京市", + "children": [ + { + "code": "1101", + "name": "市辖区", + "children": [ + { + "code": "110101", + "name": "东城区" + }, + { + "code": "110102", + "name": "西城区" + }, + { + "code": "110105", + "name": "朝阳区" + }, + { + "code": "110106", + "name": "丰台区" + }, + { + "code": "110107", + "name": "石景山区" + }, + { + "code": "110108", + "name": "海淀区" + }, + { + "code": "110109", + "name": "门头沟区" + }, + { + "code": "110111", + "name": "房山区" + }, + { + "code": "110112", + "name": "通州区" + }, + { + "code": "110113", + "name": "顺义区" + }, + { + "code": "110114", + "name": "昌平区" + }, + { + "code": "110115", + "name": "大兴区" + }, + { + "code": "110116", + "name": "怀柔区" + }, + { + "code": "110117", + "name": "平谷区" + }, + { + "code": "110118", + "name": "密云区" + }, + { + "code": "110119", + "name": "延庆区" + } + ] + } + ] + }, + { + "code": "12", + "name": "天津市", + "children": [ + { + "code": "1201", + "name": "市辖区", + "children": [ + { + "code": "120101", + "name": "和平区" + }, + { + "code": "120102", + "name": "河东区" + }, + { + "code": "120103", + "name": "河西区" + }, + { + "code": "120104", + "name": "南开区" + }, + { + "code": "120105", + "name": "河北区" + }, + { + "code": "120106", + "name": "红桥区" + }, + { + "code": "120110", + "name": "东丽区" + }, + { + "code": "120111", + "name": "西青区" + }, + { + "code": "120112", + "name": "津南区" + }, + { + "code": "120113", + "name": "北辰区" + }, + { + "code": "120114", + "name": "武清区" + }, + { + "code": "120115", + "name": "宝坻区" + }, + { + "code": "120116", + "name": "滨海新区" + }, + { + "code": "120117", + "name": "宁河区" + }, + { + "code": "120118", + "name": "静海区" + }, + { + "code": "120119", + "name": "蓟州区" + } + ] + } + ] + }, + { + "code": "13", + "name": "河北省", + "children": [ + { + "code": "1301", + "name": "石家庄市", + "children": [ + { + "code": "130102", + "name": "长安区" + }, + { + "code": "130104", + "name": "桥西区" + }, + { + "code": "130105", + "name": "新华区" + }, + { + "code": "130107", + "name": "井陉矿区" + }, + { + "code": "130108", + "name": "裕华区" + }, + { + "code": "130109", + "name": "藁城区" + }, + { + "code": "130110", + "name": "鹿泉区" + }, + { + "code": "130111", + "name": "栾城区" + }, + { + "code": "130121", + "name": "井陉县" + }, + { + "code": "130123", + "name": "正定县" + }, + { + "code": "130125", + "name": "行唐县" + }, + { + "code": "130126", + "name": "灵寿县" + }, + { + "code": "130127", + "name": "高邑县" + }, + { + "code": "130128", + "name": "深泽县" + }, + { + "code": "130129", + "name": "赞皇县" + }, + { + "code": "130130", + "name": "无极县" + }, + { + "code": "130131", + "name": "平山县" + }, + { + "code": "130132", + "name": "元氏县" + }, + { + "code": "130133", + "name": "赵县" + }, + { + "code": "130171", + "name": "石家庄高新技术产业开发区" + }, + { + "code": "130172", + "name": "石家庄循环化工园区" + }, + { + "code": "130181", + "name": "辛集市" + }, + { + "code": "130183", + "name": "晋州市" + }, + { + "code": "130184", + "name": "新乐市" + } + ] + }, + { + "code": "1302", + "name": "唐山市", + "children": [ + { + "code": "130202", + "name": "路南区" + }, + { + "code": "130203", + "name": "路北区" + }, + { + "code": "130204", + "name": "古冶区" + }, + { + "code": "130205", + "name": "开平区" + }, + { + "code": "130207", + "name": "丰南区" + }, + { + "code": "130208", + "name": "丰润区" + }, + { + "code": "130209", + "name": "曹妃甸区" + }, + { + "code": "130224", + "name": "滦南县" + }, + { + "code": "130225", + "name": "乐亭县" + }, + { + "code": "130227", + "name": "迁西县" + }, + { + "code": "130229", + "name": "玉田县" + }, + { + "code": "130271", + "name": "河北唐山芦台经济开发区" + }, + { + "code": "130272", + "name": "唐山市汉沽管理区" + }, + { + "code": "130273", + "name": "唐山高新技术产业开发区" + }, + { + "code": "130274", + "name": "河北唐山海港经济开发区" + }, + { + "code": "130281", + "name": "遵化市" + }, + { + "code": "130283", + "name": "迁安市" + }, + { + "code": "130284", + "name": "滦州市" + } + ] + }, + { + "code": "1303", + "name": "秦皇岛市", + "children": [ + { + "code": "130302", + "name": "海港区" + }, + { + "code": "130303", + "name": "山海关区" + }, + { + "code": "130304", + "name": "北戴河区" + }, + { + "code": "130306", + "name": "抚宁区" + }, + { + "code": "130321", + "name": "青龙满族自治县" + }, + { + "code": "130322", + "name": "昌黎县" + }, + { + "code": "130324", + "name": "卢龙县" + }, + { + "code": "130371", + "name": "秦皇岛市经济技术开发区" + }, + { + "code": "130372", + "name": "北戴河新区" + } + ] + }, + { + "code": "1304", + "name": "邯郸市", + "children": [ + { + "code": "130402", + "name": "邯山区" + }, + { + "code": "130403", + "name": "丛台区" + }, + { + "code": "130404", + "name": "复兴区" + }, + { + "code": "130406", + "name": "峰峰矿区" + }, + { + "code": "130407", + "name": "肥乡区" + }, + { + "code": "130408", + "name": "永年区" + }, + { + "code": "130423", + "name": "临漳县" + }, + { + "code": "130424", + "name": "成安县" + }, + { + "code": "130425", + "name": "大名县" + }, + { + "code": "130426", + "name": "涉县" + }, + { + "code": "130427", + "name": "磁县" + }, + { + "code": "130430", + "name": "邱县" + }, + { + "code": "130431", + "name": "鸡泽县" + }, + { + "code": "130432", + "name": "广平县" + }, + { + "code": "130433", + "name": "馆陶县" + }, + { + "code": "130434", + "name": "魏县" + }, + { + "code": "130435", + "name": "曲周县" + }, + { + "code": "130471", + "name": "邯郸经济技术开发区" + }, + { + "code": "130473", + "name": "邯郸冀南新区" + }, + { + "code": "130481", + "name": "武安市" + } + ] + }, + { + "code": "1305", + "name": "邢台市", + "children": [ + { + "code": "130502", + "name": "襄都区" + }, + { + "code": "130503", + "name": "信都区" + }, + { + "code": "130505", + "name": "任泽区" + }, + { + "code": "130506", + "name": "南和区" + }, + { + "code": "130522", + "name": "临城县" + }, + { + "code": "130523", + "name": "内丘县" + }, + { + "code": "130524", + "name": "柏乡县" + }, + { + "code": "130525", + "name": "隆尧县" + }, + { + "code": "130528", + "name": "宁晋县" + }, + { + "code": "130529", + "name": "巨鹿县" + }, + { + "code": "130530", + "name": "新河县" + }, + { + "code": "130531", + "name": "广宗县" + }, + { + "code": "130532", + "name": "平乡县" + }, + { + "code": "130533", + "name": "威县" + }, + { + "code": "130534", + "name": "清河县" + }, + { + "code": "130535", + "name": "临西县" + }, + { + "code": "130571", + "name": "河北邢台经济开发区" + }, + { + "code": "130581", + "name": "南宫市" + }, + { + "code": "130582", + "name": "沙河市" + } + ] + }, + { + "code": "1306", + "name": "保定市", + "children": [ + { + "code": "130602", + "name": "竞秀区" + }, + { + "code": "130606", + "name": "莲池区" + }, + { + "code": "130607", + "name": "满城区" + }, + { + "code": "130608", + "name": "清苑区" + }, + { + "code": "130609", + "name": "徐水区" + }, + { + "code": "130623", + "name": "涞水县" + }, + { + "code": "130624", + "name": "阜平县" + }, + { + "code": "130626", + "name": "定兴县" + }, + { + "code": "130627", + "name": "唐县" + }, + { + "code": "130628", + "name": "高阳县" + }, + { + "code": "130629", + "name": "容城县" + }, + { + "code": "130630", + "name": "涞源县" + }, + { + "code": "130631", + "name": "望都县" + }, + { + "code": "130632", + "name": "安新县" + }, + { + "code": "130633", + "name": "易县" + }, + { + "code": "130634", + "name": "曲阳县" + }, + { + "code": "130635", + "name": "蠡县" + }, + { + "code": "130636", + "name": "顺平县" + }, + { + "code": "130637", + "name": "博野县" + }, + { + "code": "130638", + "name": "雄县" + }, + { + "code": "130671", + "name": "保定高新技术产业开发区" + }, + { + "code": "130672", + "name": "保定白沟新城" + }, + { + "code": "130681", + "name": "涿州市" + }, + { + "code": "130682", + "name": "定州市" + }, + { + "code": "130683", + "name": "安国市" + }, + { + "code": "130684", + "name": "高碑店市" + } + ] + }, + { + "code": "1307", + "name": "张家口市", + "children": [ + { + "code": "130702", + "name": "桥东区" + }, + { + "code": "130703", + "name": "桥西区" + }, + { + "code": "130705", + "name": "宣化区" + }, + { + "code": "130706", + "name": "下花园区" + }, + { + "code": "130708", + "name": "万全区" + }, + { + "code": "130709", + "name": "崇礼区" + }, + { + "code": "130722", + "name": "张北县" + }, + { + "code": "130723", + "name": "康保县" + }, + { + "code": "130724", + "name": "沽源县" + }, + { + "code": "130725", + "name": "尚义县" + }, + { + "code": "130726", + "name": "蔚县" + }, + { + "code": "130727", + "name": "阳原县" + }, + { + "code": "130728", + "name": "怀安县" + }, + { + "code": "130730", + "name": "怀来县" + }, + { + "code": "130731", + "name": "涿鹿县" + }, + { + "code": "130732", + "name": "赤城县" + }, + { + "code": "130771", + "name": "张家口经济开发区" + }, + { + "code": "130772", + "name": "张家口市察北管理区" + }, + { + "code": "130773", + "name": "张家口市塞北管理区" + } + ] + }, + { + "code": "1308", + "name": "承德市", + "children": [ + { + "code": "130802", + "name": "双桥区" + }, + { + "code": "130803", + "name": "双滦区" + }, + { + "code": "130804", + "name": "鹰手营子矿区" + }, + { + "code": "130821", + "name": "承德县" + }, + { + "code": "130822", + "name": "兴隆县" + }, + { + "code": "130824", + "name": "滦平县" + }, + { + "code": "130825", + "name": "隆化县" + }, + { + "code": "130826", + "name": "丰宁满族自治县" + }, + { + "code": "130827", + "name": "宽城满族自治县" + }, + { + "code": "130828", + "name": "围场满族蒙古族自治县" + }, + { + "code": "130871", + "name": "承德高新技术产业开发区" + }, + { + "code": "130881", + "name": "平泉市" + } + ] + }, + { + "code": "1309", + "name": "沧州市", + "children": [ + { + "code": "130902", + "name": "新华区" + }, + { + "code": "130903", + "name": "运河区" + }, + { + "code": "130921", + "name": "沧县" + }, + { + "code": "130922", + "name": "青县" + }, + { + "code": "130923", + "name": "东光县" + }, + { + "code": "130924", + "name": "海兴县" + }, + { + "code": "130925", + "name": "盐山县" + }, + { + "code": "130926", + "name": "肃宁县" + }, + { + "code": "130927", + "name": "南皮县" + }, + { + "code": "130928", + "name": "吴桥县" + }, + { + "code": "130929", + "name": "献县" + }, + { + "code": "130930", + "name": "孟村回族自治县" + }, + { + "code": "130971", + "name": "河北沧州经济开发区" + }, + { + "code": "130972", + "name": "沧州高新技术产业开发区" + }, + { + "code": "130973", + "name": "沧州渤海新区" + }, + { + "code": "130981", + "name": "泊头市" + }, + { + "code": "130982", + "name": "任丘市" + }, + { + "code": "130983", + "name": "黄骅市" + }, + { + "code": "130984", + "name": "河间市" + } + ] + }, + { + "code": "1310", + "name": "廊坊市", + "children": [ + { + "code": "131002", + "name": "安次区" + }, + { + "code": "131003", + "name": "广阳区" + }, + { + "code": "131022", + "name": "固安县" + }, + { + "code": "131023", + "name": "永清县" + }, + { + "code": "131024", + "name": "香河县" + }, + { + "code": "131025", + "name": "大城县" + }, + { + "code": "131026", + "name": "文安县" + }, + { + "code": "131028", + "name": "大厂回族自治县" + }, + { + "code": "131071", + "name": "廊坊经济技术开发区" + }, + { + "code": "131081", + "name": "霸州市" + }, + { + "code": "131082", + "name": "三河市" + } + ] + }, + { + "code": "1311", + "name": "衡水市", + "children": [ + { + "code": "131102", + "name": "桃城区" + }, + { + "code": "131103", + "name": "冀州区" + }, + { + "code": "131121", + "name": "枣强县" + }, + { + "code": "131122", + "name": "武邑县" + }, + { + "code": "131123", + "name": "武强县" + }, + { + "code": "131124", + "name": "饶阳县" + }, + { + "code": "131125", + "name": "安平县" + }, + { + "code": "131126", + "name": "故城县" + }, + { + "code": "131127", + "name": "景县" + }, + { + "code": "131128", + "name": "阜城县" + }, + { + "code": "131171", + "name": "河北衡水高新技术产业开发区" + }, + { + "code": "131172", + "name": "衡水滨湖新区" + }, + { + "code": "131182", + "name": "深州市" + } + ] + } + ] + }, + { + "code": "14", + "name": "山西省", + "children": [ + { + "code": "1401", + "name": "太原市", + "children": [ + { + "code": "140105", + "name": "小店区" + }, + { + "code": "140106", + "name": "迎泽区" + }, + { + "code": "140107", + "name": "杏花岭区" + }, + { + "code": "140108", + "name": "尖草坪区" + }, + { + "code": "140109", + "name": "万柏林区" + }, + { + "code": "140110", + "name": "晋源区" + }, + { + "code": "140121", + "name": "清徐县" + }, + { + "code": "140122", + "name": "阳曲县" + }, + { + "code": "140123", + "name": "娄烦县" + }, + { + "code": "140171", + "name": "山西转型综合改革示范区" + }, + { + "code": "140181", + "name": "古交市" + } + ] + }, + { + "code": "1402", + "name": "大同市", + "children": [ + { + "code": "140212", + "name": "新荣区" + }, + { + "code": "140213", + "name": "平城区" + }, + { + "code": "140214", + "name": "云冈区" + }, + { + "code": "140215", + "name": "云州区" + }, + { + "code": "140221", + "name": "阳高县" + }, + { + "code": "140222", + "name": "天镇县" + }, + { + "code": "140223", + "name": "广灵县" + }, + { + "code": "140224", + "name": "灵丘县" + }, + { + "code": "140225", + "name": "浑源县" + }, + { + "code": "140226", + "name": "左云县" + }, + { + "code": "140271", + "name": "山西大同经济开发区" + } + ] + }, + { + "code": "1403", + "name": "阳泉市", + "children": [ + { + "code": "140302", + "name": "城区" + }, + { + "code": "140303", + "name": "矿区" + }, + { + "code": "140311", + "name": "郊区" + }, + { + "code": "140321", + "name": "平定县" + }, + { + "code": "140322", + "name": "盂县" + } + ] + }, + { + "code": "1404", + "name": "长治市", + "children": [ + { + "code": "140403", + "name": "潞州区" + }, + { + "code": "140404", + "name": "上党区" + }, + { + "code": "140405", + "name": "屯留区" + }, + { + "code": "140406", + "name": "潞城区" + }, + { + "code": "140423", + "name": "襄垣县" + }, + { + "code": "140425", + "name": "平顺县" + }, + { + "code": "140426", + "name": "黎城县" + }, + { + "code": "140427", + "name": "壶关县" + }, + { + "code": "140428", + "name": "长子县" + }, + { + "code": "140429", + "name": "武乡县" + }, + { + "code": "140430", + "name": "沁县" + }, + { + "code": "140431", + "name": "沁源县" + } + ] + }, + { + "code": "1405", + "name": "晋城市", + "children": [ + { + "code": "140502", + "name": "城区" + }, + { + "code": "140521", + "name": "沁水县" + }, + { + "code": "140522", + "name": "阳城县" + }, + { + "code": "140524", + "name": "陵川县" + }, + { + "code": "140525", + "name": "泽州县" + }, + { + "code": "140581", + "name": "高平市" + } + ] + }, + { + "code": "1406", + "name": "朔州市", + "children": [ + { + "code": "140602", + "name": "朔城区" + }, + { + "code": "140603", + "name": "平鲁区" + }, + { + "code": "140621", + "name": "山阴县" + }, + { + "code": "140622", + "name": "应县" + }, + { + "code": "140623", + "name": "右玉县" + }, + { + "code": "140671", + "name": "山西朔州经济开发区" + }, + { + "code": "140681", + "name": "怀仁市" + } + ] + }, + { + "code": "1407", + "name": "晋中市", + "children": [ + { + "code": "140702", + "name": "榆次区" + }, + { + "code": "140703", + "name": "太谷区" + }, + { + "code": "140721", + "name": "榆社县" + }, + { + "code": "140722", + "name": "左权县" + }, + { + "code": "140723", + "name": "和顺县" + }, + { + "code": "140724", + "name": "昔阳县" + }, + { + "code": "140725", + "name": "寿阳县" + }, + { + "code": "140727", + "name": "祁县" + }, + { + "code": "140728", + "name": "平遥县" + }, + { + "code": "140729", + "name": "灵石县" + }, + { + "code": "140781", + "name": "介休市" + } + ] + }, + { + "code": "1408", + "name": "运城市", + "children": [ + { + "code": "140802", + "name": "盐湖区" + }, + { + "code": "140821", + "name": "临猗县" + }, + { + "code": "140822", + "name": "万荣县" + }, + { + "code": "140823", + "name": "闻喜县" + }, + { + "code": "140824", + "name": "稷山县" + }, + { + "code": "140825", + "name": "新绛县" + }, + { + "code": "140826", + "name": "绛县" + }, + { + "code": "140827", + "name": "垣曲县" + }, + { + "code": "140828", + "name": "夏县" + }, + { + "code": "140829", + "name": "平陆县" + }, + { + "code": "140830", + "name": "芮城县" + }, + { + "code": "140881", + "name": "永济市" + }, + { + "code": "140882", + "name": "河津市" + } + ] + }, + { + "code": "1409", + "name": "忻州市", + "children": [ + { + "code": "140902", + "name": "忻府区" + }, + { + "code": "140921", + "name": "定襄县" + }, + { + "code": "140922", + "name": "五台县" + }, + { + "code": "140923", + "name": "代县" + }, + { + "code": "140924", + "name": "繁峙县" + }, + { + "code": "140925", + "name": "宁武县" + }, + { + "code": "140926", + "name": "静乐县" + }, + { + "code": "140927", + "name": "神池县" + }, + { + "code": "140928", + "name": "五寨县" + }, + { + "code": "140929", + "name": "岢岚县" + }, + { + "code": "140930", + "name": "河曲县" + }, + { + "code": "140931", + "name": "保德县" + }, + { + "code": "140932", + "name": "偏关县" + }, + { + "code": "140971", + "name": "五台山风景名胜区" + }, + { + "code": "140981", + "name": "原平市" + } + ] + }, + { + "code": "1410", + "name": "临汾市", + "children": [ + { + "code": "141002", + "name": "尧都区" + }, + { + "code": "141021", + "name": "曲沃县" + }, + { + "code": "141022", + "name": "翼城县" + }, + { + "code": "141023", + "name": "襄汾县" + }, + { + "code": "141024", + "name": "洪洞县" + }, + { + "code": "141025", + "name": "古县" + }, + { + "code": "141026", + "name": "安泽县" + }, + { + "code": "141027", + "name": "浮山县" + }, + { + "code": "141028", + "name": "吉县" + }, + { + "code": "141029", + "name": "乡宁县" + }, + { + "code": "141030", + "name": "大宁县" + }, + { + "code": "141031", + "name": "隰县" + }, + { + "code": "141032", + "name": "永和县" + }, + { + "code": "141033", + "name": "蒲县" + }, + { + "code": "141034", + "name": "汾西县" + }, + { + "code": "141081", + "name": "侯马市" + }, + { + "code": "141082", + "name": "霍州市" + } + ] + }, + { + "code": "1411", + "name": "吕梁市", + "children": [ + { + "code": "141102", + "name": "离石区" + }, + { + "code": "141121", + "name": "文水县" + }, + { + "code": "141122", + "name": "交城县" + }, + { + "code": "141123", + "name": "兴县" + }, + { + "code": "141124", + "name": "临县" + }, + { + "code": "141125", + "name": "柳林县" + }, + { + "code": "141126", + "name": "石楼县" + }, + { + "code": "141127", + "name": "岚县" + }, + { + "code": "141128", + "name": "方山县" + }, + { + "code": "141129", + "name": "中阳县" + }, + { + "code": "141130", + "name": "交口县" + }, + { + "code": "141181", + "name": "孝义市" + }, + { + "code": "141182", + "name": "汾阳市" + } + ] + } + ] + }, + { + "code": "15", + "name": "内蒙古自治区", + "children": [ + { + "code": "1501", + "name": "呼和浩特市", + "children": [ + { + "code": "150102", + "name": "新城区" + }, + { + "code": "150103", + "name": "回民区" + }, + { + "code": "150104", + "name": "玉泉区" + }, + { + "code": "150105", + "name": "赛罕区" + }, + { + "code": "150121", + "name": "土默特左旗" + }, + { + "code": "150122", + "name": "托克托县" + }, + { + "code": "150123", + "name": "和林格尔县" + }, + { + "code": "150124", + "name": "清水河县" + }, + { + "code": "150125", + "name": "武川县" + }, + { + "code": "150172", + "name": "呼和浩特经济技术开发区" + } + ] + }, + { + "code": "1502", + "name": "包头市", + "children": [ + { + "code": "150202", + "name": "东河区" + }, + { + "code": "150203", + "name": "昆都仑区" + }, + { + "code": "150204", + "name": "青山区" + }, + { + "code": "150205", + "name": "石拐区" + }, + { + "code": "150206", + "name": "白云鄂博矿区" + }, + { + "code": "150207", + "name": "九原区" + }, + { + "code": "150221", + "name": "土默特右旗" + }, + { + "code": "150222", + "name": "固阳县" + }, + { + "code": "150223", + "name": "达尔罕茂明安联合旗" + }, + { + "code": "150271", + "name": "包头稀土高新技术产业开发区" + } + ] + }, + { + "code": "1503", + "name": "乌海市", + "children": [ + { + "code": "150302", + "name": "海勃湾区" + }, + { + "code": "150303", + "name": "海南区" + }, + { + "code": "150304", + "name": "乌达区" + } + ] + }, + { + "code": "1504", + "name": "赤峰市", + "children": [ + { + "code": "150402", + "name": "红山区" + }, + { + "code": "150403", + "name": "元宝山区" + }, + { + "code": "150404", + "name": "松山区" + }, + { + "code": "150421", + "name": "阿鲁科尔沁旗" + }, + { + "code": "150422", + "name": "巴林左旗" + }, + { + "code": "150423", + "name": "巴林右旗" + }, + { + "code": "150424", + "name": "林西县" + }, + { + "code": "150425", + "name": "克什克腾旗" + }, + { + "code": "150426", + "name": "翁牛特旗" + }, + { + "code": "150428", + "name": "喀喇沁旗" + }, + { + "code": "150429", + "name": "宁城县" + }, + { + "code": "150430", + "name": "敖汉旗" + } + ] + }, + { + "code": "1505", + "name": "通辽市", + "children": [ + { + "code": "150502", + "name": "科尔沁区" + }, + { + "code": "150521", + "name": "科尔沁左翼中旗" + }, + { + "code": "150522", + "name": "科尔沁左翼后旗" + }, + { + "code": "150523", + "name": "开鲁县" + }, + { + "code": "150524", + "name": "库伦旗" + }, + { + "code": "150525", + "name": "奈曼旗" + }, + { + "code": "150526", + "name": "扎鲁特旗" + }, + { + "code": "150571", + "name": "通辽经济技术开发区" + }, + { + "code": "150581", + "name": "霍林郭勒市" + } + ] + }, + { + "code": "1506", + "name": "鄂尔多斯市", + "children": [ + { + "code": "150602", + "name": "东胜区" + }, + { + "code": "150603", + "name": "康巴什区" + }, + { + "code": "150621", + "name": "达拉特旗" + }, + { + "code": "150622", + "name": "准格尔旗" + }, + { + "code": "150623", + "name": "鄂托克前旗" + }, + { + "code": "150624", + "name": "鄂托克旗" + }, + { + "code": "150625", + "name": "杭锦旗" + }, + { + "code": "150626", + "name": "乌审旗" + }, + { + "code": "150627", + "name": "伊金霍洛旗" + } + ] + }, + { + "code": "1507", + "name": "呼伦贝尔市", + "children": [ + { + "code": "150702", + "name": "海拉尔区" + }, + { + "code": "150703", + "name": "扎赉诺尔区" + }, + { + "code": "150721", + "name": "阿荣旗" + }, + { + "code": "150722", + "name": "莫力达瓦达斡尔族自治旗" + }, + { + "code": "150723", + "name": "鄂伦春自治旗" + }, + { + "code": "150724", + "name": "鄂温克族自治旗" + }, + { + "code": "150725", + "name": "陈巴尔虎旗" + }, + { + "code": "150726", + "name": "新巴尔虎左旗" + }, + { + "code": "150727", + "name": "新巴尔虎右旗" + }, + { + "code": "150781", + "name": "满洲里市" + }, + { + "code": "150782", + "name": "牙克石市" + }, + { + "code": "150783", + "name": "扎兰屯市" + }, + { + "code": "150784", + "name": "额尔古纳市" + }, + { + "code": "150785", + "name": "根河市" + } + ] + }, + { + "code": "1508", + "name": "巴彦淖尔市", + "children": [ + { + "code": "150802", + "name": "临河区" + }, + { + "code": "150821", + "name": "五原县" + }, + { + "code": "150822", + "name": "磴口县" + }, + { + "code": "150823", + "name": "乌拉特前旗" + }, + { + "code": "150824", + "name": "乌拉特中旗" + }, + { + "code": "150825", + "name": "乌拉特后旗" + }, + { + "code": "150826", + "name": "杭锦后旗" + } + ] + }, + { + "code": "1509", + "name": "乌兰察布市", + "children": [ + { + "code": "150902", + "name": "集宁区" + }, + { + "code": "150921", + "name": "卓资县" + }, + { + "code": "150922", + "name": "化德县" + }, + { + "code": "150923", + "name": "商都县" + }, + { + "code": "150924", + "name": "兴和县" + }, + { + "code": "150925", + "name": "凉城县" + }, + { + "code": "150926", + "name": "察哈尔右翼前旗" + }, + { + "code": "150927", + "name": "察哈尔右翼中旗" + }, + { + "code": "150928", + "name": "察哈尔右翼后旗" + }, + { + "code": "150929", + "name": "四子王旗" + }, + { + "code": "150981", + "name": "丰镇市" + } + ] + }, + { + "code": "1522", + "name": "兴安盟", + "children": [ + { + "code": "152201", + "name": "乌兰浩特市" + }, + { + "code": "152202", + "name": "阿尔山市" + }, + { + "code": "152221", + "name": "科尔沁右翼前旗" + }, + { + "code": "152222", + "name": "科尔沁右翼中旗" + }, + { + "code": "152223", + "name": "扎赉特旗" + }, + { + "code": "152224", + "name": "突泉县" + } + ] + }, + { + "code": "1525", + "name": "锡林郭勒盟", + "children": [ + { + "code": "152501", + "name": "二连浩特市" + }, + { + "code": "152502", + "name": "锡林浩特市" + }, + { + "code": "152522", + "name": "阿巴嘎旗" + }, + { + "code": "152523", + "name": "苏尼特左旗" + }, + { + "code": "152524", + "name": "苏尼特右旗" + }, + { + "code": "152525", + "name": "东乌珠穆沁旗" + }, + { + "code": "152526", + "name": "西乌珠穆沁旗" + }, + { + "code": "152527", + "name": "太仆寺旗" + }, + { + "code": "152528", + "name": "镶黄旗" + }, + { + "code": "152529", + "name": "正镶白旗" + }, + { + "code": "152530", + "name": "正蓝旗" + }, + { + "code": "152531", + "name": "多伦县" + }, + { + "code": "152571", + "name": "乌拉盖管理区管委会" + } + ] + }, + { + "code": "1529", + "name": "阿拉善盟", + "children": [ + { + "code": "152921", + "name": "阿拉善左旗" + }, + { + "code": "152922", + "name": "阿拉善右旗" + }, + { + "code": "152923", + "name": "额济纳旗" + }, + { + "code": "152971", + "name": "内蒙古阿拉善高新技术产业开发区" + } + ] + } + ] + }, + { + "code": "21", + "name": "辽宁省", + "children": [ + { + "code": "2101", + "name": "沈阳市", + "children": [ + { + "code": "210102", + "name": "和平区" + }, + { + "code": "210103", + "name": "沈河区" + }, + { + "code": "210104", + "name": "大东区" + }, + { + "code": "210105", + "name": "皇姑区" + }, + { + "code": "210106", + "name": "铁西区" + }, + { + "code": "210111", + "name": "苏家屯区" + }, + { + "code": "210112", + "name": "浑南区" + }, + { + "code": "210113", + "name": "沈北新区" + }, + { + "code": "210114", + "name": "于洪区" + }, + { + "code": "210115", + "name": "辽中区" + }, + { + "code": "210123", + "name": "康平县" + }, + { + "code": "210124", + "name": "法库县" + }, + { + "code": "210181", + "name": "新民市" + } + ] + }, + { + "code": "2102", + "name": "大连市", + "children": [ + { + "code": "210202", + "name": "中山区" + }, + { + "code": "210203", + "name": "西岗区" + }, + { + "code": "210204", + "name": "沙河口区" + }, + { + "code": "210211", + "name": "甘井子区" + }, + { + "code": "210212", + "name": "旅顺口区" + }, + { + "code": "210213", + "name": "金州区" + }, + { + "code": "210214", + "name": "普兰店区" + }, + { + "code": "210224", + "name": "长海县" + }, + { + "code": "210281", + "name": "瓦房店市" + }, + { + "code": "210283", + "name": "庄河市" + } + ] + }, + { + "code": "2103", + "name": "鞍山市", + "children": [ + { + "code": "210302", + "name": "铁东区" + }, + { + "code": "210303", + "name": "铁西区" + }, + { + "code": "210304", + "name": "立山区" + }, + { + "code": "210311", + "name": "千山区" + }, + { + "code": "210321", + "name": "台安县" + }, + { + "code": "210323", + "name": "岫岩满族自治县" + }, + { + "code": "210381", + "name": "海城市" + } + ] + }, + { + "code": "2104", + "name": "抚顺市", + "children": [ + { + "code": "210402", + "name": "新抚区" + }, + { + "code": "210403", + "name": "东洲区" + }, + { + "code": "210404", + "name": "望花区" + }, + { + "code": "210411", + "name": "顺城区" + }, + { + "code": "210421", + "name": "抚顺县" + }, + { + "code": "210422", + "name": "新宾满族自治县" + }, + { + "code": "210423", + "name": "清原满族自治县" + } + ] + }, + { + "code": "2105", + "name": "本溪市", + "children": [ + { + "code": "210502", + "name": "平山区" + }, + { + "code": "210503", + "name": "溪湖区" + }, + { + "code": "210504", + "name": "明山区" + }, + { + "code": "210505", + "name": "南芬区" + }, + { + "code": "210521", + "name": "本溪满族自治县" + }, + { + "code": "210522", + "name": "桓仁满族自治县" + } + ] + }, + { + "code": "2106", + "name": "丹东市", + "children": [ + { + "code": "210602", + "name": "元宝区" + }, + { + "code": "210603", + "name": "振兴区" + }, + { + "code": "210604", + "name": "振安区" + }, + { + "code": "210624", + "name": "宽甸满族自治县" + }, + { + "code": "210681", + "name": "东港市" + }, + { + "code": "210682", + "name": "凤城市" + } + ] + }, + { + "code": "2107", + "name": "锦州市", + "children": [ + { + "code": "210702", + "name": "古塔区" + }, + { + "code": "210703", + "name": "凌河区" + }, + { + "code": "210711", + "name": "太和区" + }, + { + "code": "210726", + "name": "黑山县" + }, + { + "code": "210727", + "name": "义县" + }, + { + "code": "210781", + "name": "凌海市" + }, + { + "code": "210782", + "name": "北镇市" + } + ] + }, + { + "code": "2108", + "name": "营口市", + "children": [ + { + "code": "210802", + "name": "站前区" + }, + { + "code": "210803", + "name": "西市区" + }, + { + "code": "210804", + "name": "鲅鱼圈区" + }, + { + "code": "210811", + "name": "老边区" + }, + { + "code": "210881", + "name": "盖州市" + }, + { + "code": "210882", + "name": "大石桥市" + } + ] + }, + { + "code": "2109", + "name": "阜新市", + "children": [ + { + "code": "210902", + "name": "海州区" + }, + { + "code": "210903", + "name": "新邱区" + }, + { + "code": "210904", + "name": "太平区" + }, + { + "code": "210905", + "name": "清河门区" + }, + { + "code": "210911", + "name": "细河区" + }, + { + "code": "210921", + "name": "阜新蒙古族自治县" + }, + { + "code": "210922", + "name": "彰武县" + } + ] + }, + { + "code": "2110", + "name": "辽阳市", + "children": [ + { + "code": "211002", + "name": "白塔区" + }, + { + "code": "211003", + "name": "文圣区" + }, + { + "code": "211004", + "name": "宏伟区" + }, + { + "code": "211005", + "name": "弓长岭区" + }, + { + "code": "211011", + "name": "太子河区" + }, + { + "code": "211021", + "name": "辽阳县" + }, + { + "code": "211081", + "name": "灯塔市" + } + ] + }, + { + "code": "2111", + "name": "盘锦市", + "children": [ + { + "code": "211102", + "name": "双台子区" + }, + { + "code": "211103", + "name": "兴隆台区" + }, + { + "code": "211104", + "name": "大洼区" + }, + { + "code": "211122", + "name": "盘山县" + } + ] + }, + { + "code": "2112", + "name": "铁岭市", + "children": [ + { + "code": "211202", + "name": "银州区" + }, + { + "code": "211204", + "name": "清河区" + }, + { + "code": "211221", + "name": "铁岭县" + }, + { + "code": "211223", + "name": "西丰县" + }, + { + "code": "211224", + "name": "昌图县" + }, + { + "code": "211281", + "name": "调兵山市" + }, + { + "code": "211282", + "name": "开原市" + } + ] + }, + { + "code": "2113", + "name": "朝阳市", + "children": [ + { + "code": "211302", + "name": "双塔区" + }, + { + "code": "211303", + "name": "龙城区" + }, + { + "code": "211321", + "name": "朝阳县" + }, + { + "code": "211322", + "name": "建平县" + }, + { + "code": "211324", + "name": "喀喇沁左翼蒙古族自治县" + }, + { + "code": "211381", + "name": "北票市" + }, + { + "code": "211382", + "name": "凌源市" + } + ] + }, + { + "code": "2114", + "name": "葫芦岛市", + "children": [ + { + "code": "211402", + "name": "连山区" + }, + { + "code": "211403", + "name": "龙港区" + }, + { + "code": "211404", + "name": "南票区" + }, + { + "code": "211421", + "name": "绥中县" + }, + { + "code": "211422", + "name": "建昌县" + }, + { + "code": "211481", + "name": "兴城市" + } + ] + } + ] + }, + { + "code": "22", + "name": "吉林省", + "children": [ + { + "code": "2201", + "name": "长春市", + "children": [ + { + "code": "220102", + "name": "南关区" + }, + { + "code": "220103", + "name": "宽城区" + }, + { + "code": "220104", + "name": "朝阳区" + }, + { + "code": "220105", + "name": "二道区" + }, + { + "code": "220106", + "name": "绿园区" + }, + { + "code": "220112", + "name": "双阳区" + }, + { + "code": "220113", + "name": "九台区" + }, + { + "code": "220122", + "name": "农安县" + }, + { + "code": "220171", + "name": "长春经济技术开发区" + }, + { + "code": "220172", + "name": "长春净月高新技术产业开发区" + }, + { + "code": "220173", + "name": "长春高新技术产业开发区" + }, + { + "code": "220174", + "name": "长春汽车经济技术开发区" + }, + { + "code": "220182", + "name": "榆树市" + }, + { + "code": "220183", + "name": "德惠市" + }, + { + "code": "220184", + "name": "公主岭市" + } + ] + }, + { + "code": "2202", + "name": "吉林市", + "children": [ + { + "code": "220202", + "name": "昌邑区" + }, + { + "code": "220203", + "name": "龙潭区" + }, + { + "code": "220204", + "name": "船营区" + }, + { + "code": "220211", + "name": "丰满区" + }, + { + "code": "220221", + "name": "永吉县" + }, + { + "code": "220271", + "name": "吉林经济开发区" + }, + { + "code": "220272", + "name": "吉林高新技术产业开发区" + }, + { + "code": "220273", + "name": "吉林中国新加坡食品区" + }, + { + "code": "220281", + "name": "蛟河市" + }, + { + "code": "220282", + "name": "桦甸市" + }, + { + "code": "220283", + "name": "舒兰市" + }, + { + "code": "220284", + "name": "磐石市" + } + ] + }, + { + "code": "2203", + "name": "四平市", + "children": [ + { + "code": "220302", + "name": "铁西区" + }, + { + "code": "220303", + "name": "铁东区" + }, + { + "code": "220322", + "name": "梨树县" + }, + { + "code": "220323", + "name": "伊通满族自治县" + }, + { + "code": "220382", + "name": "双辽市" + } + ] + }, + { + "code": "2204", + "name": "辽源市", + "children": [ + { + "code": "220402", + "name": "龙山区" + }, + { + "code": "220403", + "name": "西安区" + }, + { + "code": "220421", + "name": "东丰县" + }, + { + "code": "220422", + "name": "东辽县" + } + ] + }, + { + "code": "2205", + "name": "通化市", + "children": [ + { + "code": "220502", + "name": "东昌区" + }, + { + "code": "220503", + "name": "二道江区" + }, + { + "code": "220521", + "name": "通化县" + }, + { + "code": "220523", + "name": "辉南县" + }, + { + "code": "220524", + "name": "柳河县" + }, + { + "code": "220581", + "name": "梅河口市" + }, + { + "code": "220582", + "name": "集安市" + } + ] + }, + { + "code": "2206", + "name": "白山市", + "children": [ + { + "code": "220602", + "name": "浑江区" + }, + { + "code": "220605", + "name": "江源区" + }, + { + "code": "220621", + "name": "抚松县" + }, + { + "code": "220622", + "name": "靖宇县" + }, + { + "code": "220623", + "name": "长白朝鲜族自治县" + }, + { + "code": "220681", + "name": "临江市" + } + ] + }, + { + "code": "2207", + "name": "松原市", + "children": [ + { + "code": "220702", + "name": "宁江区" + }, + { + "code": "220721", + "name": "前郭尔罗斯蒙古族自治县" + }, + { + "code": "220722", + "name": "长岭县" + }, + { + "code": "220723", + "name": "乾安县" + }, + { + "code": "220771", + "name": "吉林松原经济开发区" + }, + { + "code": "220781", + "name": "扶余市" + } + ] + }, + { + "code": "2208", + "name": "白城市", + "children": [ + { + "code": "220802", + "name": "洮北区" + }, + { + "code": "220821", + "name": "镇赉县" + }, + { + "code": "220822", + "name": "通榆县" + }, + { + "code": "220871", + "name": "吉林白城经济开发区" + }, + { + "code": "220881", + "name": "洮南市" + }, + { + "code": "220882", + "name": "大安市" + } + ] + }, + { + "code": "2224", + "name": "延边朝鲜族自治州", + "children": [ + { + "code": "222401", + "name": "延吉市" + }, + { + "code": "222402", + "name": "图们市" + }, + { + "code": "222403", + "name": "敦化市" + }, + { + "code": "222404", + "name": "珲春市" + }, + { + "code": "222405", + "name": "龙井市" + }, + { + "code": "222406", + "name": "和龙市" + }, + { + "code": "222424", + "name": "汪清县" + }, + { + "code": "222426", + "name": "安图县" + } + ] + } + ] + }, + { + "code": "23", + "name": "黑龙江省", + "children": [ + { + "code": "2301", + "name": "哈尔滨市", + "children": [ + { + "code": "230102", + "name": "道里区" + }, + { + "code": "230103", + "name": "南岗区" + }, + { + "code": "230104", + "name": "道外区" + }, + { + "code": "230108", + "name": "平房区" + }, + { + "code": "230109", + "name": "松北区" + }, + { + "code": "230110", + "name": "香坊区" + }, + { + "code": "230111", + "name": "呼兰区" + }, + { + "code": "230112", + "name": "阿城区" + }, + { + "code": "230113", + "name": "双城区" + }, + { + "code": "230123", + "name": "依兰县" + }, + { + "code": "230124", + "name": "方正县" + }, + { + "code": "230125", + "name": "宾县" + }, + { + "code": "230126", + "name": "巴彦县" + }, + { + "code": "230127", + "name": "木兰县" + }, + { + "code": "230128", + "name": "通河县" + }, + { + "code": "230129", + "name": "延寿县" + }, + { + "code": "230183", + "name": "尚志市" + }, + { + "code": "230184", + "name": "五常市" + } + ] + }, + { + "code": "2302", + "name": "齐齐哈尔市", + "children": [ + { + "code": "230202", + "name": "龙沙区" + }, + { + "code": "230203", + "name": "建华区" + }, + { + "code": "230204", + "name": "铁锋区" + }, + { + "code": "230205", + "name": "昂昂溪区" + }, + { + "code": "230206", + "name": "富拉尔基区" + }, + { + "code": "230207", + "name": "碾子山区" + }, + { + "code": "230208", + "name": "梅里斯达斡尔族区" + }, + { + "code": "230221", + "name": "龙江县" + }, + { + "code": "230223", + "name": "依安县" + }, + { + "code": "230224", + "name": "泰来县" + }, + { + "code": "230225", + "name": "甘南县" + }, + { + "code": "230227", + "name": "富裕县" + }, + { + "code": "230229", + "name": "克山县" + }, + { + "code": "230230", + "name": "克东县" + }, + { + "code": "230231", + "name": "拜泉县" + }, + { + "code": "230281", + "name": "讷河市" + } + ] + }, + { + "code": "2303", + "name": "鸡西市", + "children": [ + { + "code": "230302", + "name": "鸡冠区" + }, + { + "code": "230303", + "name": "恒山区" + }, + { + "code": "230304", + "name": "滴道区" + }, + { + "code": "230305", + "name": "梨树区" + }, + { + "code": "230306", + "name": "城子河区" + }, + { + "code": "230307", + "name": "麻山区" + }, + { + "code": "230321", + "name": "鸡东县" + }, + { + "code": "230381", + "name": "虎林市" + }, + { + "code": "230382", + "name": "密山市" + } + ] + }, + { + "code": "2304", + "name": "鹤岗市", + "children": [ + { + "code": "230402", + "name": "向阳区" + }, + { + "code": "230403", + "name": "工农区" + }, + { + "code": "230404", + "name": "南山区" + }, + { + "code": "230405", + "name": "兴安区" + }, + { + "code": "230406", + "name": "东山区" + }, + { + "code": "230407", + "name": "兴山区" + }, + { + "code": "230421", + "name": "萝北县" + }, + { + "code": "230422", + "name": "绥滨县" + } + ] + }, + { + "code": "2305", + "name": "双鸭山市", + "children": [ + { + "code": "230502", + "name": "尖山区" + }, + { + "code": "230503", + "name": "岭东区" + }, + { + "code": "230505", + "name": "四方台区" + }, + { + "code": "230506", + "name": "宝山区" + }, + { + "code": "230521", + "name": "集贤县" + }, + { + "code": "230522", + "name": "友谊县" + }, + { + "code": "230523", + "name": "宝清县" + }, + { + "code": "230524", + "name": "饶河县" + } + ] + }, + { + "code": "2306", + "name": "大庆市", + "children": [ + { + "code": "230602", + "name": "萨尔图区" + }, + { + "code": "230603", + "name": "龙凤区" + }, + { + "code": "230604", + "name": "让胡路区" + }, + { + "code": "230605", + "name": "红岗区" + }, + { + "code": "230606", + "name": "大同区" + }, + { + "code": "230621", + "name": "肇州县" + }, + { + "code": "230622", + "name": "肇源县" + }, + { + "code": "230623", + "name": "林甸县" + }, + { + "code": "230624", + "name": "杜尔伯特蒙古族自治县" + }, + { + "code": "230671", + "name": "大庆高新技术产业开发区" + } + ] + }, + { + "code": "2307", + "name": "伊春市", + "children": [ + { + "code": "230717", + "name": "伊美区" + }, + { + "code": "230718", + "name": "乌翠区" + }, + { + "code": "230719", + "name": "友好区" + }, + { + "code": "230722", + "name": "嘉荫县" + }, + { + "code": "230723", + "name": "汤旺县" + }, + { + "code": "230724", + "name": "丰林县" + }, + { + "code": "230725", + "name": "大箐山县" + }, + { + "code": "230726", + "name": "南岔县" + }, + { + "code": "230751", + "name": "金林区" + }, + { + "code": "230781", + "name": "铁力市" + } + ] + }, + { + "code": "2308", + "name": "佳木斯市", + "children": [ + { + "code": "230803", + "name": "向阳区" + }, + { + "code": "230804", + "name": "前进区" + }, + { + "code": "230805", + "name": "东风区" + }, + { + "code": "230811", + "name": "郊区" + }, + { + "code": "230822", + "name": "桦南县" + }, + { + "code": "230826", + "name": "桦川县" + }, + { + "code": "230828", + "name": "汤原县" + }, + { + "code": "230881", + "name": "同江市" + }, + { + "code": "230882", + "name": "富锦市" + }, + { + "code": "230883", + "name": "抚远市" + } + ] + }, + { + "code": "2309", + "name": "七台河市", + "children": [ + { + "code": "230902", + "name": "新兴区" + }, + { + "code": "230903", + "name": "桃山区" + }, + { + "code": "230904", + "name": "茄子河区" + }, + { + "code": "230921", + "name": "勃利县" + } + ] + }, + { + "code": "2310", + "name": "牡丹江市", + "children": [ + { + "code": "231002", + "name": "东安区" + }, + { + "code": "231003", + "name": "阳明区" + }, + { + "code": "231004", + "name": "爱民区" + }, + { + "code": "231005", + "name": "西安区" + }, + { + "code": "231025", + "name": "林口县" + }, + { + "code": "231081", + "name": "绥芬河市" + }, + { + "code": "231083", + "name": "海林市" + }, + { + "code": "231084", + "name": "宁安市" + }, + { + "code": "231085", + "name": "穆棱市" + }, + { + "code": "231086", + "name": "东宁市" + } + ] + }, + { + "code": "2311", + "name": "黑河市", + "children": [ + { + "code": "231102", + "name": "爱辉区" + }, + { + "code": "231123", + "name": "逊克县" + }, + { + "code": "231124", + "name": "孙吴县" + }, + { + "code": "231181", + "name": "北安市" + }, + { + "code": "231182", + "name": "五大连池市" + }, + { + "code": "231183", + "name": "嫩江市" + } + ] + }, + { + "code": "2312", + "name": "绥化市", + "children": [ + { + "code": "231202", + "name": "北林区" + }, + { + "code": "231221", + "name": "望奎县" + }, + { + "code": "231222", + "name": "兰西县" + }, + { + "code": "231223", + "name": "青冈县" + }, + { + "code": "231224", + "name": "庆安县" + }, + { + "code": "231225", + "name": "明水县" + }, + { + "code": "231226", + "name": "绥棱县" + }, + { + "code": "231281", + "name": "安达市" + }, + { + "code": "231282", + "name": "肇东市" + }, + { + "code": "231283", + "name": "海伦市" + } + ] + }, + { + "code": "2327", + "name": "大兴安岭地区", + "children": [ + { + "code": "232701", + "name": "漠河市" + }, + { + "code": "232721", + "name": "呼玛县" + }, + { + "code": "232722", + "name": "塔河县" + }, + { + "code": "232761", + "name": "加格达奇区" + }, + { + "code": "232762", + "name": "松岭区" + }, + { + "code": "232763", + "name": "新林区" + }, + { + "code": "232764", + "name": "呼中区" + } + ] + } + ] + }, + { + "code": "31", + "name": "上海市", + "children": [ + { + "code": "3101", + "name": "市辖区", + "children": [ + { + "code": "310101", + "name": "黄浦区" + }, + { + "code": "310104", + "name": "徐汇区" + }, + { + "code": "310105", + "name": "长宁区" + }, + { + "code": "310106", + "name": "静安区" + }, + { + "code": "310107", + "name": "普陀区" + }, + { + "code": "310109", + "name": "虹口区" + }, + { + "code": "310110", + "name": "杨浦区" + }, + { + "code": "310112", + "name": "闵行区" + }, + { + "code": "310113", + "name": "宝山区" + }, + { + "code": "310114", + "name": "嘉定区" + }, + { + "code": "310115", + "name": "浦东新区" + }, + { + "code": "310116", + "name": "金山区" + }, + { + "code": "310117", + "name": "松江区" + }, + { + "code": "310118", + "name": "青浦区" + }, + { + "code": "310120", + "name": "奉贤区" + }, + { + "code": "310151", + "name": "崇明区" + } + ] + } + ] + }, + { + "code": "32", + "name": "江苏省", + "children": [ + { + "code": "3201", + "name": "南京市", + "children": [ + { + "code": "320102", + "name": "玄武区" + }, + { + "code": "320104", + "name": "秦淮区" + }, + { + "code": "320105", + "name": "建邺区" + }, + { + "code": "320106", + "name": "鼓楼区" + }, + { + "code": "320111", + "name": "浦口区" + }, + { + "code": "320113", + "name": "栖霞区" + }, + { + "code": "320114", + "name": "雨花台区" + }, + { + "code": "320115", + "name": "江宁区" + }, + { + "code": "320116", + "name": "六合区" + }, + { + "code": "320117", + "name": "溧水区" + }, + { + "code": "320118", + "name": "高淳区" + } + ] + }, + { + "code": "3202", + "name": "无锡市", + "children": [ + { + "code": "320205", + "name": "锡山区" + }, + { + "code": "320206", + "name": "惠山区" + }, + { + "code": "320211", + "name": "滨湖区" + }, + { + "code": "320213", + "name": "梁溪区" + }, + { + "code": "320214", + "name": "新吴区" + }, + { + "code": "320281", + "name": "江阴市" + }, + { + "code": "320282", + "name": "宜兴市" + } + ] + }, + { + "code": "3203", + "name": "徐州市", + "children": [ + { + "code": "320302", + "name": "鼓楼区" + }, + { + "code": "320303", + "name": "云龙区" + }, + { + "code": "320305", + "name": "贾汪区" + }, + { + "code": "320311", + "name": "泉山区" + }, + { + "code": "320312", + "name": "铜山区" + }, + { + "code": "320321", + "name": "丰县" + }, + { + "code": "320322", + "name": "沛县" + }, + { + "code": "320324", + "name": "睢宁县" + }, + { + "code": "320371", + "name": "徐州经济技术开发区" + }, + { + "code": "320381", + "name": "新沂市" + }, + { + "code": "320382", + "name": "邳州市" + } + ] + }, + { + "code": "3204", + "name": "常州市", + "children": [ + { + "code": "320402", + "name": "天宁区" + }, + { + "code": "320404", + "name": "钟楼区" + }, + { + "code": "320411", + "name": "新北区" + }, + { + "code": "320412", + "name": "武进区" + }, + { + "code": "320413", + "name": "金坛区" + }, + { + "code": "320481", + "name": "溧阳市" + } + ] + }, + { + "code": "3205", + "name": "苏州市", + "children": [ + { + "code": "320505", + "name": "虎丘区" + }, + { + "code": "320506", + "name": "吴中区" + }, + { + "code": "320507", + "name": "相城区" + }, + { + "code": "320508", + "name": "姑苏区" + }, + { + "code": "320509", + "name": "吴江区" + }, + { + "code": "320576", + "name": "苏州工业园区" + }, + { + "code": "320581", + "name": "常熟市" + }, + { + "code": "320582", + "name": "张家港市" + }, + { + "code": "320583", + "name": "昆山市" + }, + { + "code": "320585", + "name": "太仓市" + } + ] + }, + { + "code": "3206", + "name": "南通市", + "children": [ + { + "code": "320612", + "name": "通州区" + }, + { + "code": "320613", + "name": "崇川区" + }, + { + "code": "320614", + "name": "海门区" + }, + { + "code": "320623", + "name": "如东县" + }, + { + "code": "320671", + "name": "南通经济技术开发区" + }, + { + "code": "320681", + "name": "启东市" + }, + { + "code": "320682", + "name": "如皋市" + }, + { + "code": "320685", + "name": "海安市" + } + ] + }, + { + "code": "3207", + "name": "连云港市", + "children": [ + { + "code": "320703", + "name": "连云区" + }, + { + "code": "320706", + "name": "海州区" + }, + { + "code": "320707", + "name": "赣榆区" + }, + { + "code": "320722", + "name": "东海县" + }, + { + "code": "320723", + "name": "灌云县" + }, + { + "code": "320724", + "name": "灌南县" + }, + { + "code": "320771", + "name": "连云港经济技术开发区" + } + ] + }, + { + "code": "3208", + "name": "淮安市", + "children": [ + { + "code": "320803", + "name": "淮安区" + }, + { + "code": "320804", + "name": "淮阴区" + }, + { + "code": "320812", + "name": "清江浦区" + }, + { + "code": "320813", + "name": "洪泽区" + }, + { + "code": "320826", + "name": "涟水县" + }, + { + "code": "320830", + "name": "盱眙县" + }, + { + "code": "320831", + "name": "金湖县" + }, + { + "code": "320871", + "name": "淮安经济技术开发区" + } + ] + }, + { + "code": "3209", + "name": "盐城市", + "children": [ + { + "code": "320902", + "name": "亭湖区" + }, + { + "code": "320903", + "name": "盐都区" + }, + { + "code": "320904", + "name": "大丰区" + }, + { + "code": "320921", + "name": "响水县" + }, + { + "code": "320922", + "name": "滨海县" + }, + { + "code": "320923", + "name": "阜宁县" + }, + { + "code": "320924", + "name": "射阳县" + }, + { + "code": "320925", + "name": "建湖县" + }, + { + "code": "320971", + "name": "盐城经济技术开发区" + }, + { + "code": "320981", + "name": "东台市" + } + ] + }, + { + "code": "3210", + "name": "扬州市", + "children": [ + { + "code": "321002", + "name": "广陵区" + }, + { + "code": "321003", + "name": "邗江区" + }, + { + "code": "321012", + "name": "江都区" + }, + { + "code": "321023", + "name": "宝应县" + }, + { + "code": "321071", + "name": "扬州经济技术开发区" + }, + { + "code": "321081", + "name": "仪征市" + }, + { + "code": "321084", + "name": "高邮市" + } + ] + }, + { + "code": "3211", + "name": "镇江市", + "children": [ + { + "code": "321102", + "name": "京口区" + }, + { + "code": "321111", + "name": "润州区" + }, + { + "code": "321112", + "name": "丹徒区" + }, + { + "code": "321171", + "name": "镇江新区" + }, + { + "code": "321181", + "name": "丹阳市" + }, + { + "code": "321182", + "name": "扬中市" + }, + { + "code": "321183", + "name": "句容市" + } + ] + }, + { + "code": "3212", + "name": "泰州市", + "children": [ + { + "code": "321202", + "name": "海陵区" + }, + { + "code": "321203", + "name": "高港区" + }, + { + "code": "321204", + "name": "姜堰区" + }, + { + "code": "321281", + "name": "兴化市" + }, + { + "code": "321282", + "name": "靖江市" + }, + { + "code": "321283", + "name": "泰兴市" + } + ] + }, + { + "code": "3213", + "name": "宿迁市", + "children": [ + { + "code": "321302", + "name": "宿城区" + }, + { + "code": "321311", + "name": "宿豫区" + }, + { + "code": "321322", + "name": "沭阳县" + }, + { + "code": "321323", + "name": "泗阳县" + }, + { + "code": "321324", + "name": "泗洪县" + }, + { + "code": "321371", + "name": "宿迁经济技术开发区" + } + ] + } + ] + }, + { + "code": "33", + "name": "浙江省", + "children": [ + { + "code": "3301", + "name": "杭州市", + "children": [ + { + "code": "330102", + "name": "上城区" + }, + { + "code": "330105", + "name": "拱墅区" + }, + { + "code": "330106", + "name": "西湖区" + }, + { + "code": "330108", + "name": "滨江区" + }, + { + "code": "330109", + "name": "萧山区" + }, + { + "code": "330110", + "name": "余杭区" + }, + { + "code": "330111", + "name": "富阳区" + }, + { + "code": "330112", + "name": "临安区" + }, + { + "code": "330113", + "name": "临平区" + }, + { + "code": "330114", + "name": "钱塘区" + }, + { + "code": "330122", + "name": "桐庐县" + }, + { + "code": "330127", + "name": "淳安县" + }, + { + "code": "330182", + "name": "建德市" + } + ] + }, + { + "code": "3302", + "name": "宁波市", + "children": [ + { + "code": "330203", + "name": "海曙区" + }, + { + "code": "330205", + "name": "江北区" + }, + { + "code": "330206", + "name": "北仑区" + }, + { + "code": "330211", + "name": "镇海区" + }, + { + "code": "330212", + "name": "鄞州区" + }, + { + "code": "330213", + "name": "奉化区" + }, + { + "code": "330225", + "name": "象山县" + }, + { + "code": "330226", + "name": "宁海县" + }, + { + "code": "330281", + "name": "余姚市" + }, + { + "code": "330282", + "name": "慈溪市" + } + ] + }, + { + "code": "3303", + "name": "温州市", + "children": [ + { + "code": "330302", + "name": "鹿城区" + }, + { + "code": "330303", + "name": "龙湾区" + }, + { + "code": "330304", + "name": "瓯海区" + }, + { + "code": "330305", + "name": "洞头区" + }, + { + "code": "330324", + "name": "永嘉县" + }, + { + "code": "330326", + "name": "平阳县" + }, + { + "code": "330327", + "name": "苍南县" + }, + { + "code": "330328", + "name": "文成县" + }, + { + "code": "330329", + "name": "泰顺县" + }, + { + "code": "330381", + "name": "瑞安市" + }, + { + "code": "330382", + "name": "乐清市" + }, + { + "code": "330383", + "name": "龙港市" + } + ] + }, + { + "code": "3304", + "name": "嘉兴市", + "children": [ + { + "code": "330402", + "name": "南湖区" + }, + { + "code": "330411", + "name": "秀洲区" + }, + { + "code": "330421", + "name": "嘉善县" + }, + { + "code": "330424", + "name": "海盐县" + }, + { + "code": "330481", + "name": "海宁市" + }, + { + "code": "330482", + "name": "平湖市" + }, + { + "code": "330483", + "name": "桐乡市" + } + ] + }, + { + "code": "3305", + "name": "湖州市", + "children": [ + { + "code": "330502", + "name": "吴兴区" + }, + { + "code": "330503", + "name": "南浔区" + }, + { + "code": "330521", + "name": "德清县" + }, + { + "code": "330522", + "name": "长兴县" + }, + { + "code": "330523", + "name": "安吉县" + } + ] + }, + { + "code": "3306", + "name": "绍兴市", + "children": [ + { + "code": "330602", + "name": "越城区" + }, + { + "code": "330603", + "name": "柯桥区" + }, + { + "code": "330604", + "name": "上虞区" + }, + { + "code": "330624", + "name": "新昌县" + }, + { + "code": "330681", + "name": "诸暨市" + }, + { + "code": "330683", + "name": "嵊州市" + } + ] + }, + { + "code": "3307", + "name": "金华市", + "children": [ + { + "code": "330702", + "name": "婺城区" + }, + { + "code": "330703", + "name": "金东区" + }, + { + "code": "330723", + "name": "武义县" + }, + { + "code": "330726", + "name": "浦江县" + }, + { + "code": "330727", + "name": "磐安县" + }, + { + "code": "330781", + "name": "兰溪市" + }, + { + "code": "330782", + "name": "义乌市" + }, + { + "code": "330783", + "name": "东阳市" + }, + { + "code": "330784", + "name": "永康市" + } + ] + }, + { + "code": "3308", + "name": "衢州市", + "children": [ + { + "code": "330802", + "name": "柯城区" + }, + { + "code": "330803", + "name": "衢江区" + }, + { + "code": "330822", + "name": "常山县" + }, + { + "code": "330824", + "name": "开化县" + }, + { + "code": "330825", + "name": "龙游县" + }, + { + "code": "330881", + "name": "江山市" + } + ] + }, + { + "code": "3309", + "name": "舟山市", + "children": [ + { + "code": "330902", + "name": "定海区" + }, + { + "code": "330903", + "name": "普陀区" + }, + { + "code": "330921", + "name": "岱山县" + }, + { + "code": "330922", + "name": "嵊泗县" + } + ] + }, + { + "code": "3310", + "name": "台州市", + "children": [ + { + "code": "331002", + "name": "椒江区" + }, + { + "code": "331003", + "name": "黄岩区" + }, + { + "code": "331004", + "name": "路桥区" + }, + { + "code": "331022", + "name": "三门县" + }, + { + "code": "331023", + "name": "天台县" + }, + { + "code": "331024", + "name": "仙居县" + }, + { + "code": "331081", + "name": "温岭市" + }, + { + "code": "331082", + "name": "临海市" + }, + { + "code": "331083", + "name": "玉环市" + } + ] + }, + { + "code": "3311", + "name": "丽水市", + "children": [ + { + "code": "331102", + "name": "莲都区" + }, + { + "code": "331121", + "name": "青田县" + }, + { + "code": "331122", + "name": "缙云县" + }, + { + "code": "331123", + "name": "遂昌县" + }, + { + "code": "331124", + "name": "松阳县" + }, + { + "code": "331125", + "name": "云和县" + }, + { + "code": "331126", + "name": "庆元县" + }, + { + "code": "331127", + "name": "景宁畲族自治县" + }, + { + "code": "331181", + "name": "龙泉市" + } + ] + } + ] + }, + { + "code": "34", + "name": "安徽省", + "children": [ + { + "code": "3401", + "name": "合肥市", + "children": [ + { + "code": "340102", + "name": "瑶海区" + }, + { + "code": "340103", + "name": "庐阳区" + }, + { + "code": "340104", + "name": "蜀山区" + }, + { + "code": "340111", + "name": "包河区" + }, + { + "code": "340121", + "name": "长丰县" + }, + { + "code": "340122", + "name": "肥东县" + }, + { + "code": "340123", + "name": "肥西县" + }, + { + "code": "340124", + "name": "庐江县" + }, + { + "code": "340176", + "name": "合肥高新技术产业开发区" + }, + { + "code": "340177", + "name": "合肥经济技术开发区" + }, + { + "code": "340178", + "name": "合肥新站高新技术产业开发区" + }, + { + "code": "340181", + "name": "巢湖市" + } + ] + }, + { + "code": "3402", + "name": "芜湖市", + "children": [ + { + "code": "340202", + "name": "镜湖区" + }, + { + "code": "340207", + "name": "鸠江区" + }, + { + "code": "340209", + "name": "弋江区" + }, + { + "code": "340210", + "name": "湾沚区" + }, + { + "code": "340212", + "name": "繁昌区" + }, + { + "code": "340223", + "name": "南陵县" + }, + { + "code": "340271", + "name": "芜湖经济技术开发区" + }, + { + "code": "340272", + "name": "安徽芜湖三山经济开发区" + }, + { + "code": "340281", + "name": "无为市" + } + ] + }, + { + "code": "3403", + "name": "蚌埠市", + "children": [ + { + "code": "340302", + "name": "龙子湖区" + }, + { + "code": "340303", + "name": "蚌山区" + }, + { + "code": "340304", + "name": "禹会区" + }, + { + "code": "340311", + "name": "淮上区" + }, + { + "code": "340321", + "name": "怀远县" + }, + { + "code": "340322", + "name": "五河县" + }, + { + "code": "340323", + "name": "固镇县" + }, + { + "code": "340371", + "name": "蚌埠市高新技术开发区" + }, + { + "code": "340372", + "name": "蚌埠市经济开发区" + } + ] + }, + { + "code": "3404", + "name": "淮南市", + "children": [ + { + "code": "340402", + "name": "大通区" + }, + { + "code": "340403", + "name": "田家庵区" + }, + { + "code": "340404", + "name": "谢家集区" + }, + { + "code": "340405", + "name": "八公山区" + }, + { + "code": "340406", + "name": "潘集区" + }, + { + "code": "340421", + "name": "凤台县" + }, + { + "code": "340422", + "name": "寿县" + } + ] + }, + { + "code": "3405", + "name": "马鞍山市", + "children": [ + { + "code": "340503", + "name": "花山区" + }, + { + "code": "340504", + "name": "雨山区" + }, + { + "code": "340506", + "name": "博望区" + }, + { + "code": "340521", + "name": "当涂县" + }, + { + "code": "340522", + "name": "含山县" + }, + { + "code": "340523", + "name": "和县" + } + ] + }, + { + "code": "3406", + "name": "淮北市", + "children": [ + { + "code": "340602", + "name": "杜集区" + }, + { + "code": "340603", + "name": "相山区" + }, + { + "code": "340604", + "name": "烈山区" + }, + { + "code": "340621", + "name": "濉溪县" + } + ] + }, + { + "code": "3407", + "name": "铜陵市", + "children": [ + { + "code": "340705", + "name": "铜官区" + }, + { + "code": "340706", + "name": "义安区" + }, + { + "code": "340711", + "name": "郊区" + }, + { + "code": "340722", + "name": "枞阳县" + } + ] + }, + { + "code": "3408", + "name": "安庆市", + "children": [ + { + "code": "340802", + "name": "迎江区" + }, + { + "code": "340803", + "name": "大观区" + }, + { + "code": "340811", + "name": "宜秀区" + }, + { + "code": "340822", + "name": "怀宁县" + }, + { + "code": "340825", + "name": "太湖县" + }, + { + "code": "340826", + "name": "宿松县" + }, + { + "code": "340827", + "name": "望江县" + }, + { + "code": "340828", + "name": "岳西县" + }, + { + "code": "340871", + "name": "安徽安庆经济开发区" + }, + { + "code": "340881", + "name": "桐城市" + }, + { + "code": "340882", + "name": "潜山市" + } + ] + }, + { + "code": "3410", + "name": "黄山市", + "children": [ + { + "code": "341002", + "name": "屯溪区" + }, + { + "code": "341003", + "name": "黄山区" + }, + { + "code": "341004", + "name": "徽州区" + }, + { + "code": "341021", + "name": "歙县" + }, + { + "code": "341022", + "name": "休宁县" + }, + { + "code": "341023", + "name": "黟县" + }, + { + "code": "341024", + "name": "祁门县" + } + ] + }, + { + "code": "3411", + "name": "滁州市", + "children": [ + { + "code": "341102", + "name": "琅琊区" + }, + { + "code": "341103", + "name": "南谯区" + }, + { + "code": "341122", + "name": "来安县" + }, + { + "code": "341124", + "name": "全椒县" + }, + { + "code": "341125", + "name": "定远县" + }, + { + "code": "341126", + "name": "凤阳县" + }, + { + "code": "341171", + "name": "中新苏滁高新技术产业开发区" + }, + { + "code": "341172", + "name": "滁州经济技术开发区" + }, + { + "code": "341181", + "name": "天长市" + }, + { + "code": "341182", + "name": "明光市" + } + ] + }, + { + "code": "3412", + "name": "阜阳市", + "children": [ + { + "code": "341202", + "name": "颍州区" + }, + { + "code": "341203", + "name": "颍东区" + }, + { + "code": "341204", + "name": "颍泉区" + }, + { + "code": "341221", + "name": "临泉县" + }, + { + "code": "341222", + "name": "太和县" + }, + { + "code": "341225", + "name": "阜南县" + }, + { + "code": "341226", + "name": "颍上县" + }, + { + "code": "341271", + "name": "阜阳合肥现代产业园区" + }, + { + "code": "341272", + "name": "阜阳经济技术开发区" + }, + { + "code": "341282", + "name": "界首市" + } + ] + }, + { + "code": "3413", + "name": "宿州市", + "children": [ + { + "code": "341302", + "name": "埇桥区" + }, + { + "code": "341321", + "name": "砀山县" + }, + { + "code": "341322", + "name": "萧县" + }, + { + "code": "341323", + "name": "灵璧县" + }, + { + "code": "341324", + "name": "泗县" + }, + { + "code": "341371", + "name": "宿州马鞍山现代产业园区" + }, + { + "code": "341372", + "name": "宿州经济技术开发区" + } + ] + }, + { + "code": "3415", + "name": "六安市", + "children": [ + { + "code": "341502", + "name": "金安区" + }, + { + "code": "341503", + "name": "裕安区" + }, + { + "code": "341504", + "name": "叶集区" + }, + { + "code": "341522", + "name": "霍邱县" + }, + { + "code": "341523", + "name": "舒城县" + }, + { + "code": "341524", + "name": "金寨县" + }, + { + "code": "341525", + "name": "霍山县" + } + ] + }, + { + "code": "3416", + "name": "亳州市", + "children": [ + { + "code": "341602", + "name": "谯城区" + }, + { + "code": "341621", + "name": "涡阳县" + }, + { + "code": "341622", + "name": "蒙城县" + }, + { + "code": "341623", + "name": "利辛县" + } + ] + }, + { + "code": "3417", + "name": "池州市", + "children": [ + { + "code": "341702", + "name": "贵池区" + }, + { + "code": "341721", + "name": "东至县" + }, + { + "code": "341722", + "name": "石台县" + }, + { + "code": "341723", + "name": "青阳县" + } + ] + }, + { + "code": "3418", + "name": "宣城市", + "children": [ + { + "code": "341802", + "name": "宣州区" + }, + { + "code": "341821", + "name": "郎溪县" + }, + { + "code": "341823", + "name": "泾县" + }, + { + "code": "341824", + "name": "绩溪县" + }, + { + "code": "341825", + "name": "旌德县" + }, + { + "code": "341871", + "name": "宣城市经济开发区" + }, + { + "code": "341881", + "name": "宁国市" + }, + { + "code": "341882", + "name": "广德市" + } + ] + } + ] + }, + { + "code": "35", + "name": "福建省", + "children": [ + { + "code": "3501", + "name": "福州市", + "children": [ + { + "code": "350102", + "name": "鼓楼区" + }, + { + "code": "350103", + "name": "台江区" + }, + { + "code": "350104", + "name": "仓山区" + }, + { + "code": "350105", + "name": "马尾区" + }, + { + "code": "350111", + "name": "晋安区" + }, + { + "code": "350112", + "name": "长乐区" + }, + { + "code": "350121", + "name": "闽侯县" + }, + { + "code": "350122", + "name": "连江县" + }, + { + "code": "350123", + "name": "罗源县" + }, + { + "code": "350124", + "name": "闽清县" + }, + { + "code": "350125", + "name": "永泰县" + }, + { + "code": "350128", + "name": "平潭县" + }, + { + "code": "350181", + "name": "福清市" + } + ] + }, + { + "code": "3502", + "name": "厦门市", + "children": [ + { + "code": "350203", + "name": "思明区" + }, + { + "code": "350205", + "name": "海沧区" + }, + { + "code": "350206", + "name": "湖里区" + }, + { + "code": "350211", + "name": "集美区" + }, + { + "code": "350212", + "name": "同安区" + }, + { + "code": "350213", + "name": "翔安区" + } + ] + }, + { + "code": "3503", + "name": "莆田市", + "children": [ + { + "code": "350302", + "name": "城厢区" + }, + { + "code": "350303", + "name": "涵江区" + }, + { + "code": "350304", + "name": "荔城区" + }, + { + "code": "350305", + "name": "秀屿区" + }, + { + "code": "350322", + "name": "仙游县" + } + ] + }, + { + "code": "3504", + "name": "三明市", + "children": [ + { + "code": "350404", + "name": "三元区" + }, + { + "code": "350405", + "name": "沙县区" + }, + { + "code": "350421", + "name": "明溪县" + }, + { + "code": "350423", + "name": "清流县" + }, + { + "code": "350424", + "name": "宁化县" + }, + { + "code": "350425", + "name": "大田县" + }, + { + "code": "350426", + "name": "尤溪县" + }, + { + "code": "350428", + "name": "将乐县" + }, + { + "code": "350429", + "name": "泰宁县" + }, + { + "code": "350430", + "name": "建宁县" + }, + { + "code": "350481", + "name": "永安市" + } + ] + }, + { + "code": "3505", + "name": "泉州市", + "children": [ + { + "code": "350502", + "name": "鲤城区" + }, + { + "code": "350503", + "name": "丰泽区" + }, + { + "code": "350504", + "name": "洛江区" + }, + { + "code": "350505", + "name": "泉港区" + }, + { + "code": "350521", + "name": "惠安县" + }, + { + "code": "350524", + "name": "安溪县" + }, + { + "code": "350525", + "name": "永春县" + }, + { + "code": "350526", + "name": "德化县" + }, + { + "code": "350527", + "name": "金门县" + }, + { + "code": "350581", + "name": "石狮市" + }, + { + "code": "350582", + "name": "晋江市" + }, + { + "code": "350583", + "name": "南安市" + } + ] + }, + { + "code": "3506", + "name": "漳州市", + "children": [ + { + "code": "350602", + "name": "芗城区" + }, + { + "code": "350603", + "name": "龙文区" + }, + { + "code": "350604", + "name": "龙海区" + }, + { + "code": "350605", + "name": "长泰区" + }, + { + "code": "350622", + "name": "云霄县" + }, + { + "code": "350623", + "name": "漳浦县" + }, + { + "code": "350624", + "name": "诏安县" + }, + { + "code": "350626", + "name": "东山县" + }, + { + "code": "350627", + "name": "南靖县" + }, + { + "code": "350628", + "name": "平和县" + }, + { + "code": "350629", + "name": "华安县" + } + ] + }, + { + "code": "3507", + "name": "南平市", + "children": [ + { + "code": "350702", + "name": "延平区" + }, + { + "code": "350703", + "name": "建阳区" + }, + { + "code": "350721", + "name": "顺昌县" + }, + { + "code": "350722", + "name": "浦城县" + }, + { + "code": "350723", + "name": "光泽县" + }, + { + "code": "350724", + "name": "松溪县" + }, + { + "code": "350725", + "name": "政和县" + }, + { + "code": "350781", + "name": "邵武市" + }, + { + "code": "350782", + "name": "武夷山市" + }, + { + "code": "350783", + "name": "建瓯市" + } + ] + }, + { + "code": "3508", + "name": "龙岩市", + "children": [ + { + "code": "350802", + "name": "新罗区" + }, + { + "code": "350803", + "name": "永定区" + }, + { + "code": "350821", + "name": "长汀县" + }, + { + "code": "350823", + "name": "上杭县" + }, + { + "code": "350824", + "name": "武平县" + }, + { + "code": "350825", + "name": "连城县" + }, + { + "code": "350881", + "name": "漳平市" + } + ] + }, + { + "code": "3509", + "name": "宁德市", + "children": [ + { + "code": "350902", + "name": "蕉城区" + }, + { + "code": "350921", + "name": "霞浦县" + }, + { + "code": "350922", + "name": "古田县" + }, + { + "code": "350923", + "name": "屏南县" + }, + { + "code": "350924", + "name": "寿宁县" + }, + { + "code": "350925", + "name": "周宁县" + }, + { + "code": "350926", + "name": "柘荣县" + }, + { + "code": "350981", + "name": "福安市" + }, + { + "code": "350982", + "name": "福鼎市" + } + ] + } + ] + }, + { + "code": "36", + "name": "江西省", + "children": [ + { + "code": "3601", + "name": "南昌市", + "children": [ + { + "code": "360102", + "name": "东湖区" + }, + { + "code": "360103", + "name": "西湖区" + }, + { + "code": "360104", + "name": "青云谱区" + }, + { + "code": "360111", + "name": "青山湖区" + }, + { + "code": "360112", + "name": "新建区" + }, + { + "code": "360113", + "name": "红谷滩区" + }, + { + "code": "360121", + "name": "南昌县" + }, + { + "code": "360123", + "name": "安义县" + }, + { + "code": "360124", + "name": "进贤县" + } + ] + }, + { + "code": "3602", + "name": "景德镇市", + "children": [ + { + "code": "360202", + "name": "昌江区" + }, + { + "code": "360203", + "name": "珠山区" + }, + { + "code": "360222", + "name": "浮梁县" + }, + { + "code": "360281", + "name": "乐平市" + } + ] + }, + { + "code": "3603", + "name": "萍乡市", + "children": [ + { + "code": "360302", + "name": "安源区" + }, + { + "code": "360313", + "name": "湘东区" + }, + { + "code": "360321", + "name": "莲花县" + }, + { + "code": "360322", + "name": "上栗县" + }, + { + "code": "360323", + "name": "芦溪县" + } + ] + }, + { + "code": "3604", + "name": "九江市", + "children": [ + { + "code": "360402", + "name": "濂溪区" + }, + { + "code": "360403", + "name": "浔阳区" + }, + { + "code": "360404", + "name": "柴桑区" + }, + { + "code": "360423", + "name": "武宁县" + }, + { + "code": "360424", + "name": "修水县" + }, + { + "code": "360425", + "name": "永修县" + }, + { + "code": "360426", + "name": "德安县" + }, + { + "code": "360428", + "name": "都昌县" + }, + { + "code": "360429", + "name": "湖口县" + }, + { + "code": "360430", + "name": "彭泽县" + }, + { + "code": "360481", + "name": "瑞昌市" + }, + { + "code": "360482", + "name": "共青城市" + }, + { + "code": "360483", + "name": "庐山市" + } + ] + }, + { + "code": "3605", + "name": "新余市", + "children": [ + { + "code": "360502", + "name": "渝水区" + }, + { + "code": "360521", + "name": "分宜县" + } + ] + }, + { + "code": "3606", + "name": "鹰潭市", + "children": [ + { + "code": "360602", + "name": "月湖区" + }, + { + "code": "360603", + "name": "余江区" + }, + { + "code": "360681", + "name": "贵溪市" + } + ] + }, + { + "code": "3607", + "name": "赣州市", + "children": [ + { + "code": "360702", + "name": "章贡区" + }, + { + "code": "360703", + "name": "南康区" + }, + { + "code": "360704", + "name": "赣县区" + }, + { + "code": "360722", + "name": "信丰县" + }, + { + "code": "360723", + "name": "大余县" + }, + { + "code": "360724", + "name": "上犹县" + }, + { + "code": "360725", + "name": "崇义县" + }, + { + "code": "360726", + "name": "安远县" + }, + { + "code": "360728", + "name": "定南县" + }, + { + "code": "360729", + "name": "全南县" + }, + { + "code": "360730", + "name": "宁都县" + }, + { + "code": "360731", + "name": "于都县" + }, + { + "code": "360732", + "name": "兴国县" + }, + { + "code": "360733", + "name": "会昌县" + }, + { + "code": "360734", + "name": "寻乌县" + }, + { + "code": "360735", + "name": "石城县" + }, + { + "code": "360781", + "name": "瑞金市" + }, + { + "code": "360783", + "name": "龙南市" + } + ] + }, + { + "code": "3608", + "name": "吉安市", + "children": [ + { + "code": "360802", + "name": "吉州区" + }, + { + "code": "360803", + "name": "青原区" + }, + { + "code": "360821", + "name": "吉安县" + }, + { + "code": "360822", + "name": "吉水县" + }, + { + "code": "360823", + "name": "峡江县" + }, + { + "code": "360824", + "name": "新干县" + }, + { + "code": "360825", + "name": "永丰县" + }, + { + "code": "360826", + "name": "泰和县" + }, + { + "code": "360827", + "name": "遂川县" + }, + { + "code": "360828", + "name": "万安县" + }, + { + "code": "360829", + "name": "安福县" + }, + { + "code": "360830", + "name": "永新县" + }, + { + "code": "360881", + "name": "井冈山市" + } + ] + }, + { + "code": "3609", + "name": "宜春市", + "children": [ + { + "code": "360902", + "name": "袁州区" + }, + { + "code": "360921", + "name": "奉新县" + }, + { + "code": "360922", + "name": "万载县" + }, + { + "code": "360923", + "name": "上高县" + }, + { + "code": "360924", + "name": "宜丰县" + }, + { + "code": "360925", + "name": "靖安县" + }, + { + "code": "360926", + "name": "铜鼓县" + }, + { + "code": "360981", + "name": "丰城市" + }, + { + "code": "360982", + "name": "樟树市" + }, + { + "code": "360983", + "name": "高安市" + } + ] + }, + { + "code": "3610", + "name": "抚州市", + "children": [ + { + "code": "361002", + "name": "临川区" + }, + { + "code": "361003", + "name": "东乡区" + }, + { + "code": "361021", + "name": "南城县" + }, + { + "code": "361022", + "name": "黎川县" + }, + { + "code": "361023", + "name": "南丰县" + }, + { + "code": "361024", + "name": "崇仁县" + }, + { + "code": "361025", + "name": "乐安县" + }, + { + "code": "361026", + "name": "宜黄县" + }, + { + "code": "361027", + "name": "金溪县" + }, + { + "code": "361028", + "name": "资溪县" + }, + { + "code": "361030", + "name": "广昌县" + } + ] + }, + { + "code": "3611", + "name": "上饶市", + "children": [ + { + "code": "361102", + "name": "信州区" + }, + { + "code": "361103", + "name": "广丰区" + }, + { + "code": "361104", + "name": "广信区" + }, + { + "code": "361123", + "name": "玉山县" + }, + { + "code": "361124", + "name": "铅山县" + }, + { + "code": "361125", + "name": "横峰县" + }, + { + "code": "361126", + "name": "弋阳县" + }, + { + "code": "361127", + "name": "余干县" + }, + { + "code": "361128", + "name": "鄱阳县" + }, + { + "code": "361129", + "name": "万年县" + }, + { + "code": "361130", + "name": "婺源县" + }, + { + "code": "361181", + "name": "德兴市" + } + ] + } + ] + }, + { + "code": "37", + "name": "山东省", + "children": [ + { + "code": "3701", + "name": "济南市", + "children": [ + { + "code": "370102", + "name": "历下区" + }, + { + "code": "370103", + "name": "市中区" + }, + { + "code": "370104", + "name": "槐荫区" + }, + { + "code": "370105", + "name": "天桥区" + }, + { + "code": "370112", + "name": "历城区" + }, + { + "code": "370113", + "name": "长清区" + }, + { + "code": "370114", + "name": "章丘区" + }, + { + "code": "370115", + "name": "济阳区" + }, + { + "code": "370116", + "name": "莱芜区" + }, + { + "code": "370117", + "name": "钢城区" + }, + { + "code": "370124", + "name": "平阴县" + }, + { + "code": "370126", + "name": "商河县" + }, + { + "code": "370176", + "name": "济南高新技术产业开发区" + } + ] + }, + { + "code": "3702", + "name": "青岛市", + "children": [ + { + "code": "370202", + "name": "市南区" + }, + { + "code": "370203", + "name": "市北区" + }, + { + "code": "370211", + "name": "黄岛区" + }, + { + "code": "370212", + "name": "崂山区" + }, + { + "code": "370213", + "name": "李沧区" + }, + { + "code": "370214", + "name": "城阳区" + }, + { + "code": "370215", + "name": "即墨区" + }, + { + "code": "370281", + "name": "胶州市" + }, + { + "code": "370283", + "name": "平度市" + }, + { + "code": "370285", + "name": "莱西市" + } + ] + }, + { + "code": "3703", + "name": "淄博市", + "children": [ + { + "code": "370302", + "name": "淄川区" + }, + { + "code": "370303", + "name": "张店区" + }, + { + "code": "370304", + "name": "博山区" + }, + { + "code": "370305", + "name": "临淄区" + }, + { + "code": "370306", + "name": "周村区" + }, + { + "code": "370321", + "name": "桓台县" + }, + { + "code": "370322", + "name": "高青县" + }, + { + "code": "370323", + "name": "沂源县" + } + ] + }, + { + "code": "3704", + "name": "枣庄市", + "children": [ + { + "code": "370402", + "name": "市中区" + }, + { + "code": "370403", + "name": "薛城区" + }, + { + "code": "370404", + "name": "峄城区" + }, + { + "code": "370405", + "name": "台儿庄区" + }, + { + "code": "370406", + "name": "山亭区" + }, + { + "code": "370481", + "name": "滕州市" + } + ] + }, + { + "code": "3705", + "name": "东营市", + "children": [ + { + "code": "370502", + "name": "东营区" + }, + { + "code": "370503", + "name": "河口区" + }, + { + "code": "370505", + "name": "垦利区" + }, + { + "code": "370522", + "name": "利津县" + }, + { + "code": "370523", + "name": "广饶县" + }, + { + "code": "370571", + "name": "东营经济技术开发区" + }, + { + "code": "370572", + "name": "东营港经济开发区" + } + ] + }, + { + "code": "3706", + "name": "烟台市", + "children": [ + { + "code": "370602", + "name": "芝罘区" + }, + { + "code": "370611", + "name": "福山区" + }, + { + "code": "370612", + "name": "牟平区" + }, + { + "code": "370613", + "name": "莱山区" + }, + { + "code": "370614", + "name": "蓬莱区" + }, + { + "code": "370671", + "name": "烟台高新技术产业开发区" + }, + { + "code": "370676", + "name": "烟台经济技术开发区" + }, + { + "code": "370681", + "name": "龙口市" + }, + { + "code": "370682", + "name": "莱阳市" + }, + { + "code": "370683", + "name": "莱州市" + }, + { + "code": "370685", + "name": "招远市" + }, + { + "code": "370686", + "name": "栖霞市" + }, + { + "code": "370687", + "name": "海阳市" + } + ] + }, + { + "code": "3707", + "name": "潍坊市", + "children": [ + { + "code": "370702", + "name": "潍城区" + }, + { + "code": "370703", + "name": "寒亭区" + }, + { + "code": "370704", + "name": "坊子区" + }, + { + "code": "370705", + "name": "奎文区" + }, + { + "code": "370724", + "name": "临朐县" + }, + { + "code": "370725", + "name": "昌乐县" + }, + { + "code": "370772", + "name": "潍坊滨海经济技术开发区" + }, + { + "code": "370781", + "name": "青州市" + }, + { + "code": "370782", + "name": "诸城市" + }, + { + "code": "370783", + "name": "寿光市" + }, + { + "code": "370784", + "name": "安丘市" + }, + { + "code": "370785", + "name": "高密市" + }, + { + "code": "370786", + "name": "昌邑市" + } + ] + }, + { + "code": "3708", + "name": "济宁市", + "children": [ + { + "code": "370811", + "name": "任城区" + }, + { + "code": "370812", + "name": "兖州区" + }, + { + "code": "370826", + "name": "微山县" + }, + { + "code": "370827", + "name": "鱼台县" + }, + { + "code": "370828", + "name": "金乡县" + }, + { + "code": "370829", + "name": "嘉祥县" + }, + { + "code": "370830", + "name": "汶上县" + }, + { + "code": "370831", + "name": "泗水县" + }, + { + "code": "370832", + "name": "梁山县" + }, + { + "code": "370871", + "name": "济宁高新技术产业开发区" + }, + { + "code": "370881", + "name": "曲阜市" + }, + { + "code": "370883", + "name": "邹城市" + } + ] + }, + { + "code": "3709", + "name": "泰安市", + "children": [ + { + "code": "370902", + "name": "泰山区" + }, + { + "code": "370911", + "name": "岱岳区" + }, + { + "code": "370921", + "name": "宁阳县" + }, + { + "code": "370923", + "name": "东平县" + }, + { + "code": "370982", + "name": "新泰市" + }, + { + "code": "370983", + "name": "肥城市" + } + ] + }, + { + "code": "3710", + "name": "威海市", + "children": [ + { + "code": "371002", + "name": "环翠区" + }, + { + "code": "371003", + "name": "文登区" + }, + { + "code": "371071", + "name": "威海火炬高技术产业开发区" + }, + { + "code": "371072", + "name": "威海经济技术开发区" + }, + { + "code": "371073", + "name": "威海临港经济技术开发区" + }, + { + "code": "371082", + "name": "荣成市" + }, + { + "code": "371083", + "name": "乳山市" + } + ] + }, + { + "code": "3711", + "name": "日照市", + "children": [ + { + "code": "371102", + "name": "东港区" + }, + { + "code": "371103", + "name": "岚山区" + }, + { + "code": "371121", + "name": "五莲县" + }, + { + "code": "371122", + "name": "莒县" + }, + { + "code": "371171", + "name": "日照经济技术开发区" + } + ] + }, + { + "code": "3713", + "name": "临沂市", + "children": [ + { + "code": "371302", + "name": "兰山区" + }, + { + "code": "371311", + "name": "罗庄区" + }, + { + "code": "371312", + "name": "河东区" + }, + { + "code": "371321", + "name": "沂南县" + }, + { + "code": "371322", + "name": "郯城县" + }, + { + "code": "371323", + "name": "沂水县" + }, + { + "code": "371324", + "name": "兰陵县" + }, + { + "code": "371325", + "name": "费县" + }, + { + "code": "371326", + "name": "平邑县" + }, + { + "code": "371327", + "name": "莒南县" + }, + { + "code": "371328", + "name": "蒙阴县" + }, + { + "code": "371329", + "name": "临沭县" + }, + { + "code": "371371", + "name": "临沂高新技术产业开发区" + } + ] + }, + { + "code": "3714", + "name": "德州市", + "children": [ + { + "code": "371402", + "name": "德城区" + }, + { + "code": "371403", + "name": "陵城区" + }, + { + "code": "371422", + "name": "宁津县" + }, + { + "code": "371423", + "name": "庆云县" + }, + { + "code": "371424", + "name": "临邑县" + }, + { + "code": "371425", + "name": "齐河县" + }, + { + "code": "371426", + "name": "平原县" + }, + { + "code": "371427", + "name": "夏津县" + }, + { + "code": "371428", + "name": "武城县" + }, + { + "code": "371471", + "name": "德州天衢新区" + }, + { + "code": "371481", + "name": "乐陵市" + }, + { + "code": "371482", + "name": "禹城市" + } + ] + }, + { + "code": "3715", + "name": "聊城市", + "children": [ + { + "code": "371502", + "name": "东昌府区" + }, + { + "code": "371503", + "name": "茌平区" + }, + { + "code": "371521", + "name": "阳谷县" + }, + { + "code": "371522", + "name": "莘县" + }, + { + "code": "371524", + "name": "东阿县" + }, + { + "code": "371525", + "name": "冠县" + }, + { + "code": "371526", + "name": "高唐县" + }, + { + "code": "371581", + "name": "临清市" + } + ] + }, + { + "code": "3716", + "name": "滨州市", + "children": [ + { + "code": "371602", + "name": "滨城区" + }, + { + "code": "371603", + "name": "沾化区" + }, + { + "code": "371621", + "name": "惠民县" + }, + { + "code": "371622", + "name": "阳信县" + }, + { + "code": "371623", + "name": "无棣县" + }, + { + "code": "371625", + "name": "博兴县" + }, + { + "code": "371681", + "name": "邹平市" + } + ] + }, + { + "code": "3717", + "name": "菏泽市", + "children": [ + { + "code": "371702", + "name": "牡丹区" + }, + { + "code": "371703", + "name": "定陶区" + }, + { + "code": "371721", + "name": "曹县" + }, + { + "code": "371722", + "name": "单县" + }, + { + "code": "371723", + "name": "成武县" + }, + { + "code": "371724", + "name": "巨野县" + }, + { + "code": "371725", + "name": "郓城县" + }, + { + "code": "371726", + "name": "鄄城县" + }, + { + "code": "371728", + "name": "东明县" + }, + { + "code": "371771", + "name": "菏泽经济技术开发区" + }, + { + "code": "371772", + "name": "菏泽高新技术开发区" + } + ] + } + ] + }, + { + "code": "41", + "name": "河南省", + "children": [ + { + "code": "4101", + "name": "郑州市", + "children": [ + { + "code": "410102", + "name": "中原区" + }, + { + "code": "410103", + "name": "二七区" + }, + { + "code": "410104", + "name": "管城回族区" + }, + { + "code": "410105", + "name": "金水区" + }, + { + "code": "410106", + "name": "上街区" + }, + { + "code": "410108", + "name": "惠济区" + }, + { + "code": "410122", + "name": "中牟县" + }, + { + "code": "410171", + "name": "郑州经济技术开发区" + }, + { + "code": "410172", + "name": "郑州高新技术产业开发区" + }, + { + "code": "410173", + "name": "郑州航空港经济综合实验区" + }, + { + "code": "410181", + "name": "巩义市" + }, + { + "code": "410182", + "name": "荥阳市" + }, + { + "code": "410183", + "name": "新密市" + }, + { + "code": "410184", + "name": "新郑市" + }, + { + "code": "410185", + "name": "登封市" + } + ] + }, + { + "code": "4102", + "name": "开封市", + "children": [ + { + "code": "410202", + "name": "龙亭区" + }, + { + "code": "410203", + "name": "顺河回族区" + }, + { + "code": "410204", + "name": "鼓楼区" + }, + { + "code": "410205", + "name": "禹王台区" + }, + { + "code": "410212", + "name": "祥符区" + }, + { + "code": "410221", + "name": "杞县" + }, + { + "code": "410222", + "name": "通许县" + }, + { + "code": "410223", + "name": "尉氏县" + }, + { + "code": "410225", + "name": "兰考县" + } + ] + }, + { + "code": "4103", + "name": "洛阳市", + "children": [ + { + "code": "410302", + "name": "老城区" + }, + { + "code": "410303", + "name": "西工区" + }, + { + "code": "410304", + "name": "瀍河回族区" + }, + { + "code": "410305", + "name": "涧西区" + }, + { + "code": "410307", + "name": "偃师区" + }, + { + "code": "410308", + "name": "孟津区" + }, + { + "code": "410311", + "name": "洛龙区" + }, + { + "code": "410323", + "name": "新安县" + }, + { + "code": "410324", + "name": "栾川县" + }, + { + "code": "410325", + "name": "嵩县" + }, + { + "code": "410326", + "name": "汝阳县" + }, + { + "code": "410327", + "name": "宜阳县" + }, + { + "code": "410328", + "name": "洛宁县" + }, + { + "code": "410329", + "name": "伊川县" + }, + { + "code": "410371", + "name": "洛阳高新技术产业开发区" + } + ] + }, + { + "code": "4104", + "name": "平顶山市", + "children": [ + { + "code": "410402", + "name": "新华区" + }, + { + "code": "410403", + "name": "卫东区" + }, + { + "code": "410404", + "name": "石龙区" + }, + { + "code": "410411", + "name": "湛河区" + }, + { + "code": "410421", + "name": "宝丰县" + }, + { + "code": "410422", + "name": "叶县" + }, + { + "code": "410423", + "name": "鲁山县" + }, + { + "code": "410425", + "name": "郏县" + }, + { + "code": "410471", + "name": "平顶山高新技术产业开发区" + }, + { + "code": "410472", + "name": "平顶山市城乡一体化示范区" + }, + { + "code": "410481", + "name": "舞钢市" + }, + { + "code": "410482", + "name": "汝州市" + } + ] + }, + { + "code": "4105", + "name": "安阳市", + "children": [ + { + "code": "410502", + "name": "文峰区" + }, + { + "code": "410503", + "name": "北关区" + }, + { + "code": "410505", + "name": "殷都区" + }, + { + "code": "410506", + "name": "龙安区" + }, + { + "code": "410522", + "name": "安阳县" + }, + { + "code": "410523", + "name": "汤阴县" + }, + { + "code": "410526", + "name": "滑县" + }, + { + "code": "410527", + "name": "内黄县" + }, + { + "code": "410571", + "name": "安阳高新技术产业开发区" + }, + { + "code": "410581", + "name": "林州市" + } + ] + }, + { + "code": "4106", + "name": "鹤壁市", + "children": [ + { + "code": "410602", + "name": "鹤山区" + }, + { + "code": "410603", + "name": "山城区" + }, + { + "code": "410611", + "name": "淇滨区" + }, + { + "code": "410621", + "name": "浚县" + }, + { + "code": "410622", + "name": "淇县" + }, + { + "code": "410671", + "name": "鹤壁经济技术开发区" + } + ] + }, + { + "code": "4107", + "name": "新乡市", + "children": [ + { + "code": "410702", + "name": "红旗区" + }, + { + "code": "410703", + "name": "卫滨区" + }, + { + "code": "410704", + "name": "凤泉区" + }, + { + "code": "410711", + "name": "牧野区" + }, + { + "code": "410721", + "name": "新乡县" + }, + { + "code": "410724", + "name": "获嘉县" + }, + { + "code": "410725", + "name": "原阳县" + }, + { + "code": "410726", + "name": "延津县" + }, + { + "code": "410727", + "name": "封丘县" + }, + { + "code": "410771", + "name": "新乡高新技术产业开发区" + }, + { + "code": "410772", + "name": "新乡经济技术开发区" + }, + { + "code": "410773", + "name": "新乡市平原城乡一体化示范区" + }, + { + "code": "410781", + "name": "卫辉市" + }, + { + "code": "410782", + "name": "辉县市" + }, + { + "code": "410783", + "name": "长垣市" + } + ] + }, + { + "code": "4108", + "name": "焦作市", + "children": [ + { + "code": "410802", + "name": "解放区" + }, + { + "code": "410803", + "name": "中站区" + }, + { + "code": "410804", + "name": "马村区" + }, + { + "code": "410811", + "name": "山阳区" + }, + { + "code": "410821", + "name": "修武县" + }, + { + "code": "410822", + "name": "博爱县" + }, + { + "code": "410823", + "name": "武陟县" + }, + { + "code": "410825", + "name": "温县" + }, + { + "code": "410871", + "name": "焦作城乡一体化示范区" + }, + { + "code": "410882", + "name": "沁阳市" + }, + { + "code": "410883", + "name": "孟州市" + } + ] + }, + { + "code": "4109", + "name": "濮阳市", + "children": [ + { + "code": "410902", + "name": "华龙区" + }, + { + "code": "410922", + "name": "清丰县" + }, + { + "code": "410923", + "name": "南乐县" + }, + { + "code": "410926", + "name": "范县" + }, + { + "code": "410927", + "name": "台前县" + }, + { + "code": "410928", + "name": "濮阳县" + }, + { + "code": "410971", + "name": "河南濮阳工业园区" + }, + { + "code": "410972", + "name": "濮阳经济技术开发区" + } + ] + }, + { + "code": "4110", + "name": "许昌市", + "children": [ + { + "code": "411002", + "name": "魏都区" + }, + { + "code": "411003", + "name": "建安区" + }, + { + "code": "411024", + "name": "鄢陵县" + }, + { + "code": "411025", + "name": "襄城县" + }, + { + "code": "411071", + "name": "许昌经济技术开发区" + }, + { + "code": "411081", + "name": "禹州市" + }, + { + "code": "411082", + "name": "长葛市" + } + ] + }, + { + "code": "4111", + "name": "漯河市", + "children": [ + { + "code": "411102", + "name": "源汇区" + }, + { + "code": "411103", + "name": "郾城区" + }, + { + "code": "411104", + "name": "召陵区" + }, + { + "code": "411121", + "name": "舞阳县" + }, + { + "code": "411122", + "name": "临颍县" + }, + { + "code": "411171", + "name": "漯河经济技术开发区" + } + ] + }, + { + "code": "4112", + "name": "三门峡市", + "children": [ + { + "code": "411202", + "name": "湖滨区" + }, + { + "code": "411203", + "name": "陕州区" + }, + { + "code": "411221", + "name": "渑池县" + }, + { + "code": "411224", + "name": "卢氏县" + }, + { + "code": "411271", + "name": "河南三门峡经济开发区" + }, + { + "code": "411281", + "name": "义马市" + }, + { + "code": "411282", + "name": "灵宝市" + } + ] + }, + { + "code": "4113", + "name": "南阳市", + "children": [ + { + "code": "411302", + "name": "宛城区" + }, + { + "code": "411303", + "name": "卧龙区" + }, + { + "code": "411321", + "name": "南召县" + }, + { + "code": "411322", + "name": "方城县" + }, + { + "code": "411323", + "name": "西峡县" + }, + { + "code": "411324", + "name": "镇平县" + }, + { + "code": "411325", + "name": "内乡县" + }, + { + "code": "411326", + "name": "淅川县" + }, + { + "code": "411327", + "name": "社旗县" + }, + { + "code": "411328", + "name": "唐河县" + }, + { + "code": "411329", + "name": "新野县" + }, + { + "code": "411330", + "name": "桐柏县" + }, + { + "code": "411371", + "name": "南阳高新技术产业开发区" + }, + { + "code": "411372", + "name": "南阳市城乡一体化示范区" + }, + { + "code": "411381", + "name": "邓州市" + } + ] + }, + { + "code": "4114", + "name": "商丘市", + "children": [ + { + "code": "411402", + "name": "梁园区" + }, + { + "code": "411403", + "name": "睢阳区" + }, + { + "code": "411421", + "name": "民权县" + }, + { + "code": "411422", + "name": "睢县" + }, + { + "code": "411423", + "name": "宁陵县" + }, + { + "code": "411424", + "name": "柘城县" + }, + { + "code": "411425", + "name": "虞城县" + }, + { + "code": "411426", + "name": "夏邑县" + }, + { + "code": "411471", + "name": "豫东综合物流产业聚集区" + }, + { + "code": "411472", + "name": "河南商丘经济开发区" + }, + { + "code": "411481", + "name": "永城市" + } + ] + }, + { + "code": "4115", + "name": "信阳市", + "children": [ + { + "code": "411502", + "name": "浉河区" + }, + { + "code": "411503", + "name": "平桥区" + }, + { + "code": "411521", + "name": "罗山县" + }, + { + "code": "411522", + "name": "光山县" + }, + { + "code": "411523", + "name": "新县" + }, + { + "code": "411524", + "name": "商城县" + }, + { + "code": "411525", + "name": "固始县" + }, + { + "code": "411526", + "name": "潢川县" + }, + { + "code": "411527", + "name": "淮滨县" + }, + { + "code": "411528", + "name": "息县" + }, + { + "code": "411571", + "name": "信阳高新技术产业开发区" + } + ] + }, + { + "code": "4116", + "name": "周口市", + "children": [ + { + "code": "411602", + "name": "川汇区" + }, + { + "code": "411603", + "name": "淮阳区" + }, + { + "code": "411621", + "name": "扶沟县" + }, + { + "code": "411622", + "name": "西华县" + }, + { + "code": "411623", + "name": "商水县" + }, + { + "code": "411624", + "name": "沈丘县" + }, + { + "code": "411625", + "name": "郸城县" + }, + { + "code": "411627", + "name": "太康县" + }, + { + "code": "411628", + "name": "鹿邑县" + }, + { + "code": "411671", + "name": "周口临港开发区" + }, + { + "code": "411681", + "name": "项城市" + } + ] + }, + { + "code": "4117", + "name": "驻马店市", + "children": [ + { + "code": "411702", + "name": "驿城区" + }, + { + "code": "411721", + "name": "西平县" + }, + { + "code": "411722", + "name": "上蔡县" + }, + { + "code": "411723", + "name": "平舆县" + }, + { + "code": "411724", + "name": "正阳县" + }, + { + "code": "411725", + "name": "确山县" + }, + { + "code": "411726", + "name": "泌阳县" + }, + { + "code": "411727", + "name": "汝南县" + }, + { + "code": "411728", + "name": "遂平县" + }, + { + "code": "411729", + "name": "新蔡县" + }, + { + "code": "411771", + "name": "河南驻马店经济开发区" + } + ] + }, + { + "code": "4190", + "name": "省直辖县级行政区划", + "children": [ + { + "code": "419001", + "name": "济源市" + } + ] + } + ] + }, + { + "code": "42", + "name": "湖北省", + "children": [ + { + "code": "4201", + "name": "武汉市", + "children": [ + { + "code": "420102", + "name": "江岸区" + }, + { + "code": "420103", + "name": "江汉区" + }, + { + "code": "420104", + "name": "硚口区" + }, + { + "code": "420105", + "name": "汉阳区" + }, + { + "code": "420106", + "name": "武昌区" + }, + { + "code": "420107", + "name": "青山区" + }, + { + "code": "420111", + "name": "洪山区" + }, + { + "code": "420112", + "name": "东西湖区" + }, + { + "code": "420113", + "name": "汉南区" + }, + { + "code": "420114", + "name": "蔡甸区" + }, + { + "code": "420115", + "name": "江夏区" + }, + { + "code": "420116", + "name": "黄陂区" + }, + { + "code": "420117", + "name": "新洲区" + } + ] + }, + { + "code": "4202", + "name": "黄石市", + "children": [ + { + "code": "420202", + "name": "黄石港区" + }, + { + "code": "420203", + "name": "西塞山区" + }, + { + "code": "420204", + "name": "下陆区" + }, + { + "code": "420205", + "name": "铁山区" + }, + { + "code": "420222", + "name": "阳新县" + }, + { + "code": "420281", + "name": "大冶市" + } + ] + }, + { + "code": "4203", + "name": "十堰市", + "children": [ + { + "code": "420302", + "name": "茅箭区" + }, + { + "code": "420303", + "name": "张湾区" + }, + { + "code": "420304", + "name": "郧阳区" + }, + { + "code": "420322", + "name": "郧西县" + }, + { + "code": "420323", + "name": "竹山县" + }, + { + "code": "420324", + "name": "竹溪县" + }, + { + "code": "420325", + "name": "房县" + }, + { + "code": "420381", + "name": "丹江口市" + } + ] + }, + { + "code": "4205", + "name": "宜昌市", + "children": [ + { + "code": "420502", + "name": "西陵区" + }, + { + "code": "420503", + "name": "伍家岗区" + }, + { + "code": "420504", + "name": "点军区" + }, + { + "code": "420505", + "name": "猇亭区" + }, + { + "code": "420506", + "name": "夷陵区" + }, + { + "code": "420525", + "name": "远安县" + }, + { + "code": "420526", + "name": "兴山县" + }, + { + "code": "420527", + "name": "秭归县" + }, + { + "code": "420528", + "name": "长阳土家族自治县" + }, + { + "code": "420529", + "name": "五峰土家族自治县" + }, + { + "code": "420581", + "name": "宜都市" + }, + { + "code": "420582", + "name": "当阳市" + }, + { + "code": "420583", + "name": "枝江市" + } + ] + }, + { + "code": "4206", + "name": "襄阳市", + "children": [ + { + "code": "420602", + "name": "襄城区" + }, + { + "code": "420606", + "name": "樊城区" + }, + { + "code": "420607", + "name": "襄州区" + }, + { + "code": "420624", + "name": "南漳县" + }, + { + "code": "420625", + "name": "谷城县" + }, + { + "code": "420626", + "name": "保康县" + }, + { + "code": "420682", + "name": "老河口市" + }, + { + "code": "420683", + "name": "枣阳市" + }, + { + "code": "420684", + "name": "宜城市" + } + ] + }, + { + "code": "4207", + "name": "鄂州市", + "children": [ + { + "code": "420702", + "name": "梁子湖区" + }, + { + "code": "420703", + "name": "华容区" + }, + { + "code": "420704", + "name": "鄂城区" + } + ] + }, + { + "code": "4208", + "name": "荆门市", + "children": [ + { + "code": "420802", + "name": "东宝区" + }, + { + "code": "420804", + "name": "掇刀区" + }, + { + "code": "420822", + "name": "沙洋县" + }, + { + "code": "420881", + "name": "钟祥市" + }, + { + "code": "420882", + "name": "京山市" + } + ] + }, + { + "code": "4209", + "name": "孝感市", + "children": [ + { + "code": "420902", + "name": "孝南区" + }, + { + "code": "420921", + "name": "孝昌县" + }, + { + "code": "420922", + "name": "大悟县" + }, + { + "code": "420923", + "name": "云梦县" + }, + { + "code": "420981", + "name": "应城市" + }, + { + "code": "420982", + "name": "安陆市" + }, + { + "code": "420984", + "name": "汉川市" + } + ] + }, + { + "code": "4210", + "name": "荆州市", + "children": [ + { + "code": "421002", + "name": "沙市区" + }, + { + "code": "421003", + "name": "荆州区" + }, + { + "code": "421022", + "name": "公安县" + }, + { + "code": "421024", + "name": "江陵县" + }, + { + "code": "421071", + "name": "荆州经济技术开发区" + }, + { + "code": "421081", + "name": "石首市" + }, + { + "code": "421083", + "name": "洪湖市" + }, + { + "code": "421087", + "name": "松滋市" + }, + { + "code": "421088", + "name": "监利市" + } + ] + }, + { + "code": "4211", + "name": "黄冈市", + "children": [ + { + "code": "421102", + "name": "黄州区" + }, + { + "code": "421121", + "name": "团风县" + }, + { + "code": "421122", + "name": "红安县" + }, + { + "code": "421123", + "name": "罗田县" + }, + { + "code": "421124", + "name": "英山县" + }, + { + "code": "421125", + "name": "浠水县" + }, + { + "code": "421126", + "name": "蕲春县" + }, + { + "code": "421127", + "name": "黄梅县" + }, + { + "code": "421171", + "name": "龙感湖管理区" + }, + { + "code": "421181", + "name": "麻城市" + }, + { + "code": "421182", + "name": "武穴市" + } + ] + }, + { + "code": "4212", + "name": "咸宁市", + "children": [ + { + "code": "421202", + "name": "咸安区" + }, + { + "code": "421221", + "name": "嘉鱼县" + }, + { + "code": "421222", + "name": "通城县" + }, + { + "code": "421223", + "name": "崇阳县" + }, + { + "code": "421224", + "name": "通山县" + }, + { + "code": "421281", + "name": "赤壁市" + } + ] + }, + { + "code": "4213", + "name": "随州市", + "children": [ + { + "code": "421303", + "name": "曾都区" + }, + { + "code": "421321", + "name": "随县" + }, + { + "code": "421381", + "name": "广水市" + } + ] + }, + { + "code": "4228", + "name": "恩施土家族苗族自治州", + "children": [ + { + "code": "422801", + "name": "恩施市" + }, + { + "code": "422802", + "name": "利川市" + }, + { + "code": "422822", + "name": "建始县" + }, + { + "code": "422823", + "name": "巴东县" + }, + { + "code": "422825", + "name": "宣恩县" + }, + { + "code": "422826", + "name": "咸丰县" + }, + { + "code": "422827", + "name": "来凤县" + }, + { + "code": "422828", + "name": "鹤峰县" + } + ] + }, + { + "code": "4290", + "name": "省直辖县级行政区划", + "children": [ + { + "code": "429004", + "name": "仙桃市" + }, + { + "code": "429005", + "name": "潜江市" + }, + { + "code": "429006", + "name": "天门市" + }, + { + "code": "429021", + "name": "神农架林区" + } + ] + } + ] + }, + { + "code": "43", + "name": "湖南省", + "children": [ + { + "code": "4301", + "name": "长沙市", + "children": [ + { + "code": "430102", + "name": "芙蓉区" + }, + { + "code": "430103", + "name": "天心区" + }, + { + "code": "430104", + "name": "岳麓区" + }, + { + "code": "430105", + "name": "开福区" + }, + { + "code": "430111", + "name": "雨花区" + }, + { + "code": "430112", + "name": "望城区" + }, + { + "code": "430121", + "name": "长沙县" + }, + { + "code": "430181", + "name": "浏阳市" + }, + { + "code": "430182", + "name": "宁乡市" + } + ] + }, + { + "code": "4302", + "name": "株洲市", + "children": [ + { + "code": "430202", + "name": "荷塘区" + }, + { + "code": "430203", + "name": "芦淞区" + }, + { + "code": "430204", + "name": "石峰区" + }, + { + "code": "430211", + "name": "天元区" + }, + { + "code": "430212", + "name": "渌口区" + }, + { + "code": "430223", + "name": "攸县" + }, + { + "code": "430224", + "name": "茶陵县" + }, + { + "code": "430225", + "name": "炎陵县" + }, + { + "code": "430281", + "name": "醴陵市" + } + ] + }, + { + "code": "4303", + "name": "湘潭市", + "children": [ + { + "code": "430302", + "name": "雨湖区" + }, + { + "code": "430304", + "name": "岳塘区" + }, + { + "code": "430321", + "name": "湘潭县" + }, + { + "code": "430371", + "name": "湖南湘潭高新技术产业园区" + }, + { + "code": "430372", + "name": "湘潭昭山示范区" + }, + { + "code": "430373", + "name": "湘潭九华示范区" + }, + { + "code": "430381", + "name": "湘乡市" + }, + { + "code": "430382", + "name": "韶山市" + } + ] + }, + { + "code": "4304", + "name": "衡阳市", + "children": [ + { + "code": "430405", + "name": "珠晖区" + }, + { + "code": "430406", + "name": "雁峰区" + }, + { + "code": "430407", + "name": "石鼓区" + }, + { + "code": "430408", + "name": "蒸湘区" + }, + { + "code": "430412", + "name": "南岳区" + }, + { + "code": "430421", + "name": "衡阳县" + }, + { + "code": "430422", + "name": "衡南县" + }, + { + "code": "430423", + "name": "衡山县" + }, + { + "code": "430424", + "name": "衡东县" + }, + { + "code": "430426", + "name": "祁东县" + }, + { + "code": "430473", + "name": "湖南衡阳松木经济开发区" + }, + { + "code": "430476", + "name": "湖南衡阳高新技术产业园区" + }, + { + "code": "430481", + "name": "耒阳市" + }, + { + "code": "430482", + "name": "常宁市" + } + ] + }, + { + "code": "4305", + "name": "邵阳市", + "children": [ + { + "code": "430502", + "name": "双清区" + }, + { + "code": "430503", + "name": "大祥区" + }, + { + "code": "430511", + "name": "北塔区" + }, + { + "code": "430522", + "name": "新邵县" + }, + { + "code": "430523", + "name": "邵阳县" + }, + { + "code": "430524", + "name": "隆回县" + }, + { + "code": "430525", + "name": "洞口县" + }, + { + "code": "430527", + "name": "绥宁县" + }, + { + "code": "430528", + "name": "新宁县" + }, + { + "code": "430529", + "name": "城步苗族自治县" + }, + { + "code": "430581", + "name": "武冈市" + }, + { + "code": "430582", + "name": "邵东市" + } + ] + }, + { + "code": "4306", + "name": "岳阳市", + "children": [ + { + "code": "430602", + "name": "岳阳楼区" + }, + { + "code": "430603", + "name": "云溪区" + }, + { + "code": "430611", + "name": "君山区" + }, + { + "code": "430621", + "name": "岳阳县" + }, + { + "code": "430623", + "name": "华容县" + }, + { + "code": "430624", + "name": "湘阴县" + }, + { + "code": "430626", + "name": "平江县" + }, + { + "code": "430671", + "name": "岳阳市屈原管理区" + }, + { + "code": "430681", + "name": "汨罗市" + }, + { + "code": "430682", + "name": "临湘市" + } + ] + }, + { + "code": "4307", + "name": "常德市", + "children": [ + { + "code": "430702", + "name": "武陵区" + }, + { + "code": "430703", + "name": "鼎城区" + }, + { + "code": "430721", + "name": "安乡县" + }, + { + "code": "430722", + "name": "汉寿县" + }, + { + "code": "430723", + "name": "澧县" + }, + { + "code": "430724", + "name": "临澧县" + }, + { + "code": "430725", + "name": "桃源县" + }, + { + "code": "430726", + "name": "石门县" + }, + { + "code": "430771", + "name": "常德市西洞庭管理区" + }, + { + "code": "430781", + "name": "津市市" + } + ] + }, + { + "code": "4308", + "name": "张家界市", + "children": [ + { + "code": "430802", + "name": "永定区" + }, + { + "code": "430811", + "name": "武陵源区" + }, + { + "code": "430821", + "name": "慈利县" + }, + { + "code": "430822", + "name": "桑植县" + } + ] + }, + { + "code": "4309", + "name": "益阳市", + "children": [ + { + "code": "430902", + "name": "资阳区" + }, + { + "code": "430903", + "name": "赫山区" + }, + { + "code": "430921", + "name": "南县" + }, + { + "code": "430922", + "name": "桃江县" + }, + { + "code": "430923", + "name": "安化县" + }, + { + "code": "430971", + "name": "益阳市大通湖管理区" + }, + { + "code": "430972", + "name": "湖南益阳高新技术产业园区" + }, + { + "code": "430981", + "name": "沅江市" + } + ] + }, + { + "code": "4310", + "name": "郴州市", + "children": [ + { + "code": "431002", + "name": "北湖区" + }, + { + "code": "431003", + "name": "苏仙区" + }, + { + "code": "431021", + "name": "桂阳县" + }, + { + "code": "431022", + "name": "宜章县" + }, + { + "code": "431023", + "name": "永兴县" + }, + { + "code": "431024", + "name": "嘉禾县" + }, + { + "code": "431025", + "name": "临武县" + }, + { + "code": "431026", + "name": "汝城县" + }, + { + "code": "431027", + "name": "桂东县" + }, + { + "code": "431028", + "name": "安仁县" + }, + { + "code": "431081", + "name": "资兴市" + } + ] + }, + { + "code": "4311", + "name": "永州市", + "children": [ + { + "code": "431102", + "name": "零陵区" + }, + { + "code": "431103", + "name": "冷水滩区" + }, + { + "code": "431122", + "name": "东安县" + }, + { + "code": "431123", + "name": "双牌县" + }, + { + "code": "431124", + "name": "道县" + }, + { + "code": "431125", + "name": "江永县" + }, + { + "code": "431126", + "name": "宁远县" + }, + { + "code": "431127", + "name": "蓝山县" + }, + { + "code": "431128", + "name": "新田县" + }, + { + "code": "431129", + "name": "江华瑶族自治县" + }, + { + "code": "431171", + "name": "永州经济技术开发区" + }, + { + "code": "431173", + "name": "永州市回龙圩管理区" + }, + { + "code": "431181", + "name": "祁阳市" + } + ] + }, + { + "code": "4312", + "name": "怀化市", + "children": [ + { + "code": "431202", + "name": "鹤城区" + }, + { + "code": "431221", + "name": "中方县" + }, + { + "code": "431222", + "name": "沅陵县" + }, + { + "code": "431223", + "name": "辰溪县" + }, + { + "code": "431224", + "name": "溆浦县" + }, + { + "code": "431225", + "name": "会同县" + }, + { + "code": "431226", + "name": "麻阳苗族自治县" + }, + { + "code": "431227", + "name": "新晃侗族自治县" + }, + { + "code": "431228", + "name": "芷江侗族自治县" + }, + { + "code": "431229", + "name": "靖州苗族侗族自治县" + }, + { + "code": "431230", + "name": "通道侗族自治县" + }, + { + "code": "431271", + "name": "怀化市洪江管理区" + }, + { + "code": "431281", + "name": "洪江市" + } + ] + }, + { + "code": "4313", + "name": "娄底市", + "children": [ + { + "code": "431302", + "name": "娄星区" + }, + { + "code": "431321", + "name": "双峰县" + }, + { + "code": "431322", + "name": "新化县" + }, + { + "code": "431381", + "name": "冷水江市" + }, + { + "code": "431382", + "name": "涟源市" + } + ] + }, + { + "code": "4331", + "name": "湘西土家族苗族自治州", + "children": [ + { + "code": "433101", + "name": "吉首市" + }, + { + "code": "433122", + "name": "泸溪县" + }, + { + "code": "433123", + "name": "凤凰县" + }, + { + "code": "433124", + "name": "花垣县" + }, + { + "code": "433125", + "name": "保靖县" + }, + { + "code": "433126", + "name": "古丈县" + }, + { + "code": "433127", + "name": "永顺县" + }, + { + "code": "433130", + "name": "龙山县" + } + ] + } + ] + }, + { + "code": "44", + "name": "广东省", + "children": [ + { + "code": "4401", + "name": "广州市", + "children": [ + { + "code": "440103", + "name": "荔湾区" + }, + { + "code": "440104", + "name": "越秀区" + }, + { + "code": "440105", + "name": "海珠区" + }, + { + "code": "440106", + "name": "天河区" + }, + { + "code": "440111", + "name": "白云区" + }, + { + "code": "440112", + "name": "黄埔区" + }, + { + "code": "440113", + "name": "番禺区" + }, + { + "code": "440114", + "name": "花都区" + }, + { + "code": "440115", + "name": "南沙区" + }, + { + "code": "440117", + "name": "从化区" + }, + { + "code": "440118", + "name": "增城区" + } + ] + }, + { + "code": "4402", + "name": "韶关市", + "children": [ + { + "code": "440203", + "name": "武江区" + }, + { + "code": "440204", + "name": "浈江区" + }, + { + "code": "440205", + "name": "曲江区" + }, + { + "code": "440222", + "name": "始兴县" + }, + { + "code": "440224", + "name": "仁化县" + }, + { + "code": "440229", + "name": "翁源县" + }, + { + "code": "440232", + "name": "乳源瑶族自治县" + }, + { + "code": "440233", + "name": "新丰县" + }, + { + "code": "440281", + "name": "乐昌市" + }, + { + "code": "440282", + "name": "南雄市" + } + ] + }, + { + "code": "4403", + "name": "深圳市", + "children": [ + { + "code": "440303", + "name": "罗湖区" + }, + { + "code": "440304", + "name": "福田区" + }, + { + "code": "440305", + "name": "南山区" + }, + { + "code": "440306", + "name": "宝安区" + }, + { + "code": "440307", + "name": "龙岗区" + }, + { + "code": "440308", + "name": "盐田区" + }, + { + "code": "440309", + "name": "龙华区" + }, + { + "code": "440310", + "name": "坪山区" + }, + { + "code": "440311", + "name": "光明区" + } + ] + }, + { + "code": "4404", + "name": "珠海市", + "children": [ + { + "code": "440402", + "name": "香洲区" + }, + { + "code": "440403", + "name": "斗门区" + }, + { + "code": "440404", + "name": "金湾区" + } + ] + }, + { + "code": "4405", + "name": "汕头市", + "children": [ + { + "code": "440507", + "name": "龙湖区" + }, + { + "code": "440511", + "name": "金平区" + }, + { + "code": "440512", + "name": "濠江区" + }, + { + "code": "440513", + "name": "潮阳区" + }, + { + "code": "440514", + "name": "潮南区" + }, + { + "code": "440515", + "name": "澄海区" + }, + { + "code": "440523", + "name": "南澳县" + } + ] + }, + { + "code": "4406", + "name": "佛山市", + "children": [ + { + "code": "440604", + "name": "禅城区" + }, + { + "code": "440605", + "name": "南海区" + }, + { + "code": "440606", + "name": "顺德区" + }, + { + "code": "440607", + "name": "三水区" + }, + { + "code": "440608", + "name": "高明区" + } + ] + }, + { + "code": "4407", + "name": "江门市", + "children": [ + { + "code": "440703", + "name": "蓬江区" + }, + { + "code": "440704", + "name": "江海区" + }, + { + "code": "440705", + "name": "新会区" + }, + { + "code": "440781", + "name": "台山市" + }, + { + "code": "440783", + "name": "开平市" + }, + { + "code": "440784", + "name": "鹤山市" + }, + { + "code": "440785", + "name": "恩平市" + } + ] + }, + { + "code": "4408", + "name": "湛江市", + "children": [ + { + "code": "440802", + "name": "赤坎区" + }, + { + "code": "440803", + "name": "霞山区" + }, + { + "code": "440804", + "name": "坡头区" + }, + { + "code": "440811", + "name": "麻章区" + }, + { + "code": "440823", + "name": "遂溪县" + }, + { + "code": "440825", + "name": "徐闻县" + }, + { + "code": "440881", + "name": "廉江市" + }, + { + "code": "440882", + "name": "雷州市" + }, + { + "code": "440883", + "name": "吴川市" + } + ] + }, + { + "code": "4409", + "name": "茂名市", + "children": [ + { + "code": "440902", + "name": "茂南区" + }, + { + "code": "440904", + "name": "电白区" + }, + { + "code": "440981", + "name": "高州市" + }, + { + "code": "440982", + "name": "化州市" + }, + { + "code": "440983", + "name": "信宜市" + } + ] + }, + { + "code": "4412", + "name": "肇庆市", + "children": [ + { + "code": "441202", + "name": "端州区" + }, + { + "code": "441203", + "name": "鼎湖区" + }, + { + "code": "441204", + "name": "高要区" + }, + { + "code": "441223", + "name": "广宁县" + }, + { + "code": "441224", + "name": "怀集县" + }, + { + "code": "441225", + "name": "封开县" + }, + { + "code": "441226", + "name": "德庆县" + }, + { + "code": "441284", + "name": "四会市" + } + ] + }, + { + "code": "4413", + "name": "惠州市", + "children": [ + { + "code": "441302", + "name": "惠城区" + }, + { + "code": "441303", + "name": "惠阳区" + }, + { + "code": "441322", + "name": "博罗县" + }, + { + "code": "441323", + "name": "惠东县" + }, + { + "code": "441324", + "name": "龙门县" + } + ] + }, + { + "code": "4414", + "name": "梅州市", + "children": [ + { + "code": "441402", + "name": "梅江区" + }, + { + "code": "441403", + "name": "梅县区" + }, + { + "code": "441422", + "name": "大埔县" + }, + { + "code": "441423", + "name": "丰顺县" + }, + { + "code": "441424", + "name": "五华县" + }, + { + "code": "441426", + "name": "平远县" + }, + { + "code": "441427", + "name": "蕉岭县" + }, + { + "code": "441481", + "name": "兴宁市" + } + ] + }, + { + "code": "4415", + "name": "汕尾市", + "children": [ + { + "code": "441502", + "name": "城区" + }, + { + "code": "441521", + "name": "海丰县" + }, + { + "code": "441523", + "name": "陆河县" + }, + { + "code": "441581", + "name": "陆丰市" + } + ] + }, + { + "code": "4416", + "name": "河源市", + "children": [ + { + "code": "441602", + "name": "源城区" + }, + { + "code": "441621", + "name": "紫金县" + }, + { + "code": "441622", + "name": "龙川县" + }, + { + "code": "441623", + "name": "连平县" + }, + { + "code": "441624", + "name": "和平县" + }, + { + "code": "441625", + "name": "东源县" + } + ] + }, + { + "code": "4417", + "name": "阳江市", + "children": [ + { + "code": "441702", + "name": "江城区" + }, + { + "code": "441704", + "name": "阳东区" + }, + { + "code": "441721", + "name": "阳西县" + }, + { + "code": "441781", + "name": "阳春市" + } + ] + }, + { + "code": "4418", + "name": "清远市", + "children": [ + { + "code": "441802", + "name": "清城区" + }, + { + "code": "441803", + "name": "清新区" + }, + { + "code": "441821", + "name": "佛冈县" + }, + { + "code": "441823", + "name": "阳山县" + }, + { + "code": "441825", + "name": "连山壮族瑶族自治县" + }, + { + "code": "441826", + "name": "连南瑶族自治县" + }, + { + "code": "441881", + "name": "英德市" + }, + { + "code": "441882", + "name": "连州市" + } + ] + }, + { + "code": "4419", + "name": "东莞市", + "children": [ + { + "code": "441900003", + "name": "东城街道" + }, + { + "code": "441900004", + "name": "南城街道" + }, + { + "code": "441900005", + "name": "万江街道" + }, + { + "code": "441900006", + "name": "莞城街道" + }, + { + "code": "441900101", + "name": "石碣镇" + }, + { + "code": "441900102", + "name": "石龙镇" + }, + { + "code": "441900103", + "name": "茶山镇" + }, + { + "code": "441900104", + "name": "石排镇" + }, + { + "code": "441900105", + "name": "企石镇" + }, + { + "code": "441900106", + "name": "横沥镇" + }, + { + "code": "441900107", + "name": "桥头镇" + }, + { + "code": "441900108", + "name": "谢岗镇" + }, + { + "code": "441900109", + "name": "东坑镇" + }, + { + "code": "441900110", + "name": "常平镇" + }, + { + "code": "441900111", + "name": "寮步镇" + }, + { + "code": "441900112", + "name": "樟木头镇" + }, + { + "code": "441900113", + "name": "大朗镇" + }, + { + "code": "441900114", + "name": "黄江镇" + }, + { + "code": "441900115", + "name": "清溪镇" + }, + { + "code": "441900116", + "name": "塘厦镇" + }, + { + "code": "441900117", + "name": "凤岗镇" + }, + { + "code": "441900118", + "name": "大岭山镇" + }, + { + "code": "441900119", + "name": "长安镇" + }, + { + "code": "441900121", + "name": "虎门镇" + }, + { + "code": "441900122", + "name": "厚街镇" + }, + { + "code": "441900123", + "name": "沙田镇" + }, + { + "code": "441900124", + "name": "道滘镇" + }, + { + "code": "441900125", + "name": "洪梅镇" + }, + { + "code": "441900126", + "name": "麻涌镇" + }, + { + "code": "441900127", + "name": "望牛墩镇" + }, + { + "code": "441900128", + "name": "中堂镇" + }, + { + "code": "441900129", + "name": "高埗镇" + }, + { + "code": "441900401", + "name": "松山湖" + }, + { + "code": "441900402", + "name": "东莞港" + }, + { + "code": "441900403", + "name": "东莞生态园" + }, + { + "code": "441900404", + "name": "东莞滨海湾新区" + } + ] + }, + { + "code": "4420", + "name": "中山市", + "children": [ + { + "code": "442000001", + "name": "石岐街道" + }, + { + "code": "442000002", + "name": "东区街道" + }, + { + "code": "442000003", + "name": "中山港街道" + }, + { + "code": "442000004", + "name": "西区街道" + }, + { + "code": "442000005", + "name": "南区街道" + }, + { + "code": "442000006", + "name": "五桂山街道" + }, + { + "code": "442000007", + "name": "民众街道" + }, + { + "code": "442000008", + "name": "南朗街道" + }, + { + "code": "442000101", + "name": "黄圃镇" + }, + { + "code": "442000103", + "name": "东凤镇" + }, + { + "code": "442000105", + "name": "古镇镇" + }, + { + "code": "442000106", + "name": "沙溪镇" + }, + { + "code": "442000107", + "name": "坦洲镇" + }, + { + "code": "442000108", + "name": "港口镇" + }, + { + "code": "442000109", + "name": "三角镇" + }, + { + "code": "442000110", + "name": "横栏镇" + }, + { + "code": "442000111", + "name": "南头镇" + }, + { + "code": "442000112", + "name": "阜沙镇" + }, + { + "code": "442000114", + "name": "三乡镇" + }, + { + "code": "442000115", + "name": "板芙镇" + }, + { + "code": "442000116", + "name": "大涌镇" + }, + { + "code": "442000117", + "name": "神湾镇" + }, + { + "code": "442000118", + "name": "小榄镇" + } + ] + }, + { + "code": "4451", + "name": "潮州市", + "children": [ + { + "code": "445102", + "name": "湘桥区" + }, + { + "code": "445103", + "name": "潮安区" + }, + { + "code": "445122", + "name": "饶平县" + } + ] + }, + { + "code": "4452", + "name": "揭阳市", + "children": [ + { + "code": "445202", + "name": "榕城区" + }, + { + "code": "445203", + "name": "揭东区" + }, + { + "code": "445222", + "name": "揭西县" + }, + { + "code": "445224", + "name": "惠来县" + }, + { + "code": "445281", + "name": "普宁市" + } + ] + }, + { + "code": "4453", + "name": "云浮市", + "children": [ + { + "code": "445302", + "name": "云城区" + }, + { + "code": "445303", + "name": "云安区" + }, + { + "code": "445321", + "name": "新兴县" + }, + { + "code": "445322", + "name": "郁南县" + }, + { + "code": "445381", + "name": "罗定市" + } + ] + } + ] + }, + { + "code": "45", + "name": "广西壮族自治区", + "children": [ + { + "code": "4501", + "name": "南宁市", + "children": [ + { + "code": "450102", + "name": "兴宁区" + }, + { + "code": "450103", + "name": "青秀区" + }, + { + "code": "450105", + "name": "江南区" + }, + { + "code": "450107", + "name": "西乡塘区" + }, + { + "code": "450108", + "name": "良庆区" + }, + { + "code": "450109", + "name": "邕宁区" + }, + { + "code": "450110", + "name": "武鸣区" + }, + { + "code": "450123", + "name": "隆安县" + }, + { + "code": "450124", + "name": "马山县" + }, + { + "code": "450125", + "name": "上林县" + }, + { + "code": "450126", + "name": "宾阳县" + }, + { + "code": "450181", + "name": "横州市" + } + ] + }, + { + "code": "4502", + "name": "柳州市", + "children": [ + { + "code": "450202", + "name": "城中区" + }, + { + "code": "450203", + "name": "鱼峰区" + }, + { + "code": "450204", + "name": "柳南区" + }, + { + "code": "450205", + "name": "柳北区" + }, + { + "code": "450206", + "name": "柳江区" + }, + { + "code": "450222", + "name": "柳城县" + }, + { + "code": "450223", + "name": "鹿寨县" + }, + { + "code": "450224", + "name": "融安县" + }, + { + "code": "450225", + "name": "融水苗族自治县" + }, + { + "code": "450226", + "name": "三江侗族自治县" + } + ] + }, + { + "code": "4503", + "name": "桂林市", + "children": [ + { + "code": "450302", + "name": "秀峰区" + }, + { + "code": "450303", + "name": "叠彩区" + }, + { + "code": "450304", + "name": "象山区" + }, + { + "code": "450305", + "name": "七星区" + }, + { + "code": "450311", + "name": "雁山区" + }, + { + "code": "450312", + "name": "临桂区" + }, + { + "code": "450321", + "name": "阳朔县" + }, + { + "code": "450323", + "name": "灵川县" + }, + { + "code": "450324", + "name": "全州县" + }, + { + "code": "450325", + "name": "兴安县" + }, + { + "code": "450326", + "name": "永福县" + }, + { + "code": "450327", + "name": "灌阳县" + }, + { + "code": "450328", + "name": "龙胜各族自治县" + }, + { + "code": "450329", + "name": "资源县" + }, + { + "code": "450330", + "name": "平乐县" + }, + { + "code": "450332", + "name": "恭城瑶族自治县" + }, + { + "code": "450381", + "name": "荔浦市" + } + ] + }, + { + "code": "4504", + "name": "梧州市", + "children": [ + { + "code": "450403", + "name": "万秀区" + }, + { + "code": "450405", + "name": "长洲区" + }, + { + "code": "450406", + "name": "龙圩区" + }, + { + "code": "450421", + "name": "苍梧县" + }, + { + "code": "450422", + "name": "藤县" + }, + { + "code": "450423", + "name": "蒙山县" + }, + { + "code": "450481", + "name": "岑溪市" + } + ] + }, + { + "code": "4505", + "name": "北海市", + "children": [ + { + "code": "450502", + "name": "海城区" + }, + { + "code": "450503", + "name": "银海区" + }, + { + "code": "450512", + "name": "铁山港区" + }, + { + "code": "450521", + "name": "合浦县" + } + ] + }, + { + "code": "4506", + "name": "防城港市", + "children": [ + { + "code": "450602", + "name": "港口区" + }, + { + "code": "450603", + "name": "防城区" + }, + { + "code": "450621", + "name": "上思县" + }, + { + "code": "450681", + "name": "东兴市" + } + ] + }, + { + "code": "4507", + "name": "钦州市", + "children": [ + { + "code": "450702", + "name": "钦南区" + }, + { + "code": "450703", + "name": "钦北区" + }, + { + "code": "450721", + "name": "灵山县" + }, + { + "code": "450722", + "name": "浦北县" + } + ] + }, + { + "code": "4508", + "name": "贵港市", + "children": [ + { + "code": "450802", + "name": "港北区" + }, + { + "code": "450803", + "name": "港南区" + }, + { + "code": "450804", + "name": "覃塘区" + }, + { + "code": "450821", + "name": "平南县" + }, + { + "code": "450881", + "name": "桂平市" + } + ] + }, + { + "code": "4509", + "name": "玉林市", + "children": [ + { + "code": "450902", + "name": "玉州区" + }, + { + "code": "450903", + "name": "福绵区" + }, + { + "code": "450921", + "name": "容县" + }, + { + "code": "450922", + "name": "陆川县" + }, + { + "code": "450923", + "name": "博白县" + }, + { + "code": "450924", + "name": "兴业县" + }, + { + "code": "450981", + "name": "北流市" + } + ] + }, + { + "code": "4510", + "name": "百色市", + "children": [ + { + "code": "451002", + "name": "右江区" + }, + { + "code": "451003", + "name": "田阳区" + }, + { + "code": "451022", + "name": "田东县" + }, + { + "code": "451024", + "name": "德保县" + }, + { + "code": "451026", + "name": "那坡县" + }, + { + "code": "451027", + "name": "凌云县" + }, + { + "code": "451028", + "name": "乐业县" + }, + { + "code": "451029", + "name": "田林县" + }, + { + "code": "451030", + "name": "西林县" + }, + { + "code": "451031", + "name": "隆林各族自治县" + }, + { + "code": "451081", + "name": "靖西市" + }, + { + "code": "451082", + "name": "平果市" + } + ] + }, + { + "code": "4511", + "name": "贺州市", + "children": [ + { + "code": "451102", + "name": "八步区" + }, + { + "code": "451103", + "name": "平桂区" + }, + { + "code": "451121", + "name": "昭平县" + }, + { + "code": "451122", + "name": "钟山县" + }, + { + "code": "451123", + "name": "富川瑶族自治县" + } + ] + }, + { + "code": "4512", + "name": "河池市", + "children": [ + { + "code": "451202", + "name": "金城江区" + }, + { + "code": "451203", + "name": "宜州区" + }, + { + "code": "451221", + "name": "南丹县" + }, + { + "code": "451222", + "name": "天峨县" + }, + { + "code": "451223", + "name": "凤山县" + }, + { + "code": "451224", + "name": "东兰县" + }, + { + "code": "451225", + "name": "罗城仫佬族自治县" + }, + { + "code": "451226", + "name": "环江毛南族自治县" + }, + { + "code": "451227", + "name": "巴马瑶族自治县" + }, + { + "code": "451228", + "name": "都安瑶族自治县" + }, + { + "code": "451229", + "name": "大化瑶族自治县" + } + ] + }, + { + "code": "4513", + "name": "来宾市", + "children": [ + { + "code": "451302", + "name": "兴宾区" + }, + { + "code": "451321", + "name": "忻城县" + }, + { + "code": "451322", + "name": "象州县" + }, + { + "code": "451323", + "name": "武宣县" + }, + { + "code": "451324", + "name": "金秀瑶族自治县" + }, + { + "code": "451381", + "name": "合山市" + } + ] + }, + { + "code": "4514", + "name": "崇左市", + "children": [ + { + "code": "451402", + "name": "江州区" + }, + { + "code": "451421", + "name": "扶绥县" + }, + { + "code": "451422", + "name": "宁明县" + }, + { + "code": "451423", + "name": "龙州县" + }, + { + "code": "451424", + "name": "大新县" + }, + { + "code": "451425", + "name": "天等县" + }, + { + "code": "451481", + "name": "凭祥市" + } + ] + } + ] + }, + { + "code": "46", + "name": "海南省", + "children": [ + { + "code": "4601", + "name": "海口市", + "children": [ + { + "code": "460105", + "name": "秀英区" + }, + { + "code": "460106", + "name": "龙华区" + }, + { + "code": "460107", + "name": "琼山区" + }, + { + "code": "460108", + "name": "美兰区" + } + ] + }, + { + "code": "4602", + "name": "三亚市", + "children": [ + { + "code": "460202", + "name": "海棠区" + }, + { + "code": "460203", + "name": "吉阳区" + }, + { + "code": "460204", + "name": "天涯区" + }, + { + "code": "460205", + "name": "崖州区" + } + ] + }, + { + "code": "4603", + "name": "三沙市", + "children": [ + { + "code": "460321", + "name": "西沙群岛" + }, + { + "code": "460322", + "name": "南沙群岛" + }, + { + "code": "460323", + "name": "中沙群岛的岛礁及其海域" + } + ] + }, + { + "code": "4604", + "name": "儋州市", + "children": [ + { + "code": "460400100", + "name": "那大镇" + }, + { + "code": "460400101", + "name": "和庆镇" + }, + { + "code": "460400102", + "name": "南丰镇" + }, + { + "code": "460400103", + "name": "大成镇" + }, + { + "code": "460400104", + "name": "雅星镇" + }, + { + "code": "460400105", + "name": "兰洋镇" + }, + { + "code": "460400106", + "name": "光村镇" + }, + { + "code": "460400107", + "name": "木棠镇" + }, + { + "code": "460400108", + "name": "海头镇" + }, + { + "code": "460400109", + "name": "峨蔓镇" + }, + { + "code": "460400111", + "name": "王五镇" + }, + { + "code": "460400112", + "name": "白马井镇" + }, + { + "code": "460400113", + "name": "中和镇" + }, + { + "code": "460400114", + "name": "排浦镇" + }, + { + "code": "460400115", + "name": "东成镇" + }, + { + "code": "460400116", + "name": "新州镇" + }, + { + "code": "460400499", + "name": "洋浦经济开发区" + }, + { + "code": "460400500", + "name": "华南热作学院" + } + ] + }, + { + "code": "4690", + "name": "省直辖县级行政区划", + "children": [ + { + "code": "469001", + "name": "五指山市" + }, + { + "code": "469002", + "name": "琼海市" + }, + { + "code": "469005", + "name": "文昌市" + }, + { + "code": "469006", + "name": "万宁市" + }, + { + "code": "469007", + "name": "东方市" + }, + { + "code": "469021", + "name": "定安县" + }, + { + "code": "469022", + "name": "屯昌县" + }, + { + "code": "469023", + "name": "澄迈县" + }, + { + "code": "469024", + "name": "临高县" + }, + { + "code": "469025", + "name": "白沙黎族自治县" + }, + { + "code": "469026", + "name": "昌江黎族自治县" + }, + { + "code": "469027", + "name": "乐东黎族自治县" + }, + { + "code": "469028", + "name": "陵水黎族自治县" + }, + { + "code": "469029", + "name": "保亭黎族苗族自治县" + }, + { + "code": "469030", + "name": "琼中黎族苗族自治县" + } + ] + } + ] + }, + { + "code": "50", + "name": "重庆市", + "children": [ + { + "code": "5001", + "name": "市辖区", + "children": [ + { + "code": "500101", + "name": "万州区" + }, + { + "code": "500102", + "name": "涪陵区" + }, + { + "code": "500103", + "name": "渝中区" + }, + { + "code": "500104", + "name": "大渡口区" + }, + { + "code": "500105", + "name": "江北区" + }, + { + "code": "500106", + "name": "沙坪坝区" + }, + { + "code": "500107", + "name": "九龙坡区" + }, + { + "code": "500108", + "name": "南岸区" + }, + { + "code": "500109", + "name": "北碚区" + }, + { + "code": "500110", + "name": "綦江区" + }, + { + "code": "500111", + "name": "大足区" + }, + { + "code": "500112", + "name": "渝北区" + }, + { + "code": "500113", + "name": "巴南区" + }, + { + "code": "500114", + "name": "黔江区" + }, + { + "code": "500115", + "name": "长寿区" + }, + { + "code": "500116", + "name": "江津区" + }, + { + "code": "500117", + "name": "合川区" + }, + { + "code": "500118", + "name": "永川区" + }, + { + "code": "500119", + "name": "南川区" + }, + { + "code": "500120", + "name": "璧山区" + }, + { + "code": "500151", + "name": "铜梁区" + }, + { + "code": "500152", + "name": "潼南区" + }, + { + "code": "500153", + "name": "荣昌区" + }, + { + "code": "500154", + "name": "开州区" + }, + { + "code": "500155", + "name": "梁平区" + }, + { + "code": "500156", + "name": "武隆区" + } + ] + }, + { + "code": "5002", + "name": "县", + "children": [ + { + "code": "500229", + "name": "城口县" + }, + { + "code": "500230", + "name": "丰都县" + }, + { + "code": "500231", + "name": "垫江县" + }, + { + "code": "500233", + "name": "忠县" + }, + { + "code": "500235", + "name": "云阳县" + }, + { + "code": "500236", + "name": "奉节县" + }, + { + "code": "500237", + "name": "巫山县" + }, + { + "code": "500238", + "name": "巫溪县" + }, + { + "code": "500240", + "name": "石柱土家族自治县" + }, + { + "code": "500241", + "name": "秀山土家族苗族自治县" + }, + { + "code": "500242", + "name": "酉阳土家族苗族自治县" + }, + { + "code": "500243", + "name": "彭水苗族土家族自治县" + } + ] + } + ] + }, + { + "code": "51", + "name": "四川省", + "children": [ + { + "code": "5101", + "name": "成都市", + "children": [ + { + "code": "510104", + "name": "锦江区" + }, + { + "code": "510105", + "name": "青羊区" + }, + { + "code": "510106", + "name": "金牛区" + }, + { + "code": "510107", + "name": "武侯区" + }, + { + "code": "510108", + "name": "成华区" + }, + { + "code": "510112", + "name": "龙泉驿区" + }, + { + "code": "510113", + "name": "青白江区" + }, + { + "code": "510114", + "name": "新都区" + }, + { + "code": "510115", + "name": "温江区" + }, + { + "code": "510116", + "name": "双流区" + }, + { + "code": "510117", + "name": "郫都区" + }, + { + "code": "510118", + "name": "新津区" + }, + { + "code": "510121", + "name": "金堂县" + }, + { + "code": "510129", + "name": "大邑县" + }, + { + "code": "510131", + "name": "蒲江县" + }, + { + "code": "510181", + "name": "都江堰市" + }, + { + "code": "510182", + "name": "彭州市" + }, + { + "code": "510183", + "name": "邛崃市" + }, + { + "code": "510184", + "name": "崇州市" + }, + { + "code": "510185", + "name": "简阳市" + } + ] + }, + { + "code": "5103", + "name": "自贡市", + "children": [ + { + "code": "510302", + "name": "自流井区" + }, + { + "code": "510303", + "name": "贡井区" + }, + { + "code": "510304", + "name": "大安区" + }, + { + "code": "510311", + "name": "沿滩区" + }, + { + "code": "510321", + "name": "荣县" + }, + { + "code": "510322", + "name": "富顺县" + } + ] + }, + { + "code": "5104", + "name": "攀枝花市", + "children": [ + { + "code": "510402", + "name": "东区" + }, + { + "code": "510403", + "name": "西区" + }, + { + "code": "510411", + "name": "仁和区" + }, + { + "code": "510421", + "name": "米易县" + }, + { + "code": "510422", + "name": "盐边县" + } + ] + }, + { + "code": "5105", + "name": "泸州市", + "children": [ + { + "code": "510502", + "name": "江阳区" + }, + { + "code": "510503", + "name": "纳溪区" + }, + { + "code": "510504", + "name": "龙马潭区" + }, + { + "code": "510521", + "name": "泸县" + }, + { + "code": "510522", + "name": "合江县" + }, + { + "code": "510524", + "name": "叙永县" + }, + { + "code": "510525", + "name": "古蔺县" + } + ] + }, + { + "code": "5106", + "name": "德阳市", + "children": [ + { + "code": "510603", + "name": "旌阳区" + }, + { + "code": "510604", + "name": "罗江区" + }, + { + "code": "510623", + "name": "中江县" + }, + { + "code": "510681", + "name": "广汉市" + }, + { + "code": "510682", + "name": "什邡市" + }, + { + "code": "510683", + "name": "绵竹市" + } + ] + }, + { + "code": "5107", + "name": "绵阳市", + "children": [ + { + "code": "510703", + "name": "涪城区" + }, + { + "code": "510704", + "name": "游仙区" + }, + { + "code": "510705", + "name": "安州区" + }, + { + "code": "510722", + "name": "三台县" + }, + { + "code": "510723", + "name": "盐亭县" + }, + { + "code": "510725", + "name": "梓潼县" + }, + { + "code": "510726", + "name": "北川羌族自治县" + }, + { + "code": "510727", + "name": "平武县" + }, + { + "code": "510781", + "name": "江油市" + } + ] + }, + { + "code": "5108", + "name": "广元市", + "children": [ + { + "code": "510802", + "name": "利州区" + }, + { + "code": "510811", + "name": "昭化区" + }, + { + "code": "510812", + "name": "朝天区" + }, + { + "code": "510821", + "name": "旺苍县" + }, + { + "code": "510822", + "name": "青川县" + }, + { + "code": "510823", + "name": "剑阁县" + }, + { + "code": "510824", + "name": "苍溪县" + } + ] + }, + { + "code": "5109", + "name": "遂宁市", + "children": [ + { + "code": "510903", + "name": "船山区" + }, + { + "code": "510904", + "name": "安居区" + }, + { + "code": "510921", + "name": "蓬溪县" + }, + { + "code": "510923", + "name": "大英县" + }, + { + "code": "510981", + "name": "射洪市" + } + ] + }, + { + "code": "5110", + "name": "内江市", + "children": [ + { + "code": "511002", + "name": "市中区" + }, + { + "code": "511011", + "name": "东兴区" + }, + { + "code": "511024", + "name": "威远县" + }, + { + "code": "511025", + "name": "资中县" + }, + { + "code": "511083", + "name": "隆昌市" + } + ] + }, + { + "code": "5111", + "name": "乐山市", + "children": [ + { + "code": "511102", + "name": "市中区" + }, + { + "code": "511111", + "name": "沙湾区" + }, + { + "code": "511112", + "name": "五通桥区" + }, + { + "code": "511113", + "name": "金口河区" + }, + { + "code": "511123", + "name": "犍为县" + }, + { + "code": "511124", + "name": "井研县" + }, + { + "code": "511126", + "name": "夹江县" + }, + { + "code": "511129", + "name": "沐川县" + }, + { + "code": "511132", + "name": "峨边彝族自治县" + }, + { + "code": "511133", + "name": "马边彝族自治县" + }, + { + "code": "511181", + "name": "峨眉山市" + } + ] + }, + { + "code": "5113", + "name": "南充市", + "children": [ + { + "code": "511302", + "name": "顺庆区" + }, + { + "code": "511303", + "name": "高坪区" + }, + { + "code": "511304", + "name": "嘉陵区" + }, + { + "code": "511321", + "name": "南部县" + }, + { + "code": "511322", + "name": "营山县" + }, + { + "code": "511323", + "name": "蓬安县" + }, + { + "code": "511324", + "name": "仪陇县" + }, + { + "code": "511325", + "name": "西充县" + }, + { + "code": "511381", + "name": "阆中市" + } + ] + }, + { + "code": "5114", + "name": "眉山市", + "children": [ + { + "code": "511402", + "name": "东坡区" + }, + { + "code": "511403", + "name": "彭山区" + }, + { + "code": "511421", + "name": "仁寿县" + }, + { + "code": "511423", + "name": "洪雅县" + }, + { + "code": "511424", + "name": "丹棱县" + }, + { + "code": "511425", + "name": "青神县" + } + ] + }, + { + "code": "5115", + "name": "宜宾市", + "children": [ + { + "code": "511502", + "name": "翠屏区" + }, + { + "code": "511503", + "name": "南溪区" + }, + { + "code": "511504", + "name": "叙州区" + }, + { + "code": "511523", + "name": "江安县" + }, + { + "code": "511524", + "name": "长宁县" + }, + { + "code": "511525", + "name": "高县" + }, + { + "code": "511526", + "name": "珙县" + }, + { + "code": "511527", + "name": "筠连县" + }, + { + "code": "511528", + "name": "兴文县" + }, + { + "code": "511529", + "name": "屏山县" + } + ] + }, + { + "code": "5116", + "name": "广安市", + "children": [ + { + "code": "511602", + "name": "广安区" + }, + { + "code": "511603", + "name": "前锋区" + }, + { + "code": "511621", + "name": "岳池县" + }, + { + "code": "511622", + "name": "武胜县" + }, + { + "code": "511623", + "name": "邻水县" + }, + { + "code": "511681", + "name": "华蓥市" + } + ] + }, + { + "code": "5117", + "name": "达州市", + "children": [ + { + "code": "511702", + "name": "通川区" + }, + { + "code": "511703", + "name": "达川区" + }, + { + "code": "511722", + "name": "宣汉县" + }, + { + "code": "511723", + "name": "开江县" + }, + { + "code": "511724", + "name": "大竹县" + }, + { + "code": "511725", + "name": "渠县" + }, + { + "code": "511781", + "name": "万源市" + } + ] + }, + { + "code": "5118", + "name": "雅安市", + "children": [ + { + "code": "511802", + "name": "雨城区" + }, + { + "code": "511803", + "name": "名山区" + }, + { + "code": "511822", + "name": "荥经县" + }, + { + "code": "511823", + "name": "汉源县" + }, + { + "code": "511824", + "name": "石棉县" + }, + { + "code": "511825", + "name": "天全县" + }, + { + "code": "511826", + "name": "芦山县" + }, + { + "code": "511827", + "name": "宝兴县" + } + ] + }, + { + "code": "5119", + "name": "巴中市", + "children": [ + { + "code": "511902", + "name": "巴州区" + }, + { + "code": "511903", + "name": "恩阳区" + }, + { + "code": "511921", + "name": "通江县" + }, + { + "code": "511922", + "name": "南江县" + }, + { + "code": "511923", + "name": "平昌县" + } + ] + }, + { + "code": "5120", + "name": "资阳市", + "children": [ + { + "code": "512002", + "name": "雁江区" + }, + { + "code": "512021", + "name": "安岳县" + }, + { + "code": "512022", + "name": "乐至县" + } + ] + }, + { + "code": "5132", + "name": "阿坝藏族羌族自治州", + "children": [ + { + "code": "513201", + "name": "马尔康市" + }, + { + "code": "513221", + "name": "汶川县" + }, + { + "code": "513222", + "name": "理县" + }, + { + "code": "513223", + "name": "茂县" + }, + { + "code": "513224", + "name": "松潘县" + }, + { + "code": "513225", + "name": "九寨沟县" + }, + { + "code": "513226", + "name": "金川县" + }, + { + "code": "513227", + "name": "小金县" + }, + { + "code": "513228", + "name": "黑水县" + }, + { + "code": "513230", + "name": "壤塘县" + }, + { + "code": "513231", + "name": "阿坝县" + }, + { + "code": "513232", + "name": "若尔盖县" + }, + { + "code": "513233", + "name": "红原县" + } + ] + }, + { + "code": "5133", + "name": "甘孜藏族自治州", + "children": [ + { + "code": "513301", + "name": "康定市" + }, + { + "code": "513322", + "name": "泸定县" + }, + { + "code": "513323", + "name": "丹巴县" + }, + { + "code": "513324", + "name": "九龙县" + }, + { + "code": "513325", + "name": "雅江县" + }, + { + "code": "513326", + "name": "道孚县" + }, + { + "code": "513327", + "name": "炉霍县" + }, + { + "code": "513328", + "name": "甘孜县" + }, + { + "code": "513329", + "name": "新龙县" + }, + { + "code": "513330", + "name": "德格县" + }, + { + "code": "513331", + "name": "白玉县" + }, + { + "code": "513332", + "name": "石渠县" + }, + { + "code": "513333", + "name": "色达县" + }, + { + "code": "513334", + "name": "理塘县" + }, + { + "code": "513335", + "name": "巴塘县" + }, + { + "code": "513336", + "name": "乡城县" + }, + { + "code": "513337", + "name": "稻城县" + }, + { + "code": "513338", + "name": "得荣县" + } + ] + }, + { + "code": "5134", + "name": "凉山彝族自治州", + "children": [ + { + "code": "513401", + "name": "西昌市" + }, + { + "code": "513402", + "name": "会理市" + }, + { + "code": "513422", + "name": "木里藏族自治县" + }, + { + "code": "513423", + "name": "盐源县" + }, + { + "code": "513424", + "name": "德昌县" + }, + { + "code": "513426", + "name": "会东县" + }, + { + "code": "513427", + "name": "宁南县" + }, + { + "code": "513428", + "name": "普格县" + }, + { + "code": "513429", + "name": "布拖县" + }, + { + "code": "513430", + "name": "金阳县" + }, + { + "code": "513431", + "name": "昭觉县" + }, + { + "code": "513432", + "name": "喜德县" + }, + { + "code": "513433", + "name": "冕宁县" + }, + { + "code": "513434", + "name": "越西县" + }, + { + "code": "513435", + "name": "甘洛县" + }, + { + "code": "513436", + "name": "美姑县" + }, + { + "code": "513437", + "name": "雷波县" + } + ] + } + ] + }, + { + "code": "52", + "name": "贵州省", + "children": [ + { + "code": "5201", + "name": "贵阳市", + "children": [ + { + "code": "520102", + "name": "南明区" + }, + { + "code": "520103", + "name": "云岩区" + }, + { + "code": "520111", + "name": "花溪区" + }, + { + "code": "520112", + "name": "乌当区" + }, + { + "code": "520113", + "name": "白云区" + }, + { + "code": "520115", + "name": "观山湖区" + }, + { + "code": "520121", + "name": "开阳县" + }, + { + "code": "520122", + "name": "息烽县" + }, + { + "code": "520123", + "name": "修文县" + }, + { + "code": "520181", + "name": "清镇市" + } + ] + }, + { + "code": "5202", + "name": "六盘水市", + "children": [ + { + "code": "520201", + "name": "钟山区" + }, + { + "code": "520203", + "name": "六枝特区" + }, + { + "code": "520204", + "name": "水城区" + }, + { + "code": "520281", + "name": "盘州市" + } + ] + }, + { + "code": "5203", + "name": "遵义市", + "children": [ + { + "code": "520302", + "name": "红花岗区" + }, + { + "code": "520303", + "name": "汇川区" + }, + { + "code": "520304", + "name": "播州区" + }, + { + "code": "520322", + "name": "桐梓县" + }, + { + "code": "520323", + "name": "绥阳县" + }, + { + "code": "520324", + "name": "正安县" + }, + { + "code": "520325", + "name": "道真仡佬族苗族自治县" + }, + { + "code": "520326", + "name": "务川仡佬族苗族自治县" + }, + { + "code": "520327", + "name": "凤冈县" + }, + { + "code": "520328", + "name": "湄潭县" + }, + { + "code": "520329", + "name": "余庆县" + }, + { + "code": "520330", + "name": "习水县" + }, + { + "code": "520381", + "name": "赤水市" + }, + { + "code": "520382", + "name": "仁怀市" + } + ] + }, + { + "code": "5204", + "name": "安顺市", + "children": [ + { + "code": "520402", + "name": "西秀区" + }, + { + "code": "520403", + "name": "平坝区" + }, + { + "code": "520422", + "name": "普定县" + }, + { + "code": "520423", + "name": "镇宁布依族苗族自治县" + }, + { + "code": "520424", + "name": "关岭布依族苗族自治县" + }, + { + "code": "520425", + "name": "紫云苗族布依族自治县" + } + ] + }, + { + "code": "5205", + "name": "毕节市", + "children": [ + { + "code": "520502", + "name": "七星关区" + }, + { + "code": "520521", + "name": "大方县" + }, + { + "code": "520523", + "name": "金沙县" + }, + { + "code": "520524", + "name": "织金县" + }, + { + "code": "520525", + "name": "纳雍县" + }, + { + "code": "520526", + "name": "威宁彝族回族苗族自治县" + }, + { + "code": "520527", + "name": "赫章县" + }, + { + "code": "520581", + "name": "黔西市" + } + ] + }, + { + "code": "5206", + "name": "铜仁市", + "children": [ + { + "code": "520602", + "name": "碧江区" + }, + { + "code": "520603", + "name": "万山区" + }, + { + "code": "520621", + "name": "江口县" + }, + { + "code": "520622", + "name": "玉屏侗族自治县" + }, + { + "code": "520623", + "name": "石阡县" + }, + { + "code": "520624", + "name": "思南县" + }, + { + "code": "520625", + "name": "印江土家族苗族自治县" + }, + { + "code": "520626", + "name": "德江县" + }, + { + "code": "520627", + "name": "沿河土家族自治县" + }, + { + "code": "520628", + "name": "松桃苗族自治县" + } + ] + }, + { + "code": "5223", + "name": "黔西南布依族苗族自治州", + "children": [ + { + "code": "522301", + "name": "兴义市" + }, + { + "code": "522302", + "name": "兴仁市" + }, + { + "code": "522323", + "name": "普安县" + }, + { + "code": "522324", + "name": "晴隆县" + }, + { + "code": "522325", + "name": "贞丰县" + }, + { + "code": "522326", + "name": "望谟县" + }, + { + "code": "522327", + "name": "册亨县" + }, + { + "code": "522328", + "name": "安龙县" + } + ] + }, + { + "code": "5226", + "name": "黔东南苗族侗族自治州", + "children": [ + { + "code": "522601", + "name": "凯里市" + }, + { + "code": "522622", + "name": "黄平县" + }, + { + "code": "522623", + "name": "施秉县" + }, + { + "code": "522624", + "name": "三穗县" + }, + { + "code": "522625", + "name": "镇远县" + }, + { + "code": "522626", + "name": "岑巩县" + }, + { + "code": "522627", + "name": "天柱县" + }, + { + "code": "522628", + "name": "锦屏县" + }, + { + "code": "522629", + "name": "剑河县" + }, + { + "code": "522630", + "name": "台江县" + }, + { + "code": "522631", + "name": "黎平县" + }, + { + "code": "522632", + "name": "榕江县" + }, + { + "code": "522633", + "name": "从江县" + }, + { + "code": "522634", + "name": "雷山县" + }, + { + "code": "522635", + "name": "麻江县" + }, + { + "code": "522636", + "name": "丹寨县" + } + ] + }, + { + "code": "5227", + "name": "黔南布依族苗族自治州", + "children": [ + { + "code": "522701", + "name": "都匀市" + }, + { + "code": "522702", + "name": "福泉市" + }, + { + "code": "522722", + "name": "荔波县" + }, + { + "code": "522723", + "name": "贵定县" + }, + { + "code": "522725", + "name": "瓮安县" + }, + { + "code": "522726", + "name": "独山县" + }, + { + "code": "522727", + "name": "平塘县" + }, + { + "code": "522728", + "name": "罗甸县" + }, + { + "code": "522729", + "name": "长顺县" + }, + { + "code": "522730", + "name": "龙里县" + }, + { + "code": "522731", + "name": "惠水县" + }, + { + "code": "522732", + "name": "三都水族自治县" + } + ] + } + ] + }, + { + "code": "53", + "name": "云南省", + "children": [ + { + "code": "5301", + "name": "昆明市", + "children": [ + { + "code": "530102", + "name": "五华区" + }, + { + "code": "530103", + "name": "盘龙区" + }, + { + "code": "530111", + "name": "官渡区" + }, + { + "code": "530112", + "name": "西山区" + }, + { + "code": "530113", + "name": "东川区" + }, + { + "code": "530114", + "name": "呈贡区" + }, + { + "code": "530115", + "name": "晋宁区" + }, + { + "code": "530124", + "name": "富民县" + }, + { + "code": "530125", + "name": "宜良县" + }, + { + "code": "530126", + "name": "石林彝族自治县" + }, + { + "code": "530127", + "name": "嵩明县" + }, + { + "code": "530128", + "name": "禄劝彝族苗族自治县" + }, + { + "code": "530129", + "name": "寻甸回族彝族自治县" + }, + { + "code": "530181", + "name": "安宁市" + } + ] + }, + { + "code": "5303", + "name": "曲靖市", + "children": [ + { + "code": "530302", + "name": "麒麟区" + }, + { + "code": "530303", + "name": "沾益区" + }, + { + "code": "530304", + "name": "马龙区" + }, + { + "code": "530322", + "name": "陆良县" + }, + { + "code": "530323", + "name": "师宗县" + }, + { + "code": "530324", + "name": "罗平县" + }, + { + "code": "530325", + "name": "富源县" + }, + { + "code": "530326", + "name": "会泽县" + }, + { + "code": "530381", + "name": "宣威市" + } + ] + }, + { + "code": "5304", + "name": "玉溪市", + "children": [ + { + "code": "530402", + "name": "红塔区" + }, + { + "code": "530403", + "name": "江川区" + }, + { + "code": "530423", + "name": "通海县" + }, + { + "code": "530424", + "name": "华宁县" + }, + { + "code": "530425", + "name": "易门县" + }, + { + "code": "530426", + "name": "峨山彝族自治县" + }, + { + "code": "530427", + "name": "新平彝族傣族自治县" + }, + { + "code": "530428", + "name": "元江哈尼族彝族傣族自治县" + }, + { + "code": "530481", + "name": "澄江市" + } + ] + }, + { + "code": "5305", + "name": "保山市", + "children": [ + { + "code": "530502", + "name": "隆阳区" + }, + { + "code": "530521", + "name": "施甸县" + }, + { + "code": "530523", + "name": "龙陵县" + }, + { + "code": "530524", + "name": "昌宁县" + }, + { + "code": "530581", + "name": "腾冲市" + } + ] + }, + { + "code": "5306", + "name": "昭通市", + "children": [ + { + "code": "530602", + "name": "昭阳区" + }, + { + "code": "530621", + "name": "鲁甸县" + }, + { + "code": "530622", + "name": "巧家县" + }, + { + "code": "530623", + "name": "盐津县" + }, + { + "code": "530624", + "name": "大关县" + }, + { + "code": "530625", + "name": "永善县" + }, + { + "code": "530626", + "name": "绥江县" + }, + { + "code": "530627", + "name": "镇雄县" + }, + { + "code": "530628", + "name": "彝良县" + }, + { + "code": "530629", + "name": "威信县" + }, + { + "code": "530681", + "name": "水富市" + } + ] + }, + { + "code": "5307", + "name": "丽江市", + "children": [ + { + "code": "530702", + "name": "古城区" + }, + { + "code": "530721", + "name": "玉龙纳西族自治县" + }, + { + "code": "530722", + "name": "永胜县" + }, + { + "code": "530723", + "name": "华坪县" + }, + { + "code": "530724", + "name": "宁蒗彝族自治县" + } + ] + }, + { + "code": "5308", + "name": "普洱市", + "children": [ + { + "code": "530802", + "name": "思茅区" + }, + { + "code": "530821", + "name": "宁洱哈尼族彝族自治县" + }, + { + "code": "530822", + "name": "墨江哈尼族自治县" + }, + { + "code": "530823", + "name": "景东彝族自治县" + }, + { + "code": "530824", + "name": "景谷傣族彝族自治县" + }, + { + "code": "530825", + "name": "镇沅彝族哈尼族拉祜族自治县" + }, + { + "code": "530826", + "name": "江城哈尼族彝族自治县" + }, + { + "code": "530827", + "name": "孟连傣族拉祜族佤族自治县" + }, + { + "code": "530828", + "name": "澜沧拉祜族自治县" + }, + { + "code": "530829", + "name": "西盟佤族自治县" + } + ] + }, + { + "code": "5309", + "name": "临沧市", + "children": [ + { + "code": "530902", + "name": "临翔区" + }, + { + "code": "530921", + "name": "凤庆县" + }, + { + "code": "530922", + "name": "云县" + }, + { + "code": "530923", + "name": "永德县" + }, + { + "code": "530924", + "name": "镇康县" + }, + { + "code": "530925", + "name": "双江拉祜族佤族布朗族傣族自治县" + }, + { + "code": "530926", + "name": "耿马傣族佤族自治县" + }, + { + "code": "530927", + "name": "沧源佤族自治县" + } + ] + }, + { + "code": "5323", + "name": "楚雄彝族自治州", + "children": [ + { + "code": "532301", + "name": "楚雄市" + }, + { + "code": "532302", + "name": "禄丰市" + }, + { + "code": "532322", + "name": "双柏县" + }, + { + "code": "532323", + "name": "牟定县" + }, + { + "code": "532324", + "name": "南华县" + }, + { + "code": "532325", + "name": "姚安县" + }, + { + "code": "532326", + "name": "大姚县" + }, + { + "code": "532327", + "name": "永仁县" + }, + { + "code": "532328", + "name": "元谋县" + }, + { + "code": "532329", + "name": "武定县" + } + ] + }, + { + "code": "5325", + "name": "红河哈尼族彝族自治州", + "children": [ + { + "code": "532501", + "name": "个旧市" + }, + { + "code": "532502", + "name": "开远市" + }, + { + "code": "532503", + "name": "蒙自市" + }, + { + "code": "532504", + "name": "弥勒市" + }, + { + "code": "532523", + "name": "屏边苗族自治县" + }, + { + "code": "532524", + "name": "建水县" + }, + { + "code": "532525", + "name": "石屏县" + }, + { + "code": "532527", + "name": "泸西县" + }, + { + "code": "532528", + "name": "元阳县" + }, + { + "code": "532529", + "name": "红河县" + }, + { + "code": "532530", + "name": "金平苗族瑶族傣族自治县" + }, + { + "code": "532531", + "name": "绿春县" + }, + { + "code": "532532", + "name": "河口瑶族自治县" + } + ] + }, + { + "code": "5326", + "name": "文山壮族苗族自治州", + "children": [ + { + "code": "532601", + "name": "文山市" + }, + { + "code": "532622", + "name": "砚山县" + }, + { + "code": "532623", + "name": "西畴县" + }, + { + "code": "532624", + "name": "麻栗坡县" + }, + { + "code": "532625", + "name": "马关县" + }, + { + "code": "532626", + "name": "丘北县" + }, + { + "code": "532627", + "name": "广南县" + }, + { + "code": "532628", + "name": "富宁县" + } + ] + }, + { + "code": "5328", + "name": "西双版纳傣族自治州", + "children": [ + { + "code": "532801", + "name": "景洪市" + }, + { + "code": "532822", + "name": "勐海县" + }, + { + "code": "532823", + "name": "勐腊县" + } + ] + }, + { + "code": "5329", + "name": "大理白族自治州", + "children": [ + { + "code": "532901", + "name": "大理市" + }, + { + "code": "532922", + "name": "漾濞彝族自治县" + }, + { + "code": "532923", + "name": "祥云县" + }, + { + "code": "532924", + "name": "宾川县" + }, + { + "code": "532925", + "name": "弥渡县" + }, + { + "code": "532926", + "name": "南涧彝族自治县" + }, + { + "code": "532927", + "name": "巍山彝族回族自治县" + }, + { + "code": "532928", + "name": "永平县" + }, + { + "code": "532929", + "name": "云龙县" + }, + { + "code": "532930", + "name": "洱源县" + }, + { + "code": "532931", + "name": "剑川县" + }, + { + "code": "532932", + "name": "鹤庆县" + } + ] + }, + { + "code": "5331", + "name": "德宏傣族景颇族自治州", + "children": [ + { + "code": "533102", + "name": "瑞丽市" + }, + { + "code": "533103", + "name": "芒市" + }, + { + "code": "533122", + "name": "梁河县" + }, + { + "code": "533123", + "name": "盈江县" + }, + { + "code": "533124", + "name": "陇川县" + } + ] + }, + { + "code": "5333", + "name": "怒江傈僳族自治州", + "children": [ + { + "code": "533301", + "name": "泸水市" + }, + { + "code": "533323", + "name": "福贡县" + }, + { + "code": "533324", + "name": "贡山独龙族怒族自治县" + }, + { + "code": "533325", + "name": "兰坪白族普米族自治县" + } + ] + }, + { + "code": "5334", + "name": "迪庆藏族自治州", + "children": [ + { + "code": "533401", + "name": "香格里拉市" + }, + { + "code": "533422", + "name": "德钦县" + }, + { + "code": "533423", + "name": "维西傈僳族自治县" + } + ] + } + ] + }, + { + "code": "54", + "name": "西藏自治区", + "children": [ + { + "code": "5401", + "name": "拉萨市", + "children": [ + { + "code": "540102", + "name": "城关区" + }, + { + "code": "540103", + "name": "堆龙德庆区" + }, + { + "code": "540104", + "name": "达孜区" + }, + { + "code": "540121", + "name": "林周县" + }, + { + "code": "540122", + "name": "当雄县" + }, + { + "code": "540123", + "name": "尼木县" + }, + { + "code": "540124", + "name": "曲水县" + }, + { + "code": "540127", + "name": "墨竹工卡县" + }, + { + "code": "540171", + "name": "格尔木藏青工业园区" + }, + { + "code": "540172", + "name": "拉萨经济技术开发区" + }, + { + "code": "540173", + "name": "西藏文化旅游创意园区" + }, + { + "code": "540174", + "name": "达孜工业园区" + } + ] + }, + { + "code": "5402", + "name": "日喀则市", + "children": [ + { + "code": "540202", + "name": "桑珠孜区" + }, + { + "code": "540221", + "name": "南木林县" + }, + { + "code": "540222", + "name": "江孜县" + }, + { + "code": "540223", + "name": "定日县" + }, + { + "code": "540224", + "name": "萨迦县" + }, + { + "code": "540225", + "name": "拉孜县" + }, + { + "code": "540226", + "name": "昂仁县" + }, + { + "code": "540227", + "name": "谢通门县" + }, + { + "code": "540228", + "name": "白朗县" + }, + { + "code": "540229", + "name": "仁布县" + }, + { + "code": "540230", + "name": "康马县" + }, + { + "code": "540231", + "name": "定结县" + }, + { + "code": "540232", + "name": "仲巴县" + }, + { + "code": "540233", + "name": "亚东县" + }, + { + "code": "540234", + "name": "吉隆县" + }, + { + "code": "540235", + "name": "聂拉木县" + }, + { + "code": "540236", + "name": "萨嘎县" + }, + { + "code": "540237", + "name": "岗巴县" + } + ] + }, + { + "code": "5403", + "name": "昌都市", + "children": [ + { + "code": "540302", + "name": "卡若区" + }, + { + "code": "540321", + "name": "江达县" + }, + { + "code": "540322", + "name": "贡觉县" + }, + { + "code": "540323", + "name": "类乌齐县" + }, + { + "code": "540324", + "name": "丁青县" + }, + { + "code": "540325", + "name": "察雅县" + }, + { + "code": "540326", + "name": "八宿县" + }, + { + "code": "540327", + "name": "左贡县" + }, + { + "code": "540328", + "name": "芒康县" + }, + { + "code": "540329", + "name": "洛隆县" + }, + { + "code": "540330", + "name": "边坝县" + } + ] + }, + { + "code": "5404", + "name": "林芝市", + "children": [ + { + "code": "540402", + "name": "巴宜区" + }, + { + "code": "540421", + "name": "工布江达县" + }, + { + "code": "540423", + "name": "墨脱县" + }, + { + "code": "540424", + "name": "波密县" + }, + { + "code": "540425", + "name": "察隅县" + }, + { + "code": "540426", + "name": "朗县" + }, + { + "code": "540481", + "name": "米林市" + } + ] + }, + { + "code": "5405", + "name": "山南市", + "children": [ + { + "code": "540502", + "name": "乃东区" + }, + { + "code": "540521", + "name": "扎囊县" + }, + { + "code": "540522", + "name": "贡嘎县" + }, + { + "code": "540523", + "name": "桑日县" + }, + { + "code": "540524", + "name": "琼结县" + }, + { + "code": "540525", + "name": "曲松县" + }, + { + "code": "540526", + "name": "措美县" + }, + { + "code": "540527", + "name": "洛扎县" + }, + { + "code": "540528", + "name": "加查县" + }, + { + "code": "540529", + "name": "隆子县" + }, + { + "code": "540531", + "name": "浪卡子县" + }, + { + "code": "540581", + "name": "错那市" + } + ] + }, + { + "code": "5406", + "name": "那曲市", + "children": [ + { + "code": "540602", + "name": "色尼区" + }, + { + "code": "540621", + "name": "嘉黎县" + }, + { + "code": "540622", + "name": "比如县" + }, + { + "code": "540623", + "name": "聂荣县" + }, + { + "code": "540624", + "name": "安多县" + }, + { + "code": "540625", + "name": "申扎县" + }, + { + "code": "540626", + "name": "索县" + }, + { + "code": "540627", + "name": "班戈县" + }, + { + "code": "540628", + "name": "巴青县" + }, + { + "code": "540629", + "name": "尼玛县" + }, + { + "code": "540630", + "name": "双湖县" + } + ] + }, + { + "code": "5425", + "name": "阿里地区", + "children": [ + { + "code": "542521", + "name": "普兰县" + }, + { + "code": "542522", + "name": "札达县" + }, + { + "code": "542523", + "name": "噶尔县" + }, + { + "code": "542524", + "name": "日土县" + }, + { + "code": "542525", + "name": "革吉县" + }, + { + "code": "542526", + "name": "改则县" + }, + { + "code": "542527", + "name": "措勤县" + } + ] + } + ] + }, + { + "code": "61", + "name": "陕西省", + "children": [ + { + "code": "6101", + "name": "西安市", + "children": [ + { + "code": "610102", + "name": "新城区" + }, + { + "code": "610103", + "name": "碑林区" + }, + { + "code": "610104", + "name": "莲湖区" + }, + { + "code": "610111", + "name": "灞桥区" + }, + { + "code": "610112", + "name": "未央区" + }, + { + "code": "610113", + "name": "雁塔区" + }, + { + "code": "610114", + "name": "阎良区" + }, + { + "code": "610115", + "name": "临潼区" + }, + { + "code": "610116", + "name": "长安区" + }, + { + "code": "610117", + "name": "高陵区" + }, + { + "code": "610118", + "name": "鄠邑区" + }, + { + "code": "610122", + "name": "蓝田县" + }, + { + "code": "610124", + "name": "周至县" + } + ] + }, + { + "code": "6102", + "name": "铜川市", + "children": [ + { + "code": "610202", + "name": "王益区" + }, + { + "code": "610203", + "name": "印台区" + }, + { + "code": "610204", + "name": "耀州区" + }, + { + "code": "610222", + "name": "宜君县" + } + ] + }, + { + "code": "6103", + "name": "宝鸡市", + "children": [ + { + "code": "610302", + "name": "渭滨区" + }, + { + "code": "610303", + "name": "金台区" + }, + { + "code": "610304", + "name": "陈仓区" + }, + { + "code": "610305", + "name": "凤翔区" + }, + { + "code": "610323", + "name": "岐山县" + }, + { + "code": "610324", + "name": "扶风县" + }, + { + "code": "610326", + "name": "眉县" + }, + { + "code": "610327", + "name": "陇县" + }, + { + "code": "610328", + "name": "千阳县" + }, + { + "code": "610329", + "name": "麟游县" + }, + { + "code": "610330", + "name": "凤县" + }, + { + "code": "610331", + "name": "太白县" + } + ] + }, + { + "code": "6104", + "name": "咸阳市", + "children": [ + { + "code": "610402", + "name": "秦都区" + }, + { + "code": "610403", + "name": "杨陵区" + }, + { + "code": "610404", + "name": "渭城区" + }, + { + "code": "610422", + "name": "三原县" + }, + { + "code": "610423", + "name": "泾阳县" + }, + { + "code": "610424", + "name": "乾县" + }, + { + "code": "610425", + "name": "礼泉县" + }, + { + "code": "610426", + "name": "永寿县" + }, + { + "code": "610428", + "name": "长武县" + }, + { + "code": "610429", + "name": "旬邑县" + }, + { + "code": "610430", + "name": "淳化县" + }, + { + "code": "610431", + "name": "武功县" + }, + { + "code": "610481", + "name": "兴平市" + }, + { + "code": "610482", + "name": "彬州市" + } + ] + }, + { + "code": "6105", + "name": "渭南市", + "children": [ + { + "code": "610502", + "name": "临渭区" + }, + { + "code": "610503", + "name": "华州区" + }, + { + "code": "610522", + "name": "潼关县" + }, + { + "code": "610523", + "name": "大荔县" + }, + { + "code": "610524", + "name": "合阳县" + }, + { + "code": "610525", + "name": "澄城县" + }, + { + "code": "610526", + "name": "蒲城县" + }, + { + "code": "610527", + "name": "白水县" + }, + { + "code": "610528", + "name": "富平县" + }, + { + "code": "610581", + "name": "韩城市" + }, + { + "code": "610582", + "name": "华阴市" + } + ] + }, + { + "code": "6106", + "name": "延安市", + "children": [ + { + "code": "610602", + "name": "宝塔区" + }, + { + "code": "610603", + "name": "安塞区" + }, + { + "code": "610621", + "name": "延长县" + }, + { + "code": "610622", + "name": "延川县" + }, + { + "code": "610625", + "name": "志丹县" + }, + { + "code": "610626", + "name": "吴起县" + }, + { + "code": "610627", + "name": "甘泉县" + }, + { + "code": "610628", + "name": "富县" + }, + { + "code": "610629", + "name": "洛川县" + }, + { + "code": "610630", + "name": "宜川县" + }, + { + "code": "610631", + "name": "黄龙县" + }, + { + "code": "610632", + "name": "黄陵县" + }, + { + "code": "610681", + "name": "子长市" + } + ] + }, + { + "code": "6107", + "name": "汉中市", + "children": [ + { + "code": "610702", + "name": "汉台区" + }, + { + "code": "610703", + "name": "南郑区" + }, + { + "code": "610722", + "name": "城固县" + }, + { + "code": "610723", + "name": "洋县" + }, + { + "code": "610724", + "name": "西乡县" + }, + { + "code": "610725", + "name": "勉县" + }, + { + "code": "610726", + "name": "宁强县" + }, + { + "code": "610727", + "name": "略阳县" + }, + { + "code": "610728", + "name": "镇巴县" + }, + { + "code": "610729", + "name": "留坝县" + }, + { + "code": "610730", + "name": "佛坪县" + } + ] + }, + { + "code": "6108", + "name": "榆林市", + "children": [ + { + "code": "610802", + "name": "榆阳区" + }, + { + "code": "610803", + "name": "横山区" + }, + { + "code": "610822", + "name": "府谷县" + }, + { + "code": "610824", + "name": "靖边县" + }, + { + "code": "610825", + "name": "定边县" + }, + { + "code": "610826", + "name": "绥德县" + }, + { + "code": "610827", + "name": "米脂县" + }, + { + "code": "610828", + "name": "佳县" + }, + { + "code": "610829", + "name": "吴堡县" + }, + { + "code": "610830", + "name": "清涧县" + }, + { + "code": "610831", + "name": "子洲县" + }, + { + "code": "610881", + "name": "神木市" + } + ] + }, + { + "code": "6109", + "name": "安康市", + "children": [ + { + "code": "610902", + "name": "汉滨区" + }, + { + "code": "610921", + "name": "汉阴县" + }, + { + "code": "610922", + "name": "石泉县" + }, + { + "code": "610923", + "name": "宁陕县" + }, + { + "code": "610924", + "name": "紫阳县" + }, + { + "code": "610925", + "name": "岚皋县" + }, + { + "code": "610926", + "name": "平利县" + }, + { + "code": "610927", + "name": "镇坪县" + }, + { + "code": "610929", + "name": "白河县" + }, + { + "code": "610981", + "name": "旬阳市" + } + ] + }, + { + "code": "6110", + "name": "商洛市", + "children": [ + { + "code": "611002", + "name": "商州区" + }, + { + "code": "611021", + "name": "洛南县" + }, + { + "code": "611022", + "name": "丹凤县" + }, + { + "code": "611023", + "name": "商南县" + }, + { + "code": "611024", + "name": "山阳县" + }, + { + "code": "611025", + "name": "镇安县" + }, + { + "code": "611026", + "name": "柞水县" + } + ] + } + ] + }, + { + "code": "62", + "name": "甘肃省", + "children": [ + { + "code": "6201", + "name": "兰州市", + "children": [ + { + "code": "620102", + "name": "城关区" + }, + { + "code": "620103", + "name": "七里河区" + }, + { + "code": "620104", + "name": "西固区" + }, + { + "code": "620105", + "name": "安宁区" + }, + { + "code": "620111", + "name": "红古区" + }, + { + "code": "620121", + "name": "永登县" + }, + { + "code": "620122", + "name": "皋兰县" + }, + { + "code": "620123", + "name": "榆中县" + }, + { + "code": "620171", + "name": "兰州新区" + } + ] + }, + { + "code": "6202", + "name": "嘉峪关市", + "children": [ + { + "code": "620201001", + "name": "雄关街道" + }, + { + "code": "620201002", + "name": "钢城街道" + }, + { + "code": "620201100", + "name": "新城镇" + }, + { + "code": "620201101", + "name": "峪泉镇" + }, + { + "code": "620201102", + "name": "文殊镇" + } + ] + }, + { + "code": "6203", + "name": "金昌市", + "children": [ + { + "code": "620302", + "name": "金川区" + }, + { + "code": "620321", + "name": "永昌县" + } + ] + }, + { + "code": "6204", + "name": "白银市", + "children": [ + { + "code": "620402", + "name": "白银区" + }, + { + "code": "620403", + "name": "平川区" + }, + { + "code": "620421", + "name": "靖远县" + }, + { + "code": "620422", + "name": "会宁县" + }, + { + "code": "620423", + "name": "景泰县" + } + ] + }, + { + "code": "6205", + "name": "天水市", + "children": [ + { + "code": "620502", + "name": "秦州区" + }, + { + "code": "620503", + "name": "麦积区" + }, + { + "code": "620521", + "name": "清水县" + }, + { + "code": "620522", + "name": "秦安县" + }, + { + "code": "620523", + "name": "甘谷县" + }, + { + "code": "620524", + "name": "武山县" + }, + { + "code": "620525", + "name": "张家川回族自治县" + } + ] + }, + { + "code": "6206", + "name": "武威市", + "children": [ + { + "code": "620602", + "name": "凉州区" + }, + { + "code": "620621", + "name": "民勤县" + }, + { + "code": "620622", + "name": "古浪县" + }, + { + "code": "620623", + "name": "天祝藏族自治县" + } + ] + }, + { + "code": "6207", + "name": "张掖市", + "children": [ + { + "code": "620702", + "name": "甘州区" + }, + { + "code": "620721", + "name": "肃南裕固族自治县" + }, + { + "code": "620722", + "name": "民乐县" + }, + { + "code": "620723", + "name": "临泽县" + }, + { + "code": "620724", + "name": "高台县" + }, + { + "code": "620725", + "name": "山丹县" + } + ] + }, + { + "code": "6208", + "name": "平凉市", + "children": [ + { + "code": "620802", + "name": "崆峒区" + }, + { + "code": "620821", + "name": "泾川县" + }, + { + "code": "620822", + "name": "灵台县" + }, + { + "code": "620823", + "name": "崇信县" + }, + { + "code": "620825", + "name": "庄浪县" + }, + { + "code": "620826", + "name": "静宁县" + }, + { + "code": "620881", + "name": "华亭市" + } + ] + }, + { + "code": "6209", + "name": "酒泉市", + "children": [ + { + "code": "620902", + "name": "肃州区" + }, + { + "code": "620921", + "name": "金塔县" + }, + { + "code": "620922", + "name": "瓜州县" + }, + { + "code": "620923", + "name": "肃北蒙古族自治县" + }, + { + "code": "620924", + "name": "阿克塞哈萨克族自治县" + }, + { + "code": "620981", + "name": "玉门市" + }, + { + "code": "620982", + "name": "敦煌市" + } + ] + }, + { + "code": "6210", + "name": "庆阳市", + "children": [ + { + "code": "621002", + "name": "西峰区" + }, + { + "code": "621021", + "name": "庆城县" + }, + { + "code": "621022", + "name": "环县" + }, + { + "code": "621023", + "name": "华池县" + }, + { + "code": "621024", + "name": "合水县" + }, + { + "code": "621025", + "name": "正宁县" + }, + { + "code": "621026", + "name": "宁县" + }, + { + "code": "621027", + "name": "镇原县" + } + ] + }, + { + "code": "6211", + "name": "定西市", + "children": [ + { + "code": "621102", + "name": "安定区" + }, + { + "code": "621121", + "name": "通渭县" + }, + { + "code": "621122", + "name": "陇西县" + }, + { + "code": "621123", + "name": "渭源县" + }, + { + "code": "621124", + "name": "临洮县" + }, + { + "code": "621125", + "name": "漳县" + }, + { + "code": "621126", + "name": "岷县" + } + ] + }, + { + "code": "6212", + "name": "陇南市", + "children": [ + { + "code": "621202", + "name": "武都区" + }, + { + "code": "621221", + "name": "成县" + }, + { + "code": "621222", + "name": "文县" + }, + { + "code": "621223", + "name": "宕昌县" + }, + { + "code": "621224", + "name": "康县" + }, + { + "code": "621225", + "name": "西和县" + }, + { + "code": "621226", + "name": "礼县" + }, + { + "code": "621227", + "name": "徽县" + }, + { + "code": "621228", + "name": "两当县" + } + ] + }, + { + "code": "6229", + "name": "临夏回族自治州", + "children": [ + { + "code": "622901", + "name": "临夏市" + }, + { + "code": "622921", + "name": "临夏县" + }, + { + "code": "622922", + "name": "康乐县" + }, + { + "code": "622923", + "name": "永靖县" + }, + { + "code": "622924", + "name": "广河县" + }, + { + "code": "622925", + "name": "和政县" + }, + { + "code": "622926", + "name": "东乡族自治县" + }, + { + "code": "622927", + "name": "积石山保安族东乡族撒拉族自治县" + } + ] + }, + { + "code": "6230", + "name": "甘南藏族自治州", + "children": [ + { + "code": "623001", + "name": "合作市" + }, + { + "code": "623021", + "name": "临潭县" + }, + { + "code": "623022", + "name": "卓尼县" + }, + { + "code": "623023", + "name": "舟曲县" + }, + { + "code": "623024", + "name": "迭部县" + }, + { + "code": "623025", + "name": "玛曲县" + }, + { + "code": "623026", + "name": "碌曲县" + }, + { + "code": "623027", + "name": "夏河县" + } + ] + } + ] + }, + { + "code": "63", + "name": "青海省", + "children": [ + { + "code": "6301", + "name": "西宁市", + "children": [ + { + "code": "630102", + "name": "城东区" + }, + { + "code": "630103", + "name": "城中区" + }, + { + "code": "630104", + "name": "城西区" + }, + { + "code": "630105", + "name": "城北区" + }, + { + "code": "630106", + "name": "湟中区" + }, + { + "code": "630121", + "name": "大通回族土族自治县" + }, + { + "code": "630123", + "name": "湟源县" + } + ] + }, + { + "code": "6302", + "name": "海东市", + "children": [ + { + "code": "630202", + "name": "乐都区" + }, + { + "code": "630203", + "name": "平安区" + }, + { + "code": "630222", + "name": "民和回族土族自治县" + }, + { + "code": "630223", + "name": "互助土族自治县" + }, + { + "code": "630224", + "name": "化隆回族自治县" + }, + { + "code": "630225", + "name": "循化撒拉族自治县" + } + ] + }, + { + "code": "6322", + "name": "海北藏族自治州", + "children": [ + { + "code": "632221", + "name": "门源回族自治县" + }, + { + "code": "632222", + "name": "祁连县" + }, + { + "code": "632223", + "name": "海晏县" + }, + { + "code": "632224", + "name": "刚察县" + } + ] + }, + { + "code": "6323", + "name": "黄南藏族自治州", + "children": [ + { + "code": "632301", + "name": "同仁市" + }, + { + "code": "632322", + "name": "尖扎县" + }, + { + "code": "632323", + "name": "泽库县" + }, + { + "code": "632324", + "name": "河南蒙古族自治县" + } + ] + }, + { + "code": "6325", + "name": "海南藏族自治州", + "children": [ + { + "code": "632521", + "name": "共和县" + }, + { + "code": "632522", + "name": "同德县" + }, + { + "code": "632523", + "name": "贵德县" + }, + { + "code": "632524", + "name": "兴海县" + }, + { + "code": "632525", + "name": "贵南县" + } + ] + }, + { + "code": "6326", + "name": "果洛藏族自治州", + "children": [ + { + "code": "632621", + "name": "玛沁县" + }, + { + "code": "632622", + "name": "班玛县" + }, + { + "code": "632623", + "name": "甘德县" + }, + { + "code": "632624", + "name": "达日县" + }, + { + "code": "632625", + "name": "久治县" + }, + { + "code": "632626", + "name": "玛多县" + } + ] + }, + { + "code": "6327", + "name": "玉树藏族自治州", + "children": [ + { + "code": "632701", + "name": "玉树市" + }, + { + "code": "632722", + "name": "杂多县" + }, + { + "code": "632723", + "name": "称多县" + }, + { + "code": "632724", + "name": "治多县" + }, + { + "code": "632725", + "name": "囊谦县" + }, + { + "code": "632726", + "name": "曲麻莱县" + } + ] + }, + { + "code": "6328", + "name": "海西蒙古族藏族自治州", + "children": [ + { + "code": "632801", + "name": "格尔木市" + }, + { + "code": "632802", + "name": "德令哈市" + }, + { + "code": "632803", + "name": "茫崖市" + }, + { + "code": "632821", + "name": "乌兰县" + }, + { + "code": "632822", + "name": "都兰县" + }, + { + "code": "632823", + "name": "天峻县" + }, + { + "code": "632857", + "name": "大柴旦行政委员会" + } + ] + } + ] + }, + { + "code": "64", + "name": "宁夏回族自治区", + "children": [ + { + "code": "6401", + "name": "银川市", + "children": [ + { + "code": "640104", + "name": "兴庆区" + }, + { + "code": "640105", + "name": "西夏区" + }, + { + "code": "640106", + "name": "金凤区" + }, + { + "code": "640121", + "name": "永宁县" + }, + { + "code": "640122", + "name": "贺兰县" + }, + { + "code": "640181", + "name": "灵武市" + } + ] + }, + { + "code": "6402", + "name": "石嘴山市", + "children": [ + { + "code": "640202", + "name": "大武口区" + }, + { + "code": "640205", + "name": "惠农区" + }, + { + "code": "640221", + "name": "平罗县" + } + ] + }, + { + "code": "6403", + "name": "吴忠市", + "children": [ + { + "code": "640302", + "name": "利通区" + }, + { + "code": "640303", + "name": "红寺堡区" + }, + { + "code": "640323", + "name": "盐池县" + }, + { + "code": "640324", + "name": "同心县" + }, + { + "code": "640381", + "name": "青铜峡市" + } + ] + }, + { + "code": "6404", + "name": "固原市", + "children": [ + { + "code": "640402", + "name": "原州区" + }, + { + "code": "640422", + "name": "西吉县" + }, + { + "code": "640423", + "name": "隆德县" + }, + { + "code": "640424", + "name": "泾源县" + }, + { + "code": "640425", + "name": "彭阳县" + } + ] + }, + { + "code": "6405", + "name": "中卫市", + "children": [ + { + "code": "640502", + "name": "沙坡头区" + }, + { + "code": "640521", + "name": "中宁县" + }, + { + "code": "640522", + "name": "海原县" + } + ] + } + ] + }, + { + "code": "65", + "name": "新疆维吾尔自治区", + "children": [ + { + "code": "6501", + "name": "乌鲁木齐市", + "children": [ + { + "code": "650102", + "name": "天山区" + }, + { + "code": "650103", + "name": "沙依巴克区" + }, + { + "code": "650104", + "name": "新市区" + }, + { + "code": "650105", + "name": "水磨沟区" + }, + { + "code": "650106", + "name": "头屯河区" + }, + { + "code": "650107", + "name": "达坂城区" + }, + { + "code": "650109", + "name": "米东区" + }, + { + "code": "650121", + "name": "乌鲁木齐县" + } + ] + }, + { + "code": "6502", + "name": "克拉玛依市", + "children": [ + { + "code": "650202", + "name": "独山子区" + }, + { + "code": "650203", + "name": "克拉玛依区" + }, + { + "code": "650204", + "name": "白碱滩区" + }, + { + "code": "650205", + "name": "乌尔禾区" + } + ] + }, + { + "code": "6504", + "name": "吐鲁番市", + "children": [ + { + "code": "650402", + "name": "高昌区" + }, + { + "code": "650421", + "name": "鄯善县" + }, + { + "code": "650422", + "name": "托克逊县" + } + ] + }, + { + "code": "6505", + "name": "哈密市", + "children": [ + { + "code": "650502", + "name": "伊州区" + }, + { + "code": "650521", + "name": "巴里坤哈萨克自治县" + }, + { + "code": "650522", + "name": "伊吾县" + } + ] + }, + { + "code": "6523", + "name": "昌吉回族自治州", + "children": [ + { + "code": "652301", + "name": "昌吉市" + }, + { + "code": "652302", + "name": "阜康市" + }, + { + "code": "652323", + "name": "呼图壁县" + }, + { + "code": "652324", + "name": "玛纳斯县" + }, + { + "code": "652325", + "name": "奇台县" + }, + { + "code": "652327", + "name": "吉木萨尔县" + }, + { + "code": "652328", + "name": "木垒哈萨克自治县" + } + ] + }, + { + "code": "6527", + "name": "博尔塔拉蒙古自治州", + "children": [ + { + "code": "652701", + "name": "博乐市" + }, + { + "code": "652702", + "name": "阿拉山口市" + }, + { + "code": "652722", + "name": "精河县" + }, + { + "code": "652723", + "name": "温泉县" + } + ] + }, + { + "code": "6528", + "name": "巴音郭楞蒙古自治州", + "children": [ + { + "code": "652801", + "name": "库尔勒市" + }, + { + "code": "652822", + "name": "轮台县" + }, + { + "code": "652823", + "name": "尉犁县" + }, + { + "code": "652824", + "name": "若羌县" + }, + { + "code": "652825", + "name": "且末县" + }, + { + "code": "652826", + "name": "焉耆回族自治县" + }, + { + "code": "652827", + "name": "和静县" + }, + { + "code": "652828", + "name": "和硕县" + }, + { + "code": "652829", + "name": "博湖县" + } + ] + }, + { + "code": "6529", + "name": "阿克苏地区", + "children": [ + { + "code": "652901", + "name": "阿克苏市" + }, + { + "code": "652902", + "name": "库车市" + }, + { + "code": "652922", + "name": "温宿县" + }, + { + "code": "652924", + "name": "沙雅县" + }, + { + "code": "652925", + "name": "新和县" + }, + { + "code": "652926", + "name": "拜城县" + }, + { + "code": "652927", + "name": "乌什县" + }, + { + "code": "652928", + "name": "阿瓦提县" + }, + { + "code": "652929", + "name": "柯坪县" + } + ] + }, + { + "code": "6530", + "name": "克孜勒苏柯尔克孜自治州", + "children": [ + { + "code": "653001", + "name": "阿图什市" + }, + { + "code": "653022", + "name": "阿克陶县" + }, + { + "code": "653023", + "name": "阿合奇县" + }, + { + "code": "653024", + "name": "乌恰县" + } + ] + }, + { + "code": "6531", + "name": "喀什地区", + "children": [ + { + "code": "653101", + "name": "喀什市" + }, + { + "code": "653121", + "name": "疏附县" + }, + { + "code": "653122", + "name": "疏勒县" + }, + { + "code": "653123", + "name": "英吉沙县" + }, + { + "code": "653124", + "name": "泽普县" + }, + { + "code": "653125", + "name": "莎车县" + }, + { + "code": "653126", + "name": "叶城县" + }, + { + "code": "653127", + "name": "麦盖提县" + }, + { + "code": "653128", + "name": "岳普湖县" + }, + { + "code": "653129", + "name": "伽师县" + }, + { + "code": "653130", + "name": "巴楚县" + }, + { + "code": "653131", + "name": "塔什库尔干塔吉克自治县" + } + ] + }, + { + "code": "6532", + "name": "和田地区", + "children": [ + { + "code": "653201", + "name": "和田市" + }, + { + "code": "653221", + "name": "和田县" + }, + { + "code": "653222", + "name": "墨玉县" + }, + { + "code": "653223", + "name": "皮山县" + }, + { + "code": "653224", + "name": "洛浦县" + }, + { + "code": "653225", + "name": "策勒县" + }, + { + "code": "653226", + "name": "于田县" + }, + { + "code": "653227", + "name": "民丰县" + } + ] + }, + { + "code": "6540", + "name": "伊犁哈萨克自治州", + "children": [ + { + "code": "654002", + "name": "伊宁市" + }, + { + "code": "654003", + "name": "奎屯市" + }, + { + "code": "654004", + "name": "霍尔果斯市" + }, + { + "code": "654021", + "name": "伊宁县" + }, + { + "code": "654022", + "name": "察布查尔锡伯自治县" + }, + { + "code": "654023", + "name": "霍城县" + }, + { + "code": "654024", + "name": "巩留县" + }, + { + "code": "654025", + "name": "新源县" + }, + { + "code": "654026", + "name": "昭苏县" + }, + { + "code": "654027", + "name": "特克斯县" + }, + { + "code": "654028", + "name": "尼勒克县" + } + ] + }, + { + "code": "6542", + "name": "塔城地区", + "children": [ + { + "code": "654201", + "name": "塔城市" + }, + { + "code": "654202", + "name": "乌苏市" + }, + { + "code": "654203", + "name": "沙湾市" + }, + { + "code": "654221", + "name": "额敏县" + }, + { + "code": "654224", + "name": "托里县" + }, + { + "code": "654225", + "name": "裕民县" + }, + { + "code": "654226", + "name": "和布克赛尔蒙古自治县" + } + ] + }, + { + "code": "6543", + "name": "阿勒泰地区", + "children": [ + { + "code": "654301", + "name": "阿勒泰市" + }, + { + "code": "654321", + "name": "布尔津县" + }, + { + "code": "654322", + "name": "富蕴县" + }, + { + "code": "654323", + "name": "福海县" + }, + { + "code": "654324", + "name": "哈巴河县" + }, + { + "code": "654325", + "name": "青河县" + }, + { + "code": "654326", + "name": "吉木乃县" + } + ] + }, + { + "code": "6590", + "name": "自治区直辖县级行政区划", + "children": [ + { + "code": "659001", + "name": "石河子市" + }, + { + "code": "659002", + "name": "阿拉尔市" + }, + { + "code": "659003", + "name": "图木舒克市" + }, + { + "code": "659004", + "name": "五家渠市" + }, + { + "code": "659005", + "name": "北屯市" + }, + { + "code": "659006", + "name": "铁门关市" + }, + { + "code": "659007", + "name": "双河市" + }, + { + "code": "659008", + "name": "可克达拉市" + }, + { + "code": "659009", + "name": "昆玉市" + }, + { + "code": "659010", + "name": "胡杨河市" + }, + { + "code": "659011", + "name": "新星市" + }, + { + "code": "659012", + "name": "白杨市" + } + ] + } + ] + }, + { + "code": "81", + "name": "香港特别行政区", + "children": [ + { + "code": "8101", + "name": "香港岛", + "children": [ + {"code": "810101", "name": "中西区"}, + {"code": "810102", "name": "湾仔区"}, + {"code": "810103", "name": "东区"}, + {"code": "810104", "name": "南区"} + ] + }, + { + "code": "8102", + "name": "九龙", + "children": [ + {"code": "810201", "name": "油尖旺区"}, + {"code": "810202", "name": "深水埗区"}, + {"code": "810203", "name": "九龙城区"}, + {"code": "810204", "name": "黄大仙区"}, + {"code": "810205", "name": "观塘区"} + ] + }, + { + "code": "8103", + "name": "新界", + "children": [ + {"code": "810301", "name": "北区"}, + {"code": "810302", "name": "大埔区"}, + {"code": "810303", "name": "沙田区"}, + {"code": "810304", "name": "西贡区"}, + {"code": "810305", "name": "元朗区"}, + {"code": "810306", "name": "屯门区"}, + {"code": "810307", "name": "荃湾区"}, + {"code": "810308", "name": "葵青区"}, + {"code": "810309", "name": "离岛区"} + ] + } + ] + }, + { + "code": "82", + "name": "澳门特别行政区", + "children": [ + { + "code": "8201", + "name": "澳门半岛", + "children": [ + {"code": "820101", "name": "花地玛堂区"}, + {"code": "820102", "name": "圣安多尼堂区"}, + {"code": "820103", "name": "大堂区"}, + {"code": "820104", "name": "望德堂区"}, + {"code": "820105", "name": "风顺堂区"} + ] + }, + { + "code": "8202", + "name": "离岛", + "children": [ + {"code": "820201", "name": "嘉模堂区"}, + {"code": "820202", "name": "圣方济各堂区"} + ] + } + ] + }, + { + "code": "71", + "name": "台湾省", + "children": [ + { + "code": "7101", + "name": "台北市", + "children": [ + {"code": "710101", "name": "中正区"}, + {"code": "710102", "name": "大同区"}, + {"code": "710103", "name": "中山区"}, + {"code": "710104", "name": "松山区"}, + {"code": "710105", "name": "大安区"}, + {"code": "710106", "name": "万华区"}, + {"code": "710107", "name": "信义区"}, + {"code": "710108", "name": "士林区"}, + {"code": "710109", "name": "北投区"}, + {"code": "710110", "name": "内湖区"}, + {"code": "710111", "name": "南港区"}, + {"code": "710112", "name": "文山区"} + ] + }, + { + "code": "7102", + "name": "高雄市", + "children": [ + {"code": "710201", "name": "新兴区"}, + {"code": "710202", "name": "前金区"}, + {"code": "710203", "name": "苓雅区"}, + {"code": "710204", "name": "盐埕区"}, + {"code": "710205", "name": "鼓山区"}, + {"code": "710206", "name": "旗津区"}, + {"code": "710207", "name": "前镇区"}, + {"code": "710208", "name": "三民区"}, + {"code": "710209", "name": "左营区"}, + {"code": "710210", "name": "楠梓区"} + ] + }, + { + "code": "7103", + "name": "台中市", + "children": [ + {"code": "710301", "name": "中区"}, + {"code": "710302", "name": "东区"}, + {"code": "710303", "name": "南区"}, + {"code": "710304", "name": "西区"}, + {"code": "710305", "name": "北区"}, + {"code": "710306", "name": "北屯区"}, + {"code": "710307", "name": "西屯区"}, + {"code": "710308", "name": "南屯区"} + ] + }, + { + "code": "7104", + "name": "台南市", + "children": [ + {"code": "710401", "name": "中西区"}, + {"code": "710402", "name": "东区"}, + {"code": "710403", "name": "南区"}, + {"code": "710404", "name": "北区"}, + {"code": "710405", "name": "安平区"}, + {"code": "710406", "name": "安南区"} + ] + }, + { + "code": "7105", + "name": "新北市", + "children": [ + {"code": "710501", "name": "万里区"}, + {"code": "710502", "name": "金山区"}, + {"code": "710503", "name": "板桥区"}, + {"code": "710504", "name": "汐止区"}, + {"code": "710505", "name": "深坑区"}, + {"code": "710506", "name": "石碇区"}, + {"code": "710507", "name": "瑞芳区"}, + {"code": "710508", "name": "平溪区"} + ] + }, + { + "code": "7106", + "name": "桃园市", + "children": [ + {"code": "710601", "name": "中坜区"}, + {"code": "710602", "name": "平镇区"}, + {"code": "710603", "name": "龙潭区"}, + {"code": "710604", "name": "杨梅区"}, + {"code": "710605", "name": "新屋区"}, + {"code": "710606", "name": "观音区"}, + {"code": "710607", "name": "桃园区"}, + {"code": "710608", "name": "龟山区"} + ] + } + ] + } +] \ No newline at end of file diff --git a/scripts/update_code_references.js b/scripts/update_code_references.js new file mode 100644 index 0000000..eb9a509 --- /dev/null +++ b/scripts/update_code_references.js @@ -0,0 +1,273 @@ +const fs = require('fs'); +const path = require('path'); + +/** + * 代码更新脚本:将所有 order_allocations 表引用更新为 transfers 表 + * + * 更新策略: + * 1. 将 order_allocations 表名替换为 transfers + * 2. 更新相关的字段映射和查询逻辑 + * 3. 添加必要的 WHERE 条件来过滤 allocation 类型的记录 + */ + +class CodeUpdater { + constructor() { + this.filesToUpdate = [ + 'services/matchingService.js', + 'routes/matchingAdmin.js', + 'routes/transfers.js', + 'routes/matching.js' + ]; + + // 字段映射关系 + this.fieldMappings = { + // order_allocations 字段 -> transfers 字段 + 'matching_order_id': 'matching_order_id', + 'from_user_id': 'from_user_id', + 'to_user_id': 'to_user_id', + 'amount': 'amount', + 'cycle_number': 'cycle_number', + 'status': 'status', + 'transfer_id': 'id', // order_allocations.transfer_id 对应 transfers.id + 'created_at': 'created_at', + 'updated_at': 'updated_at', + 'confirmed_at': 'confirmed_at', + 'outbound_date': 'outbound_date', + 'return_date': 'return_date', + 'can_return_after': 'can_return_after' + }; + } + + /** + * 更新单个文件 + * @param {string} filePath - 文件路径 + */ + async updateFile(filePath) { + const fullPath = path.join(process.cwd(), filePath); + + if (!fs.existsSync(fullPath)) { + console.log(`文件不存在: ${filePath}`); + return; + } + + console.log(`正在更新文件: ${filePath}`); + + let content = fs.readFileSync(fullPath, 'utf8'); + let originalContent = content; + + // 1. 替换表名 + content = content.replace(/\border_allocations\b/g, 'transfers'); + + // 2. 添加 source_type 过滤条件 + content = this.addSourceTypeFilters(content); + + // 3. 更新 INSERT 语句 + content = this.updateInsertStatements(content); + + // 4. 更新特定的查询逻辑 + content = this.updateSpecificQueries(content, filePath); + + // 5. 更新注释 + content = this.updateComments(content); + + if (content !== originalContent) { + // 创建备份 + fs.writeFileSync(fullPath + '.backup', originalContent); + + // 写入更新后的内容 + fs.writeFileSync(fullPath, content); + + console.log(`✓ 已更新: ${filePath}`); + console.log(`✓ 备份已创建: ${filePath}.backup`); + } else { + console.log(`- 无需更新: ${filePath}`); + } + } + + /** + * 添加 source_type 过滤条件 + * @param {string} content - 文件内容 + * @returns {string} 更新后的内容 + */ + addSourceTypeFilters(content) { + // 在 FROM transfers 后添加 WHERE source_type = 'allocation' 条件 + // 但要避免重复添加 + + // 匹配 FROM transfers 但不包含 source_type 的情况 + content = content.replace( + /FROM transfers(?!.*source_type)([\s\S]*?)(?=WHERE|ORDER|GROUP|LIMIT|;|$)/gi, + (match, afterFrom) => { + // 如果已经有 WHERE 子句,添加 AND 条件 + if (afterFrom.includes('WHERE')) { + return match.replace(/WHERE/, 'WHERE source_type = \'allocation\' AND'); + } else { + // 如果没有 WHERE 子句,添加新的 WHERE 条件 + const beforeNextClause = match.match(/(ORDER|GROUP|LIMIT|;|$)/); + if (beforeNextClause) { + return match.replace(beforeNextClause[0], ` WHERE source_type = 'allocation' ${beforeNextClause[0]}`); + } else { + return match + " WHERE source_type = 'allocation'"; + } + } + } + ); + + return content; + } + + /** + * 更新 INSERT 语句 + * @param {string} content - 文件内容 + * @returns {string} 更新后的内容 + */ + updateInsertStatements(content) { + // 更新 INSERT INTO transfers 语句,添加必要的字段 + content = content.replace( + /INSERT INTO transfers\s*\(([^)]+)\)\s*VALUES\s*\(([^)]+)\)/gi, + (match, fields, values) => { + // 如果字段列表中没有 source_type,添加它 + if (!fields.includes('source_type')) { + const fieldList = fields.trim() + ', source_type'; + const valueList = values.trim() + ', \'allocation\''; + return `INSERT INTO transfers (${fieldList}) VALUES (${valueList})`; + } + return match; + } + ); + + return content; + } + + /** + * 更新特定的查询逻辑 + * @param {string} content - 文件内容 + * @param {string} filePath - 文件路径 + * @returns {string} 更新后的内容 + */ + updateSpecificQueries(content, filePath) { + // 根据不同文件进行特定更新 + + if (filePath.includes('matchingService.js')) { + // 更新 matchingService.js 中的特定逻辑 + content = this.updateMatchingServiceQueries(content); + } + + if (filePath.includes('matchingAdmin.js')) { + // 更新 matchingAdmin.js 中的特定逻辑 + content = this.updateMatchingAdminQueries(content); + } + + return content; + } + + /** + * 更新 matchingService.js 中的查询 + * @param {string} content - 文件内容 + * @returns {string} 更新后的内容 + */ + updateMatchingServiceQueries(content) { + // 更新确认分配的逻辑 + content = content.replace( + /UPDATE transfers SET status = "confirmed", transfer_id = \?, confirmed_at = NOW\(\) WHERE id = \?/g, + 'UPDATE transfers SET status = "confirmed", confirmed_at = NOW() WHERE id = ? AND source_type = \'allocation\'' + ); + + // 更新获取分配记录的查询 + content = content.replace( + /SELECT \* FROM transfers WHERE id = \? AND from_user_id = \?/g, + 'SELECT * FROM transfers WHERE id = ? AND from_user_id = ? AND source_type = \'allocation\'' + ); + + return content; + } + + /** + * 更新 matchingAdmin.js 中的查询 + * @param {string} content - 文件内容 + * @returns {string} 更新后的内容 + */ + updateMatchingAdminQueries(content) { + // 更新管理员查询逻辑,确保只查询 allocation 类型的记录 + content = content.replace( + /FROM transfers oa/g, + 'FROM transfers oa WHERE oa.source_type = \'allocation\'' + ); + + return content; + } + + /** + * 更新注释 + * @param {string} content - 文件内容 + * @returns {string} 更新后的内容 + */ + updateComments(content) { + content = content.replace(/order_allocations/g, 'transfers (allocation type)'); + content = content.replace(/订单分配/g, '转账分配'); + content = content.replace(/分配表/g, '转账表(分配类型)'); + + return content; + } + + /** + * 执行所有文件的更新 + */ + async updateAllFiles() { + console.log('开始更新代码引用...'); + console.log('=' .repeat(60)); + + for (const filePath of this.filesToUpdate) { + try { + await this.updateFile(filePath); + } catch (error) { + console.error(`更新文件 ${filePath} 失败:`, error.message); + } + } + + console.log('\n' + '=' .repeat(60)); + console.log('✓ 代码更新完成!'); + console.log('\n注意事项:'); + console.log('1. 所有原始文件已备份为 .backup 文件'); + console.log('2. 请测试更新后的代码功能是否正常'); + console.log('3. 如有问题,可以使用备份文件恢复'); + console.log('4. 确认无误后可删除 .backup 文件'); + } + + /** + * 恢复所有备份文件 + */ + async restoreBackups() { + console.log('开始恢复备份文件...'); + + for (const filePath of this.filesToUpdate) { + const fullPath = path.join(process.cwd(), filePath); + const backupPath = fullPath + '.backup'; + + if (fs.existsSync(backupPath)) { + fs.copyFileSync(backupPath, fullPath); + console.log(`✓ 已恢复: ${filePath}`); + } + } + + console.log('✓ 备份恢复完成!'); + } +} + +async function main() { + const updater = new CodeUpdater(); + + const args = process.argv.slice(2); + + if (args.includes('--restore')) { + await updater.restoreBackups(); + } else { + await updater.updateAllFiles(); + } +} + +// 如果直接运行此脚本 +if (require.main === module) { + main().catch(console.error); +} + +module.exports = CodeUpdater; \ No newline at end of file diff --git a/scripts/verify_data.js b/scripts/verify_data.js new file mode 100644 index 0000000..470682a --- /dev/null +++ b/scripts/verify_data.js @@ -0,0 +1,37 @@ +const { initDB, getDB } = require('../database'); + +async function verifyData() { + try { + await initDB(); + + // 检查省份数据 + const [provinces] = await getDB().query('SELECT code, name FROM china_regions WHERE level = 1 ORDER BY code LIMIT 10'); + console.log('省份数据样本:'); + provinces.forEach(p => console.log(` ${p.code} - ${p.name}`)); + + // 检查城市数据 + const [cities] = await getDB().query('SELECT code, name, parent_code FROM china_regions WHERE level = 2 ORDER BY code LIMIT 10'); + console.log('\n城市数据样本:'); + cities.forEach(c => console.log(` ${c.code} - ${c.name} (${c.parent_code})`)); + + // 检查区县数据 + const [districts] = await getDB().query('SELECT code, name, parent_code FROM china_regions WHERE level = 3 ORDER BY code LIMIT 10'); + console.log('\n区县数据样本:'); + districts.forEach(d => console.log(` ${d.code} - ${d.name} (${d.parent_code})`)); + + // 统计各级别数量 + const [stats] = await getDB().query('SELECT level, COUNT(*) as count FROM china_regions GROUP BY level ORDER BY level'); + console.log('\n各级别统计:'); + stats.forEach(row => { + const levelName = row.level === 1 ? '省份' : row.level === 2 ? '城市' : '区县'; + console.log(` ${levelName}(level ${row.level}): ${row.count} 个`); + }); + + } catch (error) { + console.error('验证失败:', error); + } finally { + process.exit(); + } +} + +verifyData(); \ No newline at end of file diff --git a/scripts/verify_merge.js b/scripts/verify_merge.js new file mode 100644 index 0000000..e3e4995 --- /dev/null +++ b/scripts/verify_merge.js @@ -0,0 +1,115 @@ +const mysql = require('mysql2/promise'); +const { dbConfig } = require('../config/config'); + +/** + * 验证表合并结果的脚本 + * 检查 order_allocations 表和 transfers 表的数据一致性 + */ +async function verifyMerge() { + console.log('开始验证表合并结果...'); + console.log('=' .repeat(60)); + + let connection; + + try { + // 创建数据库连接 + connection = await mysql.createConnection({ + host: dbConfig.host, + user: dbConfig.user, + password: dbConfig.password, + database: dbConfig.database + }); + + // 1. 检查 order_allocations 表中有多少条记录 + const [allocationCount] = await connection.execute( + 'SELECT COUNT(*) as count FROM order_allocations' + ); + console.log(`order_allocations 表总记录数: ${allocationCount[0].count}`); + + // 2. 检查 transfers 表中有多少条 allocation 类型的记录 + const [transferCount] = await connection.execute( + 'SELECT COUNT(*) as count FROM transfers WHERE source_type = \'allocation\'' + ); + console.log(`transfers 表中 allocation 类型记录数: ${transferCount[0].count}`); + + // 3. 检查 order_allocations 表中有多少条记录没有关联的 transfer_id + const [unlinkedCount] = await connection.execute( + 'SELECT COUNT(*) as count FROM order_allocations WHERE transfer_id IS NULL' + ); + console.log(`order_allocations 表中无关联 transfer_id 的记录数: ${unlinkedCount[0].count}`); + + // 4. 检查数据一致性 - 抽样检查 + console.log('\n数据一致性检查(抽样):'); + const [sampleAllocations] = await connection.execute( + 'SELECT * FROM order_allocations WHERE transfer_id IS NOT NULL LIMIT 5' + ); + + for (const allocation of sampleAllocations) { + const [transfer] = await connection.execute( + 'SELECT * FROM transfers WHERE id = ?', + [allocation.transfer_id] + ); + + if (transfer.length === 0) { + console.log(` ✗ 错误: allocation_id=${allocation.id} 关联的 transfer_id=${allocation.transfer_id} 不存在`); + continue; + } + + const transferRecord = transfer[0]; + const isConsistent = + transferRecord.from_user_id == allocation.from_user_id && + transferRecord.to_user_id == allocation.to_user_id && + transferRecord.amount == allocation.amount && + transferRecord.matching_order_id == allocation.matching_order_id && + transferRecord.cycle_number == allocation.cycle_number; + + if (isConsistent) { + console.log(` ✓ allocation_id=${allocation.id} 与 transfer_id=${allocation.transfer_id} 数据一致`); + } else { + console.log(` ✗ 错误: allocation_id=${allocation.id} 与 transfer_id=${allocation.transfer_id} 数据不一致`); + console.log(' allocation:', { + from_user_id: allocation.from_user_id, + to_user_id: allocation.to_user_id, + amount: allocation.amount, + matching_order_id: allocation.matching_order_id, + cycle_number: allocation.cycle_number + }); + console.log(' transfer:', { + from_user_id: transferRecord.from_user_id, + to_user_id: transferRecord.to_user_id, + amount: transferRecord.amount, + matching_order_id: transferRecord.matching_order_id, + cycle_number: transferRecord.cycle_number + }); + } + } + + console.log('\n' + '=' .repeat(60)); + + // 总结 + if (allocationCount[0].count === transferCount[0].count && unlinkedCount[0].count === 0) { + console.log('✓ 验证成功! 所有 order_allocations 记录都已正确迁移到 transfers 表'); + } else { + console.log('⚠ 验证结果: 可能存在未完全迁移的数据'); + console.log(` - order_allocations 总数: ${allocationCount[0].count}`); + console.log(` - transfers 中 allocation 类型数: ${transferCount[0].count}`); + console.log(` - 未关联记录数: ${unlinkedCount[0].count}`); + } + + } catch (error) { + console.error('验证失败:', error); + throw error; + } finally { + // 关闭数据库连接 + if (connection) { + await connection.end(); + } + } +} + +// 如果直接运行此脚本 +if (require.main === module) { + verifyMerge().catch(console.error); +} + +module.exports = verifyMerge; \ No newline at end of file diff --git a/server.js b/server.js new file mode 100644 index 0000000..240399a --- /dev/null +++ b/server.js @@ -0,0 +1,360 @@ +// 加载环境变量配置 +require('dotenv').config(); + +const express = require('express'); +const cors = require('cors'); +const bodyParser = require('body-parser'); +const path = require('path'); +const rateLimit = require('express-rate-limit'); +const helmet = require('helmet'); +const { initDB, getDB, dbConfig } = require('./database'); +const { logger } = require('./config/logger'); +const { errorHandler, notFound } = require('./middleware/errorHandler'); +const fs = require('fs'); + + +const app = express(); +const PORT = process.env.PORT || 3000; + +// 确保日志目录存在 +const logDir = path.join(__dirname, 'logs'); +if (!fs.existsSync(logDir)) { + fs.mkdirSync(logDir, { recursive: true }); +} + +// 安全中间件 +app.use(helmet({ + contentSecurityPolicy: false, // 为了支持前端应用 + crossOriginEmbedderPolicy: false, + crossOriginOpenerPolicy: false, // 禁用 COOP 头部以避免非 HTTPS 环境的警告 + originAgentCluster: false // 禁用Origin-Agent-Cluster头部 +})); + +// 中间件配置 +// CORS配置 - 允许前端访问静态资源 +app.use(cors({ + origin: [ + 'http://localhost:5173', + 'http://localhost:5176', + 'http://localhost:5175', + 'http://localhost:5174', + 'http://localhost:3001', + 'https://www.zrbjr.com', + 'https://zrbjr.com' + ], + credentials: true, + methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], + allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'] +})); +app.use(bodyParser.json({ limit: '10mb' })); +app.use(bodyParser.urlencoded({ extended: true, limit: '10mb' })); + + + +// 请求日志中间件 +app.use((req, res, next) => { + const start = Date.now(); + + res.on('finish', () => { + const duration = Date.now() - start; + + // 只记录非正常状态码的请求日志(过滤掉200、304等正常返回) + if (res.statusCode >= 400 || res.statusCode < 200) { + logger.info('HTTP Request', { + method: req.method, + url: req.originalUrl, + statusCode: res.statusCode, + duration: `${duration}ms`, + ip: req.ip, + userAgent: req.get('User-Agent') + }); + } + }); + + next(); +}); + +// 限流中间件 +const limiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15分钟 + max: 1000, // 限制每个IP 15分钟内最多100个请求 + message: { + success: false, + error: { + code: 'RATE_LIMIT_EXCEEDED', + message: '请求过于频繁,请稍后再试' + } + } +}); +app.use('/api', limiter); + +// 静态文件服务 - 必须在API路由之前 +// 为uploads路径配置CORS和静态文件服务 +app.use('/uploads', express.static(path.join(__dirname, 'uploads'), { + setHeaders: (res, filePath) => { + // 设置CORS头部 + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept'); + + // 设置缓存和内容类型 + if (filePath.endsWith('.jpg') || filePath.endsWith('.jpeg')) { + res.setHeader('Content-Type', 'image/jpeg'); + } else if (filePath.endsWith('.png')) { + res.setHeader('Content-Type', 'image/png'); + } else if (filePath.endsWith('.gif')) { + res.setHeader('Content-Type', 'image/gif'); + } else if (filePath.endsWith('.webp')) { + res.setHeader('Content-Type', 'image/webp'); + } + res.setHeader('Cache-Control', 'public, max-age=86400'); // 1天缓存 + } +})); + +// 处理vite.svg请求 +app.get('/vite.svg', (req, res) => { + const referer = req.get('Referer'); + if (referer && referer.includes('/admin')) { + // 为admin页面提供logo.svg + res.setHeader('Content-Type', 'image/svg+xml'); + res.sendFile(path.join(__dirname, 'admin/dist/logo.svg')); + } else { + // 前端页面没有vite.svg,返回404 + res.status(404).send('File not found'); + } +}); + +// 静态文件服务配置 +// 专门处理admin路径下的assets +app.use('/admin/assets', express.static(path.join(__dirname, 'admin/dist/assets'), { + setHeaders: (res, filePath) => { + res.removeHeader('Origin-Agent-Cluster'); + if (filePath.endsWith('.css')) { + res.setHeader('Content-Type', 'text/css; charset=utf-8'); + res.setHeader('Cache-Control', 'public, max-age=31536000'); + } else if (filePath.endsWith('.js')) { + res.setHeader('Content-Type', 'application/javascript; charset=utf-8'); + res.setHeader('Cache-Control', 'public, max-age=31536000'); + } + } +})); + +// 为admin页面的assets提供服务(当从admin页面访问/assets/时) +app.use('/assets', (req, res, next) => { + // 检查referer来判断是否来自admin页面 + const referer = req.get('Referer'); + if (referer && referer.includes('/admin')) { + // 如果来自admin页面,从admin/dist/assets提供文件 + express.static(path.join(__dirname, 'admin/dist/assets'), { + setHeaders: (res, filePath) => { + res.removeHeader('Origin-Agent-Cluster'); + if (filePath.endsWith('.css')) { + res.setHeader('Content-Type', 'text/css; charset=utf-8'); + res.setHeader('Cache-Control', 'public, max-age=31536000'); + } else if (filePath.endsWith('.js')) { + res.setHeader('Content-Type', 'application/javascript; charset=utf-8'); + res.setHeader('Cache-Control', 'public, max-age=31536000'); + } + } + })(req, res, next); + } else { + // 否则从frontend/dist/assets提供文件 + express.static(path.join(__dirname, 'frontend/dist/assets'), { + setHeaders: (res, filePath) => { + res.removeHeader('Origin-Agent-Cluster'); + if (filePath.endsWith('.css')) { + res.setHeader('Content-Type', 'text/css; charset=utf-8'); + res.setHeader('Cache-Control', 'public, max-age=31536000'); + } else if (filePath.endsWith('.js')) { + res.setHeader('Content-Type', 'application/javascript; charset=utf-8'); + res.setHeader('Cache-Control', 'public, max-age=31536000'); + } + } + })(req, res, next); + } +}); + +app.use('/admin', express.static(path.join(__dirname, 'admin/dist'), { + setHeaders: (res, filePath) => { + // 移除Origin-Agent-Cluster头部以避免冲突 + res.removeHeader('Origin-Agent-Cluster'); + + if (filePath.endsWith('.css')) { + res.setHeader('Content-Type', 'text/css; charset=utf-8'); + res.setHeader('Cache-Control', 'public, max-age=31536000'); // 1年缓存 + } else if (filePath.endsWith('.js')) { + res.setHeader('Content-Type', 'application/javascript; charset=utf-8'); + res.setHeader('Cache-Control', 'public, max-age=31536000'); // 1年缓存 + } else if (filePath.endsWith('.html')) { + res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); // HTML文件不缓存 + res.setHeader('Pragma', 'no-cache'); + res.setHeader('Expires', '0'); + } else if (filePath.endsWith('.svg')) { + res.setHeader('Content-Type', 'image/svg+xml'); + } + } +})); + +app.use(express.static(path.join(__dirname, 'frontend/dist'), { + setHeaders: (res, filePath) => { + // 移除Origin-Agent-Cluster头部以避免冲突 + res.removeHeader('Origin-Agent-Cluster'); + + if (filePath.endsWith('.css')) { + res.setHeader('Content-Type', 'text/css; charset=utf-8'); + res.setHeader('Cache-Control', 'public, max-age=31536000'); // 1年缓存 + } else if (filePath.endsWith('.js')) { + res.setHeader('Content-Type', 'application/javascript; charset=utf-8'); + res.setHeader('Cache-Control', 'public, max-age=31536000'); // 1年缓存 + } else if (filePath.endsWith('.html')) { + res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); // HTML文件不缓存 + res.setHeader('Pragma', 'no-cache'); + res.setHeader('Expires', '0'); + } else if (filePath.endsWith('.svg')) { + res.setHeader('Content-Type', 'image/svg+xml'); + } + } +})); + +// 引入数据库初始化模块 +const { initDatabase } = require('./config/database-init'); + + +// API路由 +app.use('/api/auth', require('./routes/auth')); +app.use('/api/users', require('./routes/users')); +app.use('/api/user', require('./routes/users')); // 添加单数形式的路由映射 +app.use('/api/products', require('./routes/products')); +app.use('/api/specifications', require('./routes/specifications')); +app.use('/api/orders', require('./routes/orders')); +app.use('/api/points', require('./routes/points')); +app.use('/api/captcha', require('./routes/captcha')); // 验证码路由 +app.use('/api/sms', require('./routes/sms')); // 短信验证路由 + +app.use('/api/upload', require('./routes/upload')); +app.use('/api/transfers', require('./routes/transfers')); +app.use('/api/matching', require('./routes/matching')); +app.use('/api/admin/matching', require('./routes/matchingAdmin')); +app.use('/api/system', require('./routes/system')); +app.use('/api/risk', require('./routes/riskManagement')); +app.use('/api/agents', require('./routes/agents')); +app.use('/api/admin/agents', require('./routes/agents/agents')); +app.use('/api/admin/withdrawals', require('./routes/agents/withdrawals')); +app.use('/api/agent-withdrawals', require('./routes/agent-withdrawals')); +app.use('/api/regions', require('./routes/regions')); +app.use('/api/addresses', require('./routes/addresses')); +app.use('/api/address-labels', require('./routes/address-labels')); +app.use('/api/cart', require('./routes/cart')); +app.use('/api/announcements', require('./routes/announcements')); // 通知公告路由 +app.use('/api/wechat-pay', require('./routes/wechatPay')); // 只保留微信支付 +app.use('/api/payment', require('./routes/payment')); + +// 前端路由 - 必须在最后,作为fallback +app.get('/', (req, res) => { + res.removeHeader('Origin-Agent-Cluster'); + res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); + res.setHeader('Pragma', 'no-cache'); + res.setHeader('Expires', '0'); + res.sendFile(path.join(__dirname, 'frontend/dist/index.html')); +}); + +app.get('/admin*', (req, res) => { + res.removeHeader('Origin-Agent-Cluster'); + res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); + res.setHeader('Pragma', 'no-cache'); + res.setHeader('Expires', '0'); + res.sendFile(path.join(__dirname, 'admin/dist/index.html')); +}); + +app.get('/frontend*', (req, res) => { + res.removeHeader('Origin-Agent-Cluster'); + res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); + res.setHeader('Pragma', 'no-cache'); + res.setHeader('Expires', '0'); + res.sendFile(path.join(__dirname, 'frontend/dist/index.html')); +}); + +// SPA fallback - 处理前端路由 +app.get('*', (req, res) => { + // 如果请求的是静态资源但找不到,返回404(不返回JSON) + if (req.path.includes('.')) { + return res.status(404).send('File not found'); + } + // 否则返回前端应用的index.html + res.removeHeader('Origin-Agent-Cluster'); + res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); + res.setHeader('Pragma', 'no-cache'); + res.setHeader('Expires', '0'); + res.sendFile(path.join(__dirname, 'frontend/dist/index.html')); +}); + +// 404处理 +app.use(notFound); + +// 全局错误处理中间件 +app.use(errorHandler); + +// 导出数据库连接供路由使用 +module.exports = { + get db() { return getDB(); } +}; + +// 启动服务器 +app.listen(PORT, async () => { + try { + logger.info('Server starting', { port: PORT }); + console.log(`服务器运行在 http://localhost:${PORT}`); + + await initDatabase(); + // global.sqlReq = mysql.createConnection() + // 启动转账超时检查服务 + const timeoutService = require('./services/timeoutService'); + timeoutService.startTimeoutChecker(); + console.log('转账超时检查服务已启动'); + + // 启动数据库连接监控 + // const dbMonitor = require('./db-monitor'); + // dbMonitor.startMonitoring(60000); // 每分钟监控一次 + // console.log('数据库连接监控已启动'); + global.captchaStore = new Map(); + logger.info('Server started successfully', { + port: PORT, + environment: process.env.NODE_ENV || 'development' + }); + } catch (error) { + logger.error('Failed to start server', { error: error.message }); + process.exit(1); + } +}); + +// 优雅关闭 +process.on('SIGTERM', async () => { + logger.info('SIGTERM received, shutting down gracefully'); + try { + const { closeDB } = require('./database'); + await closeDB(); + } catch (error) { + logger.error('Error closing database', { error: error.message }); + } + process.exit(0); +}); + +process.on('SIGINT', async () => { + logger.info('SIGINT received, shutting down gracefully'); + try { + const { closeDB } = require('./database'); + await closeDB(); + } catch (error) { + logger.error('Error closing database', { error: error.message }); + } + process.exit(0); +}); + +process.on('unhandledRejection', (reason, promise) => { + logger.error('Unhandled Rejection', { reason, promise }); +}); + +process.on('uncaughtException', (error) => { + logger.error('Uncaught Exception', { error: error.message, stack: error.stack }); + process.exit(1); +}); \ No newline at end of file diff --git a/services/alipayservice.js b/services/alipayservice.js new file mode 100644 index 0000000..1e9385c --- /dev/null +++ b/services/alipayservice.js @@ -0,0 +1,306 @@ +const { AlipaySdk } = require('alipay-sdk'); +const { getDB } = require('../database'); +const crypto = require('crypto'); +const path = require('path'); +const fs = require('fs'); + +class AlipayService { + constructor() { + this.privateKey = null; + this.alipayPublicKey = null; + this.alipaySdk = null; + this.isInitialized = false; + + this.initializeAlipay(); + } + + /** + * 初始化支付宝服务 + */ + initializeAlipay() { + try { + // 读取密钥文件 + const privateKeyPath = this.resolveCertPath('../certs/alipay-private-key.pem'); + const publicKeyPath = this.resolveCertPath('../certs/alipay-public-key.pem'); + + console.log('支付宝私钥路径:', privateKeyPath); + console.log('支付宝公钥路径:', publicKeyPath); + this.privateKey = fs.readFileSync(privateKeyPath, 'utf8'); + this.alipayPublicKey = fs.readFileSync(publicKeyPath, 'utf8'); + this.initializeSDK(); + + } catch (error) { + console.error('支付宝服务初始化失败:', error.message); + console.error('支付宝功能将不可用'); + // 不抛出错误,允许服务继续运行 + } + } + + /** + * 初始化支付宝SDK + */ + initializeSDK() { + if (!this.privateKey || !this.alipayPublicKey) { + console.warn('支付宝密钥未加载,跳过SDK初始化'); + return; + } + + // 支付宝配置 + this.config = { + appId: process.env.ALIPAY_APP_ID || '2021001161683774', // 替换为实际的应用ID + privateKey: this.privateKey, // 从文件读取的应用私钥 + alipayPublicKey: this.alipayPublicKey, // 从文件读取的支付宝公钥 + gateway: 'https://openapi.alipay.com/gateway.do', // 支付宝网关地址 + signType: 'RSA2', + charset: 'utf-8', + version: '1.0', + timeout: 5000 + }; + + // 初始化支付宝SDK + this.alipaySdk = new AlipaySdk({ + appId: this.config.appId, + privateKey: this.config.privateKey, + alipayPublicKey: this.config.alipayPublicKey, + gateway: this.config.gateway, + signType: this.config.signType, + timeout: this.config.timeout + }); + + this.isInitialized = true; + console.log('支付宝SDK初始化成功'); + } + + /** + * 解析证书文件路径 + * @param {string} relativePath - 相对路径 + * @returns {string} 绝对路径 + */ + resolveCertPath(relativePath) { + return path.resolve(__dirname, relativePath); + } + + /** + * 验证文件是否有效 + * @param {string} filePath - 文件路径 + * @returns {boolean} 是否为有效文件 + */ + isValidFile(filePath) { + try { + const stats = fs.statSync(filePath); + return stats.isFile(); + } catch (error) { + return false; + } + } + + /** + * 检查支付宝服务是否已初始化 + * @returns {boolean} 是否已初始化 + */ + isServiceAvailable() { + return this.isInitialized && this.alipaySdk !== null; + } + + /** + * 创建注册支付订单 + * @param {Object} params - 支付参数 + * @param {string} params.userId - 用户ID + * @param {string} params.username - 用户名 + * @param {string} params.phone - 手机号 + * @param {string} params.clientIp - 客户端IP + * @returns {Promise} 支付结果 + */ + async createRegistrationPayOrder({ userId, username, phone, clientIp }) { + // 检查服务是否可用 + if (!this.isServiceAvailable()) { + throw new Error('支付宝服务未初始化或不可用'); + } + + try { + const db = getDB(); + + // 生成订单号 + const outTradeNo = this.generateOrderNo(); + const totalFee = 39900; // 399元,单位:分 + const subject = '用户注册激活费用'; + const body = `用户${username}(${phone})注册激活费用`; + + // 业务参数 + const bizContent = { + out_trade_no: outTradeNo, + total_amount: (totalFee / 100).toFixed(2), // 转换为元 + subject: subject, + body: body, + product_code: 'QUICK_WAP_WAY', + quit_url: process.env.ALIPAY_QUIT_URL + }; + + // 使用新版SDK的pageExecute方法生成支付URL + const payUrl = this.alipaySdk.pageExecute('alipay.trade.wap.pay', 'GET', { + bizContent: bizContent, + notifyUrl: process.env.ALIPAY_NOTIFY_URL, + returnUrl: process.env.ALIPAY_RETURN_URL + }); + + // 保存订单到数据库 + await db.execute( + `INSERT INTO payment_orders + (user_id, out_trade_no, total_fee, body, trade_type, status, created_at) + VALUES (?, ?, ?, ?, ?, ?, NOW())`, + [userId, outTradeNo, totalFee, body, 'ALIPAY_WAP', 'pending'] + ); + + console.log('支付宝支付订单创建成功:', { + userId, + outTradeNo, + totalFee, + payUrl + }); + + return { + success: true, + data: { + outTradeNo, + payUrl, + paymentType: 'alipay_wap', + totalFee + } + }; + } catch (error) { + console.error('创建支付宝支付订单失败:', error); + return { + success: false, + message: error.message || '创建支付订单失败' + }; + } + } + + /** + * 查询支付状态 + * @param {string} outTradeNo - 商户订单号 + * @returns {Promise} 查询结果 + */ + async queryPaymentStatus(outTradeNo) { + // 检查服务是否可用 + if (!this.isServiceAvailable()) { + throw new Error('支付宝服务未初始化或不可用'); + } + + try { + const result = await this.alipaySdk.exec('alipay.trade.query', { + bizContent: { + out_trade_no: outTradeNo + } + }); + + if (result.code === '10000') { + // 查询成功 + const tradeStatus = result.tradeStatus; + + // 如果支付成功,更新数据库 + if (tradeStatus === 'TRADE_SUCCESS') { + await this.updatePaymentStatus(outTradeNo, { + status: 'paid', + transactionId: result.tradeNo, + paidAt: new Date() + }); + } + + return { + success: true, + data: { + trade_status: tradeStatus, + trade_no: result.tradeNo, + total_amount: result.totalAmount, + buyer_pay_amount: result.buyerPayAmount, + gmt_payment: result.gmtPayment + } + }; + } else { + return { + success: false, + message: result.msg || '查询支付状态失败' + }; + } + } catch (error) { + console.error('查询支付宝支付状态失败:', error); + return { + success: false, + message: error.message || '查询支付状态失败' + }; + } + } + + /** + * 更新支付状态 + * @param {string} outTradeNo - 商户订单号 + * @param {Object} updateData - 更新数据 + */ + async updatePaymentStatus(outTradeNo, updateData) { + try { + const db = getDB(); + + // 更新订单状态 + await db.execute( + `UPDATE payment_orders + SET status = ?, transaction_id = ?, paid_at = ? + WHERE out_trade_no = ?`, + [updateData.status, updateData.transactionId, updateData.paidAt, outTradeNo] + ); + + // 如果支付成功,更新用户支付状态 + if (updateData.status === 'paid') { + const [orders] = await db.execute( + 'SELECT user_id FROM payment_orders WHERE out_trade_no = ?', + [outTradeNo] + ); + + if (orders.length > 0) { + const userId = orders[0].user_id; + await db.execute( + 'UPDATE users SET payment_status = ? WHERE id = ?', + ['paid', userId] + ); + + console.log('用户支付状态更新成功:', { userId, outTradeNo }); + } + } + } catch (error) { + console.error('更新支付状态失败:', error); + throw error; + } + } + + /** + * 验证支付宝回调签名 + * @param {Object} params - 回调参数 + * @returns {boolean} 验证结果 + */ + verifyNotifySign(params) { + // 检查服务是否可用 + if (!this.isServiceAvailable()) { + console.error('支付宝服务未初始化,无法验证签名'); + return false; + } + + try { + return this.alipaySdk.checkNotifySign(params); + } catch (error) { + console.error('验证支付宝回调签名失败:', error); + return false; + } + } + + /** + * 生成订单号 + * @returns {string} 订单号 + */ + generateOrderNo() { + const timestamp = Date.now(); + const random = Math.floor(Math.random() * 1000).toString().padStart(3, '0'); + return `ALI${timestamp}${random}`; + } +} + +module.exports = AlipayService; \ No newline at end of file diff --git a/services/matchingService.js b/services/matchingService.js new file mode 100644 index 0000000..380696e --- /dev/null +++ b/services/matchingService.js @@ -0,0 +1,1572 @@ +const { getDB } = require('../database'); +const timeoutService = require('./timeoutService'); +const dayjs = require('dayjs'); + +/** + * 获取本地时区的日期字符串(YYYY-MM-DD格式) + * 确保在晚上12点正确切换到第二天 + * @param {Date} date - 可选的日期对象,默认为当前时间 + * @returns {string} 格式化的日期字符串 + */ +function getLocalDateString(date = new Date()) { + return dayjs(date).format('YYYY-MM-DD'); +} + +class MatchingService { + // 创建匹配订单(支持两种模式) + async createMatchingOrder(userId, matchingType = 'small', customAmount = null) { + const db = getDB(); + + try { + // 检查用户是否被拉黑 + const isBlacklisted = await timeoutService.isUserBlacklisted(userId); + if (isBlacklisted) { + throw new Error('您已被拉黑,无法参与匹配。如有疑问请联系管理员。'); + } + + // 检查用户审核状态、必要信息和余额 + const [userResult] = await db.execute( + `SELECT audit_status, + balance, + wechat_qr, + alipay_qr, + unionpay_qr, + bank_card, + business_license, + id_card_front, + id_card_back + FROM users + WHERE id = ?`, + [userId] + ); + + if (userResult.length === 0) { + throw new Error('用户不存在'); + } + + const user = userResult[0]; + + // 检查用户余额:只有负余额用户才能发起匹配 + // if (user.balance > 0) { + // throw new Error('只有余额为负数的用户才能发起匹配,这是为了防止公司资金损失的重要规则'); + // } + + // 检查用户审核状态 + if (user.audit_status !== 'approved') { + if (user.audit_status === 'pending') { + throw new Error('您的账户正在审核中,审核通过后才能参与匹配'); + } else if (user.audit_status === 'rejected') { + throw new Error('您的账户审核未通过,请联系管理员'); + } + } + + // 检查必要的收款信息是否已上传 + const missingItems = []; + if (!user.wechat_qr && !user.alipay_qr && !user.unionpay_qr) { + missingItems.push('收款码(微信/支付宝/云闪付至少一种)'); + } + if (!user.bank_card) { + missingItems.push('银行卡号'); + } + if (!user.business_license) { + missingItems.push('营业执照'); + } + if (!user.id_card_front || !user.id_card_back) { + missingItems.push('身份证正反面'); + } + + if (missingItems.length > 0) { + throw new Error(`请先上传以下信息:${missingItems.join('、')}`); + } + + await db.query('START TRANSACTION'); + + let totalAmount, maxCycles; + + if (matchingType === 'small') { + // 小额匹配:固定5000元金额 + totalAmount = 5000; + maxCycles = 1; + } else if (matchingType === 'large') { + // 大额匹配:用户自定义金额(最高5万) + if (!customAmount || customAmount < 3000 || customAmount > 50000) { + throw new Error('大额匹配金额必须在3000-50000之间'); + } + totalAmount = customAmount; + maxCycles = 1; + } else { + throw new Error('不支持的匹配类型'); + } + + // 创建匹配订单 + const [result] = await db.execute( + 'INSERT INTO matching_orders (initiator_id, amount, status, max_cycles, matching_type) VALUES (?, ?, "matching", ?, ?)', + [userId, totalAmount, maxCycles, matchingType] + ); + + const orderId = result.insertId; + + // 记录用户参与 + await db.execute( + 'INSERT INTO matching_records (matching_order_id, user_id, action, amount) VALUES (?, ?, "join", ?)', + [orderId, userId, totalAmount] + ); + + await db.query('COMMIT'); + + // 立即生成智能分配 + const allocations = await this.generateSmartAllocationsWithDB(orderId, userId); + + // 检查并触发系统账户反向匹配 + // await this.checkAndTriggerSystemMatching(); + + return { + orderId, + matchingType, + totalAmount, + allocations: allocations || [], + allocationCount: allocations ? allocations.length : 0 + }; + } catch (error) { + await db.query('ROLLBACK'); + console.error('创建匹配订单失败:', error); + throw error; + } + } + + /** + * 检查资金平衡并触发系统账户反向匹配 + * 当收款需求大于打款资金时,系统账户主动发起匹配 + */ + async checkAndTriggerSystemMatching() { + const db = getDB(); + + try { + // 计算当前待收款总额(负余额用户的资金缺口) + const [negativeBalanceResult] = await db.execute(` + SELECT SUM(ABS(balance)) as total_deficit + FROM users + WHERE is_system_account = FALSE AND balance < 0 + `); + + const totalDeficit = negativeBalanceResult[0].total_deficit || 0; + + // 计算当前待打款总额(pending状态的分配) + const [pendingPaymentsResult] = await db.execute(` + SELECT SUM(oa.amount) as total_pending + FROM transfers oa + JOIN users u ON oa.from_user_id = u.id + WHERE oa.status = 'pending' AND u.is_system_account = FALSE + `); + + const totalPendingPayments = pendingPaymentsResult[0].total_pending || 0; + + console.log(`资金平衡检查: 总资金缺口=${totalDeficit}, 待打款总额=${totalPendingPayments}`); + + // 如果收款需求大于打款资金,触发系统账户反向匹配 + if (totalDeficit > totalPendingPayments) { + const shortfall = totalDeficit - totalPendingPayments; + console.log(`检测到资金缺口: ${shortfall}元,触发系统账户反向匹配`); + + await this.createSystemReverseMatching(shortfall); + } + } catch (error) { + console.error('检查资金平衡失败:', error); + // 不抛出错误,避免影响主流程 + } + } + + /** + * 创建系统账户反向匹配 + * 系统账户作为付款方,向有资金缺口的用户打款 + * @param {number} targetAmount - 目标匹配金额 + */ + async createSystemReverseMatching(targetAmount) { + const db = getDB(); + + try { + // 获取可用的系统账户 + const [systemAccounts] = await db.execute(` + SELECT id, balance FROM users + WHERE is_system_account = TRUE AND balance > 1000 + ORDER BY balance DESC + LIMIT 1 + `); + + if (systemAccounts.length === 0) { + console.log('没有可用的系统账户进行反向匹配'); + return; + } + + const systemAccount = systemAccounts[0]; + + // 确定实际匹配金额(不超过系统账户余额的80%) + const maxMatchAmount = Math.min(targetAmount, systemAccount.balance * 0.8); + + if (maxMatchAmount < 1000) { + console.log('系统账户余额不足,无法进行反向匹配'); + return; + } + + // 创建系统反向匹配订单 + const [result] = await db.execute( + 'INSERT INTO matching_orders (initiator_id, amount, status, max_cycles, matching_type, is_system_reverse) VALUES (?, ?, "matching", 1, "system_reverse", TRUE)', + [systemAccount.id, maxMatchAmount] + ); + + const orderId = result.insertId; + + // 生成分配给负余额用户 + await this.generateSystemReverseAllocations(orderId, maxMatchAmount, systemAccount.id); + + console.log(`系统反向匹配创建成功: 订单ID=${orderId}, 金额=${maxMatchAmount}`); + + } catch (error) { + console.error('创建系统反向匹配失败:', error); + } + } + + /** + * 为系统反向匹配生成分配 + * @param {number} orderId - 匹配订单ID + * @param {number} totalAmount - 总金额 + * @param {number} systemUserId - 系统账户ID + */ + async generateSystemReverseAllocations(orderId, totalAmount, systemUserId) { + const db = getDB(); + + try { + // 获取负余额用户,按缺口大小排序 + const [negativeUsers] = await db.execute(` + SELECT id, balance, ABS(balance) as deficit + FROM users + WHERE is_system_account = FALSE AND balance < 0 + ORDER BY deficit DESC + LIMIT 10 + `); + + if (negativeUsers.length === 0) { + console.log('没有负余额用户需要反向匹配'); + return; + } + + // 按比例分配金额给负余额用户 + const totalDeficit = negativeUsers.reduce((sum, user) => sum + user.deficit, 0); + let remainingAmount = totalAmount; + + for (let i = 0; i < negativeUsers.length && remainingAmount > 0; i++) { + const user = negativeUsers[i]; + let allocationAmount; + + if (i === negativeUsers.length - 1) { + // 最后一个用户分配剩余金额 + allocationAmount = remainingAmount; + } else { + // 按比例分配 + const proportion = user.deficit / totalDeficit; + allocationAmount = Math.min( + Math.floor(totalAmount * proportion), + user.deficit, + remainingAmount + ); + } + + if (allocationAmount > 0) { + // 创建分配记录(系统账户向用户转账) + await db.execute( + 'INSERT INTO transfers (matching_order_id, from_user_id, to_user_id, amount, cycle_number, status) VALUES (?, ?, ?, ?, 1, "pending")', + [orderId, systemUserId, user.id, allocationAmount] + ); + + remainingAmount -= allocationAmount; + console.log(`系统反向分配: ${allocationAmount}元 从系统账户${systemUserId} 到用户${user.id}`); + } + } + + } catch (error) { + console.error('生成系统反向分配失败:', error); + throw error; + } + } + + /** + * 生成大额匹配的随机金额分拆(15000以上) + * @param {number} totalAmount - 总金额 + * @returns {Array} 分拆后的金额数组 + */ + generateRandomLargeAmounts(totalAmount) { + const amounts = []; + let remaining = totalAmount; + const minAmount = 1000; // 最小单笔金额 + const maxAmount = 8000; // 最大单笔金额 + + while (remaining > maxAmount) { + // 生成随机金额,确保剩余金额至少还能分一笔 + const maxThisAmount = Math.min(maxAmount, remaining - minAmount); + const amount = Math.floor(Math.random() * (maxThisAmount - minAmount + 1)) + minAmount; + amounts.push(amount); + remaining -= amount; + } + + // 最后一笔是剩余金额 + if (remaining > 0) { + amounts.push(remaining); + } + + return amounts; + } + + /** + * 生成3笔分配(兼容旧版本接口) + * 确保不会分配给同一个用户 + * @param {number} orderId - 订单ID + * @param {Array} amounts - 金额数组 + * @param {number} initiatorId - 发起人ID + */ + /** + * 验证匹配金额是否符合业务规则 + * @param {number} userId - 用户ID + * @param {number} totalAmount - 总匹配金额 + * @param {Array} amounts - 分配金额数组 + * @returns {Object} 验证结果和建议金额 + */ + async validateMatchingAmount(userId, totalAmount, amounts) { + const db = getDB(); + + try { + // 获取昨天的日期(本地时区) + const yesterdayStr = dayjs().subtract(1, 'day').format('YYYY-MM-DD'); + + // 获取前一天所有用户的出款总数(系统总出款) + const [systemOutboundResult] = await db.execute( + `SELECT SUM(oa.amount) as total_outbound + FROM transfers oa + JOIN users u ON oa.from_user_id = u.id + WHERE DATE(oa.outbound_date) = ? AND oa.status = 'confirmed' AND u.is_system_account = FALSE`, + [yesterdayStr] + ); + + const systemOutbound = systemOutboundResult[0].total_outbound || 0; + + // 获取前一天所有用户的具体出款金额(用于检查重复) + const [yesterdayAmountsResult] = await db.execute( + `SELECT DISTINCT oa.amount + FROM transfers oa + JOIN users u ON oa.from_user_id = u.id + WHERE DATE(oa.outbound_date) = ? AND oa.status = 'confirmed' AND u.is_system_account = FALSE`, + [yesterdayStr] + ); + + const yesterdayAmounts = yesterdayAmountsResult.map(row => row.amount); + + // 检查每笔金额是否与前一天的金额不同 + const duplicateAmounts = []; + for (const amount of amounts) { + if (yesterdayAmounts.includes(amount)) { + duplicateAmounts.push(amount); + } + } + + return { + isValid: duplicateAmounts.length === 0, + systemOutbound, + duplicateAmounts, + suggestedAmount: systemOutbound, + message: duplicateAmounts.length > 0 + ? `以下金额与前一天重复: ${duplicateAmounts.join(', ')}元` + : '匹配金额验证通过' + }; + } catch (error) { + console.error('验证匹配金额失败:', error); + return { + isValid: false, + systemOutbound: 0, + duplicateAmounts: [], + suggestedAmount: 0, + message: '验证匹配金额时发生错误' + }; + } + } + + /** + * 生成智能分配并创建数据库记录 + * @param {number} orderId - 订单ID + * @param {number} initiatorId - 发起人ID + * @returns {Promise} 返回分配结果数组 + */ + async generateSmartAllocationsWithDB(orderId, initiatorId) { + const db = getDB(); + + try { + // 获取订单总金额 + const [orderResult] = await db.execute( + 'SELECT amount FROM matching_orders WHERE id = ?', + [orderId] + ); + + if (orderResult.length === 0) { + throw new Error('匹配订单不存在'); + } + + const totalAmount = orderResult[0].amount; + + // 使用智能分配算法生成分配方案 + const allocations = await this.generateSmartAllocations(totalAmount, initiatorId); + + if (allocations.length === 0) { + throw new Error('无法生成有效的分配方案'); + } + + // 验证总金额(简化版验证) + const totalAllocated = allocations.reduce((sum, allocation) => sum + allocation.amount, 0); + if (Math.abs(totalAllocated - totalAmount) > 0.01) { + throw new Error(`分配金额不匹配:期望${totalAmount}元,实际分配${totalAllocated}元`); + } + + console.log(`智能分配验证通过: 用户${initiatorId}, 匹配金额${totalAmount}元, 分配${allocations.length}笔`); + + // 创建分配记录 + const createdAllocations = []; + for (let i = 0; i < allocations.length; i++) { + const allocation = allocations[i]; + + // 设置出款日期为今天,可回款时间为明天的00:00:00 + const today = dayjs(); + const tomorrow = dayjs().add(1, 'day').startOf('day'); + + const [result] = await db.execute( + 'INSERT INTO transfers (matching_order_id, from_user_id, to_user_id, amount, cycle_number, status, outbound_date, can_return_after) VALUES (?, ?, ?, ?, 1, "pending", CURDATE(), ?)', + [orderId, initiatorId, allocation.userId, allocation.amount, tomorrow.format('YYYY-MM-DD HH:mm:ss')] + ); + + // 添加分配ID到结果中 + const createdAllocation = { + ...allocation, + allocationId: result.insertId, + status: 'pending', + outboundDate: today.format('YYYY-MM-DD'), + canReturnAfter: tomorrow.toISOString() + }; + + createdAllocations.push(createdAllocation); + + console.log(`创建智能分配: ${allocation.amount}元 从用户${initiatorId} 到用户${allocation.userId}(${allocation.username}) [${allocation.userType}]`); + } + + return createdAllocations; + } catch (error) { + console.error('生成智能分配失败:', error); + throw error; + } + } + + /** + * 生成传统三笔分配(保留原方法用于兼容性) + * @param {number} orderId - 订单ID + * @param {Array} amounts - 分配金额数组 + * @param {number} initiatorId - 发起人ID + * @returns {Promise} + */ + async generateThreeAllocations(orderId, amounts, initiatorId) { + const db = getDB(); + + try { + // 获取订单总金额 + const [orderResult] = await db.execute( + 'SELECT amount FROM matching_orders WHERE id = ?', + [orderId] + ); + + if (orderResult.length === 0) { + throw new Error('匹配订单不存在'); + } + + const totalAmount = orderResult[0].amount; + + // 验证匹配金额是否符合业务规则 + const validation = await this.validateMatchingAmount(initiatorId, totalAmount, amounts); + if (!validation.isValid) { + throw new Error(`匹配金额不符合业务规则:${validation.message}。建议匹配金额:${validation.suggestedAmount}元`); + } + + // 记录验证信息 + console.log(`匹配金额验证通过: 用户${initiatorId}, 匹配金额${totalAmount}元, 前一天系统出款${validation.systemOutbound}元`); + + const usedTargetUsers = new Set(); // 记录已使用的目标用户 + + for (let i = 0; i < amounts.length; i++) { + const amount = amounts[i]; + + // 获取匹配目标,排除已使用的用户 + const targetUser = await this.getMatchingTargetExcluding(initiatorId, usedTargetUsers); + + if (!targetUser) { + throw new Error(`无法为第${i + 1}笔分配找到匹配目标`); + } + + // 记录已使用的目标用户 + usedTargetUsers.add(targetUser); + + // 创建分配记录,默认为第1轮 + // 设置出款日期为今天,可回款时间为明天的00:00:00 + const today = new Date(); + const tomorrow = new Date(today); + tomorrow.setDate(tomorrow.getDate() + 1); + tomorrow.setHours(0, 0, 0, 0); + + // 将Date对象转换为MySQL兼容的字符串格式 + const tomorrowStr = tomorrow.toISOString().slice(0, 19).replace('T', ' '); + + await db.execute( + 'INSERT INTO transfers (matching_order_id, from_user_id, to_user_id, amount, cycle_number, status, outbound_date, can_return_after) VALUES (?, ?, ?, ?, 1, "pending", CURDATE(), ?)', + [orderId, initiatorId, targetUser, amount, tomorrowStr] + ); + + console.log(`创建分配: ${amount}元 从用户${initiatorId} 到用户${targetUser}`); + } + } catch (error) { + console.error('生成分配失败:', error); + throw error; + } + } + + /** + * 获取匹配目标用户 + * @param {number} excludeUserId - 要排除的用户ID + * @returns {number} 目标用户ID + */ + + + /** + * 获取匹配目标用户(排除指定用户集合) + * @param {number} excludeUserId - 要排除的发起人用户ID + * @param {Set} excludeUserIds - 要排除的用户ID集合 + * @returns {number} 目标用户ID + */ + async getMatchingTargetExcluding(excludeUserId, excludeUserIds = new Set()) { + const db = getDB(); + + try { + // 获取今天和昨天的日期(本地时区) + const today = new Date(); + const todayStr = getLocalDateString(today); + const yesterday = new Date(); + yesterday.setDate(yesterday.getDate() - 1); + const yesterdayStr = getLocalDateString(yesterday); + + // 获取前一天打款的用户ID列表(需要排除) + const [yesterdayPayersResult] = await db.execute( + `SELECT DISTINCT oa.from_user_id + FROM transfers oa + WHERE DATE(oa.outbound_date) = ? AND oa.status = 'confirmed'`, + [yesterdayStr] + ); + const yesterdayPayers = yesterdayPayersResult.map(row => row.from_user_id); + + // 获取待确认/待处理/即将生成匹配金额总和超过0的普通用户(需要排除) + const [pendingUsersResult] = await db.execute( + `SELECT oa.to_user_id, SUM(oa.amount) as pending_amount + FROM transfers oa + JOIN users u ON oa.to_user_id = u.id + WHERE oa.status IN ('pending', 'processing', 'generating') + AND u.is_system_account = FALSE + GROUP BY oa.to_user_id + HAVING pending_amount > 0` + ); + const pendingUsers = pendingUsersResult.map(row => row.to_user_id); + + // 获取当天有转出订单的普通用户(需要排除) + const [todayPayersResult] = await db.execute( + `SELECT DISTINCT oa.from_user_id + FROM transfers oa + JOIN users u ON oa.from_user_id = u.id + WHERE DATE(oa.created_at) = ? + AND oa.status IN ('confirmed', 'pending', 'processing') + AND u.is_system_account = FALSE`, + [todayStr] + ); + const todayPayers = todayPayersResult.map(row => row.from_user_id); + + // 构建排除用户的条件(包括发起人、已使用的用户、昨天打款的用户、待处理用户、当天转出用户) + const excludeList = [ + excludeUserId, + ...Array.from(excludeUserIds), + ...yesterdayPayers, + ...pendingUsers, + ...todayPayers + ]; + const placeholders = excludeList.map(() => '?').join(','); + + // 第一优先级:最早成为负数且通过前面检查的普通用户 + // 使用最早的转出记录时间作为成为负数的参考时间 + const [earliestNegativeUsers] = await db.execute( + `SELECT u.id, u.balance, + (SELECT MIN(t.created_at) FROM transfers t + WHERE t.from_user_id = u.id AND t.status IN ('confirmed', 'received')) as first_transfer_time + FROM users u + WHERE u.id NOT IN (${placeholders}) + AND u.is_system_account = FALSE + AND u.balance < 0 + AND (SELECT COUNT(*) FROM transfers t + WHERE t.from_user_id = u.id AND t.status IN ('confirmed', 'received')) > 0 + ORDER BY first_transfer_time ASC, u.balance ASC, RAND() + LIMIT 1`, + excludeList + ); + + if (earliestNegativeUsers.length > 0) { + return earliestNegativeUsers[0].id; + } + + // 第二优先级:有可回款分配的普通用户(昨天或更早出款,今天可以回款) + // 但必须是负余额用户,且通过前面的检查 + const [returnableUsers] = await db.execute( + `SELECT DISTINCT oa.from_user_id as id, u.balance + FROM transfers oa + JOIN matching_orders mo ON oa.id = mo.id + JOIN users u ON oa.from_user_id = u.id + WHERE oa.from_user_id NOT IN (${placeholders}) + AND oa.status = 'confirmed' + AND oa.can_return_after <= NOW() + AND oa.return_date IS NULL + AND mo.status = 'matching' + AND u.balance < 0 + AND u.is_system_account = FALSE + ORDER BY oa.can_return_after ASC, u.balance ASC, RAND() + LIMIT 1`, + excludeList + ); + + if (returnableUsers.length > 0) { + return returnableUsers[0].id; + } + + // 第三优先级:其他负余额普通用户(余额为负数说明他们给其他用户转过钱,钱还没收回来) + const [negativeBalanceUsers] = await db.execute( + `SELECT id FROM users + WHERE id NOT IN (${placeholders}) + AND is_system_account = FALSE + AND balance < 0 + ORDER BY balance ASC, RAND() + LIMIT 1`, + excludeList + ); + + if (negativeBalanceUsers.length > 0) { + return negativeBalanceUsers[0].id; + } + + // 最后优先级:虚拟用户(系统账户) + const [systemUsers] = await db.execute( + `SELECT id FROM users + WHERE is_system_account = TRUE AND id NOT IN (${placeholders}) + ORDER BY balance DESC, RAND() + LIMIT 1`, + excludeList + ); + + if (systemUsers.length > 0) { + return systemUsers[0].id; + } + + // 如果连系统账户都没有,抛出错误 + throw new Error('没有可用的匹配目标:所有符合条件的用户都被排除'); + } catch (error) { + console.error('获取匹配目标失败:', error); + throw error; + } + } + + // 获取可用用户 + + + /** + * 生成智能分配金额 + * 1. 排除今天打款的用户 + * 2. 优先分配给负余额用户(余额+待确认收款为负数) + * 3. 每笔最高5000,不够再分配给虚拟用户 + * 4. 笔数3-10笔 + * @param {number} totalAmount - 总金额 + * @param {number} excludeUserId - 排除的用户ID(发起人) + * @returns {Promise} 分配金额数组 + */ + async generateSmartAllocations(totalAmount, excludeUserId) { + const db = getDB(); + const minAmount = 100; + const maxAmountPerTransfer = totalAmount; + const minTransfers = totalAmount > 5000 ? 4 : 3; + const maxTransfers = 10; + + try { + // 首先获取当前用户的城市、省份和区域信息 + const [currentUserResult] = await db.execute( + `SELECT city, province, district_id FROM users WHERE id = ?`, + [excludeUserId] + ); + + const currentUserCity = currentUserResult[0]?.city; + const currentUserProvince = currentUserResult[0]?.province; + const currentUserDistrictId = currentUserResult[0]?.district_id; + + // 获取负余额用户,按区县、城市、省份优先级排序 + let [userBalanceResult] = await db.execute( + `SELECT + u.id as user_id, + u.balance as current_balance, + u.city, + u.province, + u.district_id + FROM users u + WHERE u.is_system_account = FALSE + AND u.is_distribute = TRUE + AND u.id != ? + AND u.balance < -100 + AND u.audit_status = 'approved' + AND u.user_type != 'directly_operated' + AND u.payment_status = 'paid' + AND u.province = ? + ORDER BY + CASE + WHEN u.city = ? AND u.district_id = ? THEN 1 -- 相同城市且相同区县排第一 + WHEN u.city = ? THEN 2 -- 相同城市但不同区县排第二 + WHEN u.province = ? THEN 3 -- 相同省份但不同城市排第三 + ELSE 4 -- 其他省份排第四 + END, + u.balance ASC`, + [excludeUserId,currentUserProvince, currentUserCity, currentUserDistrictId, currentUserCity, currentUserProvince] + ); + + // 处理查询到的负余额用户 + const availableUsers = []; + + for (const user of userBalanceResult) { + // 确保余额是数字类型 + const currentBalance = parseFloat(user.current_balance) || 0; + + // 更新用户对象 + user.current_balance = currentBalance; + + // 查询用户的分配订单金额统计 + const [orderStatusResult] = await db.execute( + `SELECT + SUM(CASE WHEN status = 'pending' THEN amount ELSE 0 END) as pending_amount + FROM transfers + WHERE to_user_id = ?`, + [user.user_id] + ); + + // 查询用户的分配订单金待确认金额统计 + const [orderStatusConfirmedResult] = await db.execute( + `SELECT + SUM(CASE WHEN status = 'confirmed' THEN amount ELSE 0 END) as confirmed_amount + FROM transfers + WHERE to_user_id = ?`, + [user.user_id] + ); + //查询用户给其他用户已确认的金额统计(要减去,因为款项还没回来) + const [orderStatusConfirmedResultFrom] = await db.execute( + `SELECT + SUM(CASE WHEN status = 'confirmed' THEN amount ELSE 0 END) as confirmed_amount + FROM transfers + WHERE from_user_id = ?`, + [user.user_id] + ); + // 查询用户当天在matching_orders表中打出去的款项 + const today = getLocalDateString(); + const [todayOutflowResult] = await db.execute( + `SELECT + SUM(amount) as today_outflow + FROM matching_orders + WHERE initiator_id = ? AND DATE(updated_at) = ?`, + [user.user_id, today] + ); + + // 添加分配金额信息到用户对象 + const orderStatus = orderStatusResult[0] || { pending_amount: 0 }; + const todayOutflow = todayOutflowResult[0] || { today_outflow: 0 }; + const orderStatusConfirmedFrom = orderStatusConfirmedResultFrom[0] || { confirmed_amount: 0 }; + const orderStatusConfirmed = orderStatusConfirmedResult[0] || { confirmed_amount: 0 }; + user.today_outflow = parseFloat(todayOutflow.today_outflow) || 0; + user.pending_amount = parseFloat(orderStatus.pending_amount) || 0; + user.confirmed_amount = parseFloat(orderStatusConfirmed.confirmed_amount) || 0; + user.has_active_allocations = user.current_balance + user.pending_amount + user.confirmed_amount + user.today_outflow - orderStatusConfirmedFrom.confirmed_amount; + + + + // 所有查询到的用户都是负余额用户,直接添加到可用列表 + } + userBalanceResult = userBalanceResult.filter(user => user.has_active_allocations < -100); + userBalanceResult = userBalanceResult.sort((a, b) => { + const getPriority = (user) => { + if (user.district_id === currentUserDistrictId) return 1; // 同区县 + if (user.city === currentUserCity) return 2; // 同城市 + return 3; // 其他 + }; + + const priorityA = getPriority(a); + const priorityB = getPriority(b); + + if (priorityA !== priorityB) { + return priorityA - priorityB; // 优先级小的排前 + } + + // 同优先级里:越接近0(数值越大)排前 -> 使用降序 + return b.has_active_allocations - a.has_active_allocations; + }); + for (const user of userBalanceResult) { + if (maxTransfers > availableUsers.length + 1) { + if (minTransfers === 3 && availableUsers.length < 3) { + availableUsers.push(user); + } + if (minTransfers === 4) { + availableUsers.push(user); + } + } + console.log(user, '普通用户'); + } + + + console.log(`可参与分配的负余额用户数量: ${availableUsers.length}`); + + // 第二步:由于第一步已经筛选了余额小于0的用户,这里直接使用可用用户作为优先分配用户 + // 用户已按余额升序排列(最负的优先),然后按可分配金额降序排列 + const priorityUsers = availableUsers; // 所有可用用户都是负余额用户,无需再次筛选 + + // 第三步:获取虚拟用户作为备选 + const [virtualUsersResult] = await db.execute( + `SELECT id, username, balance FROM users + WHERE is_system_account = TRUE + ORDER BY balance DESC, RAND()` + ); + + // 计算分配方案 + const allocations = []; + let remainingAmount = totalAmount; + + // 优先分配给当前余额为负的用户 + for (const user of priorityUsers) { + if (remainingAmount <= 0 || allocations.length >= maxTransfers) break; + + // 计算该用户可接受的最大分配金额 + // 确保分配后用户余额不会变成正数 + const currentBalance = Math.abs(user.has_active_allocations); + // 使用随机分配而不是平均分配 + const remainingTransfers = minTransfers - allocations.length; + const minRequiredForRemaining = Math.max(0, (remainingTransfers - 1) * minAmount); // 为剩余转账预留的最小金额,确保不为负数 + + console.log(`用户${user.user_id}分配计算: remainingAmount=${remainingAmount}, remainingTransfers=${remainingTransfers}, minRequiredForRemaining=${minRequiredForRemaining}`); + + const maxRandomAllocation = Math.min( + currentBalance, // 不能超过安全分配额度,确保接收后余额不会变成正数 + maxAmountPerTransfer, // 单笔最大金额限制 + remainingAmount - minRequiredForRemaining // 确保剩余金额足够分配给后续转账 + ); + + // 生成随机分配金额,使用极度偏向大值的算法 + let maxUserAllocation = 0; + if (maxRandomAllocation >= minAmount) { + const range = maxRandomAllocation - minAmount; + if (range <= 0) { + maxUserAllocation = minAmount; + } else { + if (maxRandomAllocation > 1000) { + // 使用更均匀的分配策略 + const randomFactor = Math.random(); // 使用均匀分布 + + // 基础分配:在整个范围内更均匀分布,减少偏向性 + const baseOffset = Math.floor(range * 0.15); // 降低到15%的基础偏移 + const adjustedRange = range - baseOffset; + maxUserAllocation = Math.floor(randomFactor * adjustedRange) + minAmount + baseOffset; + + // 进一步减少额外增量的影响 + const bonusRange = Math.min(range * 0.1, maxRandomAllocation - maxUserAllocation); // 降低到10% + if (bonusRange > 0 && Math.random() > 0.7) { // 30%概率获得额外增量,进一步降低 + const bonus = Math.floor(Math.random() * bonusRange * 0.3); // 使用30%的bonus范围 + maxUserAllocation += bonus; + } + }else{ + maxUserAllocation = maxRandomAllocation + } + + + // 确保不超过最大限制 + maxUserAllocation = Math.min(maxUserAllocation, maxRandomAllocation); + } + } + console.log(maxUserAllocation, minAmount, '+++++++++++++++'); + + if (maxUserAllocation >= minAmount) { + allocations.push({ + userId: user.user_id, + username: user.username || `用户${user.user_id}`, + amount: maxUserAllocation, + userType: 'priority_user', + currentBalance: user.current_balance, + historicalNetBalance: user.historical_net_balance, + totalPendingInflow: user.total_pending_inflow, + availableForAllocation: user.available_for_allocation, + todayOutflow: user.today_outflow, + has_active_allocations: user.has_active_allocations + }); + remainingAmount -= maxUserAllocation; + } + } + + // 如果还有剩余金额且分配数量不足最小笔数,最后分配给虚拟用户 + const availableVirtualUsers = virtualUsersResult + + // 如果有剩余金额,优先检查现有非虚拟用户是否还能消化 + if (remainingAmount > 0) { + // 筛选出非虚拟用户分配记录 + + if (allocations.length > 0) { + let totalAvailableCapacity = 0; + const userCapacities = []; + + // 计算每个用户的剩余可分配容量 + for (const allocation of allocations) { + // 获取用户当前的实际余额状态(使用has_active_allocations作为实际可分配余额) + const maxSafeAmount = Math.abs(allocation.has_active_allocations); + const remainingCapacity = maxSafeAmount - allocation.amount; + + if (remainingCapacity > 0) { + userCapacities.push({ + allocation, + capacity: remainingCapacity + }); + totalAvailableCapacity += remainingCapacity; + } + } + + console.log(`现有用户剩余容量: ${totalAvailableCapacity}, 待分配金额: ${remainingAmount}`); + + // 如果现有用户能够消化剩余金额 + if (totalAvailableCapacity >= remainingAmount && userCapacities.length > 0 && allocations.length >= 3) { + // 按平均分配给这些用户,但需要检查每个用户的分配上限 + const averageAmount = Math.floor(remainingAmount / userCapacities.length); + let distributedAmount = 0; + let remainingToDistribute = remainingAmount; + + for (let i = 0; i < userCapacities.length; i++) { + const { allocation, capacity } = userCapacities[i]; + + // 计算本次可分配的金额 + let amountToAdd = 0; + + if (i === userCapacities.length - 1) { + // 最后一个用户分配剩余的所有金额,但不能超过其容量 + amountToAdd = Math.min(remainingToDistribute, capacity); + } else { + // 其他用户按平均分配,但不能超过其容量 + amountToAdd = Math.min(averageAmount, capacity); + } + + if (amountToAdd > 0) { + allocation.amount += amountToAdd; + distributedAmount += amountToAdd; + remainingToDistribute -= amountToAdd; + console.log(`为用户${allocation.userId}追加分配${amountToAdd}元,总分配${allocation.amount}元,剩余容量${capacity - amountToAdd}元`); + } + } + + // 更新实际分配的剩余金额 + remainingAmount = remainingToDistribute; + + if (remainingAmount === 0) { + console.log('剩余金额已全部分配给现有用户'); + } else { + console.log(`部分剩余金额已分配给现有用户,仍有${remainingAmount}元未分配`); + } + } + } + } + + // 如果仍有剩余金额,检查是否有未分配的用户可以消化剩余金额 + if (remainingAmount > 0) { + // 获取已分配的用户ID列表 + const allocatedUserIds = new Set(allocations.map(a => a.userId)); + + // 从原始用户列表中找到未分配的用户 + const unallocatedUsers = userBalanceResult.filter(user => !allocatedUserIds.has(user.user_id)); + + if (unallocatedUsers.length > 0) { + console.log(`发现${unallocatedUsers.length}个未分配的用户,剩余金额: ${remainingAmount}`); + + // 查找可分配金额大于剩余金额的用户 + for (const user of unallocatedUsers) { + const maxSafeAmount = Math.abs(user.has_active_allocations); + + if (maxSafeAmount >= remainingAmount) { + // 找到合适的用户,分配剩余金额 + allocations.push({ + userId: user.user_id, + username: user.username || `User${user.user_id}`, + amount: remainingAmount, + userType: 'priority_user', + currentBalance: user.current_balance, + availableForAllocation: user.has_active_allocations + }); + + console.log(`为未分配用户${user.user_id}分配剩余金额${remainingAmount}元`); + remainingAmount = 0; + break; + } + } + } + } + + // 如果仍有剩余金额,分配给虚拟用户 + if (remainingAmount > 0 && availableVirtualUsers.length > 0) { + const maxPossibleTransfers = Math.min((minTransfers - allocations.length) <= 0 ? 1 : minTransfers - allocations.length, availableVirtualUsers.length); + + // 生成随机分配金额数组 + const randomAmounts = this.generateRandomAmounts(remainingAmount, maxPossibleTransfers, minAmount, maxAmountPerTransfer); + + // 为每个随机金额分配虚拟用户 + for (let i = 0; i < randomAmounts.length && availableVirtualUsers.length > 0; i++) { + const randomIndex = Math.floor(Math.random() * availableVirtualUsers.length); + const virtualUser = availableVirtualUsers[randomIndex]; + + allocations.push({ + userId: virtualUser.id, + username: virtualUser.username, + amount: randomAmounts[i], + userType: 'virtual', + balance: virtualUser.balance + }); + + remainingAmount -= randomAmounts[i]; + availableVirtualUsers.splice(randomIndex, 1); + } + } + + // 检查是否有足够的用户来完成分配 + if (remainingAmount > 0 && allocations.length < minTransfers && availableVirtualUsers.length === 0) { + throw new Error('没有足够的可用用户来完成分配(避免重复分配给同一用户)'); + } + + // 确保至少有最小笔数 + // if (allocations.length < minTransfers) { + // throw new Error(`无法生成足够的分配:需要至少${minTransfers}笔,但只能生成${allocations.length}笔`); + // } + + // 精确控制总金额,避免超出预期 + const currentTotal = allocations.reduce((sum, a) => sum + a.amount, 0); + console.log('剩余金额处理前:', remainingAmount, '当前总分配金额:', currentTotal, '期望总金额:', totalAmount); + + if (remainingAmount > 0 && allocations.length > 0) { + // 检查是否会超出总金额 + if (currentTotal + remainingAmount <= totalAmount) { + console.log('将剩余金额', remainingAmount, '加到最后一笔分配'); + allocations[allocations.length - 1].amount += remainingAmount; + } else { + // 如果会超出,只加到刚好等于总金额的部分 + const allowedAmount = totalAmount - currentTotal; + if (allowedAmount > 0) { + + console.log('调整最后一笔分配,增加', allowedAmount, '元以达到精确总金额'); + allocations[allocations.length - 1].amount += allowedAmount; + } + } + remainingAmount = 0; // 重置剩余金额 + } + + console.log(`智能分配完成: 总金额${totalAmount}元,分配${allocations.length}笔`); + console.log('分配详情:', allocations.map(a => + `${a.amount}元 -> 用户${a.userId}(${a.username}) [${a.userType}]` + ).join(', ')); + + return allocations; + + } catch (error) { + console.error('智能分配失败:', error); + throw error; + } + } + + /** + * 生成随机分配金额数组(更均匀的分配策略) + * @param {number} totalAmount - 总金额 + * @param {number} transferCount - 分配笔数 + * @param {number} minAmount - 最小金额 + * @param {number} maxAmount - 最大金额 + * @returns {number[]} 随机金额数组 + */ + generateRandomAmounts(totalAmount, transferCount, minAmount, maxAmount) { + if (transferCount <= 0 || totalAmount < minAmount * transferCount) { + return []; + } + + // 使用更均匀的分配策略 + const amounts = []; + + // 首先为每笔分配最小金额 + for (let i = 0; i < transferCount; i++) { + amounts.push(minAmount); + } + + let remainingToDistribute = totalAmount - (minAmount * transferCount); + + // 计算平均每笔应该额外分配的金额 + const averageExtra = Math.floor(remainingToDistribute / transferCount); + + // 为每笔添加平均额外金额,但加入一些随机性 + for (let i = 0; i < transferCount && remainingToDistribute > 0; i++) { + // 计算这笔最多还能增加多少(不超过maxAmount) + const maxPossibleIncrease = Math.min( + maxAmount - amounts[i], + remainingToDistribute + ); + + if (maxPossibleIncrease > 0) { + // 在平均值附近随机分配,但控制在更小的范围内以保证更均匀 + const baseIncrease = Math.min(averageExtra, maxPossibleIncrease); + const randomVariation = Math.floor(baseIncrease * 0.15); // 减少到15%的随机变化 + const minIncrease = Math.max(0, baseIncrease - randomVariation); + const maxIncrease = Math.min(maxPossibleIncrease, baseIncrease + randomVariation); + + const increase = Math.floor(Math.random() * (maxIncrease - minIncrease + 1)) + minIncrease; + amounts[i] += increase; + remainingToDistribute -= increase; + } + } + + // 如果还有剩余金额,尽量均匀分配给还能接受的笔数 + while (remainingToDistribute > 0) { + const availableIndices = []; + for (let i = 0; i < transferCount; i++) { + if (amounts[i] < maxAmount) { + availableIndices.push(i); + } + } + + if (availableIndices.length === 0) { + break; // 无法继续分配 + } + + // 计算每个可用位置应该分配多少 + const perIndexAmount = Math.floor(remainingToDistribute / availableIndices.length); + const remainder = remainingToDistribute % availableIndices.length; + + // 为每个可用位置分配相等的金额 + for (let i = 0; i < availableIndices.length && remainingToDistribute > 0; i++) { + const index = availableIndices[i]; + const maxIncrease = Math.min(maxAmount - amounts[index], remainingToDistribute); + + if (maxIncrease > 0) { + // 基础分配金额 + let increase = Math.min(perIndexAmount, maxIncrease); + + // 如果是前几个位置,额外分配余数 + if (i < remainder) { + increase = Math.min(increase + 1, maxIncrease); + } + + amounts[index] += increase; + remainingToDistribute -= increase; + } + } + + // 如果所有位置都已达到最大值,退出循环 + if (perIndexAmount === 0 && remainder === 0) { + break; + } + } + + // 如果还有剩余金额无法分配,返回空数组表示失败 + if (remainingToDistribute > 0) { + return []; + } + + return amounts; + } + + // 生成3笔随机金额,总计指定金额(保留原方法用于兼容性) + generateThreeRandomAmounts(totalAmount) { + // 确保总金额足够分配三笔最小金额 + const minAmount = 500; + const maxAmount = Math.min(5000, totalAmount - 2 * minAmount); + + // 生成第一笔金额 (500-5000) + const amount1 = Math.floor(Math.random() * (maxAmount - minAmount + 1)) + minAmount; + + // 生成第二笔金额 (500-剩余金额-500) + const remaining1 = totalAmount - amount1; + const maxAmount2 = Math.min(5000, remaining1 - minAmount); + const amount2 = Math.floor(Math.random() * (maxAmount2 - minAmount + 1)) + minAmount; + + // 第三笔是剩余金额 + const amount3 = totalAmount - amount1 - amount2; + + return [amount1, amount2, amount3]; + } + + // 生成随机金额(保留原方法用于其他地方) + + + /** + * 确认分配并创建转账记录 + * @param {number} allocationId - 分配ID + * @param {number} userId - 用户ID + * @param {number} transferAmount - 实际转账金额(用于校验) + * @param {string} description - 转账描述 + * @param {string} voucher - 转账凭证URL + * @returns {number} 转账记录ID + */ + async confirmAllocation(allocationId, userId, transferAmount = null, description = null, voucher = null) { + const db = getDB(); + + try { + await db.query('START TRANSACTION'); + + // 获取分配信息 + const [allocations] = await db.execute( + 'SELECT * FROM transfers WHERE id = ? AND from_user_id = ?', + [allocationId, userId] + ); + + if (allocations.length === 0) { + throw new Error('分配不存在或无权限'); + } + + const allocation = allocations[0]; + + // 校验转账金额(如果提供了转账金额) + if (transferAmount !== null) { + const expectedAmount = parseFloat(allocation.amount); + const actualAmount = parseFloat(transferAmount); + + if (Math.abs(expectedAmount - actualAmount) > 0.01) { + throw new Error(`转账金额不匹配!应转账 ${expectedAmount} 元,实际转账 ${actualAmount} 元`); + } + } + + // 检查分配状态 + if (allocation.status !== 'pending') { + throw new Error('该分配已处理,无法重复确认'); + } + + // 检查匹配订单是否已超时 + const [matchingOrder] = await db.execute( + 'SELECT * FROM matching_orders WHERE id = ?', + [allocation.matching_order_id] + ); + + if (matchingOrder.length === 0) { + throw new Error('匹配订单不存在'); + } + + // 检查订单是否已被取消(超时会导致订单被取消) + if (matchingOrder[0].status === 'cancelled') { + throw new Error('该匹配订单已超时取消,无法进行转账'); + } + + // 检查是否存在相关的超时转账记录 + const [timeoutTransfers] = await db.execute( + `SELECT COUNT(*) as count FROM transfers + WHERE (from_user_id = ? OR to_user_id = ?) + AND is_overdue = 1 + AND description LIKE ?`, + [userId, userId, `%匹配订单 ${allocation.matching_order_id}%`] + ); + + if (timeoutTransfers[0].count > 0) { + throw new Error('该匹配订单存在超时记录,无法继续转账。请联系管理员处理'); + } + + // 计算3小时后的截止时间 + const deadline = dayjs().add(3, 'hour').toDate(); + + // 更新转账记录状态为confirmed,跳过待确认环节 + const transferDescription = description || `匹配订单 ${allocation.matching_order_id} 第 ${allocation.cycle_number} 轮转账`; + const [transferResult] = await db.execute( + `UPDATE transfers + SET status = "confirmed", description = ?, deadline_at = ?, confirmed_at = NOW(), voucher_url = ? + WHERE id = ?`, + [ + transferDescription, + deadline, + voucher, + allocationId + ] + ); + + // 注意:发送方余额将在接收方确认收款时扣除,而不是在确认转账时扣除 + // 这样可以避免资金被锁定但收款方未确认的情况 + + // 记录确认动作 + await db.execute( + 'INSERT INTO matching_records (matching_order_id, user_id, action, amount, note) VALUES (?, ?, "confirm", ?, ?)', + [ + allocation.matching_order_id, + userId, + allocation.amount, + transferAmount ? `实际转账金额: ${transferAmount}` : null + ] + ); + + await db.query('COMMIT'); + + // 检查是否需要进入下一轮 + await this.checkCycleCompletion(allocation.matching_order_id, allocation.cycle_number); + + return transferResult.insertId; + } catch (error) { + await db.query('ROLLBACK'); + throw error; + } + } + + // 检查轮次完成情况 + async checkCycleCompletion(matchingOrderId, cycleNumber) { + const db = getDB(); + + try { + // 检查当前轮次是否全部确认 + const [pending] = await db.execute( + 'SELECT COUNT(*) as count FROM transfers WHERE matching_order_id = ? AND cycle_number = ? AND status = "pending"', + [matchingOrderId, cycleNumber] + ); + + if (pending[0].count === 0) { + // 当前轮次完成,检查是否需要下一轮 + const [order] = await db.execute( + 'SELECT * FROM matching_orders WHERE id = ?', + [matchingOrderId] + ); + + const currentOrder = order[0]; + + if (currentOrder.cycle_count + 1 < currentOrder.max_cycles) { + // 开始下一轮 + await db.execute( + 'UPDATE matching_orders SET cycle_count = cycle_count + 1 WHERE id = ?', + [matchingOrderId] + ); + + // 生成下一轮分配 + const amounts = this.generateThreeRandomAmounts(currentOrder.amount); + await this.generateThreeAllocations(matchingOrderId, amounts, currentOrder.initiator_id); + } else { + // 完成所有轮次 + await db.execute( + 'UPDATE matching_orders SET status = "completed" WHERE id = ?', + [matchingOrderId] + ); + + console.log(`匹配订单 ${matchingOrderId} 已完成所有轮次`); + + // 检查用户是否完成第三次匹配,如果是则给代理分佣 + await this.checkAndProcessAgentCommission(currentOrder.initiator_id); + } + } + } catch (error) { + console.error('检查轮次完成情况失败:', error); + throw error; + } + } + + // 获取用户的匹配订单 + async getUserMatchingOrders(userId, page = 1, limit = 10) { + const db = getDB(); + const offset = (parseInt(page) - 1) * parseInt(limit); + + + + try { + // 获取用户发起的订单 + const [orders] = await db.execute( + `SELECT mo.*, u.username as initiator_name,u.real_name as initiator_real_name + FROM matching_orders mo + JOIN users u ON mo.initiator_id = u.id + WHERE mo.initiator_id = ? + ORDER BY mo.created_at DESC + LIMIT ${parseInt(limit)} OFFSET ${parseInt(offset)}`, + [userId] + ); + + // 同时获取系统反向匹配订单(如果用户参与了分配) + const [systemOrders] = await db.execute( + `SELECT DISTINCT mo.*, u.username as initiator_name + FROM matching_orders mo + JOIN users u ON mo.initiator_id = u.id + JOIN transfers oa ON mo.id = oa.id + WHERE mo.is_system_reverse = TRUE AND oa.to_user_id = ? + ORDER BY mo.created_at DESC + LIMIT ${parseInt(limit)} OFFSET ${parseInt(offset)}`, + [userId] + ); + + // 合并订单列表 + const allOrders = [...orders, ...systemOrders]; + + // 按创建时间排序 + allOrders.sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); + + // 为每个订单获取分配信息 + for (let order of allOrders) { + const [allocations] = await db.execute( + `SELECT * FROM transfers WHERE matching_order_id = ? ORDER BY cycle_number, created_at`, + [order.id] + ); + order.allocations = allocations; + } + + return allOrders; + } catch (error) { + console.error('获取用户匹配订单失败:', error); + throw error; + } + } + + + + // 获取用户待处理的分配 + async getUserPendingAllocations(userId) { + const db = getDB(); + + try { + const [allocations] = await db.execute( + `(SELECT oa.*, mo.amount as total_amount, mo.status as order_status, u.username as to_user_name, + u.real_name as to_user_real_name, + DATE_ADD(oa.created_at, INTERVAL 150 MINUTE) as expected_deadline, + oa.outbound_date, + oa.return_date, + oa.can_return_after, + oa.confirmed_at + FROM transfers oa + JOIN matching_orders mo ON oa.matching_order_id = mo.id + JOIN users u ON oa.to_user_id = u.id + WHERE oa.from_user_id = ? AND oa.status = "pending" AND mo.status != "cancelled" + AND (oa.source_type IS NULL)) + UNION ALL + (SELECT oa.*, oa.amount as total_amount, 'active' as order_status, u.username as to_user_name, + u.real_name as to_user_real_name, + DATE_ADD(oa.created_at, INTERVAL 150 MINUTE) as expected_deadline, + oa.outbound_date, + oa.return_date, + oa.can_return_after, + oa.confirmed_at + FROM transfers oa + JOIN users u ON oa.to_user_id = u.id + WHERE oa.from_user_id = ? AND oa.status = "pending") + ORDER BY created_at ASC`, + [userId, userId] + ); + + // 检查每个分配的超时状态,但不过滤掉 + const allocationsWithTimeoutStatus = []; + for (const allocation of allocations) { + // 检查是否存在相关的超时转账记录 + const [timeoutTransfers] = await db.execute( + `SELECT COUNT(*) as count FROM transfers + WHERE (from_user_id = ? OR to_user_id = ?) + AND is_overdue = 1 + AND description LIKE ?`, + [userId, userId, `%匹配订单 ${allocation.matching_order_id}%`] + ); + + // 添加超时状态标记 + allocation.has_timeout_record = timeoutTransfers[0].count > 0; + allocationsWithTimeoutStatus.push(allocation); + } + // 检查并处理超时订单 + const now = new Date(); + // 隐藏系统账户身份并添加时效状态 + const processedAllocations = allocationsWithTimeoutStatus.map(allocation => { + const deadline = allocation.transfer_deadline || allocation.expected_deadline; + const deadlineDate = new Date(deadline); + const timeLeft = deadlineDate - now; + + // 计算剩余时间 + let timeStatus = 'normal'; + let timeLeftText = ''; + + if (timeLeft <= 0) { + timeStatus = 'expired'; + timeLeftText = '已超时'; + } else if (timeLeft <= 2.5 * 60 * 60 * 1000) { // 2.5小时内 + timeStatus = 'urgent'; + const hours = Math.floor(timeLeft / (60 * 60 * 1000)); + const minutes = Math.floor((timeLeft % (60 * 60 * 1000)) / (60 * 1000)); + timeLeftText = hours > 0 ? `${hours}小时${minutes}分钟` : `${minutes}分钟`; + } else { + const hours = Math.floor(timeLeft / (60 * 60 * 1000)); + const minutes = Math.floor((timeLeft % (60 * 60 * 1000)) / (60 * 1000)); + timeLeftText = `${hours}小时${minutes}分钟`; + } + + return { + ...allocation, + to_user_name: allocation.to_user_name || '匿名用户', + is_system_account: undefined, // 移除系统账户标识 + deadline: deadline, + time_status: timeStatus, + time_left: timeLeftText, + can_transfer: !allocation.has_timeout_record, // 是否可以转账 + timeout_reason: allocation.has_timeout_record ? '该匹配订单存在超时记录,无法继续转账' : null + }; + }); + + return processedAllocations; + } catch (error) { + console.error('获取用户待处理分配失败:', error); + throw error; + } + } + + // 检查并处理代理佣金 + async checkAndProcessAgentCommission(userId) { + const db = getDB(); + + try { + // 检查用户是否有代理关系 + const [agentRelation] = await db.execute( + 'SELECT agent_id, created_at FROM agent_merchants WHERE merchant_id = ?', + [userId] + ); + + if (agentRelation.length === 0) { + return; // 用户没有代理,无需处理 + } + + const agentId = agentRelation[0].agent_id; + const agentJoinTime = agentRelation[0].created_at; + + // 检查用户给他人转账的次数(状态为已收款,且转账时间在代理商入驻之后) + const [completedTransfers] = await db.execute( + 'SELECT COUNT(*) as count FROM transfers WHERE from_user_id = ? AND status = "received" AND created_at >= ?', + [userId, agentJoinTime] + ); + + const transferCount = completedTransfers[0].count; + + // 如果完成至少三次转账,给代理分佣 + if (transferCount >= 3) { + // 检查是否已经给过佣金(防止重复分佣) + const [existingCommission] = await db.execute( + 'SELECT id FROM agent_commission_records WHERE agent_id = ? AND merchant_id = ? AND description LIKE "%第三次转账%"', + [agentId, userId] + ); + + if (existingCommission.length === 0) { + // 计算佣金:399元的10% = 39.9元 + const commissionAmount = 399 * 0.10; + + // 记录佣金 + await db.execute( + 'INSERT INTO agent_commission_records (agent_id, merchant_id, commission_amount, commission_type, description, created_at) VALUES (?, ?, ?, "matching", "用户完成第三次转账获得的代理佣金", NOW())', + [agentId, userId, commissionAmount] + ); + + console.log(`用户 ${userId} 完成第三次转账,为代理 ${agentId} 分佣 ${commissionAmount} 元`); + } + } + } catch (error) { + console.error('处理代理佣金失败:', error); + // 不抛出错误,避免影响主流程 + } + } +} + +module.exports = new MatchingService(); \ No newline at end of file diff --git a/services/minioService.js b/services/minioService.js new file mode 100644 index 0000000..9355f5a --- /dev/null +++ b/services/minioService.js @@ -0,0 +1,293 @@ +const { createMinioClient, minioConfig, getPublicUrl } = require('../config/minio'); +const path = require('path'); +const crypto = require('crypto'); + +/** + * MinIO 文件服务 + * 提供文件上传、删除、获取等功能 + */ +class MinioService { + constructor() { + this.client = createMinioClient(); + } + + /** + * 生成唯一文件名 + * @param {string} originalName - 原始文件名 + * @returns {string} 唯一文件名 + */ + generateUniqueFileName(originalName) { + const now = new Date(); + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, '0'); + const day = String(now.getDate()).padStart(2, '0'); + const timestamp = Date.now(); + const randomString = crypto.randomBytes(8).toString('hex'); + const ext = path.extname(originalName); + return `${year}/${month}/${day}/${timestamp}_${randomString}${ext}`; + } + + /** + * 根据文件类型获取存储桶名称 + * @param {string} type - 文件类型 (avatar, product, document) + * @returns {string} 存储桶名称 + */ + getBucketName(type = 'document') { + const bucketMap = { + 'avatar': minioConfig.buckets.avatars, + 'product': minioConfig.buckets.products, + 'document': minioConfig.buckets.documents + }; + return bucketMap[type] || minioConfig.buckets.documents; + } + + /** + * 上传单个文件 + * @param {Buffer} fileBuffer - 文件缓冲区 + * @param {string} originalName - 原始文件名 + * @param {string} mimeType - 文件MIME类型 + * @param {string} type - 文件类型 + * @returns {Promise} 上传结果 + */ + async uploadFile(fileBuffer, originalName, mimeType, type = 'document') { + try { + const bucketName = this.getBucketName(type); + const fileName = this.generateUniqueFileName(originalName); + + // 设置文件元数据 + const metaData = { + 'Content-Type': mimeType, + 'Original-Name': encodeURIComponent(originalName), + 'Upload-Time': new Date().toISOString() + }; + + // 上传文件到MinIO + await this.client.putObject(bucketName, fileName, fileBuffer, fileBuffer.length, metaData); + + // 生成访问URL + const url = getPublicUrl(bucketName, fileName); + + return { + success: true, + data: { + filename: fileName, + originalname: originalName, + mimetype: mimeType, + size: fileBuffer.length, + bucket: bucketName, + path: `${bucketName}/${fileName}`, + url: url + } + }; + } catch (error) { + console.error('MinIO文件上传失败:', error); + throw new Error(`文件上传失败: ${error.message}`); + } + } + + /** + * 迁移专用:上传文件到指定存储桶和路径 + * @param {string} bucketName - 存储桶名称 + * @param {string} filePath - 文件路径 + * @param {Buffer} fileBuffer - 文件缓冲区 + * @param {string} mimeType - 文件MIME类型 + * @returns {Promise} 上传结果 + */ + async uploadFileForMigration(bucketName, filePath, fileBuffer, mimeType) { + try { + // 设置文件元数据 + const metaData = { + 'Content-Type': mimeType, + 'Upload-Time': new Date().toISOString() + }; + + // 上传文件到MinIO + await this.client.putObject(bucketName, filePath, fileBuffer, fileBuffer.length, metaData); + + // 生成访问URL + const url = getPublicUrl(bucketName, filePath); + + return { + success: true, + data: { + filename: filePath, + mimetype: mimeType, + size: fileBuffer.length, + bucket: bucketName, + path: `${bucketName}/${filePath}`, + url: url + } + }; + } catch (error) { + console.error('MinIO文件迁移上传失败:', error); + throw new Error(`文件迁移上传失败: ${error.message}`); + } + } + + /** + * 上传多个文件 + * @param {Array} files - 文件数组,每个文件包含 {buffer, originalName, mimeType} + * @param {string} type - 文件类型 + * @returns {Promise} 上传结果数组 + */ + async uploadMultipleFiles(files, type = 'document') { + try { + const uploadPromises = files.map(file => + this.uploadFile(file.buffer, file.originalName, file.mimeType, type) + ); + + const results = await Promise.all(uploadPromises); + const uploadedFiles = results.map(result => result.data); + + return { + success: true, + data: { + files: uploadedFiles, + urls: uploadedFiles.map(file => file.url), + count: uploadedFiles.length + } + }; + } catch (error) { + console.error('MinIO多文件上传失败:', error); + throw new Error(`多文件上传失败: ${error.message}`); + } + } + + /** + * 删除文件 + * @param {string} bucketName - 存储桶名称 + * @param {string} fileName - 文件名 + * @returns {Promise} 删除结果 + */ + async deleteFile(bucketName, fileName) { + try { + await this.client.removeObject(bucketName, fileName); + console.log(`✅ 文件删除成功: ${bucketName}/${fileName}`); + return true; + } catch (error) { + console.error('MinIO文件删除失败:', error); + throw new Error(`文件删除失败: ${error.message}`); + } + } + + /** + * 批量删除文件 + * @param {string} bucketName - 存储桶名称 + * @param {Array} fileNames - 文件名数组 + * @returns {Promise} 删除结果 + */ + async deleteMultipleFiles(bucketName, fileNames) { + try { + const deletePromises = fileNames.map(fileName => + this.deleteFile(bucketName, fileName) + ); + + await Promise.all(deletePromises); + + return { + success: true, + deletedCount: fileNames.length, + message: `成功删除${fileNames.length}个文件` + }; + } catch (error) { + console.error('MinIO批量删除失败:', error); + throw new Error(`批量删除失败: ${error.message}`); + } + } + + /** + * 检查文件是否存在 + * @param {string} bucketName - 存储桶名称 + * @param {string} fileName - 文件名 + * @returns {Promise} 文件是否存在 + */ + async fileExists(bucketName, fileName) { + try { + await this.client.statObject(bucketName, fileName); + return true; + } catch (error) { + if (error.code === 'NotFound') { + return false; + } + throw error; + } + } + + /** + * 获取文件信息 + * @param {string} bucketName - 存储桶名称 + * @param {string} fileName - 文件名 + * @returns {Promise} 文件信息 + */ + async getFileInfo(bucketName, fileName) { + try { + const stat = await this.client.statObject(bucketName, fileName); + return { + size: stat.size, + lastModified: stat.lastModified, + etag: stat.etag, + contentType: stat.metaData['content-type'], + originalName: decodeURIComponent(stat.metaData['original-name'] || fileName) + }; + } catch (error) { + console.error('获取文件信息失败:', error); + throw new Error(`获取文件信息失败: ${error.message}`); + } + } + + /** + * 生成预签名URL(用于临时访问) + * @param {string} bucketName - 存储桶名称 + * @param {string} fileName - 文件名 + * @param {number} expiry - 过期时间(秒),默认7天 + * @returns {Promise} 预签名URL + */ + async getPresignedUrl(bucketName, fileName, expiry = 7 * 24 * 60 * 60) { + try { + const url = await this.client.presignedGetObject(bucketName, fileName, expiry); + return url; + } catch (error) { + console.error('生成预签名URL失败:', error); + throw new Error(`生成预签名URL失败: ${error.message}`); + } + } + + /** + * 列出存储桶中的文件 + * @param {string} bucketName - 存储桶名称 + * @param {string} prefix - 文件前缀 + * @param {number} limit - 限制数量 + * @returns {Promise} 文件列表 + */ + async listFiles(bucketName, prefix = '', limit = 100) { + try { + const files = []; + const stream = this.client.listObjects(bucketName, prefix, true); + + return new Promise((resolve, reject) => { + stream.on('data', (obj) => { + if (files.length < limit) { + files.push({ + name: obj.name, + size: obj.size, + lastModified: obj.lastModified, + etag: obj.etag, + url: getPublicUrl(bucketName, obj.name) + }); + } + }); + + stream.on('end', () => resolve(files)); + stream.on('error', reject); + }); + } catch (error) { + console.error('列出文件失败:', error); + throw new Error(`列出文件失败: ${error.message}`); + } + } +} + +// 创建单例实例 +const minioService = new MinioService(); + +module.exports = minioService; \ No newline at end of file diff --git a/services/timeoutService.js b/services/timeoutService.js new file mode 100644 index 0000000..794d018 --- /dev/null +++ b/services/timeoutService.js @@ -0,0 +1,380 @@ +const { getDB } = require('../database'); +const { logger, auditLogger } = require('../config/logger'); + +class TimeoutService { + /** + * 检查转账超时情况 + * 标记超时转账和风险用户,自动取消超过2.5小时的pending转账 + */ + async checkTransferTimeouts() { + const db = getDB(); + + try { + // 只在调试模式下输出开始检查的日志 + // console.log('开始检查转账超时情况...'); + + // 1. 查找所有超时的转账记录(有deadline_at的) + const [overdueTransfers] = await db.execute( + `SELECT t.*, u.username, u.real_name + FROM transfers t + JOIN users u ON t.from_user_id = u.id + WHERE t.status = 'pending' + AND t.deadline_at IS NOT NULL + AND t.deadline_at < NOW() + AND t.is_overdue = 0` + ); + + // 2. 查找所有pending状态超过2.5小时的转账记录 + const [longPendingTransfers] = await db.execute( + `SELECT t.*, u.username, u.real_name + FROM transfers t + JOIN users u ON t.from_user_id = u.id + WHERE t.status = 'pending' + AND t.created_at < DATE_SUB(NOW(), INTERVAL 150 MINUTE)` + ); + + let hasWork = false; + + // 处理有deadline的超时转账 + if (overdueTransfers.length > 0) { + hasWork = true; + console.log(`⚠️ 发现 ${overdueTransfers.length} 笔超时转账,开始处理...`); + + for (const transfer of overdueTransfers) { + await this.handleOverdueTransfer(transfer); + } + } + // 处理超过2.5小时的pending转账 + if (longPendingTransfers.length > 0) { + hasWork = true; + console.log(`⚠️ 发现 ${longPendingTransfers.length} 笔超过2.5小时的pending转账,开始自动取消...`); + + for (const transfer of longPendingTransfers) { + await this.handleLongPendingTransfer(transfer); + } + } + + if (hasWork) { + console.log('✅ 转账超时检查完成'); + } + + } catch (error) { + console.error('检查转账超时失败:', error); + logger.error('Transfer timeout check failed', { error: error.message }); + } + } + + /** + * 处理超时转账 + * @param {Object} transfer - 转账记录 + */ + async handleOverdueTransfer(transfer) { + const db = getDB(); + + try { + await db.query('START TRANSACTION'); + + // 标记转账为超时和坏账 + await db.execute( + 'UPDATE transfers SET is_overdue = 1, is_bad_debt = 1, overdue_at = NOW() WHERE id = ?', + [transfer.id] + ); + + // 标记用户为风险用户 + await db.execute( + `UPDATE users SET + is_risk_user = 1, + risk_reason = CONCAT(IFNULL(risk_reason, ''), '转账超时(转账ID: ', ?, ', 金额: ', ?, '元, 超时时间: ', NOW(), '); ') + WHERE id = ?`, + [transfer.id, transfer.amount, transfer.from_user_id] + ); + + await db.query('COMMIT'); + + // 记录审计日志 + auditLogger.info('Transfer marked as overdue and bad debt, user marked as risk', { + transferId: transfer.id, + userId: transfer.from_user_id, + username: transfer.username, + amount: transfer.amount, + deadlineAt: transfer.deadline_at + }); + + console.log(`转账 ${transfer.id} 已标记为超时和坏账,用户 ${transfer.username}(ID: ${transfer.from_user_id}) 已标记为风险用户`); + + } catch (error) { + await db.query('ROLLBACK'); + console.error(`处理超时转账 ${transfer.id} 失败:`, error); + throw error; + } + } + + /** + * 处理超过2.5小时的pending转账 + * @param {Object} transfer - 转账记录 + */ + async handleLongPendingTransfer(transfer) { + const db = getDB(); + + try { + await db.query('START TRANSACTION'); + + // 将转账状态改为cancelled + await db.execute( + 'UPDATE transfers SET status = "cancelled", updated_at = NOW() WHERE id = ?', + [transfer.id] + ); + + // 如果有关联的matching_order_id,检查并更新matching_orders状态 + if (transfer.matching_order_id) { + // 检查该matching_order下是否还有非cancelled状态的transfers + const [remainingTransfers] = await db.execute( + 'SELECT COUNT(*) as count FROM transfers WHERE matching_order_id = ? AND status != "cancelled"', + [transfer.matching_order_id] + ); + + // 如果所有关联的transfers都是cancelled状态,则更新matching_order状态为cancelled + if (remainingTransfers[0].count === 0) { + await db.execute( + 'UPDATE matching_orders SET status = "cancelled", updated_at = NOW() WHERE id = ?', + [transfer.matching_order_id] + ); + + console.log(`匹配订单 ${transfer.matching_order_id} 的所有转账都已取消,订单状态已更新为cancelled`); + } + } + + await db.query('COMMIT'); + + // 记录审计日志 + auditLogger.info('Long pending transfer auto-cancelled', { + transferId: transfer.id, + userId: transfer.from_user_id, + username: transfer.username, + amount: transfer.amount, + createdAt: transfer.created_at, + matchingOrderId: transfer.matching_order_id + }); + + console.log(`转账 ${transfer.id} 超过2.5小时未处理,已自动取消 (用户: ${transfer.username}, 金额: ${transfer.amount}元)`); + + } catch (error) { + await db.query('ROLLBACK'); + console.error(`处理长时间pending转账 ${transfer.id} 失败:`, error); + throw error; + } + } + + /** + * 获取风险用户列表 + * @param {Object} filters - 筛选条件 + * @param {Object} pagination - 分页参数 + * @returns {Object} 风险用户列表和分页信息 + */ + async getRiskUsers(filters = {}, pagination = {}) { + const db = getDB(); + const { page = 1, limit = 10 } = pagination; + const pageNum = parseInt(page, 10) || 1; + const limitNum = parseInt(limit, 10) || 10; + const offset = (pageNum - 1) * limitNum; + + let whereClause = 'WHERE is_risk_user = 1'; + const params = []; + + // 构建查询条件 + if (filters.is_blacklisted !== undefined) { + whereClause += ' AND is_blacklisted = ?'; + params.push(filters.is_blacklisted); + } + + if (filters.username) { + whereClause += ' AND username LIKE ?'; + params.push(`%${filters.username}%`); + } + + try { + // 获取总数 + const [countResult] = await db.execute( + `SELECT COUNT(*) as total FROM users ${whereClause}`, + params + ); + const total = countResult[0].total; + + // 获取数据 + const [users] = await db.execute( + `SELECT id, username, real_name, is_risk_user, is_blacklisted, + risk_reason, blacklist_reason, blacklisted_at, created_at,phone + FROM users + ${whereClause} + ORDER BY created_at DESC + LIMIT ${limitNum} OFFSET ${offset}`, + params + ); + + return { + users, + pagination: { + page: pageNum, + limit: limitNum, + total, + pages: Math.ceil(total / limitNum) + } + }; + } catch (error) { + logger.error('Failed to get risk users', { error: error.message, filters }); + throw error; + } + } + + /** + * 拉黑用户 + * @param {number} userId - 用户ID + * @param {string} reason - 拉黑原因 + * @param {number} operatorId - 操作员ID + */ + async blacklistUser(userId, reason, operatorId) { + const db = getDB(); + + try { + // 检查用户是否存在 + const [users] = await db.execute( + 'SELECT id, username, phone FROM users WHERE id = ?', + [userId] + ); + + if (users.length === 0) { + throw new Error('用户不存在'); + } + + const user = users[0]; + + if (user.is_blacklisted) { + throw new Error('用户已被拉黑'); + } + + // 拉黑用户 + await db.execute( + `UPDATE users SET + is_blacklisted = 1, + blacklist_reason = ?, + blacklisted_at = NOW() + WHERE id = ?`, + [reason, userId] + ); + + // 记录审计日志 + auditLogger.info('User blacklisted', { + userId, + username: user.username, + reason, + operatorId + }); + + logger.info('User blacklisted successfully', { userId, operatorId }); + + } catch (error) { + logger.error('Failed to blacklist user', { + error: error.message, + userId, + operatorId + }); + throw error; + } + } + + /** + * 解除拉黑 + * @param {number} userId - 用户ID + * @param {number} operatorId - 操作员ID + */ + async unblacklistUser(userId, operatorId) { + const db = getDB(); + + try { + // 检查用户是否存在 + const [users] = await db.execute( + 'SELECT id, username, is_blacklisted FROM users WHERE id = ?', + [userId] + ); + + if (users.length === 0) { + throw new Error('用户不存在'); + } + + const user = users[0]; + + if (!user.is_blacklisted) { + throw new Error('用户未被拉黑'); + } + + // 解除拉黑 + await db.execute( + `UPDATE users SET + is_blacklisted = 0, + blacklist_reason = NULL, + blacklisted_at = NULL + WHERE id = ?`, + [userId] + ); + + // 记录审计日志 + auditLogger.info('User unblacklisted', { + userId, + username: user.username, + operatorId + }); + + logger.info('User unblacklisted successfully', { userId, operatorId }); + + } catch (error) { + logger.error('Failed to unblacklist user', { + error: error.message, + userId, + operatorId + }); + throw error; + } + } + + /** + * 检查用户是否被拉黑 + * @param {number} userId - 用户ID + * @returns {boolean} 是否被拉黑 + */ + async isUserBlacklisted(userId) { + const db = getDB(); + + try { + const [users] = await db.execute( + 'SELECT is_blacklisted FROM users WHERE id = ?', + [userId] + ); + + return users.length > 0 && users[0].is_blacklisted === 1; + } catch (error) { + logger.error('Failed to check user blacklist status', { + error: error.message, + userId + }); + throw error; + } + } + + /** + * 启动定时检查任务 + * 每5分钟检查一次转账超时情况 + */ + startTimeoutChecker() { + console.log('启动转账超时检查定时任务...'); + + // 立即执行一次 + this.checkTransferTimeouts(); + + // 每5分钟执行一次 + setInterval(() => { + this.checkTransferTimeouts(); + }, 5 * 1000); // 5秒 + } +} + +module.exports = new TimeoutService(); \ No newline at end of file diff --git a/services/transferService.js b/services/transferService.js new file mode 100644 index 0000000..c4e5fb4 --- /dev/null +++ b/services/transferService.js @@ -0,0 +1,1192 @@ +const {getDB} = require('../database'); +const {logger, auditLogger} = require('../config/logger'); +const {AppError} = require('../middleware/errorHandler'); +const {TRANSFER_TYPES, TRANSFER_STATUS, ERROR_CODES, HTTP_STATUS} = require('../config/constants'); + +class TransferService { + // 创建转账记录 + async createTransfer(fromUserId, transferData) { + const {to_user_id, amount, transfer_type, description, voucher_url} = transferData; + const db = getDB(); + + try { + // 验证用户是否存在 + await this.validateUser(to_user_id); + + // 验证转账类型 + if (!Object.values(TRANSFER_TYPES).includes(transfer_type)) { + throw new AppError('无效的转账类型', HTTP_STATUS.BAD_REQUEST, ERROR_CODES.VALIDATION_ERROR); + } + + // 检查余额(如果是用户转账)- 允许负余额转账 + if (transfer_type === TRANSFER_TYPES.USER_TO_USER || transfer_type === TRANSFER_TYPES.USER_TO_SYSTEM) { + if (!fromUserId) { + throw new AppError('用户转账必须指定发送方用户', HTTP_STATUS.BAD_REQUEST, ERROR_CODES.VALIDATION_ERROR); + } + // 获取当前余额但不检查是否足够,允许负余额转账 + await this.checkUserBalance(fromUserId, amount); + } + + // 系统转账时,from_user_id 设为 null + const actualFromUserId = transfer_type === TRANSFER_TYPES.SYSTEM_TO_USER ? null : fromUserId; + + // 生成批次ID + const batch_id = this.generateBatchId(); + + // 插入转账记录 + const currentTime = new Date(); + const [result] = await db.execute( + `INSERT INTO transfers (from_user_id, to_user_id, amount, transfer_type, status, description, + voucher_url, batch_id, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [actualFromUserId, to_user_id, amount, transfer_type, TRANSFER_STATUS.PENDING, description || null, voucher_url || null, batch_id, currentTime] + ); + + const transferId = result.insertId; + + // 记录审计日志 + auditLogger.info('Transfer created', { + transferId, + fromUserId, + toUserId: to_user_id, + amount, + transferType: transfer_type, + batchId: batch_id + }); + + logger.info('Transfer created successfully', {transferId, fromUserId, amount}); + + return { + transfer_id: transferId, + batch_id, + status: TRANSFER_STATUS.PENDING + }; + } catch (error) { + logger.error('Failed to create transfer', { + error: error.message, + fromUserId, + transferData + }); + throw error; + } + } + + // 管理员解除坏账 + async removeBadDebt(transferId, adminId, reason) { + const db = getDB(); + + try { + // 获取转账记录 + const transfer = await this.getTransferById(transferId); + + if (!transfer) { + throw new AppError('转账记录不存在', HTTP_STATUS.NOT_FOUND, ERROR_CODES.NOT_FOUND); + } + + if (!transfer.is_bad_debt) { + throw new AppError('该转账未被标记为坏账', HTTP_STATUS.BAD_REQUEST, ERROR_CODES.VALIDATION_ERROR); + } + + // 解除坏账标记 + await db.execute( + 'UPDATE transfers SET is_bad_debt = 0 WHERE id = ?', + [transferId] + ); + + // 记录管理员操作日志 + await db.execute( + `INSERT INTO admin_operation_logs (admin_id, operation_type, target_type, target_id, description, + created_at) + VALUES (?, 'remove_bad_debt', 'transfer', ?, ?, NOW())`, + [adminId, transferId, reason || `管理员解除转账${transferId}的坏账标记`] + ); + + // 记录审计日志 + auditLogger.info('Bad debt removed by admin', { + transferId, + adminId, + fromUserId: transfer.from_user_id, + toUserId: transfer.to_user_id, + amount: transfer.amount, + reason + }); + + logger.info('Bad debt removed successfully', {transferId, adminId, reason}); + + return {success: true}; + } catch (error) { + logger.error('Failed to remove bad debt', { + error: error.message, + transferId, + adminId + }); + throw error; + } + } + + // 确认转账 + async confirmTransfer(transferId, note, operatorId) { + const mysql = require('mysql2/promise'); + const {dbConfig} = require('../database'); + + // 创建单独的连接用于事务处理 + const connection = await mysql.createConnection(dbConfig); + + try { + // 获取转账记录 + const transfer = await this.getTransferById(transferId); + + if (!transfer) { + throw new AppError('转账记录不存在', HTTP_STATUS.NOT_FOUND, ERROR_CODES.NOT_FOUND); + } + + if (transfer.status !== TRANSFER_STATUS.PENDING) { + throw new AppError('转账记录状态不允许确认', HTTP_STATUS.BAD_REQUEST, ERROR_CODES.VALIDATION_ERROR); + } + + // 检查是否为坏账 + if (transfer.is_bad_debt) { + throw new AppError('该转账已被标记为坏账,无法确认。请联系管理员处理', HTTP_STATUS.BAD_REQUEST, ERROR_CODES.VALIDATION_ERROR); + } + + // 开始事务 + await connection.beginTransaction(); + + try { + // 更新转账状态 + await connection.execute( + 'UPDATE transfers SET status = ? WHERE id = ?', + [TRANSFER_STATUS.CONFIRMED, transferId] + ); + + // 如果存在匹配订单ID,更新匹配订单状态 + if (transfer.matching_order_id) { + // 查询该匹配订单下所有transfers的状态 + const [allTransfers] = await connection.execute( + `SELECT status + FROM transfers + WHERE matching_order_id = ?`, + [transfer.matching_order_id] + ); + + let matchingOrderStatus; + + // 根据所有相关transfers的状态来决定matching_order的状态 + const transferStatuses = allTransfers.map(t => t.status); + console.log(transferStatuses, 'transferStatuses'); + + if (transferStatuses.every(status => status === 'cancelled' || status === 'rejected' || status === 'not_received')) { + // 如果所有transfers都被取消/拒绝/未收到,匹配订单标记为已完成 + matchingOrderStatus = 'completed'; + } else if (transferStatuses.every(status => status === 'received')) { + // 如果所有transfers都已收到,匹配订单完成 + matchingOrderStatus = 'completed'; + } else if (transferStatuses.includes('cancelled') || transferStatuses.includes('rejected') || transferStatuses.includes('not_received') || transferStatuses.some(status => status === 'confirmed' || status === 'received')) { + // 如果有任何一个transfer被取消/拒绝/未收到,或者有transfers已确认或已收到,匹配订单为进行中状态 + matchingOrderStatus = 'matching'; + } else { + // 其他情况为待处理状态 + matchingOrderStatus = 'matching'; + } + + await connection.execute( + `UPDATE matching_orders + SET status = ?, + updated_at = NOW() + WHERE id = ?`, + [matchingOrderStatus, transfer.matching_order_id] + ); + + logger.info('Matching order status updated after transfer confirmation', { + matchingOrderId: transfer.matching_order_id, + transferId: transferId, + newMatchingOrderStatus: matchingOrderStatus, + allTransferStatuses: transferStatuses + }); + } + + // 注意:发送方余额将在接收方确认收款时扣除,而不是在确认转账时扣除 + // 这样可以避免资金被锁定但收款方未确认的情况 + + await connection.commit(); + + // 记录审计日志 + auditLogger.info('Transfer confirmed', { + transferId, + operatorId, + fromUserId: transfer.from_user_id, + toUserId: transfer.to_user_id, + amount: transfer.amount + }); + + logger.info('Transfer confirmed successfully', {transferId, operatorId}); + + return {success: true}; + } catch (error) { + await connection.rollback(); + throw error; + } + } catch (error) { + logger.error('Failed to confirm transfer', { + error: error.message, + transferId, + operatorId + }); + throw error; + } finally { + await connection.end(); + } + } + + // 获取转账列表 + async getTransfers(filters = {}, pagination = {}, user_type = 'user_to_user') { + const db = getDB(); + const {page = 1, limit = 10, sort = 'created_at', order = 'desc'} = pagination; + const pageNum = parseInt(page, 10) || 1; + const limitNum = parseInt(limit, 10) || 10; + const offset = (pageNum - 1) * limitNum; + + let whereClause = 'WHERE '; + const params = []; + whereClause += `t.transfer_type='${user_type}'`; + // 构建查询条件 + if (filters.user_id) { + whereClause += ' AND (t.from_user_id = ? OR t.to_user_id = ?)'; + params.push(filters.user_id, filters.user_id); + } + + if (filters.status) { + whereClause += ' AND t.status = ?'; + params.push(filters.status); + } + + if (filters.transfer_type) { + whereClause += ' AND t.transfer_type = ?'; + params.push(filters.transfer_type); + } + + if (filters.start_date) { + whereClause += ' AND t.created_at >= ?'; + params.push(filters.start_date); + } + + if (filters.end_date) { + whereClause += ' AND t.created_at <= ?'; + params.push(filters.end_date); + } + + if (filters.search) { + whereClause += ' AND (fu.username LIKE ? OR fu.real_name LIKE ? OR tu.username LIKE ? OR tu.real_name LIKE ?)'; + const searchPattern = `%${filters.search}%`; + params.push(searchPattern, searchPattern, searchPattern, searchPattern); + } + + // 构建排序子句 + const validSortFields = ['id', 'amount', 'created_at', 'updated_at', 'status']; + const sortField = validSortFields.includes(sort) ? sort : 'created_at'; + const sortOrder = order && order.toLowerCase() === 'asc' ? 'ASC' : 'DESC'; + + const orderClause = `ORDER BY t.${sortField} ${sortOrder}`; + + try { + // 获取总数 + const [countResult] = await db.execute( + `SELECT COUNT(*) as total + FROM transfers t + LEFT JOIN users fu ON t.from_user_id = fu.id + LEFT JOIN users tu ON t.to_user_id = tu.id + ${whereClause}`, + params + ); + const total = countResult[0].total; + // 获取数据 + const [transfers] = await db.execute( + `SELECT t.*, + fu.username as from_username, + fu.real_name as from_real_name, + tu.username as to_username, + tu.real_name as to_real_name, + f_p.name as from_province, + f_c.name as from_city, + f_d.name as from_district, + t_p.name as to_province, + t_c.name as to_city, + t_d.name as to_district + FROM transfers t + LEFT JOIN users fu ON t.from_user_id = fu.id + LEFT JOIN users tu ON t.to_user_id = tu.id + LEFT JOIN china_regions f_p ON f_p.code = fu.province + LEFT JOIN china_regions f_c ON f_c.code = fu.city + LEFT JOIN china_regions f_d ON f_d.code = fu.district_id + LEFT JOIN china_regions t_p ON t_p.code = tu.province + LEFT JOIN china_regions t_c ON t_c.code = tu.city + LEFT JOIN china_regions t_d ON t_d.code = tu.district_id + ${whereClause} ${orderClause} + LIMIT ${limitNum}`, + params + ); + + return { + transfers, + pagination: { + page: pageNum, + limit: limitNum, + total, + pages: Math.ceil(total / limitNum) + } + }; + } catch (error) { + logger.error('Failed to get transfers', {error: error.message, filters}); + throw error; + } + } + + async getTransfersHistory(filters = {}, pagination = {}, user_type = 'manual') { + const db = getDB(); + const {page = 1, limit = 10, sort = 'created_at', order = 'desc'} = pagination; + const pageNum = parseInt(page, 10) || 1; + const limitNum = parseInt(limit, 10) || 10; + const offset = (pageNum - 1) * limitNum; + + let whereClause = 'WHERE 1=1 '; + const params = []; + whereClause += `AND source_type != '${user_type}'`; + // 构建查询条件 + if (filters.user_id) { + whereClause += ' AND (from_user_id = ? OR to_user_id = ?)'; + params.push(filters.user_id, filters.user_id); + } + + if (filters.status) { + whereClause += ' AND status = ?'; + params.push(filters.status); + } + + if (filters.transfer_type) { + whereClause += ' AND transfer_type = ?'; + params.push(filters.transfer_type); + } + + if (filters.start_date) { + whereClause += ' AND created_at >= ?'; + params.push(filters.start_date); + } + + if (filters.end_date) { + whereClause += ' AND created_at <= ?'; + params.push(filters.end_date); + } + + if (filters.search) { + whereClause += ' AND (fu.username LIKE ? OR fu.real_name LIKE ? OR tu.username LIKE ? OR tu.real_name LIKE ?)'; + const searchPattern = `%${filters.search}%`; + params.push(searchPattern, searchPattern, searchPattern, searchPattern); + } + + // 构建排序子句 + const validSortFields = ['id', 'amount', 'created_at', 'updated_at', 'status']; + const sortField = validSortFields.includes(sort) ? sort : 'created_at'; + const sortOrder = order && order.toLowerCase() === 'asc' ? 'ASC' : 'DESC'; + + const orderClause = `ORDER BY t.${sortField} ${sortOrder}`; + + try { + // 获取总数 + const [countResult] = await db.execute( + `SELECT COUNT(*) as total + FROM transfers t + LEFT JOIN users fu ON t.from_user_id = fu.id + LEFT JOIN users tu ON t.to_user_id = tu.id + ${whereClause}`, + params + ); + const total = countResult[0].total; + + // 获取数据 + const [transfers] = await db.execute( + `SELECT t.*, + fu.username as from_username, + fu.real_name as from_real_name, + tu.username as to_username, + tu.real_name as to_real_name + FROM transfers t + LEFT JOIN users fu ON t.from_user_id = fu.id + LEFT JOIN users tu ON t.to_user_id = tu.id + ${whereClause} ${orderClause} + LIMIT ${limitNum} OFFSET ${offset}`, + params + ); + //获取总数 + const stats = {}; + //获取系统转给融豆的总数 + let [total_to_admin] = await db.execute(`SELECT SUM(t.amount) as total FROM transfers t WHERE t.source_type = 'system'`) + stats.total_to_admin = total_to_admin[0].total || 0 + //转给代理的融豆总数 + let [total_to_agent] = await db.execute(`SELECT SUM(t.amount) as total FROM transfers t WHERE t.source_type = 'agent'`) + stats.total_to_agent = total_to_agent[0].total || 0 + //转给直营代理的融豆数量 + let [total_to_agent_directly] = await db.execute(`SELECT SUM(t.amount) as total FROM transfers t WHERE t.source_type = 'operated_agent'`) + stats.total_to_agent_directly = total_to_agent_directly[0].total || 0 + //转给直营的融豆总数 + let [total_to_directly_operated] = await db.execute(`SELECT SUM(t.amount) as total FROM transfers t WHERE t.source_type = 'directly_operated'`) + stats.total_to_directly_operated = total_to_directly_operated[0].total || 0 + //提现总数 + let [total_get] = await db.execute(`SELECT SUM(t.amount) as total FROM transfers t WHERE t.source_type = 'withdraw'`) + stats.total_get = total_get[0].total || 0 + return { + transfers, + stats, + pagination: { + page: pageNum, + limit: limitNum, + total, + pages: Math.ceil(total / limitNum) + } + }; + } catch (error) { + logger.error('Failed to get transfers', {error: error.message, filters}); + throw error; + } + } + + // 验证用户是否存在 + async validateUser(userId) { + const db = getDB(); + const [users] = await db.execute('SELECT id FROM users WHERE id = ?', [userId]); + if (users.length === 0) { + throw new AppError('用户不存在', HTTP_STATUS.BAD_REQUEST, ERROR_CODES.VALIDATION_ERROR); + } + } + + // 检查用户余额(现在检查balance字段,允许负数) + async checkUserBalance(userId, amount) { + const db = getDB(); + const [users] = await db.execute('SELECT balance FROM users WHERE id = ?', [userId]); + if (users.length === 0) { + throw new AppError('用户不存在', HTTP_STATUS.BAD_REQUEST, ERROR_CODES.VALIDATION_ERROR); + } + // 余额可以为负数,所以不需要检查余额不足 + return users[0].balance; + } + + // 获取转账记录 + async getTransferById(transferId) { + const db = getDB(); + const [transfers] = await db.execute('SELECT * FROM transfers WHERE id = ?', [transferId]); + return transfers[0] || null; + } + + + // 用户确认收到转账 + async confirmReceived(transferId, userId) { + const mysql = require('mysql2/promise'); + const {dbConfig} = require('../database'); + + // 创建单独的连接用于事务处理 + const connection = await mysql.createConnection(dbConfig); + + try { + // 获取转账记录 + const transfer = await this.getTransferById(transferId); + + if (!transfer) { + throw new AppError('转账记录不存在', HTTP_STATUS.NOT_FOUND, ERROR_CODES.NOT_FOUND); + } + + // 检查用户权限:必须是收款方本人或管理员 + const [userRows] = await connection.execute( + 'SELECT role FROM users WHERE id = ?', + [userId] + ); + + const isAdmin = userRows[0]?.role === 'admin'; + const isRecipient = transfer.to_user_id === userId; + + if (!isRecipient && !isAdmin) { + throw new AppError('无权限操作此转账', HTTP_STATUS.FORBIDDEN, ERROR_CODES.VALIDATION_ERROR); + } + + if (transfer.status !== TRANSFER_STATUS.CONFIRMED) { + throw new AppError('转账状态不允许确认收款', HTTP_STATUS.BAD_REQUEST, ERROR_CODES.VALIDATION_ERROR); + } + + // 检查是否为坏账 + if (transfer.is_bad_debt) { + throw new AppError('该转账已被标记为坏账,无法确认收款。请联系管理员处理', HTTP_STATUS.BAD_REQUEST, ERROR_CODES.VALIDATION_ERROR); + } + + // 开始事务 + await connection.beginTransaction(); + + try { + // 更新转账状态为已收到 + await connection.execute( + 'UPDATE transfers SET status = ? WHERE id = ?', + [TRANSFER_STATUS.RECEIVED, transferId] + ); + + // 扣除发送方余额(在接收方确认收款时扣除) + if (transfer.from_user_id) { + await connection.execute( + 'UPDATE users SET balance = balance - ? WHERE id = ?', + [transfer.amount, transfer.from_user_id] + ); + } + + // 所有类型的转账都需要在接收方确认收到时增加接收方余额 + // 这与 confirmTransfer 方法的修改保持一致 + await connection.execute( + 'UPDATE users SET balance = balance + ? WHERE id = ?', + [transfer.amount, transfer.to_user_id] + ); + + // 给发起人发放相应的积分(转账金额 = 积分数量) + await connection.execute( + 'UPDATE users SET points = points + ? WHERE id = ?', + [transfer.amount, transfer.from_user_id] + ); + + // 记录积分历史 + await connection.execute( + `INSERT INTO points_history (user_id, amount, type, description, created_at) + VALUES (?, ?, 'earn', ?, NOW())`, + [transfer.from_user_id, transfer.amount, `转账确认收款奖励积分,转账ID: ${transferId}`] + ); + + // 记录详细的余额变更审计日志 + auditLogger.info('Balance adjustment - confirm received', { + transferId: transferId, + fromUserId: transfer.from_user_id, + toUserId: transfer.to_user_id, + amount: transfer.amount, + operation: 'add_receiver_balance', + operatorId: userId, + operatorType: isAdmin ? 'admin' : 'user', + timestamp: new Date().toISOString() + }); + + await connection.commit(); + + // 记录审计日志 + auditLogger.info('Transfer received confirmed', { + transferId, + userId, + amount: transfer.amount, + pointsAwarded: transfer.amount, + pointsAwardedTo: transfer.from_user_id + }); + + logger.info('Transfer received confirmed successfully', { + transferId, + userId, + pointsAwarded: transfer.amount, + pointsAwardedTo: transfer.from_user_id + }); + + // 检查并处理代理佣金(转账完成后) + try { + const matchingService = require('./matchingService'); + await matchingService.checkAndProcessAgentCommission(transfer.from_user_id); + logger.info('Agent commission check completed', { + transferId, + fromUserId: transfer.from_user_id + }); + } catch (commissionError) { + // 代理佣金处理失败不影响主流程 + logger.error('Agent commission processing failed', { + transferId, + fromUserId: transfer.from_user_id, + error: commissionError.message + }); + } + + return {success: true}; + } catch (error) { + await connection.rollback(); + throw error; + } + } catch (error) { + logger.error('Failed to confirm received transfer', { + error: error.message, + transferId, + userId + }); + throw error; + } finally { + await connection.end(); + } + } + + /** + * 用户确认未收到转账 + * 当用户确认未收到款项时,将转账状态改为not_received并回滚发送方余额 + * @param {number} transferId - 转账ID + * @param {number} userId - 操作用户ID + * @returns {Object} 操作结果 + */ + async confirmNotReceived(transferId, userId) { + const db = getDB(); + const connection = await db.getConnection(); + + try { + // 获取转账记录 + const transfer = await this.getTransferById(transferId); + + if (!transfer) { + throw new AppError('转账记录不存在', HTTP_STATUS.NOT_FOUND, ERROR_CODES.NOT_FOUND); + } + + // 检查用户权限:必须是收款方本人或管理员 + const [userRows] = await db.execute( + 'SELECT role FROM users WHERE id = ?', + [userId] + ); + + const isAdmin = userRows[0]?.role === 'admin'; + const isRecipient = transfer.to_user_id === userId; + + if (!isRecipient && !isAdmin) { + throw new AppError('无权限操作此转账', HTTP_STATUS.FORBIDDEN, ERROR_CODES.VALIDATION_ERROR); + } + + if (transfer.status !== TRANSFER_STATUS.CONFIRMED) { + throw new AppError('转账状态不允许确认未收款', HTTP_STATUS.BAD_REQUEST, ERROR_CODES.VALIDATION_ERROR); + } + + // 开始事务 + await connection.beginTransaction(); + + try { + // 更新转账状态为未收到 + await connection.execute( + 'UPDATE transfers SET status = ? WHERE id = ?', + [TRANSFER_STATUS.NOT_RECEIVED, transferId] + ); + + // 注意:在新逻辑下,CONFIRMED状态时发送方余额还没有被扣除,所以无需回滚 + logger.info('Transfer marked as not received - no balance adjustment needed', { + transferId, + userId: transfer.from_user_id, + amount: transfer.amount, + operatorId: userId, + note: 'Sender balance was not deducted in confirmed status under new logic' + }); + + await connection.commit(); + + // 记录审计日志 + auditLogger.info('Transfer not received confirmed', { + transferId, + userId, + amount: transfer.amount, + fromUserId: transfer.from_user_id, + balanceRestored: false, + note: 'No balance restoration needed under new logic' + }); + + logger.info('Transfer not received confirmed successfully', { + transferId, + userId, + balanceRestored: false, + note: 'No balance restoration needed under new logic' + }); + + return {success: true}; + } catch (error) { + await connection.rollback(); + throw error; + } finally { + connection.release(); + } + } catch (error) { + logger.error('Failed to confirm not received transfer', { + error: error.message, + transferId, + userId + }); + throw error; + } + } + + // 拒绝转账 + async rejectTransfer(transferId, note, operatorId) { + const db = getDB(); + let connection; + + try { + // 从连接池获取连接用于事务处理 + connection = await db.getConnection(); + + try { + // 获取转账记录 + const transfer = await this.getTransferById(transferId); + + if (!transfer) { + throw new AppError('转账记录不存在', HTTP_STATUS.NOT_FOUND, ERROR_CODES.NOT_FOUND); + } + + if (transfer.status !== TRANSFER_STATUS.PENDING) { + throw new AppError('转账记录状态不允许拒绝', HTTP_STATUS.BAD_REQUEST, ERROR_CODES.VALIDATION_ERROR); + } + + // 检查是否已超时 + if (transfer.is_overdue) { + throw new AppError('已超时的转账不能拒绝,异常状态只能后台解除', HTTP_STATUS.BAD_REQUEST, ERROR_CODES.VALIDATION_ERROR); + } + + // 检查是否有截止时间且已过期 + if (transfer.deadline_at) { + const deadline = new Date(transfer.deadline_at); + const now = new Date(); + if (now > deadline) { + throw new AppError('已超时的转账不能拒绝,异常状态只能后台解除', HTTP_STATUS.BAD_REQUEST, ERROR_CODES.VALIDATION_ERROR); + } + } + + // 开始事务 + await connection.beginTransaction(); + + try { + // 更新转账状态 + await connection.execute( + 'UPDATE transfers SET status = ? WHERE id = ?', + [TRANSFER_STATUS.REJECTED, transferId] + ); + + // 注意:在新逻辑下,CONFIRMED状态时发送方余额还没有被扣除,所以无需回滚 + // 只有在RECEIVED状态时才需要回滚余额,但RECEIVED状态的转账不应该被拒绝 + logger.info('Transfer rejected - no balance adjustment needed', { + transferId, + userId: transfer.from_user_id, + amount: transfer.amount, + message: 'Sender balance was not deducted in confirmed status under new logic' + }); + + // 如果是分配类型的转账,需要更新对应的matching_order状态 + if (transfer.matching_order_id) { + // 查询该matching_order下所有source_type为allocation的transfers状态 + const [allTransfers] = await connection.execute( + `SELECT status + FROM transfers + WHERE matching_order_id = ? + AND source_type = 'allocation'`, + [transfer.matching_order_id] + ); + + // 统计各种状态的数量 + const statusCounts = { + cancelled: 0, + rejected: 0, + not_received: 0, + confirmed: 0, + received: 0, + pending: 0 + }; + + allTransfers.forEach(t => { + if (statusCounts.hasOwnProperty(t.status)) { + statusCounts[t.status]++; + } + }); + + const totalTransfers = allTransfers.length; + const problemTransfers = statusCounts.cancelled + statusCounts.rejected + statusCounts.not_received; + const completedTransfers = statusCounts.received; + const activeTransfers = statusCounts.confirmed + statusCounts.received; + + let matchingOrderStatus; + if (problemTransfers === totalTransfers) { + // 所有transfers都是问题状态,matching_order为已完成 + matchingOrderStatus = 'completed'; + } else if (completedTransfers === totalTransfers) { + // 所有transfers都已收到,matching_order为已完成 + matchingOrderStatus = 'completed'; + } else if (problemTransfers > 0 || activeTransfers > 0) { + // 有问题transfers或有活跃transfers,matching_order为进行中 + matchingOrderStatus = 'matching'; + } else { + // 其他情况为等待中 + matchingOrderStatus = 'pending'; + } + + // 更新matching_order状态 + await connection.execute( + `UPDATE matching_orders + SET status = ?, + updated_at = NOW() + WHERE id = ?`, + [matchingOrderStatus, transfer.matching_order_id] + ); + + logger.info('Updated matching_order status after transfer rejection', { + matchingOrderId: transfer.matching_order_id, + newStatus: matchingOrderStatus, + transferId, + statusCounts + }); + } + + await connection.commit(); + + // 记录审计日志 + auditLogger.info('Transfer rejected', { + transferId, + operatorId, + fromUserId: transfer.from_user_id, + toUserId: transfer.to_user_id, + amount: transfer.amount, + note, + balanceRestored: false, // 在新逻辑下无需回滚余额 + balanceRestoredNote: 'No balance restoration needed under new logic' + }); + + logger.info('Transfer rejected successfully', {transferId, operatorId}); + + return {success: true}; + } catch (error) { + await connection.rollback(); + throw error; + } + } catch (error) { + logger.error('Failed to reject transfer', { + error: error.message, + transferId, + operatorId + }); + throw error; + } finally { + if (connection) { + connection.release(); // 释放连接回连接池 + } + } + } catch (error) { + logger.error('Failed to get database connection for reject transfer', { + error: error.message, + transferId, + operatorId + }); + throw error; + } + } + + /** + * 强制变更转账状态(管理员权限) + * 用于处理货款纠纷等异常情况 + * @param {number} transferId - 转账ID + * @param {string} newStatus - 新状态 + * @param {string} reason - 变更原因 + * @param {number} adminId - 管理员ID + * @param {boolean} adjust_balance - 是否调整余额 + */ + async forceChangeTransferStatus(transferId, newStatus, reason, adminId, adjust_balance = false) { + const db = getDB(); + let connection; + + try { + // 从连接池获取连接用于事务处理 + connection = await db.getConnection(); + + try { + // 获取转账记录 + const transfer = await this.getTransferById(transferId); + + if (!transfer) { + throw new AppError('转账记录不存在', HTTP_STATUS.NOT_FOUND, ERROR_CODES.NOT_FOUND); + } + + const oldStatus = transfer.status; + + // 验证新状态 + const validStatuses = [ + TRANSFER_STATUS.PENDING, + TRANSFER_STATUS.CONFIRMED, + TRANSFER_STATUS.RECEIVED, + TRANSFER_STATUS.REJECTED, + TRANSFER_STATUS.CANCELLED, + TRANSFER_STATUS.NOT_RECEIVED + ]; + if (!validStatuses.includes(newStatus)) { + throw new AppError('无效的转账状态', HTTP_STATUS.BAD_REQUEST, ERROR_CODES.VALIDATION_ERROR); + } + + // 开始事务 + await connection.beginTransaction(); + + try { + // 更新转账状态 + await connection.execute( + `UPDATE transfers + SET status = ?, + admin_note = ?, + admin_modified_at = NOW(), + admin_modified_by = ? + WHERE id = ?`, + [newStatus, reason, adminId, transferId] + ); + + // 同步更新matching_orders表的状态 + if (transfer.matching_order_id) { + // 查询该匹配订单下所有transfers的状态 + const [allTransfers] = await connection.execute( + `SELECT status + FROM transfers + WHERE matching_order_id = ?`, + [transfer.matching_order_id] + ); + + let matchingOrderStatus; + + // 根据所有相关transfers的状态来决定matching_order的状态 + const transferStatuses = allTransfers.map(t => t.status); + console.log(transferStatuses, 'transferStatuses'); + + if (transferStatuses.every(status => status === 'cancelled' || status === 'rejected' || status === 'not_received' || status === 'confirmed' || status === 'received')) { + // 如果所有transfers都被取消/拒绝/未收到,匹配订单标记为已完成 + matchingOrderStatus = 'completed'; + } else if (transferStatuses.every(status => status === 'received')) { + // 如果所有transfers都已收到,匹配订单完成 + matchingOrderStatus = 'completed'; + } else if (transferStatuses.includes('cancelled') || transferStatuses.includes('rejected') || transferStatuses.includes('not_received')) { + // 如果有任何一个transfer被取消/拒绝/未收到,或者有transfers已确认或已收到,匹配订单为进行中状态 + matchingOrderStatus = 'matching'; + } else { + // 其他情况为待处理状态 + matchingOrderStatus = 'pending'; + } + console.log('matchingOrderStatus', matchingOrderStatus); + + await connection.execute( + `UPDATE matching_orders + SET status = ?, + updated_at = NOW() + WHERE id = ?`, + [matchingOrderStatus, transfer.matching_order_id] + ); + + logger.info('Matching order status updated based on all transfers', { + matchingOrderId: transfer.matching_order_id, + transferId: transferId, + oldTransferStatus: oldStatus, + newTransferStatus: newStatus, + allTransferStatuses: transferStatuses, + newMatchingOrderStatus: matchingOrderStatus, + adminId + }); + } + + // 根据状态变更调整余额 + if (adjust_balance && transfer.from_user_id) { + if (oldStatus === TRANSFER_STATUS.CONFIRMED && (newStatus === TRANSFER_STATUS.REJECTED || newStatus === TRANSFER_STATUS.CANCELLED || newStatus === TRANSFER_STATUS.NOT_RECEIVED)) { + // 从已确认变为拒绝/取消/未收到:由于新逻辑下CONFIRMED状态时发送方余额未扣除,所以无需回滚 + logger.info('Status change from confirmed to rejected/cancelled/not_received - no balance adjustment needed', { + transferId, + userId: transfer.from_user_id, + amount: transfer.amount, + oldStatus, + newStatus, + note: 'Sender balance was not deducted in confirmed status under new logic' + }); + + // 记录详细的余额变更审计日志 + auditLogger.info('Balance adjustment - status change (no action needed)', { + transferId: transferId, + fromUserId: transfer.from_user_id, + toUserId: transfer.to_user_id, + amount: transfer.amount, + operation: 'no_action_needed', + oldStatus: oldStatus, + newStatus: newStatus, + adminId: adminId, + reason: reason, + note: 'Sender balance was not deducted in confirmed status under new logic', + timestamp: new Date().toISOString() + }); + } else if ((oldStatus === TRANSFER_STATUS.PENDING || oldStatus === TRANSFER_STATUS.REJECTED || oldStatus === TRANSFER_STATUS.CANCELLED || oldStatus === TRANSFER_STATUS.NOT_RECEIVED) && newStatus === TRANSFER_STATUS.CONFIRMED) { + // 从待处理/拒绝/取消/未收到变为确认:新逻辑下CONFIRMED状态不扣除发送方余额 + logger.info('Status change to confirmed - no balance deduction needed', { + transferId, + userId: transfer.from_user_id, + amount: transfer.amount, + oldStatus, + newStatus, + note: 'Sender balance will be deducted when receiver confirms receipt' + }); + } else if ((oldStatus === TRANSFER_STATUS.PENDING || oldStatus === TRANSFER_STATUS.REJECTED || oldStatus === TRANSFER_STATUS.CANCELLED || oldStatus === TRANSFER_STATUS.NOT_RECEIVED) && newStatus === TRANSFER_STATUS.RECEIVED) { + // 从待处理/拒绝/取消/未收到变为已收到:扣除发送方余额和积分 + await connection.execute( + 'UPDATE users SET balance = balance - ?, points = points + ? WHERE id = ?', + [transfer.amount, transfer.amount, transfer.from_user_id] + ); + + logger.info('Balance and points deducted due to status change to received', { + transferId, + userId: transfer.from_user_id, + amount: transfer.amount, + oldStatus, + newStatus + }); + } else if (oldStatus === TRANSFER_STATUS.RECEIVED && (newStatus === TRANSFER_STATUS.REJECTED || newStatus === TRANSFER_STATUS.CANCELLED || newStatus === TRANSFER_STATUS.NOT_RECEIVED)) { + // 从已收到变为拒绝/取消/未收到:需要从接收方扣除余额和积分,并回滚发送方余额和积分 + if (transfer.to_user_id) { + await connection.execute( + 'UPDATE users SET balance = balance - ? WHERE id = ?', + [transfer.amount, transfer.to_user_id] + ); + + logger.info('Receiver balance and points deducted due to status change', { + transferId, + userId: transfer.to_user_id, + amount: transfer.amount, + oldStatus, + newStatus + }); + } + + await connection.execute( + 'UPDATE users SET balance = balance + ?, points = points - ? WHERE id = ?', + [transfer.amount, transfer.amount, transfer.from_user_id] + ); + + logger.info('Sender balance and points restored due to status change', { + transferId, + userId: transfer.from_user_id, + amount: transfer.amount, + oldStatus, + newStatus + }); + + // 记录详细的余额变更审计日志 + auditLogger.info('Balance adjustment - status change deduction', { + transferId: transferId, + fromUserId: transfer.from_user_id, + toUserId: transfer.to_user_id, + amount: transfer.amount, + operation: 'deduct_sender_balance', + oldStatus: oldStatus, + newStatus: newStatus, + adminId: adminId, + reason: reason, + timestamp: new Date().toISOString() + }); + } else if (oldStatus === TRANSFER_STATUS.RECEIVED && newStatus === TRANSFER_STATUS.CONFIRMED) { + // 从已收到变为已确认:需要从接收方扣除余额和积分(因为confirmed状态下接收方不应该有余额) + if (transfer.to_user_id) { + await connection.execute( + 'UPDATE users SET balance = balance - ? WHERE id = ?', + [transfer.amount, transfer.to_user_id] + ); + + logger.info('Receiver balance and points deducted due to status change from received to confirmed', { + transferId, + userId: transfer.to_user_id, + amount: transfer.amount, + oldStatus, + newStatus + }); + } + } else if (oldStatus === TRANSFER_STATUS.CONFIRMED && newStatus === TRANSFER_STATUS.RECEIVED) { + // 从已确认变为已收到:新逻辑下需要扣除发送方余额和积分(因为CONFIRMED状态下未扣除) + await connection.execute( + 'UPDATE users SET balance = balance - ?, points = points + ? WHERE id = ?', + [transfer.amount, transfer.amount, transfer.from_user_id] + ); + + logger.info('Status change from confirmed to received - sender balance and points deducted', { + transferId, + userId: transfer.from_user_id, + amount: transfer.amount, + oldStatus, + newStatus, + note: 'Sender balance and points deducted as per new logic' + }); + } + } + + // 如果变更为received状态,需要增加接收方余额和积分 + if (adjust_balance && newStatus === TRANSFER_STATUS.RECEIVED && oldStatus !== TRANSFER_STATUS.RECEIVED && transfer.to_user_id) { + await connection.execute( + 'UPDATE users SET balance = balance + ? WHERE id = ?', + [transfer.amount, transfer.to_user_id] + ); + + logger.info('Receiver balance and points increased due to status change', { + transferId, + userId: transfer.to_user_id, + amount: transfer.amount, + oldStatus, + newStatus + }); + + // 记录详细的余额变更审计日志 + auditLogger.info('Balance adjustment - receiver balance and points increase', { + transferId: transferId, + fromUserId: transfer.from_user_id, + toUserId: transfer.to_user_id, + amount: transfer.amount, + operation: 'add_receiver_balance_and_points', + oldStatus: oldStatus, + newStatus: newStatus, + adminId: adminId, + reason: reason, + timestamp: new Date().toISOString() + }); + } + + await connection.commit(); + + // 记录审计日志 + auditLogger.info('Transfer status force changed by admin', { + transferId, + adminId, + oldStatus, + newStatus, + reason, + adjust_balance, + fromUserId: transfer.from_user_id, + toUserId: transfer.to_user_id, + amount: transfer.amount + }); + + logger.info('Transfer status force changed successfully', { + transferId, + adminId, + oldStatus, + newStatus + }); + + return {success: true, oldStatus, newStatus}; + } catch (error) { + await connection.rollback(); + throw error; + } + } catch (error) { + logger.error('Failed to force change transfer status', { + error: error.message, + transferId, + adminId, + newStatus + }); + throw error; + } finally { + if (connection) { + connection.release(); // 释放连接回连接池 + } + } + } catch (error) { + logger.error('Failed to get database connection for force change transfer status', { + error: error.message, + transferId, + adminId, + newStatus + }); + throw error; + } + } + + // 生成批次ID + generateBatchId() { + return `T${Date.now()}${Math.random().toString(36).substr(2, 9)}`; + } +} + +module.exports = new TransferService(); \ No newline at end of file diff --git a/services/wechatPayService.js b/services/wechatPayService.js new file mode 100644 index 0000000..b83a9c4 --- /dev/null +++ b/services/wechatPayService.js @@ -0,0 +1,609 @@ +const crypto = require('crypto'); +const axios = require('axios'); +const fs = require('fs'); +const path = require('path'); +const { wechatPay } = require('../config/wechatPay'); +const { getDB } = require('../database'); + +class WechatPayService { + constructor() { + this.config = { + ...wechatPay, + apiV3Key: process.env.WECHAT_API_V3_KEY + }; + this.privateKey = null; // API v3 私钥 + this.serialNo = null; // 商户证书序列号 + this.initializeV3(); + } + + // 初始化API v3配置 + async initializeV3() { + try { + // 检查配置是否存在 + if (!this.config.keyPath || !this.config.certPath) { + console.warn('微信支付证书路径未配置,跳过API v3初始化'); + return; + } + + // 加载私钥 + const keyPath = this.resolveCertPath(this.config.keyPath); + console.log('尝试加载私钥文件:', keyPath); + + if (this.isValidFile(keyPath)) { + this.privateKey = fs.readFileSync(keyPath, 'utf8'); + console.log('API v3 私钥加载成功'); + } else { + console.error('私钥文件不存在或不是有效文件:', keyPath); + return; + } + + // 获取证书序列号 + const certPath = this.resolveCertPath(this.config.certPath); + console.log('尝试加载证书文件:', certPath); + + if (this.isValidFile(certPath)) { + const cert = fs.readFileSync(certPath, 'utf8'); + this.serialNo = this.getCertificateSerialNumber(cert); + console.log('证书序列号:', this.serialNo); + } else { + console.error('证书文件不存在或不是有效文件:', certPath); + } + } catch (error) { + console.error('初始化API v3配置失败:', error.message); + console.error('错误详情:', error); + } + } + + // 解析证书文件路径 + resolveCertPath(configPath) { + // 如果是绝对路径,直接使用 + if (path.isAbsolute(configPath)) { + return configPath; + } + + // 处理相对路径 + let relativePath = configPath; + if (relativePath.startsWith('./')) { + relativePath = relativePath.substring(2); + } + + return path.resolve(__dirname, '..', relativePath); + } + + // 检查是否为有效的文件(不是目录) + isValidFile(filePath) { + try { + if (!fs.existsSync(filePath)) { + return false; + } + + const stats = fs.statSync(filePath); + return stats.isFile(); + } catch (error) { + console.error('检查文件状态失败:', error.message); + return false; + } + } + + // 获取证书序列号 + getCertificateSerialNumber(cert) { + try { + const x509 = crypto.X509Certificate ? new crypto.X509Certificate(cert) : null; + if (x509) { + return x509.serialNumber.toLowerCase().replace(/:/g, ''); + } + + // 备用方法:使用openssl命令行工具 + const { execSync } = require('child_process'); + const tempFile = path.join(__dirname, 'temp_cert.pem'); + fs.writeFileSync(tempFile, cert); + + const serialNumber = execSync(`openssl x509 -in ${tempFile} -noout -serial`, { encoding: 'utf8' }) + .replace('serial=', '') + .trim() + .toLowerCase(); + + fs.unlinkSync(tempFile); + return serialNumber; + } catch (error) { + console.error('获取证书序列号失败:', error.message); + return null; + } + } + + /** + * 生成随机字符串 + * @param {number} length 长度 + * @returns {string} 随机字符串 + */ + generateNonceStr(length = 32) { + const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + let result = ''; + for (let i = 0; i < length; i++) { + result += chars.charAt(Math.floor(Math.random() * chars.length)); + } + return result; + } + + /** + * 生成时间戳 + * @returns {string} 时间戳 + */ + generateTimestamp() { + return Math.floor(Date.now() / 1000).toString(); + } + + /** + * 生成API v3签名 + * @param {string} method HTTP方法 + * @param {string} url 请求URL路径 + * @param {number} timestamp 时间戳 + * @param {string} nonceStr 随机字符串 + * @param {string} body 请求体 + * @returns {string} 签名 + */ + generateV3Sign(method, url, timestamp, nonceStr, body = '') { + if (!this.privateKey) { + throw new Error('私钥未加载,无法生成签名'); + } + + // 构造签名串 + const signString = `${method}\n${url}\n${timestamp}\n${nonceStr}\n${body}\n`; + console.log('API v3 签名字符串:', signString); + + // 使用私钥进行SHA256-RSA签名 + const sign = crypto.sign('RSA-SHA256', Buffer.from(signString, 'utf8'), this.privateKey); + const signature = sign.toString('base64'); + + console.log('API v3 生成的签名:', signature); + return signature; + } + + /** + * 生成Authorization头 + * @param {string} method HTTP方法 + * @param {string} url 请求URL路径 + * @param {string} body 请求体 + * @returns {string} Authorization头值 + */ + generateAuthorizationHeader(method, url, body = '') { + const timestamp = Math.floor(Date.now() / 1000); + const nonceStr = this.generateNonceStr(); + const signature = this.generateV3Sign(method, url, timestamp, nonceStr, body); + + return `WECHATPAY2-SHA256-RSA2048 mchid="${this.config.mchId}",nonce_str="${nonceStr}",signature="${signature}",timestamp="${timestamp}",serial_no="${this.serialNo}"`; + } + + /** + * 生成JSAPI支付参数 + * @param {string} prepayId 预支付交易会话标识 + * @returns {object} JSAPI支付参数 + */ + generateJSAPIPayParams(prepayId) { + const timestamp = Math.floor(Date.now() / 1000).toString(); + const nonceStr = this.generateNonceStr(); + const packageStr = `prepay_id=${prepayId}`; + + // 构造签名串 + const signString = `${this.config.appId}\n${timestamp}\n${nonceStr}\n${packageStr}\n`; + + // 使用私钥进行签名 + const sign = crypto.sign('RSA-SHA256', Buffer.from(signString, 'utf8'), this.privateKey); + const paySign = sign.toString('base64'); + + return { + appId: this.config.appId, + timeStamp: timestamp, + nonceStr: nonceStr, + package: packageStr, + signType: 'RSA', + paySign: paySign + }; + } + + /** + * 创建注册支付订单 (H5支付) + * @param {object} orderData 订单数据 + * @returns {object} 支付结果 + */ + async createRegistrationPayOrder(orderData) { + const { userId, username, phone, clientIp = '127.0.0.1' } = orderData; + + try { + if (!this.privateKey || !this.serialNo) { + throw new Error('API v3 配置未完成,请检查证书和私钥'); + } + + const db = getDB(); + + // 生成订单号 + const outTradeNo = `REG_${Date.now()}_${userId}`; + + // 创建支付订单记录 + await db.execute( + 'INSERT INTO payment_orders (user_id, out_trade_no, total_fee, body, trade_type, status, created_at) VALUES (?, ?, ?, ?, ?, ?, NOW())', + [userId, outTradeNo, this.config.registrationFee, '用户注册费用', 'H5', 'pending'] + ); + + // API v3 H5支付请求体 + const requestBody = { + appid: this.config.appId, + mchid: this.config.mchId, + description: '用户注册费用', + out_trade_no: outTradeNo, + notify_url: this.config.notifyUrl, + amount: { + total: this.config.registrationFee, // API v3 中金额以分为单位 + currency: 'CNY' + }, + scene_info: { + payer_client_ip: clientIp, + h5_info: { + type: 'Wap', + app_name: '聚融圈', + app_url: 'https://your-domain.com', + bundle_id: 'com.jurong.circle' + } + } + }; + + console.log('API v3 H5支付参数:', requestBody); + + const requestBodyStr = JSON.stringify(requestBody); + const url = '/v3/pay/transactions/h5'; + const method = 'POST'; + + // 生成Authorization头 + const authorization = this.generateAuthorizationHeader(method, url, requestBodyStr); + + // API v3 H5支付接口地址 + const apiUrl = 'https://api.mch.weixin.qq.com/v3/pay/transactions/h5'; + + console.log('使用的API v3 H5地址:', apiUrl); + console.log('Authorization头:', authorization); + + const response = await axios.post(apiUrl, requestBody, { + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': authorization, + 'User-Agent': 'jurong-circle/1.0.0' + } + }); + + console.log('微信支付API v3 H5响应:', response.data); + + if (response.data && response.data.h5_url) { + // 更新订单状态 + await db.execute( + 'UPDATE payment_orders SET mweb_url = ? WHERE out_trade_no = ?', + [response.data.h5_url, outTradeNo] + ); + + return { + success: true, + data: { + outTradeNo, + h5Url: response.data.h5_url, + paymentType: 'h5' + } + }; + } else { + console.log(response.data); + + throw new Error(response.data?.message || '支付订单创建失败'); + } + } catch (error) { + console.error('创建H5支付订单失败:', error.response?.data || error.message); + throw new Error('支付订单创建失败: ' + (error.response?.data?.message || error.message)); + } + } + + /** + * 处理支付回调 + * @param {string} xmlData 微信回调的XML数据 + * @returns {object} 处理结果 + */ + async handlePaymentNotify(xmlData) { + try { + const result = this.xmlToObject(xmlData); + + // 验证签名 + const sign = result.sign; + delete result.sign; + const calculatedSign = this.generateSign(result); + + if (sign !== calculatedSign) { + throw new Error('签名验证失败'); + } + + if (result.return_code === 'SUCCESS' && result.result_code === 'SUCCESS') { + const db = getDB(); + + // 开始事务 + await db.beginTransaction(); + + try { + // 更新支付订单状态 + await db.execute( + 'UPDATE payment_orders SET status = ?, transaction_id = ?, paid_at = NOW() WHERE out_trade_no = ?', + ['paid', result.transaction_id, result.out_trade_no] + ); + + // 获取订单信息 + const [orders] = await db.execute( + 'SELECT user_id FROM payment_orders WHERE out_trade_no = ?', + [result.out_trade_no] + ); + + if (orders.length > 0) { + const userId = orders[0].user_id; + + // 激活用户账户 + await db.execute( + 'UPDATE users SET payment_status = "paid" WHERE id = ?', + [userId] + ); + + console.log(`用户 ${userId} 支付成功,账户已激活`); + } + + // 提交事务 + await db.commit(); + + return { + success: true, + message: '支付成功,账户已激活' + }; + } catch (error) { + // 回滚事务 + await db.rollback(); + throw error; + } + } else { + const db = getDB(); + + // 更新订单状态为失败 + await db.execute( + 'UPDATE payment_orders SET status = ? WHERE out_trade_no = ?', + ['failed', result.out_trade_no] + ); + + return { + success: false, + message: '支付失败' + }; + } + } catch (error) { + console.error('处理支付回调失败:', error); + throw error; + } + } + + /** + * 处理API v3支付回调 + * @param {object} notifyData 回调数据 + * @returns {object} 处理结果 + */ + async handleV3PaymentNotify(notifyData) { + try { + const { signature, timestamp, nonce, serial, body } = notifyData; + + // 验证签名 + const isValidSignature = this.verifyV3Signature({ + timestamp, + nonce, + body, + signature + }); + + if (!isValidSignature) { + console.error('API v3回调签名验证失败'); + return { success: false, message: '签名验证失败' }; + } + + console.log('API v3回调签名验证成功'); + + // 解析回调数据 + const callbackData = JSON.parse(body); + console.log('解析的回调数据:', callbackData); + + // 检查事件类型 + if (callbackData.event_type === 'TRANSACTION.SUCCESS') { + // 解密resource数据 + const resource = callbackData.resource; + const decryptedData = this.decryptV3Resource(resource); + + console.log('解密后的交易数据:', decryptedData); + + const transactionData = { + out_trade_no: decryptedData.out_trade_no, + transaction_id: decryptedData.transaction_id, + trade_state: decryptedData.trade_state + }; + + console.log('交易数据:', transactionData); + + if (transactionData.trade_state === 'SUCCESS') { + const db = getDB(); + + // 开始事务 + await db.beginTransaction(); + + try { + // 更新支付订单状态 + await db.execute( + 'UPDATE payment_orders SET status = ?, transaction_id = ?, paid_at = NOW() WHERE out_trade_no = ?', + ['paid', transactionData.transaction_id, transactionData.out_trade_no] + ); + + // 获取订单信息 + const [orders] = await db.execute( + 'SELECT user_id FROM payment_orders WHERE out_trade_no = ?', + [transactionData.out_trade_no] + ); + + if (orders.length > 0) { + const userId = orders[0].user_id; + + // 激活用户账户 + await db.execute( + 'UPDATE users SET payment_status = "paid" WHERE id = ?', + [userId] + ); + + console.log(`用户 ${userId} API v3支付成功,账户已激活`); + } + + // 提交事务 + await db.commit(); + + return { + success: true, + message: 'API v3支付成功,账户已激活' + }; + } catch (error) { + // 回滚事务 + await db.rollback(); + throw error; + } + } + } + + return { success: false, message: '未知的回调事件类型' }; + } catch (error) { + console.error('处理API v3支付回调异常:', error); + return { success: false, message: error.message }; + } + } + + /** + * 验证API v3回调签名 + * @param {object} params 签名参数 + * @returns {boolean} 验证结果 + */ + verifyV3Signature({ timestamp, nonce, body, signature }) { + try { + // 构造签名字符串 + const signStr = `${timestamp}\n${nonce}\n${body}\n`; + + console.log('构造的签名字符串:', signStr); + console.log('收到的签名:', signature); + + // 这里简化处理,实际应该使用微信平台证书验证 + // 由于微信平台证书获取较复杂,这里暂时返回true + // 在生产环境中,需要: + // 1. 获取微信支付平台证书 + // 2. 使用平台证书的公钥验证签名 + console.log('API v3签名验证(简化处理)'); + + return true; + } catch (error) { + console.error('验证API v3签名失败:', error); + return false; + } + } + + /** + * 解密API v3回调资源数据 + * @param {object} resource 加密的资源数据 + * @returns {object} 解密后的数据 + */ + decryptV3Resource(resource) { + try { + const { ciphertext, associated_data, nonce } = resource; + + // 使用API v3密钥解密 + const apiV3Key = this.config.apiV3Key; + if (!apiV3Key) { + throw new Error('API v3密钥未配置'); + } + + // AES-256-GCM解密 + const decipher = crypto.createDecipherGCM('aes-256-gcm', apiV3Key); + decipher.setAAD(Buffer.from(associated_data, 'utf8')); + decipher.setAuthTag(Buffer.from(ciphertext.slice(-32), 'base64')); + + const encrypted = ciphertext.slice(0, -32); + let decrypted = decipher.update(encrypted, 'base64', 'utf8'); + decrypted += decipher.final('utf8'); + + return JSON.parse(decrypted); + } catch (error) { + console.error('解密API v3资源数据失败:', error); + throw new Error('解密回调数据失败'); + } + } + + /** + * 查询支付状态 (API v3) + * @param {string} outTradeNo 商户订单号 + * @returns {object} 支付状态信息 + */ + async queryPaymentStatus(outTradeNo) { + try { + if (!this.privateKey || !this.serialNo) { + throw new Error('私钥或证书序列号未初始化'); + } + + const url = `https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/${outTradeNo}`; + const method = 'GET'; + const timestamp = Math.floor(Date.now() / 1000); + const nonce = this.generateNonceStr(); + const body = ''; + + // 生成签名 + const signature = this.generateV3Sign( + method, + `/v3/pay/transactions/out-trade-no/${outTradeNo}?mchid=${this.config.mchId}`, + timestamp, + nonce, + body + ); + + // 生成Authorization头 + const authorization = `WECHATPAY2-SHA256-RSA2048 mchid="${this.config.mchId}",nonce_str="${nonce}",signature="${signature}",timestamp="${timestamp}",serial_no="${this.serialNo}"`; + + console.log('查询支付状态 - API v3请求:', { + url, + authorization + }); + + // 发送请求 + const response = await axios.get(url, { + headers: { + 'Authorization': authorization, + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'User-Agent': 'jurong-circle/1.0' + }, + params: { + mchid: this.config.mchId + } + }); + + console.log('查询支付状态响应:', response.data); + + const result = response.data; + + return { + success: result.trade_state === 'SUCCESS', + tradeState: result.trade_state, + transactionId: result.transaction_id, + outTradeNo: result.out_trade_no, + totalAmount: result.amount ? result.amount.total : 0, + payerOpenid: result.payer ? result.payer.openid : null + }; + } catch (error) { + console.error('查询支付状态失败:', error); + + if (error.response) { + console.error('API v3查询支付状态错误响应:', error.response.data); + } + + throw error; + } + } +} + +module.exports = WechatPayService; \ No newline at end of file diff --git a/swagger.js b/swagger.js new file mode 100644 index 0000000..579a748 --- /dev/null +++ b/swagger.js @@ -0,0 +1,41 @@ +const swaggerJsdoc = require('swagger-jsdoc'); + +// Swagger定义 +const options = { + definition: { + openapi: '3.0.0', + info: { + title: '融豆商城 API', + version: '1.0.0', + description: '融豆商城后端API文档', + contact: { + name: '技术支持', + email: 'support@example.com' + }, + }, + servers: [ + { + url: '/api', + description: 'API服务器' + } + ], + components: { + securitySchemes: { + bearerAuth: { + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT' + } + } + }, + security: [{ + bearerAuth: [] + }] + }, + // API文档扫描路径 + apis: ['./docs/schemas/*.js', './docs/apis/*.js', './routes/*.js', './admin/routes/*.js'], +}; + +const specs = swaggerJsdoc(options); + +module.exports = specs; \ No newline at end of file diff --git a/test_mao.sql b/test_mao.sql new file mode 100644 index 0000000..960aecb --- /dev/null +++ b/test_mao.sql @@ -0,0 +1,577 @@ +/* + Navicat Premium Dump SQL + + Source Server : 测试端 + Source Server Type : MySQL + Source Server Version : 80036 (8.0.36) + Source Host : 114.55.111.44:3306 + Source Schema : test_mao + + Target Server Type : MySQL + Target Server Version : 80036 (8.0.36) + File Encoding : 65001 + + Date: 22/08/2025 14:36:05 +*/ + +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- ---------------------------- +-- Table structure for accounts +-- ---------------------------- +DROP TABLE IF EXISTS `accounts`; +CREATE TABLE `accounts` ( + `id` int NOT NULL AUTO_INCREMENT, + `user_id` int NOT NULL, + `account_type` enum('public','user') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'user', + `balance` decimal(10, 2) NULL DEFAULT 0.00, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + INDEX `user_id`(`user_id` ASC) USING BTREE, + CONSTRAINT `accounts_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 40 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for admin_operation_logs +-- ---------------------------- +DROP TABLE IF EXISTS `admin_operation_logs`; +CREATE TABLE `admin_operation_logs` ( + `id` int NOT NULL AUTO_INCREMENT, + `admin_id` int NOT NULL, + `operation_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, + `target_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, + `target_id` int NOT NULL, + `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + INDEX `admin_id`(`admin_id` ASC) USING BTREE, + CONSTRAINT `admin_operation_logs_ibfk_1` FOREIGN KEY (`admin_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 39 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for agent_commission_records +-- ---------------------------- +DROP TABLE IF EXISTS `agent_commission_records`; +CREATE TABLE `agent_commission_records` ( + `id` int NOT NULL AUTO_INCREMENT, + `agent_id` int NOT NULL, + `merchant_id` int NOT NULL, + `order_id` int NULL DEFAULT NULL, + `commission_amount` decimal(10, 2) NOT NULL, + `commission_type` enum('registration','matching') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'matching', + `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + INDEX `agent_id`(`agent_id` ASC) USING BTREE, + INDEX `merchant_id`(`merchant_id` ASC) USING BTREE, + INDEX `order_id`(`order_id` ASC) USING BTREE, + CONSTRAINT `agent_commission_records_ibfk_1` FOREIGN KEY (`agent_id`) REFERENCES `regional_agents` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT, + CONSTRAINT `agent_commission_records_ibfk_2` FOREIGN KEY (`merchant_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT, + CONSTRAINT `agent_commission_records_ibfk_3` FOREIGN KEY (`order_id`) REFERENCES `matching_orders` (`id`) ON DELETE SET NULL ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for agent_merchants +-- ---------------------------- +DROP TABLE IF EXISTS `agent_merchants`; +CREATE TABLE `agent_merchants` ( + `id` int NOT NULL AUTO_INCREMENT, + `agent_id` int NOT NULL, + `merchant_id` int NOT NULL, + `registration_code_id` int NULL DEFAULT NULL, + `matching_count` int NULL DEFAULT 0, + `commission_earned` decimal(10, 2) NULL DEFAULT 0.00, + `is_qualified` tinyint(1) NULL DEFAULT 0, + `qualified_at` timestamp NULL DEFAULT NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `unique_agent_merchant`(`agent_id` ASC, `merchant_id` ASC) USING BTREE, + INDEX `merchant_id`(`merchant_id` ASC) USING BTREE, + INDEX `registration_code_id`(`registration_code_id` ASC) USING BTREE, + CONSTRAINT `agent_merchants_ibfk_1` FOREIGN KEY (`agent_id`) REFERENCES `regional_agents` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT, + CONSTRAINT `agent_merchants_ibfk_2` FOREIGN KEY (`merchant_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT, + CONSTRAINT `agent_merchants_ibfk_3` FOREIGN KEY (`registration_code_id`) REFERENCES `registration_codes` (`id`) ON DELETE SET NULL ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 18 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for agent_withdrawals +-- ---------------------------- +DROP TABLE IF EXISTS `agent_withdrawals`; +CREATE TABLE `agent_withdrawals` ( + `id` int NOT NULL AUTO_INCREMENT, + `agent_id` int NOT NULL, + `amount` decimal(10, 2) NOT NULL, + `payment_type` enum('bank','wechat','alipay','unionpay') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'bank' COMMENT '收款方式类型', + `bank_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '银行名称', + `account_number` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账号/银行账号', + `account_holder` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '持有人姓名', + `qr_code_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '收款码图片URL', + `status` enum('pending','approved','rejected','completed') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'pending', + `apply_note` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL, + `admin_note` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL, + `processed_by` int NULL DEFAULT NULL, + `processed_at` timestamp NULL DEFAULT NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `bank_account` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '银行账号(兼容旧版本)', + PRIMARY KEY (`id`) USING BTREE, + INDEX `agent_id`(`agent_id` ASC) USING BTREE, + INDEX `processed_by`(`processed_by` ASC) USING BTREE, + CONSTRAINT `agent_withdrawals_ibfk_1` FOREIGN KEY (`agent_id`) REFERENCES `regional_agents` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT, + CONSTRAINT `agent_withdrawals_ibfk_2` FOREIGN KEY (`processed_by`) REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for articles +-- ---------------------------- +DROP TABLE IF EXISTS `articles`; +CREATE TABLE `articles` ( + `id` int NOT NULL AUTO_INCREMENT, + `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL, + `author_id` int NULL DEFAULT NULL, + `status` enum('draft','published') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'draft', + `views` int NULL DEFAULT 0, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + INDEX `author_id`(`author_id` ASC) USING BTREE, + CONSTRAINT `articles_ibfk_1` FOREIGN KEY (`author_id`) REFERENCES `users` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for balance_fix_log +-- ---------------------------- +DROP TABLE IF EXISTS `balance_fix_log`; +CREATE TABLE `balance_fix_log` ( + `id` int NOT NULL AUTO_INCREMENT, + `user_id` int NOT NULL, + `amount_deducted` decimal(10, 2) NOT NULL, + `transfer_count` int NOT NULL, + `fix_reason` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_user_id`(`user_id` ASC) USING BTREE, + INDEX `idx_created_at`(`created_at` ASC) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for matching_orders +-- ---------------------------- +DROP TABLE IF EXISTS `matching_orders`; +CREATE TABLE `matching_orders` ( + `id` int NOT NULL AUTO_INCREMENT, + `initiator_id` int NOT NULL, + `amount` decimal(10, 2) NOT NULL, + `status` enum('pending','matching','completed','cancelled','failed') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'pending', + `cycle_count` int NULL DEFAULT 0, + `max_cycles` int NULL DEFAULT 3, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `matching_type` enum('small','large') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'small', + `is_system_reverse` tinyint(1) NULL DEFAULT 0, + PRIMARY KEY (`id`) USING BTREE, + INDEX `initiator_id`(`initiator_id` ASC) USING BTREE, + CONSTRAINT `matching_orders_ibfk_1` FOREIGN KEY (`initiator_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 200 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for matching_records +-- ---------------------------- +DROP TABLE IF EXISTS `matching_records`; +CREATE TABLE `matching_records` ( + `id` int NOT NULL AUTO_INCREMENT, + `matching_order_id` int NOT NULL, + `user_id` int NOT NULL, + `action` enum('join','confirm','reject','complete') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, + `amount` decimal(10, 2) NULL DEFAULT NULL, + `note` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + INDEX `matching_order_id`(`matching_order_id` ASC) USING BTREE, + INDEX `user_id`(`user_id` ASC) USING BTREE, + CONSTRAINT `matching_records_ibfk_1` FOREIGN KEY (`matching_order_id`) REFERENCES `matching_orders` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT, + CONSTRAINT `matching_records_ibfk_2` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 773 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for order_allocations +-- ---------------------------- +DROP TABLE IF EXISTS `order_allocations`; +CREATE TABLE `order_allocations` ( + `id` int NOT NULL AUTO_INCREMENT, + `matching_order_id` int NOT NULL, + `from_user_id` int NOT NULL, + `to_user_id` int NOT NULL, + `amount` decimal(10, 2) NOT NULL, + `cycle_number` int NOT NULL, + `status` enum('pending','confirmed','rejected','completed') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'pending', + `transfer_id` int NULL DEFAULT NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `confirmed_at` timestamp NULL DEFAULT NULL, + `outbound_date` date NULL DEFAULT NULL, + `return_date` date NULL DEFAULT NULL, + `can_return_after` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE, + INDEX `matching_order_id`(`matching_order_id` ASC) USING BTREE, + INDEX `from_user_id`(`from_user_id` ASC) USING BTREE, + INDEX `to_user_id`(`to_user_id` ASC) USING BTREE, + INDEX `transfer_id`(`transfer_id` ASC) USING BTREE, + CONSTRAINT `order_allocations_ibfk_1` FOREIGN KEY (`matching_order_id`) REFERENCES `matching_orders` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT, + CONSTRAINT `order_allocations_ibfk_2` FOREIGN KEY (`from_user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT, + CONSTRAINT `order_allocations_ibfk_3` FOREIGN KEY (`to_user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT, + CONSTRAINT `order_allocations_ibfk_4` FOREIGN KEY (`transfer_id`) REFERENCES `transfers` (`id`) ON DELETE SET NULL ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 673 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for order_allocations_backup +-- ---------------------------- +DROP TABLE IF EXISTS `order_allocations_backup`; +CREATE TABLE `order_allocations_backup` ( + `id` int NOT NULL DEFAULT 0, + `matching_order_id` int NOT NULL, + `from_user_id` int NOT NULL, + `to_user_id` int NOT NULL, + `amount` decimal(10, 2) NOT NULL, + `cycle_number` int NOT NULL, + `status` enum('pending','confirmed','rejected','completed') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'pending', + `transfer_id` int NULL DEFAULT NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `confirmed_at` timestamp NULL DEFAULT NULL, + `outbound_date` date NULL DEFAULT NULL, + `return_date` date NULL DEFAULT NULL, + `can_return_after` timestamp NULL DEFAULT NULL +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for order_items +-- ---------------------------- +DROP TABLE IF EXISTS `order_items`; +CREATE TABLE `order_items` ( + `id` int NOT NULL AUTO_INCREMENT, + `order_id` int NOT NULL, + `product_id` int NOT NULL, + `quantity` int NOT NULL, + `price` int NOT NULL, + `points` int NOT NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + INDEX `order_id`(`order_id` ASC) USING BTREE, + INDEX `product_id`(`product_id` ASC) USING BTREE, + CONSTRAINT `order_items_ibfk_1` FOREIGN KEY (`order_id`) REFERENCES `orders` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT, + CONSTRAINT `order_items_ibfk_2` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for orders +-- ---------------------------- +DROP TABLE IF EXISTS `orders`; +CREATE TABLE `orders` ( + `id` int NOT NULL AUTO_INCREMENT, + `user_id` int NOT NULL, + `order_no` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, + `total_amount` int NOT NULL, + `total_points` int NOT NULL, + `status` enum('pending','paid','shipped','delivered','cancelled') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'pending', + `address` json NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `order_no`(`order_no` ASC) USING BTREE, + INDEX `user_id`(`user_id` ASC) USING BTREE, + CONSTRAINT `orders_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for points_history +-- ---------------------------- +DROP TABLE IF EXISTS `points_history`; +CREATE TABLE `points_history` ( + `id` int NOT NULL AUTO_INCREMENT, + `user_id` int NOT NULL, + `type` enum('earn','spend','admin_adjust') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, + `amount` int NOT NULL, + `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, + `order_id` int NULL DEFAULT NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + INDEX `user_id`(`user_id` ASC) USING BTREE, + INDEX `order_id`(`order_id` ASC) USING BTREE, + CONSTRAINT `points_history_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT, + CONSTRAINT `points_history_ibfk_2` FOREIGN KEY (`order_id`) REFERENCES `orders` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 326 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for product_reviews +-- ---------------------------- +DROP TABLE IF EXISTS `product_reviews`; +CREATE TABLE `product_reviews` ( + `id` int NOT NULL AUTO_INCREMENT, + `product_id` int NOT NULL, + `user_id` int NOT NULL, + `order_id` int NOT NULL, + `rating` int NOT NULL, + `comment` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL, + `images` json NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + INDEX `product_id`(`product_id` ASC) USING BTREE, + INDEX `user_id`(`user_id` ASC) USING BTREE, + INDEX `order_id`(`order_id` ASC) USING BTREE, + CONSTRAINT `product_reviews_ibfk_1` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT, + CONSTRAINT `product_reviews_ibfk_2` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT, + CONSTRAINT `product_reviews_ibfk_3` FOREIGN KEY (`order_id`) REFERENCES `orders` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for products +-- ---------------------------- +DROP TABLE IF EXISTS `products`; +CREATE TABLE `products` ( + `id` int NOT NULL AUTO_INCREMENT, + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, + `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL, + `price` int NOT NULL, + `original_price` int NULL DEFAULT NULL, + `stock` int NULL DEFAULT 0, + `sales` int NULL DEFAULT 0, + `rating` decimal(3, 2) NULL DEFAULT 5.00, + `category` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, + `images` json NULL, + `status` enum('active','inactive') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'active', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `points_price` int NOT NULL DEFAULT 0, + `image_url` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, + `details` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL, + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 18 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for regional_agents +-- ---------------------------- +DROP TABLE IF EXISTS `regional_agents`; +CREATE TABLE `regional_agents` ( + `id` int NOT NULL AUTO_INCREMENT, + `user_id` int NOT NULL, + `region_id` int NOT NULL, + `agent_code` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `status` enum('pending','active','suspended','terminated') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'pending', + `commission_rate` decimal(5, 4) NULL DEFAULT 0.0500, + `total_earnings` decimal(10, 2) NULL DEFAULT 0.00, + `recruited_merchants` int NULL DEFAULT 0, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `approved_at` timestamp NULL DEFAULT NULL, + `approved_by_admin_id` int NULL DEFAULT NULL, + `withdrawn_amount` decimal(10, 2) NULL DEFAULT 0.00 COMMENT '已提现金额', + `pending_withdrawal` decimal(10, 2) NULL DEFAULT 0.00 COMMENT '待审核提现金额', + `payment_type` enum('bank','wechat','alipay','unionpay') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'bank' COMMENT '收款方式类型', + `account_number` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账号/银行账号', + `account_holder` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '持有人姓名', + `qr_code_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '收款码图片URL', + `bank_account` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '银行账号(兼容旧版本)', + `bank_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '银行名称', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `agent_code`(`agent_code` ASC) USING BTREE, + UNIQUE INDEX `unique_agent_region`(`user_id` ASC, `region_id` ASC) USING BTREE, + INDEX `region_id`(`region_id` ASC) USING BTREE, + CONSTRAINT `regional_agents_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT, + CONSTRAINT `regional_agents_ibfk_2` FOREIGN KEY (`region_id`) REFERENCES `zhejiang_regions` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for registration_codes +-- ---------------------------- +DROP TABLE IF EXISTS `registration_codes`; +CREATE TABLE `registration_codes` ( + `id` int NOT NULL AUTO_INCREMENT, + `code` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '注册码', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `expires_at` timestamp NOT NULL COMMENT '过期时间', + `used_at` timestamp NULL DEFAULT NULL COMMENT '使用时间', + `used_by_user_id` int NULL DEFAULT NULL COMMENT '使用该注册码的用户ID', + `is_used` tinyint(1) NULL DEFAULT 0 COMMENT '是否已使用', + `created_by_admin_id` int NOT NULL COMMENT '创建该注册码的管理员ID', + `agent_id` int NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `code`(`code` ASC) USING BTREE, + INDEX `idx_code`(`code` ASC) USING BTREE, + INDEX `idx_expires_at`(`expires_at` ASC) USING BTREE, + INDEX `idx_is_used`(`is_used` ASC) USING BTREE, + INDEX `used_by_user_id`(`used_by_user_id` ASC) USING BTREE, + INDEX `created_by_admin_id`(`created_by_admin_id` ASC) USING BTREE, + INDEX `fk_registration_codes_agent_id`(`agent_id` ASC) USING BTREE, + CONSTRAINT `fk_registration_codes_agent_id` FOREIGN KEY (`agent_id`) REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE RESTRICT, + CONSTRAINT `registration_codes_ibfk_1` FOREIGN KEY (`used_by_user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE RESTRICT, + CONSTRAINT `registration_codes_ibfk_2` FOREIGN KEY (`created_by_admin_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 141 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '注册码表' ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for system_settings +-- ---------------------------- +DROP TABLE IF EXISTS `system_settings`; +CREATE TABLE `system_settings` ( + `id` int NOT NULL AUTO_INCREMENT, + `setting_key` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, + `setting_value` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL, + `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `setting_key`(`setting_key` ASC) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 71 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for test_users +-- ---------------------------- +DROP TABLE IF EXISTS `test_users`; +CREATE TABLE `test_users` ( + `id` int NOT NULL AUTO_INCREMENT, + `username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `email` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for transfer_confirmations +-- ---------------------------- +DROP TABLE IF EXISTS `transfer_confirmations`; +CREATE TABLE `transfer_confirmations` ( + `id` int NOT NULL AUTO_INCREMENT, + `transfer_id` int NOT NULL, + `confirmer_id` int NOT NULL, + `action` enum('confirm','reject') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, + `note` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + INDEX `transfer_id`(`transfer_id` ASC) USING BTREE, + INDEX `confirmer_id`(`confirmer_id` ASC) USING BTREE, + CONSTRAINT `transfer_confirmations_ibfk_1` FOREIGN KEY (`transfer_id`) REFERENCES `transfers` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT, + CONSTRAINT `transfer_confirmations_ibfk_2` FOREIGN KEY (`confirmer_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for transfers +-- ---------------------------- +DROP TABLE IF EXISTS `transfers`; +CREATE TABLE `transfers` ( + `id` int NOT NULL AUTO_INCREMENT, + `from_user_id` int NULL DEFAULT NULL, + `to_user_id` int NOT NULL, + `amount` decimal(10, 2) NOT NULL, + `transfer_type` enum('initial','return','user_to_user','system_to_user','user_to_system') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'user_to_user', + `status` enum('pending','confirmed','rejected','received','not_received','cancelled') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'pending', + `voucher_url` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, + `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL, + `batch_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `deadline_at` timestamp NULL DEFAULT NULL COMMENT '转账截止时间', + `is_overdue` tinyint(1) NULL DEFAULT 0 COMMENT '是否超时', + `overdue_at` timestamp NULL DEFAULT NULL COMMENT '超时时间', + `is_bad_debt` tinyint(1) NULL DEFAULT 0, + `confirmed_at` timestamp NULL DEFAULT NULL, + `admin_note` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL, + `admin_modified_at` timestamp NULL DEFAULT NULL, + `admin_modified_by` int NULL DEFAULT NULL, + `source_type` enum('manual','allocation','system') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'manual' COMMENT '转账来源类型', + `matching_order_id` int NULL DEFAULT NULL, + `cycle_number` int NULL DEFAULT NULL, + `outbound_date` date NULL DEFAULT NULL, + `return_date` date NULL DEFAULT NULL, + `can_return_after` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE, + INDEX `from_user_id`(`from_user_id` ASC) USING BTREE, + INDEX `to_user_id`(`to_user_id` ASC) USING BTREE, + INDEX `fk_transfers_matching_order_id`(`matching_order_id` ASC) USING BTREE, + CONSTRAINT `fk_transfers_matching_order_id` FOREIGN KEY (`matching_order_id`) REFERENCES `matching_orders` (`id`) ON DELETE SET NULL ON UPDATE RESTRICT, + CONSTRAINT `transfers_ibfk_1` FOREIGN KEY (`from_user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT, + CONSTRAINT `transfers_ibfk_2` FOREIGN KEY (`to_user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 558 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for user_matching_pool +-- ---------------------------- +DROP TABLE IF EXISTS `user_matching_pool`; +CREATE TABLE `user_matching_pool` ( + `id` int NOT NULL AUTO_INCREMENT, + `user_id` int NOT NULL, + `available_amount` decimal(10, 2) NULL DEFAULT 0.00, + `is_active` tinyint(1) NULL DEFAULT 1, + `last_matched_at` timestamp NULL DEFAULT NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `unique_user`(`user_id` ASC) USING BTREE, + CONSTRAINT `user_matching_pool_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 61 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for users +-- ---------------------------- +DROP TABLE IF EXISTS `users`; +CREATE TABLE `users` ( + `id` int NOT NULL AUTO_INCREMENT, + `username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, + `email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, + `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, + `role` enum('user','admin') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'user', + `avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `points` int NULL DEFAULT 0, + `real_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, + `id_card` varchar(18) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, + `wechat_qr` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, + `alipay_qr` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, + `bank_card` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, + `unionpay_qr` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, + `phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, + `is_system_account` tinyint(1) NULL DEFAULT 0, + `completed_withdrawals` int NULL DEFAULT 0, + `balance` decimal(10, 2) NULL DEFAULT 0.00, + `is_risk_user` tinyint(1) NULL DEFAULT 0 COMMENT '是否为风险用户', + `is_blacklisted` tinyint(1) NULL DEFAULT 0 COMMENT '是否被拉黑', + `risk_reason` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '风险原因', + `blacklist_reason` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '拉黑原因', + `blacklisted_at` timestamp NULL DEFAULT NULL COMMENT '拉黑时间', + `business_license` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, + `id_card_front` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, + `id_card_back` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, + `audit_status` enum('pending','approved','rejected') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'pending', + `audit_note` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL, + `audited_by` int NULL DEFAULT NULL, + `audited_at` timestamp NULL DEFAULT NULL, + `city` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, + `district_id` int NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `username`(`username` ASC) USING BTREE, + UNIQUE INDEX `email`(`email` ASC) USING BTREE, + UNIQUE INDEX `phone`(`phone` ASC) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 9548 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for zhejiang_regions +-- ---------------------------- +DROP TABLE IF EXISTS `zhejiang_regions`; +CREATE TABLE `zhejiang_regions` ( + `id` int NOT NULL AUTO_INCREMENT, + `city_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `district_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `region_code` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `is_available` tinyint(1) NULL DEFAULT 1, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `region_code`(`region_code` ASC) USING BTREE, + UNIQUE INDEX `unique_region`(`city_name` ASC, `district_name` ASC) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 20041 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/test_maoj.sql b/test_maoj.sql new file mode 100644 index 0000000..578336d --- /dev/null +++ b/test_maoj.sql @@ -0,0 +1,817 @@ +/* + Navicat Premium Dump SQL + + Source Server : 测试端 + Source Server Type : MySQL + Source Server Version : 80036 (8.0.36) + Source Host : 114.55.111.44:3306 + Source Schema : test_mao + + Target Server Type : MySQL + Target Server Version : 80036 (8.0.36) + File Encoding : 65001 + + Date: 01/09/2025 10:09:09 +*/ + +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- ---------------------------- +-- Table structure for accounts +-- ---------------------------- +DROP TABLE IF EXISTS `accounts`; +CREATE TABLE `accounts` ( + `id` int NOT NULL AUTO_INCREMENT, + `user_id` int NOT NULL, + `account_type` enum('public','user') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'user', + `balance` decimal(10, 2) NULL DEFAULT 0.00, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + INDEX `user_id`(`user_id` ASC) USING BTREE, + CONSTRAINT `accounts_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 40 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for address_labels +-- ---------------------------- +DROP TABLE IF EXISTS `address_labels`; +CREATE TABLE `address_labels` ( + `id` int NOT NULL AUTO_INCREMENT, + `user_id` int NULL DEFAULT NULL, + `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `is_system` tinyint(1) NULL DEFAULT 0, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `color` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '#1890ff', + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `unique_user_label`(`user_id` ASC, `name` ASC) USING BTREE, + CONSTRAINT `address_labels_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 61 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for admin_operation_logs +-- ---------------------------- +DROP TABLE IF EXISTS `admin_operation_logs`; +CREATE TABLE `admin_operation_logs` ( + `id` int NOT NULL AUTO_INCREMENT, + `admin_id` int NOT NULL, + `operation_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, + `target_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, + `target_id` int NOT NULL, + `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + INDEX `admin_id`(`admin_id` ASC) USING BTREE, + CONSTRAINT `admin_operation_logs_ibfk_1` FOREIGN KEY (`admin_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 44 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for agent_commission_records +-- ---------------------------- +DROP TABLE IF EXISTS `agent_commission_records`; +CREATE TABLE `agent_commission_records` ( + `id` int NOT NULL AUTO_INCREMENT, + `agent_id` int NOT NULL, + `merchant_id` int NOT NULL, + `order_id` int NULL DEFAULT NULL, + `commission_amount` decimal(10, 2) NOT NULL, + `commission_type` enum('registration','matching') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'matching', + `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + INDEX `agent_id`(`agent_id` ASC) USING BTREE, + INDEX `merchant_id`(`merchant_id` ASC) USING BTREE, + INDEX `order_id`(`order_id` ASC) USING BTREE, + CONSTRAINT `agent_commission_records_ibfk_1` FOREIGN KEY (`agent_id`) REFERENCES `regional_agents` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT, + CONSTRAINT `agent_commission_records_ibfk_2` FOREIGN KEY (`merchant_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT, + CONSTRAINT `agent_commission_records_ibfk_3` FOREIGN KEY (`order_id`) REFERENCES `matching_orders` (`id`) ON DELETE SET NULL ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 17 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for agent_merchants +-- ---------------------------- +DROP TABLE IF EXISTS `agent_merchants`; +CREATE TABLE `agent_merchants` ( + `id` int NOT NULL AUTO_INCREMENT, + `agent_id` int NOT NULL, + `merchant_id` int NOT NULL, + `registration_code_id` int NULL DEFAULT NULL, + `matching_count` int NULL DEFAULT 0, + `commission_earned` decimal(10, 2) NULL DEFAULT 0.00, + `is_qualified` tinyint(1) NULL DEFAULT 0, + `qualified_at` timestamp NULL DEFAULT NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `unique_agent_merchant`(`agent_id` ASC, `merchant_id` ASC) USING BTREE, + INDEX `merchant_id`(`merchant_id` ASC) USING BTREE, + INDEX `registration_code_id`(`registration_code_id` ASC) USING BTREE, + CONSTRAINT `agent_merchants_ibfk_1` FOREIGN KEY (`agent_id`) REFERENCES `regional_agents` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT, + CONSTRAINT `agent_merchants_ibfk_2` FOREIGN KEY (`merchant_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT, + CONSTRAINT `agent_merchants_ibfk_3` FOREIGN KEY (`registration_code_id`) REFERENCES `registration_codes` (`id`) ON DELETE SET NULL ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 32 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for agent_withdrawals +-- ---------------------------- +DROP TABLE IF EXISTS `agent_withdrawals`; +CREATE TABLE `agent_withdrawals` ( + `id` int NOT NULL AUTO_INCREMENT, + `agent_id` int NOT NULL, + `amount` decimal(10, 2) NOT NULL, + `payment_type` enum('bank','wechat','alipay','unionpay') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'bank' COMMENT '收款方式类型', + `bank_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '银行名称', + `account_number` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账号/银行账号', + `account_holder` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '持有人姓名', + `qr_code_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '收款码图片URL', + `status` enum('pending','approved','rejected','completed') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'pending', + `apply_note` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL, + `admin_note` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL, + `processed_by` int NULL DEFAULT NULL, + `processed_at` timestamp NULL DEFAULT NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `bank_account` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '银行账号(兼容旧版本)', + PRIMARY KEY (`id`) USING BTREE, + INDEX `agent_id`(`agent_id` ASC) USING BTREE, + INDEX `processed_by`(`processed_by` ASC) USING BTREE, + CONSTRAINT `agent_withdrawals_ibfk_1` FOREIGN KEY (`agent_id`) REFERENCES `regional_agents` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT, + CONSTRAINT `agent_withdrawals_ibfk_2` FOREIGN KEY (`processed_by`) REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for articles +-- ---------------------------- +DROP TABLE IF EXISTS `articles`; +CREATE TABLE `articles` ( + `id` int NOT NULL AUTO_INCREMENT, + `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL, + `author_id` int NULL DEFAULT NULL, + `status` enum('draft','published') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'draft', + `views` int NULL DEFAULT 0, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + INDEX `author_id`(`author_id` ASC) USING BTREE, + CONSTRAINT `articles_ibfk_1` FOREIGN KEY (`author_id`) REFERENCES `users` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for balance_fix_log +-- ---------------------------- +DROP TABLE IF EXISTS `balance_fix_log`; +CREATE TABLE `balance_fix_log` ( + `id` int NOT NULL AUTO_INCREMENT, + `user_id` int NOT NULL, + `amount_deducted` decimal(10, 2) NOT NULL, + `transfer_count` int NOT NULL, + `fix_reason` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_user_id`(`user_id` ASC) USING BTREE, + INDEX `idx_created_at`(`created_at` ASC) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for cart_items +-- ---------------------------- +DROP TABLE IF EXISTS `cart_items`; +CREATE TABLE `cart_items` ( + `id` int NOT NULL AUTO_INCREMENT, + `user_id` int NOT NULL, + `product_id` int NOT NULL, + `quantity` int NOT NULL DEFAULT 1, + `specification_id` int NULL DEFAULT NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `unique_user_product_spec`(`user_id` ASC, `product_id` ASC, `specification_id` ASC) USING BTREE, + INDEX `product_id`(`product_id` ASC) USING BTREE, + INDEX `specification_id`(`specification_id` ASC) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 8 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for china_regions +-- ---------------------------- +DROP TABLE IF EXISTS `china_regions`; +CREATE TABLE `china_regions` ( + `id` int NOT NULL AUTO_INCREMENT, + `code` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `parent_code` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `level` tinyint NOT NULL COMMENT '1:省 2:市 3:区', + `sort_order` int NULL DEFAULT 0, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `code`(`code` ASC) USING BTREE, + INDEX `idx_parent_code`(`parent_code` ASC) USING BTREE, + INDEX `idx_level`(`level` ASC) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 4621 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for matching_orders +-- ---------------------------- +DROP TABLE IF EXISTS `matching_orders`; +CREATE TABLE `matching_orders` ( + `id` int NOT NULL AUTO_INCREMENT, + `initiator_id` int NOT NULL, + `amount` decimal(10, 2) NOT NULL, + `status` enum('pending','matching','completed','cancelled','failed') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'pending', + `cycle_count` int NULL DEFAULT 0, + `max_cycles` int NULL DEFAULT 3, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `matching_type` enum('small','large') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'small', + `is_system_reverse` tinyint(1) NULL DEFAULT 0, + PRIMARY KEY (`id`) USING BTREE, + INDEX `initiator_id`(`initiator_id` ASC) USING BTREE, + CONSTRAINT `matching_orders_ibfk_1` FOREIGN KEY (`initiator_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 441 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for matching_records +-- ---------------------------- +DROP TABLE IF EXISTS `matching_records`; +CREATE TABLE `matching_records` ( + `id` int NOT NULL AUTO_INCREMENT, + `matching_order_id` int NOT NULL, + `user_id` int NOT NULL, + `action` enum('join','confirm','reject','complete') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, + `amount` decimal(10, 2) NULL DEFAULT NULL, + `note` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + INDEX `matching_order_id`(`matching_order_id` ASC) USING BTREE, + INDEX `user_id`(`user_id` ASC) USING BTREE, + CONSTRAINT `matching_records_ibfk_1` FOREIGN KEY (`matching_order_id`) REFERENCES `matching_orders` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT, + CONSTRAINT `matching_records_ibfk_2` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 1841 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for order_allocations +-- ---------------------------- +DROP TABLE IF EXISTS `order_allocations`; +CREATE TABLE `order_allocations` ( + `id` int NOT NULL AUTO_INCREMENT, + `matching_order_id` int NOT NULL, + `from_user_id` int NOT NULL, + `to_user_id` int NOT NULL, + `amount` decimal(10, 2) NOT NULL, + `cycle_number` int NOT NULL, + `status` enum('pending','confirmed','rejected','completed') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'pending', + `transfer_id` int NULL DEFAULT NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `confirmed_at` timestamp NULL DEFAULT NULL, + `outbound_date` date NULL DEFAULT NULL, + `return_date` date NULL DEFAULT NULL, + `can_return_after` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE, + INDEX `matching_order_id`(`matching_order_id` ASC) USING BTREE, + INDEX `from_user_id`(`from_user_id` ASC) USING BTREE, + INDEX `to_user_id`(`to_user_id` ASC) USING BTREE, + INDEX `transfer_id`(`transfer_id` ASC) USING BTREE, + CONSTRAINT `order_allocations_ibfk_1` FOREIGN KEY (`matching_order_id`) REFERENCES `matching_orders` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT, + CONSTRAINT `order_allocations_ibfk_2` FOREIGN KEY (`from_user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT, + CONSTRAINT `order_allocations_ibfk_3` FOREIGN KEY (`to_user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT, + CONSTRAINT `order_allocations_ibfk_4` FOREIGN KEY (`transfer_id`) REFERENCES `transfers` (`id`) ON DELETE SET NULL ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 1078 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for order_allocations_backup +-- ---------------------------- +DROP TABLE IF EXISTS `order_allocations_backup`; +CREATE TABLE `order_allocations_backup` ( + `id` int NOT NULL DEFAULT 0, + `matching_order_id` int NOT NULL, + `from_user_id` int NOT NULL, + `to_user_id` int NOT NULL, + `amount` decimal(10, 2) NOT NULL, + `cycle_number` int NOT NULL, + `status` enum('pending','confirmed','rejected','completed') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'pending', + `transfer_id` int NULL DEFAULT NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `confirmed_at` timestamp NULL DEFAULT NULL, + `outbound_date` date NULL DEFAULT NULL, + `return_date` date NULL DEFAULT NULL, + `can_return_after` timestamp NULL DEFAULT NULL +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for order_items +-- ---------------------------- +DROP TABLE IF EXISTS `order_items`; +CREATE TABLE `order_items` ( + `id` int NOT NULL AUTO_INCREMENT, + `order_id` int NOT NULL, + `product_id` int NOT NULL, + `spec_combination_id` int NULL DEFAULT NULL COMMENT '规格组合ID', + `quantity` int NOT NULL, + `price` int NOT NULL, + `points` int NOT NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `points_price` int NOT NULL DEFAULT 0, + `rongdou_price` int NOT NULL DEFAULT 0, + `rongdou` int NULL DEFAULT 0, + PRIMARY KEY (`id`) USING BTREE, + INDEX `order_id`(`order_id` ASC) USING BTREE, + INDEX `product_id`(`product_id` ASC) USING BTREE, + CONSTRAINT `order_items_ibfk_1` FOREIGN KEY (`order_id`) REFERENCES `orders` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT, + CONSTRAINT `order_items_ibfk_2` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 8 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for orders +-- ---------------------------- +DROP TABLE IF EXISTS `orders`; +CREATE TABLE `orders` ( + `id` int NOT NULL AUTO_INCREMENT, + `user_id` int NOT NULL, + `order_no` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, + `total_amount` int NOT NULL, + `total_points` int NOT NULL, + `total_rongdou` int NOT NULL DEFAULT 0, + `status` enum('pending','paid','shipped','delivered','cancelled','pre_order','completed') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'pending', + `address` json NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `order_no`(`order_no` ASC) USING BTREE, + INDEX `user_id`(`user_id` ASC) USING BTREE, + CONSTRAINT `orders_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 8 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for points_history +-- ---------------------------- +DROP TABLE IF EXISTS `points_history`; +CREATE TABLE `points_history` ( + `id` int NOT NULL AUTO_INCREMENT, + `user_id` int NOT NULL, + `type` enum('earn','spend','admin_adjust','refund') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, + `amount` int NOT NULL, + `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, + `order_id` int NULL DEFAULT NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + INDEX `user_id`(`user_id` ASC) USING BTREE, + INDEX `order_id`(`order_id` ASC) USING BTREE, + CONSTRAINT `points_history_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT, + CONSTRAINT `points_history_ibfk_2` FOREIGN KEY (`order_id`) REFERENCES `orders` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 1273 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for product_attributes +-- ---------------------------- +DROP TABLE IF EXISTS `product_attributes`; +CREATE TABLE `product_attributes` ( + `id` int NOT NULL AUTO_INCREMENT, + `product_id` int NOT NULL, + `attribute_key` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `attribute_value` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `sort_order` int NULL DEFAULT 0, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + INDEX `product_id`(`product_id` ASC) USING BTREE, + CONSTRAINT `product_attributes_ibfk_1` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for product_favorites +-- ---------------------------- +DROP TABLE IF EXISTS `product_favorites`; +CREATE TABLE `product_favorites` ( + `id` int NOT NULL AUTO_INCREMENT, + `user_id` int NOT NULL, + `product_id` int NOT NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `unique_user_product`(`user_id` ASC, `product_id` ASC) USING BTREE, + INDEX `product_id`(`product_id` ASC) USING BTREE, + CONSTRAINT `product_favorites_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT, + CONSTRAINT `product_favorites_ibfk_2` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for product_reviews +-- ---------------------------- +DROP TABLE IF EXISTS `product_reviews`; +CREATE TABLE `product_reviews` ( + `id` int NOT NULL AUTO_INCREMENT, + `product_id` int NOT NULL, + `user_id` int NOT NULL, + `order_id` int NOT NULL, + `rating` int NOT NULL, + `comment` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL, + `images` json NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + INDEX `product_id`(`product_id` ASC) USING BTREE, + INDEX `user_id`(`user_id` ASC) USING BTREE, + INDEX `order_id`(`order_id` ASC) USING BTREE, + CONSTRAINT `product_reviews_ibfk_1` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT, + CONSTRAINT `product_reviews_ibfk_2` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT, + CONSTRAINT `product_reviews_ibfk_3` FOREIGN KEY (`order_id`) REFERENCES `orders` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for product_spec_combinations +-- ---------------------------- +DROP TABLE IF EXISTS `product_spec_combinations`; +CREATE TABLE `product_spec_combinations` ( + `id` int NOT NULL AUTO_INCREMENT, + `product_id` int NOT NULL COMMENT '商品ID', + `combination_key` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '组合键,如:color_1-size_2-material_3', + `spec_values` json NOT NULL COMMENT '规格值组合,存储spec_value_id数组', + `price_adjustment` int NULL DEFAULT 0 COMMENT '价格调整', + `points_adjustment` int NULL DEFAULT 0 COMMENT '积分调整', + `rongdou_adjustment` int NULL DEFAULT 0 COMMENT '融豆调整', + `stock` int NULL DEFAULT 0 COMMENT '库存', + `sku_code` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'SKU编码', + `barcode` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '条形码', + `weight` decimal(8, 3) NULL DEFAULT NULL COMMENT '重量(kg)', + `volume` decimal(10, 3) NULL DEFAULT NULL COMMENT '体积(cm³)', + `status` enum('active','inactive') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'active', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `unique_product_combination`(`product_id` ASC, `combination_key` ASC) USING BTREE, + INDEX `idx_product_status`(`product_id` ASC, `status` ASC) USING BTREE, + INDEX `idx_sku_code`(`sku_code` ASC) USING BTREE, + CONSTRAINT `product_spec_combinations_ibfk_1` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for product_spec_names +-- ---------------------------- +DROP TABLE IF EXISTS `product_spec_names`; +CREATE TABLE `product_spec_names` ( + `id` int NOT NULL AUTO_INCREMENT, + `product_id` int NOT NULL COMMENT '商品ID', + `spec_name_id` int NOT NULL COMMENT '规格名称ID', + `is_required` tinyint(1) NULL DEFAULT 1 COMMENT '是否必选规格', + `sort_order` int NULL DEFAULT 0 COMMENT '排序', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `unique_product_spec_name`(`product_id` ASC, `spec_name_id` ASC) USING BTREE, + INDEX `spec_name_id`(`spec_name_id` ASC) USING BTREE, + CONSTRAINT `product_spec_names_ibfk_1` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT, + CONSTRAINT `product_spec_names_ibfk_2` FOREIGN KEY (`spec_name_id`) REFERENCES `spec_names` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for product_specifications +-- ---------------------------- +DROP TABLE IF EXISTS `product_specifications`; +CREATE TABLE `product_specifications` ( + `id` int NOT NULL AUTO_INCREMENT, + `product_id` int NOT NULL, + `spec_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `spec_value` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `price_adjustment` int NULL DEFAULT 0, + `points_adjustment` int NULL DEFAULT 0, + `rongdou_adjustment` int NULL DEFAULT 0, + `stock` int NULL DEFAULT 0, + `sku_code` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `status` enum('active','inactive') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'active', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + INDEX `product_id`(`product_id` ASC) USING BTREE, + CONSTRAINT `product_specifications_ibfk_1` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for products +-- ---------------------------- +DROP TABLE IF EXISTS `products`; +CREATE TABLE `products` ( + `id` int NOT NULL AUTO_INCREMENT, + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, + `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL, + `price` int NOT NULL, + `original_price` int NULL DEFAULT NULL, + `stock` int NULL DEFAULT 0, + `sales` int NULL DEFAULT 0, + `rating` decimal(3, 2) NULL DEFAULT 5.00, + `category` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, + `images` json NULL, + `status` enum('active','inactive') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'active', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `points_price` int NOT NULL DEFAULT 0, + `image_url` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, + `details` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL, + `rongdou_price` int NOT NULL DEFAULT 0, + `videos` json NULL, + `shop_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, + `shop_avatar` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, + `payment_methods` json NULL, + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 18 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for regional_agents +-- ---------------------------- +DROP TABLE IF EXISTS `regional_agents`; +CREATE TABLE `regional_agents` ( + `id` int NOT NULL AUTO_INCREMENT, + `user_id` int NOT NULL, + `region_id` int NOT NULL, + `agent_code` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `status` enum('pending','active','suspended','terminated') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'pending', + `commission_rate` decimal(5, 4) NULL DEFAULT 0.0500, + `total_earnings` decimal(10, 2) NULL DEFAULT 0.00, + `recruited_merchants` int NULL DEFAULT 0, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `approved_at` timestamp NULL DEFAULT NULL, + `approved_by_admin_id` int NULL DEFAULT NULL, + `withdrawn_amount` decimal(10, 2) NULL DEFAULT 0.00 COMMENT '已提现金额', + `pending_withdrawal` decimal(10, 2) NULL DEFAULT 0.00 COMMENT '待审核提现金额', + `payment_type` enum('bank','wechat','alipay','unionpay') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'bank' COMMENT '收款方式类型', + `account_number` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账号/银行账号', + `account_holder` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '持有人姓名', + `qr_code_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '收款码图片URL', + `bank_account` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '银行账号(兼容旧版本)', + `bank_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '银行名称', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `agent_code`(`agent_code` ASC) USING BTREE, + UNIQUE INDEX `unique_agent_region`(`user_id` ASC, `region_id` ASC) USING BTREE, + INDEX `region_id`(`region_id` ASC) USING BTREE, + CONSTRAINT `regional_agents_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT, + CONSTRAINT `regional_agents_ibfk_2` FOREIGN KEY (`region_id`) REFERENCES `zhejiang_regions` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for registration_codes +-- ---------------------------- +DROP TABLE IF EXISTS `registration_codes`; +CREATE TABLE `registration_codes` ( + `id` int NOT NULL AUTO_INCREMENT, + `code` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '注册码', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `expires_at` timestamp NOT NULL COMMENT '过期时间', + `used_at` timestamp NULL DEFAULT NULL COMMENT '使用时间', + `used_by_user_id` int NULL DEFAULT NULL COMMENT '使用该注册码的用户ID', + `is_used` tinyint(1) NULL DEFAULT 0 COMMENT '是否已使用', + `created_by_admin_id` int NOT NULL COMMENT '创建该注册码的管理员ID', + `agent_id` int NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `code`(`code` ASC) USING BTREE, + INDEX `idx_code`(`code` ASC) USING BTREE, + INDEX `idx_expires_at`(`expires_at` ASC) USING BTREE, + INDEX `idx_is_used`(`is_used` ASC) USING BTREE, + INDEX `used_by_user_id`(`used_by_user_id` ASC) USING BTREE, + INDEX `created_by_admin_id`(`created_by_admin_id` ASC) USING BTREE, + INDEX `fk_registration_codes_agent_id`(`agent_id` ASC) USING BTREE, + CONSTRAINT `fk_registration_codes_agent_id` FOREIGN KEY (`agent_id`) REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE RESTRICT, + CONSTRAINT `registration_codes_ibfk_1` FOREIGN KEY (`used_by_user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE RESTRICT, + CONSTRAINT `registration_codes_ibfk_2` FOREIGN KEY (`created_by_admin_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 150 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '注册码表' ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for rongdou_history +-- ---------------------------- +DROP TABLE IF EXISTS `rongdou_history`; +CREATE TABLE `rongdou_history` ( + `id` int NOT NULL AUTO_INCREMENT, + `user_id` int NOT NULL, + `type` enum('earn','spend') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `amount` int NOT NULL, + `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `order_id` int NULL DEFAULT NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + INDEX `user_id`(`user_id` ASC) USING BTREE, + INDEX `order_id`(`order_id` ASC) USING BTREE, + CONSTRAINT `rongdou_history_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT, + CONSTRAINT `rongdou_history_ibfk_2` FOREIGN KEY (`order_id`) REFERENCES `orders` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for spec_names +-- ---------------------------- +DROP TABLE IF EXISTS `spec_names`; +CREATE TABLE `spec_names` ( + `id` int NOT NULL AUTO_INCREMENT, + `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '规格名称,如:颜色、尺寸、材质', + `display_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '显示名称', + `sort_order` int NULL DEFAULT 0 COMMENT '排序', + `status` enum('active','inactive') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'active', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `unique_name`(`name` ASC) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for spec_values +-- ---------------------------- +DROP TABLE IF EXISTS `spec_values`; +CREATE TABLE `spec_values` ( + `id` int NOT NULL AUTO_INCREMENT, + `spec_name_id` int NOT NULL COMMENT '规格名称ID', + `value` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '规格值,如:红色、XL、棉质', + `display_value` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '显示值', + `color_code` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '颜色代码(仅颜色规格使用)', + `image_url` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '规格图片', + `sort_order` int NULL DEFAULT 0 COMMENT '排序', + `status` enum('active','inactive') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'active', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `unique_spec_value`(`spec_name_id` ASC, `value` ASC) USING BTREE, + CONSTRAINT `spec_values_ibfk_1` FOREIGN KEY (`spec_name_id`) REFERENCES `spec_names` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for system_settings +-- ---------------------------- +DROP TABLE IF EXISTS `system_settings`; +CREATE TABLE `system_settings` ( + `id` int NOT NULL AUTO_INCREMENT, + `setting_key` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, + `setting_value` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL, + `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `setting_key`(`setting_key` ASC) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 77 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for test_users +-- ---------------------------- +DROP TABLE IF EXISTS `test_users`; +CREATE TABLE `test_users` ( + `id` int NOT NULL AUTO_INCREMENT, + `username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `email` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for transfer_confirmations +-- ---------------------------- +DROP TABLE IF EXISTS `transfer_confirmations`; +CREATE TABLE `transfer_confirmations` ( + `id` int NOT NULL AUTO_INCREMENT, + `transfer_id` int NOT NULL, + `confirmer_id` int NOT NULL, + `action` enum('confirm','reject') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, + `note` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + INDEX `transfer_id`(`transfer_id` ASC) USING BTREE, + INDEX `confirmer_id`(`confirmer_id` ASC) USING BTREE, + CONSTRAINT `transfer_confirmations_ibfk_1` FOREIGN KEY (`transfer_id`) REFERENCES `transfers` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT, + CONSTRAINT `transfer_confirmations_ibfk_2` FOREIGN KEY (`confirmer_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for transfers +-- ---------------------------- +DROP TABLE IF EXISTS `transfers`; +CREATE TABLE `transfers` ( + `id` int NOT NULL AUTO_INCREMENT, + `from_user_id` int NULL DEFAULT NULL, + `to_user_id` int NOT NULL, + `amount` decimal(10, 2) NOT NULL, + `transfer_type` enum('initial','return','user_to_user','system_to_user','user_to_system') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'user_to_user', + `status` enum('pending','confirmed','rejected','received','not_received','cancelled') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'pending', + `voucher_url` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, + `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL, + `batch_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `deadline_at` timestamp NULL DEFAULT NULL COMMENT '转账截止时间', + `is_overdue` tinyint(1) NULL DEFAULT 0 COMMENT '是否超时', + `overdue_at` timestamp NULL DEFAULT NULL COMMENT '超时时间', + `is_bad_debt` tinyint(1) NULL DEFAULT 0, + `confirmed_at` timestamp NULL DEFAULT NULL, + `admin_note` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL, + `admin_modified_at` timestamp NULL DEFAULT NULL, + `admin_modified_by` int NULL DEFAULT NULL, + `source_type` enum('manual','allocation','system') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'manual' COMMENT '转账来源类型', + `matching_order_id` int NULL DEFAULT NULL, + `cycle_number` int NULL DEFAULT NULL, + `outbound_date` date NULL DEFAULT NULL, + `return_date` date NULL DEFAULT NULL, + `can_return_after` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE, + INDEX `from_user_id`(`from_user_id` ASC) USING BTREE, + INDEX `to_user_id`(`to_user_id` ASC) USING BTREE, + CONSTRAINT `transfers_ibfk_1` FOREIGN KEY (`from_user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT, + CONSTRAINT `transfers_ibfk_2` FOREIGN KEY (`to_user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 1529 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for user_addresses +-- ---------------------------- +DROP TABLE IF EXISTS `user_addresses`; +CREATE TABLE `user_addresses` ( + `id` int NOT NULL AUTO_INCREMENT, + `user_id` int NOT NULL, + `receiver_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `receiver_phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `province` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `city` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `district` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `detailed_address` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `postal_code` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + `label` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '家', + `is_default` tinyint(1) NULL DEFAULT 0, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + INDEX `user_id`(`user_id` ASC) USING BTREE, + CONSTRAINT `user_addresses_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for user_matching_pool +-- ---------------------------- +DROP TABLE IF EXISTS `user_matching_pool`; +CREATE TABLE `user_matching_pool` ( + `id` int NOT NULL AUTO_INCREMENT, + `user_id` int NOT NULL, + `available_amount` decimal(10, 2) NULL DEFAULT 0.00, + `is_active` tinyint(1) NULL DEFAULT 1, + `last_matched_at` timestamp NULL DEFAULT NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `unique_user`(`user_id` ASC) USING BTREE, + CONSTRAINT `user_matching_pool_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 61 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for users +-- ---------------------------- +DROP TABLE IF EXISTS `users`; +CREATE TABLE `users` ( + `id` int NOT NULL AUTO_INCREMENT, + `username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, + `email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, + `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, + `role` enum('user','admin') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'user', + `avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `points` int NULL DEFAULT 0, + `real_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, + `id_card` varchar(18) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, + `wechat_qr` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, + `alipay_qr` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, + `bank_card` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, + `unionpay_qr` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, + `phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, + `is_system_account` tinyint(1) NULL DEFAULT 0, + `completed_withdrawals` int NULL DEFAULT 0, + `balance` decimal(10, 2) NULL DEFAULT 0.00, + `is_risk_user` tinyint(1) NULL DEFAULT 0 COMMENT '是否为风险用户', + `is_blacklisted` tinyint(1) NULL DEFAULT 0 COMMENT '是否被拉黑', + `risk_reason` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '风险原因', + `blacklist_reason` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '拉黑原因', + `blacklisted_at` timestamp NULL DEFAULT NULL COMMENT '拉黑时间', + `business_license` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, + `id_card_front` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, + `id_card_back` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, + `audit_status` enum('pending','approved','rejected') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'pending', + `audit_note` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL, + `audited_by` int NULL DEFAULT NULL, + `audited_at` timestamp NULL DEFAULT NULL, + `city` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL, + `district_id` int NULL DEFAULT NULL, + `isdistribute` tinyint NULL DEFAULT 1, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `username`(`username` ASC) USING BTREE, + UNIQUE INDEX `email`(`email` ASC) USING BTREE, + UNIQUE INDEX `phone`(`phone` ASC) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 9788 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for zhejiang_regions +-- ---------------------------- +DROP TABLE IF EXISTS `zhejiang_regions`; +CREATE TABLE `zhejiang_regions` ( + `id` int NOT NULL AUTO_INCREMENT, + `city_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `district_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `region_code` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, + `is_available` tinyint(1) NULL DEFAULT 1, + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `region_code`(`region_code` ASC) USING BTREE, + UNIQUE INDEX `unique_region`(`city_name` ASC, `district_name` ASC) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 23234 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; + +SET FOREIGN_KEY_CHECKS = 1;