升级商城逻辑

This commit is contained in:
2025-09-02 09:29:20 +08:00
parent 16bfc525c2
commit 49eed40ad0
30 changed files with 22710 additions and 1339 deletions

3
.idea/vcs.xml generated
View File

@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/frontend" vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$/admin" vcs="Git" />
</component>
</project>

16
.vscode/launch.json vendored Normal file
View 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

View File

@@ -53,6 +53,7 @@ async function createTables() {
role ENUM('user', 'admin') DEFAULT 'user',
avatar VARCHAR(255),
points INT DEFAULT 0,
rongdou INT DEFAULT 0,
balance DECIMAL(10,2) DEFAULT 0.00,
real_name VARCHAR(100),
id_card VARCHAR(18),
@@ -111,7 +112,8 @@ async function createTables() {
order_no VARCHAR(50) UNIQUE NOT NULL,
total_amount 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,
created_at TIMESTAMP DEFAULT 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,
order_id INT NOT NULL,
product_id INT NOT NULL,
spec_combination_id INT NULL COMMENT '规格组合ID',
quantity INT NOT NULL,
price 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,
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(`
CREATE TABLE IF NOT EXISTS admin_operation_logs (
@@ -213,22 +236,75 @@ async function createTables() {
)
`);
// 商品规格表
// 规格名称表(规格维度)
await getDB().execute(`
CREATE TABLE IF NOT EXISTS product_specifications (
CREATE TABLE IF NOT EXISTS spec_names (
id INT AUTO_INCREMENT PRIMARY KEY,
product_id INT NOT NULL,
spec_name VARCHAR(100) NOT NULL,
spec_value VARCHAR(100) NOT NULL,
price_adjustment INT DEFAULT 0,
points_adjustment INT DEFAULT 0,
rongdou_adjustment INT DEFAULT 0,
stock INT DEFAULT 0,
sku_code VARCHAR(100),
name VARCHAR(100) NOT NULL COMMENT '规格名称,如:颜色、尺寸、材质',
display_name VARCHAR(100) NOT NULL 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 (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(`
CREATE TABLE IF NOT EXISTS user_addresses (

View File

@@ -10,7 +10,7 @@ const dbConfig = {
user: 'test_mao',
password: 'nK2mPbWriBp25BRd',
database: 'test_mao',
// charset: 'utf8mb4',
charset: 'utf8mb4',
// 连接池配置
connectionLimit: 20, // 连接池最大连接数
queueLimit: 0, // 排队等待连接的最大数量0表示无限制

79
docs/README.md Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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: 手机号码
*/

View File

@@ -4,11 +4,11 @@
"description": "Vue3 + Node.js 集成系统",
"main": "server.js",
"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:admin": "cd admin && npm run dev",
"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:admin": "cd admin && npm run build",
"start": "node server.js"

View File

@@ -102,10 +102,14 @@ router.get('/', auth, async (req, res) => {
const userId = req.user.id;
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
LEFT JOIN address_labels al ON ua.label_id = al.id
WHERE ua.user_id = ? AND ua.deleted_at IS NULL
LEFT JOIN address_labels al ON ua.label = al.id
LEFT JOIN china_regions p ON ua.province = p.code
LEFT JOIN china_regions c ON ua.city = c.code
LEFT JOIN china_regions d ON ua.district = d.code
WHERE ua.user_id = ?
ORDER BY ua.is_default DESC, ua.created_at DESC`,
[userId]
);
@@ -259,14 +263,9 @@ router.post('/', auth, async (req, res) => {
recipient_name,
phone,
province_code,
province_name,
city_code,
city_name,
district_code,
district_name,
detailed_address,
postal_code,
label_id,
is_default = false
} = req.body;
@@ -278,19 +277,19 @@ router.post('/', auth, async (req, res) => {
// 如果设置为默认地址,先取消其他默认地址
if (is_default) {
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]
);
}
const [result] = await getDB().execute(
`INSERT INTO user_addresses (
user_id, recipient_name, phone, province_code, province_name, city_code, city_name,
district_code, district_name, detailed_address, postal_code, label_id, is_default, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())`,
user_id, receiver_name, receiver_phone, province, city,
district, detailed_address, is_default, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())`,
[
userId, recipient_name, phone, province_code, province_name, city_code, city_name,
district_code, district_name, detailed_address, postal_code, label_id, is_default
userId, recipient_name, phone, province_code, city_code,
district_code, detailed_address, is_default
]
);
@@ -383,20 +382,18 @@ router.put('/:id', auth, async (req, res) => {
recipient_name,
phone,
province_code,
province_name,
city_code,
city_name,
district_code,
district_name,
detailed_address,
postal_code,
label_id,
is_default
} = req.body;
if (!recipient_name || !phone || !province_code || !city_code || !district_code || !detailed_address) {
return res.status(400).json({ message: '收件人姓名、电话、省市区和详细地址不能为空' });
}
// 检查地址是否存在且属于当前用户
const [existing] = await getDB().execute(
'SELECT id FROM user_addresses WHERE id = ? AND user_id = ? AND deleted_at IS NULL',
'SELECT id FROM user_addresses WHERE id = ? AND user_id = ? ',
[addressId, userId]
);
@@ -407,20 +404,19 @@ router.put('/:id', auth, async (req, res) => {
// 如果设置为默认地址,先取消其他默认地址
if (is_default) {
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]
);
}
const [result] = await getDB().execute(
`UPDATE user_addresses SET
recipient_name = ?, phone = ?, province_code = ?, province_name = ?,
city_code = ?, city_name = ?, district_code = ?, district_name = ?,
detailed_address = ?, postal_code = ?, label_id = ?, is_default = ?, updated_at = NOW()
receiver_name = ?, receiver_phone = ?, province = ?, city = ?,
district = ?, detailed_address = ?, is_default = ?, updated_at = NOW()
WHERE id = ? AND user_id = ?`,
[
recipient_name, phone, province_code, province_name, city_code, city_name,
district_code, district_name, detailed_address, postal_code, label_id, is_default,
recipient_name, phone, province_code, city_code,
district_code, detailed_address, is_default,
addressId, userId
]
);
@@ -439,7 +435,7 @@ router.put('/:id', auth, async (req, res) => {
* @swagger
* /addresses/{id}:
* delete:
* summary: 删除收货地址(软删除)
* summary: 删除收货地址
* tags: [Addresses]
* security:
* - bearerAuth: []
@@ -477,7 +473,7 @@ router.delete('/:id', auth, async (req, res) => {
const userId = req.user.id;
const [result] = await getDB().execute(
'UPDATE user_addresses SET deleted_at = NOW() WHERE id = ? AND user_id = ? AND deleted_at IS NULL',
'DELETE FROM user_addresses WHERE id = ? AND user_id = ?',
[addressId, userId]
);

935
routes/cart.js Normal file
View 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;

File diff suppressed because it is too large Load Diff

View File

@@ -4,123 +4,7 @@ const { auth, adminAuth } = require('../middleware/auth');
const router = express.Router();
/**
* @swagger
* tags:
* name: Products
* description: 商品管理API
*/
/**
* @swagger
* components:
* schemas:
* Product:
* type: object
* required:
* - name
* - points_price
* - stock
* properties:
* id:
* type: integer
* description: 商品ID
* name:
* type: string
* description: 商品名称
* category:
* type: string
* description: 商品分类
* points_price:
* type: integer
* description: 积分价格
* stock:
* type: integer
* description: 库存数量
* image_url:
* type: string
* description: 商品图片URL
* description:
* type: string
* description: 商品描述
* status:
* type: string
* description: 商品状态
* enum: [active, inactive]
* created_at:
* type: string
* format: date-time
* description: 创建时间
* updated_at:
* type: string
* format: date-time
* description: 更新时间
*/
/**
* @swagger
* /products:
* get:
* summary: 获取商品列表
* tags: [Products]
* parameters:
* - in: query
* name: page
* schema:
* type: integer
* default: 1
* description: 页码
* - in: query
* name: limit
* schema:
* type: integer
* default: 10
* description: 每页数量
* - in: query
* name: search
* schema:
* type: string
* description: 搜索关键词
* - in: query
* name: category
* schema:
* type: string
* description: 商品分类
* - in: query
* name: status
* schema:
* type: string
* enum: [active, inactive]
* description: 商品状态
* responses:
* 200:
* description: 成功获取商品列表
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
* products:
* type: array
* items:
* $ref: '#/components/schemas/Product'
* pagination:
* type: object
* properties:
* page:
* type: integer
* limit:
* type: integer
* total:
* type: integer
* pages:
* type: integer
*/
// 商品管理路由
router.get('/', async (req, res) => {
try {
const { page = 1, limit = 10, search = '', category = '', status = '' } = req.query;
@@ -159,7 +43,7 @@ router.get('/', async (req, res) => {
// 获取商品列表
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
${whereClause}
ORDER BY created_at DESC
@@ -170,7 +54,10 @@ router.get('/', async (req, res) => {
const queryParams = [...params];
console.log('Query params:', queryParams, 'Query:', query);
const [products] = await getDB().execute(query, queryParams);
products.forEach(item=>{
item.payment_methods = JSON.parse(item.payment_methods)
})
console.log('查询结果:', products);
res.json({
success: true,
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) => {
try {
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
* /products/{id}:
* get:
* summary: 获取单个商品详情
* summary: 获取单个商品详情(包含增强规格信息)
* tags: [Products]
* parameters:
* - in: path
@@ -246,7 +210,7 @@ router.get('/categories', async (req, res) => {
* description: 商品ID
* responses:
* 200:
* description: 成功获取商品详情
* description: 成功获取商品详情,包含完整的规格信息
* content:
* application/json:
* schema:
@@ -254,8 +218,116 @@ router.get('/categories', async (req, res) => {
* properties:
* success:
* type: boolean
* example: true
* data:
* $ref: '#/components/schemas/Product'
* type: object
* properties:
* product:
* type: object
* properties:
* id:
* type: integer
* name:
* type: string
* category:
* type: string
* price:
* type: number
* points_price:
* type: number
* rongdou_price:
* type: number
* stock:
* type: integer
* specifications:
* type: array
* description: 商品规格组合列表(笛卡尔积规格系统)
* items:
* type: object
* properties:
* id:
* type: integer
* description: 规格组合ID
* combination_key:
* type: string
* description: 规格组合键1-3-5
* spec_display:
* type: string
* description: 规格显示文本(如:颜色:红色 | 尺寸:XL
* spec_details:
* type: array
* description: 规格详细信息
* items:
* type: object
* properties:
* id:
* type: integer
* spec_name:
* type: string
* description: 规格名称
* spec_display_name:
* type: string
* description: 规格显示名称
* value:
* type: string
* description: 规格值
* display_value:
* type: string
* description: 规格显示值
* color_code:
* type: string
* description: 颜色代码
* image_url:
* type: string
* description: 规格图片
* price_adjustment:
* type: number
* description: 价格调整
* points_adjustment:
* type: number
* description: 积分调整
* rongdou_adjustment:
* type: number
* description: 融豆调整
* stock:
* type: integer
* description: 规格库存
* sku_code:
* type: string
* description: SKU编码
* barcode:
* type: string
* description: 条形码
* weight:
* type: number
* description: 重量
* volume:
* type: number
* description: 体积
* actual_price:
* type: number
* description: 实际价格(基础价格+调整)
* actual_points_price:
* type: number
* description: 实际积分价格
* actual_rongdou_price:
* type: number
* description: 实际融豆价格
* is_available:
* type: boolean
* description: 是否有库存
* specification_count:
* type: integer
* description: 规格总数
* available_specifications:
* type: integer
* description: 有库存的规格数量
* attributes:
* type: array
* description: 商品属性
* isFavorited:
* type: boolean
* description: 是否已收藏
* 404:
* description: 商品不存在
*/
@@ -280,12 +352,92 @@ router.get('/:id', async (req, res) => {
const product = products[0];
// 获取商品规格
const [specifications] = await getDB().execute(
'SELECT * FROM product_specifications WHERE product_id = ? ORDER BY id',
// 获取商品规格组合(新的笛卡尔积规格系统)
const [specCombinations] = await getDB().execute(
`SELECT psc.*,
GROUP_CONCAT(CONCAT(sn.display_name, ':', sv.display_value) ORDER BY sn.sort_order SEPARATOR ' | ') as spec_display
FROM product_spec_combinations psc
LEFT JOIN JSON_TABLE(psc.spec_values, '$[*]' COLUMNS (spec_value_id INT PATH '$')) jt ON TRUE
LEFT JOIN spec_values sv ON jt.spec_value_id = sv.id
LEFT JOIN spec_names sn ON sv.spec_name_id = sn.id
WHERE psc.product_id = ? AND psc.status = 'active'
GROUP BY psc.id
ORDER BY psc.combination_key`,
[id]
);
// 为每个规格组合获取详细的规格值信息
const enhancedSpecifications = [];
for (const combination of specCombinations) {
// 智能解析 spec_values 字段,兼容多种数据格式
let specValueIds = [];
try {
if (combination.spec_values) {
// 如果是 Buffer 对象,先转换为字符串
let specValuesStr = combination.spec_values;
if (Buffer.isBuffer(specValuesStr)) {
specValuesStr = specValuesStr.toString('utf8');
}
// 尝试 JSON 解析
if (typeof specValuesStr === 'string') {
specValuesStr = specValuesStr.trim();
if (specValuesStr.startsWith('[') && specValuesStr.endsWith(']')) {
// JSON 数组格式
specValueIds = JSON.parse(specValuesStr);
} else if (specValuesStr.includes(',')) {
// 逗号分隔的字符串格式
specValueIds = specValuesStr.split(',').map(id => parseInt(id.trim())).filter(id => !isNaN(id));
} else if (specValuesStr && !isNaN(parseInt(specValuesStr))) {
// 单个数字
specValueIds = [parseInt(specValuesStr)];
}
} else if (Array.isArray(specValuesStr)) {
// 已经是数组
specValueIds = specValuesStr;
}
}
} catch (parseError) {
console.warn(`解析规格值失败 (combination_id: ${combination.id}):`, parseError.message);
specValueIds = [];
}
// 获取规格值详情
if (specValueIds && specValueIds.length > 0) {
const placeholders = specValueIds.map(() => '?').join(',');
const [specDetails] = await getDB().execute(
`SELECT sv.*, sn.name as spec_name, sn.display_name as spec_display_name
FROM spec_values sv
LEFT JOIN spec_names sn ON sv.spec_name_id = sn.id
WHERE sv.id IN (${placeholders})
ORDER BY sn.sort_order, sv.sort_order`,
specValueIds
);
enhancedSpecifications.push({
id: combination.id,
combination_key: combination.combination_key,
spec_display: combination.spec_display,
spec_details: specDetails,
price_adjustment: combination.price_adjustment || 0,
points_adjustment: combination.points_adjustment || 0,
rongdou_adjustment: combination.rongdou_adjustment || 0,
stock: combination.stock,
sku_code: combination.sku_code,
barcode: combination.barcode,
weight: combination.weight,
volume: combination.volume,
actual_price: product.price + (combination.price_adjustment || 0),
actual_points_price: product.points_price + (combination.points_adjustment || 0),
actual_rongdou_price: product.rongdou_price + (combination.rongdou_adjustment || 0),
is_available: combination.stock > 0,
status: combination.status,
created_at: combination.created_at,
updated_at: combination.updated_at
});
}
}
// 获取商品属性
const [attributes] = await getDB().execute(
'SELECT * FROM product_attributes WHERE product_id = ? ORDER BY sort_order, id',
@@ -305,12 +457,72 @@ router.get('/:id', async (req, res) => {
// 构建增强的商品数据
const enhancedProduct = {
...product,
images: product.images ? JSON.parse(product.images) : (product.image_url ? [product.image_url] : []),
videos: product.videos ? JSON.parse(product.videos) : [],
payment_methods: product.payment_methods ? JSON.parse(product.payment_methods) : ['points'],
specifications,
images: (() => {
try {
if (product.images) {
let imagesStr = product.images;
if (Buffer.isBuffer(imagesStr)) {
imagesStr = imagesStr.toString('utf8');
}
if (typeof imagesStr === 'string') {
imagesStr = imagesStr.trim();
if (imagesStr.startsWith('[') && imagesStr.endsWith(']')) {
return JSON.parse(imagesStr);
}
}
}
return product.image_url ? [product.image_url] : [];
} catch (e) {
console.warn('解析商品图片失败:', e.message);
return product.image_url ? [product.image_url] : [];
}
})(),
videos: (() => {
try {
if (product.videos) {
let videosStr = product.videos;
if (Buffer.isBuffer(videosStr)) {
videosStr = videosStr.toString('utf8');
}
if (typeof videosStr === 'string') {
videosStr = videosStr.trim();
if (videosStr.startsWith('[') && videosStr.endsWith(']')) {
return JSON.parse(videosStr);
}
}
}
return [];
} catch (e) {
console.warn('解析商品视频失败:', e.message);
return [];
}
})(),
payment_methods: (() => {
try {
if (product.payment_methods) {
let methodsStr = product.payment_methods;
if (Buffer.isBuffer(methodsStr)) {
methodsStr = methodsStr.toString('utf8');
}
if (typeof methodsStr === 'string') {
methodsStr = methodsStr.trim();
if (methodsStr.startsWith('[') && methodsStr.endsWith(']')) {
return JSON.parse(methodsStr);
}
}
}
return ['points'];
} catch (e) {
console.warn('解析支付方式失败:', e.message);
return ['points'];
}
})(),
specifications: enhancedSpecifications,
attributes,
isFavorited,
// 规格统计信息
specification_count: enhancedSpecifications.length,
available_specifications: enhancedSpecifications.filter(spec => spec.is_available).length,
// 保持向后兼容
points: product.points_price,
image: product.image_url,
@@ -352,19 +564,7 @@ router.post('/', auth, adminAuth, async (req, res) => {
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) {
@@ -499,25 +699,7 @@ router.put('/:id', auth, adminAuth, async (req, res) => {
updateValues
);
// 更新商品规格
if (specifications !== undefined) {
// 删除原有规格
await getDB().execute('DELETE FROM product_specifications WHERE product_id = ?', [productId]);
// 添加新规格
if (specifications && specifications.length > 0) {
for (const spec of specifications) {
await getDB().execute(
`INSERT INTO product_specifications (product_id, spec_name, spec_value, price_adjustment,
points_adjustment, rongdou_adjustment, stock, sku_code)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
[productId, spec.name, spec.value, spec.price_adjustment || 0,
spec.points_adjustment || 0, spec.rongdou_adjustment || 0,
spec.stock || 0, spec.sku_code || null]
);
}
}
}
// 更新商品属性
if (attributes !== undefined) {
@@ -655,8 +837,8 @@ router.get('/:id/reviews', async (req, res) => {
JOIN users u ON pr.user_id = u.id
WHERE pr.product_id = ?
ORDER BY pr.created_at DESC
LIMIT ? OFFSET ?`,
[id, limit, offset]
LIMIT ${limit} OFFSET ${offset}`,
[id]
);
// 获取评论总数
@@ -832,8 +1014,8 @@ router.get('/favorites', auth, async (req, res) => {
JOIN products p ON pf.product_id = p.id
WHERE pf.user_id = ? AND p.status = 'active'
ORDER BY pf.created_at DESC
LIMIT ? OFFSET ?`,
[userId, limit, offset]
LIMIT ${limit} OFFSET ${offset}`,
[userId]
);
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) => {

View File

@@ -120,12 +120,37 @@ router.get('/zhejiang', async (req, res) => {
*/
router.get('/provinces', async (req, res) => {
try {
// 递归获取子区域的函数
async function getChildrenRecursively(parentCode, level) {
const [children] = await getDB().execute(
`SELECT code, name as label, level FROM china_regions
WHERE parent_code = ? AND level = ?
ORDER BY code`,
[parentCode, level]
);
// 为每个子区域递归获取其子区域
for (let child of children) {
if (level < 3) { // 最多到区县级别level 3
child.children = await getChildrenRecursively(child.code, level + 1);
}
}
return children;
}
// 获取所有省份
const [provinces] = await getDB().execute(
`SELECT code, name FROM china_regions
`SELECT code, name as label, level FROM china_regions
WHERE level = 1
ORDER BY code`
);
// 为每个省份递归获取城市和区县
for (let province of provinces) {
province.children = await getChildrenRecursively(province.code, 2);
}
res.json({
success: true,
data: provinces

1096
routes/specifications.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -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
${whereClause}
ORDER BY t.created_at DESC
LIMIT ? OFFSET ?
`, listParams);
LIMIT ${limitNum} OFFSET ${offset}
`, countParams);
const [countResult] = await db.execute(`
SELECT COUNT(*) as total FROM transfers t ${whereClause}
@@ -1556,10 +1556,11 @@ router.get('/daily-stats',
u.balance,
COALESCE(yesterday_out.amount, 0) as yesterday_out_amount,
COALESCE(today_in.amount, 0) as today_in_amount,
COALESCE(confirmed_from.confirmed_amount, 0) as confirmed_from_amount,
CASE
WHEN (COALESCE(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)
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
FROM users u
LEFT JOIN (
@@ -1580,13 +1581,29 @@ router.get('/daily-stats',
AND status IN ('confirmed', 'received')
GROUP BY to_user_id
) today_in ON u.id = today_in.to_user_id
left join (
select
from_user_id,
sum(amount) as confirmed_amount
from
transfers
where
status = 'received'
and created_at >= ?
and created_at <= ?
group by
from_user_id
) as confirmed_from on u.id = confirmed_from.from_user_id
WHERE u.role != 'admin'
AND u.is_system_account != 1
AND yesterday_out.amount > 0
AND u.balance < 0
ORDER BY balance_needed DESC, yesterday_out_amount DESC
`, [yesterdayStartStr, yesterdayEndStr, todayStartStr, todayEndStr]);
userStats = userStats.filter(item=>item.balance_needed >= 100)
`, [yesterdayStartStr, yesterdayEndStr, todayStartStr, todayEndStr, todayStartStr, todayEndStr]);
// userStats = userStats.filter(item=>item.balance_needed >= 100)
userStats.forEach(item=>{
item.balance_needed = Math.abs(item.balance_needed)
})
res.json({
success: true,
data: {

View File

@@ -53,6 +53,9 @@ const router = express.Router();
* is_system_account:
* type: boolean
* description: 是否为系统账户
* is_distribute:
* type: boolean
* description: 是否为分发账户
* created_at:
* type: string
* 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,
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.business_license,
u.business_license,u.is_distribute,
r.city_name, r.district_name,
COALESCE(yesterday_out.amount, 0) as yesterday_transfer_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
${whereClause}
ORDER BY u.${sortField} ${sortOrder}
LIMIT ? OFFSET ?`,
listParams
LIMIT ${limitNum} OFFSET ${offset}`,
listParams.slice(0, -2)
);
res.json({
@@ -770,8 +773,8 @@ router.get('/points/history', auth, async (req, res) => {
FROM points_history
${whereClause}
ORDER BY created_at DESC
LIMIT ? OFFSET ?`,
[...queryParams, limitNum.toString(), offset.toString()]
LIMIT ${limitNum} OFFSET ${offset}`,
queryParams
);
res.json({
@@ -1007,7 +1010,7 @@ router.get('/registration-codes', auth, adminAuth, async (req, res) => {
}
// 关键词搜索
if(keyword){
if (keyword) {
whereConditions.push(`rc.code LIKE '%${keyword}%'`);
}
@@ -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
${whereClause}
ORDER BY ${sortField} ${sortOrder}
LIMIT ? OFFSET ?
`, listParams);
LIMIT ${limit} OFFSET ${offset}
`, countParams);
// 获取总数
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: '获取用户审核详情失败' });
}
});
/**
* @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;

View File

@@ -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;

View File

@@ -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;

View 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

File diff suppressed because it is too large Load Diff

37
scripts/verify_data.js Normal file
View 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();

View File

@@ -39,6 +39,7 @@ app.use(helmet({
app.use(cors({
origin: [
'http://localhost:5173',
'http://localhost:5176',
'http://localhost:5174',
'http://localhost:3001',
'https://www.zrbjr.com',
@@ -59,6 +60,9 @@ app.use((req, res, next) => {
res.on('finish', () => {
const duration = Date.now() - start;
// 只记录非正常状态码的请求日志过滤掉200、304等正常返回
if (res.statusCode >= 400 || res.statusCode < 200) {
logger.info('HTTP Request', {
method: req.method,
url: req.originalUrl,
@@ -67,6 +71,7 @@ app.use((req, res, next) => {
ip: req.ip,
userAgent: req.get('User-Agent')
});
}
});
next();
@@ -225,6 +230,7 @@ app.use('/api/auth', require('./routes/auth'));
app.use('/api/users', require('./routes/users'));
app.use('/api/user', require('./routes/users')); // 添加单数形式的路由映射
app.use('/api/products', require('./routes/products'));
app.use('/api/specifications', require('./routes/specifications'));
app.use('/api/orders', require('./routes/orders'));
app.use('/api/points', require('./routes/points'));
app.use('/api/captcha', require('./routes/captcha')); // 验证码路由
@@ -243,6 +249,7 @@ app.use('/api/agent-withdrawals', require('./routes/agent-withdrawals'));
app.use('/api/regions', require('./routes/regions'));
app.use('/api/addresses', require('./routes/addresses'));
app.use('/api/address-labels', require('./routes/address-labels'));
app.use('/api/cart', require('./routes/cart'));
// 前端路由 - 必须在最后作为fallback
app.get('/', (req, res) => {

View File

@@ -695,6 +695,7 @@ class MatchingService {
u.balance as current_balance
FROM users u
WHERE u.is_system_account = FALSE
AND u.is_distribute = TRUE
AND u.id != ?
AND u.balance < -100
AND u.audit_status = 'approved'
@@ -715,8 +716,7 @@ class MatchingService {
// 查询用户的分配订单金额统计
const [orderStatusResult] = await db.execute(
`SELECT
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
SUM(CASE WHEN status = 'pending' THEN amount ELSE 0 END) as pending_amount
FROM transfers
WHERE to_user_id = ?`,
[user.user_id]
@@ -730,6 +730,14 @@ class MatchingService {
WHERE to_user_id = ?`,
[user.user_id]
);
//查询用户给其他用户已确认的金额统计(要减去,因为款项还没回来)
const [orderStatusConfirmedResultFrom] = await db.execute(
`SELECT
SUM(CASE WHEN status = 'confirmed' THEN amount ELSE 0 END) as confirmed_amount
FROM transfers
WHERE from_user_id = ?`,
[user.user_id]
);
// 查询用户当天在matching_orders表中打出去的款项
const today = getLocalDateString();
const [todayOutflowResult] = await db.execute(
@@ -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 orderStatusConfirmedFrom = orderStatusConfirmedResultFrom[0] || { confirmed_amount: 0 };
const orderStatusConfirmed = orderStatusConfirmedResult[0] || { confirmed_amount: 0 };
user.today_outflow = parseFloat(todayOutflow.today_outflow) || 0;
user.pending_amount = parseFloat(orderStatus.pending_amount) || 0;
user.processing_amount = parseFloat(orderStatus.processing_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);
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) {
availableUsers.push(user);
}
@@ -851,7 +860,107 @@ class MatchingService {
// 如果还有剩余金额且分配数量不足最小笔数,最后分配给虚拟用户
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) {
const maxPossibleTransfers = Math.min((minTransfers - allocations.length) <= 0 ? 1 : minTransfers - allocations.length, availableVirtualUsers.length);

View File

@@ -33,7 +33,7 @@ const options = {
}]
},
// API文档扫描路径
apis: ['./routes/*.js', './admin/routes/*.js'],
apis: ['./docs/schemas/*.js', './docs/apis/*.js', './routes/*.js', './admin/routes/*.js'],
};
const specs = swaggerJsdoc(options);

817
test_maoj.sql Normal file
View 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;