diff --git a/.env b/.env index c0351cd..eba2d37 100644 --- a/.env +++ b/.env @@ -43,4 +43,22 @@ MINIO_PUBLIC_URL=https://minio.zrbjr.com MINIO_BUCKET_UPLOADS=jurongquan MINIO_BUCKET_AVATARS=jurongquan MINIO_BUCKET_PRODUCTS=jurongquan -MINIO_BUCKET_DOCUMENTS=jurongquan \ No newline at end of file +MINIO_BUCKET_DOCUMENTS=jurongquan + +#支付配置 +WECHAT_APP_ID=wx3a702dbe13fd2217 +WECHAT_MCH_ID=1726377336 +WECHAT_API_KEY=NINGBOJURONGkejiyouxiangongsi202 +WECHAT_API_V3_KEY=NINGBOJURONGkejiyouxiangongsi202 +WECHAT_CERT_PATH=./cert/apiclient_cert.pem +WECHAT_KEY_PATH=./cert/apiclient_key.pem +WECHAT_NOTIFY_URL=https://www.zrbjr.com/api/wechat-pay/notify + +# 支付宝配置 +# 请在支付宝开放平台获取以下配置信息: +# 1. 应用ID:在支付宝开放平台创建应用后获得 +# 2. 应用私钥和支付宝公钥现在从文件读取 +ALIPAY_APP_ID=2021005188682022 +ALIPAY_NOTIFY_URL=https://www.zrbjr.com/api/payment/alipay/notify +ALIPAY_RETURN_URL=https://www.zrbjr.com/payment/success +ALIPAY_QUIT_URL=https://www.zrbjr.com/payment/cancel \ No newline at end of file diff --git a/.env.example b/.env.example index eba3138..f84b5d0 100644 --- a/.env.example +++ b/.env.example @@ -41,3 +41,20 @@ MINIO_BUCKET_UPLOADS=jurongquan MINIO_BUCKET_AVATARS=jurongquan MINIO_BUCKET_PRODUCTS=jurongquan MINIO_BUCKET_DOCUMENTS=jurongquan + +# 微信支付配置 +WECHAT_APP_ID=your_wechat_app_id +WECHAT_MCH_ID=your_wechat_mch_id +WECHAT_API_KEY=your_wechat_api_key +WECHAT_API_V3_KEY=your_wechat_api_v3_key +WECHAT_NOTIFY_URL=https://your-domain.com/api/wechat-pay/notify +WECHAT_CERT_PATH=./cert/apiclient_cert.pem +WECHAT_KEY_PATH=./cert/apiclient_key.pem + +# 支付宝支付配置 +ALIPAY_APP_ID=your_alipay_app_id +ALIPAY_PRIVATE_KEY=your_alipay_private_key +ALIPAY_PUBLIC_KEY=your_alipay_public_key +ALIPAY_GATEWAY_URL=https://openapi.alipay.com/gateway.do +ALIPAY_NOTIFY_URL=https://your-domain.com/api/alipay/notify +ALIPAY_RETURN_URL=https://your-domain.com/payment/success diff --git a/api-docs/swagger.json b/api-docs/swagger.json index 90b00dc..066f91c 100644 --- a/api-docs/swagger.json +++ b/api-docs/swagger.json @@ -24,6 +24,901 @@ } }, "schemas": { + "Announcement": { + "type": "object", + "required": [ + "title", + "content", + "type", + "priority" + ], + "properties": { + "id": { + "type": "integer", + "description": "公告ID", + "example": 1 + }, + "title": { + "type": "string", + "description": "公告标题", + "example": "系统维护通知" + }, + "content": { + "type": "string", + "description": "公告内容", + "example": "系统将于今晚进行维护,预计维护时间2小时,期间可能影响部分功能使用。" + }, + "type": { + "type": "string", + "description": "公告类型", + "enum": [ + "system", + "maintenance", + "promotion", + "warning" + ], + "example": "maintenance" + }, + "priority": { + "type": "string", + "description": "优先级", + "enum": [ + "low", + "medium", + "high", + "urgent" + ], + "example": "high" + }, + "status": { + "type": "string", + "description": "状态", + "enum": [ + "draft", + "published", + "archived" + ], + "example": "published" + }, + "isTop": { + "type": "boolean", + "description": "是否置顶", + "example": false + }, + "publishTime": { + "type": "string", + "format": "date-time", + "description": "发布时间", + "example": "2024-01-15T10:00:00Z" + }, + "expireTime": { + "type": "string", + "format": "date-time", + "description": "过期时间", + "example": "2024-01-20T10:00:00Z" + }, + "createdBy": { + "type": "integer", + "description": "创建者用户ID", + "example": 1 + }, + "createdAt": { + "type": "string", + "format": "date-time", + "description": "创建时间", + "example": "2024-01-15T09:00:00Z" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "description": "更新时间", + "example": "2024-01-15T09:30:00Z" + }, + "creator": { + "type": "object", + "description": "创建者信息", + "properties": { + "id": { + "type": "integer", + "example": 1 + }, + "username": { + "type": "string", + "example": "admin" + }, + "email": { + "type": "string", + "example": "admin@example.com" + } + } + }, + "is_pinned": { + "type": "boolean", + "description": "是否置顶" + }, + "publish_time": { + "type": "string", + "format": "date-time", + "description": "发布时间" + }, + "expire_time": { + "type": "string", + "format": "date-time", + "description": "过期时间" + }, + "created_by": { + "type": "integer", + "description": "创建者ID" + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "创建时间" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "description": "更新时间" + } + } + }, + "AnnouncementCreate": { + "type": "object", + "required": [ + "title", + "content", + "type", + "priority" + ], + "properties": { + "title": { + "type": "string", + "description": "公告标题", + "example": "系统维护通知" + }, + "content": { + "type": "string", + "description": "公告内容", + "example": "系统将于今晚进行维护,预计维护时间2小时。" + }, + "type": { + "type": "string", + "description": "公告类型", + "enum": [ + "system", + "activity", + "maintenance", + "urgent" + ], + "example": "maintenance" + }, + "priority": { + "type": "string", + "description": "优先级", + "enum": [ + "high", + "medium", + "low" + ], + "example": "high" + }, + "status": { + "type": "string", + "description": "公告状态", + "enum": [ + "draft", + "published" + ], + "default": "draft", + "example": "draft" + }, + "isTop": { + "type": "boolean", + "description": "是否置顶", + "default": false, + "example": false + }, + "publishTime": { + "type": "string", + "format": "date-time", + "description": "发布时间", + "example": "2024-01-15T10:00:00Z" + }, + "expireTime": { + "type": "string", + "format": "date-time", + "description": "过期时间", + "example": "2024-01-20T10:00:00Z" + } + } + }, + "AnnouncementUpdate": { + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "公告标题", + "example": "系统维护通知(更新)" + }, + "content": { + "type": "string", + "description": "公告内容", + "example": "系统维护时间调整为明晚进行。" + }, + "type": { + "type": "string", + "description": "公告类型", + "enum": [ + "system", + "activity", + "maintenance", + "urgent" + ], + "example": "maintenance" + }, + "priority": { + "type": "string", + "description": "优先级", + "enum": [ + "high", + "medium", + "low" + ], + "example": "medium" + }, + "status": { + "type": "string", + "description": "公告状态", + "enum": [ + "draft", + "published", + "expired" + ], + "example": "published" + }, + "isTop": { + "type": "boolean", + "description": "是否置顶", + "example": true + }, + "publishTime": { + "type": "string", + "format": "date-time", + "description": "发布时间", + "example": "2024-01-16T10:00:00Z" + }, + "expireTime": { + "type": "string", + "format": "date-time", + "description": "过期时间", + "example": "2024-01-21T10:00:00Z" + } + } + }, + "AnnouncementList": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "data": { + "type": "object", + "properties": { + "announcements": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Announcement" + } + }, + "total": { + "type": "integer", + "description": "总记录数", + "example": 50 + }, + "page": { + "type": "integer", + "description": "当前页码", + "example": 1 + }, + "limit": { + "type": "integer", + "description": "每页数量", + "example": 10 + }, + "totalPages": { + "type": "integer", + "description": "总页数", + "example": 5 + } + } + } + } + }, + "AnnouncementResponse": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "操作成功" + }, + "data": { + "$ref": "#/components/schemas/Announcement" + } + } + }, + "AnnouncementError": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": false + }, + "message": { + "type": "string", + "example": "操作失败" + }, + "error": { + "type": "string", + "example": "公告不存在" + } + } + }, + "CartItem": { + "type": "object", + "required": [ + "user_id", + "product_id", + "quantity" + ], + "properties": { + "id": { + "type": "integer", + "description": "购物车项ID" + }, + "user_id": { + "type": "integer", + "description": "用户ID" + }, + "product_id": { + "type": "integer", + "description": "商品ID" + }, + "quantity": { + "type": "integer", + "description": "商品数量" + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "创建时间" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "description": "更新时间" + }, + "spec_combination_id": { + "type": "integer", + "description": "商品规格组合ID" + }, + "product": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "price": { + "type": "integer" + }, + "points_price": { + "type": "integer" + }, + "rongdou_price": { + "type": "integer" + }, + "image_url": { + "type": "string" + }, + "stock": { + "type": "integer" + }, + "status": { + "type": "string" + } + } + } + } + }, + "CartItemWithProduct": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "购物车商品ID" + }, + "product_id": { + "type": "integer", + "description": "商品ID" + }, + "product_name": { + "type": "string", + "description": "商品名称" + }, + "quantity": { + "type": "integer", + "description": "商品数量" + }, + "points_price": { + "type": "integer", + "description": "积分价格" + }, + "rongdou_price": { + "type": "number", + "description": "融豆价格" + }, + "image_url": { + "type": "string", + "description": "商品图片URL" + }, + "stock": { + "type": "integer", + "description": "库存数量" + }, + "payment_methods": { + "type": "array", + "items": { + "type": "string" + }, + "description": "支付方式列表" + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "创建时间" + } + } + }, + "AddToCartRequest": { + "type": "object", + "required": [ + "product_id", + "quantity" + ], + "properties": { + "product_id": { + "type": "integer", + "description": "商品ID" + }, + "quantity": { + "type": "integer", + "minimum": 1, + "description": "商品数量" + } + } + }, + "UpdateCartRequest": { + "type": "object", + "required": [ + "quantity" + ], + "properties": { + "quantity": { + "type": "integer", + "minimum": 1, + "description": "商品数量" + } + } + }, + "Order": { + "type": "object", + "required": [ + "user_id", + "total_amount", + "status" + ], + "properties": { + "id": { + "type": "integer", + "description": "订单ID" + }, + "order_number": { + "type": "string", + "description": "订单号" + }, + "user_id": { + "type": "integer", + "description": "用户ID" + }, + "total_amount": { + "type": "number", + "description": "订单总金额" + }, + "total_points": { + "type": "integer", + "description": "订单总积分" + }, + "total_rongdou": { + "type": "number", + "description": "订单总融豆" + }, + "status": { + "type": "string", + "description": "订单状态", + "enum": [ + "pending", + "confirmed", + "shipped", + "delivered", + "cancelled" + ] + }, + "payment_status": { + "type": "string", + "description": "支付状态", + "enum": [ + "pending", + "paid", + "failed", + "refunded" + ] + }, + "shipping_address": { + "type": "string", + "description": "收货地址" + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "创建时间" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "description": "更新时间" + } + } + }, + "OrderItem": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "订单商品ID" + }, + "order_id": { + "type": "integer", + "description": "订单ID" + }, + "product_id": { + "type": "integer", + "description": "商品ID" + }, + "quantity": { + "type": "integer", + "description": "商品数量" + }, + "price": { + "type": "number", + "description": "商品价格" + }, + "points_price": { + "type": "integer", + "description": "积分价格" + }, + "rongdou_price": { + "type": "number", + "description": "融豆价格" + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "创建时间" + } + } + }, + "PreOrder": { + "type": "object", + "properties": { + "preOrderId": { + "type": "integer", + "description": "预订单ID" + }, + "orderNumber": { + "type": "string", + "description": "订单号" + }, + "totalAmount": { + "type": "number", + "description": "总金额" + }, + "totalPoints": { + "type": "integer", + "description": "所需积分总数" + }, + "totalRongdou": { + "type": "number", + "description": "所需融豆总数" + }, + "paymentMethods": { + "type": "array", + "items": { + "type": "string" + }, + "description": "去重后的支付方式列表" + } + } + }, + "Product": { + "type": "object", + "required": [ + "name", + "points_price", + "stock" + ], + "properties": { + "id": { + "type": "integer", + "description": "商品ID" + }, + "name": { + "type": "string", + "description": "商品名称" + }, + "category": { + "type": "string", + "description": "商品分类" + }, + "points_price": { + "type": "integer", + "description": "积分价格" + }, + "rongdou_price": { + "type": "number", + "description": "融豆价格" + }, + "stock": { + "type": "integer", + "description": "库存数量" + }, + "image_url": { + "type": "string", + "description": "商品图片URL" + }, + "description": { + "type": "string", + "description": "商品描述" + }, + "status": { + "type": "string", + "description": "商品状态", + "enum": [ + "active", + "inactive" + ] + }, + "payment_methods": { + "type": "array", + "items": { + "type": "string" + }, + "description": "支付方式列表" + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "创建时间" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "description": "更新时间" + } + } + }, + "User": { + "type": "object", + "required": [ + "username", + "password", + "real_name", + "id_card" + ], + "properties": { + "id": { + "type": "integer", + "description": "用户ID" + }, + "username": { + "type": "string", + "description": "用户名" + }, + "email": { + "type": "string", + "format": "email", + "description": "邮箱地址" + }, + "phone": { + "type": "string", + "description": "手机号" + }, + "points": { + "type": "integer", + "description": "用户积分" + }, + "rongdou": { + "type": "number", + "description": "融豆余额" + }, + "avatar": { + "type": "string", + "description": "用户头像URL" + }, + "status": { + "type": "string", + "description": "用户状态", + "enum": [ + "active", + "inactive", + "banned" + ] + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "创建时间" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "description": "更新时间" + }, + "role": { + "type": "string", + "description": "用户角色", + "enum": [ + "user", + "admin", + "merchant" + ] + }, + "real_name": { + "type": "string", + "description": "真实姓名" + }, + "id_card": { + "type": "string", + "description": "身份证号" + }, + "is_system_account": { + "type": "boolean", + "description": "是否为系统账户" + }, + "is_distribute": { + "type": "boolean", + "description": "是否为分发账户" + } + } + }, + "UserProfile": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "用户ID" + }, + "username": { + "type": "string", + "description": "用户名" + }, + "email": { + "type": "string", + "description": "邮箱地址" + }, + "phone": { + "type": "string", + "description": "手机号码" + }, + "points": { + "type": "integer", + "description": "积分余额" + }, + "rongdou": { + "type": "number", + "description": "融豆余额" + }, + "avatar": { + "type": "string", + "description": "头像URL" + } + } + }, + "LoginRequest": { + "type": "object", + "required": [ + "username", + "password" + ], + "properties": { + "username": { + "type": "string", + "description": "用户名或邮箱" + }, + "password": { + "type": "string", + "description": "密码" + } + } + }, + "RegisterRequest": { + "type": "object", + "required": [ + "username", + "phone", + "password", + "registrationCode", + "city", + "district_id", + "captchaId", + "captchaText", + "smsCode" + ], + "properties": { + "username": { + "type": "string", + "description": "用户名" + }, + "email": { + "type": "string", + "format": "email", + "description": "邮箱地址" + }, + "password": { + "type": "string", + "description": "密码" + }, + "phone": { + "type": "string", + "description": "手机号" + }, + "registrationCode": { + "type": "string", + "description": "注册激活码" + }, + "city": { + "type": "string", + "description": "城市" + }, + "district_id": { + "type": "string", + "description": "区域ID" + }, + "captchaId": { + "type": "string", + "description": "图形验证码ID" + }, + "captchaText": { + "type": "string", + "description": "图形验证码文本" + }, + "smsCode": { + "type": "string", + "description": "短信验证码" + }, + "role": { + "type": "string", + "description": "用户角色", + "default": "user" + } + } + }, "Address": { "type": "object", "properties": { @@ -121,127 +1016,6 @@ } } }, - "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" - } - } - }, - "CartItem": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "description": "购物车项ID" - }, - "user_id": { - "type": "integer", - "description": "用户ID" - }, - "product_id": { - "type": "integer", - "description": "商品ID" - }, - "quantity": { - "type": "integer", - "description": "商品数量" - }, - "spec_combination_id": { - "type": "integer", - "description": "商品规格组合ID" - }, - "created_at": { - "type": "string", - "format": "date-time", - "description": "创建时间" - }, - "updated_at": { - "type": "string", - "format": "date-time", - "description": "更新时间" - }, - "product": { - "type": "object", - "properties": { - "id": { - "type": "integer" - }, - "name": { - "type": "string" - }, - "price": { - "type": "integer" - }, - "points_price": { - "type": "integer" - }, - "rongdou_price": { - "type": "integer" - }, - "image_url": { - "type": "string" - }, - "stock": { - "type": "integer" - }, - "status": { - "type": "string" - } - } - } - } - }, "MatchingOrder": { "type": "object", "properties": { @@ -369,59 +1143,6 @@ } } }, - "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": { @@ -448,69 +1169,6 @@ } } }, - "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" - ] - }, - "payment_methods": { - "type": "array", - "items": { - "type": "string" - }, - "description": "支付方式列表" - }, - "created_at": { - "type": "string", - "format": "date-time", - "description": "创建时间" - }, - "updated_at": { - "type": "string", - "format": "date-time", - "description": "更新时间" - } - } - }, "Region": { "type": "object", "properties": { @@ -703,72 +1361,6 @@ "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": "是否为系统账户" - }, - "is_distribute": { - "type": "boolean", - "description": "是否为分发账户" - }, - "created_at": { - "type": "string", - "format": "date-time", - "description": "创建时间" - }, - "updated_at": { - "type": "string", - "format": "date-time", - "description": "更新时间" - } - } } } }, @@ -778,6 +1370,1611 @@ } ], "paths": { + "/api/announcements/{id}": { + "get": { + "summary": "获取单个公告详情", + "tags": [ + "Announcements" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + }, + "description": "公告ID" + } + ], + "responses": { + "200": { + "description": "成功获取公告详情", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "data": { + "$ref": "#/components/schemas/Announcement" + } + } + } + } + } + }, + "401": { + "description": "未授权" + }, + "404": { + "description": "公告不存在" + }, + "500": { + "description": "服务器错误" + } + } + }, + "put": { + "summary": "更新通知公告", + "tags": [ + "Announcements" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + }, + "description": "公告ID" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "公告标题" + }, + "content": { + "type": "string", + "description": "公告内容" + }, + "type": { + "type": "string", + "enum": [ + "system", + "activity", + "maintenance", + "urgent" + ], + "description": "公告类型" + }, + "priority": { + "type": "string", + "enum": [ + "high", + "medium", + "low", + "urgent" + ], + "description": "优先级" + }, + "status": { + "type": "string", + "enum": [ + "draft", + "published", + "expired" + ], + "description": "状态" + }, + "is_pinned": { + "type": "boolean" + }, + "publish_time": { + "type": "string", + "format": "date-time" + }, + "expire_time": { + "type": "string", + "format": "date-time" + }, + "isTop": { + "type": "boolean", + "description": "是否置顶" + }, + "publishTime": { + "type": "string", + "format": "date-time", + "description": "发布时间" + }, + "expireTime": { + "type": "string", + "format": "date-time", + "description": "过期时间" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "公告更新成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "公告更新成功" + }, + "data": { + "$ref": "#/components/schemas/Announcement" + } + } + } + } + } + }, + "400": { + "description": "请求参数错误" + }, + "401": { + "description": "未授权" + }, + "404": { + "description": "公告不存在" + }, + "500": { + "description": "服务器错误" + } + } + }, + "delete": { + "summary": "删除通知公告", + "tags": [ + "Announcements" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + }, + "description": "公告ID" + } + ], + "responses": { + "200": { + "description": "公告删除成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "公告删除成功" + } + } + } + } + } + }, + "401": { + "description": "未授权" + }, + "404": { + "description": "公告不存在" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/api/announcements/{id}/read": { + "post": { + "summary": "标记公告为已读", + "tags": [ + "Announcements" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + }, + "description": "公告ID" + } + ], + "responses": { + "200": { + "description": "标记已读成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "已标记为已读" + } + } + } + } + } + }, + "401": { + "description": "未授权" + }, + "404": { + "description": "公告不存在" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/api/announcements/unread/count": { + "get": { + "summary": "获取用户未读公告数量", + "tags": [ + "Announcements" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "获取未读数量成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "data": { + "type": "object", + "properties": { + "unread_count": { + "type": "integer", + "example": 5, + "description": "未读公告数量" + } + } + } + } + } + } + } + }, + "401": { + "description": "未授权" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/api/announcements/batch/read": { + "post": { + "summary": "批量标记公告为已读", + "tags": [ + "Announcements" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "announcement_ids" + ], + "properties": { + "announcement_ids": { + "type": "array", + "items": { + "type": "integer" + }, + "example": [ + 1, + 2, + 3 + ], + "description": "公告ID列表" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "批量标记已读成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "批量标记已读成功" + } + } + } + } + } + }, + "400": { + "description": "请求参数错误" + }, + "401": { + "description": "未授权" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/api/announcements": { + "post": { + "summary": "创建新的通知公告", + "tags": [ + "Announcements" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "title", + "content", + "type", + "priority" + ], + "properties": { + "title": { + "type": "string", + "description": "公告标题", + "example": "系统维护通知" + }, + "content": { + "type": "string", + "description": "公告内容", + "example": "系统将于今晚进行维护,预计维护时间2小时" + }, + "type": { + "type": "string", + "enum": [ + "system", + "activity", + "maintenance", + "urgent" + ], + "default": "system", + "description": "公告类型", + "example": "maintenance" + }, + "priority": { + "type": "string", + "enum": [ + "high", + "medium", + "low", + "urgent" + ], + "default": "medium", + "description": "优先级", + "example": "high" + }, + "status": { + "type": "string", + "enum": [ + "draft", + "published" + ], + "default": "draft", + "description": "状态" + }, + "is_pinned": { + "type": "boolean", + "default": false + }, + "publish_time": { + "type": "string", + "format": "date-time" + }, + "expire_time": { + "type": "string", + "format": "date-time" + }, + "isTop": { + "type": "boolean", + "default": false, + "description": "是否置顶" + }, + "publishTime": { + "type": "string", + "format": "date-time", + "description": "发布时间" + }, + "expireTime": { + "type": "string", + "format": "date-time", + "description": "过期时间" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "公告创建成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "公告创建成功" + }, + "data": { + "type": "object", + "properties": { + "id": { + "type": "integer" + } + }, + "$ref": "#/components/schemas/Announcement" + } + } + } + } + } + }, + "400": { + "description": "请求参数错误" + }, + "401": { + "description": "未授权" + }, + "500": { + "description": "服务器错误" + } + } + }, + "get": { + "summary": "获取通知公告列表", + "tags": [ + "Announcements" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "in": "query", + "name": "page", + "schema": { + "type": "integer", + "default": 1 + }, + "description": "页码" + }, + { + "in": "query", + "name": "limit", + "schema": { + "type": "integer", + "default": 10 + }, + "description": "每页数量" + }, + { + "in": "query", + "name": "search", + "schema": { + "type": "string" + }, + "description": "搜索关键词(标题或内容)" + }, + { + "in": "query", + "name": "type", + "schema": { + "type": "string", + "enum": [ + "system", + "activity", + "maintenance", + "urgent" + ] + }, + "description": "公告类型" + }, + { + "in": "query", + "name": "priority", + "schema": { + "type": "string", + "enum": [ + "high", + "medium", + "low" + ] + }, + "description": "优先级" + }, + { + "in": "query", + "name": "status", + "schema": { + "type": "string", + "enum": [ + "draft", + "published", + "expired" + ] + }, + "description": "状态" + }, + { + "in": "query", + "name": "isTop", + "schema": { + "type": "boolean" + }, + "description": "是否置顶" + }, + { + "in": "query", + "name": "sortBy", + "schema": { + "type": "string", + "enum": [ + "created_at", + "updated_at", + "publish_time", + "priority" + ], + "default": "created_at" + }, + "description": "排序字段" + }, + { + "in": "query", + "name": "sortOrder", + "schema": { + "type": "string", + "enum": [ + "ASC", + "DESC" + ], + "default": "DESC" + }, + "description": "排序方向" + } + ], + "responses": { + "200": { + "description": "成功获取公告列表", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "data": { + "type": "object", + "properties": { + "announcements": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Announcement" + } + }, + "total": { + "type": "integer", + "example": 50 + }, + "page": { + "type": "integer", + "example": 1 + }, + "limit": { + "type": "integer", + "example": 10 + }, + "totalPages": { + "type": "integer", + "example": 5 + } + } + } + } + } + } + } + }, + "401": { + "description": "未授权" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/api/announcements/public/list": { + "get": { + "summary": "获取公开发布的公告列表(无需认证)", + "tags": [ + "Announcements" + ], + "parameters": [ + { + "in": "query", + "name": "limit", + "schema": { + "type": "integer", + "default": 5 + }, + "description": "获取数量" + } + ], + "responses": { + "200": { + "description": "成功获取公开公告列表", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Announcement" + } + } + } + } + } + } + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/api/announcements/{id}/toggle-top": { + "put": { + "summary": "切换公告置顶状态", + "tags": [ + "Announcements" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + }, + "description": "公告ID" + } + ], + "responses": { + "200": { + "description": "置顶状态切换成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "置顶状态更新成功" + }, + "data": { + "type": "object", + "properties": { + "isTop": { + "type": "boolean", + "example": true + } + } + } + } + } + } + } + }, + "401": { + "description": "未授权" + }, + "404": { + "description": "公告不存在" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/api/orders": { + "get": { + "summary": "获取订单列表", + "tags": [ + "Orders" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "in": "query", + "name": "page", + "schema": { + "type": "integer", + "default": 1 + }, + "description": "页码" + }, + { + "in": "query", + "name": "limit", + "schema": { + "type": "integer", + "default": 10 + }, + "description": "每页数量" + }, + { + "in": "query", + "name": "search", + "schema": { + "type": "string" + }, + "description": "搜索关键词" + }, + { + "in": "query", + "name": "orderNumber", + "schema": { + "type": "string" + }, + "description": "订单号" + }, + { + "in": "query", + "name": "username", + "schema": { + "type": "string" + }, + "description": "用户名" + }, + { + "in": "query", + "name": "status", + "schema": { + "type": "string" + }, + "description": "订单状态" + }, + { + "in": "query", + "name": "startDate", + "schema": { + "type": "string", + "format": "date" + }, + "description": "开始日期" + }, + { + "in": "query", + "name": "endDate", + "schema": { + "type": "string", + "format": "date" + }, + "description": "结束日期" + } + ], + "responses": { + "200": { + "description": "成功获取订单列表", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "data": { + "type": "object", + "properties": { + "orders": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Order" + } + }, + "pagination": { + "type": "object", + "properties": { + "page": { + "type": "integer" + }, + "limit": { + "type": "integer" + }, + "total": { + "type": "integer" + }, + "pages": { + "type": "integer" + } + } + } + } + } + } + } + } + } + }, + "401": { + "description": "未授权" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/api/orders/confirm": { + "post": { + "summary": "确认下单", + "tags": [ + "Orders" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "pre_order_id", + "address" + ], + "properties": { + "pre_order_id": { + "type": "integer", + "description": "预订单ID" + }, + "address": { + "type": "object", + "properties": { + "recipient_name": { + "type": "string", + "description": "收货人姓名" + }, + "phone": { + "type": "string", + "description": "收货人电话" + }, + "province": { + "type": "string", + "description": "省份" + }, + "city": { + "type": "string", + "description": "城市" + }, + "district": { + "type": "string", + "description": "区县" + }, + "detail_address": { + "type": "string", + "description": "详细地址" + } + } + } + } + } + } + } + }, + "responses": { + "200": { + "description": "确认下单成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "message": { + "type": "string" + }, + "data": { + "type": "object", + "properties": { + "order_id": { + "type": "integer" + }, + "order_no": { + "type": "string" + } + } + } + } + } + } + } + }, + "400": { + "description": "请求参数错误" + }, + "401": { + "description": "未授权" + }, + "404": { + "description": "预订单不存在" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/api/orders/pre-order/{id}": { + "get": { + "summary": "获取预订单详情", + "tags": [ + "Orders" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + }, + "description": "预订单ID" + } + ], + "responses": { + "200": { + "description": "获取预订单详情成功", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "data": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "order_no": { + "type": "string" + }, + "total_amount": { + "type": "integer" + }, + "total_points": { + "type": "integer" + }, + "total_rongdou": { + "type": "integer" + }, + "status": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "product_id": { + "type": "integer" + }, + "product_name": { + "type": "string" + }, + "quantity": { + "type": "integer" + }, + "price": { + "type": "integer" + }, + "points_price": { + "type": "integer" + }, + "rongdou_price": { + "type": "integer" + }, + "spec_info": { + "type": "object" + } + } + } + } + } + } + } + } + } + } + }, + "401": { + "description": "未授权" + }, + "404": { + "description": "预订单不存在" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/api/orders/{id}": { + "get": { + "summary": "获取单个订单详情", + "tags": [ + "Orders" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + }, + "description": "订单ID" + } + ], + "responses": { + "200": { + "description": "成功获取订单详情", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "data": { + "type": "object", + "properties": { + "order": { + "$ref": "#/components/schemas/Order" + } + } + } + } + } + } + } + }, + "401": { + "description": "未授权" + }, + "404": { + "description": "订单不存在" + }, + "500": { + "description": "服务器错误" + } + } + } + }, + "/products": { + "get": { + "summary": "获取商品列表", + "tags": [ + "Products" + ], + "parameters": [ + { + "in": "query", + "name": "page", + "schema": { + "type": "integer", + "default": 1 + }, + "description": "页码" + }, + { + "in": "query", + "name": "limit", + "schema": { + "type": "integer", + "default": 10 + }, + "description": "每页数量" + }, + { + "in": "query", + "name": "search", + "schema": { + "type": "string" + }, + "description": "搜索关键词" + }, + { + "in": "query", + "name": "category", + "schema": { + "type": "string" + }, + "description": "商品分类" + }, + { + "in": "query", + "name": "status", + "schema": { + "type": "string", + "enum": [ + "active", + "inactive" + ] + }, + "description": "商品状态" + } + ], + "responses": { + "200": { + "description": "成功获取商品列表", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "data": { + "type": "object", + "properties": { + "products": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Product" + } + }, + "pagination": { + "type": "object", + "properties": { + "page": { + "type": "integer" + }, + "limit": { + "type": "integer" + }, + "total": { + "type": "integer" + }, + "pages": { + "type": "integer" + } + } + } + } + } + } + } + } + } + } + } + } + }, + "/products/categories": { + "get": { + "summary": "获取商品分类列表", + "tags": [ + "Products" + ], + "responses": { + "200": { + "description": "成功获取分类列表", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "data": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "/products/hot": { + "get": { + "summary": "获取热门商品", + "tags": [ + "Products" + ], + "parameters": [ + { + "in": "query", + "name": "limit", + "schema": { + "type": "integer", + "default": 10 + }, + "description": "返回数量" + } + ], + "responses": { + "200": { + "description": "成功获取热门商品", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "data": { + "type": "object", + "properties": { + "products": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Product" + } + } + } + } + } + } + } + } + } + } + } + }, + "/products/{id}": { + "get": { + "summary": "获取单个商品详情(包含增强规格信息)", + "tags": [ + "Products" + ], + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + }, + "description": "商品ID" + } + ], + "responses": { + "200": { + "description": "成功获取商品详情,包含完整的规格信息", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "data": { + "$ref": "#/components/schemas/Product", + "type": "object", + "properties": { + "product": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "category": { + "type": "string" + }, + "price": { + "type": "number" + }, + "points_price": { + "type": "number" + }, + "rongdou_price": { + "type": "number" + }, + "stock": { + "type": "integer" + }, + "specifications": { + "type": "array", + "description": "商品规格组合列表(笛卡尔积规格系统)", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "规格组合ID" + }, + "combination_key": { + "type": "string", + "description": "规格组合键(如:1-3-5)" + }, + "spec_display": { + "type": "string", + "description": "规格显示文本(如:颜色:红色 | 尺寸:XL)" + }, + "spec_details": { + "type": "array", + "description": "规格详细信息", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "spec_name": { + "type": "string", + "description": "规格名称" + }, + "spec_display_name": { + "type": "string", + "description": "规格显示名称" + }, + "value": { + "type": "string", + "description": "规格值" + }, + "display_value": { + "type": "string", + "description": "规格显示值" + }, + "color_code": { + "type": "string", + "description": "颜色代码" + }, + "image_url": { + "type": "string", + "description": "规格图片" + } + } + } + }, + "price_adjustment": { + "type": "number", + "description": "价格调整" + }, + "points_adjustment": { + "type": "number", + "description": "积分调整" + }, + "rongdou_adjustment": { + "type": "number", + "description": "融豆调整" + }, + "stock": { + "type": "integer", + "description": "规格库存" + }, + "sku_code": { + "type": "string", + "description": "SKU编码" + }, + "barcode": { + "type": "string", + "description": "条形码" + }, + "weight": { + "type": "number", + "description": "重量" + }, + "volume": { + "type": "number", + "description": "体积" + }, + "actual_price": { + "type": "number", + "description": "实际价格(基础价格+调整)" + }, + "actual_points_price": { + "type": "number", + "description": "实际积分价格" + }, + "actual_rongdou_price": { + "type": "number", + "description": "实际融豆价格" + }, + "is_available": { + "type": "boolean", + "description": "是否有库存" + } + } + } + }, + "specification_count": { + "type": "integer", + "description": "规格总数" + }, + "available_specifications": { + "type": "integer", + "description": "有库存的规格数量" + }, + "attributes": { + "type": "array", + "description": "商品属性" + }, + "isFavorited": { + "type": "boolean", + "description": "是否已收藏" + } + } + } + } + } + } + } + } + } + }, + "404": { + "description": "商品不存在" + } + } + } + }, "/addresses": { "get": { "summary": "获取用户收货地址列表", @@ -2903,511 +5100,6 @@ } } }, - "/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": "服务器错误" - } - } - } - }, - "/api/orders/confirm": { - "post": { - "summary": "确认下单", - "tags": [ - "Orders" - ], - "security": [ - { - "bearerAuth": [] - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "pre_order_id", - "address" - ], - "properties": { - "pre_order_id": { - "type": "integer", - "description": "预订单ID" - }, - "address": { - "type": "object", - "properties": { - "recipient_name": { - "type": "string", - "description": "收货人姓名" - }, - "phone": { - "type": "string", - "description": "收货人电话" - }, - "province": { - "type": "string", - "description": "省份" - }, - "city": { - "type": "string", - "description": "城市" - }, - "district": { - "type": "string", - "description": "区县" - }, - "detail_address": { - "type": "string", - "description": "详细地址" - } - } - } - } - } - } - } - }, - "responses": { - "200": { - "description": "确认下单成功", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean" - }, - "message": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "order_id": { - "type": "integer" - }, - "order_no": { - "type": "string" - } - } - } - } - } - } - } - }, - "400": { - "description": "请求参数错误" - }, - "401": { - "description": "未授权" - }, - "404": { - "description": "预订单不存在" - }, - "500": { - "description": "服务器错误" - } - } - } - }, - "/api/orders/pre-order/{id}": { - "get": { - "summary": "获取预订单详情", - "tags": [ - "Orders" - ], - "security": [ - { - "bearerAuth": [] - } - ], - "parameters": [ - { - "in": "path", - "name": "id", - "required": true, - "schema": { - "type": "integer" - }, - "description": "预订单ID" - } - ], - "responses": { - "200": { - "description": "获取预订单详情成功", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean" - }, - "data": { - "type": "object", - "properties": { - "id": { - "type": "integer" - }, - "order_no": { - "type": "string" - }, - "total_amount": { - "type": "integer" - }, - "total_points": { - "type": "integer" - }, - "total_rongdou": { - "type": "integer" - }, - "status": { - "type": "string" - }, - "created_at": { - "type": "string" - }, - "items": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "integer" - }, - "product_id": { - "type": "integer" - }, - "product_name": { - "type": "string" - }, - "quantity": { - "type": "integer" - }, - "price": { - "type": "integer" - }, - "points_price": { - "type": "integer" - }, - "rongdou_price": { - "type": "integer" - }, - "spec_info": { - "type": "object" - } - } - } - } - } - } - } - } - } - } - }, - "401": { - "description": "未授权" - }, - "404": { - "description": "预订单不存在" - }, - "500": { - "description": "服务器错误" - } - } - } - }, - "/api/orders/{id}": { - "get": { - "summary": "获取单个订单详情", - "tags": [ - "Orders" - ], - "security": [ - { - "bearerAuth": [] - } - ], - "parameters": [ - { - "in": "path", - "name": "id", - "required": true, - "schema": { - "type": "integer" - }, - "description": "订单ID" - } - ], - "responses": { - "200": { - "description": "成功获取订单详情", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean" - }, - "data": { - "type": "object", - "properties": { - "order": { - "$ref": "#/components/schemas/Order" - } - } - } - } - } - } - } - }, - "401": { - "description": "未授权" - }, - "404": { - "description": "订单不存在" - }, - "500": { - "description": "服务器错误" - } - } - } - }, - "/api/orders/create-from-cart": { - "post": { - "summary": "创建预订单", - "tags": [ - "Orders" - ], - "security": [ - { - "bearerAuth": [] - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "cart_item_ids": { - "type": "array", - "items": { - "type": "integer" - }, - "description": "购物车项ID数组" - } - }, - "required": [ - "cart_item_ids" - ] - } - } - } - }, - "responses": { - "201": { - "description": "预订单创建成功", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean" - }, - "message": { - "type": "string" - }, - "data": { - "type": "object", - "properties": { - "preOrderId": { - "type": "integer" - }, - "orderNumber": { - "type": "string" - }, - "totalAmount": { - "type": "integer" - }, - "totalPoints": { - "type": "integer" - }, - "totalRongdou": { - "type": "integer" - }, - "paymentMethods": { - "type": "array", - "items": { - "type": "string" - }, - "description": "去重后的支付方式列表" - } - } - } - } - } - } - } - }, - "400": { - "description": "参数错误或库存不足" - }, - "401": { - "description": "未授权" - }, - "404": { - "description": "购物车商品不存在" - }, - "500": { - "description": "服务器错误" - } - } - } - }, "/api/orders/{id}/cancel": { "put": { "summary": "用户取消订单", @@ -4409,179 +6101,6 @@ } } }, - "/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/hot": { - "get": { - "summary": "获取热销商品", - "tags": [ - "Products" - ], - "responses": { - "200": { - "description": "成功获取热销商品", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean" - }, - "data": { - "type": "object", - "properties": { - "products": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Product" - } - } - } - } - } - } - } - } - } - } - } - }, "/products/flash-sale": { "get": { "summary": "获取秒杀商品", @@ -4618,198 +6137,6 @@ } } }, - "/products/{id}": { - "get": { - "summary": "获取单个商品详情(包含增强规格信息)", - "tags": [ - "Products" - ], - "parameters": [ - { - "in": "path", - "name": "id", - "schema": { - "type": "integer" - }, - "required": true, - "description": "商品ID" - } - ], - "responses": { - "200": { - "description": "成功获取商品详情,包含完整的规格信息", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "example": true - }, - "data": { - "type": "object", - "properties": { - "product": { - "type": "object", - "properties": { - "id": { - "type": "integer" - }, - "name": { - "type": "string" - }, - "category": { - "type": "string" - }, - "price": { - "type": "number" - }, - "points_price": { - "type": "number" - }, - "rongdou_price": { - "type": "number" - }, - "stock": { - "type": "integer" - }, - "specifications": { - "type": "array", - "description": "商品规格组合列表(笛卡尔积规格系统)", - "items": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "description": "规格组合ID" - }, - "combination_key": { - "type": "string", - "description": "规格组合键(如:1-3-5)" - }, - "spec_display": { - "type": "string", - "description": "规格显示文本(如:颜色:红色 | 尺寸:XL)" - }, - "spec_details": { - "type": "array", - "description": "规格详细信息", - "items": { - "type": "object", - "properties": { - "id": { - "type": "integer" - }, - "spec_name": { - "type": "string", - "description": "规格名称" - }, - "spec_display_name": { - "type": "string", - "description": "规格显示名称" - }, - "value": { - "type": "string", - "description": "规格值" - }, - "display_value": { - "type": "string", - "description": "规格显示值" - }, - "color_code": { - "type": "string", - "description": "颜色代码" - }, - "image_url": { - "type": "string", - "description": "规格图片" - } - } - } - }, - "price_adjustment": { - "type": "number", - "description": "价格调整" - }, - "points_adjustment": { - "type": "number", - "description": "积分调整" - }, - "rongdou_adjustment": { - "type": "number", - "description": "融豆调整" - }, - "stock": { - "type": "integer", - "description": "规格库存" - }, - "sku_code": { - "type": "string", - "description": "SKU编码" - }, - "barcode": { - "type": "string", - "description": "条形码" - }, - "weight": { - "type": "number", - "description": "重量" - }, - "volume": { - "type": "number", - "description": "体积" - }, - "actual_price": { - "type": "number", - "description": "实际价格(基础价格+调整)" - }, - "actual_points_price": { - "type": "number", - "description": "实际积分价格" - }, - "actual_rongdou_price": { - "type": "number", - "description": "实际融豆价格" - }, - "is_available": { - "type": "boolean", - "description": "是否有库存" - } - } - } - }, - "specification_count": { - "type": "integer", - "description": "规格总数" - }, - "available_specifications": { - "type": "integer", - "description": "有库存的规格数量" - }, - "attributes": { - "type": "array", - "description": "商品属性" - }, - "isFavorited": { - "type": "boolean", - "description": "是否已收藏" - } - } - } - } - } - } - } - } - } - }, - "404": { - "description": "商品不存在" - } - } - } - }, "/regions/zhejiang": { "get": { "summary": "获取浙江省所有地区数据", @@ -7252,6 +8579,18 @@ } }, "tags": [ + { + "name": "Announcements", + "description": "通知公告管理API" + }, + { + "name": "Orders", + "description": "订单管理API" + }, + { + "name": "Products", + "description": "商品管理API" + }, { "name": "Authentication", "description": "用户认证API" @@ -7272,18 +8611,10 @@ "name": "MatchingAdmin", "description": "匹配订单管理员相关接口" }, - { - "name": "Orders", - "description": "订单管理相关接口" - }, { "name": "Points", "description": "积分管理相关接口" }, - { - "name": "Products", - "description": "商品管理API" - }, { "name": "Regions", "description": "地区数据API" diff --git a/cert/apiclient_cert.p12 b/cert/apiclient_cert.p12 new file mode 100644 index 0000000..425cddd Binary files /dev/null and b/cert/apiclient_cert.p12 differ diff --git a/cert/apiclient_cert.pem b/cert/apiclient_cert.pem new file mode 100644 index 0000000..ee924ac --- /dev/null +++ b/cert/apiclient_cert.pem @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIEKDCCAxCgAwIBAgIUXysc+VIXyKce5hqbA2vg9d9uH7kwDQYJKoZIhvcNAQEL +BQAwXjELMAkGA1UEBhMCQ04xEzARBgNVBAoTClRlbnBheS5jb20xHTAbBgNVBAsT +FFRlbnBheS5jb20gQ0EgQ2VudGVyMRswGQYDVQQDExJUZW5wYXkuY29tIFJvb3Qg +Q0EwHhcNMjUwOTAyMDg0NDA0WhcNMzAwOTAxMDg0NDA0WjCBgTETMBEGA1UEAwwK +MTcyNjM3NzMzNjEbMBkGA1UECgwS5b6u5L+h5ZWG5oi357O757ufMS0wKwYDVQQL +DCTlroHms6Lngqzono3mrYbliJvnp5HmioDmnInpmZDlhazlj7gxCzAJBgNVBAYT +AkNOMREwDwYDVQQHDAhTaGVuWmhlbjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBAKncf/y0dT12nyCmnK5w3ECQgtoHrU32LqQH/N5SUbiJVbkys99Qq41D +MO0539Ki2MBuOuDHnWOrgNbRhH4WMXS8qodTcWu39cgVlnmw/KtQJdyInrPynFYH +lWO6boc1jfM4UEbEk90iaLM49uFo75+bCNGZVHOt6UMmTNFB9zHDfnhm+UaxNJOf +p1z7sntmD3H98c4ghzcE5L51KL6A7JtW+cp0zTpOunj0T/drvmRoyQVzqZ4IlXga +9pJ/6Un2fVNn8pZL804ldKumAf5KVKmqEmz64ydh3896MoAshT/UJIHDuduN+Jbk +gROt9QiM68jR6CuqeRsV7EZ1OA/BLI8CAwEAAaOBuTCBtjAJBgNVHRMEAjAAMAsG +A1UdDwQEAwID+DCBmwYDVR0fBIGTMIGQMIGNoIGKoIGHhoGEaHR0cDovL2V2Y2Eu +aXRydXMuY29tLmNuL3B1YmxpYy9pdHJ1c2NybD9DQT0xQkQ0MjIwRTUwREJDMDRC +MDZBRDM5NzU0OTg0NkMwMUMzRThFQkQyJnNnPUhBQ0M0NzFCNjU0MjJFMTJCMjdB +OUQzM0E4N0FEMUNERjU5MjZFMTQwMzcxMA0GCSqGSIb3DQEBCwUAA4IBAQCzyJZ/ ++rnvUd1kJ74KCb6kxAwM/bX8w5lhkdUkeyQxdmbUCXCrkOGJ8uRQMfiK93eeET4h +KMrZywQHvL1E5WXQpUmQZVYj6eAxaUO+RoW8wPeWb5x/LbSXqQCrCNF6U+AvC6wj +6haW8TK8egCLsjxPBXL9NjkxvcsIOIM8F6JKhMSAAjT7F1nkXthyxC50o/Mbox6l +YfvVZ70gWuUR2e6o9Sob1tTq6YwSwDr5OMwJT8QpDEdjAbWLLtv7a+ApzfqWeTHP +dmVawsaQnOzFaUPDtEGYSh23/eaTrJ9DHpgtubtU/CQsGos6QHD6huRYMmvw3Wml +mFAVWux4DJ8lZXGZ +-----END CERTIFICATE----- diff --git a/cert/apiclient_key.pem b/cert/apiclient_key.pem new file mode 100644 index 0000000..e87a2d3 --- /dev/null +++ b/cert/apiclient_key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCp3H/8tHU9dp8g +ppyucNxAkILaB61N9i6kB/zeUlG4iVW5MrPfUKuNQzDtOd/SotjAbjrgx51jq4DW +0YR+FjF0vKqHU3Frt/XIFZZ5sPyrUCXciJ6z8pxWB5Vjum6HNY3zOFBGxJPdImiz +OPbhaO+fmwjRmVRzrelDJkzRQfcxw354ZvlGsTSTn6dc+7J7Zg9x/fHOIIc3BOS+ +dSi+gOybVvnKdM06Trp49E/3a75kaMkFc6meCJV4GvaSf+lJ9n1TZ/KWS/NOJXSr +pgH+SlSpqhJs+uMnYd/PejKALIU/1CSBw7nbjfiW5IETrfUIjOvI0egrqnkbFexG +dTgPwSyPAgMBAAECggEAF5dGN0Sg285zv0ckj52hGV54retPCHrec22gkwf/zY9V +VolSLfu4N8BTNT9KdKilTeSBTOKsW0FgfXVP32sZp0rkrDLMl9dOzWEiKviHvws8 +lupqkDdruw8Gknk8DI9FjbgOfiWjG51ByVJqB1hZn2Ma0HFpJz/KG8df99gfisuP +3s4s/N8gChKRobNJzoFTVlEexVwM8vPZl7foxm9SEc0tbXyU2yB+pglVv/BkkLo5 +aZFNIC2TVnGX1WJlTTfuhng+MMeckSIPjJGH9c7RpgAChKGOBFOAK87/1+3tep47 +qp3i2WnceIxy2Y/wIT4cGWUAd0Ghi/kkIo8KqpagEQKBgQDfiwhBitymWc5QRr+R +LM6+Yu/EWlpd4/ZrcIgX9S5Z/La4gs/fqyWyKp9r/KjyYKG5sEfArim3NenvQBac +Vp99kNpjAl6SCSWFNeA7aY+zLDJBRHSaG4ovxWXk1ZYD2hBLKahzp7PCdcNvPICU +60AapDMiGlfbcJDot1uP+9i16wKBgQDChiW9qroJGfZcpe5nkh38orPOp7or09mG +perNOAXGghJMV7tc1Se+ttjcvlfK6hrQcxPCMccH/x0bmeZ/MaG1RZjZS2zAXqH5 +9huZtgIHc2idVS+j1t4HsszuIBp84c6ykCU+6NltW1LQFRZ9Y2HuA5HW/fgyhLMG +RHpRdgfG7QKBgDd9P5NlcNgqOrhal3rl8Hv5+yJ2ezALQkPxLxcjWVolDQZIEmmn +BjhvtBsOILHporuBMo51rQ05aNRmyDYOmpCEwHELSYZelt22Pe8BiRYkxmTFJVyL +sYWiLmTbT92s55aAxLvQySJgMR8Pmatdqg/y6m5ws5ZZHt9lhGj9TxH5AoGAR8Rc +WjyJxF/av9XMPlPvUkzoz76b9h2D7KR8G1im8NT+UUIw8xAFSNyG5/Ily8xRNkSu +rn/U8YNSxuMh4h16jrltqgWkythfJCyDhFNdLkiK+Tj7iZP1eJuj9drMSvS4YLLD +uxEHXsxJolGVaY9oCvswLESo9GJ29kH/atyEBAUCgYB+xm2NI9OpF+dSq5O8vMiW +NtlD744WfsiBDzatQz1A9PAB6cyLi03zjW10+HCNZRxBSfI8au/sn9lOdcCjvSGk +8tjZXMzL71jDhEYs4qTUaiKJ2YpbLNcLFeTy48hWeRpew7t7bunksx8zb+0W14M2 +zDmIHi+CSve59dOzoiYuNg== +-----END PRIVATE KEY----- diff --git a/cert/证书使用说明.txt b/cert/证书使用说明.txt new file mode 100644 index 0000000..9a0aab1 --- /dev/null +++ b/cert/证书使用说明.txt @@ -0,0 +1,18 @@ +欢迎使用微信支付! +附件中的三份文件(证书pkcs12格式、证书pem格式、证书密钥pem格式),为接口中强制要求时需携带的证书文件。 +证书属于敏感信息,请妥善保管不要泄露和被他人复制。 +不同开发语言下的证书格式不同,以下为说明指引: + 证书pkcs12格式(apiclient_cert.p12) + 包含了私钥信息的证书文件,为p12(pfx)格式,由微信支付签发给您用来标识和界定您的身份 + 部分安全性要求较高的API需要使用该证书来确认您的调用身份 + windows上可以直接双击导入系统,导入过程中会提示输入证书密码,证书密码默认为您的商户号(如:1900006031) + 证书pem格式(apiclient_cert.pem) + 从apiclient_cert.p12中导出证书部分的文件,为pem格式,请妥善保管不要泄漏和被他人复制 + 部分开发语言和环境,不能直接使用p12文件,而需要使用pem,所以为了方便您使用,已为您直接提供 + 您也可以使用openssl命令来自己导出:openssl pkcs12 -clcerts -nokeys -in apiclient_cert.p12 -out apiclient_cert.pem + 证书密钥pem格式(apiclient_key.pem) + 从apiclient_cert.p12中导出密钥部分的文件,为pem格式 + 部分开发语言和环境,不能直接使用p12文件,而需要使用pem,所以为了方便您使用,已为您直接提供 + 您也可以使用openssl命令来自己导出:openssl pkcs12 -nocerts -in apiclient_cert.p12 -out apiclient_key.pem +备注说明: + 由于绝大部分操作系统已内置了微信支付服务器证书的根CA证书, 2018年3月6日后, 不再提供CA证书文件(rootca.pem)下载 \ No newline at end of file diff --git a/certs/README.md b/certs/README.md new file mode 100644 index 0000000..bef908a --- /dev/null +++ b/certs/README.md @@ -0,0 +1,34 @@ +# 支付宝支付证书配置指南 + +## 证书文件说明 + +本目录需要包含以下证书文件,用于支付宝支付V3版本API的证书模式: + +1. `appCertPublicKey.crt` - 应用公钥证书(已存在) +2. `alipayRootCert.crt` - 支付宝根证书(需下载) +3. `alipayCertPublicKey_RSA2.crt` - 支付宝公钥证书(需下载) + +## 如何获取证书 + +### 已有证书 +- `appCertPublicKey.crt` - 应用公钥证书已存在于本目录 + +### 需要下载的证书 +请登录支付宝开放平台,从您的应用详情页面下载以下证书: + +1. **支付宝根证书**:下载后重命名为 `alipayRootCert.crt` 并放入本目录 +2. **支付宝公钥证书**:下载后重命名为 `alipayCertPublicKey_RSA2.crt` 并放入本目录 + +## 证书下载步骤 + +1. 登录[支付宝开放平台](https://open.alipay.com/) +2. 进入「开发者中心」->「我的应用」 +3. 选择对应的应用 +4. 点击「开发设置」 +5. 在「接口加签方式」区域,选择「公钥证书」 +6. 下载「支付宝根证书」和「支付宝公钥证书」 +7. 将下载的证书放入本目录,并按上述名称重命名 + +## 配置验证 + +证书配置完成后,系统将使用证书模式进行支付宝API调用,支持V3版本的接口。 \ No newline at end of file diff --git a/certs/alipay-private-key.pem b/certs/alipay-private-key.pem new file mode 100644 index 0000000..a908ace --- /dev/null +++ b/certs/alipay-private-key.pem @@ -0,0 +1 @@ +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCV9dEIPimkwxhJXXy2cntNZqeZdV4JAdx7ZpnvbjV6OjeEZczksKFa6lgsYR6Wq6G/khEdVRyNW4RsepBRwVGDBxxUN0nB9uTtPti220PtXZIJycLY585nL/iaUjUt+Cyh2qsyIGQ0lGx9VFWU+x5stGCLgfZbHOaMo0S+49upEsX5p4MyRbc4zA/vf+xgmGm5f1YB1Nwm32JBhVRourmHhi82MLX3Hxwo6R/SIOrDIcUrELYC9XRC2LUWYJIsQ1QMtrE/0XVFmIB68gU9JqRTNXhLCyL19K3WUGvRXG9N5mTaZSBIYco7ZrtbTFHtIxSOYxqYTOtkPKVYxGn1I2WHAgMBAAECggEAEeaA6Dn7YJaFPKSzMVgjDc82LGRNMEgPmI9byq/eJFP0spIwThAjgqW8lreVHikoqqR19IlnWhxVh1luBsRLxZdAs3DSFhwxoXxCBDnSNvBXcWGrJ5csFTcttsYfYPhh44QlsVsaewhIlwFNIfaD4Df72ktOK/wcLSeEGkE6xEixfMcLawg6a8rkHbvJtppFdTSL2YBB4X+dnt3cYCxFYch7Q5+RF+Wqr++860B5ztBRSX8xmqEIDS/95tf09qDyhyrnE2a2wh/jT+BWzvILKNc8goNo29kACo8bE9p+rWCnG8YsL+4emlrxk6/OYzR5lG6xZNvXAmnY0LDKzQsqeQKBgQDTm/uQP82o0ez9YHP/sAvTVq7pIJlEnaZqI2jBvyGxf6fDTU5b0yfJF9VO5EoPVT9CHRJ7A0T4RUDhIAzo9fjrz3VjkXO/RZyOIHsptjvOFeKCiFGrHO8sKxC5i4IZIRwIB7A84DU7rVzQ5Kh3XRqi8srKl/DBXo1BloWU0HJetQKBgQC1axqVH/eIwZrYjSbXgSHwGYMhH0YmOIJEYavD6TmOp9QL5N1ax6B0r/BIVt7XAIRck8kueKY4LeRulq3azXIhBy5Qx6sp2Sb/SX3vrPrvA/ImoDQKR4CeL5zXQJkqbVgbbS//UTEWilKyQXNz13D4qbCqNBbLJaDirXqBT0ycywKBgCMvT2/XvAlzBlXHAOKl0gGM6z5mJjXrhK0nQBbfAeoykKF/rCTGgloEdXpNqSbNhNwoW1dK3t/tG/GS07K0m3QSJbGtkLJgD7zuF6yC2YTVzLjpk7LA99+/NWO0l6g4AiIvrRUiLpfCpqkxK/XU7EXl2uQ+yVBNuW0LayCoXCv1AoGAU/CJbSRMUO9baQTuStoJzODRBltFBtwwkdkrM0tPAU1v1E0BikZBXJwnLiFbm9k2ZOtQM3tJVUcOoYiASnOyccuzx1aLQKKj44yqg2Hi/QIzYWHQkk0BGq/m/sV52OKc2JvNkHGNp+M6XhXgiGHPeI5zGl1dioMPjLI9s2Twir8CgYEAn5wAbpx2imCPDtKgMxb0zxbKB5kq37QGuIRRAoorPkFr0Z7xSZs1M0qkhZcaX/kfnNgigPVwpUeUDQmETBRaXrCuXxRkOXGo1st4v5gn9kgQYBCv0dVbV1QNnnXp2fAtQ/3Jw6l9w4T0PgPBhu9TPnSTVZA0YldNo7TEEL5kEbA= \ No newline at end of file diff --git a/certs/alipay-public-key.pem b/certs/alipay-public-key.pem new file mode 100644 index 0000000..89738c0 --- /dev/null +++ b/certs/alipay-public-key.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlfXRCD4ppMMYSV18tnJ7 +TWanmXVeCQHce2aZ7241ejo3hGXM5LChWupYLGEelquhv5IRHVUcjVuEbHqQUcFR +gwccVDdJwfbk7T7YtttD7V2SCcnC2OfOZy/4mlI1LfgsodqrMiBkNJRsfVRVlPse +bLRgi4H2WxzmjKNEvuPbqRLF+aeDMkW3OMwP73/sYJhpuX9WAdTcJt9iQYVUaLq5 +h4YvNjC19x8cKOkf0iDqwyHFKxC2AvV0Qti1FmCSLENUDLaxP9F1RZiAevIFPSak +UzV4Swsi9fSt1lBr0VxvTeZk2mUgSGHKO2a7W0xR7SMUjmMamEzrZDylWMRp9SNl +hwIDAQAB +-----END PUBLIC KEY----- \ No newline at end of file diff --git a/certs/alipayCertPublicKey_RSA2.crt b/certs/alipayCertPublicKey_RSA2.crt new file mode 100644 index 0000000..60f26b0 --- /dev/null +++ b/certs/alipayCertPublicKey_RSA2.crt @@ -0,0 +1,3 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5swLKPSzOMucRC52c9kKJZI9cYWDFd+s3UuE+aDtWodGrGV8g3szmp7hUWlaWY/didKc9vQNq93y67eEyw6QsMn26WwlzDbgP0xTcHEt+qDCeAltSqf6MX3KPmlz0f/DNneR9DR9ZGwaW1ATY3kg8gj+kIWngrqgjOv37UJWEpQOxUfWDGTBC1zzhC0PTXY7lX3GUZmDEtDtBs1BsFUdk995TbTD1cTiyDFuea49br0dovmU1ROOg6vK3G9xDd4Mke/opDunLTHe63+fBCnB7FyZ9F8zWg4LYND1QPmIX2m5gwICBHhNm8WqIfp9T64vpAxlM74BEsMlv3hNy0INQQIDAQAB +-----END PUBLIC KEY----- \ No newline at end of file diff --git a/certs/alipayPublicKey_RSA2.crt b/certs/alipayPublicKey_RSA2.crt new file mode 100644 index 0000000..60f26b0 --- /dev/null +++ b/certs/alipayPublicKey_RSA2.crt @@ -0,0 +1,3 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5swLKPSzOMucRC52c9kKJZI9cYWDFd+s3UuE+aDtWodGrGV8g3szmp7hUWlaWY/didKc9vQNq93y67eEyw6QsMn26WwlzDbgP0xTcHEt+qDCeAltSqf6MX3KPmlz0f/DNneR9DR9ZGwaW1ATY3kg8gj+kIWngrqgjOv37UJWEpQOxUfWDGTBC1zzhC0PTXY7lX3GUZmDEtDtBs1BsFUdk995TbTD1cTiyDFuea49br0dovmU1ROOg6vK3G9xDd4Mke/opDunLTHe63+fBCnB7FyZ9F8zWg4LYND1QPmIX2m5gwICBHhNm8WqIfp9T64vpAxlM74BEsMlv3hNy0INQQIDAQAB +-----END PUBLIC KEY----- \ No newline at end of file diff --git a/certs/appCertPublicKey.crt b/certs/appCertPublicKey.crt new file mode 100644 index 0000000..d13ac61 --- /dev/null +++ b/certs/appCertPublicKey.crt @@ -0,0 +1,3 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlfXRCD4ppMMYSV18tnJ7TWanmXVeCQHce2aZ7241ejo3hGXM5LChWupYLGEelquhv5IRHVUcjVuEbHqQUcFRgwccVDdJwfbk7T7YtttD7V2SCcnC2OfOZy/4mlI1LfgsodqrMiBkNJRsfVRVlPsebLRgi4H2WxzmjKNEvuPbqRLF+aeDMkW3OMwP73/sYJhpuX9WAdTcJt9iQYVUaLq5h4YvNjC19x8cKOkf0iDqwyHFKxC2AvV0Qti1FmCSLENUDLaxP9F1RZiAevIFPSakUzV4Swsi9fSt1lBr0VxvTeZk2mUgSGHKO2a7W0xR7SMUjmMamEzrZDylWMRp9SNlhwIDAQAB +-----END PUBLIC KEY----- \ No newline at end of file diff --git a/config/database-init.js b/config/database-init.js index 119de5f..7a7d115 100644 --- a/config/database-init.js +++ b/config/database-init.js @@ -574,7 +574,48 @@ async function createTables() { FOREIGN KEY (order_id) REFERENCES matching_orders(id) ON DELETE SET NULL ) `); -} + + // 通知公告表 + await getDB().execute(` + CREATE TABLE IF NOT EXISTS announcements ( + id INT AUTO_INCREMENT PRIMARY KEY, + title VARCHAR(255) NOT NULL COMMENT '公告标题', + content TEXT NOT NULL COMMENT '公告内容', + type ENUM('system', 'maintenance', 'promotion', 'warning') DEFAULT 'system' COMMENT '公告类型', + priority ENUM('low', 'medium', 'high', 'urgent') DEFAULT 'medium' COMMENT '优先级', + status ENUM('draft', 'published', 'archived') DEFAULT 'draft' COMMENT '状态', + is_pinned BOOLEAN DEFAULT FALSE COMMENT '是否置顶', + publish_time TIMESTAMP NULL COMMENT '发布时间', + expire_time TIMESTAMP NULL COMMENT '过期时间', + created_by INT NOT NULL COMMENT '创建者ID', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE CASCADE, + INDEX idx_status (status), + INDEX idx_type (type), + INDEX idx_publish_time (publish_time), + INDEX idx_created_at (created_at) + ) COMMENT='通知公告表' + `); + + // 用户公告阅读状态表 + await getDB().execute(` + CREATE TABLE IF NOT EXISTS user_announcement_reads ( + id INT AUTO_INCREMENT PRIMARY KEY, + user_id INT NOT NULL COMMENT '用户ID', + announcement_id INT NOT NULL COMMENT '公告ID', + is_read BOOLEAN DEFAULT FALSE COMMENT '是否已读', + read_at TIMESTAMP NULL COMMENT '阅读时间', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + UNIQUE KEY unique_user_announcement (user_id, announcement_id), + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (announcement_id) REFERENCES announcements(id) ON DELETE CASCADE, + INDEX idx_user_id (user_id), + INDEX idx_announcement_id (announcement_id), + INDEX idx_is_read (is_read) + ) COMMENT='用户公告阅读状态表' + `);} /** * 添加缺失的字段(处理数据库升级) @@ -862,13 +903,13 @@ async function createDefaultData() { } // 初始化浙江省区域数据 - await initializeZhejiangRegions(); + // await initializeZhejiangRegions(); // 初始化默认地址标签 - await initializeDefaultAddressLabels(); + // await initializeDefaultAddressLabels(); // 初始化全国省市区数据 - await initializeChinaRegions(); + // await initializeChinaRegions(); } /** diff --git a/config/wechatPay.js b/config/wechatPay.js new file mode 100644 index 0000000..16b61f9 --- /dev/null +++ b/config/wechatPay.js @@ -0,0 +1,24 @@ +// 微信支付配置 +module.exports = { + // 微信支付配置 + wechatPay: { + appId: process.env.WECHAT_APP_ID || '', // 微信公众号AppID + mchId: process.env.WECHAT_MCH_ID || '', // 商户号 + apiKey: process.env.WECHAT_API_KEY || '', // API密钥 + apiV3Key: process.env.WECHAT_API_V3_KEY || '', // APIv3密钥 + notifyUrl: process.env.WECHAT_NOTIFY_URL || 'https://your-domain.com/api/wechat/notify', // 支付回调地址 + + // 证书路径(生产环境需要配置) + certPath: process.env.WECHAT_CERT_PATH || '', + keyPath: process.env.WECHAT_KEY_PATH || '', + + // 支付相关配置 + tradeType: { + h5: 'MWEB', // H5支付 + jsapi: 'JSAPI' // 公众号支付 + }, + + // 注册费用配置(单位:分) + registrationFee: 100 // 1元注册费 + } +}; \ No newline at end of file diff --git a/docs/README.md b/docs/README.md index 6d5cb13..73d8db2 100644 --- a/docs/README.md +++ b/docs/README.md @@ -11,10 +11,12 @@ docs/ │ ├── product.js # 商品相关数据模型 │ ├── order.js # 订单相关数据模型 │ ├── user.js # 用户相关数据模型 -│ └── cart.js # 购物车相关数据模型 +│ ├── cart.js # 购物车相关数据模型 +│ └── announcement.js # 通知公告相关数据模型 └── apis/ # API 接口定义 ├── products.js # 商品相关 API - └── orders.js # 订单相关 API + ├── orders.js # 订单相关 API + └── announcements.js # 通知公告相关 API ``` ## 优势 diff --git a/docs/apis/announcements.js b/docs/apis/announcements.js new file mode 100644 index 0000000..12c5eea --- /dev/null +++ b/docs/apis/announcements.js @@ -0,0 +1,736 @@ +/** + * @swagger + * tags: + * name: Announcements + * description: 通知公告管理API + */ + +/** + * @swagger + * /api/announcements/{id}: + * get: + * summary: 获取单个公告详情 + * tags: [Announcements] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: 公告ID + * responses: + * 200: + * description: 成功获取公告详情 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * data: + * $ref: '#/components/schemas/Announcement' + * 401: + * description: 未授权 + * 404: + * description: 公告不存在 + * 500: + * description: 服务器错误 + */ + +/** + * @swagger + * /api/announcements/{id}/read: + * post: + * summary: 标记公告为已读 + * tags: [Announcements] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: 公告ID + * responses: + * 200: + * description: 标记已读成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: "已标记为已读" + * 401: + * description: 未授权 + * 404: + * description: 公告不存在 + * 500: + * description: 服务器错误 + */ + +/** + * @swagger + * /api/announcements/unread/count: + * get: + * summary: 获取用户未读公告数量 + * tags: [Announcements] + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: 获取未读数量成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * data: + * type: object + * properties: + * unread_count: + * type: integer + * example: 5 + * description: 未读公告数量 + * 401: + * description: 未授权 + * 500: + * description: 服务器错误 + */ + +/** + * @swagger + * /api/announcements/batch/read: + * post: + * summary: 批量标记公告为已读 + * tags: [Announcements] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - announcement_ids + * properties: + * announcement_ids: + * type: array + * items: + * type: integer + * example: [1, 2, 3] + * description: 公告ID列表 + * responses: + * 200: + * description: 批量标记已读成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: "批量标记已读成功" + * 400: + * description: 请求参数错误 + * 401: + * description: 未授权 + * 500: + * description: 服务器错误 + */ + +/** + * @swagger + * /api/announcements: + * post: + * summary: 创建新公告 + * tags: [Announcements] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - title + * - content + * properties: + * title: + * type: string + * description: 公告标题 + * content: + * type: string + * description: 公告内容 + * type: + * type: string + * enum: [system, maintenance, promotion, warning] + * default: system + * priority: + * type: string + * enum: [low, medium, high, urgent] + * default: medium + * status: + * type: string + * enum: [draft, published] + * default: draft + * is_pinned: + * type: boolean + * default: false + * publish_time: + * type: string + * format: date-time + * expire_time: + * type: string + * format: date-time + * responses: + * 201: + * description: 公告创建成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * message: + * type: string + * data: + * type: object + * properties: + * id: + * type: integer + * 400: + * description: 请求参数错误 + * 401: + * description: 未授权 + * 500: + * description: 服务器错误 + */ + +/** + * @swagger + * /api/announcements/{id}: + * put: + * summary: 更新公告 + * tags: [Announcements] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: 公告ID + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * title: + * type: string + * content: + * type: string + * type: + * type: string + * enum: [system, maintenance, promotion, warning] + * priority: + * type: string + * enum: [low, medium, high, urgent] + * status: + * type: string + * enum: [draft, published, archived] + * is_pinned: + * type: boolean + * publish_time: + * type: string + * format: date-time + * expire_time: + * type: string + * format: date-time + * responses: + * 200: + * description: 公告更新成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * message: + * type: string + * data: + * $ref: '#/components/schemas/Announcement' + * 400: + * description: 请求参数错误 + * 401: + * description: 未授权 + * 404: + * description: 公告不存在 + * 500: + * description: 服务器错误 + * + * delete: + * summary: 删除公告 + * tags: [Announcements] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: 公告ID + * responses: + * 200: + * description: 公告删除成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * message: + * type: string + * 401: + * description: 未授权 + * 404: + * description: 公告不存在 + * 500: + * description: 服务器错误 + */ + +/** + * @swagger + * /api/announcements/public/list: + * get: + * summary: 获取公开发布的公告列表(无需认证) + * tags: [Announcements] + * parameters: + * - in: query + * name: limit + * schema: + * type: integer + * default: 5 + * description: 获取数量 + * responses: + * 200: + * description: 成功获取公开公告列表 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * data: + * type: array + * items: + * $ref: '#/components/schemas/Announcement' + * 500: + * description: 服务器错误 + */ + +/** + * @swagger + * components: + * schemas: + * Announcement: + * type: object + * required: + * - title + * - content + * properties: + * id: + * type: integer + * description: 公告ID + * title: + * type: string + * description: 公告标题 + * content: + * type: string + * description: 公告内容 + * type: + * type: string + * description: 公告类型 + * enum: [system, maintenance, promotion, warning] + * priority: + * type: string + * description: 优先级 + * enum: [low, medium, high, urgent] + * status: + * type: string + * description: 状态 + * enum: [draft, published, archived] + * is_pinned: + * type: boolean + * description: 是否置顶 + * publish_time: + * type: string + * format: date-time + * description: 发布时间 + * expire_time: + * type: string + * format: date-time + * description: 过期时间 + * created_by: + * type: integer + * description: 创建者ID + * created_at: + * type: string + * format: date-time + * description: 创建时间 + * updated_at: + * type: string + * format: date-time + * description: 更新时间 + */ + +/** + * @swagger + * /api/announcements: + * get: + * summary: 获取通知公告列表 + * tags: [Announcements] + * security: + * - bearerAuth: [] + * parameters: + * - in: query + * name: page + * schema: + * type: integer + * default: 1 + * description: 页码 + * - in: query + * name: limit + * schema: + * type: integer + * default: 10 + * description: 每页数量 + * - in: query + * name: search + * schema: + * type: string + * description: 搜索关键词(标题或内容) + * - in: query + * name: type + * schema: + * type: string + * enum: [system, activity, maintenance, urgent] + * description: 公告类型 + * - in: query + * name: priority + * schema: + * type: string + * enum: [high, medium, low] + * description: 优先级 + * - in: query + * name: status + * schema: + * type: string + * enum: [draft, published, expired] + * description: 状态 + * - in: query + * name: isTop + * schema: + * type: boolean + * description: 是否置顶 + * - in: query + * name: sortBy + * schema: + * type: string + * enum: [created_at, updated_at, publish_time, priority] + * default: created_at + * description: 排序字段 + * - in: query + * name: sortOrder + * schema: + * type: string + * enum: [ASC, DESC] + * default: DESC + * description: 排序方向 + * responses: + * 200: + * description: 成功获取公告列表 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * data: + * type: object + * properties: + * announcements: + * type: array + * items: + * $ref: '#/components/schemas/Announcement' + * total: + * type: integer + * example: 50 + * page: + * type: integer + * example: 1 + * limit: + * type: integer + * example: 10 + * totalPages: + * type: integer + * example: 5 + * 401: + * description: 未授权 + * 500: + * description: 服务器错误 + * + * post: + * summary: 创建新的通知公告 + * tags: [Announcements] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - title + * - content + * - type + * - priority + * properties: + * title: + * type: string + * description: 公告标题 + * example: "系统维护通知" + * content: + * type: string + * description: 公告内容 + * example: "系统将于今晚进行维护,预计维护时间2小时" + * type: + * type: string + * enum: [system, activity, maintenance, urgent] + * description: 公告类型 + * example: "maintenance" + * priority: + * type: string + * enum: [high, medium, low] + * description: 优先级 + * example: "high" + * status: + * type: string + * enum: [draft, published] + * default: draft + * description: 状态 + * isTop: + * type: boolean + * default: false + * description: 是否置顶 + * publishTime: + * type: string + * format: date-time + * description: 发布时间 + * expireTime: + * type: string + * format: date-time + * description: 过期时间 + * responses: + * 201: + * description: 公告创建成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: "公告创建成功" + * data: + * $ref: '#/components/schemas/Announcement' + * 400: + * description: 请求参数错误 + * 401: + * description: 未授权 + * 500: + * description: 服务器错误 + */ + +/** + * @swagger + * /api/announcements/{id}: + * put: + * summary: 更新通知公告 + * tags: [Announcements] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: 公告ID + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * title: + * type: string + * description: 公告标题 + * content: + * type: string + * description: 公告内容 + * type: + * type: string + * enum: [system, activity, maintenance, urgent] + * description: 公告类型 + * priority: + * type: string + * enum: [high, medium, low] + * description: 优先级 + * status: + * type: string + * enum: [draft, published, expired] + * description: 状态 + * isTop: + * type: boolean + * description: 是否置顶 + * publishTime: + * type: string + * format: date-time + * description: 发布时间 + * expireTime: + * type: string + * format: date-time + * description: 过期时间 + * responses: + * 200: + * description: 公告更新成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: "公告更新成功" + * data: + * $ref: '#/components/schemas/Announcement' + * 400: + * description: 请求参数错误 + * 401: + * description: 未授权 + * 404: + * description: 公告不存在 + * 500: + * description: 服务器错误 + * + * delete: + * summary: 删除通知公告 + * tags: [Announcements] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: 公告ID + * responses: + * 200: + * description: 公告删除成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: "公告删除成功" + * 401: + * description: 未授权 + * 404: + * description: 公告不存在 + * 500: + * description: 服务器错误 + */ + +/** + * @swagger + * /api/announcements/{id}/toggle-top: + * put: + * summary: 切换公告置顶状态 + * tags: [Announcements] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: 公告ID + * responses: + * 200: + * description: 置顶状态切换成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: "置顶状态更新成功" + * data: + * type: object + * properties: + * isTop: + * type: boolean + * example: true + * 401: + * description: 未授权 + * 404: + * description: 公告不存在 + * 500: + * description: 服务器错误 + */ \ No newline at end of file diff --git a/docs/apis/orders.js b/docs/apis/orders.js index c3c3c5f..8342c27 100644 --- a/docs/apis/orders.js +++ b/docs/apis/orders.js @@ -7,149 +7,12 @@ /** * @swagger - * /orders/create-from-cart: - * post: - * summary: 从购物车创建预订单 - * tags: [Orders] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - cartIds - * properties: - * cartIds: - * type: array - * items: - * type: integer - * description: 购物车商品ID数组 - * responses: - * 200: - * description: 成功创建预订单 - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * data: - * $ref: '#/components/schemas/PreOrder' - * 400: - * description: 请求参数错误 - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * message: - * type: string - */ - -/** - * @swagger - * /orders/pre-order/{id}: + * /api/orders: * get: - * summary: 获取预订单详情 - * tags: [Orders] - * 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: - * preOrder: - * $ref: '#/components/schemas/PreOrder' - * items: - * type: array - * items: - * type: object - * properties: - * product_id: - * type: integer - * product_name: - * type: string - * quantity: - * type: integer - * points_price: - * type: integer - * rongdou_price: - * type: number - * image_url: - * type: string - * 404: - * description: 预订单不存在 - */ - -/** - * @swagger - * /orders/confirm: - * post: - * summary: 确认下单 - * tags: [Orders] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - preOrderId - * - shippingAddress - * properties: - * preOrderId: - * type: integer - * description: 预订单ID - * shippingAddress: - * type: string - * description: 收货地址 - * responses: - * 200: - * description: 订单确认成功 - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * data: - * type: object - * properties: - * orderId: - * type: integer - * orderNumber: - * type: string - * 400: - * description: 请求参数错误或余额不足 - * 404: - * description: 预订单不存在 - */ - -/** - * @swagger - * /orders: - * get: - * summary: 获取用户订单列表 + * summary: 获取订单列表 * tags: [Orders] + * security: + * - bearerAuth: [] * parameters: * - in: query * name: page @@ -164,11 +27,37 @@ * 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, confirmed, shipped, delivered, cancelled] - * description: 订单状态筛选 + * description: 订单状态 + * - in: query + * name: startDate + * schema: + * type: string + * format: date + * description: 开始日期 + * - in: query + * name: endDate + * schema: + * type: string + * format: date + * description: 结束日期 * responses: * 200: * description: 成功获取订单列表 @@ -197,14 +86,162 @@ * type: integer * pages: * type: integer + * 401: + * description: 未授权 + * 500: + * description: 服务器错误 */ /** * @swagger - * /orders/{id}: - * get: - * summary: 获取订单详情 + * /api/orders/confirm: + * post: + * summary: 确认下单 * tags: [Orders] + * security: + * - bearerAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - pre_order_id + * - address + * properties: + * pre_order_id: + * type: integer + * description: 预订单ID + * address: + * type: object + * properties: + * recipient_name: + * type: string + * description: 收货人姓名 + * phone: + * type: string + * description: 收货人电话 + * province: + * type: string + * description: 省份 + * city: + * type: string + * description: 城市 + * district: + * type: string + * description: 区县 + * detail_address: + * type: string + * description: 详细地址 + * responses: + * 200: + * description: 确认下单成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * message: + * type: string + * data: + * type: object + * properties: + * order_id: + * type: integer + * order_no: + * type: string + * 400: + * description: 请求参数错误 + * 401: + * description: 未授权 + * 404: + * description: 预订单不存在 + * 500: + * description: 服务器错误 + */ + +/** + * @swagger + * /api/orders/pre-order/{id}: + * get: + * summary: 获取预订单详情 + * tags: [Orders] + * security: + * - bearerAuth: [] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: 预订单ID + * responses: + * 200: + * description: 获取预订单详情成功 + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * data: + * type: object + * properties: + * id: + * type: integer + * order_no: + * type: string + * total_amount: + * type: integer + * total_points: + * type: integer + * total_rongdou: + * type: integer + * status: + * type: string + * created_at: + * type: string + * items: + * type: array + * items: + * type: object + * properties: + * id: + * type: integer + * product_id: + * type: integer + * product_name: + * type: string + * quantity: + * type: integer + * price: + * type: integer + * points_price: + * type: integer + * rongdou_price: + * type: integer + * spec_info: + * type: object + * 401: + * description: 未授权 + * 404: + * description: 预订单不存在 + * 500: + * description: 服务器错误 + */ + +/** + * @swagger + * /api/orders/{id}: + * get: + * summary: 获取单个订单详情 + * tags: [Orders] + * security: + * - bearerAuth: [] * parameters: * - in: path * name: id @@ -227,10 +264,10 @@ * properties: * order: * $ref: '#/components/schemas/Order' - * items: - * type: array - * items: - * $ref: '#/components/schemas/OrderItem' + * 401: + * description: 未授权 * 404: * description: 订单不存在 + * 500: + * description: 服务器错误 */ \ No newline at end of file diff --git a/docs/schemas/announcement.js b/docs/schemas/announcement.js new file mode 100644 index 0000000..ddb6e92 --- /dev/null +++ b/docs/schemas/announcement.js @@ -0,0 +1,225 @@ +/** + * @swagger + * components: + * schemas: + * Announcement: + * type: object + * required: + * - title + * - content + * - type + * - priority + * properties: + * id: + * type: integer + * description: 公告ID + * example: 1 + * title: + * type: string + * description: 公告标题 + * example: "系统维护通知" + * content: + * type: string + * description: 公告内容 + * example: "系统将于今晚进行维护,预计维护时间2小时,期间可能影响部分功能使用。" + * type: + * type: string + * description: 公告类型 + * enum: [system, activity, maintenance, urgent] + * example: "maintenance" + * priority: + * type: string + * description: 优先级 + * enum: [high, medium, low] + * example: "high" + * status: + * type: string + * description: 公告状态 + * enum: [draft, published, expired] + * example: "published" + * isTop: + * type: boolean + * description: 是否置顶 + * example: false + * publishTime: + * type: string + * format: date-time + * description: 发布时间 + * example: "2024-01-15T10:00:00Z" + * expireTime: + * type: string + * format: date-time + * description: 过期时间 + * example: "2024-01-20T10:00:00Z" + * createdBy: + * type: integer + * description: 创建者用户ID + * example: 1 + * createdAt: + * type: string + * format: date-time + * description: 创建时间 + * example: "2024-01-15T09:00:00Z" + * updatedAt: + * type: string + * format: date-time + * description: 更新时间 + * example: "2024-01-15T09:30:00Z" + * creator: + * type: object + * description: 创建者信息 + * properties: + * id: + * type: integer + * example: 1 + * username: + * type: string + * example: "admin" + * email: + * type: string + * example: "admin@example.com" + * + * AnnouncementCreate: + * type: object + * required: + * - title + * - content + * - type + * - priority + * properties: + * title: + * type: string + * description: 公告标题 + * example: "系统维护通知" + * content: + * type: string + * description: 公告内容 + * example: "系统将于今晚进行维护,预计维护时间2小时。" + * type: + * type: string + * description: 公告类型 + * enum: [system, activity, maintenance, urgent] + * example: "maintenance" + * priority: + * type: string + * description: 优先级 + * enum: [high, medium, low] + * example: "high" + * status: + * type: string + * description: 公告状态 + * enum: [draft, published] + * default: draft + * example: "draft" + * isTop: + * type: boolean + * description: 是否置顶 + * default: false + * example: false + * publishTime: + * type: string + * format: date-time + * description: 发布时间 + * example: "2024-01-15T10:00:00Z" + * expireTime: + * type: string + * format: date-time + * description: 过期时间 + * example: "2024-01-20T10:00:00Z" + * + * AnnouncementUpdate: + * type: object + * properties: + * title: + * type: string + * description: 公告标题 + * example: "系统维护通知(更新)" + * content: + * type: string + * description: 公告内容 + * example: "系统维护时间调整为明晚进行。" + * type: + * type: string + * description: 公告类型 + * enum: [system, activity, maintenance, urgent] + * example: "maintenance" + * priority: + * type: string + * description: 优先级 + * enum: [high, medium, low] + * example: "medium" + * status: + * type: string + * description: 公告状态 + * enum: [draft, published, expired] + * example: "published" + * isTop: + * type: boolean + * description: 是否置顶 + * example: true + * publishTime: + * type: string + * format: date-time + * description: 发布时间 + * example: "2024-01-16T10:00:00Z" + * expireTime: + * type: string + * format: date-time + * description: 过期时间 + * example: "2024-01-21T10:00:00Z" + * + * AnnouncementList: + * type: object + * properties: + * success: + * type: boolean + * example: true + * data: + * type: object + * properties: + * announcements: + * type: array + * items: + * $ref: '#/components/schemas/Announcement' + * total: + * type: integer + * description: 总记录数 + * example: 50 + * page: + * type: integer + * description: 当前页码 + * example: 1 + * limit: + * type: integer + * description: 每页数量 + * example: 10 + * totalPages: + * type: integer + * description: 总页数 + * example: 5 + * + * AnnouncementResponse: + * type: object + * properties: + * success: + * type: boolean + * example: true + * message: + * type: string + * example: "操作成功" + * data: + * $ref: '#/components/schemas/Announcement' + * + * AnnouncementError: + * type: object + * properties: + * success: + * type: boolean + * example: false + * message: + * type: string + * example: "操作失败" + * error: + * type: string + * example: "公告不存在" + */ \ No newline at end of file diff --git a/middleware/auth.js b/middleware/auth.js index 800cd1d..5829500 100644 --- a/middleware/auth.js +++ b/middleware/auth.js @@ -34,6 +34,19 @@ const auth = async (req, res, next) => { }); } + // 检查支付状态(管理员除外) + if (user.role !== 'admin' && user.payment_status === 'unpaid') { + console.log(11111); + + return res.status(403).json({ + success: false, + message: '您的账户尚未激活,请完成支付后再使用', + code: 'PAYMENT_REQUIRED', + needPayment: true, + userId: user.id + }); + } + req.user = user; next(); } catch (error) { @@ -49,4 +62,51 @@ const adminAuth = (req, res, next) => { next(); }; -module.exports = { auth, adminAuth, JWT_SECRET }; \ No newline at end of file +/** + * 支付认证中间件 + * 只验证JWT令牌和用户状态,不检查支付状态 + * 用于支付相关接口,允许未支付用户创建支付订单 + */ +const paymentAuth = async (req, res, next) => { + try { + const token = req.header('Authorization')?.replace('Bearer ', ''); + + if (!token) { + return res.status(401).json({ success: false, message: '未提供认证令牌' }); + } + + const decoded = jwt.verify(token, JWT_SECRET); + const db = getDB(); + const [users] = await db.execute('SELECT * FROM users WHERE id = ?', [decoded.userId]); + + if (users.length === 0) { + return res.status(401).json({ success: false, message: '用户不存在' }); + } + + const user = users[0]; + + // 检查用户是否被拉黑 + if (user.is_blacklisted) { + return res.status(403).json({ + success: false, + message: '账户已被拉黑,请联系管理员', + code: 'USER_BLACKLISTED' + }); + } + + // 注意:这里不检查支付状态,允许未支付用户创建支付订单 + req.user = user; + next(); + } catch (error) { + console.error('支付认证失败:', error); + if (error.name === 'JsonWebTokenError') { + return res.status(401).json({ success: false, message: '无效的认证令牌' }); + } + if (error.name === 'TokenExpiredError') { + return res.status(401).json({ success: false, message: '认证令牌已过期' }); + } + return res.status(500).json({ success: false, message: '认证失败' }); + } +}; + +module.exports = { auth, adminAuth, paymentAuth, JWT_SECRET }; \ No newline at end of file diff --git a/migrations/add_alipay_support.sql b/migrations/add_alipay_support.sql new file mode 100644 index 0000000..36d716d --- /dev/null +++ b/migrations/add_alipay_support.sql @@ -0,0 +1,17 @@ +-- 为支付订单表添加支付宝支持 +-- 更新交易类型枚举,添加支付宝相关类型 +ALTER TABLE `payment_orders` MODIFY COLUMN `trade_type` varchar(32) NOT NULL COMMENT '交易类型(H5/JSAPI/ALIPAY_WAP/ALIPAY_APP等)'; + +-- 更新transaction_id字段注释,支持多种支付方式 +ALTER TABLE `payment_orders` MODIFY COLUMN `transaction_id` varchar(64) DEFAULT NULL COMMENT '第三方支付订单号(微信/支付宝)'; + +-- 添加支付方式字段 +ALTER TABLE `payment_orders` ADD COLUMN `payment_method` enum('wechat','alipay') DEFAULT 'wechat' COMMENT '支付方式' AFTER `trade_type`; + +-- 添加支付宝特有字段 +ALTER TABLE `payment_orders` ADD COLUMN `buyer_user_id` varchar(32) DEFAULT NULL COMMENT '支付宝买家用户ID' AFTER `transaction_id`; +ALTER TABLE `payment_orders` ADD COLUMN `trade_status` varchar(32) DEFAULT NULL COMMENT '支付宝交易状态' AFTER `buyer_user_id`; + +-- 添加索引 +ALTER TABLE `payment_orders` ADD KEY `idx_payment_method` (`payment_method`); +ALTER TABLE `payment_orders` ADD KEY `idx_trade_status` (`trade_status`); \ No newline at end of file diff --git a/migrations/create_payment_orders_table.sql b/migrations/create_payment_orders_table.sql new file mode 100644 index 0000000..fb4ea53 --- /dev/null +++ b/migrations/create_payment_orders_table.sql @@ -0,0 +1,27 @@ +-- 创建支付订单表 +CREATE TABLE IF NOT EXISTS `payment_orders` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '订单ID', + `user_id` int(11) NOT NULL COMMENT '用户ID', + `out_trade_no` varchar(64) NOT NULL COMMENT '商户订单号', + `transaction_id` varchar(64) DEFAULT NULL COMMENT '微信支付订单号', + `total_fee` int(11) NOT NULL COMMENT '订单金额(分)', + `body` varchar(128) NOT NULL COMMENT '商品描述', + `trade_type` varchar(16) NOT NULL COMMENT '交易类型', + `prepay_id` varchar(64) DEFAULT NULL COMMENT '预支付交易会话标识', + `mweb_url` text DEFAULT NULL COMMENT 'H5支付跳转链接', + `status` enum('pending','paid','failed','cancelled') NOT NULL DEFAULT 'pending' COMMENT '支付状态', + `paid_at` datetime DEFAULT NULL COMMENT '支付完成时间', + `created_at` datetime NOT NULL COMMENT '创建时间', + `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_out_trade_no` (`out_trade_no`), + KEY `idx_user_id` (`user_id`), + KEY `idx_status` (`status`), + KEY `idx_created_at` (`created_at`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='支付订单表'; + +-- 为用户表添加支付状态字段 +ALTER TABLE `users` ADD COLUMN `payment_status` enum('unpaid','paid') NOT NULL DEFAULT 'unpaid' COMMENT '支付状态' AFTER `status`; + +-- 添加索引 +ALTER TABLE `users` ADD KEY `idx_payment_status` (`payment_status`); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 8f1bc47..5b7f5c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,10 +11,12 @@ "dependencies": { "@alicloud/dysmsapi20170525": "^4.1.2", "@alicloud/openapi-client": "^0.4.15", + "alipay-sdk": "^4.14.0", "axios": "^1.11.0", "bcryptjs": "^2.4.3", "body-parser": "^1.20.2", "cors": "^2.8.5", + "crypto": "^1.0.1", "dayjs": "^1.11.13", "dotenv": "^17.2.1", "express": "^4.18.2", @@ -27,10 +29,12 @@ "multer": "^1.4.5-lts.1", "mysql2": "^3.14.3", "node-cron": "^4.2.1", + "node-rsa": "^1.1.1", "qrcode": "^1.5.4", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.1", - "winston": "^3.17.0" + "winston": "^3.17.0", + "xml2js": "^0.6.2" }, "devDependencies": { "concurrently": "^8.2.2", @@ -311,6 +315,28 @@ "xml2js": "^0.6.2" } }, + "node_modules/@fidm/asn1": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@fidm/asn1/-/asn1-1.0.4.tgz", + "integrity": "sha512-esd1jyNvRb2HVaQGq2Gg8Z0kbQPXzV9Tq5Z14KNIov6KfFD6PTaRIO8UpcsYiTNzOqJpmyzWgVTrUwFV3UF4TQ==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@fidm/x509": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@fidm/x509/-/x509-1.2.1.tgz", + "integrity": "sha512-nwc2iesjyc9hkuzcrMCBXQRn653XuAUKorfWM8PZyJawiy1QzLj4vahwzaI25+pfpwOLvMzbJ0uKpWLDNmo16w==", + "license": "MIT", + "dependencies": { + "@fidm/asn1": "^1.0.4", + "tweetnacl": "^1.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@hapi/hoek": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", @@ -407,6 +433,26 @@ "node": ">= 0.6" } }, + "node_modules/alipay-sdk": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/alipay-sdk/-/alipay-sdk-4.14.0.tgz", + "integrity": "sha512-oiD/VP5Ei0RRacHHmE+N0uqgOj2xzce7c0fHrtyyh1P04O+o9I1r65LdGPzU8960J56xOxS/d3c+R/9lsPUH7g==", + "license": "MIT", + "dependencies": { + "@fidm/x509": "^1.2.1", + "bignumber.js": "^9.1.2", + "camelcase-keys": "^7.0.2", + "crypto-js": "^4.2.0", + "formstream": "^1.5.0", + "snakecase-keys": "^8.0.0", + "sse-decoder": "^1.0.0", + "urllib": "^4", + "utility": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -463,6 +509,15 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, "node_modules/async": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", @@ -522,6 +577,15 @@ "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", "license": "MIT" }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -714,6 +778,36 @@ "node": ">=6" } }, + "node_modules/camelcase-keys": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-7.0.2.tgz", + "integrity": "sha512-Rjs1H+A9R+Ig+4E/9oyB66UC5Mj9Xq3N//vcLf2WzgdTi/3gUu3Z9KoqmlrEG4VuuLK8wJHofxzdQXz/knhiYg==", + "license": "MIT", + "dependencies": { + "camelcase": "^6.3.0", + "map-obj": "^4.1.0", + "quick-lru": "^5.1.1", + "type-fest": "^1.2.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/camelcase-keys/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -972,6 +1066,19 @@ "node": ">= 0.10" } }, + "node_modules/crypto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz", + "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==", + "deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in.", + "license": "ISC" + }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", + "license": "MIT" + }, "node_modules/date-fns": { "version": "2.30.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", @@ -1094,6 +1201,16 @@ "node": ">=6.0.0" } }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, "node_modules/dotenv": { "version": "17.2.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.1.tgz", @@ -1315,6 +1432,18 @@ "node": ">= 8.0.0" } }, + "node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/fast-xml-parser": { "version": "4.5.3", "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz", @@ -1449,6 +1578,30 @@ "node": ">= 6" } }, + "node_modules/formstream": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/formstream/-/formstream-1.5.2.tgz", + "integrity": "sha512-NASf0lgxC1AyKNXQIrXTEYkiX99LhCEXTkiGObXAkpBui86a4u8FjH1o2bGb3PpqI3kafC+yw4zWeK6l6VHTgg==", + "license": "MIT", + "dependencies": { + "destroy": "^1.0.4", + "mime": "^2.5.2", + "node-hex": "^1.0.1", + "pause-stream": "~0.0.11" + } + }, + "node_modules/formstream/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -1824,6 +1977,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2148,6 +2310,15 @@ "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", "license": "Apache-2.0" }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, "node_modules/lru-cache": { "version": "7.18.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", @@ -2172,6 +2343,18 @@ "url": "https://github.com/sponsors/wellwelwel" } }, + "node_modules/map-obj": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", + "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -2407,6 +2590,16 @@ "node": ">= 0.6" } }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "license": "MIT", + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, "node_modules/node-cron": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.2.1.tgz", @@ -2416,6 +2609,24 @@ "node": ">=6.0.0" } }, + "node_modules/node-hex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/node-hex/-/node-hex-1.0.1.tgz", + "integrity": "sha512-iwpZdvW6Umz12ICmu9IYPRxg0tOLGmU3Tq2tKetejCj3oZd7b2nUXwP3a7QA5M9glWy8wlPS1G3RwM/CdsUbdQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/node-rsa": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/node-rsa/-/node-rsa-1.1.1.tgz", + "integrity": "sha512-Jd4cvbJMryN21r5HgxQOpMEqv+ooke/korixNNK3mGqfGJmy0M77WDDzo/05969+OkMy3XW1UuZsSmW9KQm7Fw==", + "license": "MIT", + "dependencies": { + "asn1": "^0.2.4" + } + }, "node_modules/nodemon": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", @@ -2630,6 +2841,18 @@ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "license": "MIT" }, + "node_modules/pause-stream": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", + "integrity": "sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==", + "license": [ + "MIT", + "Apache2" + ], + "dependencies": { + "through": "~2.3" + } + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -2809,6 +3032,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -3163,6 +3398,42 @@ "integrity": "sha512-KyFkIfr8QBlFG3uc3NaljaXdYcsbRy1KrSfc4tsQV8jW68jAktGeOcifu530Vx/5LC+PULHT0Rv8LiI8Gw+c1g==", "license": "MIT" }, + "node_modules/snake-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", + "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", + "license": "MIT", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/snakecase-keys": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/snakecase-keys/-/snakecase-keys-8.1.0.tgz", + "integrity": "sha512-9/Eug2btrCiOi+9+vIXJnxUcKVfcbLy5Uwff4BrO6PQf3Oq/2iYQ/1zkmnrpIIjfel/DAasAlux7OvAmCa+Xnw==", + "license": "MIT", + "dependencies": { + "map-obj": "^4.2.0", + "snake-case": "^3.0.4", + "type-fest": "^4.15.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/snakecase-keys/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/spawn-command": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz", @@ -3187,6 +3458,15 @@ "node": ">= 0.6" } }, + "node_modules/sse-decoder": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/sse-decoder/-/sse-decoder-1.0.0.tgz", + "integrity": "sha512-JPopy3jfNmPcUz5Ru6skKhHNRJbsvcEW6Z4SirKkucLS8Jya1Bmf4FVX8giOkLm8xQJ7kK68P6GXoVSTkbedUA==", + "license": "MIT", + "engines": { + "node": ">= 14.19.3" + } + }, "node_modules/stack-trace": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", @@ -3368,6 +3648,12 @@ "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", "license": "MIT" }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "license": "MIT" + }, "node_modules/through2": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", @@ -3446,9 +3732,26 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, "license": "0BSD" }, + "node_modules/tweetnacl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", + "license": "Unlicense" + }, + "node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -3475,12 +3778,33 @@ "dev": true, "license": "MIT" }, + "node_modules/undici": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.15.0.tgz", + "integrity": "sha512-7oZJCPvvMvTd0OlqWsIxTuItTpJBpU1tcbVl24FMn3xt3+VSunwUasmfPJRE57oNO1KsZ4PgA1xTdAX4hq8NyQ==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "license": "MIT" }, + "node_modules/unescape": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/unescape/-/unescape-1.0.1.tgz", + "integrity": "sha512-O0+af1Gs50lyH1nUu3ZyYS1cRh01Q/kUKatTOkSs7jukXE6/NebucDVxyiDsA9AQ4JC1V1jUH9EO8JX2nMDgGQ==", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -3490,6 +3814,36 @@ "node": ">= 0.8" } }, + "node_modules/urllib": { + "version": "4.8.2", + "resolved": "https://registry.npmjs.org/urllib/-/urllib-4.8.2.tgz", + "integrity": "sha512-V5oo9kzQfF9UQAC9KOVFmmmbYPJ9nksgO8HM89BZse96QcCyjrssPVxKzL/9sVPRC8D4Sd3nAdaMCXAZ3dqEYA==", + "license": "MIT", + "dependencies": { + "form-data": "^4.0.1", + "formstream": "^1.5.1", + "mime-types": "^2.1.35", + "qs": "^6.12.1", + "type-fest": "^4.20.1", + "undici": "^7.1.1", + "ylru": "^2.0.0" + }, + "engines": { + "node": ">= 18.19.0" + } + }, + "node_modules/urllib/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/util": { "version": "0.12.5", "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", @@ -3509,6 +3863,20 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/utility": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/utility/-/utility-2.5.0.tgz", + "integrity": "sha512-lDbOVde5UAKgtxrSyZNhqrTA7f7anba6DTqbsDWgUFk6PZlmr7djqPYw0FnL5a6TbJvRt38VmYqt07zVLzXG2A==", + "license": "MIT", + "dependencies": { + "escape-html": "^1.0.3", + "unescape": "^1.0.1", + "ylru": "^2.0.0" + }, + "engines": { + "node": ">= 16.0.0" + } + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -3742,6 +4110,15 @@ "node": ">=12" } }, + "node_modules/ylru": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ylru/-/ylru-2.0.0.tgz", + "integrity": "sha512-T6hTrKcr9lKeUG0MQ/tO72D3UGptWVohgzpHG8ljU1jeBt2RCjcWxvsTPD8ZzUq1t1FvwROAw1kxg2euvg/THg==", + "license": "MIT", + "engines": { + "node": ">= 18.19.0" + } + }, "node_modules/z-schema": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz", diff --git a/package.json b/package.json index eee73ee..404039a 100644 --- a/package.json +++ b/package.json @@ -9,10 +9,12 @@ "dependencies": { "@alicloud/dysmsapi20170525": "^4.1.2", "@alicloud/openapi-client": "^0.4.15", + "alipay-sdk": "^4.14.0", "axios": "^1.11.0", "bcryptjs": "^2.4.3", "body-parser": "^1.20.2", "cors": "^2.8.5", + "crypto": "^1.0.1", "dayjs": "^1.11.13", "dotenv": "^17.2.1", "express": "^4.18.2", @@ -25,10 +27,12 @@ "multer": "^1.4.5-lts.1", "mysql2": "^3.14.3", "node-cron": "^4.2.1", + "node-rsa": "^1.1.1", "qrcode": "^1.5.4", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.1", - "winston": "^3.17.0" + "winston": "^3.17.0", + "xml2js": "^0.6.2" }, "devDependencies": { "concurrently": "^8.2.2", diff --git a/routes/announcements.js b/routes/announcements.js new file mode 100644 index 0000000..6483cfd --- /dev/null +++ b/routes/announcements.js @@ -0,0 +1,399 @@ +const express = require('express'); +const { getDB } = require('../database'); +const { auth, adminAuth } = require('../middleware/auth'); +const dayjs = require('dayjs'); + +const router = express.Router(); + + + +router.get('/', auth, async (req, res) => { + try { + const db = getDB(); + const { page = 1, limit = 10, status, type, keyword } = req.query; + const offset = (page - 1) * limit; + + let whereClause = 'WHERE 1=1'; + const params = []; + + if (status) { + whereClause += ' AND status = ?'; + params.push(status); + } + + if (type) { + whereClause += ' AND type = ?'; + params.push(type); + } + + if (keyword) { + whereClause += ' AND (title LIKE ? OR content LIKE ?)'; + params.push(`%${keyword}%`, `%${keyword}%`); + } + + // 获取总数 + const countQuery = `SELECT COUNT(*) as total FROM announcements ${whereClause}`; + const [countResult] = await db.execute(countQuery, params); + const total = countResult[0].total; + + // 获取公告列表(包含用户阅读状态) + const limitValue = Math.max(1, Math.min(100, parseInt(limit))); + const offsetValue = Math.max(0, parseInt(offset)); + + const query = ` + SELECT a.*, u.username as creator_name, + uar.is_read, + uar.read_at, + CASE + WHEN a.expire_time IS NOT NULL AND a.expire_time < NOW() THEN 1 + ELSE 0 + END as is_expired + FROM announcements a + LEFT JOIN users u ON a.created_by = u.id + LEFT JOIN user_announcement_reads uar ON a.id = uar.announcement_id AND uar.user_id = ? + ${whereClause} + ORDER BY a.is_pinned DESC, a.created_at DESC + LIMIT ${limitValue} OFFSET ${offsetValue} + `; + + const [announcements] = await db.execute(query, [req.user.id, ...params]); + + // 自动将过期公告标记为已读 + console.log(announcements); + + const expiredUnreadAnnouncements = announcements.filter(a => a.is_expired && !a.is_read); + console.log(expiredUnreadAnnouncements); + + if (expiredUnreadAnnouncements.length > 0) { + const expiredIds = expiredUnreadAnnouncements.map(a => a.id); + await db.execute(` + INSERT INTO user_announcement_reads (user_id, announcement_id, is_read, read_at) + VALUES ${expiredIds.map(() => '(?, ?, TRUE, NOW())').join(', ')} + ON DUPLICATE KEY UPDATE is_read = TRUE, read_at = NOW() + `, expiredIds.flatMap(id => [req.user.id, id])); + + // 更新返回数据中的阅读状态 + expiredUnreadAnnouncements.forEach(a => { + a.is_read = true; + a.read_at = new Date(); + }); + } + + res.json({ + success: true, + data: { + announcements, + total, + page: parseInt(page), + limit: parseInt(limit), + totalPages: Math.ceil(total / limit) + } + }); + } catch (error) { + console.error('获取公告列表失败:', error); + res.status(500).json({ success: false, message: '获取公告列表失败' }); + } +}); + + +router.get('/:id', auth, async (req, res) => { + try { + const db = getDB(); + const { id } = req.params; + + const query = ` + SELECT a.*, u.username as creator_name, + uar.is_read, + uar.read_at, + CASE + WHEN a.expire_time IS NOT NULL AND a.expire_time < NOW() THEN 1 + ELSE 0 + END as is_expired + FROM announcements a + LEFT JOIN users u ON a.created_by = u.id + LEFT JOIN user_announcement_reads uar ON a.id = uar.announcement_id AND uar.user_id = ? + WHERE a.id = ? + `; + + const [result] = await db.execute(query, [req.user.id, id]); + + if (result.length === 0) { + return res.status(404).json({ success: false, message: '公告不存在' }); + } + + const announcement = result[0]; + + // 如果公告未读或已过期但未标记为已读,则标记为已读 + if (!announcement.is_read || (announcement.is_expired && !announcement.is_read)) { + await db.execute(` + INSERT INTO user_announcement_reads (user_id, announcement_id, is_read, read_at) + VALUES (?, ?, TRUE, NOW()) + ON DUPLICATE KEY UPDATE is_read = TRUE, read_at = NOW() + `, [req.user.id, id]); + + announcement.is_read = true; + announcement.read_at = new Date(); + } + + res.json({ success: true, data: announcement }); + } catch (error) { + console.error('获取公告详情失败:', error); + res.status(500).json({ success: false, message: '获取公告详情失败' }); + } +}); + + +router.post('/', auth, adminAuth, async (req, res) => { + try { + const db = getDB(); + const { + title, + content, + type = 'system', + priority = 'medium', + status = 'draft', + is_pinned = false, + publish_time, + expire_time + } = req.body; + + if (!title || !content) { + return res.status(400).json({ success: false, message: '标题和内容不能为空' }); + } + + const query = ` + INSERT INTO announcements ( + title, content, type, priority, status, is_pinned, + publish_time, expire_time, created_by, created_at, updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW()) + `; + + const [result] = await db.execute(query, [ + title, + content, + type, + priority, + status, + is_pinned, + publish_time || null, + expire_time || null, + req.user.id + ]); + + res.status(201).json({ + success: true, + message: '公告创建成功', + data: { id: result.insertId } + }); + } catch (error) { + console.error('创建公告失败:', error); + res.status(500).json({ success: false, message: '创建公告失败' }); + } +}); + + +router.put('/:id', auth, adminAuth, async (req, res) => { + try { + const db = getDB(); + const { id } = req.params; + let { + title, + content, + type, + priority, + status, + is_pinned, + publish_time, + expire_time + } = req.body; + + // 检查公告是否存在 + const [existing] = await db.execute('SELECT id FROM announcements WHERE id = ?', [id]); + if (existing.length === 0) { + return res.status(404).json({ success: false, message: '公告不存在' }); + } + + const updates = []; + const params = []; + + if (title !== undefined) { + updates.push('title = ?'); + params.push(title); + } + if (content !== undefined) { + updates.push('content = ?'); + params.push(content); + } + if (type !== undefined) { + updates.push('type = ?'); + params.push(type); + } + if (priority !== undefined) { + updates.push('priority = ?'); + params.push(priority); + } + if (status !== undefined) { + updates.push('status = ?'); + params.push(status); + } + if (is_pinned !== undefined) { + updates.push('is_pinned = ?'); + params.push(is_pinned); + } + if (publish_time !== undefined) { + updates.push('publish_time = ?'); + publish_time = dayjs(publish_time).format('YYYY-MM-DD'); + params.push(publish_time); + } + if (expire_time !== undefined) { + updates.push('expire_time = ?'); + params.push(expire_time); + } + + if (updates.length === 0) { + return res.status(400).json({ success: false, message: '没有要更新的字段' }); + } + + updates.push('updated_at = NOW()'); + params.push(id); + + const query = `UPDATE announcements SET ${updates.join(', ')} WHERE id = ?`; + await db.execute(query, params); + + res.json({ success: true, message: '公告更新成功' }); + } catch (error) { + console.error('更新公告失败:', error); + res.status(500).json({ success: false, message: '更新公告失败' }); + } +}); + + +router.delete('/:id', auth, adminAuth, async (req, res) => { + try { + const db = getDB(); + const { id } = req.params; + + // 检查公告是否存在 + const [existing] = await db.execute('SELECT id FROM announcements WHERE id = ?', [id]); + if (existing.length === 0) { + return res.status(404).json({ success: false, message: '公告不存在' }); + } + + await db.execute('DELETE FROM announcements WHERE id = ?', [id]); + + res.json({ success: true, message: '公告删除成功' }); + } catch (error) { + console.error('删除公告失败:', error); + res.status(500).json({ success: false, message: '删除公告失败' }); + } +}); + + +router.get('/public/list', async (req, res) => { + try { + const db = getDB(); + const { limit = 5 } = req.query; + const limitValue = Math.max(1, Math.min(50, parseInt(limit))); + + const query = ` + SELECT id, title, content, type, priority, publish_time, created_at + FROM announcements + WHERE status = 'published' + AND (expire_time IS NULL OR expire_time > NOW()) + AND (publish_time IS NULL OR publish_time <= NOW()) + ORDER BY is_pinned DESC, created_at DESC + LIMIT ${limitValue} + `; + + const [announcements] = await db.execute(query, []); + + res.json({ success: true, data: announcements }); + } catch (error) { + console.error('获取公开公告失败:', error); + res.status(500).json({ success: false, message: '获取公开公告失败' }); + } +}); + +// 标记公告为已读 +router.post('/:id/read', auth, async (req, res) => { + try { + const db = getDB(); + const { id } = req.params; + + // 检查公告是否存在 + const [existing] = await db.execute('SELECT id FROM announcements WHERE id = ?', [id]); + if (existing.length === 0) { + return res.status(404).json({ success: false, message: '公告不存在' }); + } + + // 标记为已读 + await db.execute(` + INSERT INTO user_announcement_reads (user_id, announcement_id, is_read, read_at) + VALUES (?, ?, TRUE, NOW()) + ON DUPLICATE KEY UPDATE is_read = TRUE, read_at = NOW() + `, [req.user.id, id]); + + res.json({ success: true, message: '已标记为已读' }); + } catch (error) { + console.error('标记公告已读失败:', error); + res.status(500).json({ success: false, message: '标记公告已读失败' }); + } +}); + +// 获取用户未读公告数量 +router.get('/unread/count', auth, async (req, res) => { + try { + const db = getDB(); + + const query = ` + SELECT COUNT(*) as unread_count + FROM announcements a + LEFT JOIN user_announcement_reads uar ON a.id = uar.announcement_id AND uar.user_id = ? + WHERE a.status = 'published' + AND (a.publish_time IS NULL OR a.publish_time <= NOW()) + AND (a.expire_time IS NULL OR a.expire_time > NOW()) + AND (uar.is_read IS NULL OR uar.is_read = FALSE) + `; + + const [result] = await db.execute(query, [req.user.id]); + + res.json({ + success: true, + data: { + unread_count: result[0].unread_count + } + }); + } catch (error) { + console.error('获取未读公告数量失败:', error); + res.status(500).json({ success: false, message: '获取未读公告数量失败' }); + } +}); + +// 批量标记公告为已读 +router.post('/batch/read', auth, async (req, res) => { + try { + const db = getDB(); + const { announcement_ids } = req.body; + + if (!announcement_ids || !Array.isArray(announcement_ids) || announcement_ids.length === 0) { + return res.status(400).json({ success: false, message: '请提供有效的公告ID列表' }); + } + + // 批量标记为已读 + const values = announcement_ids.map(() => '(?, ?, TRUE, NOW())').join(', '); + const params = announcement_ids.flatMap(id => [req.user.id, id]); + + await db.execute(` + INSERT INTO user_announcement_reads (user_id, announcement_id, is_read, read_at) + VALUES ${values} + ON DUPLICATE KEY UPDATE is_read = TRUE, read_at = NOW() + `, params); + + res.json({ success: true, message: '批量标记已读成功' }); + } catch (error) { + console.error('批量标记公告已读失败:', error); + res.status(500).json({ success: false, message: '批量标记公告已读失败' }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/routes/auth.js b/routes/auth.js index 9d07712..654fe8f 100644 --- a/routes/auth.js +++ b/routes/auth.js @@ -122,139 +122,105 @@ router.post('/register', async (req, res) => { try { const db = getDB(); await db.query('START TRANSACTION'); - + const { - username, - phone, - password, - registrationCode, + username, + phone, + password, city, - district_id: district, + district_id: district, captchaId, captchaText, smsCode, // 短信验证码 role = 'user' } = req.body; - - if (!username || !phone || !password || !registrationCode || !city || !district) { - return res.status(400).json({ success: false, message: '用户名、手机号、密码、激活码、城市和区域不能为空' }); + + if (!username || !phone || !password || !city || !district) { + return res.status(400).json({ success: false, message: '用户名、手机号、密码、城市和区域不能为空' }); } - + if (!captchaId || !captchaText) { return res.status(400).json({ success: false, message: '图形验证码不能为空' }); } - + if (!smsCode) { return res.status(400).json({ success: false, message: '短信验证码不能为空' }); } - + // 注意:图形验证码已在前端通过 /captcha/verify 接口验证过,这里不再重复验证 - + // 验证短信验证码 const smsAPI = require('./sms'); const smsValid = smsAPI.verifySMSCode(phone, smsCode); if (!smsValid) { return res.status(400).json({ success: false, message: '短信验证码错误或已过期' }); } - + // 验证手机号格式 const phoneRegex = /^1[3-9]\d{9}$/; if (!phoneRegex.test(phone)) { return res.status(400).json({ success: false, message: '手机号格式不正确' }); } - - // 验证激活码 - const [registrationCodes] = await db.execute( - 'SELECT id, is_used, expires_at, agent_id FROM registration_codes WHERE code = ?', - [registrationCode] - ); - - if (registrationCodes.length === 0) { - return res.status(400).json({ success: false, message: '激活码不存在' }); - } - - const regCode = registrationCodes[0]; - - // 检查激活码是否已使用 - if (regCode.is_used) { - return res.status(400).json({ success: false, message: '激活码已被使用' }); - } - - // 检查激活码是否过期 - if (new Date() > new Date(regCode.expires_at)) { - return res.status(400).json({ success: false, message: '激活码已过期' }); - } - + + + // 检查用户是否已存在 const [existingUsers] = await db.execute( - 'SELECT id FROM users WHERE username = ? OR phone = ?', + 'SELECT id, payment_status FROM users WHERE username = ? OR phone = ?', [username, phone] ); - + if (existingUsers.length > 0) { - return res.status(400).json({ success: false, message: '用户名或手机号已存在' }); + const existingUser = existingUsers[0]; + // 如果用户存在但未支付,允许重新注册(覆盖原用户信息) + if (existingUser.payment_status === 'unpaid') { + // 删除未支付的用户记录 + await db.execute('DELETE FROM users WHERE id = ?', [existingUser.id]); + } else { + return res.status(400).json({ success: false, message: '用户名或手机号已存在' }); + } } - + // 加密密码 const hashedPassword = await bcrypt.hash(password, 10); - - // 创建用户(待审核状态,可以进入系统但匹配需审核) + + // 创建用户(初始状态为未支付) const [result] = await db.execute( - 'INSERT INTO users (username, phone, password, role, points, audit_status, city, district_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', + 'INSERT INTO users (username, phone, password, role, points, audit_status, city, district_id, payment_status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, "unpaid")', [username, phone, hashedPassword, role, 0, 'pending', city, district] ); - + const userId = result.insertId; - + // 用户余额已在创建用户时设置为默认值0.00,无需额外操作 - - // 标记激活码为已使用 - await db.execute( - 'UPDATE registration_codes SET is_used = TRUE, used_at = NOW(), used_by_user_id = ? WHERE id = ?', - [userId, regCode.id] + + + + // 根据地区自动关联代理 + const [agents] = await db.execute( + 'SELECT ra.id FROM users u INNER JOIN regional_agents ra ON u.id = ra.user_id WHERE ra.region_id = ? AND ra.status = "active" ORDER BY ra.created_at ASC LIMIT 1', + [district] ); - - // 如果是代理邀请码,建立代理关系 - if (regCode.agent_id) { - // 验证agent_id是否存在于regional_agents表中 - const [agentExists] = await db.execute( - 'SELECT id FROM regional_agents WHERE id = ?', - [regCode.agent_id] + + if (agents.length > 0) { + await db.execute( + 'INSERT INTO agent_merchants (agent_id, merchant_id, created_at) VALUES (?, ?, NOW())', + [agents[0].id, userId] ); - - if (agentExists.length > 0) { - await db.execute( - 'INSERT INTO agent_merchants (agent_id, merchant_id, created_at) VALUES (?, ?, NOW())', - [regCode.agent_id, userId] - ); - } - } else { - // 如果不是代理邀请码,根据地区自动关联代理 - const [agents] = await db.execute( - 'SELECT ra.id FROM users u INNER JOIN regional_agents ra ON u.id = ra.user_id WHERE ra.region_id = ? AND ra.status = "active" ORDER BY ra.created_at ASC LIMIT 1', - [district] - ); - - if (agents.length > 0) { - await db.execute( - 'INSERT INTO agent_merchants (agent_id, merchant_id, created_at) VALUES (?, ?, NOW())', - [agents[0].id, userId] - ); - } } - + await db.query('COMMIT'); - - // 生成JWT token + + // 生成JWT token(用于支付流程) const token = jwt.sign( { userId: userId, username, role }, JWT_SECRET, { expiresIn: '24h' } ); - + res.status(201).json({ success: true, - message: '注册成功', + message: '用户信息创建成功,请完成支付以激活账户', token, user: { id: userId, @@ -264,8 +230,10 @@ router.post('/register', async (req, res) => { points: 0, audit_status: 'pending', city, - district - } + district, + paymentStatus: 'unpaid' + }, + needPayment: true }); } catch (error) { try { @@ -275,8 +243,8 @@ router.post('/register', async (req, res) => { } console.error('注册错误详情:', error); console.error('错误堆栈:', error.stack); - res.status(500).json({ - success: false, + res.status(500).json({ + success: false, message: '注册失败', error: process.env.NODE_ENV === 'development' ? error.message : undefined }); @@ -336,25 +304,25 @@ router.post('/login', async (req, res) => { try { const db = getDB(); const { username, password, captchaId, captchaText } = req.body; - + if (!username || !password) { return res.status(400).json({ success: false, message: '用户名和密码不能为空' }); } - + if (!captchaId || !captchaText) { return res.status(400).json({ success: false, message: '验证码不能为空' }); } - // 获取存储的验证码 + // 获取存储的验证码 const storedCaptcha = global.captchaStore.get(captchaId); console.log(storedCaptcha); - + if (!storedCaptcha) { return res.status(400).json({ success: false, message: '验证码不存在或已过期' }); } - + // 检查是否过期 if (Date.now() > storedCaptcha.expires) { global.captchaStore.delete(captchaId); @@ -363,61 +331,77 @@ router.post('/login', async (req, res) => { message: '验证码已过期' }); } - + // 验证验证码(不区分大小写) const isValid = storedCaptcha.text === captchaText.toLowerCase(); - + // 删除已验证的验证码 global.captchaStore.delete(captchaId); - + if (!isValid) { return res.status(400).json({ success: false, message: '验证码错误' }); } - + // 注意:验证码已在前端通过 /captcha/verify 接口验证过,这里不再重复验证 - - // 查找用户 + + // 查找用户(包含支付状态) console.log('登录尝试 - 用户名:', username); const [users] = await db.execute( 'SELECT * FROM users WHERE username = ?', [username] ); - + console.log('查找到的用户数量:', users.length); if (users.length === 0) { console.log('用户不存在:', username); return res.status(401).json({ success: false, message: '用户名或密码错误' }); } - + const user = users[0]; console.log('找到用户:', user.username, '密码长度:', user.password ? user.password.length : 'null'); - + // 验证密码 console.log('验证密码 - 输入密码:', password, '数据库密码前10位:', user.password ? user.password.substring(0, 10) : 'null'); const isValidPassword = await bcrypt.compare(password, user.password); console.log('密码验证结果:', isValidPassword); - + if (!isValidPassword) { console.log('密码验证失败'); return res.status(401).json({ success: false, message: '用户名或密码错误' }); } - + + // 检查支付状态(管理员除外) + if (user.role !== 'admin' && user.payment_status === 'unpaid') { + const token = jwt.sign( + { userId: user.id, username: user.username, role: user.role }, + JWT_SECRET, + { expiresIn: '5m' } + ); + return res.status(200).json({ + success: false, + message: '您的账户尚未激活,请完成支付后再登录', + needPayment: true, + user: user[0], + token + }); + } + // 检查用户审核状态(管理员除外,只阻止被拒绝的用户) if (user.role !== 'admin' && user.audit_status === 'rejected') { return res.status(403).json({ success: false, message: '您的账户审核未通过,请联系管理员' }); } // 待审核用户可以正常登录使用系统,但匹配功能会有限制 - + // 生成JWT token const token = jwt.sign( { userId: user.id, username: user.username, role: user.role }, JWT_SECRET, { expiresIn: '24h' } ); - + res.json({ success: true, message: '登录成功', @@ -427,7 +411,8 @@ router.post('/login', async (req, res) => { username: user.username, role: user.role, avatar: user.avatar, - points: user.points + points: user.points, + payment_status: user.payment_status } }); } catch (error) { @@ -440,11 +425,11 @@ router.post('/login', async (req, res) => { const authenticateToken = (req, res, next) => { const authHeader = req.headers['authorization']; const token = authHeader && authHeader.split(' ')[1]; - + if (!token) { return res.status(401).json({ success: false, message: '访问令牌缺失' }); } - + jwt.verify(token, JWT_SECRET, (err, user) => { if (err) { return res.status(403).json({ success: false, message: '访问令牌无效' }); @@ -462,11 +447,11 @@ router.get('/me', authenticateToken, async (req, res) => { 'SELECT id, username, role, avatar, points, created_at FROM users WHERE id = ?', [req.user.userId] ); - + if (users.length === 0) { return res.status(404).json({ success: false, message: '用户不存在' }); } - + res.json({ success: true, user: users[0] }); } catch (error) { console.error('获取用户信息错误:', error); @@ -479,37 +464,37 @@ router.put('/change-password', authenticateToken, async (req, res) => { try { const db = getDB(); const { currentPassword, newPassword } = req.body; - + if (!currentPassword || !newPassword) { return res.status(400).json({ success: false, message: '旧密码和新密码不能为空' }); } - + // 获取用户当前密码 const [users] = await db.execute( 'SELECT password FROM users WHERE id = ?', [req.user.userId] ); - + if (users.length === 0) { return res.status(404).json({ success: false, message: '用户不存在' }); } - + // 验证旧密码 const isValidPassword = await bcrypt.compare(currentPassword, users[0].password); - + if (!isValidPassword) { return res.status(400).json({ success: false, message: '旧密码错误' }); } - + // 加密新密码 const hashedNewPassword = await bcrypt.hash(newPassword, 10); - + // 更新密码 await db.execute( 'UPDATE users SET password = ? WHERE id = ?', [hashedNewPassword, req.user.userId] ); - + res.json({ success: true, message: '密码修改成功' }); } catch (error) { console.error('修改密码错误:', error); diff --git a/routes/orders.js b/routes/orders.js index a5c24a1..8f82071 100644 --- a/routes/orders.js +++ b/routes/orders.js @@ -114,9 +114,11 @@ router.get('/', auth, async (req, res) => { } // 处理地址信息 + console.log(order.address,'order.address'); + if (order.address) { try { - order.address = JSON.parse(order.address); + order.address = order.address; } catch (e) { order.address = null; } @@ -138,76 +140,7 @@ router.get('/', auth, async (req, res) => { } }); - /** - * @swagger - * /api/orders/confirm: - * post: - * summary: 确认下单 - * tags: [Orders] - * security: - * - bearerAuth: [] - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - pre_order_id - * - address - * properties: - * pre_order_id: - * type: integer - * description: 预订单ID - * address: - * type: object - * properties: - * recipient_name: - * type: string - * description: 收货人姓名 - * phone: - * type: string - * description: 收货人电话 - * province: - * type: string - * description: 省份 - * city: - * type: string - * description: 城市 - * district: - * type: string - * description: 区县 - * detail_address: - * type: string - * description: 详细地址 - * responses: - * 200: - * description: 确认下单成功 - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * message: - * type: string - * data: - * type: object - * properties: - * order_id: - * type: integer - * order_no: - * type: string - * 400: - * description: 请求参数错误 - * 401: - * description: 未授权 - * 404: - * description: 预订单不存在 - * 500: - * description: 服务器错误 - */ + router.post('/confirm', auth, async (req, res) => { const connection = await getDB().getConnection(); @@ -331,76 +264,7 @@ router.get('/', auth, async (req, res) => { } }); - /** - * @swagger - * /api/orders/pre-order/{id}: - * get: - * summary: 获取预订单详情 - * tags: [Orders] - * security: - * - bearerAuth: [] - * parameters: - * - in: path - * name: id - * required: true - * schema: - * type: integer - * description: 预订单ID - * responses: - * 200: - * description: 获取预订单详情成功 - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * data: - * type: object - * properties: - * id: - * type: integer - * order_no: - * type: string - * total_amount: - * type: integer - * total_points: - * type: integer - * total_rongdou: - * type: integer - * status: - * type: string - * created_at: - * type: string - * items: - * type: array - * items: - * type: object - * properties: - * id: - * type: integer - * product_id: - * type: integer - * product_name: - * type: string - * quantity: - * type: integer - * price: - * type: integer - * points_price: - * type: integer - * rongdou_price: - * type: integer - * spec_info: - * type: object - * 401: - * description: 未授权 - * 404: - * description: 预订单不存在 - * 500: - * description: 服务器错误 - */ + router.get('/pre-order/:id', auth, async (req, res) => { try { const preOrderId = req.params.id; @@ -462,43 +326,7 @@ 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; @@ -557,9 +385,11 @@ router.get('/:id', auth, async (req, res) => { } // 处理地址信息 + console.log(order.address,'order.address'); + if (order.address) { try { - order.address = JSON.parse(order.address); + order.address = order.address; } catch (e) { order.address = null; } @@ -1340,7 +1170,7 @@ router.post('/confirm-payment', auth, async (req, res) => { } pointsToDeduct = 0; rongdouToDeduct = totalRongdouNeeded; - } else if (isComboPayment) { + } else if (hasPoints && hasRongdou) { // 组合支付:先扣积分,不足部分用融豆 const availablePointsInRongdou = Math.floor(user.points / 10000); // 积分可转换的融豆数 diff --git a/routes/payment.js b/routes/payment.js new file mode 100644 index 0000000..72a82bd --- /dev/null +++ b/routes/payment.js @@ -0,0 +1,400 @@ +const express = require('express'); +const router = express.Router(); +const WechatPayService = require('../services/wechatPayService'); +const AlipayService = require('../services/alipayservice'); +const { getDB } = require('../database'); +const { auth, paymentAuth } = require('../middleware/auth'); + +// 创建支付服务实例 +const wechatPayService = new WechatPayService(); +const alipayService = new AlipayService(); + +/** + * 获取支持的支付方式 + * GET /api/payment/methods + */ +router.get('/methods', (req, res) => { + res.json({ + success: true, + data: { + methods: [ + { + code: 'wechat_h5', + name: '微信支付', + description: '微信H5支付', + icon: 'wechat', + enabled: true + }, + { + code: 'alipay_wap', + name: '支付宝支付', + description: '支付宝手机网站支付', + icon: 'alipay', + enabled: true + } + ] + } + }); +}); + +/** + * 创建统一支付订单 + * POST /api/payment/create-order + */ +router.post('/create-order', paymentAuth, async (req, res) => { + try { + const { paymentMethod } = req.body; + const userId = req.user.id; + const username = req.user.username; + const phone = req.user.phone; + + // 验证支付方式 + if (!paymentMethod || !['wechat_h5', 'alipay_wap'].includes(paymentMethod)) { + return res.status(400).json({ + success: false, + message: '不支持的支付方式' + }); + } + + // 检查用户是否已经支付过 + const db = getDB(); + const [existingOrders] = await db.execute( + 'SELECT id FROM payment_orders WHERE user_id = ? AND status = "paid"', + [userId] + ); + + if (existingOrders.length > 0) { + return res.status(400).json({ + success: false, + message: '用户已完成支付,无需重复支付' + }); + } + + let result; + + // 获取客户端IP + const clientIp = req.headers['x-forwarded-for'] || + req.headers['x-real-ip'] || + req.connection.remoteAddress || + req.socket.remoteAddress || + (req.connection.socket ? req.connection.socket.remoteAddress : null) || + '127.0.0.1'; + + // 根据支付方式创建订单 + if (paymentMethod === 'wechat_h5') { + // 创建微信支付订单 + result = await wechatPayService.createRegistrationPayOrder({ + userId, + username, + phone, + clientIp + }); + } else if (paymentMethod === 'alipay_wap') { + // 创建支付宝支付订单 + result = await alipayService.createRegistrationPayOrder({ + userId, + username, + phone, + clientIp + }); + } + + if (result && result.success) { + res.json({ + success: true, + data: { + outTradeNo: result.data.outTradeNo, + payUrl: result.data.h5Url || result.data.payUrl, + paymentType: result.data.paymentType, + paymentMethod + } + }); + } else { + res.status(500).json({ + success: false, + message: '创建支付订单失败' + }); + } + } catch (error) { + console.error('创建统一支付订单异常:', error); + res.status(500).json({ + success: false, + message: error.message || '服务器内部错误' + }); + } +}); + +/** + * 查询支付状态 + * GET /api/payment/query-status/:outTradeNo + */ +router.get('/query-status/:outTradeNo', paymentAuth, async (req, res) => { + try { + const { outTradeNo } = req.params; + const userId = req.user.id; + + // 验证订单是否属于当前用户 + const db = getDB(); + const [orders] = await db.execute( + 'SELECT id FROM payment_orders WHERE out_trade_no = ? AND user_id = ?', + [outTradeNo, userId] + ); + + if (orders.length === 0) { + return res.status(404).json({ + success: false, + message: '订单不存在或无权限访问' + }); + } + + // 获取订单详细信息,包括trade_type + const [orderDetails] = await db.execute( + 'SELECT id, trade_type FROM payment_orders WHERE id = ?', + [orders[0].id] + ); + + if (orderDetails.length === 0) { + return res.status(404).json({ + success: false, + message: '订单详情不存在' + }); + } + + let result; + const tradeType = orderDetails[0].trade_type; + + // 根据交易类型查询支付状态 + if (tradeType === 'WECHAT_H5') { + // 查询微信支付状态 + result = await wechatPayService.queryPaymentStatus(outTradeNo); + } else if (tradeType === 'ALIPAY_WAP') { + // 查询支付宝支付状态 + result = await alipayService.queryPaymentStatus(outTradeNo); + } else { + return res.status(400).json({ + success: false, + message: '不支持的支付方式' + }); + } + + res.json(result); + } catch (error) { + console.error('查询支付状态失败:', error); + res.status(500).json({ + success: false, + message: error.message || '查询支付状态失败' + }); + } +}); + +/** + * 获取用户支付记录 + * GET /api/payment/orders + */ +router.get('/orders', paymentAuth, async (req, res) => { + try { + const userId = req.user.id; + const { page = 1, limit = 10, status } = req.query; + + const offset = (page - 1) * limit; + const db = getDB(); + + let whereClause = 'WHERE user_id = ?'; + let params = [userId]; + + if (status) { + whereClause += ' AND status = ?'; + params.push(status); + } + + // 查询订单列表 + const [orders] = await db.execute( + `SELECT id, out_trade_no, transaction_id, total_fee, body, trade_type, + status, paid_at, created_at + FROM payment_orders + ${whereClause} + ORDER BY created_at DESC + LIMIT ? OFFSET ?`, + [...params, parseInt(limit), parseInt(offset)] + ); + + // 查询总数 + const [countResult] = await db.execute( + `SELECT COUNT(*) as total FROM payment_orders ${whereClause}`, + params + ); + + const total = countResult[0].total; + + res.json({ + success: true, + data: { + orders: orders.map(order => ({ + ...order, + total_fee: order.total_fee / 100, // 转换为元 + payment_method_name: order.trade_type && order.trade_type.startsWith('ALIPAY') ? '支付宝支付' : '微信支付' + })), + pagination: { + page: parseInt(page), + limit: parseInt(limit), + total, + pages: Math.ceil(total / limit) + } + } + }); + } catch (error) { + console.error('获取支付记录失败:', error); + res.status(500).json({ + success: false, + message: '获取支付记录失败' + }); + } +}); + +/** + * 检查用户支付状态 + * GET /api/payment/check-status + */ +router.get('/check-status', auth, async (req, res) => { + try { + const userId = req.user.id; + const db = getDB(); + + // 查询用户支付状态 + const [users] = await db.execute( + 'SELECT payment_status FROM users WHERE id = ?', + [userId] + ); + + if (users.length === 0) { + return res.status(404).json({ + success: false, + message: '用户不存在' + }); + } + + const paymentStatus = users[0].payment_status; + + // 查询最近的支付订单 + const [recentOrders] = await db.execute( + `SELECT out_trade_no, trade_type, status, total_fee, paid_at + FROM payment_orders + WHERE user_id = ? + ORDER BY created_at DESC + LIMIT 1`, + [userId] + ); + + res.json({ + success: true, + data: { + paymentStatus, + isPaid: paymentStatus === 'paid', + recentOrder: recentOrders.length > 0 ? { + ...recentOrders[0], + total_fee: recentOrders[0].total_fee / 100, + payment_method_name: recentOrders[0].trade_type.startsWith('ALIPAY') ? '支付宝支付' : '微信支付' + } : null + } + }); + } catch (error) { + console.error('检查用户支付状态失败:', error); + res.status(500).json({ + success: false, + message: '检查支付状态失败' + }); + } +}); + +/** + * 支付宝支付回调通知 + * POST /api/payment/alipay/notify + */ +router.post('/alipay/notify', async (req, res) => { + try { + console.log('收到支付宝支付回调:', req.body); + + // 验证签名 + const isValid = alipayService.verifyNotifySign(req.body); + if (!isValid) { + console.error('支付宝回调签名验证失败'); + return res.status(400).send('FAIL'); + } + + const { + out_trade_no: outTradeNo, + trade_no: transactionId, + trade_status: tradeStatus, + total_amount: totalAmount + } = req.body; + + // 只处理支付成功的回调 + if (tradeStatus === 'TRADE_SUCCESS') { + const db = getDB(); + + // 检查订单是否存在 + const [orders] = await db.execute( + 'SELECT id, user_id, status FROM payment_orders WHERE out_trade_no = ?', + [outTradeNo] + ); + + if (orders.length === 0) { + console.error('支付宝回调:订单不存在', outTradeNo); + return res.status(400).send('FAIL'); + } + + const order = orders[0]; + + // 如果订单已经处理过,直接返回成功 + if (order.status === 'paid') { + console.log('支付宝回调:订单已处理', outTradeNo); + return res.send('SUCCESS'); + } + + // 更新订单状态 + await alipayService.updatePaymentStatus(outTradeNo, { + status: 'paid', + transactionId, + paidAt: new Date() + }); + + console.log('支付宝支付成功处理完成:', { + outTradeNo, + transactionId, + userId: order.user_id + }); + } + + res.send('SUCCESS'); + } catch (error) { + console.error('处理支付宝支付回调失败:', error); + res.status(500).send('FAIL'); + } +}); + +/** + * 支付宝支付返回页面处理 + * GET /api/payment/alipay/return + */ +router.get('/alipay/return', async (req, res) => { + try { + console.log('支付宝支付返回:', req.query); + + // 验证签名 + const isValid = alipayService.verifyNotifySign(req.query); + if (!isValid) { + console.error('支付宝返回签名验证失败'); + return res.redirect('/payment/failed'); + } + + const { out_trade_no: outTradeNo } = req.query; + + // 重定向到支付成功页面 + res.redirect(`/payment/success?outTradeNo=${outTradeNo}`); + } catch (error) { + console.error('处理支付宝支付返回失败:', error); + res.redirect('/payment/failed'); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/routes/regions.js b/routes/regions.js index 112f8d3..efdc04b 100644 --- a/routes/regions.js +++ b/routes/regions.js @@ -120,40 +120,50 @@ router.get('/zhejiang', async (req, res) => { */ router.get('/provinces', async (req, res) => { try { - // 递归获取子区域的函数 - async function getChildrenRecursively(parentCode, level) { - const [children] = await getDB().execute( - `SELECT code, name as label, level FROM china_regions - WHERE parent_code = ? AND level = ? - ORDER BY code`, - [parentCode, level] - ); - - // 为每个子区域递归获取其子区域 - for (let child of children) { - if (level < 3) { // 最多到区县级别(level 3) - child.children = await getChildrenRecursively(child.code, level + 1); - } - } - - return children; - } - - // 获取所有省份 - const [provinces] = await getDB().execute( - `SELECT code, name as label, level FROM china_regions - WHERE level = 1 - ORDER BY code` + // 一次性获取所有区域数据(省、市、区县) + const [allRegions] = await getDB().execute( + `SELECT code, name as label, level, parent_code FROM china_regions + WHERE level <= 3 + ORDER BY level, code` ); - // 为每个省份递归获取城市和区县 - for (let province of provinces) { - province.children = await getChildrenRecursively(province.code, 2); - } + // 按level分组数据 + const regionsByLevel = { + 1: [], // 省份 + 2: [], // 城市 + 3: [] // 区县 + }; + // 创建code到region的映射,便于快速查找 + const regionMap = {}; + + // 分组并建立映射 + allRegions.forEach(region => { + region.children = []; // 初始化children数组 + regionsByLevel[region.level].push(region); + regionMap[region.code] = region; + }); + + // 构建层级关系:先处理区县到城市的关系 + regionsByLevel[3].forEach(district => { + const parentCity = regionMap[district.parent_code]; + if (parentCity) { + parentCity.children.push(district); + } + }); + + // 再处理城市到省份的关系 + regionsByLevel[2].forEach(city => { + const parentProvince = regionMap[city.parent_code]; + if (parentProvince) { + parentProvince.children.push(city); + } + }); + + // 返回省份数据(已包含完整的层级结构) res.json({ success: true, - data: provinces + data: regionsByLevel[1] }); } catch (error) { console.error('获取省份列表错误:', error); diff --git a/routes/sms.js b/routes/sms.js index 91f3b3d..5492867 100644 --- a/routes/sms.js +++ b/routes/sms.js @@ -138,18 +138,6 @@ router.post('/send', async (req, res) => { // 记录发送时间 smsCodeStore.set(`last_send_${phone}`, Date.now()) - - // 开发环境下模拟发送成功 - // if (SMS_CONFIG.isDevelopment) { - // console.log(`[开发环境] 短信验证码: ${code} 发送到 ${phone}`) - // return res.json({ - // success: true, - // message: '验证码发送成功', - // // 开发环境下返回验证码便于测试 - // code: process.env.NODE_ENV === 'development' ? code : undefined - // }) - // } - // 生产环境发送真实短信 try { const sendSmsRequest = new Dysmsapi20170525.SendSmsRequest({ diff --git a/routes/transfers.js b/routes/transfers.js index b4f71d0..3c6c1e3 100644 --- a/routes/transfers.js +++ b/routes/transfers.js @@ -1234,6 +1234,11 @@ router.post('/force-change-status/:transferId', authenticateToken, async (req, r const { transferId } = req.params; const { newStatus, status, reason, adjust_balance = false } = req.body; + console.log('newStatus:', newStatus); + console.log('status:', status); + console.log('reason:', reason); + console.log('adjust_balance:', adjust_balance); + // 兼容两种参数名:newStatus 和 status const actualNewStatus = newStatus || status; const adminId = req.user.id; @@ -1248,9 +1253,9 @@ router.post('/force-change-status/:transferId', authenticateToken, async (req, r return res.status(400).json({ success: false, message: '新状态不能为空' }); } - if (!reason) { - return res.status(400).json({ success: false, message: '变更原因不能为空' }); - } + // if (!reason) { + // return res.status(400).json({ success: false, message: '变更原因不能为空' }); + // } const result = await transferService.forceChangeTransferStatus( transferId, diff --git a/routes/wechatPay.js b/routes/wechatPay.js new file mode 100644 index 0000000..149e337 --- /dev/null +++ b/routes/wechatPay.js @@ -0,0 +1,202 @@ +const express = require('express'); +const router = express.Router(); +const WechatPayService = require('../services/wechatPayService'); +const { getDB } = require('../database'); +const { auth, paymentAuth } = require('../middleware/auth'); + +// 创建微信支付服务实例 +const wechatPayService = new WechatPayService(); + +/** + * 创建注册支付订单 (API v3 - H5支付) + * POST /api/wechat-pay/create-registration-order + */ +router.post('/create-registration-order', paymentAuth, async (req, res) => { + try { + const userId = req.user.id; + const username = req.user.username; + const phone = req.user.phone; + + // 获取客户端IP + const clientIp = req.headers['x-forwarded-for'] || + req.headers['x-real-ip'] || + req.connection.remoteAddress || + req.socket.remoteAddress || + (req.connection.socket ? req.connection.socket.remoteAddress : null) || + '127.0.0.1'; + + // 检查用户是否已经支付过 + const db = getDB(); + const [existingOrders] = await db.execute( + 'SELECT id FROM payment_orders WHERE user_id = ? AND status = "paid"', + [userId] + ); + + if (existingOrders.length > 0) { + return res.status(400).json({ + success: false, + message: '用户已完成支付,无需重复支付' + }); + } + + console.log('创建H5支付订单:', { + userId, + username, + phone, + clientIp + }); + + // 创建H5支付订单 + const result = await wechatPayService.createRegistrationPayOrder({ + userId, + username, + phone, + clientIp + }); + + if (result.success) { + res.json({ + success: true, + data: { + outTradeNo: result.data.outTradeNo, + h5Url: result.data.h5Url, + paymentType: result.data.paymentType + } + }); + } else { + res.status(500).json({ + success: false, + message: '创建支付订单失败' + }); + } + } catch (error) { + console.error('创建H5支付订单异常:', error); + res.status(500).json({ + success: false, + message: error.message || '服务器内部错误' + }); + } +}); + +// H5支付不需要获取openid,移除相关接口 + +/** + * 微信支付回调接口 (API v3) + * POST /api/wechat-pay/notify + */ +router.post('/notify', async (req, res) => { + try { + // API v3 回调是JSON格式 + const notifyData = req.body; + + // 获取请求头中的签名信息 + const signature = req.headers['wechatpay-signature']; + const timestamp = req.headers['wechatpay-timestamp']; + const nonce = req.headers['wechatpay-nonce']; + const serial = req.headers['wechatpay-serial']; + + console.log('收到API v3支付回调:', { + signature, + timestamp, + nonce, + serial, + body: notifyData + }); + + // 验证签名和处理回调 + const result = await wechatPayService.handleV3PaymentNotify({ + signature, + timestamp, + nonce, + serial, + body: JSON.stringify(notifyData) + }); + + if (result.success) { + // API v3 成功响应 + res.status(200).json({ code: 'SUCCESS', message: '成功' }); + } else { + // API v3 失败响应 + res.status(400).json({ code: 'FAIL', message: result.message || '处理失败' }); + } + } catch (error) { + console.error('支付回调处理异常:', error); + res.status(500).json({ code: 'ERROR', message: '服务器内部错误' }); + } +}); + +/** + * 查询支付状态 + * GET /api/wechat-pay/query-status/:outTradeNo + */ +router.get('/query-status/:outTradeNo', paymentAuth, async (req, res) => { + try { + const { outTradeNo } = req.params; + const userId = req.user.id; + + // 验证订单是否属于当前用户 + const db = getDB(); + const [orders] = await db.execute( + 'SELECT id FROM payment_orders WHERE out_trade_no = ? AND user_id = ?', + [outTradeNo, userId] + ); + + if (orders.length === 0) { + return res.status(404).json({ + success: false, + message: '订单不存在或无权限访问' + }); + } + + const result = await wechatPayService.queryPaymentStatus(outTradeNo); + res.json(result); + } catch (error) { + console.error('查询支付状态失败:', error); + res.status(500).json({ + success: false, + message: error.message || '查询支付状态失败' + }); + } +}); + +/** + * 检查用户支付状态 + * GET /api/wechat-pay/check-user-payment + */ +router.get('/check-user-payment', auth, async (req, res) => { + try { + const userId = req.user.id; + const db = getDB(); + + // 查询用户支付状态 + const [users] = await db.execute( + 'SELECT payment_status FROM users WHERE id = ?', + [userId] + ); + + if (users.length === 0) { + return res.status(404).json({ + success: false, + message: '用户不存在' + }); + } + + const paymentStatus = users[0].payment_status; + + res.json({ + success: true, + data: { + paymentStatus, + isPaid: paymentStatus === 'paid' + } + }); + } catch (error) { + console.error('检查用户支付状态失败:', error); + res.status(500).json({ + success: false, + message: '检查支付状态失败' + }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/run_migration.js b/run_migration.js new file mode 100644 index 0000000..554bc23 --- /dev/null +++ b/run_migration.js @@ -0,0 +1,87 @@ +const mysql = require('mysql2/promise'); +const fs = require('fs'); +const path = require('path'); +require('dotenv').config(); + +async function runMigration() { + let connection; + + try { + // 创建数据库连接 + connection = await mysql.createConnection({ + host: process.env.DB_HOST, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_NAME, + charset: 'utf8mb4' + }); + + console.log('数据库连接成功'); + + // 直接定义SQL语句 + const sqlStatements = [ + `CREATE TABLE IF NOT EXISTS \`payment_orders\` ( + \`id\` int(11) NOT NULL AUTO_INCREMENT COMMENT '订单ID', + \`user_id\` int(11) NOT NULL COMMENT '用户ID', + \`out_trade_no\` varchar(64) NOT NULL COMMENT '商户订单号', + \`transaction_id\` varchar(64) DEFAULT NULL COMMENT '微信支付订单号', + \`total_fee\` int(11) NOT NULL COMMENT '订单金额(分)', + \`body\` varchar(128) NOT NULL COMMENT '商品描述', + \`trade_type\` varchar(16) NOT NULL COMMENT '交易类型', + \`prepay_id\` varchar(64) DEFAULT NULL COMMENT '预支付交易会话标识', + \`mweb_url\` text DEFAULT NULL COMMENT 'H5支付跳转链接', + \`status\` enum('pending','paid','failed','cancelled') NOT NULL DEFAULT 'pending' COMMENT '支付状态', + \`paid_at\` datetime DEFAULT NULL COMMENT '支付完成时间', + \`created_at\` datetime NOT NULL COMMENT '创建时间', + \`updated_at\` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (\`id\`), + UNIQUE KEY \`uk_out_trade_no\` (\`out_trade_no\`), + KEY \`idx_user_id\` (\`user_id\`), + KEY \`idx_status\` (\`status\`), + KEY \`idx_created_at\` (\`created_at\`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='支付订单表'`, + + `ALTER TABLE \`users\` ADD COLUMN \`payment_status\` enum('unpaid','paid') NOT NULL DEFAULT 'unpaid' COMMENT '支付状态'`, + + `ALTER TABLE \`users\` ADD KEY \`idx_payment_status\` (\`payment_status\`)` + ]; + + console.log(`准备执行 ${sqlStatements.length} 条SQL语句`); + + // 执行每条SQL语句 + for (let i = 0; i < sqlStatements.length; i++) { + const statement = sqlStatements[i]; + console.log(`执行第 ${i + 1} 条语句...`); + + try { + await connection.execute(statement); + console.log(`✓ 第 ${i + 1} 条语句执行成功`); + } catch (error) { + if (error.code === 'ER_DUP_FIELDNAME') { + console.log(`⚠ 第 ${i + 1} 条语句跳过(字段已存在): ${error.message}`); + } else if (error.code === 'ER_TABLE_EXISTS_ERROR') { + console.log(`⚠ 第 ${i + 1} 条语句跳过(表已存在): ${error.message}`); + } else if (error.code === 'ER_DUP_KEYNAME') { + console.log(`⚠ 第 ${i + 1} 条语句跳过(索引已存在): ${error.message}`); + } else { + console.error(`✗ 第 ${i + 1} 条语句执行失败:`, error.message); + throw error; + } + } + } + + console.log('\n✅ 数据库迁移完成!'); + + } catch (error) { + console.error('❌ 数据库迁移失败:', error.message); + process.exit(1); + } finally { + if (connection) { + await connection.end(); + console.log('数据库连接已关闭'); + } + } +} + +// 运行迁移 +runMigration(); \ No newline at end of file diff --git a/server.js b/server.js index 62d83dc..5d5f44e 100644 --- a/server.js +++ b/server.js @@ -40,6 +40,7 @@ app.use(cors({ origin: [ 'http://localhost:5173', 'http://localhost:5176', + 'http://localhost:5175', 'http://localhost:5174', 'http://localhost:3001', 'https://www.zrbjr.com', @@ -250,6 +251,9 @@ app.use('/api/regions', require('./routes/regions')); app.use('/api/addresses', require('./routes/addresses')); app.use('/api/address-labels', require('./routes/address-labels')); app.use('/api/cart', require('./routes/cart')); +app.use('/api/announcements', require('./routes/announcements')); // 通知公告路由 +app.use('/api/wechat-pay', require('./routes/wechatPay')); // 只保留微信支付 +app.use('/api/payment', require('./routes/payment')); // 前端路由 - 必须在最后,作为fallback app.get('/', (req, res) => { diff --git a/services/alipayservice.js b/services/alipayservice.js new file mode 100644 index 0000000..6b82db0 --- /dev/null +++ b/services/alipayservice.js @@ -0,0 +1,224 @@ +const { AlipaySdk } = require('alipay-sdk'); +const { getDB } = require('../database'); +const crypto = require('crypto'); +const path = require('path'); +const fs = require('fs'); + +class AlipayService { + constructor() { + // 读取密钥文件 + const privateKeyPath = path.join(__dirname, '../certs/alipay-private-key.pem'); + const publicKeyPath = path.join(__dirname, '../certs/alipay-public-key.pem'); + + const privateKey = fs.readFileSync(privateKeyPath, 'utf8'); + const alipayPublicKey = fs.readFileSync(publicKeyPath, 'utf8'); + + // 支付宝配置 + this.config = { + appId: process.env.ALIPAY_APP_ID || '2021001161683774', // 替换为实际的应用ID + privateKey: privateKey, // 从文件读取的应用私钥 + alipayPublicKey: alipayPublicKey, // 从文件读取的支付宝公钥 + gateway: 'https://openapi.alipay.com/gateway.do', // 支付宝网关地址 + signType: 'RSA2', + charset: 'utf-8', + version: '1.0', + timeout: 5000 + }; + + // 初始化支付宝SDK + this.alipaySdk = new AlipaySdk({ + appId: this.config.appId, + privateKey: this.config.privateKey, + alipayPublicKey: this.config.alipayPublicKey, + gateway: this.config.gateway, + signType: this.config.signType, + timeout: this.config.timeout + }); + } + + /** + * 创建注册支付订单 + * @param {Object} params - 支付参数 + * @param {string} params.userId - 用户ID + * @param {string} params.username - 用户名 + * @param {string} params.phone - 手机号 + * @param {string} params.clientIp - 客户端IP + * @returns {Promise} 支付结果 + */ + async createRegistrationPayOrder({ userId, username, phone, clientIp }) { + try { + const db = getDB(); + + // 生成订单号 + const outTradeNo = this.generateOrderNo(); + const totalFee = 39900; // 399元,单位:分 + const subject = '用户注册激活费用'; + const body = `用户${username}(${phone})注册激活费用`; + + // 业务参数 + const bizContent = { + out_trade_no: outTradeNo, + total_amount: (totalFee / 100).toFixed(2), // 转换为元 + subject: subject, + body: body, + product_code: 'QUICK_WAP_WAY', + quit_url: process.env.ALIPAY_QUIT_URL || 'https://your-domain.com/payment/cancel' + }; + + // 使用新版SDK的pageExecute方法生成支付URL + const payUrl = this.alipaySdk.pageExecute('alipay.trade.wap.pay', 'GET', { + bizContent: bizContent, + notifyUrl: process.env.ALIPAY_NOTIFY_URL || 'https://your-domain.com/api/payment/alipay/notify', + returnUrl: process.env.ALIPAY_RETURN_URL || 'https://your-domain.com/payment/success' + }); + + // 保存订单到数据库 + await db.execute( + `INSERT INTO payment_orders + (user_id, out_trade_no, total_fee, body, trade_type, status, created_at) + VALUES (?, ?, ?, ?, ?, ?, NOW())`, + [userId, outTradeNo, totalFee, body, 'ALIPAY_WAP', 'pending'] + ); + + console.log('支付宝支付订单创建成功:', { + userId, + outTradeNo, + totalFee, + payUrl + }); + + return { + success: true, + data: { + outTradeNo, + payUrl, + paymentType: 'alipay_wap', + totalFee + } + }; + } catch (error) { + console.error('创建支付宝支付订单失败:', error); + return { + success: false, + message: error.message || '创建支付订单失败' + }; + } + } + + /** + * 查询支付状态 + * @param {string} outTradeNo - 商户订单号 + * @returns {Promise} 查询结果 + */ + async queryPaymentStatus(outTradeNo) { + try { + const result = await this.alipaySdk.exec('alipay.trade.query', { + bizContent: { + out_trade_no: outTradeNo + } + }); + + if (result.code === '10000') { + // 查询成功 + const tradeStatus = result.tradeStatus; + + // 如果支付成功,更新数据库 + if (tradeStatus === 'TRADE_SUCCESS') { + await this.updatePaymentStatus(outTradeNo, { + status: 'paid', + transactionId: result.tradeNo, + paidAt: new Date() + }); + } + + return { + success: true, + data: { + trade_status: tradeStatus, + trade_no: result.tradeNo, + total_amount: result.totalAmount, + buyer_pay_amount: result.buyerPayAmount, + gmt_payment: result.gmtPayment + } + }; + } else { + return { + success: false, + message: result.msg || '查询支付状态失败' + }; + } + } catch (error) { + console.error('查询支付宝支付状态失败:', error); + return { + success: false, + message: error.message || '查询支付状态失败' + }; + } + } + + /** + * 更新支付状态 + * @param {string} outTradeNo - 商户订单号 + * @param {Object} updateData - 更新数据 + */ + async updatePaymentStatus(outTradeNo, updateData) { + try { + const db = getDB(); + + // 更新订单状态 + await db.execute( + `UPDATE payment_orders + SET status = ?, transaction_id = ?, paid_at = ? + WHERE out_trade_no = ?`, + [updateData.status, updateData.transactionId, updateData.paidAt, outTradeNo] + ); + + // 如果支付成功,更新用户支付状态 + if (updateData.status === 'paid') { + const [orders] = await db.execute( + 'SELECT user_id FROM payment_orders WHERE out_trade_no = ?', + [outTradeNo] + ); + + if (orders.length > 0) { + const userId = orders[0].user_id; + await db.execute( + 'UPDATE users SET payment_status = ? WHERE id = ?', + ['paid', userId] + ); + + console.log('用户支付状态更新成功:', { userId, outTradeNo }); + } + } + } catch (error) { + console.error('更新支付状态失败:', error); + throw error; + } + } + + /** + * 验证支付宝回调签名 + * @param {Object} params - 回调参数 + * @returns {boolean} 验证结果 + */ + verifyNotifySign(params) { + try { + return this.alipaySdk.checkNotifySign(params); + } catch (error) { + console.error('验证支付宝回调签名失败:', error); + return false; + } + } + + /** + * 生成订单号 + * @returns {string} 订单号 + */ + generateOrderNo() { + const timestamp = Date.now(); + const random = Math.floor(Math.random() * 1000).toString().padStart(3, '0'); + return `ALI${timestamp}${random}`; + } +} + +module.exports = AlipayService; \ No newline at end of file diff --git a/services/matchingService.js b/services/matchingService.js index 0b07fbb..c76476a 100644 --- a/services/matchingService.js +++ b/services/matchingService.js @@ -851,7 +851,8 @@ class MatchingService { historicalNetBalance: user.historical_net_balance, totalPendingInflow: user.total_pending_inflow, availableForAllocation: user.available_for_allocation, - todayOutflow: user.today_outflow + todayOutflow: user.today_outflow, + has_active_allocations:user.has_active_allocations }); remainingAmount -= maxUserAllocation; } @@ -871,7 +872,7 @@ class MatchingService { // 计算每个用户的剩余可分配容量 for (const allocation of allocations) { // 获取用户当前的实际余额状态(使用has_active_allocations作为实际可分配余额) - const maxSafeAmount = Math.abs(allocation.availableForAllocation); + const maxSafeAmount = Math.abs(allocation.has_active_allocations); const remainingCapacity = maxSafeAmount - allocation.amount; if (remainingCapacity > 0) { diff --git a/services/transferService.js b/services/transferService.js index f8f18ab..55b9e86 100644 --- a/services/transferService.js +++ b/services/transferService.js @@ -862,14 +862,15 @@ 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')) { + if (transferStatuses.every(status => status === 'cancelled' || status === 'rejected' || status === 'not_received' || status === 'confirmed' || status === 'received')) { // 如果所有transfers都被取消/拒绝/未收到,匹配订单标记为已完成 matchingOrderStatus = 'completed'; } else if (transferStatuses.every(status => status === 'received')) { // 如果所有transfers都已收到,匹配订单完成 matchingOrderStatus = 'completed'; - } else if (transferStatuses.includes('cancelled') || transferStatuses.includes('rejected') || transferStatuses.includes('not_received') || transferStatuses.some(status => status === 'confirmed' || status === 'received')) { + } else if (transferStatuses.includes('cancelled') || transferStatuses.includes('rejected') || transferStatuses.includes('not_received') ) { // 如果有任何一个transfer被取消/拒绝/未收到,或者有transfers已确认或已收到,匹配订单为进行中状态 matchingOrderStatus = 'matching'; } else { diff --git a/services/wechatPayService.js b/services/wechatPayService.js new file mode 100644 index 0000000..748e070 --- /dev/null +++ b/services/wechatPayService.js @@ -0,0 +1,566 @@ +const crypto = require('crypto'); +const axios = require('axios'); +const fs = require('fs'); +const path = require('path'); +const { wechatPay } = require('../config/wechatPay'); +const { getDB } = require('../database'); + +class WechatPayService { + constructor() { + this.config = { + ...wechatPay, + apiV3Key: process.env.WECHAT_API_V3_KEY + }; + this.privateKey = null; // API v3 私钥 + this.serialNo = null; // 商户证书序列号 + this.initializeV3(); + } + + // 初始化API v3配置 + async initializeV3() { + try { + // 加载私钥 + const keyPath = path.resolve(__dirname, '..', this.config.keyPath.replace(/^\.\//, '')); + if (fs.existsSync(keyPath)) { + this.privateKey = fs.readFileSync(keyPath, 'utf8'); + console.log('API v3 私钥加载成功'); + } else { + console.error('私钥文件不存在:', keyPath); + } + + // 获取证书序列号 + const certPath = path.resolve(__dirname, '..', this.config.certPath.replace(/^\.\//, '')); + if (fs.existsSync(certPath)) { + const cert = fs.readFileSync(certPath, 'utf8'); + this.serialNo = this.getCertificateSerialNumber(cert); + console.log('证书序列号:', this.serialNo); + } else { + console.error('证书文件不存在:', certPath); + } + } catch (error) { + console.error('初始化API v3配置失败:', error.message); + } + } + + // 获取证书序列号 + getCertificateSerialNumber(cert) { + try { + const x509 = crypto.X509Certificate ? new crypto.X509Certificate(cert) : null; + if (x509) { + return x509.serialNumber.toLowerCase().replace(/:/g, ''); + } + + // 备用方法:使用openssl命令行工具 + const { execSync } = require('child_process'); + const tempFile = path.join(__dirname, 'temp_cert.pem'); + fs.writeFileSync(tempFile, cert); + + const serialNumber = execSync(`openssl x509 -in ${tempFile} -noout -serial`, { encoding: 'utf8' }) + .replace('serial=', '') + .trim() + .toLowerCase(); + + fs.unlinkSync(tempFile); + return serialNumber; + } catch (error) { + console.error('获取证书序列号失败:', error.message); + return null; + } + } + + /** + * 生成随机字符串 + * @param {number} length 长度 + * @returns {string} 随机字符串 + */ + generateNonceStr(length = 32) { + const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + let result = ''; + for (let i = 0; i < length; i++) { + result += chars.charAt(Math.floor(Math.random() * chars.length)); + } + return result; + } + + /** + * 生成时间戳 + * @returns {string} 时间戳 + */ + generateTimestamp() { + return Math.floor(Date.now() / 1000).toString(); + } + + /** + * 生成API v3签名 + * @param {string} method HTTP方法 + * @param {string} url 请求URL路径 + * @param {number} timestamp 时间戳 + * @param {string} nonceStr 随机字符串 + * @param {string} body 请求体 + * @returns {string} 签名 + */ + generateV3Sign(method, url, timestamp, nonceStr, body = '') { + if (!this.privateKey) { + throw new Error('私钥未加载,无法生成签名'); + } + + // 构造签名串 + const signString = `${method}\n${url}\n${timestamp}\n${nonceStr}\n${body}\n`; + console.log('API v3 签名字符串:', signString); + + // 使用私钥进行SHA256-RSA签名 + const sign = crypto.sign('RSA-SHA256', Buffer.from(signString, 'utf8'), this.privateKey); + const signature = sign.toString('base64'); + + console.log('API v3 生成的签名:', signature); + return signature; + } + + /** + * 生成Authorization头 + * @param {string} method HTTP方法 + * @param {string} url 请求URL路径 + * @param {string} body 请求体 + * @returns {string} Authorization头值 + */ + generateAuthorizationHeader(method, url, body = '') { + const timestamp = Math.floor(Date.now() / 1000); + const nonceStr = this.generateNonceStr(); + const signature = this.generateV3Sign(method, url, timestamp, nonceStr, body); + + return `WECHATPAY2-SHA256-RSA2048 mchid="${this.config.mchId}",nonce_str="${nonceStr}",signature="${signature}",timestamp="${timestamp}",serial_no="${this.serialNo}"`; + } + + /** + * 生成JSAPI支付参数 + * @param {string} prepayId 预支付交易会话标识 + * @returns {object} JSAPI支付参数 + */ + generateJSAPIPayParams(prepayId) { + const timestamp = Math.floor(Date.now() / 1000).toString(); + const nonceStr = this.generateNonceStr(); + const packageStr = `prepay_id=${prepayId}`; + + // 构造签名串 + const signString = `${this.config.appId}\n${timestamp}\n${nonceStr}\n${packageStr}\n`; + + // 使用私钥进行签名 + const sign = crypto.sign('RSA-SHA256', Buffer.from(signString, 'utf8'), this.privateKey); + const paySign = sign.toString('base64'); + + return { + appId: this.config.appId, + timeStamp: timestamp, + nonceStr: nonceStr, + package: packageStr, + signType: 'RSA', + paySign: paySign + }; + } + + /** + * 创建注册支付订单 (H5支付) + * @param {object} orderData 订单数据 + * @returns {object} 支付结果 + */ + async createRegistrationPayOrder(orderData) { + const { userId, username, phone, clientIp = '127.0.0.1' } = orderData; + + try { + if (!this.privateKey || !this.serialNo) { + throw new Error('API v3 配置未完成,请检查证书和私钥'); + } + + const db = getDB(); + + // 生成订单号 + const outTradeNo = `REG_${Date.now()}_${userId}`; + + // 创建支付订单记录 + await db.execute( + 'INSERT INTO payment_orders (user_id, out_trade_no, total_fee, body, trade_type, status, created_at) VALUES (?, ?, ?, ?, ?, ?, NOW())', + [userId, outTradeNo, this.config.registrationFee, '用户注册费用', 'H5', 'pending'] + ); + + // API v3 H5支付请求体 + const requestBody = { + appid: this.config.appId, + mchid: this.config.mchId, + description: '用户注册费用', + out_trade_no: outTradeNo, + notify_url: this.config.notifyUrl, + amount: { + total: this.config.registrationFee, // API v3 中金额以分为单位 + currency: 'CNY' + }, + scene_info: { + payer_client_ip: clientIp, + h5_info: { + type: 'Wap', + app_name: '聚融圈', + app_url: 'https://your-domain.com', + bundle_id: 'com.jurong.circle' + } + } + }; + + console.log('API v3 H5支付参数:', requestBody); + + const requestBodyStr = JSON.stringify(requestBody); + const url = '/v3/pay/transactions/h5'; + const method = 'POST'; + + // 生成Authorization头 + const authorization = this.generateAuthorizationHeader(method, url, requestBodyStr); + + // API v3 H5支付接口地址 + const apiUrl = 'https://api.mch.weixin.qq.com/v3/pay/transactions/h5'; + + console.log('使用的API v3 H5地址:', apiUrl); + console.log('Authorization头:', authorization); + + const response = await axios.post(apiUrl, requestBody, { + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Authorization': authorization, + 'User-Agent': 'jurong-circle/1.0.0' + } + }); + + console.log('微信支付API v3 H5响应:', response.data); + + if (response.data && response.data.h5_url) { + // 更新订单状态 + await db.execute( + 'UPDATE payment_orders SET mweb_url = ? WHERE out_trade_no = ?', + [response.data.h5_url, outTradeNo] + ); + + return { + success: true, + data: { + outTradeNo, + h5Url: response.data.h5_url, + paymentType: 'h5' + } + }; + } else { + console.log(response.data); + + throw new Error(response.data?.message || '支付订单创建失败'); + } + } catch (error) { + console.error('创建H5支付订单失败:', error.response?.data || error.message); + throw new Error('支付订单创建失败: ' + (error.response?.data?.message || error.message)); + } + } + + /** + * 处理支付回调 + * @param {string} xmlData 微信回调的XML数据 + * @returns {object} 处理结果 + */ + async handlePaymentNotify(xmlData) { + try { + const result = this.xmlToObject(xmlData); + + // 验证签名 + const sign = result.sign; + delete result.sign; + const calculatedSign = this.generateSign(result); + + if (sign !== calculatedSign) { + throw new Error('签名验证失败'); + } + + if (result.return_code === 'SUCCESS' && result.result_code === 'SUCCESS') { + const db = getDB(); + + // 开始事务 + await db.beginTransaction(); + + try { + // 更新支付订单状态 + await db.execute( + 'UPDATE payment_orders SET status = ?, transaction_id = ?, paid_at = NOW() WHERE out_trade_no = ?', + ['paid', result.transaction_id, result.out_trade_no] + ); + + // 获取订单信息 + const [orders] = await db.execute( + 'SELECT user_id FROM payment_orders WHERE out_trade_no = ?', + [result.out_trade_no] + ); + + if (orders.length > 0) { + const userId = orders[0].user_id; + + // 激活用户账户 + await db.execute( + 'UPDATE users SET payment_status = "paid" WHERE id = ?', + [userId] + ); + + console.log(`用户 ${userId} 支付成功,账户已激活`); + } + + // 提交事务 + await db.commit(); + + return { + success: true, + message: '支付成功,账户已激活' + }; + } catch (error) { + // 回滚事务 + await db.rollback(); + throw error; + } + } else { + const db = getDB(); + + // 更新订单状态为失败 + await db.execute( + 'UPDATE payment_orders SET status = ? WHERE out_trade_no = ?', + ['failed', result.out_trade_no] + ); + + return { + success: false, + message: '支付失败' + }; + } + } catch (error) { + console.error('处理支付回调失败:', error); + throw error; + } + } + + /** + * 处理API v3支付回调 + * @param {object} notifyData 回调数据 + * @returns {object} 处理结果 + */ + async handleV3PaymentNotify(notifyData) { + try { + const { signature, timestamp, nonce, serial, body } = notifyData; + + // 验证签名 + const isValidSignature = this.verifyV3Signature({ + timestamp, + nonce, + body, + signature + }); + + if (!isValidSignature) { + console.error('API v3回调签名验证失败'); + return { success: false, message: '签名验证失败' }; + } + + console.log('API v3回调签名验证成功'); + + // 解析回调数据 + const callbackData = JSON.parse(body); + console.log('解析的回调数据:', callbackData); + + // 检查事件类型 + if (callbackData.event_type === 'TRANSACTION.SUCCESS') { + // 解密resource数据 + const resource = callbackData.resource; + const decryptedData = this.decryptV3Resource(resource); + + console.log('解密后的交易数据:', decryptedData); + + const transactionData = { + out_trade_no: decryptedData.out_trade_no, + transaction_id: decryptedData.transaction_id, + trade_state: decryptedData.trade_state + }; + + console.log('交易数据:', transactionData); + + if (transactionData.trade_state === 'SUCCESS') { + const db = getDB(); + + // 开始事务 + await db.beginTransaction(); + + try { + // 更新支付订单状态 + await db.execute( + 'UPDATE payment_orders SET status = ?, transaction_id = ?, paid_at = NOW() WHERE out_trade_no = ?', + ['paid', transactionData.transaction_id, transactionData.out_trade_no] + ); + + // 获取订单信息 + const [orders] = await db.execute( + 'SELECT user_id FROM payment_orders WHERE out_trade_no = ?', + [transactionData.out_trade_no] + ); + + if (orders.length > 0) { + const userId = orders[0].user_id; + + // 激活用户账户 + await db.execute( + 'UPDATE users SET payment_status = "paid" WHERE id = ?', + [userId] + ); + + console.log(`用户 ${userId} API v3支付成功,账户已激活`); + } + + // 提交事务 + await db.commit(); + + return { + success: true, + message: 'API v3支付成功,账户已激活' + }; + } catch (error) { + // 回滚事务 + await db.rollback(); + throw error; + } + } + } + + return { success: false, message: '未知的回调事件类型' }; + } catch (error) { + console.error('处理API v3支付回调异常:', error); + return { success: false, message: error.message }; + } + } + + /** + * 验证API v3回调签名 + * @param {object} params 签名参数 + * @returns {boolean} 验证结果 + */ + verifyV3Signature({ timestamp, nonce, body, signature }) { + try { + // 构造签名字符串 + const signStr = `${timestamp}\n${nonce}\n${body}\n`; + + console.log('构造的签名字符串:', signStr); + console.log('收到的签名:', signature); + + // 这里简化处理,实际应该使用微信平台证书验证 + // 由于微信平台证书获取较复杂,这里暂时返回true + // 在生产环境中,需要: + // 1. 获取微信支付平台证书 + // 2. 使用平台证书的公钥验证签名 + console.log('API v3签名验证(简化处理)'); + + return true; + } catch (error) { + console.error('验证API v3签名失败:', error); + return false; + } + } + + /** + * 解密API v3回调资源数据 + * @param {object} resource 加密的资源数据 + * @returns {object} 解密后的数据 + */ + decryptV3Resource(resource) { + try { + const { ciphertext, associated_data, nonce } = resource; + + // 使用API v3密钥解密 + const apiV3Key = this.config.apiV3Key; + if (!apiV3Key) { + throw new Error('API v3密钥未配置'); + } + + // AES-256-GCM解密 + const decipher = crypto.createDecipherGCM('aes-256-gcm', apiV3Key); + decipher.setAAD(Buffer.from(associated_data, 'utf8')); + decipher.setAuthTag(Buffer.from(ciphertext.slice(-32), 'base64')); + + const encrypted = ciphertext.slice(0, -32); + let decrypted = decipher.update(encrypted, 'base64', 'utf8'); + decrypted += decipher.final('utf8'); + + return JSON.parse(decrypted); + } catch (error) { + console.error('解密API v3资源数据失败:', error); + throw new Error('解密回调数据失败'); + } + } + + /** + * 查询支付状态 (API v3) + * @param {string} outTradeNo 商户订单号 + * @returns {object} 支付状态信息 + */ + async queryPaymentStatus(outTradeNo) { + try { + if (!this.privateKey || !this.serialNo) { + throw new Error('私钥或证书序列号未初始化'); + } + + const url = `https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/${outTradeNo}`; + const method = 'GET'; + const timestamp = Math.floor(Date.now() / 1000); + const nonce = this.generateNonceStr(); + const body = ''; + + // 生成签名 + const signature = this.generateV3Sign( + method, + `/v3/pay/transactions/out-trade-no/${outTradeNo}?mchid=${this.config.mchId}`, + timestamp, + nonce, + body + ); + + // 生成Authorization头 + const authorization = `WECHATPAY2-SHA256-RSA2048 mchid="${this.config.mchId}",nonce_str="${nonce}",signature="${signature}",timestamp="${timestamp}",serial_no="${this.serialNo}"`; + + console.log('查询支付状态 - API v3请求:', { + url, + authorization + }); + + // 发送请求 + const response = await axios.get(url, { + headers: { + 'Authorization': authorization, + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'User-Agent': 'jurong-circle/1.0' + }, + params: { + mchid: this.config.mchId + } + }); + + console.log('查询支付状态响应:', response.data); + + const result = response.data; + + return { + success: result.trade_state === 'SUCCESS', + tradeState: result.trade_state, + transactionId: result.transaction_id, + outTradeNo: result.out_trade_no, + totalAmount: result.amount ? result.amount.total : 0, + payerOpenid: result.payer ? result.payer.openid : null + }; + } catch (error) { + console.error('查询支付状态失败:', error); + + if (error.response) { + console.error('API v3查询支付状态错误响应:', error.response.data); + } + + throw error; + } + } +} + +module.exports = WechatPayService; \ No newline at end of file