chore: mqsrv backend

This commit is contained in:
Chen Gu
2026-05-09 00:52:04 +08:00
commit b84f111e8f
21 changed files with 4593 additions and 0 deletions

20
.env Normal file
View File

@@ -0,0 +1,20 @@
# 数据库连接
DATABASE_URL="postgresql://maqt:maqt123456@localhost:5432/maqt?schema=public"
# JWT 密钥
JWT_SECRET="maqt-jwt-secret-key-2026-change-in-production"
# JWT 过期时间
JWT_EXPIRES_IN="7d"
# 数据加密密钥32字节
ENCRYPTION_KEY="maqt-encryption-key-32bytes!"
# 服务端口
PORT=3001
# 环境
NODE_ENV="development"
# VIP 卡密批次密钥
BATCH_SECRET="maqt-batch-secret-2026"

20
.env.example Normal file
View File

@@ -0,0 +1,20 @@
# 数据库连接
DATABASE_URL="postgresql://postgres:password@localhost:5432/maqt?schema=public"
# JWT 密钥(请修改为随机字符串)
JWT_SECRET="your-super-secret-jwt-key-change-this-in-production"
# JWT 过期时间
JWT_EXPIRES_IN="7d"
# 数据加密密钥32字节请修改
ENCRYPTION_KEY="0123456789abcdef0123456789abcdef"
# 服务端口
PORT=3001
# 环境
NODE_ENV="development"
# VIP 卡密批次密钥(用于生成卡密签名)
BATCH_SECRET="your-batch-secret-key"

192
API_FULL_ANALYSIS.md Normal file
View File

@@ -0,0 +1,192 @@
# 码枪堂 API 全线分析报告
> 基于 Fiddler 抓包 ss3.saz原版 API+ ss4.saz自定义服务器对比
---
## 一、API 域名概览
| 域名 | 用途 |
|------|------|
| `https://maqt.top/api/*` | 主业务 API |
| `https://pop.maqt.top/api/*` | 弹窗广告 API |
| `https://update.maqt.top/*` | 更新服务 |
| `https://tuku.maqt.top/*` | 图片托管 |
| `http://100.105.17.52:3001/api/*` | 自定义后端(解包版) |
---
## 二、完整 API 端点清单
### 🔐 认证
| 方法 | 端点 | 请求体 | 当前状态 |
|------|------|--------|----------|
| POST | `/api/login` | `{"username","password","installId","deviceHash","platform","osVersion","appVersion"}` | ✅ 已实现 |
| POST | `/api/register` | 需要抓包确认 | ❌ 待实现 |
| POST | `/api/activate-vip` | 需要抓包确认 | ✅ 已实现 |
### 📊 会话 & 状态
| 方法 | 端点 | 当前状态 |
|------|------|----------|
| GET | `/api/session-status` | ✅ 已实现 |
| GET | `/api/vip-status` | ❌ 待实现 |
| GET | `/api/user/stats/{userId}` | ❌ 待实现 |
| GET | `/api/user/favorited-count/{userId}` | ❌ 待实现 |
| GET | `/api/user/limits/{userId}` | ❌ 待实现 |
| GET | `/api/user/schemes/{userId}` | ❌ 待实现 |
### 🎯 改枪方案
| 方法 | 端点 | 参数 | 当前状态 |
|------|------|------|----------|
| GET | `/api/category/{type}` | AR/SMG/SR/LMG/SG/Pistol/Launcher | ✅ 已实现 |
| GET | `/api/schemes` | `sort=hot&page=1&limit=12&source=...` | ✅ 已实现 |
| GET | `/api/schemes` | `sort=hot&page=1&limit=12&weaponCategory&weaponName` | ✅ 已实现 |
| POST | `/api/schemes/{id}/use` | 需要抓包确认 | ❌ 待实现 |
### ❤️ 收藏
| 方法 | 端点 | 当前状态 |
|------|------|----------|
| GET | `/api/favorites/count` | ✅ 已实现 |
| GET | `/api/favorites/check?schemeId={id}&source=...` | ✅ 已实现 |
### 🎯 弹出广告
| 方法 | 端点 | 当前状态 |
|------|------|----------|
| GET | `/api/popups/test_beta_01` | ❌ 待实现 |
| GET | `/api/aftersale-tutorial-popup` | ❌ 待实现 |
| GET | `/api/software-version-ad` | ❌ 待实现 |
### 🔄 更新服务
| 方法 | 端点 | 备注 |
|------|------|------|
| GET | `/latest.yml` | Electron auto-updater 配置 |
| GET | `/update-config.json` | 更新配置 |
### 🖼️ 图片托管
| 方法 | 端点 | 备注 |
|------|------|------|
| GET | `tuku.maqt.top/i/2026/03/22/*.png` | 静态图片资源 |
---
## 三、ss3 vs ss4 请求对比(关键差异)
### 原版ss3完整流程
```
1. 检查更新: GET update.maqt.top/{latest.yml,update-config.json}
2. 弹窗广告: GET pop.maqt.top/api/software-version-ad
3. 会话状态: GET maqt.top/api/session-status (高频轮询!)
4. 注册/登录: POST maqt.top/api/register
5. 获取分类: GET maqt.top/api/category/{type}
6. 获取方案: GET maqt.top/api/schemes?...
7. 收藏检查: GET maqt.top/api/favorites/check?schemeId=...
8. 激活VIP: POST maqt.top/api/activate-vip
9. VIP状态: GET maqt.top/api/vip-status
10. 用户数据: GET maqt.top/api/user/{stats,favorited-count,limits,schemes}/{id}
11. 方案使用: POST maqt.top/api/schemes/{id}/use
12. 图片加载: GET tuku.maqt.top/... (持续轮询)
```
### 自定义服务器ss4 后半段)请求序列:
```
162: GET /api/popups/test_beta_01
163: GET /api/session-status
164: GET /api/software-version-ad
165: GET /api/aftersale-tutorial-popup
166: GET /update-config.json
169: GET /api/software-version-ad
170: GET /api/popups/test_beta_01
171: GET /api/aftersale-tutorial-popup
172: GET /update-config.json
173: GET /api/category/AR
174: GET /api/schemes?sort=hot&page=1&limit=12&source=...
175: GET /api/category/SMG
176: GET /api/category/SR
177: GET /api/category/LMG
178: GET /api/category/SG
179: GET /api/category/Pistol
180: GET /api/category/Launcher
181: POST /api/login
182: GET /api/session-status
183: GET /api/favorites/count
184: GET /api/schemes?...
185: GET /api/schemes?...
186: GET /api/favorites/count
187: GET /api/session-status
190: GET /api/session-status
194-199: GET /api/session-status (轮询...)
```
### ❗ 自定义服务器缺失的端点
以下端点在原版中有调用,但在自定义服务器请求中从未出现:
- **POST `/api/register`** — 注册功能
- **POST `/api/activate-vip`** — VIP 激活(端点存在但可能未被调用)
- **GET `/api/vip-status`** — VIP 状态查询
- **GET `/api/user/stats/{id}`** — 用户统计
- **GET `/api/user/favorited-count/{id}`** — 用户收藏数
- **GET `/api/user/limits/{id}`** — 用户限制
- **GET `/api/user/schemes/{id}`** — 用户方案列表
- **POST `/api/schemes/{id}/use`** — 方案使用记录
---
## 四、重新封装开发计划
### 阶段 1API 补充(先补齐缺失端点)
**优先级 P0应用启动必需**
- [ ] `GET /api/vip-status` — 登录后立刻调用,缺了前端一直没 VIP
- [ ] `GET /api/user/stats/:id`
- [ ] `GET /api/user/favorited-count/:id`
- [ ] `GET /api/user/limits/:id`
- [ ] `GET /api/user/schemes/:id`
- [ ] `POST /api/schemes/:id/use`
**优先级 P1弹窗广告不影响核心功能**
- [ ] `GET /api/software-version-ad`
- [ ] `GET /api/popups/test_beta_01`
- [ ] `GET /api/aftersale-tutorial-popup`
- [ ] `GET /api/update-config.json`
**优先级 P2注册一次性的**
- [ ] `POST /api/register`(含 installId, deviceHash
### 阶段 2桌面端复刻Electron + React
等 API 补全后,桌面端从头写:
```
maqt-desktop/
├── electron/
│ ├── main.ts # 窗口管理
│ └── preload.ts # electronAPI
├── src/
│ ├── App.tsx
│ ├── pages/
│ │ ├── Home.tsx # 桌面首页
│ │ ├── Login.tsx
│ │ ├── Schemes.tsx # 改枪方案
│ │ ├── Filters.tsx # 画面滤镜
│ │ ├── Optimization.tsx
│ │ └── ...
│ ├── components/
│ │ ├── Dock.tsx # 底部导航
│ │ ├── DesktopIcons.tsx
│ │ └── ...
│ └── hooks/
│ ├── useAuth.ts
│ └── useApi.ts
├── native/ # 原生程序
│ ├── MaqiangTangh1.exe
│ ├── nvidiaProfileInspector.exe
│ └── tools/
└── package.json
```
### 阶段 3原生功能对接
- [ ] spawn `MaqiangTangh1.exe` 执行系统优化
- [ ] spawn `MaqiangTangXiXiOverlay.exe` 游戏内 Overlay
- [ ] 调用 PowerShell 脚本 Gamma 校准
- [ ] NVIDIA Profile Inspector 配置
- [ ] 硬件监控面板

28
Dockerfile Normal file
View File

@@ -0,0 +1,28 @@
# 第一阶段: 构建
FROM node:22-alpine AS builder
WORKDIR /app
# 安装必要的系统依赖Prisma 需要)
RUN apk add --no-cache openssl
COPY package*.json ./
RUN npm ci
COPY . .
RUN npx prisma generate
RUN npx tsc
# 第二阶段: 运行
FROM node:22-alpine
WORKDIR /app
# 安装 OpenSSLPrisma 运行时依赖)
RUN apk add --no-cache openssl
COPY --from=builder /app/package*.json ./
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/prisma ./prisma
EXPOSE 3001
CMD ["node", "dist/index.js"]

167
README.md Normal file
View File

@@ -0,0 +1,167 @@
# 码枪堂后端 API
基于 Express + Prisma + PostgreSQL 的后端服务。
## 快速开始
### 1. 安装依赖
```bash
pnpm install
# 或
npm install
```
### 2. 配置环境变量
```bash
cp .env.example .env
```
编辑 `.env` 文件:
```env
# 数据库连接
DATABASE_URL="postgresql://postgres:password@localhost:5432/maqt?schema=public"
# JWT 密钥(请修改为随机字符串)
JWT_SECRET="your-super-secret-jwt-key-change-this-in-production"
# 数据加密密钥32字节
ENCRYPTION_KEY="0123456789abcdef0123456789abcdef"
# 服务端口
PORT=3001
```
### 3. 初始化数据库
```bash
# 生成 Prisma Client
pnpm db:generate
# 同步数据库结构
pnpm db:push
# 生成种子数据VIP卡密等
pnpm db:seed
```
### 4. 启动服务
```bash
# 开发模式(热重载)
pnpm dev
# 生产模式
pnpm build
pnpm start
```
## API 接口
### 认证
| 接口 | 方法 | 说明 |
|------|------|------|
| `/api/register` | POST | 注册用户 |
| `/api/login` | POST | 用户登录 |
| `/api/session-status` | GET | 会话状态 |
| `/api/vip-status` | GET | VIP 状态 |
| `/api/activate-vip` | POST | VIP 卡密激活 |
### 用户管理
| 接口 | 方法 | 说明 |
|------|------|------|
| `/api/user/username` | PUT | 更新用户名 |
| `/api/user/email` | PUT | 更新邮箱 |
| `/api/user/avatar` | PUT | 更新头像 |
| `/api/user/stats/:userId` | GET | 用户统计 |
| `/api/user/schemes/:userId` | GET | 用户方案列表 |
### 方案分享
| 接口 | 方法 | 说明 |
|------|------|------|
| `/api/schemes/` | GET | 烽火地带方案列表 |
| `/api/schemes/:id` | GET | 方案详情 |
| `/api/schemes/` | POST | 创建方案 |
| `/api/schemes/:id` | DELETE | 删除方案 |
| `/api/schemes_aob/` | GET | 全面战场方案列表 |
| `/api/schemes_aob/:id` | GET | 方案详情 |
| `/api/schemes_aob/` | POST | 创建方案 |
### 滤镜分享
| 接口 | 方法 | 说明 |
|------|------|------|
| `/api/filter-shares/` | GET | 滤镜列表 |
| `/api/filter-shares/:id` | GET | 滤镜详情 |
| `/api/filter-shares/` | POST | 创建滤镜 |
| `/api/filter-shares/:id` | DELETE | 删除滤镜 |
### 收藏
| 接口 | 方法 | 说明 |
|------|------|------|
| `/api/favorites/` | GET | 收藏列表 |
| `/api/favorites/` | POST | 添加收藏 |
| `/api/favorites/:id` | DELETE | 取消收藏 |
| `/api/favorites/check` | GET | 检查是否收藏 |
## VIP 卡密管理
### 生成卡密
运行种子脚本会自动生成卡密:
```bash
pnpm db:seed
```
生成的卡密格式:
- 月卡:`VIP30-XXXX-XXXX-XXXX-XXXX`
- 季卡:`VIP90-XXXX-XXXX-XXXX-XXXX`
- 年卡:`VIP365-XXXX-XXXX-XXXX-XXXX`
### 查询卡密
```bash
npx prisma studio
```
打开 Prisma Studio 可视化管理数据库。
## 项目结构
```
maqt-backend/
├── prisma/
│ ├── schema.prisma # 数据库模型
│ └── seed.ts # 种子数据
├── src/
│ ├── index.ts # 主入口
│ ├── middleware/
│ │ └── auth.ts # 认证中间件
│ ├── routes/
│ │ ├── auth.ts # 认证路由
│ │ ├── user.ts # 用户路由
│ │ ├── schemes.ts # 烽火地带方案
│ │ ├── schemesAob.ts# 全面战场方案
│ │ ├── filters.ts # 滤镜分享
│ │ ├── favorites.ts # 收藏
│ │ └── vip.ts # VIP 激活
│ └── utils/
│ ├── jwt.ts # JWT 工具
│ └── encryption.ts# 加密工具
├── package.json
├── tsconfig.json
└── .env.example
```
## 注意事项
1. **生产环境**请务必修改 `.env` 中的密钥
2. **数据库**推荐使用 PostgreSQL
3. **部署**建议配合 PM2 或 Docker

1882
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

39
package.json Normal file
View File

@@ -0,0 +1,39 @@
{
"name": "maqt-backend",
"version": "1.0.0",
"description": "码枪堂后端 API",
"main": "dist/index.js",
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc",
"start": "node dist/index.js",
"db:generate": "prisma generate",
"db:push": "prisma db push",
"db:migrate": "prisma migrate dev",
"db:studio": "prisma studio",
"db:seed": "tsx prisma/seed.ts"
},
"dependencies": {
"@prisma/client": "^5.15.0",
"bcryptjs": "^2.4.3",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"express-rate-limit": "^7.3.1",
"helmet": "^7.1.0",
"jsonwebtoken": "^9.0.2",
"uuid": "^9.0.1",
"zod": "^3.23.8"
},
"devDependencies": {
"@types/bcryptjs": "^2.4.6",
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/jsonwebtoken": "^9.0.6",
"@types/node": "^20.14.2",
"@types/uuid": "^9.0.8",
"prisma": "^5.15.0",
"tsx": "^4.15.4",
"typescript": "^5.4.5"
}
}

279
prisma/schema.prisma Normal file
View File

@@ -0,0 +1,279 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// ============================================
// 用户表
// ============================================
model User {
id String @id @default(uuid())
username String @unique @db.VarChar(50)
email String @unique @db.VarChar(100)
passwordHash String @map("password_hash") @db.VarChar(255)
avatar String? @db.VarChar(255)
// VIP 状态
isVip Boolean @default(false) @map("is_vip")
vipLevel Int @default(0) @map("vip_level")
vipExpireAt DateTime? @map("vip_expire_at")
// 统计
schemesCount Int @default(0) @map("schemes_count")
favoritesCount Int @default(0) @map("favorites_count")
// 设备绑定
installId String? @map("install_id") @db.VarChar(64)
deviceHash String? @map("device_hash") @db.VarChar(64)
lastDeviceCheck DateTime? @map("last_device_check")
// 元数据
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
lastLoginAt DateTime? @map("last_login_at")
deletedAt DateTime? @map("deleted_at")
// 关系
schemes Scheme[]
schemesAob SchemeAob[]
filterShares FilterShare[]
favorites Favorite[]
likes Like[]
logs UserLog[]
usedVipCards VipCard[] @relation("UsedCards")
deviceBindings DeviceBinding[]
@@map("users")
}
// ============================================
// VIP 卡密表
// ============================================
model VipCard {
id String @id @default(uuid())
cardKey String @unique @map("card_key") @db.VarChar(50)
cardType String @map("card_type") @db.VarChar(20)
days Int
status String @default("UNUSED") @db.VarChar(20)
usedBy String? @map("used_by")
usedAt DateTime? @map("used_at")
batchId String? @map("batch_id")
generatedAt DateTime @default(now()) @map("generated_at")
originalPrice Decimal? @map("original_price") @db.Decimal(10, 2)
salePrice Decimal? @map("sale_price") @db.Decimal(10, 2)
user User? @relation("UsedCards", fields: [usedBy], references: [id], onDelete: SetNull)
@@map("vip_cards")
}
// ============================================
// 方案表 - 烽火地带
// ============================================
model Scheme {
id String @id @default(uuid())
userId String @map("user_id")
title String? @db.VarChar(100)
description String? @db.Text
weaponName String? @map("weapon_name") @db.VarChar(100)
category String? @db.VarChar(50)
schemeContent String @map("scheme_content") @db.Text
contentEncrypted Boolean @default(true) @map("content_encrypted")
price Int @default(0)
viewsCount Int @default(0) @map("views_count")
downloadsCount Int @default(0) @map("downloads_count")
likesCount Int @default(0) @map("likes_count")
favoritesCount Int @default(0) @map("favorites_count")
gpuModel String? @map("gpu_model") @db.VarChar(100)
driverVersion String? @map("driver_version") @db.VarChar(50)
appVersion String? @map("app_version") @db.VarChar(20)
status String @default("DRAFT") @db.VarChar(20)
isOfficial Boolean @default(false) @map("is_official")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
@@index([weaponName])
@@index([category])
@@index([status])
@@map("schemes")
}
// ============================================
// 方案表 - 全面战场 (AOB)
// ============================================
model SchemeAob {
id String @id @default(uuid())
userId String @map("user_id")
title String? @db.VarChar(100)
description String? @db.Text
weaponName String? @map("weapon_name") @db.VarChar(100)
category String? @db.VarChar(50)
schemeContent String @map("scheme_content") @db.Text
contentEncrypted Boolean @default(true) @map("content_encrypted")
price Int @default(0)
viewsCount Int @default(0) @map("views_count")
downloadsCount Int @default(0) @map("downloads_count")
likesCount Int @default(0) @map("likes_count")
favoritesCount Int @default(0) @map("favorites_count")
status String @default("DRAFT") @db.VarChar(20)
isOfficial Boolean @default(false) @map("is_official")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
@@index([weaponName])
@@map("schemes_aob")
}
// ============================================
// 滤镜分享表
// ============================================
model FilterShare {
id String @id @default(uuid())
userId String @map("user_id")
title String @db.VarChar(100)
description String? @db.Text
category String? @db.VarChar(50)
filterContent String @map("filter_content") @db.Text
contentFormat String @default("MQTS1") @map("content_format") @db.VarChar(20)
viewsCount Int @default(0) @map("views_count")
likesCount Int @default(0) @map("likes_count")
status String @default("DRAFT") @db.VarChar(20)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
@@map("filter_shares")
}
// ============================================
// 收藏表
// ============================================
model Favorite {
id String @id @default(uuid())
userId String @map("user_id")
targetType String @map("target_type") @db.VarChar(20)
targetId String @map("target_id")
createdAt DateTime @default(now()) @map("created_at")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([userId, targetType, targetId])
@@index([userId])
@@map("favorites")
}
// ============================================
// 点赞表
// ============================================
model Like {
id String @id @default(uuid())
userId String @map("user_id")
targetType String @map("target_type") @db.VarChar(20)
targetId String @map("target_id")
createdAt DateTime @default(now()) @map("created_at")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([userId, targetType, targetId])
@@index([userId])
@@index([targetType, targetId])
@@map("likes")
}
// ============================================
// 分类表
// ============================================
model Category {
id String @id @default(uuid())
name String @db.VarChar(50)
type String @db.VarChar(20)
sortOrder Int @default(0) @map("sort_order")
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
@@map("categories")
}
// ============================================
// 用户日志表
// ============================================
model UserLog {
id String @id @default(uuid())
userId String? @map("user_id")
action String @db.VarChar(50)
targetType String? @map("target_type") @db.VarChar(50)
targetId String? @map("target_id")
installId String? @map("install_id") @db.VarChar(64)
deviceHash String? @map("device_hash") @db.VarChar(64)
ipAddress String? @map("ip_address") @db.VarChar(45)
createdAt DateTime @default(now()) @map("created_at")
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
@@index([userId])
@@index([action])
@@map("user_logs")
}
// ============================================
// 设备绑定表
// ============================================
model DeviceBinding {
id String @id @default(uuid())
userId String @map("user_id")
installId String @map("install_id") @db.VarChar(64)
deviceHash String @map("device_hash") @db.VarChar(64)
boundAt DateTime @default(now()) @map("bound_at")
lastActiveAt DateTime? @map("last_active_at")
isActive Boolean @default(true) @map("is_active")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([userId, installId])
@@map("device_bindings")
}

90
prisma/seed.ts Normal file
View File

@@ -0,0 +1,90 @@
import { PrismaClient } from '@prisma/client';
import crypto from 'crypto';
const prisma = new PrismaClient();
/**
* 生成 VIP 卡密
*/
function generateCardKey(days: number): string {
const prefix = `VIP${days}`;
const segments = [];
for (let i = 0; i < 4; i++) {
segments.push(crypto.randomBytes(2).toString('hex').toUpperCase());
}
return `${prefix}-${segments.join('-')}`;
}
async function main() {
console.log('🌱 开始种子数据初始化...');
// ============================================
// 创建分类
// ============================================
const categories = [
// 烽火地带
{ name: '突击步枪', type: 'SCHEME' },
{ name: '冲锋枪', type: 'SCHEME' },
{ name: '狙击步枪', type: 'SCHEME' },
{ name: '轻机枪', type: 'SCHEME' },
{ name: '霰弹枪', type: 'SCHEME' },
{ name: '手枪', type: 'SCHEME' },
// 全面战场
{ name: '突击步枪', type: 'SCHEME_AOB' },
{ name: '冲锋枪', type: 'SCHEME_AOB' },
{ name: '狙击步枪', type: 'SCHEME_AOB' },
{ name: '轻机枪', type: 'SCHEME_AOB' },
{ name: '霰弹枪', type: 'SCHEME_AOB' },
// 滤镜
{ name: '烽火地带', type: 'FILTER' },
{ name: '全面战场', type: 'FILTER' },
{ name: '通用', type: 'FILTER' },
];
for (const cat of categories) {
await prisma.category.create({
data: cat,
}).catch(() => {}); // 忽略重复错误
}
console.log(`✅ 创建 ${categories.length} 个分类`);
// ============================================
// 生成 VIP 卡密
// ============================================
const cardConfigs = [
{ type: 'MONTH', days: 30, count: 100 },
{ type: 'QUARTER', days: 90, count: 50 },
{ type: 'YEAR', days: 365, count: 20 },
];
let totalCards = 0;
for (const config of cardConfigs) {
const cards = [];
for (let i = 0; i < config.count; i++) {
cards.push({
cardKey: generateCardKey(config.days),
cardType: config.type,
days: config.days,
});
}
await prisma.vipCard.createMany({ data: cards, skipDuplicates: true });
totalCards += config.count;
console.log(`✅ 生成 ${config.type} 卡密 ${config.count}`);
}
console.log(`\n🎉 种子数据初始化完成!`);
console.log(` - 分类:${categories.length}`);
console.log(` - VIP 卡密:${totalCards}`);
}
main()
.catch((e) => {
console.error('❌ 种子数据初始化失败:', e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});

212
src/index.ts Normal file
View File

@@ -0,0 +1,212 @@
import express, { Request, Response } from 'express';
import cors from 'cors';
import helmet from 'helmet';
import rateLimit from 'express-rate-limit';
import dotenv from 'dotenv';
dotenv.config();
import authRoutes from './routes/auth';
import userRoutes from './routes/user';
import schemeRoutes from './routes/schemes';
import schemeAobRoutes from './routes/schemesAob';
import filterRoutes from './routes/filters';
import vipRoutes from './routes/vip';
import favoriteRoutes from './routes/favorites';
const app = express();
const PORT = process.env.PORT || 3001;
// ============================================
// 中间件
// ============================================
// 安全头 - 开发模式禁用 CSP
app.use(helmet({
contentSecurityPolicy: false,
crossOriginEmbedderPolicy: false,
}));
// CORS
app.use(cors({
origin: true, // 允许所有来源(开发模式)
credentials: true,
}));
// JSON 解析
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));
// 速率限制
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 分钟
max: 100, // 每个 IP 最多 100 次请求
message: { success: false, message: '请求过于频繁,请稍后再试' },
});
app.use('/api/', limiter);
// 登录接口更严格的限制
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 10,
message: { success: false, message: '登录尝试过于频繁,请稍后再试' },
});
app.use('/api/login', loginLimiter);
app.use('/api/register', loginLimiter);
// ============================================
// 路由
// ============================================
app.get('/api/activity/ping', (req, res) => {
res.json({ success: true, message: 'pong', timestamp: Date.now() });
});
app.use('/api', authRoutes);
app.use('/api/user', userRoutes);
app.use('/api/schemes', schemeRoutes);
app.use('/api/schemes_aob', schemeAobRoutes);
app.use('/api/filter-shares', filterRoutes);
app.use('/api', vipRoutes);
// 收藏计数(放在 favorites 路由之前,避免 auth 中间件拦截)
app.get('/api/favorites/count', async (req: Request, res: Response) => {
try {
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
const count = await prisma.favorite.count();
res.json({ success: true, count });
} catch (e) {
res.json({ success: true, count: 0 });
}
});
app.use('/api/favorites', favoriteRoutes);
// ============================================
// 缺失的存根接口(防止前端 404
// ============================================
// 二维码
app.get('/api/qrcode/public/current', (req, res) => {
res.json({ success: true, data: { qrcode: null, url: '' } });
});
// 弹窗
app.get('/api/popups/', (req, res) => {
res.json({ success: true, data: [] });
});
// 售后教程弹窗
app.get('/api/aftersale-tutorial-popup', (req, res) => {
res.json({ success: true, data: { shown: false } });
});
// 软件版本广告空数据dock 无广告项)
app.get('/api/software-version-ad', (req, res) => {
res.json({ success: true, data: { items: [] } });
});
// 游戏地图密码缓存
app.get('/api/game/map-password/cached', (req, res) => {
res.json({ success: true, data: [] });
});
// 筛选分类
app.get('/api/filter-share/categories', (req, res) => {
res.json({ success: true, data: [] });
});
// ============================================
// 武器调谐窗 (weapon-tuner) 所需的 API
// ============================================
// 武器分类列表
app.get('/api/weapon-categories', (req, res) => {
const categories = [
{ category: 'AR', name: '突击步枪' },
{ category: 'SMG', name: '冲锋枪' },
{ category: 'SR', name: '狙击步枪' },
{ category: 'LMG', name: '轻机枪' },
{ category: 'SG', name: '霰弹枪' },
{ category: 'Pistol', name: '手枪' },
{ category: 'Launcher', name: '发射器' },
];
res.json({ success: true, data: categories });
});
// 武器列表(按分类筛选)
app.get('/api/weapons', (req, res) => {
res.json({ success: true, data: [] });
});
// 分类下的武器
app.get('/api/category/:code', (req, res) => {
res.json({ success: true, data: [] });
});
// 广告列表
app.get('/api/adverts/list', (req, res) => {
res.json({ success: true, data: [
{
id: 1,
title: '码枪堂2.0 新版发布',
description: '全新界面,更多功能,一键优化三角洲行动游戏体验!',
author: '码枪堂官方',
avatar: null,
image_url: '',
link_url: 'https://wwamt.lanzout.com/b00odpq4wb',
isAdvert: true,
isVip: true,
shareTime: new Date().toISOString(),
}
] });
});
// 广告点击
app.post('/api/adverts/:id/click', (req, res) => {
res.json({ success: true, message: 'ok' });
});
// 头像列表
app.get('/api/avatars', (req, res) => {
res.json({ success: true, data: [] });
});
// ============================================
// 错误处理
// ============================================
app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
console.error('Error:', err);
if (err.name === 'UnauthorizedError') {
return res.status(401).json({ success: false, message: '访问令牌无效', code: 'INVALID_TOKEN' });
}
if (err.name === 'ZodError') {
return res.status(400).json({ success: false, message: '参数验证失败', errors: err.errors });
}
res.status(500).json({ success: false, message: '服务器内部错误' });
});
// 404
app.use((req, res) => {
res.status(404).json({ success: false, message: '接口不存在' });
});
// ============================================
// 启动
// ============================================
app.listen(PORT, () => {
console.log(`🚀 码枪堂 API 运行在 http://localhost:${PORT}`);
console.log(`📋 可用接口:`);
console.log(` POST /api/login`);
console.log(` POST /api/register`);
console.log(` POST /api/activate-vip`);
console.log(` GET /api/session-status`);
console.log(` GET /api/vip-status`);
});
export default app;

114
src/middleware/auth.ts Normal file
View File

@@ -0,0 +1,114 @@
import { Request, Response, NextFunction } from 'express';
import { verifyToken, extractToken } from '../utils/jwt';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
// 扩展 Request 类型
declare global {
namespace Express {
interface Request {
user?: {
userId: string;
username: string;
email: string;
isVip: boolean;
vipLevel: number;
};
}
}
}
/**
* 认证中间件
*/
export async function authMiddleware(req: Request, res: Response, next: NextFunction) {
try {
const token = extractToken(req.headers.authorization);
if (!token) {
return res.status(401).json({
success: false,
message: '访问令牌缺失,请先登录',
code: 'NO_TOKEN',
});
}
const payload = verifyToken(token);
if (!payload) {
return res.status(401).json({
success: false,
message: '访问令牌无效或已过期',
code: 'INVALID_TOKEN',
});
}
// 检查用户是否存在且未被删除
const user = await prisma.user.findUnique({
where: { id: payload.userId },
select: { id: true, deletedAt: true },
});
if (!user || user.deletedAt) {
return res.status(401).json({
success: false,
message: '用户不存在或已被禁用',
code: 'USER_NOT_FOUND',
});
}
// 将用户信息附加到请求对象
req.user = payload;
next();
} catch (error) {
console.error('Auth middleware error:', error);
return res.status(500).json({
success: false,
message: '认证服务异常',
});
}
}
/**
* 可选认证中间件(不强制要求登录)
*/
export async function optionalAuth(req: Request, res: Response, next: NextFunction) {
try {
const token = extractToken(req.headers.authorization);
if (token) {
const payload = verifyToken(token);
if (payload) {
req.user = payload;
}
}
next();
} catch (error) {
next();
}
}
/**
* VIP 认证中间件(要求 VIP 用户)
*/
export async function vipMiddleware(req: Request, res: Response, next: NextFunction) {
if (!req.user) {
return res.status(401).json({
success: false,
message: '请先登录',
code: 'NO_TOKEN',
});
}
if (!req.user.isVip) {
return res.status(403).json({
success: false,
message: '此功能仅限 VIP 用户使用',
code: 'VIP_REQUIRED',
});
}
next();
}

269
src/routes/auth.ts Normal file
View File

@@ -0,0 +1,269 @@
import { Router, Request, Response } from 'express';
import bcrypt from 'bcryptjs';
import { PrismaClient } from '@prisma/client';
import { z } from 'zod';
import { generateToken } from '../utils/jwt';
import { authMiddleware } from '../middleware/auth';
const router = Router();
const prisma = new PrismaClient();
// ============================================
// 注册
// ============================================
const registerSchema = z.object({
username: z.string().min(3).max(50),
password: z.string().min(6).max(100),
email: z.string().email(),
installId: z.string().optional(),
deviceHash: z.string().optional(),
});
router.post('/register', async (req: Request, res: Response) => {
try {
const body = registerSchema.parse(req.body);
// 检查用户名是否已存在
const existingUsername = await prisma.user.findUnique({
where: { username: body.username },
});
if (existingUsername) {
return res.status(400).json({
success: false,
message: '用户名已被使用',
});
}
// 检查邮箱是否已存在
const existingEmail = await prisma.user.findUnique({
where: { email: body.email },
});
if (existingEmail) {
return res.status(400).json({
success: false,
message: '邮箱已被注册',
});
}
// 密码哈希
const passwordHash = await bcrypt.hash(body.password, 10);
// 创建用户
const user = await prisma.user.create({
data: {
username: body.username,
email: body.email,
passwordHash,
installId: body.installId,
deviceHash: body.deviceHash,
},
});
// 生成 Token
const token = generateToken({
userId: user.id,
username: user.username,
email: user.email,
isVip: user.isVip,
vipLevel: user.vipLevel,
});
// 记录日志
await prisma.userLog.create({
data: {
userId: user.id,
action: 'REGISTER',
installId: body.installId,
deviceHash: body.deviceHash,
ipAddress: req.ip,
},
});
res.json({
success: true,
token,
user: {
id: user.id,
username: user.username,
email: user.email,
avatar: user.avatar,
isVip: user.isVip,
vipExpireAt: user.vipExpireAt,
vipLevel: user.vipLevel,
},
});
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({
success: false,
message: '参数验证失败',
errors: error.errors,
});
}
console.error('Register error:', error);
res.status(500).json({ success: false, message: '注册失败' });
}
});
// ============================================
// 登录
// ============================================
const loginSchema = z.object({
username: z.string(), // 可以是用户名或邮箱
password: z.string(),
installId: z.string().optional(),
deviceHash: z.string().optional(),
platform: z.string().optional(),
osVersion: z.string().optional(),
appVersion: z.string().optional(),
});
router.post('/login', async (req: Request, res: Response) => {
try {
const body = loginSchema.parse(req.body);
// 查找用户(支持用户名或邮箱登录)
const user = await prisma.user.findFirst({
where: {
OR: [
{ username: body.username },
{ email: body.username },
],
deletedAt: null,
},
});
if (!user) {
return res.status(401).json({
success: false,
message: '用户名或密码错误',
});
}
// 验证密码
const validPassword = await bcrypt.compare(body.password, user.passwordHash);
if (!validPassword) {
return res.status(401).json({
success: false,
message: '用户名或密码错误',
});
}
// 更新最后登录时间和设备信息
await prisma.user.update({
where: { id: user.id },
data: {
lastLoginAt: new Date(),
installId: body.installId || user.installId,
deviceHash: body.deviceHash || user.deviceHash,
},
});
// 生成 Token
const token = generateToken({
userId: user.id,
username: user.username,
email: user.email,
isVip: user.isVip,
vipLevel: user.vipLevel,
});
// 记录日志
await prisma.userLog.create({
data: {
userId: user.id,
action: 'LOGIN',
installId: body.installId,
deviceHash: body.deviceHash,
ipAddress: req.ip,
},
});
res.json({
success: true,
token,
user: {
id: user.id,
username: user.username,
email: user.email,
avatar: user.avatar,
isVip: user.isVip,
vipExpireAt: user.vipExpireAt,
vipLevel: user.vipLevel,
},
});
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({
success: false,
message: '参数验证失败',
errors: error.errors,
});
}
console.error('Login error:', error);
res.status(500).json({ success: false, message: '登录失败' });
}
});
// ============================================
// 会话状态
// ============================================
router.get('/session-status', authMiddleware, async (req: Request, res: Response) => {
res.json({
success: true,
valid: true,
user: req.user,
});
});
// ============================================
// VIP 状态
// ============================================
router.get('/vip-status', authMiddleware, async (req: Request, res: Response) => {
try {
const user = await prisma.user.findUnique({
where: { id: req.user!.userId },
select: { isVip: true, vipExpireAt: true, vipLevel: true },
});
if (!user) {
return res.status(404).json({
success: false,
message: '用户不存在',
});
}
// 检查 VIP 是否过期
let isVip = user.isVip;
if (user.vipExpireAt && new Date() > user.vipExpireAt) {
isVip = false;
// 更新数据库
await prisma.user.update({
where: { id: req.user!.userId },
data: { isVip: false },
});
}
const daysRemaining = user.vipExpireAt
? Math.max(0, Math.ceil((user.vipExpireAt.getTime() - Date.now()) / (1000 * 60 * 60 * 24)))
: 0;
res.json({
success: true,
isVip,
vipExpireAt: user.vipExpireAt,
vipLevel: user.vipLevel,
daysRemaining,
});
} catch (error) {
console.error('VIP status error:', error);
res.status(500).json({ success: false, message: '获取 VIP 状态失败' });
}
});
export default router;

167
src/routes/favorites.ts Normal file
View File

@@ -0,0 +1,167 @@
import { Router, Request, Response } from 'express';
import { PrismaClient } from '@prisma/client';
import { z } from 'zod';
import { authMiddleware } from '../middleware/auth';
const router = Router();
const prisma = new PrismaClient();
router.use(authMiddleware);
const TARGET_TYPES = ['SCHEME', 'SCHEME_AOB', 'FILTER'] as const;
// ============================================
// 获取收藏列表
// ============================================
router.get('/', async (req: Request, res: Response) => {
try {
const { type, page = 1, limit = 20 } = req.query;
const skip = (Number(page) - 1) * Number(limit);
const where: any = { userId: req.user!.userId };
if (type && TARGET_TYPES.includes(type as any)) {
where.targetType = type;
}
const favorites = await prisma.favorite.findMany({
where,
orderBy: { createdAt: 'desc' },
skip,
take: Number(limit),
});
res.json({ success: true, data: favorites });
} catch (error) {
console.error('Get favorites error:', error);
res.status(500).json({ success: false, message: '获取失败' });
}
});
// ============================================
// 添加收藏
// ============================================
const addFavoriteSchema = z.object({
targetType: z.enum(TARGET_TYPES),
targetId: z.string().uuid(),
});
router.post('/', async (req: Request, res: Response) => {
try {
const body = addFavoriteSchema.parse(req.body);
// 检查是否已收藏
const existing = await prisma.favorite.findFirst({
where: {
userId: req.user!.userId,
targetType: body.targetType,
targetId: body.targetId,
},
});
if (existing) {
return res.status(400).json({ success: false, message: '已收藏' });
}
// 验证目标是否存在
let targetExists = false;
if (body.targetType === 'SCHEME') {
targetExists = !!(await prisma.scheme.findFirst({
where: { id: body.targetId, status: 'PUBLISHED' },
}));
} else if (body.targetType === 'SCHEME_AOB') {
targetExists = !!(await prisma.schemeAob.findFirst({
where: { id: body.targetId, status: 'PUBLISHED' },
}));
} else if (body.targetType === 'FILTER') {
targetExists = !!(await prisma.filterShare.findFirst({
where: { id: body.targetId, status: 'PUBLISHED' },
}));
}
if (!targetExists) {
return res.status(404).json({ success: false, message: '目标不存在' });
}
// 创建收藏
await prisma.favorite.create({
data: {
userId: req.user!.userId,
targetType: body.targetType,
targetId: body.targetId,
},
});
// 更新用户收藏数
await prisma.user.update({
where: { id: req.user!.userId },
data: { favoritesCount: { increment: 1 } },
});
res.json({ success: true, message: '收藏成功' });
} catch (error) {
console.error('Add favorite error:', error);
res.status(500).json({ success: false, message: '收藏失败' });
}
});
// ============================================
// 取消收藏
// ============================================
router.delete('/:id', async (req: Request, res: Response) => {
try {
const { id } = req.params;
const favorite = await prisma.favorite.findUnique({
where: { id },
});
if (!favorite || favorite.userId !== req.user!.userId) {
return res.status(404).json({ success: false, message: '收藏不存在' });
}
await prisma.favorite.delete({ where: { id } });
// 更新用户收藏数
await prisma.user.update({
where: { id: req.user!.userId },
data: { favoritesCount: { decrement: 1 } },
});
res.json({ success: true, message: '已取消收藏' });
} catch (error) {
console.error('Remove favorite error:', error);
res.status(500).json({ success: false, message: '取消收藏失败' });
}
});
// ============================================
// 检查是否已收藏
// ============================================
router.get('/check', async (req: Request, res: Response) => {
try {
const { targetType, targetId } = req.query;
if (!targetType || !targetId) {
return res.status(400).json({ success: false, message: '参数缺失' });
}
const favorite = await prisma.favorite.findFirst({
where: {
userId: req.user!.userId,
targetType: String(targetType),
targetId: String(targetId),
},
});
res.json({
success: true,
isFavorited: !!favorite,
favoriteId: favorite?.id || null,
});
} catch (error) {
console.error('Check favorite error:', error);
res.status(500).json({ success: false, message: '检查失败' });
}
});
export default router;

135
src/routes/filters.ts Normal file
View File

@@ -0,0 +1,135 @@
import { Router, Request, Response } from 'express';
import { PrismaClient } from '@prisma/client';
import { authMiddleware, optionalAuth } from '../middleware/auth';
const router = Router();
const prisma = new PrismaClient();
// ============================================
// 获取滤镜列表
// ============================================
router.get('/', optionalAuth, async (req: Request, res: Response) => {
try {
const { page = 1, limit = 20, category } = req.query;
const skip = (Number(page) - 1) * Number(limit);
const where: any = { status: 'PUBLISHED' };
if (category) where.category = String(category);
const filters = await prisma.filterShare.findMany({
where,
orderBy: { createdAt: 'desc' },
skip,
take: Number(limit),
select: {
id: true,
title: true,
description: true,
category: true,
viewsCount: true,
likesCount: true,
createdAt: true,
user: { select: { id: true, username: true, avatar: true } },
},
});
res.json({ success: true, data: filters });
} catch (error) {
console.error('Get filter_shares error:', error);
res.status(500).json({ success: false, message: '获取失败' });
}
});
// ============================================
// 获取单个滤镜详情
// ============================================
router.get('/:id', optionalAuth, async (req: Request, res: Response) => {
try {
const { id } = req.params;
const filter = await prisma.filterShare.findUnique({
where: { id },
include: {
user: { select: { id: true, username: true, avatar: true } },
},
});
if (!filter || (filter.status !== 'PUBLISHED' && filter.userId !== req.user?.userId)) {
return res.status(404).json({ success: false, message: '滤镜不存在' });
}
await prisma.filterShare.update({
where: { id },
data: { viewsCount: { increment: 1 } },
});
res.json({ success: true, data: filter });
} catch (error) {
console.error('Get filter_share error:', error);
res.status(500).json({ success: false, message: '获取失败' });
}
});
// ============================================
// 创建滤镜
// ============================================
router.post('/', authMiddleware, async (req: Request, res: Response) => {
try {
const { title, description, category, filterContent, contentFormat = 'MQTS1' } = req.body;
if (!title || !filterContent) {
return res.status(400).json({ success: false, message: '标题和内容不能为空' });
}
const filter = await prisma.filterShare.create({
data: {
userId: req.user!.userId,
title,
description,
category,
filterContent,
contentFormat,
status: 'PUBLISHED',
},
});
res.json({ success: true, message: '滤镜创建成功', data: filter });
} catch (error) {
console.error('Create filter_share error:', error);
res.status(500).json({ success: false, message: '创建失败' });
}
});
// ============================================
// 删除滤镜
// ============================================
router.delete('/:id', authMiddleware, async (req: Request, res: Response) => {
try {
const { id } = req.params;
const filter = await prisma.filterShare.findUnique({
where: { id },
select: { userId: true },
});
if (!filter) {
return res.status(404).json({ success: false, message: '滤镜不存在' });
}
if (filter.userId !== req.user!.userId) {
return res.status(403).json({ success: false, message: '无权删除此滤镜' });
}
await prisma.filterShare.update({
where: { id },
data: { status: 'DELETED' },
});
res.json({ success: true, message: '滤镜已删除' });
} catch (error) {
console.error('Delete filter_share error:', error);
res.status(500).json({ success: false, message: '删除失败' });
}
});
export default router;

278
src/routes/schemes.ts Normal file
View File

@@ -0,0 +1,278 @@
import { Router, Request, Response } from 'express';
import { PrismaClient } from '@prisma/client';
import { z } from 'zod';
import { authMiddleware, optionalAuth } from '../middleware/auth';
import { encrypt } from '../utils/encryption';
const router = Router();
const prisma = new PrismaClient();
// ============================================
// 获取方案列表
// ============================================
router.get('/', optionalAuth, async (req: Request, res: Response) => {
try {
const {
page = 1,
limit = 20,
weapon,
category,
sort = 'newest',
} = req.query;
const skip = (Number(page) - 1) * Number(limit);
const where: any = { status: 'PUBLISHED' };
if (weapon) where.weaponName = { contains: String(weapon) };
if (category) where.category = String(category);
let orderBy: any = { createdAt: 'desc' };
if (sort === 'popular') orderBy = { viewsCount: 'desc' };
if (sort === 'downloads') orderBy = { downloadsCount: 'desc' };
const schemes = await prisma.scheme.findMany({
where,
orderBy,
skip,
take: Number(limit),
select: {
id: true,
title: true,
description: true,
weaponName: true,
category: true,
price: true,
viewsCount: true,
downloadsCount: true,
likesCount: true,
isOfficial: true,
createdAt: true,
user: {
select: {
id: true,
username: true,
avatar: true,
},
},
},
});
res.json({
success: true,
data: schemes,
});
} catch (error) {
console.error('Get schemes error:', error);
res.status(500).json({ success: false, message: '获取失败' });
}
});
// ============================================
// 获取单个方案详情
// ============================================
router.get('/:id', optionalAuth, async (req: Request, res: Response) => {
try {
const { id } = req.params;
const scheme = await prisma.scheme.findUnique({
where: { id },
include: {
user: {
select: {
id: true,
username: true,
avatar: true,
},
},
},
});
if (!scheme) {
return res.status(404).json({
success: false,
message: '方案不存在',
});
}
if (scheme.status !== 'PUBLISHED' && scheme.userId !== req.user?.userId) {
return res.status(404).json({
success: false,
message: '方案不存在',
});
}
// 增加浏览量
await prisma.scheme.update({
where: { id },
data: { viewsCount: { increment: 1 } },
});
// 检查是否收藏
let isFavorited = false;
if (req.user) {
const fav = await prisma.favorite.findFirst({
where: {
userId: req.user.userId,
targetType: 'SCHEME',
targetId: id,
},
});
isFavorited = !!fav;
}
// 加密方案内容
const encryptedContent = encrypt(scheme.schemeContent);
res.json({
success: true,
data: {
...scheme,
schemeContent: encryptedContent,
isFavorited,
},
});
} catch (error) {
console.error('Get scheme error:', error);
res.status(500).json({ success: false, message: '获取失败' });
}
});
// ============================================
// 创建方案
// ============================================
const createSchemeSchema = z.object({
title: z.string().min(1).max(100),
description: z.string().optional(),
weaponName: z.string().optional(),
category: z.string().optional(),
schemeContent: z.string().min(1),
price: z.number().int().min(0).default(0),
gpuModel: z.string().optional(),
driverVersion: z.string().optional(),
appVersion: z.string().optional(),
});
router.post('/', authMiddleware, async (req: Request, res: Response) => {
try {
const body = createSchemeSchema.parse(req.body);
const scheme = await prisma.scheme.create({
data: {
userId: req.user!.userId,
title: body.title,
description: body.description,
weaponName: body.weaponName,
category: body.category,
schemeContent: body.schemeContent,
price: body.price,
gpuModel: body.gpuModel,
driverVersion: body.driverVersion,
appVersion: body.appVersion,
status: 'PUBLISHED',
},
});
// 更新用户方案数
await prisma.user.update({
where: { id: req.user!.userId },
data: { schemesCount: { increment: 1 } },
});
res.json({
success: true,
message: '方案创建成功',
data: scheme,
});
} catch (error) {
console.error('Create scheme error:', error);
res.status(500).json({ success: false, message: '创建失败' });
}
});
// ============================================
// 删除方案
// ============================================
router.delete('/:id', authMiddleware, async (req: Request, res: Response) => {
try {
const { id } = req.params;
const scheme = await prisma.scheme.findUnique({
where: { id },
select: { userId: true },
});
if (!scheme) {
return res.status(404).json({
success: false,
message: '方案不存在',
});
}
if (scheme.userId !== req.user!.userId) {
return res.status(403).json({
success: false,
message: '无权删除此方案',
});
}
await prisma.scheme.update({
where: { id },
data: { status: 'DELETED' },
});
res.json({
success: true,
message: '方案已删除',
});
} catch (error) {
console.error('Delete scheme error:', error);
res.status(500).json({ success: false, message: '删除失败' });
}
});
// 记录方案使用(增加下载计数)
router.post('/:id/use', authMiddleware, async (req: Request, res: Response) => {
try {
const { id } = req.params;
const { source } = req.body;
const scheme = await prisma.scheme.findUnique({
where: { id },
select: { id: true, downloadsCount: true },
});
if (!scheme) {
return res.status(404).json({
success: false,
message: '方案不存在',
});
}
// 使用计数 +1增加 downloads 计数)
const updated = await prisma.scheme.update({
where: { id },
data: { downloadsCount: { increment: 1 } },
select: { downloadsCount: true },
});
// 记录日志
await prisma.userLog.create({
data: {
userId: req.user!.userId,
action: 'SchemeUse',
targetType: 'Scheme',
targetId: id,
},
});
res.json({
success: true,
downloadsCount: updated.downloadsCount,
});
} catch (error) {
console.error('Scheme use error:', error);
res.status(500).json({ success: false, message: '记录失败' });
}
});
export default router;

122
src/routes/schemesAob.ts Normal file
View File

@@ -0,0 +1,122 @@
import { Router, Request, Response } from 'express';
import { PrismaClient } from '@prisma/client';
import { authMiddleware, optionalAuth } from '../middleware/auth';
import { encrypt } from '../utils/encryption';
const router = Router();
const prisma = new PrismaClient();
// ============================================
// 获取全面战场方案列表
// ============================================
router.get('/', optionalAuth, async (req: Request, res: Response) => {
try {
const { page = 1, limit = 20, weapon, category, sort = 'newest' } = req.query;
const skip = (Number(page) - 1) * Number(limit);
const where: any = { status: 'PUBLISHED' };
if (weapon) where.weaponName = { contains: String(weapon) };
if (category) where.category = String(category);
let orderBy: any = { createdAt: 'desc' };
if (sort === 'popular') orderBy = { viewsCount: 'desc' };
if (sort === 'downloads') orderBy = { downloadsCount: 'desc' };
const schemes = await prisma.schemeAob.findMany({
where,
orderBy,
skip,
take: Number(limit),
select: {
id: true,
title: true,
description: true,
weaponName: true,
category: true,
price: true,
viewsCount: true,
downloadsCount: true,
likesCount: true,
isOfficial: true,
createdAt: true,
user: {
select: { id: true, username: true, avatar: true },
},
},
});
res.json({ success: true, data: schemes });
} catch (error) {
console.error('Get schemes_aob error:', error);
res.status(500).json({ success: false, message: '获取失败' });
}
});
// ============================================
// 获取单个方案详情
// ============================================
router.get('/:id', optionalAuth, async (req: Request, res: Response) => {
try {
const { id } = req.params;
const scheme = await prisma.schemeAob.findUnique({
where: { id },
include: {
user: { select: { id: true, username: true, avatar: true } },
},
});
if (!scheme || (scheme.status !== 'PUBLISHED' && scheme.userId !== req.user?.userId)) {
return res.status(404).json({ success: false, message: '方案不存在' });
}
await prisma.schemeAob.update({
where: { id },
data: { viewsCount: { increment: 1 } },
});
const encryptedContent = encrypt(scheme.schemeContent);
res.json({
success: true,
data: { ...scheme, schemeContent: encryptedContent },
});
} catch (error) {
console.error('Get scheme_aob error:', error);
res.status(500).json({ success: false, message: '获取失败' });
}
});
// ============================================
// 创建方案
// ============================================
router.post('/', authMiddleware, async (req: Request, res: Response) => {
try {
const { title, description, weaponName, category, schemeContent, price = 0 } = req.body;
if (!title || !schemeContent) {
return res.status(400).json({ success: false, message: '标题和内容不能为空' });
}
const scheme = await prisma.schemeAob.create({
data: {
userId: req.user!.userId,
title,
description,
weaponName,
category,
schemeContent,
price,
status: 'PUBLISHED',
},
});
res.json({ success: true, message: '方案创建成功', data: scheme });
} catch (error) {
console.error('Create scheme_aob error:', error);
res.status(500).json({ success: false, message: '创建失败' });
}
});
export default router;

337
src/routes/user.ts Normal file
View File

@@ -0,0 +1,337 @@
import { Router, Request, Response } from 'express';
import { PrismaClient } from '@prisma/client';
import { z } from 'zod';
import { authMiddleware } from '../middleware/auth';
const router = Router();
const prisma = new PrismaClient();
// 所有路由都需要认证
router.use(authMiddleware);
// ============================================
// 更新用户名
// ============================================
const updateUsernameSchema = z.object({
userId: z.string().uuid(),
username: z.string().min(3).max(50),
});
router.put('/username', async (req: Request, res: Response) => {
try {
const body = updateUsernameSchema.parse(req.body);
// 验证用户 ID
if (body.userId !== req.user!.userId) {
return res.status(403).json({
success: false,
message: '无权修改其他用户信息',
});
}
// 检查用户名是否已存在
const existing = await prisma.user.findUnique({
where: { username: body.username },
});
if (existing) {
return res.status(400).json({
success: false,
message: '用户名已被使用',
});
}
// 检查修改次数限制
const limit = await prisma.userLog.count({
where: {
userId: req.user!.userId,
action: 'UPDATE_USERNAME',
},
});
if (limit >= 4) {
return res.status(400).json({
success: false,
message: '用户名修改次数已达上限',
});
}
// 更新用户名
await prisma.user.update({
where: { id: body.userId },
data: { username: body.username },
});
// 记录日志
await prisma.userLog.create({
data: {
userId: req.user!.userId,
action: 'UPDATE_USERNAME',
ipAddress: req.ip,
},
});
res.json({
success: true,
message: '用户名更新成功',
username: body.username,
});
} catch (error) {
console.error('Update username error:', error);
res.status(500).json({ success: false, message: '更新失败' });
}
});
// ============================================
// 更新邮箱
// ============================================
const updateEmailSchema = z.object({
userId: z.string().uuid(),
email: z.string().email(),
});
router.put('/email', async (req: Request, res: Response) => {
try {
const body = updateEmailSchema.parse(req.body);
if (body.userId !== req.user!.userId) {
return res.status(403).json({
success: false,
message: '无权修改其他用户信息',
});
}
// 检查邮箱是否已存在
const existing = await prisma.user.findUnique({
where: { email: body.email },
});
if (existing) {
return res.status(400).json({
success: false,
message: '邮箱已被使用',
});
}
await prisma.user.update({
where: { id: body.userId },
data: { email: body.email },
});
res.json({
success: true,
message: '邮箱更新成功',
email: body.email,
});
} catch (error) {
console.error('Update email error:', error);
res.status(500).json({ success: false, message: '更新失败' });
}
});
// ============================================
// 更新头像
// ============================================
const updateAvatarSchema = z.object({
userId: z.string().uuid(),
avatar: z.string().url(),
});
router.put('/avatar', async (req: Request, res: Response) => {
try {
const body = updateAvatarSchema.parse(req.body);
if (body.userId !== req.user!.userId) {
return res.status(403).json({
success: false,
message: '无权修改其他用户信息',
});
}
await prisma.user.update({
where: { id: body.userId },
data: { avatar: body.avatar },
});
res.json({
success: true,
message: '头像更新成功',
avatar: body.avatar,
});
} catch (error) {
console.error('Update avatar error:', error);
res.status(500).json({ success: false, message: '更新失败' });
}
});
// ============================================
// 获取用户统计
// ============================================
router.get('/stats/:userId', async (req: Request, res: Response) => {
try {
const { userId } = req.params;
const user = await prisma.user.findUnique({
where: { id: userId },
select: {
schemesCount: true,
favoritesCount: true,
isVip: true,
vipExpireAt: true,
createdAt: true,
},
});
if (!user) {
return res.status(404).json({
success: false,
message: '用户不存在',
});
}
res.json({
success: true,
data: {
schemesCount: user.schemesCount,
favoritesCount: user.favoritesCount,
isVip: user.isVip,
vipExpireAt: user.vipExpireAt,
memberSince: user.createdAt,
},
});
} catch (error) {
console.error('Get user stats error:', error);
res.status(500).json({ success: false, message: '获取失败' });
}
});
// ============================================
// 获取用户方案列表
// ============================================
router.get('/schemes/:userId', async (req: Request, res: Response) => {
try {
const { userId } = req.params;
const { type = 'schemes', page = 1, limit = 20 } = req.query;
const skip = (Number(page) - 1) * Number(limit);
let schemes;
if (type === 'aob') {
schemes = await prisma.schemeAob.findMany({
where: { userId, status: 'PUBLISHED' },
orderBy: { createdAt: 'desc' },
skip,
take: Number(limit),
select: {
id: true, title: true, weaponName: true, category: true,
viewsCount: true, downloadsCount: true, likesCount: true, createdAt: true,
},
});
} else {
schemes = await prisma.scheme.findMany({
where: { userId, status: 'PUBLISHED' },
orderBy: { createdAt: 'desc' },
skip,
take: Number(limit),
select: {
id: true, title: true, weaponName: true, category: true,
viewsCount: true, downloadsCount: true, likesCount: true, createdAt: true,
},
});
}
res.json({
success: true,
data: schemes,
});
} catch (error) {
console.error('Get user schemes error:', error);
res.status(500).json({ success: false, message: '获取失败' });
}
});
// ============================================
// 获取用户被收藏次数
// ============================================
router.get('/favorited-count/:userId', async (req: Request, res: Response) => {
try {
const { userId } = req.params;
const user = await prisma.user.findUnique({
where: { id: userId },
select: { favoritesCount: true },
});
if (!user) {
return res.status(404).json({
success: false,
message: '用户不存在',
});
}
res.json({
success: true,
favoritedCount: user.favoritesCount,
});
} catch (error) {
console.error('Get favorited count error:', error);
res.status(500).json({ success: false, message: '获取失败' });
}
});
// ============================================
// 获取用户功能限制信息
// ============================================
router.get('/limits/:userId', async (req: Request, res: Response) => {
try {
const { userId } = req.params;
// 验证用户是否存在
const user = await prisma.user.findUnique({
where: { id: userId },
select: {
id: true,
isVip: true,
vipExpireAt: true,
},
});
if (!user) {
return res.status(404).json({
success: false,
message: '用户不存在',
});
}
// 检查 VIP 是否过期
const now = new Date();
const isVipActive = user.isVip && user.vipExpireAt && user.vipExpireAt > now;
// 根据 VIP 状态返回不同的限制
const limits = isVipActive
? {
maxDailyUses: 500, // VIP: 每日最多使用500次
dailyUsesLeft: 500, // 剩余次数(简化处理,实际需要统计当日使用)
maxFavorites: 1000, // VIP: 最多收藏1000个
maxSchemes: 200, // VIP: 最多创建200个方案
canUploadIcc: true, // VIP: 可以上传 ICC
}
: {
maxDailyUses: 50, // 非VIP: 每日最多使用50次
dailyUsesLeft: 50, // 剩余次数
maxFavorites: 100, // 非VIP: 最多收藏100个
maxSchemes: 50, // 非VIP: 最多创建50个方案
canUploadIcc: false, // 非VIP: 不能上传 ICC
};
res.json({
success: true,
...limits,
});
} catch (error) {
console.error('Get user limits error:', error);
res.status(500).json({ success: false, message: '获取失败' });
}
});
export default router;

112
src/routes/vip.ts Normal file
View File

@@ -0,0 +1,112 @@
import { Router, Request, Response } from 'express';
import { PrismaClient } from '@prisma/client';
import { z } from 'zod';
import { authMiddleware } from '../middleware/auth';
const router = Router();
const prisma = new PrismaClient();
// ============================================
// VIP 卡密激活
// ============================================
const activateSchema = z.object({
cardKey: z.string().regex(/^VIP\d+-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$/),
});
router.post('/activate-vip', authMiddleware, async (req: Request, res: Response) => {
try {
const body = activateSchema.parse(req.body);
// 查找卡密
const card = await prisma.vipCard.findUnique({
where: { cardKey: body.cardKey },
});
if (!card) {
return res.status(400).json({
success: false,
message: '卡密不存在',
});
}
if (card.status !== 'UNUSED') {
return res.status(400).json({
success: false,
message: '卡密已被使用或已失效',
});
}
// 获取用户当前 VIP 状态
const user = await prisma.user.findUnique({
where: { id: req.user!.userId },
select: { isVip: true, vipExpireAt: true, vipLevel: true },
});
if (!user) {
return res.status(404).json({
success: false,
message: '用户不存在',
});
}
// 计算新的 VIP 过期时间
let newExpireAt: Date;
if (user.vipExpireAt && user.isVip && new Date() < user.vipExpireAt) {
// 已是 VIP延长时间
newExpireAt = new Date(user.vipExpireAt.getTime() + card.days * 24 * 60 * 60 * 1000);
} else {
// 非 VIP从现在开始计算
newExpireAt = new Date(Date.now() + card.days * 24 * 60 * 60 * 1000);
}
// 更新用户 VIP 状态
const updatedUser = await prisma.user.update({
where: { id: req.user!.userId },
data: {
isVip: true,
vipExpireAt: newExpireAt,
vipLevel: Math.max(user.vipLevel, 1),
},
});
// 更新卡密状态
await prisma.vipCard.update({
where: { id: card.id },
data: {
status: 'USED',
usedBy: req.user!.userId,
usedAt: new Date(),
},
});
// 记录日志
await prisma.userLog.create({
data: {
userId: req.user!.userId,
action: 'VIP_ACTIVATE',
targetId: card.id,
ipAddress: req.ip,
},
});
res.json({
success: true,
isVip: true,
vipExpireAt: newExpireAt,
vipLevel: updatedUser.vipLevel,
message: `激活成功VIP 有效期延长 ${card.days}`,
});
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({
success: false,
message: '卡密格式无效',
});
}
console.error('Activate VIP error:', error);
res.status(500).json({ success: false, message: '激活失败' });
}
});
export default router;

64
src/utils/encryption.ts Normal file
View File

@@ -0,0 +1,64 @@
import crypto from 'crypto';
const ALGORITHM = 'aes-256-cbc';
const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY || '0123456789abcdef0123456789abcdef';
export interface EncryptedData {
encrypted: boolean;
iv: string;
data: string;
}
/**
* AES 加密
*/
export function encrypt(text: string): EncryptedData {
const iv = crypto.randomBytes(16);
const key = Buffer.from(ENCRYPTION_KEY, 'utf-8');
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
let encrypted = cipher.update(text, 'utf-8', 'hex');
encrypted += cipher.final('hex');
return {
encrypted: true,
iv: iv.toString('hex'),
data: encrypted,
};
}
/**
* AES 解密
*/
export function decrypt(ivHex: string, dataHex: string): string {
const iv = Buffer.from(ivHex, 'hex');
const encryptedData = Buffer.from(dataHex, 'hex');
const key = Buffer.from(ENCRYPTION_KEY, 'utf-8');
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
let decrypted = decipher.update(encryptedData, undefined, 'utf-8');
decrypted += decipher.final('utf-8');
return decrypted;
}
/**
* 密码哈希
*/
export function hashPassword(password: string): string {
return crypto.createHash('sha256').update(password).digest('hex');
}
/**
* 生成随机字符串
*/
export function randomString(length: number = 32): string {
return crypto.randomBytes(length).toString('hex').slice(0, length);
}
/**
* 生成设备哈希
*/
export function generateDeviceHash(data: string): string {
return crypto.createHash('sha256').update(data).digest('hex').slice(0, 64);
}

47
src/utils/jwt.ts Normal file
View File

@@ -0,0 +1,47 @@
import jwt from 'jsonwebtoken';
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '7d';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const jwtSign = jwt.sign as any;
export interface JwtPayload {
userId: string;
username: string;
email: string;
isVip: boolean;
vipLevel: number;
}
/**
* 生成 JWT Token
*/
export function generateToken(payload: JwtPayload): string {
return jwtSign(payload, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN });
}
/**
* 验证 JWT Token
*/
export function verifyToken(token: string): JwtPayload | null {
try {
return jwt.verify(token, JWT_SECRET) as JwtPayload;
} catch (error) {
return null;
}
}
/**
* 从请求头提取 Token
*/
export function extractToken(authHeader?: string): string | null {
if (!authHeader) return null;
const parts = authHeader.split(' ');
if (parts.length !== 2 || parts[0] !== 'Bearer') {
return null;
}
return parts[1];
}

19
tsconfig.json Normal file
View File

@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}