升级商城逻辑

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

3
.idea/vcs.xml generated
View File

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

16
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,16 @@
{
// 使用 IntelliSense 了解相关属性。
// 悬停以查看现有属性的描述。
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node-terminal",
"name": "Run Script: dev:server",
"request": "launch",
"command": "npm run dev:server",
"cwd": "${workspaceFolder}"
}
]
}

File diff suppressed because it is too large Load Diff

View File

@@ -53,6 +53,7 @@ async function createTables() {
role ENUM('user', 'admin') DEFAULT 'user', role ENUM('user', 'admin') DEFAULT 'user',
avatar VARCHAR(255), avatar VARCHAR(255),
points INT DEFAULT 0, points INT DEFAULT 0,
rongdou INT DEFAULT 0,
balance DECIMAL(10,2) DEFAULT 0.00, balance DECIMAL(10,2) DEFAULT 0.00,
real_name VARCHAR(100), real_name VARCHAR(100),
id_card VARCHAR(18), id_card VARCHAR(18),
@@ -111,7 +112,8 @@ async function createTables() {
order_no VARCHAR(50) UNIQUE NOT NULL, order_no VARCHAR(50) UNIQUE NOT NULL,
total_amount INT NOT NULL, total_amount INT NOT NULL,
total_points INT NOT NULL, total_points INT NOT NULL,
status ENUM('pending', 'paid', 'shipped', 'delivered', 'cancelled') DEFAULT 'pending', total_rongdou INT NOT NULL DEFAULT 0,
status ENUM('pending', 'paid', 'shipped', 'delivered', 'cancelled', 'pre_order') DEFAULT 'pending',
address JSON, address JSON,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
@@ -158,12 +160,18 @@ async function createTables() {
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
order_id INT NOT NULL, order_id INT NOT NULL,
product_id INT NOT NULL, product_id INT NOT NULL,
spec_combination_id INT NULL COMMENT '规格组合ID',
quantity INT NOT NULL, quantity INT NOT NULL,
price INT NOT NULL, price INT NOT NULL,
points INT NOT NULL, points INT NOT NULL,
points_price INT NOT NULL DEFAULT 0,
rongdou INT DEFAULT 0 COMMENT '融豆价格',
rongdou_price INT NOT NULL DEFAULT 0,
spec_info JSON COMMENT '规格信息快照',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (order_id) REFERENCES orders(id), FOREIGN KEY (order_id) REFERENCES orders(id),
FOREIGN KEY (product_id) REFERENCES products(id) FOREIGN KEY (product_id) REFERENCES products(id),
FOREIGN KEY (spec_combination_id) REFERENCES product_spec_combinations(id) ON DELETE SET NULL
) )
`); `);
@@ -182,6 +190,21 @@ async function createTables() {
) )
`); `);
// 融豆记录表
await getDB().execute(`
CREATE TABLE IF NOT EXISTS rongdou_history (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
type ENUM('earn', 'spend') NOT NULL,
amount INT NOT NULL,
description VARCHAR(255),
order_id INT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (order_id) REFERENCES orders(id)
)
`);
// 管理员操作日志表 // 管理员操作日志表
await getDB().execute(` await getDB().execute(`
CREATE TABLE IF NOT EXISTS admin_operation_logs ( CREATE TABLE IF NOT EXISTS admin_operation_logs (
@@ -213,22 +236,75 @@ async function createTables() {
) )
`); `);
// 商品规格表 // 规格名称表(规格维度)
await getDB().execute(` await getDB().execute(`
CREATE TABLE IF NOT EXISTS product_specifications ( CREATE TABLE IF NOT EXISTS spec_names (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
product_id INT NOT NULL, name VARCHAR(100) NOT NULL COMMENT '规格名称,如:颜色、尺寸、材质',
spec_name VARCHAR(100) NOT NULL, display_name VARCHAR(100) NOT NULL COMMENT '显示名称',
spec_value VARCHAR(100) NOT NULL, sort_order INT DEFAULT 0 COMMENT '排序',
price_adjustment INT DEFAULT 0,
points_adjustment INT DEFAULT 0,
rongdou_adjustment INT DEFAULT 0,
stock INT DEFAULT 0,
sku_code VARCHAR(100),
status ENUM('active', 'inactive') DEFAULT 'active', status ENUM('active', 'inactive') DEFAULT 'active',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE UNIQUE KEY unique_name (name)
)
`);
// 规格值表
await getDB().execute(`
CREATE TABLE IF NOT EXISTS spec_values (
id INT AUTO_INCREMENT PRIMARY KEY,
spec_name_id INT NOT NULL COMMENT '规格名称ID',
value VARCHAR(100) NOT NULL COMMENT '规格值红色、XL、棉质',
display_value VARCHAR(100) NOT NULL COMMENT '显示值',
color_code VARCHAR(20) COMMENT '颜色代码(仅颜色规格使用)',
image_url VARCHAR(500) COMMENT '规格图片',
sort_order INT DEFAULT 0 COMMENT '排序',
status ENUM('active', 'inactive') DEFAULT 'active',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (spec_name_id) REFERENCES spec_names(id) ON DELETE CASCADE,
UNIQUE KEY unique_spec_value (spec_name_id, value)
)
`);
// 商品规格组合表(笛卡尔积结果)
await getDB().execute(`
CREATE TABLE IF NOT EXISTS product_spec_combinations (
id INT AUTO_INCREMENT PRIMARY KEY,
product_id INT NOT NULL COMMENT '商品ID',
combination_key VARCHAR(255) NOT NULL COMMENT '组合键color_1-size_2-material_3',
spec_values JSON NOT NULL COMMENT '规格值组合存储spec_value_id数组',
price_adjustment INT DEFAULT 0 COMMENT '价格调整',
points_adjustment INT DEFAULT 0 COMMENT '积分调整',
rongdou_adjustment INT DEFAULT 0 COMMENT '融豆调整',
stock INT DEFAULT 0 COMMENT '库存',
sku_code VARCHAR(100) COMMENT 'SKU编码',
barcode VARCHAR(100) COMMENT '条形码',
weight DECIMAL(8,3) COMMENT '重量kg',
volume DECIMAL(10,3) COMMENT '体积cm³',
status ENUM('active', 'inactive') DEFAULT 'active',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE,
UNIQUE KEY unique_product_combination (product_id, combination_key),
INDEX idx_product_status (product_id, status),
INDEX idx_sku_code (sku_code)
)
`);
// 商品规格关联表(定义商品使用哪些规格维度)
await getDB().execute(`
CREATE TABLE IF NOT EXISTS product_spec_names (
id INT AUTO_INCREMENT PRIMARY KEY,
product_id INT NOT NULL COMMENT '商品ID',
spec_name_id INT NOT NULL COMMENT '规格名称ID',
is_required BOOLEAN DEFAULT TRUE COMMENT '是否必选规格',
sort_order INT DEFAULT 0 COMMENT '排序',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE,
FOREIGN KEY (spec_name_id) REFERENCES spec_names(id) ON DELETE CASCADE,
UNIQUE KEY unique_product_spec_name (product_id, spec_name_id)
) )
`); `);
@@ -259,6 +335,23 @@ async function createTables() {
) )
`); `);
// 购物车表
await getDB().execute(`
CREATE TABLE IF NOT EXISTS cart_items (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
product_id INT NOT NULL,
quantity INT NOT NULL DEFAULT 1,
spec_combination_id INT NULL COMMENT '规格组合ID',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE,
FOREIGN KEY (spec_combination_id) REFERENCES product_spec_combinations(id) ON DELETE CASCADE,
UNIQUE KEY unique_user_product_spec (user_id, product_id, spec_combination_id)
)
`);
// 用户收货地址表 // 用户收货地址表
await getDB().execute(` await getDB().execute(`
CREATE TABLE IF NOT EXISTS user_addresses ( CREATE TABLE IF NOT EXISTS user_addresses (

View File

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

79
docs/README.md Normal file
View File

@@ -0,0 +1,79 @@
# API 文档结构说明
本项目已将 Swagger API 文档从路由文件中分离出来,采用模块化的文档管理方式。
## 文件夹结构
```
docs/
├── README.md # 本说明文件
├── schemas/ # 数据模型定义
│ ├── product.js # 商品相关数据模型
│ ├── order.js # 订单相关数据模型
│ ├── user.js # 用户相关数据模型
│ └── cart.js # 购物车相关数据模型
└── apis/ # API 接口定义
├── products.js # 商品相关 API
└── orders.js # 订单相关 API
```
## 优势
1. **模块化管理**: 按功能模块分离文档,便于维护和查找
2. **代码分离**: 路由文件专注于业务逻辑,文档定义独立管理
3. **复用性**: Schema 定义可以在多个 API 中复用
4. **可维护性**: 文档修改不会影响业务代码,降低出错风险
## 使用方法
### 添加新的 Schema
`schemas/` 文件夹中创建新的 `.js` 文件,使用 `@swagger` 注释定义数据模型:
```javascript
/**
* @swagger
* components:
* schemas:
* YourModel:
* type: object
* properties:
* id:
* type: integer
* description: ID
*/
```
### 添加新的 API 文档
`apis/` 文件夹中创建新的 `.js` 文件,使用 `@swagger` 注释定义 API 接口:
```javascript
/**
* @swagger
* tags:
* name: YourModule
* description: 模块描述
*/
/**
* @swagger
* /your-endpoint:
* get:
* summary: 接口描述
* tags: [YourModule]
* responses:
* 200:
* description: 成功响应
*/
```
## 配置
Swagger 配置文件 `swagger.js` 已更新扫描路径:
```javascript
apis: ['./docs/schemas/*.js', './docs/apis/*.js', './routes/*.js', './admin/routes/*.js']
```
这样既保持了对现有路由文件中文档的兼容性,又支持新的模块化文档结构。

236
docs/apis/orders.js Normal file
View File

@@ -0,0 +1,236 @@
/**
* @swagger
* tags:
* name: Orders
* description: 订单管理API
*/
/**
* @swagger
* /orders/create-from-cart:
* post:
* summary: 从购物车创建预订单
* tags: [Orders]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - cartIds
* properties:
* cartIds:
* type: array
* items:
* type: integer
* description: 购物车商品ID数组
* responses:
* 200:
* description: 成功创建预订单
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* $ref: '#/components/schemas/PreOrder'
* 400:
* description: 请求参数错误
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* message:
* type: string
*/
/**
* @swagger
* /orders/pre-order/{id}:
* get:
* summary: 获取预订单详情
* tags: [Orders]
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: 预订单ID
* responses:
* 200:
* description: 成功获取预订单详情
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
* preOrder:
* $ref: '#/components/schemas/PreOrder'
* items:
* type: array
* items:
* type: object
* properties:
* product_id:
* type: integer
* product_name:
* type: string
* quantity:
* type: integer
* points_price:
* type: integer
* rongdou_price:
* type: number
* image_url:
* type: string
* 404:
* description: 预订单不存在
*/
/**
* @swagger
* /orders/confirm:
* post:
* summary: 确认下单
* tags: [Orders]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - preOrderId
* - shippingAddress
* properties:
* preOrderId:
* type: integer
* description: 预订单ID
* shippingAddress:
* type: string
* description: 收货地址
* responses:
* 200:
* description: 订单确认成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
* orderId:
* type: integer
* orderNumber:
* type: string
* 400:
* description: 请求参数错误或余额不足
* 404:
* description: 预订单不存在
*/
/**
* @swagger
* /orders:
* get:
* summary: 获取用户订单列表
* tags: [Orders]
* parameters:
* - in: query
* name: page
* schema:
* type: integer
* default: 1
* description: 页码
* - in: query
* name: limit
* schema:
* type: integer
* default: 10
* description: 每页数量
* - in: query
* name: status
* schema:
* type: string
* enum: [pending, confirmed, shipped, delivered, cancelled]
* description: 订单状态筛选
* responses:
* 200:
* description: 成功获取订单列表
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
* orders:
* type: array
* items:
* $ref: '#/components/schemas/Order'
* pagination:
* type: object
* properties:
* page:
* type: integer
* limit:
* type: integer
* total:
* type: integer
* pages:
* type: integer
*/
/**
* @swagger
* /orders/{id}:
* get:
* summary: 获取订单详情
* tags: [Orders]
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: 订单ID
* responses:
* 200:
* description: 成功获取订单详情
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
* order:
* $ref: '#/components/schemas/Order'
* items:
* type: array
* items:
* $ref: '#/components/schemas/OrderItem'
* 404:
* description: 订单不存在
*/

154
docs/apis/products.js Normal file
View File

@@ -0,0 +1,154 @@
/**
* @swagger
* tags:
* name: Products
* description: 商品管理API
*/
/**
* @swagger
* /products:
* get:
* summary: 获取商品列表
* tags: [Products]
* parameters:
* - in: query
* name: page
* schema:
* type: integer
* default: 1
* description: 页码
* - in: query
* name: limit
* schema:
* type: integer
* default: 10
* description: 每页数量
* - in: query
* name: search
* schema:
* type: string
* description: 搜索关键词
* - in: query
* name: category
* schema:
* type: string
* description: 商品分类
* - in: query
* name: status
* schema:
* type: string
* enum: [active, inactive]
* description: 商品状态
* responses:
* 200:
* description: 成功获取商品列表
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
* products:
* type: array
* items:
* $ref: '#/components/schemas/Product'
* pagination:
* type: object
* properties:
* page:
* type: integer
* limit:
* type: integer
* total:
* type: integer
* pages:
* type: integer
*/
/**
* @swagger
* /products/categories:
* get:
* summary: 获取商品分类列表
* tags: [Products]
* responses:
* 200:
* description: 成功获取分类列表
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: array
* items:
* type: string
*/
/**
* @swagger
* /products/hot:
* get:
* summary: 获取热门商品
* tags: [Products]
* parameters:
* - in: query
* name: limit
* schema:
* type: integer
* default: 10
* description: 返回数量
* responses:
* 200:
* description: 成功获取热门商品
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
* products:
* type: array
* items:
* $ref: '#/components/schemas/Product'
*/
/**
* @swagger
* /products/{id}:
* get:
* summary: 获取商品详情
* tags: [Products]
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: 商品ID
* responses:
* 200:
* description: 成功获取商品详情
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* $ref: '#/components/schemas/Product'
* 404:
* description: 商品不存在
*/

93
docs/schemas/cart.js Normal file
View File

@@ -0,0 +1,93 @@
/**
* @swagger
* components:
* schemas:
* CartItem:
* type: object
* required:
* - user_id
* - product_id
* - quantity
* properties:
* id:
* type: integer
* description: 购物车商品ID
* user_id:
* type: integer
* description: 用户ID
* product_id:
* type: integer
* description: 商品ID
* quantity:
* type: integer
* description: 商品数量
* created_at:
* type: string
* format: date-time
* description: 创建时间
* updated_at:
* type: string
* format: date-time
* description: 更新时间
*
* CartItemWithProduct:
* type: object
* properties:
* id:
* type: integer
* description: 购物车商品ID
* product_id:
* type: integer
* description: 商品ID
* product_name:
* type: string
* description: 商品名称
* quantity:
* type: integer
* description: 商品数量
* points_price:
* type: integer
* description: 积分价格
* rongdou_price:
* type: number
* description: 融豆价格
* image_url:
* type: string
* description: 商品图片URL
* stock:
* type: integer
* description: 库存数量
* payment_methods:
* type: array
* items:
* type: string
* description: 支付方式列表
* created_at:
* type: string
* format: date-time
* description: 创建时间
*
* AddToCartRequest:
* type: object
* required:
* - product_id
* - quantity
* properties:
* product_id:
* type: integer
* description: 商品ID
* quantity:
* type: integer
* minimum: 1
* description: 商品数量
*
* UpdateCartRequest:
* type: object
* required:
* - quantity
* properties:
* quantity:
* type: integer
* minimum: 1
* description: 商品数量
*/

102
docs/schemas/order.js Normal file
View File

@@ -0,0 +1,102 @@
/**
* @swagger
* components:
* schemas:
* Order:
* type: object
* required:
* - user_id
* - total_amount
* - status
* properties:
* id:
* type: integer
* description: 订单ID
* order_number:
* type: string
* description: 订单号
* user_id:
* type: integer
* description: 用户ID
* total_amount:
* type: number
* description: 订单总金额
* total_points:
* type: integer
* description: 订单总积分
* total_rongdou:
* type: number
* description: 订单总融豆
* status:
* type: string
* description: 订单状态
* enum: [pending, confirmed, shipped, delivered, cancelled]
* payment_status:
* type: string
* description: 支付状态
* enum: [pending, paid, failed, refunded]
* shipping_address:
* type: string
* description: 收货地址
* created_at:
* type: string
* format: date-time
* description: 创建时间
* updated_at:
* type: string
* format: date-time
* description: 更新时间
*
* OrderItem:
* type: object
* properties:
* id:
* type: integer
* description: 订单商品ID
* order_id:
* type: integer
* description: 订单ID
* product_id:
* type: integer
* description: 商品ID
* quantity:
* type: integer
* description: 商品数量
* price:
* type: number
* description: 商品价格
* points_price:
* type: integer
* description: 积分价格
* rongdou_price:
* type: number
* description: 融豆价格
* created_at:
* type: string
* format: date-time
* description: 创建时间
*
* PreOrder:
* type: object
* properties:
* preOrderId:
* type: integer
* description: 预订单ID
* orderNumber:
* type: string
* description: 订单号
* totalAmount:
* type: number
* description: 总金额
* totalPoints:
* type: integer
* description: 所需积分总数
* totalRongdou:
* type: number
* description: 所需融豆总数
* paymentMethods:
* type: array
* items:
* type: string
* description: 去重后的支付方式列表
*/

53
docs/schemas/product.js Normal file
View File

@@ -0,0 +1,53 @@
/**
* @swagger
* components:
* schemas:
* Product:
* type: object
* required:
* - name
* - points_price
* - stock
* properties:
* id:
* type: integer
* description: 商品ID
* name:
* type: string
* description: 商品名称
* category:
* type: string
* description: 商品分类
* points_price:
* type: integer
* description: 积分价格
* rongdou_price:
* type: number
* description: 融豆价格
* stock:
* type: integer
* description: 库存数量
* image_url:
* type: string
* description: 商品图片URL
* description:
* type: string
* description: 商品描述
* status:
* type: string
* description: 商品状态
* enum: [active, inactive]
* payment_methods:
* type: array
* items:
* type: string
* description: 支付方式列表
* created_at:
* type: string
* format: date-time
* description: 创建时间
* updated_at:
* type: string
* format: date-time
* description: 更新时间
*/

104
docs/schemas/user.js Normal file
View File

@@ -0,0 +1,104 @@
/**
* @swagger
* components:
* schemas:
* User:
* type: object
* required:
* - username
* - email
* properties:
* id:
* type: integer
* description: 用户ID
* username:
* type: string
* description: 用户名
* email:
* type: string
* format: email
* description: 邮箱地址
* phone:
* type: string
* description: 手机号码
* points:
* type: integer
* description: 积分余额
* rongdou:
* type: number
* description: 融豆余额
* avatar:
* type: string
* description: 头像URL
* status:
* type: string
* description: 用户状态
* enum: [active, inactive, banned]
* created_at:
* type: string
* format: date-time
* description: 创建时间
* updated_at:
* type: string
* format: date-time
* description: 更新时间
*
* UserProfile:
* type: object
* properties:
* id:
* type: integer
* description: 用户ID
* username:
* type: string
* description: 用户名
* email:
* type: string
* description: 邮箱地址
* phone:
* type: string
* description: 手机号码
* points:
* type: integer
* description: 积分余额
* rongdou:
* type: number
* description: 融豆余额
* avatar:
* type: string
* description: 头像URL
*
* LoginRequest:
* type: object
* required:
* - username
* - password
* properties:
* username:
* type: string
* description: 用户名或邮箱
* password:
* type: string
* description: 密码
*
* RegisterRequest:
* type: object
* required:
* - username
* - email
* - password
* properties:
* username:
* type: string
* description: 用户名
* email:
* type: string
* format: email
* description: 邮箱地址
* password:
* type: string
* description: 密码
* phone:
* type: string
* description: 手机号码
*/

View File

@@ -4,11 +4,11 @@
"description": "Vue3 + Node.js 集成系统", "description": "Vue3 + Node.js 集成系统",
"main": "server.js", "main": "server.js",
"scripts": { "scripts": {
"dev": "concurrently \"npm run dev:frontend\" \"npm run dev:admin\" \"npm run dev:server\"", "dev": "concurrently \"npm run dev:admin\" \"npm run dev:server\"",
"dev:frontend": "cd frontend && npm run dev", "dev:frontend": "cd frontend && npm run dev",
"dev:admin": "cd admin && npm run dev", "dev:admin": "cd admin && npm run dev",
"dev:server": "nodemon server.js", "dev:server": "nodemon server.js",
"build": "npm run build:frontend && npm run build:admin", "build": " npm run build:admin",
"build:frontend": "cd frontend && npm run build", "build:frontend": "cd frontend && npm run build",
"build:admin": "cd admin && npm run build", "build:admin": "cd admin && npm run build",
"start": "node server.js" "start": "node server.js"

View File

@@ -100,16 +100,20 @@ const { auth } = require('../middleware/auth');
router.get('/', auth, async (req, res) => { router.get('/', auth, async (req, res) => {
try { try {
const userId = req.user.id; const userId = req.user.id;
const [addresses] = await getDB().execute( const [addresses] = await getDB().execute(
`SELECT ua.*, al.name as label_name, al.color as label_color `SELECT ua.*, al.name as label_name, al.color as label_color,
p.name as province_name, c.name as city_name, d.name as district_name
FROM user_addresses ua FROM user_addresses ua
LEFT JOIN address_labels al ON ua.label_id = al.id LEFT JOIN address_labels al ON ua.label = al.id
WHERE ua.user_id = ? AND ua.deleted_at IS NULL LEFT JOIN china_regions p ON ua.province = p.code
LEFT JOIN china_regions c ON ua.city = c.code
LEFT JOIN china_regions d ON ua.district = d.code
WHERE ua.user_id = ?
ORDER BY ua.is_default DESC, ua.created_at DESC`, ORDER BY ua.is_default DESC, ua.created_at DESC`,
[userId] [userId]
); );
res.json({ res.json({
success: true, success: true,
data: addresses data: addresses
@@ -158,7 +162,7 @@ router.get('/:id', auth, async (req, res) => {
try { try {
const addressId = req.params.id; const addressId = req.params.id;
const userId = req.user.id; const userId = req.user.id;
const [addresses] = await getDB().execute( const [addresses] = await getDB().execute(
`SELECT ua.*, al.name as label_name, al.color as label_color `SELECT ua.*, al.name as label_name, al.color as label_color
FROM user_addresses ua FROM user_addresses ua
@@ -166,11 +170,11 @@ router.get('/:id', auth, async (req, res) => {
WHERE ua.id = ? AND ua.user_id = ? AND ua.deleted_at IS NULL`, WHERE ua.id = ? AND ua.user_id = ? AND ua.deleted_at IS NULL`,
[addressId, userId] [addressId, userId]
); );
if (addresses.length === 0) { if (addresses.length === 0) {
return res.status(404).json({ message: '收货地址不存在' }); return res.status(404).json({ message: '收货地址不存在' });
} }
res.json({ res.json({
success: true, success: true,
data: addresses[0] data: addresses[0]
@@ -259,41 +263,36 @@ router.post('/', auth, async (req, res) => {
recipient_name, recipient_name,
phone, phone,
province_code, province_code,
province_name,
city_code, city_code,
city_name,
district_code, district_code,
district_name,
detailed_address, detailed_address,
postal_code,
label_id,
is_default = false is_default = false
} = req.body; } = req.body;
// 验证必填字段 // 验证必填字段
if (!recipient_name || !phone || !province_code || !city_code || !district_code || !detailed_address) { if (!recipient_name || !phone || !province_code || !city_code || !district_code || !detailed_address) {
return res.status(400).json({ message: '收件人姓名、电话、省市区和详细地址不能为空' }); return res.status(400).json({ message: '收件人姓名、电话、省市区和详细地址不能为空' });
} }
// 如果设置为默认地址,先取消其他默认地址 // 如果设置为默认地址,先取消其他默认地址
if (is_default) { if (is_default) {
await getDB().execute( await getDB().execute(
'UPDATE user_addresses SET is_default = false WHERE user_id = ? AND deleted_at IS NULL', 'UPDATE user_addresses SET is_default = false WHERE user_id = ? ',
[userId] [userId]
); );
} }
const [result] = await getDB().execute( const [result] = await getDB().execute(
`INSERT INTO user_addresses ( `INSERT INTO user_addresses (
user_id, recipient_name, phone, province_code, province_name, city_code, city_name, user_id, receiver_name, receiver_phone, province, city,
district_code, district_name, detailed_address, postal_code, label_id, is_default, created_at, updated_at district, detailed_address, is_default, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())`, ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())`,
[ [
userId, recipient_name, phone, province_code, province_name, city_code, city_name, userId, recipient_name, phone, province_code, city_code,
district_code, district_name, detailed_address, postal_code, label_id, is_default district_code, detailed_address, is_default
] ]
); );
res.status(201).json({ res.status(201).json({
success: true, success: true,
message: '收货地址创建成功', message: '收货地址创建成功',
@@ -383,48 +382,45 @@ router.put('/:id', auth, async (req, res) => {
recipient_name, recipient_name,
phone, phone,
province_code, province_code,
province_name,
city_code, city_code,
city_name,
district_code, district_code,
district_name,
detailed_address, detailed_address,
postal_code,
label_id,
is_default is_default
} = req.body; } = req.body;
if (!recipient_name || !phone || !province_code || !city_code || !district_code || !detailed_address) {
return res.status(400).json({ message: '收件人姓名、电话、省市区和详细地址不能为空' });
}
// 检查地址是否存在且属于当前用户 // 检查地址是否存在且属于当前用户
const [existing] = await getDB().execute( const [existing] = await getDB().execute(
'SELECT id FROM user_addresses WHERE id = ? AND user_id = ? AND deleted_at IS NULL', 'SELECT id FROM user_addresses WHERE id = ? AND user_id = ? ',
[addressId, userId] [addressId, userId]
); );
if (existing.length === 0) { if (existing.length === 0) {
return res.status(404).json({ message: '收货地址不存在' }); return res.status(404).json({ message: '收货地址不存在' });
} }
// 如果设置为默认地址,先取消其他默认地址 // 如果设置为默认地址,先取消其他默认地址
if (is_default) { if (is_default) {
await getDB().execute( await getDB().execute(
'UPDATE user_addresses SET is_default = false WHERE user_id = ? AND id != ? AND deleted_at IS NULL', 'UPDATE user_addresses SET is_default = false WHERE user_id = ? AND id != ? ',
[userId, addressId] [userId, addressId]
); );
} }
const [result] = await getDB().execute( const [result] = await getDB().execute(
`UPDATE user_addresses SET `UPDATE user_addresses SET
recipient_name = ?, phone = ?, province_code = ?, province_name = ?, receiver_name = ?, receiver_phone = ?, province = ?, city = ?,
city_code = ?, city_name = ?, district_code = ?, district_name = ?, district = ?, detailed_address = ?, is_default = ?, updated_at = NOW()
detailed_address = ?, postal_code = ?, label_id = ?, is_default = ?, updated_at = NOW()
WHERE id = ? AND user_id = ?`, WHERE id = ? AND user_id = ?`,
[ [
recipient_name, phone, province_code, province_name, city_code, city_name, recipient_name, phone, province_code, city_code,
district_code, district_name, detailed_address, postal_code, label_id, is_default, district_code, detailed_address, is_default,
addressId, userId addressId, userId
] ]
); );
res.json({ res.json({
success: true, success: true,
message: '收货地址更新成功' message: '收货地址更新成功'
@@ -439,7 +435,7 @@ router.put('/:id', auth, async (req, res) => {
* @swagger * @swagger
* /addresses/{id}: * /addresses/{id}:
* delete: * delete:
* summary: 删除收货地址(软删除) * summary: 删除收货地址
* tags: [Addresses] * tags: [Addresses]
* security: * security:
* - bearerAuth: [] * - bearerAuth: []
@@ -475,16 +471,16 @@ router.delete('/:id', auth, async (req, res) => {
try { try {
const addressId = req.params.id; const addressId = req.params.id;
const userId = req.user.id; const userId = req.user.id;
const [result] = await getDB().execute( const [result] = await getDB().execute(
'UPDATE user_addresses SET deleted_at = NOW() WHERE id = ? AND user_id = ? AND deleted_at IS NULL', 'DELETE FROM user_addresses WHERE id = ? AND user_id = ?',
[addressId, userId] [addressId, userId]
); );
if (result.affectedRows === 0) { if (result.affectedRows === 0) {
return res.status(404).json({ message: '收货地址不存在' }); return res.status(404).json({ message: '收货地址不存在' });
} }
res.json({ res.json({
success: true, success: true,
message: '收货地址删除成功' message: '收货地址删除成功'
@@ -535,29 +531,29 @@ router.put('/:id/default', auth, async (req, res) => {
try { try {
const addressId = req.params.id; const addressId = req.params.id;
const userId = req.user.id; const userId = req.user.id;
// 检查地址是否存在且属于当前用户 // 检查地址是否存在且属于当前用户
const [existing] = await getDB().execute( const [existing] = await getDB().execute(
'SELECT id FROM user_addresses WHERE id = ? AND user_id = ? AND deleted_at IS NULL', 'SELECT id FROM user_addresses WHERE id = ? AND user_id = ? AND deleted_at IS NULL',
[addressId, userId] [addressId, userId]
); );
if (existing.length === 0) { if (existing.length === 0) {
return res.status(404).json({ message: '收货地址不存在' }); return res.status(404).json({ message: '收货地址不存在' });
} }
// 取消其他默认地址 // 取消其他默认地址
await getDB().execute( await getDB().execute(
'UPDATE user_addresses SET is_default = false WHERE user_id = ? AND deleted_at IS NULL', 'UPDATE user_addresses SET is_default = false WHERE user_id = ? AND deleted_at IS NULL',
[userId] [userId]
); );
// 设置当前地址为默认 // 设置当前地址为默认
await getDB().execute( await getDB().execute(
'UPDATE user_addresses SET is_default = true WHERE id = ? AND user_id = ?', 'UPDATE user_addresses SET is_default = true WHERE id = ? AND user_id = ?',
[addressId, userId] [addressId, userId]
); );
res.json({ res.json({
success: true, success: true,
message: '默认地址设置成功' message: '默认地址设置成功'

935
routes/cart.js Normal file
View File

@@ -0,0 +1,935 @@
const express = require('express');
const { getDB } = require('../database');
const { auth } = require('../middleware/auth');
const router = express.Router();
/**
* @swagger
* tags:
* name: Cart
* description: 购物车管理相关接口
*/
/**
* @swagger
* components:
* schemas:
* CartItem:
* type: object
* properties:
* id:
* type: integer
* description: 购物车项ID
* user_id:
* type: integer
* description: 用户ID
* product_id:
* type: integer
* description: 商品ID
* quantity:
* type: integer
* description: 商品数量
* spec_combination_id:
* type: integer
* description: 商品规格组合ID
* created_at:
* type: string
* format: date-time
* description: 创建时间
* updated_at:
* type: string
* format: date-time
* description: 更新时间
* product:
* type: object
* properties:
* id:
* type: integer
* name:
* type: string
* price:
* type: integer
* points_price:
* type: integer
* rongdou_price:
* type: integer
* image_url:
* type: string
* stock:
* type: integer
* status:
* type: string
*/
/**
* @swagger
* /api/cart:
* get:
* summary: 获取购物车列表
* tags: [Cart]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: 获取购物车成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
* items:
* type: array
* items:
* $ref: '#/components/schemas/CartItem'
* total_count:
* type: integer
* description: 购物车商品总数量
* total_amount:
* type: integer
* description: 购物车总金额
* total_points:
* type: integer
* description: 购物车总积分
* total_rongdou:
* type: integer
* description: 购物车总融豆
* 401:
* description: 未授权
* 500:
* description: 服务器错误
*/
router.get('/', auth, async (req, res) => {
try {
const userId = req.user.id;
// 获取购物车商品列表
const query = `
SELECT
c.id, c.user_id, c.product_id, c.quantity, c.specification_id,
c.created_at, c.updated_at,
p.name, p.price, p.points_price, p.rongdou_price, p.image_url,
p.stock, p.status, p.shop_name, p.shop_avatar,
psc.combination_key, psc.price_adjustment,
psc.points_adjustment, psc.rongdou_adjustment, psc.stock as spec_stock,
GROUP_CONCAT(CONCAT(sn.display_name, ':', sv.display_value) ORDER BY sn.sort_order SEPARATOR ' | ') as spec_display
FROM cart_items c
LEFT JOIN products p ON c.product_id = p.id
LEFT JOIN product_spec_combinations psc ON c.specification_id = psc.id
LEFT JOIN JSON_TABLE(psc.spec_values, '$[*]' COLUMNS (spec_value_id INT PATH '$')) jt ON psc.id IS NOT NULL
LEFT JOIN spec_values sv ON jt.spec_value_id = sv.id
LEFT JOIN spec_names sn ON sv.spec_name_id = sn.id
WHERE c.user_id = ? AND p.status = 'active'
GROUP BY c.id
ORDER BY c.created_at DESC
`;
const [cartItems] = await getDB().execute(query, [userId]);
// 计算总计信息
let totalCount = 0;
let totalAmount = 0;
let totalPoints = 0;
let totalRongdou = 0;
const items = cartItems.map(item => {
const finalPrice = item.price + (item.price_adjustment || 0);
const finalPointsPrice = item.points_price + (item.points_adjustment || 0);
const finalRongdouPrice = item.rongdou_price + (item.rongdou_adjustment || 0);
totalCount += item.quantity;
totalAmount += finalPrice * item.quantity;
totalPoints += finalPointsPrice * item.quantity;
totalRongdou += finalRongdouPrice * item.quantity;
return {
id: item.id,
user_id: item.user_id,
product_id: item.product_id,
quantity: item.quantity,
spec_combination_id: item.spec_combination_id,
created_at: item.created_at,
updated_at: item.updated_at,
product: {
id: item.product_id,
name: item.name,
price: finalPrice,
points_price: finalPointsPrice,
rongdou_price: finalRongdouPrice,
image_url: item.image_url,
stock: item.spec_combination_id ? item.spec_stock : item.stock,
status: item.status,
shop_name: item.shop_name,
shop_avatar: item.shop_avatar
},
specification: item.spec_combination_id ? {
id: item.spec_combination_id,
combination_key: item.combination_key,
spec_display: item.spec_display,
price_adjustment: item.price_adjustment,
points_adjustment: item.points_adjustment,
rongdou_adjustment: item.rongdou_adjustment
} : null
};
});
res.json({
success: true,
data: {
items,
total_count: totalCount,
total_amount: totalAmount,
total_points: totalPoints,
total_rongdou: totalRongdou
}
});
} catch (error) {
console.error('获取购物车失败:', error);
res.status(500).json({ success: false, message: '获取购物车失败' });
}
});
/**
* @swagger
* /api/cart:
* post:
* summary: 添加商品到购物车
* tags: [Cart]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* product_id:
* type: integer
* description: 商品ID
* quantity:
* type: integer
* description: 商品数量
* minimum: 1
* spec_combination_id:
* type: integer
* description: 商品规格组合ID可选
* required:
* - product_id
* - quantity
* responses:
* 201:
* description: 添加到购物车成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* message:
* type: string
* data:
* type: object
* properties:
* cart_item_id:
* type: integer
* 400:
* description: 参数错误或库存不足
* 401:
* description: 未授权
* 404:
* description: 商品不存在或已下架
* 500:
* description: 服务器错误
*/
router.post('/add', auth, async (req, res) => {
const db = getDB();
await db.query('START TRANSACTION');
try {
const { productId, quantity, specificationId } = req.body;
const userId = req.user.id;
// 验证必填字段
if (!productId || !quantity || quantity < 1) {
await db.query('ROLLBACK');
return res.status(400).json({ success: false, message: '请填写正确的商品信息和数量' });
}
// 检查商品是否存在且有效
const [products] = await db.execute(
'SELECT id, name, stock, status FROM products WHERE id = ?',
[productId]
);
if (products.length === 0 || products[0].status !== 'active') {
await db.query('ROLLBACK');
return res.status(404).json({ success: false, message: '商品不存在或已下架' });
}
const product = products[0];
let availableStock = product.stock;
// 如果指定了规格组合,检查规格组合库存
if (specificationId) {
const [specs] = await db.execute(
'SELECT id, stock, status FROM product_spec_combinations WHERE id = ? AND product_id = ?',
[specificationId, productId]
);
if (specs.length === 0 || specs[0].status !== 'active') {
await db.query('ROLLBACK');
return res.status(404).json({ success: false, message: '商品规格组合不存在或已下架' });
}
availableStock = specs[0].stock;
}
// 检查购物车中是否已存在相同商品和规格组合
const [existingItems] = await db.execute(
'SELECT id, quantity FROM cart_items WHERE user_id = ? AND product_id = ? AND (specification_id = ? OR (specification_id IS NULL AND ? IS NULL))',
[userId, productId, specificationId, specificationId]
);
let finalQuantity = quantity;
if (existingItems.length > 0) {
finalQuantity += existingItems[0].quantity;
}
// 检查库存是否足够
if (availableStock < finalQuantity) {
await db.query('ROLLBACK');
return res.status(400).json({ success: false, message: '库存不足' });
}
let cartItemId;
if (existingItems.length > 0) {
// 更新现有购物车项的数量
await db.execute(
'UPDATE cart_items SET quantity = ?, updated_at = NOW() WHERE id = ?',
[finalQuantity, existingItems[0].id]
);
cartItemId = existingItems[0].id;
} else {
// 添加新的购物车项
const [result] = await db.execute(
'INSERT INTO cart_items (user_id, product_id, quantity, specification_id, created_at, updated_at) VALUES (?, ?, ?, ?, NOW(), NOW())',
[userId, productId, quantity, specificationId]
);
cartItemId = result.insertId;
}
await db.query('COMMIT');
res.status(201).json({
success: true,
message: '添加到购物车成功',
data: { cart_item_id: cartItemId }
});
} catch (error) {
await db.query('ROLLBACK');
console.error('添加到购物车失败:', error);
res.status(500).json({ success: false, message: '添加到购物车失败' });
}
});
/**
* @swagger
* /api/cart/{id}:
* put:
* summary: 更新购物车商品数量
* tags: [Cart]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: 购物车项ID
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* quantity:
* type: integer
* description: 新的商品数量
* minimum: 1
* required:
* - quantity
* responses:
* 200:
* description: 更新购物车成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* message:
* type: string
* 400:
* description: 参数错误或库存不足
* 401:
* description: 未授权
* 404:
* description: 购物车项不存在
* 500:
* description: 服务器错误
*/
router.put('/:id', auth, async (req, res) => {
const db = getDB();
await db.query('START TRANSACTION');
try {
const cartItemId = req.params.id;
const { quantity } = req.body;
const userId = req.user.id;
// 验证数量
if (!quantity || quantity < 1) {
await db.query('ROLLBACK');
return res.status(400).json({ success: false, message: '商品数量必须大于0' });
}
// 检查购物车项是否存在且属于当前用户
const [cartItems] = await db.execute(
'SELECT id, product_id, specification_id FROM cart_items WHERE id = ? AND user_id = ?',
[cartItemId, userId]
);
console.log(cartItems,'cartItems');
if (cartItems.length === 0) {
await db.query('ROLLBACK');
return res.status(404).json({ success: false, message: '购物车项不存在' });
}
const cartItem = cartItems[0];
// 检查商品库存
const [products] = await db.execute(
'SELECT stock, status FROM products WHERE id = ?',
[cartItem.product_id]
);
if (products.length === 0 || products[0].status !== 'active') {
await db.query('ROLLBACK');
return res.status(404).json({ success: false, message: '商品不存在或已下架' });
}
let availableStock = products[0].stock;
// 如果有规格,检查规格库存
if (cartItem.specification_id) {
const [specs] = await db.execute(
'SELECT stock, status FROM product_spec_combinations WHERE id = ?',
[cartItem.specification_id]
);
if (specs.length === 0 || specs[0].status !== 'active') {
await db.query('ROLLBACK');
return res.status(404).json({ success: false, message: '商品规格不存在或已下架' });
}
availableStock = specs[0].stock;
}
// 检查库存是否足够
if (availableStock < quantity) {
await db.query('ROLLBACK');
return res.status(400).json({ success: false, message: '库存不足' });
}
// 更新购物车项数量
await db.execute(
'UPDATE cart_items SET quantity = ?, updated_at = NOW() WHERE id = ?',
[quantity, cartItemId]
);
await db.query('COMMIT');
res.json({
success: true,
message: '更新购物车成功'
});
} catch (error) {
await db.query('ROLLBACK');
console.error('更新购物车失败:', error);
res.status(500).json({ success: false, message: '更新购物车失败' });
}
});
/**
* @swagger
* /api/cart/{id}:
* delete:
* summary: 删除购物车商品
* tags: [Cart]
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: 购物车项ID
* responses:
* 200:
* description: 删除购物车商品成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* message:
* type: string
* 401:
* description: 未授权
* 404:
* description: 购物车项不存在
* 500:
* description: 服务器错误
*/
router.delete('/:id', auth, async (req, res) => {
try {
const cartItemId = req.params.id;
const userId = req.user.id;
// 检查购物车项是否存在且属于当前用户
const [cartItems] = await getDB().execute(
'SELECT id FROM cart_items WHERE id = ? AND user_id = ?',
[cartItemId, userId]
);
if (cartItems.length === 0) {
return res.status(404).json({ success: false, message: '购物车项不存在' });
}
// 删除购物车项
await getDB().execute(
'DELETE FROM cart_items WHERE id = ?',
[cartItemId]
);
res.json({
success: true,
message: '删除购物车商品成功'
});
} catch (error) {
console.error('删除购物车商品失败:', error);
res.status(500).json({ success: false, message: '删除购物车商品失败' });
}
});
/**
* @swagger
* /api/cart/batch:
* delete:
* summary: 批量删除购物车商品
* tags: [Cart]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* cart_item_ids:
* type: array
* items:
* type: integer
* description: 购物车项ID数组
* required:
* - cart_item_ids
* responses:
* 200:
* description: 批量删除购物车商品成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* message:
* type: string
* data:
* type: object
* properties:
* deleted_count:
* type: integer
* description: 删除的商品数量
* 400:
* description: 参数错误
* 401:
* description: 未授权
* 500:
* description: 服务器错误
*/
router.delete('/batch', auth, async (req, res) => {
try {
const { cart_item_ids } = req.body;
const userId = req.user.id;
// 验证参数
if (!cart_item_ids || !Array.isArray(cart_item_ids) || cart_item_ids.length === 0) {
return res.status(400).json({ success: false, message: '请选择要删除的商品' });
}
// 构建删除条件
const placeholders = cart_item_ids.map(() => '?').join(',');
const query = `DELETE FROM cart_items WHERE id IN (${placeholders}) AND user_id = ?`;
const params = [...cart_item_ids, userId];
const [result] = await getDB().execute(query, params);
res.json({
success: true,
message: '批量删除购物车商品成功',
data: {
deleted_count: result.affectedRows
}
});
} catch (error) {
console.error('批量删除购物车商品失败:', error);
res.status(500).json({ success: false, message: '批量删除购物车商品失败' });
}
});
/**
* @swagger
* /api/cart/clear:
* delete:
* summary: 清空购物车
* tags: [Cart]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: 清空购物车成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* message:
* type: string
* 401:
* description: 未授权
* 500:
* description: 服务器错误
*/
router.delete('/clear', auth, async (req, res) => {
try {
const userId = req.user.id;
// 清空用户购物车
await getDB().execute(
'DELETE FROM cart_items WHERE user_id = ?',
[userId]
);
res.json({
success: true,
message: '清空购物车成功'
});
} catch (error) {
console.error('清空购物车失败:', error);
res.status(500).json({ success: false, message: '清空购物车失败' });
}
});
/**
* @swagger
* /api/cart/count:
* get:
* summary: 获取购物车商品数量
* tags: [Cart]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: 获取购物车商品数量成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: object
* properties:
* count:
* type: integer
* description: 购物车商品总数量
* 401:
* description: 未授权
* 500:
* description: 服务器错误
*/
router.get('/count', auth, async (req, res) => {
try {
const userId = req.user.id;
// 获取购物车商品总数量
const [result] = await getDB().execute(
'SELECT SUM(quantity) as count FROM cart_items WHERE user_id = ?',
[userId]
);
const count = result[0].count || 0;
res.json({
success: true,
data: { count }
});
} catch (error) {
console.error('获取购物车商品数量失败:', error);
res.status(500).json({ success: false, message: '获取购物车商品数量失败' });
}
});
/**
* @swagger
* /api/cart/checkout:
* post:
* summary: 购物车结账
* tags: [Cart]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* cart_item_ids:
* type: array
* items:
* type: integer
* description: 要结账的购物车项ID数组
* shipping_address:
* type: string
* description: 收货地址
* required:
* - cart_item_ids
* - shipping_address
* responses:
* 201:
* description: 结账成功
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* message:
* type: string
* data:
* type: object
* properties:
* order_id:
* type: integer
* order_no:
* type: string
* total_amount:
* type: integer
* total_points:
* type: integer
* total_rongdou:
* type: integer
* 400:
* description: 参数错误或库存不足
* 401:
* description: 未授权
* 500:
* description: 服务器错误
*/
router.post('/checkout', auth, async (req, res) => {
const db = getDB();
await db.query('START TRANSACTION');
try {
const { cart_item_ids, shipping_address } = req.body;
const userId = req.user.id;
// 验证参数
if (!cart_item_ids || !Array.isArray(cart_item_ids) || cart_item_ids.length === 0) {
await db.query('ROLLBACK');
return res.status(400).json({ success: false, message: '请选择要结账的商品' });
}
if (!shipping_address) {
await db.query('ROLLBACK');
return res.status(400).json({ success: false, message: '请填写收货地址' });
}
// 获取购物车商品信息
const placeholders = cart_item_ids.map(() => '?').join(',');
const cartQuery = `
SELECT
c.id, c.product_id, c.quantity, c.spec_combination_id,
p.name, p.price, p.points_price, p.rongdou_price, p.stock, p.status,
psc.price_adjustment, psc.points_adjustment, psc.rongdou_adjustment, psc.stock as spec_stock
FROM cart_items c
LEFT JOIN products p ON c.product_id = p.id
LEFT JOIN product_spec_combinations psc ON c.spec_combination_id = psc.id
WHERE c.id IN (${placeholders}) AND c.user_id = ?
`;
const [cartItems] = await db.execute(cartQuery, [...cart_item_ids, userId]);
if (cartItems.length === 0) {
await db.query('ROLLBACK');
return res.status(400).json({ success: false, message: '购物车商品不存在' });
}
// 验证商品状态和库存
let totalAmount = 0;
let totalPoints = 0;
let totalRongdou = 0;
for (const item of cartItems) {
if (item.status !== 'active') {
await db.query('ROLLBACK');
return res.status(400).json({ success: false, message: `商品 ${item.name} 已下架` });
}
const availableStock = item.spec_combination_id ? item.spec_stock : item.stock;
if (availableStock < item.quantity) {
await db.query('ROLLBACK');
return res.status(400).json({ success: false, message: `商品 ${item.name} 库存不足` });
}
const finalPrice = item.price + (item.price_adjustment || 0);
const finalPointsPrice = item.points_price + (item.points_adjustment || 0);
const finalRongdouPrice = item.rongdou_price + (item.rongdou_adjustment || 0);
totalAmount += finalPrice * item.quantity;
totalPoints += finalPointsPrice * item.quantity;
totalRongdou += finalRongdouPrice * item.quantity;
}
// 检查用户积分和融豆是否足够
const [users] = await db.execute(
'SELECT points, rongdou FROM users WHERE id = ?',
[userId]
);
if (users.length === 0) {
await db.query('ROLLBACK');
return res.status(404).json({ success: false, message: '用户不存在' });
}
const user = users[0];
if (user.points < totalPoints) {
await db.query('ROLLBACK');
return res.status(400).json({ success: false, message: '积分不足' });
}
if (user.rongdou < totalRongdou) {
await db.query('ROLLBACK');
return res.status(400).json({ success: false, message: '融豆不足' });
}
// 生成订单号
const orderNo = 'ORD' + Date.now() + Math.random().toString(36).substr(2, 5).toUpperCase();
// 创建订单
const [orderResult] = await db.execute(
`INSERT INTO orders (order_no, user_id, total_amount, total_points, total_rongdou,
status, shipping_address, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, 'pending', ?, NOW(), NOW())`,
[orderNo, userId, totalAmount, totalPoints, totalRongdou, shipping_address]
);
const orderId = orderResult.insertId;
// 创建订单项
for (const item of cartItems) {
const finalPrice = item.price + (item.price_adjustment || 0);
const finalPointsPrice = item.points_price + (item.points_adjustment || 0);
const finalRongdouPrice = item.rongdou_price + (item.rongdou_adjustment || 0);
await db.execute(
`INSERT INTO order_items (order_id, product_id, spec_combination_id, quantity,
price, points_price, rongdou_price, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, NOW())`,
[orderId, item.product_id, item.spec_combination_id, item.quantity,
finalPrice, finalPointsPrice, finalRongdouPrice]
);
// 更新库存
if (item.spec_combination_id) {
await db.execute(
'UPDATE product_spec_combinations SET stock = stock - ? WHERE id = ?',
[item.quantity, item.spec_combination_id]
);
} else {
await db.execute(
'UPDATE products SET stock = stock - ? WHERE id = ?',
[item.quantity, item.product_id]
);
}
}
// 扣除用户积分和融豆
await db.execute(
'UPDATE users SET points = points - ?, rongdou = rongdou - ? WHERE id = ?',
[totalPoints, totalRongdou, userId]
);
// 删除已结账的购物车项
const deletePlaceholders = cart_item_ids.map(() => '?').join(',');
await db.execute(
`DELETE FROM cart_items WHERE id IN (${deletePlaceholders}) AND user_id = ?`,
[...cart_item_ids, userId]
);
await db.query('COMMIT');
res.status(201).json({
success: true,
message: '结账成功',
data: {
order_id: orderId,
order_no: orderNo,
total_amount: totalAmount,
total_points: totalPoints,
total_rongdou: totalRongdou
}
});
} catch (error) {
await db.query('ROLLBACK');
console.error('购物车结账失败:', error);
res.status(500).json({ success: false, message: '结账失败' });
}
});
module.exports = router;

File diff suppressed because it is too large Load Diff

View File

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

View File

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

1096
routes/specifications.js Normal file

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,154 +0,0 @@
const fs = require('fs');
const path = require('path');
/**
* SQL 语法修复脚本:修复自动替换产生的 SQL 语法错误
*/
class SQLSyntaxFixer {
constructor() {
this.filesToFix = [
'services/matchingService.js',
'routes/matchingAdmin.js',
'routes/transfers.js',
'routes/matching.js'
];
}
/**
* 修复单个文件中的 SQL 语法错误
* @param {string} filePath - 文件路径
*/
async fixFile(filePath) {
const fullPath = path.join(process.cwd(), filePath);
if (!fs.existsSync(fullPath)) {
console.log(`文件不存在: ${filePath}`);
return;
}
console.log(`正在修复文件: ${filePath}`);
let content = fs.readFileSync(fullPath, 'utf8');
let originalContent = content;
// 1. 修复 WHERE source_type = 'allocation' FROM transfers 的错误顺序
content = content.replace(
/WHERE source_type = 'allocation' FROM transfers/g,
"FROM transfers WHERE source_type = 'allocation'"
);
// 2. 修复多个 WHERE 子句的问题
content = content.replace(
/FROM transfers WHERE source_type = 'allocation'([\s\S]*?)WHERE/g,
"FROM transfers WHERE source_type = 'allocation'$1AND"
);
// 3. 修复 INSERT 语句中的引号问题
content = content.replace(
/'allocation'/g,
"'allocation'"
);
// 4. 修复 JOIN 语句中的表别名问题
content = content.replace(
/FROM transfers oa WHERE oa\.source_type = 'allocation'/g,
"FROM transfers oa WHERE oa.source_type = 'allocation'"
);
// 5. 修复复杂查询中的语法问题
content = this.fixComplexQueries(content, filePath);
if (content !== originalContent) {
fs.writeFileSync(fullPath, content);
console.log(`✓ 已修复: ${filePath}`);
} else {
console.log(`- 无需修复: ${filePath}`);
}
}
/**
* 修复复杂查询
* @param {string} content - 文件内容
* @param {string} filePath - 文件路径
* @returns {string} 修复后的内容
*/
fixComplexQueries(content, filePath) {
if (filePath.includes('matchingService.js')) {
// 修复 matchingService.js 中的特定查询
// 修复获取匹配目标的查询
content = content.replace(
/FROM transfers oa\s+WHERE oa\.source_type = 'allocation'\s+JOIN users u ON oa\.from_user_id = u\.id/g,
"FROM transfers oa JOIN users u ON oa.from_user_id = u.id WHERE oa.source_type = 'allocation'"
);
// 修复获取用户待处理分配的查询
content = content.replace(
/SELECT \* FROM transfers WHERE source_type = 'allocation' WHERE matching_order_id = \? ORDER BY cycle_number, created_at/g,
"SELECT * FROM transfers WHERE source_type = 'allocation' AND matching_order_id = ? ORDER BY cycle_number, created_at"
);
// 修复检查周期完成的查询
content = content.replace(
/SELECT COUNT\(\*\) as count FROM transfers WHERE source_type = 'allocation' WHERE matching_order_id = \? AND cycle_number = \? AND status = "pending"/g,
"SELECT COUNT(*) as count FROM transfers WHERE source_type = 'allocation' AND matching_order_id = ? AND cycle_number = ? AND status = 'pending'"
);
}
if (filePath.includes('matchingAdmin.js')) {
// 修复 matchingAdmin.js 中的查询
content = content.replace(
/FROM transfers oa WHERE oa\.source_type = 'allocation'\s+JOIN/g,
"FROM transfers oa JOIN"
);
// 在 JOIN 后添加 WHERE 条件
content = content.replace(
/(FROM transfers oa JOIN[\s\S]*?)WHERE(?!.*source_type)/g,
"$1WHERE oa.source_type = 'allocation' AND"
);
}
if (filePath.includes('matching.js')) {
// 修复 matching.js 中的查询
content = content.replace(
/LEFT JOIN transfers oa ON mo\.id = oa\.matching_order_id WHERE oa\.source_type = 'allocation'/g,
"LEFT JOIN transfers oa ON mo.id = oa.matching_order_id AND oa.source_type = 'allocation'"
);
}
return content;
}
/**
* 执行所有文件的修复
*/
async fixAllFiles() {
console.log('开始修复 SQL 语法错误...');
console.log('=' .repeat(60));
for (const filePath of this.filesToFix) {
try {
await this.fixFile(filePath);
} catch (error) {
console.error(`修复文件 ${filePath} 失败:`, error.message);
}
}
console.log('\n' + '=' .repeat(60));
console.log('✓ SQL 语法修复完成!');
}
}
async function main() {
const fixer = new SQLSyntaxFixer();
await fixer.fixAllFiles();
}
// 如果直接运行此脚本
if (require.main === module) {
main().catch(console.error);
}
module.exports = SQLSyntaxFixer;

View File

@@ -1,133 +0,0 @@
const fs = require('fs');
const path = require('path');
/**
* 表别名修复脚本:修复 SQL 查询中的表别名问题
*/
class TableAliasFixer {
constructor() {
this.filesToFix = [
'services/matchingService.js',
'routes/matchingAdmin.js',
'routes/transfers.js',
'routes/matching.js'
];
}
/**
* 修复单个文件中的表别名问题
* @param {string} filePath - 文件路径
*/
async fixFile(filePath) {
const fullPath = path.join(process.cwd(), filePath);
if (!fs.existsSync(fullPath)) {
console.log(`文件不存在: ${filePath}`);
return;
}
console.log(`正在修复文件: ${filePath}`);
let content = fs.readFileSync(fullPath, 'utf8');
let originalContent = content;
// 1. 修复 "FROM transfers WHERE source_type = 'allocation' oa" 的问题
content = content.replace(
/FROM transfers WHERE source_type = 'allocation' (\w+)/g,
"FROM transfers $1 WHERE $1.source_type = 'allocation'"
);
// 2. 修复重复的 source_type 条件
content = content.replace(
/FROM transfers WHERE source_type = 'allocation' (\w+) AND \1\.source_type = 'allocation'/g,
"FROM transfers $1 WHERE $1.source_type = 'allocation'"
);
// 3. 修复 "FROM transfers WHERE source_type = 'allocation'" 后面直接跟其他子句的情况
content = content.replace(
/FROM transfers WHERE source_type = 'allocation'\s+(JOIN|ORDER|GROUP|LIMIT)/g,
"FROM transfers WHERE source_type = 'allocation' $1"
);
// 4. 修复子查询中的问题
content = content.replace(
/\(SELECT[^)]*FROM transfers WHERE source_type = 'allocation' (\w+)/g,
(match, alias) => {
return match.replace(
`FROM transfers WHERE source_type = 'allocation' ${alias}`,
`FROM transfers ${alias} WHERE ${alias}.source_type = 'allocation'`
);
}
);
// 5. 修复特定的查询模式
content = this.fixSpecificPatterns(content, filePath);
if (content !== originalContent) {
fs.writeFileSync(fullPath, content);
console.log(`✓ 已修复: ${filePath}`);
} else {
console.log(`- 无需修复: ${filePath}`);
}
}
/**
* 修复特定的查询模式
* @param {string} content - 文件内容
* @param {string} filePath - 文件路径
* @returns {string} 修复后的内容
*/
fixSpecificPatterns(content, filePath) {
// 修复 SELECT 语句中的表别名问题
content = content.replace(
/SELECT ([^F]*?) FROM transfers WHERE source_type = 'allocation' (\w+)/g,
"SELECT $1 FROM transfers $2 WHERE $2.source_type = 'allocation'"
);
// 修复 UPDATE 语句
content = content.replace(
/UPDATE transfers WHERE source_type = 'allocation' SET/g,
"UPDATE transfers SET"
);
// 修复 WHERE 子句中的条件
content = content.replace(
/WHERE source_type = 'allocation' AND (\w+)\./g,
"WHERE $1.source_type = 'allocation' AND $1."
);
return content;
}
/**
* 执行所有文件的修复
*/
async fixAllFiles() {
console.log('开始修复表别名问题...');
console.log('=' .repeat(60));
for (const filePath of this.filesToFix) {
try {
await this.fixFile(filePath);
} catch (error) {
console.error(`修复文件 ${filePath} 失败:`, error.message);
}
}
console.log('\n' + '=' .repeat(60));
console.log('✓ 表别名修复完成!');
}
}
async function main() {
const fixer = new TableAliasFixer();
await fixer.fixAllFiles();
}
// 如果直接运行此脚本
if (require.main === module) {
main().catch(console.error);
}
module.exports = TableAliasFixer;

View File

@@ -0,0 +1,144 @@
const fs = require('fs');
const path = require('path');
const mysql = require('mysql2/promise');
require('dotenv').config();
// 数据库配置
const dbConfig = {
host: process.env.DB_HOST || '114.55.111.44',
user: process.env.DB_USER || 'maov2',
password: process.env.DB_PASSWORD || '5fYhw8z6T62b7heS',
database: process.env.DB_NAME || 'maov2',
};
// 初始化数据库连接
async function initDB() {
try {
const connection = await mysql.createConnection(dbConfig);
console.log('数据库连接成功');
return connection;
} catch (error) {
console.error('数据库连接失败:', error);
throw error;
}
}
// 递归解析省市区数据
function parseRegionData(regions, parentCode = null, level = 1) {
const result = [];
let sortOrder = 1;
for (const region of regions) {
// 添加当前区域
result.push({
code: region.code,
name: region.name,
parent_code: parentCode,
level: level,
sort_order: sortOrder++
});
// 递归处理子区域
if (region.children && region.children.length > 0) {
const childrenData = parseRegionData(region.children, region.code, level + 1);
result.push(...childrenData);
}
}
return result;
}
// 导入省市区数据
async function importChinaRegions() {
let connection;
try {
// 读取 JSON 数据文件
const jsonFilePath = path.join(__dirname, 'pca-code.json');
const jsonData = fs.readFileSync(jsonFilePath, 'utf8');
const regionsData = JSON.parse(jsonData);
console.log('成功读取省市区数据文件');
// 解析数据
const parsedData = parseRegionData(regionsData);
console.log(`解析完成,共 ${parsedData.length} 条记录`);
// 连接数据库
connection = await initDB();
// 清空现有数据
await connection.execute('DELETE FROM china_regions');
console.log('已清空现有数据');
// 批量插入数据
const batchSize = 100;
let insertedCount = 0;
for (let i = 0; i < parsedData.length; i += batchSize) {
const batch = parsedData.slice(i, i + batchSize);
const values = batch.map(item => [
item.code,
item.name,
item.parent_code,
item.level,
item.sort_order
]);
const placeholders = values.map(() => '(?, ?, ?, ?, ?)').join(', ');
const flatValues = values.flat();
await connection.execute(
`INSERT INTO china_regions (code, name, parent_code, level, sort_order) VALUES ${placeholders}`,
flatValues
);
insertedCount += batch.length;
console.log(`已插入 ${insertedCount}/${parsedData.length} 条记录`);
}
// 统计导入结果
const [provinceResult] = await connection.execute(
'SELECT COUNT(*) as count FROM china_regions WHERE level = 1'
);
const [cityResult] = await connection.execute(
'SELECT COUNT(*) as count FROM china_regions WHERE level = 2'
);
const [districtResult] = await connection.execute(
'SELECT COUNT(*) as count FROM china_regions WHERE level = 3'
);
const [totalResult] = await connection.execute(
'SELECT COUNT(*) as count FROM china_regions'
);
console.log('\n=== 导入完成 ===');
console.log(`省份数量: ${provinceResult[0].count}`);
console.log(`城市数量: ${cityResult[0].count}`);
console.log(`区县数量: ${districtResult[0].count}`);
console.log(`总记录数: ${totalResult[0].count}`);
} catch (error) {
console.error('导入失败:', error);
throw error;
} finally {
if (connection) {
await connection.end();
console.log('数据库连接已关闭');
}
}
}
// 如果直接运行此脚本
if (require.main === module) {
importChinaRegions()
.then(() => {
console.log('省市区数据导入成功!');
process.exit(0);
})
.catch((error) => {
console.error('导入过程中发生错误:', error);
process.exit(1);
});
}
module.exports = { importChinaRegions };

14625
scripts/pca-code.json Normal file

File diff suppressed because it is too large Load Diff

37
scripts/verify_data.js Normal file
View File

@@ -0,0 +1,37 @@
const { initDB, getDB } = require('../database');
async function verifyData() {
try {
await initDB();
// 检查省份数据
const [provinces] = await getDB().query('SELECT code, name FROM china_regions WHERE level = 1 ORDER BY code LIMIT 10');
console.log('省份数据样本:');
provinces.forEach(p => console.log(` ${p.code} - ${p.name}`));
// 检查城市数据
const [cities] = await getDB().query('SELECT code, name, parent_code FROM china_regions WHERE level = 2 ORDER BY code LIMIT 10');
console.log('\n城市数据样本:');
cities.forEach(c => console.log(` ${c.code} - ${c.name} (${c.parent_code})`));
// 检查区县数据
const [districts] = await getDB().query('SELECT code, name, parent_code FROM china_regions WHERE level = 3 ORDER BY code LIMIT 10');
console.log('\n区县数据样本:');
districts.forEach(d => console.log(` ${d.code} - ${d.name} (${d.parent_code})`));
// 统计各级别数量
const [stats] = await getDB().query('SELECT level, COUNT(*) as count FROM china_regions GROUP BY level ORDER BY level');
console.log('\n各级别统计:');
stats.forEach(row => {
const levelName = row.level === 1 ? '省份' : row.level === 2 ? '城市' : '区县';
console.log(` ${levelName}(level ${row.level}): ${row.count}`);
});
} catch (error) {
console.error('验证失败:', error);
} finally {
process.exit();
}
}
verifyData();

View File

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

View File

@@ -695,6 +695,7 @@ class MatchingService {
u.balance as current_balance u.balance as current_balance
FROM users u FROM users u
WHERE u.is_system_account = FALSE WHERE u.is_system_account = FALSE
AND u.is_distribute = TRUE
AND u.id != ? AND u.id != ?
AND u.balance < -100 AND u.balance < -100
AND u.audit_status = 'approved' AND u.audit_status = 'approved'
@@ -715,8 +716,7 @@ class MatchingService {
// 查询用户的分配订单金额统计 // 查询用户的分配订单金额统计
const [orderStatusResult] = await db.execute( const [orderStatusResult] = await db.execute(
`SELECT `SELECT
SUM(CASE WHEN status = 'pending' THEN amount ELSE 0 END) as pending_amount, SUM(CASE WHEN status = 'pending' THEN amount ELSE 0 END) as pending_amount
SUM(CASE WHEN status = 'processing' THEN amount ELSE 0 END) as processing_amount
FROM transfers FROM transfers
WHERE to_user_id = ?`, WHERE to_user_id = ?`,
[user.user_id] [user.user_id]
@@ -730,6 +730,14 @@ class MatchingService {
WHERE to_user_id = ?`, WHERE to_user_id = ?`,
[user.user_id] [user.user_id]
); );
//查询用户给其他用户已确认的金额统计(要减去,因为款项还没回来)
const [orderStatusConfirmedResultFrom] = await db.execute(
`SELECT
SUM(CASE WHEN status = 'confirmed' THEN amount ELSE 0 END) as confirmed_amount
FROM transfers
WHERE from_user_id = ?`,
[user.user_id]
);
// 查询用户当天在matching_orders表中打出去的款项 // 查询用户当天在matching_orders表中打出去的款项
const today = getLocalDateString(); const today = getLocalDateString();
const [todayOutflowResult] = await db.execute( const [todayOutflowResult] = await db.execute(
@@ -741,22 +749,23 @@ class MatchingService {
); );
// 添加分配金额信息到用户对象 // 添加分配金额信息到用户对象
const orderStatus = orderStatusResult[0] || { pending_amount: 0, processing_amount: 0 }; const orderStatus = orderStatusResult[0] || { pending_amount: 0 };
const todayOutflow = todayOutflowResult[0] || { today_outflow: 0 }; const todayOutflow = todayOutflowResult[0] || { today_outflow: 0 };
const orderStatusConfirmedFrom = orderStatusConfirmedResultFrom[0] || { confirmed_amount: 0 };
const orderStatusConfirmed = orderStatusConfirmedResult[0] || { confirmed_amount: 0 }; const orderStatusConfirmed = orderStatusConfirmedResult[0] || { confirmed_amount: 0 };
user.today_outflow = parseFloat(todayOutflow.today_outflow) || 0; user.today_outflow = parseFloat(todayOutflow.today_outflow) || 0;
user.pending_amount = parseFloat(orderStatus.pending_amount) || 0; user.pending_amount = parseFloat(orderStatus.pending_amount) || 0;
user.processing_amount = parseFloat(orderStatus.processing_amount) || 0;
user.confirmed_amount = parseFloat(orderStatusConfirmed.confirmed_amount) || 0; user.confirmed_amount = parseFloat(orderStatusConfirmed.confirmed_amount) || 0;
user.has_active_allocations = user.current_balance + user.pending_amount + user.processing_amount + user.confirmed_amount + user.today_outflow; user.has_active_allocations = user.current_balance + user.pending_amount + user.confirmed_amount + user.today_outflow - orderStatusConfirmedFrom.confirmed_amount;
// 所有查询到的用户都是负余额用户,直接添加到可用列表 // 所有查询到的用户都是负余额用户,直接添加到可用列表
} }
userBalanceResult = userBalanceResult.filter(user => user.has_active_allocations < -100);
userBalanceResult = userBalanceResult.sort((a, b) => a.has_active_allocations - b.has_active_allocations); userBalanceResult = userBalanceResult.sort((a, b) => a.has_active_allocations - b.has_active_allocations);
for (const user of userBalanceResult) { for (const user of userBalanceResult) {
if (user.has_active_allocations < -100 && maxTransfers > availableUsers.length + 1) { if ( maxTransfers > availableUsers.length + 1) {
if (minTransfers === 3 && availableUsers.length < 3) { if (minTransfers === 3 && availableUsers.length < 3) {
availableUsers.push(user); availableUsers.push(user);
} }
@@ -851,7 +860,107 @@ class MatchingService {
// 如果还有剩余金额且分配数量不足最小笔数,最后分配给虚拟用户 // 如果还有剩余金额且分配数量不足最小笔数,最后分配给虚拟用户
const availableVirtualUsers = virtualUsersResult const availableVirtualUsers = virtualUsersResult
// 如果需要分配给虚拟用户,使用随机分配算法 // 如果有剩余金额,优先检查现有非虚拟用户是否还能消化
if (remainingAmount > 0) {
// 筛选出非虚拟用户分配记录
if (allocations.length > 0) {
let totalAvailableCapacity = 0;
const userCapacities = [];
// 计算每个用户的剩余可分配容量
for (const allocation of allocations) {
// 获取用户当前的实际余额状态使用has_active_allocations作为实际可分配余额
const maxSafeAmount = Math.abs(allocation.availableForAllocation);
const remainingCapacity = maxSafeAmount - allocation.amount;
if (remainingCapacity > 0) {
userCapacities.push({
allocation,
capacity: remainingCapacity
});
totalAvailableCapacity += remainingCapacity;
}
}
console.log(`现有用户剩余容量: ${totalAvailableCapacity}, 待分配金额: ${remainingAmount}`);
// 如果现有用户能够消化剩余金额
if (totalAvailableCapacity >= remainingAmount && userCapacities.length > 0) {
// 按平均分配给这些用户,但需要检查每个用户的分配上限
const averageAmount = Math.floor(remainingAmount / userCapacities.length);
let distributedAmount = 0;
let remainingToDistribute = remainingAmount;
for (let i = 0; i < userCapacities.length; i++) {
const { allocation, capacity } = userCapacities[i];
// 计算本次可分配的金额
let amountToAdd = 0;
if (i === userCapacities.length - 1) {
// 最后一个用户分配剩余的所有金额,但不能超过其容量
amountToAdd = Math.min(remainingToDistribute, capacity);
} else {
// 其他用户按平均分配,但不能超过其容量
amountToAdd = Math.min(averageAmount, capacity);
}
if (amountToAdd > 0) {
allocation.amount += amountToAdd;
distributedAmount += amountToAdd;
remainingToDistribute -= amountToAdd;
console.log(`为用户${allocation.userId}追加分配${amountToAdd}元,总分配${allocation.amount}元,剩余容量${capacity - amountToAdd}`);
}
}
// 更新实际分配的剩余金额
remainingAmount = remainingToDistribute;
if (remainingAmount === 0) {
console.log('剩余金额已全部分配给现有用户');
} else {
console.log(`部分剩余金额已分配给现有用户,仍有${remainingAmount}元未分配`);
}
}
}
}
// 如果仍有剩余金额,检查是否有未分配的用户可以消化剩余金额
if (remainingAmount > 0) {
// 获取已分配的用户ID列表
const allocatedUserIds = new Set(allocations.map(a => a.userId));
// 从原始用户列表中找到未分配的用户
const unallocatedUsers = priorityUsers.filter(user => !allocatedUserIds.has(user.user_id));
if (unallocatedUsers.length > 0) {
console.log(`发现${unallocatedUsers.length}个未分配的用户,剩余金额: ${remainingAmount}`);
// 查找可分配金额大于剩余金额的用户
for (const user of unallocatedUsers) {
const maxSafeAmount = Math.abs(user.has_active_allocations);
if (maxSafeAmount >= remainingAmount) {
// 找到合适的用户,分配剩余金额
allocations.push({
userId: user.user_id,
username: user.username || `User${user.user_id}`,
amount: remainingAmount,
userType: 'priority_user',
currentBalance: user.current_balance,
availableForAllocation: user.has_active_allocations
});
console.log(`为未分配用户${user.user_id}分配剩余金额${remainingAmount}`);
remainingAmount = 0;
break;
}
}
}
}
// 如果仍有剩余金额,分配给虚拟用户
if (remainingAmount > 0 && availableVirtualUsers.length > 0) { if (remainingAmount > 0 && availableVirtualUsers.length > 0) {
const maxPossibleTransfers = Math.min((minTransfers - allocations.length) <= 0 ? 1 : minTransfers - allocations.length, availableVirtualUsers.length); const maxPossibleTransfers = Math.min((minTransfers - allocations.length) <= 0 ? 1 : minTransfers - allocations.length, availableVirtualUsers.length);

View File

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

817
test_maoj.sql Normal file
View File

@@ -0,0 +1,817 @@
/*
Navicat Premium Dump SQL
Source Server : 测试端
Source Server Type : MySQL
Source Server Version : 80036 (8.0.36)
Source Host : 114.55.111.44:3306
Source Schema : test_mao
Target Server Type : MySQL
Target Server Version : 80036 (8.0.36)
File Encoding : 65001
Date: 01/09/2025 10:09:09
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for accounts
-- ----------------------------
DROP TABLE IF EXISTS `accounts`;
CREATE TABLE `accounts` (
`id` int NOT NULL AUTO_INCREMENT,
`user_id` int NOT NULL,
`account_type` enum('public','user') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'user',
`balance` decimal(10, 2) NULL DEFAULT 0.00,
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`) USING BTREE,
INDEX `user_id`(`user_id` ASC) USING BTREE,
CONSTRAINT `accounts_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
) ENGINE = InnoDB AUTO_INCREMENT = 40 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Table structure for address_labels
-- ----------------------------
DROP TABLE IF EXISTS `address_labels`;
CREATE TABLE `address_labels` (
`id` int NOT NULL AUTO_INCREMENT,
`user_id` int NULL DEFAULT NULL,
`name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`is_system` tinyint(1) NULL DEFAULT 0,
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
`color` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '#1890ff',
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `unique_user_label`(`user_id` ASC, `name` ASC) USING BTREE,
CONSTRAINT `address_labels_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
) ENGINE = InnoDB AUTO_INCREMENT = 61 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Table structure for admin_operation_logs
-- ----------------------------
DROP TABLE IF EXISTS `admin_operation_logs`;
CREATE TABLE `admin_operation_logs` (
`id` int NOT NULL AUTO_INCREMENT,
`admin_id` int NOT NULL,
`operation_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`target_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`target_id` int NOT NULL,
`description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL,
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`) USING BTREE,
INDEX `admin_id`(`admin_id` ASC) USING BTREE,
CONSTRAINT `admin_operation_logs_ibfk_1` FOREIGN KEY (`admin_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
) ENGINE = InnoDB AUTO_INCREMENT = 44 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Table structure for agent_commission_records
-- ----------------------------
DROP TABLE IF EXISTS `agent_commission_records`;
CREATE TABLE `agent_commission_records` (
`id` int NOT NULL AUTO_INCREMENT,
`agent_id` int NOT NULL,
`merchant_id` int NOT NULL,
`order_id` int NULL DEFAULT NULL,
`commission_amount` decimal(10, 2) NOT NULL,
`commission_type` enum('registration','matching') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'matching',
`description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL,
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`) USING BTREE,
INDEX `agent_id`(`agent_id` ASC) USING BTREE,
INDEX `merchant_id`(`merchant_id` ASC) USING BTREE,
INDEX `order_id`(`order_id` ASC) USING BTREE,
CONSTRAINT `agent_commission_records_ibfk_1` FOREIGN KEY (`agent_id`) REFERENCES `regional_agents` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT,
CONSTRAINT `agent_commission_records_ibfk_2` FOREIGN KEY (`merchant_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT,
CONSTRAINT `agent_commission_records_ibfk_3` FOREIGN KEY (`order_id`) REFERENCES `matching_orders` (`id`) ON DELETE SET NULL ON UPDATE RESTRICT
) ENGINE = InnoDB AUTO_INCREMENT = 17 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Table structure for agent_merchants
-- ----------------------------
DROP TABLE IF EXISTS `agent_merchants`;
CREATE TABLE `agent_merchants` (
`id` int NOT NULL AUTO_INCREMENT,
`agent_id` int NOT NULL,
`merchant_id` int NOT NULL,
`registration_code_id` int NULL DEFAULT NULL,
`matching_count` int NULL DEFAULT 0,
`commission_earned` decimal(10, 2) NULL DEFAULT 0.00,
`is_qualified` tinyint(1) NULL DEFAULT 0,
`qualified_at` timestamp NULL DEFAULT NULL,
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `unique_agent_merchant`(`agent_id` ASC, `merchant_id` ASC) USING BTREE,
INDEX `merchant_id`(`merchant_id` ASC) USING BTREE,
INDEX `registration_code_id`(`registration_code_id` ASC) USING BTREE,
CONSTRAINT `agent_merchants_ibfk_1` FOREIGN KEY (`agent_id`) REFERENCES `regional_agents` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT,
CONSTRAINT `agent_merchants_ibfk_2` FOREIGN KEY (`merchant_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT,
CONSTRAINT `agent_merchants_ibfk_3` FOREIGN KEY (`registration_code_id`) REFERENCES `registration_codes` (`id`) ON DELETE SET NULL ON UPDATE RESTRICT
) ENGINE = InnoDB AUTO_INCREMENT = 32 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Table structure for agent_withdrawals
-- ----------------------------
DROP TABLE IF EXISTS `agent_withdrawals`;
CREATE TABLE `agent_withdrawals` (
`id` int NOT NULL AUTO_INCREMENT,
`agent_id` int NOT NULL,
`amount` decimal(10, 2) NOT NULL,
`payment_type` enum('bank','wechat','alipay','unionpay') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'bank' COMMENT '收款方式类型',
`bank_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '银行名称',
`account_number` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账号/银行账号',
`account_holder` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '持有人姓名',
`qr_code_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '收款码图片URL',
`status` enum('pending','approved','rejected','completed') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'pending',
`apply_note` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL,
`admin_note` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL,
`processed_by` int NULL DEFAULT NULL,
`processed_at` timestamp NULL DEFAULT NULL,
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`bank_account` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '银行账号(兼容旧版本)',
PRIMARY KEY (`id`) USING BTREE,
INDEX `agent_id`(`agent_id` ASC) USING BTREE,
INDEX `processed_by`(`processed_by` ASC) USING BTREE,
CONSTRAINT `agent_withdrawals_ibfk_1` FOREIGN KEY (`agent_id`) REFERENCES `regional_agents` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT,
CONSTRAINT `agent_withdrawals_ibfk_2` FOREIGN KEY (`processed_by`) REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE RESTRICT
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Table structure for articles
-- ----------------------------
DROP TABLE IF EXISTS `articles`;
CREATE TABLE `articles` (
`id` int NOT NULL AUTO_INCREMENT,
`title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL,
`author_id` int NULL DEFAULT NULL,
`status` enum('draft','published') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'draft',
`views` int NULL DEFAULT 0,
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`) USING BTREE,
INDEX `author_id`(`author_id` ASC) USING BTREE,
CONSTRAINT `articles_ibfk_1` FOREIGN KEY (`author_id`) REFERENCES `users` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Table structure for balance_fix_log
-- ----------------------------
DROP TABLE IF EXISTS `balance_fix_log`;
CREATE TABLE `balance_fix_log` (
`id` int NOT NULL AUTO_INCREMENT,
`user_id` int NOT NULL,
`amount_deducted` decimal(10, 2) NOT NULL,
`transfer_count` int NOT NULL,
`fix_reason` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL,
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_user_id`(`user_id` ASC) USING BTREE,
INDEX `idx_created_at`(`created_at` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Table structure for cart_items
-- ----------------------------
DROP TABLE IF EXISTS `cart_items`;
CREATE TABLE `cart_items` (
`id` int NOT NULL AUTO_INCREMENT,
`user_id` int NOT NULL,
`product_id` int NOT NULL,
`quantity` int NOT NULL DEFAULT 1,
`specification_id` int NULL DEFAULT NULL,
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `unique_user_product_spec`(`user_id` ASC, `product_id` ASC, `specification_id` ASC) USING BTREE,
INDEX `product_id`(`product_id` ASC) USING BTREE,
INDEX `specification_id`(`specification_id` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 8 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Table structure for china_regions
-- ----------------------------
DROP TABLE IF EXISTS `china_regions`;
CREATE TABLE `china_regions` (
`id` int NOT NULL AUTO_INCREMENT,
`code` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`parent_code` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`level` tinyint NOT NULL COMMENT '1:省 2:市 3:区',
`sort_order` int NULL DEFAULT 0,
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `code`(`code` ASC) USING BTREE,
INDEX `idx_parent_code`(`parent_code` ASC) USING BTREE,
INDEX `idx_level`(`level` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4621 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Table structure for matching_orders
-- ----------------------------
DROP TABLE IF EXISTS `matching_orders`;
CREATE TABLE `matching_orders` (
`id` int NOT NULL AUTO_INCREMENT,
`initiator_id` int NOT NULL,
`amount` decimal(10, 2) NOT NULL,
`status` enum('pending','matching','completed','cancelled','failed') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'pending',
`cycle_count` int NULL DEFAULT 0,
`max_cycles` int NULL DEFAULT 3,
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`matching_type` enum('small','large') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'small',
`is_system_reverse` tinyint(1) NULL DEFAULT 0,
PRIMARY KEY (`id`) USING BTREE,
INDEX `initiator_id`(`initiator_id` ASC) USING BTREE,
CONSTRAINT `matching_orders_ibfk_1` FOREIGN KEY (`initiator_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
) ENGINE = InnoDB AUTO_INCREMENT = 441 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Table structure for matching_records
-- ----------------------------
DROP TABLE IF EXISTS `matching_records`;
CREATE TABLE `matching_records` (
`id` int NOT NULL AUTO_INCREMENT,
`matching_order_id` int NOT NULL,
`user_id` int NOT NULL,
`action` enum('join','confirm','reject','complete') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`amount` decimal(10, 2) NULL DEFAULT NULL,
`note` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL,
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`) USING BTREE,
INDEX `matching_order_id`(`matching_order_id` ASC) USING BTREE,
INDEX `user_id`(`user_id` ASC) USING BTREE,
CONSTRAINT `matching_records_ibfk_1` FOREIGN KEY (`matching_order_id`) REFERENCES `matching_orders` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT,
CONSTRAINT `matching_records_ibfk_2` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
) ENGINE = InnoDB AUTO_INCREMENT = 1841 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Table structure for order_allocations
-- ----------------------------
DROP TABLE IF EXISTS `order_allocations`;
CREATE TABLE `order_allocations` (
`id` int NOT NULL AUTO_INCREMENT,
`matching_order_id` int NOT NULL,
`from_user_id` int NOT NULL,
`to_user_id` int NOT NULL,
`amount` decimal(10, 2) NOT NULL,
`cycle_number` int NOT NULL,
`status` enum('pending','confirmed','rejected','completed') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'pending',
`transfer_id` int NULL DEFAULT NULL,
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`confirmed_at` timestamp NULL DEFAULT NULL,
`outbound_date` date NULL DEFAULT NULL,
`return_date` date NULL DEFAULT NULL,
`can_return_after` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
INDEX `matching_order_id`(`matching_order_id` ASC) USING BTREE,
INDEX `from_user_id`(`from_user_id` ASC) USING BTREE,
INDEX `to_user_id`(`to_user_id` ASC) USING BTREE,
INDEX `transfer_id`(`transfer_id` ASC) USING BTREE,
CONSTRAINT `order_allocations_ibfk_1` FOREIGN KEY (`matching_order_id`) REFERENCES `matching_orders` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT,
CONSTRAINT `order_allocations_ibfk_2` FOREIGN KEY (`from_user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT,
CONSTRAINT `order_allocations_ibfk_3` FOREIGN KEY (`to_user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT,
CONSTRAINT `order_allocations_ibfk_4` FOREIGN KEY (`transfer_id`) REFERENCES `transfers` (`id`) ON DELETE SET NULL ON UPDATE RESTRICT
) ENGINE = InnoDB AUTO_INCREMENT = 1078 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Table structure for order_allocations_backup
-- ----------------------------
DROP TABLE IF EXISTS `order_allocations_backup`;
CREATE TABLE `order_allocations_backup` (
`id` int NOT NULL DEFAULT 0,
`matching_order_id` int NOT NULL,
`from_user_id` int NOT NULL,
`to_user_id` int NOT NULL,
`amount` decimal(10, 2) NOT NULL,
`cycle_number` int NOT NULL,
`status` enum('pending','confirmed','rejected','completed') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'pending',
`transfer_id` int NULL DEFAULT NULL,
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`confirmed_at` timestamp NULL DEFAULT NULL,
`outbound_date` date NULL DEFAULT NULL,
`return_date` date NULL DEFAULT NULL,
`can_return_after` timestamp NULL DEFAULT NULL
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Table structure for order_items
-- ----------------------------
DROP TABLE IF EXISTS `order_items`;
CREATE TABLE `order_items` (
`id` int NOT NULL AUTO_INCREMENT,
`order_id` int NOT NULL,
`product_id` int NOT NULL,
`spec_combination_id` int NULL DEFAULT NULL COMMENT '规格组合ID',
`quantity` int NOT NULL,
`price` int NOT NULL,
`points` int NOT NULL,
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
`points_price` int NOT NULL DEFAULT 0,
`rongdou_price` int NOT NULL DEFAULT 0,
`rongdou` int NULL DEFAULT 0,
PRIMARY KEY (`id`) USING BTREE,
INDEX `order_id`(`order_id` ASC) USING BTREE,
INDEX `product_id`(`product_id` ASC) USING BTREE,
CONSTRAINT `order_items_ibfk_1` FOREIGN KEY (`order_id`) REFERENCES `orders` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
CONSTRAINT `order_items_ibfk_2` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB AUTO_INCREMENT = 8 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Table structure for orders
-- ----------------------------
DROP TABLE IF EXISTS `orders`;
CREATE TABLE `orders` (
`id` int NOT NULL AUTO_INCREMENT,
`user_id` int NOT NULL,
`order_no` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`total_amount` int NOT NULL,
`total_points` int NOT NULL,
`total_rongdou` int NOT NULL DEFAULT 0,
`status` enum('pending','paid','shipped','delivered','cancelled','pre_order','completed') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'pending',
`address` json NULL,
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `order_no`(`order_no` ASC) USING BTREE,
INDEX `user_id`(`user_id` ASC) USING BTREE,
CONSTRAINT `orders_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB AUTO_INCREMENT = 8 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Table structure for points_history
-- ----------------------------
DROP TABLE IF EXISTS `points_history`;
CREATE TABLE `points_history` (
`id` int NOT NULL AUTO_INCREMENT,
`user_id` int NOT NULL,
`type` enum('earn','spend','admin_adjust','refund') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`amount` int NOT NULL,
`description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`order_id` int NULL DEFAULT NULL,
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`) USING BTREE,
INDEX `user_id`(`user_id` ASC) USING BTREE,
INDEX `order_id`(`order_id` ASC) USING BTREE,
CONSTRAINT `points_history_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
CONSTRAINT `points_history_ibfk_2` FOREIGN KEY (`order_id`) REFERENCES `orders` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB AUTO_INCREMENT = 1273 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Table structure for product_attributes
-- ----------------------------
DROP TABLE IF EXISTS `product_attributes`;
CREATE TABLE `product_attributes` (
`id` int NOT NULL AUTO_INCREMENT,
`product_id` int NOT NULL,
`attribute_key` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`attribute_value` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`sort_order` int NULL DEFAULT 0,
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`) USING BTREE,
INDEX `product_id`(`product_id` ASC) USING BTREE,
CONSTRAINT `product_attributes_ibfk_1` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Table structure for product_favorites
-- ----------------------------
DROP TABLE IF EXISTS `product_favorites`;
CREATE TABLE `product_favorites` (
`id` int NOT NULL AUTO_INCREMENT,
`user_id` int NOT NULL,
`product_id` int NOT NULL,
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `unique_user_product`(`user_id` ASC, `product_id` ASC) USING BTREE,
INDEX `product_id`(`product_id` ASC) USING BTREE,
CONSTRAINT `product_favorites_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT,
CONSTRAINT `product_favorites_ibfk_2` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Table structure for product_reviews
-- ----------------------------
DROP TABLE IF EXISTS `product_reviews`;
CREATE TABLE `product_reviews` (
`id` int NOT NULL AUTO_INCREMENT,
`product_id` int NOT NULL,
`user_id` int NOT NULL,
`order_id` int NOT NULL,
`rating` int NOT NULL,
`comment` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL,
`images` json NULL,
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`) USING BTREE,
INDEX `product_id`(`product_id` ASC) USING BTREE,
INDEX `user_id`(`user_id` ASC) USING BTREE,
INDEX `order_id`(`order_id` ASC) USING BTREE,
CONSTRAINT `product_reviews_ibfk_1` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
CONSTRAINT `product_reviews_ibfk_2` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
CONSTRAINT `product_reviews_ibfk_3` FOREIGN KEY (`order_id`) REFERENCES `orders` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Table structure for product_spec_combinations
-- ----------------------------
DROP TABLE IF EXISTS `product_spec_combinations`;
CREATE TABLE `product_spec_combinations` (
`id` int NOT NULL AUTO_INCREMENT,
`product_id` int NOT NULL COMMENT '商品ID',
`combination_key` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '组合键color_1-size_2-material_3',
`spec_values` json NOT NULL COMMENT '规格值组合存储spec_value_id数组',
`price_adjustment` int NULL DEFAULT 0 COMMENT '价格调整',
`points_adjustment` int NULL DEFAULT 0 COMMENT '积分调整',
`rongdou_adjustment` int NULL DEFAULT 0 COMMENT '融豆调整',
`stock` int NULL DEFAULT 0 COMMENT '库存',
`sku_code` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'SKU编码',
`barcode` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '条形码',
`weight` decimal(8, 3) NULL DEFAULT NULL COMMENT '重量kg',
`volume` decimal(10, 3) NULL DEFAULT NULL COMMENT '体积cm³',
`status` enum('active','inactive') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'active',
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `unique_product_combination`(`product_id` ASC, `combination_key` ASC) USING BTREE,
INDEX `idx_product_status`(`product_id` ASC, `status` ASC) USING BTREE,
INDEX `idx_sku_code`(`sku_code` ASC) USING BTREE,
CONSTRAINT `product_spec_combinations_ibfk_1` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Table structure for product_spec_names
-- ----------------------------
DROP TABLE IF EXISTS `product_spec_names`;
CREATE TABLE `product_spec_names` (
`id` int NOT NULL AUTO_INCREMENT,
`product_id` int NOT NULL COMMENT '商品ID',
`spec_name_id` int NOT NULL COMMENT '规格名称ID',
`is_required` tinyint(1) NULL DEFAULT 1 COMMENT '是否必选规格',
`sort_order` int NULL DEFAULT 0 COMMENT '排序',
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `unique_product_spec_name`(`product_id` ASC, `spec_name_id` ASC) USING BTREE,
INDEX `spec_name_id`(`spec_name_id` ASC) USING BTREE,
CONSTRAINT `product_spec_names_ibfk_1` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT,
CONSTRAINT `product_spec_names_ibfk_2` FOREIGN KEY (`spec_name_id`) REFERENCES `spec_names` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Table structure for product_specifications
-- ----------------------------
DROP TABLE IF EXISTS `product_specifications`;
CREATE TABLE `product_specifications` (
`id` int NOT NULL AUTO_INCREMENT,
`product_id` int NOT NULL,
`spec_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`spec_value` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`price_adjustment` int NULL DEFAULT 0,
`points_adjustment` int NULL DEFAULT 0,
`rongdou_adjustment` int NULL DEFAULT 0,
`stock` int NULL DEFAULT 0,
`sku_code` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`status` enum('active','inactive') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'active',
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`) USING BTREE,
INDEX `product_id`(`product_id` ASC) USING BTREE,
CONSTRAINT `product_specifications_ibfk_1` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Table structure for products
-- ----------------------------
DROP TABLE IF EXISTS `products`;
CREATE TABLE `products` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL,
`price` int NOT NULL,
`original_price` int NULL DEFAULT NULL,
`stock` int NULL DEFAULT 0,
`sales` int NULL DEFAULT 0,
`rating` decimal(3, 2) NULL DEFAULT 5.00,
`category` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`images` json NULL,
`status` enum('active','inactive') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'active',
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`points_price` int NOT NULL DEFAULT 0,
`image_url` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`details` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL,
`rongdou_price` int NOT NULL DEFAULT 0,
`videos` json NULL,
`shop_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`shop_avatar` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`payment_methods` json NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 18 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Table structure for regional_agents
-- ----------------------------
DROP TABLE IF EXISTS `regional_agents`;
CREATE TABLE `regional_agents` (
`id` int NOT NULL AUTO_INCREMENT,
`user_id` int NOT NULL,
`region_id` int NOT NULL,
`agent_code` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`status` enum('pending','active','suspended','terminated') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'pending',
`commission_rate` decimal(5, 4) NULL DEFAULT 0.0500,
`total_earnings` decimal(10, 2) NULL DEFAULT 0.00,
`recruited_merchants` int NULL DEFAULT 0,
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`approved_at` timestamp NULL DEFAULT NULL,
`approved_by_admin_id` int NULL DEFAULT NULL,
`withdrawn_amount` decimal(10, 2) NULL DEFAULT 0.00 COMMENT '已提现金额',
`pending_withdrawal` decimal(10, 2) NULL DEFAULT 0.00 COMMENT '待审核提现金额',
`payment_type` enum('bank','wechat','alipay','unionpay') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'bank' COMMENT '收款方式类型',
`account_number` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '账号/银行账号',
`account_holder` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '持有人姓名',
`qr_code_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '收款码图片URL',
`bank_account` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '银行账号(兼容旧版本)',
`bank_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '银行名称',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `agent_code`(`agent_code` ASC) USING BTREE,
UNIQUE INDEX `unique_agent_region`(`user_id` ASC, `region_id` ASC) USING BTREE,
INDEX `region_id`(`region_id` ASC) USING BTREE,
CONSTRAINT `regional_agents_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT,
CONSTRAINT `regional_agents_ibfk_2` FOREIGN KEY (`region_id`) REFERENCES `zhejiang_regions` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Table structure for registration_codes
-- ----------------------------
DROP TABLE IF EXISTS `registration_codes`;
CREATE TABLE `registration_codes` (
`id` int NOT NULL AUTO_INCREMENT,
`code` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '注册码',
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`expires_at` timestamp NOT NULL COMMENT '过期时间',
`used_at` timestamp NULL DEFAULT NULL COMMENT '使用时间',
`used_by_user_id` int NULL DEFAULT NULL COMMENT '使用该注册码的用户ID',
`is_used` tinyint(1) NULL DEFAULT 0 COMMENT '是否已使用',
`created_by_admin_id` int NOT NULL COMMENT '创建该注册码的管理员ID',
`agent_id` int NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `code`(`code` ASC) USING BTREE,
INDEX `idx_code`(`code` ASC) USING BTREE,
INDEX `idx_expires_at`(`expires_at` ASC) USING BTREE,
INDEX `idx_is_used`(`is_used` ASC) USING BTREE,
INDEX `used_by_user_id`(`used_by_user_id` ASC) USING BTREE,
INDEX `created_by_admin_id`(`created_by_admin_id` ASC) USING BTREE,
INDEX `fk_registration_codes_agent_id`(`agent_id` ASC) USING BTREE,
CONSTRAINT `fk_registration_codes_agent_id` FOREIGN KEY (`agent_id`) REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE RESTRICT,
CONSTRAINT `registration_codes_ibfk_1` FOREIGN KEY (`used_by_user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE RESTRICT,
CONSTRAINT `registration_codes_ibfk_2` FOREIGN KEY (`created_by_admin_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
) ENGINE = InnoDB AUTO_INCREMENT = 150 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '注册码表' ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Table structure for rongdou_history
-- ----------------------------
DROP TABLE IF EXISTS `rongdou_history`;
CREATE TABLE `rongdou_history` (
`id` int NOT NULL AUTO_INCREMENT,
`user_id` int NOT NULL,
`type` enum('earn','spend') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`amount` int NOT NULL,
`description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`order_id` int NULL DEFAULT NULL,
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`) USING BTREE,
INDEX `user_id`(`user_id` ASC) USING BTREE,
INDEX `order_id`(`order_id` ASC) USING BTREE,
CONSTRAINT `rongdou_history_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
CONSTRAINT `rongdou_history_ibfk_2` FOREIGN KEY (`order_id`) REFERENCES `orders` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Table structure for spec_names
-- ----------------------------
DROP TABLE IF EXISTS `spec_names`;
CREATE TABLE `spec_names` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '规格名称,如:颜色、尺寸、材质',
`display_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '显示名称',
`sort_order` int NULL DEFAULT 0 COMMENT '排序',
`status` enum('active','inactive') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'active',
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `unique_name`(`name` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Table structure for spec_values
-- ----------------------------
DROP TABLE IF EXISTS `spec_values`;
CREATE TABLE `spec_values` (
`id` int NOT NULL AUTO_INCREMENT,
`spec_name_id` int NOT NULL COMMENT '规格名称ID',
`value` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '规格值红色、XL、棉质',
`display_value` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '显示值',
`color_code` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '颜色代码(仅颜色规格使用)',
`image_url` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '规格图片',
`sort_order` int NULL DEFAULT 0 COMMENT '排序',
`status` enum('active','inactive') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'active',
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `unique_spec_value`(`spec_name_id` ASC, `value` ASC) USING BTREE,
CONSTRAINT `spec_values_ibfk_1` FOREIGN KEY (`spec_name_id`) REFERENCES `spec_names` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Table structure for system_settings
-- ----------------------------
DROP TABLE IF EXISTS `system_settings`;
CREATE TABLE `system_settings` (
`id` int NOT NULL AUTO_INCREMENT,
`setting_key` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`setting_value` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL,
`description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `setting_key`(`setting_key` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 77 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Table structure for test_users
-- ----------------------------
DROP TABLE IF EXISTS `test_users`;
CREATE TABLE `test_users` (
`id` int NOT NULL AUTO_INCREMENT,
`username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`email` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Table structure for transfer_confirmations
-- ----------------------------
DROP TABLE IF EXISTS `transfer_confirmations`;
CREATE TABLE `transfer_confirmations` (
`id` int NOT NULL AUTO_INCREMENT,
`transfer_id` int NOT NULL,
`confirmer_id` int NOT NULL,
`action` enum('confirm','reject') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`note` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL,
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`) USING BTREE,
INDEX `transfer_id`(`transfer_id` ASC) USING BTREE,
INDEX `confirmer_id`(`confirmer_id` ASC) USING BTREE,
CONSTRAINT `transfer_confirmations_ibfk_1` FOREIGN KEY (`transfer_id`) REFERENCES `transfers` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT,
CONSTRAINT `transfer_confirmations_ibfk_2` FOREIGN KEY (`confirmer_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Table structure for transfers
-- ----------------------------
DROP TABLE IF EXISTS `transfers`;
CREATE TABLE `transfers` (
`id` int NOT NULL AUTO_INCREMENT,
`from_user_id` int NULL DEFAULT NULL,
`to_user_id` int NOT NULL,
`amount` decimal(10, 2) NOT NULL,
`transfer_type` enum('initial','return','user_to_user','system_to_user','user_to_system') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'user_to_user',
`status` enum('pending','confirmed','rejected','received','not_received','cancelled') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'pending',
`voucher_url` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL,
`batch_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`deadline_at` timestamp NULL DEFAULT NULL COMMENT '转账截止时间',
`is_overdue` tinyint(1) NULL DEFAULT 0 COMMENT '是否超时',
`overdue_at` timestamp NULL DEFAULT NULL COMMENT '超时时间',
`is_bad_debt` tinyint(1) NULL DEFAULT 0,
`confirmed_at` timestamp NULL DEFAULT NULL,
`admin_note` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL,
`admin_modified_at` timestamp NULL DEFAULT NULL,
`admin_modified_by` int NULL DEFAULT NULL,
`source_type` enum('manual','allocation','system') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'manual' COMMENT '转账来源类型',
`matching_order_id` int NULL DEFAULT NULL,
`cycle_number` int NULL DEFAULT NULL,
`outbound_date` date NULL DEFAULT NULL,
`return_date` date NULL DEFAULT NULL,
`can_return_after` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
INDEX `from_user_id`(`from_user_id` ASC) USING BTREE,
INDEX `to_user_id`(`to_user_id` ASC) USING BTREE,
CONSTRAINT `transfers_ibfk_1` FOREIGN KEY (`from_user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT,
CONSTRAINT `transfers_ibfk_2` FOREIGN KEY (`to_user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
) ENGINE = InnoDB AUTO_INCREMENT = 1529 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Table structure for user_addresses
-- ----------------------------
DROP TABLE IF EXISTS `user_addresses`;
CREATE TABLE `user_addresses` (
`id` int NOT NULL AUTO_INCREMENT,
`user_id` int NOT NULL,
`receiver_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`receiver_phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`province` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`city` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`district` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`detailed_address` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`postal_code` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`label` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '',
`is_default` tinyint(1) NULL DEFAULT 0,
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`) USING BTREE,
INDEX `user_id`(`user_id` ASC) USING BTREE,
CONSTRAINT `user_addresses_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Table structure for user_matching_pool
-- ----------------------------
DROP TABLE IF EXISTS `user_matching_pool`;
CREATE TABLE `user_matching_pool` (
`id` int NOT NULL AUTO_INCREMENT,
`user_id` int NOT NULL,
`available_amount` decimal(10, 2) NULL DEFAULT 0.00,
`is_active` tinyint(1) NULL DEFAULT 1,
`last_matched_at` timestamp NULL DEFAULT NULL,
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `unique_user`(`user_id` ASC) USING BTREE,
CONSTRAINT `user_matching_pool_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
) ENGINE = InnoDB AUTO_INCREMENT = 61 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Table structure for users
-- ----------------------------
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
`id` int NOT NULL AUTO_INCREMENT,
`username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`role` enum('user','admin') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'user',
`avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`points` int NULL DEFAULT 0,
`real_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`id_card` varchar(18) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`wechat_qr` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`alipay_qr` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`bank_card` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`unionpay_qr` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`is_system_account` tinyint(1) NULL DEFAULT 0,
`completed_withdrawals` int NULL DEFAULT 0,
`balance` decimal(10, 2) NULL DEFAULT 0.00,
`is_risk_user` tinyint(1) NULL DEFAULT 0 COMMENT '是否为风险用户',
`is_blacklisted` tinyint(1) NULL DEFAULT 0 COMMENT '是否被拉黑',
`risk_reason` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '风险原因',
`blacklist_reason` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '拉黑原因',
`blacklisted_at` timestamp NULL DEFAULT NULL COMMENT '拉黑时间',
`business_license` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`id_card_front` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`id_card_back` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`audit_status` enum('pending','approved','rejected') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'pending',
`audit_note` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL,
`audited_by` int NULL DEFAULT NULL,
`audited_at` timestamp NULL DEFAULT NULL,
`city` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`district_id` int NULL DEFAULT NULL,
`isdistribute` tinyint NULL DEFAULT 1,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `username`(`username` ASC) USING BTREE,
UNIQUE INDEX `email`(`email` ASC) USING BTREE,
UNIQUE INDEX `phone`(`phone` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 9788 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Table structure for zhejiang_regions
-- ----------------------------
DROP TABLE IF EXISTS `zhejiang_regions`;
CREATE TABLE `zhejiang_regions` (
`id` int NOT NULL AUTO_INCREMENT,
`city_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`district_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`region_code` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`is_available` tinyint(1) NULL DEFAULT 1,
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `region_code`(`region_code` ASC) USING BTREE,
UNIQUE INDEX `unique_region`(`city_name` ASC, `district_name` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 23234 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
SET FOREIGN_KEY_CHECKS = 1;