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:
119
src/index.ts
119
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: [] });
|
||||
}
|
||||
});
|
||||
|
||||
// 分类下的武器
|
||||
|
||||
@@ -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;
|
||||
export default router;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
// ============================================
|
||||
// 记录使用
|
||||
// ============================================
|
||||
router.post('/:id/use', optionalAuth, async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const scheme = await prisma.schemeAob.findUnique({ where: { id }, select: { id: true } });
|
||||
if (!scheme) return res.status(404).json({ success: false, message: '方案不存在' });
|
||||
|
||||
await prisma.schemeAob.update({ where: { id }, data: { downloadsCount: { increment: 1 } } });
|
||||
|
||||
if (req.user) {
|
||||
await prisma.userLog.create({
|
||||
data: { userId: req.user.userId, action: 'SchemeAobUse', targetType: 'SchemeAob', targetId: id },
|
||||
});
|
||||
}
|
||||
|
||||
res.json({ success: true, message: '使用次数已记录' });
|
||||
} catch (error) {
|
||||
console.error('Scheme AOB use error:', error);
|
||||
res.status(500).json({ success: false, message: '记录失败' });
|
||||
}
|
||||
});
|
||||
|
||||
// 举报
|
||||
router.post('/:id/report', authMiddleware, async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const scheme = await prisma.schemeAob.findUnique({ where: { id }, select: { id: true } });
|
||||
if (!scheme) return res.status(404).json({ success: false, message: '方案不存在' });
|
||||
|
||||
await prisma.userLog.create({
|
||||
data: { userId: req.user!.userId, action: 'Report', targetType: 'SchemeAob', targetId: id },
|
||||
});
|
||||
|
||||
res.json({ success: true, message: '举报成功' });
|
||||
} catch (error) {
|
||||
console.error('Report scheme_aob error:', error);
|
||||
res.status(500).json({ success: false, message: '举报失败' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -1,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 解密
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user