docs: 更新项目文档,完善需求和技术细节

This commit is contained in:
ylweng
2025-08-31 23:29:26 +08:00
parent fcce470415
commit 028a458283
35 changed files with 13517 additions and 0 deletions

46
backend/.env Normal file
View File

@@ -0,0 +1,46 @@
# 应用配置
NODE_ENV=development
PORT=3000
JWT_SECRET=your-super-secret-jwt-key-change-in-production
# 数据库配置
DB_HOST=129.211.213.226
DB_PORT=9527
DB_USER=root
DB_PASSWORD=aiotAiot123!
DB_NAME=ajhdata
DB_CHARSET=utf8mb4
# 连接池配置
DB_CONNECTION_LIMIT=10
DB_QUEUE_LIMIT=0
# 文件上传配置
MAX_FILE_SIZE=10485760
UPLOAD_PATH=./uploads
# CORS配置
CORS_ORIGIN=http://localhost:9000,http://127.0.0.1:9000
# 日志配置
LOG_LEVEL=info
LOG_FILE=./logs/app.log
# Redis配置可选
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=
# 邮件配置(可选)
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=your-email@gmail.com
SMTP_PASS=your-app-password
# AI服务配置花卉识别
AI_SERVICE_ENDPOINT=https://api.plant.id/v2/identify
AI_SERVICE_KEY=your-plant-id-api-key
# 微信小程序配置
WX_APPID=your-wechat-appid
WX_SECRET=your-wechat-secret

39
backend/.env.development Normal file
View File

@@ -0,0 +1,39 @@
# 环境配置文件 - 开发环境
# 应用配置
NODE_ENV=development
PORT=3200
APP_NAME=爱鉴花小程序后端
# MySQL数据库配置
DB_HOST=192.168.0.240
DB_PORT=3306
DB_USERNAME=root
DB_PASSWORD=aiot$Aiot123
DB_DATABASE=ajhdata
# Redis配置
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_PASSWORD=
# 腾讯云配置
TENCENT_CLOUD_SECRET_ID=your_secret_id
TENCENT_CLOUD_SECRET_KEY=your_secret_key
COS_BUCKET=your_bucket_name
COS_REGION=ap-beijing
# 微信小程序配置
WX_APPID=your_wechat_appid
WX_APPSECRET=your_wechat_appsecret
# JWT配置
JWT_SECRET=your_jwt_secret_key
JWT_EXPIRE=7d
# 文件上传配置
UPLOAD_MAX_SIZE=10485760
UPLOAD_ALLOW_TYPES=image/jpeg,image/png,image/gif
# 跨域配置
CORS_ORIGIN=http://localhost:8080

40
backend/.env.example Normal file
View File

@@ -0,0 +1,40 @@
# 环境配置文件示例
# 请复制为 .env.development 或 .env.production 并根据环境修改
# 应用配置
NODE_ENV=development
PORT=3000
APP_NAME=爱鉴花小程序后端
# MySQL数据库配置
DB_HOST=192.168.0.240
DB_PORT=3306
DB_USERNAME=root
DB_PASSWORD=aiot$Aiot123
DB_DATABASE=ajhdata
# Redis配置
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_PASSWORD=
# 腾讯云配置
TENCENT_CLOUD_SECRET_ID=your_secret_id
TENCENT_CLOUD_SECRET_KEY=your_secret_key
COS_BUCKET=your_bucket_name
COS_REGION=ap-beijing
# 微信小程序配置
WX_APPID=your_wechat_appid
WX_APPSECRET=your_wechat_appsecret
# JWT配置
JWT_SECRET=your_jwt_secret_key
JWT_EXPIRE=7d
# 文件上传配置
UPLOAD_MAX_SIZE=10485760
UPLOAD_ALLOW_TYPES=image/jpeg,image/png,image/gif
# 跨域配置
CORS_ORIGIN=http://localhost:8080

275
backend/API接口文档.md Normal file
View File

@@ -0,0 +1,275 @@
# 爱鉴花小程序后端API接口文档
## 概述
本文档详细描述了爱鉴花小程序后端RESTful API接口规范基于OpenAPI 3.0标准。
## 基础信息
- **基础URL**: `http://localhost:3000/api/v1`
- **认证方式**: Bearer Token (JWT)
- **数据格式**: JSON
- **字符编码**: UTF-8
## 认证接口
### 用户注册
```http
POST /auth/register
```
**请求参数**:
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| username | string | 是 | 用户名 |
| password | string | 是 | 密码最少6位 |
| phone | string | 是 | 手机号 |
| email | string | 否 | 邮箱 |
| user_type | string | 否 | 用户类型farmer/buyer/admin |
**响应示例**:
```json
{
"code": 201,
"message": "注册成功",
"data": {
"user_id": 1,
"username": "testuser",
"phone": "13800138000",
"email": "user@example.com",
"user_type": "farmer",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
}
```
### 用户登录
```http
POST /auth/login
```
**请求参数**:
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| login | string | 是 | 用户名/手机号/邮箱 |
| password | string | 是 | 密码 |
**响应示例**:
```json
{
"code": 200,
"message": "登录成功",
"data": {
"user_id": 1,
"username": "testuser",
"phone": "13800138000",
"email": "user@example.com",
"user_type": "farmer",
"avatar_url": "/uploads/avatars/avatar.jpg",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
}
```
## 用户接口
### 获取用户信息
```http
GET /users/me
```
**请求头**:
```
Authorization: Bearer <token>
```
**响应示例**:
```json
{
"code": 200,
"message": "获取成功",
"data": {
"id": 1,
"username": "testuser",
"phone": "13800138000",
"email": "user@example.com",
"user_type": "farmer",
"avatar_url": "/uploads/avatars/avatar.jpg",
"created_at": "2023-01-01T00:00:00Z",
"last_login": "2023-01-01T00:00:00Z"
}
}
```
### 更新用户信息
```http
PUT /users/{id}
```
**请求参数**:
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| email | string | 否 | 邮箱 |
| real_name | string | 否 | 真实姓名 |
| avatar_url | string | 否 | 头像URL |
## 商品接口
### 获取商品列表
```http
GET /products
```
**查询参数**:
| 参数名 | 类型 | 说明 |
|--------|------|------|
| page | integer | 页码默认1 |
| limit | integer | 每页数量默认12 |
| category_id | integer | 分类ID |
| keyword | string | 搜索关键词 |
| min_price | number | 最低价格 |
| max_price | number | 最高价格 |
| sort_by | string | 排序字段name/price/created_at/stock |
| sort_order | string | 排序方向asc/desc |
**响应示例**:
```json
{
"code": 200,
"message": "获取成功",
"data": {
"products": [
{
"id": 1,
"name": "玫瑰花",
"category_id": 1,
"price": 29.9,
"stock": 100,
"image": "/uploads/products/rose.jpg",
"description": "新鲜玫瑰花,香气浓郁",
"category_name": "鲜花"
}
],
"pagination": {
"page": 1,
"limit": 12,
"total": 50,
"pages": 5
}
}
}
```
### 获取商品详情
```http
GET /products/{id}
```
## 订单接口
### 创建订单
```http
POST /orders
```
**请求参数**:
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| items | array | 是 | 商品列表 |
| shipping_address | string | 是 | 收货地址 |
**items数组结构**:
```json
[
{
"product_id": 1,
"quantity": 2
}
]
```
## 花卉识别接口
### 花卉识别
```http
POST /identifications/identify
```
**请求格式**: `multipart/form-data`
**请求参数**:
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| image | file | 是 | 花卉图片文件 |
**响应示例**:
```json
{
"code": 200,
"message": "识别成功",
"data": {
"identification_id": 1,
"image_url": "/uploads/identifications/identify-123.jpg",
"results": [
{
"name": "玫瑰",
"confidence": 0.95,
"scientificName": "Rosa rugosa",
"description": "玫瑰是蔷薇科蔷薇属的植物,具有浓郁的芳香和美丽的花朵。"
}
],
"best_result": {
"name": "玫瑰",
"confidence": 0.95,
"scientificName": "Rosa rugosa",
"description": "玫瑰是蔷薇科蔷薇属的植物,具有浓郁的芳香和美丽的花朵。"
}
}
}
```
### 获取识别历史
```http
GET /identifications
```
## 错误码说明
| 错误码 | 说明 |
|--------|------|
| 200 | 成功 |
| 201 | 创建成功 |
| 400 | 参数错误 |
| 401 | 未授权 |
| 403 | 禁止访问 |
| 404 | 资源不存在 |
| 409 | 资源冲突 |
| 500 | 服务器内部错误 |
## 安全要求
1. 所有敏感接口必须使用HTTPS
2. JWT token有效期7天
3. 密码使用bcrypt加密存储
4. 文件上传限制10MB
5. 支持CORS跨域访问
## 版本历史
- v1.0.0 (2024-01-01): 初始版本发布
- 包含用户认证、商品管理、订单管理、花卉识别等核心功能

124
backend/README_DATABASE.md Normal file
View File

@@ -0,0 +1,124 @@
# 数据库配置和使用指南
## 📋 数据库连接信息
### 测试环境
- **主机**: 192.168.0.240
- **端口**: 3306
- **用户名**: root
- **密码**: aiot$Aiot123
- **数据库**: ajhdata
### 生产环境
- **主机**: 129.211.213.226
- **端口**: 9527
- **用户名**: root
- **密码**: aiotAiot123!
- **数据库**: ajhdata
## 🚀 快速开始
### 1. 安装依赖
```bash
cd backend
npm install
```
### 2. 配置环境变量
复制环境变量模板文件:
```bash
cp .env.example .env.development
```
编辑 `.env.development` 文件,根据实际环境修改配置:
```env
NODE_ENV=development
DB_HOST=192.168.0.240
DB_PORT=3306
DB_USERNAME=root
DB_PASSWORD=aiot$Aiot123
DB_DATABASE=ajhdata
```
### 3. 验证数据库连接
```bash
# 完整初始化验证
npm run db:init
# 只检查连接状态
npm run db:check
```
## 📁 配置文件说明
### `config/database.js`
主数据库配置文件,根据环境变量自动选择配置:
- 开发环境: `NODE_ENV=development`
- 生产环境: `NODE_ENV=production`
### `.env.example`
环境变量配置模板,包含所有可配置参数。
### `utils/dbConnector.js`
数据库连接工具类,提供:
- 连接池管理
- SQL查询执行
- 事务支持
- 健康检查
### `scripts/initDatabase.js`
数据库初始化脚本,功能:
- 验证数据库连接
- 检查数据库版本
- 执行SQL文件预留
## 🔧 可用脚本命令
| 命令 | 描述 |
|------|------|
| `npm run db:init` | 完整数据库初始化验证 |
| `npm run db:check` | 只检查数据库连接状态 |
| `npm run db:migrate` | 执行数据库迁移(预留) |
| `npm run db:seed` | 填充初始数据(预留) |
## 🛡️ 安全注意事项
1. **密码保护**: 数据库密码已配置在环境变量中,不要硬编码在代码里
2. **连接池**: 使用连接池避免频繁创建连接
3. **错误处理**: 所有数据库操作都有完整的错误处理
4. **SQL注入**: 使用参数化查询防止SQL注入
## 📊 性能优化
- **连接池配置**: 最大20连接最小5连接
- **超时设置**: 获取连接超时60秒空闲连接超时30秒
- **字符编码**: UTF8MB4支持中文和emoji
- **时区设置**: 东八区(+08:00)
## 🔍 故障排除
### 连接失败
1. 检查网络是否能访问数据库服务器
2. 验证用户名密码是否正确
3. 确认数据库服务是否启动
### 权限问题
1. 检查用户是否有数据库访问权限
2. 确认数据库是否存在
### 性能问题
1. 检查连接池配置是否合理
2. 监控数据库服务器负载
## 📝 开发建议
1. 开发环境使用测试数据库配置
2. 生产环境使用生产数据库配置
3. 定期备份重要数据
4. 使用事务保证数据一致性
## 🔗 相关文档
- [MySQL官方文档](https://dev.mysql.com/doc/)
- [mysql2 npm包文档](https://www.npmjs.com/package/mysql2)
- [连接池最佳实践](https://github.com/mysqljs/mysql#pooling-connections)

108
backend/app.js Normal file
View File

@@ -0,0 +1,108 @@
#!/usr/bin/env node
/**
* 爱鉴花后端服务主入口文件
* 基于Express.js的RESTful API服务
*/
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const compression = require('compression');
const morgan = require('morgan');
const path = require('path');
require('dotenv').config();
// Swagger配置
const { specs, swaggerUi } = require('./config/swagger');
// 导入路由模块
const authRoutes = require('./routes/auth');
const userRoutes = require('./routes/users');
const productRoutes = require('./routes/products');
const orderRoutes = require('./routes/orders');
const identificationRoutes = require('./routes/identifications');
// 导入中间件
const { errorHandler } = require('./middlewares/errorHandler');
const { authMiddleware } = require('./middlewares/auth');
const app = express();
const PORT = process.env.PORT || 3000;
// 安全中间件
app.use(helmet());
app.use(compression());
// CORS配置
app.use(cors({
origin: process.env.NODE_ENV === 'production'
? ['https://your-domain.com']
: ['http://localhost:9000', 'http://127.0.0.1:9000'],
credentials: true
}));
// 日志中间件
app.use(morgan(process.env.NODE_ENV === 'production' ? 'combined' : 'dev'));
// 解析请求体
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
// 静态文件服务
app.use('/uploads', express.static(path.join(__dirname, 'uploads')));
// 健康检查接口
app.get('/health', (req, res) => {
res.json({
status: 'OK',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
environment: process.env.NODE_ENV || 'development'
});
});
// API文档
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(specs, {
explorer: true,
customCss: '.swagger-ui .topbar { display: none }',
customSiteTitle: '爱鉴花API文档'
}));
// API路由配置
app.use('/api/v1/auth', authRoutes);
app.use('/api/v1/users', authMiddleware, userRoutes);
app.use('/api/v1/products', productRoutes);
app.use('/api/v1/orders', authMiddleware, orderRoutes);
app.use('/api/v1/identifications', authMiddleware, identificationRoutes);
// 404处理
app.use('*', (req, res) => {
res.status(404).json({
code: 404,
message: '接口不存在',
data: null,
timestamp: Date.now()
});
});
// 全局错误处理
app.use(errorHandler);
// 启动服务
app.listen(PORT, () => {
console.log(`🚀 爱鉴花后端服务已启动`);
console.log(`📍 环境: ${process.env.NODE_ENV || 'development'}`);
console.log(`🌐 地址: http://localhost:${PORT}`);
console.log(`📚 API文档: http://localhost:${PORT}/api-docs`);
console.log(`❤️ 健康检查: http://localhost:${PORT}/health`);
console.log('─'.repeat(50));
});
// 优雅关闭
process.on('SIGINT', () => {
console.log('\n🛑 正在关闭服务...');
process.exit(0);
});
module.exports = app;

View File

@@ -0,0 +1,47 @@
/**
* 数据库配置文件
* 包含测试环境和生产环境的MySQL连接配置
*/
const databaseConfig = {
// 测试环境配置
development: {
host: '192.168.0.240',
port: 3306,
username: 'root',
password: 'aiot$Aiot123',
database: 'ajhdata',
dialect: 'mysql',
logging: console.log,
pool: {
max: 20,
min: 5,
acquire: 30000,
idle: 10000
}
},
// 生产环境配置
production: {
host: '129.211.213.226',
port: 9527,
username: 'root',
password: 'aiotAiot123!',
database: 'ajhdata',
dialect: 'mysql',
logging: false, // 生产环境关闭SQL日志
pool: {
max: 200,
min: 20,
acquire: 30000,
idle: 30000
}
}
};
// 根据环境变量选择配置
const env = process.env.NODE_ENV || 'development';
module.exports = databaseConfig[env];
// 导出完整配置对象(用于其他需要访问所有配置的场景)
module.exports.allConfigs = databaseConfig;

153
backend/config/swagger.js Normal file
View File

@@ -0,0 +1,153 @@
const swaggerJsdoc = require('swagger-jsdoc');
const swaggerUi = require('swagger-ui-express');
const options = {
definition: {
openapi: '3.0.0',
info: {
title: '爱鉴花小程序后端API',
version: '1.0.0',
description: '爱鉴花小程序后端RESTful API文档',
contact: {
name: 'API支持',
email: 'support@aijianhua.com'
},
license: {
name: 'MIT',
url: 'https://opensource.org/licenses/MIT'
}
},
servers: [
{
url: process.env.NODE_ENV === 'production'
? 'https://api.aijianhua.com'
: `http://localhost:${process.env.PORT || 3000}`,
description: process.env.NODE_ENV === 'production' ? '生产环境' : '开发环境'
}
],
components: {
securitySchemes: {
BearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT'
}
},
schemas: {
User: {
type: 'object',
properties: {
id: { type: 'integer', example: 1 },
username: { type: 'string', example: 'testuser' },
phone: { type: 'string', example: '13800138000' },
email: { type: 'string', example: 'user@example.com' },
user_type: { type: 'string', enum: ['farmer', 'buyer', 'admin'], example: 'farmer' },
avatar_url: { type: 'string', example: '/uploads/avatars/avatar.jpg' },
created_at: { type: 'string', format: 'date-time', example: '2023-01-01T00:00:00Z' },
last_login: { type: 'string', format: 'date-time', example: '2023-01-01T00:00:00Z' }
}
},
Product: {
type: 'object',
properties: {
id: { type: 'integer', example: 1 },
name: { type: 'string', example: '玫瑰花' },
category_id: { type: 'integer', example: 1 },
price: { type: 'number', format: 'float', example: 29.9 },
stock: { type: 'integer', example: 100 },
image: { type: 'string', example: '/uploads/products/rose.jpg' },
description: { type: 'string', example: '新鲜玫瑰花,香气浓郁' },
status: { type: 'integer', enum: [0, 1], example: 1 },
created_at: { type: 'string', format: 'date-time', example: '2023-01-01T00:00:00Z' },
updated_at: { type: 'string', format: 'date-time', example: '2023-01-01T00:00:00Z' }
}
},
Order: {
type: 'object',
properties: {
id: { type: 'integer', example: 1 },
order_number: { type: 'string', example: 'O123456789' },
user_id: { type: 'integer', example: 1 },
total_amount: { type: 'number', format: 'float', example: 99.9 },
payment_status: { type: 'string', enum: ['pending', 'paid', 'cancelled'], example: 'pending' },
shipping_status: { type: 'string', enum: ['pending', 'shipped', 'delivered'], example: 'pending' },
shipping_address: { type: 'string', example: '北京市朝阳区xxx街道' },
created_at: { type: 'string', format: 'date-time', example: '2023-01-01T00:00:00Z' },
updated_at: { type: 'string', format: 'date-time', example: '2023-01-01T00:00:00Z' }
}
},
Identification: {
type: 'object',
properties: {
id: { type: 'integer', example: 1 },
user_id: { type: 'integer', example: 1 },
image_url: { type: 'string', example: '/uploads/identifications/identify-123.jpg' },
result: { type: 'string', description: 'JSON格式的识别结果' },
confidence: { type: 'number', format: 'float', example: 0.95 },
created_at: { type: 'string', format: 'date-time', example: '2023-01-01T00:00:00Z' }
}
},
Error: {
type: 'object',
properties: {
code: { type: 'integer', example: 400 },
message: { type: 'string', example: '参数验证失败' },
data: { type: 'object', nullable: true, example: null }
}
}
},
responses: {
UnauthorizedError: {
description: '未授权访问',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/Error' },
example: {
code: 401,
message: '未提供有效的认证token',
data: null
}
}
}
},
NotFoundError: {
description: '资源不存在',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/Error' },
example: {
code: 404,
message: '资源不存在',
data: null
}
}
}
},
ValidationError: {
description: '参数验证失败',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/Error' },
example: {
code: 400,
message: '参数验证失败',
data: null
}
}
}
}
}
},
security: [
{ BearerAuth: [] }
]
},
apis: [
'./routes/*.js',
'./middlewares/*.js'
]
};
const specs = swaggerJsdoc(options);
module.exports = { specs, swaggerUi };

165
backend/middlewares/auth.js Normal file
View File

@@ -0,0 +1,165 @@
const jwt = require('jsonwebtoken');
const dbConnector = require('../utils/dbConnector');
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
/**
* JWT认证中间件
* 验证token并附加用户信息到req对象
*/
const authMiddleware = async (req, res, next) => {
try {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({
code: 401,
message: '未提供有效的认证token',
data: null
});
}
const token = authHeader.substring(7); // 移除'Bearer '前缀
try {
// 验证token
const decoded = jwt.verify(token, JWT_SECRET);
// 查询用户信息
const users = await dbConnector.query(
'SELECT id, username, phone, email, user_type, avatar_url, status FROM users WHERE id = ?',
[decoded.userId]
);
if (users.length === 0) {
return res.status(401).json({
code: 401,
message: '用户不存在',
data: null
});
}
const user = users[0];
// 检查用户状态
if (user.status !== 1) {
return res.status(401).json({
code: 401,
message: '用户已被禁用',
data: null
});
}
// 附加用户信息到请求对象
req.user = {
id: user.id,
username: user.username,
phone: user.phone,
email: user.email,
user_type: user.user_type,
avatar_url: user.avatar_url
};
next();
} catch (jwtError) {
if (jwtError.name === 'TokenExpiredError') {
return res.status(401).json({
code: 401,
message: 'token已过期',
data: null
});
}
if (jwtError.name === 'JsonWebTokenError') {
return res.status(401).json({
code: 401,
message: '无效的token',
data: null
});
}
throw jwtError;
}
} catch (error) {
console.error('认证中间件错误:', error);
return res.status(500).json({
code: 500,
message: '服务器内部错误',
data: null
});
}
};
/**
* 可选认证中间件
* 如果提供了token就验证不提供也不报错
*/
const optionalAuth = async (req, res, next) => {
try {
const authHeader = req.headers.authorization;
if (authHeader && authHeader.startsWith('Bearer ')) {
const token = authHeader.substring(7);
try {
const decoded = jwt.verify(token, JWT_SECRET);
const users = await dbConnector.query(
'SELECT id, username, phone, email, user_type, avatar_url, status FROM users WHERE id = ? AND status = 1',
[decoded.userId]
);
if (users.length > 0) {
const user = users[0];
req.user = {
id: user.id,
username: user.username,
phone: user.phone,
email: user.email,
user_type: user.user_type,
avatar_url: user.avatar_url
};
}
} catch (error) {
// token无效但不阻止请求继续
console.warn('可选认证失败:', error.message);
}
}
next();
} catch (error) {
console.error('可选认证中间件错误:', error);
next();
}
};
/**
* 管理员权限检查中间件
*/
const adminRequired = (req, res, next) => {
if (!req.user) {
return res.status(401).json({
code: 401,
message: '需要登录',
data: null
});
}
if (req.user.user_type !== 'admin') {
return res.status(403).json({
code: 403,
message: '需要管理员权限',
data: null
});
}
next();
};
module.exports = {
authMiddleware,
optionalAuth,
adminRequired
};

View File

@@ -0,0 +1,124 @@
/**
* 全局错误处理中间件
* 统一处理应用中的各种错误
*/
const errorHandler = (err, req, res, next) => {
console.error('错误详情:', {
message: err.message,
stack: err.stack,
url: req.url,
method: req.method,
body: req.body,
params: req.params,
query: req.query,
timestamp: new Date().toISOString()
});
// 数据库错误
if (err.code === 'ER_DUP_ENTRY') {
return res.status(409).json({
code: 409,
message: '数据已存在',
data: null
});
}
if (err.code === 'ER_NO_REFERENCED_ROW_2') {
return res.status(400).json({
code: 400,
message: '关联数据不存在',
data: null
});
}
if (err.code === 'ER_DATA_TOO_LONG') {
return res.status(400).json({
code: 400,
message: '数据长度超过限制',
data: null
});
}
// JWT错误
if (err.name === 'JsonWebTokenError') {
return res.status(401).json({
code: 401,
message: '无效的token',
data: null
});
}
if (err.name === 'TokenExpiredError') {
return res.status(401).json({
code: 401,
message: 'token已过期',
data: null
});
}
// 验证错误
if (err.name === 'ValidationError') {
return res.status(400).json({
code: 400,
message: err.message || '参数验证失败',
data: null
});
}
// 文件上传错误
if (err.code === 'LIMIT_FILE_SIZE') {
return res.status(400).json({
code: 400,
message: '文件大小超过限制',
data: null
});
}
if (err.code === 'LIMIT_UNEXPECTED_FILE') {
return res.status(400).json({
code: 400,
message: '不支持的文件类型',
data: null
});
}
// 默认错误处理
const statusCode = err.statusCode || err.status || 500;
const message = process.env.NODE_ENV === 'production'
? '服务器内部错误'
: err.message || '服务器内部错误';
res.status(statusCode).json({
code: statusCode,
message,
data: null,
...(process.env.NODE_ENV !== 'production' && { stack: err.stack })
});
};
/**
* 异步错误处理包装器
* 用于包装异步路由处理函数,自动捕获错误
*/
const asyncHandler = (fn) => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
/**
* 404错误处理中间件
*/
const notFoundHandler = (req, res) => {
res.status(404).json({
code: 404,
message: '接口不存在',
data: null,
timestamp: Date.now()
});
};
module.exports = {
errorHandler,
asyncHandler,
notFoundHandler
};

6642
backend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

53
backend/package.json Normal file
View File

@@ -0,0 +1,53 @@
{
"name": "aijianhua-backend",
"version": "1.0.0",
"description": "爱鉴花小程序后端API服务",
"main": "app.js",
"scripts": {
"start": "node app.js",
"dev": "nodemon app.js",
"test": "jest",
"lint": "eslint .",
"db:init": "node scripts/initDatabase.js",
"db:check": "node scripts/initDatabase.js --check",
"db:migrate": "node scripts/migrate.js",
"db:seed": "node scripts/seedData.js"
},
"dependencies": {
"express": "^4.18.2",
"mysql2": "^3.6.0",
"cors": "^2.8.5",
"helmet": "^7.0.0",
"compression": "^1.7.4",
"morgan": "^1.10.0",
"dotenv": "^16.3.1",
"jsonwebtoken": "^9.0.2",
"bcryptjs": "^2.4.3",
"validator": "^13.11.0",
"multer": "^1.4.5-lts.1",
"redis": "^4.6.8",
"socket.io": "^4.7.2",
"swagger-ui-express": "^4.6.3",
"yamljs": "^0.3.0"
},
"devDependencies": {
"nodemon": "^3.0.1",
"jest": "^29.6.2",
"eslint": "^8.47.0",
"prettier": "^3.0.2",
"@types/node": "^20.5.0",
"typescript": "^5.1.6"
},
"keywords": [
"wechat",
"mini-program",
"plant-identification",
"ecommerce",
"api"
],
"author": "爱鉴花开发团队",
"license": "MIT",
"engines": {
"node": ">=16.0.0"
}
}

218
backend/routes/auth.js Normal file
View File

@@ -0,0 +1,218 @@
const express = require('express');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const validator = require('validator');
const dbConnector = require('../utils/dbConnector');
const router = express.Router();
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
/**
* 用户注册
*/
router.post('/register', async (req, res, next) => {
try {
const { username, password, phone, email, user_type = 'farmer' } = req.body;
// 参数验证
if (!username || !password || !phone) {
return res.status(400).json({
code: 400,
message: '用户名、密码和手机号为必填项',
data: null
});
}
if (password.length < 6) {
return res.status(400).json({
code: 400,
message: '密码长度不能少于6位',
data: null
});
}
if (!validator.isMobilePhone(phone, 'zh-CN')) {
return res.status(400).json({
code: 400,
message: '手机号格式不正确',
data: null
});
}
if (email && !validator.isEmail(email)) {
return res.status(400).json({
code: 400,
message: '邮箱格式不正确',
data: null
});
}
// 检查用户是否已存在
const existingUser = await dbConnector.query(
'SELECT id FROM users WHERE username = ? OR phone = ? OR email = ?',
[username, phone, email]
);
if (existingUser.length > 0) {
return res.status(409).json({
code: 409,
message: '用户名、手机号或邮箱已存在',
data: null
});
}
// 加密密码
const hashedPassword = await bcrypt.hash(password, 12);
// 创建用户
const result = await dbConnector.query(
'INSERT INTO users (username, password_hash, phone, email, user_type) VALUES (?, ?, ?, ?, ?)',
[username, hashedPassword, phone, email, user_type]
);
// 生成JWT token
const token = jwt.sign(
{ userId: result.insertId, username, user_type },
JWT_SECRET,
{ expiresIn: '7d' }
);
res.status(201).json({
code: 201,
message: '注册成功',
data: {
user_id: result.insertId,
username,
phone,
email,
user_type,
token
}
});
} catch (error) {
next(error);
}
});
/**
* 用户登录
*/
router.post('/login', async (req, res, next) => {
try {
const { login, password } = req.body;
if (!login || !password) {
return res.status(400).json({
code: 400,
message: '登录账号和密码为必填项',
data: null
});
}
// 查询用户(支持用户名、手机号、邮箱登录)
const user = await dbConnector.query(
'SELECT * FROM users WHERE (username = ? OR phone = ? OR email = ?) AND status = 1',
[login, login, login]
);
if (user.length === 0) {
return res.status(401).json({
code: 401,
message: '用户不存在或已被禁用',
data: null
});
}
const userData = user[0];
// 验证密码
const isValidPassword = await bcrypt.compare(password, userData.password_hash);
if (!isValidPassword) {
return res.status(401).json({
code: 401,
message: '密码不正确',
data: null
});
}
// 更新最后登录时间
await dbConnector.query(
'UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = ?',
[userData.id]
);
// 生成JWT token
const token = jwt.sign(
{ userId: userData.id, username: userData.username, user_type: userData.user_type },
JWT_SECRET,
{ expiresIn: '7d' }
);
res.json({
code: 200,
message: '登录成功',
data: {
user_id: userData.id,
username: userData.username,
phone: userData.phone,
email: userData.email,
user_type: userData.user_type,
avatar_url: userData.avatar_url,
token
}
});
} catch (error) {
next(error);
}
});
/**
* 获取当前用户信息
*/
router.get('/me', async (req, res, next) => {
try {
// 从token中获取用户ID
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({
code: 401,
message: '未提供认证token',
data: null
});
}
const decoded = jwt.verify(token, JWT_SECRET);
const user = await dbConnector.query(
'SELECT id, username, phone, email, user_type, avatar_url, created_at, last_login FROM users WHERE id = ? AND status = 1',
[decoded.userId]
);
if (user.length === 0) {
return res.status(404).json({
code: 404,
message: '用户不存在',
data: null
});
}
res.json({
code: 200,
message: '获取成功',
data: user[0]
});
} catch (error) {
if (error.name === 'JsonWebTokenError') {
return res.status(401).json({
code: 401,
message: '无效的token',
data: null
});
}
next(error);
}
});
module.exports = router;

View File

@@ -0,0 +1,355 @@
const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const dbConnector = require('../utils/dbConnector');
const { asyncHandler } = require('../middlewares/errorHandler');
const router = express.Router();
// 配置multer用于文件上传
const storage = multer.diskStorage({
destination: (req, file, cb) => {
const uploadDir = path.join(__dirname, '../uploads/identifications');
// 确保上传目录存在
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
}
cb(null, uploadDir);
},
filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
cb(null, 'identification-' + uniqueSuffix + path.extname(file.originalname));
}
});
const upload = multer({
storage: storage,
limits: {
fileSize: 10 * 1024 * 1024, // 10MB限制
},
fileFilter: (req, file, cb) => {
// 只允许图片文件
if (file.mimetype.startsWith('image/')) {
cb(null, true);
} else {
cb(new Error('只支持图片文件'), false);
}
}
});
/**
* 获取识别历史记录
*/
router.get('/', asyncHandler(async (req, res) => {
const { page = 1, limit = 10 } = req.query;
const userId = req.user.id;
const offset = (page - 1) * limit;
// 查询识别记录
const identifications = await dbConnector.query(
`SELECT
id, user_id, image_url, result, confidence, created_at
FROM identifications
WHERE user_id = ?
ORDER BY created_at DESC
LIMIT ? OFFSET ?`,
[userId, parseInt(limit), offset]
);
// 查询总数
const totalResult = await dbConnector.query(
'SELECT COUNT(*) as total FROM identifications WHERE user_id = ?',
[userId]
);
res.json({
code: 200,
message: '获取成功',
data: {
identifications,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total: totalResult[0].total,
pages: Math.ceil(totalResult[0].total / limit)
}
}
});
}));
/**
* 获取单条识别记录详情
*/
router.get('/:id', asyncHandler(async (req, res) => {
const { id } = req.params;
const userId = req.user.id;
const identifications = await dbConnector.query(
`SELECT
id, user_id, image_url, result, confidence, created_at
FROM identifications
WHERE id = ? AND user_id = ?`,
[id, userId]
);
if (identifications.length === 0) {
return res.status(404).json({
code: 404,
message: '识别记录不存在',
data: null
});
}
res.json({
code: 200,
message: '获取成功',
data: identifications[0]
});
}));
/**
* 花卉识别接口
*/
router.post('/identify', upload.single('image'), asyncHandler(async (req, res) => {
const userId = req.user.id;
if (!req.file) {
return res.status(400).json({
code: 400,
message: '请上传图片文件',
data: null
});
}
// 这里应该调用AI识别服务
// 由于AI识别服务需要额外配置这里先模拟识别结果
const imageUrl = `/uploads/identifications/${req.file.filename}`;
// 模拟AI识别结果实际项目中应该调用真实的AI服务
const mockResults = [
{ name: '玫瑰', confidence: 0.95, scientificName: 'Rosa rugosa', description: '玫瑰是蔷薇科蔷薇属的植物,具有浓郁的芳香和美丽的花朵。' },
{ name: '百合', confidence: 0.87, scientificName: 'Lilium brownii', description: '百合是百合科百合属的植物,象征纯洁和高雅。' },
{ name: '菊花', confidence: 0.82, scientificName: 'Chrysanthemum morifolium', description: '菊花是菊科菊属的植物,具有很高的观赏和药用价值。' }
];
// 选择置信度最高的结果
const bestResult = mockResults[0];
// 保存识别记录到数据库
const result = await dbConnector.query(
'INSERT INTO identifications (user_id, image_url, result, confidence) VALUES (?, ?, ?, ?)',
[userId, imageUrl, JSON.stringify(mockResults), bestResult.confidence]
);
res.json({
code: 200,
message: '识别成功',
data: {
identification_id: result.insertId,
image_url: imageUrl,
results: mockResults,
best_result: bestResult,
created_at: new Date().toISOString()
}
});
}));
/**
* 批量识别历史记录
*/
router.get('/batch/history', asyncHandler(async (req, res) => {
const { start_date, end_date, min_confidence = 0.7 } = req.query;
const userId = req.user.id;
let whereClause = 'WHERE user_id = ? AND confidence >= ?';
let queryParams = [userId, parseFloat(min_confidence)];
if (start_date) {
whereClause += ' AND created_at >= ?';
queryParams.push(start_date);
}
if (end_date) {
whereClause += ' AND created_at <= ?';
queryParams.push(end_date + ' 23:59:59');
}
const identifications = await dbConnector.query(
`SELECT
id, image_url, result, confidence, created_at,
DATE(created_at) as identify_date
FROM identifications
${whereClause}
ORDER BY created_at DESC`,
queryParams
);
// 按日期分组
const groupedByDate = {};
identifications.forEach(record => {
const date = record.identify_date;
if (!groupedByDate[date]) {
groupedByDate[date] = [];
}
groupedByDate[date].push(record);
});
res.json({
code: 200,
message: '获取成功',
data: {
total: identifications.length,
by_date: groupedByDate,
statistics: {
total_count: identifications.length,
avg_confidence: identifications.reduce((sum, item) => sum + item.confidence, 0) / identifications.length,
date_range: {
start: identifications.length > 0 ? identifications[identifications.length - 1].identify_date : null,
end: identifications.length > 0 ? identifications[0].identify_date : null
}
}
}
});
}));
/**
* 识别统计信息
*/
router.get('/stats/summary', asyncHandler(async (req, res) => {
const userId = req.user.id;
// 总识别次数
const totalCountResult = await dbConnector.query(
'SELECT COUNT(*) as total FROM identifications WHERE user_id = ?',
[userId]
);
// 平均置信度
const avgConfidenceResult = await dbConnector.query(
'SELECT AVG(confidence) as avg_confidence FROM identifications WHERE user_id = ?',
[userId]
);
// 最近识别时间
const lastIdentificationResult = await dbConnector.query(
'SELECT created_at FROM identifications WHERE user_id = ? ORDER BY created_at DESC LIMIT 1',
[userId]
);
// 识别最多的花卉类型需要解析result字段
const allIdentifications = await dbConnector.query(
'SELECT result FROM identifications WHERE user_id = ?',
[userId]
);
const flowerCounts = {};
allIdentifications.forEach(item => {
try {
const results = JSON.parse(item.result);
if (results && results.length > 0) {
const bestResult = results[0];
flowerCounts[bestResult.name] = (flowerCounts[bestResult.name] || 0) + 1;
}
} catch (e) {
console.error('解析识别结果失败:', e);
}
});
// 找出识别最多的花卉
let mostIdentifiedFlower = null;
let maxCount = 0;
for (const [flower, count] of Object.entries(flowerCounts)) {
if (count > maxCount) {
mostIdentifiedFlower = flower;
maxCount = count;
}
}
res.json({
code: 200,
message: '获取成功',
data: {
total_count: totalCountResult[0].total,
avg_confidence: avgConfidenceResult[0].avg_confidence || 0,
last_identification: lastIdentificationResult[0] ? lastIdentificationResult[0].created_at : null,
most_identified_flower: mostIdentifiedFlower,
flower_counts: flowerCounts,
weekly_trend: await getWeeklyTrend(userId)
}
});
}));
/**
* 获取周趋势数据
*/
async function getWeeklyTrend(userId) {
const weeklyData = await dbConnector.query(
`SELECT
DATE(created_at) as date,
COUNT(*) as count,
AVG(confidence) as avg_confidence
FROM identifications
WHERE user_id = ? AND created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)
GROUP BY DATE(created_at)
ORDER BY date DESC`,
[userId]
);
return weeklyData;
}
/**
* 删除识别记录
*/
router.delete('/:id', asyncHandler(async (req, res) => {
const { id } = req.params;
const userId = req.user.id;
// 先获取记录信息以删除图片文件
const identification = await dbConnector.query(
'SELECT image_url FROM identifications WHERE id = ? AND user_id = ?',
[id, userId]
);
if (identification.length === 0) {
return res.status(404).json({
code: 404,
message: '识别记录不存在',
data: null
});
}
// 删除图片文件
if (identification[0].image_url) {
const imagePath = path.join(__dirname, '../', identification[0].image_url);
if (fs.existsSync(imagePath)) {
fs.unlinkSync(imagePath);
}
}
// 删除数据库记录
const result = await dbConnector.query(
'DELETE FROM identifications WHERE id = ? AND user_id = ?',
[id, userId]
);
if (result.affectedRows === 0) {
return res.status(404).json({
code: 404,
message: '识别记录不存在',
data: null
});
}
res.json({
code: 200,
message: '删除成功',
data: null
});
}));
module.exports = router;

379
backend/routes/orders.js Normal file
View File

@@ -0,0 +1,379 @@
const express = require('express');
const dbConnector = require('../utils/dbConnector');
const { asyncHandler } = require('../middlewares/errorHandler');
const router = express.Router();
/**
* 获取订单列表
*/
router.get('/', asyncHandler(async (req, res) => {
const { page = 1, limit = 10, status, start_date, end_date } = req.query;
const userId = req.user.id;
const offset = (page - 1) * limit;
let whereClause = 'WHERE o.user_id = ?';
let queryParams = [userId];
if (status) {
whereClause += ' AND o.payment_status = ?';
queryParams.push(status);
}
if (start_date) {
whereClause += ' AND o.created_at >= ?';
queryParams.push(start_date);
}
if (end_date) {
whereClause += ' AND o.created_at <= ?';
queryParams.push(end_date + ' 23:59:59');
}
// 查询订单列表
const orders = await dbConnector.query(
`SELECT
o.id, o.order_number, o.user_id, o.total_amount, o.payment_status,
o.shipping_status, o.shipping_address, o.created_at, o.updated_at,
u.username, u.phone
FROM orders o
LEFT JOIN users u ON o.user_id = u.id
${whereClause}
ORDER BY o.created_at DESC
LIMIT ? OFFSET ?`,
[...queryParams, parseInt(limit), offset]
);
// 查询总数
const totalResult = await dbConnector.query(
`SELECT COUNT(*) as total
FROM orders o
${whereClause}`,
queryParams
);
res.json({
code: 200,
message: '获取成功',
data: {
orders,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total: totalResult[0].total,
pages: Math.ceil(totalResult[0].total / limit)
}
}
});
}));
/**
* 获取订单详情
*/
router.get('/:id', asyncHandler(async (req, res) => {
const { id } = req.params;
const userId = req.user.id;
// 查询订单基本信息
const orders = await dbConnector.query(
`SELECT
o.id, o.order_number, o.user_id, o.total_amount, o.payment_status,
o.shipping_status, o.shipping_address, o.created_at, o.updated_at,
u.username, u.phone, u.email
FROM orders o
LEFT JOIN users u ON o.user_id = u.id
WHERE o.id = ? AND o.user_id = ?`,
[id, userId]
);
if (orders.length === 0) {
return res.status(404).json({
code: 404,
message: '订单不存在',
data: null
});
}
const order = orders[0];
// 查询订单商品明细
const orderItems = await dbConnector.query(
`SELECT
oi.id, oi.order_id, oi.product_id, oi.quantity, oi.unit_price,
p.name as product_name, p.image as product_image
FROM order_items oi
LEFT JOIN products p ON oi.product_id = p.id
WHERE oi.order_id = ?`,
[id]
);
order.items = orderItems;
res.json({
code: 200,
message: '获取成功',
data: order
});
}));
/**
* 创建订单
*/
router.post('/', asyncHandler(async (req, res) => {
const { items, shipping_address } = req.body;
const userId = req.user.id;
// 参数验证
if (!items || !Array.isArray(items) || items.length === 0) {
return res.status(400).json({
code: 400,
message: '订单商品不能为空',
data: null
});
}
if (!shipping_address) {
return res.status(400).json({
code: 400,
message: '收货地址不能为空',
data: null
});
}
let connection;
try {
connection = await dbConnector.getConnection();
await connection.beginTransaction();
// 验证商品库存和价格
let totalAmount = 0;
const productUpdates = [];
for (const item of items) {
const { product_id, quantity } = item;
if (!product_id || !quantity || quantity <= 0) {
throw { code: 400, message: '商品ID和数量必须为正数' };
}
// 查询商品信息
const products = await connection.query(
'SELECT id, name, price, stock FROM products WHERE id = ? AND status = 1',
[product_id]
);
if (products.length === 0) {
throw { code: 404, message: `商品ID ${product_id} 不存在` };
}
const product = products[0];
// 检查库存
if (product.stock < quantity) {
throw { code: 400, message: `商品 ${product.name} 库存不足` };
}
const itemTotal = product.price * quantity;
totalAmount += itemTotal;
// 记录商品更新信息
productUpdates.push({
product_id,
quantity,
unit_price: product.price,
new_stock: product.stock - quantity
});
}
// 生成订单号
const orderNumber = 'O' + Date.now() + Math.random().toString(36).substr(2, 6);
// 创建订单
const orderResult = await connection.query(
'INSERT INTO orders (order_number, user_id, total_amount, shipping_address) VALUES (?, ?, ?, ?)',
[orderNumber, userId, totalAmount, shipping_address]
);
const orderId = orderResult.insertId;
// 创建订单商品明细
for (const item of items) {
const { product_id, quantity } = item;
const productInfo = productUpdates.find(p => p.product_id === product_id);
await connection.query(
'INSERT INTO order_items (order_id, product_id, quantity, unit_price) VALUES (?, ?, ?, ?)',
[orderId, product_id, quantity, productInfo.unit_price]
);
// 更新商品库存
await connection.query(
'UPDATE products SET stock = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
[productInfo.new_stock, product_id]
);
}
await connection.commit();
// 获取完整的订单信息
const newOrder = await dbConnector.query(
'SELECT * FROM orders WHERE id = ?',
[orderId]
);
res.status(201).json({
code: 201,
message: '订单创建成功',
data: newOrder[0]
});
} catch (error) {
if (connection) {
await connection.rollback();
}
if (error.code && error.message) {
return res.status(error.code).json({
code: error.code,
message: error.message,
data: null
});
}
throw error;
} finally {
if (connection) {
connection.release();
}
}
}));
/**
* 取消订单
*/
router.post('/:id/cancel', asyncHandler(async (req, res) => {
const { id } = req.params;
const userId = req.user.id;
let connection;
try {
connection = await dbConnector.getConnection();
await connection.beginTransaction();
// 查询订单信息
const orders = await connection.query(
'SELECT id, payment_status, shipping_status FROM orders WHERE id = ? AND user_id = ?',
[id, userId]
);
if (orders.length === 0) {
throw { code: 404, message: '订单不存在' };
}
const order = orders[0];
// 检查订单状态是否可以取消
if (order.payment_status === 'paid' || order.shipping_status === 'shipped') {
throw { code: 400, message: '订单已支付或已发货,无法取消' };
}
// 恢复商品库存
const orderItems = await connection.query(
'SELECT product_id, quantity FROM order_items WHERE order_id = ?',
[id]
);
for (const item of orderItems) {
await connection.query(
'UPDATE products SET stock = stock + ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
[item.quantity, item.product_id]
);
}
// 更新订单状态为已取消
await connection.query(
'UPDATE orders SET payment_status = "cancelled", updated_at = CURRENT_TIMESTAMP WHERE id = ?',
[id]
);
await connection.commit();
res.json({
code: 200,
message: '订单取消成功',
data: null
});
} catch (error) {
if (connection) {
await connection.rollback();
}
if (error.code && error.message) {
return res.status(error.code).json({
code: error.code,
message: error.message,
data: null
});
}
throw error;
} finally {
if (connection) {
connection.release();
}
}
}));
/**
* 更新订单状态(支付成功回调)
*/
router.put('/:id/status', asyncHandler(async (req, res) => {
const { id } = req.params;
const { payment_status, shipping_status } = req.body;
if (!payment_status && !shipping_status) {
return res.status(400).json({
code: 400,
message: '需要提供支付状态或发货状态',
data: null
});
}
const updateFields = [];
const updateValues = [];
if (payment_status) {
updateFields.push('payment_status = ?');
updateValues.push(payment_status);
}
if (shipping_status) {
updateFields.push('shipping_status = ?');
updateValues.push(shipping_status);
}
updateFields.push('updated_at = CURRENT_TIMESTAMP');
updateValues.push(id);
const result = await dbConnector.query(
`UPDATE orders SET ${updateFields.join(', ')} WHERE id = ?`,
updateValues
);
if (result.affectedRows === 0) {
return res.status(404).json({
code: 404,
message: '订单不存在',
data: null
});
}
res.json({
code: 200,
message: '订单状态更新成功',
data: null
});
}));
module.exports = router;

336
backend/routes/products.js Normal file
View File

@@ -0,0 +1,336 @@
const express = require('express');
const dbConnector = require('../utils/dbConnector');
const { optionalAuth } = require('../middlewares/auth');
const { asyncHandler } = require('../middlewares/errorHandler');
const router = express.Router();
/**
* 获取商品列表
*/
router.get('/', optionalAuth, asyncHandler(async (req, res) => {
const {
page = 1,
limit = 12,
category_id,
keyword,
min_price,
max_price,
sort_by = 'created_at',
sort_order = 'desc'
} = req.query;
const offset = (page - 1) * limit;
let whereClause = 'WHERE p.status = 1';
let queryParams = [];
// 构建查询条件
if (category_id) {
whereClause += ' AND p.category_id = ?';
queryParams.push(category_id);
}
if (keyword) {
whereClause += ' AND (p.name LIKE ? OR p.description LIKE ?)';
const likeKeyword = `%${keyword}%`;
queryParams.push(likeKeyword, likeKeyword);
}
if (min_price) {
whereClause += ' AND p.price >= ?';
queryParams.push(parseFloat(min_price));
}
if (max_price) {
whereClause += ' AND p.price <= ?';
queryParams.push(parseFloat(max_price));
}
// 验证排序参数
const validSortFields = ['name', 'price', 'created_at', 'stock'];
const validSortOrders = ['asc', 'desc'];
const sortField = validSortFields.includes(sort_by) ? sort_by : 'created_at';
const sortOrder = validSortOrders.includes(sort_order) ? sort_order : 'desc';
// 查询商品列表
const products = await dbConnector.query(
`SELECT
p.id, p.name, p.category_id, p.price, p.stock, p.image,
p.description, p.status, p.created_at, p.updated_at,
c.name as category_name
FROM products p
LEFT JOIN categories c ON p.category_id = c.id
${whereClause}
ORDER BY p.${sortField} ${sortOrder}
LIMIT ? OFFSET ?`,
[...queryParams, parseInt(limit), offset]
);
// 查询总数
const totalResult = await dbConnector.query(
`SELECT COUNT(*) as total
FROM products p
${whereClause}`,
queryParams
);
res.json({
code: 200,
message: '获取成功',
data: {
products,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total: totalResult[0].total,
pages: Math.ceil(totalResult[0].total / limit)
}
}
});
}));
/**
* 获取商品详情
*/
router.get('/:id', optionalAuth, asyncHandler(async (req, res) => {
const { id } = req.params;
const products = await dbConnector.query(
`SELECT
p.id, p.name, p.category_id, p.price, p.stock, p.image,
p.description, p.status, p.created_at, p.updated_at,
c.name as category_name
FROM products p
LEFT JOIN categories c ON p.category_id = c.id
WHERE p.id = ? AND p.status = 1`,
[id]
);
if (products.length === 0) {
return res.status(404).json({
code: 404,
message: '商品不存在',
data: null
});
}
res.json({
code: 200,
message: '获取成功',
data: products[0]
});
}));
/**
* 创建商品(需要管理员权限)
*/
router.post('/', asyncHandler(async (req, res) => {
const { name, category_id, price, stock, image, description } = req.body;
// 参数验证
if (!name || !category_id || price === undefined || stock === undefined) {
return res.status(400).json({
code: 400,
message: '商品名称、分类、价格和库存为必填项',
data: null
});
}
if (price < 0) {
return res.status(400).json({
code: 400,
message: '价格不能为负数',
data: null
});
}
if (stock < 0) {
return res.status(400).json({
code: 400,
message: '库存不能为负数',
data: null
});
}
// 检查分类是否存在
const category = await dbConnector.query(
'SELECT id FROM categories WHERE id = ? AND status = 1',
[category_id]
);
if (category.length === 0) {
return res.status(400).json({
code: 400,
message: '分类不存在',
data: null
});
}
// 创建商品
const result = await dbConnector.query(
'INSERT INTO products (name, category_id, price, stock, image, description) VALUES (?, ?, ?, ?, ?, ?)',
[name, category_id, price, stock, image, description]
);
// 获取创建的商品信息
const newProduct = await dbConnector.query(
'SELECT * FROM products WHERE id = ?',
[result.insertId]
);
res.status(201).json({
code: 201,
message: '商品创建成功',
data: newProduct[0]
});
}));
/**
* 更新商品信息
*/
router.put('/:id', asyncHandler(async (req, res) => {
const { id } = req.params;
const { name, category_id, price, stock, image, description, status } = req.body;
// 检查商品是否存在
const existingProduct = await dbConnector.query(
'SELECT id FROM products WHERE id = ?',
[id]
);
if (existingProduct.length === 0) {
return res.status(404).json({
code: 404,
message: '商品不存在',
data: null
});
}
// 参数验证
if (price !== undefined && price < 0) {
return res.status(400).json({
code: 400,
message: '价格不能为负数',
data: null
});
}
if (stock !== undefined && stock < 0) {
return res.status(400).json({
code: 400,
message: '库存不能为负数',
data: null
});
}
if (category_id !== undefined) {
const category = await dbConnector.query(
'SELECT id FROM categories WHERE id = ? AND status = 1',
[category_id]
);
if (category.length === 0) {
return res.status(400).json({
code: 400,
message: '分类不存在',
data: null
});
}
}
// 构建更新字段
const updateFields = [];
const updateValues = [];
if (name !== undefined) {
updateFields.push('name = ?');
updateValues.push(name);
}
if (category_id !== undefined) {
updateFields.push('category_id = ?');
updateValues.push(category_id);
}
if (price !== undefined) {
updateFields.push('price = ?');
updateValues.push(price);
}
if (stock !== undefined) {
updateFields.push('stock = ?');
updateValues.push(stock);
}
if (image !== undefined) {
updateFields.push('image = ?');
updateValues.push(image);
}
if (description !== undefined) {
updateFields.push('description = ?');
updateValues.push(description);
}
if (status !== undefined) {
updateFields.push('status = ?');
updateValues.push(status);
}
if (updateFields.length === 0) {
return res.status(400).json({
code: 400,
message: '没有提供需要更新的字段',
data: null
});
}
updateFields.push('updated_at = CURRENT_TIMESTAMP');
updateValues.push(id);
await dbConnector.query(
`UPDATE products SET ${updateFields.join(', ')} WHERE id = ?`,
updateValues
);
// 获取更新后的商品信息
const updatedProduct = await dbConnector.query(
'SELECT * FROM products WHERE id = ?',
[id]
);
res.json({
code: 200,
message: '商品更新成功',
data: updatedProduct[0]
});
}));
/**
* 删除商品(软删除)
*/
router.delete('/:id', asyncHandler(async (req, res) => {
const { id } = req.params;
const result = await dbConnector.query(
'UPDATE products SET status = 0, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
[id]
);
if (result.affectedRows === 0) {
return res.status(404).json({
code: 404,
message: '商品不存在',
data: null
});
}
res.json({
code: 200,
message: '商品删除成功',
data: null
});
}));
module.exports = router;

269
backend/routes/users.js Normal file
View File

@@ -0,0 +1,269 @@
const express = require('express');
const bcrypt = require('bcryptjs');
const validator = require('validator');
const dbConnector = require('../utils/dbConnector');
const { adminRequired } = require('../middlewares/auth');
const { asyncHandler } = require('../middlewares/errorHandler');
const router = express.Router();
/**
* 获取用户列表(管理员权限)
*/
router.get('/', adminRequired, asyncHandler(async (req, res) => {
const { page = 1, limit = 10, keyword, user_type } = req.query;
const offset = (page - 1) * limit;
let whereClause = 'WHERE status = 1';
let queryParams = [];
if (keyword) {
whereClause += ' AND (username LIKE ? OR phone LIKE ? OR email LIKE ?)';
const likeKeyword = `%${keyword}%`;
queryParams.push(likeKeyword, likeKeyword, likeKeyword);
}
if (user_type) {
whereClause += ' AND user_type = ?';
queryParams.push(user_type);
}
// 获取用户列表
const users = await dbConnector.query(
`SELECT id, username, phone, email, user_type, avatar_url, created_at, last_login
FROM users ${whereClause} ORDER BY created_at DESC LIMIT ? OFFSET ?`,
[...queryParams, parseInt(limit), offset]
);
// 获取总数
const totalResult = await dbConnector.query(
`SELECT COUNT(*) as total FROM users ${whereClause}`,
queryParams
);
res.json({
code: 200,
message: '获取成功',
data: {
users,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total: totalResult[0].total,
pages: Math.ceil(totalResult[0].total / limit)
}
}
});
}));
/**
* 获取用户详情
*/
router.get('/:id', asyncHandler(async (req, res) => {
const { id } = req.params;
const users = await dbConnector.query(
`SELECT id, username, phone, email, user_type, avatar_url,
real_name, created_at, last_login
FROM users WHERE id = ? AND status = 1`,
[id]
);
if (users.length === 0) {
return res.status(404).json({
code: 404,
message: '用户不存在',
data: null
});
}
res.json({
code: 200,
message: '获取成功',
data: users[0]
});
}));
/**
* 更新用户信息
*/
router.put('/:id', asyncHandler(async (req, res) => {
const { id } = req.params;
const { email, real_name, avatar_url } = req.body;
// 检查用户是否存在
const existingUser = await dbConnector.query(
'SELECT id FROM users WHERE id = ? AND status = 1',
[id]
);
if (existingUser.length === 0) {
return res.status(404).json({
code: 404,
message: '用户不存在',
data: null
});
}
// 验证邮箱格式
if (email && !validator.isEmail(email)) {
return res.status(400).json({
code: 400,
message: '邮箱格式不正确',
data: null
});
}
// 检查邮箱是否已被其他用户使用
if (email) {
const emailUser = await dbConnector.query(
'SELECT id FROM users WHERE email = ? AND id != ?',
[email, id]
);
if (emailUser.length > 0) {
return res.status(409).json({
code: 409,
message: '邮箱已被其他用户使用',
data: null
});
}
}
// 构建更新字段
const updateFields = [];
const updateValues = [];
if (email !== undefined) {
updateFields.push('email = ?');
updateValues.push(email);
}
if (real_name !== undefined) {
updateFields.push('real_name = ?');
updateValues.push(real_name);
}
if (avatar_url !== undefined) {
updateFields.push('avatar_url = ?');
updateValues.push(avatar_url);
}
if (updateFields.length === 0) {
return res.status(400).json({
code: 400,
message: '没有提供需要更新的字段',
data: null
});
}
updateFields.push('updated_at = CURRENT_TIMESTAMP');
updateValues.push(id);
await dbConnector.query(
`UPDATE users SET ${updateFields.join(', ')} WHERE id = ?`,
updateValues
);
// 获取更新后的用户信息
const updatedUser = await dbConnector.query(
'SELECT id, username, phone, email, user_type, avatar_url, real_name FROM users WHERE id = ?',
[id]
);
res.json({
code: 200,
message: '更新成功',
data: updatedUser[0]
});
}));
/**
* 修改密码
*/
router.put('/:id/password', asyncHandler(async (req, res) => {
const { id } = req.params;
const { old_password, new_password } = req.body;
if (!old_password || !new_password) {
return res.status(400).json({
code: 400,
message: '原密码和新密码为必填项',
data: null
});
}
if (new_password.length < 6) {
return res.status(400).json({
code: 400,
message: '新密码长度不能少于6位',
data: null
});
}
// 获取用户当前密码
const users = await dbConnector.query(
'SELECT password_hash FROM users WHERE id = ?',
[id]
);
if (users.length === 0) {
return res.status(404).json({
code: 404,
message: '用户不存在',
data: null
});
}
// 验证原密码
const isValidPassword = await bcrypt.compare(old_password, users[0].password_hash);
if (!isValidPassword) {
return res.status(401).json({
code: 401,
message: '原密码不正确',
data: null
});
}
// 加密新密码
const hashedPassword = await bcrypt.hash(new_password, 12);
await dbConnector.query(
'UPDATE users SET password_hash = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
[hashedPassword, id]
);
res.json({
code: 200,
message: '密码修改成功',
data: null
});
}));
/**
* 删除用户(软删除)
*/
router.delete('/:id', adminRequired, asyncHandler(async (req, res) => {
const { id } = req.params;
const result = await dbConnector.query(
'UPDATE users SET status = 0, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
[id]
);
if (result.affectedRows === 0) {
return res.status(404).json({
code: 404,
message: '用户不存在',
data: null
});
}
res.json({
code: 200,
message: '用户删除成功',
data: null
});
}));
module.exports = router;

View File

@@ -0,0 +1,165 @@
#!/usr/bin/env node
/**
* 数据库初始化脚本
* 用于验证数据库连接和创建基础表结构
*/
const mysql = require('mysql2/promise');
const databaseConfig = require('../config/database');
const path = require('path');
const fs = require('fs');
class DatabaseInitializer {
constructor() {
this.connection = null;
}
/**
* 创建数据库连接
*/
async createConnection() {
try {
this.connection = await mysql.createConnection({
host: databaseConfig.host,
port: databaseConfig.port,
user: databaseConfig.username,
password: databaseConfig.password,
database: databaseConfig.database,
charset: 'utf8mb4',
timezone: '+08:00'
});
console.log('✅ 数据库连接成功');
return true;
} catch (error) {
console.error('❌ 数据库连接失败:', error.message);
return false;
}
}
/**
* 验证数据库连接
*/
async validateConnection() {
try {
const [rows] = await this.connection.execute('SELECT NOW() as current_time, VERSION() as mysql_version');
console.log('📊 数据库信息:');
console.log(` 当前时间: ${rows[0].current_time}`);
console.log(` MySQL版本: ${rows[0].mysql_version}`);
return true;
} catch (error) {
console.error('❌ 数据库验证失败:', error.message);
return false;
}
}
/**
* 检查表是否存在
* @param {string} tableName - 表名
*/
async checkTableExists(tableName) {
try {
const [rows] = await this.connection.execute(
`SELECT COUNT(*) as count FROM information_schema.tables
WHERE table_schema = ? AND table_name = ?`,
[databaseConfig.database, tableName]
);
return rows[0].count > 0;
} catch (error) {
console.error(`❌ 检查表 ${tableName} 存在失败:`, error.message);
return false;
}
}
/**
* 执行SQL文件
* @param {string} filePath - SQL文件路径
*/
async executeSqlFile(filePath) {
try {
const sqlContent = fs.readFileSync(filePath, 'utf8');
const statements = sqlContent.split(';').filter(stmt => stmt.trim());
for (const statement of statements) {
if (statement.trim()) {
await this.connection.execute(statement);
}
}
console.log(`✅ 成功执行SQL文件: ${path.basename(filePath)}`);
return true;
} catch (error) {
console.error(`❌ 执行SQL文件失败:`, error.message);
return false;
}
}
/**
* 关闭数据库连接
*/
async closeConnection() {
if (this.connection) {
await this.connection.end();
console.log('🔌 数据库连接已关闭');
}
}
/**
* 主初始化方法
*/
async initialize() {
console.log('🚀 开始数据库初始化...');
console.log(`📋 环境: ${process.env.NODE_ENV || 'development'}`);
console.log(`🗄️ 数据库: ${databaseConfig.database}`);
console.log(`🌐 主机: ${databaseConfig.host}:${databaseConfig.port}`);
console.log('─'.repeat(50));
// 创建连接
const connected = await this.createConnection();
if (!connected) {
process.exit(1);
}
// 验证连接
const validated = await this.validateConnection();
if (!validated) {
await this.closeConnection();
process.exit(1);
}
console.log('✅ 数据库连接验证通过');
console.log('─'.repeat(50));
// 这里可以添加具体的表创建逻辑
console.log('📋 数据库初始化完成');
console.log('✅ 所有检查通过,数据库连接正常');
await this.closeConnection();
}
}
// 执行初始化
const initializer = new DatabaseInitializer();
// 处理命令行参数
const args = process.argv.slice(2);
if (args.includes('--help') || args.includes('-h')) {
console.log(`
使用方法: node scripts/initDatabase.js [选项]
选项:
--help, -h 显示帮助信息
--check 只检查连接,不执行初始化
示例:
node scripts/initDatabase.js # 完整初始化
node scripts/initDatabase.js --check # 只检查连接
`);
process.exit(0);
}
initializer.initialize().catch(error => {
console.error('❌ 初始化过程中发生错误:', error.message);
process.exit(1);
});

View File

@@ -0,0 +1,152 @@
/**
* 数据库连接工具类
* 封装MySQL数据库连接和基本操作
*/
const mysql = require('mysql2/promise');
const databaseConfig = require('../config/database');
class DBConnector {
constructor() {
this.pool = null;
this.connection = null;
this.init();
}
/**
* 初始化数据库连接池
*/
init() {
try {
this.pool = mysql.createPool({
host: databaseConfig.host,
port: databaseConfig.port,
user: databaseConfig.username,
password: databaseConfig.password,
database: databaseConfig.database,
connectionLimit: databaseConfig.pool.max,
acquireTimeout: databaseConfig.pool.acquire,
timeout: databaseConfig.pool.idle,
charset: 'utf8mb4',
timezone: '+08:00',
decimalNumbers: true,
supportBigNumbers: true,
bigNumberStrings: false
});
console.log('✅ 数据库连接池初始化成功');
} catch (error) {
console.error('❌ 数据库连接池初始化失败:', error.message);
throw error;
}
}
/**
* 获取数据库连接
* @returns {Promise} 数据库连接对象
*/
async getConnection() {
try {
this.connection = await this.pool.getConnection();
return this.connection;
} catch (error) {
console.error('❌ 获取数据库连接失败:', error.message);
throw error;
}
}
/**
* 执行SQL查询
* @param {string} sql - SQL语句
* @param {Array} params - 参数数组
* @returns {Promise} 查询结果
*/
async query(sql, params = []) {
let connection;
try {
connection = await this.getConnection();
const [rows] = await connection.execute(sql, params);
return rows;
} catch (error) {
console.error('❌ SQL执行失败:', error.message);
console.error('SQL:', sql);
console.error('参数:', params);
throw error;
} finally {
if (connection) {
connection.release();
}
}
}
/**
* 开启事务
* @returns {Promise} 事务连接对象
*/
async beginTransaction() {
try {
const connection = await this.getConnection();
await connection.beginTransaction();
return connection;
} catch (error) {
console.error('❌ 开启事务失败:', error.message);
throw error;
}
}
/**
* 提交事务
* @param {Object} connection - 事务连接对象
*/
async commitTransaction(connection) {
try {
await connection.commit();
connection.release();
} catch (error) {
console.error('❌ 提交事务失败:', error.message);
throw error;
}
}
/**
* 回滚事务
* @param {Object} connection - 事务连接对象
*/
async rollbackTransaction(connection) {
try {
await connection.rollback();
connection.release();
} catch (error) {
console.error('❌ 回滚事务失败:', error.message);
throw error;
}
}
/**
* 关闭连接池
*/
async close() {
if (this.pool) {
await this.pool.end();
console.log('✅ 数据库连接池已关闭');
}
}
/**
* 健康检查
* @returns {Promise<boolean>} 数据库连接状态
*/
async healthCheck() {
try {
const result = await this.query('SELECT 1 as status');
return result[0].status === 1;
} catch (error) {
return false;
}
}
}
// 创建全局数据库连接实例
const dbConnector = new DBConnector();
module.exports = dbConnector;