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**: 运行时环境 - **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

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", "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
View File

@@ -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: {}

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

View File

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

View File

@@ -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('数据库连接已关闭');
}; };

View File

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