diff --git a/.gitignore b/.gitignore index dea7c1e..ab2b096 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ dist/ *.log .DS_Store .prisma/ +.venv/ diff --git a/API.md b/API.md new file mode 100644 index 0000000..fb2ca43 --- /dev/null +++ b/API.md @@ -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 ` | +| 加密算法 | 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 ` + +**响应(解密后):** +```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 ` | + +**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 ` | + +**Body:** +```json +{ + "schemeId": "方案ID", + "source": "烽火地带" +} +``` + +**响应:** +```json +{ + "success": true, + "alreadyFavorited": false +} +``` + +#### 7.2 取消收藏 + +``` +DELETE /api/favorites/{schemeId}?source={source} +``` + +`source`: `烽火地带` 或 `全面战场` + +**Headers:** `Authorization: Bearer ` + +**响应:** +```json +{ + "success": true +} +``` + +#### 7.3 检查收藏状态 + +``` +GET /api/favorites/check?schemeId={id}&source={source} +``` + +**Headers:** `Authorization: Bearer ` + +**响应:** +```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 +``` diff --git a/origet/api_spec.py b/origet/api_spec.py new file mode 100644 index 0000000..d7e9d10 --- /dev/null +++ b/origet/api_spec.py @@ -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, + }, +} diff --git a/origet/client.py b/origet/client.py new file mode 100644 index 0000000..967e6ad --- /dev/null +++ b/origet/client.py @@ -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") diff --git a/origet/decrypt.py b/origet/decrypt.py new file mode 100644 index 0000000..edac737 --- /dev/null +++ b/origet/decrypt.py @@ -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)) diff --git a/origet/requirements.txt b/origet/requirements.txt new file mode 100644 index 0000000..ded8e00 --- /dev/null +++ b/origet/requirements.txt @@ -0,0 +1,2 @@ +requests +pycryptodome diff --git a/origet/test_apis.py b/origet/test_apis.py new file mode 100644 index 0000000..79e96db --- /dev/null +++ b/origet/test_apis.py @@ -0,0 +1,120 @@ +""" +原版 maqt.top API 接口测试脚本 + +用法: + python test_apis.py + python test_apis.py --user --pass + python test_apis.py --user --pass --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() diff --git a/origet/test_local.py b/origet/test_local.py new file mode 100644 index 0000000..824d48a --- /dev/null +++ b/origet/test_local.py @@ -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() diff --git a/src/index.ts b/src/index.ts index d06f800..974b5a6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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: [] }); }); @@ -172,23 +172,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: [] }); + } }); // 分类下的武器 diff --git a/src/routes/favorites.ts b/src/routes/favorites.ts index 5931c31..1a6c9b1 100644 --- a/src/routes/favorites.ts +++ b/src/routes/favorites.ts @@ -7,31 +7,85 @@ const router = Router(); 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) => { 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 limitNum = Math.min(100, Math.max(1, parseInt(String(limit)) || 20)); - const skip = (pageNum - 1) * limitNum; - - const where: any = { userId: req.user!.userId }; - if (type && TARGET_TYPES.includes(type as any)) { - where.targetType = type; - } - + 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: '获取失败' }); @@ -39,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: '收藏失败' }); @@ -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 { - 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: '取消收藏失败' }); @@ -136,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; \ No newline at end of file +export default router; diff --git a/src/routes/schemes.ts b/src/routes/schemes.ts index 59c9735..916eaac 100644 --- a/src/routes/schemes.ts +++ b/src/routes/schemes.ts @@ -1,67 +1,98 @@ import { Router, Request, Response } from 'express'; 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(); +// ---- 响应 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) => { try { const { - page = 1, - limit = 20, - weapon, - category, - sort = 'newest', + page = '1', + limit = '12', + weaponCategory, + weaponName, + minPrice, + maxPrice, + 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)) || 20)); + 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: '获取失败' }); @@ -74,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, }, }); @@ -142,51 +152,55 @@ 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', }, }); - - // 更新用户方案数 + 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: '发布失败' }); } }); @@ -196,89 +210,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' }, - }); - - await prisma.user.update({ - where: { id: req.user!.userId }, - data: { schemesCount: { decrement: 1 } }, - }); - - 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; diff --git a/src/routes/schemesAob.ts b/src/routes/schemesAob.ts index f0fec94..7098ecd 100644 --- a/src/routes/schemesAob.ts +++ b/src/routes/schemesAob.ts @@ -1,53 +1,89 @@ import { Router, Request, Response } from 'express'; +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(); +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) => { 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 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 = { 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: '获取失败' }); @@ -55,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); @@ -92,36 +131,37 @@ 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', }, }); - + await prisma.user.update({ where: { id: req.user!.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_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) => { 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: '无权删除' }); - 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 } }, - }); + 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) { @@ -162,4 +185,46 @@ router.delete('/:id', authMiddleware, async (req: Request, res: Response) => { } }); -export default router; \ No newline at end of file +// ============================================ +// 记录使用 +// ============================================ +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; diff --git a/src/utils/encryption.ts b/src/utils/encryption.ts index 8b7c269..b0c06b0 100644 --- a/src/utils/encryption.ts +++ b/src/utils/encryption.ts @@ -1,15 +1,10 @@ import crypto from 'crypto'; const ALGORITHM = 'aes-256-cbc'; -const KEY = (() => { - const raw = process.env.ENCRYPTION_KEY || '0123456789abcdef0123456789abcdef'; - const buf = Buffer.from(raw, 'utf-8'); - if (buf.length !== 32) { - console.error(`ENCRYPTION_KEY must be exactly 32 bytes, got ${buf.length}`); - process.exit(1); - } - return buf; -})(); + +// 与前端对齐: 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; @@ -25,7 +20,7 @@ export function encrypt(text: string): EncryptedData { 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'), @@ -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 解密 */