align API response format with maqt.top real specs: weapon-categories, weapons, schemes, schemes_aob, favorites check, encryption key derivation

This commit is contained in:
2026-05-24 01:47:08 +08:00
parent ea4e0f6e07
commit 8bcb6c7e7a
13 changed files with 1861 additions and 366 deletions

1
.gitignore vendored
View File

@@ -4,3 +4,4 @@ dist/
*.log *.log
.DS_Store .DS_Store
.prisma/ .prisma/
.venv/

461
API.md Normal file
View 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
```

316
origet/api_spec.py Normal file
View 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
View 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
View 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
View File

@@ -0,0 +1,2 @@
requests
pycryptodome

120
origet/test_apis.py Normal file
View 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
View 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()

View File

@@ -164,7 +164,7 @@ app.get('/api/game/map-password/cached', (req, res) => {
}); });
// 筛选分类 // 筛选分类
app.get('/api/filter-share/categories', (req, res) => { app.get('/api/filter-share/categories', (_req, res) => {
res.json({ success: true, data: [] }); res.json({ success: true, data: [] });
}); });
@@ -172,23 +172,110 @@ app.get('/api/filter-share/categories', (req, res) => {
// 武器调谐窗 (weapon-tuner) 所需的 API // 武器调谐窗 (weapon-tuner) 所需的 API
// ============================================ // ============================================
// 武器分类列表 // 武器分类列表 (对齐 maqt.top 真实格式)
app.get('/api/weapon-categories', (req, res) => { app.get('/api/weapon-categories', async (_req, res) => {
const categories = [ try {
{ category: 'AR', name: '突击步枪' }, const categories = await Promise.all(
{ category: 'SMG', name: '冲锋枪' }, ['突击步枪','冲锋枪','射手步枪','轻机枪','狙击步枪','霰弹枪','手枪','特殊武器'].map(async (cat) => {
{ category: 'SR', name: '狙击步枪' }, const count = await prisma.scheme.count({ where: { category: cat, status: 'PUBLISHED' } });
{ category: 'LMG', name: '轻机枪' }, return { category: cat, scheme_count: count };
{ category: 'SG', name: '霰弹枪' }, })
{ category: 'Pistol', name: '手枪' }, );
{ category: 'Launcher', name: '发射器' },
];
res.json({ success: true, data: categories }); res.json({ success: true, data: categories });
} catch {
res.json({ success: true, data: [] });
}
}); });
// 武器列表(按分类筛选) // 武器列表数据 (从真实 API 同步)
app.get('/api/weapons', (req, res) => { 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: [] }); res.json({ success: true, data: [] });
}
}); });
// 分类下的武器 // 分类下的武器

View File

@@ -7,31 +7,85 @@ const router = Router();
router.use(authMiddleware); 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: parseInt(s.id, 36) || String(s.id).split('-')[0] || 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) => { router.get('/', async (req: Request, res: Response) => {
try { try {
const { type, page = 1, limit = 20 } = req.query; const { page = '1', limit = '12', sort = 'hot', weaponCategory, weaponName, search } = req.query;
const pageNum = Math.max(1, parseInt(String(page)) || 1); const pageNum = Math.max(1, parseInt(String(page)) || 1);
const limitNum = Math.min(100, Math.max(1, parseInt(String(limit)) || 20)); const limitNum = Math.min(100, Math.max(1, parseInt(String(limit)) || 12));
const skip = (pageNum - 1) * limitNum;
const where: any = { userId: req.user!.userId };
if (type && TARGET_TYPES.includes(type as any)) {
where.targetType = type;
}
// 先查用户的收藏记录
const total = await prisma.favorite.count({ where: { userId: req.user!.userId } });
const favorites = await prisma.favorite.findMany({ const favorites = await prisma.favorite.findMany({
where, where: { userId: req.user!.userId },
orderBy: { createdAt: 'desc' }, orderBy: { createdAt: 'desc' },
skip, skip: (pageNum - 1) * limitNum,
take: Number(limit), take: limitNum,
}); });
res.json({ success: true, data: favorites }); // 加载关联的方案数据
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 },
});
} catch (error) { } catch (error) {
console.error('Get favorites error:', error); console.error('Get favorites error:', error);
res.status(500).json({ success: false, message: '获取失败' }); res.status(500).json({ success: false, message: '获取失败' });
@@ -39,66 +93,43 @@ router.get('/', async (req: Request, res: Response) => {
}); });
// ============================================ // ============================================
// 添加收藏 // 添加收藏 (对齐 POST /api/favorites)
// body: { schemeId, source: "烽火地带" }
// ============================================ // ============================================
const addFavoriteSchema = z.object({ const addFavoriteSchema = z.object({
targetType: z.enum(TARGET_TYPES), schemeId: z.string().or(z.number()),
targetId: z.string().uuid(), source: z.string().optional(),
}); });
router.post('/', async (req: Request, res: Response) => { router.post('/', async (req: Request, res: Response) => {
try { try {
const body = addFavoriteSchema.parse(req.body); const body = addFavoriteSchema.parse(req.body);
const targetType = sourceToType(body.source);
const targetId = String(body.schemeId);
// 检查是否已收藏
const existing = await prisma.favorite.findFirst({ const existing = await prisma.favorite.findFirst({
where: { where: { userId: req.user!.userId, targetType, targetId },
userId: req.user!.userId,
targetType: body.targetType,
targetId: body.targetId,
},
}); });
if (existing) { if (existing) {
return res.status(400).json({ success: false, message: '已收藏' }); return res.json({ success: true, alreadyFavorited: true });
} }
// 验证目标是否存在 // 验证目标存在
let targetExists = false; let targetExists = false;
if (body.targetType === 'SCHEME') { if (targetType === 'SCHEME') {
targetExists = !!(await prisma.scheme.findFirst({ targetExists = !!(await prisma.scheme.findFirst({ where: { id: targetId, status: 'PUBLISHED' } }));
where: { id: body.targetId, status: 'PUBLISHED' }, } else {
})); targetExists = !!(await prisma.schemeAob.findFirst({ where: { id: 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) { if (!targetExists) {
return res.status(404).json({ success: false, message: '目标不存在' }); return res.status(404).json({ success: false, message: '方案不存在' });
} }
// 创建收藏
await prisma.favorite.create({ await prisma.favorite.create({
data: { data: { userId: req.user!.userId, targetType, targetId },
userId: req.user!.userId,
targetType: body.targetType,
targetId: body.targetId,
},
}); });
// 更新用户收藏数 res.json({ success: true, alreadyFavorited: false });
await prisma.user.update({
where: { id: req.user!.userId },
data: { favoritesCount: { increment: 1 } },
});
res.json({ success: true, message: '收藏成功' });
} catch (error) { } catch (error) {
console.error('Add favorite error:', error); console.error('Add favorite error:', error);
res.status(500).json({ success: false, message: '收藏失败' }); res.status(500).json({ success: false, message: '收藏失败' });
@@ -106,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 { try {
const { id } = req.params; const { schemeId } = req.params;
const source = req.query.source as string | undefined;
const targetType = sourceToType(source);
const favorite = await prisma.favorite.findUnique({ const favorite = await prisma.favorite.findFirst({
where: { id }, where: { userId: req.user!.userId, targetType, targetId: schemeId },
}); });
if (!favorite) {
if (!favorite || favorite.userId !== req.user!.userId) {
return res.status(404).json({ success: false, message: '收藏不存在' }); return res.status(404).json({ success: false, message: '收藏不存在' });
} }
await prisma.favorite.delete({ where: { id } }); await prisma.favorite.delete({ where: { id: favorite.id } });
// 更新用户收藏数 res.json({ success: true });
await prisma.user.update({
where: { id: req.user!.userId },
data: { favoritesCount: { decrement: 1 } },
});
res.json({ success: true, message: '已取消收藏' });
} catch (error) { } catch (error) {
console.error('Remove favorite error:', error); console.error('Remove favorite error:', error);
res.status(500).json({ success: false, message: '取消收藏失败' }); res.status(500).json({ success: false, message: '取消收藏失败' });
@@ -136,29 +162,21 @@ router.delete('/:id', async (req: Request, res: Response) => {
}); });
// ============================================ // ============================================
// 检查是否已收藏 // 检查收藏状态 (对齐 GET /api/favorites/check?schemeId=xx&source=xx)
// ============================================ // ============================================
router.get('/check', async (req: Request, res: Response) => { router.get('/check', async (req: Request, res: Response) => {
try { try {
const { targetType, targetId } = req.query; const { schemeId, source } = req.query;
if (!schemeId) {
if (!targetType || !targetId) {
return res.status(400).json({ success: false, message: '参数缺失' }); return res.status(400).json({ success: false, message: '参数缺失' });
} }
const targetType = sourceToType(source as string | undefined);
const favorite = await prisma.favorite.findFirst({ const favorite = await prisma.favorite.findFirst({
where: { where: { userId: req.user!.userId, targetType, targetId: String(schemeId) },
userId: req.user!.userId,
targetType: String(targetType),
targetId: String(targetId),
},
}); });
res.json({ res.json({ isFavorited: !!favorite });
success: true,
isFavorited: !!favorite,
favoriteId: favorite?.id || null,
});
} catch (error) { } catch (error) {
console.error('Check favorite error:', error); console.error('Check favorite error:', error);
res.status(500).json({ success: false, message: '检查失败' }); res.status(500).json({ success: false, message: '检查失败' });

View File

@@ -1,67 +1,98 @@
import { Router, Request, Response } from 'express'; import { Router, Request, Response } from 'express';
import { z } from 'zod'; import { z } from 'zod';
import { authMiddleware, optionalAuth } from '../middleware/auth'; import { authMiddleware, optionalAuth } from '../middleware/auth';
import { encrypt } from '../utils/encryption'; import { encrypt, encryptResponse } from '../utils/encryption';
import { prisma } from '../utils/prisma'; import { prisma } from '../utils/prisma';
const router = Router(); const router = Router();
// ---- 响应 shape 对齐 maqt.top 原版 ----
function formatScheme(s: any) {
return {
id: parseInt(s.id, 36) || String(s.id).split('-')[0] || 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,
};
}
// ============================================ // ============================================
// 获取方案列表 // 获取方案列表
// ============================================ // ============================================
router.get('/', optionalAuth, async (req: Request, res: Response) => { router.get('/', optionalAuth, async (req: Request, res: Response) => {
try { try {
const { const {
page = 1, page = '1',
limit = 20, limit = '12',
weapon, weaponCategory,
category, weaponName,
sort = 'newest', minPrice,
maxPrice,
search,
sort = 'hot',
} = req.query; } = req.query;
const pageNum = Math.max(1, parseInt(String(page)) || 1); const pageNum = Math.max(1, parseInt(String(page)) || 1);
const limitNum = Math.min(100, Math.max(1, parseInt(String(limit)) || 20)); const limitNum = Math.min(100, Math.max(1, parseInt(String(limit)) || 12));
const skip = (pageNum - 1) * limitNum; const skip = (pageNum - 1) * limitNum;
const where: any = { status: 'PUBLISHED' }; const where: any = { status: 'PUBLISHED' };
if (weapon) where.weaponName = { contains: String(weapon) }; if (weaponCategory) where.category = String(weaponCategory);
if (category) where.category = String(category); 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 = { createdAt: 'desc' }; let orderBy: any = { downloadsCount: 'desc' };
if (sort === 'popular') orderBy = { viewsCount: 'desc' }; if (sort === 'new') orderBy = { createdAt: 'desc' };
if (sort === 'downloads') orderBy = { downloadsCount: 'desc' };
const schemes = await prisma.scheme.findMany({ const schemes = await prisma.scheme.findMany({
where, where,
orderBy, orderBy,
skip, skip,
take: Number(limit), take: limitNum + 1, // fetch one extra to detect hasMore
select: { include: {
id: true, user: { select: { id: true, username: true, avatar: true } },
title: true,
description: true,
weaponName: true,
category: true,
price: true,
viewsCount: true,
downloadsCount: true,
likesCount: true,
isOfficial: true,
createdAt: true,
user: {
select: {
id: true,
username: true,
avatar: true,
},
},
}, },
}); });
res.json({ const hasMore = schemes.length > limitNum;
const data = schemes.slice(0, limitNum).map(formatScheme);
const payload = {
success: true, 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) { } catch (error) {
console.error('Get schemes error:', error); console.error('Get schemes error:', error);
res.status(500).json({ success: false, message: '获取失败' }); res.status(500).json({ success: false, message: '获取失败' });
@@ -79,56 +110,35 @@ router.get('/:id', optionalAuth, async (req: Request, res: Response) => {
where: { id }, where: { id },
include: { include: {
user: { user: {
select: { select: { id: true, username: true, avatar: true },
id: true,
username: true,
avatar: true,
},
}, },
}, },
}); });
if (!scheme) { if (!scheme || (scheme.status !== 'PUBLISHED' && scheme.userId !== req.user?.userId)) {
return res.status(404).json({ return res.status(404).json({ success: false, message: '方案不存在' });
success: false,
message: '方案不存在',
});
} }
if (scheme.status !== 'PUBLISHED' && scheme.userId !== req.user?.userId) {
return res.status(404).json({
success: false,
message: '方案不存在',
});
}
// 增加浏览量
await prisma.scheme.update({ await prisma.scheme.update({
where: { id }, where: { id },
data: { viewsCount: { increment: 1 } }, data: { viewsCount: { increment: 1 } },
}); });
// 检查是否收藏
let isFavorited = false; let isFavorited = false;
if (req.user) { if (req.user) {
const fav = await prisma.favorite.findFirst({ const fav = await prisma.favorite.findFirst({
where: { where: { userId: req.user.userId, targetType: 'SCHEME', targetId: id },
userId: req.user.userId,
targetType: 'SCHEME',
targetId: id,
},
}); });
isFavorited = !!fav; isFavorited = !!fav;
} }
// 加密方案内容
const encryptedContent = encrypt(scheme.schemeContent); const encryptedContent = encrypt(scheme.schemeContent);
res.json({ res.json({
success: true, success: true,
data: { data: {
...scheme, ...formatScheme(scheme),
schemeContent: encryptedContent, scheme_content: encryptedContent,
isFavorited, isFavorited,
}, },
}); });
@@ -142,51 +152,55 @@ router.get('/:id', optionalAuth, async (req: Request, res: Response) => {
// 创建方案 // 创建方案
// ============================================ // ============================================
const createSchemeSchema = z.object({ const createSchemeSchema = z.object({
title: z.string().min(1).max(100), description: z.string().min(1).max(50),
description: z.string().optional(),
weaponName: z.string().optional(),
category: z.string().optional(), category: z.string().optional(),
schemeContent: z.string().min(1), weaponName: z.string().optional(),
price: z.number().int().min(0).default(0), scheme: z.string().min(1),
gpuModel: z.string().optional(), price: z.union([z.number(), z.string()]).optional(),
driverVersion: z.string().optional(), tags: z.array(z.string()).optional().default([]),
appVersion: z.string().optional(),
}); });
router.post('/', authMiddleware, async (req: Request, res: Response) => { router.post('/', authMiddleware, async (req: Request, res: Response) => {
try { try {
const body = createSchemeSchema.parse(req.body); 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({ const scheme = await prisma.scheme.create({
data: { data: {
userId: req.user!.userId, userId,
title: body.title,
description: body.description, description: body.description,
weaponName: body.weaponName, weaponName: body.weaponName,
category: body.category, category: body.category,
schemeContent: body.schemeContent, schemeContent: body.scheme,
price: body.price, price: priceNum,
gpuModel: body.gpuModel,
driverVersion: body.driverVersion,
appVersion: body.appVersion,
status: 'PUBLISHED', status: 'PUBLISHED',
}, },
}); });
// 更新用户方案数
await prisma.user.update({ await prisma.user.update({
where: { id: req.user!.userId }, where: { id: userId },
data: { schemesCount: { increment: 1 } }, data: { schemesCount: { increment: 1 } },
}); });
res.json({ res.json({ success: true, message: '方案发布成功', data: formatScheme(scheme) });
success: true,
message: '方案创建成功',
data: scheme,
});
} catch (error) { } catch (error) {
console.error('Create scheme error:', error); console.error('Create scheme error:', error);
res.status(500).json({ success: false, message: '创建失败' }); res.status(500).json({ success: false, message: '发布失败' });
} }
}); });
@@ -196,89 +210,72 @@ router.post('/', authMiddleware, async (req: Request, res: Response) => {
router.delete('/:id', authMiddleware, async (req: Request, res: Response) => { router.delete('/:id', authMiddleware, async (req: Request, res: Response) => {
try { try {
const { id } = req.params; 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: '无权删除' });
const scheme = await prisma.scheme.findUnique({ await prisma.scheme.update({ where: { id }, data: { status: 'DELETED' } });
where: { id }, await prisma.user.update({ where: { id: req.user!.userId }, data: { schemesCount: { decrement: 1 } } });
select: { userId: true },
});
if (!scheme) { res.json({ success: true, message: '方案已删除' });
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) { } catch (error) {
console.error('Delete scheme error:', error); console.error('Delete scheme error:', error);
res.status(500).json({ success: false, message: '删除失败' }); 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 { try {
const { id } = req.params; const { id } = req.params;
const { source } = req.body; const scheme = await prisma.scheme.findUnique({ where: { id }, select: { id: true } });
if (!scheme) return res.status(404).json({ success: false, message: '方案不存在' });
const scheme = await prisma.scheme.findUnique({
where: { id },
select: { id: true, downloadsCount: true },
});
if (!scheme) {
return res.status(404).json({
success: false,
message: '方案不存在',
});
}
// 使用计数 +1增加 downloads 计数)
const updated = await prisma.scheme.update({ const updated = await prisma.scheme.update({
where: { id }, where: { id },
data: { downloadsCount: { increment: 1 } }, data: { downloadsCount: { increment: 1 } },
select: { downloadsCount: true }, select: { downloadsCount: true },
}); });
// 记录日志 if (req.user) {
await prisma.userLog.create({ await prisma.userLog.create({
data: { data: { userId: req.user.userId, action: 'SchemeUse', targetType: 'Scheme', targetId: id },
userId: req.user!.userId,
action: 'SchemeUse',
targetType: 'Scheme',
targetId: id,
},
}); });
}
res.json({ res.json({ success: true, message: '使用次数已记录', downloadsCount: updated.downloadsCount });
success: true,
downloadsCount: updated.downloadsCount,
});
} catch (error) { } catch (error) {
console.error('Scheme use error:', error); console.error('Scheme use error:', error);
res.status(500).json({ success: false, message: '记录失败' }); 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; export default router;

View File

@@ -1,53 +1,89 @@
import { Router, Request, Response } from 'express'; import { Router, Request, Response } from 'express';
import { z } from 'zod';
import { authMiddleware, optionalAuth } from '../middleware/auth'; import { authMiddleware, optionalAuth } from '../middleware/auth';
import { encrypt } from '../utils/encryption'; import { encrypt, encryptResponse } from '../utils/encryption';
import { prisma } from '../utils/prisma'; import { prisma } from '../utils/prisma';
const router = Router(); const router = Router();
function formatScheme(s: any) {
return {
id: parseInt(s.id, 36) || String(s.id).split('-')[0] || 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) => { router.get('/', optionalAuth, async (req: Request, res: Response) => {
try { try {
const { page = 1, limit = 20, weapon, category, sort = 'newest' } = req.query; const {
page = '1',
limit = '12',
weaponCategory,
weaponName,
search,
sort = 'hot',
} = req.query;
const pageNum = Math.max(1, parseInt(String(page)) || 1); const pageNum = Math.max(1, parseInt(String(page)) || 1);
const limitNum = Math.min(100, Math.max(1, parseInt(String(limit)) || 20)); const limitNum = Math.min(100, Math.max(1, parseInt(String(limit)) || 12));
const skip = (pageNum - 1) * limitNum; const skip = (pageNum - 1) * limitNum;
const where: any = { status: 'PUBLISHED' }; const where: any = { status: 'PUBLISHED' };
if (weapon) where.weaponName = { contains: String(weapon) }; if (weaponCategory) where.category = String(weaponCategory);
if (category) where.category = String(category); if (weaponName) where.weaponName = { contains: String(weaponName) };
if (search) where.description = { contains: String(search) };
let orderBy: any = { createdAt: 'desc' }; let orderBy: any = { downloadsCount: 'desc' };
if (sort === 'popular') orderBy = { viewsCount: 'desc' }; if (sort === 'new') orderBy = { createdAt: 'desc' };
if (sort === 'downloads') orderBy = { downloadsCount: 'desc' };
const schemes = await prisma.schemeAob.findMany({ const schemes = await prisma.schemeAob.findMany({
where, where,
orderBy, orderBy,
skip, skip,
take: Number(limit), take: limitNum + 1,
select: { include: {
id: true, user: { select: { id: true, username: true, avatar: true } },
title: true,
description: true,
weaponName: true,
category: true,
price: true,
viewsCount: true,
downloadsCount: true,
likesCount: true,
isOfficial: true,
createdAt: true,
user: {
select: { id: true, username: true, avatar: true },
},
}, },
}); });
res.json({ success: true, data: schemes }); 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) { } catch (error) {
console.error('Get schemes_aob error:', error); console.error('Get schemes_aob error:', error);
res.status(500).json({ success: false, message: '获取失败' }); res.status(500).json({ success: false, message: '获取失败' });
@@ -55,17 +91,14 @@ router.get('/', optionalAuth, async (req: Request, res: Response) => {
}); });
// ============================================ // ============================================
// 获取单个方案详情 // 获取单个方案
// ============================================ // ============================================
router.get('/:id', optionalAuth, async (req: Request, res: Response) => { router.get('/:id', optionalAuth, async (req: Request, res: Response) => {
try { try {
const { id } = req.params; const { id } = req.params;
const scheme = await prisma.schemeAob.findUnique({ const scheme = await prisma.schemeAob.findUnique({
where: { id }, where: { id },
include: { include: { user: { select: { id: true, username: true, avatar: true } } },
user: { select: { id: true, username: true, avatar: true } },
},
}); });
if (!scheme || (scheme.status !== 'PUBLISHED' && scheme.userId !== req.user?.userId)) { if (!scheme || (scheme.status !== 'PUBLISHED' && scheme.userId !== req.user?.userId)) {
@@ -77,11 +110,17 @@ router.get('/:id', optionalAuth, async (req: Request, res: Response) => {
data: { viewsCount: { increment: 1 } }, 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({ res.json({
success: true, success: true,
data: { ...scheme, schemeContent: encryptedContent }, data: { ...formatScheme(scheme), scheme_content: encrypt(scheme.schemeContent), isFavorited },
}); });
} catch (error) { } catch (error) {
console.error('Get scheme_aob error:', error); console.error('Get scheme_aob error:', error);
@@ -92,23 +131,24 @@ 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) => { router.post('/', authMiddleware, async (req: Request, res: Response) => {
try { try {
const { title, description, weaponName, category, schemeContent, price = 0 } = req.body; const body = createSchema.parse(req.body);
if (!title || !schemeContent) {
return res.status(400).json({ success: false, message: '标题和内容不能为空' });
}
const scheme = await prisma.schemeAob.create({ const scheme = await prisma.schemeAob.create({
data: { data: {
userId: req.user!.userId, userId: req.user!.userId,
title, description: body.description,
description, weaponName: body.weaponName,
weaponName, category: body.category,
category, schemeContent: body.scheme,
schemeContent,
price,
status: 'PUBLISHED', status: 'PUBLISHED',
}, },
}); });
@@ -118,10 +158,10 @@ router.post('/', authMiddleware, async (req: Request, res: Response) => {
data: { schemesCount: { increment: 1 } }, data: { schemesCount: { increment: 1 } },
}); });
res.json({ success: true, message: '方案创建成功', data: scheme }); res.json({ success: true, message: '方案发布成功', data: formatScheme(scheme) });
} catch (error) { } catch (error) {
console.error('Create scheme_aob error:', error); console.error('Create scheme_aob error:', error);
res.status(500).json({ success: false, message: '创建失败' }); res.status(500).json({ success: false, message: '发布失败' });
} }
}); });
@@ -131,29 +171,12 @@ router.post('/', authMiddleware, async (req: Request, res: Response) => {
router.delete('/:id', authMiddleware, async (req: Request, res: Response) => { router.delete('/:id', authMiddleware, async (req: Request, res: Response) => {
try { try {
const { id } = req.params; 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: '无权删除' });
const scheme = await prisma.schemeAob.findUnique({ await prisma.schemeAob.update({ where: { id }, data: { status: 'DELETED' } });
where: { id }, await prisma.user.update({ where: { id: req.user!.userId }, data: { schemesCount: { decrement: 1 } } });
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: '方案已删除' }); res.json({ success: true, message: '方案已删除' });
} catch (error) { } catch (error) {
@@ -162,4 +185,46 @@ router.delete('/:id', authMiddleware, async (req: Request, res: Response) => {
} }
}); });
// ============================================
// 记录使用
// ============================================
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; export default router;

View File

@@ -1,15 +1,10 @@
import crypto from 'crypto'; import crypto from 'crypto';
const ALGORITHM = 'aes-256-cbc'; const ALGORITHM = 'aes-256-cbc';
const KEY = (() => {
const raw = process.env.ENCRYPTION_KEY || '0123456789abcdef0123456789abcdef'; // 与前端对齐: SHA-256(KEY_STRING) → 32 bytes
const buf = Buffer.from(raw, 'utf-8'); const RAW_KEY = process.env.ENCRYPTION_KEY || 'maqt-delta-force-2024-secret-key-32';
if (buf.length !== 32) { const KEY = crypto.createHash('sha256').update(RAW_KEY).digest();
console.error(`ENCRYPTION_KEY must be exactly 32 bytes, got ${buf.length}`);
process.exit(1);
}
return buf;
})();
export interface EncryptedData { export interface EncryptedData {
encrypted: boolean; encrypted: boolean;
@@ -33,6 +28,18 @@ 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 解密 * AES 解密
*/ */