From a80c4790273532270a84c5cfc3b75f54fd23a094 Mon Sep 17 00:00:00 2001 From: ZHENG XIAOYI Date: Fri, 6 Feb 2026 14:44:14 +0800 Subject: [PATCH] refactor: migrate entire project to TypeScript --- README.md | 42 +- middleware/auth.js | 46 --- middleware/auth.ts | 53 +++ package.json | 19 +- pnpm-lock.yaml | 542 ++++++++++++++++++++++++++ routes/auth.js | 139 ------- routes/auth.ts | 144 +++++++ routes/{companies.js => companies.ts} | 149 +++---- routes/{serials.js => serials.ts} | 196 ++++------ scripts/{init-db.js => init-db.ts} | 29 +- server.js => server.ts | 38 +- tsconfig.json | 19 + types/index.d.ts | 160 ++++++++ utils/{database.js => database.ts} | 27 +- 14 files changed, 1141 insertions(+), 462 deletions(-) delete mode 100644 middleware/auth.js create mode 100644 middleware/auth.ts delete mode 100644 routes/auth.js create mode 100644 routes/auth.ts rename routes/{companies.js => companies.ts} (69%) rename routes/{serials.js => serials.ts} (59%) rename scripts/{init-db.js => init-db.ts} (82%) rename server.js => server.ts (66%) create mode 100644 tsconfig.json create mode 100644 types/index.d.ts rename utils/{database.js => database.ts} (57%) diff --git a/README.md b/README.md index 6e90138..2135aef 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,11 @@ # 授权管理系统 - 后端服务 -浙江贝凡企业授权管理系统的后端服务,基于 Node.js + Express + SQLite。 +浙江贝凡企业授权管理系统的后端服务,基于 Node.js + TypeScript + Express + SQLite。 ## 技术栈 - **Node.js**: 运行时环境 +- **TypeScript**: 类型安全 - **Express**: Web 框架 - **SQLite**: 数据库 - **JWT**: 身份认证 @@ -15,20 +16,23 @@ ``` 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 # 项目配置 +└── 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 diff --git a/middleware/auth.js b/middleware/auth.js deleted file mode 100644 index 7e577ff..0000000 --- a/middleware/auth.js +++ /dev/null @@ -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 -}; \ No newline at end of file diff --git a/middleware/auth.ts b/middleware/auth.ts new file mode 100644 index 0000000..a2a2477 --- /dev/null +++ b/middleware/auth.ts @@ -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 => { + 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(); +}; diff --git a/package.json b/package.json index 60a87b9..b8dcf69 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 901e6c5..f380b32 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: {} diff --git a/routes/auth.js b/routes/auth.js deleted file mode 100644 index 5642e77..0000000 --- a/routes/auth.js +++ /dev/null @@ -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; \ No newline at end of file diff --git a/routes/auth.ts b/routes/auth.ts new file mode 100644 index 0000000..7089d0b --- /dev/null +++ b/routes/auth.ts @@ -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 => { + try { + const { username, password } = req.body; + + if (!username || !password) { + res.status(400).json({ error: '用户名和密码不能为空' }); + return; + } + + const user = await db.get('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 => { + try { + const user = await db.get( + '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 => { + 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>('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 => { + 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( + '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; diff --git a/routes/companies.js b/routes/companies.ts similarity index 69% rename from routes/companies.js rename to routes/companies.ts index 939cfea..fbbb3a0 100644 --- a/routes/companies.js +++ b/routes/companies.ts @@ -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 => { 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 => { 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 => { 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 => { 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 => { 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 => { 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 => { 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; \ No newline at end of file +export default router; diff --git a/routes/serials.js b/routes/serials.ts similarity index 59% rename from routes/serials.js rename to routes/serials.ts index b268d74..a3b0212 100644 --- a/routes/serials.js +++ b/routes/serials.ts @@ -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 => { 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( - 'INSERT INTO serials (serial_number, company_name, valid_until, created_by) VALUES (?, ?, ?, ?)', - [serialNumber, companyName, validUntil.toISOString().slice(0, 19).replace('T', ' '), req.user.id] - ) + 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] ); 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 => { 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 => { 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 => { 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 => { 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 => { 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 => { 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( - 'INSERT INTO serials (serial_number, company_name, valid_until, created_by) VALUES (?, ?, ?, ?)', - [serialNumber, companyName, validUntil.toISOString(), req.user.id] - ) + await db.run( + 'INSERT INTO serials (serial_number, company_name, valid_until, created_by) VALUES (?, ?, ?, ?)', + [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; \ No newline at end of file +export default router; diff --git a/scripts/init-db.js b/scripts/init-db.ts similarity index 82% rename from scripts/init-db.js rename to scripts/init-db.ts index c6770da..5cfc628 100644 --- a/scripts/init-db.js +++ b/scripts/init-db.ts @@ -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 => { 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,14 +74,11 @@ const createDefaultUser = async () => { } }; -// 初始化数据库 -const initDatabase = async () => { +const initDatabase = async (): Promise => { createTables(); await createDefaultUser(); - - // 关闭数据库连接 db.close(); console.log('数据库连接已关闭'); }; -initDatabase(); \ No newline at end of file +initDatabase(); diff --git a/server.js b/server.ts similarity index 66% rename from server.js rename to server.ts index a269d9f..fba2831 100644 --- a/server.js +++ b/server.ts @@ -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,9 +58,8 @@ 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'}`); -}); \ No newline at end of file +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..e5e562a --- /dev/null +++ b/tsconfig.json @@ -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"] +} diff --git a/types/index.d.ts b/types/index.d.ts new file mode 100644 index 0000000..063616f --- /dev/null +++ b/types/index.d.ts @@ -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 { + 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 }>; +} diff --git a/utils/database.js b/utils/database.ts similarity index 57% rename from utils/database.js rename to utils/database.ts index 0b87b80..94a0e1c 100644 --- a/utils/database.js +++ b/utils/database.ts @@ -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(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(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(); \ No newline at end of file +export default new DatabaseWrapper();