升级商城逻辑
This commit is contained in:
3
.idea/vcs.xml
generated
3
.idea/vcs.xml
generated
@@ -1,6 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="VcsDirectoryMappings">
|
<component name="VcsDirectoryMappings">
|
||||||
<mapping directory="$PROJECT_DIR$/frontend" vcs="Git" />
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
<mapping directory="$PROJECT_DIR$/admin" vcs="Git" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
16
.vscode/launch.json
vendored
Normal file
16
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
// 使用 IntelliSense 了解相关属性。
|
||||||
|
// 悬停以查看现有属性的描述。
|
||||||
|
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
|
||||||
|
{
|
||||||
|
"type": "node-terminal",
|
||||||
|
"name": "Run Script: dev:server",
|
||||||
|
"request": "launch",
|
||||||
|
"command": "npm run dev:server",
|
||||||
|
"cwd": "${workspaceFolder}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -53,6 +53,7 @@ async function createTables() {
|
|||||||
role ENUM('user', 'admin') DEFAULT 'user',
|
role ENUM('user', 'admin') DEFAULT 'user',
|
||||||
avatar VARCHAR(255),
|
avatar VARCHAR(255),
|
||||||
points INT DEFAULT 0,
|
points INT DEFAULT 0,
|
||||||
|
rongdou INT DEFAULT 0,
|
||||||
balance DECIMAL(10,2) DEFAULT 0.00,
|
balance DECIMAL(10,2) DEFAULT 0.00,
|
||||||
real_name VARCHAR(100),
|
real_name VARCHAR(100),
|
||||||
id_card VARCHAR(18),
|
id_card VARCHAR(18),
|
||||||
@@ -111,7 +112,8 @@ async function createTables() {
|
|||||||
order_no VARCHAR(50) UNIQUE NOT NULL,
|
order_no VARCHAR(50) UNIQUE NOT NULL,
|
||||||
total_amount INT NOT NULL,
|
total_amount INT NOT NULL,
|
||||||
total_points INT NOT NULL,
|
total_points INT NOT NULL,
|
||||||
status ENUM('pending', 'paid', 'shipped', 'delivered', 'cancelled') DEFAULT 'pending',
|
total_rongdou INT NOT NULL DEFAULT 0,
|
||||||
|
status ENUM('pending', 'paid', 'shipped', 'delivered', 'cancelled', 'pre_order') DEFAULT 'pending',
|
||||||
address JSON,
|
address JSON,
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
@@ -158,12 +160,18 @@ async function createTables() {
|
|||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
order_id INT NOT NULL,
|
order_id INT NOT NULL,
|
||||||
product_id INT NOT NULL,
|
product_id INT NOT NULL,
|
||||||
|
spec_combination_id INT NULL COMMENT '规格组合ID',
|
||||||
quantity INT NOT NULL,
|
quantity INT NOT NULL,
|
||||||
price INT NOT NULL,
|
price INT NOT NULL,
|
||||||
points INT NOT NULL,
|
points INT NOT NULL,
|
||||||
|
points_price INT NOT NULL DEFAULT 0,
|
||||||
|
rongdou INT DEFAULT 0 COMMENT '融豆价格',
|
||||||
|
rongdou_price INT NOT NULL DEFAULT 0,
|
||||||
|
spec_info JSON COMMENT '规格信息快照',
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
FOREIGN KEY (order_id) REFERENCES orders(id),
|
FOREIGN KEY (order_id) REFERENCES orders(id),
|
||||||
FOREIGN KEY (product_id) REFERENCES products(id)
|
FOREIGN KEY (product_id) REFERENCES products(id),
|
||||||
|
FOREIGN KEY (spec_combination_id) REFERENCES product_spec_combinations(id) ON DELETE SET NULL
|
||||||
)
|
)
|
||||||
`);
|
`);
|
||||||
|
|
||||||
@@ -182,6 +190,21 @@ async function createTables() {
|
|||||||
)
|
)
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
// 融豆记录表
|
||||||
|
await getDB().execute(`
|
||||||
|
CREATE TABLE IF NOT EXISTS rongdou_history (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
user_id INT NOT NULL,
|
||||||
|
type ENUM('earn', 'spend') NOT NULL,
|
||||||
|
amount INT NOT NULL,
|
||||||
|
description VARCHAR(255),
|
||||||
|
order_id INT,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||||
|
FOREIGN KEY (order_id) REFERENCES orders(id)
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
// 管理员操作日志表
|
// 管理员操作日志表
|
||||||
await getDB().execute(`
|
await getDB().execute(`
|
||||||
CREATE TABLE IF NOT EXISTS admin_operation_logs (
|
CREATE TABLE IF NOT EXISTS admin_operation_logs (
|
||||||
@@ -213,22 +236,75 @@ async function createTables() {
|
|||||||
)
|
)
|
||||||
`);
|
`);
|
||||||
|
|
||||||
// 商品规格表
|
// 规格名称表(规格维度)
|
||||||
await getDB().execute(`
|
await getDB().execute(`
|
||||||
CREATE TABLE IF NOT EXISTS product_specifications (
|
CREATE TABLE IF NOT EXISTS spec_names (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
product_id INT NOT NULL,
|
name VARCHAR(100) NOT NULL COMMENT '规格名称,如:颜色、尺寸、材质',
|
||||||
spec_name VARCHAR(100) NOT NULL,
|
display_name VARCHAR(100) NOT NULL COMMENT '显示名称',
|
||||||
spec_value VARCHAR(100) NOT NULL,
|
sort_order INT DEFAULT 0 COMMENT '排序',
|
||||||
price_adjustment INT DEFAULT 0,
|
|
||||||
points_adjustment INT DEFAULT 0,
|
|
||||||
rongdou_adjustment INT DEFAULT 0,
|
|
||||||
stock INT DEFAULT 0,
|
|
||||||
sku_code VARCHAR(100),
|
|
||||||
status ENUM('active', 'inactive') DEFAULT 'active',
|
status ENUM('active', 'inactive') DEFAULT 'active',
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE
|
UNIQUE KEY unique_name (name)
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
// 规格值表
|
||||||
|
await getDB().execute(`
|
||||||
|
CREATE TABLE IF NOT EXISTS spec_values (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
spec_name_id INT NOT NULL COMMENT '规格名称ID',
|
||||||
|
value VARCHAR(100) NOT NULL COMMENT '规格值,如:红色、XL、棉质',
|
||||||
|
display_value VARCHAR(100) NOT NULL COMMENT '显示值',
|
||||||
|
color_code VARCHAR(20) COMMENT '颜色代码(仅颜色规格使用)',
|
||||||
|
image_url VARCHAR(500) COMMENT '规格图片',
|
||||||
|
sort_order INT DEFAULT 0 COMMENT '排序',
|
||||||
|
status ENUM('active', 'inactive') DEFAULT 'active',
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (spec_name_id) REFERENCES spec_names(id) ON DELETE CASCADE,
|
||||||
|
UNIQUE KEY unique_spec_value (spec_name_id, value)
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
// 商品规格组合表(笛卡尔积结果)
|
||||||
|
await getDB().execute(`
|
||||||
|
CREATE TABLE IF NOT EXISTS product_spec_combinations (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
product_id INT NOT NULL COMMENT '商品ID',
|
||||||
|
combination_key VARCHAR(255) NOT NULL COMMENT '组合键,如:color_1-size_2-material_3',
|
||||||
|
spec_values JSON NOT NULL COMMENT '规格值组合,存储spec_value_id数组',
|
||||||
|
price_adjustment INT DEFAULT 0 COMMENT '价格调整',
|
||||||
|
points_adjustment INT DEFAULT 0 COMMENT '积分调整',
|
||||||
|
rongdou_adjustment INT DEFAULT 0 COMMENT '融豆调整',
|
||||||
|
stock INT DEFAULT 0 COMMENT '库存',
|
||||||
|
sku_code VARCHAR(100) COMMENT 'SKU编码',
|
||||||
|
barcode VARCHAR(100) COMMENT '条形码',
|
||||||
|
weight DECIMAL(8,3) COMMENT '重量(kg)',
|
||||||
|
volume DECIMAL(10,3) COMMENT '体积(cm³)',
|
||||||
|
status ENUM('active', 'inactive') DEFAULT 'active',
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE,
|
||||||
|
UNIQUE KEY unique_product_combination (product_id, combination_key),
|
||||||
|
INDEX idx_product_status (product_id, status),
|
||||||
|
INDEX idx_sku_code (sku_code)
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
// 商品规格关联表(定义商品使用哪些规格维度)
|
||||||
|
await getDB().execute(`
|
||||||
|
CREATE TABLE IF NOT EXISTS product_spec_names (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
product_id INT NOT NULL COMMENT '商品ID',
|
||||||
|
spec_name_id INT NOT NULL COMMENT '规格名称ID',
|
||||||
|
is_required BOOLEAN DEFAULT TRUE COMMENT '是否必选规格',
|
||||||
|
sort_order INT DEFAULT 0 COMMENT '排序',
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (spec_name_id) REFERENCES spec_names(id) ON DELETE CASCADE,
|
||||||
|
UNIQUE KEY unique_product_spec_name (product_id, spec_name_id)
|
||||||
)
|
)
|
||||||
`);
|
`);
|
||||||
|
|
||||||
@@ -259,6 +335,23 @@ async function createTables() {
|
|||||||
)
|
)
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
// 购物车表
|
||||||
|
await getDB().execute(`
|
||||||
|
CREATE TABLE IF NOT EXISTS cart_items (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
user_id INT NOT NULL,
|
||||||
|
product_id INT NOT NULL,
|
||||||
|
quantity INT NOT NULL DEFAULT 1,
|
||||||
|
spec_combination_id INT NULL COMMENT '规格组合ID',
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (spec_combination_id) REFERENCES product_spec_combinations(id) ON DELETE CASCADE,
|
||||||
|
UNIQUE KEY unique_user_product_spec (user_id, product_id, spec_combination_id)
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
// 用户收货地址表
|
// 用户收货地址表
|
||||||
await getDB().execute(`
|
await getDB().execute(`
|
||||||
CREATE TABLE IF NOT EXISTS user_addresses (
|
CREATE TABLE IF NOT EXISTS user_addresses (
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const dbConfig = {
|
|||||||
user: 'test_mao',
|
user: 'test_mao',
|
||||||
password: 'nK2mPbWriBp25BRd',
|
password: 'nK2mPbWriBp25BRd',
|
||||||
database: 'test_mao',
|
database: 'test_mao',
|
||||||
// charset: 'utf8mb4',
|
charset: 'utf8mb4',
|
||||||
// 连接池配置
|
// 连接池配置
|
||||||
connectionLimit: 20, // 连接池最大连接数
|
connectionLimit: 20, // 连接池最大连接数
|
||||||
queueLimit: 0, // 排队等待连接的最大数量,0表示无限制
|
queueLimit: 0, // 排队等待连接的最大数量,0表示无限制
|
||||||
|
|||||||
79
docs/README.md
Normal file
79
docs/README.md
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
# API 文档结构说明
|
||||||
|
|
||||||
|
本项目已将 Swagger API 文档从路由文件中分离出来,采用模块化的文档管理方式。
|
||||||
|
|
||||||
|
## 文件夹结构
|
||||||
|
|
||||||
|
```
|
||||||
|
docs/
|
||||||
|
├── README.md # 本说明文件
|
||||||
|
├── schemas/ # 数据模型定义
|
||||||
|
│ ├── product.js # 商品相关数据模型
|
||||||
|
│ ├── order.js # 订单相关数据模型
|
||||||
|
│ ├── user.js # 用户相关数据模型
|
||||||
|
│ └── cart.js # 购物车相关数据模型
|
||||||
|
└── apis/ # API 接口定义
|
||||||
|
├── products.js # 商品相关 API
|
||||||
|
└── orders.js # 订单相关 API
|
||||||
|
```
|
||||||
|
|
||||||
|
## 优势
|
||||||
|
|
||||||
|
1. **模块化管理**: 按功能模块分离文档,便于维护和查找
|
||||||
|
2. **代码分离**: 路由文件专注于业务逻辑,文档定义独立管理
|
||||||
|
3. **复用性**: Schema 定义可以在多个 API 中复用
|
||||||
|
4. **可维护性**: 文档修改不会影响业务代码,降低出错风险
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### 添加新的 Schema
|
||||||
|
|
||||||
|
在 `schemas/` 文件夹中创建新的 `.js` 文件,使用 `@swagger` 注释定义数据模型:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* components:
|
||||||
|
* schemas:
|
||||||
|
* YourModel:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* id:
|
||||||
|
* type: integer
|
||||||
|
* description: ID
|
||||||
|
*/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 添加新的 API 文档
|
||||||
|
|
||||||
|
在 `apis/` 文件夹中创建新的 `.js` 文件,使用 `@swagger` 注释定义 API 接口:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* tags:
|
||||||
|
* name: YourModule
|
||||||
|
* description: 模块描述
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /your-endpoint:
|
||||||
|
* get:
|
||||||
|
* summary: 接口描述
|
||||||
|
* tags: [YourModule]
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: 成功响应
|
||||||
|
*/
|
||||||
|
```
|
||||||
|
|
||||||
|
## 配置
|
||||||
|
|
||||||
|
Swagger 配置文件 `swagger.js` 已更新扫描路径:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
apis: ['./docs/schemas/*.js', './docs/apis/*.js', './routes/*.js', './admin/routes/*.js']
|
||||||
|
```
|
||||||
|
|
||||||
|
这样既保持了对现有路由文件中文档的兼容性,又支持新的模块化文档结构。
|
||||||
236
docs/apis/orders.js
Normal file
236
docs/apis/orders.js
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* tags:
|
||||||
|
* name: Orders
|
||||||
|
* description: 订单管理API
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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}:
|
||||||
|
* 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: 获取用户订单列表
|
||||||
|
* tags: [Orders]
|
||||||
|
* parameters:
|
||||||
|
* - in: query
|
||||||
|
* name: page
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* default: 1
|
||||||
|
* description: 页码
|
||||||
|
* - in: query
|
||||||
|
* name: limit
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* default: 10
|
||||||
|
* description: 每页数量
|
||||||
|
* - in: query
|
||||||
|
* name: status
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* enum: [pending, confirmed, shipped, delivered, cancelled]
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /orders/{id}:
|
||||||
|
* 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:
|
||||||
|
* order:
|
||||||
|
* $ref: '#/components/schemas/Order'
|
||||||
|
* items:
|
||||||
|
* type: array
|
||||||
|
* items:
|
||||||
|
* $ref: '#/components/schemas/OrderItem'
|
||||||
|
* 404:
|
||||||
|
* description: 订单不存在
|
||||||
|
*/
|
||||||
154
docs/apis/products.js
Normal file
154
docs/apis/products.js
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* tags:
|
||||||
|
* name: Products
|
||||||
|
* description: 商品管理API
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /products:
|
||||||
|
* get:
|
||||||
|
* summary: 获取商品列表
|
||||||
|
* tags: [Products]
|
||||||
|
* parameters:
|
||||||
|
* - in: query
|
||||||
|
* name: page
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* default: 1
|
||||||
|
* description: 页码
|
||||||
|
* - in: query
|
||||||
|
* name: limit
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* default: 10
|
||||||
|
* description: 每页数量
|
||||||
|
* - in: query
|
||||||
|
* name: search
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* description: 搜索关键词
|
||||||
|
* - in: query
|
||||||
|
* name: category
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* description: 商品分类
|
||||||
|
* - in: query
|
||||||
|
* name: status
|
||||||
|
* schema:
|
||||||
|
* type: string
|
||||||
|
* enum: [active, inactive]
|
||||||
|
* description: 商品状态
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: 成功获取商品列表
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* success:
|
||||||
|
* type: boolean
|
||||||
|
* data:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* products:
|
||||||
|
* type: array
|
||||||
|
* items:
|
||||||
|
* $ref: '#/components/schemas/Product'
|
||||||
|
* pagination:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* page:
|
||||||
|
* type: integer
|
||||||
|
* limit:
|
||||||
|
* type: integer
|
||||||
|
* total:
|
||||||
|
* type: integer
|
||||||
|
* pages:
|
||||||
|
* type: integer
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /products/categories:
|
||||||
|
* get:
|
||||||
|
* summary: 获取商品分类列表
|
||||||
|
* tags: [Products]
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: 成功获取分类列表
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* success:
|
||||||
|
* type: boolean
|
||||||
|
* data:
|
||||||
|
* type: array
|
||||||
|
* items:
|
||||||
|
* type: string
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /products/hot:
|
||||||
|
* get:
|
||||||
|
* summary: 获取热门商品
|
||||||
|
* tags: [Products]
|
||||||
|
* parameters:
|
||||||
|
* - in: query
|
||||||
|
* name: limit
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* default: 10
|
||||||
|
* description: 返回数量
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: 成功获取热门商品
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* success:
|
||||||
|
* type: boolean
|
||||||
|
* data:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* products:
|
||||||
|
* type: array
|
||||||
|
* items:
|
||||||
|
* $ref: '#/components/schemas/Product'
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /products/{id}:
|
||||||
|
* get:
|
||||||
|
* summary: 获取商品详情
|
||||||
|
* tags: [Products]
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: id
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* description: 商品ID
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: 成功获取商品详情
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* success:
|
||||||
|
* type: boolean
|
||||||
|
* data:
|
||||||
|
* $ref: '#/components/schemas/Product'
|
||||||
|
* 404:
|
||||||
|
* description: 商品不存在
|
||||||
|
*/
|
||||||
93
docs/schemas/cart.js
Normal file
93
docs/schemas/cart.js
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* components:
|
||||||
|
* schemas:
|
||||||
|
* CartItem:
|
||||||
|
* type: object
|
||||||
|
* required:
|
||||||
|
* - user_id
|
||||||
|
* - product_id
|
||||||
|
* - quantity
|
||||||
|
* properties:
|
||||||
|
* id:
|
||||||
|
* type: integer
|
||||||
|
* description: 购物车商品ID
|
||||||
|
* user_id:
|
||||||
|
* type: integer
|
||||||
|
* description: 用户ID
|
||||||
|
* product_id:
|
||||||
|
* type: integer
|
||||||
|
* description: 商品ID
|
||||||
|
* quantity:
|
||||||
|
* type: integer
|
||||||
|
* description: 商品数量
|
||||||
|
* created_at:
|
||||||
|
* type: string
|
||||||
|
* format: date-time
|
||||||
|
* description: 创建时间
|
||||||
|
* updated_at:
|
||||||
|
* type: string
|
||||||
|
* format: date-time
|
||||||
|
* description: 更新时间
|
||||||
|
*
|
||||||
|
* CartItemWithProduct:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* id:
|
||||||
|
* type: integer
|
||||||
|
* description: 购物车商品ID
|
||||||
|
* product_id:
|
||||||
|
* type: integer
|
||||||
|
* description: 商品ID
|
||||||
|
* product_name:
|
||||||
|
* type: string
|
||||||
|
* description: 商品名称
|
||||||
|
* quantity:
|
||||||
|
* type: integer
|
||||||
|
* description: 商品数量
|
||||||
|
* points_price:
|
||||||
|
* type: integer
|
||||||
|
* description: 积分价格
|
||||||
|
* rongdou_price:
|
||||||
|
* type: number
|
||||||
|
* description: 融豆价格
|
||||||
|
* image_url:
|
||||||
|
* type: string
|
||||||
|
* description: 商品图片URL
|
||||||
|
* stock:
|
||||||
|
* type: integer
|
||||||
|
* description: 库存数量
|
||||||
|
* payment_methods:
|
||||||
|
* type: array
|
||||||
|
* items:
|
||||||
|
* type: string
|
||||||
|
* description: 支付方式列表
|
||||||
|
* created_at:
|
||||||
|
* type: string
|
||||||
|
* format: date-time
|
||||||
|
* description: 创建时间
|
||||||
|
*
|
||||||
|
* AddToCartRequest:
|
||||||
|
* type: object
|
||||||
|
* required:
|
||||||
|
* - product_id
|
||||||
|
* - quantity
|
||||||
|
* properties:
|
||||||
|
* product_id:
|
||||||
|
* type: integer
|
||||||
|
* description: 商品ID
|
||||||
|
* quantity:
|
||||||
|
* type: integer
|
||||||
|
* minimum: 1
|
||||||
|
* description: 商品数量
|
||||||
|
*
|
||||||
|
* UpdateCartRequest:
|
||||||
|
* type: object
|
||||||
|
* required:
|
||||||
|
* - quantity
|
||||||
|
* properties:
|
||||||
|
* quantity:
|
||||||
|
* type: integer
|
||||||
|
* minimum: 1
|
||||||
|
* description: 商品数量
|
||||||
|
*/
|
||||||
102
docs/schemas/order.js
Normal file
102
docs/schemas/order.js
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* components:
|
||||||
|
* schemas:
|
||||||
|
* Order:
|
||||||
|
* type: object
|
||||||
|
* required:
|
||||||
|
* - user_id
|
||||||
|
* - total_amount
|
||||||
|
* - status
|
||||||
|
* properties:
|
||||||
|
* id:
|
||||||
|
* type: integer
|
||||||
|
* description: 订单ID
|
||||||
|
* order_number:
|
||||||
|
* type: string
|
||||||
|
* description: 订单号
|
||||||
|
* user_id:
|
||||||
|
* type: integer
|
||||||
|
* description: 用户ID
|
||||||
|
* total_amount:
|
||||||
|
* type: number
|
||||||
|
* description: 订单总金额
|
||||||
|
* total_points:
|
||||||
|
* type: integer
|
||||||
|
* description: 订单总积分
|
||||||
|
* total_rongdou:
|
||||||
|
* type: number
|
||||||
|
* description: 订单总融豆
|
||||||
|
* status:
|
||||||
|
* type: string
|
||||||
|
* description: 订单状态
|
||||||
|
* enum: [pending, confirmed, shipped, delivered, cancelled]
|
||||||
|
* payment_status:
|
||||||
|
* type: string
|
||||||
|
* description: 支付状态
|
||||||
|
* enum: [pending, paid, failed, refunded]
|
||||||
|
* shipping_address:
|
||||||
|
* type: string
|
||||||
|
* description: 收货地址
|
||||||
|
* created_at:
|
||||||
|
* type: string
|
||||||
|
* format: date-time
|
||||||
|
* description: 创建时间
|
||||||
|
* updated_at:
|
||||||
|
* type: string
|
||||||
|
* format: date-time
|
||||||
|
* description: 更新时间
|
||||||
|
*
|
||||||
|
* OrderItem:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* id:
|
||||||
|
* type: integer
|
||||||
|
* description: 订单商品ID
|
||||||
|
* order_id:
|
||||||
|
* type: integer
|
||||||
|
* description: 订单ID
|
||||||
|
* product_id:
|
||||||
|
* type: integer
|
||||||
|
* description: 商品ID
|
||||||
|
* quantity:
|
||||||
|
* type: integer
|
||||||
|
* description: 商品数量
|
||||||
|
* price:
|
||||||
|
* type: number
|
||||||
|
* description: 商品价格
|
||||||
|
* points_price:
|
||||||
|
* type: integer
|
||||||
|
* description: 积分价格
|
||||||
|
* rongdou_price:
|
||||||
|
* type: number
|
||||||
|
* description: 融豆价格
|
||||||
|
* created_at:
|
||||||
|
* type: string
|
||||||
|
* format: date-time
|
||||||
|
* description: 创建时间
|
||||||
|
*
|
||||||
|
* PreOrder:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* preOrderId:
|
||||||
|
* type: integer
|
||||||
|
* description: 预订单ID
|
||||||
|
* orderNumber:
|
||||||
|
* type: string
|
||||||
|
* description: 订单号
|
||||||
|
* totalAmount:
|
||||||
|
* type: number
|
||||||
|
* description: 总金额
|
||||||
|
* totalPoints:
|
||||||
|
* type: integer
|
||||||
|
* description: 所需积分总数
|
||||||
|
* totalRongdou:
|
||||||
|
* type: number
|
||||||
|
* description: 所需融豆总数
|
||||||
|
* paymentMethods:
|
||||||
|
* type: array
|
||||||
|
* items:
|
||||||
|
* type: string
|
||||||
|
* description: 去重后的支付方式列表
|
||||||
|
*/
|
||||||
53
docs/schemas/product.js
Normal file
53
docs/schemas/product.js
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* components:
|
||||||
|
* schemas:
|
||||||
|
* Product:
|
||||||
|
* type: object
|
||||||
|
* required:
|
||||||
|
* - name
|
||||||
|
* - points_price
|
||||||
|
* - stock
|
||||||
|
* properties:
|
||||||
|
* id:
|
||||||
|
* type: integer
|
||||||
|
* description: 商品ID
|
||||||
|
* name:
|
||||||
|
* type: string
|
||||||
|
* description: 商品名称
|
||||||
|
* category:
|
||||||
|
* type: string
|
||||||
|
* description: 商品分类
|
||||||
|
* points_price:
|
||||||
|
* type: integer
|
||||||
|
* description: 积分价格
|
||||||
|
* rongdou_price:
|
||||||
|
* type: number
|
||||||
|
* description: 融豆价格
|
||||||
|
* stock:
|
||||||
|
* type: integer
|
||||||
|
* description: 库存数量
|
||||||
|
* image_url:
|
||||||
|
* type: string
|
||||||
|
* description: 商品图片URL
|
||||||
|
* description:
|
||||||
|
* type: string
|
||||||
|
* description: 商品描述
|
||||||
|
* status:
|
||||||
|
* type: string
|
||||||
|
* description: 商品状态
|
||||||
|
* enum: [active, inactive]
|
||||||
|
* payment_methods:
|
||||||
|
* type: array
|
||||||
|
* items:
|
||||||
|
* type: string
|
||||||
|
* description: 支付方式列表
|
||||||
|
* created_at:
|
||||||
|
* type: string
|
||||||
|
* format: date-time
|
||||||
|
* description: 创建时间
|
||||||
|
* updated_at:
|
||||||
|
* type: string
|
||||||
|
* format: date-time
|
||||||
|
* description: 更新时间
|
||||||
|
*/
|
||||||
104
docs/schemas/user.js
Normal file
104
docs/schemas/user.js
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* components:
|
||||||
|
* schemas:
|
||||||
|
* User:
|
||||||
|
* type: object
|
||||||
|
* required:
|
||||||
|
* - username
|
||||||
|
* - email
|
||||||
|
* properties:
|
||||||
|
* id:
|
||||||
|
* type: integer
|
||||||
|
* description: 用户ID
|
||||||
|
* username:
|
||||||
|
* type: string
|
||||||
|
* description: 用户名
|
||||||
|
* email:
|
||||||
|
* type: string
|
||||||
|
* format: email
|
||||||
|
* description: 邮箱地址
|
||||||
|
* phone:
|
||||||
|
* type: string
|
||||||
|
* description: 手机号码
|
||||||
|
* points:
|
||||||
|
* type: integer
|
||||||
|
* description: 积分余额
|
||||||
|
* rongdou:
|
||||||
|
* type: number
|
||||||
|
* description: 融豆余额
|
||||||
|
* avatar:
|
||||||
|
* type: string
|
||||||
|
* description: 头像URL
|
||||||
|
* status:
|
||||||
|
* type: string
|
||||||
|
* description: 用户状态
|
||||||
|
* enum: [active, inactive, banned]
|
||||||
|
* created_at:
|
||||||
|
* type: string
|
||||||
|
* format: date-time
|
||||||
|
* description: 创建时间
|
||||||
|
* updated_at:
|
||||||
|
* type: string
|
||||||
|
* format: date-time
|
||||||
|
* description: 更新时间
|
||||||
|
*
|
||||||
|
* UserProfile:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* id:
|
||||||
|
* type: integer
|
||||||
|
* description: 用户ID
|
||||||
|
* username:
|
||||||
|
* type: string
|
||||||
|
* description: 用户名
|
||||||
|
* email:
|
||||||
|
* type: string
|
||||||
|
* description: 邮箱地址
|
||||||
|
* phone:
|
||||||
|
* type: string
|
||||||
|
* description: 手机号码
|
||||||
|
* points:
|
||||||
|
* type: integer
|
||||||
|
* description: 积分余额
|
||||||
|
* rongdou:
|
||||||
|
* type: number
|
||||||
|
* description: 融豆余额
|
||||||
|
* avatar:
|
||||||
|
* type: string
|
||||||
|
* description: 头像URL
|
||||||
|
*
|
||||||
|
* LoginRequest:
|
||||||
|
* type: object
|
||||||
|
* required:
|
||||||
|
* - username
|
||||||
|
* - password
|
||||||
|
* properties:
|
||||||
|
* username:
|
||||||
|
* type: string
|
||||||
|
* description: 用户名或邮箱
|
||||||
|
* password:
|
||||||
|
* type: string
|
||||||
|
* description: 密码
|
||||||
|
*
|
||||||
|
* RegisterRequest:
|
||||||
|
* type: object
|
||||||
|
* required:
|
||||||
|
* - username
|
||||||
|
* - email
|
||||||
|
* - password
|
||||||
|
* properties:
|
||||||
|
* username:
|
||||||
|
* type: string
|
||||||
|
* description: 用户名
|
||||||
|
* email:
|
||||||
|
* type: string
|
||||||
|
* format: email
|
||||||
|
* description: 邮箱地址
|
||||||
|
* password:
|
||||||
|
* type: string
|
||||||
|
* description: 密码
|
||||||
|
* phone:
|
||||||
|
* type: string
|
||||||
|
* description: 手机号码
|
||||||
|
*/
|
||||||
@@ -4,11 +4,11 @@
|
|||||||
"description": "Vue3 + Node.js 集成系统",
|
"description": "Vue3 + Node.js 集成系统",
|
||||||
"main": "server.js",
|
"main": "server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "concurrently \"npm run dev:frontend\" \"npm run dev:admin\" \"npm run dev:server\"",
|
"dev": "concurrently \"npm run dev:admin\" \"npm run dev:server\"",
|
||||||
"dev:frontend": "cd frontend && npm run dev",
|
"dev:frontend": "cd frontend && npm run dev",
|
||||||
"dev:admin": "cd admin && npm run dev",
|
"dev:admin": "cd admin && npm run dev",
|
||||||
"dev:server": "nodemon server.js",
|
"dev:server": "nodemon server.js",
|
||||||
"build": "npm run build:frontend && npm run build:admin",
|
"build": " npm run build:admin",
|
||||||
"build:frontend": "cd frontend && npm run build",
|
"build:frontend": "cd frontend && npm run build",
|
||||||
"build:admin": "cd admin && npm run build",
|
"build:admin": "cd admin && npm run build",
|
||||||
"start": "node server.js"
|
"start": "node server.js"
|
||||||
|
|||||||
@@ -102,10 +102,14 @@ router.get('/', auth, async (req, res) => {
|
|||||||
const userId = req.user.id;
|
const userId = req.user.id;
|
||||||
|
|
||||||
const [addresses] = await getDB().execute(
|
const [addresses] = await getDB().execute(
|
||||||
`SELECT ua.*, al.name as label_name, al.color as label_color
|
`SELECT ua.*, al.name as label_name, al.color as label_color,
|
||||||
|
p.name as province_name, c.name as city_name, d.name as district_name
|
||||||
FROM user_addresses ua
|
FROM user_addresses ua
|
||||||
LEFT JOIN address_labels al ON ua.label_id = al.id
|
LEFT JOIN address_labels al ON ua.label = al.id
|
||||||
WHERE ua.user_id = ? AND ua.deleted_at IS NULL
|
LEFT JOIN china_regions p ON ua.province = p.code
|
||||||
|
LEFT JOIN china_regions c ON ua.city = c.code
|
||||||
|
LEFT JOIN china_regions d ON ua.district = d.code
|
||||||
|
WHERE ua.user_id = ?
|
||||||
ORDER BY ua.is_default DESC, ua.created_at DESC`,
|
ORDER BY ua.is_default DESC, ua.created_at DESC`,
|
||||||
[userId]
|
[userId]
|
||||||
);
|
);
|
||||||
@@ -259,14 +263,9 @@ router.post('/', auth, async (req, res) => {
|
|||||||
recipient_name,
|
recipient_name,
|
||||||
phone,
|
phone,
|
||||||
province_code,
|
province_code,
|
||||||
province_name,
|
|
||||||
city_code,
|
city_code,
|
||||||
city_name,
|
|
||||||
district_code,
|
district_code,
|
||||||
district_name,
|
|
||||||
detailed_address,
|
detailed_address,
|
||||||
postal_code,
|
|
||||||
label_id,
|
|
||||||
is_default = false
|
is_default = false
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
@@ -278,19 +277,19 @@ router.post('/', auth, async (req, res) => {
|
|||||||
// 如果设置为默认地址,先取消其他默认地址
|
// 如果设置为默认地址,先取消其他默认地址
|
||||||
if (is_default) {
|
if (is_default) {
|
||||||
await getDB().execute(
|
await getDB().execute(
|
||||||
'UPDATE user_addresses SET is_default = false WHERE user_id = ? AND deleted_at IS NULL',
|
'UPDATE user_addresses SET is_default = false WHERE user_id = ? ',
|
||||||
[userId]
|
[userId]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [result] = await getDB().execute(
|
const [result] = await getDB().execute(
|
||||||
`INSERT INTO user_addresses (
|
`INSERT INTO user_addresses (
|
||||||
user_id, recipient_name, phone, province_code, province_name, city_code, city_name,
|
user_id, receiver_name, receiver_phone, province, city,
|
||||||
district_code, district_name, detailed_address, postal_code, label_id, is_default, created_at, updated_at
|
district, detailed_address, is_default, created_at, updated_at
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())`,
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())`,
|
||||||
[
|
[
|
||||||
userId, recipient_name, phone, province_code, province_name, city_code, city_name,
|
userId, recipient_name, phone, province_code, city_code,
|
||||||
district_code, district_name, detailed_address, postal_code, label_id, is_default
|
district_code, detailed_address, is_default
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -383,20 +382,18 @@ router.put('/:id', auth, async (req, res) => {
|
|||||||
recipient_name,
|
recipient_name,
|
||||||
phone,
|
phone,
|
||||||
province_code,
|
province_code,
|
||||||
province_name,
|
|
||||||
city_code,
|
city_code,
|
||||||
city_name,
|
|
||||||
district_code,
|
district_code,
|
||||||
district_name,
|
|
||||||
detailed_address,
|
detailed_address,
|
||||||
postal_code,
|
|
||||||
label_id,
|
|
||||||
is_default
|
is_default
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
if (!recipient_name || !phone || !province_code || !city_code || !district_code || !detailed_address) {
|
||||||
|
return res.status(400).json({ message: '收件人姓名、电话、省市区和详细地址不能为空' });
|
||||||
|
}
|
||||||
|
|
||||||
// 检查地址是否存在且属于当前用户
|
// 检查地址是否存在且属于当前用户
|
||||||
const [existing] = await getDB().execute(
|
const [existing] = await getDB().execute(
|
||||||
'SELECT id FROM user_addresses WHERE id = ? AND user_id = ? AND deleted_at IS NULL',
|
'SELECT id FROM user_addresses WHERE id = ? AND user_id = ? ',
|
||||||
[addressId, userId]
|
[addressId, userId]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -407,20 +404,19 @@ router.put('/:id', auth, async (req, res) => {
|
|||||||
// 如果设置为默认地址,先取消其他默认地址
|
// 如果设置为默认地址,先取消其他默认地址
|
||||||
if (is_default) {
|
if (is_default) {
|
||||||
await getDB().execute(
|
await getDB().execute(
|
||||||
'UPDATE user_addresses SET is_default = false WHERE user_id = ? AND id != ? AND deleted_at IS NULL',
|
'UPDATE user_addresses SET is_default = false WHERE user_id = ? AND id != ? ',
|
||||||
[userId, addressId]
|
[userId, addressId]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [result] = await getDB().execute(
|
const [result] = await getDB().execute(
|
||||||
`UPDATE user_addresses SET
|
`UPDATE user_addresses SET
|
||||||
recipient_name = ?, phone = ?, province_code = ?, province_name = ?,
|
receiver_name = ?, receiver_phone = ?, province = ?, city = ?,
|
||||||
city_code = ?, city_name = ?, district_code = ?, district_name = ?,
|
district = ?, detailed_address = ?, is_default = ?, updated_at = NOW()
|
||||||
detailed_address = ?, postal_code = ?, label_id = ?, is_default = ?, updated_at = NOW()
|
|
||||||
WHERE id = ? AND user_id = ?`,
|
WHERE id = ? AND user_id = ?`,
|
||||||
[
|
[
|
||||||
recipient_name, phone, province_code, province_name, city_code, city_name,
|
recipient_name, phone, province_code, city_code,
|
||||||
district_code, district_name, detailed_address, postal_code, label_id, is_default,
|
district_code, detailed_address, is_default,
|
||||||
addressId, userId
|
addressId, userId
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
@@ -439,7 +435,7 @@ router.put('/:id', auth, async (req, res) => {
|
|||||||
* @swagger
|
* @swagger
|
||||||
* /addresses/{id}:
|
* /addresses/{id}:
|
||||||
* delete:
|
* delete:
|
||||||
* summary: 删除收货地址(软删除)
|
* summary: 删除收货地址
|
||||||
* tags: [Addresses]
|
* tags: [Addresses]
|
||||||
* security:
|
* security:
|
||||||
* - bearerAuth: []
|
* - bearerAuth: []
|
||||||
@@ -477,7 +473,7 @@ router.delete('/:id', auth, async (req, res) => {
|
|||||||
const userId = req.user.id;
|
const userId = req.user.id;
|
||||||
|
|
||||||
const [result] = await getDB().execute(
|
const [result] = await getDB().execute(
|
||||||
'UPDATE user_addresses SET deleted_at = NOW() WHERE id = ? AND user_id = ? AND deleted_at IS NULL',
|
'DELETE FROM user_addresses WHERE id = ? AND user_id = ?',
|
||||||
[addressId, userId]
|
[addressId, userId]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
935
routes/cart.js
Normal file
935
routes/cart.js
Normal file
@@ -0,0 +1,935 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const { getDB } = require('../database');
|
||||||
|
const { auth } = require('../middleware/auth');
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* tags:
|
||||||
|
* name: Cart
|
||||||
|
* description: 购物车管理相关接口
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* components:
|
||||||
|
* schemas:
|
||||||
|
* CartItem:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* id:
|
||||||
|
* type: integer
|
||||||
|
* description: 购物车项ID
|
||||||
|
* user_id:
|
||||||
|
* type: integer
|
||||||
|
* description: 用户ID
|
||||||
|
* product_id:
|
||||||
|
* type: integer
|
||||||
|
* description: 商品ID
|
||||||
|
* quantity:
|
||||||
|
* type: integer
|
||||||
|
* description: 商品数量
|
||||||
|
* spec_combination_id:
|
||||||
|
* type: integer
|
||||||
|
* description: 商品规格组合ID
|
||||||
|
* created_at:
|
||||||
|
* type: string
|
||||||
|
* format: date-time
|
||||||
|
* description: 创建时间
|
||||||
|
* updated_at:
|
||||||
|
* type: string
|
||||||
|
* format: date-time
|
||||||
|
* description: 更新时间
|
||||||
|
* product:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* id:
|
||||||
|
* type: integer
|
||||||
|
* name:
|
||||||
|
* type: string
|
||||||
|
* price:
|
||||||
|
* type: integer
|
||||||
|
* points_price:
|
||||||
|
* type: integer
|
||||||
|
* rongdou_price:
|
||||||
|
* type: integer
|
||||||
|
* image_url:
|
||||||
|
* type: string
|
||||||
|
* stock:
|
||||||
|
* type: integer
|
||||||
|
* status:
|
||||||
|
* type: string
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /api/cart:
|
||||||
|
* get:
|
||||||
|
* summary: 获取购物车列表
|
||||||
|
* tags: [Cart]
|
||||||
|
* security:
|
||||||
|
* - bearerAuth: []
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: 获取购物车成功
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* success:
|
||||||
|
* type: boolean
|
||||||
|
* data:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* items:
|
||||||
|
* type: array
|
||||||
|
* items:
|
||||||
|
* $ref: '#/components/schemas/CartItem'
|
||||||
|
* total_count:
|
||||||
|
* type: integer
|
||||||
|
* description: 购物车商品总数量
|
||||||
|
* total_amount:
|
||||||
|
* type: integer
|
||||||
|
* description: 购物车总金额
|
||||||
|
* total_points:
|
||||||
|
* type: integer
|
||||||
|
* description: 购物车总积分
|
||||||
|
* total_rongdou:
|
||||||
|
* type: integer
|
||||||
|
* description: 购物车总融豆
|
||||||
|
* 401:
|
||||||
|
* description: 未授权
|
||||||
|
* 500:
|
||||||
|
* description: 服务器错误
|
||||||
|
*/
|
||||||
|
router.get('/', auth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const userId = req.user.id;
|
||||||
|
|
||||||
|
// 获取购物车商品列表
|
||||||
|
const query = `
|
||||||
|
SELECT
|
||||||
|
c.id, c.user_id, c.product_id, c.quantity, c.specification_id,
|
||||||
|
c.created_at, c.updated_at,
|
||||||
|
p.name, p.price, p.points_price, p.rongdou_price, p.image_url,
|
||||||
|
p.stock, p.status, p.shop_name, p.shop_avatar,
|
||||||
|
psc.combination_key, psc.price_adjustment,
|
||||||
|
psc.points_adjustment, psc.rongdou_adjustment, psc.stock as spec_stock,
|
||||||
|
GROUP_CONCAT(CONCAT(sn.display_name, ':', sv.display_value) ORDER BY sn.sort_order SEPARATOR ' | ') as spec_display
|
||||||
|
FROM cart_items c
|
||||||
|
LEFT JOIN products p ON c.product_id = p.id
|
||||||
|
LEFT JOIN product_spec_combinations psc ON c.specification_id = psc.id
|
||||||
|
LEFT JOIN JSON_TABLE(psc.spec_values, '$[*]' COLUMNS (spec_value_id INT PATH '$')) jt ON psc.id IS NOT NULL
|
||||||
|
LEFT JOIN spec_values sv ON jt.spec_value_id = sv.id
|
||||||
|
LEFT JOIN spec_names sn ON sv.spec_name_id = sn.id
|
||||||
|
WHERE c.user_id = ? AND p.status = 'active'
|
||||||
|
GROUP BY c.id
|
||||||
|
ORDER BY c.created_at DESC
|
||||||
|
`;
|
||||||
|
|
||||||
|
const [cartItems] = await getDB().execute(query, [userId]);
|
||||||
|
|
||||||
|
// 计算总计信息
|
||||||
|
let totalCount = 0;
|
||||||
|
let totalAmount = 0;
|
||||||
|
let totalPoints = 0;
|
||||||
|
let totalRongdou = 0;
|
||||||
|
|
||||||
|
const items = cartItems.map(item => {
|
||||||
|
const finalPrice = item.price + (item.price_adjustment || 0);
|
||||||
|
const finalPointsPrice = item.points_price + (item.points_adjustment || 0);
|
||||||
|
const finalRongdouPrice = item.rongdou_price + (item.rongdou_adjustment || 0);
|
||||||
|
|
||||||
|
totalCount += item.quantity;
|
||||||
|
totalAmount += finalPrice * item.quantity;
|
||||||
|
totalPoints += finalPointsPrice * item.quantity;
|
||||||
|
totalRongdou += finalRongdouPrice * item.quantity;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
user_id: item.user_id,
|
||||||
|
product_id: item.product_id,
|
||||||
|
quantity: item.quantity,
|
||||||
|
spec_combination_id: item.spec_combination_id,
|
||||||
|
created_at: item.created_at,
|
||||||
|
updated_at: item.updated_at,
|
||||||
|
product: {
|
||||||
|
id: item.product_id,
|
||||||
|
name: item.name,
|
||||||
|
price: finalPrice,
|
||||||
|
points_price: finalPointsPrice,
|
||||||
|
rongdou_price: finalRongdouPrice,
|
||||||
|
image_url: item.image_url,
|
||||||
|
stock: item.spec_combination_id ? item.spec_stock : item.stock,
|
||||||
|
status: item.status,
|
||||||
|
shop_name: item.shop_name,
|
||||||
|
shop_avatar: item.shop_avatar
|
||||||
|
},
|
||||||
|
specification: item.spec_combination_id ? {
|
||||||
|
id: item.spec_combination_id,
|
||||||
|
combination_key: item.combination_key,
|
||||||
|
spec_display: item.spec_display,
|
||||||
|
price_adjustment: item.price_adjustment,
|
||||||
|
points_adjustment: item.points_adjustment,
|
||||||
|
rongdou_adjustment: item.rongdou_adjustment
|
||||||
|
} : null
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
items,
|
||||||
|
total_count: totalCount,
|
||||||
|
total_amount: totalAmount,
|
||||||
|
total_points: totalPoints,
|
||||||
|
total_rongdou: totalRongdou
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取购物车失败:', error);
|
||||||
|
res.status(500).json({ success: false, message: '获取购物车失败' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /api/cart:
|
||||||
|
* post:
|
||||||
|
* summary: 添加商品到购物车
|
||||||
|
* tags: [Cart]
|
||||||
|
* security:
|
||||||
|
* - bearerAuth: []
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* product_id:
|
||||||
|
* type: integer
|
||||||
|
* description: 商品ID
|
||||||
|
* quantity:
|
||||||
|
* type: integer
|
||||||
|
* description: 商品数量
|
||||||
|
* minimum: 1
|
||||||
|
* spec_combination_id:
|
||||||
|
* type: integer
|
||||||
|
* description: 商品规格组合ID(可选)
|
||||||
|
* required:
|
||||||
|
* - product_id
|
||||||
|
* - quantity
|
||||||
|
* responses:
|
||||||
|
* 201:
|
||||||
|
* description: 添加到购物车成功
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* success:
|
||||||
|
* type: boolean
|
||||||
|
* message:
|
||||||
|
* type: string
|
||||||
|
* data:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* cart_item_id:
|
||||||
|
* type: integer
|
||||||
|
* 400:
|
||||||
|
* description: 参数错误或库存不足
|
||||||
|
* 401:
|
||||||
|
* description: 未授权
|
||||||
|
* 404:
|
||||||
|
* description: 商品不存在或已下架
|
||||||
|
* 500:
|
||||||
|
* description: 服务器错误
|
||||||
|
*/
|
||||||
|
router.post('/add', auth, async (req, res) => {
|
||||||
|
const db = getDB();
|
||||||
|
await db.query('START TRANSACTION');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { productId, quantity, specificationId } = req.body;
|
||||||
|
const userId = req.user.id;
|
||||||
|
|
||||||
|
|
||||||
|
// 验证必填字段
|
||||||
|
if (!productId || !quantity || quantity < 1) {
|
||||||
|
await db.query('ROLLBACK');
|
||||||
|
return res.status(400).json({ success: false, message: '请填写正确的商品信息和数量' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查商品是否存在且有效
|
||||||
|
const [products] = await db.execute(
|
||||||
|
'SELECT id, name, stock, status FROM products WHERE id = ?',
|
||||||
|
[productId]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (products.length === 0 || products[0].status !== 'active') {
|
||||||
|
await db.query('ROLLBACK');
|
||||||
|
return res.status(404).json({ success: false, message: '商品不存在或已下架' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const product = products[0];
|
||||||
|
let availableStock = product.stock;
|
||||||
|
|
||||||
|
// 如果指定了规格组合,检查规格组合库存
|
||||||
|
if (specificationId) {
|
||||||
|
const [specs] = await db.execute(
|
||||||
|
'SELECT id, stock, status FROM product_spec_combinations WHERE id = ? AND product_id = ?',
|
||||||
|
[specificationId, productId]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (specs.length === 0 || specs[0].status !== 'active') {
|
||||||
|
await db.query('ROLLBACK');
|
||||||
|
return res.status(404).json({ success: false, message: '商品规格组合不存在或已下架' });
|
||||||
|
}
|
||||||
|
|
||||||
|
availableStock = specs[0].stock;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查购物车中是否已存在相同商品和规格组合
|
||||||
|
const [existingItems] = await db.execute(
|
||||||
|
'SELECT id, quantity FROM cart_items WHERE user_id = ? AND product_id = ? AND (specification_id = ? OR (specification_id IS NULL AND ? IS NULL))',
|
||||||
|
[userId, productId, specificationId, specificationId]
|
||||||
|
);
|
||||||
|
|
||||||
|
let finalQuantity = quantity;
|
||||||
|
if (existingItems.length > 0) {
|
||||||
|
finalQuantity += existingItems[0].quantity;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查库存是否足够
|
||||||
|
if (availableStock < finalQuantity) {
|
||||||
|
await db.query('ROLLBACK');
|
||||||
|
return res.status(400).json({ success: false, message: '库存不足' });
|
||||||
|
}
|
||||||
|
|
||||||
|
let cartItemId;
|
||||||
|
|
||||||
|
if (existingItems.length > 0) {
|
||||||
|
// 更新现有购物车项的数量
|
||||||
|
await db.execute(
|
||||||
|
'UPDATE cart_items SET quantity = ?, updated_at = NOW() WHERE id = ?',
|
||||||
|
[finalQuantity, existingItems[0].id]
|
||||||
|
);
|
||||||
|
cartItemId = existingItems[0].id;
|
||||||
|
} else {
|
||||||
|
// 添加新的购物车项
|
||||||
|
const [result] = await db.execute(
|
||||||
|
'INSERT INTO cart_items (user_id, product_id, quantity, specification_id, created_at, updated_at) VALUES (?, ?, ?, ?, NOW(), NOW())',
|
||||||
|
[userId, productId, quantity, specificationId]
|
||||||
|
);
|
||||||
|
cartItemId = result.insertId;
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.query('COMMIT');
|
||||||
|
|
||||||
|
res.status(201).json({
|
||||||
|
success: true,
|
||||||
|
message: '添加到购物车成功',
|
||||||
|
data: { cart_item_id: cartItemId }
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
await db.query('ROLLBACK');
|
||||||
|
console.error('添加到购物车失败:', error);
|
||||||
|
res.status(500).json({ success: false, message: '添加到购物车失败' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /api/cart/{id}:
|
||||||
|
* put:
|
||||||
|
* summary: 更新购物车商品数量
|
||||||
|
* tags: [Cart]
|
||||||
|
* security:
|
||||||
|
* - bearerAuth: []
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: id
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* description: 购物车项ID
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* quantity:
|
||||||
|
* type: integer
|
||||||
|
* description: 新的商品数量
|
||||||
|
* minimum: 1
|
||||||
|
* required:
|
||||||
|
* - quantity
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: 更新购物车成功
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* success:
|
||||||
|
* type: boolean
|
||||||
|
* message:
|
||||||
|
* type: string
|
||||||
|
* 400:
|
||||||
|
* description: 参数错误或库存不足
|
||||||
|
* 401:
|
||||||
|
* description: 未授权
|
||||||
|
* 404:
|
||||||
|
* description: 购物车项不存在
|
||||||
|
* 500:
|
||||||
|
* description: 服务器错误
|
||||||
|
*/
|
||||||
|
router.put('/:id', auth, async (req, res) => {
|
||||||
|
const db = getDB();
|
||||||
|
await db.query('START TRANSACTION');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const cartItemId = req.params.id;
|
||||||
|
const { quantity } = req.body;
|
||||||
|
const userId = req.user.id;
|
||||||
|
|
||||||
|
// 验证数量
|
||||||
|
if (!quantity || quantity < 1) {
|
||||||
|
await db.query('ROLLBACK');
|
||||||
|
return res.status(400).json({ success: false, message: '商品数量必须大于0' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查购物车项是否存在且属于当前用户
|
||||||
|
const [cartItems] = await db.execute(
|
||||||
|
'SELECT id, product_id, specification_id FROM cart_items WHERE id = ? AND user_id = ?',
|
||||||
|
[cartItemId, userId]
|
||||||
|
);
|
||||||
|
console.log(cartItems,'cartItems');
|
||||||
|
|
||||||
|
|
||||||
|
if (cartItems.length === 0) {
|
||||||
|
await db.query('ROLLBACK');
|
||||||
|
return res.status(404).json({ success: false, message: '购物车项不存在' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const cartItem = cartItems[0];
|
||||||
|
|
||||||
|
// 检查商品库存
|
||||||
|
const [products] = await db.execute(
|
||||||
|
'SELECT stock, status FROM products WHERE id = ?',
|
||||||
|
[cartItem.product_id]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (products.length === 0 || products[0].status !== 'active') {
|
||||||
|
await db.query('ROLLBACK');
|
||||||
|
return res.status(404).json({ success: false, message: '商品不存在或已下架' });
|
||||||
|
}
|
||||||
|
|
||||||
|
let availableStock = products[0].stock;
|
||||||
|
|
||||||
|
// 如果有规格,检查规格库存
|
||||||
|
if (cartItem.specification_id) {
|
||||||
|
const [specs] = await db.execute(
|
||||||
|
'SELECT stock, status FROM product_spec_combinations WHERE id = ?',
|
||||||
|
[cartItem.specification_id]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (specs.length === 0 || specs[0].status !== 'active') {
|
||||||
|
await db.query('ROLLBACK');
|
||||||
|
return res.status(404).json({ success: false, message: '商品规格不存在或已下架' });
|
||||||
|
}
|
||||||
|
|
||||||
|
availableStock = specs[0].stock;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查库存是否足够
|
||||||
|
if (availableStock < quantity) {
|
||||||
|
await db.query('ROLLBACK');
|
||||||
|
return res.status(400).json({ success: false, message: '库存不足' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新购物车项数量
|
||||||
|
await db.execute(
|
||||||
|
'UPDATE cart_items SET quantity = ?, updated_at = NOW() WHERE id = ?',
|
||||||
|
[quantity, cartItemId]
|
||||||
|
);
|
||||||
|
|
||||||
|
await db.query('COMMIT');
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: '更新购物车成功'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
await db.query('ROLLBACK');
|
||||||
|
console.error('更新购物车失败:', error);
|
||||||
|
res.status(500).json({ success: false, message: '更新购物车失败' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /api/cart/{id}:
|
||||||
|
* delete:
|
||||||
|
* summary: 删除购物车商品
|
||||||
|
* tags: [Cart]
|
||||||
|
* security:
|
||||||
|
* - bearerAuth: []
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: id
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* description: 购物车项ID
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: 删除购物车商品成功
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* success:
|
||||||
|
* type: boolean
|
||||||
|
* message:
|
||||||
|
* type: string
|
||||||
|
* 401:
|
||||||
|
* description: 未授权
|
||||||
|
* 404:
|
||||||
|
* description: 购物车项不存在
|
||||||
|
* 500:
|
||||||
|
* description: 服务器错误
|
||||||
|
*/
|
||||||
|
router.delete('/:id', auth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const cartItemId = req.params.id;
|
||||||
|
const userId = req.user.id;
|
||||||
|
|
||||||
|
// 检查购物车项是否存在且属于当前用户
|
||||||
|
const [cartItems] = await getDB().execute(
|
||||||
|
'SELECT id FROM cart_items WHERE id = ? AND user_id = ?',
|
||||||
|
[cartItemId, userId]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (cartItems.length === 0) {
|
||||||
|
return res.status(404).json({ success: false, message: '购物车项不存在' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除购物车项
|
||||||
|
await getDB().execute(
|
||||||
|
'DELETE FROM cart_items WHERE id = ?',
|
||||||
|
[cartItemId]
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: '删除购物车商品成功'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除购物车商品失败:', error);
|
||||||
|
res.status(500).json({ success: false, message: '删除购物车商品失败' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /api/cart/batch:
|
||||||
|
* delete:
|
||||||
|
* summary: 批量删除购物车商品
|
||||||
|
* tags: [Cart]
|
||||||
|
* security:
|
||||||
|
* - bearerAuth: []
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* cart_item_ids:
|
||||||
|
* type: array
|
||||||
|
* items:
|
||||||
|
* type: integer
|
||||||
|
* description: 购物车项ID数组
|
||||||
|
* required:
|
||||||
|
* - cart_item_ids
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: 批量删除购物车商品成功
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* success:
|
||||||
|
* type: boolean
|
||||||
|
* message:
|
||||||
|
* type: string
|
||||||
|
* data:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* deleted_count:
|
||||||
|
* type: integer
|
||||||
|
* description: 删除的商品数量
|
||||||
|
* 400:
|
||||||
|
* description: 参数错误
|
||||||
|
* 401:
|
||||||
|
* description: 未授权
|
||||||
|
* 500:
|
||||||
|
* description: 服务器错误
|
||||||
|
*/
|
||||||
|
router.delete('/batch', auth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { cart_item_ids } = req.body;
|
||||||
|
const userId = req.user.id;
|
||||||
|
|
||||||
|
// 验证参数
|
||||||
|
if (!cart_item_ids || !Array.isArray(cart_item_ids) || cart_item_ids.length === 0) {
|
||||||
|
return res.status(400).json({ success: false, message: '请选择要删除的商品' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建删除条件
|
||||||
|
const placeholders = cart_item_ids.map(() => '?').join(',');
|
||||||
|
const query = `DELETE FROM cart_items WHERE id IN (${placeholders}) AND user_id = ?`;
|
||||||
|
const params = [...cart_item_ids, userId];
|
||||||
|
|
||||||
|
const [result] = await getDB().execute(query, params);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: '批量删除购物车商品成功',
|
||||||
|
data: {
|
||||||
|
deleted_count: result.affectedRows
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('批量删除购物车商品失败:', error);
|
||||||
|
res.status(500).json({ success: false, message: '批量删除购物车商品失败' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /api/cart/clear:
|
||||||
|
* delete:
|
||||||
|
* summary: 清空购物车
|
||||||
|
* tags: [Cart]
|
||||||
|
* security:
|
||||||
|
* - bearerAuth: []
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: 清空购物车成功
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* success:
|
||||||
|
* type: boolean
|
||||||
|
* message:
|
||||||
|
* type: string
|
||||||
|
* 401:
|
||||||
|
* description: 未授权
|
||||||
|
* 500:
|
||||||
|
* description: 服务器错误
|
||||||
|
*/
|
||||||
|
router.delete('/clear', auth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const userId = req.user.id;
|
||||||
|
|
||||||
|
// 清空用户购物车
|
||||||
|
await getDB().execute(
|
||||||
|
'DELETE FROM cart_items WHERE user_id = ?',
|
||||||
|
[userId]
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: '清空购物车成功'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('清空购物车失败:', error);
|
||||||
|
res.status(500).json({ success: false, message: '清空购物车失败' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /api/cart/count:
|
||||||
|
* get:
|
||||||
|
* summary: 获取购物车商品数量
|
||||||
|
* tags: [Cart]
|
||||||
|
* security:
|
||||||
|
* - bearerAuth: []
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: 获取购物车商品数量成功
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* success:
|
||||||
|
* type: boolean
|
||||||
|
* data:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* count:
|
||||||
|
* type: integer
|
||||||
|
* description: 购物车商品总数量
|
||||||
|
* 401:
|
||||||
|
* description: 未授权
|
||||||
|
* 500:
|
||||||
|
* description: 服务器错误
|
||||||
|
*/
|
||||||
|
router.get('/count', auth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const userId = req.user.id;
|
||||||
|
|
||||||
|
// 获取购物车商品总数量
|
||||||
|
const [result] = await getDB().execute(
|
||||||
|
'SELECT SUM(quantity) as count FROM cart_items WHERE user_id = ?',
|
||||||
|
[userId]
|
||||||
|
);
|
||||||
|
|
||||||
|
const count = result[0].count || 0;
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: { count }
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取购物车商品数量失败:', error);
|
||||||
|
res.status(500).json({ success: false, message: '获取购物车商品数量失败' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /api/cart/checkout:
|
||||||
|
* post:
|
||||||
|
* summary: 购物车结账
|
||||||
|
* tags: [Cart]
|
||||||
|
* security:
|
||||||
|
* - bearerAuth: []
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* cart_item_ids:
|
||||||
|
* type: array
|
||||||
|
* items:
|
||||||
|
* type: integer
|
||||||
|
* description: 要结账的购物车项ID数组
|
||||||
|
* shipping_address:
|
||||||
|
* type: string
|
||||||
|
* description: 收货地址
|
||||||
|
* required:
|
||||||
|
* - cart_item_ids
|
||||||
|
* - shipping_address
|
||||||
|
* responses:
|
||||||
|
* 201:
|
||||||
|
* description: 结账成功
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* success:
|
||||||
|
* type: boolean
|
||||||
|
* message:
|
||||||
|
* type: string
|
||||||
|
* data:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* order_id:
|
||||||
|
* type: integer
|
||||||
|
* order_no:
|
||||||
|
* type: string
|
||||||
|
* total_amount:
|
||||||
|
* type: integer
|
||||||
|
* total_points:
|
||||||
|
* type: integer
|
||||||
|
* total_rongdou:
|
||||||
|
* type: integer
|
||||||
|
* 400:
|
||||||
|
* description: 参数错误或库存不足
|
||||||
|
* 401:
|
||||||
|
* description: 未授权
|
||||||
|
* 500:
|
||||||
|
* description: 服务器错误
|
||||||
|
*/
|
||||||
|
router.post('/checkout', auth, async (req, res) => {
|
||||||
|
const db = getDB();
|
||||||
|
await db.query('START TRANSACTION');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { cart_item_ids, shipping_address } = req.body;
|
||||||
|
const userId = req.user.id;
|
||||||
|
|
||||||
|
// 验证参数
|
||||||
|
if (!cart_item_ids || !Array.isArray(cart_item_ids) || cart_item_ids.length === 0) {
|
||||||
|
await db.query('ROLLBACK');
|
||||||
|
return res.status(400).json({ success: false, message: '请选择要结账的商品' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!shipping_address) {
|
||||||
|
await db.query('ROLLBACK');
|
||||||
|
return res.status(400).json({ success: false, message: '请填写收货地址' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取购物车商品信息
|
||||||
|
const placeholders = cart_item_ids.map(() => '?').join(',');
|
||||||
|
const cartQuery = `
|
||||||
|
SELECT
|
||||||
|
c.id, c.product_id, c.quantity, c.spec_combination_id,
|
||||||
|
p.name, p.price, p.points_price, p.rongdou_price, p.stock, p.status,
|
||||||
|
psc.price_adjustment, psc.points_adjustment, psc.rongdou_adjustment, psc.stock as spec_stock
|
||||||
|
FROM cart_items c
|
||||||
|
LEFT JOIN products p ON c.product_id = p.id
|
||||||
|
LEFT JOIN product_spec_combinations psc ON c.spec_combination_id = psc.id
|
||||||
|
WHERE c.id IN (${placeholders}) AND c.user_id = ?
|
||||||
|
`;
|
||||||
|
|
||||||
|
const [cartItems] = await db.execute(cartQuery, [...cart_item_ids, userId]);
|
||||||
|
|
||||||
|
if (cartItems.length === 0) {
|
||||||
|
await db.query('ROLLBACK');
|
||||||
|
return res.status(400).json({ success: false, message: '购物车商品不存在' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证商品状态和库存
|
||||||
|
let totalAmount = 0;
|
||||||
|
let totalPoints = 0;
|
||||||
|
let totalRongdou = 0;
|
||||||
|
|
||||||
|
for (const item of cartItems) {
|
||||||
|
if (item.status !== 'active') {
|
||||||
|
await db.query('ROLLBACK');
|
||||||
|
return res.status(400).json({ success: false, message: `商品 ${item.name} 已下架` });
|
||||||
|
}
|
||||||
|
|
||||||
|
const availableStock = item.spec_combination_id ? item.spec_stock : item.stock;
|
||||||
|
if (availableStock < item.quantity) {
|
||||||
|
await db.query('ROLLBACK');
|
||||||
|
return res.status(400).json({ success: false, message: `商品 ${item.name} 库存不足` });
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalPrice = item.price + (item.price_adjustment || 0);
|
||||||
|
const finalPointsPrice = item.points_price + (item.points_adjustment || 0);
|
||||||
|
const finalRongdouPrice = item.rongdou_price + (item.rongdou_adjustment || 0);
|
||||||
|
|
||||||
|
totalAmount += finalPrice * item.quantity;
|
||||||
|
totalPoints += finalPointsPrice * item.quantity;
|
||||||
|
totalRongdou += finalRongdouPrice * item.quantity;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查用户积分和融豆是否足够
|
||||||
|
const [users] = await db.execute(
|
||||||
|
'SELECT points, rongdou FROM users WHERE id = ?',
|
||||||
|
[userId]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (users.length === 0) {
|
||||||
|
await db.query('ROLLBACK');
|
||||||
|
return res.status(404).json({ success: false, message: '用户不存在' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = users[0];
|
||||||
|
|
||||||
|
if (user.points < totalPoints) {
|
||||||
|
await db.query('ROLLBACK');
|
||||||
|
return res.status(400).json({ success: false, message: '积分不足' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.rongdou < totalRongdou) {
|
||||||
|
await db.query('ROLLBACK');
|
||||||
|
return res.status(400).json({ success: false, message: '融豆不足' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成订单号
|
||||||
|
const orderNo = 'ORD' + Date.now() + Math.random().toString(36).substr(2, 5).toUpperCase();
|
||||||
|
|
||||||
|
// 创建订单
|
||||||
|
const [orderResult] = await db.execute(
|
||||||
|
`INSERT INTO orders (order_no, user_id, total_amount, total_points, total_rongdou,
|
||||||
|
status, shipping_address, created_at, updated_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?, 'pending', ?, NOW(), NOW())`,
|
||||||
|
[orderNo, userId, totalAmount, totalPoints, totalRongdou, shipping_address]
|
||||||
|
);
|
||||||
|
|
||||||
|
const orderId = orderResult.insertId;
|
||||||
|
|
||||||
|
// 创建订单项
|
||||||
|
for (const item of cartItems) {
|
||||||
|
const finalPrice = item.price + (item.price_adjustment || 0);
|
||||||
|
const finalPointsPrice = item.points_price + (item.points_adjustment || 0);
|
||||||
|
const finalRongdouPrice = item.rongdou_price + (item.rongdou_adjustment || 0);
|
||||||
|
|
||||||
|
await db.execute(
|
||||||
|
`INSERT INTO order_items (order_id, product_id, spec_combination_id, quantity,
|
||||||
|
price, points_price, rongdou_price, created_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, NOW())`,
|
||||||
|
[orderId, item.product_id, item.spec_combination_id, item.quantity,
|
||||||
|
finalPrice, finalPointsPrice, finalRongdouPrice]
|
||||||
|
);
|
||||||
|
|
||||||
|
// 更新库存
|
||||||
|
if (item.spec_combination_id) {
|
||||||
|
await db.execute(
|
||||||
|
'UPDATE product_spec_combinations SET stock = stock - ? WHERE id = ?',
|
||||||
|
[item.quantity, item.spec_combination_id]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await db.execute(
|
||||||
|
'UPDATE products SET stock = stock - ? WHERE id = ?',
|
||||||
|
[item.quantity, item.product_id]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 扣除用户积分和融豆
|
||||||
|
await db.execute(
|
||||||
|
'UPDATE users SET points = points - ?, rongdou = rongdou - ? WHERE id = ?',
|
||||||
|
[totalPoints, totalRongdou, userId]
|
||||||
|
);
|
||||||
|
|
||||||
|
// 删除已结账的购物车项
|
||||||
|
const deletePlaceholders = cart_item_ids.map(() => '?').join(',');
|
||||||
|
await db.execute(
|
||||||
|
`DELETE FROM cart_items WHERE id IN (${deletePlaceholders}) AND user_id = ?`,
|
||||||
|
[...cart_item_ids, userId]
|
||||||
|
);
|
||||||
|
|
||||||
|
await db.query('COMMIT');
|
||||||
|
|
||||||
|
res.status(201).json({
|
||||||
|
success: true,
|
||||||
|
message: '结账成功',
|
||||||
|
data: {
|
||||||
|
order_id: orderId,
|
||||||
|
order_no: orderNo,
|
||||||
|
total_amount: totalAmount,
|
||||||
|
total_points: totalPoints,
|
||||||
|
total_rongdou: totalRongdou
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
await db.query('ROLLBACK');
|
||||||
|
console.error('购物车结账失败:', error);
|
||||||
|
res.status(500).json({ success: false, message: '结账失败' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
1203
routes/orders.js
1203
routes/orders.js
File diff suppressed because it is too large
Load Diff
@@ -4,123 +4,7 @@ const { auth, adminAuth } = require('../middleware/auth');
|
|||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
/**
|
// 商品管理路由
|
||||||
* @swagger
|
|
||||||
* tags:
|
|
||||||
* name: Products
|
|
||||||
* description: 商品管理API
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @swagger
|
|
||||||
* components:
|
|
||||||
* schemas:
|
|
||||||
* Product:
|
|
||||||
* type: object
|
|
||||||
* required:
|
|
||||||
* - name
|
|
||||||
* - points_price
|
|
||||||
* - stock
|
|
||||||
* properties:
|
|
||||||
* id:
|
|
||||||
* type: integer
|
|
||||||
* description: 商品ID
|
|
||||||
* name:
|
|
||||||
* type: string
|
|
||||||
* description: 商品名称
|
|
||||||
* category:
|
|
||||||
* type: string
|
|
||||||
* description: 商品分类
|
|
||||||
* points_price:
|
|
||||||
* type: integer
|
|
||||||
* description: 积分价格
|
|
||||||
* stock:
|
|
||||||
* type: integer
|
|
||||||
* description: 库存数量
|
|
||||||
* image_url:
|
|
||||||
* type: string
|
|
||||||
* description: 商品图片URL
|
|
||||||
* description:
|
|
||||||
* type: string
|
|
||||||
* description: 商品描述
|
|
||||||
* status:
|
|
||||||
* type: string
|
|
||||||
* description: 商品状态
|
|
||||||
* enum: [active, inactive]
|
|
||||||
* created_at:
|
|
||||||
* type: string
|
|
||||||
* format: date-time
|
|
||||||
* description: 创建时间
|
|
||||||
* updated_at:
|
|
||||||
* type: string
|
|
||||||
* format: date-time
|
|
||||||
* description: 更新时间
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @swagger
|
|
||||||
* /products:
|
|
||||||
* get:
|
|
||||||
* summary: 获取商品列表
|
|
||||||
* tags: [Products]
|
|
||||||
* parameters:
|
|
||||||
* - in: query
|
|
||||||
* name: page
|
|
||||||
* schema:
|
|
||||||
* type: integer
|
|
||||||
* default: 1
|
|
||||||
* description: 页码
|
|
||||||
* - in: query
|
|
||||||
* name: limit
|
|
||||||
* schema:
|
|
||||||
* type: integer
|
|
||||||
* default: 10
|
|
||||||
* description: 每页数量
|
|
||||||
* - in: query
|
|
||||||
* name: search
|
|
||||||
* schema:
|
|
||||||
* type: string
|
|
||||||
* description: 搜索关键词
|
|
||||||
* - in: query
|
|
||||||
* name: category
|
|
||||||
* schema:
|
|
||||||
* type: string
|
|
||||||
* description: 商品分类
|
|
||||||
* - in: query
|
|
||||||
* name: status
|
|
||||||
* schema:
|
|
||||||
* type: string
|
|
||||||
* enum: [active, inactive]
|
|
||||||
* description: 商品状态
|
|
||||||
* responses:
|
|
||||||
* 200:
|
|
||||||
* description: 成功获取商品列表
|
|
||||||
* content:
|
|
||||||
* application/json:
|
|
||||||
* schema:
|
|
||||||
* type: object
|
|
||||||
* properties:
|
|
||||||
* success:
|
|
||||||
* type: boolean
|
|
||||||
* data:
|
|
||||||
* type: object
|
|
||||||
* properties:
|
|
||||||
* products:
|
|
||||||
* type: array
|
|
||||||
* items:
|
|
||||||
* $ref: '#/components/schemas/Product'
|
|
||||||
* pagination:
|
|
||||||
* type: object
|
|
||||||
* properties:
|
|
||||||
* page:
|
|
||||||
* type: integer
|
|
||||||
* limit:
|
|
||||||
* type: integer
|
|
||||||
* total:
|
|
||||||
* type: integer
|
|
||||||
* pages:
|
|
||||||
* type: integer
|
|
||||||
*/
|
|
||||||
router.get('/', async (req, res) => {
|
router.get('/', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { page = 1, limit = 10, search = '', category = '', status = '' } = req.query;
|
const { page = 1, limit = 10, search = '', category = '', status = '' } = req.query;
|
||||||
@@ -159,7 +43,7 @@ router.get('/', async (req, res) => {
|
|||||||
|
|
||||||
// 获取商品列表
|
// 获取商品列表
|
||||||
const query = `
|
const query = `
|
||||||
SELECT id, name, category, points_price as points, stock, image_url as image, description, status, created_at, updated_at
|
SELECT id, name, rongdou_price, category, points_price as points, stock, image_url as image, description, status, payment_methods, created_at, updated_at
|
||||||
FROM products
|
FROM products
|
||||||
${whereClause}
|
${whereClause}
|
||||||
ORDER BY created_at DESC
|
ORDER BY created_at DESC
|
||||||
@@ -170,7 +54,10 @@ router.get('/', async (req, res) => {
|
|||||||
const queryParams = [...params];
|
const queryParams = [...params];
|
||||||
console.log('Query params:', queryParams, 'Query:', query);
|
console.log('Query params:', queryParams, 'Query:', query);
|
||||||
const [products] = await getDB().execute(query, queryParams);
|
const [products] = await getDB().execute(query, queryParams);
|
||||||
|
products.forEach(item=>{
|
||||||
|
item.payment_methods = JSON.parse(item.payment_methods)
|
||||||
|
})
|
||||||
|
console.log('查询结果:', products);
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
@@ -189,30 +76,7 @@ router.get('/', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
// 获取商品分类列表
|
||||||
* @swagger
|
|
||||||
* /products/categories:
|
|
||||||
* get:
|
|
||||||
* summary: 获取商品分类列表
|
|
||||||
* tags: [Products]
|
|
||||||
* responses:
|
|
||||||
* 200:
|
|
||||||
* description: 成功获取商品分类列表
|
|
||||||
* content:
|
|
||||||
* application/json:
|
|
||||||
* schema:
|
|
||||||
* type: object
|
|
||||||
* properties:
|
|
||||||
* success:
|
|
||||||
* type: boolean
|
|
||||||
* data:
|
|
||||||
* type: object
|
|
||||||
* properties:
|
|
||||||
* categories:
|
|
||||||
* type: array
|
|
||||||
* items:
|
|
||||||
* type: string
|
|
||||||
*/
|
|
||||||
router.get('/categories', async (req, res) => {
|
router.get('/categories', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const [categories] = await getDB().execute(
|
const [categories] = await getDB().execute(
|
||||||
@@ -231,11 +95,111 @@ router.get('/categories', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 获取热销商品
|
||||||
|
router.get('/hot', async (req, res) => {
|
||||||
|
try {
|
||||||
|
// 从活跃商品中随机获取2个商品
|
||||||
|
const [products] = await getDB().execute(
|
||||||
|
`SELECT id, name, category, price, points_price, rongdou_price, stock,
|
||||||
|
image_url, images, description, shop_name, shop_avatar,
|
||||||
|
payment_methods, sales, rating, status, created_at, updated_at
|
||||||
|
FROM products
|
||||||
|
WHERE status = 'active' AND stock > 0
|
||||||
|
ORDER BY RAND()
|
||||||
|
LIMIT 2`
|
||||||
|
);
|
||||||
|
|
||||||
|
// 格式化商品数据
|
||||||
|
const formattedProducts = products.map(product => ({
|
||||||
|
...product,
|
||||||
|
images: product.images ? JSON.parse(product.images) : (product.image_url ? [product.image_url] : []),
|
||||||
|
payment_methods: product.payment_methods ? JSON.parse(product.payment_methods) : ['points'],
|
||||||
|
// 保持向后兼容
|
||||||
|
points: product.points_price,
|
||||||
|
image: product.image_url
|
||||||
|
}));
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
products: formattedProducts
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取热销商品失败:', error);
|
||||||
|
res.status(500).json({ success: false, message: '获取热销商品失败' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /products/flash-sale:
|
||||||
|
* get:
|
||||||
|
* summary: 获取秒杀商品
|
||||||
|
* tags: [Products]
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: 成功获取秒杀商品
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* success:
|
||||||
|
* type: boolean
|
||||||
|
* data:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* products:
|
||||||
|
* type: array
|
||||||
|
* items:
|
||||||
|
* $ref: '#/components/schemas/Product'
|
||||||
|
*/
|
||||||
|
router.get('/cheap', async (req, res) => {
|
||||||
|
try {
|
||||||
|
// 从活跃商品中随机获取2个商品作为秒杀商品
|
||||||
|
const [products] = await getDB().execute(
|
||||||
|
`SELECT id, name, category, price, points_price, rongdou_price, stock,
|
||||||
|
image_url, images, description, shop_name, shop_avatar,
|
||||||
|
payment_methods, sales, rating, status, created_at, updated_at
|
||||||
|
FROM products
|
||||||
|
WHERE status = 'active' AND stock > 0
|
||||||
|
ORDER BY RAND()
|
||||||
|
LIMIT 2`
|
||||||
|
);
|
||||||
|
|
||||||
|
// 格式化商品数据,为秒杀商品添加特殊标识
|
||||||
|
const formattedProducts = products.map(product => ({
|
||||||
|
...product,
|
||||||
|
images: product.images ? JSON.parse(product.images) : (product.image_url ? [product.image_url] : []),
|
||||||
|
payment_methods: product.payment_methods ? JSON.parse(product.payment_methods) : ['points'],
|
||||||
|
// 秒杀商品特殊处理:价格打8折
|
||||||
|
flash_sale_price: Math.floor(product.price * 0.8),
|
||||||
|
flash_sale_points: Math.floor(product.points_price * 0.8),
|
||||||
|
flash_sale_rongdou: Math.floor(product.rongdou_price * 0.8),
|
||||||
|
is_flash_sale: true,
|
||||||
|
// 保持向后兼容
|
||||||
|
points: product.points_price,
|
||||||
|
image: product.image_url
|
||||||
|
}));
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
products: formattedProducts
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取秒杀商品失败:', error);
|
||||||
|
res.status(500).json({ success: false, message: '获取秒杀商品失败' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @swagger
|
* @swagger
|
||||||
* /products/{id}:
|
* /products/{id}:
|
||||||
* get:
|
* get:
|
||||||
* summary: 获取单个商品详情
|
* summary: 获取单个商品详情(包含增强规格信息)
|
||||||
* tags: [Products]
|
* tags: [Products]
|
||||||
* parameters:
|
* parameters:
|
||||||
* - in: path
|
* - in: path
|
||||||
@@ -246,7 +210,7 @@ router.get('/categories', async (req, res) => {
|
|||||||
* description: 商品ID
|
* description: 商品ID
|
||||||
* responses:
|
* responses:
|
||||||
* 200:
|
* 200:
|
||||||
* description: 成功获取商品详情
|
* description: 成功获取商品详情,包含完整的规格信息
|
||||||
* content:
|
* content:
|
||||||
* application/json:
|
* application/json:
|
||||||
* schema:
|
* schema:
|
||||||
@@ -254,8 +218,116 @@ router.get('/categories', async (req, res) => {
|
|||||||
* properties:
|
* properties:
|
||||||
* success:
|
* success:
|
||||||
* type: boolean
|
* type: boolean
|
||||||
|
* example: true
|
||||||
* data:
|
* 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:
|
* 404:
|
||||||
* description: 商品不存在
|
* description: 商品不存在
|
||||||
*/
|
*/
|
||||||
@@ -280,12 +352,92 @@ router.get('/:id', async (req, res) => {
|
|||||||
|
|
||||||
const product = products[0];
|
const product = products[0];
|
||||||
|
|
||||||
// 获取商品规格
|
// 获取商品的规格组合(新的笛卡尔积规格系统)
|
||||||
const [specifications] = await getDB().execute(
|
const [specCombinations] = await getDB().execute(
|
||||||
'SELECT * FROM product_specifications WHERE product_id = ? ORDER BY id',
|
`SELECT psc.*,
|
||||||
|
GROUP_CONCAT(CONCAT(sn.display_name, ':', sv.display_value) ORDER BY sn.sort_order SEPARATOR ' | ') as spec_display
|
||||||
|
FROM product_spec_combinations psc
|
||||||
|
LEFT JOIN JSON_TABLE(psc.spec_values, '$[*]' COLUMNS (spec_value_id INT PATH '$')) jt ON TRUE
|
||||||
|
LEFT JOIN spec_values sv ON jt.spec_value_id = sv.id
|
||||||
|
LEFT JOIN spec_names sn ON sv.spec_name_id = sn.id
|
||||||
|
WHERE psc.product_id = ? AND psc.status = 'active'
|
||||||
|
GROUP BY psc.id
|
||||||
|
ORDER BY psc.combination_key`,
|
||||||
[id]
|
[id]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 为每个规格组合获取详细的规格值信息
|
||||||
|
const enhancedSpecifications = [];
|
||||||
|
for (const combination of specCombinations) {
|
||||||
|
// 智能解析 spec_values 字段,兼容多种数据格式
|
||||||
|
let specValueIds = [];
|
||||||
|
try {
|
||||||
|
if (combination.spec_values) {
|
||||||
|
// 如果是 Buffer 对象,先转换为字符串
|
||||||
|
let specValuesStr = combination.spec_values;
|
||||||
|
if (Buffer.isBuffer(specValuesStr)) {
|
||||||
|
specValuesStr = specValuesStr.toString('utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试 JSON 解析
|
||||||
|
if (typeof specValuesStr === 'string') {
|
||||||
|
specValuesStr = specValuesStr.trim();
|
||||||
|
if (specValuesStr.startsWith('[') && specValuesStr.endsWith(']')) {
|
||||||
|
// JSON 数组格式
|
||||||
|
specValueIds = JSON.parse(specValuesStr);
|
||||||
|
} else if (specValuesStr.includes(',')) {
|
||||||
|
// 逗号分隔的字符串格式
|
||||||
|
specValueIds = specValuesStr.split(',').map(id => parseInt(id.trim())).filter(id => !isNaN(id));
|
||||||
|
} else if (specValuesStr && !isNaN(parseInt(specValuesStr))) {
|
||||||
|
// 单个数字
|
||||||
|
specValueIds = [parseInt(specValuesStr)];
|
||||||
|
}
|
||||||
|
} else if (Array.isArray(specValuesStr)) {
|
||||||
|
// 已经是数组
|
||||||
|
specValueIds = specValuesStr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (parseError) {
|
||||||
|
console.warn(`解析规格值失败 (combination_id: ${combination.id}):`, parseError.message);
|
||||||
|
specValueIds = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取规格值详情
|
||||||
|
if (specValueIds && specValueIds.length > 0) {
|
||||||
|
const placeholders = specValueIds.map(() => '?').join(',');
|
||||||
|
const [specDetails] = await getDB().execute(
|
||||||
|
`SELECT sv.*, sn.name as spec_name, sn.display_name as spec_display_name
|
||||||
|
FROM spec_values sv
|
||||||
|
LEFT JOIN spec_names sn ON sv.spec_name_id = sn.id
|
||||||
|
WHERE sv.id IN (${placeholders})
|
||||||
|
ORDER BY sn.sort_order, sv.sort_order`,
|
||||||
|
specValueIds
|
||||||
|
);
|
||||||
|
|
||||||
|
enhancedSpecifications.push({
|
||||||
|
id: combination.id,
|
||||||
|
combination_key: combination.combination_key,
|
||||||
|
spec_display: combination.spec_display,
|
||||||
|
spec_details: specDetails,
|
||||||
|
price_adjustment: combination.price_adjustment || 0,
|
||||||
|
points_adjustment: combination.points_adjustment || 0,
|
||||||
|
rongdou_adjustment: combination.rongdou_adjustment || 0,
|
||||||
|
stock: combination.stock,
|
||||||
|
sku_code: combination.sku_code,
|
||||||
|
barcode: combination.barcode,
|
||||||
|
weight: combination.weight,
|
||||||
|
volume: combination.volume,
|
||||||
|
actual_price: product.price + (combination.price_adjustment || 0),
|
||||||
|
actual_points_price: product.points_price + (combination.points_adjustment || 0),
|
||||||
|
actual_rongdou_price: product.rongdou_price + (combination.rongdou_adjustment || 0),
|
||||||
|
is_available: combination.stock > 0,
|
||||||
|
status: combination.status,
|
||||||
|
created_at: combination.created_at,
|
||||||
|
updated_at: combination.updated_at
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 获取商品属性
|
// 获取商品属性
|
||||||
const [attributes] = await getDB().execute(
|
const [attributes] = await getDB().execute(
|
||||||
'SELECT * FROM product_attributes WHERE product_id = ? ORDER BY sort_order, id',
|
'SELECT * FROM product_attributes WHERE product_id = ? ORDER BY sort_order, id',
|
||||||
@@ -305,12 +457,72 @@ router.get('/:id', async (req, res) => {
|
|||||||
// 构建增强的商品数据
|
// 构建增强的商品数据
|
||||||
const enhancedProduct = {
|
const enhancedProduct = {
|
||||||
...product,
|
...product,
|
||||||
images: product.images ? JSON.parse(product.images) : (product.image_url ? [product.image_url] : []),
|
images: (() => {
|
||||||
videos: product.videos ? JSON.parse(product.videos) : [],
|
try {
|
||||||
payment_methods: product.payment_methods ? JSON.parse(product.payment_methods) : ['points'],
|
if (product.images) {
|
||||||
specifications,
|
let imagesStr = product.images;
|
||||||
|
if (Buffer.isBuffer(imagesStr)) {
|
||||||
|
imagesStr = imagesStr.toString('utf8');
|
||||||
|
}
|
||||||
|
if (typeof imagesStr === 'string') {
|
||||||
|
imagesStr = imagesStr.trim();
|
||||||
|
if (imagesStr.startsWith('[') && imagesStr.endsWith(']')) {
|
||||||
|
return JSON.parse(imagesStr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return product.image_url ? [product.image_url] : [];
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('解析商品图片失败:', e.message);
|
||||||
|
return product.image_url ? [product.image_url] : [];
|
||||||
|
}
|
||||||
|
})(),
|
||||||
|
videos: (() => {
|
||||||
|
try {
|
||||||
|
if (product.videos) {
|
||||||
|
let videosStr = product.videos;
|
||||||
|
if (Buffer.isBuffer(videosStr)) {
|
||||||
|
videosStr = videosStr.toString('utf8');
|
||||||
|
}
|
||||||
|
if (typeof videosStr === 'string') {
|
||||||
|
videosStr = videosStr.trim();
|
||||||
|
if (videosStr.startsWith('[') && videosStr.endsWith(']')) {
|
||||||
|
return JSON.parse(videosStr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('解析商品视频失败:', e.message);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
})(),
|
||||||
|
payment_methods: (() => {
|
||||||
|
try {
|
||||||
|
if (product.payment_methods) {
|
||||||
|
let methodsStr = product.payment_methods;
|
||||||
|
if (Buffer.isBuffer(methodsStr)) {
|
||||||
|
methodsStr = methodsStr.toString('utf8');
|
||||||
|
}
|
||||||
|
if (typeof methodsStr === 'string') {
|
||||||
|
methodsStr = methodsStr.trim();
|
||||||
|
if (methodsStr.startsWith('[') && methodsStr.endsWith(']')) {
|
||||||
|
return JSON.parse(methodsStr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ['points'];
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('解析支付方式失败:', e.message);
|
||||||
|
return ['points'];
|
||||||
|
}
|
||||||
|
})(),
|
||||||
|
specifications: enhancedSpecifications,
|
||||||
attributes,
|
attributes,
|
||||||
isFavorited,
|
isFavorited,
|
||||||
|
// 规格统计信息
|
||||||
|
specification_count: enhancedSpecifications.length,
|
||||||
|
available_specifications: enhancedSpecifications.filter(spec => spec.is_available).length,
|
||||||
// 保持向后兼容
|
// 保持向后兼容
|
||||||
points: product.points_price,
|
points: product.points_price,
|
||||||
image: product.image_url,
|
image: product.image_url,
|
||||||
@@ -352,19 +564,7 @@ router.post('/', auth, adminAuth, async (req, res) => {
|
|||||||
|
|
||||||
const productId = result.insertId;
|
const productId = result.insertId;
|
||||||
|
|
||||||
// 添加商品规格
|
|
||||||
if (specifications && specifications.length > 0) {
|
|
||||||
for (const spec of specifications) {
|
|
||||||
await getDB().execute(
|
|
||||||
`INSERT INTO product_specifications (product_id, spec_name, spec_value, price_adjustment,
|
|
||||||
points_adjustment, rongdou_adjustment, stock, sku_code)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
||||||
[productId, spec.name, spec.value, spec.price_adjustment || 0,
|
|
||||||
spec.points_adjustment || 0, spec.rongdou_adjustment || 0,
|
|
||||||
spec.stock || 0, spec.sku_code || null]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加商品属性
|
// 添加商品属性
|
||||||
if (attributes && attributes.length > 0) {
|
if (attributes && attributes.length > 0) {
|
||||||
@@ -499,25 +699,7 @@ router.put('/:id', auth, adminAuth, async (req, res) => {
|
|||||||
updateValues
|
updateValues
|
||||||
);
|
);
|
||||||
|
|
||||||
// 更新商品规格
|
|
||||||
if (specifications !== undefined) {
|
|
||||||
// 删除原有规格
|
|
||||||
await getDB().execute('DELETE FROM product_specifications WHERE product_id = ?', [productId]);
|
|
||||||
|
|
||||||
// 添加新规格
|
|
||||||
if (specifications && specifications.length > 0) {
|
|
||||||
for (const spec of specifications) {
|
|
||||||
await getDB().execute(
|
|
||||||
`INSERT INTO product_specifications (product_id, spec_name, spec_value, price_adjustment,
|
|
||||||
points_adjustment, rongdou_adjustment, stock, sku_code)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
||||||
[productId, spec.name, spec.value, spec.price_adjustment || 0,
|
|
||||||
spec.points_adjustment || 0, spec.rongdou_adjustment || 0,
|
|
||||||
spec.stock || 0, spec.sku_code || null]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新商品属性
|
// 更新商品属性
|
||||||
if (attributes !== undefined) {
|
if (attributes !== undefined) {
|
||||||
@@ -655,8 +837,8 @@ router.get('/:id/reviews', async (req, res) => {
|
|||||||
JOIN users u ON pr.user_id = u.id
|
JOIN users u ON pr.user_id = u.id
|
||||||
WHERE pr.product_id = ?
|
WHERE pr.product_id = ?
|
||||||
ORDER BY pr.created_at DESC
|
ORDER BY pr.created_at DESC
|
||||||
LIMIT ? OFFSET ?`,
|
LIMIT ${limit} OFFSET ${offset}`,
|
||||||
[id, limit, offset]
|
[id]
|
||||||
);
|
);
|
||||||
|
|
||||||
// 获取评论总数
|
// 获取评论总数
|
||||||
@@ -832,8 +1014,8 @@ router.get('/favorites', auth, async (req, res) => {
|
|||||||
JOIN products p ON pf.product_id = p.id
|
JOIN products p ON pf.product_id = p.id
|
||||||
WHERE pf.user_id = ? AND p.status = 'active'
|
WHERE pf.user_id = ? AND p.status = 'active'
|
||||||
ORDER BY pf.created_at DESC
|
ORDER BY pf.created_at DESC
|
||||||
LIMIT ? OFFSET ?`,
|
LIMIT ${limit} OFFSET ${offset}`,
|
||||||
[userId, limit, offset]
|
[userId]
|
||||||
);
|
);
|
||||||
|
|
||||||
const [countResult] = await getDB().execute(
|
const [countResult] = await getDB().execute(
|
||||||
@@ -867,163 +1049,13 @@ router.get('/favorites', auth, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 获取商品规格
|
|
||||||
router.get('/:id/specifications', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const productId = req.params.id;
|
|
||||||
|
|
||||||
const [specifications] = await getDB().execute(
|
|
||||||
'SELECT id, spec_name as name, spec_value as value, price_adjustment, points_adjustment, rongdou_adjustment, stock, sku_code, created_at, updated_at FROM product_specifications WHERE product_id = ? ORDER BY id',
|
|
||||||
[productId]
|
|
||||||
);
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
data: specifications
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取商品规格错误:', error);
|
|
||||||
res.status(500).json({ message: '获取商品规格失败' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 创建商品规格(管理员权限)
|
|
||||||
router.post('/:id/specifications', auth, adminAuth, async (req, res) => {
|
|
||||||
try {
|
|
||||||
const productId = req.params.id;
|
|
||||||
const { name, value, price_adjustment = 0, points_adjustment = 0, rongdou_adjustment = 0, stock = 0, sku_code } = req.body;
|
|
||||||
|
|
||||||
if (!name || !value) {
|
|
||||||
return res.status(400).json({ message: '规格名称和规格值不能为空' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查商品是否存在
|
|
||||||
const [products] = await getDB().execute('SELECT id FROM products WHERE id = ?', [productId]);
|
|
||||||
if (products.length === 0) {
|
|
||||||
return res.status(404).json({ message: '商品不存在' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const [result] = await getDB().execute(
|
|
||||||
`INSERT INTO product_specifications (product_id, spec_name, spec_value, price_adjustment,
|
|
||||||
points_adjustment, rongdou_adjustment, stock, sku_code, created_at, updated_at)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())`,
|
|
||||||
[productId, name, value, price_adjustment, points_adjustment, rongdou_adjustment, stock, sku_code || null]
|
|
||||||
);
|
|
||||||
|
|
||||||
res.status(201).json({
|
|
||||||
success: true,
|
|
||||||
message: '规格创建成功',
|
|
||||||
data: { id: result.insertId }
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('创建商品规格错误:', error);
|
|
||||||
res.status(500).json({ message: '创建商品规格失败' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 更新商品规格(管理员权限)
|
|
||||||
router.put('/:id/specifications/:specId', auth, adminAuth, async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { id: productId, specId } = req.params;
|
|
||||||
const { name, value, price_adjustment, points_adjustment, rongdou_adjustment, stock, sku_code } = req.body;
|
|
||||||
|
|
||||||
// 检查规格是否存在
|
|
||||||
const [specs] = await getDB().execute(
|
|
||||||
'SELECT id FROM product_specifications WHERE id = ? AND product_id = ?',
|
|
||||||
[specId, productId]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (specs.length === 0) {
|
|
||||||
return res.status(404).json({ message: '规格不存在' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 构建更新字段
|
|
||||||
const updateFields = [];
|
|
||||||
const updateValues = [];
|
|
||||||
|
|
||||||
if (name !== undefined) {
|
|
||||||
updateFields.push('spec_name = ?');
|
|
||||||
updateValues.push(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value !== undefined) {
|
|
||||||
updateFields.push('spec_value = ?');
|
|
||||||
updateValues.push(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (price_adjustment !== undefined) {
|
|
||||||
updateFields.push('price_adjustment = ?');
|
|
||||||
updateValues.push(price_adjustment);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (points_adjustment !== undefined) {
|
|
||||||
updateFields.push('points_adjustment = ?');
|
|
||||||
updateValues.push(points_adjustment);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rongdou_adjustment !== undefined) {
|
|
||||||
updateFields.push('rongdou_adjustment = ?');
|
|
||||||
updateValues.push(rongdou_adjustment);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stock !== undefined) {
|
|
||||||
updateFields.push('stock = ?');
|
|
||||||
updateValues.push(stock);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sku_code !== undefined) {
|
|
||||||
updateFields.push('sku_code = ?');
|
|
||||||
updateValues.push(sku_code);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updateFields.length === 0) {
|
|
||||||
return res.status(400).json({ message: '没有提供要更新的字段' });
|
|
||||||
}
|
|
||||||
|
|
||||||
updateFields.push('updated_at = NOW()');
|
|
||||||
updateValues.push(specId);
|
|
||||||
|
|
||||||
await getDB().execute(
|
|
||||||
`UPDATE product_specifications SET ${updateFields.join(', ')} WHERE id = ?`,
|
|
||||||
updateValues
|
|
||||||
);
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
message: '规格更新成功'
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('更新商品规格错误:', error);
|
|
||||||
res.status(500).json({ message: '更新商品规格失败' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 删除商品规格(管理员权限)
|
|
||||||
router.delete('/:id/specifications/:specId', auth, adminAuth, async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { id: productId, specId } = req.params;
|
|
||||||
|
|
||||||
// 检查规格是否存在
|
|
||||||
const [specs] = await getDB().execute(
|
|
||||||
'SELECT id FROM product_specifications WHERE id = ? AND product_id = ?',
|
|
||||||
[specId, productId]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (specs.length === 0) {
|
|
||||||
return res.status(404).json({ message: '规格不存在' });
|
|
||||||
}
|
|
||||||
|
|
||||||
await getDB().execute('DELETE FROM product_specifications WHERE id = ?', [specId]);
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
message: '规格删除成功'
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('删除商品规格错误:', error);
|
|
||||||
res.status(500).json({ message: '删除商品规格失败' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 获取商品属性
|
// 获取商品属性
|
||||||
router.get('/:id/attributes', async (req, res) => {
|
router.get('/:id/attributes', async (req, res) => {
|
||||||
|
|||||||
@@ -120,12 +120,37 @@ router.get('/zhejiang', async (req, res) => {
|
|||||||
*/
|
*/
|
||||||
router.get('/provinces', async (req, res) => {
|
router.get('/provinces', async (req, res) => {
|
||||||
try {
|
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(
|
const [provinces] = await getDB().execute(
|
||||||
`SELECT code, name FROM china_regions
|
`SELECT code, name as label, level FROM china_regions
|
||||||
WHERE level = 1
|
WHERE level = 1
|
||||||
ORDER BY code`
|
ORDER BY code`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 为每个省份递归获取城市和区县
|
||||||
|
for (let province of provinces) {
|
||||||
|
province.children = await getChildrenRecursively(province.code, 2);
|
||||||
|
}
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: provinces
|
data: provinces
|
||||||
|
|||||||
1096
routes/specifications.js
Normal file
1096
routes/specifications.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -821,8 +821,8 @@ router.get('/user/:userId', authenticateToken, async (req, res) => {
|
|||||||
LEFT JOIN users to_user ON t.to_user_id = to_user.id
|
LEFT JOIN users to_user ON t.to_user_id = to_user.id
|
||||||
${whereClause}
|
${whereClause}
|
||||||
ORDER BY t.created_at DESC
|
ORDER BY t.created_at DESC
|
||||||
LIMIT ? OFFSET ?
|
LIMIT ${limitNum} OFFSET ${offset}
|
||||||
`, listParams);
|
`, countParams);
|
||||||
|
|
||||||
const [countResult] = await db.execute(`
|
const [countResult] = await db.execute(`
|
||||||
SELECT COUNT(*) as total FROM transfers t ${whereClause}
|
SELECT COUNT(*) as total FROM transfers t ${whereClause}
|
||||||
@@ -1556,10 +1556,11 @@ router.get('/daily-stats',
|
|||||||
u.balance,
|
u.balance,
|
||||||
COALESCE(yesterday_out.amount, 0) as yesterday_out_amount,
|
COALESCE(yesterday_out.amount, 0) as yesterday_out_amount,
|
||||||
COALESCE(today_in.amount, 0) as today_in_amount,
|
COALESCE(today_in.amount, 0) as today_in_amount,
|
||||||
|
COALESCE(confirmed_from.confirmed_amount, 0) as confirmed_from_amount,
|
||||||
CASE
|
CASE
|
||||||
WHEN (COALESCE(yesterday_out.amount, 0) - COALESCE(today_in.amount, 0)) > ABS(u.balance)
|
WHEN (COALESCE(u.balance, 0) +COALESCE(confirmed_from.confirmed_amount, 0) ) > ABS(u.balance)
|
||||||
THEN ABS(u.balance)
|
THEN ABS(u.balance)
|
||||||
ELSE (COALESCE(yesterday_out.amount, 0) - COALESCE(today_in.amount, 0))
|
ELSE (COALESCE(u.balance, 0)+ COALESCE(confirmed_from.confirmed_amount, 0) )
|
||||||
END as balance_needed
|
END as balance_needed
|
||||||
FROM users u
|
FROM users u
|
||||||
LEFT JOIN (
|
LEFT JOIN (
|
||||||
@@ -1580,13 +1581,29 @@ router.get('/daily-stats',
|
|||||||
AND status IN ('confirmed', 'received')
|
AND status IN ('confirmed', 'received')
|
||||||
GROUP BY to_user_id
|
GROUP BY to_user_id
|
||||||
) today_in ON u.id = today_in.to_user_id
|
) today_in ON u.id = today_in.to_user_id
|
||||||
|
left join (
|
||||||
|
select
|
||||||
|
from_user_id,
|
||||||
|
sum(amount) as confirmed_amount
|
||||||
|
from
|
||||||
|
transfers
|
||||||
|
where
|
||||||
|
status = 'received'
|
||||||
|
and created_at >= ?
|
||||||
|
and created_at <= ?
|
||||||
|
group by
|
||||||
|
from_user_id
|
||||||
|
) as confirmed_from on u.id = confirmed_from.from_user_id
|
||||||
WHERE u.role != 'admin'
|
WHERE u.role != 'admin'
|
||||||
AND u.is_system_account != 1
|
AND u.is_system_account != 1
|
||||||
AND yesterday_out.amount > 0
|
AND yesterday_out.amount > 0
|
||||||
AND u.balance < 0
|
AND u.balance < 0
|
||||||
ORDER BY balance_needed DESC, yesterday_out_amount DESC
|
ORDER BY balance_needed DESC, yesterday_out_amount DESC
|
||||||
`, [yesterdayStartStr, yesterdayEndStr, todayStartStr, todayEndStr]);
|
`, [yesterdayStartStr, yesterdayEndStr, todayStartStr, todayEndStr, todayStartStr, todayEndStr]);
|
||||||
userStats = userStats.filter(item=>item.balance_needed >= 100)
|
// userStats = userStats.filter(item=>item.balance_needed >= 100)
|
||||||
|
userStats.forEach(item=>{
|
||||||
|
item.balance_needed = Math.abs(item.balance_needed)
|
||||||
|
})
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
139
routes/users.js
139
routes/users.js
@@ -53,6 +53,9 @@ const router = express.Router();
|
|||||||
* is_system_account:
|
* is_system_account:
|
||||||
* type: boolean
|
* type: boolean
|
||||||
* description: 是否为系统账户
|
* description: 是否为系统账户
|
||||||
|
* is_distribute:
|
||||||
|
* type: boolean
|
||||||
|
* description: 是否为分发账户
|
||||||
* created_at:
|
* created_at:
|
||||||
* type: string
|
* type: string
|
||||||
* format: date-time
|
* format: date-time
|
||||||
@@ -400,7 +403,7 @@ router.get('/', auth, adminAuth, async (req, res) => {
|
|||||||
`SELECT u.id, u.username, u.role, u.avatar, u.points, u.balance, u.real_name, u.id_card, u.phone,
|
`SELECT u.id, u.username, u.role, u.avatar, u.points, u.balance, u.real_name, u.id_card, u.phone,
|
||||||
u.wechat_qr, u.alipay_qr, u.bank_card, u.unionpay_qr, u.audit_status, u.is_system_account,
|
u.wechat_qr, u.alipay_qr, u.bank_card, u.unionpay_qr, u.audit_status, u.is_system_account,
|
||||||
u.created_at, u.updated_at, u.city, u.district_id,u.id_card_front,u.id_card_back,
|
u.created_at, u.updated_at, u.city, u.district_id,u.id_card_front,u.id_card_back,
|
||||||
u.business_license,
|
u.business_license,u.is_distribute,
|
||||||
r.city_name, r.district_name,
|
r.city_name, r.district_name,
|
||||||
COALESCE(yesterday_out.amount, 0) as yesterday_transfer_amount,
|
COALESCE(yesterday_out.amount, 0) as yesterday_transfer_amount,
|
||||||
COALESCE(today_in.amount, 0) as today_received_amount
|
COALESCE(today_in.amount, 0) as today_received_amount
|
||||||
@@ -424,8 +427,8 @@ router.get('/', auth, adminAuth, async (req, res) => {
|
|||||||
) today_in ON u.id = today_in.to_user_id
|
) today_in ON u.id = today_in.to_user_id
|
||||||
${whereClause}
|
${whereClause}
|
||||||
ORDER BY u.${sortField} ${sortOrder}
|
ORDER BY u.${sortField} ${sortOrder}
|
||||||
LIMIT ? OFFSET ?`,
|
LIMIT ${limitNum} OFFSET ${offset}`,
|
||||||
listParams
|
listParams.slice(0, -2)
|
||||||
);
|
);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
@@ -770,8 +773,8 @@ router.get('/points/history', auth, async (req, res) => {
|
|||||||
FROM points_history
|
FROM points_history
|
||||||
${whereClause}
|
${whereClause}
|
||||||
ORDER BY created_at DESC
|
ORDER BY created_at DESC
|
||||||
LIMIT ? OFFSET ?`,
|
LIMIT ${limitNum} OFFSET ${offset}`,
|
||||||
[...queryParams, limitNum.toString(), offset.toString()]
|
queryParams
|
||||||
);
|
);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
@@ -1067,8 +1070,8 @@ router.get('/registration-codes', auth, adminAuth, async (req, res) => {
|
|||||||
LEFT JOIN users user ON rc.used_by_user_id = user.id
|
LEFT JOIN users user ON rc.used_by_user_id = user.id
|
||||||
${whereClause}
|
${whereClause}
|
||||||
ORDER BY ${sortField} ${sortOrder}
|
ORDER BY ${sortField} ${sortOrder}
|
||||||
LIMIT ? OFFSET ?
|
LIMIT ${limit} OFFSET ${offset}
|
||||||
`, listParams);
|
`, countParams);
|
||||||
|
|
||||||
// 获取总数
|
// 获取总数
|
||||||
const [countResult] = await db.execute(`
|
const [countResult] = await db.execute(`
|
||||||
@@ -1722,5 +1725,127 @@ router.get('/:id/audit-detail', auth, adminAuth, async (req, res) => {
|
|||||||
res.status(500).json({ success: false, message: '获取用户审核详情失败' });
|
res.status(500).json({ success: false, message: '获取用户审核详情失败' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* /api/users/{id}/distribute:
|
||||||
|
* put:
|
||||||
|
* summary: 设置用户分发状态
|
||||||
|
* description: 更新指定用户的分发状态
|
||||||
|
* tags: [Users]
|
||||||
|
* security:
|
||||||
|
* - bearerAuth: []
|
||||||
|
* parameters:
|
||||||
|
* - in: path
|
||||||
|
* name: id
|
||||||
|
* required: true
|
||||||
|
* schema:
|
||||||
|
* type: integer
|
||||||
|
* description: 用户ID
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* required:
|
||||||
|
* - is_distribute
|
||||||
|
* properties:
|
||||||
|
* is_distribute:
|
||||||
|
* type: boolean
|
||||||
|
* description: 分发状态,true为启用分发,false为禁用分发
|
||||||
|
* example: true
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: 分发状态更新成功
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* success:
|
||||||
|
* type: boolean
|
||||||
|
* example: true
|
||||||
|
* message:
|
||||||
|
* type: string
|
||||||
|
* example: "分发状态更新成功"
|
||||||
|
* is_distribute:
|
||||||
|
* type: boolean
|
||||||
|
* description: 更新后的分发状态
|
||||||
|
* example: true
|
||||||
|
* 400:
|
||||||
|
* description: 请求参数错误
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* success:
|
||||||
|
* type: boolean
|
||||||
|
* example: false
|
||||||
|
* message:
|
||||||
|
* type: string
|
||||||
|
* example: "分发状态无效"
|
||||||
|
* 404:
|
||||||
|
* description: 用户不存在
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* success:
|
||||||
|
* type: boolean
|
||||||
|
* example: false
|
||||||
|
* message:
|
||||||
|
* type: string
|
||||||
|
* example: "用户不存在"
|
||||||
|
* 500:
|
||||||
|
* description: 服务器内部错误
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* type: object
|
||||||
|
* properties:
|
||||||
|
* success:
|
||||||
|
* type: boolean
|
||||||
|
* example: false
|
||||||
|
* message:
|
||||||
|
* type: string
|
||||||
|
* example: "服务器内部错误"
|
||||||
|
*/
|
||||||
|
router.put('/:id/distribute', auth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const db = getDB();
|
||||||
|
const userId = req.params.id;
|
||||||
|
const { is_distribute } = req.body;
|
||||||
|
|
||||||
|
if (typeof is_distribute !== 'boolean') {
|
||||||
|
return res.status(400).json({ success: false, message: '分发状态无效' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查用户是否存在
|
||||||
|
const [users] = await db.execute(
|
||||||
|
'SELECT id FROM users WHERE id = ?',
|
||||||
|
[userId]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (users.length === 0) {
|
||||||
|
return res.status(404).json({ success: false, message: '用户不存在' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新分发状态
|
||||||
|
await db.execute(
|
||||||
|
'UPDATE users SET is_distribute = ? WHERE id = ?',
|
||||||
|
[is_distribute, userId]
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: '分发状态更新成功',
|
||||||
|
is_distribute
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
@@ -1,154 +0,0 @@
|
|||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SQL 语法修复脚本:修复自动替换产生的 SQL 语法错误
|
|
||||||
*/
|
|
||||||
|
|
||||||
class SQLSyntaxFixer {
|
|
||||||
constructor() {
|
|
||||||
this.filesToFix = [
|
|
||||||
'services/matchingService.js',
|
|
||||||
'routes/matchingAdmin.js',
|
|
||||||
'routes/transfers.js',
|
|
||||||
'routes/matching.js'
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 修复单个文件中的 SQL 语法错误
|
|
||||||
* @param {string} filePath - 文件路径
|
|
||||||
*/
|
|
||||||
async fixFile(filePath) {
|
|
||||||
const fullPath = path.join(process.cwd(), filePath);
|
|
||||||
|
|
||||||
if (!fs.existsSync(fullPath)) {
|
|
||||||
console.log(`文件不存在: ${filePath}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`正在修复文件: ${filePath}`);
|
|
||||||
|
|
||||||
let content = fs.readFileSync(fullPath, 'utf8');
|
|
||||||
let originalContent = content;
|
|
||||||
|
|
||||||
// 1. 修复 WHERE source_type = 'allocation' FROM transfers 的错误顺序
|
|
||||||
content = content.replace(
|
|
||||||
/WHERE source_type = 'allocation' FROM transfers/g,
|
|
||||||
"FROM transfers WHERE source_type = 'allocation'"
|
|
||||||
);
|
|
||||||
|
|
||||||
// 2. 修复多个 WHERE 子句的问题
|
|
||||||
content = content.replace(
|
|
||||||
/FROM transfers WHERE source_type = 'allocation'([\s\S]*?)WHERE/g,
|
|
||||||
"FROM transfers WHERE source_type = 'allocation'$1AND"
|
|
||||||
);
|
|
||||||
|
|
||||||
// 3. 修复 INSERT 语句中的引号问题
|
|
||||||
content = content.replace(
|
|
||||||
/'allocation'/g,
|
|
||||||
"'allocation'"
|
|
||||||
);
|
|
||||||
|
|
||||||
// 4. 修复 JOIN 语句中的表别名问题
|
|
||||||
content = content.replace(
|
|
||||||
/FROM transfers oa WHERE oa\.source_type = 'allocation'/g,
|
|
||||||
"FROM transfers oa WHERE oa.source_type = 'allocation'"
|
|
||||||
);
|
|
||||||
|
|
||||||
// 5. 修复复杂查询中的语法问题
|
|
||||||
content = this.fixComplexQueries(content, filePath);
|
|
||||||
|
|
||||||
if (content !== originalContent) {
|
|
||||||
fs.writeFileSync(fullPath, content);
|
|
||||||
console.log(`✓ 已修复: ${filePath}`);
|
|
||||||
} else {
|
|
||||||
console.log(`- 无需修复: ${filePath}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 修复复杂查询
|
|
||||||
* @param {string} content - 文件内容
|
|
||||||
* @param {string} filePath - 文件路径
|
|
||||||
* @returns {string} 修复后的内容
|
|
||||||
*/
|
|
||||||
fixComplexQueries(content, filePath) {
|
|
||||||
if (filePath.includes('matchingService.js')) {
|
|
||||||
// 修复 matchingService.js 中的特定查询
|
|
||||||
|
|
||||||
// 修复获取匹配目标的查询
|
|
||||||
content = content.replace(
|
|
||||||
/FROM transfers oa\s+WHERE oa\.source_type = 'allocation'\s+JOIN users u ON oa\.from_user_id = u\.id/g,
|
|
||||||
"FROM transfers oa JOIN users u ON oa.from_user_id = u.id WHERE oa.source_type = 'allocation'"
|
|
||||||
);
|
|
||||||
|
|
||||||
// 修复获取用户待处理分配的查询
|
|
||||||
content = content.replace(
|
|
||||||
/SELECT \* FROM transfers WHERE source_type = 'allocation' WHERE matching_order_id = \? ORDER BY cycle_number, created_at/g,
|
|
||||||
"SELECT * FROM transfers WHERE source_type = 'allocation' AND matching_order_id = ? ORDER BY cycle_number, created_at"
|
|
||||||
);
|
|
||||||
|
|
||||||
// 修复检查周期完成的查询
|
|
||||||
content = content.replace(
|
|
||||||
/SELECT COUNT\(\*\) as count FROM transfers WHERE source_type = 'allocation' WHERE matching_order_id = \? AND cycle_number = \? AND status = "pending"/g,
|
|
||||||
"SELECT COUNT(*) as count FROM transfers WHERE source_type = 'allocation' AND matching_order_id = ? AND cycle_number = ? AND status = 'pending'"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filePath.includes('matchingAdmin.js')) {
|
|
||||||
// 修复 matchingAdmin.js 中的查询
|
|
||||||
content = content.replace(
|
|
||||||
/FROM transfers oa WHERE oa\.source_type = 'allocation'\s+JOIN/g,
|
|
||||||
"FROM transfers oa JOIN"
|
|
||||||
);
|
|
||||||
|
|
||||||
// 在 JOIN 后添加 WHERE 条件
|
|
||||||
content = content.replace(
|
|
||||||
/(FROM transfers oa JOIN[\s\S]*?)WHERE(?!.*source_type)/g,
|
|
||||||
"$1WHERE oa.source_type = 'allocation' AND"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filePath.includes('matching.js')) {
|
|
||||||
// 修复 matching.js 中的查询
|
|
||||||
content = content.replace(
|
|
||||||
/LEFT JOIN transfers oa ON mo\.id = oa\.matching_order_id WHERE oa\.source_type = 'allocation'/g,
|
|
||||||
"LEFT JOIN transfers oa ON mo.id = oa.matching_order_id AND oa.source_type = 'allocation'"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 执行所有文件的修复
|
|
||||||
*/
|
|
||||||
async fixAllFiles() {
|
|
||||||
console.log('开始修复 SQL 语法错误...');
|
|
||||||
console.log('=' .repeat(60));
|
|
||||||
|
|
||||||
for (const filePath of this.filesToFix) {
|
|
||||||
try {
|
|
||||||
await this.fixFile(filePath);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`修复文件 ${filePath} 失败:`, error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('\n' + '=' .repeat(60));
|
|
||||||
console.log('✓ SQL 语法修复完成!');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
const fixer = new SQLSyntaxFixer();
|
|
||||||
await fixer.fixAllFiles();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果直接运行此脚本
|
|
||||||
if (require.main === module) {
|
|
||||||
main().catch(console.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = SQLSyntaxFixer;
|
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 表别名修复脚本:修复 SQL 查询中的表别名问题
|
|
||||||
*/
|
|
||||||
|
|
||||||
class TableAliasFixer {
|
|
||||||
constructor() {
|
|
||||||
this.filesToFix = [
|
|
||||||
'services/matchingService.js',
|
|
||||||
'routes/matchingAdmin.js',
|
|
||||||
'routes/transfers.js',
|
|
||||||
'routes/matching.js'
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 修复单个文件中的表别名问题
|
|
||||||
* @param {string} filePath - 文件路径
|
|
||||||
*/
|
|
||||||
async fixFile(filePath) {
|
|
||||||
const fullPath = path.join(process.cwd(), filePath);
|
|
||||||
|
|
||||||
if (!fs.existsSync(fullPath)) {
|
|
||||||
console.log(`文件不存在: ${filePath}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`正在修复文件: ${filePath}`);
|
|
||||||
|
|
||||||
let content = fs.readFileSync(fullPath, 'utf8');
|
|
||||||
let originalContent = content;
|
|
||||||
|
|
||||||
// 1. 修复 "FROM transfers WHERE source_type = 'allocation' oa" 的问题
|
|
||||||
content = content.replace(
|
|
||||||
/FROM transfers WHERE source_type = 'allocation' (\w+)/g,
|
|
||||||
"FROM transfers $1 WHERE $1.source_type = 'allocation'"
|
|
||||||
);
|
|
||||||
|
|
||||||
// 2. 修复重复的 source_type 条件
|
|
||||||
content = content.replace(
|
|
||||||
/FROM transfers WHERE source_type = 'allocation' (\w+) AND \1\.source_type = 'allocation'/g,
|
|
||||||
"FROM transfers $1 WHERE $1.source_type = 'allocation'"
|
|
||||||
);
|
|
||||||
|
|
||||||
// 3. 修复 "FROM transfers WHERE source_type = 'allocation'" 后面直接跟其他子句的情况
|
|
||||||
content = content.replace(
|
|
||||||
/FROM transfers WHERE source_type = 'allocation'\s+(JOIN|ORDER|GROUP|LIMIT)/g,
|
|
||||||
"FROM transfers WHERE source_type = 'allocation' $1"
|
|
||||||
);
|
|
||||||
|
|
||||||
// 4. 修复子查询中的问题
|
|
||||||
content = content.replace(
|
|
||||||
/\(SELECT[^)]*FROM transfers WHERE source_type = 'allocation' (\w+)/g,
|
|
||||||
(match, alias) => {
|
|
||||||
return match.replace(
|
|
||||||
`FROM transfers WHERE source_type = 'allocation' ${alias}`,
|
|
||||||
`FROM transfers ${alias} WHERE ${alias}.source_type = 'allocation'`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// 5. 修复特定的查询模式
|
|
||||||
content = this.fixSpecificPatterns(content, filePath);
|
|
||||||
|
|
||||||
if (content !== originalContent) {
|
|
||||||
fs.writeFileSync(fullPath, content);
|
|
||||||
console.log(`✓ 已修复: ${filePath}`);
|
|
||||||
} else {
|
|
||||||
console.log(`- 无需修复: ${filePath}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 修复特定的查询模式
|
|
||||||
* @param {string} content - 文件内容
|
|
||||||
* @param {string} filePath - 文件路径
|
|
||||||
* @returns {string} 修复后的内容
|
|
||||||
*/
|
|
||||||
fixSpecificPatterns(content, filePath) {
|
|
||||||
// 修复 SELECT 语句中的表别名问题
|
|
||||||
content = content.replace(
|
|
||||||
/SELECT ([^F]*?) FROM transfers WHERE source_type = 'allocation' (\w+)/g,
|
|
||||||
"SELECT $1 FROM transfers $2 WHERE $2.source_type = 'allocation'"
|
|
||||||
);
|
|
||||||
|
|
||||||
// 修复 UPDATE 语句
|
|
||||||
content = content.replace(
|
|
||||||
/UPDATE transfers WHERE source_type = 'allocation' SET/g,
|
|
||||||
"UPDATE transfers SET"
|
|
||||||
);
|
|
||||||
|
|
||||||
// 修复 WHERE 子句中的条件
|
|
||||||
content = content.replace(
|
|
||||||
/WHERE source_type = 'allocation' AND (\w+)\./g,
|
|
||||||
"WHERE $1.source_type = 'allocation' AND $1."
|
|
||||||
);
|
|
||||||
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 执行所有文件的修复
|
|
||||||
*/
|
|
||||||
async fixAllFiles() {
|
|
||||||
console.log('开始修复表别名问题...');
|
|
||||||
console.log('=' .repeat(60));
|
|
||||||
|
|
||||||
for (const filePath of this.filesToFix) {
|
|
||||||
try {
|
|
||||||
await this.fixFile(filePath);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`修复文件 ${filePath} 失败:`, error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('\n' + '=' .repeat(60));
|
|
||||||
console.log('✓ 表别名修复完成!');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
const fixer = new TableAliasFixer();
|
|
||||||
await fixer.fixAllFiles();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果直接运行此脚本
|
|
||||||
if (require.main === module) {
|
|
||||||
main().catch(console.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = TableAliasFixer;
|
|
||||||
144
scripts/import_china_regions.js
Normal file
144
scripts/import_china_regions.js
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const mysql = require('mysql2/promise');
|
||||||
|
require('dotenv').config();
|
||||||
|
|
||||||
|
// 数据库配置
|
||||||
|
const dbConfig = {
|
||||||
|
host: process.env.DB_HOST || '114.55.111.44',
|
||||||
|
user: process.env.DB_USER || 'maov2',
|
||||||
|
password: process.env.DB_PASSWORD || '5fYhw8z6T62b7heS',
|
||||||
|
database: process.env.DB_NAME || 'maov2',
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化数据库连接
|
||||||
|
async function initDB() {
|
||||||
|
try {
|
||||||
|
const connection = await mysql.createConnection(dbConfig);
|
||||||
|
console.log('数据库连接成功');
|
||||||
|
return connection;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('数据库连接失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 递归解析省市区数据
|
||||||
|
function parseRegionData(regions, parentCode = null, level = 1) {
|
||||||
|
const result = [];
|
||||||
|
let sortOrder = 1;
|
||||||
|
|
||||||
|
for (const region of regions) {
|
||||||
|
// 添加当前区域
|
||||||
|
result.push({
|
||||||
|
code: region.code,
|
||||||
|
name: region.name,
|
||||||
|
parent_code: parentCode,
|
||||||
|
level: level,
|
||||||
|
sort_order: sortOrder++
|
||||||
|
});
|
||||||
|
|
||||||
|
// 递归处理子区域
|
||||||
|
if (region.children && region.children.length > 0) {
|
||||||
|
const childrenData = parseRegionData(region.children, region.code, level + 1);
|
||||||
|
result.push(...childrenData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导入省市区数据
|
||||||
|
async function importChinaRegions() {
|
||||||
|
let connection;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 读取 JSON 数据文件
|
||||||
|
const jsonFilePath = path.join(__dirname, 'pca-code.json');
|
||||||
|
const jsonData = fs.readFileSync(jsonFilePath, 'utf8');
|
||||||
|
const regionsData = JSON.parse(jsonData);
|
||||||
|
|
||||||
|
console.log('成功读取省市区数据文件');
|
||||||
|
|
||||||
|
// 解析数据
|
||||||
|
const parsedData = parseRegionData(regionsData);
|
||||||
|
console.log(`解析完成,共 ${parsedData.length} 条记录`);
|
||||||
|
|
||||||
|
// 连接数据库
|
||||||
|
connection = await initDB();
|
||||||
|
|
||||||
|
// 清空现有数据
|
||||||
|
await connection.execute('DELETE FROM china_regions');
|
||||||
|
console.log('已清空现有数据');
|
||||||
|
|
||||||
|
// 批量插入数据
|
||||||
|
const batchSize = 100;
|
||||||
|
let insertedCount = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < parsedData.length; i += batchSize) {
|
||||||
|
const batch = parsedData.slice(i, i + batchSize);
|
||||||
|
const values = batch.map(item => [
|
||||||
|
item.code,
|
||||||
|
item.name,
|
||||||
|
item.parent_code,
|
||||||
|
item.level,
|
||||||
|
item.sort_order
|
||||||
|
]);
|
||||||
|
|
||||||
|
const placeholders = values.map(() => '(?, ?, ?, ?, ?)').join(', ');
|
||||||
|
const flatValues = values.flat();
|
||||||
|
|
||||||
|
await connection.execute(
|
||||||
|
`INSERT INTO china_regions (code, name, parent_code, level, sort_order) VALUES ${placeholders}`,
|
||||||
|
flatValues
|
||||||
|
);
|
||||||
|
|
||||||
|
insertedCount += batch.length;
|
||||||
|
console.log(`已插入 ${insertedCount}/${parsedData.length} 条记录`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统计导入结果
|
||||||
|
const [provinceResult] = await connection.execute(
|
||||||
|
'SELECT COUNT(*) as count FROM china_regions WHERE level = 1'
|
||||||
|
);
|
||||||
|
const [cityResult] = await connection.execute(
|
||||||
|
'SELECT COUNT(*) as count FROM china_regions WHERE level = 2'
|
||||||
|
);
|
||||||
|
const [districtResult] = await connection.execute(
|
||||||
|
'SELECT COUNT(*) as count FROM china_regions WHERE level = 3'
|
||||||
|
);
|
||||||
|
const [totalResult] = await connection.execute(
|
||||||
|
'SELECT COUNT(*) as count FROM china_regions'
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('\n=== 导入完成 ===');
|
||||||
|
console.log(`省份数量: ${provinceResult[0].count}`);
|
||||||
|
console.log(`城市数量: ${cityResult[0].count}`);
|
||||||
|
console.log(`区县数量: ${districtResult[0].count}`);
|
||||||
|
console.log(`总记录数: ${totalResult[0].count}`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('导入失败:', error);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
if (connection) {
|
||||||
|
await connection.end();
|
||||||
|
console.log('数据库连接已关闭');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果直接运行此脚本
|
||||||
|
if (require.main === module) {
|
||||||
|
importChinaRegions()
|
||||||
|
.then(() => {
|
||||||
|
console.log('省市区数据导入成功!');
|
||||||
|
process.exit(0);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('导入过程中发生错误:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { importChinaRegions };
|
||||||
14625
scripts/pca-code.json
Normal file
14625
scripts/pca-code.json
Normal file
File diff suppressed because it is too large
Load Diff
37
scripts/verify_data.js
Normal file
37
scripts/verify_data.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
const { initDB, getDB } = require('../database');
|
||||||
|
|
||||||
|
async function verifyData() {
|
||||||
|
try {
|
||||||
|
await initDB();
|
||||||
|
|
||||||
|
// 检查省份数据
|
||||||
|
const [provinces] = await getDB().query('SELECT code, name FROM china_regions WHERE level = 1 ORDER BY code LIMIT 10');
|
||||||
|
console.log('省份数据样本:');
|
||||||
|
provinces.forEach(p => console.log(` ${p.code} - ${p.name}`));
|
||||||
|
|
||||||
|
// 检查城市数据
|
||||||
|
const [cities] = await getDB().query('SELECT code, name, parent_code FROM china_regions WHERE level = 2 ORDER BY code LIMIT 10');
|
||||||
|
console.log('\n城市数据样本:');
|
||||||
|
cities.forEach(c => console.log(` ${c.code} - ${c.name} (${c.parent_code})`));
|
||||||
|
|
||||||
|
// 检查区县数据
|
||||||
|
const [districts] = await getDB().query('SELECT code, name, parent_code FROM china_regions WHERE level = 3 ORDER BY code LIMIT 10');
|
||||||
|
console.log('\n区县数据样本:');
|
||||||
|
districts.forEach(d => console.log(` ${d.code} - ${d.name} (${d.parent_code})`));
|
||||||
|
|
||||||
|
// 统计各级别数量
|
||||||
|
const [stats] = await getDB().query('SELECT level, COUNT(*) as count FROM china_regions GROUP BY level ORDER BY level');
|
||||||
|
console.log('\n各级别统计:');
|
||||||
|
stats.forEach(row => {
|
||||||
|
const levelName = row.level === 1 ? '省份' : row.level === 2 ? '城市' : '区县';
|
||||||
|
console.log(` ${levelName}(level ${row.level}): ${row.count} 个`);
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('验证失败:', error);
|
||||||
|
} finally {
|
||||||
|
process.exit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
verifyData();
|
||||||
@@ -39,6 +39,7 @@ app.use(helmet({
|
|||||||
app.use(cors({
|
app.use(cors({
|
||||||
origin: [
|
origin: [
|
||||||
'http://localhost:5173',
|
'http://localhost:5173',
|
||||||
|
'http://localhost:5176',
|
||||||
'http://localhost:5174',
|
'http://localhost:5174',
|
||||||
'http://localhost:3001',
|
'http://localhost:3001',
|
||||||
'https://www.zrbjr.com',
|
'https://www.zrbjr.com',
|
||||||
@@ -59,6 +60,9 @@ app.use((req, res, next) => {
|
|||||||
|
|
||||||
res.on('finish', () => {
|
res.on('finish', () => {
|
||||||
const duration = Date.now() - start;
|
const duration = Date.now() - start;
|
||||||
|
|
||||||
|
// 只记录非正常状态码的请求日志(过滤掉200、304等正常返回)
|
||||||
|
if (res.statusCode >= 400 || res.statusCode < 200) {
|
||||||
logger.info('HTTP Request', {
|
logger.info('HTTP Request', {
|
||||||
method: req.method,
|
method: req.method,
|
||||||
url: req.originalUrl,
|
url: req.originalUrl,
|
||||||
@@ -67,6 +71,7 @@ app.use((req, res, next) => {
|
|||||||
ip: req.ip,
|
ip: req.ip,
|
||||||
userAgent: req.get('User-Agent')
|
userAgent: req.get('User-Agent')
|
||||||
});
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
next();
|
next();
|
||||||
@@ -225,6 +230,7 @@ app.use('/api/auth', require('./routes/auth'));
|
|||||||
app.use('/api/users', require('./routes/users'));
|
app.use('/api/users', require('./routes/users'));
|
||||||
app.use('/api/user', require('./routes/users')); // 添加单数形式的路由映射
|
app.use('/api/user', require('./routes/users')); // 添加单数形式的路由映射
|
||||||
app.use('/api/products', require('./routes/products'));
|
app.use('/api/products', require('./routes/products'));
|
||||||
|
app.use('/api/specifications', require('./routes/specifications'));
|
||||||
app.use('/api/orders', require('./routes/orders'));
|
app.use('/api/orders', require('./routes/orders'));
|
||||||
app.use('/api/points', require('./routes/points'));
|
app.use('/api/points', require('./routes/points'));
|
||||||
app.use('/api/captcha', require('./routes/captcha')); // 验证码路由
|
app.use('/api/captcha', require('./routes/captcha')); // 验证码路由
|
||||||
@@ -243,6 +249,7 @@ app.use('/api/agent-withdrawals', require('./routes/agent-withdrawals'));
|
|||||||
app.use('/api/regions', require('./routes/regions'));
|
app.use('/api/regions', require('./routes/regions'));
|
||||||
app.use('/api/addresses', require('./routes/addresses'));
|
app.use('/api/addresses', require('./routes/addresses'));
|
||||||
app.use('/api/address-labels', require('./routes/address-labels'));
|
app.use('/api/address-labels', require('./routes/address-labels'));
|
||||||
|
app.use('/api/cart', require('./routes/cart'));
|
||||||
|
|
||||||
// 前端路由 - 必须在最后,作为fallback
|
// 前端路由 - 必须在最后,作为fallback
|
||||||
app.get('/', (req, res) => {
|
app.get('/', (req, res) => {
|
||||||
|
|||||||
@@ -695,6 +695,7 @@ class MatchingService {
|
|||||||
u.balance as current_balance
|
u.balance as current_balance
|
||||||
FROM users u
|
FROM users u
|
||||||
WHERE u.is_system_account = FALSE
|
WHERE u.is_system_account = FALSE
|
||||||
|
AND u.is_distribute = TRUE
|
||||||
AND u.id != ?
|
AND u.id != ?
|
||||||
AND u.balance < -100
|
AND u.balance < -100
|
||||||
AND u.audit_status = 'approved'
|
AND u.audit_status = 'approved'
|
||||||
@@ -715,8 +716,7 @@ class MatchingService {
|
|||||||
// 查询用户的分配订单金额统计
|
// 查询用户的分配订单金额统计
|
||||||
const [orderStatusResult] = await db.execute(
|
const [orderStatusResult] = await db.execute(
|
||||||
`SELECT
|
`SELECT
|
||||||
SUM(CASE WHEN status = 'pending' THEN amount ELSE 0 END) as pending_amount,
|
SUM(CASE WHEN status = 'pending' THEN amount ELSE 0 END) as pending_amount
|
||||||
SUM(CASE WHEN status = 'processing' THEN amount ELSE 0 END) as processing_amount
|
|
||||||
FROM transfers
|
FROM transfers
|
||||||
WHERE to_user_id = ?`,
|
WHERE to_user_id = ?`,
|
||||||
[user.user_id]
|
[user.user_id]
|
||||||
@@ -730,6 +730,14 @@ class MatchingService {
|
|||||||
WHERE to_user_id = ?`,
|
WHERE to_user_id = ?`,
|
||||||
[user.user_id]
|
[user.user_id]
|
||||||
);
|
);
|
||||||
|
//查询用户给其他用户已确认的金额统计(要减去,因为款项还没回来)
|
||||||
|
const [orderStatusConfirmedResultFrom] = await db.execute(
|
||||||
|
`SELECT
|
||||||
|
SUM(CASE WHEN status = 'confirmed' THEN amount ELSE 0 END) as confirmed_amount
|
||||||
|
FROM transfers
|
||||||
|
WHERE from_user_id = ?`,
|
||||||
|
[user.user_id]
|
||||||
|
);
|
||||||
// 查询用户当天在matching_orders表中打出去的款项
|
// 查询用户当天在matching_orders表中打出去的款项
|
||||||
const today = getLocalDateString();
|
const today = getLocalDateString();
|
||||||
const [todayOutflowResult] = await db.execute(
|
const [todayOutflowResult] = await db.execute(
|
||||||
@@ -741,22 +749,23 @@ class MatchingService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// 添加分配金额信息到用户对象
|
// 添加分配金额信息到用户对象
|
||||||
const orderStatus = orderStatusResult[0] || { pending_amount: 0, processing_amount: 0 };
|
const orderStatus = orderStatusResult[0] || { pending_amount: 0 };
|
||||||
const todayOutflow = todayOutflowResult[0] || { today_outflow: 0 };
|
const todayOutflow = todayOutflowResult[0] || { today_outflow: 0 };
|
||||||
|
const orderStatusConfirmedFrom = orderStatusConfirmedResultFrom[0] || { confirmed_amount: 0 };
|
||||||
const orderStatusConfirmed = orderStatusConfirmedResult[0] || { confirmed_amount: 0 };
|
const orderStatusConfirmed = orderStatusConfirmedResult[0] || { confirmed_amount: 0 };
|
||||||
user.today_outflow = parseFloat(todayOutflow.today_outflow) || 0;
|
user.today_outflow = parseFloat(todayOutflow.today_outflow) || 0;
|
||||||
user.pending_amount = parseFloat(orderStatus.pending_amount) || 0;
|
user.pending_amount = parseFloat(orderStatus.pending_amount) || 0;
|
||||||
user.processing_amount = parseFloat(orderStatus.processing_amount) || 0;
|
|
||||||
user.confirmed_amount = parseFloat(orderStatusConfirmed.confirmed_amount) || 0;
|
user.confirmed_amount = parseFloat(orderStatusConfirmed.confirmed_amount) || 0;
|
||||||
user.has_active_allocations = user.current_balance + user.pending_amount + user.processing_amount + user.confirmed_amount + user.today_outflow;
|
user.has_active_allocations = user.current_balance + user.pending_amount + user.confirmed_amount + user.today_outflow - orderStatusConfirmedFrom.confirmed_amount;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 所有查询到的用户都是负余额用户,直接添加到可用列表
|
// 所有查询到的用户都是负余额用户,直接添加到可用列表
|
||||||
}
|
}
|
||||||
|
userBalanceResult = userBalanceResult.filter(user => user.has_active_allocations < -100);
|
||||||
userBalanceResult = userBalanceResult.sort((a, b) => a.has_active_allocations - b.has_active_allocations);
|
userBalanceResult = userBalanceResult.sort((a, b) => a.has_active_allocations - b.has_active_allocations);
|
||||||
for (const user of userBalanceResult) {
|
for (const user of userBalanceResult) {
|
||||||
if (user.has_active_allocations < -100 && maxTransfers > availableUsers.length + 1) {
|
if ( maxTransfers > availableUsers.length + 1) {
|
||||||
if (minTransfers === 3 && availableUsers.length < 3) {
|
if (minTransfers === 3 && availableUsers.length < 3) {
|
||||||
availableUsers.push(user);
|
availableUsers.push(user);
|
||||||
}
|
}
|
||||||
@@ -851,7 +860,107 @@ class MatchingService {
|
|||||||
// 如果还有剩余金额且分配数量不足最小笔数,最后分配给虚拟用户
|
// 如果还有剩余金额且分配数量不足最小笔数,最后分配给虚拟用户
|
||||||
const availableVirtualUsers = virtualUsersResult
|
const availableVirtualUsers = virtualUsersResult
|
||||||
|
|
||||||
// 如果需要分配给虚拟用户,使用随机分配算法
|
// 如果有剩余金额,优先检查现有非虚拟用户是否还能消化
|
||||||
|
if (remainingAmount > 0) {
|
||||||
|
// 筛选出非虚拟用户分配记录
|
||||||
|
|
||||||
|
if (allocations.length > 0) {
|
||||||
|
let totalAvailableCapacity = 0;
|
||||||
|
const userCapacities = [];
|
||||||
|
|
||||||
|
// 计算每个用户的剩余可分配容量
|
||||||
|
for (const allocation of allocations) {
|
||||||
|
// 获取用户当前的实际余额状态(使用has_active_allocations作为实际可分配余额)
|
||||||
|
const maxSafeAmount = Math.abs(allocation.availableForAllocation);
|
||||||
|
const remainingCapacity = maxSafeAmount - allocation.amount;
|
||||||
|
|
||||||
|
if (remainingCapacity > 0) {
|
||||||
|
userCapacities.push({
|
||||||
|
allocation,
|
||||||
|
capacity: remainingCapacity
|
||||||
|
});
|
||||||
|
totalAvailableCapacity += remainingCapacity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`现有用户剩余容量: ${totalAvailableCapacity}, 待分配金额: ${remainingAmount}`);
|
||||||
|
|
||||||
|
// 如果现有用户能够消化剩余金额
|
||||||
|
if (totalAvailableCapacity >= remainingAmount && userCapacities.length > 0) {
|
||||||
|
// 按平均分配给这些用户,但需要检查每个用户的分配上限
|
||||||
|
const averageAmount = Math.floor(remainingAmount / userCapacities.length);
|
||||||
|
let distributedAmount = 0;
|
||||||
|
let remainingToDistribute = remainingAmount;
|
||||||
|
|
||||||
|
for (let i = 0; i < userCapacities.length; i++) {
|
||||||
|
const { allocation, capacity } = userCapacities[i];
|
||||||
|
|
||||||
|
// 计算本次可分配的金额
|
||||||
|
let amountToAdd = 0;
|
||||||
|
|
||||||
|
if (i === userCapacities.length - 1) {
|
||||||
|
// 最后一个用户分配剩余的所有金额,但不能超过其容量
|
||||||
|
amountToAdd = Math.min(remainingToDistribute, capacity);
|
||||||
|
} else {
|
||||||
|
// 其他用户按平均分配,但不能超过其容量
|
||||||
|
amountToAdd = Math.min(averageAmount, capacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (amountToAdd > 0) {
|
||||||
|
allocation.amount += amountToAdd;
|
||||||
|
distributedAmount += amountToAdd;
|
||||||
|
remainingToDistribute -= amountToAdd;
|
||||||
|
console.log(`为用户${allocation.userId}追加分配${amountToAdd}元,总分配${allocation.amount}元,剩余容量${capacity - amountToAdd}元`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新实际分配的剩余金额
|
||||||
|
remainingAmount = remainingToDistribute;
|
||||||
|
|
||||||
|
if (remainingAmount === 0) {
|
||||||
|
console.log('剩余金额已全部分配给现有用户');
|
||||||
|
} else {
|
||||||
|
console.log(`部分剩余金额已分配给现有用户,仍有${remainingAmount}元未分配`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果仍有剩余金额,检查是否有未分配的用户可以消化剩余金额
|
||||||
|
if (remainingAmount > 0) {
|
||||||
|
// 获取已分配的用户ID列表
|
||||||
|
const allocatedUserIds = new Set(allocations.map(a => a.userId));
|
||||||
|
|
||||||
|
// 从原始用户列表中找到未分配的用户
|
||||||
|
const unallocatedUsers = priorityUsers.filter(user => !allocatedUserIds.has(user.user_id));
|
||||||
|
|
||||||
|
if (unallocatedUsers.length > 0) {
|
||||||
|
console.log(`发现${unallocatedUsers.length}个未分配的用户,剩余金额: ${remainingAmount}`);
|
||||||
|
|
||||||
|
// 查找可分配金额大于剩余金额的用户
|
||||||
|
for (const user of unallocatedUsers) {
|
||||||
|
const maxSafeAmount = Math.abs(user.has_active_allocations);
|
||||||
|
|
||||||
|
if (maxSafeAmount >= remainingAmount) {
|
||||||
|
// 找到合适的用户,分配剩余金额
|
||||||
|
allocations.push({
|
||||||
|
userId: user.user_id,
|
||||||
|
username: user.username || `User${user.user_id}`,
|
||||||
|
amount: remainingAmount,
|
||||||
|
userType: 'priority_user',
|
||||||
|
currentBalance: user.current_balance,
|
||||||
|
availableForAllocation: user.has_active_allocations
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`为未分配用户${user.user_id}分配剩余金额${remainingAmount}元`);
|
||||||
|
remainingAmount = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果仍有剩余金额,分配给虚拟用户
|
||||||
if (remainingAmount > 0 && availableVirtualUsers.length > 0) {
|
if (remainingAmount > 0 && availableVirtualUsers.length > 0) {
|
||||||
const maxPossibleTransfers = Math.min((minTransfers - allocations.length) <= 0 ? 1 : minTransfers - allocations.length, availableVirtualUsers.length);
|
const maxPossibleTransfers = Math.min((minTransfers - allocations.length) <= 0 ? 1 : minTransfers - allocations.length, availableVirtualUsers.length);
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ const options = {
|
|||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
// API文档扫描路径
|
// API文档扫描路径
|
||||||
apis: ['./routes/*.js', './admin/routes/*.js'],
|
apis: ['./docs/schemas/*.js', './docs/apis/*.js', './routes/*.js', './admin/routes/*.js'],
|
||||||
};
|
};
|
||||||
|
|
||||||
const specs = swaggerJsdoc(options);
|
const specs = swaggerJsdoc(options);
|
||||||
|
|||||||
817
test_maoj.sql
Normal file
817
test_maoj.sql
Normal file
@@ -0,0 +1,817 @@
|
|||||||
|
/*
|
||||||
|
Navicat Premium Dump SQL
|
||||||
|
|
||||||
|
Source Server : 测试端
|
||||||
|
Source Server Type : MySQL
|
||||||
|
Source Server Version : 80036 (8.0.36)
|
||||||
|
Source Host : 114.55.111.44:3306
|
||||||
|
Source Schema : test_mao
|
||||||
|
|
||||||
|
Target Server Type : MySQL
|
||||||
|
Target Server Version : 80036 (8.0.36)
|
||||||
|
File Encoding : 65001
|
||||||
|
|
||||||
|
Date: 01/09/2025 10:09:09
|
||||||
|
*/
|
||||||
|
|
||||||
|
SET NAMES utf8mb4;
|
||||||
|
SET FOREIGN_KEY_CHECKS = 0;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for accounts
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `accounts`;
|
||||||
|
CREATE TABLE `accounts` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT,
|
||||||
|
`user_id` int NOT NULL,
|
||||||
|
`account_type` enum('public','user') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'user',
|
||||||
|
`balance` decimal(10, 2) NULL DEFAULT 0.00,
|
||||||
|
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
INDEX `user_id`(`user_id` ASC) USING BTREE,
|
||||||
|
CONSTRAINT `accounts_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
|
||||||
|
) ENGINE = InnoDB AUTO_INCREMENT = 40 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for address_labels
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `address_labels`;
|
||||||
|
CREATE TABLE `address_labels` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT,
|
||||||
|
`user_id` int NULL DEFAULT NULL,
|
||||||
|
`name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||||
|
`is_system` tinyint(1) NULL DEFAULT 0,
|
||||||
|
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`color` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '#1890ff',
|
||||||
|
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
UNIQUE INDEX `unique_user_label`(`user_id` ASC, `name` ASC) USING BTREE,
|
||||||
|
CONSTRAINT `address_labels_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
|
||||||
|
) ENGINE = InnoDB AUTO_INCREMENT = 61 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for admin_operation_logs
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `admin_operation_logs`;
|
||||||
|
CREATE TABLE `admin_operation_logs` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT,
|
||||||
|
`admin_id` int NOT NULL,
|
||||||
|
`operation_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
|
||||||
|
`target_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
|
||||||
|
`target_id` int NOT NULL,
|
||||||
|
`description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL,
|
||||||
|
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
INDEX `admin_id`(`admin_id` ASC) USING BTREE,
|
||||||
|
CONSTRAINT `admin_operation_logs_ibfk_1` FOREIGN KEY (`admin_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
|
||||||
|
) ENGINE = InnoDB AUTO_INCREMENT = 44 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for agent_commission_records
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `agent_commission_records`;
|
||||||
|
CREATE TABLE `agent_commission_records` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT,
|
||||||
|
`agent_id` int NOT NULL,
|
||||||
|
`merchant_id` int NOT NULL,
|
||||||
|
`order_id` int NULL DEFAULT NULL,
|
||||||
|
`commission_amount` decimal(10, 2) NOT NULL,
|
||||||
|
`commission_type` enum('registration','matching') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'matching',
|
||||||
|
`description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL,
|
||||||
|
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
INDEX `agent_id`(`agent_id` ASC) USING BTREE,
|
||||||
|
INDEX `merchant_id`(`merchant_id` ASC) USING BTREE,
|
||||||
|
INDEX `order_id`(`order_id` ASC) USING BTREE,
|
||||||
|
CONSTRAINT `agent_commission_records_ibfk_1` FOREIGN KEY (`agent_id`) REFERENCES `regional_agents` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT,
|
||||||
|
CONSTRAINT `agent_commission_records_ibfk_2` FOREIGN KEY (`merchant_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT,
|
||||||
|
CONSTRAINT `agent_commission_records_ibfk_3` FOREIGN KEY (`order_id`) REFERENCES `matching_orders` (`id`) ON DELETE SET NULL ON UPDATE RESTRICT
|
||||||
|
) ENGINE = InnoDB AUTO_INCREMENT = 17 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for agent_merchants
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `agent_merchants`;
|
||||||
|
CREATE TABLE `agent_merchants` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT,
|
||||||
|
`agent_id` int NOT NULL,
|
||||||
|
`merchant_id` int NOT NULL,
|
||||||
|
`registration_code_id` int NULL DEFAULT NULL,
|
||||||
|
`matching_count` int NULL DEFAULT 0,
|
||||||
|
`commission_earned` decimal(10, 2) NULL DEFAULT 0.00,
|
||||||
|
`is_qualified` tinyint(1) NULL DEFAULT 0,
|
||||||
|
`qualified_at` timestamp NULL DEFAULT NULL,
|
||||||
|
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
UNIQUE INDEX `unique_agent_merchant`(`agent_id` ASC, `merchant_id` ASC) USING BTREE,
|
||||||
|
INDEX `merchant_id`(`merchant_id` ASC) USING BTREE,
|
||||||
|
INDEX `registration_code_id`(`registration_code_id` ASC) USING BTREE,
|
||||||
|
CONSTRAINT `agent_merchants_ibfk_1` FOREIGN KEY (`agent_id`) REFERENCES `regional_agents` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT,
|
||||||
|
CONSTRAINT `agent_merchants_ibfk_2` FOREIGN KEY (`merchant_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT,
|
||||||
|
CONSTRAINT `agent_merchants_ibfk_3` FOREIGN KEY (`registration_code_id`) REFERENCES `registration_codes` (`id`) ON DELETE SET NULL ON UPDATE RESTRICT
|
||||||
|
) ENGINE = InnoDB AUTO_INCREMENT = 32 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for agent_withdrawals
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `agent_withdrawals`;
|
||||||
|
CREATE TABLE `agent_withdrawals` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT,
|
||||||
|
`agent_id` int NOT NULL,
|
||||||
|
`amount` decimal(10, 2) NOT NULL,
|
||||||
|
`payment_type` enum('bank','wechat','alipay','unionpay') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'bank' COMMENT '收款方式类型',
|
||||||
|
`bank_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '银行名称',
|
||||||
|
`account_number` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账号/银行账号',
|
||||||
|
`account_holder` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '持有人姓名',
|
||||||
|
`qr_code_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '收款码图片URL',
|
||||||
|
`status` enum('pending','approved','rejected','completed') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'pending',
|
||||||
|
`apply_note` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL,
|
||||||
|
`admin_note` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL,
|
||||||
|
`processed_by` int NULL DEFAULT NULL,
|
||||||
|
`processed_at` timestamp NULL DEFAULT NULL,
|
||||||
|
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
`bank_account` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '银行账号(兼容旧版本)',
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
INDEX `agent_id`(`agent_id` ASC) USING BTREE,
|
||||||
|
INDEX `processed_by`(`processed_by` ASC) USING BTREE,
|
||||||
|
CONSTRAINT `agent_withdrawals_ibfk_1` FOREIGN KEY (`agent_id`) REFERENCES `regional_agents` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT,
|
||||||
|
CONSTRAINT `agent_withdrawals_ibfk_2` FOREIGN KEY (`processed_by`) REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE RESTRICT
|
||||||
|
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for articles
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `articles`;
|
||||||
|
CREATE TABLE `articles` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT,
|
||||||
|
`title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
|
||||||
|
`content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL,
|
||||||
|
`author_id` int NULL DEFAULT NULL,
|
||||||
|
`status` enum('draft','published') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'draft',
|
||||||
|
`views` int NULL DEFAULT 0,
|
||||||
|
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
INDEX `author_id`(`author_id` ASC) USING BTREE,
|
||||||
|
CONSTRAINT `articles_ibfk_1` FOREIGN KEY (`author_id`) REFERENCES `users` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
|
||||||
|
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for balance_fix_log
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `balance_fix_log`;
|
||||||
|
CREATE TABLE `balance_fix_log` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT,
|
||||||
|
`user_id` int NOT NULL,
|
||||||
|
`amount_deducted` decimal(10, 2) NOT NULL,
|
||||||
|
`transfer_count` int NOT NULL,
|
||||||
|
`fix_reason` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL,
|
||||||
|
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
INDEX `idx_user_id`(`user_id` ASC) USING BTREE,
|
||||||
|
INDEX `idx_created_at`(`created_at` ASC) USING BTREE
|
||||||
|
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = DYNAMIC;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for cart_items
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `cart_items`;
|
||||||
|
CREATE TABLE `cart_items` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT,
|
||||||
|
`user_id` int NOT NULL,
|
||||||
|
`product_id` int NOT NULL,
|
||||||
|
`quantity` int NOT NULL DEFAULT 1,
|
||||||
|
`specification_id` int NULL DEFAULT NULL,
|
||||||
|
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
UNIQUE INDEX `unique_user_product_spec`(`user_id` ASC, `product_id` ASC, `specification_id` ASC) USING BTREE,
|
||||||
|
INDEX `product_id`(`product_id` ASC) USING BTREE,
|
||||||
|
INDEX `specification_id`(`specification_id` ASC) USING BTREE
|
||||||
|
) ENGINE = InnoDB AUTO_INCREMENT = 8 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for china_regions
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `china_regions`;
|
||||||
|
CREATE TABLE `china_regions` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT,
|
||||||
|
`code` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||||
|
`name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||||
|
`parent_code` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
|
||||||
|
`level` tinyint NOT NULL COMMENT '1:省 2:市 3:区',
|
||||||
|
`sort_order` int NULL DEFAULT 0,
|
||||||
|
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
UNIQUE INDEX `code`(`code` ASC) USING BTREE,
|
||||||
|
INDEX `idx_parent_code`(`parent_code` ASC) USING BTREE,
|
||||||
|
INDEX `idx_level`(`level` ASC) USING BTREE
|
||||||
|
) ENGINE = InnoDB AUTO_INCREMENT = 4621 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for matching_orders
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `matching_orders`;
|
||||||
|
CREATE TABLE `matching_orders` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT,
|
||||||
|
`initiator_id` int NOT NULL,
|
||||||
|
`amount` decimal(10, 2) NOT NULL,
|
||||||
|
`status` enum('pending','matching','completed','cancelled','failed') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'pending',
|
||||||
|
`cycle_count` int NULL DEFAULT 0,
|
||||||
|
`max_cycles` int NULL DEFAULT 3,
|
||||||
|
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
`matching_type` enum('small','large') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'small',
|
||||||
|
`is_system_reverse` tinyint(1) NULL DEFAULT 0,
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
INDEX `initiator_id`(`initiator_id` ASC) USING BTREE,
|
||||||
|
CONSTRAINT `matching_orders_ibfk_1` FOREIGN KEY (`initiator_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
|
||||||
|
) ENGINE = InnoDB AUTO_INCREMENT = 441 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for matching_records
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `matching_records`;
|
||||||
|
CREATE TABLE `matching_records` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT,
|
||||||
|
`matching_order_id` int NOT NULL,
|
||||||
|
`user_id` int NOT NULL,
|
||||||
|
`action` enum('join','confirm','reject','complete') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
|
||||||
|
`amount` decimal(10, 2) NULL DEFAULT NULL,
|
||||||
|
`note` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL,
|
||||||
|
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
INDEX `matching_order_id`(`matching_order_id` ASC) USING BTREE,
|
||||||
|
INDEX `user_id`(`user_id` ASC) USING BTREE,
|
||||||
|
CONSTRAINT `matching_records_ibfk_1` FOREIGN KEY (`matching_order_id`) REFERENCES `matching_orders` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT,
|
||||||
|
CONSTRAINT `matching_records_ibfk_2` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
|
||||||
|
) ENGINE = InnoDB AUTO_INCREMENT = 1841 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for order_allocations
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `order_allocations`;
|
||||||
|
CREATE TABLE `order_allocations` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT,
|
||||||
|
`matching_order_id` int NOT NULL,
|
||||||
|
`from_user_id` int NOT NULL,
|
||||||
|
`to_user_id` int NOT NULL,
|
||||||
|
`amount` decimal(10, 2) NOT NULL,
|
||||||
|
`cycle_number` int NOT NULL,
|
||||||
|
`status` enum('pending','confirmed','rejected','completed') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'pending',
|
||||||
|
`transfer_id` int NULL DEFAULT NULL,
|
||||||
|
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
`confirmed_at` timestamp NULL DEFAULT NULL,
|
||||||
|
`outbound_date` date NULL DEFAULT NULL,
|
||||||
|
`return_date` date NULL DEFAULT NULL,
|
||||||
|
`can_return_after` timestamp NULL DEFAULT NULL,
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
INDEX `matching_order_id`(`matching_order_id` ASC) USING BTREE,
|
||||||
|
INDEX `from_user_id`(`from_user_id` ASC) USING BTREE,
|
||||||
|
INDEX `to_user_id`(`to_user_id` ASC) USING BTREE,
|
||||||
|
INDEX `transfer_id`(`transfer_id` ASC) USING BTREE,
|
||||||
|
CONSTRAINT `order_allocations_ibfk_1` FOREIGN KEY (`matching_order_id`) REFERENCES `matching_orders` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT,
|
||||||
|
CONSTRAINT `order_allocations_ibfk_2` FOREIGN KEY (`from_user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT,
|
||||||
|
CONSTRAINT `order_allocations_ibfk_3` FOREIGN KEY (`to_user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT,
|
||||||
|
CONSTRAINT `order_allocations_ibfk_4` FOREIGN KEY (`transfer_id`) REFERENCES `transfers` (`id`) ON DELETE SET NULL ON UPDATE RESTRICT
|
||||||
|
) ENGINE = InnoDB AUTO_INCREMENT = 1078 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for order_allocations_backup
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `order_allocations_backup`;
|
||||||
|
CREATE TABLE `order_allocations_backup` (
|
||||||
|
`id` int NOT NULL DEFAULT 0,
|
||||||
|
`matching_order_id` int NOT NULL,
|
||||||
|
`from_user_id` int NOT NULL,
|
||||||
|
`to_user_id` int NOT NULL,
|
||||||
|
`amount` decimal(10, 2) NOT NULL,
|
||||||
|
`cycle_number` int NOT NULL,
|
||||||
|
`status` enum('pending','confirmed','rejected','completed') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'pending',
|
||||||
|
`transfer_id` int NULL DEFAULT NULL,
|
||||||
|
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
`confirmed_at` timestamp NULL DEFAULT NULL,
|
||||||
|
`outbound_date` date NULL DEFAULT NULL,
|
||||||
|
`return_date` date NULL DEFAULT NULL,
|
||||||
|
`can_return_after` timestamp NULL DEFAULT NULL
|
||||||
|
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for order_items
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `order_items`;
|
||||||
|
CREATE TABLE `order_items` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT,
|
||||||
|
`order_id` int NOT NULL,
|
||||||
|
`product_id` int NOT NULL,
|
||||||
|
`spec_combination_id` int NULL DEFAULT NULL COMMENT '规格组合ID',
|
||||||
|
`quantity` int NOT NULL,
|
||||||
|
`price` int NOT NULL,
|
||||||
|
`points` int NOT NULL,
|
||||||
|
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`points_price` int NOT NULL DEFAULT 0,
|
||||||
|
`rongdou_price` int NOT NULL DEFAULT 0,
|
||||||
|
`rongdou` int NULL DEFAULT 0,
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
INDEX `order_id`(`order_id` ASC) USING BTREE,
|
||||||
|
INDEX `product_id`(`product_id` ASC) USING BTREE,
|
||||||
|
CONSTRAINT `order_items_ibfk_1` FOREIGN KEY (`order_id`) REFERENCES `orders` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
|
||||||
|
CONSTRAINT `order_items_ibfk_2` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
|
||||||
|
) ENGINE = InnoDB AUTO_INCREMENT = 8 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for orders
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `orders`;
|
||||||
|
CREATE TABLE `orders` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT,
|
||||||
|
`user_id` int NOT NULL,
|
||||||
|
`order_no` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
|
||||||
|
`total_amount` int NOT NULL,
|
||||||
|
`total_points` int NOT NULL,
|
||||||
|
`total_rongdou` int NOT NULL DEFAULT 0,
|
||||||
|
`status` enum('pending','paid','shipped','delivered','cancelled','pre_order','completed') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'pending',
|
||||||
|
`address` json NULL,
|
||||||
|
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
UNIQUE INDEX `order_no`(`order_no` ASC) USING BTREE,
|
||||||
|
INDEX `user_id`(`user_id` ASC) USING BTREE,
|
||||||
|
CONSTRAINT `orders_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
|
||||||
|
) ENGINE = InnoDB AUTO_INCREMENT = 8 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for points_history
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `points_history`;
|
||||||
|
CREATE TABLE `points_history` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT,
|
||||||
|
`user_id` int NOT NULL,
|
||||||
|
`type` enum('earn','spend','admin_adjust','refund') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
|
||||||
|
`amount` int NOT NULL,
|
||||||
|
`description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
|
||||||
|
`order_id` int NULL DEFAULT NULL,
|
||||||
|
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
INDEX `user_id`(`user_id` ASC) USING BTREE,
|
||||||
|
INDEX `order_id`(`order_id` ASC) USING BTREE,
|
||||||
|
CONSTRAINT `points_history_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
|
||||||
|
CONSTRAINT `points_history_ibfk_2` FOREIGN KEY (`order_id`) REFERENCES `orders` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
|
||||||
|
) ENGINE = InnoDB AUTO_INCREMENT = 1273 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for product_attributes
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `product_attributes`;
|
||||||
|
CREATE TABLE `product_attributes` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT,
|
||||||
|
`product_id` int NOT NULL,
|
||||||
|
`attribute_key` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||||
|
`attribute_value` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||||
|
`sort_order` int NULL DEFAULT 0,
|
||||||
|
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
INDEX `product_id`(`product_id` ASC) USING BTREE,
|
||||||
|
CONSTRAINT `product_attributes_ibfk_1` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
|
||||||
|
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for product_favorites
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `product_favorites`;
|
||||||
|
CREATE TABLE `product_favorites` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT,
|
||||||
|
`user_id` int NOT NULL,
|
||||||
|
`product_id` int NOT NULL,
|
||||||
|
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
UNIQUE INDEX `unique_user_product`(`user_id` ASC, `product_id` ASC) USING BTREE,
|
||||||
|
INDEX `product_id`(`product_id` ASC) USING BTREE,
|
||||||
|
CONSTRAINT `product_favorites_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT,
|
||||||
|
CONSTRAINT `product_favorites_ibfk_2` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
|
||||||
|
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for product_reviews
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `product_reviews`;
|
||||||
|
CREATE TABLE `product_reviews` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT,
|
||||||
|
`product_id` int NOT NULL,
|
||||||
|
`user_id` int NOT NULL,
|
||||||
|
`order_id` int NOT NULL,
|
||||||
|
`rating` int NOT NULL,
|
||||||
|
`comment` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL,
|
||||||
|
`images` json NULL,
|
||||||
|
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
INDEX `product_id`(`product_id` ASC) USING BTREE,
|
||||||
|
INDEX `user_id`(`user_id` ASC) USING BTREE,
|
||||||
|
INDEX `order_id`(`order_id` ASC) USING BTREE,
|
||||||
|
CONSTRAINT `product_reviews_ibfk_1` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
|
||||||
|
CONSTRAINT `product_reviews_ibfk_2` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
|
||||||
|
CONSTRAINT `product_reviews_ibfk_3` FOREIGN KEY (`order_id`) REFERENCES `orders` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
|
||||||
|
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for product_spec_combinations
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `product_spec_combinations`;
|
||||||
|
CREATE TABLE `product_spec_combinations` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT,
|
||||||
|
`product_id` int NOT NULL COMMENT '商品ID',
|
||||||
|
`combination_key` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '组合键,如:color_1-size_2-material_3',
|
||||||
|
`spec_values` json NOT NULL COMMENT '规格值组合,存储spec_value_id数组',
|
||||||
|
`price_adjustment` int NULL DEFAULT 0 COMMENT '价格调整',
|
||||||
|
`points_adjustment` int NULL DEFAULT 0 COMMENT '积分调整',
|
||||||
|
`rongdou_adjustment` int NULL DEFAULT 0 COMMENT '融豆调整',
|
||||||
|
`stock` int NULL DEFAULT 0 COMMENT '库存',
|
||||||
|
`sku_code` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'SKU编码',
|
||||||
|
`barcode` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '条形码',
|
||||||
|
`weight` decimal(8, 3) NULL DEFAULT NULL COMMENT '重量(kg)',
|
||||||
|
`volume` decimal(10, 3) NULL DEFAULT NULL COMMENT '体积(cm³)',
|
||||||
|
`status` enum('active','inactive') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'active',
|
||||||
|
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
UNIQUE INDEX `unique_product_combination`(`product_id` ASC, `combination_key` ASC) USING BTREE,
|
||||||
|
INDEX `idx_product_status`(`product_id` ASC, `status` ASC) USING BTREE,
|
||||||
|
INDEX `idx_sku_code`(`sku_code` ASC) USING BTREE,
|
||||||
|
CONSTRAINT `product_spec_combinations_ibfk_1` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
|
||||||
|
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for product_spec_names
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `product_spec_names`;
|
||||||
|
CREATE TABLE `product_spec_names` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT,
|
||||||
|
`product_id` int NOT NULL COMMENT '商品ID',
|
||||||
|
`spec_name_id` int NOT NULL COMMENT '规格名称ID',
|
||||||
|
`is_required` tinyint(1) NULL DEFAULT 1 COMMENT '是否必选规格',
|
||||||
|
`sort_order` int NULL DEFAULT 0 COMMENT '排序',
|
||||||
|
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
UNIQUE INDEX `unique_product_spec_name`(`product_id` ASC, `spec_name_id` ASC) USING BTREE,
|
||||||
|
INDEX `spec_name_id`(`spec_name_id` ASC) USING BTREE,
|
||||||
|
CONSTRAINT `product_spec_names_ibfk_1` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT,
|
||||||
|
CONSTRAINT `product_spec_names_ibfk_2` FOREIGN KEY (`spec_name_id`) REFERENCES `spec_names` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
|
||||||
|
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for product_specifications
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `product_specifications`;
|
||||||
|
CREATE TABLE `product_specifications` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT,
|
||||||
|
`product_id` int NOT NULL,
|
||||||
|
`spec_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||||
|
`spec_value` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||||
|
`price_adjustment` int NULL DEFAULT 0,
|
||||||
|
`points_adjustment` int NULL DEFAULT 0,
|
||||||
|
`rongdou_adjustment` int NULL DEFAULT 0,
|
||||||
|
`stock` int NULL DEFAULT 0,
|
||||||
|
`sku_code` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
|
||||||
|
`status` enum('active','inactive') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'active',
|
||||||
|
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
INDEX `product_id`(`product_id` ASC) USING BTREE,
|
||||||
|
CONSTRAINT `product_specifications_ibfk_1` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
|
||||||
|
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for products
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `products`;
|
||||||
|
CREATE TABLE `products` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT,
|
||||||
|
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
|
||||||
|
`description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL,
|
||||||
|
`price` int NOT NULL,
|
||||||
|
`original_price` int NULL DEFAULT NULL,
|
||||||
|
`stock` int NULL DEFAULT 0,
|
||||||
|
`sales` int NULL DEFAULT 0,
|
||||||
|
`rating` decimal(3, 2) NULL DEFAULT 5.00,
|
||||||
|
`category` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
|
||||||
|
`images` json NULL,
|
||||||
|
`status` enum('active','inactive') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'active',
|
||||||
|
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
`points_price` int NOT NULL DEFAULT 0,
|
||||||
|
`image_url` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
|
||||||
|
`details` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL,
|
||||||
|
`rongdou_price` int NOT NULL DEFAULT 0,
|
||||||
|
`videos` json NULL,
|
||||||
|
`shop_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
|
||||||
|
`shop_avatar` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
|
||||||
|
`payment_methods` json NULL,
|
||||||
|
PRIMARY KEY (`id`) USING BTREE
|
||||||
|
) ENGINE = InnoDB AUTO_INCREMENT = 18 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for regional_agents
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `regional_agents`;
|
||||||
|
CREATE TABLE `regional_agents` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT,
|
||||||
|
`user_id` int NOT NULL,
|
||||||
|
`region_id` int NOT NULL,
|
||||||
|
`agent_code` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||||
|
`status` enum('pending','active','suspended','terminated') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'pending',
|
||||||
|
`commission_rate` decimal(5, 4) NULL DEFAULT 0.0500,
|
||||||
|
`total_earnings` decimal(10, 2) NULL DEFAULT 0.00,
|
||||||
|
`recruited_merchants` int NULL DEFAULT 0,
|
||||||
|
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
`approved_at` timestamp NULL DEFAULT NULL,
|
||||||
|
`approved_by_admin_id` int NULL DEFAULT NULL,
|
||||||
|
`withdrawn_amount` decimal(10, 2) NULL DEFAULT 0.00 COMMENT '已提现金额',
|
||||||
|
`pending_withdrawal` decimal(10, 2) NULL DEFAULT 0.00 COMMENT '待审核提现金额',
|
||||||
|
`payment_type` enum('bank','wechat','alipay','unionpay') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'bank' COMMENT '收款方式类型',
|
||||||
|
`account_number` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账号/银行账号',
|
||||||
|
`account_holder` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '持有人姓名',
|
||||||
|
`qr_code_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '收款码图片URL',
|
||||||
|
`bank_account` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '银行账号(兼容旧版本)',
|
||||||
|
`bank_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '银行名称',
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
UNIQUE INDEX `agent_code`(`agent_code` ASC) USING BTREE,
|
||||||
|
UNIQUE INDEX `unique_agent_region`(`user_id` ASC, `region_id` ASC) USING BTREE,
|
||||||
|
INDEX `region_id`(`region_id` ASC) USING BTREE,
|
||||||
|
CONSTRAINT `regional_agents_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT,
|
||||||
|
CONSTRAINT `regional_agents_ibfk_2` FOREIGN KEY (`region_id`) REFERENCES `zhejiang_regions` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
|
||||||
|
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for registration_codes
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `registration_codes`;
|
||||||
|
CREATE TABLE `registration_codes` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT,
|
||||||
|
`code` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '注册码',
|
||||||
|
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
`expires_at` timestamp NOT NULL COMMENT '过期时间',
|
||||||
|
`used_at` timestamp NULL DEFAULT NULL COMMENT '使用时间',
|
||||||
|
`used_by_user_id` int NULL DEFAULT NULL COMMENT '使用该注册码的用户ID',
|
||||||
|
`is_used` tinyint(1) NULL DEFAULT 0 COMMENT '是否已使用',
|
||||||
|
`created_by_admin_id` int NOT NULL COMMENT '创建该注册码的管理员ID',
|
||||||
|
`agent_id` int NULL DEFAULT NULL,
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
UNIQUE INDEX `code`(`code` ASC) USING BTREE,
|
||||||
|
INDEX `idx_code`(`code` ASC) USING BTREE,
|
||||||
|
INDEX `idx_expires_at`(`expires_at` ASC) USING BTREE,
|
||||||
|
INDEX `idx_is_used`(`is_used` ASC) USING BTREE,
|
||||||
|
INDEX `used_by_user_id`(`used_by_user_id` ASC) USING BTREE,
|
||||||
|
INDEX `created_by_admin_id`(`created_by_admin_id` ASC) USING BTREE,
|
||||||
|
INDEX `fk_registration_codes_agent_id`(`agent_id` ASC) USING BTREE,
|
||||||
|
CONSTRAINT `fk_registration_codes_agent_id` FOREIGN KEY (`agent_id`) REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE RESTRICT,
|
||||||
|
CONSTRAINT `registration_codes_ibfk_1` FOREIGN KEY (`used_by_user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE RESTRICT,
|
||||||
|
CONSTRAINT `registration_codes_ibfk_2` FOREIGN KEY (`created_by_admin_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
|
||||||
|
) ENGINE = InnoDB AUTO_INCREMENT = 150 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '注册码表' ROW_FORMAT = DYNAMIC;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for rongdou_history
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `rongdou_history`;
|
||||||
|
CREATE TABLE `rongdou_history` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT,
|
||||||
|
`user_id` int NOT NULL,
|
||||||
|
`type` enum('earn','spend') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||||
|
`amount` int NOT NULL,
|
||||||
|
`description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
|
||||||
|
`order_id` int NULL DEFAULT NULL,
|
||||||
|
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
INDEX `user_id`(`user_id` ASC) USING BTREE,
|
||||||
|
INDEX `order_id`(`order_id` ASC) USING BTREE,
|
||||||
|
CONSTRAINT `rongdou_history_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
|
||||||
|
CONSTRAINT `rongdou_history_ibfk_2` FOREIGN KEY (`order_id`) REFERENCES `orders` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
|
||||||
|
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for spec_names
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `spec_names`;
|
||||||
|
CREATE TABLE `spec_names` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT,
|
||||||
|
`name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '规格名称,如:颜色、尺寸、材质',
|
||||||
|
`display_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '显示名称',
|
||||||
|
`sort_order` int NULL DEFAULT 0 COMMENT '排序',
|
||||||
|
`status` enum('active','inactive') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'active',
|
||||||
|
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
UNIQUE INDEX `unique_name`(`name` ASC) USING BTREE
|
||||||
|
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for spec_values
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `spec_values`;
|
||||||
|
CREATE TABLE `spec_values` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT,
|
||||||
|
`spec_name_id` int NOT NULL COMMENT '规格名称ID',
|
||||||
|
`value` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '规格值,如:红色、XL、棉质',
|
||||||
|
`display_value` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '显示值',
|
||||||
|
`color_code` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '颜色代码(仅颜色规格使用)',
|
||||||
|
`image_url` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '规格图片',
|
||||||
|
`sort_order` int NULL DEFAULT 0 COMMENT '排序',
|
||||||
|
`status` enum('active','inactive') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'active',
|
||||||
|
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
UNIQUE INDEX `unique_spec_value`(`spec_name_id` ASC, `value` ASC) USING BTREE,
|
||||||
|
CONSTRAINT `spec_values_ibfk_1` FOREIGN KEY (`spec_name_id`) REFERENCES `spec_names` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
|
||||||
|
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for system_settings
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `system_settings`;
|
||||||
|
CREATE TABLE `system_settings` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT,
|
||||||
|
`setting_key` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
|
||||||
|
`setting_value` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL,
|
||||||
|
`description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
|
||||||
|
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
UNIQUE INDEX `setting_key`(`setting_key` ASC) USING BTREE
|
||||||
|
) ENGINE = InnoDB AUTO_INCREMENT = 77 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for test_users
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `test_users`;
|
||||||
|
CREATE TABLE `test_users` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT,
|
||||||
|
`username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||||
|
`email` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||||
|
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` timestamp NULL DEFAULT NULL,
|
||||||
|
PRIMARY KEY (`id`) USING BTREE
|
||||||
|
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for transfer_confirmations
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `transfer_confirmations`;
|
||||||
|
CREATE TABLE `transfer_confirmations` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT,
|
||||||
|
`transfer_id` int NOT NULL,
|
||||||
|
`confirmer_id` int NOT NULL,
|
||||||
|
`action` enum('confirm','reject') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
|
||||||
|
`note` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL,
|
||||||
|
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
INDEX `transfer_id`(`transfer_id` ASC) USING BTREE,
|
||||||
|
INDEX `confirmer_id`(`confirmer_id` ASC) USING BTREE,
|
||||||
|
CONSTRAINT `transfer_confirmations_ibfk_1` FOREIGN KEY (`transfer_id`) REFERENCES `transfers` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT,
|
||||||
|
CONSTRAINT `transfer_confirmations_ibfk_2` FOREIGN KEY (`confirmer_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
|
||||||
|
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for transfers
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `transfers`;
|
||||||
|
CREATE TABLE `transfers` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT,
|
||||||
|
`from_user_id` int NULL DEFAULT NULL,
|
||||||
|
`to_user_id` int NOT NULL,
|
||||||
|
`amount` decimal(10, 2) NOT NULL,
|
||||||
|
`transfer_type` enum('initial','return','user_to_user','system_to_user','user_to_system') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'user_to_user',
|
||||||
|
`status` enum('pending','confirmed','rejected','received','not_received','cancelled') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'pending',
|
||||||
|
`voucher_url` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
|
||||||
|
`description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL,
|
||||||
|
`batch_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
|
||||||
|
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
`deadline_at` timestamp NULL DEFAULT NULL COMMENT '转账截止时间',
|
||||||
|
`is_overdue` tinyint(1) NULL DEFAULT 0 COMMENT '是否超时',
|
||||||
|
`overdue_at` timestamp NULL DEFAULT NULL COMMENT '超时时间',
|
||||||
|
`is_bad_debt` tinyint(1) NULL DEFAULT 0,
|
||||||
|
`confirmed_at` timestamp NULL DEFAULT NULL,
|
||||||
|
`admin_note` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL,
|
||||||
|
`admin_modified_at` timestamp NULL DEFAULT NULL,
|
||||||
|
`admin_modified_by` int NULL DEFAULT NULL,
|
||||||
|
`source_type` enum('manual','allocation','system') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'manual' COMMENT '转账来源类型',
|
||||||
|
`matching_order_id` int NULL DEFAULT NULL,
|
||||||
|
`cycle_number` int NULL DEFAULT NULL,
|
||||||
|
`outbound_date` date NULL DEFAULT NULL,
|
||||||
|
`return_date` date NULL DEFAULT NULL,
|
||||||
|
`can_return_after` timestamp NULL DEFAULT NULL,
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
INDEX `from_user_id`(`from_user_id` ASC) USING BTREE,
|
||||||
|
INDEX `to_user_id`(`to_user_id` ASC) USING BTREE,
|
||||||
|
CONSTRAINT `transfers_ibfk_1` FOREIGN KEY (`from_user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT,
|
||||||
|
CONSTRAINT `transfers_ibfk_2` FOREIGN KEY (`to_user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
|
||||||
|
) ENGINE = InnoDB AUTO_INCREMENT = 1529 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for user_addresses
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `user_addresses`;
|
||||||
|
CREATE TABLE `user_addresses` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT,
|
||||||
|
`user_id` int NOT NULL,
|
||||||
|
`receiver_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||||
|
`receiver_phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||||
|
`province` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||||
|
`city` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||||
|
`district` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||||
|
`detailed_address` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||||
|
`postal_code` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
|
||||||
|
`label` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '家',
|
||||||
|
`is_default` tinyint(1) NULL DEFAULT 0,
|
||||||
|
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
INDEX `user_id`(`user_id` ASC) USING BTREE,
|
||||||
|
CONSTRAINT `user_addresses_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
|
||||||
|
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for user_matching_pool
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `user_matching_pool`;
|
||||||
|
CREATE TABLE `user_matching_pool` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT,
|
||||||
|
`user_id` int NOT NULL,
|
||||||
|
`available_amount` decimal(10, 2) NULL DEFAULT 0.00,
|
||||||
|
`is_active` tinyint(1) NULL DEFAULT 1,
|
||||||
|
`last_matched_at` timestamp NULL DEFAULT NULL,
|
||||||
|
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
UNIQUE INDEX `unique_user`(`user_id` ASC) USING BTREE,
|
||||||
|
CONSTRAINT `user_matching_pool_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
|
||||||
|
) ENGINE = InnoDB AUTO_INCREMENT = 61 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for users
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `users`;
|
||||||
|
CREATE TABLE `users` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT,
|
||||||
|
`username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
|
||||||
|
`email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
|
||||||
|
`password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
|
||||||
|
`role` enum('user','admin') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'user',
|
||||||
|
`avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
|
||||||
|
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
`points` int NULL DEFAULT 0,
|
||||||
|
`real_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
|
||||||
|
`id_card` varchar(18) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
|
||||||
|
`wechat_qr` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
|
||||||
|
`alipay_qr` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
|
||||||
|
`bank_card` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
|
||||||
|
`unionpay_qr` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
|
||||||
|
`phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
|
||||||
|
`is_system_account` tinyint(1) NULL DEFAULT 0,
|
||||||
|
`completed_withdrawals` int NULL DEFAULT 0,
|
||||||
|
`balance` decimal(10, 2) NULL DEFAULT 0.00,
|
||||||
|
`is_risk_user` tinyint(1) NULL DEFAULT 0 COMMENT '是否为风险用户',
|
||||||
|
`is_blacklisted` tinyint(1) NULL DEFAULT 0 COMMENT '是否被拉黑',
|
||||||
|
`risk_reason` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '风险原因',
|
||||||
|
`blacklist_reason` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '拉黑原因',
|
||||||
|
`blacklisted_at` timestamp NULL DEFAULT NULL COMMENT '拉黑时间',
|
||||||
|
`business_license` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
|
||||||
|
`id_card_front` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
|
||||||
|
`id_card_back` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
|
||||||
|
`audit_status` enum('pending','approved','rejected') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'pending',
|
||||||
|
`audit_note` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL,
|
||||||
|
`audited_by` int NULL DEFAULT NULL,
|
||||||
|
`audited_at` timestamp NULL DEFAULT NULL,
|
||||||
|
`city` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
|
||||||
|
`district_id` int NULL DEFAULT NULL,
|
||||||
|
`isdistribute` tinyint NULL DEFAULT 1,
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
UNIQUE INDEX `username`(`username` ASC) USING BTREE,
|
||||||
|
UNIQUE INDEX `email`(`email` ASC) USING BTREE,
|
||||||
|
UNIQUE INDEX `phone`(`phone` ASC) USING BTREE
|
||||||
|
) ENGINE = InnoDB AUTO_INCREMENT = 9788 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for zhejiang_regions
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `zhejiang_regions`;
|
||||||
|
CREATE TABLE `zhejiang_regions` (
|
||||||
|
`id` int NOT NULL AUTO_INCREMENT,
|
||||||
|
`city_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||||
|
`district_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||||
|
`region_code` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||||
|
`is_available` tinyint(1) NULL DEFAULT 1,
|
||||||
|
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
UNIQUE INDEX `region_code`(`region_code` ASC) USING BTREE,
|
||||||
|
UNIQUE INDEX `unique_region`(`city_name` ASC, `district_name` ASC) USING BTREE
|
||||||
|
) ENGINE = InnoDB AUTO_INCREMENT = 23234 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
|
||||||
|
|
||||||
|
SET FOREIGN_KEY_CHECKS = 1;
|
||||||
Reference in New Issue
Block a user