Compare commits
16 Commits
f09cadf1a7
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| cd9fb924d5 | |||
| 2c0d6ba2eb | |||
| 773ae2e3fb | |||
| 8bcb6c7e7a | |||
| ea4e0f6e07 | |||
| 68f74bbc8c | |||
| 9b924d5e5d | |||
| 3044b3d921 | |||
| c7ad849346 | |||
| 21709e5d97 | |||
| 5b23c88df9 | |||
|
|
1b625c70c6 | ||
|
|
4bfa750a84 | ||
|
|
111e61431b | ||
|
|
e711e1329d | ||
|
|
a5ddcb8bb7 |
16
.env.example
16
.env.example
@@ -1,16 +1,13 @@
|
||||
# 数据库连接
|
||||
DATABASE_URL="postgresql://postgres:password@localhost:5432/maqt?schema=public"
|
||||
|
||||
# PostgreSQL 密码(docker-compose 使用,请修改)
|
||||
POSTGRES_PASSWORD="your-postgres-password"
|
||||
|
||||
# JWT 密钥(请修改为随机字符串)
|
||||
JWT_SECRET="your-super-secret-jwt-key-change-this-in-production"
|
||||
# JWT 密钥(务必修改为随机长字符串)
|
||||
JWT_SECRET="change-me-to-a-random-64-char-string"
|
||||
|
||||
# JWT 过期时间
|
||||
JWT_EXPIRES_IN="7d"
|
||||
|
||||
# 数据加密密钥(32字节,请修改)
|
||||
# 数据加密密钥(32字节,务必修改)
|
||||
ENCRYPTION_KEY="0123456789abcdef0123456789abcdef"
|
||||
|
||||
# 服务端口
|
||||
@@ -19,5 +16,8 @@ PORT=3001
|
||||
# 环境
|
||||
NODE_ENV="development"
|
||||
|
||||
# VIP 卡密批次密钥(用于生成卡密签名)
|
||||
BATCH_SECRET="your-batch-secret-key"
|
||||
# CORS 允许的来源 (逗号分隔)
|
||||
ALLOWED_ORIGINS="http://localhost:5173,app://.,file://"
|
||||
|
||||
# 管理员密钥 (用于 /api/admin 端点)
|
||||
ADMIN_SECRET="change-me-to-random"
|
||||
|
||||
96
.gitignore
vendored
96
.gitignore
vendored
@@ -1,93 +1,11 @@
|
||||
# 环境变量(敏感信息)
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
!.env.example
|
||||
|
||||
# 依赖
|
||||
node_modules/
|
||||
.pnp
|
||||
.pnp.js
|
||||
|
||||
# 构建
|
||||
dist/
|
||||
build/
|
||||
out/
|
||||
*.tsbuildinfo
|
||||
|
||||
# 日志
|
||||
logs/
|
||||
.env
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# 运行时数据
|
||||
pids/
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# 系统文件
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
Desktop.ini
|
||||
|
||||
# IDE / 编辑器
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
*.sublime-workspace
|
||||
*.sublime-project
|
||||
.fleet/
|
||||
|
||||
# 测试
|
||||
coverage/
|
||||
.nyc_output/
|
||||
*.lcov
|
||||
|
||||
# Prisma
|
||||
prisma/*.db
|
||||
prisma/*.db-journal
|
||||
migrations/
|
||||
|
||||
# 临时文件
|
||||
*.tmp
|
||||
*.temp
|
||||
.cache/
|
||||
.parcel-cache/
|
||||
|
||||
# Docker
|
||||
docker-compose.override.yml
|
||||
|
||||
# 打包文件
|
||||
*.zip
|
||||
*.tar.gz
|
||||
*.tgz
|
||||
*.rar
|
||||
|
||||
# 密钥和证书(不应上传)
|
||||
*.pem
|
||||
*.key
|
||||
*.cert
|
||||
*.crt
|
||||
|
||||
# 调试
|
||||
.eslintcache
|
||||
.stylelintcache
|
||||
|
||||
# TypeScript
|
||||
*.tsbuildinfo
|
||||
|
||||
# 其他
|
||||
*.bak
|
||||
*.backup
|
||||
*.orig
|
||||
.prisma/
|
||||
.venv/
|
||||
`n__pycache__/
|
||||
test_output*.json
|
||||
results_v*.json
|
||||
`n__pycache__/`noriget/ test_output_*.json`noriget/results_v*.json`n*.pyc
|
||||
|
||||
461
API.md
Normal file
461
API.md
Normal file
@@ -0,0 +1,461 @@
|
||||
# 码枪堂 weapon-tuner API 文档
|
||||
|
||||
> 来源:原版 `app.asar` 中 `dist/weapon-tuner.html` (v7.0.4)
|
||||
> BASE_URL: `https://maqt.top`
|
||||
> 复刻目标: `https://gch3n.online/delta`
|
||||
|
||||
---
|
||||
|
||||
## 全局约定
|
||||
|
||||
| 项目 | 值 |
|
||||
|------|-----|
|
||||
| 基础路径 | `/api` |
|
||||
| 认证方式 | `Authorization: Bearer <JWT>` |
|
||||
| 加密算法 | AES-256-CBC |
|
||||
| 加密密钥 | `maqt-delta-force-2024-secret-key-32` |
|
||||
| 密钥派生 | SHA-256(KEY) → 32字节 AES key |
|
||||
| 密文格式 | `{ "encrypted": true, "iv": "hex", "data": "hex" }` |
|
||||
|
||||
### Unicode-safe Base64
|
||||
|
||||
```js
|
||||
function base64EncodeUnicode(str) {
|
||||
return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g,
|
||||
function(match, p1) { return String.fromCharCode('0x' + p1); }));
|
||||
}
|
||||
```
|
||||
|
||||
### 响应加密(服务端可选)
|
||||
|
||||
当响应中 `encrypted` 为 `true` 时,客户端执行:
|
||||
```
|
||||
hex(iv) → ArrayBuffer
|
||||
hex(data) → ArrayBuffer
|
||||
SHA-256(KEY) → AES-CBC.decrypt(iv, data) → UTF-8 → JSON.parse
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 端点列表
|
||||
|
||||
### 1. 获取武器分类
|
||||
|
||||
```
|
||||
GET /api/weapon-categories
|
||||
```
|
||||
|
||||
**响应:** (注意:部分条目缺 `category` 字段,前端实际用索引区分)
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{ "category": "突击步枪", "scheme_count": 887 },
|
||||
{ "scheme_count": 353 },
|
||||
{ "category": "射手步枪", "scheme_count": 260 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 获取武器列表
|
||||
|
||||
```
|
||||
GET /api/weapons → 全部
|
||||
GET /api/weapons?category=突击步枪 → 按分类(传中文名!)
|
||||
```
|
||||
|
||||
**Query:**
|
||||
| 参数 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `category` | 否 | **中文分类名**如 `突击步枪`,空则返回全部 |
|
||||
|
||||
**响应:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"weapon_name": "M7",
|
||||
"display_name": "M7战斗步枪",
|
||||
"category": "突击步枪",
|
||||
"use_count": 143
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
GET /api/weapon-categories
|
||||
```
|
||||
|
||||
**响应:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{ "category": "AR", "display_name": "突击步枪" },
|
||||
{ "category": "SMG", "display_name": "冲锋枪" },
|
||||
{ "category": "SR", "display_name": "狙击步枪" },
|
||||
{ "category": "LMG", "display_name": "轻机枪" },
|
||||
{ "category": "SG", "display_name": "霰弹枪" },
|
||||
{ "category": "Pistol", "display_name": "手枪" },
|
||||
{ "category": "Launcher", "display_name": "发射器" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. 获取武器列表
|
||||
|
||||
```
|
||||
GET /api/weapons
|
||||
GET /api/weapons?category=AR
|
||||
```
|
||||
|
||||
**Query:**
|
||||
| 参数 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `category` | 否 | 分类代码,例如 `AR`、`SMG`。空则返回全部 |
|
||||
|
||||
**响应:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{ "display_name": "M4A1" },
|
||||
{ "display_name": "AK-47" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. 获取方案列表
|
||||
|
||||
```
|
||||
GET /api/schemes → 烽火地带
|
||||
GET /api/schemes_aob → 全面战场(AOB)
|
||||
GET /api/favorites → 我的收藏(需登录)
|
||||
```
|
||||
|
||||
**Query:**
|
||||
| 参数 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `sort` | 是 | `hot` / `new` |
|
||||
| `page` | 是 | 页码,从 1 开始 |
|
||||
| `limit` | 是 | 固定 `12` |
|
||||
| `weaponCategory` | 否 | 武器分类中文名,如 `突击步枪` |
|
||||
| `weaponName` | 否 | 武器中文显示名,如 `M4A1` |
|
||||
| `minPrice` | 否 | 最低价格(万) |
|
||||
| `maxPrice` | 否 | 最高价格(万) |
|
||||
| `search` | 否 | 搜索关键词 |
|
||||
| `category` | 否 | `favorites` 模式用此字段传分类 |
|
||||
|
||||
**收藏模式额外 Headers:** `Authorization: Bearer <token>`
|
||||
|
||||
**响应(解密后):**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"id": 1237,
|
||||
"user_id": 1006,
|
||||
"username": "揽贝电竞-迟到了",
|
||||
"avatar": "https://...",
|
||||
"description": "M7满改方案",
|
||||
"scheme_content": "M7战斗步枪-烽火地带-6I17GK0059L1ICRB4NQP4",
|
||||
"category": "突击步枪",
|
||||
"weapon_name": "M7战斗步枪",
|
||||
"price": "97W",
|
||||
"tags": [],
|
||||
"uses": 10299,
|
||||
"total_historical_uses": 10298,
|
||||
"status": "normal",
|
||||
"source": 1,
|
||||
"created_at": "2025-11-20T16:00:00.000Z",
|
||||
"updated_at": "2026-05-23T17:09:11.000Z",
|
||||
"partner_type": "club",
|
||||
"partner_level": "bronze",
|
||||
"partner_badge": "揽贝电竞",
|
||||
"partner_logo": "https://...",
|
||||
"social_link": null
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"page": 1,
|
||||
"limit": 12,
|
||||
"hasMore": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> **注意:** `price` 是字符串含 `"W"` 单位(如 `"97W"`),`pagination` 只有 `{page,limit,hasMore}` 不返回 total。schemes_aob 模式不含 price 字段。
|
||||
|
||||
**响应(加密):**
|
||||
```json
|
||||
{
|
||||
"encrypted": true,
|
||||
"iv": "a1b2c3d4...",
|
||||
"data": "e5f6a7b8..."
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. 发布方案
|
||||
|
||||
```
|
||||
POST /api/schemes → 烽火地带
|
||||
POST /api/schemes_aob → 全面战场
|
||||
```
|
||||
|
||||
**Headers:**
|
||||
| 键 | 值 |
|
||||
|----|-----|
|
||||
| `Content-Type` | `application/json` |
|
||||
| `x-user-info` | `Base64({ id: number, username: string })` |
|
||||
|
||||
**Body:**
|
||||
```json
|
||||
{
|
||||
"description": "方案描述(≤50字)",
|
||||
"category": "武器分类中文名,如 突击步枪",
|
||||
"weaponName": "武器中文名,如 M4A1",
|
||||
"scheme": "配置代码字符串",
|
||||
"tags": [],
|
||||
"price": 15
|
||||
}
|
||||
```
|
||||
- `price` 仅烽火模式需要,范围 1-999
|
||||
- `tags` 目前传空数组
|
||||
|
||||
**响应(成功):**
|
||||
```json
|
||||
{
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
**响应(含敏感词):**
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "内容包含敏感词",
|
||||
"sensitiveWords": ["词1", "词2"]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. 记录方案使用次数
|
||||
|
||||
```
|
||||
POST /api/schemes/{schemeId}/use
|
||||
POST /api/schemes_aob/{schemeId}/use
|
||||
```
|
||||
|
||||
**响应:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "使用次数已记录"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6. 举报方案
|
||||
|
||||
```
|
||||
POST /api/schemes/{schemeId}/report
|
||||
POST /api/schemes_aob/{schemeId}/report
|
||||
```
|
||||
|
||||
**Headers:**
|
||||
| 键 | 值 |
|
||||
|----|-----|
|
||||
| `Content-Type` | `application/json` |
|
||||
| `Authorization` | `Bearer <token>` |
|
||||
|
||||
**Body:**
|
||||
```json
|
||||
{
|
||||
"reason": "invalid",
|
||||
"description": "详细举报说明(≤200字)"
|
||||
}
|
||||
```
|
||||
|
||||
`reason` 枚举:
|
||||
- `invalid` — 方案失效
|
||||
- `inappropriate` — 内容不当
|
||||
|
||||
**响应:**
|
||||
```json
|
||||
{
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 7. 收藏操作
|
||||
|
||||
#### 7.1 添加收藏
|
||||
|
||||
```
|
||||
POST /api/favorites
|
||||
```
|
||||
|
||||
**Headers:**
|
||||
| 键 | 值 |
|
||||
|----|-----|
|
||||
| `Content-Type` | `application/json` |
|
||||
| `Authorization` | `Bearer <token>` |
|
||||
|
||||
**Body:**
|
||||
```json
|
||||
{
|
||||
"schemeId": "方案ID",
|
||||
"source": "烽火地带"
|
||||
}
|
||||
```
|
||||
|
||||
**响应:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"alreadyFavorited": false
|
||||
}
|
||||
```
|
||||
|
||||
#### 7.2 取消收藏
|
||||
|
||||
```
|
||||
DELETE /api/favorites/{schemeId}?source={source}
|
||||
```
|
||||
|
||||
`source`: `烽火地带` 或 `全面战场`
|
||||
|
||||
**Headers:** `Authorization: Bearer <token>`
|
||||
|
||||
**响应:**
|
||||
```json
|
||||
{
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 7.3 检查收藏状态
|
||||
|
||||
```
|
||||
GET /api/favorites/check?schemeId={id}&source={source}
|
||||
```
|
||||
|
||||
**Headers:** `Authorization: Bearer <token>`
|
||||
|
||||
**响应:**
|
||||
```json
|
||||
{
|
||||
"isFavorited": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 8. 广告
|
||||
|
||||
#### 8.1 获取广告列表
|
||||
|
||||
```
|
||||
GET /api/adverts/list
|
||||
```
|
||||
|
||||
**响应:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"id": "string",
|
||||
"author": "广告主昵称",
|
||||
"avatar": "头像URL",
|
||||
"shareTime": "ISO 8601",
|
||||
"title": "广告标题",
|
||||
"description": "简短描述",
|
||||
"image_url": "图片URL(可选)",
|
||||
"link_url": "跳转链接",
|
||||
"isVip": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### 8.2 记录广告点击
|
||||
|
||||
```
|
||||
POST /api/adverts/{advertId}/click
|
||||
```
|
||||
|
||||
**响应:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "点击已记录"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 9. 旧版武器分类(逐个获取)
|
||||
|
||||
```
|
||||
GET /api/category/AR
|
||||
GET /api/category/SMG
|
||||
GET /api/category/SR
|
||||
GET /api/category/LMG
|
||||
GET /api/category/SG
|
||||
GET /api/category/Pistol
|
||||
GET /api/category/Launcher
|
||||
```
|
||||
|
||||
**响应:** 直接返回武器对象数组(不包裹 `success`):
|
||||
```json
|
||||
[
|
||||
{ "display_name": "M4A1" },
|
||||
{ "display_name": "AK-47" }
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 分类代码映射
|
||||
|
||||
| 代码 | 中文名 |
|
||||
|------|--------|
|
||||
| `AR` | 突击步枪 |
|
||||
| `SMG` | 冲锋枪 |
|
||||
| `SR` | 狙击步枪 |
|
||||
| `LMG` | 轻机枪 |
|
||||
| `SG` | 霰弹枪 |
|
||||
| `Pistol` | 手枪 |
|
||||
| `Launcher` | 发射器 |
|
||||
|
||||
---
|
||||
|
||||
## 前端状态变量
|
||||
|
||||
```js
|
||||
let currentMode = 'schemes'; // 'schemes' | 'schemes_aob' | 'favorites'
|
||||
let currentSort = 'hot'; // 'hot' | 'new'
|
||||
let currentPage = 1;
|
||||
let currentFilters = {
|
||||
weaponCategory: '', // 分类代码如 'AR'
|
||||
weaponName: '', // 武器中文显示名
|
||||
minPrice: '', // 字符串
|
||||
maxPrice: '',
|
||||
search: ''
|
||||
};
|
||||
let hasMore = true;
|
||||
let userToken = ''; // JWT token
|
||||
```
|
||||
@@ -1,192 +0,0 @@
|
||||
# 码枪堂 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`** — 方案使用记录
|
||||
|
||||
---
|
||||
|
||||
## 四、重新封装开发计划
|
||||
|
||||
### 阶段 1:API 补充(先补齐缺失端点)
|
||||
|
||||
**优先级 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 配置
|
||||
- [ ] 硬件监控面板
|
||||
@@ -23,6 +23,8 @@ 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
|
||||
COPY start.sh ./
|
||||
RUN chmod +x start.sh
|
||||
|
||||
EXPOSE 3001
|
||||
CMD ["node", "dist/index.js"]
|
||||
CMD ["./start.sh"]
|
||||
|
||||
35
docker-compose.yml
Normal file
35
docker-compose.yml
Normal file
@@ -0,0 +1,35 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
container_name: maqt-postgres
|
||||
environment:
|
||||
POSTGRES_USER: ${POSTGRES_USER:-maqt}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-changeme}
|
||||
POSTGRES_DB: ${POSTGRES_DB:-maqt}
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
ports:
|
||||
- "127.0.0.1:5432:5432"
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-maqt}"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
mqsrv:
|
||||
build: .
|
||||
container_name: maqt-backend
|
||||
ports:
|
||||
- "127.0.0.1:3002:3001"
|
||||
env_file:
|
||||
- .env
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
1252
origet/ test_output_202605240119.json
Normal file
1252
origet/ test_output_202605240119.json
Normal file
File diff suppressed because it is too large
Load Diff
BIN
origet/__pycache__/api_spec.cpython-313.pyc
Normal file
BIN
origet/__pycache__/api_spec.cpython-313.pyc
Normal file
Binary file not shown.
BIN
origet/__pycache__/client.cpython-312.pyc
Normal file
BIN
origet/__pycache__/client.cpython-312.pyc
Normal file
Binary file not shown.
BIN
origet/__pycache__/client.cpython-313.pyc
Normal file
BIN
origet/__pycache__/client.cpython-313.pyc
Normal file
Binary file not shown.
BIN
origet/__pycache__/decrypt.cpython-313.pyc
Normal file
BIN
origet/__pycache__/decrypt.cpython-313.pyc
Normal file
Binary file not shown.
316
origet/api_spec.py
Normal file
316
origet/api_spec.py
Normal file
@@ -0,0 +1,316 @@
|
||||
"""
|
||||
码枪堂 原版 (maqt.top) API 接口规格
|
||||
来源: app.asar v7.0.4, dist/weapon-tuner.html + dist/assets/CfKCgw5l.js
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import base64
|
||||
import json
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional, Any
|
||||
|
||||
BASE_URL = "https://maqt.top"
|
||||
ENCRYPTION_KEY = "maqt-delta-force-2024-secret-key-32"
|
||||
AES_KEY = hashlib.sha256(ENCRYPTION_KEY.encode("utf-8")).digest() # 32 bytes
|
||||
|
||||
CATEGORY_MAP = {
|
||||
"AR": "突击步枪",
|
||||
"SMG": "冲锋枪",
|
||||
"SR": "狙击步枪",
|
||||
"LMG": "轻机枪",
|
||||
"SG": "霰弹枪",
|
||||
"Pistol": "手枪",
|
||||
"Launcher": "发射器",
|
||||
}
|
||||
|
||||
# ---- Data Models ----
|
||||
|
||||
@dataclass
|
||||
class WeaponCategory:
|
||||
category: str # 实际返回中文名 e.g. "突击步枪"
|
||||
scheme_count: int = 0
|
||||
|
||||
@dataclass
|
||||
class Weapon:
|
||||
id: int
|
||||
weapon_name: str # e.g. "M4A1"
|
||||
display_name: str # e.g. "M4A1突击步枪"
|
||||
category: str # 中文分类名
|
||||
use_count: int = 0
|
||||
|
||||
@dataclass
|
||||
class SchemeItem:
|
||||
id: int
|
||||
user_id: int
|
||||
username: str
|
||||
avatar: str
|
||||
description: str
|
||||
scheme_content: str # 含武器名+模式前缀,如 "M4A1突击步枪-烽火地带-..."
|
||||
category: str
|
||||
weapon_name: str
|
||||
price: Optional[str] # 实际是字符串 e.g. "97W"
|
||||
tags: list[str]
|
||||
uses: int
|
||||
total_historical_uses: int
|
||||
status: Optional[str]
|
||||
source: Optional[int] # 1:自行 2:别人分享 3:工具生成
|
||||
created_at: str
|
||||
updated_at: str
|
||||
# 合作方字段
|
||||
partner_type: Optional[str] # "club" | "none"
|
||||
partner_level: Optional[str] # "bronze" | "gold" | "none"
|
||||
partner_badge: Optional[str]
|
||||
partner_logo: Optional[str]
|
||||
social_link: Optional[str]
|
||||
|
||||
@dataclass
|
||||
class Pagination:
|
||||
page: int
|
||||
limit: int
|
||||
hasMore: bool
|
||||
|
||||
@dataclass
|
||||
class SchemeListResponse:
|
||||
success: bool
|
||||
data: list[SchemeItem]
|
||||
pagination: Pagination
|
||||
|
||||
@dataclass
|
||||
class AdvertItem:
|
||||
id: str
|
||||
author: str
|
||||
avatar: str
|
||||
shareTime: str
|
||||
title: str
|
||||
description: str
|
||||
image_url: Optional[str]
|
||||
link_url: str
|
||||
isVip: bool
|
||||
|
||||
@dataclass
|
||||
class UserInfo:
|
||||
id: int
|
||||
username: str
|
||||
avatar: Optional[str] = None
|
||||
email: Optional[str] = None
|
||||
status: str = "active"
|
||||
isVip: int = 0
|
||||
vipExpireAt: Optional[str] = None
|
||||
freezeUntil: Optional[str] = None
|
||||
|
||||
@dataclass
|
||||
class LoginResponse:
|
||||
success: bool
|
||||
message: str
|
||||
token: Optional[str] = None
|
||||
user: Optional[UserInfo] = None
|
||||
|
||||
@dataclass
|
||||
class SessionStatusResponse:
|
||||
success: bool
|
||||
loggedIn: bool
|
||||
user: Optional[UserInfo] = None
|
||||
message: Optional[str] = None
|
||||
|
||||
@dataclass
|
||||
class VipStatusData:
|
||||
success: bool
|
||||
isVip: bool
|
||||
vipExpireAt: Optional[str]
|
||||
daysRemaining: int
|
||||
activatedAt: Optional[str]
|
||||
activatedCardKey: Optional[str]
|
||||
expired: bool
|
||||
message: Optional[str]
|
||||
|
||||
# ---- Endpoints ----
|
||||
|
||||
ENDPOINTS = {
|
||||
"weapon_categories": {
|
||||
"method": "GET",
|
||||
"path": "/api/weapon-categories",
|
||||
"auth": False,
|
||||
"params": None,
|
||||
"returns": "list[WeaponCategory]",
|
||||
},
|
||||
"weapons": {
|
||||
"method": "GET",
|
||||
"path": "/api/weapons",
|
||||
"auth": False,
|
||||
"params": {"category": "武器分类代码, optional"},
|
||||
"returns": "list[Weapon]",
|
||||
},
|
||||
"schemes": {
|
||||
"method": "GET",
|
||||
"path": "/api/schemes",
|
||||
"auth": False,
|
||||
"params": {
|
||||
"sort": "hot | new",
|
||||
"page": "页码 (1-based)",
|
||||
"limit": "固定 12",
|
||||
"weaponCategory": "中文分类名, optional",
|
||||
"weaponName": "武器中文名, optional",
|
||||
"minPrice": "最低价格, optional",
|
||||
"maxPrice": "最高价格, optional",
|
||||
"search": "搜索关键词, optional",
|
||||
},
|
||||
"returns": "SchemeListResponse (可能加密)",
|
||||
},
|
||||
"schemes_aob": {
|
||||
"method": "GET",
|
||||
"path": "/api/schemes_aob",
|
||||
"auth": False,
|
||||
"params": { # 同上
|
||||
"sort": "hot | new",
|
||||
"page": "页码 (1-based)",
|
||||
"limit": "固定 12",
|
||||
},
|
||||
"returns": "SchemeListResponse (可能加密)",
|
||||
},
|
||||
"favorites": {
|
||||
"method": "GET",
|
||||
"path": "/api/favorites",
|
||||
"auth": True,
|
||||
"params": {"sort": "", "page": "", "limit": "12"},
|
||||
"returns": "SchemeListResponse",
|
||||
},
|
||||
"login": {
|
||||
"method": "POST",
|
||||
"path": "/api/login",
|
||||
"auth": False,
|
||||
"body": {
|
||||
"username": "string",
|
||||
"password": "string",
|
||||
"installId": "string, optional",
|
||||
"deviceHash": "string, optional",
|
||||
"platform": "string, optional",
|
||||
"osVersion": "string, optional",
|
||||
"appVersion": "string, optional",
|
||||
},
|
||||
"returns": "LoginResponse",
|
||||
},
|
||||
"register": {
|
||||
"method": "POST",
|
||||
"path": "/api/register",
|
||||
"auth": False,
|
||||
"body": {"username": "", "password": "", "email": ""},
|
||||
"returns": "LoginResponse (with token)",
|
||||
},
|
||||
"session_status": {
|
||||
"method": "GET",
|
||||
"path": "/api/session-status",
|
||||
"auth": True,
|
||||
"params": None,
|
||||
"returns": "SessionStatusResponse",
|
||||
},
|
||||
"vip_status": {
|
||||
"method": "GET",
|
||||
"path": "/api/vip-status",
|
||||
"auth": True,
|
||||
"params": None,
|
||||
"returns": "VipStatusData (可能加密)",
|
||||
},
|
||||
"activate_vip": {
|
||||
"method": "POST",
|
||||
"path": "/api/activate-vip",
|
||||
"auth": True,
|
||||
"body": {"cardKey": "VIP卡密"},
|
||||
},
|
||||
"favorites_add": {
|
||||
"method": "POST",
|
||||
"path": "/api/favorites",
|
||||
"auth": True,
|
||||
"body": {"schemeId": "", "source": "烽火地带"},
|
||||
},
|
||||
"favorites_remove": {
|
||||
"method": "DELETE",
|
||||
"path": "/api/favorites/{schemeId}?source={source}",
|
||||
"auth": True,
|
||||
},
|
||||
"favorites_check": {
|
||||
"method": "GET",
|
||||
"path": "/api/favorites/check?schemeId={id}&source={source}",
|
||||
"auth": True,
|
||||
},
|
||||
"favorites_count": {
|
||||
"method": "GET",
|
||||
"path": "/api/favorites/count",
|
||||
"auth": True,
|
||||
},
|
||||
"scheme_use": {
|
||||
"method": "POST",
|
||||
"path": "/api/{mode}/{schemeId}/use",
|
||||
"auth": False,
|
||||
},
|
||||
"scheme_report": {
|
||||
"method": "POST",
|
||||
"path": "/api/{mode}/{schemeId}/report",
|
||||
"auth": True,
|
||||
"body": {"reason": "invalid|inappropriate", "description": ""},
|
||||
},
|
||||
"scheme_share": {
|
||||
"method": "POST",
|
||||
"path": "/api/schemes", # or /api/schemes_aob
|
||||
"auth": False,
|
||||
"headers": {"x-user-info": "base64(json({id,username}))"},
|
||||
"body": {
|
||||
"description": "方案描述 <=50字",
|
||||
"category": "武器分类中文名",
|
||||
"weaponName": "武器中文名",
|
||||
"scheme": "配置代码",
|
||||
"tags": [],
|
||||
"price": 15, # 烽火模式必填
|
||||
},
|
||||
},
|
||||
"adverts_list": {
|
||||
"method": "GET",
|
||||
"path": "/api/adverts/list",
|
||||
"auth": False,
|
||||
},
|
||||
"adverts_click": {
|
||||
"method": "POST",
|
||||
"path": "/api/adverts/{advertId}/click",
|
||||
"auth": False,
|
||||
},
|
||||
"user_stats": {
|
||||
"method": "GET",
|
||||
"path": "/api/user/stats/{userId}",
|
||||
"auth": False,
|
||||
},
|
||||
"user_schemes": {
|
||||
"method": "GET",
|
||||
"path": "/api/user/schemes/{userId}",
|
||||
"auth": False,
|
||||
},
|
||||
"user_favorited_count": {
|
||||
"method": "GET",
|
||||
"path": "/api/user/favorited-count/{userId}",
|
||||
"auth": True,
|
||||
},
|
||||
"filter_share_categories": {
|
||||
"method": "GET",
|
||||
"path": "/api/filter-share/categories",
|
||||
"auth": False,
|
||||
},
|
||||
"filter_shares": {
|
||||
"method": "GET",
|
||||
"path": "/api/filter-shares",
|
||||
"auth": False,
|
||||
},
|
||||
"filter_share_copy": {
|
||||
"method": "POST",
|
||||
"path": "/api/filter-shares/{id}/copy",
|
||||
"auth": False,
|
||||
"body": {"slot": "primary"},
|
||||
},
|
||||
"activity_ping": {
|
||||
"method": "GET",
|
||||
"path": "/api/activity/ping",
|
||||
"auth": True,
|
||||
},
|
||||
"game_map_password": {
|
||||
"method": "GET",
|
||||
"path": "/api/game/map-password",
|
||||
"auth": False,
|
||||
},
|
||||
}
|
||||
211
origet/client.py
Normal file
211
origet/client.py
Normal file
@@ -0,0 +1,211 @@
|
||||
"""
|
||||
码枪堂原版 (maqt.top) API 客户端
|
||||
|
||||
用法:
|
||||
from client import MaqtClient
|
||||
|
||||
c = MaqtClient()
|
||||
c.login("username", "password")
|
||||
schemes = c.get_schemes(page=1, sort="hot")
|
||||
print(schemes)
|
||||
"""
|
||||
import json
|
||||
import requests
|
||||
from typing import Optional, Any
|
||||
from api_spec import BASE_URL, ENDPOINTS
|
||||
|
||||
# ---- Unicode-safe Base64 ----
|
||||
def b64_encode_unicode(s: str) -> str:
|
||||
import base64
|
||||
encoded = base64.b64encode(s.encode("utf-8")).decode("ascii")
|
||||
return encoded
|
||||
|
||||
# ---- Client ----
|
||||
class MaqtClient:
|
||||
def __init__(self, base_url: str = BASE_URL, token: Optional[str] = None):
|
||||
self.base_url = base_url
|
||||
self.token = token
|
||||
self.session = requests.Session()
|
||||
self.session.headers["User-Agent"] = "MaqiangTang/7.0.4"
|
||||
|
||||
# ---- low-level helpers ----
|
||||
|
||||
def _auth_headers(self) -> dict:
|
||||
if self.token:
|
||||
return {"Authorization": f"Bearer {self.token}"}
|
||||
return {}
|
||||
|
||||
def _get(self, path: str, params: Optional[dict] = None, auth: bool = False) -> dict:
|
||||
h = self._auth_headers() if auth else {}
|
||||
r = self.session.get(f"{self.base_url}{path}", params=params, headers=h, timeout=15)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
def _post(self, path: str, body: dict, extra_headers: Optional[dict] = None, auth: bool = False) -> dict:
|
||||
h = {"Content-Type": "application/json"}
|
||||
if auth:
|
||||
h.update(self._auth_headers())
|
||||
if extra_headers:
|
||||
h.update(extra_headers)
|
||||
r = self.session.post(f"{self.base_url}{path}", json=body, headers=h, timeout=15)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
def _delete(self, path: str, auth: bool = True) -> dict:
|
||||
h = self._auth_headers() if auth else {}
|
||||
r = self.session.delete(f"{self.base_url}{path}", headers=h, timeout=15)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
# ---- auth ----
|
||||
|
||||
def login(self, username: str, password: str) -> dict:
|
||||
resp = self._post("/api/login", {"username": username, "password": password})
|
||||
if resp.get("success") and resp.get("token"):
|
||||
self.token = resp["token"]
|
||||
return resp
|
||||
|
||||
def register(self, username: str, password: str, email: str) -> dict:
|
||||
return self._post("/api/register", {"username": username, "password": password, "email": email})
|
||||
|
||||
def session_status(self) -> dict:
|
||||
return self._get("/api/session-status", auth=True)
|
||||
|
||||
# ---- VIP ----
|
||||
|
||||
def vip_status(self) -> dict:
|
||||
return self._get("/api/vip-status", auth=True)
|
||||
|
||||
def activate_vip(self, card_key: str) -> dict:
|
||||
return self._post("/api/activate-vip", {"cardKey": card_key}, auth=True)
|
||||
|
||||
# ---- weapon data ----
|
||||
|
||||
def get_weapon_categories(self) -> dict:
|
||||
return self._get("/api/weapon-categories")
|
||||
|
||||
def get_weapons(self, category: Optional[str] = None) -> dict:
|
||||
"""category 传中文名如'突击步枪',不传返回全部"""
|
||||
params = {"category": category} if category else None
|
||||
return self._get("/api/weapons", params=params)
|
||||
|
||||
# ---- schemes ----
|
||||
|
||||
def get_schemes(
|
||||
self,
|
||||
page: int = 1,
|
||||
sort: str = "hot",
|
||||
limit: int = 12,
|
||||
mode: str = "schemes",
|
||||
weapon_category: Optional[str] = None,
|
||||
weapon_name: Optional[str] = None,
|
||||
min_price: Optional[int] = None,
|
||||
max_price: Optional[int] = None,
|
||||
search: Optional[str] = None,
|
||||
) -> dict:
|
||||
params = {"sort": sort, "page": str(page), "limit": str(limit)}
|
||||
if weapon_category:
|
||||
params["weaponCategory"] = weapon_category
|
||||
if weapon_name:
|
||||
params["weaponName"] = weapon_name
|
||||
if min_price is not None:
|
||||
params["minPrice"] = str(min_price)
|
||||
if max_price is not None:
|
||||
params["maxPrice"] = str(max_price)
|
||||
if search:
|
||||
params["search"] = search
|
||||
return self._get(f"/api/{mode}", params=params)
|
||||
|
||||
def get_favorites(self, page: int = 1, sort: str = "hot") -> dict:
|
||||
return self._get("/api/favorites", params={"sort": sort, "page": str(page), "limit": "12"}, auth=True)
|
||||
|
||||
def record_use(self, scheme_id: str, mode: str = "schemes") -> dict:
|
||||
return self._post(f"/api/{mode}/{scheme_id}/use", {})
|
||||
|
||||
def report_scheme(self, scheme_id: str, reason: str, description: str = "", mode: str = "schemes") -> dict:
|
||||
return self._post(
|
||||
f"/api/{mode}/{scheme_id}/report",
|
||||
{"reason": reason, "description": description},
|
||||
auth=True,
|
||||
)
|
||||
|
||||
def share_scheme(
|
||||
self,
|
||||
description: str,
|
||||
category: str,
|
||||
weapon_name: str,
|
||||
scheme: str,
|
||||
price: Optional[int] = None,
|
||||
user_id: int = 0,
|
||||
username: str = "",
|
||||
mode: str = "schemes",
|
||||
) -> dict:
|
||||
user_info = json.dumps({"id": user_id, "username": username}, ensure_ascii=False)
|
||||
body = {
|
||||
"description": description,
|
||||
"category": category,
|
||||
"weaponName": weapon_name,
|
||||
"scheme": scheme,
|
||||
"tags": [],
|
||||
}
|
||||
if price is not None:
|
||||
body["price"] = price
|
||||
return self._post(
|
||||
f"/api/{mode}",
|
||||
body,
|
||||
extra_headers={"x-user-info": b64_encode_unicode(user_info)},
|
||||
)
|
||||
|
||||
# ---- favorites ----
|
||||
|
||||
def add_favorite(self, scheme_id: str, source: str = "烽火地带") -> dict:
|
||||
return self._post("/api/favorites", {"schemeId": scheme_id, "source": source}, auth=True)
|
||||
|
||||
def remove_favorite(self, scheme_id: str, source: str = "烽火地带") -> dict:
|
||||
import urllib.parse
|
||||
qs = urllib.parse.urlencode({"source": source})
|
||||
return self._delete(f"/api/favorites/{scheme_id}?{qs}")
|
||||
|
||||
def check_favorite(self, scheme_id: str, source: str = "烽火地带") -> dict:
|
||||
import urllib.parse
|
||||
qs = urllib.parse.urlencode({"schemeId": scheme_id, "source": source})
|
||||
return self._get(f"/api/favorites/check?{qs}", auth=True)
|
||||
|
||||
def favorites_count(self) -> dict:
|
||||
return self._get("/api/favorites/count", auth=True)
|
||||
|
||||
# ---- adverts ----
|
||||
|
||||
def get_adverts(self) -> dict:
|
||||
return self._get("/api/adverts/list")
|
||||
|
||||
def click_advert(self, advert_id: str) -> dict:
|
||||
return self._post(f"/api/adverts/{advert_id}/click", {})
|
||||
|
||||
# ---- user ----
|
||||
|
||||
def user_stats(self, user_id: int) -> dict:
|
||||
return self._get(f"/api/user/stats/{user_id}")
|
||||
|
||||
def user_schemes(self, user_id: int) -> dict:
|
||||
return self._get(f"/api/user/schemes/{user_id}")
|
||||
|
||||
def user_favorited_count(self, user_id: int) -> dict:
|
||||
return self._get(f"/api/user/favorited-count/{user_id}", auth=True)
|
||||
|
||||
# ---- misc ----
|
||||
|
||||
def get_filter_share_categories(self) -> dict:
|
||||
return self._get("/api/filter-share/categories")
|
||||
|
||||
def get_filter_shares(self) -> dict:
|
||||
return self._get("/api/filter-shares")
|
||||
|
||||
def copy_filter_share(self, share_id: str, slot: str = "primary") -> dict:
|
||||
return self._post(f"/api/filter-shares/{share_id}/copy", {"slot": slot})
|
||||
|
||||
def activity_ping(self) -> dict:
|
||||
return self._get("/api/activity/ping", auth=True)
|
||||
|
||||
def get_game_map_password(self) -> dict:
|
||||
return self._get("/api/game/map-password")
|
||||
63
origet/decrypt.py
Normal file
63
origet/decrypt.py
Normal file
@@ -0,0 +1,63 @@
|
||||
"""
|
||||
码枪堂 AES-256-CBC 解密工具
|
||||
|
||||
加密密钥: maqt-delta-force-2024-secret-key-32
|
||||
流程: KEY -> SHA-256 -> 32-byte AES key -> AES-CBC decrypt(iv, data)
|
||||
"""
|
||||
import hashlib
|
||||
import json
|
||||
from typing import Any
|
||||
from Crypto.Cipher import AES
|
||||
from Crypto.Util.Padding import unpad
|
||||
|
||||
KEY_STR = "maqt-delta-force-2024-secret-key-32"
|
||||
AES_KEY = hashlib.sha256(KEY_STR.encode("utf-8")).digest()
|
||||
|
||||
|
||||
def decrypt(encrypted_obj: dict) -> dict:
|
||||
"""
|
||||
解密 API 响应
|
||||
|
||||
encrypted_obj 格式: {"encrypted": true, "iv": "hex_string", "data": "hex_string"}
|
||||
返回解密后的 JSON 对象
|
||||
"""
|
||||
iv = bytes.fromhex(encrypted_obj["iv"])
|
||||
data = bytes.fromhex(encrypted_obj["data"])
|
||||
cipher = AES.new(AES_KEY, AES.MODE_CBC, iv=iv)
|
||||
decrypted = unpad(cipher.decrypt(data), AES.block_size)
|
||||
return json.loads(decrypted.decode("utf-8"))
|
||||
|
||||
|
||||
def try_decrypt(response: dict) -> dict:
|
||||
"""如果响应已加密则解密,否则原样返回"""
|
||||
if response.get("encrypted") is True:
|
||||
return decrypt(response)
|
||||
return response
|
||||
|
||||
|
||||
def encrypt(payload: dict) -> dict:
|
||||
"""
|
||||
加密数据(用于复刻服务端)
|
||||
返回: {"encrypted": true, "iv": "hex", "data": "hex"}
|
||||
"""
|
||||
from Crypto.Random import get_random_bytes
|
||||
|
||||
plaintext = json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
|
||||
iv = get_random_bytes(16)
|
||||
cipher = AES.new(AES_KEY, AES.MODE_CBC, iv=iv)
|
||||
from Crypto.Util.Padding import pad
|
||||
|
||||
ciphertext = cipher.encrypt(pad(plaintext, AES.block_size))
|
||||
return {"encrypted": True, "iv": iv.hex(), "data": ciphertext.hex()}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 测试解密
|
||||
from client import MaqtClient
|
||||
|
||||
c = MaqtClient()
|
||||
resp = c.get_schemes(page=1)
|
||||
print(f"encrypted={resp.get('encrypted')}")
|
||||
if resp.get("encrypted"):
|
||||
result = decrypt(resp)
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||
2
origet/requirements.txt
Normal file
2
origet/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
requests
|
||||
pycryptodome
|
||||
120
origet/test_apis.py
Normal file
120
origet/test_apis.py
Normal file
@@ -0,0 +1,120 @@
|
||||
"""
|
||||
原版 maqt.top API 接口测试脚本
|
||||
|
||||
用法:
|
||||
python test_apis.py
|
||||
python test_apis.py --user <username> --pass <password>
|
||||
python test_apis.py --user <username> --pass <password> --output results.json
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
import argparse
|
||||
from datetime import datetime
|
||||
from client import MaqtClient
|
||||
from decrypt import try_decrypt
|
||||
|
||||
OUTPUT = None # global output dict
|
||||
|
||||
|
||||
def banner(msg: str):
|
||||
print(f"\n{'='*60}")
|
||||
print(f" {msg}")
|
||||
print(f"{'='*60}")
|
||||
|
||||
|
||||
def test(name: str, fn, *args, **kwargs):
|
||||
print(f"\n--- {name} ---")
|
||||
rv = None
|
||||
err = None
|
||||
try:
|
||||
result = fn(*args, **kwargs)
|
||||
decrypted = try_decrypt(result)
|
||||
print(f" OK (keys: {list(decrypted.keys()) if isinstance(decrypted, dict) else '?'})")
|
||||
if isinstance(decrypted, dict):
|
||||
if "data" in decrypted and isinstance(decrypted["data"], list):
|
||||
print(f" data_len={len(decrypted['data'])}")
|
||||
if decrypted.get("encrypted"):
|
||||
print(f" (was encrypted)")
|
||||
elif isinstance(decrypted, list):
|
||||
print(f" list_len={len(decrypted)}")
|
||||
rv = decrypted
|
||||
except Exception as e:
|
||||
print(f" [ERROR] {e}")
|
||||
err = str(e)
|
||||
OUTPUT[name] = {"ok": rv is not None, "data": rv, "error": err}
|
||||
return rv
|
||||
|
||||
|
||||
def main():
|
||||
global OUTPUT
|
||||
OUTPUT = {}
|
||||
|
||||
parser = argparse.ArgumentParser(description="maqt.top API tester")
|
||||
parser.add_argument("--user", default="sixteenth")
|
||||
parser.add_argument("--pass", dest="password", default="")
|
||||
parser.add_argument("--output", default=None, help="Output JSON file path")
|
||||
args = parser.parse_args()
|
||||
|
||||
outfile = args.output or f"test_output_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
||||
|
||||
print(f"[{datetime.now().isoformat()}] 开始测试...")
|
||||
print(f"输出文件: {outfile}")
|
||||
|
||||
c = MaqtClient()
|
||||
|
||||
# ---- 1. 无需认证的接口 ----
|
||||
banner("1. 武器分类")
|
||||
cats = test("weapon-categories", c.get_weapon_categories)
|
||||
|
||||
banner("2. 武器列表 (AR)")
|
||||
test("weapons?category=AR", c.get_weapons, category="AR")
|
||||
|
||||
banner("3. 武器列表 (无参数)")
|
||||
test("weapons (all)", c.get_weapons)
|
||||
|
||||
banner("4. 方案列表 (schemes, hot, p1)")
|
||||
schemes = test("schemes?sort=hot&page=1&limit=12", c.get_schemes, page=1, sort="hot")
|
||||
|
||||
banner("5. 方案列表 (schemes_aob)")
|
||||
test("schemes_aob", c.get_schemes, mode="schemes_aob", page=1, sort="hot")
|
||||
|
||||
banner("6. 广告列表")
|
||||
test("adverts", c.get_adverts)
|
||||
|
||||
# ---- 2. 登录 ----
|
||||
if args.password:
|
||||
banner("7. 登录")
|
||||
login_resp = test("login", c.login, args.user, args.password)
|
||||
if login_resp and login_resp.get("token"):
|
||||
print(f" Token: {login_resp['token'][:50]}...")
|
||||
|
||||
banner("8. 会话状态")
|
||||
test("session-status", c.session_status)
|
||||
|
||||
banner("9. VIP状态")
|
||||
test("vip-status", c.vip_status)
|
||||
|
||||
banner("10. 收藏列表")
|
||||
test("favorites", c.get_favorites, page=1)
|
||||
|
||||
banner("11. 收藏数量")
|
||||
test("favorites/count", c.favorites_count)
|
||||
|
||||
banner("12. 用户统计")
|
||||
uid = login_resp.get("user", {}).get("id", 0)
|
||||
test(f"user/stats/{uid}", c.user_stats, uid)
|
||||
|
||||
banner("13. 活跃Ping")
|
||||
test("activity/ping", c.activity_ping)
|
||||
else:
|
||||
print("\n 跳过登录测试 (未提供 --pass)")
|
||||
|
||||
banner("DONE")
|
||||
|
||||
with open(outfile, "w", encoding="utf-8") as f:
|
||||
json.dump(OUTPUT, f, ensure_ascii=False, indent=2, default=str)
|
||||
print(f"\n结果已保存到: {outfile}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
147
origet/test_local.py
Normal file
147
origet/test_local.py
Normal file
@@ -0,0 +1,147 @@
|
||||
"""
|
||||
本地 mqsrv (http://localhost:3001) 接口测试脚本
|
||||
|
||||
用法:
|
||||
python test_local.py
|
||||
python test_local.py --user sixteenth --pass "@WT65eijh10"
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
import argparse
|
||||
from datetime import datetime
|
||||
import requests
|
||||
|
||||
LOCAL_BASE = "http://localhost:3001"
|
||||
|
||||
# 复用 origet 的 AES 解密
|
||||
import os, sys as _sys
|
||||
_origet_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
if _origet_dir not in _sys.path:
|
||||
_sys.path.insert(0, _origet_dir)
|
||||
from decrypt import try_decrypt
|
||||
|
||||
|
||||
def test(name, fn, *args, **kwargs):
|
||||
print(f"\n--- {name} ---")
|
||||
try:
|
||||
result = fn(*args, **kwargs)
|
||||
decrypted = try_decrypt(result)
|
||||
if isinstance(decrypted, dict):
|
||||
keys = list(decrypted.keys())
|
||||
print(f" OK keys: {keys}")
|
||||
if "data" in decrypted and isinstance(decrypted["data"], list):
|
||||
print(f" data_len={len(decrypted['data'])}")
|
||||
if decrypted["data"]:
|
||||
print(f" first: {json.dumps(decrypted['data'][0], ensure_ascii=False)[:300]}")
|
||||
if "pagination" in decrypted:
|
||||
print(f" pagination: {decrypted['pagination']}")
|
||||
elif isinstance(decrypted, list):
|
||||
print(f" OK list_len={len(decrypted)}")
|
||||
if decrypted:
|
||||
print(f" first: {json.dumps(decrypted[0], ensure_ascii=False)[:200]}")
|
||||
return decrypted
|
||||
except requests.exceptions.ConnectionError:
|
||||
print(f" [ERROR] 连接失败 - 请确保服务端已启动 (npm run dev)")
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f" [ERROR] {e}")
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--user", default="sixteenth")
|
||||
parser.add_argument("--pass", dest="password", default="")
|
||||
args = parser.parse_args()
|
||||
|
||||
s = requests.Session()
|
||||
|
||||
def get(path, params=None, auth=False, token=None):
|
||||
h = {}
|
||||
if auth and token:
|
||||
h["Authorization"] = f"Bearer {token}"
|
||||
r = s.get(f"{LOCAL_BASE}{path}", params=params, headers=h, timeout=10)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
def post(path, body, auth=False, token=None, extra_headers=None):
|
||||
h = {"Content-Type": "application/json"}
|
||||
if auth and token:
|
||||
h["Authorization"] = f"Bearer {token}"
|
||||
if extra_headers:
|
||||
h.update(extra_headers)
|
||||
r = s.post(f"{LOCAL_BASE}{path}", json=body, headers=h, timeout=10)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
def delete(path, token=None):
|
||||
h = {"Authorization": f"Bearer {token}"} if token else {}
|
||||
r = s.delete(f"{LOCAL_BASE}{path}", headers=h, timeout=10)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
print(f"[{datetime.now().isoformat()}] 测试 {LOCAL_BASE}")
|
||||
|
||||
# 1. 公开接口
|
||||
test("武器分类", get, "/api/weapon-categories")
|
||||
test("武器列表 (全部)", get, "/api/weapons")
|
||||
test("武器列表 (突击步枪)", get, "/api/weapons", params={"category": "突击步枪"})
|
||||
test("方案列表 (schemes)", get, "/api/schemes", params={"sort": "hot", "page": "1", "limit": "6"})
|
||||
test("方案列表 (schemes_aob)", get, "/api/schemes_aob", params={"sort": "hot", "page": "1", "limit": "6"})
|
||||
test("广告列表", get, "/api/adverts/list")
|
||||
|
||||
# 2. 注册 + 登录
|
||||
if args.password:
|
||||
test("注册 (可能已存在)", post, "/api/register", body={
|
||||
"username": args.user, "password": args.password, "email": f"{args.user}@test.com"
|
||||
})
|
||||
login_resp = test("登录", post, "/api/login", body={
|
||||
"username": args.user, "password": args.password
|
||||
})
|
||||
token = login_resp.get("token") if login_resp else None
|
||||
|
||||
if token:
|
||||
print(f"\n Token: {token[:50]}...")
|
||||
|
||||
# 3. 认证接口
|
||||
test("会话状态", get, "/api/session-status", auth=True, token=token)
|
||||
test("VIP状态", get, "/api/vip-status", auth=True, token=token)
|
||||
|
||||
# 4. 创建方案
|
||||
scheme_resp = test("创建方案", post, "/api/schemes", body={
|
||||
"description": "测试M4A1方案",
|
||||
"category": "突击步枪",
|
||||
"weaponName": "M4A1突击步枪",
|
||||
"scheme": "M4A1突击步枪-烽火地带-TESTCODE123",
|
||||
"price": 30,
|
||||
}, auth=True, token=token,
|
||||
extra_headers={
|
||||
"x-user-info": "eyJpZCI6NDc5MTcsInVzZXJuYW1lIjoic2l4dGVlbnRoIn0=" # test userinfo base64
|
||||
})
|
||||
scheme_id = scheme_resp.get("data", {}).get("id") if scheme_resp else None
|
||||
|
||||
if scheme_id:
|
||||
# 5. 收藏
|
||||
test("添加收藏", post, "/api/favorites", body={
|
||||
"schemeId": scheme_id, "source": "烽火地带"
|
||||
}, auth=True, token=token)
|
||||
test("检查收藏", get, "/api/favorites/check", params={
|
||||
"schemeId": scheme_id, "source": "烽火地带"
|
||||
}, auth=True, token=token)
|
||||
test("收藏列表", get, "/api/favorites", auth=True, token=token)
|
||||
test("收藏数量", get, "/api/favorites/count")
|
||||
test("取消收藏", delete, f"/api/favorites/{scheme_id}?source=烽火地带", token=token)
|
||||
|
||||
# 6. 使用记录
|
||||
test("记录使用", post, f"/api/schemes/{scheme_id}/use", body={}, token=token)
|
||||
else:
|
||||
print("\n 登录失败(可能未注册)")
|
||||
else:
|
||||
print("\n 跳过登录测试 (未提供 --pass)")
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(" DONE")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -24,6 +24,9 @@ model User {
|
||||
isVip Boolean @default(false) @map("is_vip")
|
||||
vipLevel Int @default(0) @map("vip_level")
|
||||
vipExpireAt DateTime? @map("vip_expire_at")
|
||||
|
||||
// Token 吊销 (递增版本号使旧 token 失效)
|
||||
tokenVersion Int @default(0) @map("token_version")
|
||||
|
||||
// 统计
|
||||
schemesCount Int @default(0) @map("schemes_count")
|
||||
@@ -277,3 +280,31 @@ model DeviceBinding {
|
||||
@@unique([userId, installId])
|
||||
@@map("device_bindings")
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 地图密码批次表
|
||||
// ============================================
|
||||
model MapPassword {
|
||||
id String @id @default(uuid())
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
maps MapPasswordItem[]
|
||||
|
||||
@@map("map_passwords")
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 地图密码明细表
|
||||
// ============================================
|
||||
model MapPasswordItem {
|
||||
id String @id @default(uuid())
|
||||
batchId String @map("batch_id")
|
||||
mapName String @map("map_name") @db.VarChar(50)
|
||||
password String @db.VarChar(20)
|
||||
location String @db.Text
|
||||
image String @db.VarChar(500)
|
||||
|
||||
batch MapPassword @relation(fields: [batchId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@map("map_password_items")
|
||||
}
|
||||
|
||||
@@ -4,15 +4,17 @@ import crypto from 'crypto';
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
/**
|
||||
* 生成 VIP 卡密
|
||||
* 生成 VIP 卡密 (原始 + 哈希)
|
||||
*/
|
||||
function generateCardKey(days: number): string {
|
||||
function generateCardKey(days: number): { raw: string; hash: 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('-')}`;
|
||||
const raw = `${prefix}-${segments.join('-')}`;
|
||||
const hash = crypto.createHash('sha256').update(raw).digest('hex');
|
||||
return { raw, hash };
|
||||
}
|
||||
|
||||
async function main() {
|
||||
@@ -22,37 +24,38 @@ async function main() {
|
||||
// 创建分类
|
||||
// ============================================
|
||||
const categories = [
|
||||
// 烽火地带
|
||||
{ name: '突击步枪', type: 'SCHEME' },
|
||||
{ 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(() => {}); // 忽略重复错误
|
||||
await prisma.category.create({ data: cat }).catch(() => {});
|
||||
}
|
||||
console.log(`✅ 创建 ${categories.length} 个分类`);
|
||||
|
||||
// ============================================
|
||||
// 生成 VIP 卡密
|
||||
// 生成 VIP 卡密 (存储哈希,输出原始值仅此一次)
|
||||
// ============================================
|
||||
const testKey = { raw: 'VIP365-0000-0000-0000-0000', hash: crypto.createHash('sha256').update('VIP365-0000-0000-0000-0000').digest('hex') };
|
||||
|
||||
await prisma.vipCard.create({
|
||||
data: { cardKey: testKey.hash, cardType: 'YEAR', days: 365 },
|
||||
}).catch(() => {});
|
||||
console.log(`\n🔑 测试卡密 (仅显示一次): ${testKey.raw}`);
|
||||
|
||||
const cardConfigs = [
|
||||
{ type: 'MONTH', days: 30, count: 100 },
|
||||
{ type: 'QUARTER', days: 90, count: 50 },
|
||||
@@ -62,29 +65,18 @@ async function main() {
|
||||
let totalCards = 0;
|
||||
for (const config of cardConfigs) {
|
||||
const cards = [];
|
||||
console.log(`\n📦 ${config.type} 卡密 (${config.count} 张):`);
|
||||
for (let i = 0; i < config.count; i++) {
|
||||
cards.push({
|
||||
cardKey: generateCardKey(config.days),
|
||||
cardType: config.type,
|
||||
days: config.days,
|
||||
});
|
||||
const { raw, hash } = generateCardKey(config.days);
|
||||
cards.push({ cardKey: hash, cardType: config.type, days: config.days });
|
||||
console.log(` ${raw}`);
|
||||
}
|
||||
|
||||
await prisma.vipCard.createMany({ data: cards, skipDuplicates: true });
|
||||
totalCards += config.count;
|
||||
console.log(`✅ 生成 ${config.type} 卡密 ${config.count} 张`);
|
||||
}
|
||||
|
||||
console.log(`\n🎉 种子数据初始化完成!`);
|
||||
console.log(` ⚠️ 以上卡密仅显示一次,请妥善保存`);
|
||||
console.log(` - 分类:${categories.length} 个`);
|
||||
console.log(` - VIP 卡密:${totalCards} 张`);
|
||||
console.log(` - VIP 卡密:${totalCards + 1} 张`);
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error('❌ 种子数据初始化失败:', e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
|
||||
308
src/index.ts
308
src/index.ts
@@ -13,10 +13,15 @@ import schemeAobRoutes from './routes/schemesAob';
|
||||
import filterRoutes from './routes/filters';
|
||||
import vipRoutes from './routes/vip';
|
||||
import favoriteRoutes from './routes/favorites';
|
||||
import likeRoutes from './routes/likes';
|
||||
import { prisma } from './utils/prisma';
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3001;
|
||||
|
||||
// 信任代理 (用于 express-rate-limit 正确识别客户端 IP)
|
||||
app.set('trust proxy', 1);
|
||||
|
||||
// ============================================
|
||||
// 中间件
|
||||
// ============================================
|
||||
@@ -27,9 +32,21 @@ app.use(helmet({
|
||||
crossOriginEmbedderPolicy: false,
|
||||
}));
|
||||
|
||||
// CORS
|
||||
// CORS — 仅允许白名单来源
|
||||
const ALLOWED_ORIGINS = process.env.ALLOWED_ORIGINS?.split(',') || [
|
||||
'http://localhost:5173',
|
||||
'http://localhost:3000',
|
||||
'app://.',
|
||||
'file://',
|
||||
];
|
||||
app.use(cors({
|
||||
origin: true, // 允许所有来源(开发模式)
|
||||
origin: (origin, callback) => {
|
||||
if (!origin || ALLOWED_ORIGINS.some(o => origin.startsWith(o))) {
|
||||
callback(null, true);
|
||||
} else {
|
||||
callback(new Error('CORS blocked'));
|
||||
}
|
||||
},
|
||||
credentials: true,
|
||||
}));
|
||||
|
||||
@@ -37,22 +54,31 @@ app.use(cors({
|
||||
app.use(express.json({ limit: '10mb' }));
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
|
||||
// 速率限制
|
||||
// 速率限制 — 全局
|
||||
const limiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 分钟
|
||||
max: 100, // 每个 IP 最多 100 次请求
|
||||
windowMs: 15 * 60 * 1000,
|
||||
max: 1000,
|
||||
message: { success: false, message: '请求过于频繁,请稍后再试' },
|
||||
});
|
||||
app.use('/api/', limiter);
|
||||
|
||||
// 登录接口更严格的限制
|
||||
const loginLimiter = rateLimit({
|
||||
// 敏感端点更严格的限制
|
||||
const strictLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000,
|
||||
max: 10,
|
||||
message: { success: false, message: '登录尝试过于频繁,请稍后再试' },
|
||||
message: { success: false, message: '操作过于频繁,请稍后再试' },
|
||||
});
|
||||
app.use('/api/login', loginLimiter);
|
||||
app.use('/api/register', loginLimiter);
|
||||
app.use('/api/login', strictLimiter);
|
||||
app.use('/api/register', strictLimiter);
|
||||
|
||||
// 会话轮询限流 (防止高频 session-status 刷库)
|
||||
const pollLimiter = rateLimit({
|
||||
windowMs: 60 * 1000,
|
||||
max: 60,
|
||||
message: { success: false, message: '轮询过于频繁' },
|
||||
});
|
||||
app.use('/api/session-status', pollLimiter);
|
||||
app.use('/api/vip-status', pollLimiter);
|
||||
|
||||
// ============================================
|
||||
// 路由
|
||||
@@ -62,6 +88,17 @@ app.get('/api/activity/ping', (req, res) => {
|
||||
res.json({ success: true, message: 'pong', timestamp: Date.now() });
|
||||
});
|
||||
|
||||
// 版本/健康检查 (用于验证部署)
|
||||
app.get('/api/version', (req, res) => {
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let ver = 'unknown';
|
||||
try {
|
||||
ver = fs.readFileSync(path.join(__dirname, '..', 'version.txt'), 'utf8').trim();
|
||||
} catch {}
|
||||
res.json({ version: ver });
|
||||
});
|
||||
|
||||
app.use('/api', authRoutes);
|
||||
app.use('/api/user', userRoutes);
|
||||
app.use('/api/schemes', schemeRoutes);
|
||||
@@ -71,8 +108,6 @@ 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) {
|
||||
@@ -81,6 +116,7 @@ app.get('/api/favorites/count', async (req: Request, res: Response) => {
|
||||
});
|
||||
|
||||
app.use('/api/favorites', favoriteRoutes);
|
||||
app.use('/api/likes', likeRoutes);
|
||||
|
||||
// ============================================
|
||||
// 缺失的存根接口(防止前端 404)
|
||||
@@ -91,9 +127,25 @@ 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/popups/:name', (req, res) => {
|
||||
const { name } = req.params;
|
||||
const popups: Record<string, any> = {
|
||||
test_beta_01: { id: 'test_beta_01', title: '测试弹窗', content: '', shown: false },
|
||||
};
|
||||
res.json({ success: true, data: popups[name] ?? null });
|
||||
});
|
||||
|
||||
// 嘉豪弹窗
|
||||
app.get('/api/jiahao', (req, res) => {
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
enabled: false,
|
||||
message: '',
|
||||
imageUrl: '',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// 售后教程弹窗
|
||||
@@ -106,13 +158,34 @@ 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: [] });
|
||||
// 游戏地图密码缓存(对齐 maqt.top 格式)
|
||||
app.get('/api/game/map-password/cached', async (_req, res) => {
|
||||
try {
|
||||
// 从 DB 取最新一批地图密码
|
||||
const latest = await prisma.mapPassword.findFirst({
|
||||
orderBy: { createdAt: 'desc' },
|
||||
include: { maps: { select: { mapName: true, password: true, location: true, image: true } } },
|
||||
});
|
||||
if (latest) {
|
||||
const d = new Date(latest.createdAt);
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
maps: latest.maps,
|
||||
updateTime: `${String(d.getDate()).padStart(2,'0')}月${String(d.getMonth()+1).padStart(2,'0')}日`,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// 默认空数据
|
||||
res.json({ success: true, data: { maps: [], updateTime: '' } });
|
||||
}
|
||||
} catch {
|
||||
res.json({ success: true, data: { maps: [], updateTime: '' } });
|
||||
}
|
||||
});
|
||||
|
||||
// 筛选分类
|
||||
app.get('/api/filter-share/categories', (req, res) => {
|
||||
app.get('/api/filter-share/categories', (_req, res) => {
|
||||
res.json({ success: true, data: [] });
|
||||
});
|
||||
|
||||
@@ -120,23 +193,110 @@ app.get('/api/filter-share/categories', (req, res) => {
|
||||
// 武器调谐窗 (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 });
|
||||
// 武器分类列表 (对齐 maqt.top 真实格式)
|
||||
app.get('/api/weapon-categories', async (_req, res) => {
|
||||
try {
|
||||
const categories = await Promise.all(
|
||||
['突击步枪','冲锋枪','射手步枪','轻机枪','狙击步枪','霰弹枪','手枪','特殊武器'].map(async (cat) => {
|
||||
const count = await prisma.scheme.count({ where: { category: cat, status: 'PUBLISHED' } });
|
||||
return { category: cat, scheme_count: count };
|
||||
})
|
||||
);
|
||||
res.json({ success: true, data: categories });
|
||||
} catch {
|
||||
res.json({ success: true, data: [] });
|
||||
}
|
||||
});
|
||||
|
||||
// 武器列表(按分类筛选)
|
||||
app.get('/api/weapons', (req, res) => {
|
||||
res.json({ success: true, data: [] });
|
||||
// 武器列表数据 (从真实 API 同步)
|
||||
const WEAPONS_SEED = [
|
||||
{ id:57, weapon_name:"m14", display_name:"M14射手步枪", category:"射手步枪" },
|
||||
{ id:1, weapon_name:"M7", display_name:"M7战斗步枪", category:"突击步枪" },
|
||||
{ id:4, weapon_name:"K416", display_name:"K416突击步枪", category:"突击步枪" },
|
||||
{ id:6, weapon_name:"ASVal", display_name:"AS Val突击步枪", category:"突击步枪" },
|
||||
{ id:51, weapon_name:"pkm", display_name:"PKM通用机枪", category:"轻机枪" },
|
||||
{ id:7, weapon_name:"M4A1", display_name:"M4A1突击步枪", category:"突击步枪" },
|
||||
{ id:42, weapon_name:"mp7", display_name:"MP7冲锋枪", category:"冲锋枪" },
|
||||
{ id:46, weapon_name:"sr3m", display_name:"SR-3M紧凑突击步枪", category:"冲锋枪" },
|
||||
{ id:52, weapon_name:"qjb201", display_name:"QJB201轻机枪", category:"轻机枪" },
|
||||
{ id:5, weapon_name:"KC17", display_name:"KC17突击步枪", category:"突击步枪" },
|
||||
{ id:78, weapon_name:"mk17", display_name:"MK47突击步枪", category:"突击步枪" },
|
||||
{ id:2, weapon_name:"K437", display_name:"K437突击步枪", category:"突击步枪" },
|
||||
{ id:45, weapon_name:"smg", display_name:"SMG-45冲锋枪", category:"冲锋枪" },
|
||||
{ id:12, weapon_name:"TL", display_name:"腾龙突击步枪", category:"突击步枪" },
|
||||
{ id:10, weapon_name:"SCAR-H", display_name:"SCAR-H战斗步枪", category:"突击步枪" },
|
||||
{ id:43, weapon_name:"p90", display_name:"P90冲锋枪", category:"冲锋枪" },
|
||||
{ id:14, weapon_name:"G3", display_name:"G3战斗步枪", category:"突击步枪" },
|
||||
{ id:8, weapon_name:"AUG", display_name:"AUG突击步枪", category:"突击步枪" },
|
||||
{ id:49, weapon_name:"m249", display_name:"M249轻机枪", category:"轻机枪" },
|
||||
{ id:11, weapon_name:"AKM", display_name:"AKM突击步枪", category:"突击步枪" },
|
||||
{ id:50, weapon_name:"m250", display_name:"M250通用机枪", category:"轻机枪" },
|
||||
{ id:44, weapon_name:"qcq171", display_name:"QCQ171冲锋枪", category:"冲锋枪" },
|
||||
{ id:3, weapon_name:"ASH-12", display_name:"ASh-12战斗步枪", category:"突击步枪" },
|
||||
{ id:41, weapon_name:"mp5", display_name:"MP5冲锋枪", category:"冲锋枪" },
|
||||
{ id:21, weapon_name:"ys", display_name:"勇士冲锋枪", category:"冲锋枪" },
|
||||
{ id:9, weapon_name:"AK-12", display_name:"AK-12突击步枪", category:"突击步枪" },
|
||||
{ id:53, weapon_name:"awm", display_name:"AWM狙击步枪", category:"狙击步枪" },
|
||||
{ id:16, weapon_name:"PTR-32", display_name:"PTR-32突击步枪", category:"突击步枪" },
|
||||
{ id:54, weapon_name:"m700", display_name:"M700狙击步枪", category:"狙击步枪" },
|
||||
{ id:79, weapon_name:"mk4", display_name:"MK4冲锋枪", category:"冲锋枪" },
|
||||
{ id:68, weapon_name:"s12k", display_name:"S12K霰弹枪", category:"霰弹枪" },
|
||||
{ id:62, weapon_name:"sr25", display_name:"SR-25射手步枪", category:"射手步枪" },
|
||||
{ id:48, weapon_name:"vkt", display_name:"Vector冲锋枪", category:"冲锋枪" },
|
||||
{ id:63, weapon_name:"svd", display_name:"SVD狙击步枪", category:"射手步枪" },
|
||||
{ id:82, weapon_name:"ar57", display_name:"AR57突击步枪", category:"突击步枪" },
|
||||
{ id:15, weapon_name:"QBZ95-1", display_name:"QBZ95-1突击步枪", category:"突击步枪" },
|
||||
{ id:13, weapon_name:"SG552", display_name:"SG552突击步枪", category:"突击步枪" },
|
||||
{ id:64, weapon_name:"vss", display_name:"VSS射手步枪", category:"射手步枪" },
|
||||
{ id:20, weapon_name:"yn", display_name:"野牛冲锋枪", category:"冲锋枪" },
|
||||
{ id:17, weapon_name:"CAR-15", display_name:"CAR-15突击步枪", category:"突击步枪" },
|
||||
{ id:59, weapon_name:"psg1", display_name:"PSG-1射手步枪", category:"射手步枪" },
|
||||
{ id:60, weapon_name:"sks", display_name:"SKS射手步枪", category:"射手步枪" },
|
||||
{ id:18, weapon_name:"M16A4", display_name:"M16A4突击步枪", category:"突击步枪" },
|
||||
{ id:47, weapon_name:"uzi", display_name:"UZI冲锋枪", category:"冲锋枪" },
|
||||
{ id:72, weapon_name:"g18", display_name:"G18", category:"手枪" },
|
||||
{ id:67, weapon_name:"m1014", display_name:"M1014霰弹枪", category:"霰弹枪" },
|
||||
{ id:77, weapon_name:"marlin", display_name:"Marlin杠杆步枪", category:"射手步枪" },
|
||||
{ id:58, weapon_name:"mini", display_name:"Mini-14射手步枪", category:"射手步枪" },
|
||||
{ id:65, weapon_name:"725", display_name:"725双管霰弹枪", category:"霰弹枪" },
|
||||
{ id:69, weapon_name:"93r", display_name:"93R", category:"手枪" },
|
||||
{ id:55, weapon_name:"r93", display_name:"R93狙击步枪", category:"狙击步枪" },
|
||||
{ id:56, weapon_name:"sv98", display_name:"SV-98狙击步枪", category:"狙击步枪" },
|
||||
{ id:80, weapon_name:"mcxlt", display_name:"MCX LT突击步枪", category:"突击步枪" },
|
||||
{ id:81, weapon_name:"fs12", display_name:"FS-12霰弹枪", category:"霰弹枪" },
|
||||
{ id:61, weapon_name:"sr9", display_name:"SR9射手步枪", category:"射手步枪" },
|
||||
{ id:76, weapon_name:"fhg", display_name:"复合弓", category:"特殊武器" },
|
||||
{ id:19, weapon_name:"AKS-7U", display_name:"AKS-7U突击步枪", category:"突击步枪" },
|
||||
{ id:66, weapon_name:"m870", display_name:"M870霰弹枪", category:"霰弹枪" },
|
||||
{ id:70, weapon_name:"357zl", display_name:".357左轮", category:"手枪" },
|
||||
{ id:74, weapon_name:"qsz92g", display_name:"QSZ92G", category:"手枪" },
|
||||
{ id:75, weapon_name:"smzy", display_name:"沙漠之鹰", category:"手枪" },
|
||||
{ id:71, weapon_name:"g17", display_name:"G17", category:"手枪" },
|
||||
{ id:73, weapon_name:"m1911", display_name:"M1911", category:"手枪" },
|
||||
{ id:83, weapon_name:"M80", display_name:"M82狙击步枪", category:"狙击步枪" },
|
||||
];
|
||||
|
||||
// 武器列表(对齐真实 API: 可按中文分类筛选,带 use_count)
|
||||
app.get('/api/weapons', async (req, res) => {
|
||||
try {
|
||||
const category = req.query.category as string | undefined;
|
||||
let weapons = WEAPONS_SEED;
|
||||
if (category) {
|
||||
weapons = weapons.filter(w => w.category === category);
|
||||
}
|
||||
// 带使用计数
|
||||
const result = await Promise.all(
|
||||
weapons.map(async (w) => {
|
||||
const use_count = await prisma.scheme.count({ where: { weaponName: w.display_name, status: 'PUBLISHED' } });
|
||||
return { ...w, use_count };
|
||||
})
|
||||
);
|
||||
// 按使用量降序排序
|
||||
result.sort((a, b) => b.use_count - a.use_count);
|
||||
res.json({ success: true, data: result });
|
||||
} catch {
|
||||
res.json({ success: true, data: [] });
|
||||
}
|
||||
});
|
||||
|
||||
// 分类下的武器
|
||||
@@ -172,6 +332,86 @@ app.get('/api/avatars', (req, res) => {
|
||||
res.json({ success: true, data: [] });
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// 更新服务 (electron-updater)
|
||||
// ============================================
|
||||
|
||||
// 更新配置
|
||||
app.get('/update-config.json', (req, res) => {
|
||||
res.json({
|
||||
version: '7.0.4',
|
||||
url: '',
|
||||
notes: '',
|
||||
mandatory: false,
|
||||
});
|
||||
});
|
||||
|
||||
// latest.yml (electron-updater 标准格式)
|
||||
app.get('/latest.yml', (req, res) => {
|
||||
res.type('text/yaml');
|
||||
res.send(`version: 7.0.4
|
||||
files: []
|
||||
releaseDate: '2024-01-01T00:00:00.000Z'
|
||||
`);
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// 开发工具端点 (需 ADMIN_SECRET)
|
||||
// ============================================
|
||||
app.post('/api/admin/set-vip', async (req, res) => {
|
||||
const secret = process.env.ADMIN_SECRET;
|
||||
if (!secret || req.headers['x-admin-secret'] !== secret) {
|
||||
return res.status(403).json({ success: false, message: '禁止访问' });
|
||||
}
|
||||
try {
|
||||
const { username, isVip } = req.body;
|
||||
if (!username) return res.status(400).json({ success: false, message: '缺少 username' });
|
||||
const user = await prisma.user.update({
|
||||
where: { username },
|
||||
data: {
|
||||
isVip: isVip !== false,
|
||||
vipExpireAt: isVip !== false ? new Date(Date.now() + 365 * 24 * 60 * 60 * 1000) : null,
|
||||
vipLevel: isVip !== false ? 1 : 0,
|
||||
},
|
||||
});
|
||||
res.json({ success: true, username: user.username, isVip: user.isVip });
|
||||
} catch (e: any) {
|
||||
console.error('Admin set-vip error:', e);
|
||||
res.status(500).json({ success: false, message: '操作失败' });
|
||||
}
|
||||
});
|
||||
|
||||
// 管理员更新地图密码
|
||||
app.post('/api/admin/map-passwords', async (req, res) => {
|
||||
const secret = process.env.ADMIN_SECRET;
|
||||
if (!secret || req.headers['x-admin-secret'] !== secret) {
|
||||
return res.status(403).json({ success: false, message: '禁止访问' });
|
||||
}
|
||||
try {
|
||||
const { maps } = req.body;
|
||||
if (!Array.isArray(maps) || maps.length === 0) {
|
||||
return res.status(400).json({ success: false, message: '缺少 maps 数组' });
|
||||
}
|
||||
const batch = await prisma.mapPassword.create({
|
||||
data: {
|
||||
maps: {
|
||||
create: maps.map((m: any) => ({
|
||||
mapName: m.mapName,
|
||||
password: m.password,
|
||||
location: m.location || '',
|
||||
image: m.image || '',
|
||||
})),
|
||||
},
|
||||
},
|
||||
include: { maps: true },
|
||||
});
|
||||
res.json({ success: true, data: batch });
|
||||
} catch (e: any) {
|
||||
console.error('Admin map-password error:', e);
|
||||
res.status(500).json({ success: false, message: '更新失败: ' + e.message });
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// 错误处理
|
||||
// ============================================
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { verifyToken, extractToken } from '../utils/jwt';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
import { prisma } from '../utils/prisma';
|
||||
|
||||
// 扩展 Request 类型
|
||||
declare global {
|
||||
@@ -44,12 +42,12 @@ export async function authMiddleware(req: Request, res: Response, next: NextFunc
|
||||
});
|
||||
}
|
||||
|
||||
// 检查用户是否存在且未被删除
|
||||
// 检查用户是否存在且未被删除,同时验证 tokenVersion 一致
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: payload.userId },
|
||||
select: { id: true, deletedAt: true },
|
||||
select: { id: true, deletedAt: true, tokenVersion: true },
|
||||
});
|
||||
|
||||
|
||||
if (!user || user.deletedAt) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
@@ -57,6 +55,15 @@ export async function authMiddleware(req: Request, res: Response, next: NextFunc
|
||||
code: 'USER_NOT_FOUND',
|
||||
});
|
||||
}
|
||||
|
||||
// token 已被吊销 (登出或密码修改后旧 token 失效)
|
||||
if (user.tokenVersion !== payload.tokenVersion) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: '访问令牌已失效,请重新登录',
|
||||
code: 'TOKEN_EXPIRED',
|
||||
});
|
||||
}
|
||||
|
||||
// 将用户信息附加到请求对象
|
||||
req.user = payload;
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
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';
|
||||
import { prisma } from '../utils/prisma';
|
||||
|
||||
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),
|
||||
password: z.string().min(8, '密码至少8位').max(100)
|
||||
.regex(/[A-Z]/, '密码需包含大写字母')
|
||||
.regex(/[a-z]/, '密码需包含小写字母')
|
||||
.regex(/[0-9]/, '密码需包含数字'),
|
||||
email: z.string().email(),
|
||||
installId: z.string().optional(),
|
||||
deviceHash: z.string().optional(),
|
||||
@@ -22,34 +24,34 @@ const registerSchema = z.object({
|
||||
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: {
|
||||
@@ -60,7 +62,7 @@ router.post('/register', async (req: Request, res: Response) => {
|
||||
deviceHash: body.deviceHash,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
// 生成 Token
|
||||
const token = generateToken({
|
||||
userId: user.id,
|
||||
@@ -68,8 +70,9 @@ router.post('/register', async (req: Request, res: Response) => {
|
||||
email: user.email,
|
||||
isVip: user.isVip,
|
||||
vipLevel: user.vipLevel,
|
||||
tokenVersion: user.tokenVersion,
|
||||
});
|
||||
|
||||
|
||||
// 记录日志
|
||||
await prisma.userLog.create({
|
||||
data: {
|
||||
@@ -80,7 +83,7 @@ router.post('/register', async (req: Request, res: Response) => {
|
||||
ipAddress: req.ip,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
token,
|
||||
@@ -102,7 +105,7 @@ router.post('/register', async (req: Request, res: Response) => {
|
||||
errors: error.errors,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
console.error('Register error:', error);
|
||||
res.status(500).json({ success: false, message: '注册失败' });
|
||||
}
|
||||
@@ -170,8 +173,9 @@ router.post('/login', async (req: Request, res: Response) => {
|
||||
email: user.email,
|
||||
isVip: user.isVip,
|
||||
vipLevel: user.vipLevel,
|
||||
tokenVersion: user.tokenVersion,
|
||||
});
|
||||
|
||||
|
||||
// 记录日志
|
||||
await prisma.userLog.create({
|
||||
data: {
|
||||
@@ -182,7 +186,7 @@ router.post('/login', async (req: Request, res: Response) => {
|
||||
ipAddress: req.ip,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
token,
|
||||
@@ -204,21 +208,80 @@ router.post('/login', async (req: Request, res: Response) => {
|
||||
errors: error.errors,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
console.error('Login error:', error);
|
||||
res.status(500).json({ success: false, message: '登录失败' });
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// 会话状态
|
||||
// 登出 (使当前 token 失效)
|
||||
// ============================================
|
||||
router.post('/logout', authMiddleware, async (req: Request, res: Response) => {
|
||||
try {
|
||||
await prisma.user.update({
|
||||
where: { id: req.user!.userId },
|
||||
data: { tokenVersion: { increment: 1 } },
|
||||
});
|
||||
res.json({ success: true, message: '已登出' });
|
||||
} catch (error) {
|
||||
console.error('Logout error:', error);
|
||||
res.status(500).json({ success: false, message: '登出失败' });
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// 会话状态 (从数据库查询最新状态,避免 token 缓存导致 isVip 过期)
|
||||
// ============================================
|
||||
router.get('/session-status', authMiddleware, async (req: Request, res: Response) => {
|
||||
res.json({
|
||||
success: true,
|
||||
valid: true,
|
||||
user: req.user,
|
||||
});
|
||||
try {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: req.user!.userId },
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
email: true,
|
||||
avatar: true,
|
||||
isVip: true,
|
||||
vipExpireAt: true,
|
||||
vipLevel: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: '用户不存在',
|
||||
code: 'USER_NOT_FOUND',
|
||||
});
|
||||
}
|
||||
|
||||
// 检查 VIP 是否过期
|
||||
let isVip = user.isVip;
|
||||
if (user.vipExpireAt && new Date() > user.vipExpireAt) {
|
||||
isVip = false;
|
||||
await prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: { isVip: false },
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
valid: true,
|
||||
user: {
|
||||
userId: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
avatar: user.avatar,
|
||||
isVip,
|
||||
vipLevel: user.vipLevel,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Session status error:', error);
|
||||
res.status(500).json({ success: false, message: '获取会话状态失败' });
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================
|
||||
|
||||
@@ -1,36 +1,91 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { z } from 'zod';
|
||||
import { authMiddleware } from '../middleware/auth';
|
||||
import { prisma } from '../utils/prisma';
|
||||
|
||||
const router = Router();
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
router.use(authMiddleware);
|
||||
|
||||
const TARGET_TYPES = ['SCHEME', 'SCHEME_AOB', 'FILTER'] as const;
|
||||
// source → targetType 映射
|
||||
function sourceToType(source?: string): string {
|
||||
if (source === '全面战场' || source === 'schemes_aob') return 'SCHEME_AOB';
|
||||
return 'SCHEME'; // 烽火地带 / schemes
|
||||
}
|
||||
|
||||
// 格式化方案卡片(对齐顶层 API)
|
||||
function formatScheme(s: any) {
|
||||
return {
|
||||
id: s.id,
|
||||
user_id: s.userId,
|
||||
description: s.description || '',
|
||||
scheme_content: s.schemeContent || '',
|
||||
category: s.category || '',
|
||||
weapon_name: s.weaponName || '',
|
||||
price: s.price ? `${s.price}W` : null,
|
||||
tags: [],
|
||||
uses: s.downloadsCount || 0,
|
||||
status: 'normal',
|
||||
comments: null,
|
||||
shares: null,
|
||||
created_at: s.createdAt?.toISOString?.() || s.createdAt,
|
||||
updated_at: s.updatedAt?.toISOString?.() || s.updatedAt,
|
||||
source: null,
|
||||
total_historical_uses: s.downloadsCount || 0,
|
||||
username: s.user?.username || '',
|
||||
avatar: s.user?.avatar || '',
|
||||
partner_type: null,
|
||||
partner_level: null,
|
||||
partner_badge: null,
|
||||
partner_logo: null,
|
||||
social_link: null,
|
||||
likes: null,
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 获取收藏列表
|
||||
// 获取收藏列表 (对齐 GET /api/favorites)
|
||||
// ============================================
|
||||
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 { page = '1', limit = '12', sort = 'hot', weaponCategory, weaponName, search } = req.query;
|
||||
const pageNum = Math.max(1, parseInt(String(page)) || 1);
|
||||
const limitNum = Math.min(100, Math.max(1, parseInt(String(limit)) || 12));
|
||||
|
||||
// 先查用户的收藏记录
|
||||
const total = await prisma.favorite.count({ where: { userId: req.user!.userId } });
|
||||
const favorites = await prisma.favorite.findMany({
|
||||
where,
|
||||
where: { userId: req.user!.userId },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip,
|
||||
take: Number(limit),
|
||||
skip: (pageNum - 1) * limitNum,
|
||||
take: limitNum,
|
||||
});
|
||||
|
||||
// 加载关联的方案数据
|
||||
const data: any[] = [];
|
||||
for (const fav of favorites) {
|
||||
let scheme: any = null;
|
||||
if (fav.targetType === 'SCHEME') {
|
||||
scheme = await prisma.scheme.findUnique({
|
||||
where: { id: fav.targetId },
|
||||
include: { user: { select: { username: true, avatar: true } } },
|
||||
});
|
||||
} else if (fav.targetType === 'SCHEME_AOB') {
|
||||
scheme = await prisma.schemeAob.findUnique({
|
||||
where: { id: fav.targetId },
|
||||
include: { user: { select: { username: true, avatar: true } } },
|
||||
});
|
||||
}
|
||||
if (scheme && scheme.status === 'PUBLISHED') {
|
||||
data.push(formatScheme(scheme));
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data,
|
||||
pagination: { page: pageNum, limit: limitNum, hasMore: pageNum * limitNum < total },
|
||||
});
|
||||
|
||||
res.json({ success: true, data: favorites });
|
||||
} catch (error) {
|
||||
console.error('Get favorites error:', error);
|
||||
res.status(500).json({ success: false, message: '获取失败' });
|
||||
@@ -38,66 +93,43 @@ router.get('/', async (req: Request, res: Response) => {
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// 添加收藏
|
||||
// 添加收藏 (对齐 POST /api/favorites)
|
||||
// body: { schemeId, source: "烽火地带" }
|
||||
// ============================================
|
||||
const addFavoriteSchema = z.object({
|
||||
targetType: z.enum(TARGET_TYPES),
|
||||
targetId: z.string().uuid(),
|
||||
schemeId: z.string().or(z.number()),
|
||||
source: z.string().optional(),
|
||||
});
|
||||
|
||||
router.post('/', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const body = addFavoriteSchema.parse(req.body);
|
||||
|
||||
// 检查是否已收藏
|
||||
const targetType = sourceToType(body.source);
|
||||
const targetId = String(body.schemeId);
|
||||
|
||||
const existing = await prisma.favorite.findFirst({
|
||||
where: {
|
||||
userId: req.user!.userId,
|
||||
targetType: body.targetType,
|
||||
targetId: body.targetId,
|
||||
},
|
||||
where: { userId: req.user!.userId, targetType, targetId },
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
return res.status(400).json({ success: false, message: '已收藏' });
|
||||
return res.json({ success: true, alreadyFavorited: true });
|
||||
}
|
||||
|
||||
// 验证目标是否存在
|
||||
|
||||
// 验证目标存在
|
||||
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 (targetType === 'SCHEME') {
|
||||
targetExists = !!(await prisma.scheme.findFirst({ where: { id: targetId, status: 'PUBLISHED' } }));
|
||||
} else {
|
||||
targetExists = !!(await prisma.schemeAob.findFirst({ where: { id: targetId, status: 'PUBLISHED' } }));
|
||||
}
|
||||
|
||||
if (!targetExists) {
|
||||
return res.status(404).json({ success: false, message: '目标不存在' });
|
||||
return res.status(404).json({ success: false, message: '方案不存在' });
|
||||
}
|
||||
|
||||
// 创建收藏
|
||||
|
||||
await prisma.favorite.create({
|
||||
data: {
|
||||
userId: req.user!.userId,
|
||||
targetType: body.targetType,
|
||||
targetId: body.targetId,
|
||||
},
|
||||
data: { userId: req.user!.userId, targetType, targetId },
|
||||
});
|
||||
|
||||
// 更新用户收藏数
|
||||
await prisma.user.update({
|
||||
where: { id: req.user!.userId },
|
||||
data: { favoritesCount: { increment: 1 } },
|
||||
});
|
||||
|
||||
res.json({ success: true, message: '收藏成功' });
|
||||
|
||||
res.json({ success: true, alreadyFavorited: false });
|
||||
} catch (error) {
|
||||
console.error('Add favorite error:', error);
|
||||
res.status(500).json({ success: false, message: '收藏失败' });
|
||||
@@ -105,29 +137,24 @@ router.post('/', async (req: Request, res: Response) => {
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// 取消收藏
|
||||
// 取消收藏 (对齐 DELETE /api/favorites/:schemeId?source=xxx)
|
||||
// ============================================
|
||||
router.delete('/:id', async (req: Request, res: Response) => {
|
||||
router.delete('/:schemeId', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const favorite = await prisma.favorite.findUnique({
|
||||
where: { id },
|
||||
const { schemeId } = req.params;
|
||||
const source = req.query.source as string | undefined;
|
||||
const targetType = sourceToType(source);
|
||||
|
||||
const favorite = await prisma.favorite.findFirst({
|
||||
where: { userId: req.user!.userId, targetType, targetId: schemeId },
|
||||
});
|
||||
|
||||
if (!favorite || favorite.userId !== req.user!.userId) {
|
||||
if (!favorite) {
|
||||
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: '已取消收藏' });
|
||||
|
||||
await prisma.favorite.delete({ where: { id: favorite.id } });
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Remove favorite error:', error);
|
||||
res.status(500).json({ success: false, message: '取消收藏失败' });
|
||||
@@ -135,33 +162,25 @@ router.delete('/:id', async (req: Request, res: Response) => {
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// 检查是否已收藏
|
||||
// 检查收藏状态 (对齐 GET /api/favorites/check?schemeId=xx&source=xx)
|
||||
// ============================================
|
||||
router.get('/check', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { targetType, targetId } = req.query;
|
||||
|
||||
if (!targetType || !targetId) {
|
||||
const { schemeId, source } = req.query;
|
||||
if (!schemeId) {
|
||||
return res.status(400).json({ success: false, message: '参数缺失' });
|
||||
}
|
||||
|
||||
const targetType = sourceToType(source as string | undefined);
|
||||
|
||||
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,
|
||||
where: { userId: req.user!.userId, targetType, targetId: String(schemeId) },
|
||||
});
|
||||
|
||||
res.json({ isFavorited: !!favorite });
|
||||
} catch (error) {
|
||||
console.error('Check favorite error:', error);
|
||||
res.status(500).json({ success: false, message: '检查失败' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
export default router;
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { authMiddleware, optionalAuth } from '../middleware/auth';
|
||||
import { prisma } from '../utils/prisma';
|
||||
|
||||
const router = Router();
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
// ============================================
|
||||
// 获取滤镜列表
|
||||
@@ -11,7 +10,9 @@ 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 pageNum = Math.max(1, parseInt(String(page)) || 1);
|
||||
const limitNum = Math.min(100, Math.max(1, parseInt(String(limit)) || 20));
|
||||
const skip = (pageNum - 1) * limitNum;
|
||||
|
||||
const where: any = { status: 'PUBLISHED' };
|
||||
if (category) where.category = String(category);
|
||||
@@ -100,6 +101,46 @@ router.post('/', authMiddleware, async (req: Request, res: Response) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// 更新滤镜 (PUT)
|
||||
// ============================================
|
||||
router.put('/:id', authMiddleware, async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { title, description, category, filterContent, contentFormat = 'MQTS1', brand, author } = req.body;
|
||||
|
||||
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: '无权修改此滤镜' });
|
||||
}
|
||||
|
||||
const updateData: any = {};
|
||||
if (title !== undefined) updateData.title = title;
|
||||
if (description !== undefined) updateData.description = description;
|
||||
if (category !== undefined) updateData.category = category;
|
||||
if (filterContent !== undefined) updateData.filterContent = filterContent;
|
||||
if (contentFormat !== undefined) updateData.contentFormat = contentFormat;
|
||||
|
||||
const updated = await prisma.filterShare.update({
|
||||
where: { id },
|
||||
data: updateData,
|
||||
});
|
||||
|
||||
res.json({ success: true, message: '滤镜已更新', data: updated });
|
||||
} catch (error) {
|
||||
console.error('Update filter_share error:', error);
|
||||
res.status(500).json({ success: false, message: '更新失败' });
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// 删除滤镜
|
||||
// ============================================
|
||||
|
||||
148
src/routes/likes.ts
Normal file
148
src/routes/likes.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { z } from 'zod';
|
||||
import { authMiddleware } from '../middleware/auth';
|
||||
import { prisma } from '../utils/prisma';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use(authMiddleware);
|
||||
|
||||
const TARGET_TYPES = ['SCHEME', 'SCHEME_AOB', 'FILTER'] as const;
|
||||
|
||||
router.post('/', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const body = z.object({
|
||||
targetType: z.enum(TARGET_TYPES),
|
||||
targetId: z.string().uuid(),
|
||||
}).parse(req.body);
|
||||
|
||||
const existing = await prisma.like.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.like.create({
|
||||
data: {
|
||||
userId: req.user!.userId,
|
||||
targetType: body.targetType,
|
||||
targetId: body.targetId,
|
||||
},
|
||||
});
|
||||
|
||||
if (body.targetType === 'SCHEME') {
|
||||
await prisma.scheme.update({
|
||||
where: { id: body.targetId },
|
||||
data: { likesCount: { increment: 1 } },
|
||||
});
|
||||
} else if (body.targetType === 'SCHEME_AOB') {
|
||||
await prisma.schemeAob.update({
|
||||
where: { id: body.targetId },
|
||||
data: { likesCount: { increment: 1 } },
|
||||
});
|
||||
} else if (body.targetType === 'FILTER') {
|
||||
await prisma.filterShare.update({
|
||||
where: { id: body.targetId },
|
||||
data: { likesCount: { increment: 1 } },
|
||||
});
|
||||
}
|
||||
|
||||
res.json({ success: true, message: '点赞成功' });
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return res.status(400).json({ success: false, message: '参数验证失败', errors: error.errors });
|
||||
}
|
||||
console.error('Add like error:', error);
|
||||
res.status(500).json({ success: false, message: '点赞失败' });
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/:id', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const like = await prisma.like.findUnique({ where: { id } });
|
||||
|
||||
if (!like || like.userId !== req.user!.userId) {
|
||||
return res.status(404).json({ success: false, message: '点赞记录不存在' });
|
||||
}
|
||||
|
||||
await prisma.like.delete({ where: { id } });
|
||||
|
||||
if (like.targetType === 'SCHEME') {
|
||||
await prisma.scheme.update({
|
||||
where: { id: like.targetId },
|
||||
data: { likesCount: { decrement: 1 } },
|
||||
});
|
||||
} else if (like.targetType === 'SCHEME_AOB') {
|
||||
await prisma.schemeAob.update({
|
||||
where: { id: like.targetId },
|
||||
data: { likesCount: { decrement: 1 } },
|
||||
});
|
||||
} else if (like.targetType === 'FILTER') {
|
||||
await prisma.filterShare.update({
|
||||
where: { id: like.targetId },
|
||||
data: { likesCount: { decrement: 1 } },
|
||||
});
|
||||
}
|
||||
|
||||
res.json({ success: true, message: '已取消点赞' });
|
||||
} catch (error) {
|
||||
console.error('Remove like 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 like = await prisma.like.findFirst({
|
||||
where: {
|
||||
userId: req.user!.userId,
|
||||
targetType: String(targetType),
|
||||
targetId: String(targetId),
|
||||
},
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
isLiked: !!like,
|
||||
likeId: like?.id || null,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Check like error:', error);
|
||||
res.status(500).json({ success: false, message: '检查失败' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -1,11 +1,41 @@
|
||||
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';
|
||||
import { encrypt, encryptResponse } from '../utils/encryption';
|
||||
import { prisma } from '../utils/prisma';
|
||||
|
||||
const router = Router();
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
// ---- 响应 shape 对齐 maqt.top 原版 ----
|
||||
|
||||
function formatScheme(s: any) {
|
||||
return {
|
||||
id: s.id,
|
||||
user_id: s.userId,
|
||||
description: s.description || '',
|
||||
scheme_content: s.schemeContent || '',
|
||||
category: s.category || '',
|
||||
weapon_name: s.weaponName || '',
|
||||
price: s.price ? `${s.price}W` : null,
|
||||
tags: [],
|
||||
likes: s.likesCount ?? null,
|
||||
uses: s.downloadsCount || 0,
|
||||
status: s.status === 'PUBLISHED' ? 'normal' : null,
|
||||
comments: null,
|
||||
shares: null,
|
||||
created_at: s.createdAt?.toISOString?.() || s.createdAt,
|
||||
updated_at: s.updatedAt?.toISOString?.() || s.updatedAt,
|
||||
source: s.source ?? null,
|
||||
total_historical_uses: s.downloadsCount || 0,
|
||||
username: s.user?.username || s.username || '',
|
||||
avatar: s.user?.avatar || s.avatar || 'https://tuku.maqt.top/i/2026/03/22/sv3fg9.png',
|
||||
partner_type: null,
|
||||
partner_level: null,
|
||||
partner_badge: null,
|
||||
partner_logo: null,
|
||||
social_link: null,
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 获取方案列表
|
||||
@@ -13,54 +43,56 @@ const prisma = new PrismaClient();
|
||||
router.get('/', optionalAuth, async (req: Request, res: Response) => {
|
||||
try {
|
||||
const {
|
||||
page = 1,
|
||||
limit = 20,
|
||||
weapon,
|
||||
category,
|
||||
sort = 'newest',
|
||||
page = '1',
|
||||
limit = '12',
|
||||
weaponCategory,
|
||||
weaponName,
|
||||
minPrice,
|
||||
maxPrice,
|
||||
search,
|
||||
sort = 'hot',
|
||||
} = req.query;
|
||||
|
||||
const skip = (Number(page) - 1) * Number(limit);
|
||||
|
||||
|
||||
const pageNum = Math.max(1, parseInt(String(page)) || 1);
|
||||
const limitNum = Math.min(100, Math.max(1, parseInt(String(limit)) || 12));
|
||||
const skip = (pageNum - 1) * limitNum;
|
||||
|
||||
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' };
|
||||
|
||||
if (weaponCategory) where.category = String(weaponCategory);
|
||||
if (weaponName) where.weaponName = { contains: String(weaponName) };
|
||||
if (minPrice) where.price = { ...(where.price || {}), gte: parseInt(String(minPrice)) };
|
||||
if (maxPrice) where.price = { ...(where.price || {}), lte: parseInt(String(maxPrice)) };
|
||||
if (search) where.description = { contains: String(search) };
|
||||
|
||||
let orderBy: any = { downloadsCount: 'desc' };
|
||||
if (sort === 'new') orderBy = { createdAt: '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,
|
||||
},
|
||||
},
|
||||
take: limitNum + 1, // fetch one extra to detect hasMore
|
||||
include: {
|
||||
user: { select: { id: true, username: true, avatar: true } },
|
||||
},
|
||||
});
|
||||
|
||||
res.json({
|
||||
|
||||
const hasMore = schemes.length > limitNum;
|
||||
const data = schemes.slice(0, limitNum).map(formatScheme);
|
||||
|
||||
const payload = {
|
||||
success: true,
|
||||
data: schemes,
|
||||
});
|
||||
data,
|
||||
pagination: { page: pageNum, limit: limitNum, hasMore },
|
||||
};
|
||||
|
||||
// 支持加密输出
|
||||
const useEncryption = req.query.encrypted === '1';
|
||||
if (useEncryption) {
|
||||
res.json(encryptResponse(payload));
|
||||
} else {
|
||||
res.json(payload);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Get schemes error:', error);
|
||||
res.status(500).json({ success: false, message: '获取失败' });
|
||||
@@ -73,61 +105,40 @@ router.get('/', optionalAuth, async (req: Request, res: Response) => {
|
||||
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,
|
||||
},
|
||||
select: { id: true, username: true, avatar: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!scheme) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '方案不存在',
|
||||
});
|
||||
|
||||
if (!scheme || (scheme.status !== 'PUBLISHED' && scheme.userId !== req.user?.userId)) {
|
||||
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,
|
||||
},
|
||||
where: { userId: req.user.userId, targetType: 'SCHEME', targetId: id },
|
||||
});
|
||||
isFavorited = !!fav;
|
||||
}
|
||||
|
||||
// 加密方案内容
|
||||
|
||||
const encryptedContent = encrypt(scheme.schemeContent);
|
||||
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
...scheme,
|
||||
schemeContent: encryptedContent,
|
||||
...formatScheme(scheme),
|
||||
scheme_content: encryptedContent,
|
||||
isFavorited,
|
||||
},
|
||||
});
|
||||
@@ -141,51 +152,58 @@ router.get('/:id', optionalAuth, async (req: Request, res: Response) => {
|
||||
// 创建方案
|
||||
// ============================================
|
||||
const createSchemeSchema = z.object({
|
||||
title: z.string().min(1).max(100),
|
||||
description: z.string().optional(),
|
||||
weaponName: z.string().optional(),
|
||||
description: z.string().min(1).max(50),
|
||||
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(),
|
||||
weaponName: z.string().optional(),
|
||||
scheme: z.string().min(1),
|
||||
price: z.union([z.number(), z.string()]).optional(),
|
||||
tags: z.array(z.string()).optional().default([]),
|
||||
});
|
||||
|
||||
router.post('/', authMiddleware, async (req: Request, res: Response) => {
|
||||
try {
|
||||
const body = createSchemeSchema.parse(req.body);
|
||||
|
||||
|
||||
// 支持 x-user-info header (Base64 JSON {id, username})
|
||||
const userInfoHeader = req.headers['x-user-info'] as string | undefined;
|
||||
let userId = req.user!.userId;
|
||||
if (userInfoHeader) {
|
||||
try {
|
||||
const decoded = JSON.parse(Buffer.from(userInfoHeader, 'base64').toString('utf-8'));
|
||||
if (decoded.id && decoded.username) {
|
||||
// 用 header 中的 id 查找对应本地用户
|
||||
const localUser = await prisma.user.findUnique({ where: { username: decoded.username } });
|
||||
if (localUser) userId = localUser.id;
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
const priceNum = typeof body.price === 'string' ? parseInt(body.price.replace(/[W万]/g, '')) : (body.price || 0);
|
||||
|
||||
const scheme = await prisma.scheme.create({
|
||||
data: {
|
||||
userId: req.user!.userId,
|
||||
title: body.title,
|
||||
userId,
|
||||
description: body.description,
|
||||
weaponName: body.weaponName,
|
||||
category: body.category,
|
||||
schemeContent: body.schemeContent,
|
||||
price: body.price,
|
||||
gpuModel: body.gpuModel,
|
||||
driverVersion: body.driverVersion,
|
||||
appVersion: body.appVersion,
|
||||
schemeContent: body.scheme,
|
||||
price: priceNum,
|
||||
status: 'PUBLISHED',
|
||||
},
|
||||
include: {
|
||||
user: { select: { id: true, username: true, avatar: true } },
|
||||
},
|
||||
});
|
||||
|
||||
// 更新用户方案数
|
||||
|
||||
await prisma.user.update({
|
||||
where: { id: req.user!.userId },
|
||||
where: { id: userId },
|
||||
data: { schemesCount: { increment: 1 } },
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '方案创建成功',
|
||||
data: scheme,
|
||||
});
|
||||
|
||||
res.json({ success: true, message: '方案发布成功', data: formatScheme(scheme) });
|
||||
} catch (error) {
|
||||
console.error('Create scheme error:', error);
|
||||
res.status(500).json({ success: false, message: '创建失败' });
|
||||
res.status(500).json({ success: false, message: '发布失败' });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -195,84 +213,72 @@ router.post('/', authMiddleware, async (req: Request, res: Response) => {
|
||||
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: '方案已删除',
|
||||
});
|
||||
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' } });
|
||||
await prisma.user.update({ where: { id: req.user!.userId }, data: { schemesCount: { decrement: 1 } } });
|
||||
|
||||
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) => {
|
||||
// ============================================
|
||||
// 记录使用 (POST /api/schemes/:id/use)
|
||||
// ============================================
|
||||
router.post('/:id/use', optionalAuth, 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 scheme = await prisma.scheme.findUnique({ where: { id }, select: { id: true } });
|
||||
if (!scheme) return res.status(404).json({ success: false, message: '方案不存在' });
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
if (req.user) {
|
||||
await prisma.userLog.create({
|
||||
data: { userId: req.user.userId, action: 'SchemeUse', targetType: 'Scheme', targetId: id },
|
||||
});
|
||||
}
|
||||
|
||||
res.json({ success: true, message: '使用次数已记录', downloadsCount: updated.downloadsCount });
|
||||
} catch (error) {
|
||||
console.error('Scheme use error:', error);
|
||||
res.status(500).json({ success: false, message: '记录失败' });
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// 举报 (POST /api/schemes/:id/report)
|
||||
// ============================================
|
||||
router.post('/:id/report', authMiddleware, async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { reason, description } = req.body;
|
||||
const scheme = await prisma.scheme.findUnique({ where: { id }, select: { id: true } });
|
||||
if (!scheme) return res.status(404).json({ success: false, message: '方案不存在' });
|
||||
|
||||
await prisma.userLog.create({
|
||||
data: {
|
||||
userId: req.user!.userId,
|
||||
action: 'Report',
|
||||
targetType: 'Scheme',
|
||||
targetId: id,
|
||||
},
|
||||
});
|
||||
|
||||
res.json({ success: true, message: '举报成功' });
|
||||
} catch (error) {
|
||||
console.error('Report error:', error);
|
||||
res.status(500).json({ success: false, message: '举报失败' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -1,52 +1,89 @@
|
||||
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';
|
||||
import { encrypt, encryptResponse } from '../utils/encryption';
|
||||
import { prisma } from '../utils/prisma';
|
||||
|
||||
const router = Router();
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
function formatScheme(s: any) {
|
||||
return {
|
||||
id: s.id,
|
||||
user_id: s.userId,
|
||||
description: s.description || '',
|
||||
scheme_content: s.schemeContent || '',
|
||||
category: s.category || '',
|
||||
weapon_name: s.weaponName || '',
|
||||
tags: [],
|
||||
likes: s.likesCount ?? null,
|
||||
uses: s.downloadsCount || 0,
|
||||
status: s.status === 'PUBLISHED' ? 'normal' : null,
|
||||
comments: null,
|
||||
shares: null,
|
||||
created_at: s.createdAt?.toISOString?.() || s.createdAt,
|
||||
updated_at: s.updatedAt?.toISOString?.() || s.updatedAt,
|
||||
source: s.source ?? null,
|
||||
total_historical_uses: s.downloadsCount || 0,
|
||||
username: s.user?.username || s.username || '',
|
||||
avatar: s.user?.avatar || s.avatar || 'https://tuku.maqt.top/i/2026/03/22/sv3fg9.png',
|
||||
partner_type: null,
|
||||
partner_level: null,
|
||||
partner_badge: null,
|
||||
partner_logo: null,
|
||||
social_link: null,
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 获取全面战场方案列表
|
||||
// 获取方案列表
|
||||
// ============================================
|
||||
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 {
|
||||
page = '1',
|
||||
limit = '12',
|
||||
weaponCategory,
|
||||
weaponName,
|
||||
search,
|
||||
sort = 'hot',
|
||||
} = req.query;
|
||||
|
||||
const pageNum = Math.max(1, parseInt(String(page)) || 1);
|
||||
const limitNum = Math.min(100, Math.max(1, parseInt(String(limit)) || 12));
|
||||
const skip = (pageNum - 1) * limitNum;
|
||||
|
||||
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' };
|
||||
|
||||
if (weaponCategory) where.category = String(weaponCategory);
|
||||
if (weaponName) where.weaponName = { contains: String(weaponName) };
|
||||
if (search) where.description = { contains: String(search) };
|
||||
|
||||
let orderBy: any = { downloadsCount: 'desc' };
|
||||
if (sort === 'new') orderBy = { createdAt: '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 },
|
||||
},
|
||||
take: limitNum + 1,
|
||||
include: {
|
||||
user: { select: { id: true, username: true, avatar: true } },
|
||||
},
|
||||
});
|
||||
|
||||
res.json({ success: true, data: schemes });
|
||||
|
||||
const hasMore = schemes.length > limitNum;
|
||||
const data = schemes.slice(0, limitNum).map(formatScheme);
|
||||
|
||||
const payload = {
|
||||
success: true,
|
||||
data,
|
||||
pagination: { page: pageNum, limit: limitNum, hasMore },
|
||||
};
|
||||
|
||||
if (req.query.encrypted === '1') {
|
||||
res.json(encryptResponse(payload));
|
||||
} else {
|
||||
res.json(payload);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Get schemes_aob error:', error);
|
||||
res.status(500).json({ success: false, message: '获取失败' });
|
||||
@@ -54,33 +91,36 @@ router.get('/', optionalAuth, async (req: Request, res: Response) => {
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// 获取单个方案详情
|
||||
// 获取单个方案
|
||||
// ============================================
|
||||
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 } },
|
||||
},
|
||||
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);
|
||||
|
||||
|
||||
let isFavorited = false;
|
||||
if (req.user) {
|
||||
const fav = await prisma.favorite.findFirst({
|
||||
where: { userId: req.user.userId, targetType: 'SCHEME_AOB', targetId: id },
|
||||
});
|
||||
isFavorited = !!fav;
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { ...scheme, schemeContent: encryptedContent },
|
||||
data: { ...formatScheme(scheme), scheme_content: encrypt(scheme.schemeContent), isFavorited },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Get scheme_aob error:', error);
|
||||
@@ -91,32 +131,103 @@ router.get('/:id', optionalAuth, async (req: Request, res: Response) => {
|
||||
// ============================================
|
||||
// 创建方案
|
||||
// ============================================
|
||||
const createSchema = z.object({
|
||||
description: z.string().min(1).max(50),
|
||||
category: z.string().optional(),
|
||||
weaponName: z.string().optional(),
|
||||
scheme: z.string().min(1),
|
||||
tags: z.array(z.string()).optional().default([]),
|
||||
});
|
||||
|
||||
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 body = createSchema.parse(req.body);
|
||||
const scheme = await prisma.schemeAob.create({
|
||||
data: {
|
||||
userId: req.user!.userId,
|
||||
title,
|
||||
description,
|
||||
weaponName,
|
||||
category,
|
||||
schemeContent,
|
||||
price,
|
||||
description: body.description,
|
||||
weaponName: body.weaponName,
|
||||
category: body.category,
|
||||
schemeContent: body.scheme,
|
||||
status: 'PUBLISHED',
|
||||
},
|
||||
include: {
|
||||
user: { select: { id: true, username: true, avatar: true } },
|
||||
},
|
||||
});
|
||||
|
||||
res.json({ success: true, message: '方案创建成功', data: scheme });
|
||||
|
||||
await prisma.user.update({
|
||||
where: { id: req.user!.userId },
|
||||
data: { schemesCount: { increment: 1 } },
|
||||
});
|
||||
|
||||
res.json({ success: true, message: '方案发布成功', data: formatScheme(scheme) });
|
||||
} catch (error) {
|
||||
console.error('Create scheme_aob error:', error);
|
||||
res.status(500).json({ success: false, message: '创建失败' });
|
||||
res.status(500).json({ success: false, message: '发布失败' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
// ============================================
|
||||
// 删除方案
|
||||
// ============================================
|
||||
router.delete('/:id', authMiddleware, async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const scheme = await prisma.schemeAob.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.schemeAob.update({ where: { id }, data: { status: 'DELETED' } });
|
||||
await prisma.user.update({ where: { id: req.user!.userId }, data: { schemesCount: { decrement: 1 } } });
|
||||
|
||||
res.json({ success: true, message: '方案已删除' });
|
||||
} catch (error) {
|
||||
console.error('Delete scheme_aob error:', error);
|
||||
res.status(500).json({ success: false, message: '删除失败' });
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// 记录使用
|
||||
// ============================================
|
||||
router.post('/:id/use', optionalAuth, async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const scheme = await prisma.schemeAob.findUnique({ where: { id }, select: { id: true } });
|
||||
if (!scheme) return res.status(404).json({ success: false, message: '方案不存在' });
|
||||
|
||||
await prisma.schemeAob.update({ where: { id }, data: { downloadsCount: { increment: 1 } } });
|
||||
|
||||
if (req.user) {
|
||||
await prisma.userLog.create({
|
||||
data: { userId: req.user.userId, action: 'SchemeAobUse', targetType: 'SchemeAob', targetId: id },
|
||||
});
|
||||
}
|
||||
|
||||
res.json({ success: true, message: '使用次数已记录' });
|
||||
} catch (error) {
|
||||
console.error('Scheme AOB use error:', error);
|
||||
res.status(500).json({ success: false, message: '记录失败' });
|
||||
}
|
||||
});
|
||||
|
||||
// 举报
|
||||
router.post('/:id/report', authMiddleware, async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const scheme = await prisma.schemeAob.findUnique({ where: { id }, select: { id: true } });
|
||||
if (!scheme) return res.status(404).json({ success: false, message: '方案不存在' });
|
||||
|
||||
await prisma.userLog.create({
|
||||
data: { userId: req.user!.userId, action: 'Report', targetType: 'SchemeAob', targetId: id },
|
||||
});
|
||||
|
||||
res.json({ success: true, message: '举报成功' });
|
||||
} catch (error) {
|
||||
console.error('Report scheme_aob error:', error);
|
||||
res.status(500).json({ success: false, message: '举报失败' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -1,17 +1,173 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { z } from 'zod';
|
||||
import { authMiddleware } from '../middleware/auth';
|
||||
import { prisma } from '../utils/prisma';
|
||||
|
||||
const router = Router();
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
// 所有路由都需要认证
|
||||
// ============================================
|
||||
// 获取用户统计 (公开)
|
||||
// ============================================
|
||||
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 pageNum = Math.max(1, parseInt(String(page)) || 1);
|
||||
const limitNum = Math.min(100, Math.max(1, parseInt(String(limit)) || 20));
|
||||
const skip = (pageNum - 1) * limitNum;
|
||||
|
||||
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: '用户不存在' });
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const isVipActive = user.isVip && user.vipExpireAt && user.vipExpireAt > now;
|
||||
|
||||
const limits = isVipActive
|
||||
? {
|
||||
maxDailyUses: 500,
|
||||
dailyUsesLeft: 500,
|
||||
maxFavorites: 1000,
|
||||
maxSchemes: 200,
|
||||
canUploadIcc: true,
|
||||
}
|
||||
: {
|
||||
maxDailyUses: 50,
|
||||
dailyUsesLeft: 50,
|
||||
maxFavorites: 100,
|
||||
maxSchemes: 50,
|
||||
canUploadIcc: false,
|
||||
};
|
||||
|
||||
res.json({ success: true, ...limits });
|
||||
} catch (error) {
|
||||
console.error('Get user limits error:', error);
|
||||
res.status(500).json({ success: false, message: '获取失败' });
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// 以下路由需要认证
|
||||
// ============================================
|
||||
router.use(authMiddleware);
|
||||
|
||||
// ============================================
|
||||
// 更新用户名
|
||||
// ============================================
|
||||
const updateUsernameSchema = z.object({
|
||||
userId: z.string().uuid(),
|
||||
username: z.string().min(3).max(50),
|
||||
@@ -77,6 +233,9 @@ router.put('/username', async (req: Request, res: Response) => {
|
||||
username: body.username,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return res.status(400).json({ success: false, message: '参数验证失败', errors: error.errors });
|
||||
}
|
||||
console.error('Update username error:', error);
|
||||
res.status(500).json({ success: false, message: '更新失败' });
|
||||
}
|
||||
@@ -124,6 +283,9 @@ router.put('/email', async (req: Request, res: Response) => {
|
||||
email: body.email,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return res.status(400).json({ success: false, message: '参数验证失败', errors: error.errors });
|
||||
}
|
||||
console.error('Update email error:', error);
|
||||
res.status(500).json({ success: false, message: '更新失败' });
|
||||
}
|
||||
@@ -159,179 +321,12 @@ router.put('/avatar', async (req: Request, res: Response) => {
|
||||
avatar: body.avatar,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return res.status(400).json({ success: false, message: '参数验证失败', errors: error.errors });
|
||||
}
|
||||
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;
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import crypto from 'crypto';
|
||||
import { z } from 'zod';
|
||||
import { authMiddleware } from '../middleware/auth';
|
||||
import { generateToken } from '../utils/jwt';
|
||||
import { prisma } from '../utils/prisma';
|
||||
|
||||
const router = Router();
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
function hashCardKey(raw: string): string {
|
||||
return crypto.createHash('sha256').update(raw).digest('hex');
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// VIP 卡密激活
|
||||
@@ -17,9 +22,10 @@ router.post('/activate-vip', authMiddleware, async (req: Request, res: Response)
|
||||
try {
|
||||
const body = activateSchema.parse(req.body);
|
||||
|
||||
// 查找卡密
|
||||
// 查找卡密 (比对哈希)
|
||||
const hashedKey = hashCardKey(body.cardKey);
|
||||
const card = await prisma.vipCard.findUnique({
|
||||
where: { cardKey: body.cardKey },
|
||||
where: { cardKey: hashedKey },
|
||||
});
|
||||
|
||||
if (!card) {
|
||||
@@ -89,8 +95,19 @@ router.post('/activate-vip', authMiddleware, async (req: Request, res: Response)
|
||||
},
|
||||
});
|
||||
|
||||
// 生成新 token(包含最新的 VIP 状态)
|
||||
const token = generateToken({
|
||||
userId: updatedUser.id,
|
||||
username: updatedUser.username,
|
||||
email: updatedUser.email,
|
||||
isVip: true,
|
||||
vipLevel: updatedUser.vipLevel,
|
||||
tokenVersion: updatedUser.tokenVersion,
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
token,
|
||||
isVip: true,
|
||||
vipExpireAt: newExpireAt,
|
||||
vipLevel: updatedUser.vipLevel,
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import crypto from 'crypto';
|
||||
|
||||
const ALGORITHM = 'aes-256-cbc';
|
||||
const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY || '0123456789abcdef0123456789abcdef';
|
||||
|
||||
// 与前端对齐: SHA-256(KEY_STRING) → 32 bytes
|
||||
const RAW_KEY = process.env.ENCRYPTION_KEY || 'maqt-delta-force-2024-secret-key-32';
|
||||
const KEY = crypto.createHash('sha256').update(RAW_KEY).digest();
|
||||
|
||||
export interface EncryptedData {
|
||||
encrypted: boolean;
|
||||
@@ -14,12 +17,10 @@ export interface EncryptedData {
|
||||
*/
|
||||
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);
|
||||
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'),
|
||||
@@ -27,15 +28,25 @@ export function encrypt(text: string): EncryptedData {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 加密整个 JSON 响应体(对齐前端 decryptData 格式)
|
||||
*/
|
||||
export function encryptResponse(payload: object): EncryptedData {
|
||||
const json = JSON.stringify(payload);
|
||||
const iv = crypto.randomBytes(16);
|
||||
const cipher = crypto.createCipheriv(ALGORITHM, KEY, iv);
|
||||
let encrypted = cipher.update(json, '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);
|
||||
const decipher = crypto.createDecipheriv(ALGORITHM, KEY, iv);
|
||||
let decrypted = decipher.update(encryptedData, undefined, 'utf-8');
|
||||
decrypted += decipher.final('utf-8');
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ export interface JwtPayload {
|
||||
email: string;
|
||||
isVip: boolean;
|
||||
vipLevel: number;
|
||||
tokenVersion: number;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
7
src/utils/prisma.ts
Normal file
7
src/utils/prisma.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };
|
||||
|
||||
export const prisma = globalForPrisma.prisma || new PrismaClient();
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;
|
||||
6
start.sh
Normal file
6
start.sh
Normal file
@@ -0,0 +1,6 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
echo "▶ 同步数据库 schema..."
|
||||
npx prisma db push --skip-generate
|
||||
echo "▶ 启动服务..."
|
||||
exec node dist/index.js
|
||||
1
version.txt
Normal file
1
version.txt
Normal file
@@ -0,0 +1 @@
|
||||
v0.3.0
|
||||
Reference in New Issue
Block a user