更新政府端和银行端

This commit is contained in:
2025-09-17 18:04:28 +08:00
parent f35ceef31f
commit e4287b83fe
185 changed files with 78320 additions and 189 deletions

View 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
View 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
## 🙏 致谢
感谢所有为这个项目做出贡献的开发者和开源社区。
---
**注意**: 这是一个演示项目,请勿在生产环境中使用默认的密码和密钥。

View 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;

View 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;

View 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}`;
}

View 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}`;
}

View 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: '服务器内部错误'
});
}
};

View 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
View 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

View 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
};

View 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
};

View 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;

View 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
View 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;

View 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
View 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;

View 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

File diff suppressed because it is too large Load Diff

114
bank-backend/package.json Normal file
View 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"
}

View 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;

View 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;

View 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;

View 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;

View 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;

View 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);
}
})();

View 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');

View 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
}

View 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
View 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;

View 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;