升级商城逻辑
This commit is contained in:
3
.idea/vcs.xml
generated
3
.idea/vcs.xml
generated
@@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<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
16
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
// 使用 IntelliSense 了解相关属性。
|
||||
// 悬停以查看现有属性的描述。
|
||||
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
|
||||
{
|
||||
"type": "node-terminal",
|
||||
"name": "Run Script: dev:server",
|
||||
"request": "launch",
|
||||
"command": "npm run dev:server",
|
||||
"cwd": "${workspaceFolder}"
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -53,6 +53,7 @@ async function createTables() {
|
||||
role ENUM('user', 'admin') DEFAULT 'user',
|
||||
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 (
|
||||
|
||||
@@ -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
79
docs/README.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# API 文档结构说明
|
||||
|
||||
本项目已将 Swagger API 文档从路由文件中分离出来,采用模块化的文档管理方式。
|
||||
|
||||
## 文件夹结构
|
||||
|
||||
```
|
||||
docs/
|
||||
├── README.md # 本说明文件
|
||||
├── schemas/ # 数据模型定义
|
||||
│ ├── product.js # 商品相关数据模型
|
||||
│ ├── order.js # 订单相关数据模型
|
||||
│ ├── user.js # 用户相关数据模型
|
||||
│ └── cart.js # 购物车相关数据模型
|
||||
└── apis/ # API 接口定义
|
||||
├── products.js # 商品相关 API
|
||||
└── orders.js # 订单相关 API
|
||||
```
|
||||
|
||||
## 优势
|
||||
|
||||
1. **模块化管理**: 按功能模块分离文档,便于维护和查找
|
||||
2. **代码分离**: 路由文件专注于业务逻辑,文档定义独立管理
|
||||
3. **复用性**: Schema 定义可以在多个 API 中复用
|
||||
4. **可维护性**: 文档修改不会影响业务代码,降低出错风险
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 添加新的 Schema
|
||||
|
||||
在 `schemas/` 文件夹中创建新的 `.js` 文件,使用 `@swagger` 注释定义数据模型:
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* @swagger
|
||||
* components:
|
||||
* schemas:
|
||||
* YourModel:
|
||||
* type: object
|
||||
* properties:
|
||||
* id:
|
||||
* type: integer
|
||||
* description: ID
|
||||
*/
|
||||
```
|
||||
|
||||
### 添加新的 API 文档
|
||||
|
||||
在 `apis/` 文件夹中创建新的 `.js` 文件,使用 `@swagger` 注释定义 API 接口:
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* @swagger
|
||||
* tags:
|
||||
* name: YourModule
|
||||
* description: 模块描述
|
||||
*/
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /your-endpoint:
|
||||
* get:
|
||||
* summary: 接口描述
|
||||
* tags: [YourModule]
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 成功响应
|
||||
*/
|
||||
```
|
||||
|
||||
## 配置
|
||||
|
||||
Swagger 配置文件 `swagger.js` 已更新扫描路径:
|
||||
|
||||
```javascript
|
||||
apis: ['./docs/schemas/*.js', './docs/apis/*.js', './routes/*.js', './admin/routes/*.js']
|
||||
```
|
||||
|
||||
这样既保持了对现有路由文件中文档的兼容性,又支持新的模块化文档结构。
|
||||
236
docs/apis/orders.js
Normal file
236
docs/apis/orders.js
Normal file
@@ -0,0 +1,236 @@
|
||||
/**
|
||||
* @swagger
|
||||
* tags:
|
||||
* name: Orders
|
||||
* description: 订单管理API
|
||||
*/
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /orders/create-from-cart:
|
||||
* post:
|
||||
* summary: 从购物车创建预订单
|
||||
* tags: [Orders]
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - cartIds
|
||||
* properties:
|
||||
* cartIds:
|
||||
* type: array
|
||||
* items:
|
||||
* type: integer
|
||||
* description: 购物车商品ID数组
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 成功创建预订单
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* data:
|
||||
* $ref: '#/components/schemas/PreOrder'
|
||||
* 400:
|
||||
* description: 请求参数错误
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* message:
|
||||
* type: string
|
||||
*/
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /orders/pre-order/{id}:
|
||||
* get:
|
||||
* summary: 获取预订单详情
|
||||
* tags: [Orders]
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: 预订单ID
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 成功获取预订单详情
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* preOrder:
|
||||
* $ref: '#/components/schemas/PreOrder'
|
||||
* items:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* properties:
|
||||
* product_id:
|
||||
* type: integer
|
||||
* product_name:
|
||||
* type: string
|
||||
* quantity:
|
||||
* type: integer
|
||||
* points_price:
|
||||
* type: integer
|
||||
* rongdou_price:
|
||||
* type: number
|
||||
* image_url:
|
||||
* type: string
|
||||
* 404:
|
||||
* description: 预订单不存在
|
||||
*/
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /orders/confirm:
|
||||
* post:
|
||||
* summary: 确认下单
|
||||
* tags: [Orders]
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - preOrderId
|
||||
* - shippingAddress
|
||||
* properties:
|
||||
* preOrderId:
|
||||
* type: integer
|
||||
* description: 预订单ID
|
||||
* shippingAddress:
|
||||
* type: string
|
||||
* description: 收货地址
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 订单确认成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* orderId:
|
||||
* type: integer
|
||||
* orderNumber:
|
||||
* type: string
|
||||
* 400:
|
||||
* description: 请求参数错误或余额不足
|
||||
* 404:
|
||||
* description: 预订单不存在
|
||||
*/
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /orders:
|
||||
* get:
|
||||
* summary: 获取用户订单列表
|
||||
* tags: [Orders]
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: page
|
||||
* schema:
|
||||
* type: integer
|
||||
* default: 1
|
||||
* description: 页码
|
||||
* - in: query
|
||||
* name: limit
|
||||
* schema:
|
||||
* type: integer
|
||||
* default: 10
|
||||
* description: 每页数量
|
||||
* - in: query
|
||||
* name: status
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [pending, confirmed, shipped, delivered, cancelled]
|
||||
* description: 订单状态筛选
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 成功获取订单列表
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* orders:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/Order'
|
||||
* pagination:
|
||||
* type: object
|
||||
* properties:
|
||||
* page:
|
||||
* type: integer
|
||||
* limit:
|
||||
* type: integer
|
||||
* total:
|
||||
* type: integer
|
||||
* pages:
|
||||
* type: integer
|
||||
*/
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /orders/{id}:
|
||||
* get:
|
||||
* summary: 获取订单详情
|
||||
* tags: [Orders]
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: 订单ID
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 成功获取订单详情
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* order:
|
||||
* $ref: '#/components/schemas/Order'
|
||||
* items:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/OrderItem'
|
||||
* 404:
|
||||
* description: 订单不存在
|
||||
*/
|
||||
154
docs/apis/products.js
Normal file
154
docs/apis/products.js
Normal file
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* @swagger
|
||||
* tags:
|
||||
* name: Products
|
||||
* description: 商品管理API
|
||||
*/
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /products:
|
||||
* get:
|
||||
* summary: 获取商品列表
|
||||
* tags: [Products]
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: page
|
||||
* schema:
|
||||
* type: integer
|
||||
* default: 1
|
||||
* description: 页码
|
||||
* - in: query
|
||||
* name: limit
|
||||
* schema:
|
||||
* type: integer
|
||||
* default: 10
|
||||
* description: 每页数量
|
||||
* - in: query
|
||||
* name: search
|
||||
* schema:
|
||||
* type: string
|
||||
* description: 搜索关键词
|
||||
* - in: query
|
||||
* name: category
|
||||
* schema:
|
||||
* type: string
|
||||
* description: 商品分类
|
||||
* - in: query
|
||||
* name: status
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [active, inactive]
|
||||
* description: 商品状态
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 成功获取商品列表
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* products:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/Product'
|
||||
* pagination:
|
||||
* type: object
|
||||
* properties:
|
||||
* page:
|
||||
* type: integer
|
||||
* limit:
|
||||
* type: integer
|
||||
* total:
|
||||
* type: integer
|
||||
* pages:
|
||||
* type: integer
|
||||
*/
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /products/categories:
|
||||
* get:
|
||||
* summary: 获取商品分类列表
|
||||
* tags: [Products]
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 成功获取分类列表
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* data:
|
||||
* type: array
|
||||
* items:
|
||||
* type: string
|
||||
*/
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /products/hot:
|
||||
* get:
|
||||
* summary: 获取热门商品
|
||||
* tags: [Products]
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: limit
|
||||
* schema:
|
||||
* type: integer
|
||||
* default: 10
|
||||
* description: 返回数量
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 成功获取热门商品
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* products:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/Product'
|
||||
*/
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /products/{id}:
|
||||
* get:
|
||||
* summary: 获取商品详情
|
||||
* tags: [Products]
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: 商品ID
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 成功获取商品详情
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* data:
|
||||
* $ref: '#/components/schemas/Product'
|
||||
* 404:
|
||||
* description: 商品不存在
|
||||
*/
|
||||
93
docs/schemas/cart.js
Normal file
93
docs/schemas/cart.js
Normal file
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* @swagger
|
||||
* components:
|
||||
* schemas:
|
||||
* CartItem:
|
||||
* type: object
|
||||
* required:
|
||||
* - user_id
|
||||
* - product_id
|
||||
* - quantity
|
||||
* properties:
|
||||
* id:
|
||||
* type: integer
|
||||
* description: 购物车商品ID
|
||||
* user_id:
|
||||
* type: integer
|
||||
* description: 用户ID
|
||||
* product_id:
|
||||
* type: integer
|
||||
* description: 商品ID
|
||||
* quantity:
|
||||
* type: integer
|
||||
* description: 商品数量
|
||||
* created_at:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: 创建时间
|
||||
* updated_at:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: 更新时间
|
||||
*
|
||||
* CartItemWithProduct:
|
||||
* type: object
|
||||
* properties:
|
||||
* id:
|
||||
* type: integer
|
||||
* description: 购物车商品ID
|
||||
* product_id:
|
||||
* type: integer
|
||||
* description: 商品ID
|
||||
* product_name:
|
||||
* type: string
|
||||
* description: 商品名称
|
||||
* quantity:
|
||||
* type: integer
|
||||
* description: 商品数量
|
||||
* points_price:
|
||||
* type: integer
|
||||
* description: 积分价格
|
||||
* rongdou_price:
|
||||
* type: number
|
||||
* description: 融豆价格
|
||||
* image_url:
|
||||
* type: string
|
||||
* description: 商品图片URL
|
||||
* stock:
|
||||
* type: integer
|
||||
* description: 库存数量
|
||||
* payment_methods:
|
||||
* type: array
|
||||
* items:
|
||||
* type: string
|
||||
* description: 支付方式列表
|
||||
* created_at:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: 创建时间
|
||||
*
|
||||
* AddToCartRequest:
|
||||
* type: object
|
||||
* required:
|
||||
* - product_id
|
||||
* - quantity
|
||||
* properties:
|
||||
* product_id:
|
||||
* type: integer
|
||||
* description: 商品ID
|
||||
* quantity:
|
||||
* type: integer
|
||||
* minimum: 1
|
||||
* description: 商品数量
|
||||
*
|
||||
* UpdateCartRequest:
|
||||
* type: object
|
||||
* required:
|
||||
* - quantity
|
||||
* properties:
|
||||
* quantity:
|
||||
* type: integer
|
||||
* minimum: 1
|
||||
* description: 商品数量
|
||||
*/
|
||||
102
docs/schemas/order.js
Normal file
102
docs/schemas/order.js
Normal file
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* @swagger
|
||||
* components:
|
||||
* schemas:
|
||||
* Order:
|
||||
* type: object
|
||||
* required:
|
||||
* - user_id
|
||||
* - total_amount
|
||||
* - status
|
||||
* properties:
|
||||
* id:
|
||||
* type: integer
|
||||
* description: 订单ID
|
||||
* order_number:
|
||||
* type: string
|
||||
* description: 订单号
|
||||
* user_id:
|
||||
* type: integer
|
||||
* description: 用户ID
|
||||
* total_amount:
|
||||
* type: number
|
||||
* description: 订单总金额
|
||||
* total_points:
|
||||
* type: integer
|
||||
* description: 订单总积分
|
||||
* total_rongdou:
|
||||
* type: number
|
||||
* description: 订单总融豆
|
||||
* status:
|
||||
* type: string
|
||||
* description: 订单状态
|
||||
* enum: [pending, confirmed, shipped, delivered, cancelled]
|
||||
* payment_status:
|
||||
* type: string
|
||||
* description: 支付状态
|
||||
* enum: [pending, paid, failed, refunded]
|
||||
* shipping_address:
|
||||
* type: string
|
||||
* description: 收货地址
|
||||
* created_at:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: 创建时间
|
||||
* updated_at:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: 更新时间
|
||||
*
|
||||
* OrderItem:
|
||||
* type: object
|
||||
* properties:
|
||||
* id:
|
||||
* type: integer
|
||||
* description: 订单商品ID
|
||||
* order_id:
|
||||
* type: integer
|
||||
* description: 订单ID
|
||||
* product_id:
|
||||
* type: integer
|
||||
* description: 商品ID
|
||||
* quantity:
|
||||
* type: integer
|
||||
* description: 商品数量
|
||||
* price:
|
||||
* type: number
|
||||
* description: 商品价格
|
||||
* points_price:
|
||||
* type: integer
|
||||
* description: 积分价格
|
||||
* rongdou_price:
|
||||
* type: number
|
||||
* description: 融豆价格
|
||||
* created_at:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: 创建时间
|
||||
*
|
||||
* PreOrder:
|
||||
* type: object
|
||||
* properties:
|
||||
* preOrderId:
|
||||
* type: integer
|
||||
* description: 预订单ID
|
||||
* orderNumber:
|
||||
* type: string
|
||||
* description: 订单号
|
||||
* totalAmount:
|
||||
* type: number
|
||||
* description: 总金额
|
||||
* totalPoints:
|
||||
* type: integer
|
||||
* description: 所需积分总数
|
||||
* totalRongdou:
|
||||
* type: number
|
||||
* description: 所需融豆总数
|
||||
* paymentMethods:
|
||||
* type: array
|
||||
* items:
|
||||
* type: string
|
||||
* description: 去重后的支付方式列表
|
||||
*/
|
||||
53
docs/schemas/product.js
Normal file
53
docs/schemas/product.js
Normal file
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* @swagger
|
||||
* components:
|
||||
* schemas:
|
||||
* Product:
|
||||
* type: object
|
||||
* required:
|
||||
* - name
|
||||
* - points_price
|
||||
* - stock
|
||||
* properties:
|
||||
* id:
|
||||
* type: integer
|
||||
* description: 商品ID
|
||||
* name:
|
||||
* type: string
|
||||
* description: 商品名称
|
||||
* category:
|
||||
* type: string
|
||||
* description: 商品分类
|
||||
* points_price:
|
||||
* type: integer
|
||||
* description: 积分价格
|
||||
* rongdou_price:
|
||||
* type: number
|
||||
* description: 融豆价格
|
||||
* stock:
|
||||
* type: integer
|
||||
* description: 库存数量
|
||||
* image_url:
|
||||
* type: string
|
||||
* description: 商品图片URL
|
||||
* description:
|
||||
* type: string
|
||||
* description: 商品描述
|
||||
* status:
|
||||
* type: string
|
||||
* description: 商品状态
|
||||
* enum: [active, inactive]
|
||||
* payment_methods:
|
||||
* type: array
|
||||
* items:
|
||||
* type: string
|
||||
* description: 支付方式列表
|
||||
* created_at:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: 创建时间
|
||||
* updated_at:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: 更新时间
|
||||
*/
|
||||
104
docs/schemas/user.js
Normal file
104
docs/schemas/user.js
Normal file
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* @swagger
|
||||
* components:
|
||||
* schemas:
|
||||
* User:
|
||||
* type: object
|
||||
* required:
|
||||
* - username
|
||||
* - email
|
||||
* properties:
|
||||
* id:
|
||||
* type: integer
|
||||
* description: 用户ID
|
||||
* username:
|
||||
* type: string
|
||||
* description: 用户名
|
||||
* email:
|
||||
* type: string
|
||||
* format: email
|
||||
* description: 邮箱地址
|
||||
* phone:
|
||||
* type: string
|
||||
* description: 手机号码
|
||||
* points:
|
||||
* type: integer
|
||||
* description: 积分余额
|
||||
* rongdou:
|
||||
* type: number
|
||||
* description: 融豆余额
|
||||
* avatar:
|
||||
* type: string
|
||||
* description: 头像URL
|
||||
* status:
|
||||
* type: string
|
||||
* description: 用户状态
|
||||
* enum: [active, inactive, banned]
|
||||
* created_at:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: 创建时间
|
||||
* updated_at:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: 更新时间
|
||||
*
|
||||
* UserProfile:
|
||||
* type: object
|
||||
* properties:
|
||||
* id:
|
||||
* type: integer
|
||||
* description: 用户ID
|
||||
* username:
|
||||
* type: string
|
||||
* description: 用户名
|
||||
* email:
|
||||
* type: string
|
||||
* description: 邮箱地址
|
||||
* phone:
|
||||
* type: string
|
||||
* description: 手机号码
|
||||
* points:
|
||||
* type: integer
|
||||
* description: 积分余额
|
||||
* rongdou:
|
||||
* type: number
|
||||
* description: 融豆余额
|
||||
* avatar:
|
||||
* type: string
|
||||
* description: 头像URL
|
||||
*
|
||||
* LoginRequest:
|
||||
* type: object
|
||||
* required:
|
||||
* - username
|
||||
* - password
|
||||
* properties:
|
||||
* username:
|
||||
* type: string
|
||||
* description: 用户名或邮箱
|
||||
* password:
|
||||
* type: string
|
||||
* description: 密码
|
||||
*
|
||||
* RegisterRequest:
|
||||
* type: object
|
||||
* required:
|
||||
* - username
|
||||
* - email
|
||||
* - password
|
||||
* properties:
|
||||
* username:
|
||||
* type: string
|
||||
* description: 用户名
|
||||
* email:
|
||||
* type: string
|
||||
* format: email
|
||||
* description: 邮箱地址
|
||||
* password:
|
||||
* type: string
|
||||
* description: 密码
|
||||
* phone:
|
||||
* type: string
|
||||
* description: 手机号码
|
||||
*/
|
||||
@@ -4,11 +4,11 @@
|
||||
"description": "Vue3 + Node.js 集成系统",
|
||||
"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"
|
||||
|
||||
@@ -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
935
routes/cart.js
Normal file
@@ -0,0 +1,935 @@
|
||||
const express = require('express');
|
||||
const { getDB } = require('../database');
|
||||
const { auth } = require('../middleware/auth');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* tags:
|
||||
* name: Cart
|
||||
* description: 购物车管理相关接口
|
||||
*/
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* components:
|
||||
* schemas:
|
||||
* CartItem:
|
||||
* type: object
|
||||
* properties:
|
||||
* id:
|
||||
* type: integer
|
||||
* description: 购物车项ID
|
||||
* user_id:
|
||||
* type: integer
|
||||
* description: 用户ID
|
||||
* product_id:
|
||||
* type: integer
|
||||
* description: 商品ID
|
||||
* quantity:
|
||||
* type: integer
|
||||
* description: 商品数量
|
||||
* spec_combination_id:
|
||||
* type: integer
|
||||
* description: 商品规格组合ID
|
||||
* created_at:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: 创建时间
|
||||
* updated_at:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* description: 更新时间
|
||||
* product:
|
||||
* type: object
|
||||
* properties:
|
||||
* id:
|
||||
* type: integer
|
||||
* name:
|
||||
* type: string
|
||||
* price:
|
||||
* type: integer
|
||||
* points_price:
|
||||
* type: integer
|
||||
* rongdou_price:
|
||||
* type: integer
|
||||
* image_url:
|
||||
* type: string
|
||||
* stock:
|
||||
* type: integer
|
||||
* status:
|
||||
* type: string
|
||||
*/
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/cart:
|
||||
* get:
|
||||
* summary: 获取购物车列表
|
||||
* tags: [Cart]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 获取购物车成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* items:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/CartItem'
|
||||
* total_count:
|
||||
* type: integer
|
||||
* description: 购物车商品总数量
|
||||
* total_amount:
|
||||
* type: integer
|
||||
* description: 购物车总金额
|
||||
* total_points:
|
||||
* type: integer
|
||||
* description: 购物车总积分
|
||||
* total_rongdou:
|
||||
* type: integer
|
||||
* description: 购物车总融豆
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 500:
|
||||
* description: 服务器错误
|
||||
*/
|
||||
router.get('/', auth, async (req, res) => {
|
||||
try {
|
||||
const userId = req.user.id;
|
||||
|
||||
// 获取购物车商品列表
|
||||
const query = `
|
||||
SELECT
|
||||
c.id, c.user_id, c.product_id, c.quantity, c.specification_id,
|
||||
c.created_at, c.updated_at,
|
||||
p.name, p.price, p.points_price, p.rongdou_price, p.image_url,
|
||||
p.stock, p.status, p.shop_name, p.shop_avatar,
|
||||
psc.combination_key, psc.price_adjustment,
|
||||
psc.points_adjustment, psc.rongdou_adjustment, psc.stock as spec_stock,
|
||||
GROUP_CONCAT(CONCAT(sn.display_name, ':', sv.display_value) ORDER BY sn.sort_order SEPARATOR ' | ') as spec_display
|
||||
FROM cart_items c
|
||||
LEFT JOIN products p ON c.product_id = p.id
|
||||
LEFT JOIN product_spec_combinations psc ON c.specification_id = psc.id
|
||||
LEFT JOIN JSON_TABLE(psc.spec_values, '$[*]' COLUMNS (spec_value_id INT PATH '$')) jt ON psc.id IS NOT NULL
|
||||
LEFT JOIN spec_values sv ON jt.spec_value_id = sv.id
|
||||
LEFT JOIN spec_names sn ON sv.spec_name_id = sn.id
|
||||
WHERE c.user_id = ? AND p.status = 'active'
|
||||
GROUP BY c.id
|
||||
ORDER BY c.created_at DESC
|
||||
`;
|
||||
|
||||
const [cartItems] = await getDB().execute(query, [userId]);
|
||||
|
||||
// 计算总计信息
|
||||
let totalCount = 0;
|
||||
let totalAmount = 0;
|
||||
let totalPoints = 0;
|
||||
let totalRongdou = 0;
|
||||
|
||||
const items = cartItems.map(item => {
|
||||
const finalPrice = item.price + (item.price_adjustment || 0);
|
||||
const finalPointsPrice = item.points_price + (item.points_adjustment || 0);
|
||||
const finalRongdouPrice = item.rongdou_price + (item.rongdou_adjustment || 0);
|
||||
|
||||
totalCount += item.quantity;
|
||||
totalAmount += finalPrice * item.quantity;
|
||||
totalPoints += finalPointsPrice * item.quantity;
|
||||
totalRongdou += finalRongdouPrice * item.quantity;
|
||||
|
||||
return {
|
||||
id: item.id,
|
||||
user_id: item.user_id,
|
||||
product_id: item.product_id,
|
||||
quantity: item.quantity,
|
||||
spec_combination_id: item.spec_combination_id,
|
||||
created_at: item.created_at,
|
||||
updated_at: item.updated_at,
|
||||
product: {
|
||||
id: item.product_id,
|
||||
name: item.name,
|
||||
price: finalPrice,
|
||||
points_price: finalPointsPrice,
|
||||
rongdou_price: finalRongdouPrice,
|
||||
image_url: item.image_url,
|
||||
stock: item.spec_combination_id ? item.spec_stock : item.stock,
|
||||
status: item.status,
|
||||
shop_name: item.shop_name,
|
||||
shop_avatar: item.shop_avatar
|
||||
},
|
||||
specification: item.spec_combination_id ? {
|
||||
id: item.spec_combination_id,
|
||||
combination_key: item.combination_key,
|
||||
spec_display: item.spec_display,
|
||||
price_adjustment: item.price_adjustment,
|
||||
points_adjustment: item.points_adjustment,
|
||||
rongdou_adjustment: item.rongdou_adjustment
|
||||
} : null
|
||||
};
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
items,
|
||||
total_count: totalCount,
|
||||
total_amount: totalAmount,
|
||||
total_points: totalPoints,
|
||||
total_rongdou: totalRongdou
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取购物车失败:', error);
|
||||
res.status(500).json({ success: false, message: '获取购物车失败' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/cart:
|
||||
* post:
|
||||
* summary: 添加商品到购物车
|
||||
* tags: [Cart]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* product_id:
|
||||
* type: integer
|
||||
* description: 商品ID
|
||||
* quantity:
|
||||
* type: integer
|
||||
* description: 商品数量
|
||||
* minimum: 1
|
||||
* spec_combination_id:
|
||||
* type: integer
|
||||
* description: 商品规格组合ID(可选)
|
||||
* required:
|
||||
* - product_id
|
||||
* - quantity
|
||||
* responses:
|
||||
* 201:
|
||||
* description: 添加到购物车成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* message:
|
||||
* type: string
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* cart_item_id:
|
||||
* type: integer
|
||||
* 400:
|
||||
* description: 参数错误或库存不足
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 404:
|
||||
* description: 商品不存在或已下架
|
||||
* 500:
|
||||
* description: 服务器错误
|
||||
*/
|
||||
router.post('/add', auth, async (req, res) => {
|
||||
const db = getDB();
|
||||
await db.query('START TRANSACTION');
|
||||
|
||||
try {
|
||||
const { productId, quantity, specificationId } = req.body;
|
||||
const userId = req.user.id;
|
||||
|
||||
|
||||
// 验证必填字段
|
||||
if (!productId || !quantity || quantity < 1) {
|
||||
await db.query('ROLLBACK');
|
||||
return res.status(400).json({ success: false, message: '请填写正确的商品信息和数量' });
|
||||
}
|
||||
|
||||
// 检查商品是否存在且有效
|
||||
const [products] = await db.execute(
|
||||
'SELECT id, name, stock, status FROM products WHERE id = ?',
|
||||
[productId]
|
||||
);
|
||||
|
||||
if (products.length === 0 || products[0].status !== 'active') {
|
||||
await db.query('ROLLBACK');
|
||||
return res.status(404).json({ success: false, message: '商品不存在或已下架' });
|
||||
}
|
||||
|
||||
const product = products[0];
|
||||
let availableStock = product.stock;
|
||||
|
||||
// 如果指定了规格组合,检查规格组合库存
|
||||
if (specificationId) {
|
||||
const [specs] = await db.execute(
|
||||
'SELECT id, stock, status FROM product_spec_combinations WHERE id = ? AND product_id = ?',
|
||||
[specificationId, productId]
|
||||
);
|
||||
|
||||
if (specs.length === 0 || specs[0].status !== 'active') {
|
||||
await db.query('ROLLBACK');
|
||||
return res.status(404).json({ success: false, message: '商品规格组合不存在或已下架' });
|
||||
}
|
||||
|
||||
availableStock = specs[0].stock;
|
||||
}
|
||||
|
||||
// 检查购物车中是否已存在相同商品和规格组合
|
||||
const [existingItems] = await db.execute(
|
||||
'SELECT id, quantity FROM cart_items WHERE user_id = ? AND product_id = ? AND (specification_id = ? OR (specification_id IS NULL AND ? IS NULL))',
|
||||
[userId, productId, specificationId, specificationId]
|
||||
);
|
||||
|
||||
let finalQuantity = quantity;
|
||||
if (existingItems.length > 0) {
|
||||
finalQuantity += existingItems[0].quantity;
|
||||
}
|
||||
|
||||
// 检查库存是否足够
|
||||
if (availableStock < finalQuantity) {
|
||||
await db.query('ROLLBACK');
|
||||
return res.status(400).json({ success: false, message: '库存不足' });
|
||||
}
|
||||
|
||||
let cartItemId;
|
||||
|
||||
if (existingItems.length > 0) {
|
||||
// 更新现有购物车项的数量
|
||||
await db.execute(
|
||||
'UPDATE cart_items SET quantity = ?, updated_at = NOW() WHERE id = ?',
|
||||
[finalQuantity, existingItems[0].id]
|
||||
);
|
||||
cartItemId = existingItems[0].id;
|
||||
} else {
|
||||
// 添加新的购物车项
|
||||
const [result] = await db.execute(
|
||||
'INSERT INTO cart_items (user_id, product_id, quantity, specification_id, created_at, updated_at) VALUES (?, ?, ?, ?, NOW(), NOW())',
|
||||
[userId, productId, quantity, specificationId]
|
||||
);
|
||||
cartItemId = result.insertId;
|
||||
}
|
||||
|
||||
await db.query('COMMIT');
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '添加到购物车成功',
|
||||
data: { cart_item_id: cartItemId }
|
||||
});
|
||||
} catch (error) {
|
||||
await db.query('ROLLBACK');
|
||||
console.error('添加到购物车失败:', error);
|
||||
res.status(500).json({ success: false, message: '添加到购物车失败' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/cart/{id}:
|
||||
* put:
|
||||
* summary: 更新购物车商品数量
|
||||
* tags: [Cart]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: 购物车项ID
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* quantity:
|
||||
* type: integer
|
||||
* description: 新的商品数量
|
||||
* minimum: 1
|
||||
* required:
|
||||
* - quantity
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 更新购物车成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* message:
|
||||
* type: string
|
||||
* 400:
|
||||
* description: 参数错误或库存不足
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 404:
|
||||
* description: 购物车项不存在
|
||||
* 500:
|
||||
* description: 服务器错误
|
||||
*/
|
||||
router.put('/:id', auth, async (req, res) => {
|
||||
const db = getDB();
|
||||
await db.query('START TRANSACTION');
|
||||
|
||||
try {
|
||||
const cartItemId = req.params.id;
|
||||
const { quantity } = req.body;
|
||||
const userId = req.user.id;
|
||||
|
||||
// 验证数量
|
||||
if (!quantity || quantity < 1) {
|
||||
await db.query('ROLLBACK');
|
||||
return res.status(400).json({ success: false, message: '商品数量必须大于0' });
|
||||
}
|
||||
|
||||
// 检查购物车项是否存在且属于当前用户
|
||||
const [cartItems] = await db.execute(
|
||||
'SELECT id, product_id, specification_id FROM cart_items WHERE id = ? AND user_id = ?',
|
||||
[cartItemId, userId]
|
||||
);
|
||||
console.log(cartItems,'cartItems');
|
||||
|
||||
|
||||
if (cartItems.length === 0) {
|
||||
await db.query('ROLLBACK');
|
||||
return res.status(404).json({ success: false, message: '购物车项不存在' });
|
||||
}
|
||||
|
||||
const cartItem = cartItems[0];
|
||||
|
||||
// 检查商品库存
|
||||
const [products] = await db.execute(
|
||||
'SELECT stock, status FROM products WHERE id = ?',
|
||||
[cartItem.product_id]
|
||||
);
|
||||
|
||||
if (products.length === 0 || products[0].status !== 'active') {
|
||||
await db.query('ROLLBACK');
|
||||
return res.status(404).json({ success: false, message: '商品不存在或已下架' });
|
||||
}
|
||||
|
||||
let availableStock = products[0].stock;
|
||||
|
||||
// 如果有规格,检查规格库存
|
||||
if (cartItem.specification_id) {
|
||||
const [specs] = await db.execute(
|
||||
'SELECT stock, status FROM product_spec_combinations WHERE id = ?',
|
||||
[cartItem.specification_id]
|
||||
);
|
||||
|
||||
if (specs.length === 0 || specs[0].status !== 'active') {
|
||||
await db.query('ROLLBACK');
|
||||
return res.status(404).json({ success: false, message: '商品规格不存在或已下架' });
|
||||
}
|
||||
|
||||
availableStock = specs[0].stock;
|
||||
}
|
||||
|
||||
// 检查库存是否足够
|
||||
if (availableStock < quantity) {
|
||||
await db.query('ROLLBACK');
|
||||
return res.status(400).json({ success: false, message: '库存不足' });
|
||||
}
|
||||
|
||||
// 更新购物车项数量
|
||||
await db.execute(
|
||||
'UPDATE cart_items SET quantity = ?, updated_at = NOW() WHERE id = ?',
|
||||
[quantity, cartItemId]
|
||||
);
|
||||
|
||||
await db.query('COMMIT');
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '更新购物车成功'
|
||||
});
|
||||
} catch (error) {
|
||||
await db.query('ROLLBACK');
|
||||
console.error('更新购物车失败:', error);
|
||||
res.status(500).json({ success: false, message: '更新购物车失败' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/cart/{id}:
|
||||
* delete:
|
||||
* summary: 删除购物车商品
|
||||
* tags: [Cart]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: 购物车项ID
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 删除购物车商品成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* message:
|
||||
* type: string
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 404:
|
||||
* description: 购物车项不存在
|
||||
* 500:
|
||||
* description: 服务器错误
|
||||
*/
|
||||
router.delete('/:id', auth, async (req, res) => {
|
||||
try {
|
||||
const cartItemId = req.params.id;
|
||||
const userId = req.user.id;
|
||||
|
||||
// 检查购物车项是否存在且属于当前用户
|
||||
const [cartItems] = await getDB().execute(
|
||||
'SELECT id FROM cart_items WHERE id = ? AND user_id = ?',
|
||||
[cartItemId, userId]
|
||||
);
|
||||
|
||||
if (cartItems.length === 0) {
|
||||
return res.status(404).json({ success: false, message: '购物车项不存在' });
|
||||
}
|
||||
|
||||
// 删除购物车项
|
||||
await getDB().execute(
|
||||
'DELETE FROM cart_items WHERE id = ?',
|
||||
[cartItemId]
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '删除购物车商品成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('删除购物车商品失败:', error);
|
||||
res.status(500).json({ success: false, message: '删除购物车商品失败' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/cart/batch:
|
||||
* delete:
|
||||
* summary: 批量删除购物车商品
|
||||
* tags: [Cart]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* cart_item_ids:
|
||||
* type: array
|
||||
* items:
|
||||
* type: integer
|
||||
* description: 购物车项ID数组
|
||||
* required:
|
||||
* - cart_item_ids
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 批量删除购物车商品成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* message:
|
||||
* type: string
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* deleted_count:
|
||||
* type: integer
|
||||
* description: 删除的商品数量
|
||||
* 400:
|
||||
* description: 参数错误
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 500:
|
||||
* description: 服务器错误
|
||||
*/
|
||||
router.delete('/batch', auth, async (req, res) => {
|
||||
try {
|
||||
const { cart_item_ids } = req.body;
|
||||
const userId = req.user.id;
|
||||
|
||||
// 验证参数
|
||||
if (!cart_item_ids || !Array.isArray(cart_item_ids) || cart_item_ids.length === 0) {
|
||||
return res.status(400).json({ success: false, message: '请选择要删除的商品' });
|
||||
}
|
||||
|
||||
// 构建删除条件
|
||||
const placeholders = cart_item_ids.map(() => '?').join(',');
|
||||
const query = `DELETE FROM cart_items WHERE id IN (${placeholders}) AND user_id = ?`;
|
||||
const params = [...cart_item_ids, userId];
|
||||
|
||||
const [result] = await getDB().execute(query, params);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '批量删除购物车商品成功',
|
||||
data: {
|
||||
deleted_count: result.affectedRows
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('批量删除购物车商品失败:', error);
|
||||
res.status(500).json({ success: false, message: '批量删除购物车商品失败' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/cart/clear:
|
||||
* delete:
|
||||
* summary: 清空购物车
|
||||
* tags: [Cart]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 清空购物车成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* message:
|
||||
* type: string
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 500:
|
||||
* description: 服务器错误
|
||||
*/
|
||||
router.delete('/clear', auth, async (req, res) => {
|
||||
try {
|
||||
const userId = req.user.id;
|
||||
|
||||
// 清空用户购物车
|
||||
await getDB().execute(
|
||||
'DELETE FROM cart_items WHERE user_id = ?',
|
||||
[userId]
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '清空购物车成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('清空购物车失败:', error);
|
||||
res.status(500).json({ success: false, message: '清空购物车失败' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/cart/count:
|
||||
* get:
|
||||
* summary: 获取购物车商品数量
|
||||
* tags: [Cart]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 获取购物车商品数量成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* count:
|
||||
* type: integer
|
||||
* description: 购物车商品总数量
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 500:
|
||||
* description: 服务器错误
|
||||
*/
|
||||
router.get('/count', auth, async (req, res) => {
|
||||
try {
|
||||
const userId = req.user.id;
|
||||
|
||||
// 获取购物车商品总数量
|
||||
const [result] = await getDB().execute(
|
||||
'SELECT SUM(quantity) as count FROM cart_items WHERE user_id = ?',
|
||||
[userId]
|
||||
);
|
||||
|
||||
const count = result[0].count || 0;
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { count }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取购物车商品数量失败:', error);
|
||||
res.status(500).json({ success: false, message: '获取购物车商品数量失败' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/cart/checkout:
|
||||
* post:
|
||||
* summary: 购物车结账
|
||||
* tags: [Cart]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* cart_item_ids:
|
||||
* type: array
|
||||
* items:
|
||||
* type: integer
|
||||
* description: 要结账的购物车项ID数组
|
||||
* shipping_address:
|
||||
* type: string
|
||||
* description: 收货地址
|
||||
* required:
|
||||
* - cart_item_ids
|
||||
* - shipping_address
|
||||
* responses:
|
||||
* 201:
|
||||
* description: 结账成功
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* message:
|
||||
* type: string
|
||||
* data:
|
||||
* type: object
|
||||
* properties:
|
||||
* order_id:
|
||||
* type: integer
|
||||
* order_no:
|
||||
* type: string
|
||||
* total_amount:
|
||||
* type: integer
|
||||
* total_points:
|
||||
* type: integer
|
||||
* total_rongdou:
|
||||
* type: integer
|
||||
* 400:
|
||||
* description: 参数错误或库存不足
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 500:
|
||||
* description: 服务器错误
|
||||
*/
|
||||
router.post('/checkout', auth, async (req, res) => {
|
||||
const db = getDB();
|
||||
await db.query('START TRANSACTION');
|
||||
|
||||
try {
|
||||
const { cart_item_ids, shipping_address } = req.body;
|
||||
const userId = req.user.id;
|
||||
|
||||
// 验证参数
|
||||
if (!cart_item_ids || !Array.isArray(cart_item_ids) || cart_item_ids.length === 0) {
|
||||
await db.query('ROLLBACK');
|
||||
return res.status(400).json({ success: false, message: '请选择要结账的商品' });
|
||||
}
|
||||
|
||||
if (!shipping_address) {
|
||||
await db.query('ROLLBACK');
|
||||
return res.status(400).json({ success: false, message: '请填写收货地址' });
|
||||
}
|
||||
|
||||
// 获取购物车商品信息
|
||||
const placeholders = cart_item_ids.map(() => '?').join(',');
|
||||
const cartQuery = `
|
||||
SELECT
|
||||
c.id, c.product_id, c.quantity, c.spec_combination_id,
|
||||
p.name, p.price, p.points_price, p.rongdou_price, p.stock, p.status,
|
||||
psc.price_adjustment, psc.points_adjustment, psc.rongdou_adjustment, psc.stock as spec_stock
|
||||
FROM cart_items c
|
||||
LEFT JOIN products p ON c.product_id = p.id
|
||||
LEFT JOIN product_spec_combinations psc ON c.spec_combination_id = psc.id
|
||||
WHERE c.id IN (${placeholders}) AND c.user_id = ?
|
||||
`;
|
||||
|
||||
const [cartItems] = await db.execute(cartQuery, [...cart_item_ids, userId]);
|
||||
|
||||
if (cartItems.length === 0) {
|
||||
await db.query('ROLLBACK');
|
||||
return res.status(400).json({ success: false, message: '购物车商品不存在' });
|
||||
}
|
||||
|
||||
// 验证商品状态和库存
|
||||
let totalAmount = 0;
|
||||
let totalPoints = 0;
|
||||
let totalRongdou = 0;
|
||||
|
||||
for (const item of cartItems) {
|
||||
if (item.status !== 'active') {
|
||||
await db.query('ROLLBACK');
|
||||
return res.status(400).json({ success: false, message: `商品 ${item.name} 已下架` });
|
||||
}
|
||||
|
||||
const availableStock = item.spec_combination_id ? item.spec_stock : item.stock;
|
||||
if (availableStock < item.quantity) {
|
||||
await db.query('ROLLBACK');
|
||||
return res.status(400).json({ success: false, message: `商品 ${item.name} 库存不足` });
|
||||
}
|
||||
|
||||
const finalPrice = item.price + (item.price_adjustment || 0);
|
||||
const finalPointsPrice = item.points_price + (item.points_adjustment || 0);
|
||||
const finalRongdouPrice = item.rongdou_price + (item.rongdou_adjustment || 0);
|
||||
|
||||
totalAmount += finalPrice * item.quantity;
|
||||
totalPoints += finalPointsPrice * item.quantity;
|
||||
totalRongdou += finalRongdouPrice * item.quantity;
|
||||
}
|
||||
|
||||
// 检查用户积分和融豆是否足够
|
||||
const [users] = await db.execute(
|
||||
'SELECT points, rongdou FROM users WHERE id = ?',
|
||||
[userId]
|
||||
);
|
||||
|
||||
if (users.length === 0) {
|
||||
await db.query('ROLLBACK');
|
||||
return res.status(404).json({ success: false, message: '用户不存在' });
|
||||
}
|
||||
|
||||
const user = users[0];
|
||||
|
||||
if (user.points < totalPoints) {
|
||||
await db.query('ROLLBACK');
|
||||
return res.status(400).json({ success: false, message: '积分不足' });
|
||||
}
|
||||
|
||||
if (user.rongdou < totalRongdou) {
|
||||
await db.query('ROLLBACK');
|
||||
return res.status(400).json({ success: false, message: '融豆不足' });
|
||||
}
|
||||
|
||||
// 生成订单号
|
||||
const orderNo = 'ORD' + Date.now() + Math.random().toString(36).substr(2, 5).toUpperCase();
|
||||
|
||||
// 创建订单
|
||||
const [orderResult] = await db.execute(
|
||||
`INSERT INTO orders (order_no, user_id, total_amount, total_points, total_rongdou,
|
||||
status, shipping_address, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, 'pending', ?, NOW(), NOW())`,
|
||||
[orderNo, userId, totalAmount, totalPoints, totalRongdou, shipping_address]
|
||||
);
|
||||
|
||||
const orderId = orderResult.insertId;
|
||||
|
||||
// 创建订单项
|
||||
for (const item of cartItems) {
|
||||
const finalPrice = item.price + (item.price_adjustment || 0);
|
||||
const finalPointsPrice = item.points_price + (item.points_adjustment || 0);
|
||||
const finalRongdouPrice = item.rongdou_price + (item.rongdou_adjustment || 0);
|
||||
|
||||
await db.execute(
|
||||
`INSERT INTO order_items (order_id, product_id, spec_combination_id, quantity,
|
||||
price, points_price, rongdou_price, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, NOW())`,
|
||||
[orderId, item.product_id, item.spec_combination_id, item.quantity,
|
||||
finalPrice, finalPointsPrice, finalRongdouPrice]
|
||||
);
|
||||
|
||||
// 更新库存
|
||||
if (item.spec_combination_id) {
|
||||
await db.execute(
|
||||
'UPDATE product_spec_combinations SET stock = stock - ? WHERE id = ?',
|
||||
[item.quantity, item.spec_combination_id]
|
||||
);
|
||||
} else {
|
||||
await db.execute(
|
||||
'UPDATE products SET stock = stock - ? WHERE id = ?',
|
||||
[item.quantity, item.product_id]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 扣除用户积分和融豆
|
||||
await db.execute(
|
||||
'UPDATE users SET points = points - ?, rongdou = rongdou - ? WHERE id = ?',
|
||||
[totalPoints, totalRongdou, userId]
|
||||
);
|
||||
|
||||
// 删除已结账的购物车项
|
||||
const deletePlaceholders = cart_item_ids.map(() => '?').join(',');
|
||||
await db.execute(
|
||||
`DELETE FROM cart_items WHERE id IN (${deletePlaceholders}) AND user_id = ?`,
|
||||
[...cart_item_ids, userId]
|
||||
);
|
||||
|
||||
await db.query('COMMIT');
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '结账成功',
|
||||
data: {
|
||||
order_id: orderId,
|
||||
order_no: orderNo,
|
||||
total_amount: totalAmount,
|
||||
total_points: totalPoints,
|
||||
total_rongdou: totalRongdou
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
await db.query('ROLLBACK');
|
||||
console.error('购物车结账失败:', error);
|
||||
res.status(500).json({ success: false, message: '结账失败' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
1209
routes/orders.js
1209
routes/orders.js
File diff suppressed because it is too large
Load Diff
@@ -4,123 +4,7 @@ const { auth, adminAuth } = require('../middleware/auth');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
/**
|
||||
* @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) => {
|
||||
|
||||
@@ -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
1096
routes/specifications.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -821,8 +821,8 @@ router.get('/user/:userId', authenticateToken, async (req, res) => {
|
||||
LEFT JOIN users to_user ON t.to_user_id = to_user.id
|
||||
${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: {
|
||||
|
||||
141
routes/users.js
141
routes/users.js
@@ -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;
|
||||
@@ -1,154 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
/**
|
||||
* SQL 语法修复脚本:修复自动替换产生的 SQL 语法错误
|
||||
*/
|
||||
|
||||
class SQLSyntaxFixer {
|
||||
constructor() {
|
||||
this.filesToFix = [
|
||||
'services/matchingService.js',
|
||||
'routes/matchingAdmin.js',
|
||||
'routes/transfers.js',
|
||||
'routes/matching.js'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 修复单个文件中的 SQL 语法错误
|
||||
* @param {string} filePath - 文件路径
|
||||
*/
|
||||
async fixFile(filePath) {
|
||||
const fullPath = path.join(process.cwd(), filePath);
|
||||
|
||||
if (!fs.existsSync(fullPath)) {
|
||||
console.log(`文件不存在: ${filePath}`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`正在修复文件: ${filePath}`);
|
||||
|
||||
let content = fs.readFileSync(fullPath, 'utf8');
|
||||
let originalContent = content;
|
||||
|
||||
// 1. 修复 WHERE source_type = 'allocation' FROM transfers 的错误顺序
|
||||
content = content.replace(
|
||||
/WHERE source_type = 'allocation' FROM transfers/g,
|
||||
"FROM transfers WHERE source_type = 'allocation'"
|
||||
);
|
||||
|
||||
// 2. 修复多个 WHERE 子句的问题
|
||||
content = content.replace(
|
||||
/FROM transfers WHERE source_type = 'allocation'([\s\S]*?)WHERE/g,
|
||||
"FROM transfers WHERE source_type = 'allocation'$1AND"
|
||||
);
|
||||
|
||||
// 3. 修复 INSERT 语句中的引号问题
|
||||
content = content.replace(
|
||||
/'allocation'/g,
|
||||
"'allocation'"
|
||||
);
|
||||
|
||||
// 4. 修复 JOIN 语句中的表别名问题
|
||||
content = content.replace(
|
||||
/FROM transfers oa WHERE oa\.source_type = 'allocation'/g,
|
||||
"FROM transfers oa WHERE oa.source_type = 'allocation'"
|
||||
);
|
||||
|
||||
// 5. 修复复杂查询中的语法问题
|
||||
content = this.fixComplexQueries(content, filePath);
|
||||
|
||||
if (content !== originalContent) {
|
||||
fs.writeFileSync(fullPath, content);
|
||||
console.log(`✓ 已修复: ${filePath}`);
|
||||
} else {
|
||||
console.log(`- 无需修复: ${filePath}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 修复复杂查询
|
||||
* @param {string} content - 文件内容
|
||||
* @param {string} filePath - 文件路径
|
||||
* @returns {string} 修复后的内容
|
||||
*/
|
||||
fixComplexQueries(content, filePath) {
|
||||
if (filePath.includes('matchingService.js')) {
|
||||
// 修复 matchingService.js 中的特定查询
|
||||
|
||||
// 修复获取匹配目标的查询
|
||||
content = content.replace(
|
||||
/FROM transfers oa\s+WHERE oa\.source_type = 'allocation'\s+JOIN users u ON oa\.from_user_id = u\.id/g,
|
||||
"FROM transfers oa JOIN users u ON oa.from_user_id = u.id WHERE oa.source_type = 'allocation'"
|
||||
);
|
||||
|
||||
// 修复获取用户待处理分配的查询
|
||||
content = content.replace(
|
||||
/SELECT \* FROM transfers WHERE source_type = 'allocation' WHERE matching_order_id = \? ORDER BY cycle_number, created_at/g,
|
||||
"SELECT * FROM transfers WHERE source_type = 'allocation' AND matching_order_id = ? ORDER BY cycle_number, created_at"
|
||||
);
|
||||
|
||||
// 修复检查周期完成的查询
|
||||
content = content.replace(
|
||||
/SELECT COUNT\(\*\) as count FROM transfers WHERE source_type = 'allocation' WHERE matching_order_id = \? AND cycle_number = \? AND status = "pending"/g,
|
||||
"SELECT COUNT(*) as count FROM transfers WHERE source_type = 'allocation' AND matching_order_id = ? AND cycle_number = ? AND status = 'pending'"
|
||||
);
|
||||
}
|
||||
|
||||
if (filePath.includes('matchingAdmin.js')) {
|
||||
// 修复 matchingAdmin.js 中的查询
|
||||
content = content.replace(
|
||||
/FROM transfers oa WHERE oa\.source_type = 'allocation'\s+JOIN/g,
|
||||
"FROM transfers oa JOIN"
|
||||
);
|
||||
|
||||
// 在 JOIN 后添加 WHERE 条件
|
||||
content = content.replace(
|
||||
/(FROM transfers oa JOIN[\s\S]*?)WHERE(?!.*source_type)/g,
|
||||
"$1WHERE oa.source_type = 'allocation' AND"
|
||||
);
|
||||
}
|
||||
|
||||
if (filePath.includes('matching.js')) {
|
||||
// 修复 matching.js 中的查询
|
||||
content = content.replace(
|
||||
/LEFT JOIN transfers oa ON mo\.id = oa\.matching_order_id WHERE oa\.source_type = 'allocation'/g,
|
||||
"LEFT JOIN transfers oa ON mo.id = oa.matching_order_id AND oa.source_type = 'allocation'"
|
||||
);
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行所有文件的修复
|
||||
*/
|
||||
async fixAllFiles() {
|
||||
console.log('开始修复 SQL 语法错误...');
|
||||
console.log('=' .repeat(60));
|
||||
|
||||
for (const filePath of this.filesToFix) {
|
||||
try {
|
||||
await this.fixFile(filePath);
|
||||
} catch (error) {
|
||||
console.error(`修复文件 ${filePath} 失败:`, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n' + '=' .repeat(60));
|
||||
console.log('✓ SQL 语法修复完成!');
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const fixer = new SQLSyntaxFixer();
|
||||
await fixer.fixAllFiles();
|
||||
}
|
||||
|
||||
// 如果直接运行此脚本
|
||||
if (require.main === module) {
|
||||
main().catch(console.error);
|
||||
}
|
||||
|
||||
module.exports = SQLSyntaxFixer;
|
||||
@@ -1,133 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
/**
|
||||
* 表别名修复脚本:修复 SQL 查询中的表别名问题
|
||||
*/
|
||||
|
||||
class TableAliasFixer {
|
||||
constructor() {
|
||||
this.filesToFix = [
|
||||
'services/matchingService.js',
|
||||
'routes/matchingAdmin.js',
|
||||
'routes/transfers.js',
|
||||
'routes/matching.js'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 修复单个文件中的表别名问题
|
||||
* @param {string} filePath - 文件路径
|
||||
*/
|
||||
async fixFile(filePath) {
|
||||
const fullPath = path.join(process.cwd(), filePath);
|
||||
|
||||
if (!fs.existsSync(fullPath)) {
|
||||
console.log(`文件不存在: ${filePath}`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`正在修复文件: ${filePath}`);
|
||||
|
||||
let content = fs.readFileSync(fullPath, 'utf8');
|
||||
let originalContent = content;
|
||||
|
||||
// 1. 修复 "FROM transfers WHERE source_type = 'allocation' oa" 的问题
|
||||
content = content.replace(
|
||||
/FROM transfers WHERE source_type = 'allocation' (\w+)/g,
|
||||
"FROM transfers $1 WHERE $1.source_type = 'allocation'"
|
||||
);
|
||||
|
||||
// 2. 修复重复的 source_type 条件
|
||||
content = content.replace(
|
||||
/FROM transfers WHERE source_type = 'allocation' (\w+) AND \1\.source_type = 'allocation'/g,
|
||||
"FROM transfers $1 WHERE $1.source_type = 'allocation'"
|
||||
);
|
||||
|
||||
// 3. 修复 "FROM transfers WHERE source_type = 'allocation'" 后面直接跟其他子句的情况
|
||||
content = content.replace(
|
||||
/FROM transfers WHERE source_type = 'allocation'\s+(JOIN|ORDER|GROUP|LIMIT)/g,
|
||||
"FROM transfers WHERE source_type = 'allocation' $1"
|
||||
);
|
||||
|
||||
// 4. 修复子查询中的问题
|
||||
content = content.replace(
|
||||
/\(SELECT[^)]*FROM transfers WHERE source_type = 'allocation' (\w+)/g,
|
||||
(match, alias) => {
|
||||
return match.replace(
|
||||
`FROM transfers WHERE source_type = 'allocation' ${alias}`,
|
||||
`FROM transfers ${alias} WHERE ${alias}.source_type = 'allocation'`
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
// 5. 修复特定的查询模式
|
||||
content = this.fixSpecificPatterns(content, filePath);
|
||||
|
||||
if (content !== originalContent) {
|
||||
fs.writeFileSync(fullPath, content);
|
||||
console.log(`✓ 已修复: ${filePath}`);
|
||||
} else {
|
||||
console.log(`- 无需修复: ${filePath}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 修复特定的查询模式
|
||||
* @param {string} content - 文件内容
|
||||
* @param {string} filePath - 文件路径
|
||||
* @returns {string} 修复后的内容
|
||||
*/
|
||||
fixSpecificPatterns(content, filePath) {
|
||||
// 修复 SELECT 语句中的表别名问题
|
||||
content = content.replace(
|
||||
/SELECT ([^F]*?) FROM transfers WHERE source_type = 'allocation' (\w+)/g,
|
||||
"SELECT $1 FROM transfers $2 WHERE $2.source_type = 'allocation'"
|
||||
);
|
||||
|
||||
// 修复 UPDATE 语句
|
||||
content = content.replace(
|
||||
/UPDATE transfers WHERE source_type = 'allocation' SET/g,
|
||||
"UPDATE transfers SET"
|
||||
);
|
||||
|
||||
// 修复 WHERE 子句中的条件
|
||||
content = content.replace(
|
||||
/WHERE source_type = 'allocation' AND (\w+)\./g,
|
||||
"WHERE $1.source_type = 'allocation' AND $1."
|
||||
);
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行所有文件的修复
|
||||
*/
|
||||
async fixAllFiles() {
|
||||
console.log('开始修复表别名问题...');
|
||||
console.log('=' .repeat(60));
|
||||
|
||||
for (const filePath of this.filesToFix) {
|
||||
try {
|
||||
await this.fixFile(filePath);
|
||||
} catch (error) {
|
||||
console.error(`修复文件 ${filePath} 失败:`, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n' + '=' .repeat(60));
|
||||
console.log('✓ 表别名修复完成!');
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const fixer = new TableAliasFixer();
|
||||
await fixer.fixAllFiles();
|
||||
}
|
||||
|
||||
// 如果直接运行此脚本
|
||||
if (require.main === module) {
|
||||
main().catch(console.error);
|
||||
}
|
||||
|
||||
module.exports = TableAliasFixer;
|
||||
144
scripts/import_china_regions.js
Normal file
144
scripts/import_china_regions.js
Normal file
@@ -0,0 +1,144 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const mysql = require('mysql2/promise');
|
||||
require('dotenv').config();
|
||||
|
||||
// 数据库配置
|
||||
const dbConfig = {
|
||||
host: process.env.DB_HOST || '114.55.111.44',
|
||||
user: process.env.DB_USER || 'maov2',
|
||||
password: process.env.DB_PASSWORD || '5fYhw8z6T62b7heS',
|
||||
database: process.env.DB_NAME || 'maov2',
|
||||
};
|
||||
|
||||
// 初始化数据库连接
|
||||
async function initDB() {
|
||||
try {
|
||||
const connection = await mysql.createConnection(dbConfig);
|
||||
console.log('数据库连接成功');
|
||||
return connection;
|
||||
} catch (error) {
|
||||
console.error('数据库连接失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 递归解析省市区数据
|
||||
function parseRegionData(regions, parentCode = null, level = 1) {
|
||||
const result = [];
|
||||
let sortOrder = 1;
|
||||
|
||||
for (const region of regions) {
|
||||
// 添加当前区域
|
||||
result.push({
|
||||
code: region.code,
|
||||
name: region.name,
|
||||
parent_code: parentCode,
|
||||
level: level,
|
||||
sort_order: sortOrder++
|
||||
});
|
||||
|
||||
// 递归处理子区域
|
||||
if (region.children && region.children.length > 0) {
|
||||
const childrenData = parseRegionData(region.children, region.code, level + 1);
|
||||
result.push(...childrenData);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// 导入省市区数据
|
||||
async function importChinaRegions() {
|
||||
let connection;
|
||||
|
||||
try {
|
||||
// 读取 JSON 数据文件
|
||||
const jsonFilePath = path.join(__dirname, 'pca-code.json');
|
||||
const jsonData = fs.readFileSync(jsonFilePath, 'utf8');
|
||||
const regionsData = JSON.parse(jsonData);
|
||||
|
||||
console.log('成功读取省市区数据文件');
|
||||
|
||||
// 解析数据
|
||||
const parsedData = parseRegionData(regionsData);
|
||||
console.log(`解析完成,共 ${parsedData.length} 条记录`);
|
||||
|
||||
// 连接数据库
|
||||
connection = await initDB();
|
||||
|
||||
// 清空现有数据
|
||||
await connection.execute('DELETE FROM china_regions');
|
||||
console.log('已清空现有数据');
|
||||
|
||||
// 批量插入数据
|
||||
const batchSize = 100;
|
||||
let insertedCount = 0;
|
||||
|
||||
for (let i = 0; i < parsedData.length; i += batchSize) {
|
||||
const batch = parsedData.slice(i, i + batchSize);
|
||||
const values = batch.map(item => [
|
||||
item.code,
|
||||
item.name,
|
||||
item.parent_code,
|
||||
item.level,
|
||||
item.sort_order
|
||||
]);
|
||||
|
||||
const placeholders = values.map(() => '(?, ?, ?, ?, ?)').join(', ');
|
||||
const flatValues = values.flat();
|
||||
|
||||
await connection.execute(
|
||||
`INSERT INTO china_regions (code, name, parent_code, level, sort_order) VALUES ${placeholders}`,
|
||||
flatValues
|
||||
);
|
||||
|
||||
insertedCount += batch.length;
|
||||
console.log(`已插入 ${insertedCount}/${parsedData.length} 条记录`);
|
||||
}
|
||||
|
||||
// 统计导入结果
|
||||
const [provinceResult] = await connection.execute(
|
||||
'SELECT COUNT(*) as count FROM china_regions WHERE level = 1'
|
||||
);
|
||||
const [cityResult] = await connection.execute(
|
||||
'SELECT COUNT(*) as count FROM china_regions WHERE level = 2'
|
||||
);
|
||||
const [districtResult] = await connection.execute(
|
||||
'SELECT COUNT(*) as count FROM china_regions WHERE level = 3'
|
||||
);
|
||||
const [totalResult] = await connection.execute(
|
||||
'SELECT COUNT(*) as count FROM china_regions'
|
||||
);
|
||||
|
||||
console.log('\n=== 导入完成 ===');
|
||||
console.log(`省份数量: ${provinceResult[0].count}`);
|
||||
console.log(`城市数量: ${cityResult[0].count}`);
|
||||
console.log(`区县数量: ${districtResult[0].count}`);
|
||||
console.log(`总记录数: ${totalResult[0].count}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('导入失败:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
if (connection) {
|
||||
await connection.end();
|
||||
console.log('数据库连接已关闭');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果直接运行此脚本
|
||||
if (require.main === module) {
|
||||
importChinaRegions()
|
||||
.then(() => {
|
||||
console.log('省市区数据导入成功!');
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('导入过程中发生错误:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { importChinaRegions };
|
||||
14625
scripts/pca-code.json
Normal file
14625
scripts/pca-code.json
Normal file
File diff suppressed because it is too large
Load Diff
37
scripts/verify_data.js
Normal file
37
scripts/verify_data.js
Normal file
@@ -0,0 +1,37 @@
|
||||
const { initDB, getDB } = require('../database');
|
||||
|
||||
async function verifyData() {
|
||||
try {
|
||||
await initDB();
|
||||
|
||||
// 检查省份数据
|
||||
const [provinces] = await getDB().query('SELECT code, name FROM china_regions WHERE level = 1 ORDER BY code LIMIT 10');
|
||||
console.log('省份数据样本:');
|
||||
provinces.forEach(p => console.log(` ${p.code} - ${p.name}`));
|
||||
|
||||
// 检查城市数据
|
||||
const [cities] = await getDB().query('SELECT code, name, parent_code FROM china_regions WHERE level = 2 ORDER BY code LIMIT 10');
|
||||
console.log('\n城市数据样本:');
|
||||
cities.forEach(c => console.log(` ${c.code} - ${c.name} (${c.parent_code})`));
|
||||
|
||||
// 检查区县数据
|
||||
const [districts] = await getDB().query('SELECT code, name, parent_code FROM china_regions WHERE level = 3 ORDER BY code LIMIT 10');
|
||||
console.log('\n区县数据样本:');
|
||||
districts.forEach(d => console.log(` ${d.code} - ${d.name} (${d.parent_code})`));
|
||||
|
||||
// 统计各级别数量
|
||||
const [stats] = await getDB().query('SELECT level, COUNT(*) as count FROM china_regions GROUP BY level ORDER BY level');
|
||||
console.log('\n各级别统计:');
|
||||
stats.forEach(row => {
|
||||
const levelName = row.level === 1 ? '省份' : row.level === 2 ? '城市' : '区县';
|
||||
console.log(` ${levelName}(level ${row.level}): ${row.count} 个`);
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('验证失败:', error);
|
||||
} finally {
|
||||
process.exit();
|
||||
}
|
||||
}
|
||||
|
||||
verifyData();
|
||||
@@ -39,6 +39,7 @@ app.use(helmet({
|
||||
app.use(cors({
|
||||
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) => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
817
test_maoj.sql
Normal file
@@ -0,0 +1,817 @@
|
||||
/*
|
||||
Navicat Premium Dump SQL
|
||||
|
||||
Source Server : 测试端
|
||||
Source Server Type : MySQL
|
||||
Source Server Version : 80036 (8.0.36)
|
||||
Source Host : 114.55.111.44:3306
|
||||
Source Schema : test_mao
|
||||
|
||||
Target Server Type : MySQL
|
||||
Target Server Version : 80036 (8.0.36)
|
||||
File Encoding : 65001
|
||||
|
||||
Date: 01/09/2025 10:09:09
|
||||
*/
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
SET FOREIGN_KEY_CHECKS = 0;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for accounts
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `accounts`;
|
||||
CREATE TABLE `accounts` (
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`user_id` int NOT NULL,
|
||||
`account_type` enum('public','user') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'user',
|
||||
`balance` decimal(10, 2) NULL DEFAULT 0.00,
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
INDEX `user_id`(`user_id` ASC) USING BTREE,
|
||||
CONSTRAINT `accounts_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 40 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for address_labels
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `address_labels`;
|
||||
CREATE TABLE `address_labels` (
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`user_id` int NULL DEFAULT NULL,
|
||||
`name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||
`is_system` tinyint(1) NULL DEFAULT 0,
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`color` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '#1890ff',
|
||||
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
UNIQUE INDEX `unique_user_label`(`user_id` ASC, `name` ASC) USING BTREE,
|
||||
CONSTRAINT `address_labels_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 61 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for admin_operation_logs
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `admin_operation_logs`;
|
||||
CREATE TABLE `admin_operation_logs` (
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`admin_id` int NOT NULL,
|
||||
`operation_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
|
||||
`target_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
|
||||
`target_id` int NOT NULL,
|
||||
`description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL,
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
INDEX `admin_id`(`admin_id` ASC) USING BTREE,
|
||||
CONSTRAINT `admin_operation_logs_ibfk_1` FOREIGN KEY (`admin_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 44 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for agent_commission_records
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `agent_commission_records`;
|
||||
CREATE TABLE `agent_commission_records` (
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`agent_id` int NOT NULL,
|
||||
`merchant_id` int NOT NULL,
|
||||
`order_id` int NULL DEFAULT NULL,
|
||||
`commission_amount` decimal(10, 2) NOT NULL,
|
||||
`commission_type` enum('registration','matching') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'matching',
|
||||
`description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL,
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
INDEX `agent_id`(`agent_id` ASC) USING BTREE,
|
||||
INDEX `merchant_id`(`merchant_id` ASC) USING BTREE,
|
||||
INDEX `order_id`(`order_id` ASC) USING BTREE,
|
||||
CONSTRAINT `agent_commission_records_ibfk_1` FOREIGN KEY (`agent_id`) REFERENCES `regional_agents` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT,
|
||||
CONSTRAINT `agent_commission_records_ibfk_2` FOREIGN KEY (`merchant_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT,
|
||||
CONSTRAINT `agent_commission_records_ibfk_3` FOREIGN KEY (`order_id`) REFERENCES `matching_orders` (`id`) ON DELETE SET NULL ON UPDATE RESTRICT
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 17 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for agent_merchants
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `agent_merchants`;
|
||||
CREATE TABLE `agent_merchants` (
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`agent_id` int NOT NULL,
|
||||
`merchant_id` int NOT NULL,
|
||||
`registration_code_id` int NULL DEFAULT NULL,
|
||||
`matching_count` int NULL DEFAULT 0,
|
||||
`commission_earned` decimal(10, 2) NULL DEFAULT 0.00,
|
||||
`is_qualified` tinyint(1) NULL DEFAULT 0,
|
||||
`qualified_at` timestamp NULL DEFAULT NULL,
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
UNIQUE INDEX `unique_agent_merchant`(`agent_id` ASC, `merchant_id` ASC) USING BTREE,
|
||||
INDEX `merchant_id`(`merchant_id` ASC) USING BTREE,
|
||||
INDEX `registration_code_id`(`registration_code_id` ASC) USING BTREE,
|
||||
CONSTRAINT `agent_merchants_ibfk_1` FOREIGN KEY (`agent_id`) REFERENCES `regional_agents` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT,
|
||||
CONSTRAINT `agent_merchants_ibfk_2` FOREIGN KEY (`merchant_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT,
|
||||
CONSTRAINT `agent_merchants_ibfk_3` FOREIGN KEY (`registration_code_id`) REFERENCES `registration_codes` (`id`) ON DELETE SET NULL ON UPDATE RESTRICT
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 32 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for agent_withdrawals
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `agent_withdrawals`;
|
||||
CREATE TABLE `agent_withdrawals` (
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`agent_id` int NOT NULL,
|
||||
`amount` decimal(10, 2) NOT NULL,
|
||||
`payment_type` enum('bank','wechat','alipay','unionpay') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'bank' COMMENT '收款方式类型',
|
||||
`bank_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '银行名称',
|
||||
`account_number` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账号/银行账号',
|
||||
`account_holder` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '持有人姓名',
|
||||
`qr_code_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '收款码图片URL',
|
||||
`status` enum('pending','approved','rejected','completed') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'pending',
|
||||
`apply_note` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL,
|
||||
`admin_note` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL,
|
||||
`processed_by` int NULL DEFAULT NULL,
|
||||
`processed_at` timestamp NULL DEFAULT NULL,
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
`bank_account` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '银行账号(兼容旧版本)',
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
INDEX `agent_id`(`agent_id` ASC) USING BTREE,
|
||||
INDEX `processed_by`(`processed_by` ASC) USING BTREE,
|
||||
CONSTRAINT `agent_withdrawals_ibfk_1` FOREIGN KEY (`agent_id`) REFERENCES `regional_agents` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT,
|
||||
CONSTRAINT `agent_withdrawals_ibfk_2` FOREIGN KEY (`processed_by`) REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE RESTRICT
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for articles
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `articles`;
|
||||
CREATE TABLE `articles` (
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
|
||||
`content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL,
|
||||
`author_id` int NULL DEFAULT NULL,
|
||||
`status` enum('draft','published') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'draft',
|
||||
`views` int NULL DEFAULT 0,
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
INDEX `author_id`(`author_id` ASC) USING BTREE,
|
||||
CONSTRAINT `articles_ibfk_1` FOREIGN KEY (`author_id`) REFERENCES `users` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for balance_fix_log
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `balance_fix_log`;
|
||||
CREATE TABLE `balance_fix_log` (
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`user_id` int NOT NULL,
|
||||
`amount_deducted` decimal(10, 2) NOT NULL,
|
||||
`transfer_count` int NOT NULL,
|
||||
`fix_reason` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL,
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
INDEX `idx_user_id`(`user_id` ASC) USING BTREE,
|
||||
INDEX `idx_created_at`(`created_at` ASC) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = DYNAMIC;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for cart_items
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `cart_items`;
|
||||
CREATE TABLE `cart_items` (
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`user_id` int NOT NULL,
|
||||
`product_id` int NOT NULL,
|
||||
`quantity` int NOT NULL DEFAULT 1,
|
||||
`specification_id` int NULL DEFAULT NULL,
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
UNIQUE INDEX `unique_user_product_spec`(`user_id` ASC, `product_id` ASC, `specification_id` ASC) USING BTREE,
|
||||
INDEX `product_id`(`product_id` ASC) USING BTREE,
|
||||
INDEX `specification_id`(`specification_id` ASC) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 8 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for china_regions
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `china_regions`;
|
||||
CREATE TABLE `china_regions` (
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`code` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||
`name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||
`parent_code` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
|
||||
`level` tinyint NOT NULL COMMENT '1:省 2:市 3:区',
|
||||
`sort_order` int NULL DEFAULT 0,
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
UNIQUE INDEX `code`(`code` ASC) USING BTREE,
|
||||
INDEX `idx_parent_code`(`parent_code` ASC) USING BTREE,
|
||||
INDEX `idx_level`(`level` ASC) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 4621 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for matching_orders
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `matching_orders`;
|
||||
CREATE TABLE `matching_orders` (
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`initiator_id` int NOT NULL,
|
||||
`amount` decimal(10, 2) NOT NULL,
|
||||
`status` enum('pending','matching','completed','cancelled','failed') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'pending',
|
||||
`cycle_count` int NULL DEFAULT 0,
|
||||
`max_cycles` int NULL DEFAULT 3,
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
`matching_type` enum('small','large') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'small',
|
||||
`is_system_reverse` tinyint(1) NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
INDEX `initiator_id`(`initiator_id` ASC) USING BTREE,
|
||||
CONSTRAINT `matching_orders_ibfk_1` FOREIGN KEY (`initiator_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 441 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for matching_records
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `matching_records`;
|
||||
CREATE TABLE `matching_records` (
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`matching_order_id` int NOT NULL,
|
||||
`user_id` int NOT NULL,
|
||||
`action` enum('join','confirm','reject','complete') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
|
||||
`amount` decimal(10, 2) NULL DEFAULT NULL,
|
||||
`note` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL,
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
INDEX `matching_order_id`(`matching_order_id` ASC) USING BTREE,
|
||||
INDEX `user_id`(`user_id` ASC) USING BTREE,
|
||||
CONSTRAINT `matching_records_ibfk_1` FOREIGN KEY (`matching_order_id`) REFERENCES `matching_orders` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT,
|
||||
CONSTRAINT `matching_records_ibfk_2` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 1841 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for order_allocations
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `order_allocations`;
|
||||
CREATE TABLE `order_allocations` (
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`matching_order_id` int NOT NULL,
|
||||
`from_user_id` int NOT NULL,
|
||||
`to_user_id` int NOT NULL,
|
||||
`amount` decimal(10, 2) NOT NULL,
|
||||
`cycle_number` int NOT NULL,
|
||||
`status` enum('pending','confirmed','rejected','completed') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'pending',
|
||||
`transfer_id` int NULL DEFAULT NULL,
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
`confirmed_at` timestamp NULL DEFAULT NULL,
|
||||
`outbound_date` date NULL DEFAULT NULL,
|
||||
`return_date` date NULL DEFAULT NULL,
|
||||
`can_return_after` timestamp NULL DEFAULT NULL,
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
INDEX `matching_order_id`(`matching_order_id` ASC) USING BTREE,
|
||||
INDEX `from_user_id`(`from_user_id` ASC) USING BTREE,
|
||||
INDEX `to_user_id`(`to_user_id` ASC) USING BTREE,
|
||||
INDEX `transfer_id`(`transfer_id` ASC) USING BTREE,
|
||||
CONSTRAINT `order_allocations_ibfk_1` FOREIGN KEY (`matching_order_id`) REFERENCES `matching_orders` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT,
|
||||
CONSTRAINT `order_allocations_ibfk_2` FOREIGN KEY (`from_user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT,
|
||||
CONSTRAINT `order_allocations_ibfk_3` FOREIGN KEY (`to_user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT,
|
||||
CONSTRAINT `order_allocations_ibfk_4` FOREIGN KEY (`transfer_id`) REFERENCES `transfers` (`id`) ON DELETE SET NULL ON UPDATE RESTRICT
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 1078 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for order_allocations_backup
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `order_allocations_backup`;
|
||||
CREATE TABLE `order_allocations_backup` (
|
||||
`id` int NOT NULL DEFAULT 0,
|
||||
`matching_order_id` int NOT NULL,
|
||||
`from_user_id` int NOT NULL,
|
||||
`to_user_id` int NOT NULL,
|
||||
`amount` decimal(10, 2) NOT NULL,
|
||||
`cycle_number` int NOT NULL,
|
||||
`status` enum('pending','confirmed','rejected','completed') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'pending',
|
||||
`transfer_id` int NULL DEFAULT NULL,
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
`confirmed_at` timestamp NULL DEFAULT NULL,
|
||||
`outbound_date` date NULL DEFAULT NULL,
|
||||
`return_date` date NULL DEFAULT NULL,
|
||||
`can_return_after` timestamp NULL DEFAULT NULL
|
||||
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for order_items
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `order_items`;
|
||||
CREATE TABLE `order_items` (
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`order_id` int NOT NULL,
|
||||
`product_id` int NOT NULL,
|
||||
`spec_combination_id` int NULL DEFAULT NULL COMMENT '规格组合ID',
|
||||
`quantity` int NOT NULL,
|
||||
`price` int NOT NULL,
|
||||
`points` int NOT NULL,
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`points_price` int NOT NULL DEFAULT 0,
|
||||
`rongdou_price` int NOT NULL DEFAULT 0,
|
||||
`rongdou` int NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
INDEX `order_id`(`order_id` ASC) USING BTREE,
|
||||
INDEX `product_id`(`product_id` ASC) USING BTREE,
|
||||
CONSTRAINT `order_items_ibfk_1` FOREIGN KEY (`order_id`) REFERENCES `orders` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
|
||||
CONSTRAINT `order_items_ibfk_2` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 8 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for orders
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `orders`;
|
||||
CREATE TABLE `orders` (
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`user_id` int NOT NULL,
|
||||
`order_no` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
|
||||
`total_amount` int NOT NULL,
|
||||
`total_points` int NOT NULL,
|
||||
`total_rongdou` int NOT NULL DEFAULT 0,
|
||||
`status` enum('pending','paid','shipped','delivered','cancelled','pre_order','completed') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'pending',
|
||||
`address` json NULL,
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
UNIQUE INDEX `order_no`(`order_no` ASC) USING BTREE,
|
||||
INDEX `user_id`(`user_id` ASC) USING BTREE,
|
||||
CONSTRAINT `orders_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 8 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for points_history
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `points_history`;
|
||||
CREATE TABLE `points_history` (
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`user_id` int NOT NULL,
|
||||
`type` enum('earn','spend','admin_adjust','refund') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
|
||||
`amount` int NOT NULL,
|
||||
`description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
|
||||
`order_id` int NULL DEFAULT NULL,
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
INDEX `user_id`(`user_id` ASC) USING BTREE,
|
||||
INDEX `order_id`(`order_id` ASC) USING BTREE,
|
||||
CONSTRAINT `points_history_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
|
||||
CONSTRAINT `points_history_ibfk_2` FOREIGN KEY (`order_id`) REFERENCES `orders` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 1273 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for product_attributes
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `product_attributes`;
|
||||
CREATE TABLE `product_attributes` (
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`product_id` int NOT NULL,
|
||||
`attribute_key` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||
`attribute_value` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||
`sort_order` int NULL DEFAULT 0,
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
INDEX `product_id`(`product_id` ASC) USING BTREE,
|
||||
CONSTRAINT `product_attributes_ibfk_1` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for product_favorites
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `product_favorites`;
|
||||
CREATE TABLE `product_favorites` (
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`user_id` int NOT NULL,
|
||||
`product_id` int NOT NULL,
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
UNIQUE INDEX `unique_user_product`(`user_id` ASC, `product_id` ASC) USING BTREE,
|
||||
INDEX `product_id`(`product_id` ASC) USING BTREE,
|
||||
CONSTRAINT `product_favorites_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT,
|
||||
CONSTRAINT `product_favorites_ibfk_2` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for product_reviews
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `product_reviews`;
|
||||
CREATE TABLE `product_reviews` (
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`product_id` int NOT NULL,
|
||||
`user_id` int NOT NULL,
|
||||
`order_id` int NOT NULL,
|
||||
`rating` int NOT NULL,
|
||||
`comment` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL,
|
||||
`images` json NULL,
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
INDEX `product_id`(`product_id` ASC) USING BTREE,
|
||||
INDEX `user_id`(`user_id` ASC) USING BTREE,
|
||||
INDEX `order_id`(`order_id` ASC) USING BTREE,
|
||||
CONSTRAINT `product_reviews_ibfk_1` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
|
||||
CONSTRAINT `product_reviews_ibfk_2` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
|
||||
CONSTRAINT `product_reviews_ibfk_3` FOREIGN KEY (`order_id`) REFERENCES `orders` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for product_spec_combinations
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `product_spec_combinations`;
|
||||
CREATE TABLE `product_spec_combinations` (
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`product_id` int NOT NULL COMMENT '商品ID',
|
||||
`combination_key` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '组合键,如:color_1-size_2-material_3',
|
||||
`spec_values` json NOT NULL COMMENT '规格值组合,存储spec_value_id数组',
|
||||
`price_adjustment` int NULL DEFAULT 0 COMMENT '价格调整',
|
||||
`points_adjustment` int NULL DEFAULT 0 COMMENT '积分调整',
|
||||
`rongdou_adjustment` int NULL DEFAULT 0 COMMENT '融豆调整',
|
||||
`stock` int NULL DEFAULT 0 COMMENT '库存',
|
||||
`sku_code` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'SKU编码',
|
||||
`barcode` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '条形码',
|
||||
`weight` decimal(8, 3) NULL DEFAULT NULL COMMENT '重量(kg)',
|
||||
`volume` decimal(10, 3) NULL DEFAULT NULL COMMENT '体积(cm³)',
|
||||
`status` enum('active','inactive') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'active',
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
UNIQUE INDEX `unique_product_combination`(`product_id` ASC, `combination_key` ASC) USING BTREE,
|
||||
INDEX `idx_product_status`(`product_id` ASC, `status` ASC) USING BTREE,
|
||||
INDEX `idx_sku_code`(`sku_code` ASC) USING BTREE,
|
||||
CONSTRAINT `product_spec_combinations_ibfk_1` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for product_spec_names
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `product_spec_names`;
|
||||
CREATE TABLE `product_spec_names` (
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`product_id` int NOT NULL COMMENT '商品ID',
|
||||
`spec_name_id` int NOT NULL COMMENT '规格名称ID',
|
||||
`is_required` tinyint(1) NULL DEFAULT 1 COMMENT '是否必选规格',
|
||||
`sort_order` int NULL DEFAULT 0 COMMENT '排序',
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
UNIQUE INDEX `unique_product_spec_name`(`product_id` ASC, `spec_name_id` ASC) USING BTREE,
|
||||
INDEX `spec_name_id`(`spec_name_id` ASC) USING BTREE,
|
||||
CONSTRAINT `product_spec_names_ibfk_1` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT,
|
||||
CONSTRAINT `product_spec_names_ibfk_2` FOREIGN KEY (`spec_name_id`) REFERENCES `spec_names` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for product_specifications
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `product_specifications`;
|
||||
CREATE TABLE `product_specifications` (
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`product_id` int NOT NULL,
|
||||
`spec_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||
`spec_value` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||
`price_adjustment` int NULL DEFAULT 0,
|
||||
`points_adjustment` int NULL DEFAULT 0,
|
||||
`rongdou_adjustment` int NULL DEFAULT 0,
|
||||
`stock` int NULL DEFAULT 0,
|
||||
`sku_code` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
|
||||
`status` enum('active','inactive') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'active',
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
INDEX `product_id`(`product_id` ASC) USING BTREE,
|
||||
CONSTRAINT `product_specifications_ibfk_1` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for products
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `products`;
|
||||
CREATE TABLE `products` (
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
|
||||
`description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL,
|
||||
`price` int NOT NULL,
|
||||
`original_price` int NULL DEFAULT NULL,
|
||||
`stock` int NULL DEFAULT 0,
|
||||
`sales` int NULL DEFAULT 0,
|
||||
`rating` decimal(3, 2) NULL DEFAULT 5.00,
|
||||
`category` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
|
||||
`images` json NULL,
|
||||
`status` enum('active','inactive') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'active',
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
`points_price` int NOT NULL DEFAULT 0,
|
||||
`image_url` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
|
||||
`details` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL,
|
||||
`rongdou_price` int NOT NULL DEFAULT 0,
|
||||
`videos` json NULL,
|
||||
`shop_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
|
||||
`shop_avatar` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
|
||||
`payment_methods` json NULL,
|
||||
PRIMARY KEY (`id`) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 18 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for regional_agents
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `regional_agents`;
|
||||
CREATE TABLE `regional_agents` (
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`user_id` int NOT NULL,
|
||||
`region_id` int NOT NULL,
|
||||
`agent_code` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||
`status` enum('pending','active','suspended','terminated') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'pending',
|
||||
`commission_rate` decimal(5, 4) NULL DEFAULT 0.0500,
|
||||
`total_earnings` decimal(10, 2) NULL DEFAULT 0.00,
|
||||
`recruited_merchants` int NULL DEFAULT 0,
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
`approved_at` timestamp NULL DEFAULT NULL,
|
||||
`approved_by_admin_id` int NULL DEFAULT NULL,
|
||||
`withdrawn_amount` decimal(10, 2) NULL DEFAULT 0.00 COMMENT '已提现金额',
|
||||
`pending_withdrawal` decimal(10, 2) NULL DEFAULT 0.00 COMMENT '待审核提现金额',
|
||||
`payment_type` enum('bank','wechat','alipay','unionpay') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'bank' COMMENT '收款方式类型',
|
||||
`account_number` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账号/银行账号',
|
||||
`account_holder` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '持有人姓名',
|
||||
`qr_code_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '收款码图片URL',
|
||||
`bank_account` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '银行账号(兼容旧版本)',
|
||||
`bank_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '银行名称',
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
UNIQUE INDEX `agent_code`(`agent_code` ASC) USING BTREE,
|
||||
UNIQUE INDEX `unique_agent_region`(`user_id` ASC, `region_id` ASC) USING BTREE,
|
||||
INDEX `region_id`(`region_id` ASC) USING BTREE,
|
||||
CONSTRAINT `regional_agents_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT,
|
||||
CONSTRAINT `regional_agents_ibfk_2` FOREIGN KEY (`region_id`) REFERENCES `zhejiang_regions` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for registration_codes
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `registration_codes`;
|
||||
CREATE TABLE `registration_codes` (
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`code` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '注册码',
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`expires_at` timestamp NOT NULL COMMENT '过期时间',
|
||||
`used_at` timestamp NULL DEFAULT NULL COMMENT '使用时间',
|
||||
`used_by_user_id` int NULL DEFAULT NULL COMMENT '使用该注册码的用户ID',
|
||||
`is_used` tinyint(1) NULL DEFAULT 0 COMMENT '是否已使用',
|
||||
`created_by_admin_id` int NOT NULL COMMENT '创建该注册码的管理员ID',
|
||||
`agent_id` int NULL DEFAULT NULL,
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
UNIQUE INDEX `code`(`code` ASC) USING BTREE,
|
||||
INDEX `idx_code`(`code` ASC) USING BTREE,
|
||||
INDEX `idx_expires_at`(`expires_at` ASC) USING BTREE,
|
||||
INDEX `idx_is_used`(`is_used` ASC) USING BTREE,
|
||||
INDEX `used_by_user_id`(`used_by_user_id` ASC) USING BTREE,
|
||||
INDEX `created_by_admin_id`(`created_by_admin_id` ASC) USING BTREE,
|
||||
INDEX `fk_registration_codes_agent_id`(`agent_id` ASC) USING BTREE,
|
||||
CONSTRAINT `fk_registration_codes_agent_id` FOREIGN KEY (`agent_id`) REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE RESTRICT,
|
||||
CONSTRAINT `registration_codes_ibfk_1` FOREIGN KEY (`used_by_user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE RESTRICT,
|
||||
CONSTRAINT `registration_codes_ibfk_2` FOREIGN KEY (`created_by_admin_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 150 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '注册码表' ROW_FORMAT = DYNAMIC;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for rongdou_history
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `rongdou_history`;
|
||||
CREATE TABLE `rongdou_history` (
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`user_id` int NOT NULL,
|
||||
`type` enum('earn','spend') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||
`amount` int NOT NULL,
|
||||
`description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
|
||||
`order_id` int NULL DEFAULT NULL,
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
INDEX `user_id`(`user_id` ASC) USING BTREE,
|
||||
INDEX `order_id`(`order_id` ASC) USING BTREE,
|
||||
CONSTRAINT `rongdou_history_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
|
||||
CONSTRAINT `rongdou_history_ibfk_2` FOREIGN KEY (`order_id`) REFERENCES `orders` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for spec_names
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `spec_names`;
|
||||
CREATE TABLE `spec_names` (
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '规格名称,如:颜色、尺寸、材质',
|
||||
`display_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '显示名称',
|
||||
`sort_order` int NULL DEFAULT 0 COMMENT '排序',
|
||||
`status` enum('active','inactive') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'active',
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
UNIQUE INDEX `unique_name`(`name` ASC) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for spec_values
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `spec_values`;
|
||||
CREATE TABLE `spec_values` (
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`spec_name_id` int NOT NULL COMMENT '规格名称ID',
|
||||
`value` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '规格值,如:红色、XL、棉质',
|
||||
`display_value` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '显示值',
|
||||
`color_code` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '颜色代码(仅颜色规格使用)',
|
||||
`image_url` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '规格图片',
|
||||
`sort_order` int NULL DEFAULT 0 COMMENT '排序',
|
||||
`status` enum('active','inactive') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'active',
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
UNIQUE INDEX `unique_spec_value`(`spec_name_id` ASC, `value` ASC) USING BTREE,
|
||||
CONSTRAINT `spec_values_ibfk_1` FOREIGN KEY (`spec_name_id`) REFERENCES `spec_names` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for system_settings
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `system_settings`;
|
||||
CREATE TABLE `system_settings` (
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`setting_key` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
|
||||
`setting_value` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL,
|
||||
`description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
UNIQUE INDEX `setting_key`(`setting_key` ASC) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 77 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for test_users
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `test_users`;
|
||||
CREATE TABLE `test_users` (
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||
`email` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` timestamp NULL DEFAULT NULL,
|
||||
PRIMARY KEY (`id`) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for transfer_confirmations
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `transfer_confirmations`;
|
||||
CREATE TABLE `transfer_confirmations` (
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`transfer_id` int NOT NULL,
|
||||
`confirmer_id` int NOT NULL,
|
||||
`action` enum('confirm','reject') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
|
||||
`note` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL,
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
INDEX `transfer_id`(`transfer_id` ASC) USING BTREE,
|
||||
INDEX `confirmer_id`(`confirmer_id` ASC) USING BTREE,
|
||||
CONSTRAINT `transfer_confirmations_ibfk_1` FOREIGN KEY (`transfer_id`) REFERENCES `transfers` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT,
|
||||
CONSTRAINT `transfer_confirmations_ibfk_2` FOREIGN KEY (`confirmer_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for transfers
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `transfers`;
|
||||
CREATE TABLE `transfers` (
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`from_user_id` int NULL DEFAULT NULL,
|
||||
`to_user_id` int NOT NULL,
|
||||
`amount` decimal(10, 2) NOT NULL,
|
||||
`transfer_type` enum('initial','return','user_to_user','system_to_user','user_to_system') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'user_to_user',
|
||||
`status` enum('pending','confirmed','rejected','received','not_received','cancelled') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'pending',
|
||||
`voucher_url` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
|
||||
`description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL,
|
||||
`batch_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
`deadline_at` timestamp NULL DEFAULT NULL COMMENT '转账截止时间',
|
||||
`is_overdue` tinyint(1) NULL DEFAULT 0 COMMENT '是否超时',
|
||||
`overdue_at` timestamp NULL DEFAULT NULL COMMENT '超时时间',
|
||||
`is_bad_debt` tinyint(1) NULL DEFAULT 0,
|
||||
`confirmed_at` timestamp NULL DEFAULT NULL,
|
||||
`admin_note` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL,
|
||||
`admin_modified_at` timestamp NULL DEFAULT NULL,
|
||||
`admin_modified_by` int NULL DEFAULT NULL,
|
||||
`source_type` enum('manual','allocation','system') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'manual' COMMENT '转账来源类型',
|
||||
`matching_order_id` int NULL DEFAULT NULL,
|
||||
`cycle_number` int NULL DEFAULT NULL,
|
||||
`outbound_date` date NULL DEFAULT NULL,
|
||||
`return_date` date NULL DEFAULT NULL,
|
||||
`can_return_after` timestamp NULL DEFAULT NULL,
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
INDEX `from_user_id`(`from_user_id` ASC) USING BTREE,
|
||||
INDEX `to_user_id`(`to_user_id` ASC) USING BTREE,
|
||||
CONSTRAINT `transfers_ibfk_1` FOREIGN KEY (`from_user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT,
|
||||
CONSTRAINT `transfers_ibfk_2` FOREIGN KEY (`to_user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 1529 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for user_addresses
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `user_addresses`;
|
||||
CREATE TABLE `user_addresses` (
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`user_id` int NOT NULL,
|
||||
`receiver_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||
`receiver_phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||
`province` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||
`city` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||
`district` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||
`detailed_address` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||
`postal_code` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
|
||||
`label` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '家',
|
||||
`is_default` tinyint(1) NULL DEFAULT 0,
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
INDEX `user_id`(`user_id` ASC) USING BTREE,
|
||||
CONSTRAINT `user_addresses_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for user_matching_pool
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `user_matching_pool`;
|
||||
CREATE TABLE `user_matching_pool` (
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`user_id` int NOT NULL,
|
||||
`available_amount` decimal(10, 2) NULL DEFAULT 0.00,
|
||||
`is_active` tinyint(1) NULL DEFAULT 1,
|
||||
`last_matched_at` timestamp NULL DEFAULT NULL,
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
UNIQUE INDEX `unique_user`(`user_id` ASC) USING BTREE,
|
||||
CONSTRAINT `user_matching_pool_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 61 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for users
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `users`;
|
||||
CREATE TABLE `users` (
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
|
||||
`email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
|
||||
`password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
|
||||
`role` enum('user','admin') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'user',
|
||||
`avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
`points` int NULL DEFAULT 0,
|
||||
`real_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
|
||||
`id_card` varchar(18) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
|
||||
`wechat_qr` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
|
||||
`alipay_qr` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
|
||||
`bank_card` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
|
||||
`unionpay_qr` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
|
||||
`phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
|
||||
`is_system_account` tinyint(1) NULL DEFAULT 0,
|
||||
`completed_withdrawals` int NULL DEFAULT 0,
|
||||
`balance` decimal(10, 2) NULL DEFAULT 0.00,
|
||||
`is_risk_user` tinyint(1) NULL DEFAULT 0 COMMENT '是否为风险用户',
|
||||
`is_blacklisted` tinyint(1) NULL DEFAULT 0 COMMENT '是否被拉黑',
|
||||
`risk_reason` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '风险原因',
|
||||
`blacklist_reason` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '拉黑原因',
|
||||
`blacklisted_at` timestamp NULL DEFAULT NULL COMMENT '拉黑时间',
|
||||
`business_license` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
|
||||
`id_card_front` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
|
||||
`id_card_back` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
|
||||
`audit_status` enum('pending','approved','rejected') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'pending',
|
||||
`audit_note` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL,
|
||||
`audited_by` int NULL DEFAULT NULL,
|
||||
`audited_at` timestamp NULL DEFAULT NULL,
|
||||
`city` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
|
||||
`district_id` int NULL DEFAULT NULL,
|
||||
`isdistribute` tinyint NULL DEFAULT 1,
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
UNIQUE INDEX `username`(`username` ASC) USING BTREE,
|
||||
UNIQUE INDEX `email`(`email` ASC) USING BTREE,
|
||||
UNIQUE INDEX `phone`(`phone` ASC) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 9788 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for zhejiang_regions
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `zhejiang_regions`;
|
||||
CREATE TABLE `zhejiang_regions` (
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`city_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||
`district_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||
`region_code` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
|
||||
`is_available` tinyint(1) NULL DEFAULT 1,
|
||||
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
UNIQUE INDEX `region_code`(`region_code` ASC) USING BTREE,
|
||||
UNIQUE INDEX `unique_region`(`city_name` ASC, `district_name` ASC) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 23234 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
|
||||
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
Reference in New Issue
Block a user