重构后端服务架构并优化前端错误处理

This commit is contained in:
ylweng
2025-09-05 01:18:40 +08:00
parent 86322c6f50
commit 5853953f79
20 changed files with 608 additions and 772 deletions

View File

@@ -5,19 +5,19 @@ NODE_ENV=development
VITE_APP_TITLE=活牛采购智能数字化系统 - 管理后台
# API接口地址
VITE_API_BASE_URL=http://localhost:3001/api
VITE_API_BASE_URL=http://localhost:3002/api
# WebSocket地址
VITE_WS_BASE_URL=ws://localhost:3001
VITE_WS_BASE_URL=ws://localhost:3002
# 上传文件地址
VITE_UPLOAD_URL=http://localhost:3001/api/upload
VITE_UPLOAD_URL=http://localhost:3002/api/upload
# 静态资源地址
VITE_STATIC_URL=http://localhost:3001/static
VITE_STATIC_URL=http://localhost:3002/static
# 是否启用Mock数据
VITE_USE_MOCK=true
VITE_USE_MOCK=false
# 是否启用开发工具
VITE_DEV_TOOLS=true

View File

@@ -0,0 +1,91 @@
{
"globals": {
"Component": true,
"ComponentPublicInstance": true,
"ComputedRef": true,
"DirectiveBinding": true,
"EffectScope": true,
"ExtractDefaultPropTypes": true,
"ExtractPropTypes": true,
"ExtractPublicPropTypes": true,
"InjectionKey": true,
"MaybeRef": true,
"MaybeRefOrGetter": true,
"PropType": true,
"Ref": true,
"VNode": true,
"WritableComputedRef": true,
"acceptHMRUpdate": true,
"computed": true,
"createApp": true,
"createPinia": true,
"customRef": true,
"defineAsyncComponent": true,
"defineComponent": true,
"defineStore": true,
"effectScope": true,
"getActivePinia": true,
"getCurrentInstance": true,
"getCurrentScope": true,
"h": true,
"inject": true,
"isProxy": true,
"isReactive": true,
"isReadonly": true,
"isRef": true,
"mapActions": true,
"mapGetters": true,
"mapState": true,
"mapStores": true,
"mapWritableState": true,
"markRaw": true,
"nextTick": true,
"onActivated": true,
"onBeforeMount": true,
"onBeforeRouteLeave": true,
"onBeforeRouteUpdate": true,
"onBeforeUnmount": true,
"onBeforeUpdate": true,
"onDeactivated": true,
"onErrorCaptured": true,
"onMounted": true,
"onRenderTracked": true,
"onRenderTriggered": true,
"onScopeDispose": true,
"onServerPrefetch": true,
"onUnmounted": true,
"onUpdated": true,
"onWatcherCleanup": true,
"provide": true,
"reactive": true,
"readonly": true,
"ref": true,
"resolveComponent": true,
"setActivePinia": true,
"setMapStoreSuffix": true,
"shallowReactive": true,
"shallowReadonly": true,
"shallowRef": true,
"storeToRefs": true,
"toRaw": true,
"toRef": true,
"toRefs": true,
"toValue": true,
"triggerRef": true,
"unref": true,
"useAttrs": true,
"useCssModule": true,
"useCssVars": true,
"useId": true,
"useLink": true,
"useModel": true,
"useRoute": true,
"useRouter": true,
"useSlots": true,
"useTemplateRef": true,
"watch": true,
"watchEffect": true,
"watchPostEffect": true,
"watchSyncEffect": true
}
}

View File

@@ -1287,9 +1287,9 @@
}
},
"node_modules/@types/node": {
"version": "20.19.12",
"resolved": "https://registry.npmmirror.com/@types/node/-/node-20.19.12.tgz",
"integrity": "sha512-lSOjyS6vdO2G2g2CWrETTV3Jz2zlCXHpu1rcubLKpz9oj+z/1CceHlj+yq53W+9zgb98nSov/wjEKYDNauD+Hw==",
"version": "20.19.13",
"resolved": "https://registry.npmmirror.com/@types/node/-/node-20.19.13.tgz",
"integrity": "sha512-yCAeZl7a0DxgNVteXFHt9+uyFbqXGy/ShC4BlcHkoE0AfGXYv/BUiplV72DjMYXHDBXFjhvr6DD1NiRVfB4j8g==",
"dev": true,
"license": "MIT",
"dependencies": {

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100">
<rect width="100" height="100" rx="20" fill="#4CAF50" />
<text x="50" y="65" font-family="Arial, sans-serif" font-size="50" font-weight="bold" text-anchor="middle" fill="white">N</text>
</svg>

After

Width:  |  Height:  |  Size: 284 B

View File

@@ -3,9 +3,9 @@
<!-- 侧边栏 -->
<el-aside :width="isCollapse ? '64px' : '240px'" class="layout-aside">
<div class="logo-container">
<img v-if="!isCollapse" src="/logo.png" alt="Logo" class="logo" />
<img v-if="!isCollapse" src="/logo.svg" alt="Logo" class="logo" />
<span v-if="!isCollapse" class="logo-text">NiuMall</span>
<img v-else src="/logo.png" alt="Logo" class="logo-mini" />
<img v-else src="/logo.svg" alt="Logo" class="logo-mini" />
</div>
<el-menu

View File

@@ -29,10 +29,14 @@ export const useUserStore = defineStore('user', () => {
localStorage.setItem('token', access_token)
localStorage.setItem('userInfo', JSON.stringify(user))
ElMessage.success('登录成功')
ElMessage.success({
message: '登录成功',
grouping: true,
duration: 3000
})
return Promise.resolve()
} catch (error: any) {
ElMessage.error(error.message || '登录失败')
// 错误信息已在request拦截器中统一处理
return Promise.reject(error)
}
}
@@ -57,8 +61,13 @@ export const useUserStore = defineStore('user', () => {
const logoutAction = async () => {
try {
await logout()
} catch (error) {
console.error('登出接口调用失败:', error)
ElMessage.success({
message: '已退出登录',
grouping: true,
duration: 3000
})
} catch (error: any) {
// 错误信息已在request拦截器中统一处理
} finally {
// 清除状态和本地存储
token.value = ''

View File

@@ -3,6 +3,19 @@ import type { AxiosResponse, AxiosError } from 'axios'
import { ElMessage } from 'element-plus'
import { useUserStore } from '@/stores/user'
// 错误代码映射表
const ERROR_CODES: Record<string, string> = {
'AUTH_INVALID_CREDENTIALS': '用户名或密码错误',
'AUTH_ACCOUNT_LOCKED': '账户已被锁定,请联系管理员',
'AUTH_ACCOUNT_DISABLED': '账户已被禁用',
'AUTH_TOKEN_EXPIRED': '登录已过期,请重新登录',
'AUTH_INVALID_TOKEN': '无效的登录凭证',
'NETWORK_ERROR': '网络错误,请检查网络连接',
'TIMEOUT_ERROR': '请求超时,请稍后重试',
'SERVER_ERROR': '服务器内部错误',
'UNKNOWN_ERROR': '未知错误,请联系管理员'
}
// 创建axios实例
const request = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000/api',
@@ -36,8 +49,14 @@ request.interceptors.response.use(
// 检查业务状态码
if (data.success === false) {
ElMessage.error(data.message || '请求失败')
return Promise.reject(new Error(data.message || '请求失败'))
const errorCode = data.code || 'UNKNOWN_ERROR'
const errorMsg = ERROR_CODES[errorCode] || data.message || '请求失败'
ElMessage.error({
message: errorMsg,
grouping: true,
duration: 5000
})
return Promise.reject(new Error(errorMsg))
}
return data
@@ -47,30 +66,61 @@ request.interceptors.response.use(
const { response } = error
if (response) {
const errorCode = (response.data as any)?.code
const errorMsg = errorCode ? ERROR_CODES[errorCode] : undefined
switch (response.status) {
case 401:
ElMessage.error('未授权,请重新登录')
ElMessage.error({
message: errorMsg || '未授权,请重新登录',
grouping: true,
duration: 5000
})
// 清除登录状态并跳转到登录页
const userStore = useUserStore()
userStore.logoutAction()
window.location.href = '/login'
break
case 403:
ElMessage.error('访问被拒绝,权限不足')
ElMessage.error({
message: errorMsg || '访问被拒绝,权限不足',
grouping: true,
duration: 5000
})
break
case 404:
ElMessage.error('请求的资源不存在')
ElMessage.error({
message: errorMsg || '请求的资源不存在',
grouping: true,
duration: 5000
})
break
case 500:
ElMessage.error('服务器内部错误')
ElMessage.error({
message: errorMsg || '服务器内部错误',
grouping: true,
duration: 5000
})
break
default:
ElMessage.error(`请求失败: ${response.status}`)
ElMessage.error({
message: errorMsg || `请求失败: ${response.status}`,
grouping: true,
duration: 5000
})
}
} else if (error.code === 'ECONNABORTED') {
ElMessage.error('请求超时,请稍后重试')
ElMessage.error({
message: '请求超时,请稍后重试',
grouping: true,
duration: 5000
})
} else {
ElMessage.error('网络错误,请检查网络连接')
ElMessage.error({
message: '网络错误,请检查网络连接',
grouping: true,
duration: 5000
})
}
return Promise.reject(error)

View File

@@ -3,7 +3,7 @@
<div class="login-box">
<div class="login-header">
<div class="logo">
<img src="/logo.png" alt="Logo" class="logo-img" />
<img src="/logo.svg" alt="Logo" class="logo-img" />
<h1 class="title">活牛采购智能数字化系统</h1>
</div>
<p class="subtitle">管理后台</p>

View File

@@ -95,7 +95,7 @@ export default defineConfig(({ mode }) => {
css: {
preprocessorOptions: {
scss: {
additionalData: `@import "@/assets/styles/variables.scss"; @import "@/assets/styles/mixins.scss";`
additionalData: `@import "@/style/variables.scss"; @import "@/style/mixins.scss";`
}
}
}

23
backend/.env Normal file
View File

@@ -0,0 +1,23 @@
# 数据库配置
DB_HOST=129.211.213.226
DB_PORT=9527
DB_USERNAME=root
DB_PASSWORD=aiotAiot123!
DB_NAME=jiebandata
# Redis配置
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=
# JWT配置
JWT_SECRET=niumall_jwt_secret_key_2024
JWT_EXPIRES_IN=24h
# 应用配置
NODE_ENV=development
PORT=3002
API_PREFIX=/api
# 日志配置
LOG_LEVEL=info

View File

@@ -1,61 +0,0 @@
# 应用配置
NODE_ENV=development
PORT=3001
APP_NAME=活牛采购智能数字化系统
# 数据库配置
DB_HOST=129.211.213.226
DB_PORT=9527
DB_NAME=jiebandata
DB_USER=root
DB_PASSWORD=aiotAiot123!
DB_DIALECT=mysql
# Redis配置 (本地开发)
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=
REDIS_DB=0
# JWT配置
JWT_SECRET=niumall_jwt_secret_2024_cattle_procurement_system
JWT_EXPIRES_IN=24h
JWT_REFRESH_EXPIRES_IN=7d
# 文件上传配置
UPLOAD_PATH=./uploads
MAX_FILE_SIZE=50MB
ALLOWED_FILE_TYPES=jpg,jpeg,png,gif,pdf,doc,docx,xls,xlsx,mp4,avi
# 日志配置
LOG_LEVEL=info
LOG_FILE=./logs/app.log
# 短信配置
SMS_PROVIDER=aliyun
SMS_ACCESS_KEY=
SMS_SECRET_KEY=
# 支付配置
PAYMENT_PROVIDER=wechat
WECHAT_APPID=
WECHAT_SECRET=
WECHAT_MERCHANT_ID=
WECHAT_API_KEY=
# WebSocket配置
WS_PORT=3002
# 监控配置
ENABLE_MONITORING=true
MONITORING_TOKEN=
# 邮件配置
SMTP_HOST=
SMTP_PORT=587
SMTP_USER=
SMTP_PASS=
# API限流配置
RATE_LIMIT_WINDOW=15
RATE_LIMIT_MAX_REQUESTS=100

View File

@@ -4,10 +4,11 @@ const helmet = require('helmet')
const morgan = require('morgan')
const rateLimit = require('express-rate-limit')
const compression = require('compression')
const { testConnection, syncDatabase } = require('./config/database')
const { createInitialUsers } = require('./scripts/initData')
require('dotenv').config()
// 数据库连接
const { testConnection, syncModels } = require('./models')
const app = express()
// 中间件配置
@@ -77,16 +78,13 @@ const startServer = async () => {
// 测试数据库连接
const dbConnected = await testConnection();
if (!dbConnected) {
console.log('⚠️ 数据库连接失败,使用模拟数据模式');
} else {
// 同步数据库模型(开发环境)
if (process.env.NODE_ENV === 'development') {
await syncDatabase({ alter: true });
// 创建初始用户数据
await createInitialUsers();
}
console.error(' 数据库连接失败,服务器启动终止');
process.exit(1);
}
// 同步数据库模型
await syncModels();
app.listen(PORT, () => {
console.log(`🚀 服务器启动成功`)
console.log(`📱 运行环境: ${process.env.NODE_ENV || 'development'}`)
@@ -100,4 +98,4 @@ const startServer = async () => {
}
}
startServer();
startServer()

View File

@@ -1,55 +1,58 @@
const { Sequelize } = require('sequelize');
// 数据库配置文件
require('dotenv').config();
// 数据库连接配置
const sequelize = new Sequelize({
host: process.env.DB_HOST || '129.211.213.226',
port: process.env.DB_PORT || 9527,
database: process.env.DB_NAME || 'jiebandata',
username: process.env.DB_USER || 'root',
password: process.env.DB_PASSWORD || 'aiotAiot123!',
dialect: process.env.DB_DIALECT || 'mysql',
logging: process.env.NODE_ENV === 'development' ? console.log : false,
pool: {
max: 5,
min: 0,
acquire: 30000,
idle: 10000
},
define: {
timestamps: true,
underscored: true,
freezeTableName: true
},
timezone: '+08:00'
});
// 测试数据库连接
const testConnection = async () => {
try {
await sequelize.authenticate();
console.log('✅ 数据库连接成功');
return true;
} catch (error) {
console.error('❌ 数据库连接失败:', error.message);
return false;
}
};
// 同步数据库模型
const syncDatabase = async (options = {}) => {
try {
await sequelize.sync(options);
console.log('✅ 数据库同步成功');
} catch (error) {
console.error('❌ 数据库同步失败:', error);
throw error;
}
};
module.exports = {
sequelize,
testConnection,
syncDatabase,
Sequelize
development: {
username: process.env.DB_USERNAME || 'root',
password: process.env.DB_PASSWORD || 'aiotAiot123!',
database: process.env.DB_NAME || 'jiebandata',
host: process.env.DB_HOST || '129.211.213.226',
port: process.env.DB_PORT || 9527,
dialect: 'mysql',
dialectOptions: {
charset: 'utf8mb4',
dateStrings: true,
typeCast: true
},
timezone: '+08:00',
logging: console.log,
pool: {
max: 20,
min: 0,
acquire: 60000,
idle: 10000
}
},
test: {
username: process.env.TEST_DB_USERNAME || 'root',
password: process.env.TEST_DB_PASSWORD || 'aiotAiot123!',
database: process.env.TEST_DB_NAME || 'jiebandata_test',
host: process.env.TEST_DB_HOST || '129.211.213.226',
port: process.env.TEST_DB_PORT || 9527,
dialect: 'mysql',
dialectOptions: {
charset: 'utf8mb4'
},
timezone: '+08:00',
logging: false
},
production: {
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
host: process.env.DB_HOST,
port: process.env.DB_PORT,
dialect: 'mysql',
dialectOptions: {
charset: 'utf8mb4'
},
timezone: '+08:00',
logging: false,
pool: {
max: 50,
min: 5,
acquire: 60000,
idle: 10000
}
}
};

View File

@@ -1,184 +0,0 @@
const jwt = require('jsonwebtoken');
const User = require('../models/User');
// JWT认证中间件
const authenticateToken = async (req, res, next) => {
try {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
if (!token) {
return res.status(401).json({
success: false,
message: '访问令牌缺失',
code: 'TOKEN_MISSING'
});
}
// 验证token
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// 查找用户
const user = await User.findByPk(decoded.userId, {
attributes: { exclude: ['password_hash'] }
});
if (!user) {
return res.status(401).json({
success: false,
message: '用户不存在',
code: 'USER_NOT_FOUND'
});
}
if (user.status !== 'active') {
return res.status(401).json({
success: false,
message: '用户账号已被禁用',
code: 'USER_DISABLED'
});
}
// 将用户信息附加到请求对象
req.user = user;
next();
} catch (error) {
if (error.name === 'JsonWebTokenError') {
return res.status(401).json({
success: false,
message: '无效的访问令牌',
code: 'INVALID_TOKEN'
});
} else if (error.name === 'TokenExpiredError') {
return res.status(401).json({
success: false,
message: '访问令牌已过期',
code: 'TOKEN_EXPIRED'
});
}
return res.status(500).json({
success: false,
message: '认证服务错误',
error: error.message
});
}
};
// 权限检查中间件
const requireRole = (roles) => {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({
success: false,
message: '用户未认证',
code: 'USER_NOT_AUTHENTICATED'
});
}
const userRoles = Array.isArray(roles) ? roles : [roles];
if (!userRoles.includes(req.user.user_type)) {
return res.status(403).json({
success: false,
message: '权限不足',
code: 'INSUFFICIENT_PERMISSIONS',
requiredRoles: userRoles,
userRole: req.user.user_type
});
}
next();
};
};
// 生成JWT token
const generateToken = (user) => {
const payload = {
userId: user.id,
username: user.username,
userType: user.user_type
};
return {
accessToken: jwt.sign(payload, process.env.JWT_SECRET, {
expiresIn: process.env.JWT_EXPIRES_IN || '24h'
}),
refreshToken: jwt.sign(
{ userId: user.id },
process.env.JWT_SECRET + '_refresh',
{ expiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '7d' }
)
};
};
// 刷新token
const refreshToken = async (req, res, next) => {
try {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(400).json({
success: false,
message: '刷新令牌缺失'
});
}
const decoded = jwt.verify(refreshToken, process.env.JWT_SECRET + '_refresh');
const user = await User.findByPk(decoded.userId, {
attributes: { exclude: ['password_hash'] }
});
if (!user || user.status !== 'active') {
return res.status(401).json({
success: false,
message: '无效的刷新令牌'
});
}
const tokens = generateToken(user);
res.json({
success: true,
message: '令牌刷新成功',
data: tokens
});
} catch (error) {
return res.status(401).json({
success: false,
message: '刷新令牌无效或已过期'
});
}
};
// 可选认证中间件(不强制要求登录)
const optionalAuth = async (req, res, next) => {
try {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (token) {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const user = await User.findByPk(decoded.userId, {
attributes: { exclude: ['password_hash'] }
});
if (user && user.status === 'active') {
req.user = user;
}
}
next();
} catch (error) {
// 忽略错误,继续请求
next();
}
};
module.exports = {
authenticateToken,
requireRole,
generateToken,
refreshToken,
optionalAuth
};

View File

@@ -1,122 +0,0 @@
const { DataTypes } = require('sequelize');
const { sequelize } = require('../config/database');
const bcrypt = require('bcryptjs');
// 用户模型
const User = sequelize.define('User', {
id: {
type: DataTypes.BIGINT,
primaryKey: true,
autoIncrement: true
},
uuid: {
type: DataTypes.STRING(36),
allowNull: false,
unique: true,
defaultValue: DataTypes.UUIDV4
},
username: {
type: DataTypes.STRING(50),
allowNull: false,
unique: true,
validate: {
len: [2, 50]
}
},
password_hash: {
type: DataTypes.STRING(255),
allowNull: false
},
phone: {
type: DataTypes.STRING(20),
allowNull: false,
unique: true,
validate: {
is: /^1[3-9]\d{9}$/
}
},
email: {
type: DataTypes.STRING(100),
validate: {
isEmail: true
}
},
real_name: DataTypes.STRING(50),
avatar_url: DataTypes.STRING(255),
user_type: {
type: DataTypes.ENUM('client', 'supplier', 'driver', 'staff', 'admin'),
allowNull: false,
defaultValue: 'client'
},
status: {
type: DataTypes.ENUM('active', 'inactive', 'locked'),
defaultValue: 'active'
},
last_login_at: DataTypes.DATE,
login_count: {
type: DataTypes.INTEGER,
defaultValue: 0
}
}, {
tableName: 'users',
timestamps: true,
paranoid: true, // 软删除
indexes: [
{ fields: ['phone'] },
{ fields: ['user_type'] },
{ fields: ['status'] },
{ fields: ['username'] }
],
hooks: {
beforeCreate: async (user) => {
if (user.password_hash) {
user.password_hash = await bcrypt.hash(user.password_hash, 12);
}
},
beforeUpdate: async (user) => {
if (user.changed('password_hash')) {
user.password_hash = await bcrypt.hash(user.password_hash, 12);
}
}
}
});
// 实例方法:验证密码
User.prototype.validatePassword = async function(password) {
return await bcrypt.compare(password, this.password_hash);
};
// 实例方法:更新登录信息
User.prototype.updateLoginInfo = async function() {
this.last_login_at = new Date();
this.login_count += 1;
await this.save();
};
// 类方法:根据用户名或手机号查找用户
User.findByLoginIdentifier = async function(identifier) {
return await this.findOne({
where: {
[sequelize.Sequelize.Op.or]: [
{ username: identifier },
{ phone: identifier }
]
}
});
};
// 类方法:创建用户
User.createUser = async function(userData) {
const { username, password, phone, email, real_name, user_type = 'client' } = userData;
return await this.create({
username,
password_hash: password,
phone,
email,
real_name,
user_type
});
};
module.exports = User;

140
backend/models/index.js Normal file
View File

@@ -0,0 +1,140 @@
// 数据库连接和模型定义
const { Sequelize } = require('sequelize');
const config = require('../config/database.js');
// 根据环境变量选择配置
const env = process.env.NODE_ENV || 'development';
const dbConfig = config[env];
// 创建Sequelize实例
const sequelize = new Sequelize(
dbConfig.database,
dbConfig.username,
dbConfig.password,
{
host: dbConfig.host,
port: dbConfig.port,
dialect: dbConfig.dialect,
dialectOptions: dbConfig.dialectOptions,
timezone: dbConfig.timezone,
logging: dbConfig.logging,
pool: dbConfig.pool
}
);
// 测试数据库连接
const testConnection = async () => {
try {
await sequelize.authenticate();
console.log('✅ 数据库连接成功');
return true;
} catch (error) {
console.error('❌ 数据库连接失败:', error);
return false;
}
};
// 定义模型
const models = {
sequelize,
Sequelize,
// 用户模型(匹配实际数据库结构)
User: sequelize.define('User', {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true
},
openid: {
type: Sequelize.STRING(64),
allowNull: false,
unique: true
},
nickname: {
type: Sequelize.STRING(50),
allowNull: false
},
avatar: {
type: Sequelize.STRING(255)
},
gender: {
type: Sequelize.ENUM('male', 'female', 'other')
},
birthday: {
type: Sequelize.DATE
},
phone: {
type: Sequelize.STRING(20),
unique: true
},
email: {
type: Sequelize.STRING(100),
unique: true
},
uuid: {
type: Sequelize.STRING(36),
unique: true
}
}, {
tableName: 'users',
timestamps: true,
createdAt: 'created_at',
updatedAt: 'updated_at'
}),
// 为了兼容现有API创建一个简化版的用户模型
ApiUser: sequelize.define('ApiUser', {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true
},
username: {
type: Sequelize.STRING(50),
allowNull: false,
unique: true
},
password_hash: {
type: Sequelize.STRING(255),
allowNull: false
},
phone: {
type: Sequelize.STRING(20),
allowNull: false,
unique: true
},
email: {
type: Sequelize.STRING(100)
},
user_type: {
type: Sequelize.ENUM('client', 'supplier', 'driver', 'staff', 'admin'),
allowNull: false
},
status: {
type: Sequelize.ENUM('active', 'inactive', 'locked'),
defaultValue: 'active'
}
}, {
tableName: 'api_users',
timestamps: true
})
};
// 同步数据库模型
const syncModels = async () => {
try {
// 只同步API用户表如果不存在则创建
await models.ApiUser.sync({ alter: true });
console.log('✅ API用户表同步成功');
console.log('✅ 数据库模型同步完成');
} catch (error) {
console.error('❌ 数据库模型同步失败:', error);
}
};
module.exports = {
...models,
testConnection,
syncModels
};

View File

@@ -2,10 +2,10 @@
"name": "niumall-backend",
"version": "1.0.0",
"description": "活牛采购智能数字化系统 - 后端服务",
"main": "app.js",
"main": "src/app.js",
"scripts": {
"start": "node app.js",
"dev": "nodemon app.js",
"start": "node src/app.js",
"dev": "nodemon src/app.js",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",

View File

@@ -1,186 +1,172 @@
const express = require('express');
const router = express.Router();
const Joi = require('joi');
const User = require('../models/User');
const { generateToken, refreshToken, authenticateToken } = require('../middleware/auth');
const express = require('express')
const bcrypt = require('bcryptjs')
const jwt = require('jsonwebtoken')
const Joi = require('joi')
const router = express.Router()
// 验证schema
// 引入数据库模型
const { ApiUser } = require('../models')
// 登录参数验证
const loginSchema = Joi.object({
username: Joi.string().min(2).max(50).required(),
password: Joi.string().min(6).max(100).required()
});
})
const passwordResetRequestSchema = Joi.object({
phone: Joi.string().pattern(/^1[3-9]\d{9}$/).required()
});
const passwordResetConfirmSchema = Joi.object({
phone: Joi.string().pattern(/^1[3-9]\d{9}$/).required(),
resetCode: Joi.string().required(),
newPassword: Joi.string().min(6).max(100).required()
});
const changePasswordSchema = Joi.object({
oldPassword: Joi.string().required(),
newPassword: Joi.string().min(6).max(100).required()
});
// 生成JWT token
const generateToken = (user) => {
return jwt.sign(
{
id: user.id,
username: user.username,
role: user.user_type
},
process.env.JWT_SECRET || 'niumall-secret-key',
{ expiresIn: process.env.JWT_EXPIRES_IN || '24h' }
)
}
// 用户登录
router.post('/login', async (req, res) => {
try {
const { error, value } = loginSchema.validate(req.body);
// 参数验证
const { error, value } = loginSchema.validate(req.body)
if (error) {
return res.status(400).json({
success: false,
message: '参数验证失败',
errors: error.details.map(detail => detail.message)
});
details: error.details[0].message
})
}
const { username, password } = value;
// 查找用户
const user = await User.findByLoginIdentifier(username);
const { username, password } = value
// 查找用户
const user = await ApiUser.findOne({
where: {
[require('sequelize').Op.or]: [
{ username: username },
{ phone: username },
{ email: username }
]
}
});
if (!user) {
return res.status(401).json({
success: false,
message: '用户名或密码错误',
code: 'INVALID_CREDENTIALS'
});
message: '用户名或密码错误'
})
}
// 验证密码
const isValidPassword = await user.validatePassword(password);
if (!isValidPassword) {
const isPasswordValid = await bcrypt.compare(password, user.password_hash)
if (!isPasswordValid) {
return res.status(401).json({
success: false,
message: '用户名或密码错误',
code: 'INVALID_CREDENTIALS'
});
message: '用户名或密码错误'
})
}
// 检查用户状态
if (user.status !== 'active') {
return res.status(401).json({
return res.status(403).json({
success: false,
message: '账已被禁用,请联系管理员',
code: 'ACCOUNT_DISABLED'
});
message: '账已被禁用,请联系管理员'
})
}
// 更新登录信息
await user.updateLoginInfo();
// 生成token
const token = generateToken(user)
// 生成JWT token
const tokens = generateToken(user);
res.json({
success: true,
message: '登录成功',
data: {
...tokens,
access_token: token,
token_type: 'Bearer',
expires_in: 86400, // 24小时
user: {
id: user.id,
uuid: user.uuid,
username: user.username,
phone: user.phone,
email: user.email,
real_name: user.real_name,
avatar_url: user.avatar_url,
user_type: user.user_type,
status: user.status,
last_login_at: user.last_login_at,
login_count: user.login_count
role: user.user_type,
status: user.status
}
}
});
})
} catch (error) {
console.error('登录错误:', error);
console.error('登录失败:', error)
res.status(500).json({
success: false,
message: '登录失败',
error: error.message
});
message: '登录失败,请稍后重试'
})
}
});
})
// 获取用户信息
// 获取当前用户信息
router.get('/me', authenticateToken, async (req, res) => {
try {
// req.user 已经通过中间件注入
const user = await ApiUser.findByPk(req.user.id)
if (!user) {
return res.status(404).json({
success: false,
message: '用户不存在'
})
}
res.json({
success: true,
data: {
id: req.user.id,
uuid: req.user.uuid,
username: req.user.username,
phone: req.user.phone,
email: req.user.email,
real_name: req.user.real_name,
avatar_url: req.user.avatar_url,
user_type: req.user.user_type,
status: req.user.status,
last_login_at: req.user.last_login_at,
login_count: req.user.login_count,
created_at: req.user.created_at,
updated_at: req.user.updated_at
}
});
} catch (error) {
console.error('获取用户信息错误:', error);
res.status(500).json({
success: false,
message: '获取用户信息失败',
error: error.message
});
}
});
// 用户登出
router.post('/logout', authenticateToken, async (req, res) => {
try {
// TODO: 实际项目中可以将token加入黑名单或Redis
res.json({
success: true,
message: '退出登录成功'
});
} catch (error) {
console.error('退出登录错误:', error);
res.status(500).json({
success: false,
message: '退出登录失败',
error: error.message
});
}
});
// 刷新token
router.post('/refresh', refreshToken);
// 验证token有效性
router.post('/verify', authenticateToken, (req, res) => {
try {
// 如果通过认证中间件说明token有效
res.json({
success: true,
message: 'Token有效',
data: {
valid: true,
user: {
id: req.user.id,
username: req.user.username,
user_type: req.user.user_type
id: user.id,
username: user.username,
email: user.email,
role: user.user_type,
status: user.status
}
}
});
})
} catch (error) {
console.error('Token验证错误:', error);
console.error('获取用户信息失败:', error)
res.status(500).json({
success: false,
message: 'Token验证失败',
error: error.message
});
message: '获取用户信息失败'
})
}
});
})
module.exports = router;
// 用户登出
router.post('/logout', authenticateToken, (req, res) => {
// 在实际项目中可以将token加入黑名单
res.json({
success: true,
message: '登出成功'
})
})
// JWT token验证中间件
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization']
const token = authHeader && authHeader.split(' ')[1]
if (!token) {
return res.status(401).json({
success: false,
message: '访问令牌缺失'
})
}
jwt.verify(token, process.env.JWT_SECRET || 'niumall-secret-key', (err, user) => {
if (err) {
return res.status(403).json({
success: false,
message: '访问令牌无效或已过期'
})
}
req.user = user
next()
})
}
module.exports = router

View File

@@ -3,39 +3,9 @@ const bcrypt = require('bcryptjs')
const Joi = require('joi')
const router = express.Router()
// 模拟用户数据
let users = [
{
id: 1,
username: 'admin',
email: 'admin@example.com',
phone: '13800138000',
role: 'admin',
status: 'active',
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z'
},
{
id: 2,
username: 'buyer01',
email: 'buyer01@example.com',
phone: '13800138001',
role: 'buyer',
status: 'active',
createdAt: '2024-01-02T00:00:00Z',
updatedAt: '2024-01-02T00:00:00Z'
},
{
id: 3,
username: 'supplier01',
email: 'supplier01@example.com',
phone: '13800138002',
role: 'supplier',
status: 'inactive',
createdAt: '2024-01-03T00:00:00Z',
updatedAt: '2024-01-03T00:00:00Z'
}
]
// 引入数据库模型
const { ApiUser } = require('../models')
const sequelize = require('sequelize')
// 验证模式
const createUserSchema = Joi.object({
@@ -43,60 +13,55 @@ const createUserSchema = Joi.object({
email: Joi.string().email().required(),
phone: Joi.string().pattern(/^1[3-9]\d{9}$/).allow(''),
password: Joi.string().min(6).max(100).required(),
role: Joi.string().valid('admin', 'buyer', 'trader', 'supplier', 'driver').required(),
status: Joi.string().valid('active', 'inactive').default('active')
user_type: Joi.string().valid('client', 'supplier', 'driver', 'staff', 'admin').required(),
status: Joi.string().valid('active', 'inactive', 'locked').default('active')
})
const updateUserSchema = Joi.object({
username: Joi.string().min(2).max(50),
email: Joi.string().email(),
phone: Joi.string().pattern(/^1[3-9]\d{9}$/).allow(''),
role: Joi.string().valid('admin', 'buyer', 'trader', 'supplier', 'driver'),
status: Joi.string().valid('active', 'inactive', 'banned')
user_type: Joi.string().valid('client', 'supplier', 'driver', 'staff', 'admin'),
status: Joi.string().valid('active', 'inactive', 'locked')
})
// 获取用户列表
router.get('/', (req, res) => {
router.get('/', async (req, res) => {
try {
const { page = 1, pageSize = 20, keyword, role, status } = req.query
const { page = 1, pageSize = 20, keyword, user_type, status } = req.query
let filteredUsers = [...users]
// 关键词搜索
// 构建查询条件
const where = {}
if (keyword) {
filteredUsers = filteredUsers.filter(user =>
user.username.includes(keyword) ||
user.email.includes(keyword)
)
where[sequelize.Op.or] = [
{ username: { [sequelize.Op.like]: `%${keyword}%` } },
{ email: { [sequelize.Op.like]: `%${keyword}%` } },
{ phone: { [sequelize.Op.like]: `%${keyword}%` } }
]
}
if (user_type) where.user_type = user_type
if (status) where.status = status
// 角色筛选
if (role) {
filteredUsers = filteredUsers.filter(user => user.role === role)
}
// 状态筛选
if (status) {
filteredUsers = filteredUsers.filter(user => user.status === status)
}
// 分页
const total = filteredUsers.length
const startIndex = (page - 1) * pageSize
const endIndex = startIndex + parseInt(pageSize)
const paginatedUsers = filteredUsers.slice(startIndex, endIndex)
// 分页查询
const result = await ApiUser.findAndCountAll({
where,
limit: parseInt(pageSize),
offset: (parseInt(page) - 1) * parseInt(pageSize),
order: [['createdAt', 'DESC']]
})
res.json({
success: true,
data: {
items: paginatedUsers,
total: total,
items: result.rows,
total: result.count,
page: parseInt(page),
pageSize: parseInt(pageSize),
totalPages: Math.ceil(total / pageSize)
totalPages: Math.ceil(result.count / parseInt(pageSize))
}
})
} catch (error) {
console.error('获取用户列表失败:', error)
res.status(500).json({
success: false,
message: '获取用户列表失败'
@@ -105,10 +70,10 @@ router.get('/', (req, res) => {
})
// 获取用户详情
router.get('/:id', (req, res) => {
router.get('/:id', async (req, res) => {
try {
const { id } = req.params
const user = users.find(u => u.id === parseInt(id))
const user = await ApiUser.findByPk(id)
if (!user) {
return res.status(404).json({
@@ -122,6 +87,7 @@ router.get('/:id', (req, res) => {
data: user
})
} catch (error) {
console.error('获取用户详情失败:', error)
res.status(500).json({
success: false,
message: '获取用户详情失败'
@@ -142,37 +108,39 @@ router.post('/', async (req, res) => {
})
}
const { username, email, phone, password, role, status } = value
const { username, email, phone, password, user_type, status } = value
// 检查用户名是否已存在
if (users.find(u => u.username === username)) {
const existingUser = await ApiUser.findOne({
where: {
[sequelize.Op.or]: [
{ username: username },
{ email: email },
{ phone: phone }
]
}
})
if (existingUser) {
return res.status(400).json({
success: false,
message: '用户名已存在'
message: '用户名、邮箱或手机号已存在'
})
}
// 检查邮箱是否已存在
if (users.find(u => u.email === email)) {
return res.status(400).json({
success: false,
message: '邮箱已存在'
})
}
// 密码加密
const saltRounds = 10
const password_hash = await bcrypt.hash(password, saltRounds)
// 创建新用户
const newUser = {
id: Math.max(...users.map(u => u.id)) + 1,
const newUser = await ApiUser.create({
username,
email,
phone: phone || '',
role,
password_hash,
user_type,
status,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
}
users.push(newUser)
})
res.status(201).json({
success: true,
@@ -180,6 +148,7 @@ router.post('/', async (req, res) => {
data: newUser
})
} catch (error) {
console.error('创建用户失败:', error)
res.status(500).json({
success: false,
message: '创建用户失败'
@@ -188,12 +157,12 @@ router.post('/', async (req, res) => {
})
// 更新用户
router.put('/:id', (req, res) => {
router.put('/:id', async (req, res) => {
try {
const { id } = req.params
const userIndex = users.findIndex(u => u.id === parseInt(id))
const user = await ApiUser.findByPk(id)
if (userIndex === -1) {
if (!user) {
return res.status(404).json({
success: false,
message: '用户不存在'
@@ -211,18 +180,15 @@ router.put('/:id', (req, res) => {
}
// 更新用户信息
users[userIndex] = {
...users[userIndex],
...value,
updatedAt: new Date().toISOString()
}
await user.update(value)
res.json({
success: true,
message: '用户更新成功',
data: users[userIndex]
data: user
})
} catch (error) {
console.error('更新用户失败:', error)
res.status(500).json({
success: false,
message: '更新用户失败'
@@ -231,25 +197,26 @@ router.put('/:id', (req, res) => {
})
// 删除用户
router.delete('/:id', (req, res) => {
router.delete('/:id', async (req, res) => {
try {
const { id } = req.params
const userIndex = users.findIndex(u => u.id === parseInt(id))
const user = await ApiUser.findByPk(id)
if (userIndex === -1) {
if (!user) {
return res.status(404).json({
success: false,
message: '用户不存在'
})
}
users.splice(userIndex, 1)
await user.destroy()
res.json({
success: true,
message: '用户删除成功'
})
} catch (error) {
console.error('删除用户失败:', error)
res.status(500).json({
success: false,
message: '删除用户失败'
@@ -258,7 +225,7 @@ router.delete('/:id', (req, res) => {
})
// 批量删除用户
router.delete('/batch', (req, res) => {
router.delete('/batch', async (req, res) => {
try {
const { ids } = req.body
@@ -269,13 +236,18 @@ router.delete('/batch', (req, res) => {
})
}
users = users.filter(user => !ids.includes(user.id))
await ApiUser.destroy({
where: {
id: ids
}
})
res.json({
success: true,
message: `成功删除 ${ids.length} 个用户`
})
} catch (error) {
console.error('批量删除用户失败:', error)
res.status(500).json({
success: false,
message: '批量删除用户失败'
@@ -289,8 +261,8 @@ router.put('/:id/password', async (req, res) => {
const { id } = req.params
const { password } = req.body
const userIndex = users.findIndex(u => u.id === parseInt(id))
if (userIndex === -1) {
const user = await ApiUser.findByPk(id)
if (!user) {
return res.status(404).json({
success: false,
message: '用户不存在'
@@ -304,14 +276,19 @@ router.put('/:id/password', async (req, res) => {
})
}
// 在实际项目中,这里会对密码进行加密
users[userIndex].updatedAt = new Date().toISOString()
// 密码加密
const saltRounds = 10
const password_hash = await bcrypt.hash(password, saltRounds)
// 更新密码
await user.update({ password_hash })
res.json({
success: true,
message: '密码重置成功'
})
} catch (error) {
console.error('重置密码失败:', error)
res.status(500).json({
success: false,
message: '重置密码失败'
@@ -320,35 +297,35 @@ router.put('/:id/password', async (req, res) => {
})
// 更新用户状态
router.put('/:id/status', (req, res) => {
router.put('/:id/status', async (req, res) => {
try {
const { id } = req.params
const { status } = req.body
const userIndex = users.findIndex(u => u.id === parseInt(id))
if (userIndex === -1) {
const user = await ApiUser.findByPk(id)
if (!user) {
return res.status(404).json({
success: false,
message: '用户不存在'
})
}
if (!['active', 'inactive', 'banned'].includes(status)) {
if (!['active', 'inactive', 'locked'].includes(status)) {
return res.status(400).json({
success: false,
message: '无效的用户状态'
})
}
users[userIndex].status = status
users[userIndex].updatedAt = new Date().toISOString()
await user.update({ status })
res.json({
success: true,
message: '用户状态更新成功',
data: users[userIndex]
data: user
})
} catch (error) {
console.error('更新用户状态失败:', error)
res.status(500).json({
success: false,
message: '更新用户状态失败'

View File

@@ -1,78 +0,0 @@
const User = require('../models/User');
// 创建初始用户数据
const createInitialUsers = async () => {
try {
// 检查是否已存在用户
const existingAdmin = await User.findOne({ where: { username: 'admin' } });
if (existingAdmin) {
console.log('✅ 初始用户已存在,跳过创建');
return;
}
// 创建管理员用户
const adminUser = await User.createUser({
username: 'admin',
password: 'admin123',
phone: '13800138000',
email: 'admin@niumall.com',
real_name: '系统管理员',
user_type: 'admin'
});
// 创建采购人用户
const buyerUser = await User.createUser({
username: 'buyer',
password: 'buyer123',
phone: '13800138001',
email: 'buyer@niumall.com',
real_name: '采购经理',
user_type: 'client'
});
// 创建贸易商用户
const traderUser = await User.createUser({
username: 'trader',
password: 'trader123',
phone: '13800138002',
email: 'trader@niumall.com',
real_name: '贸易商经理',
user_type: 'staff'
});
// 创建供应商用户
const supplierUser = await User.createUser({
username: 'supplier',
password: 'supplier123',
phone: '13800138003',
email: 'supplier@niumall.com',
real_name: '供应商代表',
user_type: 'supplier'
});
// 创建司机用户
const driverUser = await User.createUser({
username: 'driver',
password: 'driver123',
phone: '13800138004',
email: 'driver@niumall.com',
real_name: '运输司机',
user_type: 'driver'
});
console.log('✅ 初始用户创建成功');
console.log('👤 用户账号信息:');
console.log(' 管理员: admin / admin123');
console.log(' 采购人: buyer / buyer123');
console.log(' 贸易商: trader / trader123');
console.log(' 供应商: supplier / supplier123');
console.log(' 司机: driver / driver123');
} catch (error) {
console.error('❌ 创建初始用户失败:', error);
}
};
module.exports = {
createInitialUsers
};