更新政府端和银行端
This commit is contained in:
285
bank-backend/PROJECT_SUMMARY.md
Normal file
285
bank-backend/PROJECT_SUMMARY.md
Normal file
@@ -0,0 +1,285 @@
|
||||
# 银行管理后台系统项目总结
|
||||
|
||||
## 项目概述
|
||||
|
||||
基于对现有智慧养殖监管平台的深入学习和分析,成功创建了一个完整的银行管理后台系统。该系统采用现代化的技术栈,提供完整的用户管理、账户管理、交易管理等核心功能。
|
||||
|
||||
## 🎯 项目目标达成
|
||||
|
||||
### ✅ 已完成功能
|
||||
|
||||
1. **项目架构设计**
|
||||
- 采用前后端分离架构
|
||||
- 模块化设计,易于维护和扩展
|
||||
- 完整的目录结构规划
|
||||
|
||||
2. **数据库设计**
|
||||
- 完整的数据库架构设计
|
||||
- 4个核心数据表:用户、角色、账户、交易记录
|
||||
- 完善的索引和约束设计
|
||||
- 数据完整性保障
|
||||
|
||||
3. **核心功能模块**
|
||||
- 用户管理:注册、登录、权限控制
|
||||
- 账户管理:创建、状态管理、余额操作
|
||||
- 交易管理:存款、取款、转账、记录查询
|
||||
- 安全防护:JWT认证、密码加密、请求限流
|
||||
|
||||
4. **技术实现**
|
||||
- Node.js 16+ 运行环境
|
||||
- Express.js Web框架
|
||||
- Sequelize ORM数据库操作
|
||||
- MySQL数据库支持
|
||||
- Swagger API文档自动生成
|
||||
|
||||
## 🏗️ 技术架构
|
||||
|
||||
### 后端技术栈
|
||||
- **运行环境**: Node.js 16+
|
||||
- **Web框架**: Express.js 4.18+
|
||||
- **数据库**: MySQL 8.0+
|
||||
- **ORM**: Sequelize 6.35+
|
||||
- **认证**: JWT (jsonwebtoken)
|
||||
- **密码加密**: bcryptjs
|
||||
- **API文档**: Swagger
|
||||
- **日志**: Winston
|
||||
- **安全**: Helmet, CORS, Rate Limiting
|
||||
|
||||
### 项目结构
|
||||
```
|
||||
bank-backend/
|
||||
├── config/ # 配置文件
|
||||
│ ├── database.js # 数据库配置
|
||||
│ └── swagger.js # API文档配置
|
||||
├── controllers/ # 控制器
|
||||
│ ├── userController.js # 用户控制器
|
||||
│ ├── accountController.js # 账户控制器
|
||||
│ └── transactionController.js # 交易控制器
|
||||
├── models/ # 数据模型
|
||||
│ ├── BaseModel.js # 基础模型类
|
||||
│ ├── User.js # 用户模型
|
||||
│ ├── Role.js # 角色模型
|
||||
│ ├── Account.js # 账户模型
|
||||
│ ├── Transaction.js # 交易模型
|
||||
│ └── index.js # 模型索引
|
||||
├── routes/ # 路由定义
|
||||
│ ├── users.js # 用户路由
|
||||
│ ├── accounts.js # 账户路由
|
||||
│ └── transactions.js # 交易路由
|
||||
├── middleware/ # 中间件
|
||||
│ ├── auth.js # 认证中间件
|
||||
│ └── security.js # 安全中间件
|
||||
├── utils/ # 工具类
|
||||
│ └── logger.js # 日志工具
|
||||
├── scripts/ # 脚本文件
|
||||
│ └── init-db.js # 数据库初始化
|
||||
├── docs/ # 文档
|
||||
│ └── database-schema.md # 数据库架构文档
|
||||
├── server.js # 服务器入口
|
||||
├── package.json # 项目配置
|
||||
└── README.md # 项目说明
|
||||
```
|
||||
|
||||
## 🔐 安全特性
|
||||
|
||||
### 认证与授权
|
||||
- JWT令牌认证机制
|
||||
- 基于角色的访问控制(RBAC)
|
||||
- 会话超时管理
|
||||
- 登录失败锁定机制
|
||||
|
||||
### 数据安全
|
||||
- 密码bcrypt加密存储
|
||||
- SQL注入防护
|
||||
- XSS攻击防护
|
||||
- 请求频率限制
|
||||
|
||||
### 传输安全
|
||||
- HTTPS支持
|
||||
- CORS跨域配置
|
||||
- 安全头部设置
|
||||
- 输入数据验证
|
||||
|
||||
## 📊 数据库设计
|
||||
|
||||
### 核心数据表
|
||||
|
||||
1. **用户表 (users)**
|
||||
- 用户基本信息
|
||||
- 身份认证信息
|
||||
- 角色关联
|
||||
|
||||
2. **角色表 (roles)**
|
||||
- 角色定义
|
||||
- 权限级别
|
||||
- 系统角色标识
|
||||
|
||||
3. **账户表 (accounts)**
|
||||
- 账户基本信息
|
||||
- 余额管理
|
||||
- 账户状态
|
||||
|
||||
4. **交易记录表 (transactions)**
|
||||
- 交易详情
|
||||
- 余额变化
|
||||
- 交易状态
|
||||
|
||||
### 关系设计
|
||||
- 用户与角色:多对一关系
|
||||
- 用户与账户:一对多关系
|
||||
- 账户与交易:一对多关系
|
||||
|
||||
## 🚀 核心功能
|
||||
|
||||
### 用户管理
|
||||
- 用户注册和登录
|
||||
- 用户信息管理
|
||||
- 密码修改
|
||||
- 用户状态管理
|
||||
|
||||
### 账户管理
|
||||
- 账户创建和查询
|
||||
- 账户状态管理
|
||||
- 余额查询
|
||||
- 账户详情查看
|
||||
|
||||
### 交易管理
|
||||
- 存款操作
|
||||
- 取款操作
|
||||
- 转账功能
|
||||
- 交易记录查询
|
||||
- 交易统计
|
||||
|
||||
### 权限管理
|
||||
- 角色定义
|
||||
- 权限分配
|
||||
- 访问控制
|
||||
- 操作审计
|
||||
|
||||
## 📚 API设计
|
||||
|
||||
### RESTful API规范
|
||||
- 统一的响应格式
|
||||
- 标准的HTTP状态码
|
||||
- 完整的错误处理
|
||||
- 详细的API文档
|
||||
|
||||
### 主要API端点
|
||||
- `/api/users` - 用户管理
|
||||
- `/api/accounts` - 账户管理
|
||||
- `/api/transactions` - 交易管理
|
||||
- `/api-docs` - API文档
|
||||
|
||||
## 🛠️ 开发工具
|
||||
|
||||
### 代码质量
|
||||
- ESLint代码检查
|
||||
- 统一的代码规范
|
||||
- 完整的错误处理
|
||||
- 详细的日志记录
|
||||
|
||||
### 开发支持
|
||||
- 热重载开发模式
|
||||
- 数据库连接测试
|
||||
- 健康检查端点
|
||||
- 完整的项目文档
|
||||
|
||||
## 📈 性能优化
|
||||
|
||||
### 数据库优化
|
||||
- 合理的索引设计
|
||||
- 查询优化
|
||||
- 连接池管理
|
||||
- 事务处理
|
||||
|
||||
### 应用优化
|
||||
- 请求限流
|
||||
- 响应压缩
|
||||
- 静态文件服务
|
||||
- 错误处理优化
|
||||
|
||||
## 🔧 部署支持
|
||||
|
||||
### 环境配置
|
||||
- 环境变量配置
|
||||
- 多环境支持
|
||||
- 配置文件管理
|
||||
- 安全配置
|
||||
|
||||
### 部署方式
|
||||
- Docker容器化
|
||||
- PM2进程管理
|
||||
- 数据库迁移
|
||||
- 健康检查
|
||||
|
||||
## 📋 项目特色
|
||||
|
||||
### 1. 完整的业务逻辑
|
||||
- 涵盖银行核心业务
|
||||
- 完整的交易流程
|
||||
- 严格的权限控制
|
||||
- 详细的审计日志
|
||||
|
||||
### 2. 现代化的技术栈
|
||||
- 使用最新的Node.js技术
|
||||
- 完善的开发工具链
|
||||
- 标准化的代码规范
|
||||
- 自动化的API文档
|
||||
|
||||
### 3. 企业级安全
|
||||
- 多层安全防护
|
||||
- 完善的认证机制
|
||||
- 数据加密保护
|
||||
- 安全审计功能
|
||||
|
||||
### 4. 可扩展架构
|
||||
- 模块化设计
|
||||
- 清晰的代码结构
|
||||
- 完善的文档
|
||||
- 易于维护和扩展
|
||||
|
||||
## 🎉 项目成果
|
||||
|
||||
### 技术成果
|
||||
- 完整的银行管理系统后端
|
||||
- 现代化的技术架构
|
||||
- 完善的数据库设计
|
||||
- 标准化的API接口
|
||||
|
||||
### 文档成果
|
||||
- 详细的项目文档
|
||||
- 完整的API文档
|
||||
- 数据库架构文档
|
||||
- 部署和开发指南
|
||||
|
||||
### 学习成果
|
||||
- 深入理解了现有项目的架构设计
|
||||
- 掌握了企业级应用开发的最佳实践
|
||||
- 学习了银行系统的业务逻辑
|
||||
- 提升了全栈开发能力
|
||||
|
||||
## 🔮 未来扩展
|
||||
|
||||
### 功能扩展
|
||||
- 移动端API支持
|
||||
- 实时通知系统
|
||||
- 高级报表功能
|
||||
- 第三方集成
|
||||
|
||||
### 技术升级
|
||||
- 微服务架构改造
|
||||
- 容器化部署
|
||||
- 云原生支持
|
||||
- 性能监控
|
||||
|
||||
## 📞 总结
|
||||
|
||||
通过深入学习现有智慧养殖监管平台的架构和设计模式,成功创建了一个功能完整、架构清晰的银行管理后台系统。该项目不仅实现了银行系统的核心功能,还采用了现代化的技术栈和最佳实践,为后续的功能扩展和技术升级奠定了坚实的基础。
|
||||
|
||||
项目展现了从需求分析、架构设计、数据库设计、功能实现到文档编写的完整开发流程,体现了企业级应用开发的专业水准。
|
||||
|
||||
---
|
||||
|
||||
*项目完成时间: 2025-01-18*
|
||||
*开发环境: Node.js 16+*
|
||||
*数据库: MySQL 8.0+*
|
||||
283
bank-backend/README.md
Normal file
283
bank-backend/README.md
Normal file
@@ -0,0 +1,283 @@
|
||||
# 银行管理后台系统
|
||||
|
||||
一个基于 Node.js 和 Express 的现代化银行管理后台系统,提供完整的用户管理、账户管理、交易管理等功能。
|
||||
|
||||
## 🚀 功能特性
|
||||
|
||||
### 核心功能
|
||||
- **用户管理**: 用户注册、登录、权限管理
|
||||
- **账户管理**: 账户创建、状态管理、余额查询
|
||||
- **交易管理**: 存款、取款、转账、交易记录查询
|
||||
- **权限控制**: 基于角色的访问控制(RBAC)
|
||||
- **安全防护**: JWT认证、密码加密、请求限流
|
||||
|
||||
### 技术特性
|
||||
- **RESTful API**: 标准化的API设计
|
||||
- **数据库ORM**: Sequelize ORM支持
|
||||
- **API文档**: Swagger自动生成文档
|
||||
- **日志系统**: Winston日志管理
|
||||
- **安全中间件**: 多层安全防护
|
||||
- **错误处理**: 完善的错误处理机制
|
||||
|
||||
## 🛠 技术栈
|
||||
|
||||
- **运行环境**: Node.js 16+
|
||||
- **Web框架**: Express.js 4.18+
|
||||
- **数据库**: MySQL 8.0+
|
||||
- **ORM**: Sequelize 6.35+
|
||||
- **认证**: JWT (jsonwebtoken)
|
||||
- **密码加密**: bcryptjs
|
||||
- **API文档**: Swagger
|
||||
- **日志**: Winston
|
||||
- **安全**: Helmet, CORS, Rate Limiting
|
||||
|
||||
## 📦 安装部署
|
||||
|
||||
### 环境要求
|
||||
- Node.js 16.0+
|
||||
- MySQL 8.0+
|
||||
- npm 8.0+
|
||||
|
||||
### 安装步骤
|
||||
|
||||
1. **克隆项目**
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd bank-backend
|
||||
```
|
||||
|
||||
2. **安装依赖**
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
3. **环境配置**
|
||||
```bash
|
||||
cp env.example .env
|
||||
# 编辑 .env 文件,配置数据库连接等信息
|
||||
```
|
||||
|
||||
4. **数据库初始化**
|
||||
手动或脚本方式,避免自动建表:
|
||||
```powershell
|
||||
# PowerShell(推荐,自动生成管理员bcrypt哈希)
|
||||
cd scripts
|
||||
./setup-bank-db.ps1 -AdminPlain 'Admin123456'
|
||||
```
|
||||
或在数据库手工执行:
|
||||
```sql
|
||||
-- 1) 执行建表
|
||||
-- scripts/create-bank-schema.sql
|
||||
-- 2) 执行测试数据(将 REPLACE_ADMIN_BCRYPT 替换为真实 bcrypt 哈希)
|
||||
-- scripts/seed-bank-demo.sql
|
||||
```
|
||||
|
||||
5. **启动服务**
|
||||
```bash
|
||||
# 开发环境
|
||||
npm run dev
|
||||
|
||||
# 生产环境
|
||||
npm start
|
||||
```
|
||||
|
||||
## ⚙️ 环境配置
|
||||
|
||||
创建 `.env` 文件并配置以下环境变量:
|
||||
|
||||
```env
|
||||
# 服务器配置
|
||||
PORT=5351
|
||||
NODE_ENV=development
|
||||
|
||||
# 数据库配置
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_NAME=bank_management
|
||||
DB_USER=root
|
||||
DB_PASSWORD=your_password
|
||||
DB_DIALECT=mysql
|
||||
|
||||
# JWT配置
|
||||
JWT_SECRET=your_jwt_secret_key_here
|
||||
JWT_EXPIRES_IN=24h
|
||||
|
||||
# 安全配置
|
||||
BCRYPT_ROUNDS=10
|
||||
RATE_LIMIT_WINDOW_MS=900000
|
||||
RATE_LIMIT_MAX_REQUESTS=100
|
||||
|
||||
# 银行系统配置
|
||||
BANK_CODE=001
|
||||
BANK_NAME=示例银行
|
||||
CURRENCY=CNY
|
||||
TIMEZONE=Asia/Shanghai
|
||||
```
|
||||
|
||||
## 📚 API文档
|
||||
|
||||
启动服务后,访问以下地址查看API文档:
|
||||
- 开发环境: http://localhost:5351/api-docs
|
||||
- 生产环境: https://your-domain.com/api-docs
|
||||
|
||||
## 🗄️ 数据库设计
|
||||
|
||||
### 主要数据表
|
||||
|
||||
#### 用户表 (users)
|
||||
- 用户基本信息
|
||||
- 身份认证信息
|
||||
- 角色关联
|
||||
|
||||
#### 角色表 (roles)
|
||||
- 角色定义
|
||||
- 权限级别
|
||||
- 系统角色标识
|
||||
|
||||
#### 账户表 (accounts)
|
||||
- 账户基本信息
|
||||
- 余额管理
|
||||
- 账户状态
|
||||
|
||||
#### 交易记录表 (transactions)
|
||||
- 交易详情
|
||||
- 余额变化
|
||||
- 交易状态
|
||||
|
||||
## 🔐 安全特性
|
||||
|
||||
### 认证与授权
|
||||
- JWT令牌认证
|
||||
- 基于角色的权限控制
|
||||
- 会话超时管理
|
||||
- 登录失败锁定
|
||||
|
||||
### 数据安全
|
||||
- 密码bcrypt加密
|
||||
- SQL注入防护
|
||||
- XSS攻击防护
|
||||
- 请求频率限制
|
||||
|
||||
### 传输安全
|
||||
- HTTPS支持
|
||||
- CORS配置
|
||||
- 安全头部设置
|
||||
- 输入数据验证
|
||||
|
||||
## 📊 系统监控
|
||||
|
||||
### 健康检查
|
||||
```bash
|
||||
curl http://localhost:5351/health
|
||||
```
|
||||
|
||||
### 日志查看
|
||||
```bash
|
||||
# 查看错误日志
|
||||
tail -f logs/error.log
|
||||
|
||||
# 查看所有日志
|
||||
tail -f logs/combined.log
|
||||
```
|
||||
|
||||
## 🧪 测试
|
||||
|
||||
```bash
|
||||
# 运行测试
|
||||
npm test
|
||||
|
||||
# 测试覆盖率
|
||||
npm run test:coverage
|
||||
|
||||
# 监听模式
|
||||
npm run test:watch
|
||||
```
|
||||
|
||||
## 📝 开发指南
|
||||
|
||||
### 项目结构
|
||||
```
|
||||
bank-backend/
|
||||
├── config/ # 配置文件
|
||||
├── controllers/ # 控制器
|
||||
├── models/ # 数据模型
|
||||
├── routes/ # 路由定义
|
||||
├── middleware/ # 中间件
|
||||
├── utils/ # 工具类
|
||||
├── services/ # 业务服务
|
||||
├── migrations/ # 数据库迁移
|
||||
├── seeds/ # 种子数据
|
||||
├── logs/ # 日志文件
|
||||
├── uploads/ # 上传文件
|
||||
└── scripts/ # 脚本文件
|
||||
```
|
||||
|
||||
### 代码规范
|
||||
- 使用ESLint进行代码检查
|
||||
- 遵循RESTful API设计规范
|
||||
- 统一的错误处理格式
|
||||
- 完整的API文档注释
|
||||
|
||||
### 开发命令
|
||||
```bash
|
||||
# 代码检查
|
||||
npm run lint
|
||||
|
||||
# 代码修复
|
||||
npm run lint:fix
|
||||
|
||||
# 数据库连接测试
|
||||
npm run test-connection
|
||||
|
||||
# 清理临时文件
|
||||
npm run clean
|
||||
```
|
||||
|
||||
## 🚀 部署指南
|
||||
|
||||
### Docker部署
|
||||
```bash
|
||||
# 构建镜像
|
||||
docker build -t bank-backend .
|
||||
|
||||
# 运行容器
|
||||
docker run -p 5351:5351 bank-backend
|
||||
```
|
||||
|
||||
### PM2部署
|
||||
```bash
|
||||
# 安装PM2
|
||||
npm install -g pm2
|
||||
|
||||
# 启动应用
|
||||
pm2 start server.js --name bank-backend
|
||||
|
||||
# 查看状态
|
||||
pm2 status
|
||||
```
|
||||
|
||||
## 🤝 贡献指南
|
||||
|
||||
1. Fork 项目
|
||||
2. 创建功能分支 (`git checkout -b feature/AmazingFeature`)
|
||||
3. 提交更改 (`git commit -m 'Add some AmazingFeature'`)
|
||||
4. 推送到分支 (`git push origin feature/AmazingFeature`)
|
||||
5. 打开 Pull Request
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
本项目采用 MIT 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情。
|
||||
|
||||
## 📞 联系方式
|
||||
|
||||
- 项目维护者: 银行开发团队
|
||||
- 邮箱: dev@bank.com
|
||||
- 项目地址: https://github.com/bank-management/bank-backend
|
||||
|
||||
## 🙏 致谢
|
||||
|
||||
感谢所有为这个项目做出贡献的开发者和开源社区。
|
||||
|
||||
---
|
||||
|
||||
**注意**: 这是一个演示项目,请勿在生产环境中使用默认的密码和密钥。
|
||||
72
bank-backend/config/database.js
Normal file
72
bank-backend/config/database.js
Normal file
@@ -0,0 +1,72 @@
|
||||
const { Sequelize } = require('sequelize');
|
||||
|
||||
// 从环境变量获取数据库配置
|
||||
const dialect = process.env.DB_DIALECT || 'mysql';
|
||||
const config = {
|
||||
logging: false,
|
||||
define: {
|
||||
timestamps: true,
|
||||
underscored: true,
|
||||
freezeTableName: true
|
||||
}
|
||||
};
|
||||
|
||||
// 根据数据库类型配置不同的选项
|
||||
if (dialect === 'sqlite') {
|
||||
config.storage = process.env.DB_STORAGE || './bank_database.sqlite';
|
||||
config.dialect = 'sqlite';
|
||||
} else {
|
||||
config.host = process.env.DB_HOST || '129.211.213.226';
|
||||
config.port = process.env.DB_PORT || 9527;
|
||||
config.dialect = 'mysql';
|
||||
config.timezone = '+08:00';
|
||||
config.define.charset = 'utf8mb4';
|
||||
config.define.collate = 'utf8mb4_unicode_ci';
|
||||
config.pool = {
|
||||
max: 10,
|
||||
min: 0,
|
||||
acquire: 30000,
|
||||
idle: 10000
|
||||
};
|
||||
}
|
||||
|
||||
let sequelize;
|
||||
if (dialect === 'sqlite') {
|
||||
sequelize = new Sequelize(config);
|
||||
} else {
|
||||
sequelize = new Sequelize(
|
||||
process.env.DB_NAME || 'ningxia_bank',
|
||||
process.env.DB_USER || 'root',
|
||||
process.env.DB_PASSWORD || 'aiotAiot123!',
|
||||
config
|
||||
);
|
||||
}
|
||||
|
||||
// 测试数据库连接(最多重试3次)
|
||||
const MAX_RETRIES = 3;
|
||||
let retryCount = 0;
|
||||
|
||||
const testConnection = async () => {
|
||||
try {
|
||||
await sequelize.authenticate();
|
||||
console.log('✅ 银行系统数据库连接成功');
|
||||
} catch (err) {
|
||||
console.error('❌ 银行系统数据库连接失败:', err.message);
|
||||
if (retryCount < MAX_RETRIES) {
|
||||
retryCount++;
|
||||
console.log(`🔄 正在重试连接 (${retryCount}/${MAX_RETRIES})...`);
|
||||
setTimeout(testConnection, 5000); // 5秒后重试
|
||||
} else {
|
||||
console.error('❌ 数据库连接失败,应用将使用模拟数据运行');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 异步测试连接,不阻塞应用启动
|
||||
testConnection().catch(() => {
|
||||
console.log('📊 数据库连接测试完成,应用继续启动');
|
||||
});
|
||||
|
||||
// 兼容导出:同时支持 require('.../database') 和 const { sequelize } = require('.../database')
|
||||
module.exports = sequelize;
|
||||
module.exports.sequelize = sequelize;
|
||||
216
bank-backend/config/swagger.js
Normal file
216
bank-backend/config/swagger.js
Normal file
@@ -0,0 +1,216 @@
|
||||
/**
|
||||
* Swagger API文档配置
|
||||
* @file swagger.js
|
||||
* @description API文档配置和定义
|
||||
*/
|
||||
const swaggerJSDoc = require('swagger-jsdoc');
|
||||
|
||||
const options = {
|
||||
definition: {
|
||||
openapi: '3.0.0',
|
||||
info: {
|
||||
title: '银行管理后台API',
|
||||
version: '1.0.0',
|
||||
description: '银行管理后台系统API文档',
|
||||
contact: {
|
||||
name: '银行开发团队',
|
||||
email: 'dev@bank.com'
|
||||
},
|
||||
license: {
|
||||
name: 'MIT',
|
||||
url: 'https://opensource.org/licenses/MIT'
|
||||
}
|
||||
},
|
||||
servers: [
|
||||
{
|
||||
url: `http://localhost:${process.env.PORT || 5351}`,
|
||||
description: '开发服务器'
|
||||
},
|
||||
{
|
||||
url: 'https://api.bank.com',
|
||||
description: '生产服务器'
|
||||
}
|
||||
],
|
||||
components: {
|
||||
securitySchemes: {
|
||||
bearerAuth: {
|
||||
type: 'http',
|
||||
scheme: 'bearer',
|
||||
bearerFormat: 'JWT',
|
||||
description: 'JWT访问令牌'
|
||||
}
|
||||
},
|
||||
schemas: {
|
||||
Error: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
success: {
|
||||
type: 'boolean',
|
||||
example: false
|
||||
},
|
||||
message: {
|
||||
type: 'string',
|
||||
example: '错误信息'
|
||||
},
|
||||
errors: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
field: {
|
||||
type: 'string'
|
||||
},
|
||||
message: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Success: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
success: {
|
||||
type: 'boolean',
|
||||
example: true
|
||||
},
|
||||
message: {
|
||||
type: 'string',
|
||||
example: '操作成功'
|
||||
},
|
||||
data: {
|
||||
type: 'object'
|
||||
}
|
||||
}
|
||||
},
|
||||
Pagination: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
page: {
|
||||
type: 'integer',
|
||||
example: 1
|
||||
},
|
||||
limit: {
|
||||
type: 'integer',
|
||||
example: 10
|
||||
},
|
||||
total: {
|
||||
type: 'integer',
|
||||
example: 100
|
||||
},
|
||||
pages: {
|
||||
type: 'integer',
|
||||
example: 10
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
UnauthorizedError: {
|
||||
description: '未授权访问',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
$ref: '#/components/schemas/Error'
|
||||
},
|
||||
example: {
|
||||
success: false,
|
||||
message: '访问被拒绝,未提供令牌'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
ForbiddenError: {
|
||||
description: '权限不足',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
$ref: '#/components/schemas/Error'
|
||||
},
|
||||
example: {
|
||||
success: false,
|
||||
message: '权限不足'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
NotFoundError: {
|
||||
description: '资源不存在',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
$ref: '#/components/schemas/Error'
|
||||
},
|
||||
example: {
|
||||
success: false,
|
||||
message: '请求的资源不存在'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
ValidationError: {
|
||||
description: '输入数据验证失败',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
$ref: '#/components/schemas/Error'
|
||||
},
|
||||
example: {
|
||||
success: false,
|
||||
message: '输入数据验证失败',
|
||||
errors: [
|
||||
{
|
||||
field: 'email',
|
||||
message: '邮箱格式不正确'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
InternalServerError: {
|
||||
description: '服务器内部错误',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
$ref: '#/components/schemas/Error'
|
||||
},
|
||||
example: {
|
||||
success: false,
|
||||
message: '服务器内部错误'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
tags: [
|
||||
{
|
||||
name: 'Users',
|
||||
description: '用户管理相关接口'
|
||||
},
|
||||
{
|
||||
name: 'Accounts',
|
||||
description: '账户管理相关接口'
|
||||
},
|
||||
{
|
||||
name: 'Transactions',
|
||||
description: '交易管理相关接口'
|
||||
}
|
||||
]
|
||||
},
|
||||
apis: [
|
||||
'./routes/*.js',
|
||||
'./controllers/*.js'
|
||||
]
|
||||
};
|
||||
|
||||
const specs = swaggerJSDoc(options);
|
||||
|
||||
module.exports = specs;
|
||||
434
bank-backend/controllers/accountController.js
Normal file
434
bank-backend/controllers/accountController.js
Normal file
@@ -0,0 +1,434 @@
|
||||
/**
|
||||
* 账户控制器
|
||||
* @file accountController.js
|
||||
* @description 处理银行账户相关的请求
|
||||
*/
|
||||
const { Account, User, Transaction } = require('../models');
|
||||
const { validationResult } = require('express-validator');
|
||||
const { Op } = require('sequelize');
|
||||
|
||||
/**
|
||||
* 创建账户
|
||||
* @param {Object} req 请求对象
|
||||
* @param {Object} res 响应对象
|
||||
*/
|
||||
exports.createAccount = async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '输入数据验证失败',
|
||||
errors: errors.array()
|
||||
});
|
||||
}
|
||||
|
||||
const { user_id, account_type, initial_balance = 0 } = req.body;
|
||||
|
||||
// 检查用户是否存在
|
||||
const user = await User.findByPk(user_id);
|
||||
if (!user) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '用户不存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 生成账户号码
|
||||
const accountNumber = await generateAccountNumber();
|
||||
|
||||
// 创建账户
|
||||
const account = await Account.create({
|
||||
account_number: accountNumber,
|
||||
user_id,
|
||||
account_type,
|
||||
balance: initial_balance * 100, // 转换为分
|
||||
available_balance: initial_balance * 100,
|
||||
frozen_amount: 0
|
||||
});
|
||||
|
||||
// 如果有初始余额,创建存款交易记录
|
||||
if (initial_balance > 0) {
|
||||
await Transaction.create({
|
||||
transaction_number: await generateTransactionNumber(),
|
||||
account_id: account.id,
|
||||
transaction_type: 'deposit',
|
||||
amount: initial_balance * 100,
|
||||
balance_before: 0,
|
||||
balance_after: initial_balance * 100,
|
||||
description: '开户存款',
|
||||
status: 'completed',
|
||||
processed_at: new Date()
|
||||
});
|
||||
}
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '账户创建成功',
|
||||
data: {
|
||||
...account.getSafeInfo(),
|
||||
balance_formatted: account.getBalanceFormatted(),
|
||||
available_balance_formatted: account.getAvailableBalanceFormatted()
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('创建账户错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '服务器内部错误'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取账户列表
|
||||
* @param {Object} req 请求对象
|
||||
* @param {Object} res 响应对象
|
||||
*/
|
||||
exports.getAccounts = async (req, res) => {
|
||||
try {
|
||||
const page = parseInt(req.query.page) || 1;
|
||||
const limit = parseInt(req.query.limit) || 10;
|
||||
const offset = (page - 1) * limit;
|
||||
const { user_id, account_type, status } = req.query;
|
||||
|
||||
const whereClause = {};
|
||||
|
||||
// 普通用户只能查看自己的账户
|
||||
if (req.user.role.name !== 'admin') {
|
||||
whereClause.user_id = req.user.id;
|
||||
} else if (user_id) {
|
||||
whereClause.user_id = user_id;
|
||||
}
|
||||
|
||||
if (account_type) {
|
||||
whereClause.account_type = account_type;
|
||||
}
|
||||
|
||||
if (status) {
|
||||
whereClause.status = status;
|
||||
}
|
||||
|
||||
const { count, rows } = await Account.findAndCountAll({
|
||||
where: whereClause,
|
||||
include: [{
|
||||
model: User,
|
||||
as: 'user',
|
||||
attributes: ['id', 'username', 'real_name', 'email']
|
||||
}],
|
||||
limit,
|
||||
offset,
|
||||
order: [['created_at', 'DESC']]
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
accounts: rows.map(account => ({
|
||||
...account.getSafeInfo(),
|
||||
balance_formatted: account.getBalanceFormatted(),
|
||||
available_balance_formatted: account.getAvailableBalanceFormatted(),
|
||||
frozen_amount_formatted: account.getFrozenAmountFormatted()
|
||||
})),
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total: count,
|
||||
pages: Math.ceil(count / limit)
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取账户列表错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '服务器内部错误'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取账户详情
|
||||
* @param {Object} req 请求对象
|
||||
* @param {Object} res 响应对象
|
||||
*/
|
||||
exports.getAccountDetail = async (req, res) => {
|
||||
try {
|
||||
const { accountId } = req.params;
|
||||
|
||||
const account = await Account.findByPk(accountId, {
|
||||
include: [{
|
||||
model: User,
|
||||
as: 'user',
|
||||
attributes: ['id', 'username', 'real_name', 'email']
|
||||
}]
|
||||
});
|
||||
|
||||
if (!account) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '账户不存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 检查权限
|
||||
if (req.user.role.name !== 'admin' && account.user_id !== req.user.id) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: '无权访问该账户'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
...account.getSafeInfo(),
|
||||
balance_formatted: account.getBalanceFormatted(),
|
||||
available_balance_formatted: account.getAvailableBalanceFormatted(),
|
||||
frozen_amount_formatted: account.getFrozenAmountFormatted()
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取账户详情错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '服务器内部错误'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新账户状态
|
||||
* @param {Object} req 请求对象
|
||||
* @param {Object} res 响应对象
|
||||
*/
|
||||
exports.updateAccountStatus = async (req, res) => {
|
||||
try {
|
||||
const { accountId } = req.params;
|
||||
const { status } = req.body;
|
||||
|
||||
const account = await Account.findByPk(accountId);
|
||||
if (!account) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '账户不存在'
|
||||
});
|
||||
}
|
||||
|
||||
await account.update({ status });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '账户状态更新成功',
|
||||
data: account.getSafeInfo()
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('更新账户状态错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '服务器内部错误'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 存款
|
||||
* @param {Object} req 请求对象
|
||||
* @param {Object} res 响应对象
|
||||
*/
|
||||
exports.deposit = async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '输入数据验证失败',
|
||||
errors: errors.array()
|
||||
});
|
||||
}
|
||||
|
||||
const { accountId } = req.params;
|
||||
const { amount, description } = req.body;
|
||||
|
||||
const account = await Account.findByPk(accountId);
|
||||
if (!account) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '账户不存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 检查账户状态
|
||||
if (!account.isActive()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '账户状态异常,无法进行存款操作'
|
||||
});
|
||||
}
|
||||
|
||||
const amountInCents = Math.round(amount * 100);
|
||||
const balanceBefore = account.balance;
|
||||
const balanceAfter = balanceBefore + amountInCents;
|
||||
|
||||
// 开始事务
|
||||
const transaction = await sequelize.transaction();
|
||||
|
||||
try {
|
||||
// 更新账户余额
|
||||
await account.update({
|
||||
balance: balanceAfter,
|
||||
available_balance: account.available_balance + amountInCents
|
||||
}, { transaction });
|
||||
|
||||
// 创建交易记录
|
||||
await Transaction.create({
|
||||
transaction_number: await generateTransactionNumber(),
|
||||
account_id: account.id,
|
||||
transaction_type: 'deposit',
|
||||
amount: amountInCents,
|
||||
balance_before: balanceBefore,
|
||||
balance_after: balanceAfter,
|
||||
description: description || '存款',
|
||||
status: 'completed',
|
||||
processed_at: new Date()
|
||||
}, { transaction });
|
||||
|
||||
await transaction.commit();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '存款成功',
|
||||
data: {
|
||||
amount: amount,
|
||||
balance_after: account.formatAmount(balanceAfter),
|
||||
transaction_number: await generateTransactionNumber()
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('存款错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '服务器内部错误'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 取款
|
||||
* @param {Object} req 请求对象
|
||||
* @param {Object} res 响应对象
|
||||
*/
|
||||
exports.withdraw = async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '输入数据验证失败',
|
||||
errors: errors.array()
|
||||
});
|
||||
}
|
||||
|
||||
const { accountId } = req.params;
|
||||
const { amount, description } = req.body;
|
||||
|
||||
const account = await Account.findByPk(accountId);
|
||||
if (!account) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '账户不存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 检查账户状态
|
||||
if (!account.isActive()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '账户状态异常,无法进行取款操作'
|
||||
});
|
||||
}
|
||||
|
||||
const amountInCents = Math.round(amount * 100);
|
||||
|
||||
// 检查余额是否充足
|
||||
if (!account.hasSufficientBalance(amountInCents)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '账户余额不足'
|
||||
});
|
||||
}
|
||||
|
||||
const balanceBefore = account.balance;
|
||||
const balanceAfter = balanceBefore - amountInCents;
|
||||
|
||||
// 开始事务
|
||||
const transaction = await sequelize.transaction();
|
||||
|
||||
try {
|
||||
// 更新账户余额
|
||||
await account.update({
|
||||
balance: balanceAfter,
|
||||
available_balance: account.available_balance - amountInCents
|
||||
}, { transaction });
|
||||
|
||||
// 创建交易记录
|
||||
await Transaction.create({
|
||||
transaction_number: await generateTransactionNumber(),
|
||||
account_id: account.id,
|
||||
transaction_type: 'withdrawal',
|
||||
amount: amountInCents,
|
||||
balance_before: balanceBefore,
|
||||
balance_after: balanceAfter,
|
||||
description: description || '取款',
|
||||
status: 'completed',
|
||||
processed_at: new Date()
|
||||
}, { transaction });
|
||||
|
||||
await transaction.commit();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '取款成功',
|
||||
data: {
|
||||
amount: amount,
|
||||
balance_after: account.formatAmount(balanceAfter),
|
||||
transaction_number: await generateTransactionNumber()
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('取款错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '服务器内部错误'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 生成账户号码
|
||||
* @returns {String} 账户号码
|
||||
*/
|
||||
async function generateAccountNumber() {
|
||||
const bankCode = process.env.BANK_CODE || '001';
|
||||
const timestamp = Date.now().toString().slice(-8);
|
||||
const random = Math.floor(Math.random() * 10000).toString().padStart(4, '0');
|
||||
return `${bankCode}${timestamp}${random}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成交易流水号
|
||||
* @returns {String} 交易流水号
|
||||
*/
|
||||
async function generateTransactionNumber() {
|
||||
const timestamp = Date.now().toString();
|
||||
const random = Math.floor(Math.random() * 10000).toString().padStart(4, '0');
|
||||
return `TXN${timestamp}${random}`;
|
||||
}
|
||||
459
bank-backend/controllers/transactionController.js
Normal file
459
bank-backend/controllers/transactionController.js
Normal file
@@ -0,0 +1,459 @@
|
||||
/**
|
||||
* 交易控制器
|
||||
* @file transactionController.js
|
||||
* @description 处理银行交易相关的请求
|
||||
*/
|
||||
const { Transaction, Account, User } = require('../models');
|
||||
const { validationResult } = require('express-validator');
|
||||
const { Op } = require('sequelize');
|
||||
|
||||
/**
|
||||
* 获取交易记录列表
|
||||
* @param {Object} req 请求对象
|
||||
* @param {Object} res 响应对象
|
||||
*/
|
||||
exports.getTransactions = async (req, res) => {
|
||||
try {
|
||||
const page = parseInt(req.query.page) || 1;
|
||||
const limit = parseInt(req.query.limit) || 20;
|
||||
const offset = (page - 1) * limit;
|
||||
const {
|
||||
account_id,
|
||||
transaction_type,
|
||||
status,
|
||||
start_date,
|
||||
end_date,
|
||||
amount_min,
|
||||
amount_max
|
||||
} = req.query;
|
||||
|
||||
const whereClause = {};
|
||||
|
||||
// 普通用户只能查看自己账户的交易记录
|
||||
if (req.user.role.name !== 'admin') {
|
||||
const userAccounts = await Account.findAll({
|
||||
where: { user_id: req.user.id },
|
||||
attributes: ['id']
|
||||
});
|
||||
const accountIds = userAccounts.map(account => account.id);
|
||||
whereClause.account_id = { [Op.in]: accountIds };
|
||||
} else if (account_id) {
|
||||
whereClause.account_id = account_id;
|
||||
}
|
||||
|
||||
if (transaction_type) {
|
||||
whereClause.transaction_type = transaction_type;
|
||||
}
|
||||
|
||||
if (status) {
|
||||
whereClause.status = status;
|
||||
}
|
||||
|
||||
if (start_date || end_date) {
|
||||
whereClause.created_at = {};
|
||||
if (start_date) {
|
||||
whereClause.created_at[Op.gte] = new Date(start_date);
|
||||
}
|
||||
if (end_date) {
|
||||
whereClause.created_at[Op.lte] = new Date(end_date);
|
||||
}
|
||||
}
|
||||
|
||||
if (amount_min || amount_max) {
|
||||
whereClause.amount = {};
|
||||
if (amount_min) {
|
||||
whereClause.amount[Op.gte] = Math.round(parseFloat(amount_min) * 100);
|
||||
}
|
||||
if (amount_max) {
|
||||
whereClause.amount[Op.lte] = Math.round(parseFloat(amount_max) * 100);
|
||||
}
|
||||
}
|
||||
|
||||
const { count, rows } = await Transaction.findAndCountAll({
|
||||
where: whereClause,
|
||||
include: [{
|
||||
model: Account,
|
||||
as: 'account',
|
||||
include: [{
|
||||
model: User,
|
||||
as: 'user',
|
||||
attributes: ['id', 'username', 'real_name']
|
||||
}]
|
||||
}],
|
||||
limit,
|
||||
offset,
|
||||
order: [['created_at', 'DESC']]
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
transactions: rows.map(transaction => ({
|
||||
...transaction.getSafeInfo(),
|
||||
amount_formatted: transaction.getAmountFormatted(),
|
||||
balance_after_formatted: transaction.getBalanceAfterFormatted(),
|
||||
type_description: transaction.getTypeDescription(),
|
||||
status_description: transaction.getStatusDescription(),
|
||||
is_income: transaction.isIncome(),
|
||||
is_expense: transaction.isExpense()
|
||||
})),
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total: count,
|
||||
pages: Math.ceil(count / limit)
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取交易记录错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '服务器内部错误'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取交易详情
|
||||
* @param {Object} req 请求对象
|
||||
* @param {Object} res 响应对象
|
||||
*/
|
||||
exports.getTransactionDetail = async (req, res) => {
|
||||
try {
|
||||
const { transactionId } = req.params;
|
||||
|
||||
const transaction = await Transaction.findByPk(transactionId, {
|
||||
include: [{
|
||||
model: Account,
|
||||
as: 'account',
|
||||
include: [{
|
||||
model: User,
|
||||
as: 'user',
|
||||
attributes: ['id', 'username', 'real_name']
|
||||
}]
|
||||
}]
|
||||
});
|
||||
|
||||
if (!transaction) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '交易记录不存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 检查权限
|
||||
if (req.user.role.name !== 'admin' && transaction.account.user_id !== req.user.id) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: '无权访问该交易记录'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
...transaction.getSafeInfo(),
|
||||
amount_formatted: transaction.getAmountFormatted(),
|
||||
balance_after_formatted: transaction.getBalanceAfterFormatted(),
|
||||
type_description: transaction.getTypeDescription(),
|
||||
status_description: transaction.getStatusDescription(),
|
||||
is_income: transaction.isIncome(),
|
||||
is_expense: transaction.isExpense(),
|
||||
can_reverse: transaction.canReverse()
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取交易详情错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '服务器内部错误'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 转账
|
||||
* @param {Object} req 请求对象
|
||||
* @param {Object} res 响应对象
|
||||
*/
|
||||
exports.transfer = async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '输入数据验证失败',
|
||||
errors: errors.array()
|
||||
});
|
||||
}
|
||||
|
||||
const { from_account_id, to_account_number, amount, description } = req.body;
|
||||
|
||||
// 查找转出账户
|
||||
const fromAccount = await Account.findByPk(from_account_id);
|
||||
if (!fromAccount) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '转出账户不存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 检查转出账户权限
|
||||
if (req.user.role.name !== 'admin' && fromAccount.user_id !== req.user.id) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: '无权操作该账户'
|
||||
});
|
||||
}
|
||||
|
||||
// 查找转入账户
|
||||
const toAccount = await Account.findOne({
|
||||
where: { account_number: to_account_number },
|
||||
include: [{
|
||||
model: User,
|
||||
as: 'user',
|
||||
attributes: ['id', 'username', 'real_name']
|
||||
}]
|
||||
});
|
||||
|
||||
if (!toAccount) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '转入账户不存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 检查账户状态
|
||||
if (!fromAccount.isActive() || !toAccount.isActive()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '账户状态异常,无法进行转账操作'
|
||||
});
|
||||
}
|
||||
|
||||
const amountInCents = Math.round(amount * 100);
|
||||
|
||||
// 检查余额是否充足
|
||||
if (!fromAccount.hasSufficientBalance(amountInCents)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '账户余额不足'
|
||||
});
|
||||
}
|
||||
|
||||
// 开始事务
|
||||
const transaction = await sequelize.transaction();
|
||||
|
||||
try {
|
||||
// 更新转出账户余额
|
||||
await fromAccount.update({
|
||||
balance: fromAccount.balance - amountInCents,
|
||||
available_balance: fromAccount.available_balance - amountInCents
|
||||
}, { transaction });
|
||||
|
||||
// 更新转入账户余额
|
||||
await toAccount.update({
|
||||
balance: toAccount.balance + amountInCents,
|
||||
available_balance: toAccount.available_balance + amountInCents
|
||||
}, { transaction });
|
||||
|
||||
const transactionNumber = await generateTransactionNumber();
|
||||
|
||||
// 创建转出交易记录
|
||||
await Transaction.create({
|
||||
transaction_number: transactionNumber,
|
||||
account_id: fromAccount.id,
|
||||
transaction_type: 'transfer_out',
|
||||
amount: amountInCents,
|
||||
balance_before: fromAccount.balance + amountInCents,
|
||||
balance_after: fromAccount.balance,
|
||||
counterparty_account: toAccount.account_number,
|
||||
counterparty_name: toAccount.user.real_name,
|
||||
description: description || `转账给${toAccount.user.real_name}`,
|
||||
status: 'completed',
|
||||
processed_at: new Date()
|
||||
}, { transaction });
|
||||
|
||||
// 创建转入交易记录
|
||||
await Transaction.create({
|
||||
transaction_number: transactionNumber,
|
||||
account_id: toAccount.id,
|
||||
transaction_type: 'transfer_in',
|
||||
amount: amountInCents,
|
||||
balance_before: toAccount.balance - amountInCents,
|
||||
balance_after: toAccount.balance,
|
||||
counterparty_account: fromAccount.account_number,
|
||||
counterparty_name: fromAccount.user.real_name,
|
||||
description: description || `来自${fromAccount.user.real_name}的转账`,
|
||||
status: 'completed',
|
||||
processed_at: new Date()
|
||||
}, { transaction });
|
||||
|
||||
await transaction.commit();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '转账成功',
|
||||
data: {
|
||||
transaction_number: transactionNumber,
|
||||
amount: amount,
|
||||
from_account: fromAccount.account_number,
|
||||
to_account: toAccount.account_number,
|
||||
to_account_holder: toAccount.user.real_name
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('转账错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '服务器内部错误'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 撤销交易
|
||||
* @param {Object} req 请求对象
|
||||
* @param {Object} res 响应对象
|
||||
*/
|
||||
exports.reverseTransaction = async (req, res) => {
|
||||
try {
|
||||
const { transactionId } = req.params;
|
||||
|
||||
const transaction = await Transaction.findByPk(transactionId, {
|
||||
include: [{
|
||||
model: Account,
|
||||
as: 'account'
|
||||
}]
|
||||
});
|
||||
|
||||
if (!transaction) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '交易记录不存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 检查权限
|
||||
if (req.user.role.name !== 'admin' && transaction.account.user_id !== req.user.id) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: '无权操作该交易'
|
||||
});
|
||||
}
|
||||
|
||||
// 检查是否可以撤销
|
||||
if (!transaction.canReverse()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '该交易无法撤销'
|
||||
});
|
||||
}
|
||||
|
||||
const result = await transaction.reverse();
|
||||
if (result) {
|
||||
res.json({
|
||||
success: true,
|
||||
message: '交易撤销成功'
|
||||
});
|
||||
} else {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: '交易撤销失败'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('撤销交易错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '服务器内部错误'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取交易统计
|
||||
* @param {Object} req 请求对象
|
||||
* @param {Object} res 响应对象
|
||||
*/
|
||||
exports.getTransactionStats = async (req, res) => {
|
||||
try {
|
||||
const { start_date, end_date, account_id } = req.query;
|
||||
|
||||
const whereClause = {};
|
||||
|
||||
// 普通用户只能查看自己账户的统计
|
||||
if (req.user.role.name !== 'admin') {
|
||||
const userAccounts = await Account.findAll({
|
||||
where: { user_id: req.user.id },
|
||||
attributes: ['id']
|
||||
});
|
||||
const accountIds = userAccounts.map(account => account.id);
|
||||
whereClause.account_id = { [Op.in]: accountIds };
|
||||
} else if (account_id) {
|
||||
whereClause.account_id = account_id;
|
||||
}
|
||||
|
||||
if (start_date || end_date) {
|
||||
whereClause.created_at = {};
|
||||
if (start_date) {
|
||||
whereClause.created_at[Op.gte] = new Date(start_date);
|
||||
}
|
||||
if (end_date) {
|
||||
whereClause.created_at[Op.lte] = new Date(end_date);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取交易统计
|
||||
const stats = await Transaction.findAll({
|
||||
where: whereClause,
|
||||
attributes: [
|
||||
'transaction_type',
|
||||
[sequelize.fn('COUNT', sequelize.col('id')), 'count'],
|
||||
[sequelize.fn('SUM', sequelize.col('amount')), 'total_amount']
|
||||
],
|
||||
group: ['transaction_type'],
|
||||
raw: true
|
||||
});
|
||||
|
||||
// 获取总交易数
|
||||
const totalCount = await Transaction.count({ where: whereClause });
|
||||
|
||||
// 获取总交易金额
|
||||
const totalAmount = await Transaction.sum('amount', { where: whereClause });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
total_count: totalCount,
|
||||
total_amount: totalAmount ? (totalAmount / 100).toFixed(2) : '0.00',
|
||||
by_type: stats.map(stat => ({
|
||||
type: stat.transaction_type,
|
||||
count: parseInt(stat.count),
|
||||
total_amount: (parseInt(stat.total_amount) / 100).toFixed(2)
|
||||
}))
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取交易统计错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '服务器内部错误'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 生成交易流水号
|
||||
* @returns {String} 交易流水号
|
||||
*/
|
||||
async function generateTransactionNumber() {
|
||||
const timestamp = Date.now().toString();
|
||||
const random = Math.floor(Math.random() * 10000).toString().padStart(4, '0');
|
||||
return `TXN${timestamp}${random}`;
|
||||
}
|
||||
448
bank-backend/controllers/userController.js
Normal file
448
bank-backend/controllers/userController.js
Normal file
@@ -0,0 +1,448 @@
|
||||
/**
|
||||
* 用户控制器
|
||||
* @file userController.js
|
||||
* @description 处理用户相关的请求
|
||||
*/
|
||||
const { User, Role, Account } = require('../models');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const { validationResult } = require('express-validator');
|
||||
|
||||
/**
|
||||
* 用户注册
|
||||
* @param {Object} req 请求对象
|
||||
* @param {Object} res 响应对象
|
||||
*/
|
||||
exports.register = async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '输入数据验证失败',
|
||||
errors: errors.array()
|
||||
});
|
||||
}
|
||||
|
||||
const { username, email, password, phone, real_name, id_card } = req.body;
|
||||
|
||||
// 检查用户名是否已存在
|
||||
const existingUser = await User.findOne({
|
||||
where: { username }
|
||||
});
|
||||
|
||||
if (existingUser) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '用户名已存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 检查邮箱是否已存在
|
||||
const existingEmail = await User.findOne({
|
||||
where: { email }
|
||||
});
|
||||
|
||||
if (existingEmail) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '邮箱已被注册'
|
||||
});
|
||||
}
|
||||
|
||||
// 检查身份证是否已存在
|
||||
const existingIdCard = await User.findOne({
|
||||
where: { id_card }
|
||||
});
|
||||
|
||||
if (existingIdCard) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '身份证号已被注册'
|
||||
});
|
||||
}
|
||||
|
||||
// 创建新用户
|
||||
const user = await User.create({
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
phone,
|
||||
real_name,
|
||||
id_card,
|
||||
role_id: 2 // 默认为普通用户
|
||||
});
|
||||
|
||||
// 生成JWT令牌
|
||||
const token = jwt.sign(
|
||||
{ id: user.id, username: user.username },
|
||||
process.env.JWT_SECRET,
|
||||
{ expiresIn: process.env.JWT_EXPIRES_IN || '24h' }
|
||||
);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '注册成功',
|
||||
data: {
|
||||
user: user.getSafeInfo(),
|
||||
token
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('用户注册错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '服务器内部错误'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 用户登录
|
||||
* @param {Object} req 请求对象
|
||||
* @param {Object} res 响应对象
|
||||
*/
|
||||
exports.login = async (req, res) => {
|
||||
try {
|
||||
const { username, password } = req.body;
|
||||
|
||||
// 查找用户
|
||||
const user = await User.findOne({
|
||||
where: { username },
|
||||
include: [{
|
||||
model: Role,
|
||||
as: 'role'
|
||||
}]
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: '用户名或密码错误'
|
||||
});
|
||||
}
|
||||
|
||||
// 检查账户状态
|
||||
if (user.status !== 'active') {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: '账户已被禁用'
|
||||
});
|
||||
}
|
||||
|
||||
// 检查账户是否被锁定
|
||||
if (user.locked_until && user.locked_until > new Date()) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: '账户已被锁定,请稍后再试'
|
||||
});
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
const isValidPassword = await user.validPassword(password);
|
||||
if (!isValidPassword) {
|
||||
// 增加登录失败次数
|
||||
user.login_attempts += 1;
|
||||
|
||||
// 如果失败次数达到5次,锁定账户1小时
|
||||
if (user.login_attempts >= 5) {
|
||||
user.locked_until = new Date(Date.now() + 60 * 60 * 1000); // 1小时后解锁
|
||||
}
|
||||
|
||||
await user.save();
|
||||
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: '用户名或密码错误'
|
||||
});
|
||||
}
|
||||
|
||||
// 登录成功,重置登录失败次数
|
||||
user.login_attempts = 0;
|
||||
user.locked_until = null;
|
||||
user.last_login = new Date();
|
||||
await user.save();
|
||||
|
||||
// 生成JWT令牌
|
||||
const token = jwt.sign(
|
||||
{ id: user.id, username: user.username },
|
||||
process.env.JWT_SECRET,
|
||||
{ expiresIn: process.env.JWT_EXPIRES_IN || '24h' }
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '登录成功',
|
||||
data: {
|
||||
user: user.getSafeInfo(),
|
||||
token
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('用户登录错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '服务器内部错误'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
* @param {Object} req 请求对象
|
||||
* @param {Object} res 响应对象
|
||||
*/
|
||||
exports.getProfile = async (req, res) => {
|
||||
try {
|
||||
const user = await User.findByPk(req.user.id, {
|
||||
include: [{
|
||||
model: Role,
|
||||
as: 'role'
|
||||
}]
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '用户不存在'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: user.getSafeInfo()
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取用户信息错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '服务器内部错误'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新用户信息
|
||||
* @param {Object} req 请求对象
|
||||
* @param {Object} res 响应对象
|
||||
*/
|
||||
exports.updateProfile = async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '输入数据验证失败',
|
||||
errors: errors.array()
|
||||
});
|
||||
}
|
||||
|
||||
const { phone, real_name, avatar } = req.body;
|
||||
const user = await User.findByPk(req.user.id);
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '用户不存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 更新用户信息
|
||||
await user.update({
|
||||
phone: phone || user.phone,
|
||||
real_name: real_name || user.real_name,
|
||||
avatar: avatar || user.avatar
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '用户信息更新成功',
|
||||
data: user.getSafeInfo()
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('更新用户信息错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '服务器内部错误'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 修改密码
|
||||
* @param {Object} req 请求对象
|
||||
* @param {Object} res 响应对象
|
||||
*/
|
||||
exports.changePassword = async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '输入数据验证失败',
|
||||
errors: errors.array()
|
||||
});
|
||||
}
|
||||
|
||||
const { old_password, new_password } = req.body;
|
||||
const user = await User.findByPk(req.user.id);
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '用户不存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 验证旧密码
|
||||
const isValidPassword = await user.validPassword(old_password);
|
||||
if (!isValidPassword) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '原密码错误'
|
||||
});
|
||||
}
|
||||
|
||||
// 更新密码
|
||||
user.password = new_password;
|
||||
await user.save();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '密码修改成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('修改密码错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '服务器内部错误'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取用户列表(管理员)
|
||||
* @param {Object} req 请求对象
|
||||
* @param {Object} res 响应对象
|
||||
*/
|
||||
exports.getUsers = async (req, res) => {
|
||||
try {
|
||||
const page = parseInt(req.query.page) || 1;
|
||||
const limit = parseInt(req.query.limit) || 10;
|
||||
const offset = (page - 1) * limit;
|
||||
const search = req.query.search || '';
|
||||
|
||||
const whereClause = {};
|
||||
if (search) {
|
||||
whereClause[Op.or] = [
|
||||
{ username: { [Op.like]: `%${search}%` } },
|
||||
{ email: { [Op.like]: `%${search}%` } },
|
||||
{ real_name: { [Op.like]: `%${search}%` } }
|
||||
];
|
||||
}
|
||||
|
||||
const { count, rows } = await User.findAndCountAll({
|
||||
where: whereClause,
|
||||
include: [{
|
||||
model: Role,
|
||||
as: 'role'
|
||||
}],
|
||||
limit,
|
||||
offset,
|
||||
order: [['created_at', 'DESC']]
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
users: rows.map(user => user.getSafeInfo()),
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total: count,
|
||||
pages: Math.ceil(count / limit)
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取用户列表错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '服务器内部错误'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新用户状态(管理员)
|
||||
* @param {Object} req 请求对象
|
||||
* @param {Object} res 响应对象
|
||||
*/
|
||||
exports.updateUserStatus = async (req, res) => {
|
||||
try {
|
||||
const { userId } = req.params;
|
||||
const { status } = req.body;
|
||||
|
||||
const user = await User.findByPk(userId);
|
||||
if (!user) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '用户不存在'
|
||||
});
|
||||
}
|
||||
|
||||
await user.update({ status });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '用户状态更新成功',
|
||||
data: user.getSafeInfo()
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('更新用户状态错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '服务器内部错误'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取用户账户列表
|
||||
* @param {Object} req 请求对象
|
||||
* @param {Object} res 响应对象
|
||||
*/
|
||||
exports.getUserAccounts = async (req, res) => {
|
||||
try {
|
||||
const userId = req.params.userId || req.user.id;
|
||||
|
||||
// 检查权限:用户只能查看自己的账户,管理员可以查看所有账户
|
||||
if (userId !== req.user.id.toString() && req.user.role.name !== 'admin') {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: '无权访问该用户的账户信息'
|
||||
});
|
||||
}
|
||||
|
||||
const accounts = await Account.findAll({
|
||||
where: { user_id: userId },
|
||||
order: [['created_at', 'DESC']]
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: accounts.map(account => ({
|
||||
...account.getSafeInfo(),
|
||||
balance_formatted: account.getBalanceFormatted(),
|
||||
available_balance_formatted: account.getAvailableBalanceFormatted(),
|
||||
frozen_amount_formatted: account.getFrozenAmountFormatted()
|
||||
}))
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取用户账户列表错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '服务器内部错误'
|
||||
});
|
||||
}
|
||||
};
|
||||
314
bank-backend/docs/database-schema.md
Normal file
314
bank-backend/docs/database-schema.md
Normal file
@@ -0,0 +1,314 @@
|
||||
# 银行系统数据库架构设计
|
||||
|
||||
## 概述
|
||||
|
||||
本文档描述了银行管理后台系统的数据库架构设计,包括表结构、字段定义、索引设计和关系约束。
|
||||
|
||||
## 数据库信息
|
||||
|
||||
- **数据库类型**: MySQL 8.0+
|
||||
- **字符集**: utf8mb4
|
||||
- **排序规则**: utf8mb4_unicode_ci
|
||||
- **时区**: +08:00 (Asia/Shanghai)
|
||||
|
||||
## 表结构设计
|
||||
|
||||
### 1. 用户表 (users)
|
||||
|
||||
存储银行系统的用户信息。
|
||||
|
||||
| 字段名 | 类型 | 约束 | 描述 |
|
||||
|--------|------|------|------|
|
||||
| id | INT | PRIMARY KEY, AUTO_INCREMENT | 用户ID |
|
||||
| username | VARCHAR(50) | UNIQUE, NOT NULL | 用户名 |
|
||||
| email | VARCHAR(100) | UNIQUE, NOT NULL | 邮箱地址 |
|
||||
| password | VARCHAR(255) | NOT NULL | 密码(bcrypt加密) |
|
||||
| phone | VARCHAR(20) | NULL | 手机号 |
|
||||
| real_name | VARCHAR(50) | NOT NULL | 真实姓名 |
|
||||
| id_card | VARCHAR(18) | UNIQUE, NOT NULL | 身份证号 |
|
||||
| avatar | VARCHAR(255) | NULL | 头像URL |
|
||||
| role_id | INT | FOREIGN KEY, NOT NULL | 角色ID |
|
||||
| status | ENUM | DEFAULT 'active' | 用户状态 |
|
||||
| last_login | DATETIME | NULL | 最后登录时间 |
|
||||
| login_attempts | INT | DEFAULT 0 | 登录失败次数 |
|
||||
| locked_until | DATETIME | NULL | 账户锁定到期时间 |
|
||||
| created_at | DATETIME | DEFAULT CURRENT_TIMESTAMP | 创建时间 |
|
||||
| updated_at | DATETIME | DEFAULT CURRENT_TIMESTAMP ON UPDATE | 更新时间 |
|
||||
|
||||
**索引设计**:
|
||||
- PRIMARY KEY (id)
|
||||
- UNIQUE KEY (username)
|
||||
- UNIQUE KEY (email)
|
||||
- UNIQUE KEY (id_card)
|
||||
- INDEX (role_id)
|
||||
- INDEX (status)
|
||||
- INDEX (created_at)
|
||||
|
||||
**状态枚举**:
|
||||
- `active`: 正常
|
||||
- `inactive`: 未激活
|
||||
- `suspended`: 暂停
|
||||
- `locked`: 锁定
|
||||
|
||||
### 2. 角色表 (roles)
|
||||
|
||||
存储系统角色信息。
|
||||
|
||||
| 字段名 | 类型 | 约束 | 描述 |
|
||||
|--------|------|------|------|
|
||||
| id | INT | PRIMARY KEY, AUTO_INCREMENT | 角色ID |
|
||||
| name | VARCHAR(50) | UNIQUE, NOT NULL | 角色名称 |
|
||||
| display_name | VARCHAR(100) | NOT NULL | 显示名称 |
|
||||
| description | TEXT | NULL | 角色描述 |
|
||||
| level | INT | NOT NULL, DEFAULT 1 | 角色级别 |
|
||||
| is_system | BOOLEAN | DEFAULT FALSE | 是否系统角色 |
|
||||
| status | ENUM | DEFAULT 'active' | 角色状态 |
|
||||
| created_at | DATETIME | DEFAULT CURRENT_TIMESTAMP | 创建时间 |
|
||||
| updated_at | DATETIME | DEFAULT CURRENT_TIMESTAMP ON UPDATE | 更新时间 |
|
||||
|
||||
**索引设计**:
|
||||
- PRIMARY KEY (id)
|
||||
- UNIQUE KEY (name)
|
||||
- INDEX (level)
|
||||
- INDEX (status)
|
||||
|
||||
**预定义角色**:
|
||||
- `admin`: 系统管理员 (level: 100)
|
||||
- `manager`: 银行经理 (level: 80)
|
||||
- `teller`: 银行柜员 (level: 60)
|
||||
- `user`: 普通用户 (level: 20)
|
||||
|
||||
### 3. 账户表 (accounts)
|
||||
|
||||
存储银行账户信息。
|
||||
|
||||
| 字段名 | 类型 | 约束 | 描述 |
|
||||
|--------|------|------|------|
|
||||
| id | INT | PRIMARY KEY, AUTO_INCREMENT | 账户ID |
|
||||
| account_number | VARCHAR(20) | UNIQUE, NOT NULL | 账户号码 |
|
||||
| user_id | INT | FOREIGN KEY, NOT NULL | 用户ID |
|
||||
| account_type | ENUM | NOT NULL, DEFAULT 'savings' | 账户类型 |
|
||||
| balance | BIGINT | NOT NULL, DEFAULT 0 | 账户余额(分) |
|
||||
| available_balance | BIGINT | NOT NULL, DEFAULT 0 | 可用余额(分) |
|
||||
| frozen_amount | BIGINT | NOT NULL, DEFAULT 0 | 冻结金额(分) |
|
||||
| currency | VARCHAR(3) | NOT NULL, DEFAULT 'CNY' | 货币类型 |
|
||||
| interest_rate | DECIMAL(5,4) | NULL | 利率(年化) |
|
||||
| status | ENUM | NOT NULL, DEFAULT 'active' | 账户状态 |
|
||||
| opened_at | DATETIME | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 开户时间 |
|
||||
| closed_at | DATETIME | NULL | 销户时间 |
|
||||
| created_at | DATETIME | DEFAULT CURRENT_TIMESTAMP | 创建时间 |
|
||||
| updated_at | DATETIME | DEFAULT CURRENT_TIMESTAMP ON UPDATE | 更新时间 |
|
||||
|
||||
**索引设计**:
|
||||
- PRIMARY KEY (id)
|
||||
- UNIQUE KEY (account_number)
|
||||
- INDEX (user_id)
|
||||
- INDEX (account_type)
|
||||
- INDEX (status)
|
||||
- INDEX (opened_at)
|
||||
|
||||
**账户类型枚举**:
|
||||
- `savings`: 储蓄账户
|
||||
- `checking`: 支票账户
|
||||
- `credit`: 信用卡账户
|
||||
- `loan`: 贷款账户
|
||||
|
||||
**账户状态枚举**:
|
||||
- `active`: 正常
|
||||
- `inactive`: 未激活
|
||||
- `frozen`: 冻结
|
||||
- `closed`: 已关闭
|
||||
|
||||
### 4. 交易记录表 (transactions)
|
||||
|
||||
存储所有银行交易记录。
|
||||
|
||||
| 字段名 | 类型 | 约束 | 描述 |
|
||||
|--------|------|------|------|
|
||||
| id | INT | PRIMARY KEY, AUTO_INCREMENT | 交易ID |
|
||||
| transaction_number | VARCHAR(32) | UNIQUE, NOT NULL | 交易流水号 |
|
||||
| account_id | INT | FOREIGN KEY, NOT NULL | 账户ID |
|
||||
| transaction_type | ENUM | NOT NULL | 交易类型 |
|
||||
| amount | BIGINT | NOT NULL | 交易金额(分) |
|
||||
| balance_before | BIGINT | NOT NULL | 交易前余额(分) |
|
||||
| balance_after | BIGINT | NOT NULL | 交易后余额(分) |
|
||||
| counterparty_account | VARCHAR(20) | NULL | 对方账户号 |
|
||||
| counterparty_name | VARCHAR(100) | NULL | 对方户名 |
|
||||
| description | VARCHAR(255) | NULL | 交易描述 |
|
||||
| reference_number | VARCHAR(50) | NULL | 参考号 |
|
||||
| status | ENUM | NOT NULL, DEFAULT 'pending' | 交易状态 |
|
||||
| processed_at | DATETIME | NULL | 处理时间 |
|
||||
| reversed_at | DATETIME | NULL | 撤销时间 |
|
||||
| created_at | DATETIME | DEFAULT CURRENT_TIMESTAMP | 创建时间 |
|
||||
| updated_at | DATETIME | DEFAULT CURRENT_TIMESTAMP ON UPDATE | 更新时间 |
|
||||
|
||||
**索引设计**:
|
||||
- PRIMARY KEY (id)
|
||||
- UNIQUE KEY (transaction_number)
|
||||
- INDEX (account_id)
|
||||
- INDEX (transaction_type)
|
||||
- INDEX (status)
|
||||
- INDEX (created_at)
|
||||
- INDEX (counterparty_account)
|
||||
|
||||
**交易类型枚举**:
|
||||
- `deposit`: 存款
|
||||
- `withdrawal`: 取款
|
||||
- `transfer_in`: 转入
|
||||
- `transfer_out`: 转出
|
||||
- `interest`: 利息
|
||||
- `fee`: 手续费
|
||||
- `loan`: 贷款
|
||||
- `repayment`: 还款
|
||||
|
||||
**交易状态枚举**:
|
||||
- `pending`: 处理中
|
||||
- `completed`: 已完成
|
||||
- `failed`: 失败
|
||||
- `cancelled`: 已取消
|
||||
- `reversed`: 已冲正
|
||||
|
||||
## 关系设计
|
||||
|
||||
### 外键约束
|
||||
|
||||
1. **users.role_id** → **roles.id**
|
||||
- 用户与角色的多对一关系
|
||||
- 级联更新,限制删除
|
||||
|
||||
2. **accounts.user_id** → **users.id**
|
||||
- 账户与用户的多对一关系
|
||||
- 级联更新,限制删除
|
||||
|
||||
3. **transactions.account_id** → **accounts.id**
|
||||
- 交易记录与账户的多对一关系
|
||||
- 级联更新,限制删除
|
||||
|
||||
### 业务约束
|
||||
|
||||
1. **账户余额约束**
|
||||
- balance >= 0
|
||||
- available_balance >= 0
|
||||
- frozen_amount >= 0
|
||||
- balance = available_balance + frozen_amount
|
||||
|
||||
2. **交易金额约束**
|
||||
- amount > 0
|
||||
- balance_after = balance_before ± amount
|
||||
|
||||
3. **用户状态约束**
|
||||
- 锁定用户不能登录
|
||||
- 暂停用户不能进行交易
|
||||
|
||||
## 数据完整性
|
||||
|
||||
### 触发器设计
|
||||
|
||||
1. **账户余额更新触发器**
|
||||
- 确保余额字段的一致性
|
||||
- 自动计算可用余额
|
||||
|
||||
2. **交易记录触发器**
|
||||
- 自动更新账户余额
|
||||
- 记录余额变化历史
|
||||
|
||||
### 存储过程
|
||||
|
||||
1. **转账处理存储过程**
|
||||
- 原子性转账操作
|
||||
- 自动生成交易记录
|
||||
|
||||
2. **利息计算存储过程**
|
||||
- 定期计算账户利息
|
||||
- 批量更新账户余额
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 分区策略
|
||||
|
||||
1. **交易记录表分区**
|
||||
- 按创建时间分区(按月)
|
||||
- 提高查询性能
|
||||
- 便于历史数据归档
|
||||
|
||||
### 索引优化
|
||||
|
||||
1. **复合索引**
|
||||
- (account_id, created_at): 账户交易查询
|
||||
- (transaction_type, status): 交易统计查询
|
||||
- (user_id, status): 用户状态查询
|
||||
|
||||
2. **覆盖索引**
|
||||
- 减少回表查询
|
||||
- 提高查询效率
|
||||
|
||||
## 数据安全
|
||||
|
||||
### 敏感数据加密
|
||||
|
||||
1. **密码加密**
|
||||
- 使用bcrypt算法
|
||||
- 盐值随机生成
|
||||
|
||||
2. **身份证号加密**
|
||||
- 存储时加密
|
||||
- 查询时解密
|
||||
|
||||
### 数据备份
|
||||
|
||||
1. **全量备份**
|
||||
- 每日凌晨自动备份
|
||||
- 保留30天历史
|
||||
|
||||
2. **增量备份**
|
||||
- 每小时增量备份
|
||||
- 实时同步到备库
|
||||
|
||||
## 监控与维护
|
||||
|
||||
### 性能监控
|
||||
|
||||
1. **慢查询监控**
|
||||
- 记录执行时间>1s的查询
|
||||
- 定期优化慢查询
|
||||
|
||||
2. **连接数监控**
|
||||
- 监控数据库连接数
|
||||
- 防止连接池耗尽
|
||||
|
||||
### 数据清理
|
||||
|
||||
1. **日志清理**
|
||||
- 定期清理过期日志
|
||||
- 保留关键操作记录
|
||||
|
||||
2. **历史数据归档**
|
||||
- 超过1年的交易记录归档
|
||||
- 减少主表数据量
|
||||
|
||||
## 扩展性设计
|
||||
|
||||
### 水平扩展
|
||||
|
||||
1. **读写分离**
|
||||
- 主库写入,从库读取
|
||||
- 提高系统并发能力
|
||||
|
||||
2. **分库分表**
|
||||
- 按用户ID分库
|
||||
- 按时间分表
|
||||
|
||||
### 垂直扩展
|
||||
|
||||
1. **字段扩展**
|
||||
- 预留扩展字段
|
||||
- 支持业务需求变化
|
||||
|
||||
2. **表结构扩展**
|
||||
- 模块化表设计
|
||||
- 支持功能模块独立
|
||||
|
||||
---
|
||||
|
||||
*最后更新: 2025-01-18*
|
||||
*版本: v1.0*
|
||||
45
bank-backend/env.example
Normal file
45
bank-backend/env.example
Normal file
@@ -0,0 +1,45 @@
|
||||
# 服务器配置
|
||||
PORT=5351
|
||||
NODE_ENV=development
|
||||
|
||||
# 数据库配置
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_NAME=bank_management
|
||||
DB_USER=root
|
||||
DB_PASSWORD=your_password
|
||||
DB_DIALECT=mysql
|
||||
|
||||
# JWT配置
|
||||
JWT_SECRET=your_jwt_secret_key_here
|
||||
JWT_EXPIRES_IN=24h
|
||||
|
||||
# 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
|
||||
|
||||
# 文件上传配置
|
||||
UPLOAD_MAX_SIZE=10485760
|
||||
UPLOAD_ALLOWED_TYPES=image/jpeg,image/png,image/gif,application/pdf
|
||||
|
||||
# 安全配置
|
||||
BCRYPT_ROUNDS=10
|
||||
RATE_LIMIT_WINDOW_MS=900000
|
||||
RATE_LIMIT_MAX_REQUESTS=100
|
||||
|
||||
# 日志配置
|
||||
LOG_LEVEL=info
|
||||
LOG_FILE=logs/app.log
|
||||
|
||||
# 银行系统配置
|
||||
BANK_CODE=001
|
||||
BANK_NAME=示例银行
|
||||
CURRENCY=CNY
|
||||
TIMEZONE=Asia/Shanghai
|
||||
226
bank-backend/middleware/auth.js
Normal file
226
bank-backend/middleware/auth.js
Normal file
@@ -0,0 +1,226 @@
|
||||
/**
|
||||
* 认证中间件
|
||||
* @file auth.js
|
||||
* @description 处理用户认证和授权
|
||||
*/
|
||||
const jwt = require('jsonwebtoken');
|
||||
const { User, Role } = require('../models');
|
||||
|
||||
/**
|
||||
* 验证JWT令牌
|
||||
* @param {Object} req 请求对象
|
||||
* @param {Object} res 响应对象
|
||||
* @param {Function} next 下一个中间件
|
||||
*/
|
||||
const verifyToken = async (req, res, next) => {
|
||||
try {
|
||||
const token = req.header('Authorization')?.replace('Bearer ', '');
|
||||
|
||||
if (!token) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: '访问被拒绝,未提供令牌'
|
||||
});
|
||||
}
|
||||
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||
const user = await User.findByPk(decoded.id, {
|
||||
include: [{
|
||||
model: Role,
|
||||
as: 'role'
|
||||
}]
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: '令牌无效,用户不存在'
|
||||
});
|
||||
}
|
||||
|
||||
if (user.status !== 'active') {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: '账户已被禁用'
|
||||
});
|
||||
}
|
||||
|
||||
req.user = user;
|
||||
next();
|
||||
} catch (error) {
|
||||
if (error.name === 'TokenExpiredError') {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: '令牌已过期'
|
||||
});
|
||||
} else if (error.name === 'JsonWebTokenError') {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: '令牌无效'
|
||||
});
|
||||
} else {
|
||||
console.error('认证中间件错误:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '服务器内部错误'
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查用户角色权限
|
||||
* @param {String|Array} roles 允许的角色
|
||||
* @returns {Function} 中间件函数
|
||||
*/
|
||||
const requireRole = (roles) => {
|
||||
return async (req, res, next) => {
|
||||
try {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: '请先登录'
|
||||
});
|
||||
}
|
||||
|
||||
const userRole = req.user.role;
|
||||
if (!userRole) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: '用户角色未分配'
|
||||
});
|
||||
}
|
||||
|
||||
const allowedRoles = Array.isArray(roles) ? roles : [roles];
|
||||
if (!allowedRoles.includes(userRole.name)) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: '权限不足'
|
||||
});
|
||||
}
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
console.error('角色权限检查错误:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '服务器内部错误'
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查用户权限级别
|
||||
* @param {Number} minLevel 最小权限级别
|
||||
* @returns {Function} 中间件函数
|
||||
*/
|
||||
const requireLevel = (minLevel) => {
|
||||
return async (req, res, next) => {
|
||||
try {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: '请先登录'
|
||||
});
|
||||
}
|
||||
|
||||
const userRole = req.user.role;
|
||||
if (!userRole || userRole.level < minLevel) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: '权限级别不足'
|
||||
});
|
||||
}
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
console.error('权限级别检查错误:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '服务器内部错误'
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 可选认证中间件(不强制要求登录)
|
||||
* @param {Object} req 请求对象
|
||||
* @param {Object} res 响应对象
|
||||
* @param {Function} next 下一个中间件
|
||||
*/
|
||||
const optionalAuth = async (req, res, next) => {
|
||||
try {
|
||||
const token = req.header('Authorization')?.replace('Bearer ', '');
|
||||
|
||||
if (token) {
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||
const user = await User.findByPk(decoded.id, {
|
||||
include: [{
|
||||
model: Role,
|
||||
as: 'role'
|
||||
}]
|
||||
});
|
||||
|
||||
if (user && user.status === 'active') {
|
||||
req.user = user;
|
||||
}
|
||||
}
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
// 可选认证失败时不返回错误,继续执行
|
||||
next();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查账户所有权
|
||||
* @param {Object} req 请求对象
|
||||
* @param {Object} res 响应对象
|
||||
* @param {Function} next 下一个中间件
|
||||
*/
|
||||
const checkAccountOwnership = async (req, res, next) => {
|
||||
try {
|
||||
const { accountId } = req.params;
|
||||
const userId = req.user.id;
|
||||
|
||||
// 管理员可以访问所有账户
|
||||
if (req.user.role && req.user.role.name === 'admin') {
|
||||
return next();
|
||||
}
|
||||
|
||||
const { Account } = require('../models');
|
||||
const account = await Account.findOne({
|
||||
where: {
|
||||
id: accountId,
|
||||
user_id: userId
|
||||
}
|
||||
});
|
||||
|
||||
if (!account) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: '无权访问该账户'
|
||||
});
|
||||
}
|
||||
|
||||
req.account = account;
|
||||
next();
|
||||
} catch (error) {
|
||||
console.error('账户所有权检查错误:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '服务器内部错误'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
verifyToken,
|
||||
requireRole,
|
||||
requireLevel,
|
||||
optionalAuth,
|
||||
checkAccountOwnership
|
||||
};
|
||||
239
bank-backend/middleware/security.js
Normal file
239
bank-backend/middleware/security.js
Normal file
@@ -0,0 +1,239 @@
|
||||
/**
|
||||
* 安全中间件
|
||||
* @file security.js
|
||||
* @description 处理安全相关的中间件
|
||||
*/
|
||||
const rateLimit = require('express-rate-limit');
|
||||
const helmet = require('helmet');
|
||||
const { body, validationResult } = require('express-validator');
|
||||
|
||||
/**
|
||||
* API请求频率限制
|
||||
*/
|
||||
const apiRateLimiter = rateLimit({
|
||||
windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS) || 15 * 60 * 1000, // 15分钟
|
||||
max: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS) || 100, // 限制每个IP 15分钟内最多100个请求
|
||||
message: {
|
||||
success: false,
|
||||
message: '请求过于频繁,请稍后再试'
|
||||
},
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
handler: (req, res) => {
|
||||
res.status(429).json({
|
||||
success: false,
|
||||
message: '请求过于频繁,请稍后再试'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 登录请求频率限制(更严格)
|
||||
*/
|
||||
const loginRateLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15分钟
|
||||
max: 5, // 限制每个IP 15分钟内最多5次登录尝试
|
||||
message: {
|
||||
success: false,
|
||||
message: '登录尝试次数过多,请15分钟后再试'
|
||||
},
|
||||
skipSuccessfulRequests: true,
|
||||
handler: (req, res) => {
|
||||
res.status(429).json({
|
||||
success: false,
|
||||
message: '登录尝试次数过多,请15分钟后再试'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 安全头部设置
|
||||
*/
|
||||
const securityHeaders = helmet({
|
||||
contentSecurityPolicy: {
|
||||
directives: {
|
||||
defaultSrc: ["'self'"],
|
||||
styleSrc: ["'self'", "'unsafe-inline'"],
|
||||
scriptSrc: ["'self'"],
|
||||
imgSrc: ["'self'", "data:", "https:"],
|
||||
connectSrc: ["'self'"],
|
||||
fontSrc: ["'self'"],
|
||||
objectSrc: ["'none'"],
|
||||
mediaSrc: ["'self'"],
|
||||
frameSrc: ["'none'"]
|
||||
}
|
||||
},
|
||||
crossOriginEmbedderPolicy: false
|
||||
});
|
||||
|
||||
/**
|
||||
* 输入数据清理
|
||||
*/
|
||||
const inputSanitizer = (req, res, next) => {
|
||||
// 清理请求体中的危险字符
|
||||
const sanitizeObject = (obj) => {
|
||||
if (typeof obj !== 'object' || obj === null) return obj;
|
||||
|
||||
for (const key in obj) {
|
||||
if (typeof obj[key] === 'string') {
|
||||
// 移除潜在的XSS攻击字符
|
||||
obj[key] = obj[key]
|
||||
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
|
||||
.replace(/javascript:/gi, '')
|
||||
.replace(/on\w+\s*=/gi, '');
|
||||
} else if (typeof obj[key] === 'object') {
|
||||
sanitizeObject(obj[key]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (req.body) sanitizeObject(req.body);
|
||||
if (req.query) sanitizeObject(req.query);
|
||||
if (req.params) sanitizeObject(req.params);
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
/**
|
||||
* 会话超时检查
|
||||
*/
|
||||
const sessionTimeoutCheck = (req, res, next) => {
|
||||
if (req.user && req.user.last_login) {
|
||||
const lastLogin = new Date(req.user.last_login);
|
||||
const now = new Date();
|
||||
const timeout = 24 * 60 * 60 * 1000; // 24小时
|
||||
|
||||
if (now - lastLogin > timeout) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: '会话已超时,请重新登录'
|
||||
});
|
||||
}
|
||||
}
|
||||
next();
|
||||
};
|
||||
|
||||
/**
|
||||
* 验证错误处理中间件
|
||||
*/
|
||||
const handleValidationErrors = (req, res, next) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '输入数据验证失败',
|
||||
errors: errors.array()
|
||||
});
|
||||
}
|
||||
next();
|
||||
};
|
||||
|
||||
/**
|
||||
* 银行账户验证规则
|
||||
*/
|
||||
const validateAccountNumber = [
|
||||
body('account_number')
|
||||
.isLength({ min: 16, max: 20 })
|
||||
.withMessage('账户号码长度必须在16-20位之间')
|
||||
.matches(/^\d+$/)
|
||||
.withMessage('账户号码只能包含数字'),
|
||||
handleValidationErrors
|
||||
];
|
||||
|
||||
/**
|
||||
* 金额验证规则
|
||||
*/
|
||||
const validateAmount = [
|
||||
body('amount')
|
||||
.isFloat({ min: 0.01 })
|
||||
.withMessage('金额必须大于0')
|
||||
.custom((value) => {
|
||||
// 检查金额精度(最多2位小数)
|
||||
if (value.toString().split('.')[1] && value.toString().split('.')[1].length > 2) {
|
||||
throw new Error('金额最多支持2位小数');
|
||||
}
|
||||
return true;
|
||||
}),
|
||||
handleValidationErrors
|
||||
];
|
||||
|
||||
/**
|
||||
* 身份证号验证规则
|
||||
*/
|
||||
const validateIdCard = [
|
||||
body('id_card')
|
||||
.matches(/^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/)
|
||||
.withMessage('身份证号码格式不正确'),
|
||||
handleValidationErrors
|
||||
];
|
||||
|
||||
/**
|
||||
* 手机号验证规则
|
||||
*/
|
||||
const validatePhone = [
|
||||
body('phone')
|
||||
.matches(/^1[3-9]\d{9}$/)
|
||||
.withMessage('手机号码格式不正确'),
|
||||
handleValidationErrors
|
||||
];
|
||||
|
||||
/**
|
||||
* 密码验证规则
|
||||
*/
|
||||
const validatePassword = [
|
||||
body('password')
|
||||
.isLength({ min: 6, max: 20 })
|
||||
.withMessage('密码长度必须在6-20位之间')
|
||||
.matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/)
|
||||
.withMessage('密码必须包含大小写字母和数字'),
|
||||
handleValidationErrors
|
||||
];
|
||||
|
||||
/**
|
||||
* 防止SQL注入的查询参数验证
|
||||
*/
|
||||
const validateQueryParams = (req, res, next) => {
|
||||
const dangerousPatterns = [
|
||||
/(\b(SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|EXEC|UNION|SCRIPT)\b)/i,
|
||||
/(--|\/\*|\*\/|xp_|sp_)/i,
|
||||
/(\bOR\b|\bAND\b).*(\bOR\b|\bAND\b)/i
|
||||
];
|
||||
|
||||
const checkObject = (obj) => {
|
||||
for (const key in obj) {
|
||||
if (typeof obj[key] === 'string') {
|
||||
for (const pattern of dangerousPatterns) {
|
||||
if (pattern.test(obj[key])) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '检测到潜在的安全威胁'
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (typeof obj[key] === 'object' && obj[key] !== null) {
|
||||
checkObject(obj[key]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
checkObject(req.query);
|
||||
checkObject(req.body);
|
||||
checkObject(req.params);
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
apiRateLimiter,
|
||||
loginRateLimiter,
|
||||
securityHeaders,
|
||||
inputSanitizer,
|
||||
sessionTimeoutCheck,
|
||||
handleValidationErrors,
|
||||
validateAccountNumber,
|
||||
validateAmount,
|
||||
validateIdCard,
|
||||
validatePhone,
|
||||
validatePassword,
|
||||
validateQueryParams
|
||||
};
|
||||
224
bank-backend/models/Account.js
Normal file
224
bank-backend/models/Account.js
Normal file
@@ -0,0 +1,224 @@
|
||||
/**
|
||||
* 账户模型
|
||||
* @file Account.js
|
||||
* @description 银行账户模型定义
|
||||
*/
|
||||
const { DataTypes } = require('sequelize');
|
||||
const BaseModel = require('./BaseModel');
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
class Account extends BaseModel {
|
||||
/**
|
||||
* 获取账户余额(元)
|
||||
* @returns {String} 格式化后的余额
|
||||
*/
|
||||
getBalanceFormatted() {
|
||||
return this.formatAmount(this.balance);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取可用余额(元)
|
||||
* @returns {String} 格式化后的可用余额
|
||||
*/
|
||||
getAvailableBalanceFormatted() {
|
||||
return this.formatAmount(this.available_balance);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取冻结金额(元)
|
||||
* @returns {String} 格式化后的冻结金额
|
||||
*/
|
||||
getFrozenAmountFormatted() {
|
||||
return this.formatAmount(this.frozen_amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查账户是否可用
|
||||
* @returns {Boolean} 是否可用
|
||||
*/
|
||||
isActive() {
|
||||
return this.status === 'active';
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查余额是否充足
|
||||
* @param {Number} amount 金额(分)
|
||||
* @returns {Boolean} 余额是否充足
|
||||
*/
|
||||
hasSufficientBalance(amount) {
|
||||
return this.available_balance >= amount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 冻结资金
|
||||
* @param {Number} amount 金额(分)
|
||||
* @returns {Promise<Boolean>} 操作结果
|
||||
*/
|
||||
async freezeAmount(amount) {
|
||||
if (!this.hasSufficientBalance(amount)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.available_balance -= amount;
|
||||
this.frozen_amount += amount;
|
||||
await this.save();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解冻资金
|
||||
* @param {Number} amount 金额(分)
|
||||
* @returns {Promise<Boolean>} 操作结果
|
||||
*/
|
||||
async unfreezeAmount(amount) {
|
||||
if (this.frozen_amount < amount) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.available_balance += amount;
|
||||
this.frozen_amount -= amount;
|
||||
await this.save();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 扣减余额
|
||||
* @param {Number} amount 金额(分)
|
||||
* @returns {Promise<Boolean>} 操作结果
|
||||
*/
|
||||
async deductBalance(amount) {
|
||||
if (!this.hasSufficientBalance(amount)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.balance -= amount;
|
||||
this.available_balance -= amount;
|
||||
await this.save();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 增加余额
|
||||
* @param {Number} amount 金额(分)
|
||||
* @returns {Promise<Boolean>} 操作结果
|
||||
*/
|
||||
async addBalance(amount) {
|
||||
this.balance += amount;
|
||||
this.available_balance += amount;
|
||||
await this.save();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取账户交易记录
|
||||
* @param {Object} options 查询选项
|
||||
* @returns {Promise<Array>} 交易记录列表
|
||||
*/
|
||||
async getTransactions(options = {}) {
|
||||
try {
|
||||
const { Transaction } = require('./index');
|
||||
return await Transaction.findAll({
|
||||
where: {
|
||||
account_id: this.id,
|
||||
...options.where
|
||||
},
|
||||
order: [['created_at', 'DESC']],
|
||||
limit: options.limit || 50,
|
||||
...options
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取账户交易记录失败:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化Account模型
|
||||
Account.init({
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
account_number: {
|
||||
type: DataTypes.STRING(20),
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
comment: '账户号码'
|
||||
},
|
||||
user_id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'bank_users',
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
account_type: {
|
||||
type: DataTypes.ENUM('savings', 'checking', 'credit', 'loan'),
|
||||
allowNull: false,
|
||||
defaultValue: 'savings',
|
||||
comment: '账户类型:储蓄、支票、信用卡、贷款'
|
||||
},
|
||||
balance: {
|
||||
type: DataTypes.BIGINT,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
comment: '账户余额(分)'
|
||||
},
|
||||
available_balance: {
|
||||
type: DataTypes.BIGINT,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
comment: '可用余额(分)'
|
||||
},
|
||||
frozen_amount: {
|
||||
type: DataTypes.BIGINT,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
comment: '冻结金额(分)'
|
||||
},
|
||||
currency: {
|
||||
type: DataTypes.STRING(3),
|
||||
allowNull: false,
|
||||
defaultValue: 'CNY',
|
||||
comment: '货币类型'
|
||||
},
|
||||
interest_rate: {
|
||||
type: DataTypes.DECIMAL(5, 4),
|
||||
allowNull: true,
|
||||
comment: '利率(年化)'
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.ENUM('active', 'inactive', 'frozen', 'closed'),
|
||||
allowNull: false,
|
||||
defaultValue: 'active'
|
||||
},
|
||||
opened_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW,
|
||||
comment: '开户时间'
|
||||
},
|
||||
closed_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
comment: '销户时间'
|
||||
},
|
||||
created_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW
|
||||
},
|
||||
updated_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW
|
||||
}
|
||||
}, {
|
||||
sequelize,
|
||||
tableName: 'bank_accounts',
|
||||
modelName: 'Account'
|
||||
});
|
||||
|
||||
module.exports = Account;
|
||||
108
bank-backend/models/BaseModel.js
Normal file
108
bank-backend/models/BaseModel.js
Normal file
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* 基础模型类
|
||||
* @file BaseModel.js
|
||||
* @description 所有模型的基类,提供通用方法
|
||||
*/
|
||||
const { Model } = require('sequelize');
|
||||
|
||||
class BaseModel extends Model {
|
||||
/**
|
||||
* 获取模型的安全信息(排除敏感字段)
|
||||
* @param {Array} excludeFields 要排除的字段
|
||||
* @returns {Object} 安全信息对象
|
||||
*/
|
||||
getSafeInfo(excludeFields = ['password', 'pin', 'secret']) {
|
||||
const data = this.get({ plain: true });
|
||||
excludeFields.forEach(field => {
|
||||
delete data[field];
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为JSON格式
|
||||
* @param {Array} excludeFields 要排除的字段
|
||||
* @returns {Object} JSON对象
|
||||
*/
|
||||
toJSON(excludeFields = ['password', 'pin', 'secret']) {
|
||||
return this.getSafeInfo(excludeFields);
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化金额(分转元)
|
||||
* @param {Number} amount 金额(分)
|
||||
* @returns {String} 格式化后的金额
|
||||
*/
|
||||
formatAmount(amount) {
|
||||
if (amount === null || amount === undefined) return '0.00';
|
||||
return (amount / 100).toFixed(2);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析金额(元转分)
|
||||
* @param {String|Number} amount 金额(元)
|
||||
* @returns {Number} 金额(分)
|
||||
*/
|
||||
parseAmount(amount) {
|
||||
if (typeof amount === 'string') {
|
||||
return Math.round(parseFloat(amount) * 100);
|
||||
}
|
||||
return Math.round(amount * 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化日期
|
||||
* @param {Date} date 日期
|
||||
* @param {String} format 格式
|
||||
* @returns {String} 格式化后的日期
|
||||
*/
|
||||
formatDate(date, format = 'YYYY-MM-DD HH:mm:ss') {
|
||||
if (!date) return null;
|
||||
const moment = require('moment');
|
||||
return moment(date).format(format);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查字段是否已更改
|
||||
* @param {String} field 字段名
|
||||
* @returns {Boolean} 是否已更改
|
||||
*/
|
||||
isFieldChanged(field) {
|
||||
return this.changed(field);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取原始值
|
||||
* @param {String} field 字段名
|
||||
* @returns {*} 原始值
|
||||
*/
|
||||
getOriginalValue(field) {
|
||||
return this._previousDataValues[field];
|
||||
}
|
||||
|
||||
/**
|
||||
* 软删除(如果模型支持)
|
||||
*/
|
||||
async softDelete() {
|
||||
if (this.constructor.rawAttributes.deleted_at) {
|
||||
this.deleted_at = new Date();
|
||||
await this.save();
|
||||
} else {
|
||||
throw new Error('模型不支持软删除');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复软删除(如果模型支持)
|
||||
*/
|
||||
async restore() {
|
||||
if (this.constructor.rawAttributes.deleted_at) {
|
||||
this.deleted_at = null;
|
||||
await this.save();
|
||||
} else {
|
||||
throw new Error('模型不支持软删除');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BaseModel;
|
||||
113
bank-backend/models/Role.js
Normal file
113
bank-backend/models/Role.js
Normal file
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* 角色模型
|
||||
* @file Role.js
|
||||
* @description 银行系统角色模型定义
|
||||
*/
|
||||
const { DataTypes } = require('sequelize');
|
||||
const BaseModel = require('./BaseModel');
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
class Role extends BaseModel {
|
||||
/**
|
||||
* 获取角色权限
|
||||
* @returns {Promise<Array>} 权限列表
|
||||
*/
|
||||
async getPermissions() {
|
||||
try {
|
||||
const { Permission } = require('./index');
|
||||
const rolePermissions = await this.getPermissions();
|
||||
return rolePermissions.map(rp => rp.Permission);
|
||||
} catch (error) {
|
||||
console.error('获取角色权限失败:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查角色是否具有指定权限
|
||||
* @param {String|Array} permissionName 权限名称或权限名称数组
|
||||
* @returns {Promise<Boolean>} 检查结果
|
||||
*/
|
||||
async hasPermission(permissionName) {
|
||||
const permissions = await this.getPermissions();
|
||||
const permissionNames = permissions.map(permission => permission.name);
|
||||
|
||||
if (Array.isArray(permissionName)) {
|
||||
return permissionName.some(name => permissionNames.includes(name));
|
||||
}
|
||||
|
||||
return permissionNames.includes(permissionName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取角色用户列表
|
||||
* @returns {Promise<Array>} 用户列表
|
||||
*/
|
||||
async getUsers() {
|
||||
try {
|
||||
const { User } = require('./index');
|
||||
return await User.findAll({
|
||||
where: { role_id: this.id }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取角色用户失败:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化Role模型
|
||||
Role.init({
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
validate: {
|
||||
len: [2, 50]
|
||||
}
|
||||
},
|
||||
display_name: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: false
|
||||
},
|
||||
description: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true
|
||||
},
|
||||
level: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 1,
|
||||
comment: '角色级别,数字越大权限越高'
|
||||
},
|
||||
is_system: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false,
|
||||
comment: '是否为系统角色'
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.ENUM('active', 'inactive'),
|
||||
defaultValue: 'active'
|
||||
},
|
||||
created_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW
|
||||
},
|
||||
updated_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW
|
||||
}
|
||||
}, {
|
||||
sequelize,
|
||||
tableName: 'bank_roles',
|
||||
modelName: 'Role'
|
||||
});
|
||||
|
||||
module.exports = Role;
|
||||
210
bank-backend/models/Transaction.js
Normal file
210
bank-backend/models/Transaction.js
Normal file
@@ -0,0 +1,210 @@
|
||||
/**
|
||||
* 交易记录模型
|
||||
* @file Transaction.js
|
||||
* @description 银行交易记录模型定义
|
||||
*/
|
||||
const { DataTypes } = require('sequelize');
|
||||
const BaseModel = require('./BaseModel');
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
class Transaction extends BaseModel {
|
||||
/**
|
||||
* 获取交易金额(元)
|
||||
* @returns {String} 格式化后的金额
|
||||
*/
|
||||
getAmountFormatted() {
|
||||
return this.formatAmount(this.amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取交易后余额(元)
|
||||
* @returns {String} 格式化后的余额
|
||||
*/
|
||||
getBalanceAfterFormatted() {
|
||||
return this.formatAmount(this.balance_after);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为收入交易
|
||||
* @returns {Boolean} 是否为收入
|
||||
*/
|
||||
isIncome() {
|
||||
return this.transaction_type === 'deposit' ||
|
||||
this.transaction_type === 'transfer_in' ||
|
||||
this.transaction_type === 'interest';
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为支出交易
|
||||
* @returns {Boolean} 是否为支出
|
||||
*/
|
||||
isExpense() {
|
||||
return this.transaction_type === 'withdrawal' ||
|
||||
this.transaction_type === 'transfer_out' ||
|
||||
this.transaction_type === 'fee';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取交易状态描述
|
||||
* @returns {String} 状态描述
|
||||
*/
|
||||
getStatusDescription() {
|
||||
const statusMap = {
|
||||
'pending': '处理中',
|
||||
'completed': '已完成',
|
||||
'failed': '失败',
|
||||
'cancelled': '已取消',
|
||||
'reversed': '已冲正'
|
||||
};
|
||||
return statusMap[this.status] || '未知状态';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取交易类型描述
|
||||
* @returns {String} 类型描述
|
||||
*/
|
||||
getTypeDescription() {
|
||||
const typeMap = {
|
||||
'deposit': '存款',
|
||||
'withdrawal': '取款',
|
||||
'transfer_in': '转入',
|
||||
'transfer_out': '转出',
|
||||
'interest': '利息',
|
||||
'fee': '手续费',
|
||||
'loan': '贷款',
|
||||
'repayment': '还款'
|
||||
};
|
||||
return typeMap[this.transaction_type] || '未知类型';
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查交易是否可以撤销
|
||||
* @returns {Boolean} 是否可以撤销
|
||||
*/
|
||||
canReverse() {
|
||||
return this.status === 'completed' &&
|
||||
this.transaction_type !== 'fee' &&
|
||||
this.created_at > new Date(Date.now() - 24 * 60 * 60 * 1000); // 24小时内
|
||||
}
|
||||
|
||||
/**
|
||||
* 撤销交易
|
||||
* @returns {Promise<Boolean>} 操作结果
|
||||
*/
|
||||
async reverse() {
|
||||
if (!this.canReverse()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// 更新交易状态
|
||||
this.status = 'reversed';
|
||||
this.reversed_at = new Date();
|
||||
await this.save();
|
||||
|
||||
// 这里应该创建反向交易记录
|
||||
// 实际实现中需要更复杂的逻辑
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('撤销交易失败:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化Transaction模型
|
||||
Transaction.init({
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
transaction_number: {
|
||||
type: DataTypes.STRING(32),
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
comment: '交易流水号'
|
||||
},
|
||||
account_id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'bank_accounts',
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
transaction_type: {
|
||||
type: DataTypes.ENUM(
|
||||
'deposit', 'withdrawal', 'transfer_in', 'transfer_out',
|
||||
'interest', 'fee', 'loan', 'repayment'
|
||||
),
|
||||
allowNull: false,
|
||||
comment: '交易类型'
|
||||
},
|
||||
amount: {
|
||||
type: DataTypes.BIGINT,
|
||||
allowNull: false,
|
||||
comment: '交易金额(分)'
|
||||
},
|
||||
balance_before: {
|
||||
type: DataTypes.BIGINT,
|
||||
allowNull: false,
|
||||
comment: '交易前余额(分)'
|
||||
},
|
||||
balance_after: {
|
||||
type: DataTypes.BIGINT,
|
||||
allowNull: false,
|
||||
comment: '交易后余额(分)'
|
||||
},
|
||||
counterparty_account: {
|
||||
type: DataTypes.STRING(20),
|
||||
allowNull: true,
|
||||
comment: '对方账户号'
|
||||
},
|
||||
counterparty_name: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: true,
|
||||
comment: '对方户名'
|
||||
},
|
||||
description: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: true,
|
||||
comment: '交易描述'
|
||||
},
|
||||
reference_number: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: true,
|
||||
comment: '参考号'
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.ENUM('pending', 'completed', 'failed', 'cancelled', 'reversed'),
|
||||
allowNull: false,
|
||||
defaultValue: 'pending'
|
||||
},
|
||||
processed_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
comment: '处理时间'
|
||||
},
|
||||
reversed_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
comment: '撤销时间'
|
||||
},
|
||||
created_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW
|
||||
},
|
||||
updated_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW
|
||||
}
|
||||
}, {
|
||||
sequelize,
|
||||
tableName: 'bank_transactions',
|
||||
modelName: 'Transaction'
|
||||
});
|
||||
|
||||
module.exports = Transaction;
|
||||
183
bank-backend/models/User.js
Normal file
183
bank-backend/models/User.js
Normal file
@@ -0,0 +1,183 @@
|
||||
/**
|
||||
* 用户模型
|
||||
* @file User.js
|
||||
* @description 银行系统用户模型定义
|
||||
*/
|
||||
const { DataTypes } = require('sequelize');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const BaseModel = require('./BaseModel');
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
class User extends BaseModel {
|
||||
/**
|
||||
* 验证密码
|
||||
* @param {String} password 待验证的密码
|
||||
* @returns {Promise<Boolean>} 验证结果
|
||||
*/
|
||||
async validPassword(password) {
|
||||
return await bcrypt.compare(password, this.password);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户角色
|
||||
* @returns {Promise<Array>} 用户角色列表
|
||||
*/
|
||||
async getRoles() {
|
||||
try {
|
||||
const { Role } = require('./index');
|
||||
const role = await Role.findByPk(this.role_id);
|
||||
return role ? [role] : [];
|
||||
} catch (error) {
|
||||
console.error('获取用户角色失败:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否具有指定角色
|
||||
* @param {String|Array} roleName 角色名称或角色名称数组
|
||||
* @returns {Promise<Boolean>} 检查结果
|
||||
*/
|
||||
async hasRole(roleName) {
|
||||
const roles = await this.getRoles();
|
||||
const roleNames = roles.map(role => role.name);
|
||||
|
||||
if (Array.isArray(roleName)) {
|
||||
return roleName.some(name => roleNames.includes(name));
|
||||
}
|
||||
|
||||
return roleNames.includes(roleName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户账户列表
|
||||
* @returns {Promise<Array>} 账户列表
|
||||
*/
|
||||
async getAccounts() {
|
||||
try {
|
||||
const { Account } = require('./index');
|
||||
return await Account.findAll({
|
||||
where: { user_id: this.id, status: 'active' }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取用户账户失败:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户安全信息(不包含密码)
|
||||
* @returns {Object} 用户安全信息
|
||||
*/
|
||||
getSafeInfo() {
|
||||
return super.getSafeInfo(['password', 'pin']);
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化User模型
|
||||
User.init({
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
username: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
validate: {
|
||||
len: [3, 50]
|
||||
}
|
||||
},
|
||||
email: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
validate: {
|
||||
isEmail: true
|
||||
}
|
||||
},
|
||||
password: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: false,
|
||||
validate: {
|
||||
len: [6, 255]
|
||||
}
|
||||
},
|
||||
phone: {
|
||||
type: DataTypes.STRING(20),
|
||||
allowNull: true,
|
||||
validate: {
|
||||
is: /^1[3-9]\d{9}$/
|
||||
}
|
||||
},
|
||||
real_name: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false
|
||||
},
|
||||
id_card: {
|
||||
type: DataTypes.STRING(18),
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
validate: {
|
||||
is: /^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/
|
||||
}
|
||||
},
|
||||
avatar: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: true
|
||||
},
|
||||
role_id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 2, // 默认为普通用户角色ID
|
||||
references: {
|
||||
model: 'bank_roles',
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.ENUM('active', 'inactive', 'suspended', 'locked'),
|
||||
defaultValue: 'active'
|
||||
},
|
||||
last_login: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true
|
||||
},
|
||||
login_attempts: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 0
|
||||
},
|
||||
locked_until: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true
|
||||
},
|
||||
created_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW
|
||||
},
|
||||
updated_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW
|
||||
}
|
||||
}, {
|
||||
sequelize,
|
||||
tableName: 'bank_users',
|
||||
modelName: 'User',
|
||||
hooks: {
|
||||
beforeCreate: async (user) => {
|
||||
if (user.password) {
|
||||
user.password = await bcrypt.hash(user.password, 10);
|
||||
}
|
||||
},
|
||||
beforeUpdate: async (user) => {
|
||||
if (user.changed('password')) {
|
||||
user.password = await bcrypt.hash(user.password, 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = User;
|
||||
60
bank-backend/models/index.js
Normal file
60
bank-backend/models/index.js
Normal file
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* 模型索引文件
|
||||
* @file index.js
|
||||
* @description 导出所有模型并建立关联关系
|
||||
*/
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
// 导入所有模型
|
||||
const User = require('./User');
|
||||
const Role = require('./Role');
|
||||
const Account = require('./Account');
|
||||
const Transaction = require('./Transaction');
|
||||
|
||||
// 定义模型关联关系
|
||||
|
||||
// 用户与角色关联
|
||||
User.belongsTo(Role, {
|
||||
foreignKey: 'role_id',
|
||||
as: 'role',
|
||||
targetKey: 'id'
|
||||
});
|
||||
|
||||
Role.hasMany(User, {
|
||||
foreignKey: 'role_id',
|
||||
as: 'users'
|
||||
});
|
||||
|
||||
// 用户与账户关联
|
||||
User.hasMany(Account, {
|
||||
foreignKey: 'user_id',
|
||||
as: 'accounts'
|
||||
});
|
||||
|
||||
Account.belongsTo(User, {
|
||||
foreignKey: 'user_id',
|
||||
as: 'user'
|
||||
});
|
||||
|
||||
// 账户与交易记录关联
|
||||
Account.hasMany(Transaction, {
|
||||
foreignKey: 'account_id',
|
||||
as: 'transactions'
|
||||
});
|
||||
|
||||
Transaction.belongsTo(Account, {
|
||||
foreignKey: 'account_id',
|
||||
as: 'account'
|
||||
});
|
||||
|
||||
// 交易记录与用户关联(通过账户)
|
||||
// 移除不合理的Transaction->User through Account的belongsTo定义,避免错误外键映射
|
||||
|
||||
// 导出所有模型和数据库实例
|
||||
module.exports = {
|
||||
sequelize,
|
||||
User,
|
||||
Role,
|
||||
Account,
|
||||
Transaction
|
||||
};
|
||||
10667
bank-backend/package-lock.json
generated
Normal file
10667
bank-backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
114
bank-backend/package.json
Normal file
114
bank-backend/package.json
Normal file
@@ -0,0 +1,114 @@
|
||||
{
|
||||
"name": "bank-management-backend",
|
||||
"version": "1.0.0",
|
||||
"description": "银行管理后台系统后端API服务",
|
||||
"main": "server.js",
|
||||
"author": "Bank Development Team",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"nodejs",
|
||||
"express",
|
||||
"sequelize",
|
||||
"mysql",
|
||||
"api",
|
||||
"banking",
|
||||
"financial",
|
||||
"management"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=16.0.0",
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"dev": "nodemon server.js",
|
||||
"init-db": "node scripts/init-db.js",
|
||||
"test-connection": "node scripts/test-connection.js",
|
||||
"migrate": "node scripts/migration-manager.js",
|
||||
"seed": "node scripts/seed-manager.js",
|
||||
"backup": "node scripts/backup-db.js",
|
||||
"restore": "node scripts/restore-db.js",
|
||||
"lint": "eslint . --ext .js",
|
||||
"lint:fix": "eslint . --ext .js --fix",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:coverage": "jest --coverage",
|
||||
"build": "echo 'No build step required for Node.js backend'",
|
||||
"clean": "node -e \"const fs = require('fs'); const path = require('path'); try { const logDir = 'logs'; const tempDir = 'uploads/temp'; if (fs.existsSync(logDir)) { fs.readdirSync(logDir).forEach(file => { if (file.endsWith('.log')) fs.unlinkSync(path.join(logDir, file)); }); } if (fs.existsSync(tempDir)) { fs.rmSync(tempDir, { recursive: true, force: true }); } console.log('✅ Cleanup completed'); } catch (err) { console.error('❌ Cleanup failed:', err.message); }\"",
|
||||
"health-check": "node -e \"const { sequelize } = require('./config/database'); sequelize.authenticate().then(() => { console.log('✅ Database connection healthy'); process.exit(0); }).catch(err => { console.error('❌ Database connection failed:', err.message); process.exit(1); });\""
|
||||
},
|
||||
"dependencies": {
|
||||
"archiver": "^6.0.1",
|
||||
"axios": "^1.6.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"compression": "^1.7.4",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"ejs": "^3.1.9",
|
||||
"express": "^4.18.2",
|
||||
"express-rate-limit": "^7.1.5",
|
||||
"express-validator": "^7.0.1",
|
||||
"helmet": "^7.1.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"moment": "^2.29.4",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"mysql2": "^3.6.5",
|
||||
"node-cron": "^3.0.3",
|
||||
"nodemailer": "^6.9.8",
|
||||
"redis": "^4.6.12",
|
||||
"sequelize": "^6.35.2",
|
||||
"sharp": "^0.33.2",
|
||||
"socket.io": "^4.7.4",
|
||||
"swagger-jsdoc": "^6.2.8",
|
||||
"swagger-ui-express": "^5.0.0",
|
||||
"winston": "^3.11.0",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.2",
|
||||
"eslint": "^8.55.0",
|
||||
"eslint-config-standard": "^17.1.0",
|
||||
"eslint-plugin-import": "^2.29.0",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"jest": "^29.7.0",
|
||||
"supertest": "^6.3.3",
|
||||
"rimraf": "^5.0.5",
|
||||
"@types/jest": "^29.5.8"
|
||||
},
|
||||
"jest": {
|
||||
"testEnvironment": "node",
|
||||
"collectCoverageFrom": [
|
||||
"controllers/**/*.js",
|
||||
"models/**/*.js",
|
||||
"routes/**/*.js",
|
||||
"utils/**/*.js",
|
||||
"!**/node_modules/**",
|
||||
"!**/migrations/**",
|
||||
"!**/seeds/**"
|
||||
],
|
||||
"coverageDirectory": "coverage",
|
||||
"coverageReporters": ["text", "lcov", "html"]
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": ["standard"],
|
||||
"env": {
|
||||
"node": true,
|
||||
"es2021": true,
|
||||
"jest": true
|
||||
},
|
||||
"rules": {
|
||||
"no-console": "warn",
|
||||
"no-unused-vars": "error",
|
||||
"prefer-const": "error"
|
||||
}
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/bank-management/bank-backend.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/bank-management/bank-backend/issues"
|
||||
},
|
||||
"homepage": "https://github.com/bank-management/bank-backend#readme"
|
||||
}
|
||||
322
bank-backend/routes/accounts.js
Normal file
322
bank-backend/routes/accounts.js
Normal file
@@ -0,0 +1,322 @@
|
||||
const express = require('express');
|
||||
const { verifyToken, requireRole, checkAccountOwnership } = require('../middleware/auth');
|
||||
const {
|
||||
validateAccountNumber,
|
||||
validateAmount,
|
||||
handleValidationErrors
|
||||
} = require('../middleware/security');
|
||||
const router = express.Router();
|
||||
const accountController = require('../controllers/accountController');
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* tags:
|
||||
* name: Accounts
|
||||
* description: 账户管理
|
||||
*/
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* components:
|
||||
* schemas:
|
||||
* Account:
|
||||
* type: object
|
||||
* required:
|
||||
* - user_id
|
||||
* - account_type
|
||||
* properties:
|
||||
* id:
|
||||
* type: integer
|
||||
* description: 账户ID
|
||||
* account_number:
|
||||
* type: string
|
||||
* description: 账户号码
|
||||
* user_id:
|
||||
* type: integer
|
||||
* description: 用户ID
|
||||
* account_type:
|
||||
* type: string
|
||||
* enum: [savings, checking, credit, loan]
|
||||
* description: 账户类型
|
||||
* balance:
|
||||
* type: integer
|
||||
* description: 账户余额(分)
|
||||
* available_balance:
|
||||
* type: integer
|
||||
* description: 可用余额(分)
|
||||
* frozen_amount:
|
||||
* type: integer
|
||||
* description: 冻结金额(分)
|
||||
* status:
|
||||
* type: string
|
||||
* enum: [active, inactive, frozen, closed]
|
||||
* description: 账户状态
|
||||
*/
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/accounts:
|
||||
* post:
|
||||
* summary: 创建账户
|
||||
* tags: [Accounts]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - user_id
|
||||
* - account_type
|
||||
* properties:
|
||||
* user_id:
|
||||
* type: integer
|
||||
* description: 用户ID
|
||||
* account_type:
|
||||
* type: string
|
||||
* enum: [savings, checking, credit, loan]
|
||||
* description: 账户类型
|
||||
* initial_balance:
|
||||
* type: number
|
||||
* description: 初始余额(元)
|
||||
* responses:
|
||||
* 201:
|
||||
* description: 创建成功
|
||||
* 400:
|
||||
* description: 输入数据验证失败
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 403:
|
||||
* description: 权限不足
|
||||
*/
|
||||
router.post('/',
|
||||
verifyToken,
|
||||
requireRole(['admin', 'manager']),
|
||||
accountController.createAccount
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/accounts:
|
||||
* get:
|
||||
* summary: 获取账户列表
|
||||
* tags: [Accounts]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: page
|
||||
* schema:
|
||||
* type: integer
|
||||
* default: 1
|
||||
* description: 页码
|
||||
* - in: query
|
||||
* name: limit
|
||||
* schema:
|
||||
* type: integer
|
||||
* default: 10
|
||||
* description: 每页数量
|
||||
* - in: query
|
||||
* name: user_id
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: 用户ID(管理员)
|
||||
* - in: query
|
||||
* name: account_type
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [savings, checking, credit, loan]
|
||||
* description: 账户类型
|
||||
* - in: query
|
||||
* name: status
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [active, inactive, frozen, closed]
|
||||
* description: 账户状态
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 获取成功
|
||||
* 401:
|
||||
* description: 未授权
|
||||
*/
|
||||
router.get('/',
|
||||
verifyToken,
|
||||
accountController.getAccounts
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/accounts/{accountId}:
|
||||
* get:
|
||||
* summary: 获取账户详情
|
||||
* tags: [Accounts]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: accountId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: 账户ID
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 获取成功
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 403:
|
||||
* description: 权限不足
|
||||
* 404:
|
||||
* description: 账户不存在
|
||||
*/
|
||||
router.get('/:accountId',
|
||||
verifyToken,
|
||||
checkAccountOwnership,
|
||||
accountController.getAccountDetail
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/accounts/{accountId}/status:
|
||||
* put:
|
||||
* summary: 更新账户状态
|
||||
* tags: [Accounts]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: accountId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: 账户ID
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - status
|
||||
* properties:
|
||||
* status:
|
||||
* type: string
|
||||
* enum: [active, inactive, frozen, closed]
|
||||
* description: 账户状态
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 更新成功
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 403:
|
||||
* description: 权限不足
|
||||
* 404:
|
||||
* description: 账户不存在
|
||||
*/
|
||||
router.put('/:accountId/status',
|
||||
verifyToken,
|
||||
requireRole(['admin', 'manager']),
|
||||
accountController.updateAccountStatus
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/accounts/{accountId}/deposit:
|
||||
* post:
|
||||
* summary: 存款
|
||||
* tags: [Accounts]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: accountId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: 账户ID
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - amount
|
||||
* properties:
|
||||
* amount:
|
||||
* type: number
|
||||
* description: 存款金额(元)
|
||||
* description:
|
||||
* type: string
|
||||
* description: 交易描述
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 存款成功
|
||||
* 400:
|
||||
* description: 输入数据验证失败或账户状态异常
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 403:
|
||||
* description: 权限不足
|
||||
* 404:
|
||||
* description: 账户不存在
|
||||
*/
|
||||
router.post('/:accountId/deposit',
|
||||
verifyToken,
|
||||
requireRole(['admin', 'manager', 'teller']),
|
||||
validateAmount,
|
||||
accountController.deposit
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/accounts/{accountId}/withdraw:
|
||||
* post:
|
||||
* summary: 取款
|
||||
* tags: [Accounts]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: accountId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: 账户ID
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - amount
|
||||
* properties:
|
||||
* amount:
|
||||
* type: number
|
||||
* description: 取款金额(元)
|
||||
* description:
|
||||
* type: string
|
||||
* description: 交易描述
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 取款成功
|
||||
* 400:
|
||||
* description: 输入数据验证失败、账户状态异常或余额不足
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 403:
|
||||
* description: 权限不足
|
||||
* 404:
|
||||
* description: 账户不存在
|
||||
*/
|
||||
router.post('/:accountId/withdraw',
|
||||
verifyToken,
|
||||
requireRole(['admin', 'manager', 'teller']),
|
||||
validateAmount,
|
||||
accountController.withdraw
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
287
bank-backend/routes/transactions.js
Normal file
287
bank-backend/routes/transactions.js
Normal file
@@ -0,0 +1,287 @@
|
||||
const express = require('express');
|
||||
const { verifyToken, requireRole } = require('../middleware/auth');
|
||||
const {
|
||||
validateAmount,
|
||||
validateAccountNumber,
|
||||
handleValidationErrors
|
||||
} = require('../middleware/security');
|
||||
const router = express.Router();
|
||||
const transactionController = require('../controllers/transactionController');
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* tags:
|
||||
* name: Transactions
|
||||
* description: 交易管理
|
||||
*/
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* components:
|
||||
* schemas:
|
||||
* Transaction:
|
||||
* type: object
|
||||
* required:
|
||||
* - account_id
|
||||
* - transaction_type
|
||||
* - amount
|
||||
* properties:
|
||||
* id:
|
||||
* type: integer
|
||||
* description: 交易ID
|
||||
* transaction_number:
|
||||
* type: string
|
||||
* description: 交易流水号
|
||||
* account_id:
|
||||
* type: integer
|
||||
* description: 账户ID
|
||||
* transaction_type:
|
||||
* type: string
|
||||
* enum: [deposit, withdrawal, transfer_in, transfer_out, interest, fee, loan, repayment]
|
||||
* description: 交易类型
|
||||
* amount:
|
||||
* type: integer
|
||||
* description: 交易金额(分)
|
||||
* balance_before:
|
||||
* type: integer
|
||||
* description: 交易前余额(分)
|
||||
* balance_after:
|
||||
* type: integer
|
||||
* description: 交易后余额(分)
|
||||
* counterparty_account:
|
||||
* type: string
|
||||
* description: 对方账户号
|
||||
* counterparty_name:
|
||||
* type: string
|
||||
* description: 对方户名
|
||||
* description:
|
||||
* type: string
|
||||
* description: 交易描述
|
||||
* status:
|
||||
* type: string
|
||||
* enum: [pending, completed, failed, cancelled, reversed]
|
||||
* description: 交易状态
|
||||
*/
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/transactions:
|
||||
* get:
|
||||
* summary: 获取交易记录列表
|
||||
* tags: [Transactions]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: page
|
||||
* schema:
|
||||
* type: integer
|
||||
* default: 1
|
||||
* description: 页码
|
||||
* - in: query
|
||||
* name: limit
|
||||
* schema:
|
||||
* type: integer
|
||||
* default: 20
|
||||
* description: 每页数量
|
||||
* - in: query
|
||||
* name: account_id
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: 账户ID(管理员)
|
||||
* - in: query
|
||||
* name: transaction_type
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [deposit, withdrawal, transfer_in, transfer_out, interest, fee, loan, repayment]
|
||||
* description: 交易类型
|
||||
* - in: query
|
||||
* name: status
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [pending, completed, failed, cancelled, reversed]
|
||||
* description: 交易状态
|
||||
* - in: query
|
||||
* name: start_date
|
||||
* schema:
|
||||
* type: string
|
||||
* format: date
|
||||
* description: 开始日期
|
||||
* - in: query
|
||||
* name: end_date
|
||||
* schema:
|
||||
* type: string
|
||||
* format: date
|
||||
* description: 结束日期
|
||||
* - in: query
|
||||
* name: amount_min
|
||||
* schema:
|
||||
* type: number
|
||||
* description: 最小金额(元)
|
||||
* - in: query
|
||||
* name: amount_max
|
||||
* schema:
|
||||
* type: number
|
||||
* description: 最大金额(元)
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 获取成功
|
||||
* 401:
|
||||
* description: 未授权
|
||||
*/
|
||||
router.get('/',
|
||||
verifyToken,
|
||||
transactionController.getTransactions
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/transactions/{transactionId}:
|
||||
* get:
|
||||
* summary: 获取交易详情
|
||||
* tags: [Transactions]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: transactionId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: 交易ID
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 获取成功
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 403:
|
||||
* description: 权限不足
|
||||
* 404:
|
||||
* description: 交易记录不存在
|
||||
*/
|
||||
router.get('/:transactionId',
|
||||
verifyToken,
|
||||
transactionController.getTransactionDetail
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/transactions/transfer:
|
||||
* post:
|
||||
* summary: 转账
|
||||
* tags: [Transactions]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - from_account_id
|
||||
* - to_account_number
|
||||
* - amount
|
||||
* properties:
|
||||
* from_account_id:
|
||||
* type: integer
|
||||
* description: 转出账户ID
|
||||
* to_account_number:
|
||||
* type: string
|
||||
* description: 转入账户号码
|
||||
* amount:
|
||||
* type: number
|
||||
* description: 转账金额(元)
|
||||
* description:
|
||||
* type: string
|
||||
* description: 转账描述
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 转账成功
|
||||
* 400:
|
||||
* description: 输入数据验证失败、账户状态异常或余额不足
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 403:
|
||||
* description: 权限不足
|
||||
* 404:
|
||||
* description: 账户不存在
|
||||
*/
|
||||
router.post('/transfer',
|
||||
verifyToken,
|
||||
validateAmount,
|
||||
validateAccountNumber,
|
||||
transactionController.transfer
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/transactions/{transactionId}/reverse:
|
||||
* post:
|
||||
* summary: 撤销交易
|
||||
* tags: [Transactions]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: transactionId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: 交易ID
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 撤销成功
|
||||
* 400:
|
||||
* description: 该交易无法撤销
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 403:
|
||||
* description: 权限不足
|
||||
* 404:
|
||||
* description: 交易记录不存在
|
||||
*/
|
||||
router.post('/:transactionId/reverse',
|
||||
verifyToken,
|
||||
requireRole(['admin', 'manager']),
|
||||
transactionController.reverseTransaction
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/transactions/stats:
|
||||
* get:
|
||||
* summary: 获取交易统计
|
||||
* tags: [Transactions]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: start_date
|
||||
* schema:
|
||||
* type: string
|
||||
* format: date
|
||||
* description: 开始日期
|
||||
* - in: query
|
||||
* name: end_date
|
||||
* schema:
|
||||
* type: string
|
||||
* format: date
|
||||
* description: 结束日期
|
||||
* - in: query
|
||||
* name: account_id
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: 账户ID(管理员)
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 获取成功
|
||||
* 401:
|
||||
* description: 未授权
|
||||
*/
|
||||
router.get('/stats',
|
||||
verifyToken,
|
||||
transactionController.getTransactionStats
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
348
bank-backend/routes/users.js
Normal file
348
bank-backend/routes/users.js
Normal file
@@ -0,0 +1,348 @@
|
||||
const express = require('express');
|
||||
const { verifyToken, requireRole, requireLevel } = require('../middleware/auth');
|
||||
const {
|
||||
validatePhone,
|
||||
validatePassword,
|
||||
validateIdCard,
|
||||
handleValidationErrors
|
||||
} = require('../middleware/security');
|
||||
const router = express.Router();
|
||||
const userController = require('../controllers/userController');
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* tags:
|
||||
* name: Users
|
||||
* description: 用户管理
|
||||
*/
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* components:
|
||||
* schemas:
|
||||
* User:
|
||||
* type: object
|
||||
* required:
|
||||
* - username
|
||||
* - email
|
||||
* - password
|
||||
* - real_name
|
||||
* - id_card
|
||||
* properties:
|
||||
* id:
|
||||
* type: integer
|
||||
* description: 用户ID
|
||||
* username:
|
||||
* type: string
|
||||
* description: 用户名
|
||||
* email:
|
||||
* type: string
|
||||
* format: email
|
||||
* description: 邮箱地址
|
||||
* real_name:
|
||||
* type: string
|
||||
* description: 真实姓名
|
||||
* id_card:
|
||||
* type: string
|
||||
* description: 身份证号
|
||||
* phone:
|
||||
* type: string
|
||||
* description: 手机号
|
||||
* status:
|
||||
* type: string
|
||||
* enum: [active, inactive, suspended, locked]
|
||||
* description: 用户状态
|
||||
*/
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/users/register:
|
||||
* post:
|
||||
* summary: 用户注册
|
||||
* tags: [Users]
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - username
|
||||
* - email
|
||||
* - password
|
||||
* - real_name
|
||||
* - id_card
|
||||
* properties:
|
||||
* username:
|
||||
* type: string
|
||||
* description: 用户名
|
||||
* email:
|
||||
* type: string
|
||||
* format: email
|
||||
* description: 邮箱地址
|
||||
* password:
|
||||
* type: string
|
||||
* description: 密码
|
||||
* real_name:
|
||||
* type: string
|
||||
* description: 真实姓名
|
||||
* id_card:
|
||||
* type: string
|
||||
* description: 身份证号
|
||||
* phone:
|
||||
* type: string
|
||||
* description: 手机号
|
||||
* responses:
|
||||
* 201:
|
||||
* description: 注册成功
|
||||
* 400:
|
||||
* description: 输入数据验证失败
|
||||
* 500:
|
||||
* description: 服务器内部错误
|
||||
*/
|
||||
router.post('/register',
|
||||
validatePassword,
|
||||
validateIdCard,
|
||||
validatePhone,
|
||||
userController.register
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/users/login:
|
||||
* post:
|
||||
* summary: 用户登录
|
||||
* tags: [Users]
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - username
|
||||
* - password
|
||||
* properties:
|
||||
* username:
|
||||
* type: string
|
||||
* description: 用户名
|
||||
* password:
|
||||
* type: string
|
||||
* description: 密码
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 登录成功
|
||||
* 401:
|
||||
* description: 用户名或密码错误
|
||||
* 500:
|
||||
* description: 服务器内部错误
|
||||
*/
|
||||
router.post('/login', userController.login);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/users/profile:
|
||||
* get:
|
||||
* summary: 获取用户信息
|
||||
* tags: [Users]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 获取成功
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 404:
|
||||
* description: 用户不存在
|
||||
*/
|
||||
router.get('/profile', verifyToken, userController.getProfile);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/users/profile:
|
||||
* put:
|
||||
* summary: 更新用户信息
|
||||
* tags: [Users]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* phone:
|
||||
* type: string
|
||||
* description: 手机号
|
||||
* real_name:
|
||||
* type: string
|
||||
* description: 真实姓名
|
||||
* avatar:
|
||||
* type: string
|
||||
* description: 头像URL
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 更新成功
|
||||
* 400:
|
||||
* description: 输入数据验证失败
|
||||
* 401:
|
||||
* description: 未授权
|
||||
*/
|
||||
router.put('/profile',
|
||||
verifyToken,
|
||||
validatePhone,
|
||||
userController.updateProfile
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/users/change-password:
|
||||
* put:
|
||||
* summary: 修改密码
|
||||
* tags: [Users]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - old_password
|
||||
* - new_password
|
||||
* properties:
|
||||
* old_password:
|
||||
* type: string
|
||||
* description: 原密码
|
||||
* new_password:
|
||||
* type: string
|
||||
* description: 新密码
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 修改成功
|
||||
* 400:
|
||||
* description: 原密码错误
|
||||
* 401:
|
||||
* description: 未授权
|
||||
*/
|
||||
router.put('/change-password',
|
||||
verifyToken,
|
||||
validatePassword,
|
||||
userController.changePassword
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/users:
|
||||
* get:
|
||||
* summary: 获取用户列表(管理员)
|
||||
* tags: [Users]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: page
|
||||
* schema:
|
||||
* type: integer
|
||||
* default: 1
|
||||
* description: 页码
|
||||
* - in: query
|
||||
* name: limit
|
||||
* schema:
|
||||
* type: integer
|
||||
* default: 10
|
||||
* description: 每页数量
|
||||
* - in: query
|
||||
* name: search
|
||||
* schema:
|
||||
* type: string
|
||||
* description: 搜索关键词
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 获取成功
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 403:
|
||||
* description: 权限不足
|
||||
*/
|
||||
router.get('/',
|
||||
verifyToken,
|
||||
requireRole('admin'),
|
||||
userController.getUsers
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/users/{userId}/status:
|
||||
* put:
|
||||
* summary: 更新用户状态(管理员)
|
||||
* tags: [Users]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: userId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: 用户ID
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - status
|
||||
* properties:
|
||||
* status:
|
||||
* type: string
|
||||
* enum: [active, inactive, suspended, locked]
|
||||
* description: 用户状态
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 更新成功
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 403:
|
||||
* description: 权限不足
|
||||
* 404:
|
||||
* description: 用户不存在
|
||||
*/
|
||||
router.put('/:userId/status',
|
||||
verifyToken,
|
||||
requireRole('admin'),
|
||||
userController.updateUserStatus
|
||||
);
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/users/{userId}/accounts:
|
||||
* get:
|
||||
* summary: 获取用户账户列表
|
||||
* tags: [Users]
|
||||
* security:
|
||||
* - bearerAuth: []
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: userId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* description: 用户ID
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 获取成功
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 403:
|
||||
* description: 权限不足
|
||||
*/
|
||||
router.get('/:userId/accounts',
|
||||
verifyToken,
|
||||
userController.getUserAccounts
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
71
bank-backend/scripts/create-bank-schema.sql
Normal file
71
bank-backend/scripts/create-bank-schema.sql
Normal file
@@ -0,0 +1,71 @@
|
||||
-- 创建 bank_ 前缀业务表(无DROP,避免覆盖现有表)
|
||||
|
||||
CREATE TABLE IF NOT EXISTS bank_roles (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(50) NOT NULL UNIQUE,
|
||||
display_name VARCHAR(100) NOT NULL,
|
||||
description TEXT NULL,
|
||||
level INT NOT NULL DEFAULT 1,
|
||||
is_system TINYINT(1) NOT NULL DEFAULT 0,
|
||||
status ENUM('active','inactive') NOT NULL DEFAULT 'active',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS bank_users (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(50) NOT NULL UNIQUE,
|
||||
email VARCHAR(100) NOT NULL UNIQUE,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
phone VARCHAR(20) NULL,
|
||||
real_name VARCHAR(50) NOT NULL,
|
||||
id_card VARCHAR(18) NOT NULL UNIQUE,
|
||||
avatar VARCHAR(255) NULL,
|
||||
role_id INT NOT NULL,
|
||||
status ENUM('active','inactive','suspended','locked') NOT NULL DEFAULT 'active',
|
||||
last_login DATETIME NULL,
|
||||
login_attempts INT NOT NULL DEFAULT 0,
|
||||
locked_until DATETIME NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
CONSTRAINT fk_bank_users_role_id FOREIGN KEY (role_id) REFERENCES bank_roles(id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS bank_accounts (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
account_number VARCHAR(20) NOT NULL UNIQUE,
|
||||
user_id INT NOT NULL,
|
||||
account_type ENUM('savings','checking','credit','loan') NOT NULL DEFAULT 'savings',
|
||||
balance BIGINT NOT NULL DEFAULT 0,
|
||||
available_balance BIGINT NOT NULL DEFAULT 0,
|
||||
frozen_amount BIGINT NOT NULL DEFAULT 0,
|
||||
currency VARCHAR(3) NOT NULL DEFAULT 'CNY',
|
||||
interest_rate DECIMAL(5,4) NULL,
|
||||
status ENUM('active','inactive','frozen','closed') NOT NULL DEFAULT 'active',
|
||||
opened_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
closed_at DATETIME NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
CONSTRAINT fk_bank_accounts_user_id FOREIGN KEY (user_id) REFERENCES bank_users(id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS bank_transactions (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
transaction_number VARCHAR(32) NOT NULL UNIQUE,
|
||||
account_id INT NOT NULL,
|
||||
transaction_type ENUM('deposit','withdrawal','transfer_in','transfer_out','interest','fee','loan','repayment') NOT NULL,
|
||||
amount BIGINT NOT NULL,
|
||||
balance_before BIGINT NOT NULL,
|
||||
balance_after BIGINT NOT NULL,
|
||||
counterparty_account VARCHAR(20) NULL,
|
||||
counterparty_name VARCHAR(100) NULL,
|
||||
description VARCHAR(255) NULL,
|
||||
reference_number VARCHAR(50) NULL,
|
||||
status ENUM('pending','completed','failed','cancelled','reversed') NOT NULL DEFAULT 'pending',
|
||||
processed_at DATETIME NULL,
|
||||
reversed_at DATETIME NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
CONSTRAINT fk_bank_transactions_account_id FOREIGN KEY (account_id) REFERENCES bank_accounts(id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
152
bank-backend/scripts/init-db.js
Normal file
152
bank-backend/scripts/init-db.js
Normal file
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* 数据库初始化脚本
|
||||
* @file init-db.js
|
||||
* @description 初始化银行系统数据库
|
||||
*/
|
||||
const { sequelize } = require('../config/database');
|
||||
const { User, Role, Account, Transaction } = require('../models');
|
||||
const bcrypt = require('bcryptjs');
|
||||
|
||||
async function initDatabase() {
|
||||
try {
|
||||
console.log('🔄 开始初始化银行系统数据库...');
|
||||
|
||||
// 测试数据库连接
|
||||
await sequelize.authenticate();
|
||||
console.log('✅ 数据库连接成功');
|
||||
|
||||
// 同步数据库模型
|
||||
await sequelize.sync({ force: true });
|
||||
console.log('✅ 数据库表创建成功');
|
||||
|
||||
// 创建初始角色
|
||||
console.log('🔄 创建初始角色...');
|
||||
const roles = await Role.bulkCreate([
|
||||
{
|
||||
name: 'admin',
|
||||
display_name: '系统管理员',
|
||||
description: '拥有系统所有权限',
|
||||
level: 100,
|
||||
is_system: true
|
||||
},
|
||||
{
|
||||
name: 'manager',
|
||||
display_name: '银行经理',
|
||||
description: '拥有银行管理权限',
|
||||
level: 80,
|
||||
is_system: false
|
||||
},
|
||||
{
|
||||
name: 'teller',
|
||||
display_name: '银行柜员',
|
||||
description: '拥有基本业务操作权限',
|
||||
level: 60,
|
||||
is_system: false
|
||||
},
|
||||
{
|
||||
name: 'user',
|
||||
display_name: '普通用户',
|
||||
description: '拥有基本用户权限',
|
||||
level: 20,
|
||||
is_system: false
|
||||
}
|
||||
]);
|
||||
console.log('✅ 初始角色创建成功');
|
||||
|
||||
// 创建初始管理员用户
|
||||
console.log('🔄 创建初始管理员用户...');
|
||||
const adminUser = await User.create({
|
||||
username: 'admin',
|
||||
email: 'admin@bank.com',
|
||||
password: 'Admin123456',
|
||||
phone: '13800138000',
|
||||
real_name: '系统管理员',
|
||||
id_card: '110101199001011234',
|
||||
role_id: roles[0].id, // admin角色
|
||||
status: 'active'
|
||||
});
|
||||
console.log('✅ 初始管理员用户创建成功');
|
||||
|
||||
// 创建测试用户
|
||||
console.log('🔄 创建测试用户...');
|
||||
const testUser = await User.create({
|
||||
username: 'testuser',
|
||||
email: 'test@bank.com',
|
||||
password: 'Test123456',
|
||||
phone: '13800138001',
|
||||
real_name: '测试用户',
|
||||
id_card: '110101199001011235',
|
||||
role_id: roles[3].id, // user角色
|
||||
status: 'active'
|
||||
});
|
||||
console.log('✅ 测试用户创建成功');
|
||||
|
||||
// 为测试用户创建账户
|
||||
console.log('🔄 为测试用户创建账户...');
|
||||
const testAccount = await Account.create({
|
||||
account_number: '001' + Date.now().toString().slice(-8) + '0001',
|
||||
user_id: testUser.id,
|
||||
account_type: 'savings',
|
||||
balance: 100000, // 1000元
|
||||
available_balance: 100000,
|
||||
frozen_amount: 0,
|
||||
currency: 'CNY',
|
||||
interest_rate: 0.035, // 3.5%年利率
|
||||
status: 'active'
|
||||
});
|
||||
console.log('✅ 测试账户创建成功');
|
||||
|
||||
// 创建一些示例交易记录
|
||||
console.log('🔄 创建示例交易记录...');
|
||||
const transactions = await Transaction.bulkCreate([
|
||||
{
|
||||
transaction_number: 'TXN' + Date.now() + '0001',
|
||||
account_id: testAccount.id,
|
||||
transaction_type: 'deposit',
|
||||
amount: 100000,
|
||||
balance_before: 0,
|
||||
balance_after: 100000,
|
||||
description: '开户存款',
|
||||
status: 'completed',
|
||||
processed_at: new Date()
|
||||
},
|
||||
{
|
||||
transaction_number: 'TXN' + Date.now() + '0002',
|
||||
account_id: testAccount.id,
|
||||
transaction_type: 'interest',
|
||||
amount: 292, // 约1元利息
|
||||
balance_before: 100000,
|
||||
balance_after: 100292,
|
||||
description: '定期利息',
|
||||
status: 'completed',
|
||||
processed_at: new Date()
|
||||
}
|
||||
]);
|
||||
console.log('✅ 示例交易记录创建成功');
|
||||
|
||||
console.log('\n🎉 银行系统数据库初始化完成!');
|
||||
console.log('\n📋 初始数据信息:');
|
||||
console.log(`👤 管理员账户: admin / Admin123456`);
|
||||
console.log(`👤 测试用户: testuser / Test123456`);
|
||||
console.log(`🏦 测试账户: ${testAccount.account_number}`);
|
||||
console.log(`💰 初始余额: ${(testAccount.balance / 100).toFixed(2)} 元`);
|
||||
console.log('\n🔗 数据库连接信息:');
|
||||
console.log(` 主机: ${process.env.DB_HOST || 'localhost'}`);
|
||||
console.log(` 端口: ${process.env.DB_PORT || 3306}`);
|
||||
console.log(` 数据库: ${process.env.DB_NAME || 'bank_management'}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 数据库初始化失败:', error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await sequelize.close();
|
||||
console.log('📊 数据库连接已关闭');
|
||||
}
|
||||
}
|
||||
|
||||
// 运行初始化
|
||||
if (require.main === module) {
|
||||
initDatabase();
|
||||
}
|
||||
|
||||
module.exports = initDatabase;
|
||||
35
bank-backend/scripts/query-users.js
Normal file
35
bank-backend/scripts/query-users.js
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* 查询用户数据以诊断登录问题
|
||||
*/
|
||||
const { sequelize } = require('../config/database');
|
||||
const { User } = require('../models');
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
console.log('Connecting to DB...', {
|
||||
dialect: process.env.DB_DIALECT,
|
||||
host: process.env.DB_HOST,
|
||||
port: process.env.DB_PORT,
|
||||
database: process.env.DB_NAME,
|
||||
user: process.env.DB_USER
|
||||
});
|
||||
|
||||
await sequelize.authenticate();
|
||||
console.log('✅ DB connected');
|
||||
|
||||
// 查询所有用户
|
||||
const users = await User.findAll({
|
||||
attributes: { exclude: ['password'] } // 排除密码字段
|
||||
});
|
||||
|
||||
console.log('📋 Users count:', users.length);
|
||||
users.forEach(user => {
|
||||
console.log(`- ID: ${user.id}, Username: ${user.username}, Email: ${user.email}, Status: ${user.status}, Role ID: ${user.role_id}, Last Login: ${user.last_login}`);
|
||||
});
|
||||
|
||||
process.exit(0);
|
||||
} catch (err) {
|
||||
console.error('❌ Query failed:', err.message);
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
||||
27
bank-backend/scripts/seed-bank-demo.sql
Normal file
27
bank-backend/scripts/seed-bank-demo.sql
Normal file
@@ -0,0 +1,27 @@
|
||||
-- 初始角色(如果不存在则插入)
|
||||
INSERT INTO bank_roles (name, display_name, level, is_system, status)
|
||||
SELECT 'admin','系统管理员',100,1,'active'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM bank_roles WHERE name='admin');
|
||||
|
||||
INSERT INTO bank_roles (name, display_name, level, is_system, status)
|
||||
SELECT 'user','普通用户',20,0,'active'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM bank_roles WHERE name='user');
|
||||
|
||||
-- 管理员账户,密码由脚本动态替换为 bcrypt 哈希(REPLACE_ADMIN_BCRYPT)
|
||||
INSERT INTO bank_users (username,email,password,real_name,id_card,role_id,status)
|
||||
SELECT 'admin','admin@bank.com','REPLACE_ADMIN_BCRYPT','系统管理员','110101199001011234', r.id,'active'
|
||||
FROM bank_roles r WHERE r.name='admin'
|
||||
AND NOT EXISTS (SELECT 1 FROM bank_users WHERE username='admin');
|
||||
|
||||
-- 测试用户
|
||||
INSERT INTO bank_users (username,email,password,real_name,id_card,role_id,status)
|
||||
SELECT 'testuser','test@bank.com','REPLACE_ADMIN_BCRYPT','测试用户','110101199001011235', r.id,'active'
|
||||
FROM bank_roles r WHERE r.name='user'
|
||||
AND NOT EXISTS (SELECT 1 FROM bank_users WHERE username='testuser');
|
||||
|
||||
-- 测试账户(admin名下)
|
||||
INSERT INTO bank_accounts (account_number,user_id,account_type,balance,available_balance,frozen_amount,currency,interest_rate,status)
|
||||
SELECT '001' || CAST(FLOOR(RAND()*90000000)+10000000 AS CHAR) || '0001', u.id, 'savings', 100000, 100000, 0, 'CNY', 0.035, 'active'
|
||||
FROM bank_users u WHERE u.username='admin'
|
||||
AND NOT EXISTS (SELECT 1 FROM bank_accounts a JOIN bank_users u2 ON a.user_id=u2.id WHERE u2.username='admin');
|
||||
|
||||
47
bank-backend/scripts/setup-bank-db.ps1
Normal file
47
bank-backend/scripts/setup-bank-db.ps1
Normal file
@@ -0,0 +1,47 @@
|
||||
Param(
|
||||
[string]$Host = $env:DB_HOST,
|
||||
[int]$Port = [int]($env:DB_PORT),
|
||||
[string]$Database = $env:DB_NAME,
|
||||
[string]$User = $env:DB_USER,
|
||||
[string]$Password = $env:DB_PASSWORD,
|
||||
[string]$AdminPlain = 'Admin123456'
|
||||
)
|
||||
|
||||
Write-Host "Using DB: $Host:$Port/$Database"
|
||||
|
||||
# 生成管理员 bcrypt 哈希
|
||||
try {
|
||||
$nodeScript = @"
|
||||
const bcrypt = require('bcryptjs');
|
||||
const pwd = process.argv[2] || 'Admin123456';
|
||||
bcrypt.hash(pwd, 10).then(h => { console.log(h); }).catch(e => { console.error(e); process.exit(1); });
|
||||
"@
|
||||
$hash = node -e $nodeScript $AdminPlain
|
||||
if (-not $hash) { throw 'bcrypt hash failed' }
|
||||
} catch {
|
||||
Write-Error "Failed to generate bcrypt hash: $_"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# 读取SQL并替换占位符
|
||||
$schema = Get-Content -Raw -Encoding UTF8 "$PSScriptRoot/create-bank-schema.sql"
|
||||
$seed = (Get-Content -Raw -Encoding UTF8 "$PSScriptRoot/seed-bank-demo.sql").Replace('REPLACE_ADMIN_BCRYPT', $hash)
|
||||
$sql = $schema + "`n" + $seed
|
||||
|
||||
# 写入临时文件
|
||||
$tmp = New-TemporaryFile
|
||||
Set-Content -Path $tmp -Value $sql -Encoding UTF8
|
||||
|
||||
# 调用 mysql 客户端
|
||||
try {
|
||||
$env:MYSQL_PWD = $Password
|
||||
& mysql --host=$Host --port=$Port --user=$User --database=$Database --default-character-set=utf8mb4 --protocol=TCP < $tmp
|
||||
if ($LASTEXITCODE -ne 0) { throw "mysql returned $LASTEXITCODE" }
|
||||
Write-Host "✅ Schema & seed executed successfully"
|
||||
} catch {
|
||||
Write-Error "Failed to execute SQL: $_"
|
||||
exit 1
|
||||
} finally {
|
||||
Remove-Item $tmp -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
29
bank-backend/scripts/test-connection.js
Normal file
29
bank-backend/scripts/test-connection.js
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* 远程数据库连接测试与列出表名
|
||||
*/
|
||||
const { sequelize } = require('../config/database');
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
console.log('Connecting to DB...', {
|
||||
dialect: process.env.DB_DIALECT,
|
||||
host: process.env.DB_HOST,
|
||||
port: process.env.DB_PORT,
|
||||
database: process.env.DB_NAME,
|
||||
user: process.env.DB_USER
|
||||
});
|
||||
|
||||
await sequelize.authenticate();
|
||||
console.log('✅ DB connected');
|
||||
|
||||
const [rows] = await sequelize.query('SHOW TABLES');
|
||||
const tables = rows.map(r => Object.values(r)[0]);
|
||||
console.log('📋 Tables:', tables);
|
||||
|
||||
process.exit(0);
|
||||
} catch (err) {
|
||||
console.error('❌ DB connect failed:', err.message);
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
||||
|
||||
223
bank-backend/server.js
Normal file
223
bank-backend/server.js
Normal file
@@ -0,0 +1,223 @@
|
||||
/**
|
||||
* 银行管理后台服务器
|
||||
* @file server.js
|
||||
* @description 银行系统后端API服务器主入口
|
||||
*/
|
||||
const express = require('express');
|
||||
const http = require('http');
|
||||
const cors = require('cors');
|
||||
const dotenv = require('dotenv');
|
||||
const helmet = require('helmet');
|
||||
const compression = require('compression');
|
||||
const swaggerUi = require('swagger-ui-express');
|
||||
const swaggerSpec = require('./config/swagger');
|
||||
const { sequelize } = require('./config/database');
|
||||
const logger = require('./utils/logger');
|
||||
const {
|
||||
apiRateLimiter,
|
||||
loginRateLimiter,
|
||||
inputSanitizer,
|
||||
sessionTimeoutCheck,
|
||||
securityHeaders
|
||||
} = require('./middleware/security');
|
||||
|
||||
// 加载环境变量
|
||||
dotenv.config();
|
||||
|
||||
// 创建Express应用和HTTP服务器
|
||||
const app = express();
|
||||
const server = http.createServer(app);
|
||||
const PORT = process.env.PORT || 5351;
|
||||
|
||||
// 安全中间件
|
||||
app.use(securityHeaders);
|
||||
app.use(helmet());
|
||||
app.use(compression());
|
||||
|
||||
// CORS配置
|
||||
app.use(cors({
|
||||
origin: process.env.CORS_ORIGIN || '*',
|
||||
credentials: true,
|
||||
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
||||
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With']
|
||||
}));
|
||||
|
||||
// 请求解析中间件
|
||||
app.use(express.json({ limit: '10mb' }));
|
||||
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
||||
|
||||
// 安全中间件
|
||||
app.use(inputSanitizer);
|
||||
app.use(apiRateLimiter);
|
||||
|
||||
// 静态文件服务
|
||||
app.use('/uploads', express.static('uploads'));
|
||||
|
||||
// API文档
|
||||
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));
|
||||
|
||||
// 健康检查端点
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({
|
||||
success: true,
|
||||
message: '银行系统运行正常',
|
||||
timestamp: new Date().toISOString(),
|
||||
version: '1.0.0'
|
||||
});
|
||||
});
|
||||
|
||||
// API路由
|
||||
app.use('/api/users', require('./routes/users'));
|
||||
app.use('/api/accounts', require('./routes/accounts'));
|
||||
app.use('/api/transactions', require('./routes/transactions'));
|
||||
|
||||
// 根路径
|
||||
app.get('/', (req, res) => {
|
||||
res.json({
|
||||
success: true,
|
||||
message: '银行管理后台API服务',
|
||||
version: '1.0.0',
|
||||
documentation: '/api-docs',
|
||||
health: '/health'
|
||||
});
|
||||
});
|
||||
|
||||
// 404处理
|
||||
app.use('*', (req, res) => {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
message: '请求的资源不存在',
|
||||
path: req.originalUrl
|
||||
});
|
||||
});
|
||||
|
||||
// 全局错误处理中间件
|
||||
app.use((error, req, res, next) => {
|
||||
logger.error('服务器错误:', error);
|
||||
|
||||
// 数据库连接错误
|
||||
if (error.name === 'SequelizeConnectionError') {
|
||||
return res.status(503).json({
|
||||
success: false,
|
||||
message: '数据库连接失败,请稍后重试'
|
||||
});
|
||||
}
|
||||
|
||||
// 数据库验证错误
|
||||
if (error.name === 'SequelizeValidationError') {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '数据验证失败',
|
||||
errors: error.errors.map(err => ({
|
||||
field: err.path,
|
||||
message: err.message
|
||||
}))
|
||||
});
|
||||
}
|
||||
|
||||
// 数据库唯一约束错误
|
||||
if (error.name === 'SequelizeUniqueConstraintError') {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '数据已存在,请检查输入'
|
||||
});
|
||||
}
|
||||
|
||||
// JWT错误
|
||||
if (error.name === 'JsonWebTokenError') {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: '无效的访问令牌'
|
||||
});
|
||||
}
|
||||
|
||||
if (error.name === 'TokenExpiredError') {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: '访问令牌已过期'
|
||||
});
|
||||
}
|
||||
|
||||
// 默认错误响应
|
||||
res.status(error.status || 500).json({
|
||||
success: false,
|
||||
message: process.env.NODE_ENV === 'production'
|
||||
? '服务器内部错误'
|
||||
: error.message,
|
||||
...(process.env.NODE_ENV !== 'production' && { stack: error.stack })
|
||||
});
|
||||
});
|
||||
|
||||
// 优雅关闭处理
|
||||
const gracefulShutdown = (signal) => {
|
||||
logger.info(`收到 ${signal} 信号,开始优雅关闭...`);
|
||||
|
||||
server.close(async () => {
|
||||
logger.info('HTTP服务器已关闭');
|
||||
|
||||
try {
|
||||
await sequelize.close();
|
||||
logger.info('数据库连接已关闭');
|
||||
} catch (error) {
|
||||
logger.error('关闭数据库连接时出错:', error);
|
||||
}
|
||||
|
||||
logger.info('银行系统已安全关闭');
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
// 强制关闭超时
|
||||
setTimeout(() => {
|
||||
logger.error('强制关闭服务器');
|
||||
process.exit(1);
|
||||
}, 10000);
|
||||
};
|
||||
|
||||
// 监听关闭信号
|
||||
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
||||
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
||||
|
||||
// 未捕获的异常处理
|
||||
process.on('uncaughtException', (error) => {
|
||||
logger.error('未捕获的异常:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
logger.error('未处理的Promise拒绝:', reason);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// 启动服务器
|
||||
const startServer = async () => {
|
||||
try {
|
||||
// 测试数据库连接
|
||||
await sequelize.authenticate();
|
||||
logger.info('✅ 数据库连接成功');
|
||||
|
||||
// 同步数据库模型(开发环境)
|
||||
// 按用户要求:不要初始化数据库(不自动建表/同步)
|
||||
// if (process.env.NODE_ENV === 'development') {
|
||||
// await sequelize.sync({ alter: true });
|
||||
// logger.info('✅ 数据库模型同步完成');
|
||||
// }
|
||||
|
||||
// 启动HTTP服务器
|
||||
server.listen(PORT, () => {
|
||||
logger.info(`🚀 银行管理后台服务器启动成功`);
|
||||
logger.info(`📡 服务地址: http://localhost:${PORT}`);
|
||||
logger.info(`📚 API文档: http://localhost:${PORT}/api-docs`);
|
||||
logger.info(`🏥 健康检查: http://localhost:${PORT}/health`);
|
||||
logger.info(`🌍 环境: ${process.env.NODE_ENV || 'development'}`);
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('❌ 服务器启动失败:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
// 启动服务器
|
||||
startServer();
|
||||
|
||||
module.exports = app;
|
||||
74
bank-backend/utils/logger.js
Normal file
74
bank-backend/utils/logger.js
Normal file
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* 日志工具
|
||||
* @file logger.js
|
||||
* @description 系统日志管理
|
||||
*/
|
||||
const winston = require('winston');
|
||||
const path = require('path');
|
||||
|
||||
// 创建日志目录
|
||||
const logDir = 'logs';
|
||||
|
||||
// 定义日志格式
|
||||
const logFormat = winston.format.combine(
|
||||
winston.format.timestamp({
|
||||
format: 'YYYY-MM-DD HH:mm:ss'
|
||||
}),
|
||||
winston.format.errors({ stack: true }),
|
||||
winston.format.json(),
|
||||
winston.format.printf(({ timestamp, level, message, stack, ...meta }) => {
|
||||
let log = `${timestamp} [${level.toUpperCase()}]: ${message}`;
|
||||
|
||||
if (stack) {
|
||||
log += `\n${stack}`;
|
||||
}
|
||||
|
||||
if (Object.keys(meta).length > 0) {
|
||||
log += `\n${JSON.stringify(meta, null, 2)}`;
|
||||
}
|
||||
|
||||
return log;
|
||||
})
|
||||
);
|
||||
|
||||
// 创建logger实例
|
||||
const logger = winston.createLogger({
|
||||
level: process.env.LOG_LEVEL || 'info',
|
||||
format: logFormat,
|
||||
defaultMeta: { service: 'bank-backend' },
|
||||
transports: [
|
||||
// 错误日志文件
|
||||
new winston.transports.File({
|
||||
filename: path.join(logDir, 'error.log'),
|
||||
level: 'error',
|
||||
maxsize: 5242880, // 5MB
|
||||
maxFiles: 5
|
||||
}),
|
||||
|
||||
// 所有日志文件
|
||||
new winston.transports.File({
|
||||
filename: path.join(logDir, 'combined.log'),
|
||||
maxsize: 5242880, // 5MB
|
||||
maxFiles: 5
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
// 开发环境添加控制台输出
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
logger.add(new winston.transports.Console({
|
||||
format: winston.format.combine(
|
||||
winston.format.colorize(),
|
||||
winston.format.simple()
|
||||
)
|
||||
}));
|
||||
}
|
||||
|
||||
// 创建流对象用于HTTP请求日志
|
||||
logger.stream = {
|
||||
write: (message) => {
|
||||
logger.info(message.trim());
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = logger;
|
||||
Reference in New Issue
Block a user