285 lines
9.2 KiB
TypeScript
285 lines
9.2 KiB
TypeScript
import { Router, Request, Response } from 'express';
|
|
import { z } from 'zod';
|
|
import { authMiddleware, optionalAuth } from '../middleware/auth';
|
|
import { encrypt, encryptResponse } from '../utils/encryption';
|
|
import { prisma } from '../utils/prisma';
|
|
|
|
const router = Router();
|
|
|
|
// ---- 响应 shape 对齐 maqt.top 原版 ----
|
|
|
|
function formatScheme(s: any) {
|
|
return {
|
|
id: s.id,
|
|
user_id: s.userId,
|
|
description: s.description || '',
|
|
scheme_content: s.schemeContent || '',
|
|
category: s.category || '',
|
|
weapon_name: s.weaponName || '',
|
|
price: s.price ? `${s.price}W` : null,
|
|
tags: [],
|
|
likes: s.likesCount ?? null,
|
|
uses: s.downloadsCount || 0,
|
|
status: s.status === 'PUBLISHED' ? 'normal' : null,
|
|
comments: null,
|
|
shares: null,
|
|
created_at: s.createdAt?.toISOString?.() || s.createdAt,
|
|
updated_at: s.updatedAt?.toISOString?.() || s.updatedAt,
|
|
source: s.source ?? null,
|
|
total_historical_uses: s.downloadsCount || 0,
|
|
username: s.user?.username || s.username || '',
|
|
avatar: s.user?.avatar || s.avatar || 'https://tuku.maqt.top/i/2026/03/22/sv3fg9.png',
|
|
partner_type: null,
|
|
partner_level: null,
|
|
partner_badge: null,
|
|
partner_logo: null,
|
|
social_link: null,
|
|
};
|
|
}
|
|
|
|
// ============================================
|
|
// 获取方案列表
|
|
// ============================================
|
|
router.get('/', optionalAuth, async (req: Request, res: Response) => {
|
|
try {
|
|
const {
|
|
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)) || 12));
|
|
const skip = (pageNum - 1) * limitNum;
|
|
|
|
const where: any = { status: 'PUBLISHED' };
|
|
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: limitNum + 1, // fetch one extra to detect hasMore
|
|
include: {
|
|
user: { select: { id: true, username: true, avatar: true } },
|
|
},
|
|
});
|
|
|
|
const hasMore = schemes.length > limitNum;
|
|
const data = schemes.slice(0, limitNum).map(formatScheme);
|
|
|
|
const payload = {
|
|
success: true,
|
|
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: '获取失败' });
|
|
}
|
|
});
|
|
|
|
// ============================================
|
|
// 获取单个方案详情
|
|
// ============================================
|
|
router.get('/:id', optionalAuth, async (req: Request, res: Response) => {
|
|
try {
|
|
const { id } = req.params;
|
|
|
|
const scheme = await prisma.scheme.findUnique({
|
|
where: { id },
|
|
include: {
|
|
user: {
|
|
select: { id: true, username: true, avatar: true },
|
|
},
|
|
},
|
|
});
|
|
|
|
if (!scheme || (scheme.status !== 'PUBLISHED' && scheme.userId !== req.user?.userId)) {
|
|
return res.status(404).json({ success: false, message: '方案不存在' });
|
|
}
|
|
|
|
await prisma.scheme.update({
|
|
where: { id },
|
|
data: { viewsCount: { increment: 1 } },
|
|
});
|
|
|
|
let isFavorited = false;
|
|
if (req.user) {
|
|
const fav = await prisma.favorite.findFirst({
|
|
where: { userId: req.user.userId, targetType: 'SCHEME', targetId: id },
|
|
});
|
|
isFavorited = !!fav;
|
|
}
|
|
|
|
const encryptedContent = encrypt(scheme.schemeContent);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: {
|
|
...formatScheme(scheme),
|
|
scheme_content: encryptedContent,
|
|
isFavorited,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
console.error('Get scheme error:', error);
|
|
res.status(500).json({ success: false, message: '获取失败' });
|
|
}
|
|
});
|
|
|
|
// ============================================
|
|
// 创建方案
|
|
// ============================================
|
|
const createSchemeSchema = z.object({
|
|
description: z.string().min(1).max(50),
|
|
category: 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,
|
|
description: body.description,
|
|
weaponName: body.weaponName,
|
|
category: body.category,
|
|
schemeContent: body.scheme,
|
|
price: priceNum,
|
|
status: 'PUBLISHED',
|
|
},
|
|
include: {
|
|
user: { select: { id: true, username: true, avatar: true } },
|
|
},
|
|
});
|
|
|
|
await prisma.user.update({
|
|
where: { id: userId },
|
|
data: { schemesCount: { increment: 1 } },
|
|
});
|
|
|
|
res.json({ success: true, message: '方案发布成功', data: formatScheme(scheme) });
|
|
} catch (error) {
|
|
console.error('Create scheme error:', error);
|
|
res.status(500).json({ success: false, message: '发布失败' });
|
|
}
|
|
});
|
|
|
|
// ============================================
|
|
// 删除方案
|
|
// ============================================
|
|
router.delete('/:id', authMiddleware, async (req: Request, res: Response) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const scheme = await prisma.scheme.findUnique({ where: { id }, select: { userId: true } });
|
|
if (!scheme) return res.status(404).json({ success: false, message: '方案不存在' });
|
|
if (scheme.userId !== req.user!.userId) return res.status(403).json({ success: false, message: '无权删除' });
|
|
|
|
await prisma.scheme.update({ where: { id }, data: { status: 'DELETED' } });
|
|
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: '删除失败' });
|
|
}
|
|
});
|
|
|
|
// ============================================
|
|
// 记录使用 (POST /api/schemes/:id/use)
|
|
// ============================================
|
|
router.post('/:id/use', optionalAuth, async (req: Request, res: Response) => {
|
|
try {
|
|
const { id } = req.params;
|
|
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 },
|
|
});
|
|
|
|
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;
|