refactor: migrate entire project to TypeScript
This commit is contained in:
40
README.md
40
README.md
@@ -1,10 +1,11 @@
|
|||||||
# 授权管理系统 - 后端服务
|
# 授权管理系统 - 后端服务
|
||||||
|
|
||||||
浙江贝凡企业授权管理系统的后端服务,基于 Node.js + Express + SQLite。
|
浙江贝凡企业授权管理系统的后端服务,基于 Node.js + TypeScript + Express + SQLite。
|
||||||
|
|
||||||
## 技术栈
|
## 技术栈
|
||||||
|
|
||||||
- **Node.js**: 运行时环境
|
- **Node.js**: 运行时环境
|
||||||
|
- **TypeScript**: 类型安全
|
||||||
- **Express**: Web 框架
|
- **Express**: Web 框架
|
||||||
- **SQLite**: 数据库
|
- **SQLite**: 数据库
|
||||||
- **JWT**: 身份认证
|
- **JWT**: 身份认证
|
||||||
@@ -15,18 +16,21 @@
|
|||||||
```
|
```
|
||||||
backend/
|
backend/
|
||||||
├── routes/ # API 路由
|
├── routes/ # API 路由
|
||||||
│ ├── auth.js # 认证路由
|
│ ├── auth.ts # 认证路由
|
||||||
│ ├── serials.js # 序列号路由
|
│ ├── serials.ts # 序列号路由
|
||||||
│ └── companies.js # 企业路由
|
│ └── companies.ts # 企业路由
|
||||||
├── middleware/ # 中间件
|
├── middleware/ # 中间件
|
||||||
│ └── auth.js # 认证中间件
|
│ └── auth.ts # 认证中间件
|
||||||
├── scripts/ # 脚本
|
├── scripts/ # 脚本
|
||||||
│ └── init-db.js # 数据库初始化
|
│ └── init-db.ts # 数据库初始化
|
||||||
├── utils/ # 工具函数
|
├── utils/ # 工具函数
|
||||||
│ └── database.js # 数据库连接
|
│ └── database.ts # 数据库连接
|
||||||
|
├── types/ # 类型定义
|
||||||
|
│ └── index.d.ts # TypeScript 类型
|
||||||
├── data/ # 数据文件
|
├── data/ # 数据文件
|
||||||
│ └── database.sqlite
|
│ └── database.sqlite
|
||||||
├── server.js # 服务器入口
|
├── server.ts # 服务器入口
|
||||||
|
├── tsconfig.json # TypeScript 配置
|
||||||
├── .env # 环境变量
|
├── .env # 环境变量
|
||||||
└── package.json # 项目配置
|
└── package.json # 项目配置
|
||||||
```
|
```
|
||||||
@@ -47,6 +51,14 @@ pnpm dev
|
|||||||
|
|
||||||
服务器将在 http://localhost:3000 运行
|
服务器将在 http://localhost:3000 运行
|
||||||
|
|
||||||
|
## 构建
|
||||||
|
|
||||||
|
构建 TypeScript 为 JavaScript:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm build
|
||||||
|
```
|
||||||
|
|
||||||
## 生产
|
## 生产
|
||||||
|
|
||||||
启动生产服务器:
|
启动生产服务器:
|
||||||
@@ -83,22 +95,28 @@ JWT_SECRET=your-secret-key-here
|
|||||||
### 认证接口
|
### 认证接口
|
||||||
|
|
||||||
- `POST /api/auth/login` - 用户登录
|
- `POST /api/auth/login` - 用户登录
|
||||||
- `POST /api/auth/logout` - 用户登出
|
- `GET /api/auth/profile` - 获取用户信息
|
||||||
- `POST /api/auth/change-password` - 修改密码
|
- `POST /api/auth/change-password` - 修改密码
|
||||||
|
- `PUT /api/auth/profile` - 更新用户资料
|
||||||
|
|
||||||
### 序列号接口
|
### 序列号接口
|
||||||
|
|
||||||
- `POST /api/serials/generate` - 生成序列号
|
- `POST /api/serials/generate` - 生成序列号
|
||||||
|
- `POST /api/serials/generate-with-prefix` - 使用自定义前缀生成序列号
|
||||||
|
- `POST /api/serials/:serialNumber/qrcode` - 生成二维码
|
||||||
- `GET /api/serials/:serialNumber/query` - 查询序列号
|
- `GET /api/serials/:serialNumber/query` - 查询序列号
|
||||||
- `POST /api/serials/:serialNumber/revoke` - 吊销序列号
|
|
||||||
- `GET /api/serials/` - 获取序列号列表
|
- `GET /api/serials/` - 获取序列号列表
|
||||||
|
- `PATCH /api/serials/:serialNumber` - 更新序列号
|
||||||
|
- `POST /api/serials/:serialNumber/revoke` - 吊销序列号
|
||||||
|
|
||||||
### 企业接口
|
### 企业接口
|
||||||
|
|
||||||
- `GET /api/companies/` - 获取企业列表
|
- `GET /api/companies/` - 获取企业列表
|
||||||
- `GET /api/companies/:companyName` - 获取企业详情
|
- `GET /api/companies/:companyName` - 获取企业详情
|
||||||
- `POST /api/companies/:companyName/revoke` - 吊销企业
|
- `PATCH /api/companies/:companyName` - 更新企业信息
|
||||||
- `DELETE /api/companies/:companyName` - 删除企业
|
- `DELETE /api/companies/:companyName` - 删除企业
|
||||||
|
- `DELETE /api/companies/:companyName/serials/:serialNumber` - 删除企业下的序列号
|
||||||
|
- `POST /api/companies/:companyName/revoke` - 吊销企业
|
||||||
- `GET /api/companies/stats/overview` - 获取统计数据
|
- `GET /api/companies/stats/overview` - 获取统计数据
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
const jwt = require('jsonwebtoken');
|
|
||||||
const db = require('../utils/database');
|
|
||||||
|
|
||||||
// 验证JWT令牌
|
|
||||||
const authenticateToken = async (req, res, next) => {
|
|
||||||
// 从请求头获取令牌
|
|
||||||
const authHeader = req.headers['authorization'];
|
|
||||||
const token = authHeader && authHeader.split(' ')[1];
|
|
||||||
|
|
||||||
if (!token) {
|
|
||||||
return res.status(401).json({ error: '访问令牌缺失' });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 验证令牌
|
|
||||||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
|
||||||
|
|
||||||
// 获取用户信息
|
|
||||||
const user = await db.get('SELECT id, username, name, role FROM users WHERE id = ?', [decoded.userId]);
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return res.status(401).json({ error: '用户不存在' });
|
|
||||||
}
|
|
||||||
|
|
||||||
req.user = user;
|
|
||||||
next();
|
|
||||||
} catch (error) {
|
|
||||||
if (error.name === 'TokenExpiredError') {
|
|
||||||
return res.status(401).json({ error: '令牌已过期' });
|
|
||||||
}
|
|
||||||
return res.status(403).json({ error: '无效的令牌' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 验证管理员权限
|
|
||||||
const requireAdmin = (req, res, next) => {
|
|
||||||
if (req.user.role !== 'admin') {
|
|
||||||
return res.status(403).json({ error: '需要管理员权限' });
|
|
||||||
}
|
|
||||||
next();
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
authenticateToken,
|
|
||||||
requireAdmin
|
|
||||||
};
|
|
||||||
53
middleware/auth.ts
Normal file
53
middleware/auth.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
import db from '../utils/database';
|
||||||
|
import { AuthUser } from '../types';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
namespace Express {
|
||||||
|
interface Request {
|
||||||
|
user?: AuthUser;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const authenticateToken = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||||
|
const authHeader = req.headers['authorization'];
|
||||||
|
const token = authHeader && authHeader.split(' ')[1];
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
res.status(401).json({ error: '访问令牌缺失' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const decoded = jwt.verify(token, process.env.JWT_SECRET!) as { userId: number; username: string; role: string };
|
||||||
|
|
||||||
|
const user = await db.get(
|
||||||
|
'SELECT id, username, name, role FROM users WHERE id = ?',
|
||||||
|
[decoded.userId]
|
||||||
|
) as AuthUser | undefined;
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
res.status(401).json({ error: '用户不存在' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
req.user = user;
|
||||||
|
next();
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.name === 'TokenExpiredError') {
|
||||||
|
res.status(401).json({ error: '令牌已过期' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.status(403).json({ error: '无效的令牌' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const requireAdmin = (req: Request, res: Response, next: NextFunction): void => {
|
||||||
|
if (req.user?.role !== 'admin') {
|
||||||
|
res.status(403).json({ error: '需要管理员权限' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
};
|
||||||
19
package.json
19
package.json
@@ -1,12 +1,14 @@
|
|||||||
|
{
|
||||||
{
|
{
|
||||||
"name": "trace-backend",
|
"name": "trace-backend",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "浙江贝凡企业授权管理系统 - 后端服务",
|
"description": "浙江贝凡企业授权管理系统 - 后端服务",
|
||||||
"main": "server.js",
|
"main": "dist/server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node server.js",
|
"start": "node dist/server.js",
|
||||||
"dev": "nodemon server.js",
|
"dev": "nodemon --exec tsx server.ts",
|
||||||
"init-db": "node scripts/init-db.js"
|
"build": "tsc",
|
||||||
|
"init-db": "tsx scripts/init-db.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bcryptjs": "^3.0.3",
|
"bcryptjs": "^3.0.3",
|
||||||
@@ -17,7 +19,14 @@
|
|||||||
"jsonwebtoken": "^9.0.2"
|
"jsonwebtoken": "^9.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"nodemon": "^3.0.1"
|
"@types/cors": "^2.8.19",
|
||||||
|
"@types/express": "^5.0.6",
|
||||||
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
|
"@types/node": "^25.2.1",
|
||||||
|
"nodemon": "^3.0.1",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"tsx": "^4.21.0",
|
||||||
|
"typescript": "^5.9.3"
|
||||||
},
|
},
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
|
|||||||
542
pnpm-lock.yaml
generated
542
pnpm-lock.yaml
generated
@@ -27,20 +27,274 @@ importers:
|
|||||||
specifier: ^9.0.2
|
specifier: ^9.0.2
|
||||||
version: 9.0.3
|
version: 9.0.3
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@types/cors':
|
||||||
|
specifier: ^2.8.19
|
||||||
|
version: 2.8.19
|
||||||
|
'@types/express':
|
||||||
|
specifier: ^5.0.6
|
||||||
|
version: 5.0.6
|
||||||
|
'@types/jsonwebtoken':
|
||||||
|
specifier: ^9.0.10
|
||||||
|
version: 9.0.10
|
||||||
|
'@types/node':
|
||||||
|
specifier: ^25.2.1
|
||||||
|
version: 25.2.1
|
||||||
nodemon:
|
nodemon:
|
||||||
specifier: ^3.0.1
|
specifier: ^3.0.1
|
||||||
version: 3.1.11
|
version: 3.1.11
|
||||||
|
ts-node:
|
||||||
|
specifier: ^10.9.2
|
||||||
|
version: 10.9.2(@types/node@25.2.1)(typescript@5.9.3)
|
||||||
|
tsx:
|
||||||
|
specifier: ^4.21.0
|
||||||
|
version: 4.21.0
|
||||||
|
typescript:
|
||||||
|
specifier: ^5.9.3
|
||||||
|
version: 5.9.3
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
|
'@cspotcode/source-map-support@0.8.1':
|
||||||
|
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
'@esbuild/aix-ppc64@0.27.3':
|
||||||
|
resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [ppc64]
|
||||||
|
os: [aix]
|
||||||
|
|
||||||
|
'@esbuild/android-arm64@0.27.3':
|
||||||
|
resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
'@esbuild/android-arm@0.27.3':
|
||||||
|
resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
'@esbuild/android-x64@0.27.3':
|
||||||
|
resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
'@esbuild/darwin-arm64@0.27.3':
|
||||||
|
resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@esbuild/darwin-x64@0.27.3':
|
||||||
|
resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@esbuild/freebsd-arm64@0.27.3':
|
||||||
|
resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [freebsd]
|
||||||
|
|
||||||
|
'@esbuild/freebsd-x64@0.27.3':
|
||||||
|
resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [freebsd]
|
||||||
|
|
||||||
|
'@esbuild/linux-arm64@0.27.3':
|
||||||
|
resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-arm@0.27.3':
|
||||||
|
resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-ia32@0.27.3':
|
||||||
|
resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [ia32]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-loong64@0.27.3':
|
||||||
|
resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [loong64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-mips64el@0.27.3':
|
||||||
|
resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [mips64el]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-ppc64@0.27.3':
|
||||||
|
resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [ppc64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-riscv64@0.27.3':
|
||||||
|
resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [riscv64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-s390x@0.27.3':
|
||||||
|
resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [s390x]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-x64@0.27.3':
|
||||||
|
resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/netbsd-arm64@0.27.3':
|
||||||
|
resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [netbsd]
|
||||||
|
|
||||||
|
'@esbuild/netbsd-x64@0.27.3':
|
||||||
|
resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [netbsd]
|
||||||
|
|
||||||
|
'@esbuild/openbsd-arm64@0.27.3':
|
||||||
|
resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [openbsd]
|
||||||
|
|
||||||
|
'@esbuild/openbsd-x64@0.27.3':
|
||||||
|
resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [openbsd]
|
||||||
|
|
||||||
|
'@esbuild/openharmony-arm64@0.27.3':
|
||||||
|
resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [openharmony]
|
||||||
|
|
||||||
|
'@esbuild/sunos-x64@0.27.3':
|
||||||
|
resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [sunos]
|
||||||
|
|
||||||
|
'@esbuild/win32-arm64@0.27.3':
|
||||||
|
resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@esbuild/win32-ia32@0.27.3':
|
||||||
|
resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [ia32]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@esbuild/win32-x64@0.27.3':
|
||||||
|
resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@jridgewell/resolve-uri@3.1.2':
|
||||||
|
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
|
||||||
|
engines: {node: '>=6.0.0'}
|
||||||
|
|
||||||
|
'@jridgewell/sourcemap-codec@1.5.5':
|
||||||
|
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
|
||||||
|
|
||||||
|
'@jridgewell/trace-mapping@0.3.9':
|
||||||
|
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
|
||||||
|
|
||||||
|
'@tsconfig/node10@1.0.12':
|
||||||
|
resolution: {integrity: sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==}
|
||||||
|
|
||||||
|
'@tsconfig/node12@1.0.11':
|
||||||
|
resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==}
|
||||||
|
|
||||||
|
'@tsconfig/node14@1.0.3':
|
||||||
|
resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==}
|
||||||
|
|
||||||
|
'@tsconfig/node16@1.0.4':
|
||||||
|
resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==}
|
||||||
|
|
||||||
|
'@types/body-parser@1.19.6':
|
||||||
|
resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==}
|
||||||
|
|
||||||
|
'@types/connect@3.4.38':
|
||||||
|
resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
|
||||||
|
|
||||||
|
'@types/cors@2.8.19':
|
||||||
|
resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==}
|
||||||
|
|
||||||
|
'@types/express-serve-static-core@5.1.1':
|
||||||
|
resolution: {integrity: sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==}
|
||||||
|
|
||||||
|
'@types/express@5.0.6':
|
||||||
|
resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==}
|
||||||
|
|
||||||
|
'@types/http-errors@2.0.5':
|
||||||
|
resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==}
|
||||||
|
|
||||||
|
'@types/jsonwebtoken@9.0.10':
|
||||||
|
resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==}
|
||||||
|
|
||||||
|
'@types/ms@2.1.0':
|
||||||
|
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
|
||||||
|
|
||||||
|
'@types/node@25.2.1':
|
||||||
|
resolution: {integrity: sha512-CPrnr8voK8vC6eEtyRzvMpgp3VyVRhgclonE7qYi6P9sXwYb59ucfrnmFBTaP0yUi8Gk4yZg/LlTJULGxvTNsg==}
|
||||||
|
|
||||||
|
'@types/qs@6.14.0':
|
||||||
|
resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==}
|
||||||
|
|
||||||
|
'@types/range-parser@1.2.7':
|
||||||
|
resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==}
|
||||||
|
|
||||||
|
'@types/send@1.2.1':
|
||||||
|
resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==}
|
||||||
|
|
||||||
|
'@types/serve-static@2.2.0':
|
||||||
|
resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==}
|
||||||
|
|
||||||
accepts@2.0.0:
|
accepts@2.0.0:
|
||||||
resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==}
|
resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
acorn-walk@8.3.4:
|
||||||
|
resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==}
|
||||||
|
engines: {node: '>=0.4.0'}
|
||||||
|
|
||||||
|
acorn@8.15.0:
|
||||||
|
resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==}
|
||||||
|
engines: {node: '>=0.4.0'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
anymatch@3.1.3:
|
anymatch@3.1.3:
|
||||||
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
|
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
|
|
||||||
|
arg@4.1.3:
|
||||||
|
resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==}
|
||||||
|
|
||||||
balanced-match@1.0.2:
|
balanced-match@1.0.2:
|
||||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||||
|
|
||||||
@@ -124,6 +378,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==}
|
resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==}
|
||||||
engines: {node: '>= 0.10'}
|
engines: {node: '>= 0.10'}
|
||||||
|
|
||||||
|
create-require@1.1.1:
|
||||||
|
resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==}
|
||||||
|
|
||||||
debug@4.4.3:
|
debug@4.4.3:
|
||||||
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
|
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
|
||||||
engines: {node: '>=6.0'}
|
engines: {node: '>=6.0'}
|
||||||
@@ -149,6 +406,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
diff@4.0.4:
|
||||||
|
resolution: {integrity: sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==}
|
||||||
|
engines: {node: '>=0.3.1'}
|
||||||
|
|
||||||
dotenv@17.2.4:
|
dotenv@17.2.4:
|
||||||
resolution: {integrity: sha512-mudtfb4zRB4bVvdj0xRo+e6duH1csJRM8IukBqfTRvHotn9+LBXB8ynAidP9zHqoRC/fsllXgk4kCKlR21fIhw==}
|
resolution: {integrity: sha512-mudtfb4zRB4bVvdj0xRo+e6duH1csJRM8IukBqfTRvHotn9+LBXB8ynAidP9zHqoRC/fsllXgk4kCKlR21fIhw==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@@ -182,6 +443,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
|
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
esbuild@0.27.3:
|
||||||
|
resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
escape-html@1.0.3:
|
escape-html@1.0.3:
|
||||||
resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
|
resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
|
||||||
|
|
||||||
@@ -235,6 +501,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
|
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
get-tsconfig@4.13.4:
|
||||||
|
resolution: {integrity: sha512-gKvvu/fh0hxWmR/Ty0Goc3u/GADL9IgyhNAPD8hElRVO9dTOawCuyGNURCjaSTB4ZNP/OAUaSXmR2LhitzkLug==}
|
||||||
|
|
||||||
github-from-package@0.0.0:
|
github-from-package@0.0.0:
|
||||||
resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==}
|
resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==}
|
||||||
|
|
||||||
@@ -332,6 +601,9 @@ packages:
|
|||||||
lodash.once@4.1.1:
|
lodash.once@4.1.1:
|
||||||
resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==}
|
resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==}
|
||||||
|
|
||||||
|
make-error@1.3.6:
|
||||||
|
resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==}
|
||||||
|
|
||||||
math-intrinsics@1.1.0:
|
math-intrinsics@1.1.0:
|
||||||
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -453,6 +725,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
|
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
|
||||||
engines: {node: '>=8.10.0'}
|
engines: {node: '>=8.10.0'}
|
||||||
|
|
||||||
|
resolve-pkg-maps@1.0.0:
|
||||||
|
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
|
||||||
|
|
||||||
router@2.2.0:
|
router@2.2.0:
|
||||||
resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==}
|
resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==}
|
||||||
engines: {node: '>= 18'}
|
engines: {node: '>= 18'}
|
||||||
@@ -539,6 +814,25 @@ packages:
|
|||||||
resolution: {integrity: sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==}
|
resolution: {integrity: sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
ts-node@10.9.2:
|
||||||
|
resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==}
|
||||||
|
hasBin: true
|
||||||
|
peerDependencies:
|
||||||
|
'@swc/core': '>=1.2.50'
|
||||||
|
'@swc/wasm': '>=1.2.50'
|
||||||
|
'@types/node': '*'
|
||||||
|
typescript: '>=2.7'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@swc/core':
|
||||||
|
optional: true
|
||||||
|
'@swc/wasm':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
tsx@4.21.0:
|
||||||
|
resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==}
|
||||||
|
engines: {node: '>=18.0.0'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
tunnel-agent@0.6.0:
|
tunnel-agent@0.6.0:
|
||||||
resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==}
|
resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==}
|
||||||
|
|
||||||
@@ -546,9 +840,17 @@ packages:
|
|||||||
resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==}
|
resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
typescript@5.9.3:
|
||||||
|
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
||||||
|
engines: {node: '>=14.17'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
undefsafe@2.0.5:
|
undefsafe@2.0.5:
|
||||||
resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==}
|
resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==}
|
||||||
|
|
||||||
|
undici-types@7.16.0:
|
||||||
|
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
|
||||||
|
|
||||||
unpipe@1.0.0:
|
unpipe@1.0.0:
|
||||||
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
|
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
@@ -556,6 +858,9 @@ packages:
|
|||||||
util-deprecate@1.0.2:
|
util-deprecate@1.0.2:
|
||||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||||
|
|
||||||
|
v8-compile-cache-lib@3.0.1:
|
||||||
|
resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==}
|
||||||
|
|
||||||
vary@1.1.2:
|
vary@1.1.2:
|
||||||
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
|
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
@@ -563,18 +868,181 @@ packages:
|
|||||||
wrappy@1.0.2:
|
wrappy@1.0.2:
|
||||||
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
||||||
|
|
||||||
|
yn@3.1.1:
|
||||||
|
resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
snapshots:
|
snapshots:
|
||||||
|
|
||||||
|
'@cspotcode/source-map-support@0.8.1':
|
||||||
|
dependencies:
|
||||||
|
'@jridgewell/trace-mapping': 0.3.9
|
||||||
|
|
||||||
|
'@esbuild/aix-ppc64@0.27.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/android-arm64@0.27.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/android-arm@0.27.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/android-x64@0.27.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/darwin-arm64@0.27.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/darwin-x64@0.27.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/freebsd-arm64@0.27.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/freebsd-x64@0.27.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-arm64@0.27.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-arm@0.27.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-ia32@0.27.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-loong64@0.27.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-mips64el@0.27.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-ppc64@0.27.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-riscv64@0.27.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-s390x@0.27.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-x64@0.27.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/netbsd-arm64@0.27.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/netbsd-x64@0.27.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/openbsd-arm64@0.27.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/openbsd-x64@0.27.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/openharmony-arm64@0.27.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/sunos-x64@0.27.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/win32-arm64@0.27.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/win32-ia32@0.27.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/win32-x64@0.27.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@jridgewell/resolve-uri@3.1.2': {}
|
||||||
|
|
||||||
|
'@jridgewell/sourcemap-codec@1.5.5': {}
|
||||||
|
|
||||||
|
'@jridgewell/trace-mapping@0.3.9':
|
||||||
|
dependencies:
|
||||||
|
'@jridgewell/resolve-uri': 3.1.2
|
||||||
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
|
||||||
|
'@tsconfig/node10@1.0.12': {}
|
||||||
|
|
||||||
|
'@tsconfig/node12@1.0.11': {}
|
||||||
|
|
||||||
|
'@tsconfig/node14@1.0.3': {}
|
||||||
|
|
||||||
|
'@tsconfig/node16@1.0.4': {}
|
||||||
|
|
||||||
|
'@types/body-parser@1.19.6':
|
||||||
|
dependencies:
|
||||||
|
'@types/connect': 3.4.38
|
||||||
|
'@types/node': 25.2.1
|
||||||
|
|
||||||
|
'@types/connect@3.4.38':
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 25.2.1
|
||||||
|
|
||||||
|
'@types/cors@2.8.19':
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 25.2.1
|
||||||
|
|
||||||
|
'@types/express-serve-static-core@5.1.1':
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 25.2.1
|
||||||
|
'@types/qs': 6.14.0
|
||||||
|
'@types/range-parser': 1.2.7
|
||||||
|
'@types/send': 1.2.1
|
||||||
|
|
||||||
|
'@types/express@5.0.6':
|
||||||
|
dependencies:
|
||||||
|
'@types/body-parser': 1.19.6
|
||||||
|
'@types/express-serve-static-core': 5.1.1
|
||||||
|
'@types/serve-static': 2.2.0
|
||||||
|
|
||||||
|
'@types/http-errors@2.0.5': {}
|
||||||
|
|
||||||
|
'@types/jsonwebtoken@9.0.10':
|
||||||
|
dependencies:
|
||||||
|
'@types/ms': 2.1.0
|
||||||
|
'@types/node': 25.2.1
|
||||||
|
|
||||||
|
'@types/ms@2.1.0': {}
|
||||||
|
|
||||||
|
'@types/node@25.2.1':
|
||||||
|
dependencies:
|
||||||
|
undici-types: 7.16.0
|
||||||
|
|
||||||
|
'@types/qs@6.14.0': {}
|
||||||
|
|
||||||
|
'@types/range-parser@1.2.7': {}
|
||||||
|
|
||||||
|
'@types/send@1.2.1':
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 25.2.1
|
||||||
|
|
||||||
|
'@types/serve-static@2.2.0':
|
||||||
|
dependencies:
|
||||||
|
'@types/http-errors': 2.0.5
|
||||||
|
'@types/node': 25.2.1
|
||||||
|
|
||||||
accepts@2.0.0:
|
accepts@2.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
mime-types: 3.0.2
|
mime-types: 3.0.2
|
||||||
negotiator: 1.0.0
|
negotiator: 1.0.0
|
||||||
|
|
||||||
|
acorn-walk@8.3.4:
|
||||||
|
dependencies:
|
||||||
|
acorn: 8.15.0
|
||||||
|
|
||||||
|
acorn@8.15.0: {}
|
||||||
|
|
||||||
anymatch@3.1.3:
|
anymatch@3.1.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
normalize-path: 3.0.0
|
normalize-path: 3.0.0
|
||||||
picomatch: 2.3.1
|
picomatch: 2.3.1
|
||||||
|
|
||||||
|
arg@4.1.3: {}
|
||||||
|
|
||||||
balanced-match@1.0.2: {}
|
balanced-match@1.0.2: {}
|
||||||
|
|
||||||
base64-js@1.5.1: {}
|
base64-js@1.5.1: {}
|
||||||
@@ -669,6 +1137,8 @@ snapshots:
|
|||||||
object-assign: 4.1.1
|
object-assign: 4.1.1
|
||||||
vary: 1.1.2
|
vary: 1.1.2
|
||||||
|
|
||||||
|
create-require@1.1.1: {}
|
||||||
|
|
||||||
debug@4.4.3(supports-color@5.5.0):
|
debug@4.4.3(supports-color@5.5.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
@@ -685,6 +1155,8 @@ snapshots:
|
|||||||
|
|
||||||
detect-libc@2.1.2: {}
|
detect-libc@2.1.2: {}
|
||||||
|
|
||||||
|
diff@4.0.4: {}
|
||||||
|
|
||||||
dotenv@17.2.4: {}
|
dotenv@17.2.4: {}
|
||||||
|
|
||||||
dunder-proto@1.0.1:
|
dunder-proto@1.0.1:
|
||||||
@@ -713,6 +1185,35 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
es-errors: 1.3.0
|
es-errors: 1.3.0
|
||||||
|
|
||||||
|
esbuild@0.27.3:
|
||||||
|
optionalDependencies:
|
||||||
|
'@esbuild/aix-ppc64': 0.27.3
|
||||||
|
'@esbuild/android-arm': 0.27.3
|
||||||
|
'@esbuild/android-arm64': 0.27.3
|
||||||
|
'@esbuild/android-x64': 0.27.3
|
||||||
|
'@esbuild/darwin-arm64': 0.27.3
|
||||||
|
'@esbuild/darwin-x64': 0.27.3
|
||||||
|
'@esbuild/freebsd-arm64': 0.27.3
|
||||||
|
'@esbuild/freebsd-x64': 0.27.3
|
||||||
|
'@esbuild/linux-arm': 0.27.3
|
||||||
|
'@esbuild/linux-arm64': 0.27.3
|
||||||
|
'@esbuild/linux-ia32': 0.27.3
|
||||||
|
'@esbuild/linux-loong64': 0.27.3
|
||||||
|
'@esbuild/linux-mips64el': 0.27.3
|
||||||
|
'@esbuild/linux-ppc64': 0.27.3
|
||||||
|
'@esbuild/linux-riscv64': 0.27.3
|
||||||
|
'@esbuild/linux-s390x': 0.27.3
|
||||||
|
'@esbuild/linux-x64': 0.27.3
|
||||||
|
'@esbuild/netbsd-arm64': 0.27.3
|
||||||
|
'@esbuild/netbsd-x64': 0.27.3
|
||||||
|
'@esbuild/openbsd-arm64': 0.27.3
|
||||||
|
'@esbuild/openbsd-x64': 0.27.3
|
||||||
|
'@esbuild/openharmony-arm64': 0.27.3
|
||||||
|
'@esbuild/sunos-x64': 0.27.3
|
||||||
|
'@esbuild/win32-arm64': 0.27.3
|
||||||
|
'@esbuild/win32-ia32': 0.27.3
|
||||||
|
'@esbuild/win32-x64': 0.27.3
|
||||||
|
|
||||||
escape-html@1.0.3: {}
|
escape-html@1.0.3: {}
|
||||||
|
|
||||||
etag@1.8.1: {}
|
etag@1.8.1: {}
|
||||||
@@ -798,6 +1299,10 @@ snapshots:
|
|||||||
dunder-proto: 1.0.1
|
dunder-proto: 1.0.1
|
||||||
es-object-atoms: 1.1.1
|
es-object-atoms: 1.1.1
|
||||||
|
|
||||||
|
get-tsconfig@4.13.4:
|
||||||
|
dependencies:
|
||||||
|
resolve-pkg-maps: 1.0.0
|
||||||
|
|
||||||
github-from-package@0.0.0: {}
|
github-from-package@0.0.0: {}
|
||||||
|
|
||||||
glob-parent@5.1.2:
|
glob-parent@5.1.2:
|
||||||
@@ -888,6 +1393,8 @@ snapshots:
|
|||||||
|
|
||||||
lodash.once@4.1.1: {}
|
lodash.once@4.1.1: {}
|
||||||
|
|
||||||
|
make-error@1.3.6: {}
|
||||||
|
|
||||||
math-intrinsics@1.1.0: {}
|
math-intrinsics@1.1.0: {}
|
||||||
|
|
||||||
media-typer@1.1.0: {}
|
media-typer@1.1.0: {}
|
||||||
@@ -1010,6 +1517,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
picomatch: 2.3.1
|
picomatch: 2.3.1
|
||||||
|
|
||||||
|
resolve-pkg-maps@1.0.0: {}
|
||||||
|
|
||||||
router@2.2.0:
|
router@2.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.4.3(supports-color@5.5.0)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
@@ -1128,6 +1637,31 @@ snapshots:
|
|||||||
|
|
||||||
touch@3.1.1: {}
|
touch@3.1.1: {}
|
||||||
|
|
||||||
|
ts-node@10.9.2(@types/node@25.2.1)(typescript@5.9.3):
|
||||||
|
dependencies:
|
||||||
|
'@cspotcode/source-map-support': 0.8.1
|
||||||
|
'@tsconfig/node10': 1.0.12
|
||||||
|
'@tsconfig/node12': 1.0.11
|
||||||
|
'@tsconfig/node14': 1.0.3
|
||||||
|
'@tsconfig/node16': 1.0.4
|
||||||
|
'@types/node': 25.2.1
|
||||||
|
acorn: 8.15.0
|
||||||
|
acorn-walk: 8.3.4
|
||||||
|
arg: 4.1.3
|
||||||
|
create-require: 1.1.1
|
||||||
|
diff: 4.0.4
|
||||||
|
make-error: 1.3.6
|
||||||
|
typescript: 5.9.3
|
||||||
|
v8-compile-cache-lib: 3.0.1
|
||||||
|
yn: 3.1.1
|
||||||
|
|
||||||
|
tsx@4.21.0:
|
||||||
|
dependencies:
|
||||||
|
esbuild: 0.27.3
|
||||||
|
get-tsconfig: 4.13.4
|
||||||
|
optionalDependencies:
|
||||||
|
fsevents: 2.3.3
|
||||||
|
|
||||||
tunnel-agent@0.6.0:
|
tunnel-agent@0.6.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
safe-buffer: 5.2.1
|
safe-buffer: 5.2.1
|
||||||
@@ -1138,12 +1672,20 @@ snapshots:
|
|||||||
media-typer: 1.1.0
|
media-typer: 1.1.0
|
||||||
mime-types: 3.0.2
|
mime-types: 3.0.2
|
||||||
|
|
||||||
|
typescript@5.9.3: {}
|
||||||
|
|
||||||
undefsafe@2.0.5: {}
|
undefsafe@2.0.5: {}
|
||||||
|
|
||||||
|
undici-types@7.16.0: {}
|
||||||
|
|
||||||
unpipe@1.0.0: {}
|
unpipe@1.0.0: {}
|
||||||
|
|
||||||
util-deprecate@1.0.2: {}
|
util-deprecate@1.0.2: {}
|
||||||
|
|
||||||
|
v8-compile-cache-lib@3.0.1: {}
|
||||||
|
|
||||||
vary@1.1.2: {}
|
vary@1.1.2: {}
|
||||||
|
|
||||||
wrappy@1.0.2: {}
|
wrappy@1.0.2: {}
|
||||||
|
|
||||||
|
yn@3.1.1: {}
|
||||||
|
|||||||
139
routes/auth.js
139
routes/auth.js
@@ -1,139 +0,0 @@
|
|||||||
const express = require('express');
|
|
||||||
const jwt = require('jsonwebtoken');
|
|
||||||
const bcrypt = require('bcryptjs');
|
|
||||||
const db = require('../utils/database');
|
|
||||||
const { authenticateToken } = require('../middleware/auth');
|
|
||||||
|
|
||||||
const router = express.Router();
|
|
||||||
|
|
||||||
// 用户登录
|
|
||||||
router.post('/login', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { username, password } = req.body;
|
|
||||||
|
|
||||||
if (!username || !password) {
|
|
||||||
return res.status(400).json({ error: '用户名和密码不能为空' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 查询用户
|
|
||||||
const user = await db.get('SELECT * FROM users WHERE username = ?', [username]);
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return res.status(401).json({ error: '用户名或密码错误' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证密码
|
|
||||||
const isValidPassword = await bcrypt.compare(password, user.password);
|
|
||||||
|
|
||||||
if (!isValidPassword) {
|
|
||||||
return res.status(401).json({ error: '用户名或密码错误' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生成JWT令牌
|
|
||||||
const token = jwt.sign(
|
|
||||||
{ userId: user.id, username: user.username, role: user.role },
|
|
||||||
process.env.JWT_SECRET,
|
|
||||||
{ expiresIn: '24h' }
|
|
||||||
);
|
|
||||||
|
|
||||||
// 返回用户信息和令牌
|
|
||||||
res.json({
|
|
||||||
accessToken: token,
|
|
||||||
user: {
|
|
||||||
id: user.id,
|
|
||||||
username: user.username,
|
|
||||||
name: user.name,
|
|
||||||
email: user.email,
|
|
||||||
role: user.role
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('登录错误:', error);
|
|
||||||
res.status(500).json({ error: '服务器内部错误' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 获取用户信息
|
|
||||||
router.get('/profile', authenticateToken, async (req, res) => {
|
|
||||||
try {
|
|
||||||
const user = await db.get('SELECT id, username, name, email, role, created_at FROM users WHERE id = ?', [req.user.id]);
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return res.status(404).json({ error: '用户不存在' });
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json(user);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取用户信息错误:', error);
|
|
||||||
res.status(500).json({ error: '服务器内部错误' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 修改密码
|
|
||||||
router.post('/change-password', authenticateToken, async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { currentPassword, newPassword } = req.body;
|
|
||||||
|
|
||||||
if (!currentPassword || !newPassword) {
|
|
||||||
return res.status(400).json({ error: '当前密码和新密码不能为空' });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newPassword.length < 6) {
|
|
||||||
return res.status(400).json({ error: '新密码长度至少为6位' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 查询用户
|
|
||||||
const user = await db.get('SELECT password FROM users WHERE id = ?', [req.user.id]);
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return res.status(404).json({ error: '用户不存在' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证当前密码
|
|
||||||
const isValidPassword = await bcrypt.compare(currentPassword, user.password);
|
|
||||||
|
|
||||||
if (!isValidPassword) {
|
|
||||||
return res.status(401).json({ error: '当前密码错误' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 哈希新密码
|
|
||||||
const hashedPassword = await bcrypt.hash(newPassword, 10);
|
|
||||||
|
|
||||||
// 更新密码
|
|
||||||
await db.run('UPDATE users SET password = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?', [hashedPassword, req.user.id]);
|
|
||||||
|
|
||||||
res.json({ message: '密码修改成功' });
|
|
||||||
} catch (error) {
|
|
||||||
console.error('修改密码错误:', error);
|
|
||||||
res.status(500).json({ error: '服务器内部错误' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 更新用户资料
|
|
||||||
router.put('/profile', authenticateToken, async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { name, email } = req.body;
|
|
||||||
|
|
||||||
// 验证输入
|
|
||||||
if (!name) {
|
|
||||||
return res.status(400).json({ error: '姓名不能为空' });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (email && !email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) {
|
|
||||||
return res.status(400).json({ error: '邮箱格式不正确' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新用户资料
|
|
||||||
await db.run('UPDATE users SET name = ?, email = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?', [name, email, req.user.id]);
|
|
||||||
|
|
||||||
// 获取更新后的用户资料
|
|
||||||
const updatedUser = await db.get('SELECT id, username, name, email, role, created_at FROM users WHERE id = ?', [req.user.id]);
|
|
||||||
|
|
||||||
res.json(updatedUser);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('更新用户资料错误:', error);
|
|
||||||
res.status(500).json({ error: '服务器内部错误' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = router;
|
|
||||||
144
routes/auth.ts
Normal file
144
routes/auth.ts
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
import express, { Request, Response } from 'express';
|
||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
import bcrypt from 'bcryptjs';
|
||||||
|
import db from '../utils/database';
|
||||||
|
import { authenticateToken } from '../middleware/auth';
|
||||||
|
import { User, LoginRequest, ChangePasswordRequest } from '../types';
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.post('/login', async (req: Request<{}, {}, LoginRequest>, res: Response): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const { username, password } = req.body;
|
||||||
|
|
||||||
|
if (!username || !password) {
|
||||||
|
res.status(400).json({ error: '用户名和密码不能为空' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await db.get<User>('SELECT * FROM users WHERE username = ?', [username]);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
res.status(401).json({ error: '用户名或密码错误' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValidPassword = await bcrypt.compare(password, user.password);
|
||||||
|
|
||||||
|
if (!isValidPassword) {
|
||||||
|
res.status(401).json({ error: '用户名或密码错误' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = jwt.sign(
|
||||||
|
{ userId: user.id, username: user.username, role: user.role },
|
||||||
|
process.env.JWT_SECRET!,
|
||||||
|
{ expiresIn: '24h' }
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
accessToken: token,
|
||||||
|
user: {
|
||||||
|
id: user.id,
|
||||||
|
username: user.username,
|
||||||
|
name: user.name,
|
||||||
|
email: user.email,
|
||||||
|
role: user.role
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('登录错误:', error);
|
||||||
|
res.status(500).json({ error: '服务器内部错误' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/profile', authenticateToken, async (req: Request, res: Response): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const user = await db.get<User>(
|
||||||
|
'SELECT id, username, name, email, role, created_at FROM users WHERE id = ?',
|
||||||
|
[req.user!.id]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
res.status(404).json({ error: '用户不存在' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(user);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取用户信息错误:', error);
|
||||||
|
res.status(500).json({ error: '服务器内部错误' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/change-password', authenticateToken, async (req: Request<{}, {}, ChangePasswordRequest>, res: Response): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const { currentPassword, newPassword } = req.body;
|
||||||
|
|
||||||
|
if (!currentPassword || !newPassword) {
|
||||||
|
res.status(400).json({ error: '当前密码和新密码不能为空' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newPassword.length < 6) {
|
||||||
|
res.status(400).json({ error: '新密码长度至少为6位' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await db.get<Pick<User, 'password'>>('SELECT password FROM users WHERE id = ?', [req.user!.id]);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
res.status(404).json({ error: '用户不存在' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValidPassword = await bcrypt.compare(currentPassword, user.password);
|
||||||
|
|
||||||
|
if (!isValidPassword) {
|
||||||
|
res.status(401).json({ error: '当前密码错误' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hashedPassword = await bcrypt.hash(newPassword, 10);
|
||||||
|
|
||||||
|
await db.run('UPDATE users SET password = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?', [hashedPassword, req.user!.id]);
|
||||||
|
|
||||||
|
res.json({ message: '密码修改成功' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('修改密码错误:', error);
|
||||||
|
res.status(500).json({ error: '服务器内部错误' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.put('/profile', authenticateToken, async (req: Request, res: Response): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const { name, email } = req.body;
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
res.status(400).json({ error: '姓名不能为空' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (email && !email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) {
|
||||||
|
res.status(400).json({ error: '邮箱格式不正确' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.run(
|
||||||
|
'UPDATE users SET name = ?, email = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
|
||||||
|
[name, email, req.user!.id]
|
||||||
|
);
|
||||||
|
|
||||||
|
const updatedUser = await db.get<User>(
|
||||||
|
'SELECT id, username, name, email, role, created_at FROM users WHERE id = ?',
|
||||||
|
[req.user!.id]
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json(updatedUser);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新用户资料错误:', error);
|
||||||
|
res.status(500).json({ error: '服务器内部错误' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -1,23 +1,18 @@
|
|||||||
const express = require('express');
|
import express, { Request, Response } from 'express';
|
||||||
const db = require('../utils/database');
|
import db from '../utils/database';
|
||||||
const { authenticateToken, requireAdmin } = require('../middleware/auth');
|
import { authenticateToken, requireAdmin } from '../middleware/auth';
|
||||||
|
import { PaginationQuery, UpdateCompanyRequest, CompanyDetail, StatsOverview, MonthlyStat, SerialListItem, CompanyListItem } from '../types';
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
// 获取企业列表
|
router.get('/', authenticateToken, requireAdmin, async (req: Request<{}, {}, {}, PaginationQuery>, res: Response): Promise<void> => {
|
||||||
router.get('/', authenticateToken, requireAdmin, async (req, res) => {
|
|
||||||
try {
|
try {
|
||||||
const { page = 1, limit = 20, search = '' } = req.query;
|
const { page = 1, limit = 20, search = '' } = req.query;
|
||||||
const offset = (page - 1) * limit;
|
const offset = (page - 1) * limit;
|
||||||
|
|
||||||
let query = `
|
let query = 'SELECT c.company_name, c.created_at as first_created, c.updated_at as last_created, c.is_active, (SELECT COUNT(*) FROM serials s WHERE s.company_name = c.company_name) as serial_count, (SELECT COUNT(*) FROM serials s WHERE s.company_name = c.company_name AND s.is_active = 1) as active_count FROM companies c';
|
||||||
SELECT c.company_name, c.created_at as first_created, c.updated_at as last_created, c.is_active,
|
|
||||||
(SELECT COUNT(*) FROM serials s WHERE s.company_name = c.company_name) as serial_count,
|
|
||||||
(SELECT COUNT(*) FROM serials s WHERE s.company_name = c.company_name AND s.is_active = 1) as active_count
|
|
||||||
FROM companies c
|
|
||||||
`;
|
|
||||||
let countQuery = 'SELECT COUNT(*) as total FROM companies';
|
let countQuery = 'SELECT COUNT(*) as total FROM companies';
|
||||||
let params = [];
|
let params: any[] = [];
|
||||||
|
|
||||||
if (search) {
|
if (search) {
|
||||||
query += ' WHERE c.company_name LIKE ?';
|
query += ' WHERE c.company_name LIKE ?';
|
||||||
@@ -26,19 +21,19 @@ router.get('/', authenticateToken, requireAdmin, async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
query += ' ORDER BY c.updated_at DESC LIMIT ? OFFSET ?';
|
query += ' ORDER BY c.updated_at DESC LIMIT ? OFFSET ?';
|
||||||
params.push(parseInt(limit), parseInt(offset));
|
params.push(parseInt(limit.toString()), parseInt(offset.toString()));
|
||||||
|
|
||||||
const [companies, countResult] = await Promise.all([
|
const [companies, countResult] = await Promise.all([
|
||||||
db.all(query, params),
|
db.all(query, params),
|
||||||
db.get(countQuery, params.slice(0, -2))
|
db.get<{ total: number }>(countQuery, params.slice(0, -2))
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const total = countResult ? countResult.total : 0;
|
const total = countResult?.total || 0;
|
||||||
const totalPages = Math.ceil(total / limit);
|
const totalPages = Math.ceil(total / limit);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
message: '获取企业列表成功',
|
message: '获取企业列表成功',
|
||||||
data: companies.map(company => ({
|
data: companies.map((company: any) => ({
|
||||||
companyName: company.company_name,
|
companyName: company.company_name,
|
||||||
firstCreated: company.first_created,
|
firstCreated: company.first_created,
|
||||||
lastCreated: company.last_created,
|
lastCreated: company.last_created,
|
||||||
@@ -47,8 +42,8 @@ router.get('/', authenticateToken, requireAdmin, async (req, res) => {
|
|||||||
status: company.is_active ? 'active' : 'disabled'
|
status: company.is_active ? 'active' : 'disabled'
|
||||||
})),
|
})),
|
||||||
pagination: {
|
pagination: {
|
||||||
page: parseInt(page),
|
page: parseInt(page.toString()),
|
||||||
limit: parseInt(limit),
|
limit: parseInt(limit.toString()),
|
||||||
total,
|
total,
|
||||||
totalPages
|
totalPages
|
||||||
}
|
}
|
||||||
@@ -59,22 +54,20 @@ router.get('/', authenticateToken, requireAdmin, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 获取企业详情
|
router.get('/:companyName', authenticateToken, requireAdmin, async (req: Request<{ companyName: string }, {}, {}, PaginationQuery>, res: Response): Promise<void> => {
|
||||||
router.get('/:companyName', authenticateToken, requireAdmin, async (req, res) => {
|
|
||||||
try {
|
try {
|
||||||
const { companyName } = req.params;
|
const { companyName } = req.params;
|
||||||
const decodedCompanyName = decodeURIComponent(companyName);
|
const decodedCompanyName = decodeURIComponent(companyName);
|
||||||
const { page = 1, limit = 20 } = req.query;
|
const { page = 1, limit = 20 } = req.query;
|
||||||
const offset = (page - 1) * limit;
|
const offset = (page - 1) * limit;
|
||||||
|
|
||||||
// 获取企业基本信息
|
|
||||||
const companyInfo = await db.get('SELECT * FROM companies WHERE company_name = ?', [decodedCompanyName]);
|
const companyInfo = await db.get('SELECT * FROM companies WHERE company_name = ?', [decodedCompanyName]);
|
||||||
|
|
||||||
if (!companyInfo) {
|
if (!companyInfo) {
|
||||||
return res.status(404).json({ error: '企业不存在' });
|
res.status(404).json({ error: '企业不存在' });
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取序列号统计信息
|
|
||||||
const serialStats = await db.get(`
|
const serialStats = await db.get(`
|
||||||
SELECT COUNT(*) as serial_count,
|
SELECT COUNT(*) as serial_count,
|
||||||
SUM(CASE WHEN is_active = 1 THEN 1 ELSE 0 END) as active_count,
|
SUM(CASE WHEN is_active = 1 THEN 1 ELSE 0 END) as active_count,
|
||||||
@@ -84,7 +77,6 @@ router.get('/:companyName', authenticateToken, requireAdmin, async (req, res) =>
|
|||||||
WHERE company_name = ?
|
WHERE company_name = ?
|
||||||
`, [decodedCompanyName]);
|
`, [decodedCompanyName]);
|
||||||
|
|
||||||
// 获取企业的序列号列表
|
|
||||||
const serials = await db.all(`
|
const serials = await db.all(`
|
||||||
SELECT s.*, u.name as created_by_name
|
SELECT s.*, u.name as created_by_name
|
||||||
FROM serials s
|
FROM serials s
|
||||||
@@ -92,10 +84,9 @@ router.get('/:companyName', authenticateToken, requireAdmin, async (req, res) =>
|
|||||||
WHERE s.company_name = ?
|
WHERE s.company_name = ?
|
||||||
ORDER BY s.created_at DESC
|
ORDER BY s.created_at DESC
|
||||||
LIMIT ? OFFSET ?
|
LIMIT ? OFFSET ?
|
||||||
`, [decodedCompanyName, parseInt(limit), parseInt(offset)]);
|
`, [decodedCompanyName, parseInt(limit.toString()), parseInt(offset.toString())]);
|
||||||
|
|
||||||
// 获取统计数据
|
const stats = await db.all<{ month: string; count: number }>(`
|
||||||
const stats = await db.all(`
|
|
||||||
SELECT strftime('%Y-%m', created_at) as month,
|
SELECT strftime('%Y-%m', created_at) as month,
|
||||||
COUNT(*) as count
|
COUNT(*) as count
|
||||||
FROM serials
|
FROM serials
|
||||||
@@ -113,10 +104,10 @@ router.get('/:companyName', authenticateToken, requireAdmin, async (req, res) =>
|
|||||||
activeCount: serialStats?.active_count || 0,
|
activeCount: serialStats?.active_count || 0,
|
||||||
disabledCount: serialStats?.disabled_count || 0,
|
disabledCount: serialStats?.disabled_count || 0,
|
||||||
expiredCount: serialStats?.expired_count || 0,
|
expiredCount: serialStats?.expired_count || 0,
|
||||||
firstCreated: companyInfo.created_at,
|
firstCreated: (companyInfo as any).created_at,
|
||||||
lastCreated: companyInfo.updated_at,
|
lastCreated: (companyInfo as any).updated_at,
|
||||||
status: companyInfo.is_active ? 'active' : 'disabled',
|
status: (companyInfo as any).is_active ? 'active' : 'disabled',
|
||||||
serials: serials.map(s => ({
|
serials: serials.map((s: any) => ({
|
||||||
serialNumber: s.serial_number,
|
serialNumber: s.serial_number,
|
||||||
validUntil: s.valid_until,
|
validUntil: s.valid_until,
|
||||||
isActive: s.is_active,
|
isActive: s.is_active,
|
||||||
@@ -135,38 +126,37 @@ router.get('/:companyName', authenticateToken, requireAdmin, async (req, res) =>
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 更新企业信息
|
router.patch('/:companyName', authenticateToken, requireAdmin, async (req: Request<{ companyName: string }, {}, UpdateCompanyRequest>, res: Response): Promise<void> => {
|
||||||
router.patch('/:companyName', authenticateToken, requireAdmin, async (req, res) => {
|
|
||||||
try {
|
try {
|
||||||
const { companyName } = req.params;
|
const { companyName } = req.params;
|
||||||
const decodedCompanyName = decodeURIComponent(companyName);
|
const decodedCompanyName = decodeURIComponent(companyName);
|
||||||
const { newCompanyName } = req.body;
|
const { newCompanyName } = req.body;
|
||||||
|
|
||||||
if (!newCompanyName || newCompanyName.trim() === '') {
|
if (!newCompanyName || newCompanyName.trim() === '') {
|
||||||
return res.status(400).json({ error: '新企业名称不能为空' });
|
res.status(400).json({ error: '新企业名称不能为空' });
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查企业是否存在
|
|
||||||
const existingCompany = db.get(
|
const existingCompany = db.get(
|
||||||
'SELECT COUNT(*) as count FROM serials WHERE company_name = ?',
|
'SELECT COUNT(*) as count FROM serials WHERE company_name = ?',
|
||||||
[decodedCompanyName]
|
[decodedCompanyName]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!existingCompany || existingCompany.count === 0) {
|
if (!existingCompany || existingCompany.count === 0) {
|
||||||
return res.status(404).json({ error: '企业不存在' });
|
res.status(404).json({ error: '企业不存在' });
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查新企业名称是否已存在
|
|
||||||
const duplicateCompany = db.get(
|
const duplicateCompany = db.get(
|
||||||
'SELECT COUNT(*) as count FROM serials WHERE company_name = ?',
|
'SELECT COUNT(*) as count FROM serials WHERE company_name = ?',
|
||||||
[newCompanyName]
|
[newCompanyName]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (duplicateCompany && duplicateCompany.count > 0) {
|
if (duplicateCompany && duplicateCompany.count > 0) {
|
||||||
return res.status(400).json({ error: '企业名称已存在' });
|
res.status(400).json({ error: '企业名称已存在' });
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新企业名称
|
|
||||||
db.run(
|
db.run(
|
||||||
'UPDATE serials SET company_name = ?, updated_at = CURRENT_TIMESTAMP WHERE company_name = ?',
|
'UPDATE serials SET company_name = ?, updated_at = CURRENT_TIMESTAMP WHERE company_name = ?',
|
||||||
[newCompanyName, decodedCompanyName]
|
[newCompanyName, decodedCompanyName]
|
||||||
@@ -185,45 +175,38 @@ router.patch('/:companyName', authenticateToken, requireAdmin, async (req, res)
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 删除企业(物理删除,完全删除企业和所有序列号)
|
router.delete('/:companyName', authenticateToken, requireAdmin, async (req: Request<{ companyName: string }>, res: Response): Promise<void> => {
|
||||||
router.delete('/:companyName', authenticateToken, requireAdmin, async (req, res) => {
|
|
||||||
try {
|
try {
|
||||||
const { companyName } = req.params;
|
const { companyName } = req.params;
|
||||||
console.log('原始参数:', companyName);
|
|
||||||
const decodedCompanyName = decodeURIComponent(companyName);
|
const decodedCompanyName = decodeURIComponent(companyName);
|
||||||
console.log('解码后参数:', decodedCompanyName);
|
|
||||||
|
|
||||||
// 检查企业是否存在
|
|
||||||
const existingCompany = await db.get(
|
const existingCompany = await db.get(
|
||||||
'SELECT * FROM companies WHERE company_name = ?',
|
'SELECT * FROM companies WHERE company_name = ?',
|
||||||
[decodedCompanyName]
|
[decodedCompanyName]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!existingCompany) {
|
if (!existingCompany) {
|
||||||
return res.status(404).json({ error: '企业不存在' });
|
res.status(404).json({ error: '企业不存在' });
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 开始事务
|
|
||||||
db.run('BEGIN TRANSACTION');
|
db.run('BEGIN TRANSACTION');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 删除该企业的所有序列号
|
|
||||||
const serialDeleteResult = db.run(
|
const serialDeleteResult = db.run(
|
||||||
'DELETE FROM serials WHERE company_name = ?',
|
'DELETE FROM serials WHERE company_name = ?',
|
||||||
[decodedCompanyName]
|
[decodedCompanyName]
|
||||||
);
|
);
|
||||||
console.log('删除序列号结果:', serialDeleteResult);
|
|
||||||
|
|
||||||
// 删除企业记录
|
|
||||||
const companyDeleteResult = db.run(
|
const companyDeleteResult = db.run(
|
||||||
'DELETE FROM companies WHERE company_name = ?',
|
'DELETE FROM companies WHERE company_name = ?',
|
||||||
[decodedCompanyName]
|
[decodedCompanyName]
|
||||||
);
|
);
|
||||||
console.log('删除企业结果:', companyDeleteResult);
|
|
||||||
|
|
||||||
if (companyDeleteResult.changes === 0) {
|
if (companyDeleteResult.changes === 0) {
|
||||||
db.run('ROLLBACK');
|
db.run('ROLLBACK');
|
||||||
return res.status(404).json({ error: '企业不存在' });
|
res.status(404).json({ error: '企业不存在' });
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
db.run('COMMIT');
|
db.run('COMMIT');
|
||||||
@@ -247,23 +230,18 @@ router.delete('/:companyName', authenticateToken, requireAdmin, async (req, res)
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 获取企业统计数据
|
router.get('/stats/overview', authenticateToken, requireAdmin, async (req: Request, res: Response): Promise<void> => {
|
||||||
router.get('/stats/overview', authenticateToken, requireAdmin, async (req, res) => {
|
|
||||||
try {
|
try {
|
||||||
// 获取总企业数
|
const companyCount = await db.get<{ count: number }>('SELECT COUNT(*) as count FROM companies');
|
||||||
const companyCount = await db.get('SELECT COUNT(*) as count FROM companies');
|
|
||||||
|
|
||||||
// 获取总序列号数
|
const serialCount = await db.get<{ count: number }>('SELECT COUNT(*) as count FROM serials');
|
||||||
const serialCount = await db.get('SELECT COUNT(*) as count FROM serials');
|
|
||||||
|
|
||||||
// 获取活跃序列号数
|
const activeCount = await db.get<{ count: number }>(`
|
||||||
const activeCount = await db.get(`
|
|
||||||
SELECT COUNT(*) as count FROM serials
|
SELECT COUNT(*) as count FROM serials
|
||||||
WHERE is_active = 1 AND (valid_until IS NULL OR valid_until > datetime('now'))
|
WHERE is_active = 1 AND (valid_until IS NULL OR valid_until > datetime('now'))
|
||||||
`);
|
`);
|
||||||
|
|
||||||
// 按月份统计 - 使用正确的日期格式
|
const monthlyStats = await db.all<{ month: string; company_count: number; serial_count: number }>(`
|
||||||
const monthlyStats = await db.all(`
|
|
||||||
SELECT strftime('%Y-%m', created_at) as month,
|
SELECT strftime('%Y-%m', created_at) as month,
|
||||||
COUNT(DISTINCT company_name) as company_count,
|
COUNT(DISTINCT company_name) as company_count,
|
||||||
COUNT(*) as serial_count
|
COUNT(*) as serial_count
|
||||||
@@ -273,29 +251,28 @@ router.get('/stats/overview', authenticateToken, requireAdmin, async (req, res)
|
|||||||
ORDER BY month ASC
|
ORDER BY month ASC
|
||||||
`);
|
`);
|
||||||
|
|
||||||
// 获取最新添加的企业
|
const recentCompanies = await db.all<{ company_name: string; last_created: string; is_active: number }>(`
|
||||||
const recentCompanies = await db.all(`
|
|
||||||
SELECT c.company_name, c.created_at as last_created, c.is_active
|
SELECT c.company_name, c.created_at as last_created, c.is_active
|
||||||
FROM companies c
|
FROM companies c
|
||||||
ORDER BY c.updated_at DESC
|
ORDER BY c.updated_at DESC
|
||||||
LIMIT 10
|
LIMIT 10
|
||||||
`);
|
`);
|
||||||
|
|
||||||
// 获取最近生成的序列号
|
const recentSerials = await db.all<{ serial_number: string; company_name: string; is_active: number; created_at: string }>(`
|
||||||
const recentSerials = await db.all(`
|
|
||||||
SELECT s.serial_number, s.company_name, s.is_active, s.created_at
|
SELECT s.serial_number, s.company_name, s.is_active, s.created_at
|
||||||
FROM serials s
|
FROM serials s
|
||||||
ORDER BY s.created_at DESC
|
ORDER BY s.created_at DESC
|
||||||
LIMIT 10
|
LIMIT 10
|
||||||
`);
|
`);
|
||||||
|
|
||||||
// 如果没有数据,生成过去12个月的空数据
|
let finalMonthlyStats = monthlyStats;
|
||||||
if (monthlyStats.length === 0) {
|
if (monthlyStats.length === 0) {
|
||||||
|
finalMonthlyStats = [];
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
for (let i = 11; i >=0; i--) {
|
for (let i = 11; i >= 0; i--) {
|
||||||
const date = new Date(now.getFullYear(), now.getMonth() - i, 1);
|
const date = new Date(now.getFullYear(), now.getMonth() - i, 1);
|
||||||
const month = date.toISOString().substr(0, 7);
|
const month = date.toISOString().substr(0, 7);
|
||||||
monthlyStats.push({
|
finalMonthlyStats.push({
|
||||||
month,
|
month,
|
||||||
company_count: 0,
|
company_count: 0,
|
||||||
serial_count: 0
|
serial_count: 0
|
||||||
@@ -307,12 +284,12 @@ router.get('/stats/overview', authenticateToken, requireAdmin, async (req, res)
|
|||||||
message: '获取统计数据成功',
|
message: '获取统计数据成功',
|
||||||
data: {
|
data: {
|
||||||
overview: {
|
overview: {
|
||||||
totalCompanies: companyCount.count || 0,
|
totalCompanies: companyCount?.count || 0,
|
||||||
totalSerials: serialCount.count || 0,
|
totalSerials: serialCount?.count || 0,
|
||||||
activeSerials: activeCount.count || 0,
|
activeSerials: activeCount?.count || 0,
|
||||||
inactiveSerials: (serialCount.count || 0) - (activeCount.count || 0)
|
inactiveSerials: (serialCount?.count || 0) - (activeCount?.count || 0)
|
||||||
},
|
},
|
||||||
monthlyStats: monthlyStats.map(stat => ({
|
monthlyStats: finalMonthlyStats.map(stat => ({
|
||||||
month: stat.month,
|
month: stat.month,
|
||||||
company_count: stat.company_count,
|
company_count: stat.company_count,
|
||||||
serial_count: stat.serial_count
|
serial_count: stat.serial_count
|
||||||
@@ -325,7 +302,7 @@ router.get('/stats/overview', authenticateToken, requireAdmin, async (req, res)
|
|||||||
recentSerials: recentSerials.map(s => ({
|
recentSerials: recentSerials.map(s => ({
|
||||||
serialNumber: s.serial_number,
|
serialNumber: s.serial_number,
|
||||||
companyName: s.company_name,
|
companyName: s.company_name,
|
||||||
isActive: s.is_active,
|
isActive: !!s.is_active,
|
||||||
createdAt: s.created_at
|
createdAt: s.created_at
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@@ -336,57 +313,53 @@ router.get('/stats/overview', authenticateToken, requireAdmin, async (req, res)
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 吊销单个序列号
|
router.delete('/:companyName/serials/:serialNumber', authenticateToken, requireAdmin, async (req: Request<{ companyName: string; serialNumber: string }>, res: Response): Promise<void> => {
|
||||||
router.delete('/:companyName/serials/:serialNumber', authenticateToken, requireAdmin, async (req, res) => {
|
|
||||||
try {
|
try {
|
||||||
const { companyName, serialNumber } = req.params;
|
const { companyName, serialNumber } = req.params;
|
||||||
|
|
||||||
// 检查序列号是否存在且属于该企业
|
|
||||||
const serial = await db.get(
|
const serial = await db.get(
|
||||||
'SELECT * FROM serials WHERE serial_number = ? AND company_name = ?',
|
'SELECT * FROM serials WHERE serial_number = ? AND company_name = ?',
|
||||||
[serialNumber.toUpperCase(), companyName]
|
[serialNumber.toUpperCase(), companyName]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!serial) {
|
if (!serial) {
|
||||||
return res.status(404).json({ error: '序列号不存在或不属于该企业' });
|
res.status(404).json({ error: '序列号不存在或不属于该企业' });
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 物理删除序列号
|
|
||||||
await db.run(
|
await db.run(
|
||||||
'DELETE FROM serials WHERE serial_number = ? AND company_name = ?',
|
'DELETE FROM serials WHERE serial_number = ? AND company_name = ?',
|
||||||
[serialNumber.toUpperCase(), companyName]
|
[serialNumber.toUpperCase(), companyName]
|
||||||
);
|
);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
message: '序列号已成功吊删除',
|
message: '序列号已成功删除',
|
||||||
data: {
|
data: {
|
||||||
serialNumber: serial.serial_number,
|
serialNumber: serialNumber.toUpperCase(),
|
||||||
companyName
|
companyName
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('吊销序列号错误:', error);
|
console.error('删除序列号错误:', error);
|
||||||
res.status(500).json({ error: '服务器内部错误' });
|
res.status(500).json({ error: '服务器内部错误' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 吊销企业
|
router.post('/:companyName/revoke', authenticateToken, requireAdmin, async (req: Request<{ companyName: string }>, res: Response): Promise<void> => {
|
||||||
router.post('/:companyName/revoke', authenticateToken, requireAdmin, async (req, res) => {
|
|
||||||
try {
|
try {
|
||||||
const { companyName } = req.params;
|
const { companyName } = req.params;
|
||||||
const decodedCompanyName = decodeURIComponent(companyName);
|
const decodedCompanyName = decodeURIComponent(companyName);
|
||||||
|
|
||||||
// 检查企业是否存在
|
|
||||||
const existingCompany = await db.get(
|
const existingCompany = await db.get(
|
||||||
'SELECT COUNT(*) as count FROM serials WHERE company_name = ?',
|
'SELECT COUNT(*) as count FROM serials WHERE company_name = ?',
|
||||||
[decodedCompanyName]
|
[decodedCompanyName]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!existingCompany || existingCompany.count === 0) {
|
if (!existingCompany || existingCompany.count === 0) {
|
||||||
return res.status(404).json({ error: '企业不存在' });
|
res.status(404).json({ error: '企业不存在' });
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 吊销该企业的所有序列号(将 is_active 设为 0)
|
|
||||||
await db.run(
|
await db.run(
|
||||||
'UPDATE serials SET is_active = 0, updated_at = CURRENT_TIMESTAMP WHERE company_name = ?',
|
'UPDATE serials SET is_active = 0, updated_at = CURRENT_TIMESTAMP WHERE company_name = ?',
|
||||||
[decodedCompanyName]
|
[decodedCompanyName]
|
||||||
@@ -404,4 +377,4 @@ router.post('/:companyName/revoke', authenticateToken, requireAdmin, async (req,
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = router;
|
export default router;
|
||||||
@@ -1,51 +1,44 @@
|
|||||||
const express = require('express');
|
import express, { Request, Response } from 'express';
|
||||||
const QRCode = require('qrcode');
|
import QRCode from 'qrcode';
|
||||||
const db = require('../utils/database');
|
import db from '../utils/database';
|
||||||
const { authenticateToken, requireAdmin } = require('../middleware/auth');
|
import { authenticateToken, requireAdmin } from '../middleware/auth';
|
||||||
|
import { GenerateSerialRequest, GenerateSerialWithPrefixRequest, QRCodeRequest, UpdateSerialRequest, PaginationQuery, SerialListItem } from '../types';
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
// 生成序列号
|
router.post('/generate', authenticateToken, requireAdmin, async (req: Request<{}, {}, GenerateSerialRequest>, res: Response): Promise<void> => {
|
||||||
router.post('/generate', authenticateToken, requireAdmin, async (req, res) => {
|
|
||||||
try {
|
try {
|
||||||
const { companyName, quantity = 1, validDays = 365 } = req.body;
|
const { companyName, quantity = 1, validDays = 365 } = req.body;
|
||||||
|
|
||||||
if (!companyName) {
|
if (!companyName) {
|
||||||
return res.status(400).json({ error: '企业名称不能为空' });
|
res.status(400).json({ error: '企业名称不能为空' });
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (quantity < 1 || quantity > 100) {
|
if (quantity < 1 || quantity > 100) {
|
||||||
return res.status(400).json({ error: '生成数量必须在1-100之间' });
|
res.status(400).json({ error: '生成数量必须在1-100之间' });
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算有效期
|
|
||||||
const validUntil = new Date();
|
const validUntil = new Date();
|
||||||
validUntil.setDate(validUntil.getDate() + validDays);
|
validUntil.setDate(validUntil.getDate() + validDays);
|
||||||
|
|
||||||
// 确保企业存在
|
const existingCompany = await db.get('xSELECT * FROM companies WHERE company_name = ?', [companyName]);
|
||||||
const existingCompany = await db.get('SELECT * FROM companies WHERE company_name = ?', [companyName]);
|
|
||||||
if (!existingCompany) {
|
if (!existingCompany) {
|
||||||
await db.run('INSERT INTO companies (company_name, is_active) VALUES (?, 1)', [companyName]);
|
await db.run('INSERT INTO companies (company_name, is_active) VALUES (?, 1)', [companyName]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成序列号
|
const serials: SerialListItem[] = [];
|
||||||
const serials = [];
|
|
||||||
const prefix = 'BF';
|
const prefix = 'BF';
|
||||||
const datePart = new Date().getFullYear().toString().substr(2);
|
const datePart = new Date().getFullYear().toString().substr(2);
|
||||||
|
|
||||||
// 批量插入序列号
|
|
||||||
const insertPromises = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < quantity; i++) {
|
for (let i = 0; i < quantity; i++) {
|
||||||
// 使用随机数生成序列号,避免重复
|
|
||||||
const randomPart = Math.floor(Math.random() * 1000000).toString().padStart(6, '0');
|
const randomPart = Math.floor(Math.random() * 1000000).toString().padStart(6, '0');
|
||||||
const serialNumber = `${prefix}${datePart}${randomPart}`;
|
const serialNumber = `${prefix}${datePart}${randomPart}`;
|
||||||
|
|
||||||
insertPromises.push(
|
await db.run(
|
||||||
db.run(
|
|
||||||
'INSERT INTO serials (serial_number, company_name, valid_until, created_by) VALUES (?, ?, ?, ?)',
|
'INSERT INTO serials (serial_number, company_name, valid_until, created_by) VALUES (?, ?, ?, ?)',
|
||||||
[serialNumber, companyName, validUntil.toISOString().slice(0, 19).replace('T', ' '), req.user.id]
|
[serialNumber, companyName, validUntil.toISOString().slice(0, 19).replace('T', ' '), req.user!.id]
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
serials.push({
|
serials.push({
|
||||||
@@ -56,8 +49,6 @@ router.post('/generate', authenticateToken, requireAdmin, async (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all(insertPromises);
|
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
message: `成功生成${quantity}个序列号`,
|
message: `成功生成${quantity}个序列号`,
|
||||||
serials
|
serials
|
||||||
@@ -68,36 +59,36 @@ router.post('/generate', authenticateToken, requireAdmin, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 生成二维码
|
router.post('/:serialNumber/qrcode', authenticateToken, async (req: Request<{ serialNumber: string }, {}, QRCodeRequest>, res: Response): Promise<void> => {
|
||||||
router.post('/:serialNumber/qrcode', authenticateToken, async (req, res) => {
|
|
||||||
try {
|
try {
|
||||||
const { serialNumber } = req.params;
|
const { serialNumber } = req.params;
|
||||||
let { baseUrl } = req.body;
|
let { baseUrl } = req.body;
|
||||||
|
|
||||||
if (!serialNumber) {
|
if (!serialNumber) {
|
||||||
return res.status(400).json({ error: '序列号不能为空' });
|
res.status(400).json({ error: '序列号不能为空' });
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证序列号是否存在
|
const serial = await db.get<{ serial_number: string; company_name: string; is_active: number; valid_until: string | null }>(
|
||||||
const serial = await db.get(
|
|
||||||
'SELECT s.*, u.name as created_by_name FROM serials s LEFT JOIN users u ON s.created_by = u.id WHERE s.serial_number = ?',
|
'SELECT s.*, u.name as created_by_name FROM serials s LEFT JOIN users u ON s.created_by = u.id WHERE s.serial_number = ?',
|
||||||
[serialNumber.toUpperCase()]
|
[serialNumber.toUpperCase()]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!serial) {
|
if (!serial) {
|
||||||
return res.status(404).json({ error: '序列号不存在' });
|
res.status(404).json({ error: '序列号不存在' });
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!serial.is_active) {
|
if (!serial.is_active) {
|
||||||
return res.status(400).json({ error: '序列号已被禁用' });
|
res.status(400).json({ error: '序列号已被禁用' });
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否过期
|
|
||||||
if (serial.valid_until && new Date(serial.valid_until) < new Date()) {
|
if (serial.valid_until && new Date(serial.valid_until) < new Date()) {
|
||||||
return res.status(400).json({ error: '序列号已过期' });
|
res.status(400).json({ error: '序列号已过期' });
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成查询URL
|
|
||||||
if (!baseUrl) {
|
if (!baseUrl) {
|
||||||
baseUrl = `${req.protocol}://${req.get('host')}/query.html`;
|
baseUrl = `${req.protocol}://${req.get('host')}/query.html`;
|
||||||
}
|
}
|
||||||
@@ -106,7 +97,6 @@ router.post('/:serialNumber/qrcode', authenticateToken, async (req, res) => {
|
|||||||
? `${baseUrl}&serial=${serial.serial_number}`
|
? `${baseUrl}&serial=${serial.serial_number}`
|
||||||
: `${baseUrl}?serial=${serial.serial_number}`;
|
: `${baseUrl}?serial=${serial.serial_number}`;
|
||||||
|
|
||||||
// 生成二维码
|
|
||||||
const qrCodeData = await QRCode.toDataURL(queryUrl, {
|
const qrCodeData = await QRCode.toDataURL(queryUrl, {
|
||||||
width: 200,
|
width: 200,
|
||||||
color: {
|
color: {
|
||||||
@@ -129,28 +119,28 @@ router.post('/:serialNumber/qrcode', authenticateToken, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 查询序列号
|
router.get('/:serialNumber/query', async (req: Request<{ serialNumber: string }>, res: Response): Promise<void> => {
|
||||||
router.get('/:serialNumber/query', async (req, res) => {
|
|
||||||
try {
|
try {
|
||||||
const { serialNumber } = req.params;
|
const { serialNumber } = req.params;
|
||||||
|
|
||||||
if (!serialNumber) {
|
if (!serialNumber) {
|
||||||
return res.status(400).json({ error: '序列号不能为空' });
|
res.status(400).json({ error: '序列号不能为空' });
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查询序列号
|
const serial = await db.get<{ serial_number: string; company_name: string; valid_until: string | null; is_active: number; created_at: string; created_by_name: string }>(
|
||||||
const serial = await db.get(
|
|
||||||
'SELECT s.*, u.name as created_by_name FROM serials s LEFT JOIN users u ON s.created_by = u.id WHERE s.serial_number = ?',
|
'SELECT s.*, u.name as created_by_name FROM serials s LEFT JOIN users u ON s.created_by = u.id WHERE s.serial_number = ?',
|
||||||
[serialNumber.toUpperCase()]
|
[serialNumber.toUpperCase()]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!serial) {
|
if (!serial) {
|
||||||
return res.status(404).json({ error: '序列号不存在' });
|
res.status(404).json({ error: '序列号不存在' });
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否过期
|
|
||||||
if (serial.valid_until && new Date(serial.valid_until) < new Date()) {
|
if (serial.valid_until && new Date(serial.valid_until) < new Date()) {
|
||||||
return res.status(400).json({ error: '序列号已过期' });
|
res.status(400).json({ error: '序列号已过期' });
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
@@ -160,7 +150,7 @@ router.get('/:serialNumber/query', async (req, res) => {
|
|||||||
companyName: serial.company_name,
|
companyName: serial.company_name,
|
||||||
validUntil: serial.valid_until,
|
validUntil: serial.valid_until,
|
||||||
status: serial.is_active ? 'active' : 'disabled',
|
status: serial.is_active ? 'active' : 'disabled',
|
||||||
isActive: serial.is_active,
|
isActive: !!serial.is_active,
|
||||||
createdAt: serial.created_at,
|
createdAt: serial.created_at,
|
||||||
createdBy: serial.created_by_name
|
createdBy: serial.created_by_name
|
||||||
}
|
}
|
||||||
@@ -171,19 +161,14 @@ router.get('/:serialNumber/query', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 获取序列号列表
|
router.get('/', authenticateToken, async (req: Request<{}, {}, {}, PaginationQuery>, res: Response): Promise<void> => {
|
||||||
router.get('/', authenticateToken, async (req, res) => {
|
|
||||||
try {
|
try {
|
||||||
const { page = 1, limit = 20, search = '' } = req.query;
|
const { page = 1, limit = 20, search = '' } = req.query;
|
||||||
const offset = (page - 1) * limit;
|
const offset = (page - 1) * limit;
|
||||||
|
|
||||||
let query = `
|
let query = 'SELECT s.*, u.name as created_by_name FROM serials s LEFT JOIN users u ON s.created_by = u.id';
|
||||||
SELECT s.*, u.name as created_by_name
|
|
||||||
FROM serials s
|
|
||||||
LEFT JOIN users u ON s.created_by = u.id
|
|
||||||
`;
|
|
||||||
let countQuery = 'SELECT COUNT(*) as total FROM serials s';
|
let countQuery = 'SELECT COUNT(*) as total FROM serials s';
|
||||||
let params = [];
|
let params: any[] = [];
|
||||||
|
|
||||||
if (search) {
|
if (search) {
|
||||||
query += ' WHERE s.serial_number LIKE ? OR s.company_name LIKE ?';
|
query += ' WHERE s.serial_number LIKE ? OR s.company_name LIKE ?';
|
||||||
@@ -193,19 +178,19 @@ router.get('/', authenticateToken, async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
query += ' ORDER BY s.created_at DESC LIMIT ? OFFSET ?';
|
query += ' ORDER BY s.created_at DESC LIMIT ? OFFSET ?';
|
||||||
params.push(parseInt(limit), parseInt(offset));
|
params.push(parseInt(limit.toString()), parseInt(offset.toString()));
|
||||||
|
|
||||||
const [serials, countResult] = await Promise.all([
|
const [serials, countResult] = await Promise.all([
|
||||||
db.all(query, params),
|
db.all(query, params),
|
||||||
db.get(countQuery, params.slice(0, -2))
|
db.get<{ total: number }>(countQuery, params.slice(0, -2))
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const total = countResult ? countResult.total : 0;
|
const total = countResult?.total || 0;
|
||||||
const totalPages = Math.ceil(total / limit);
|
const totalPages = Math.ceil(total / limit);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
message: '获取序列号列表成功',
|
message: '获取序列号列表成功',
|
||||||
data: serials.map(s => ({
|
data: serials.map((s: any) => ({
|
||||||
serialNumber: s.serial_number,
|
serialNumber: s.serial_number,
|
||||||
companyName: s.company_name,
|
companyName: s.company_name,
|
||||||
validUntil: s.valid_until,
|
validUntil: s.valid_until,
|
||||||
@@ -214,8 +199,8 @@ router.get('/', authenticateToken, async (req, res) => {
|
|||||||
createdBy: s.created_by_name
|
createdBy: s.created_by_name
|
||||||
})),
|
})),
|
||||||
pagination: {
|
pagination: {
|
||||||
page: parseInt(page),
|
page: parseInt(page.toString()),
|
||||||
limit: parseInt(limit),
|
limit: parseInt(limit.toString()),
|
||||||
total,
|
total,
|
||||||
totalPages
|
totalPages
|
||||||
}
|
}
|
||||||
@@ -226,26 +211,25 @@ router.get('/', authenticateToken, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 更新序列号
|
router.patch('/:serialNumber', authenticateToken, requireAdmin, async (req: Request<{ serialNumber: string }, {}, UpdateSerialRequest>, res: Response): Promise<void> => {
|
||||||
router.patch('/:serialNumber', authenticateToken, requireAdmin, async (req, res) => {
|
|
||||||
try {
|
try {
|
||||||
const { serialNumber } = req.params;
|
const { serialNumber } = req.params;
|
||||||
const { companyName, validUntil, isActive } = req.body;
|
const { companyName, validUntil, isActive } = req.body;
|
||||||
|
|
||||||
if (!serialNumber) {
|
if (!serialNumber) {
|
||||||
return res.status(400).json({ error: '序列号不能为空' });
|
res.status(400).json({ error: '序列号不能为空' });
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查序列号是否存在
|
const existingSerial = await db.get<{ is_active: number }>('SELECT * FROM serials WHERE serial_number = ?', [serialNumber.toUpperCase()]);
|
||||||
const existingSerial = await db.get('SELECT * FROM serials WHERE serial_number = ?', [serialNumber.toUpperCase()]);
|
|
||||||
|
|
||||||
if (!existingSerial) {
|
if (!existingSerial) {
|
||||||
return res.status(404).json({ error: '序列号不存在' });
|
res.status(404).json({ error: '序列号不存在' });
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 构建更新字段
|
const updateFields: string[] = [];
|
||||||
const updateFields = [];
|
const params: any[] = [];
|
||||||
const params = [];
|
|
||||||
|
|
||||||
if (companyName !== undefined) {
|
if (companyName !== undefined) {
|
||||||
updateFields.push('company_name = ?');
|
updateFields.push('company_name = ?');
|
||||||
@@ -263,7 +247,8 @@ router.patch('/:serialNumber', authenticateToken, requireAdmin, async (req, res)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (updateFields.length === 0) {
|
if (updateFields.length === 0) {
|
||||||
return res.status(400).json({ error: '没有提供更新字段' });
|
res.status(400).json({ error: '没有提供更新字段' });
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateFields.push('updated_at = CURRENT_TIMESTAMP');
|
updateFields.push('updated_at = CURRENT_TIMESTAMP');
|
||||||
@@ -274,22 +259,18 @@ router.patch('/:serialNumber', authenticateToken, requireAdmin, async (req, res)
|
|||||||
params
|
params
|
||||||
);
|
);
|
||||||
|
|
||||||
// 获取更新后的序列号信息
|
const updatedSerial = await db.get('SELECT s.*, u.name as created_by_name FROM serials s LEFT JOIN users u ON s.created_by = u.id WHERE s.serial_number = ?', [serialNumber.toUpperCase()]);
|
||||||
const updatedSerial = await db.get(
|
|
||||||
'SELECT s.*, u.name as created_by_name FROM serials s LEFT JOIN users u ON s.created_by = u.id WHERE s.serial_number = ?',
|
|
||||||
[serialNumber.toUpperCase()]
|
|
||||||
);
|
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
message: '序列号更新成功',
|
message: '序列号更新成功',
|
||||||
serial: {
|
serial: {
|
||||||
serialNumber: updatedSerial.serial_number,
|
serialNumber: (updatedSerial as any).serial_number,
|
||||||
companyName: updatedSerial.company_name,
|
companyName: (updatedSerial as any).company_name,
|
||||||
validUntil: updatedSerial.valid_until,
|
validUntil: (updatedSerial as any).valid_until,
|
||||||
isActive: updatedSerial.is_active,
|
isActive: (updatedSerial as any).is_active,
|
||||||
createdAt: updatedSerial.created_at,
|
createdAt: (updatedSerial as any).created_at,
|
||||||
updatedAt: updatedSerial.updated_at,
|
updatedAt: (updatedSerial as any).updated_at,
|
||||||
createdBy: updatedSerial.created_by_name
|
createdBy: (updatedSerial as any).created_by_name
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -298,31 +279,27 @@ router.patch('/:serialNumber', authenticateToken, requireAdmin, async (req, res)
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 吊销序列号
|
router.post('/:serialNumber/revoke', authenticateToken, requireAdmin, async (req: Request<{ serialNumber: string }>, res: Response): Promise<void> => {
|
||||||
router.post('/:serialNumber/revoke', authenticateToken, requireAdmin, async (req, res) => {
|
|
||||||
try {
|
try {
|
||||||
const { serialNumber } = req.params;
|
const { serialNumber } = req.params;
|
||||||
|
|
||||||
if (!serialNumber) {
|
if (!serialNumber) {
|
||||||
return res.status(400).json({ error: '序列号不能为空' });
|
res.status(400).json({ error: '序列号不能为空' });
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查序列号是否存在
|
const existingSerial = await db.get<{ is_active: number }>('SELECT * FROM serials WHERE serial_number = ?', [serialNumber.toUpperCase()]);
|
||||||
const existingSerial = await db.get(
|
|
||||||
'SELECT * FROM serials WHERE serial_number = ?',
|
|
||||||
[serialNumber.toUpperCase()]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!existingSerial) {
|
if (!existingSerial) {
|
||||||
return res.status(404).json({ error: '序列号不存在' });
|
res.status(404).json({ error: '序列号不存在' });
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果已经吊销,返回提示
|
|
||||||
if (!existingSerial.is_active) {
|
if (!existingSerial.is_active) {
|
||||||
return res.status(400).json({ error: '序列号已被吊销' });
|
res.status(400).json({ error: '序列号已被吊销' });
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 吊销序列号(将 is_active 设为 0)
|
|
||||||
await db.run(
|
await db.run(
|
||||||
'UPDATE serials SET is_active = 0, updated_at = CURRENT_TIMESTAMP WHERE serial_number = ?',
|
'UPDATE serials SET is_active = 0, updated_at = CURRENT_TIMESTAMP WHERE serial_number = ?',
|
||||||
[serialNumber.toUpperCase()]
|
[serialNumber.toUpperCase()]
|
||||||
@@ -340,48 +317,43 @@ router.post('/:serialNumber/revoke', authenticateToken, requireAdmin, async (req
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 自定义前缀生成序列号(管理员权限)
|
router.post('/generate-with-prefix', authenticateToken, requireAdmin, async (req: Request<{}, {}, GenerateSerialWithPrefixRequest>, res: Response): Promise<void> => {
|
||||||
router.post('/generate-with-prefix', authenticateToken, requireAdmin, async (req, res) => {
|
|
||||||
try {
|
try {
|
||||||
const { companyName, quantity = 1, validDays = 365, serialPrefix } = req.body;
|
const { companyName, quantity = 1, validDays = 365, serialPrefix } = req.body;
|
||||||
|
|
||||||
if (!companyName) {
|
if (!companyName) {
|
||||||
return res.status(400).json({ error: '企业名称不能为空' });
|
res.status(400).json({ error: '企业名称不能为空' });
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!serialPrefix || serialPrefix.length > 10) {
|
if (!serialPrefix || serialPrefix.length > 10) {
|
||||||
return res.status(400).json({ error: '自定义前缀不能为空且不能超过10个字符' });
|
res.status(400).json({ error: '自定义前缀不能为空且不能超过10个字符' });
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (quantity < 1 || quantity > 100) {
|
if (quantity < 1 || quantity > 100) {
|
||||||
return res.status(400).json({ error: '生成数量必须在1-100之间' });
|
res.status(400).json({ error: '生成数量必须在1-100之间' });
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算有效期
|
|
||||||
const validUntil = new Date();
|
const validUntil = new Date();
|
||||||
validUntil.setDate(validUntil.getDate() + validDays);
|
validUntil.setDate(validUntil.getDate() + validDays);
|
||||||
|
|
||||||
// 生成序列号
|
const serials: SerialListItem[] = [];
|
||||||
const serials = [];
|
|
||||||
const prefix = serialPrefix.toUpperCase().replace(/[^A-Z0-9]/g, '');
|
const prefix = serialPrefix.toUpperCase().replace(/[^A-Z0-9]/g, '');
|
||||||
|
|
||||||
if (!prefix) {
|
if (!prefix) {
|
||||||
return res.status(400).json({ error: '自定义前缀包含无效字符,只能包含字母和数字' });
|
res.status(400).json({ error: '自定义前缀包含无效字符,只能包含字母和数字' });
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 批量插入序列号
|
|
||||||
const insertPromises = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < quantity; i++) {
|
for (let i = 0; i < quantity; i++) {
|
||||||
// 使用随机数生成序列号,避免重复
|
|
||||||
const randomPart = Math.floor(Math.random() * 1000000).toString().padStart(6, '0');
|
const randomPart = Math.floor(Math.random() * 1000000).toString().padStart(6, '0');
|
||||||
const serialNumber = `${prefix}${randomPart}`;
|
const serialNumber = `${prefix}${randomPart}`;
|
||||||
|
|
||||||
insertPromises.push(
|
await db.run(
|
||||||
db.run(
|
|
||||||
'INSERT INTO serials (serial_number, company_name, valid_until, created_by) VALUES (?, ?, ?, ?)',
|
'INSERT INTO serials (serial_number, company_name, valid_until, created_by) VALUES (?, ?, ?, ?)',
|
||||||
[serialNumber, companyName, validUntil.toISOString(), req.user.id]
|
[serialNumber, companyName, validUntil.toISOString(), req.user!.id]
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
serials.push({
|
serials.push({
|
||||||
@@ -392,8 +364,6 @@ router.post('/generate-with-prefix', authenticateToken, requireAdmin, async (req
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all(insertPromises);
|
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
message: `成功生成${quantity}个序列号`,
|
message: `成功生成${quantity}个序列号`,
|
||||||
serials
|
serials
|
||||||
@@ -404,4 +374,4 @@ router.post('/generate-with-prefix', authenticateToken, requireAdmin, async (req
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = router;
|
export default router;
|
||||||
@@ -1,14 +1,11 @@
|
|||||||
const Database = require('better-sqlite3');
|
import Database from 'better-sqlite3';
|
||||||
const bcrypt = require('bcryptjs');
|
import bcrypt from 'bcryptjs';
|
||||||
const path = require('path');
|
import path from 'path';
|
||||||
|
|
||||||
// 创建数据库连接
|
|
||||||
const dbPath = path.join(__dirname, '../data/database.sqlite');
|
const dbPath = path.join(__dirname, '../data/database.sqlite');
|
||||||
const db = new Database(dbPath, { verbose: console.log });
|
const db = new Database(dbPath, { verbose: console.log });
|
||||||
|
|
||||||
// 创建表
|
const createTables = (): void => {
|
||||||
const createTables = () => {
|
|
||||||
// 用户表
|
|
||||||
db.exec(`
|
db.exec(`
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
@@ -22,18 +19,16 @@ const createTables = () => {
|
|||||||
)
|
)
|
||||||
`);
|
`);
|
||||||
|
|
||||||
// 企业表
|
|
||||||
db.exec(`
|
db.exec(`
|
||||||
CREATE TABLE IF NOT EXISTS companies (
|
CREATE TABLE IF NOT EXISTS companies (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
company_name TEXT UNIQUE NOT NULL,
|
company_name TEXT UNIQUE NOT NULL,
|
||||||
is_active BOOLEAN DEFAULT 1,
|
is_active BOOLEAN BOOLEAN DEFAULT 1,
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
)
|
)
|
||||||
`);
|
`);
|
||||||
|
|
||||||
// 序列号表
|
|
||||||
db.exec(`
|
db.exec(`
|
||||||
CREATE TABLE IF NOT EXISTS serials (
|
CREATE TABLE IF NOT EXISTS serials (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
@@ -49,7 +44,6 @@ const createTables = () => {
|
|||||||
)
|
)
|
||||||
`);
|
`);
|
||||||
|
|
||||||
// 创建索引
|
|
||||||
db.exec('CREATE INDEX IF NOT EXISTS idx_company_name_companies ON companies (company_name)');
|
db.exec('CREATE INDEX IF NOT EXISTS idx_company_name_companies ON companies (company_name)');
|
||||||
db.exec('CREATE INDEX IF NOT EXISTS idx_serial_number ON serials (serial_number)');
|
db.exec('CREATE INDEX IF NOT EXISTS idx_serial_number ON serials (serial_number)');
|
||||||
db.exec('CREATE INDEX IF NOT EXISTS idx_company_name_serials ON serials (company_name)');
|
db.exec('CREATE INDEX IF NOT EXISTS idx_company_name_serials ON serials (company_name)');
|
||||||
@@ -58,20 +52,16 @@ const createTables = () => {
|
|||||||
console.log('数据库表创建完成');
|
console.log('数据库表创建完成');
|
||||||
};
|
};
|
||||||
|
|
||||||
// 创建默认管理员用户
|
const createDefaultUser = async (): Promise<void> => {
|
||||||
const createDefaultUser = async () => {
|
|
||||||
const username = 'admin';
|
const username = 'admin';
|
||||||
const password = 'Beifan@2026';
|
const password = 'Beifan@2026';
|
||||||
const name = '系统管理员';
|
const name = '系统管理员';
|
||||||
|
|
||||||
// 检查用户是否已存在
|
|
||||||
const user = db.prepare('SELECT * FROM users WHERE username = ?').get(username);
|
const user = db.prepare('SELECT * FROM users WHERE username = ?').get(username);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
// 哈希密码
|
|
||||||
const hashedPassword = await bcrypt.hash(password, 10);
|
const hashedPassword = await bcrypt.hash(password, 10);
|
||||||
|
|
||||||
// 创建用户
|
|
||||||
db.prepare(
|
db.prepare(
|
||||||
'INSERT INTO users (username, password, name, email, role) VALUES (?, ?, ?, ?, ?)'
|
'INSERT INTO users (username, password, name, email, role) VALUES (?, ?, ?, ?, ?)'
|
||||||
).run(username, hashedPassword, name, 'admin@example.com', 'admin');
|
).run(username, hashedPassword, name, 'admin@example.com', 'admin');
|
||||||
@@ -84,12 +74,9 @@ const createDefaultUser = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 初始化数据库
|
const initDatabase = async (): Promise<void> => {
|
||||||
const initDatabase = async () => {
|
|
||||||
createTables();
|
createTables();
|
||||||
await createDefaultUser();
|
await createDefaultUser();
|
||||||
|
|
||||||
// 关闭数据库连接
|
|
||||||
db.close();
|
db.close();
|
||||||
console.log('数据库连接已关闭');
|
console.log('数据库连接已关闭');
|
||||||
};
|
};
|
||||||
@@ -1,37 +1,31 @@
|
|||||||
require('dotenv').config({ path: __dirname + '/.env' });
|
import 'dotenv/config';
|
||||||
const express = require('express');
|
import express, { Request, Response, NextFunction } from 'express';
|
||||||
const cors = require('cors');
|
import cors from 'cors';
|
||||||
const path = require('path');
|
import path from 'path';
|
||||||
|
import authRoutes from './routes/auth';
|
||||||
// 导入路由
|
import serialRoutes from './routes/serials';
|
||||||
const authRoutes = require('./routes/auth');
|
import companyRoutes from './routes/companies';
|
||||||
const serialRoutes = require('./routes/serials');
|
|
||||||
const companyRoutes = require('./routes/companies');
|
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = process.env.PORT || 3000;
|
const PORT = process.env.PORT || 3000;
|
||||||
|
|
||||||
// 中间件
|
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
app.use(express.json({ limit: '10mb' }));
|
app.use(express.json({ limit: '10mb' }));
|
||||||
app.use(express.urlencoded({ extended: true }));
|
app.use(express.urlencoded({ extended: true }));
|
||||||
// 设置响应编码
|
|
||||||
app.use((req, res, next) => {
|
app.use((req: Request, res: Response, next: NextFunction): void => {
|
||||||
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
// API路由(必须在静态文件服务之前)
|
|
||||||
app.use('/api/auth', authRoutes);
|
app.use('/api/auth', authRoutes);
|
||||||
app.use('/api/serials', serialRoutes);
|
app.use('/api/serials', serialRoutes);
|
||||||
app.use('/api/companies', companyRoutes);
|
app.use('/api/companies', companyRoutes);
|
||||||
|
|
||||||
// 健康检查
|
app.get('/api/health', (req: Request, res: Response): void => {
|
||||||
app.get('/api/health', (req, res) => {
|
|
||||||
res.json({ status: 'ok', message: '服务器运行正常' });
|
res.json({ status: 'ok', message: '服务器运行正常' });
|
||||||
});
|
});
|
||||||
|
|
||||||
// 静态文件服务
|
|
||||||
const frontendPath = path.join(__dirname, '..', 'frontend');
|
const frontendPath = path.join(__dirname, '..', 'frontend');
|
||||||
const distPath = path.join(frontendPath, 'dist');
|
const distPath = path.join(frontendPath, 'dist');
|
||||||
const publicPath = path.join(frontendPath, 'public');
|
const publicPath = path.join(frontendPath, 'public');
|
||||||
@@ -42,12 +36,10 @@ if (process.env.NODE_ENV === 'production') {
|
|||||||
app.use(express.static(publicPath));
|
app.use(express.static(publicPath));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 404处理
|
app.use((req: Request, res: Response): void => {
|
||||||
app.use((req, res) => {
|
|
||||||
if (req.path.startsWith('/api/')) {
|
if (req.path.startsWith('/api/')) {
|
||||||
res.status(404).json({ error: 'API接口不存在' });
|
res.status(404).json({ error: 'API接口不存在' });
|
||||||
} else {
|
} else {
|
||||||
// 对于非API请求,返回index.html(用于React Router)
|
|
||||||
if (process.env.NODE_ENV === 'production') {
|
if (process.env.NODE_ENV === 'production') {
|
||||||
res.sendFile(path.join(distPath, 'index.html'));
|
res.sendFile(path.join(distPath, 'index.html'));
|
||||||
} else {
|
} else {
|
||||||
@@ -56,8 +48,7 @@ app.use((req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 错误处理中间件
|
app.use((error: Error, req: Request, res: Response, next: NextFunction): void => {
|
||||||
app.use((error, req, res, next) => {
|
|
||||||
console.error('服务器错误:', error);
|
console.error('服务器错误:', error);
|
||||||
|
|
||||||
if (req.path.startsWith('/api/')) {
|
if (req.path.startsWith('/api/')) {
|
||||||
@@ -67,8 +58,7 @@ app.use((error, req, res, next) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 启动服务器
|
app.listen(PORT, (): void => {
|
||||||
app.listen(PORT, () => {
|
|
||||||
console.log(`服务器运行在 http://localhost:${PORT}`);
|
console.log(`服务器运行在 http://localhost:${PORT}`);
|
||||||
console.log(`API文档: http://localhost:${PORT}/api/health`);
|
console.log(`API文档: http://localhost:${PORT}/api/health`);
|
||||||
console.log(`环境: ${process.env.NODE_ENV || 'development'}`);
|
console.log(`环境: ${process.env.NODE_ENV || 'development'}`);
|
||||||
19
tsconfig.json
Normal file
19
tsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"module": "commonjs",
|
||||||
|
"lib": ["ES2020"],
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"types": ["node"]
|
||||||
|
},
|
||||||
|
"include": ["**/*.ts"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
160
types/index.d.ts
vendored
Normal file
160
types/index.d.ts
vendored
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
export interface User {
|
||||||
|
id: number;
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
name: string;
|
||||||
|
email: string | null;
|
||||||
|
role: 'admin' | 'user';
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Company {
|
||||||
|
id: number;
|
||||||
|
company_name: string;
|
||||||
|
is_active: boolean;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Serial {
|
||||||
|
id: number;
|
||||||
|
serial_number: string;
|
||||||
|
company_name: string;
|
||||||
|
valid_until: string | null;
|
||||||
|
is_active: boolean;
|
||||||
|
created_by: number | null;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
created_by_name?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthUser {
|
||||||
|
id: number;
|
||||||
|
username: string;
|
||||||
|
name: string;
|
||||||
|
role: 'admin' | 'user';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface JWTPayload {
|
||||||
|
userId: number;
|
||||||
|
username: string;
|
||||||
|
role: 'admin' | 'user';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginRequest {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChangePasswordRequest {
|
||||||
|
currentPassword: string;
|
||||||
|
newPassword: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GenerateSerialRequest {
|
||||||
|
companyName: string;
|
||||||
|
quantity?: number;
|
||||||
|
validDays?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GenerateSerialWithPrefixRequest {
|
||||||
|
companyName: string;
|
||||||
|
quantity?: number;
|
||||||
|
validDays?: number;
|
||||||
|
serialPrefix: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QRCodeRequest {
|
||||||
|
baseUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateSerialRequest {
|
||||||
|
companyName?: string;
|
||||||
|
validUntil?: string;
|
||||||
|
isActive?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateCompanyRequest {
|
||||||
|
newCompanyName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaginationQuery {
|
||||||
|
page?: number;
|
||||||
|
limit?: number;
|
||||||
|
search?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaginationResponse {
|
||||||
|
page: number;
|
||||||
|
limit: number;
|
||||||
|
total: number;
|
||||||
|
totalPages: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiResponse<T = any> {
|
||||||
|
message: string;
|
||||||
|
data?: T;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginResponse {
|
||||||
|
accessToken: string;
|
||||||
|
user: {
|
||||||
|
id: number;
|
||||||
|
username: string;
|
||||||
|
name: string;
|
||||||
|
email: string | null;
|
||||||
|
role: 'admin' | 'user';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SerialListItem {
|
||||||
|
serialNumber: string;
|
||||||
|
companyName: string;
|
||||||
|
validUntil: string | null;
|
||||||
|
isActive: boolean;
|
||||||
|
createdAt: string;
|
||||||
|
createdBy?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CompanyListItem {
|
||||||
|
companyName: string;
|
||||||
|
firstCreated: string;
|
||||||
|
lastCreated: string;
|
||||||
|
serialCount: number;
|
||||||
|
activeCount: number;
|
||||||
|
status: 'active' | 'disabled';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CompanyDetail {
|
||||||
|
companyName: string;
|
||||||
|
serialCount: number;
|
||||||
|
activeCount: number;
|
||||||
|
disabledCount: number;
|
||||||
|
expiredCount: number;
|
||||||
|
firstCreated: string;
|
||||||
|
lastCreated: string;
|
||||||
|
status: 'active' | 'disabled';
|
||||||
|
serials: SerialListItem[];
|
||||||
|
monthlyStats: MonthlyStat[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MonthlyStat {
|
||||||
|
month: string;
|
||||||
|
count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StatsOverview {
|
||||||
|
totalCompanies: number;
|
||||||
|
totalSerials: number;
|
||||||
|
activeSerials: number;
|
||||||
|
inactiveSerials: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StatsResponse {
|
||||||
|
overview: StatsOverview;
|
||||||
|
monthlyStats: Array<{ month: string; company_count: number; serial_count: number }>;
|
||||||
|
recentCompanies: Array<{ companyName: string; lastCreated: string; status: 'active' | 'disabled' }>;
|
||||||
|
recentSerials: Array<{ serialNumber: string; companyName: string; isActive: boolean; createdAt: string }>;
|
||||||
|
}
|
||||||
@@ -1,50 +1,49 @@
|
|||||||
const Database = require('better-sqlite3');
|
import Database from 'better-sqlite3';
|
||||||
const path = require('path');
|
import path from 'path';
|
||||||
|
|
||||||
class DatabaseWrapper {
|
class DatabaseWrapper {
|
||||||
|
private db: Database.Database;
|
||||||
|
private dbPath: string;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.dbPath = process.env.DB_PATH || path.join(__dirname, '../data/database.sqlite');
|
this.dbPath = process.env.DB_PATH || path.join(__dirname, '../data/database.sqlite');
|
||||||
this.db = new Database(this.dbPath, { verbose: console.log });
|
this.db = new Database(this.dbPath, { verbose: console.log });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查询单个记录
|
get<T = any>(sql: string, params: any[] = []): T | undefined {
|
||||||
get(sql, params = []) {
|
|
||||||
try {
|
try {
|
||||||
const stmt = this.db.prepare(sql);
|
const stmt = this.db.prepare(sql);
|
||||||
return stmt.get(params);
|
return stmt.get(params) as T | undefined;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('数据库查询错误:', error);
|
console.error('数据库查询错误:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查询多个记录
|
all<T = any>(sql: string, params: any[] = []): T[] {
|
||||||
all(sql, params = []) {
|
|
||||||
try {
|
try {
|
||||||
const stmt = this.db.prepare(sql);
|
const stmt = this.db.prepare(sql);
|
||||||
return stmt.all(params);
|
return stmt.all(params) as T[];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('数据库查询错误:', error);
|
console.error('数据库查询错误:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 执行插入、更新、删除操作
|
run(sql: string, params: any[] = []): { id: number; changes: number } {
|
||||||
run(sql, params = []) {
|
|
||||||
try {
|
try {
|
||||||
const stmt = this.db.prepare(sql);
|
const stmt = this.db.prepare(sql);
|
||||||
const result = stmt.run(params);
|
const result = stmt.run(params);
|
||||||
return { id: result.lastInsertRowid, changes: result.changes };
|
return { id: result.lastInsertRowid as number, changes: result.changes };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('数据库操作错误:', error);
|
console.error('数据库操作错误:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 关闭数据库连接
|
close(): void {
|
||||||
close() {
|
|
||||||
this.db.close();
|
this.db.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = new DatabaseWrapper();
|
export default new DatabaseWrapper();
|
||||||
Reference in New Issue
Block a user