refactor: migrate entire project to TypeScript

This commit is contained in:
2026-02-06 14:44:14 +08:00
parent e98dbcb0f4
commit a80c479027
14 changed files with 1141 additions and 462 deletions

View File

@@ -1,10 +1,11 @@
# 授权管理系统 - 后端服务
浙江贝凡企业授权管理系统的后端服务,基于 Node.js + Express + SQLite。
浙江贝凡企业授权管理系统的后端服务,基于 Node.js + TypeScript + Express + SQLite。
## 技术栈
- **Node.js**: 运行时环境
- **TypeScript**: 类型安全
- **Express**: Web 框架
- **SQLite**: 数据库
- **JWT**: 身份认证
@@ -15,18 +16,21 @@
```
backend/
├── routes/ # API 路由
│ ├── auth.js # 认证路由
│ ├── serials.js # 序列号路由
│ └── companies.js # 企业路由
│ ├── auth.ts # 认证路由
│ ├── serials.ts # 序列号路由
│ └── companies.ts # 企业路由
├── middleware/ # 中间件
│ └── auth.js # 认证中间件
│ └── auth.ts # 认证中间件
├── scripts/ # 脚本
│ └── init-db.js # 数据库初始化
│ └── init-db.ts # 数据库初始化
├── utils/ # 工具函数
│ └── database.js # 数据库连接
│ └── database.ts # 数据库连接
├── types/ # 类型定义
│ └── index.d.ts # TypeScript 类型
├── data/ # 数据文件
│ └── database.sqlite
├── server.js # 服务器入口
├── server.ts # 服务器入口
├── tsconfig.json # TypeScript 配置
├── .env # 环境变量
└── package.json # 项目配置
```
@@ -47,6 +51,14 @@ pnpm dev
服务器将在 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/logout` - 用户登出
- `GET /api/auth/profile` - 获取用户信息
- `POST /api/auth/change-password` - 修改密码
- `PUT /api/auth/profile` - 更新用户资料
### 序列号接口
- `POST /api/serials/generate` - 生成序列号
- `POST /api/serials/generate-with-prefix` - 使用自定义前缀生成序列号
- `POST /api/serials/:serialNumber/qrcode` - 生成二维码
- `GET /api/serials/:serialNumber/query` - 查询序列号
- `POST /api/serials/:serialNumber/revoke` - 吊销序列号
- `GET /api/serials/` - 获取序列号列表
- `PATCH /api/serials/:serialNumber` - 更新序列号
- `POST /api/serials/:serialNumber/revoke` - 吊销序列号
### 企业接口
- `GET /api/companies/` - 获取企业列表
- `GET /api/companies/:companyName` - 获取企业详情
- `POST /api/companies/:companyName/revoke` - 吊销企业
- `PATCH /api/companies/:companyName` - 更新企业信息
- `DELETE /api/companies/:companyName` - 删除企业
- `DELETE /api/companies/:companyName/serials/:serialNumber` - 删除企业下的序列号
- `POST /api/companies/:companyName/revoke` - 吊销企业
- `GET /api/companies/stats/overview` - 获取统计数据
## License

View File

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

View File

@@ -1,12 +1,14 @@
{
{
"name": "trace-backend",
"version": "1.0.0",
"description": "浙江贝凡企业授权管理系统 - 后端服务",
"main": "server.js",
"main": "dist/server.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js",
"init-db": "node scripts/init-db.js"
"start": "node dist/server.js",
"dev": "nodemon --exec tsx server.ts",
"build": "tsc",
"init-db": "tsx scripts/init-db.ts"
},
"dependencies": {
"bcryptjs": "^3.0.3",
@@ -17,7 +19,14 @@
"jsonwebtoken": "^9.0.2"
},
"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": "",
"license": "MIT"

542
pnpm-lock.yaml generated
View File

@@ -27,20 +27,274 @@ importers:
specifier: ^9.0.2
version: 9.0.3
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:
specifier: ^3.0.1
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:
'@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:
resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==}
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:
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
engines: {node: '>= 8'}
arg@4.1.3:
resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==}
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
@@ -124,6 +378,9 @@ packages:
resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==}
engines: {node: '>= 0.10'}
create-require@1.1.1:
resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==}
debug@4.4.3:
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
engines: {node: '>=6.0'}
@@ -149,6 +406,10 @@ packages:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
diff@4.0.4:
resolution: {integrity: sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==}
engines: {node: '>=0.3.1'}
dotenv@17.2.4:
resolution: {integrity: sha512-mudtfb4zRB4bVvdj0xRo+e6duH1csJRM8IukBqfTRvHotn9+LBXB8ynAidP9zHqoRC/fsllXgk4kCKlR21fIhw==}
engines: {node: '>=12'}
@@ -182,6 +443,11 @@ packages:
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
engines: {node: '>= 0.4'}
esbuild@0.27.3:
resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==}
engines: {node: '>=18'}
hasBin: true
escape-html@1.0.3:
resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
@@ -235,6 +501,9 @@ packages:
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
engines: {node: '>= 0.4'}
get-tsconfig@4.13.4:
resolution: {integrity: sha512-gKvvu/fh0hxWmR/Ty0Goc3u/GADL9IgyhNAPD8hElRVO9dTOawCuyGNURCjaSTB4ZNP/OAUaSXmR2LhitzkLug==}
github-from-package@0.0.0:
resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==}
@@ -332,6 +601,9 @@ packages:
lodash.once@4.1.1:
resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==}
make-error@1.3.6:
resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==}
math-intrinsics@1.1.0:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
@@ -453,6 +725,9 @@ packages:
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
engines: {node: '>=8.10.0'}
resolve-pkg-maps@1.0.0:
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
router@2.2.0:
resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==}
engines: {node: '>= 18'}
@@ -539,6 +814,25 @@ packages:
resolution: {integrity: sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==}
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:
resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==}
@@ -546,9 +840,17 @@ packages:
resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==}
engines: {node: '>= 0.6'}
typescript@5.9.3:
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
engines: {node: '>=14.17'}
hasBin: true
undefsafe@2.0.5:
resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==}
undici-types@7.16.0:
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
unpipe@1.0.0:
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
engines: {node: '>= 0.8'}
@@ -556,6 +858,9 @@ packages:
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
v8-compile-cache-lib@3.0.1:
resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==}
vary@1.1.2:
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
engines: {node: '>= 0.8'}
@@ -563,18 +868,181 @@ packages:
wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
yn@3.1.1:
resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==}
engines: {node: '>=6'}
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:
dependencies:
mime-types: 3.0.2
negotiator: 1.0.0
acorn-walk@8.3.4:
dependencies:
acorn: 8.15.0
acorn@8.15.0: {}
anymatch@3.1.3:
dependencies:
normalize-path: 3.0.0
picomatch: 2.3.1
arg@4.1.3: {}
balanced-match@1.0.2: {}
base64-js@1.5.1: {}
@@ -669,6 +1137,8 @@ snapshots:
object-assign: 4.1.1
vary: 1.1.2
create-require@1.1.1: {}
debug@4.4.3(supports-color@5.5.0):
dependencies:
ms: 2.1.3
@@ -685,6 +1155,8 @@ snapshots:
detect-libc@2.1.2: {}
diff@4.0.4: {}
dotenv@17.2.4: {}
dunder-proto@1.0.1:
@@ -713,6 +1185,35 @@ snapshots:
dependencies:
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: {}
etag@1.8.1: {}
@@ -798,6 +1299,10 @@ snapshots:
dunder-proto: 1.0.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: {}
glob-parent@5.1.2:
@@ -888,6 +1393,8 @@ snapshots:
lodash.once@4.1.1: {}
make-error@1.3.6: {}
math-intrinsics@1.1.0: {}
media-typer@1.1.0: {}
@@ -1010,6 +1517,8 @@ snapshots:
dependencies:
picomatch: 2.3.1
resolve-pkg-maps@1.0.0: {}
router@2.2.0:
dependencies:
debug: 4.4.3(supports-color@5.5.0)
@@ -1128,6 +1637,31 @@ snapshots:
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:
dependencies:
safe-buffer: 5.2.1
@@ -1138,12 +1672,20 @@ snapshots:
media-typer: 1.1.0
mime-types: 3.0.2
typescript@5.9.3: {}
undefsafe@2.0.5: {}
undici-types@7.16.0: {}
unpipe@1.0.0: {}
util-deprecate@1.0.2: {}
v8-compile-cache-lib@3.0.1: {}
vary@1.1.2: {}
wrappy@1.0.2: {}
yn@3.1.1: {}

View File

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

View File

@@ -1,23 +1,18 @@
const express = require('express');
const db = require('../utils/database');
const { authenticateToken, requireAdmin } = require('../middleware/auth');
import express, { Request, Response } from 'express';
import db from '../utils/database';
import { authenticateToken, requireAdmin } from '../middleware/auth';
import { PaginationQuery, UpdateCompanyRequest, CompanyDetail, StatsOverview, MonthlyStat, SerialListItem, CompanyListItem } from '../types';
const router = express.Router();
// 获取企业列表
router.get('/', authenticateToken, requireAdmin, async (req, res) => {
router.get('/', authenticateToken, requireAdmin, async (req: Request<{}, {}, {}, PaginationQuery>, res: Response): Promise<void> => {
try {
const { page = 1, limit = 20, search = '' } = req.query;
const offset = (page - 1) * limit;
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
`;
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';
let countQuery = 'SELECT COUNT(*) as total FROM companies';
let params = [];
let params: any[] = [];
if (search) {
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 ?';
params.push(parseInt(limit), parseInt(offset));
params.push(parseInt(limit.toString()), parseInt(offset.toString()));
const [companies, countResult] = await Promise.all([
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);
res.json({
message: '获取企业列表成功',
data: companies.map(company => ({
data: companies.map((company: any) => ({
companyName: company.company_name,
firstCreated: company.first_created,
lastCreated: company.last_created,
@@ -47,8 +42,8 @@ router.get('/', authenticateToken, requireAdmin, async (req, res) => {
status: company.is_active ? 'active' : 'disabled'
})),
pagination: {
page: parseInt(page),
limit: parseInt(limit),
page: parseInt(page.toString()),
limit: parseInt(limit.toString()),
total,
totalPages
}
@@ -59,22 +54,20 @@ router.get('/', authenticateToken, requireAdmin, async (req, res) => {
}
});
// 获取企业详情
router.get('/:companyName', authenticateToken, requireAdmin, async (req, res) => {
router.get('/:companyName', authenticateToken, requireAdmin, async (req: Request<{ companyName: string }, {}, {}, PaginationQuery>, res: Response): Promise<void> => {
try {
const { companyName } = req.params;
const decodedCompanyName = decodeURIComponent(companyName);
const { page = 1, limit = 20 } = req.query;
const offset = (page - 1) * limit;
// 获取企业基本信息
const companyInfo = await db.get('SELECT * FROM companies WHERE company_name = ?', [decodedCompanyName]);
if (!companyInfo) {
return res.status(404).json({ error: '企业不存在' });
res.status(404).json({ error: '企业不存在' });
return;
}
// 获取序列号统计信息
const serialStats = await db.get(`
SELECT COUNT(*) as serial_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 = ?
`, [decodedCompanyName]);
// 获取企业的序列号列表
const serials = await db.all(`
SELECT s.*, u.name as created_by_name
FROM serials s
@@ -92,10 +84,9 @@ router.get('/:companyName', authenticateToken, requireAdmin, async (req, res) =>
WHERE s.company_name = ?
ORDER BY s.created_at DESC
LIMIT ? OFFSET ?
`, [decodedCompanyName, parseInt(limit), parseInt(offset)]);
`, [decodedCompanyName, parseInt(limit.toString()), parseInt(offset.toString())]);
// 获取统计数据
const stats = await db.all(`
const stats = await db.all<{ month: string; count: number }>(`
SELECT strftime('%Y-%m', created_at) as month,
COUNT(*) as count
FROM serials
@@ -113,10 +104,10 @@ router.get('/:companyName', authenticateToken, requireAdmin, async (req, res) =>
activeCount: serialStats?.active_count || 0,
disabledCount: serialStats?.disabled_count || 0,
expiredCount: serialStats?.expired_count || 0,
firstCreated: companyInfo.created_at,
lastCreated: companyInfo.updated_at,
status: companyInfo.is_active ? 'active' : 'disabled',
serials: serials.map(s => ({
firstCreated: (companyInfo as any).created_at,
lastCreated: (companyInfo as any).updated_at,
status: (companyInfo as any).is_active ? 'active' : 'disabled',
serials: serials.map((s: any) => ({
serialNumber: s.serial_number,
validUntil: s.valid_until,
isActive: s.is_active,
@@ -135,38 +126,37 @@ router.get('/:companyName', authenticateToken, requireAdmin, async (req, res) =>
}
});
// 更新企业信息
router.patch('/:companyName', authenticateToken, requireAdmin, async (req, res) => {
router.patch('/:companyName', authenticateToken, requireAdmin, async (req: Request<{ companyName: string }, {}, UpdateCompanyRequest>, res: Response): Promise<void> => {
try {
const { companyName } = req.params;
const decodedCompanyName = decodeURIComponent(companyName);
const { newCompanyName } = req.body;
if (!newCompanyName || newCompanyName.trim() === '') {
return res.status(400).json({ error: '新企业名称不能为空' });
res.status(400).json({ error: '新企业名称不能为空' });
return;
}
// 检查企业是否存在
const existingCompany = db.get(
'SELECT COUNT(*) as count FROM serials WHERE company_name = ?',
[decodedCompanyName]
);
if (!existingCompany || existingCompany.count === 0) {
return res.status(404).json({ error: '企业不存在' });
res.status(404).json({ error: '企业不存在' });
return;
}
// 检查新企业名称是否已存在
const duplicateCompany = db.get(
'SELECT COUNT(*) as count FROM serials WHERE company_name = ?',
[newCompanyName]
);
if (duplicateCompany && duplicateCompany.count > 0) {
return res.status(400).json({ error: '企业名称已存在' });
res.status(400).json({ error: '企业名称已存在' });
return;
}
// 更新企业名称
db.run(
'UPDATE serials SET company_name = ?, updated_at = CURRENT_TIMESTAMP WHERE company_name = ?',
[newCompanyName, decodedCompanyName]
@@ -185,45 +175,38 @@ router.patch('/:companyName', authenticateToken, requireAdmin, async (req, res)
}
});
// 删除企业(物理删除,完全删除企业和所有序列号)
router.delete('/:companyName', authenticateToken, requireAdmin, async (req, res) => {
router.delete('/:companyName', authenticateToken, requireAdmin, async (req: Request<{ companyName: string }>, res: Response): Promise<void> => {
try {
const { companyName } = req.params;
console.log('原始参数:', companyName);
const decodedCompanyName = decodeURIComponent(companyName);
console.log('解码后参数:', decodedCompanyName);
// 检查企业是否存在
const existingCompany = await db.get(
'SELECT * FROM companies WHERE company_name = ?',
[decodedCompanyName]
);
if (!existingCompany) {
return res.status(404).json({ error: '企业不存在' });
res.status(404).json({ error: '企业不存在' });
return;
}
// 开始事务
db.run('BEGIN TRANSACTION');
try {
// 删除该企业的所有序列号
const serialDeleteResult = db.run(
'DELETE FROM serials WHERE company_name = ?',
[decodedCompanyName]
);
console.log('删除序列号结果:', serialDeleteResult);
// 删除企业记录
const companyDeleteResult = db.run(
'DELETE FROM companies WHERE company_name = ?',
[decodedCompanyName]
);
console.log('删除企业结果:', companyDeleteResult);
if (companyDeleteResult.changes === 0) {
db.run('ROLLBACK');
return res.status(404).json({ error: '企业不存在' });
res.status(404).json({ error: '企业不存在' });
return;
}
db.run('COMMIT');
@@ -247,23 +230,18 @@ router.delete('/:companyName', authenticateToken, requireAdmin, async (req, res)
}
});
// 获取企业统计数据
router.get('/stats/overview', authenticateToken, requireAdmin, async (req, res) => {
router.get('/stats/overview', authenticateToken, requireAdmin, async (req: Request, res: Response): Promise<void> => {
try {
// 获取总企业数
const companyCount = await db.get('SELECT COUNT(*) as count FROM companies');
const companyCount = await db.get<{ count: number }>('SELECT COUNT(*) as count FROM companies');
// 获取总序列号数
const serialCount = await db.get('SELECT COUNT(*) as count FROM serials');
const serialCount = await db.get<{ count: number }>('SELECT COUNT(*) as count FROM serials');
// 获取活跃序列号数
const activeCount = await db.get(`
const activeCount = await db.get<{ count: number }>(`
SELECT COUNT(*) as count FROM serials
WHERE is_active = 1 AND (valid_until IS NULL OR valid_until > datetime('now'))
`);
// 按月份统计 - 使用正确的日期格式
const monthlyStats = await db.all(`
const monthlyStats = await db.all<{ month: string; company_count: number; serial_count: number }>(`
SELECT strftime('%Y-%m', created_at) as month,
COUNT(DISTINCT company_name) as company_count,
COUNT(*) as serial_count
@@ -273,29 +251,28 @@ router.get('/stats/overview', authenticateToken, requireAdmin, async (req, res)
ORDER BY month ASC
`);
// 获取最新添加的企业
const recentCompanies = await db.all(`
const recentCompanies = await db.all<{ company_name: string; last_created: string; is_active: number }>(`
SELECT c.company_name, c.created_at as last_created, c.is_active
FROM companies c
ORDER BY c.updated_at DESC
LIMIT 10
`);
// 获取最近生成的序列号
const recentSerials = await db.all(`
const recentSerials = await db.all<{ serial_number: string; company_name: string; is_active: number; created_at: string }>(`
SELECT s.serial_number, s.company_name, s.is_active, s.created_at
FROM serials s
ORDER BY s.created_at DESC
LIMIT 10
`);
// 如果没有数据生成过去12个月的空数据
let finalMonthlyStats = monthlyStats;
if (monthlyStats.length === 0) {
finalMonthlyStats = [];
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 month = date.toISOString().substr(0, 7);
monthlyStats.push({
finalMonthlyStats.push({
month,
company_count: 0,
serial_count: 0
@@ -307,12 +284,12 @@ router.get('/stats/overview', authenticateToken, requireAdmin, async (req, res)
message: '获取统计数据成功',
data: {
overview: {
totalCompanies: companyCount.count || 0,
totalSerials: serialCount.count || 0,
activeSerials: activeCount.count || 0,
inactiveSerials: (serialCount.count || 0) - (activeCount.count || 0)
totalCompanies: companyCount?.count || 0,
totalSerials: serialCount?.count || 0,
activeSerials: activeCount?.count || 0,
inactiveSerials: (serialCount?.count || 0) - (activeCount?.count || 0)
},
monthlyStats: monthlyStats.map(stat => ({
monthlyStats: finalMonthlyStats.map(stat => ({
month: stat.month,
company_count: stat.company_count,
serial_count: stat.serial_count
@@ -325,7 +302,7 @@ router.get('/stats/overview', authenticateToken, requireAdmin, async (req, res)
recentSerials: recentSerials.map(s => ({
serialNumber: s.serial_number,
companyName: s.company_name,
isActive: s.is_active,
isActive: !!s.is_active,
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, res) => {
router.delete('/:companyName/serials/:serialNumber', authenticateToken, requireAdmin, async (req: Request<{ companyName: string; serialNumber: string }>, res: Response): Promise<void> => {
try {
const { companyName, serialNumber } = req.params;
// 检查序列号是否存在且属于该企业
const serial = await db.get(
'SELECT * FROM serials WHERE serial_number = ? AND company_name = ?',
[serialNumber.toUpperCase(), companyName]
);
if (!serial) {
return res.status(404).json({ error: '序列号不存在或不属于该企业' });
res.status(404).json({ error: '序列号不存在或不属于该企业' });
return;
}
// 物理删除序列号
await db.run(
'DELETE FROM serials WHERE serial_number = ? AND company_name = ?',
[serialNumber.toUpperCase(), companyName]
);
res.json({
message: '序列号已成功删除',
message: '序列号已成功删除',
data: {
serialNumber: serial.serial_number,
serialNumber: serialNumber.toUpperCase(),
companyName
}
});
} catch (error) {
console.error('吊销序列号错误:', error);
console.error('删除序列号错误:', error);
res.status(500).json({ error: '服务器内部错误' });
}
});
// 吊销企业
router.post('/:companyName/revoke', authenticateToken, requireAdmin, async (req, res) => {
router.post('/:companyName/revoke', authenticateToken, requireAdmin, async (req: Request<{ companyName: string }>, res: Response): Promise<void> => {
try {
const { companyName } = req.params;
const decodedCompanyName = decodeURIComponent(companyName);
// 检查企业是否存在
const existingCompany = await db.get(
'SELECT COUNT(*) as count FROM serials WHERE company_name = ?',
[decodedCompanyName]
);
if (!existingCompany || existingCompany.count === 0) {
return res.status(404).json({ error: '企业不存在' });
res.status(404).json({ error: '企业不存在' });
return;
}
// 吊销该企业的所有序列号(将 is_active 设为 0
await db.run(
'UPDATE serials SET is_active = 0, updated_at = CURRENT_TIMESTAMP WHERE company_name = ?',
[decodedCompanyName]
@@ -404,4 +377,4 @@ router.post('/:companyName/revoke', authenticateToken, requireAdmin, async (req,
}
});
module.exports = router;
export default router;

View File

@@ -1,51 +1,44 @@
const express = require('express');
const QRCode = require('qrcode');
const db = require('../utils/database');
const { authenticateToken, requireAdmin } = require('../middleware/auth');
import express, { Request, Response } from 'express';
import QRCode from 'qrcode';
import db from '../utils/database';
import { authenticateToken, requireAdmin } from '../middleware/auth';
import { GenerateSerialRequest, GenerateSerialWithPrefixRequest, QRCodeRequest, UpdateSerialRequest, PaginationQuery, SerialListItem } from '../types';
const router = express.Router();
// 生成序列号
router.post('/generate', authenticateToken, requireAdmin, async (req, res) => {
router.post('/generate', authenticateToken, requireAdmin, async (req: Request<{}, {}, GenerateSerialRequest>, res: Response): Promise<void> => {
try {
const { companyName, quantity = 1, validDays = 365 } = req.body;
if (!companyName) {
return res.status(400).json({ error: '企业名称不能为空' });
res.status(400).json({ error: '企业名称不能为空' });
return;
}
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();
validUntil.setDate(validUntil.getDate() + validDays);
// 确保企业存在
const existingCompany = await db.get('SELECT * FROM companies WHERE company_name = ?', [companyName]);
const existingCompany = await db.get('xSELECT * FROM companies WHERE company_name = ?', [companyName]);
if (!existingCompany) {
await db.run('INSERT INTO companies (company_name, is_active) VALUES (?, 1)', [companyName]);
}
// 生成序列号
const serials = [];
const serials: SerialListItem[] = [];
const prefix = 'BF';
const datePart = new Date().getFullYear().toString().substr(2);
// 批量插入序列号
const insertPromises = [];
for (let i = 0; i < quantity; i++) {
// 使用随机数生成序列号,避免重复
const randomPart = Math.floor(Math.random() * 1000000).toString().padStart(6, '0');
const serialNumber = `${prefix}${datePart}${randomPart}`;
insertPromises.push(
db.run(
await db.run(
'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({
@@ -56,8 +49,6 @@ router.post('/generate', authenticateToken, requireAdmin, async (req, res) => {
});
}
await Promise.all(insertPromises);
res.json({
message: `成功生成${quantity}个序列号`,
serials
@@ -68,36 +59,36 @@ router.post('/generate', authenticateToken, requireAdmin, async (req, res) => {
}
});
// 生成二维码
router.post('/:serialNumber/qrcode', authenticateToken, async (req, res) => {
router.post('/:serialNumber/qrcode', authenticateToken, async (req: Request<{ serialNumber: string }, {}, QRCodeRequest>, res: Response): Promise<void> => {
try {
const { serialNumber } = req.params;
let { baseUrl } = req.body;
if (!serialNumber) {
return res.status(400).json({ error: '序列号不能为空' });
res.status(400).json({ error: '序列号不能为空' });
return;
}
// 验证序列号是否存在
const serial = await db.get(
const serial = await db.get<{ serial_number: string; company_name: string; is_active: number; valid_until: string | null }>(
'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()]
);
if (!serial) {
return res.status(404).json({ error: '序列号不存在' });
res.status(404).json({ error: '序列号不存在' });
return;
}
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()) {
return res.status(400).json({ error: '序列号已过期' });
res.status(400).json({ error: '序列号已过期' });
return;
}
// 生成查询URL
if (!baseUrl) {
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}`;
// 生成二维码
const qrCodeData = await QRCode.toDataURL(queryUrl, {
width: 200,
color: {
@@ -129,28 +119,28 @@ router.post('/:serialNumber/qrcode', authenticateToken, async (req, res) => {
}
});
// 查询序列号
router.get('/:serialNumber/query', async (req, res) => {
router.get('/:serialNumber/query', async (req: Request<{ serialNumber: string }>, res: Response): Promise<void> => {
try {
const { serialNumber } = req.params;
if (!serialNumber) {
return res.status(400).json({ error: '序列号不能为空' });
res.status(400).json({ error: '序列号不能为空' });
return;
}
// 查询序列号
const serial = await db.get(
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 }>(
'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()]
);
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()) {
return res.status(400).json({ error: '序列号已过期' });
res.status(400).json({ error: '序列号已过期' });
return;
}
res.json({
@@ -160,7 +150,7 @@ router.get('/:serialNumber/query', async (req, res) => {
companyName: serial.company_name,
validUntil: serial.valid_until,
status: serial.is_active ? 'active' : 'disabled',
isActive: serial.is_active,
isActive: !!serial.is_active,
createdAt: serial.created_at,
createdBy: serial.created_by_name
}
@@ -171,19 +161,14 @@ router.get('/:serialNumber/query', async (req, res) => {
}
});
// 获取序列号列表
router.get('/', authenticateToken, async (req, res) => {
router.get('/', authenticateToken, async (req: Request<{}, {}, {}, PaginationQuery>, res: Response): Promise<void> => {
try {
const { page = 1, limit = 20, search = '' } = req.query;
const offset = (page - 1) * limit;
let query = `
SELECT s.*, u.name as created_by_name
FROM serials s
LEFT JOIN users u ON s.created_by = u.id
`;
let query = '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 params = [];
let params: any[] = [];
if (search) {
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 ?';
params.push(parseInt(limit), parseInt(offset));
params.push(parseInt(limit.toString()), parseInt(offset.toString()));
const [serials, countResult] = await Promise.all([
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);
res.json({
message: '获取序列号列表成功',
data: serials.map(s => ({
data: serials.map((s: any) => ({
serialNumber: s.serial_number,
companyName: s.company_name,
validUntil: s.valid_until,
@@ -214,8 +199,8 @@ router.get('/', authenticateToken, async (req, res) => {
createdBy: s.created_by_name
})),
pagination: {
page: parseInt(page),
limit: parseInt(limit),
page: parseInt(page.toString()),
limit: parseInt(limit.toString()),
total,
totalPages
}
@@ -226,26 +211,25 @@ router.get('/', authenticateToken, async (req, res) => {
}
});
// 更新序列号
router.patch('/:serialNumber', authenticateToken, requireAdmin, async (req, res) => {
router.patch('/:serialNumber', authenticateToken, requireAdmin, async (req: Request<{ serialNumber: string }, {}, UpdateSerialRequest>, res: Response): Promise<void> => {
try {
const { serialNumber } = req.params;
const { companyName, validUntil, isActive } = req.body;
if (!serialNumber) {
return res.status(400).json({ error: '序列号不能为空' });
res.status(400).json({ error: '序列号不能为空' });
return;
}
// 检查序列号是否存在
const existingSerial = await db.get('SELECT * FROM serials WHERE serial_number = ?', [serialNumber.toUpperCase()]);
const existingSerial = await db.get<{ is_active: number }>('SELECT * FROM serials WHERE serial_number = ?', [serialNumber.toUpperCase()]);
if (!existingSerial) {
return res.status(404).json({ error: '序列号不存在' });
res.status(404).json({ error: '序列号不存在' });
return;
}
// 构建更新字段
const updateFields = [];
const params = [];
const updateFields: string[] = [];
const params: any[] = [];
if (companyName !== undefined) {
updateFields.push('company_name = ?');
@@ -263,7 +247,8 @@ router.patch('/:serialNumber', authenticateToken, requireAdmin, async (req, res)
}
if (updateFields.length === 0) {
return res.status(400).json({ error: '没有提供更新字段' });
res.status(400).json({ error: '没有提供更新字段' });
return;
}
updateFields.push('updated_at = CURRENT_TIMESTAMP');
@@ -274,22 +259,18 @@ router.patch('/:serialNumber', authenticateToken, requireAdmin, async (req, res)
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({
message: '序列号更新成功',
serial: {
serialNumber: updatedSerial.serial_number,
companyName: updatedSerial.company_name,
validUntil: updatedSerial.valid_until,
isActive: updatedSerial.is_active,
createdAt: updatedSerial.created_at,
updatedAt: updatedSerial.updated_at,
createdBy: updatedSerial.created_by_name
serialNumber: (updatedSerial as any).serial_number,
companyName: (updatedSerial as any).company_name,
validUntil: (updatedSerial as any).valid_until,
isActive: (updatedSerial as any).is_active,
createdAt: (updatedSerial as any).created_at,
updatedAt: (updatedSerial as any).updated_at,
createdBy: (updatedSerial as any).created_by_name
}
});
} catch (error) {
@@ -298,31 +279,27 @@ router.patch('/:serialNumber', authenticateToken, requireAdmin, async (req, res)
}
});
// 吊销序列号
router.post('/:serialNumber/revoke', authenticateToken, requireAdmin, async (req, res) => {
router.post('/:serialNumber/revoke', authenticateToken, requireAdmin, async (req: Request<{ serialNumber: string }>, res: Response): Promise<void> => {
try {
const { serialNumber } = req.params;
if (!serialNumber) {
return res.status(400).json({ error: '序列号不能为空' });
res.status(400).json({ error: '序列号不能为空' });
return;
}
// 检查序列号是否存在
const existingSerial = await db.get(
'SELECT * FROM serials WHERE serial_number = ?',
[serialNumber.toUpperCase()]
);
const existingSerial = await db.get<{ is_active: number }>('SELECT * FROM serials WHERE serial_number = ?', [serialNumber.toUpperCase()]);
if (!existingSerial) {
return res.status(404).json({ error: '序列号不存在' });
res.status(404).json({ error: '序列号不存在' });
return;
}
// 如果已经吊销,返回提示
if (!existingSerial.is_active) {
return res.status(400).json({ error: '序列号已被吊销' });
res.status(400).json({ error: '序列号已被吊销' });
return;
}
// 吊销序列号(将 is_active 设为 0
await db.run(
'UPDATE serials SET is_active = 0, updated_at = CURRENT_TIMESTAMP WHERE serial_number = ?',
[serialNumber.toUpperCase()]
@@ -340,48 +317,43 @@ router.post('/:serialNumber/revoke', authenticateToken, requireAdmin, async (req
}
});
// 自定义前缀生成序列号(管理员权限)
router.post('/generate-with-prefix', authenticateToken, requireAdmin, async (req, res) => {
router.post('/generate-with-prefix', authenticateToken, requireAdmin, async (req: Request<{}, {}, GenerateSerialWithPrefixRequest>, res: Response): Promise<void> => {
try {
const { companyName, quantity = 1, validDays = 365, serialPrefix } = req.body;
if (!companyName) {
return res.status(400).json({ error: '企业名称不能为空' });
res.status(400).json({ error: '企业名称不能为空' });
return;
}
if (!serialPrefix || serialPrefix.length > 10) {
return res.status(400).json({ error: '自定义前缀不能为空且不能超过10个字符' });
res.status(400).json({ error: '自定义前缀不能为空且不能超过10个字符' });
return;
}
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();
validUntil.setDate(validUntil.getDate() + validDays);
// 生成序列号
const serials = [];
const serials: SerialListItem[] = [];
const prefix = serialPrefix.toUpperCase().replace(/[^A-Z0-9]/g, '');
if (!prefix) {
return res.status(400).json({ error: '自定义前缀包含无效字符,只能包含字母和数字' });
res.status(400).json({ error: '自定义前缀包含无效字符,只能包含字母和数字' });
return;
}
// 批量插入序列号
const insertPromises = [];
for (let i = 0; i < quantity; i++) {
// 使用随机数生成序列号,避免重复
const randomPart = Math.floor(Math.random() * 1000000).toString().padStart(6, '0');
const serialNumber = `${prefix}${randomPart}`;
insertPromises.push(
db.run(
await db.run(
'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({
@@ -392,8 +364,6 @@ router.post('/generate-with-prefix', authenticateToken, requireAdmin, async (req
});
}
await Promise.all(insertPromises);
res.json({
message: `成功生成${quantity}个序列号`,
serials
@@ -404,4 +374,4 @@ router.post('/generate-with-prefix', authenticateToken, requireAdmin, async (req
}
});
module.exports = router;
export default router;

View File

@@ -1,14 +1,11 @@
const Database = require('better-sqlite3');
const bcrypt = require('bcryptjs');
const path = require('path');
import Database from 'better-sqlite3';
import bcrypt from 'bcryptjs';
import path from 'path';
// 创建数据库连接
const dbPath = path.join(__dirname, '../data/database.sqlite');
const db = new Database(dbPath, { verbose: console.log });
// 创建表
const createTables = () => {
// 用户表
const createTables = (): void => {
db.exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -22,18 +19,16 @@ const createTables = () => {
)
`);
// 企业表
db.exec(`
CREATE TABLE IF NOT EXISTS companies (
id INTEGER PRIMARY KEY AUTOINCREMENT,
company_name TEXT UNIQUE NOT NULL,
is_active BOOLEAN DEFAULT 1,
is_active BOOLEAN BOOLEAN DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
// 序列号表
db.exec(`
CREATE TABLE IF NOT EXISTS serials (
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_serial_number ON serials (serial_number)');
db.exec('CREATE INDEX IF NOT EXISTS idx_company_name_serials ON serials (company_name)');
@@ -58,20 +52,16 @@ const createTables = () => {
console.log('数据库表创建完成');
};
// 创建默认管理员用户
const createDefaultUser = async () => {
const createDefaultUser = async (): Promise<void> => {
const username = 'admin';
const password = 'Beifan@2026';
const name = '系统管理员';
// 检查用户是否已存在
const user = db.prepare('SELECT * FROM users WHERE username = ?').get(username);
if (!user) {
// 哈希密码
const hashedPassword = await bcrypt.hash(password, 10);
// 创建用户
db.prepare(
'INSERT INTO users (username, password, name, email, role) VALUES (?, ?, ?, ?, ?)'
).run(username, hashedPassword, name, 'admin@example.com', 'admin');
@@ -84,12 +74,9 @@ const createDefaultUser = async () => {
}
};
// 初始化数据库
const initDatabase = async () => {
const initDatabase = async (): Promise<void> => {
createTables();
await createDefaultUser();
// 关闭数据库连接
db.close();
console.log('数据库连接已关闭');
};

View File

@@ -1,37 +1,31 @@
require('dotenv').config({ path: __dirname + '/.env' });
const express = require('express');
const cors = require('cors');
const path = require('path');
// 导入路由
const authRoutes = require('./routes/auth');
const serialRoutes = require('./routes/serials');
const companyRoutes = require('./routes/companies');
import 'dotenv/config';
import express, { Request, Response, NextFunction } from 'express';
import cors from 'cors';
import path from 'path';
import authRoutes from './routes/auth';
import serialRoutes from './routes/serials';
import companyRoutes from './routes/companies';
const app = express();
const PORT = process.env.PORT || 3000;
// 中间件
app.use(cors());
app.use(express.json({ limit: '10mb' }));
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');
next();
});
// API路由必须在静态文件服务之前
app.use('/api/auth', authRoutes);
app.use('/api/serials', serialRoutes);
app.use('/api/companies', companyRoutes);
// 健康检查
app.get('/api/health', (req, res) => {
app.get('/api/health', (req: Request, res: Response): void => {
res.json({ status: 'ok', message: '服务器运行正常' });
});
// 静态文件服务
const frontendPath = path.join(__dirname, '..', 'frontend');
const distPath = path.join(frontendPath, 'dist');
const publicPath = path.join(frontendPath, 'public');
@@ -42,12 +36,10 @@ if (process.env.NODE_ENV === 'production') {
app.use(express.static(publicPath));
}
// 404处理
app.use((req, res) => {
app.use((req: Request, res: Response): void => {
if (req.path.startsWith('/api/')) {
res.status(404).json({ error: 'API接口不存在' });
} else {
// 对于非API请求返回index.html用于React Router
if (process.env.NODE_ENV === 'production') {
res.sendFile(path.join(distPath, 'index.html'));
} else {
@@ -56,8 +48,7 @@ app.use((req, res) => {
}
});
// 错误处理中间件
app.use((error, req, res, next) => {
app.use((error: Error, req: Request, res: Response, next: NextFunction): void => {
console.error('服务器错误:', error);
if (req.path.startsWith('/api/')) {
@@ -67,8 +58,7 @@ app.use((error, req, res, next) => {
}
});
// 启动服务器
app.listen(PORT, () => {
app.listen(PORT, (): void => {
console.log(`服务器运行在 http://localhost:${PORT}`);
console.log(`API文档: http://localhost:${PORT}/api/health`);
console.log(`环境: ${process.env.NODE_ENV || 'development'}`);

19
tsconfig.json Normal file
View 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
View 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 }>;
}

View File

@@ -1,50 +1,49 @@
const Database = require('better-sqlite3');
const path = require('path');
import Database from 'better-sqlite3';
import path from 'path';
class DatabaseWrapper {
private db: Database.Database;
private dbPath: string;
constructor() {
this.dbPath = process.env.DB_PATH || path.join(__dirname, '../data/database.sqlite');
this.db = new Database(this.dbPath, { verbose: console.log });
}
// 查询单个记录
get(sql, params = []) {
get<T = any>(sql: string, params: any[] = []): T | undefined {
try {
const stmt = this.db.prepare(sql);
return stmt.get(params);
return stmt.get(params) as T | undefined;
} catch (error) {
console.error('数据库查询错误:', error);
throw error;
}
}
// 查询多个记录
all(sql, params = []) {
all<T = any>(sql: string, params: any[] = []): T[] {
try {
const stmt = this.db.prepare(sql);
return stmt.all(params);
return stmt.all(params) as T[];
} catch (error) {
console.error('数据库查询错误:', error);
throw error;
}
}
// 执行插入、更新、删除操作
run(sql, params = []) {
run(sql: string, params: any[] = []): { id: number; changes: number } {
try {
const stmt = this.db.prepare(sql);
const result = stmt.run(params);
return { id: result.lastInsertRowid, changes: result.changes };
return { id: result.lastInsertRowid as number, changes: result.changes };
} catch (error) {
console.error('数据库操作错误:', error);
throw error;
}
}
// 关闭数据库连接
close() {
close(): void {
this.db.close();
}
}
module.exports = new DatabaseWrapper();
export default new DatabaseWrapper();