From 691789d5d32b7ac6e6395f282c2aada3fe5e8088 Mon Sep 17 00:00:00 2001 From: sunzhuangzhuang <961120009@qq.com> Date: Thu, 28 Aug 2025 09:14:56 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E5=95=86=E5=9F=8E=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- API-DOCS-README.md | 123 + admin | 2 +- api-docs/swagger.json | 5221 +++++++++++++++++++++++++++++++++++ apifox-sync.js | 27 + config/database-init.js | 226 +- database.js | 16 +- export-swagger.js | 25 + package-lock.json | 307 +- package.json | 2 + routes/address-labels.js | 180 ++ routes/addresses.js | 571 ++++ routes/auth.js | 200 +- routes/captcha.js | 106 +- routes/matching.js | 415 ++- routes/matchingAdmin.js | 320 ++- routes/orders.js | 407 ++- routes/points.js | 361 ++- routes/products.js | 784 +++++- routes/regions.js | 359 ++- routes/riskManagement.js | 248 +- routes/sms.js | 105 +- routes/transfers.js | 401 ++- routes/upload.js | 291 +- routes/users.js | 181 +- server.js | 11 +- services/matchingService.js | 203 +- services/transferService.js | 1 + swagger.js | 41 + 28 files changed, 10842 insertions(+), 292 deletions(-) create mode 100644 API-DOCS-README.md create mode 100644 api-docs/swagger.json create mode 100644 apifox-sync.js create mode 100644 export-swagger.js create mode 100644 routes/address-labels.js create mode 100644 routes/addresses.js create mode 100644 swagger.js 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/admin b/admin index 4c6ab13..fd64419 160000 --- a/admin +++ b/admin @@ -1 +1 @@ -Subproject commit 4c6ab13210df9fa2c5a7ffcc19d9abc4b25b28ba +Subproject commit fd644194bd1923250364dab93c951d676db85a17 diff --git a/api-docs/swagger.json b/api-docs/swagger.json new file mode 100644 index 0000000..be47090 --- /dev/null +++ b/api-docs/swagger.json @@ -0,0 +1,5221 @@ +{ + "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": { + "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": "密码" + } + } + }, + "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" + } + } + }, + "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": "发送方用户余额" + } + } + }, + "Order": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "订单ID" + }, + "order_no": { + "type": "string", + "description": "订单编号" + }, + "user_id": { + "type": "integer", + "description": "用户ID" + }, + "total_amount": { + "type": "number", + "description": "订单总金额" + }, + "total_points": { + "type": "number", + "description": "订单总积分" + }, + "status": { + "type": "string", + "enum": [ + "pending", + "shipped", + "completed", + "cancelled" + ], + "description": "订单状态" + }, + "address": { + "type": "string", + "description": "收货地址" + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "创建时间" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "description": "更新时间" + }, + "username": { + "type": "string", + "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": "创建时间" + } + } + }, + "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": "积分价格" + }, + "stock": { + "type": "integer", + "description": "库存数量" + }, + "image_url": { + "type": "string", + "description": "商品图片URL" + }, + "description": { + "type": "string", + "description": "商品描述" + }, + "status": { + "type": "string", + "description": "商品状态", + "enum": [ + "active", + "inactive" + ] + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "创建时间" + }, + "updated_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": "验证码" + } + } + }, + "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": "总页数" + } + } + }, + "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": "是否为系统账户" + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "创建时间" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "description": "更新时间" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "paths": { + "/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/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": { + "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", + "enum": [ + "pending", + "shipped", + "completed", + "cancelled" + ] + }, + "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": "服务器错误" + } + } + }, + "post": { + "summary": "创建订单", + "tags": [ + "Orders" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "product_id": { + "type": "integer", + "description": "商品ID" + }, + "quantity": { + "type": "integer", + "description": "购买数量" + }, + "shipping_address": { + "type": "string", + "description": "收货地址" + } + }, + "required": [ + "product_id", + "quantity", + "shipping_address" + ] + } + } + } + }, + "responses": { + "201": { + "description": "订单创建成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "message": { + "type": "string" + }, + "data": { + "type": "object", + "properties": { + "orderId": { + "type": "integer" + }, + "orderNumber": { + "type": "string" + }, + "pointsUsed": { + "type": "integer" + } + } + } + } + } + } + } + }, + "400": { + "description": "参数错误或积分不足" + }, + "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": "服务器错误" + } + } + } + }, + "/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/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": { + "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": "object", + "properties": { + "categories": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + } + } + } + } + } + }, + "/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" + }, + "data": { + "$ref": "#/components/schemas/Product" + } + } + } + } + } + }, + "404": { + "description": "商品不存在" + } + } + } + }, + "/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": "服务器错误" + } + } + } + }, + "/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": "服务器错误" + } + } + } + } + }, + "tags": [ + { + "name": "Authentication", + "description": "用户认证API" + }, + { + "name": "Captcha", + "description": "验证码API" + }, + { + "name": "Matching", + "description": "匹配订单相关接口" + }, + { + "name": "MatchingAdmin", + "description": "匹配订单管理员相关接口" + }, + { + "name": "Orders", + "description": "订单管理相关接口" + }, + { + "name": "Points", + "description": "积分管理相关接口" + }, + { + "name": "Products", + "description": "商品管理API" + }, + { + "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/config/database-init.js b/config/database-init.js index 8877791..ab090cc 100644 --- a/config/database-init.js +++ b/config/database-init.js @@ -84,6 +84,7 @@ async function createTables() { description TEXT, price INT NOT NULL, points_price INT NOT NULL, + rongdou_price INT NOT NULL DEFAULT 0, original_price INT, stock INT DEFAULT 0, sales INT DEFAULT 0, @@ -91,7 +92,11 @@ async function createTables() { category VARCHAR(100), image_url VARCHAR(500), images JSON, + videos JSON, details TEXT, + shop_name VARCHAR(255), + shop_avatar VARCHAR(500), + payment_methods JSON, status ENUM('active', 'inactive') DEFAULT 'active', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP @@ -208,6 +213,106 @@ async function createTables() { ) `); + // 商品规格表 + await getDB().execute(` + CREATE TABLE IF NOT EXISTS product_specifications ( + id INT AUTO_INCREMENT PRIMARY KEY, + product_id INT NOT NULL, + spec_name VARCHAR(100) NOT NULL, + spec_value VARCHAR(100) NOT NULL, + price_adjustment INT DEFAULT 0, + points_adjustment INT DEFAULT 0, + rongdou_adjustment INT DEFAULT 0, + stock INT DEFAULT 0, + sku_code VARCHAR(100), + status ENUM('active', 'inactive') DEFAULT 'active', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE + ) + `); + + // 商品属性表 + await getDB().execute(` + CREATE TABLE IF NOT EXISTS product_attributes ( + id INT AUTO_INCREMENT PRIMARY KEY, + product_id INT NOT NULL, + attribute_key VARCHAR(100) NOT NULL, + attribute_value TEXT NOT NULL, + sort_order INT DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE + ) + `); + + // 商品收藏表 + await getDB().execute(` + CREATE TABLE IF NOT EXISTS product_favorites ( + id INT AUTO_INCREMENT PRIMARY KEY, + user_id INT NOT NULL, + product_id INT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE, + UNIQUE KEY unique_user_product (user_id, product_id) + ) + `); + + // 用户收货地址表 + await getDB().execute(` + CREATE TABLE IF NOT EXISTS user_addresses ( + id INT AUTO_INCREMENT PRIMARY KEY, + user_id INT NOT NULL, + recipient_name VARCHAR(100) NOT NULL, + phone VARCHAR(20) NOT NULL, + province_code VARCHAR(20), + province_name VARCHAR(50) NOT NULL, + city_code VARCHAR(20), + city_name VARCHAR(50) NOT NULL, + district_code VARCHAR(20), + district_name VARCHAR(50) NOT NULL, + detailed_address TEXT NOT NULL, + postal_code VARCHAR(10), + label_id INT, + is_default BOOLEAN DEFAULT FALSE, + deleted_at TIMESTAMP NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (label_id) REFERENCES address_labels(id) ON DELETE SET NULL + ) + `); + + // 地址标签表 + await getDB().execute(` + CREATE TABLE IF NOT EXISTS address_labels ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(50) NOT NULL, + color VARCHAR(20) DEFAULT '#1890ff', + user_id INT NULL COMMENT '用户ID,NULL表示系统标签', + is_system BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE + ) + `); + + // 全国省市区表 + await getDB().execute(` + CREATE TABLE IF NOT EXISTS china_regions ( + id INT AUTO_INCREMENT PRIMARY KEY, + code VARCHAR(20) NOT NULL UNIQUE, + name VARCHAR(100) NOT NULL, + parent_code VARCHAR(20), + level TINYINT NOT NULL COMMENT '1:省 2:市 3:区', + sort_order INT DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + INDEX idx_parent_code (parent_code), + INDEX idx_level (level) + ) + `); + // 匹配订单表 await getDB().execute(` CREATE TABLE IF NOT EXISTS matching_orders ( @@ -436,8 +541,14 @@ async function addMissingFields() { // 为现有的products表添加字段 const productFields = [ { name: 'points_price', sql: 'ALTER TABLE products ADD COLUMN points_price INT NOT NULL DEFAULT 0' }, + { name: 'rongdou_price', sql: 'ALTER TABLE products ADD COLUMN rongdou_price INT NOT NULL DEFAULT 0' }, { name: 'image_url', sql: 'ALTER TABLE products ADD COLUMN image_url VARCHAR(500)' }, - { name: 'details', sql: 'ALTER TABLE products ADD COLUMN details TEXT' } + { name: 'images', sql: 'ALTER TABLE products ADD COLUMN images JSON' }, + { name: 'videos', sql: 'ALTER TABLE products ADD COLUMN videos JSON' }, + { name: 'details', sql: 'ALTER TABLE products ADD COLUMN details TEXT' }, + { name: 'shop_name', sql: 'ALTER TABLE products ADD COLUMN shop_name VARCHAR(255)' }, + { name: 'shop_avatar', sql: 'ALTER TABLE products ADD COLUMN shop_avatar VARCHAR(500)' }, + { name: 'payment_methods', sql: 'ALTER TABLE products ADD COLUMN payment_methods JSON' } ]; for (const field of productFields) { @@ -659,6 +770,12 @@ async function createDefaultData() { // 初始化浙江省区域数据 await initializeZhejiangRegions(); + + // 初始化默认地址标签 + await initializeDefaultAddressLabels(); + + // 初始化全国省市区数据 + await initializeChinaRegions(); } /** @@ -781,10 +898,115 @@ async function initializeZhejiangRegions() { } } +/** + * 初始化默认地址标签 + */ +async function initializeDefaultAddressLabels() { + const defaultLabels = [ + { name: '家', color: '#52c41a' }, + { name: '公司', color: '#1890ff' }, + { name: '学校', color: '#722ed1' } + ]; + + for (const label of defaultLabels) { + try { + await getDB().execute(` + INSERT IGNORE INTO address_labels (name, color, user_id, is_system) + VALUES (?, ?, NULL, TRUE) + `, [label.name, label.color]); + } catch (error) { + console.log(`默认标签${label.name}创建失败:`, error.message); + } + } + console.log('默认地址标签初始化完成'); +} + +/** + * 初始化全国省市区数据 + */ +async function initializeChinaRegions() { + const regions = [ + // 省级 + { code: '110000', name: '北京市', parent_code: null, level: 1 }, + { code: '120000', name: '天津市', parent_code: null, level: 1 }, + { code: '130000', name: '河北省', parent_code: null, level: 1 }, + { code: '140000', name: '山西省', parent_code: null, level: 1 }, + { code: '150000', name: '内蒙古自治区', parent_code: null, level: 1 }, + { code: '210000', name: '辽宁省', parent_code: null, level: 1 }, + { code: '220000', name: '吉林省', parent_code: null, level: 1 }, + { code: '230000', name: '黑龙江省', parent_code: null, level: 1 }, + { code: '310000', name: '上海市', parent_code: null, level: 1 }, + { code: '320000', name: '江苏省', parent_code: null, level: 1 }, + { code: '330000', name: '浙江省', parent_code: null, level: 1 }, + { code: '340000', name: '安徽省', parent_code: null, level: 1 }, + { code: '350000', name: '福建省', parent_code: null, level: 1 }, + { code: '360000', name: '江西省', parent_code: null, level: 1 }, + { code: '370000', name: '山东省', parent_code: null, level: 1 }, + { code: '410000', name: '河南省', parent_code: null, level: 1 }, + { code: '420000', name: '湖北省', parent_code: null, level: 1 }, + { code: '430000', name: '湖南省', parent_code: null, level: 1 }, + { code: '440000', name: '广东省', parent_code: null, level: 1 }, + { code: '450000', name: '广西壮族自治区', parent_code: null, level: 1 }, + { code: '460000', name: '海南省', parent_code: null, level: 1 }, + { code: '500000', name: '重庆市', parent_code: null, level: 1 }, + { code: '510000', name: '四川省', parent_code: null, level: 1 }, + { code: '520000', name: '贵州省', parent_code: null, level: 1 }, + { code: '530000', name: '云南省', parent_code: null, level: 1 }, + { code: '540000', name: '西藏自治区', parent_code: null, level: 1 }, + { code: '610000', name: '陕西省', parent_code: null, level: 1 }, + { code: '620000', name: '甘肃省', parent_code: null, level: 1 }, + { code: '630000', name: '青海省', parent_code: null, level: 1 }, + { code: '640000', name: '宁夏回族自治区', parent_code: null, level: 1 }, + { code: '650000', name: '新疆维吾尔自治区', parent_code: null, level: 1 }, + + // 浙江省市级 + { code: '330100', name: '杭州市', parent_code: '330000', level: 2 }, + { code: '330200', name: '宁波市', parent_code: '330000', level: 2 }, + { code: '330300', name: '温州市', parent_code: '330000', level: 2 }, + { code: '330400', name: '嘉兴市', parent_code: '330000', level: 2 }, + { code: '330500', name: '湖州市', parent_code: '330000', level: 2 }, + { code: '330600', name: '绍兴市', parent_code: '330000', level: 2 }, + { code: '330700', name: '金华市', parent_code: '330000', level: 2 }, + { code: '330800', name: '衢州市', parent_code: '330000', level: 2 }, + { code: '330900', name: '舟山市', parent_code: '330000', level: 2 }, + { code: '331000', name: '台州市', parent_code: '330000', level: 2 }, + { code: '331100', name: '丽水市', parent_code: '330000', level: 2 }, + + // 杭州市区级 + { code: '330102', name: '上城区', parent_code: '330100', level: 3 }, + { code: '330105', name: '拱墅区', parent_code: '330100', level: 3 }, + { code: '330106', name: '西湖区', parent_code: '330100', level: 3 }, + { code: '330108', name: '滨江区', parent_code: '330100', level: 3 }, + { code: '330109', name: '萧山区', parent_code: '330100', level: 3 }, + { code: '330110', name: '余杭区', parent_code: '330100', level: 3 }, + { code: '330111', name: '富阳区', parent_code: '330100', level: 3 }, + { code: '330112', name: '临安区', parent_code: '330100', level: 3 }, + { code: '330113', name: '临平区', parent_code: '330100', level: 3 }, + { code: '330114', name: '钱塘区', parent_code: '330100', level: 3 }, + { code: '330122', name: '桐庐县', parent_code: '330100', level: 3 }, + { code: '330127', name: '淳安县', parent_code: '330100', level: 3 }, + { code: '330182', name: '建德市', parent_code: '330100', level: 3 } + ]; + + for (const region of regions) { + try { + await getDB().execute(` + INSERT IGNORE INTO china_regions (code, name, parent_code, level) + VALUES (?, ?, ?, ?) + `, [region.code, region.name, region.parent_code, region.level]); + } catch (error) { + console.log(`区域${region.name}创建失败:`, error.message); + } + } + console.log('全国省市区数据初始化完成'); +} + module.exports = { initDatabase, createTables, addMissingFields, createDefaultData, - initializeZhejiangRegions + initializeZhejiangRegions, + initializeDefaultAddressLabels, + initializeChinaRegions }; \ No newline at end of file diff --git a/database.js b/database.js index 50a1445..fc6805d 100644 --- a/database.js +++ b/database.js @@ -2,14 +2,14 @@ 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', + // 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', // 连接池配置 connectionLimit: 20, // 连接池最大连接数 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/package-lock.json b/package-lock.json index b8949d6..58feb8b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,8 @@ "mysql2": "^3.14.3", "node-cron": "^4.2.1", "qrcode": "^1.5.4", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.1", "winston": "^3.17.0" }, "devDependencies": { @@ -220,6 +222,50 @@ "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", @@ -279,6 +325,19 @@ "@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", @@ -300,6 +359,12 @@ "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", @@ -378,6 +443,12 @@ "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", @@ -420,7 +491,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, "license": "MIT" }, "node_modules/bcryptjs": { @@ -470,7 +540,6 @@ "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -551,6 +620,12 @@ "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", @@ -705,11 +780,19 @@ "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==", - "dev": true, "license": "MIT" }, "node_modules/concat-stream": { @@ -894,6 +977,18 @@ "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/dotenv": { "version": "17.2.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.1.tgz", @@ -1017,6 +1112,15 @@ "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", @@ -1210,6 +1314,12 @@ "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", @@ -1289,6 +1399,27 @@ "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", @@ -1449,6 +1580,17 @@ "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", @@ -1568,6 +1710,18 @@ "@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", @@ -1659,6 +1813,13 @@ "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", @@ -1671,6 +1832,13 @@ "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", @@ -1695,6 +1863,12 @@ "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", @@ -1827,7 +2001,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -2085,6 +2258,15 @@ "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", @@ -2094,6 +2276,13 @@ "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", @@ -2148,6 +2337,15 @@ "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", @@ -2724,6 +2922,62 @@ "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", @@ -2950,6 +3204,12 @@ "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", @@ -2991,6 +3251,15 @@ "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", @@ -3019,6 +3288,36 @@ "engines": { "node": ">=12" } + }, + "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 index 3969491..50b873f 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,8 @@ "mysql2": "^3.14.3", "node-cron": "^4.2.1", "qrcode": "^1.5.4", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.1", "winston": "^3.17.0" }, "devDependencies": { 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..41dd2f5 --- /dev/null +++ b/routes/addresses.js @@ -0,0 +1,571 @@ +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 + FROM user_addresses ua + LEFT JOIN address_labels al ON ua.label_id = al.id + WHERE ua.user_id = ? AND ua.deleted_at IS NULL + 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, + province_name, + city_code, + city_name, + district_code, + district_name, + detailed_address, + postal_code, + label_id, + 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 = ? AND deleted_at IS NULL', + [userId] + ); + } + + const [result] = await getDB().execute( + `INSERT INTO user_addresses ( + user_id, recipient_name, phone, province_code, province_name, city_code, city_name, + district_code, district_name, detailed_address, postal_code, label_id, is_default, created_at, updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())`, + [ + userId, recipient_name, phone, province_code, province_name, city_code, city_name, + district_code, district_name, detailed_address, postal_code, label_id, 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, + province_name, + city_code, + city_name, + district_code, + district_name, + detailed_address, + postal_code, + label_id, + is_default + } = req.body; + + // 检查地址是否存在且属于当前用户 + 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: '收货地址不存在' }); + } + + // 如果设置为默认地址,先取消其他默认地址 + if (is_default) { + await getDB().execute( + 'UPDATE user_addresses SET is_default = false WHERE user_id = ? AND id != ? AND deleted_at IS NULL', + [userId, addressId] + ); + } + + const [result] = await getDB().execute( + `UPDATE user_addresses SET + recipient_name = ?, phone = ?, province_code = ?, province_name = ?, + city_code = ?, city_name = ?, district_code = ?, district_name = ?, + detailed_address = ?, postal_code = ?, label_id = ?, is_default = ?, updated_at = NOW() + WHERE id = ? AND user_id = ?`, + [ + recipient_name, phone, province_code, province_name, city_code, city_name, + district_code, district_name, detailed_address, postal_code, label_id, 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( + 'UPDATE user_addresses SET deleted_at = NOW() WHERE id = ? AND user_id = ? AND deleted_at IS NULL', + [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/auth.js b/routes/auth.js index 2cdfe31..9d07712 100644 --- a/routes/auth.js +++ b/routes/auth.js @@ -6,9 +6,117 @@ const { getDB } = require('../database'); const router = express.Router(); const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key'; + /** - * 用户注册 - * 需要提供有效的激活码才能注册 + * @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 + * /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: 服务器错误 */ router.post('/register', async (req, res) => { try { @@ -175,7 +283,55 @@ router.post('/register', async (req, res) => { } }); -// 用户登录 +/** + * @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: 服务器错误 + */ router.post('/login', async (req, res) => { try { const db = getDB(); @@ -185,9 +341,41 @@ router.post('/login', async (req, res) => { return res.status(400).json({ success: false, message: '用户名和密码不能为空' }); } - // if (!captchaId || !captchaText) { - // 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 接口验证过,这里不再重复验证 diff --git a/routes/captcha.js b/routes/captcha.js index 4c6ac12..52f5985 100644 --- a/routes/captcha.js +++ b/routes/captcha.js @@ -2,8 +2,15 @@ const express = require('express'); const crypto = require('crypto'); const router = express.Router(); +/** + * @swagger + * tags: + * name: Captcha + * description: 验证码API + */ + // 内存存储验证码(生产环境建议使用Redis) -const captchaStore = new Map(); + /** * 生成随机验证码字符串 @@ -101,7 +108,33 @@ function generateCaptchaSVG(text) { } /** - * 生成验证码接口 + * @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: 服务器错误 */ router.get('/generate', (req, res) => { try { @@ -112,7 +145,7 @@ router.get('/generate', (req, res) => { const captchaId = crypto.randomUUID(); // 存储验证码(5分钟过期) - captchaStore.set(captchaId, { + global.captchaStore.set(captchaId, { text: captchaText.toLowerCase(), // 存储小写用于比较 expires: Date.now() + 5 * 60 * 1000 // 5分钟过期 }); @@ -137,9 +170,56 @@ router.get('/generate', (req, res) => { }); /** - * 验证验证码接口 - * @param {string} captchaId 验证码ID - * @param {string} captchaText 用户输入的验证码 + * @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: 服务器错误 */ router.post('/verify', (req, res) => { try { @@ -153,7 +233,7 @@ router.post('/verify', (req, res) => { } // 获取存储的验证码 - const storedCaptcha = captchaStore.get(captchaId); + const storedCaptcha = global.captchaStore.get(captchaId); if (!storedCaptcha) { return res.status(400).json({ @@ -164,7 +244,7 @@ router.post('/verify', (req, res) => { // 检查是否过期 if (Date.now() > storedCaptcha.expires) { - captchaStore.delete(captchaId); + global.captchaStore.delete(captchaId); return res.status(400).json({ success: false, message: '验证码已过期' @@ -175,7 +255,7 @@ router.post('/verify', (req, res) => { const isValid = storedCaptcha.text === captchaText.toLowerCase(); // 验证后删除验证码(无论成功失败) - captchaStore.delete(captchaId); + global.captchaStore.delete(captchaId); if (isValid) { res.json({ @@ -200,9 +280,9 @@ router.post('/verify', (req, res) => { // 清理过期验证码的定时任务 setInterval(() => { const now = Date.now(); - for (const [id, captcha] of captchaStore.entries()) { + for (const [id, captcha] of global.captchaStore.entries()) { if (now > captcha.expires) { - captchaStore.delete(id); + global.captchaStore.delete(id); } } }, 60 * 1000); // 每分钟清理一次 @@ -210,7 +290,7 @@ setInterval(() => { // 导出验证函数供其他模块使用 module.exports = router; module.exports.verifyCaptcha = (captchaId, captchaText) => { - const captcha = captchaStore.get(captchaId); + const captcha = global.captchaStore.get(captchaId); if (!captcha) { return false; // 验证码不存在或已过期 } @@ -220,6 +300,6 @@ module.exports.verifyCaptcha = (captchaId, captchaText) => { } // 验证成功后删除验证码(一次性使用) - captchaStore.delete(captchaId); + global.captchaStore.delete(captchaId); return true; }; \ No newline at end of file diff --git a/routes/matching.js b/routes/matching.js index ab40d08..9d1222d 100644 --- a/routes/matching.js +++ b/routes/matching.js @@ -4,7 +4,126 @@ const { getDB } = require('../database'); const matchingService = require('../services/matchingService'); const { auth } = require('../middleware/auth'); -// 创建匹配订单 +/** + * @swagger + * tags: + * name: Matching + * 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: 服务器错误 + */ router.post('/create', auth, async (req, res) => { try { console.log('匹配订单创建请求 - 用户ID:', req.user.id); @@ -89,7 +208,46 @@ router.post('/create', auth, async (req, res) => { } }); -// 获取用户的匹配订单列表 +/** + * @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: 服务器错误 + */ router.get('/my-orders', auth, async (req, res) => { try { const userId = req.user.id; @@ -109,7 +267,33 @@ router.get('/my-orders', auth, async (req, res) => { } }); -// 获取用户待处理的分配 +/** + * @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; @@ -127,7 +311,42 @@ router.get('/pending-allocations', auth, async (req, res) => { } }); -// 获取分配详情 +/** + * @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(); @@ -173,7 +392,63 @@ router.get('/allocation/:id', auth, async (req, res) => { } }); -// 确认分配(创建转账) +/** + * @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; @@ -209,7 +484,49 @@ router.post('/confirm-allocation/:allocationId', auth, async (req, res) => { } }); -// 拒绝分配 +/** + * @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; @@ -264,7 +581,53 @@ router.post('/reject-allocation/:allocationId', auth, async (req, res) => { } }); -// 获取匹配订单详情 +/** + * @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; @@ -339,7 +702,43 @@ router.get('/order/:orderId', auth, async (req, res) => { } }); -// 获取匹配统计信息 +/** + * @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; diff --git a/routes/matchingAdmin.js b/routes/matchingAdmin.js index f6e4fbc..ec5e986 100644 --- a/routes/matchingAdmin.js +++ b/routes/matchingAdmin.js @@ -6,7 +6,106 @@ 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; @@ -72,7 +171,57 @@ router.get('/unreasonable-matches', auth, adminAuth, async (req, res) => { } }); -// 修复不合理的匹配记录 +/** + * @swagger + * /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: 服务器错误 + */ router.post('/fix-unreasonable-match/:allocationId', auth, adminAuth, async (req, res) => { try { const { allocationId } = req.params; @@ -180,7 +329,54 @@ router.post('/fix-unreasonable-match/:allocationId', auth, adminAuth, async (req } }); -// 获取匹配统计信息 +/** + * @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 { @@ -224,7 +420,47 @@ router.get('/matching-stats', auth, adminAuth, async (req, res) => { } }); -// 批量修复所有不合理匹配 +/** + * @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; @@ -312,7 +548,42 @@ router.post('/fix-all-unreasonable', auth, adminAuth, async (req, res) => { } }); -// 确认分配 +/** + * @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; @@ -405,8 +676,43 @@ router.post('/confirm-allocation/:allocationId', auth, adminAuth, async (req, re } }); - // 取消分配 - router.post('/cancel-allocation/:allocationId', auth, adminAuth, async (req, res) => { + /** + * @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; diff --git a/routes/orders.js b/routes/orders.js index e1a2f75..e969574 100644 --- a/routes/orders.js +++ b/routes/orders.js @@ -4,6 +4,55 @@ const { auth, adminAuth } = require('../middleware/auth'); const router = express.Router(); +/** + * @swagger + * tags: + * name: Orders + * description: 订单管理相关接口 + */ + +/** + * @swagger + * components: + * schemas: + * Order: + * type: object + * properties: + * id: + * type: integer + * description: 订单ID + * order_no: + * type: string + * description: 订单编号 + * user_id: + * type: integer + * description: 用户ID + * total_amount: + * type: number + * description: 订单总金额 + * total_points: + * type: number + * description: 订单总积分 + * status: + * type: string + * enum: [pending, shipped, completed, cancelled] + * description: 订单状态 + * address: + * type: string + * description: 收货地址 + * created_at: + * type: string + * format: date-time + * description: 创建时间 + * updated_at: + * type: string + * format: date-time + * description: 更新时间 + * username: + * type: string + * description: 用户名 + */ + // 生成订单号 function generateOrderNo() { const timestamp = Date.now().toString(); @@ -11,7 +60,93 @@ function generateOrderNo() { return `ORD${timestamp}${random}`.toUpperCase(); } -// 获取订单列表 +/** + * @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 + * enum: [pending, shipped, completed, cancelled] + * 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: 服务器错误 + */ router.get('/', auth, async (req, res) => { try { const { page = 1, limit = 10, search = '', orderNumber = '', username = '', status = '', startDate = '', endDate = '' } = req.query; @@ -109,7 +244,43 @@ router.get('/', auth, async (req, res) => { } }); -// 获取单个订单详情 +/** + * @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: 服务器错误 + */ router.get('/:id', auth, async (req, res) => { try { const { id } = req.params; @@ -150,7 +321,64 @@ router.get('/:id', auth, async (req, res) => { } }); -// 创建订单 +/** + * @swagger + * /api/orders: + * post: + * summary: 创建订单 + * tags: [Orders] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * product_id: + * type: integer + * description: 商品ID + * quantity: + * type: integer + * description: 购买数量 + * shipping_address: + * type: string + * description: 收货地址 + * required: + * - product_id + * - quantity + * - shipping_address + * responses: + * 201: + * description: 订单创建成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * message: + * type: string + * data: + * type: object + * properties: + * orderId: + * type: integer + * orderNumber: + * type: string + * pointsUsed: + * type: integer + * 400: + * description: 参数错误或积分不足 + * 401: + * description: 未授权 + * 404: + * description: 商品不存在或已下架 + * 500: + * description: 服务器错误 + */ router.post('/', auth, async (req, res) => { const db = getDB(); await db.query('START TRANSACTION'); @@ -253,7 +481,42 @@ router.post('/', auth, async (req, res) => { } }); -// 用户取消订单 +/** + * @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'); @@ -310,7 +573,42 @@ router.put('/:id/cancel', auth, async (req, res) => { } }); -// 确认收货 +/** + * @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; @@ -345,7 +643,57 @@ router.put('/:id/confirm', auth, async (req, res) => { } }); -// 更新订单状态(管理员) +/** + * @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'); @@ -406,7 +754,52 @@ router.put('/:id/status', auth, adminAuth, async (req, res) => { } }); -// 获取订单统计信息(管理员权限) +/** + * @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 { // 总订单数 diff --git a/routes/points.js b/routes/points.js index 371dbdb..99315d3 100644 --- a/routes/points.js +++ b/routes/points.js @@ -3,7 +3,70 @@ 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; @@ -29,7 +92,90 @@ router.get('/balance', auth, async (req, res) => { } }); -// 获取用户积分历史记录 +/** + * @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; @@ -135,7 +281,66 @@ router.get('/history', auth, async (req, res) => { } }); -// 管理员调整用户积分 +/** + * @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(); @@ -202,7 +407,64 @@ router.post('/adjust', auth, adminAuth, async (req, res) => { } }); -// 管理员给用户充值积分 +/** + * @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(); @@ -261,7 +523,53 @@ router.post('/recharge', auth, adminAuth, async (req, res) => { -// 获取积分排行榜 +/** + * @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; @@ -292,7 +600,48 @@ router.get('/leaderboard', auth, async (req, res) => { } }); -// 获取积分统计信息(管理员权限) +/** + * @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 { // 总积分发放量 diff --git a/routes/products.js b/routes/products.js index c17b888..d63aa5b 100644 --- a/routes/products.js +++ b/routes/products.js @@ -4,7 +4,123 @@ const { auth, adminAuth } = require('../middleware/auth'); const router = express.Router(); -// 获取商品列表 +/** + * @swagger + * tags: + * name: Products + * description: 商品管理API + */ + +/** + * @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: 积分价格 + * stock: + * type: integer + * description: 库存数量 + * image_url: + * type: string + * description: 商品图片URL + * description: + * type: string + * description: 商品描述 + * status: + * type: string + * description: 商品状态 + * enum: [active, inactive] + * created_at: + * type: string + * format: date-time + * description: 创建时间 + * updated_at: + * type: string + * format: date-time + * description: 更新时间 + */ + +/** + * @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 + */ router.get('/', async (req, res) => { try { const { page = 1, limit = 10, search = '', category = '', status = '' } = req.query; @@ -73,7 +189,30 @@ router.get('/', async (req, res) => { } }); -// 获取商品分类列表 +/** + * @swagger + * /products/categories: + * get: + * summary: 获取商品分类列表 + * tags: [Products] + * responses: + * 200: + * description: 成功获取商品分类列表 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * data: + * type: object + * properties: + * categories: + * type: array + * items: + * type: string + */ router.get('/categories', async (req, res) => { try { const [categories] = await getDB().execute( @@ -92,15 +231,45 @@ router.get('/categories', async (req, res) => { } }); -// 获取单个商品详情 +/** + * @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 + * data: + * $ref: '#/components/schemas/Product' + * 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 as points, stock, image_url as image, description, details, status, created_at, updated_at + 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 = ? + WHERE id = ? AND status = 'active' `; const [products] = await getDB().execute(query, [id]); @@ -109,16 +278,43 @@ router.get('/:id', async (req, res) => { return res.status(404).json({ success: false, message: '商品不存在' }); } - // 增强商品数据,添加前端需要的字段 const product = products[0]; + + // 获取商品规格 + const [specifications] = await getDB().execute( + 'SELECT * FROM product_specifications WHERE product_id = ? ORDER BY id', + [id] + ); + + // 获取商品属性 + 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: product.image ? [product.image] : ['/imgs/default-product.png'], // 将单个图片转为数组 - tags: product.category ? [product.category] : [], // 将分类作为标签 - sales: Math.floor(Math.random() * 1000) + 100, // 模拟销量数据 - rating: (Math.random() * 2 + 3).toFixed(1), // 模拟评分 3-5分 - originalPoints: product.points + Math.floor(Math.random() * 100), // 模拟原价 - discount: Math.floor(Math.random() * 3 + 7) // 模拟折扣 7-9折 + images: product.images ? JSON.parse(product.images) : (product.image_url ? [product.image_url] : []), + videos: product.videos ? JSON.parse(product.videos) : [], + payment_methods: product.payment_methods ? JSON.parse(product.payment_methods) : ['points'], + specifications, + attributes, + isFavorited, + // 保持向后兼容 + points: product.points_price, + image: product.image_url, + tags: product.category ? [product.category] : [] }; res.json({ @@ -134,23 +330,57 @@ router.get('/:id', async (req, res) => { // 创建商品(管理员权限) 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; - const { name, description, price, points_price, stock, category, image_url, details, status = 'active' } = req.body; - - if (!name || !price || !points_price || stock === undefined) { - return res.status(400).json({ message: '商品名称、原价、积分价格和库存不能为空' }); + 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, stock, category, image_url, details, status, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())`, - [name, description, price, points_price, stock, category || null, image_url, details, status] + `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 (specifications && specifications.length > 0) { + for (const spec of specifications) { + await getDB().execute( + `INSERT INTO product_specifications (product_id, spec_name, spec_value, price_adjustment, + points_adjustment, rongdou_adjustment, stock, sku_code) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, + [productId, spec.name, spec.value, spec.price_adjustment || 0, + spec.points_adjustment || 0, spec.rongdou_adjustment || 0, + spec.stock || 0, spec.sku_code || null] + ); + } + } + + // 添加商品属性 + 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: result.insertId } + data: { productId } }); } catch (error) { console.error('创建商品错误:', error); @@ -161,9 +391,12 @@ router.post('/', auth, adminAuth, async (req, res) => { // 更新商品(管理员权限) router.put('/:id', auth, adminAuth, async (req, res) => { try { - const productId = req.params.id; - const { name, description, price, points_price, stock, category, image_url, details, status } = req.body; + 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( @@ -199,6 +432,11 @@ router.put('/:id', auth, adminAuth, async (req, res) => { 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); @@ -214,13 +452,38 @@ router.put('/:id', auth, adminAuth, async (req, res) => { 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 = ?'); + updateFields.push('status = ?'); updateValues.push(status); } @@ -236,6 +499,43 @@ router.put('/:id', auth, adminAuth, async (req, res) => { updateValues ); + // 更新商品规格 + if (specifications !== undefined) { + // 删除原有规格 + await getDB().execute('DELETE FROM product_specifications WHERE product_id = ?', [productId]); + + // 添加新规格 + if (specifications && specifications.length > 0) { + for (const spec of specifications) { + await getDB().execute( + `INSERT INTO product_specifications (product_id, spec_name, spec_value, price_adjustment, + points_adjustment, rongdou_adjustment, stock, sku_code) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, + [productId, spec.name, spec.value, spec.price_adjustment || 0, + spec.points_adjustment || 0, spec.rongdou_adjustment || 0, + spec.stock || 0, spec.sku_code || null] + ); + } + } + } + + // 更新商品属性 + 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: '商品更新成功' @@ -343,51 +643,59 @@ router.get('/stats', auth, adminAuth, async (req, res) => { 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 mockReviews = [ - { - id: 1, - user: { - name: '用户1', - avatar: null - }, - rating: 5, - content: '商品质量很好,非常满意!', - createdAt: '2024-01-15 10:30:00', - images: null + // 获取评论列表 + 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 ? OFFSET ?`, + [id, limit, offset] + ); + + // 获取评论总数 + 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 }, - { - id: 2, - user: { - name: '用户2', - avatar: null - }, - rating: 4, - content: '性价比不错,值得购买。', - createdAt: '2024-01-14 15:20:00', - images: null - }, - { - id: 3, - user: { - name: '用户3', - avatar: null - }, - rating: 5, - content: '发货速度快,包装精美。', - createdAt: '2024-01-13 09:45:00', - images: null - } - ]; + rating: review.rating, + content: review.content, + createdAt: review.createdAt, + images: review.images ? JSON.parse(review.images) : null + })); res.json({ success: true, data: { - reviews: mockReviews, - total: mockReviews.length, - averageRating: 4.7 + 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) { @@ -448,4 +756,356 @@ router.get('/:id/recommended', async (req, res) => { } }); +// 收藏商品 +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 ? OFFSET ?`, + [userId, limit, offset] + ); + + 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/specifications', async (req, res) => { + try { + const productId = req.params.id; + + const [specifications] = await getDB().execute( + 'SELECT id, spec_name as name, spec_value as value, price_adjustment, points_adjustment, rongdou_adjustment, stock, sku_code, created_at, updated_at FROM product_specifications WHERE product_id = ? ORDER BY id', + [productId] + ); + + res.json({ + success: true, + data: specifications + }); + } catch (error) { + console.error('获取商品规格错误:', error); + res.status(500).json({ message: '获取商品规格失败' }); + } +}); + +// 创建商品规格(管理员权限) +router.post('/:id/specifications', auth, adminAuth, async (req, res) => { + try { + const productId = req.params.id; + const { name, value, price_adjustment = 0, points_adjustment = 0, rongdou_adjustment = 0, stock = 0, sku_code } = req.body; + + if (!name || !value) { + return res.status(400).json({ message: '规格名称和规格值不能为空' }); + } + + // 检查商品是否存在 + const [products] = await getDB().execute('SELECT id FROM products WHERE id = ?', [productId]); + if (products.length === 0) { + return res.status(404).json({ message: '商品不存在' }); + } + + const [result] = await getDB().execute( + `INSERT INTO product_specifications (product_id, spec_name, spec_value, price_adjustment, + points_adjustment, rongdou_adjustment, stock, sku_code, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())`, + [productId, name, value, price_adjustment, points_adjustment, rongdou_adjustment, stock, sku_code || null] + ); + + res.status(201).json({ + success: true, + message: '规格创建成功', + data: { id: result.insertId } + }); + } catch (error) { + console.error('创建商品规格错误:', error); + res.status(500).json({ message: '创建商品规格失败' }); + } +}); + +// 更新商品规格(管理员权限) +router.put('/:id/specifications/:specId', auth, adminAuth, async (req, res) => { + try { + const { id: productId, specId } = req.params; + const { name, value, price_adjustment, points_adjustment, rongdou_adjustment, stock, sku_code } = req.body; + + // 检查规格是否存在 + const [specs] = await getDB().execute( + 'SELECT id FROM product_specifications WHERE id = ? AND product_id = ?', + [specId, productId] + ); + + if (specs.length === 0) { + return res.status(404).json({ message: '规格不存在' }); + } + + // 构建更新字段 + const updateFields = []; + const updateValues = []; + + if (name !== undefined) { + updateFields.push('spec_name = ?'); + updateValues.push(name); + } + + if (value !== undefined) { + updateFields.push('spec_value = ?'); + updateValues.push(value); + } + + 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 (updateFields.length === 0) { + return res.status(400).json({ message: '没有提供要更新的字段' }); + } + + updateFields.push('updated_at = NOW()'); + updateValues.push(specId); + + await getDB().execute( + `UPDATE product_specifications SET ${updateFields.join(', ')} WHERE id = ?`, + updateValues + ); + + res.json({ + success: true, + message: '规格更新成功' + }); + } catch (error) { + console.error('更新商品规格错误:', error); + res.status(500).json({ message: '更新商品规格失败' }); + } +}); + +// 删除商品规格(管理员权限) +router.delete('/:id/specifications/:specId', auth, adminAuth, async (req, res) => { + try { + const { id: productId, specId } = req.params; + + // 检查规格是否存在 + const [specs] = await getDB().execute( + 'SELECT id FROM product_specifications WHERE id = ? AND product_id = ?', + [specId, productId] + ); + + if (specs.length === 0) { + return res.status(404).json({ message: '规格不存在' }); + } + + await getDB().execute('DELETE FROM product_specifications WHERE id = ?', [specId]); + + res.json({ + success: true, + message: '规格删除成功' + }); + } 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 index dc5880c..bd6d132 100644 --- a/routes/regions.js +++ b/routes/regions.js @@ -2,7 +2,73 @@ 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 = ` @@ -28,4 +94,295 @@ router.get('/zhejiang', async (req, res) => { } }) +/** + * @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 { + const [provinces] = await getDB().execute( + `SELECT code, name FROM china_regions + WHERE level = 1 + ORDER BY code` + ); + + res.json({ + success: true, + data: provinces + }); + } 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 index 00c9e39..967ecb6 100644 --- a/routes/riskManagement.js +++ b/routes/riskManagement.js @@ -1,5 +1,12 @@ const express = require('express'); -const router = express.Router(); +router = express.Router(); + +/** + * @swagger + * tags: + * name: RiskManagement + * description: 风险管理API + */ const { auth } = require('../middleware/auth'); const timeoutService = require('../services/timeoutService'); const { getDB } = require('../database'); @@ -15,7 +22,85 @@ const requireAdmin = (req, res, 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 { @@ -42,7 +127,54 @@ router.get('/users', auth, requireAdmin, async (req, res) => { }); /** - * 拉黑用户 + * @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 { @@ -67,7 +199,40 @@ router.post('/blacklist/:userId', auth, requireAdmin, async (req, res) => { }); /** - * 解除拉黑 + * @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 { @@ -87,7 +252,80 @@ router.post('/unblacklist/:userId', auth, requireAdmin, async (req, res) => { }); /** - * 获取超时转账列表 + * @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 { diff --git a/routes/sms.js b/routes/sms.js index c6f6ede..91f3b3d 100644 --- a/routes/sms.js +++ b/routes/sms.js @@ -5,6 +5,28 @@ 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 @@ -45,7 +67,41 @@ function generateSMSCode() { } /** - * 发送短信验证码 + * @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 { @@ -136,7 +192,52 @@ router.post('/send', async (req, res) => { }); /** - * 验证短信验证码 + * @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 { diff --git a/routes/transfers.js b/routes/transfers.js index bf107df..d28ce94 100644 --- a/routes/transfers.js +++ b/routes/transfers.js @@ -11,6 +11,65 @@ const dayjs = require('dayjs'); const router = express.Router(); +/** + * @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: 总页数 + */ + // 配置文件上传 const storage = multer.diskStorage({ destination: function (req, file, cb) { @@ -37,16 +96,87 @@ const upload = multer({ }); /** - * 获取转账列表 - * @param {string} status - 转账状态过滤 - * @param {string} transfer_type - 转账类型过滤 - * @param {string} start_date - 开始日期过滤 - * @param {string} end_date - 结束日期过滤 - * @param {string} search - 搜索关键词(用户名或真实姓名) - * @param {number} page - 页码 - * @param {number} limit - 每页数量 - * @param {string} sort - 排序字段 - * @param {string} order - 排序方向(asc/desc) + * @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: 服务器错误 */ router.get('/', authenticateToken, @@ -86,7 +216,84 @@ router.get('/', } ); -// 获取转账记录列表 +/** + * @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: 服务器错误 + */ router.get('/list', authenticateToken, validateQuery(transferSchemas.query), @@ -124,7 +331,49 @@ router.get('/list', } ); -// 获取公户信息 +/** + * @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: 服务器错误 + */ router.get('/public-account', authenticateToken, async (req, res) => { try { const db = getDB(); @@ -145,7 +394,66 @@ router.get('/public-account', authenticateToken, async (req, res) => { } }); -// 创建转账记录 +/** + * @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: 服务器错误 + */ router.post('/create', authenticateToken, validate(transferSchemas.create), @@ -170,7 +478,72 @@ router.post('/create', } ); -// 管理员创建转账记录 +/** + * @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: 服务器错误 + */ router.post('/admin/create', authenticateToken, async (req, res, next) => { diff --git a/routes/upload.js b/routes/upload.js index 190df41..30f97b2 100644 --- a/routes/upload.js +++ b/routes/upload.js @@ -7,6 +7,13 @@ const { authenticateToken } = require('./auth'); const router = express.Router(); +/** + * @swagger + * tags: + * name: Upload + * description: 文件上传API + */ + // 确保上传目录存在 const uploadDir = path.join(__dirname, '../uploads'); const documentsDir = path.join(uploadDir, 'documents'); @@ -50,16 +57,17 @@ const storage = multer.diskStorage({ } }); -// 文件过滤器 +// 文件过滤器 - 支持图片和视频 const fileFilter = (req, file, cb) => { - // 只允许图片文件 - if (file.mimetype.startsWith('image/')) { + // 允许图片和视频文件 + if (file.mimetype.startsWith('image/') || file.mimetype.startsWith('video/')) { cb(null, true); } else { - cb(new Error('只能上传图片文件'), false); + cb(new Error('只能上传图片或视频文件'), false); } }; +// 单文件上传配置 const upload = multer({ storage: storage, fileFilter: fileFilter, @@ -69,10 +77,63 @@ const upload = multer({ } }); +// 多文件上传配置 +const multiUpload = multer({ + storage: storage, + fileFilter: fileFilter, + limits: { + fileSize: 10 * 1024 * 1024, // 10MB (视频文件更大) + files: 10 // 最多10个文件 + } +}); + /** - * @route POST /api/upload/image - * @desc 上传图片 - * @access Private + * @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, (err) => { @@ -134,8 +195,213 @@ router.post('/image', authenticateToken, (req, res) => { }); }); -// 保持原有的上传接口兼容性 -router.post('/', auth, upload.single('file'), (req, res) => { +/** + * @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, (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 { + // 处理多个文件 + const uploadedFiles = req.files.map(file => { + const type = req.body.type || 'documents'; + let folderName = type; + if (type === 'product') { + folderName = 'products'; + } else if (type === 'avatar') { + folderName = 'avatars'; + } + const relativePath = path.join(folderName, file.filename).replace(/\\/g, '/'); + const fileUrl = `/uploads/${relativePath}`; + + return { + filename: file.filename, + originalname: file.originalname, + mimetype: file.mimetype, + size: file.size, + path: relativePath, + url: fileUrl + }; + }); + + // 如果只上传了一个文件,返回单文件格式以保持兼容性 + if (uploadedFiles.length === 1) { + res.json({ + success: true, + message: '文件上传成功', + data: { + ...uploadedFiles[0], + urls: [uploadedFiles[0].url] // 同时提供urls数组格式 + } + }); + } else { + // 多文件返回数组格式 + res.json({ + success: true, + message: `成功上传${uploadedFiles.length}个文件`, + data: { + files: uploadedFiles, + urls: uploadedFiles.map(file => file.url) + } + }); + } + } catch (error) { + console.error('文件上传错误:', error); + res.status(500).json({ success: false, 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, upload.single('file'), (req, res) => { try { if (!req.file) { return res.status(400).json({ success: false, message: '没有上传文件' }); @@ -171,11 +437,14 @@ router.post('/', auth, upload.single('file'), (req, res) => { 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: '文件大小不能超过5MB' }); + 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 === '只能上传图片文件') { + if (error.message === '只能上传图片或视频文件') { return res.status(400).json({ success: false, message: error.message }); } diff --git a/routes/users.js b/routes/users.js index 7043766..1e7dd44 100644 --- a/routes/users.js +++ b/routes/users.js @@ -6,7 +6,131 @@ const dayjs = require('dayjs'); const router = express.Router(); -// 创建用户(管理员权限) +/** + * @swagger + * tags: + * name: Users + * description: 用户管理API + */ + +/** + * @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: 是否为系统账户 + * 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: 服务器错误 + */ router.post('/', auth, adminAuth, async (req, res) => { try { const db = getDB(); @@ -88,7 +212,60 @@ router.post('/', auth, adminAuth, async (req, res) => { }); /** - * 获取待审核用户列表(管理员权限) + * @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: 服务器错误 */ router.get('/pending-audit', auth, adminAuth, async (req, res) => { try { diff --git a/server.js b/server.js index 13a745a..f7dcc7a 100644 --- a/server.js +++ b/server.js @@ -13,6 +13,10 @@ const { logger } = require('./config/logger'); const { errorHandler, notFound } = require('./middleware/errorHandler'); const fs = require('fs'); +// Swagger文档相关 +const swaggerUi = require('swagger-ui-express'); +const swaggerSpecs = require('./swagger'); + const app = express(); const PORT = process.env.PORT || 3000; @@ -213,6 +217,9 @@ app.use(express.static(path.join(__dirname, 'frontend/dist'), { // 引入数据库初始化模块 const { initDatabase } = require('./config/database-init'); +// Swagger API文档 +app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpecs, { explorer: true })); + // API路由 app.use('/api/auth', require('./routes/auth')); app.use('/api/users', require('./routes/users')); @@ -234,6 +241,8 @@ app.use('/api/admin/agents', require('./admin/routes/agents')); app.use('/api/admin/withdrawals', require('./admin/routes/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')); // 前端路由 - 必须在最后,作为fallback app.get('/', (req, res) => { @@ -304,7 +313,7 @@ app.listen(PORT, async () => { // 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' diff --git a/services/matchingService.js b/services/matchingService.js index b3553a2..e167adf 100644 --- a/services/matchingService.js +++ b/services/matchingService.js @@ -696,7 +696,7 @@ class MatchingService { FROM users u WHERE u.is_system_account = FALSE AND u.id != ? - AND u.balance < 0 + AND u.balance < -100 AND u.audit_status = 'approved' ORDER BY u.balance ASC`, [excludeUserId] @@ -811,18 +811,18 @@ class MatchingService { if (range <= 0) { maxUserAllocation = minAmount; } else { - // 使用极度偏向大值的策略 - const randomFactor = Math.pow(Math.random(), 0.15); // 0.15的幂使分布极度偏向较大值 + // 使用更均匀的分配策略 + const randomFactor = Math.random(); // 使用均匀分布 - // 基础分配:从range的80%开始,确保大部分分配都是较大值 - const baseOffset = Math.floor(range * 0.8); // 80%的基础偏移 + // 基础分配:在整个范围内更均匀分布,减少偏向性 + 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.6, maxRandomAllocation - maxUserAllocation); // 增加到60% - if (bonusRange > 0 && Math.random() > 0.1) { // 90%概率获得额外增量 - const bonus = Math.floor(Math.random() * bonusRange * 0.9); // 使用90%的bonus范围 + // 进一步减少额外增量的影响 + 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; } @@ -921,7 +921,7 @@ class MatchingService { } /** - * 生成随机分配金额数组 + * 生成随机分配金额数组(更均匀的分配策略) * @param {number} totalAmount - 总金额 * @param {number} transferCount - 分配笔数 * @param {number} minAmount - 最小金额 @@ -933,148 +933,87 @@ class MatchingService { return []; } + // 使用更均匀的分配策略 const amounts = []; - let remainingAmount = totalAmount; - - // 为前n-1笔生成随机金额 - for (let i = 0; i < transferCount - 1; i++) { - const remainingTransfers = transferCount - i; - const minForThisTransfer = minAmount; - const maxForThisTransfer = Math.min( - maxAmount, - remainingAmount - (remainingTransfers - 1) * minAmount // 确保剩余金额足够分配给后续转账 - ); - - if (maxForThisTransfer < minForThisTransfer) { - // 如果无法满足约束,重新开始整个分配过程 - return this.generateRandomAmountsWithRetry(totalAmount, transferCount, minAmount, maxAmount); - } else { - // 在有效范围内生成随机金额 - const randomAmount = Math.floor(Math.random() * (maxForThisTransfer - minForThisTransfer + 1)) + minForThisTransfer; - amounts.push(randomAmount); - } - - remainingAmount -= amounts[amounts.length - 1]; - } - - // 最后一笔分配剩余金额 - if (remainingAmount >= minAmount && remainingAmount <= maxAmount) { - amounts.push(remainingAmount); - } else { - // 如果剩余金额不符合约束,使用重试机制 - return this.generateRandomAmountsWithRetry(totalAmount, transferCount, minAmount, maxAmount); - } - - return amounts; - } - - /** - * 使用重试机制生成随机金额分配(确保完全随机性) - * @param {number} totalAmount - 总金额 - * @param {number} transferCount - 分配笔数 - * @param {number} minAmount - 最小金额 - * @param {number} maxAmount - 最大金额 - * @returns {number[]} 随机金额数组 - */ - generateRandomAmountsWithRetry(totalAmount, transferCount, minAmount, maxAmount) { - const maxRetries = 100; - - for (let retry = 0; retry < maxRetries; retry++) { - const amounts = []; - let remainingAmount = totalAmount; - let success = true; - - // 为前n-1笔生成随机金额 - for (let i = 0; i < transferCount - 1; i++) { - const remainingTransfers = transferCount - i; - const minForThisTransfer = minAmount; - const maxForThisTransfer = Math.min( - maxAmount, - remainingAmount - (remainingTransfers - 1) * minAmount - ); - - if (maxForThisTransfer < minForThisTransfer) { - success = false; - break; - } - - // 在有效范围内生成随机金额 - const randomAmount = Math.floor(Math.random() * (maxForThisTransfer - minForThisTransfer + 1)) + minForThisTransfer; - amounts.push(randomAmount); - remainingAmount -= randomAmount; - } - - // 检查最后一笔是否符合约束 - if (success && remainingAmount >= minAmount && remainingAmount <= maxAmount) { - amounts.push(remainingAmount); - return amounts; - } - } - - // 如果重试失败,使用备用的随机分配策略 - return this.generateFallbackRandomAmounts(totalAmount, transferCount, minAmount, maxAmount); - } - - /** - * 备用随机分配策略(保证随机性的最后手段) - * @param {number} totalAmount - 总金额 - * @param {number} transferCount - 分配笔数 - * @param {number} minAmount - 最小金额 - * @param {number} maxAmount - 最大金额 - * @returns {number[]} 随机金额数组 - */ - generateFallbackRandomAmounts(totalAmount, transferCount, minAmount, maxAmount) { - // 检查是否可能分配 - if (totalAmount < minAmount * transferCount || totalAmount > maxAmount * 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; } - - // 随机选择一个可用位置 - const randomIndex = availableIndices[Math.floor(Math.random() * availableIndices.length)]; - - // 计算这个位置最多还能增加多少 - const maxIncrease = Math.min( - maxAmount - amounts[randomIndex], - remainingToDistribute - ); - - if (maxIncrease > 0) { - // 随机增加1到maxIncrease之间的金额 - const increase = Math.floor(Math.random() * maxIncrease) + 1; - amounts[randomIndex] += increase; - remainingToDistribute -= increase; - } } - - // 如果还有剩余金额无法分配,说明约束条件无法满足 + + // 如果还有剩余金额无法分配,返回空数组表示失败 if (remainingToDistribute > 0) { return []; } - + return amounts; } diff --git a/services/transferService.js b/services/transferService.js index d4f7670..f8f18ab 100644 --- a/services/transferService.js +++ b/services/transferService.js @@ -172,6 +172,7 @@ class TransferService { // 根据所有相关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都被取消/拒绝/未收到,匹配订单标记为已完成 diff --git a/swagger.js b/swagger.js new file mode 100644 index 0000000..65fc4b9 --- /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: ['./routes/*.js', './admin/routes/*.js'], +}; + +const specs = swaggerJsdoc(options); + +module.exports = specs; \ No newline at end of file