chore: mqsrv backend

This commit is contained in:
Chen Gu
2026-05-09 00:52:04 +08:00
commit b84f111e8f
21 changed files with 4593 additions and 0 deletions

212
src/index.ts Normal file
View File

@@ -0,0 +1,212 @@
import express, { Request, Response } from 'express';
import cors from 'cors';
import helmet from 'helmet';
import rateLimit from 'express-rate-limit';
import dotenv from 'dotenv';
dotenv.config();
import authRoutes from './routes/auth';
import userRoutes from './routes/user';
import schemeRoutes from './routes/schemes';
import schemeAobRoutes from './routes/schemesAob';
import filterRoutes from './routes/filters';
import vipRoutes from './routes/vip';
import favoriteRoutes from './routes/favorites';
const app = express();
const PORT = process.env.PORT || 3001;
// ============================================
// 中间件
// ============================================
// 安全头 - 开发模式禁用 CSP
app.use(helmet({
contentSecurityPolicy: false,
crossOriginEmbedderPolicy: false,
}));
// CORS
app.use(cors({
origin: true, // 允许所有来源(开发模式)
credentials: true,
}));
// JSON 解析
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));
// 速率限制
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 分钟
max: 100, // 每个 IP 最多 100 次请求
message: { success: false, message: '请求过于频繁,请稍后再试' },
});
app.use('/api/', limiter);
// 登录接口更严格的限制
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 10,
message: { success: false, message: '登录尝试过于频繁,请稍后再试' },
});
app.use('/api/login', loginLimiter);
app.use('/api/register', loginLimiter);
// ============================================
// 路由
// ============================================
app.get('/api/activity/ping', (req, res) => {
res.json({ success: true, message: 'pong', timestamp: Date.now() });
});
app.use('/api', authRoutes);
app.use('/api/user', userRoutes);
app.use('/api/schemes', schemeRoutes);
app.use('/api/schemes_aob', schemeAobRoutes);
app.use('/api/filter-shares', filterRoutes);
app.use('/api', vipRoutes);
// 收藏计数(放在 favorites 路由之前,避免 auth 中间件拦截)
app.get('/api/favorites/count', async (req: Request, res: Response) => {
try {
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
const count = await prisma.favorite.count();
res.json({ success: true, count });
} catch (e) {
res.json({ success: true, count: 0 });
}
});
app.use('/api/favorites', favoriteRoutes);
// ============================================
// 缺失的存根接口(防止前端 404
// ============================================
// 二维码
app.get('/api/qrcode/public/current', (req, res) => {
res.json({ success: true, data: { qrcode: null, url: '' } });
});
// 弹窗
app.get('/api/popups/', (req, res) => {
res.json({ success: true, data: [] });
});
// 售后教程弹窗
app.get('/api/aftersale-tutorial-popup', (req, res) => {
res.json({ success: true, data: { shown: false } });
});
// 软件版本广告空数据dock 无广告项)
app.get('/api/software-version-ad', (req, res) => {
res.json({ success: true, data: { items: [] } });
});
// 游戏地图密码缓存
app.get('/api/game/map-password/cached', (req, res) => {
res.json({ success: true, data: [] });
});
// 筛选分类
app.get('/api/filter-share/categories', (req, res) => {
res.json({ success: true, data: [] });
});
// ============================================
// 武器调谐窗 (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 });
});
// 武器列表(按分类筛选)
app.get('/api/weapons', (req, res) => {
res.json({ success: true, data: [] });
});
// 分类下的武器
app.get('/api/category/:code', (req, res) => {
res.json({ success: true, data: [] });
});
// 广告列表
app.get('/api/adverts/list', (req, res) => {
res.json({ success: true, data: [
{
id: 1,
title: '码枪堂2.0 新版发布',
description: '全新界面,更多功能,一键优化三角洲行动游戏体验!',
author: '码枪堂官方',
avatar: null,
image_url: '',
link_url: 'https://wwamt.lanzout.com/b00odpq4wb',
isAdvert: true,
isVip: true,
shareTime: new Date().toISOString(),
}
] });
});
// 广告点击
app.post('/api/adverts/:id/click', (req, res) => {
res.json({ success: true, message: 'ok' });
});
// 头像列表
app.get('/api/avatars', (req, res) => {
res.json({ success: true, data: [] });
});
// ============================================
// 错误处理
// ============================================
app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
console.error('Error:', err);
if (err.name === 'UnauthorizedError') {
return res.status(401).json({ success: false, message: '访问令牌无效', code: 'INVALID_TOKEN' });
}
if (err.name === 'ZodError') {
return res.status(400).json({ success: false, message: '参数验证失败', errors: err.errors });
}
res.status(500).json({ success: false, message: '服务器内部错误' });
});
// 404
app.use((req, res) => {
res.status(404).json({ success: false, message: '接口不存在' });
});
// ============================================
// 启动
// ============================================
app.listen(PORT, () => {
console.log(`🚀 码枪堂 API 运行在 http://localhost:${PORT}`);
console.log(`📋 可用接口:`);
console.log(` POST /api/login`);
console.log(` POST /api/register`);
console.log(` POST /api/activate-vip`);
console.log(` GET /api/session-status`);
console.log(` GET /api/vip-status`);
});
export default app;

114
src/middleware/auth.ts Normal file
View File

@@ -0,0 +1,114 @@
import { Request, Response, NextFunction } from 'express';
import { verifyToken, extractToken } from '../utils/jwt';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
// 扩展 Request 类型
declare global {
namespace Express {
interface Request {
user?: {
userId: string;
username: string;
email: string;
isVip: boolean;
vipLevel: number;
};
}
}
}
/**
* 认证中间件
*/
export async function authMiddleware(req: Request, res: Response, next: NextFunction) {
try {
const token = extractToken(req.headers.authorization);
if (!token) {
return res.status(401).json({
success: false,
message: '访问令牌缺失,请先登录',
code: 'NO_TOKEN',
});
}
const payload = verifyToken(token);
if (!payload) {
return res.status(401).json({
success: false,
message: '访问令牌无效或已过期',
code: 'INVALID_TOKEN',
});
}
// 检查用户是否存在且未被删除
const user = await prisma.user.findUnique({
where: { id: payload.userId },
select: { id: true, deletedAt: true },
});
if (!user || user.deletedAt) {
return res.status(401).json({
success: false,
message: '用户不存在或已被禁用',
code: 'USER_NOT_FOUND',
});
}
// 将用户信息附加到请求对象
req.user = payload;
next();
} catch (error) {
console.error('Auth middleware error:', error);
return res.status(500).json({
success: false,
message: '认证服务异常',
});
}
}
/**
* 可选认证中间件(不强制要求登录)
*/
export async function optionalAuth(req: Request, res: Response, next: NextFunction) {
try {
const token = extractToken(req.headers.authorization);
if (token) {
const payload = verifyToken(token);
if (payload) {
req.user = payload;
}
}
next();
} catch (error) {
next();
}
}
/**
* VIP 认证中间件(要求 VIP 用户)
*/
export async function vipMiddleware(req: Request, res: Response, next: NextFunction) {
if (!req.user) {
return res.status(401).json({
success: false,
message: '请先登录',
code: 'NO_TOKEN',
});
}
if (!req.user.isVip) {
return res.status(403).json({
success: false,
message: '此功能仅限 VIP 用户使用',
code: 'VIP_REQUIRED',
});
}
next();
}

269
src/routes/auth.ts Normal file
View File

@@ -0,0 +1,269 @@
import { Router, Request, Response } from 'express';
import bcrypt from 'bcryptjs';
import { PrismaClient } from '@prisma/client';
import { z } from 'zod';
import { generateToken } from '../utils/jwt';
import { authMiddleware } from '../middleware/auth';
const router = Router();
const prisma = new PrismaClient();
// ============================================
// 注册
// ============================================
const registerSchema = z.object({
username: z.string().min(3).max(50),
password: z.string().min(6).max(100),
email: z.string().email(),
installId: z.string().optional(),
deviceHash: z.string().optional(),
});
router.post('/register', async (req: Request, res: Response) => {
try {
const body = registerSchema.parse(req.body);
// 检查用户名是否已存在
const existingUsername = await prisma.user.findUnique({
where: { username: body.username },
});
if (existingUsername) {
return res.status(400).json({
success: false,
message: '用户名已被使用',
});
}
// 检查邮箱是否已存在
const existingEmail = await prisma.user.findUnique({
where: { email: body.email },
});
if (existingEmail) {
return res.status(400).json({
success: false,
message: '邮箱已被注册',
});
}
// 密码哈希
const passwordHash = await bcrypt.hash(body.password, 10);
// 创建用户
const user = await prisma.user.create({
data: {
username: body.username,
email: body.email,
passwordHash,
installId: body.installId,
deviceHash: body.deviceHash,
},
});
// 生成 Token
const token = generateToken({
userId: user.id,
username: user.username,
email: user.email,
isVip: user.isVip,
vipLevel: user.vipLevel,
});
// 记录日志
await prisma.userLog.create({
data: {
userId: user.id,
action: 'REGISTER',
installId: body.installId,
deviceHash: body.deviceHash,
ipAddress: req.ip,
},
});
res.json({
success: true,
token,
user: {
id: user.id,
username: user.username,
email: user.email,
avatar: user.avatar,
isVip: user.isVip,
vipExpireAt: user.vipExpireAt,
vipLevel: user.vipLevel,
},
});
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({
success: false,
message: '参数验证失败',
errors: error.errors,
});
}
console.error('Register error:', error);
res.status(500).json({ success: false, message: '注册失败' });
}
});
// ============================================
// 登录
// ============================================
const loginSchema = z.object({
username: z.string(), // 可以是用户名或邮箱
password: z.string(),
installId: z.string().optional(),
deviceHash: z.string().optional(),
platform: z.string().optional(),
osVersion: z.string().optional(),
appVersion: z.string().optional(),
});
router.post('/login', async (req: Request, res: Response) => {
try {
const body = loginSchema.parse(req.body);
// 查找用户(支持用户名或邮箱登录)
const user = await prisma.user.findFirst({
where: {
OR: [
{ username: body.username },
{ email: body.username },
],
deletedAt: null,
},
});
if (!user) {
return res.status(401).json({
success: false,
message: '用户名或密码错误',
});
}
// 验证密码
const validPassword = await bcrypt.compare(body.password, user.passwordHash);
if (!validPassword) {
return res.status(401).json({
success: false,
message: '用户名或密码错误',
});
}
// 更新最后登录时间和设备信息
await prisma.user.update({
where: { id: user.id },
data: {
lastLoginAt: new Date(),
installId: body.installId || user.installId,
deviceHash: body.deviceHash || user.deviceHash,
},
});
// 生成 Token
const token = generateToken({
userId: user.id,
username: user.username,
email: user.email,
isVip: user.isVip,
vipLevel: user.vipLevel,
});
// 记录日志
await prisma.userLog.create({
data: {
userId: user.id,
action: 'LOGIN',
installId: body.installId,
deviceHash: body.deviceHash,
ipAddress: req.ip,
},
});
res.json({
success: true,
token,
user: {
id: user.id,
username: user.username,
email: user.email,
avatar: user.avatar,
isVip: user.isVip,
vipExpireAt: user.vipExpireAt,
vipLevel: user.vipLevel,
},
});
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({
success: false,
message: '参数验证失败',
errors: error.errors,
});
}
console.error('Login error:', error);
res.status(500).json({ success: false, message: '登录失败' });
}
});
// ============================================
// 会话状态
// ============================================
router.get('/session-status', authMiddleware, async (req: Request, res: Response) => {
res.json({
success: true,
valid: true,
user: req.user,
});
});
// ============================================
// VIP 状态
// ============================================
router.get('/vip-status', authMiddleware, async (req: Request, res: Response) => {
try {
const user = await prisma.user.findUnique({
where: { id: req.user!.userId },
select: { isVip: true, vipExpireAt: true, vipLevel: true },
});
if (!user) {
return res.status(404).json({
success: false,
message: '用户不存在',
});
}
// 检查 VIP 是否过期
let isVip = user.isVip;
if (user.vipExpireAt && new Date() > user.vipExpireAt) {
isVip = false;
// 更新数据库
await prisma.user.update({
where: { id: req.user!.userId },
data: { isVip: false },
});
}
const daysRemaining = user.vipExpireAt
? Math.max(0, Math.ceil((user.vipExpireAt.getTime() - Date.now()) / (1000 * 60 * 60 * 24)))
: 0;
res.json({
success: true,
isVip,
vipExpireAt: user.vipExpireAt,
vipLevel: user.vipLevel,
daysRemaining,
});
} catch (error) {
console.error('VIP status error:', error);
res.status(500).json({ success: false, message: '获取 VIP 状态失败' });
}
});
export default router;

167
src/routes/favorites.ts Normal file
View File

@@ -0,0 +1,167 @@
import { Router, Request, Response } from 'express';
import { PrismaClient } from '@prisma/client';
import { z } from 'zod';
import { authMiddleware } from '../middleware/auth';
const router = Router();
const prisma = new PrismaClient();
router.use(authMiddleware);
const TARGET_TYPES = ['SCHEME', 'SCHEME_AOB', 'FILTER'] as const;
// ============================================
// 获取收藏列表
// ============================================
router.get('/', async (req: Request, res: Response) => {
try {
const { type, page = 1, limit = 20 } = req.query;
const skip = (Number(page) - 1) * Number(limit);
const where: any = { userId: req.user!.userId };
if (type && TARGET_TYPES.includes(type as any)) {
where.targetType = type;
}
const favorites = await prisma.favorite.findMany({
where,
orderBy: { createdAt: 'desc' },
skip,
take: Number(limit),
});
res.json({ success: true, data: favorites });
} catch (error) {
console.error('Get favorites error:', error);
res.status(500).json({ success: false, message: '获取失败' });
}
});
// ============================================
// 添加收藏
// ============================================
const addFavoriteSchema = z.object({
targetType: z.enum(TARGET_TYPES),
targetId: z.string().uuid(),
});
router.post('/', async (req: Request, res: Response) => {
try {
const body = addFavoriteSchema.parse(req.body);
// 检查是否已收藏
const existing = await prisma.favorite.findFirst({
where: {
userId: req.user!.userId,
targetType: body.targetType,
targetId: body.targetId,
},
});
if (existing) {
return res.status(400).json({ success: false, message: '已收藏' });
}
// 验证目标是否存在
let targetExists = false;
if (body.targetType === 'SCHEME') {
targetExists = !!(await prisma.scheme.findFirst({
where: { id: body.targetId, status: 'PUBLISHED' },
}));
} else if (body.targetType === 'SCHEME_AOB') {
targetExists = !!(await prisma.schemeAob.findFirst({
where: { id: body.targetId, status: 'PUBLISHED' },
}));
} else if (body.targetType === 'FILTER') {
targetExists = !!(await prisma.filterShare.findFirst({
where: { id: body.targetId, status: 'PUBLISHED' },
}));
}
if (!targetExists) {
return res.status(404).json({ success: false, message: '目标不存在' });
}
// 创建收藏
await prisma.favorite.create({
data: {
userId: req.user!.userId,
targetType: body.targetType,
targetId: body.targetId,
},
});
// 更新用户收藏数
await prisma.user.update({
where: { id: req.user!.userId },
data: { favoritesCount: { increment: 1 } },
});
res.json({ success: true, message: '收藏成功' });
} catch (error) {
console.error('Add favorite error:', error);
res.status(500).json({ success: false, message: '收藏失败' });
}
});
// ============================================
// 取消收藏
// ============================================
router.delete('/:id', async (req: Request, res: Response) => {
try {
const { id } = req.params;
const favorite = await prisma.favorite.findUnique({
where: { id },
});
if (!favorite || favorite.userId !== req.user!.userId) {
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: '已取消收藏' });
} catch (error) {
console.error('Remove favorite error:', error);
res.status(500).json({ success: false, message: '取消收藏失败' });
}
});
// ============================================
// 检查是否已收藏
// ============================================
router.get('/check', async (req: Request, res: Response) => {
try {
const { targetType, targetId } = req.query;
if (!targetType || !targetId) {
return res.status(400).json({ success: false, message: '参数缺失' });
}
const 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,
});
} catch (error) {
console.error('Check favorite error:', error);
res.status(500).json({ success: false, message: '检查失败' });
}
});
export default router;

135
src/routes/filters.ts Normal file
View File

@@ -0,0 +1,135 @@
import { Router, Request, Response } from 'express';
import { PrismaClient } from '@prisma/client';
import { authMiddleware, optionalAuth } from '../middleware/auth';
const router = Router();
const prisma = new PrismaClient();
// ============================================
// 获取滤镜列表
// ============================================
router.get('/', optionalAuth, async (req: Request, res: Response) => {
try {
const { page = 1, limit = 20, category } = req.query;
const skip = (Number(page) - 1) * Number(limit);
const where: any = { status: 'PUBLISHED' };
if (category) where.category = String(category);
const filters = await prisma.filterShare.findMany({
where,
orderBy: { createdAt: 'desc' },
skip,
take: Number(limit),
select: {
id: true,
title: true,
description: true,
category: true,
viewsCount: true,
likesCount: true,
createdAt: true,
user: { select: { id: true, username: true, avatar: true } },
},
});
res.json({ success: true, data: filters });
} catch (error) {
console.error('Get filter_shares error:', error);
res.status(500).json({ success: false, message: '获取失败' });
}
});
// ============================================
// 获取单个滤镜详情
// ============================================
router.get('/:id', optionalAuth, async (req: Request, res: Response) => {
try {
const { id } = req.params;
const filter = await prisma.filterShare.findUnique({
where: { id },
include: {
user: { select: { id: true, username: true, avatar: true } },
},
});
if (!filter || (filter.status !== 'PUBLISHED' && filter.userId !== req.user?.userId)) {
return res.status(404).json({ success: false, message: '滤镜不存在' });
}
await prisma.filterShare.update({
where: { id },
data: { viewsCount: { increment: 1 } },
});
res.json({ success: true, data: filter });
} catch (error) {
console.error('Get filter_share error:', error);
res.status(500).json({ success: false, message: '获取失败' });
}
});
// ============================================
// 创建滤镜
// ============================================
router.post('/', authMiddleware, async (req: Request, res: Response) => {
try {
const { title, description, category, filterContent, contentFormat = 'MQTS1' } = req.body;
if (!title || !filterContent) {
return res.status(400).json({ success: false, message: '标题和内容不能为空' });
}
const filter = await prisma.filterShare.create({
data: {
userId: req.user!.userId,
title,
description,
category,
filterContent,
contentFormat,
status: 'PUBLISHED',
},
});
res.json({ success: true, message: '滤镜创建成功', data: filter });
} catch (error) {
console.error('Create filter_share error:', error);
res.status(500).json({ success: false, message: '创建失败' });
}
});
// ============================================
// 删除滤镜
// ============================================
router.delete('/:id', authMiddleware, async (req: Request, res: Response) => {
try {
const { id } = req.params;
const filter = await prisma.filterShare.findUnique({
where: { id },
select: { userId: true },
});
if (!filter) {
return res.status(404).json({ success: false, message: '滤镜不存在' });
}
if (filter.userId !== req.user!.userId) {
return res.status(403).json({ success: false, message: '无权删除此滤镜' });
}
await prisma.filterShare.update({
where: { id },
data: { status: 'DELETED' },
});
res.json({ success: true, message: '滤镜已删除' });
} catch (error) {
console.error('Delete filter_share error:', error);
res.status(500).json({ success: false, message: '删除失败' });
}
});
export default router;

278
src/routes/schemes.ts Normal file
View File

@@ -0,0 +1,278 @@
import { Router, Request, Response } from 'express';
import { PrismaClient } from '@prisma/client';
import { z } from 'zod';
import { authMiddleware, optionalAuth } from '../middleware/auth';
import { encrypt } from '../utils/encryption';
const router = Router();
const prisma = new PrismaClient();
// ============================================
// 获取方案列表
// ============================================
router.get('/', optionalAuth, async (req: Request, res: Response) => {
try {
const {
page = 1,
limit = 20,
weapon,
category,
sort = 'newest',
} = req.query;
const skip = (Number(page) - 1) * Number(limit);
const 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' };
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,
},
},
},
});
res.json({
success: true,
data: schemes,
});
} 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) {
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,
},
});
isFavorited = !!fav;
}
// 加密方案内容
const encryptedContent = encrypt(scheme.schemeContent);
res.json({
success: true,
data: {
...scheme,
schemeContent: encryptedContent,
isFavorited,
},
});
} catch (error) {
console.error('Get scheme error:', error);
res.status(500).json({ success: false, message: '获取失败' });
}
});
// ============================================
// 创建方案
// ============================================
const createSchemeSchema = z.object({
title: z.string().min(1).max(100),
description: z.string().optional(),
weaponName: z.string().optional(),
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(),
});
router.post('/', authMiddleware, async (req: Request, res: Response) => {
try {
const body = createSchemeSchema.parse(req.body);
const scheme = await prisma.scheme.create({
data: {
userId: req.user!.userId,
title: body.title,
description: body.description,
weaponName: body.weaponName,
category: body.category,
schemeContent: body.schemeContent,
price: body.price,
gpuModel: body.gpuModel,
driverVersion: body.driverVersion,
appVersion: body.appVersion,
status: 'PUBLISHED',
},
});
// 更新用户方案数
await prisma.user.update({
where: { id: req.user!.userId },
data: { schemesCount: { increment: 1 } },
});
res.json({
success: true,
message: '方案创建成功',
data: 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' },
});
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) => {
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 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,
});
} catch (error) {
console.error('Scheme use error:', error);
res.status(500).json({ success: false, message: '记录失败' });
}
});
export default router;

122
src/routes/schemesAob.ts Normal file
View File

@@ -0,0 +1,122 @@
import { Router, Request, Response } from 'express';
import { PrismaClient } from '@prisma/client';
import { authMiddleware, optionalAuth } from '../middleware/auth';
import { encrypt } from '../utils/encryption';
const router = Router();
const prisma = new PrismaClient();
// ============================================
// 获取全面战场方案列表
// ============================================
router.get('/', optionalAuth, async (req: Request, res: Response) => {
try {
const { page = 1, limit = 20, weapon, category, sort = 'newest' } = req.query;
const skip = (Number(page) - 1) * Number(limit);
const 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' };
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 },
},
},
});
res.json({ success: true, data: schemes });
} catch (error) {
console.error('Get schemes_aob 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.schemeAob.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.schemeAob.update({
where: { id },
data: { viewsCount: { increment: 1 } },
});
const encryptedContent = encrypt(scheme.schemeContent);
res.json({
success: true,
data: { ...scheme, schemeContent: encryptedContent },
});
} catch (error) {
console.error('Get scheme_aob error:', error);
res.status(500).json({ success: false, message: '获取失败' });
}
});
// ============================================
// 创建方案
// ============================================
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 scheme = await prisma.schemeAob.create({
data: {
userId: req.user!.userId,
title,
description,
weaponName,
category,
schemeContent,
price,
status: 'PUBLISHED',
},
});
res.json({ success: true, message: '方案创建成功', data: scheme });
} catch (error) {
console.error('Create scheme_aob error:', error);
res.status(500).json({ success: false, message: '创建失败' });
}
});
export default router;

337
src/routes/user.ts Normal file
View File

@@ -0,0 +1,337 @@
import { Router, Request, Response } from 'express';
import { PrismaClient } from '@prisma/client';
import { z } from 'zod';
import { authMiddleware } from '../middleware/auth';
const router = Router();
const prisma = new PrismaClient();
// 所有路由都需要认证
router.use(authMiddleware);
// ============================================
// 更新用户名
// ============================================
const updateUsernameSchema = z.object({
userId: z.string().uuid(),
username: z.string().min(3).max(50),
});
router.put('/username', async (req: Request, res: Response) => {
try {
const body = updateUsernameSchema.parse(req.body);
// 验证用户 ID
if (body.userId !== req.user!.userId) {
return res.status(403).json({
success: false,
message: '无权修改其他用户信息',
});
}
// 检查用户名是否已存在
const existing = await prisma.user.findUnique({
where: { username: body.username },
});
if (existing) {
return res.status(400).json({
success: false,
message: '用户名已被使用',
});
}
// 检查修改次数限制
const limit = await prisma.userLog.count({
where: {
userId: req.user!.userId,
action: 'UPDATE_USERNAME',
},
});
if (limit >= 4) {
return res.status(400).json({
success: false,
message: '用户名修改次数已达上限',
});
}
// 更新用户名
await prisma.user.update({
where: { id: body.userId },
data: { username: body.username },
});
// 记录日志
await prisma.userLog.create({
data: {
userId: req.user!.userId,
action: 'UPDATE_USERNAME',
ipAddress: req.ip,
},
});
res.json({
success: true,
message: '用户名更新成功',
username: body.username,
});
} catch (error) {
console.error('Update username error:', error);
res.status(500).json({ success: false, message: '更新失败' });
}
});
// ============================================
// 更新邮箱
// ============================================
const updateEmailSchema = z.object({
userId: z.string().uuid(),
email: z.string().email(),
});
router.put('/email', async (req: Request, res: Response) => {
try {
const body = updateEmailSchema.parse(req.body);
if (body.userId !== req.user!.userId) {
return res.status(403).json({
success: false,
message: '无权修改其他用户信息',
});
}
// 检查邮箱是否已存在
const existing = await prisma.user.findUnique({
where: { email: body.email },
});
if (existing) {
return res.status(400).json({
success: false,
message: '邮箱已被使用',
});
}
await prisma.user.update({
where: { id: body.userId },
data: { email: body.email },
});
res.json({
success: true,
message: '邮箱更新成功',
email: body.email,
});
} catch (error) {
console.error('Update email error:', error);
res.status(500).json({ success: false, message: '更新失败' });
}
});
// ============================================
// 更新头像
// ============================================
const updateAvatarSchema = z.object({
userId: z.string().uuid(),
avatar: z.string().url(),
});
router.put('/avatar', async (req: Request, res: Response) => {
try {
const body = updateAvatarSchema.parse(req.body);
if (body.userId !== req.user!.userId) {
return res.status(403).json({
success: false,
message: '无权修改其他用户信息',
});
}
await prisma.user.update({
where: { id: body.userId },
data: { avatar: body.avatar },
});
res.json({
success: true,
message: '头像更新成功',
avatar: body.avatar,
});
} catch (error) {
console.error('Update avatar error:', error);
res.status(500).json({ success: false, message: '更新失败' });
}
});
// ============================================
// 获取用户统计
// ============================================
router.get('/stats/:userId', async (req: Request, res: Response) => {
try {
const { userId } = req.params;
const user = await prisma.user.findUnique({
where: { id: userId },
select: {
schemesCount: true,
favoritesCount: true,
isVip: true,
vipExpireAt: true,
createdAt: true,
},
});
if (!user) {
return res.status(404).json({
success: false,
message: '用户不存在',
});
}
res.json({
success: true,
data: {
schemesCount: user.schemesCount,
favoritesCount: user.favoritesCount,
isVip: user.isVip,
vipExpireAt: user.vipExpireAt,
memberSince: user.createdAt,
},
});
} catch (error) {
console.error('Get user stats error:', error);
res.status(500).json({ success: false, message: '获取失败' });
}
});
// ============================================
// 获取用户方案列表
// ============================================
router.get('/schemes/:userId', async (req: Request, res: Response) => {
try {
const { userId } = req.params;
const { type = 'schemes', page = 1, limit = 20 } = req.query;
const skip = (Number(page) - 1) * Number(limit);
let schemes;
if (type === 'aob') {
schemes = await prisma.schemeAob.findMany({
where: { userId, status: 'PUBLISHED' },
orderBy: { createdAt: 'desc' },
skip,
take: Number(limit),
select: {
id: true, title: true, weaponName: true, category: true,
viewsCount: true, downloadsCount: true, likesCount: true, createdAt: true,
},
});
} else {
schemes = await prisma.scheme.findMany({
where: { userId, status: 'PUBLISHED' },
orderBy: { createdAt: 'desc' },
skip,
take: Number(limit),
select: {
id: true, title: true, weaponName: true, category: true,
viewsCount: true, downloadsCount: true, likesCount: true, createdAt: true,
},
});
}
res.json({
success: true,
data: schemes,
});
} catch (error) {
console.error('Get user schemes error:', error);
res.status(500).json({ success: false, message: '获取失败' });
}
});
// ============================================
// 获取用户被收藏次数
// ============================================
router.get('/favorited-count/:userId', async (req: Request, res: Response) => {
try {
const { userId } = req.params;
const user = await prisma.user.findUnique({
where: { id: userId },
select: { favoritesCount: true },
});
if (!user) {
return res.status(404).json({
success: false,
message: '用户不存在',
});
}
res.json({
success: true,
favoritedCount: user.favoritesCount,
});
} catch (error) {
console.error('Get favorited count error:', error);
res.status(500).json({ success: false, message: '获取失败' });
}
});
// ============================================
// 获取用户功能限制信息
// ============================================
router.get('/limits/:userId', async (req: Request, res: Response) => {
try {
const { userId } = req.params;
// 验证用户是否存在
const user = await prisma.user.findUnique({
where: { id: userId },
select: {
id: true,
isVip: true,
vipExpireAt: true,
},
});
if (!user) {
return res.status(404).json({
success: false,
message: '用户不存在',
});
}
// 检查 VIP 是否过期
const now = new Date();
const isVipActive = user.isVip && user.vipExpireAt && user.vipExpireAt > now;
// 根据 VIP 状态返回不同的限制
const limits = isVipActive
? {
maxDailyUses: 500, // VIP: 每日最多使用500次
dailyUsesLeft: 500, // 剩余次数(简化处理,实际需要统计当日使用)
maxFavorites: 1000, // VIP: 最多收藏1000个
maxSchemes: 200, // VIP: 最多创建200个方案
canUploadIcc: true, // VIP: 可以上传 ICC
}
: {
maxDailyUses: 50, // 非VIP: 每日最多使用50次
dailyUsesLeft: 50, // 剩余次数
maxFavorites: 100, // 非VIP: 最多收藏100个
maxSchemes: 50, // 非VIP: 最多创建50个方案
canUploadIcc: false, // 非VIP: 不能上传 ICC
};
res.json({
success: true,
...limits,
});
} catch (error) {
console.error('Get user limits error:', error);
res.status(500).json({ success: false, message: '获取失败' });
}
});
export default router;

112
src/routes/vip.ts Normal file
View File

@@ -0,0 +1,112 @@
import { Router, Request, Response } from 'express';
import { PrismaClient } from '@prisma/client';
import { z } from 'zod';
import { authMiddleware } from '../middleware/auth';
const router = Router();
const prisma = new PrismaClient();
// ============================================
// VIP 卡密激活
// ============================================
const activateSchema = z.object({
cardKey: z.string().regex(/^VIP\d+-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$/),
});
router.post('/activate-vip', authMiddleware, async (req: Request, res: Response) => {
try {
const body = activateSchema.parse(req.body);
// 查找卡密
const card = await prisma.vipCard.findUnique({
where: { cardKey: body.cardKey },
});
if (!card) {
return res.status(400).json({
success: false,
message: '卡密不存在',
});
}
if (card.status !== 'UNUSED') {
return res.status(400).json({
success: false,
message: '卡密已被使用或已失效',
});
}
// 获取用户当前 VIP 状态
const user = await prisma.user.findUnique({
where: { id: req.user!.userId },
select: { isVip: true, vipExpireAt: true, vipLevel: true },
});
if (!user) {
return res.status(404).json({
success: false,
message: '用户不存在',
});
}
// 计算新的 VIP 过期时间
let newExpireAt: Date;
if (user.vipExpireAt && user.isVip && new Date() < user.vipExpireAt) {
// 已是 VIP延长时间
newExpireAt = new Date(user.vipExpireAt.getTime() + card.days * 24 * 60 * 60 * 1000);
} else {
// 非 VIP从现在开始计算
newExpireAt = new Date(Date.now() + card.days * 24 * 60 * 60 * 1000);
}
// 更新用户 VIP 状态
const updatedUser = await prisma.user.update({
where: { id: req.user!.userId },
data: {
isVip: true,
vipExpireAt: newExpireAt,
vipLevel: Math.max(user.vipLevel, 1),
},
});
// 更新卡密状态
await prisma.vipCard.update({
where: { id: card.id },
data: {
status: 'USED',
usedBy: req.user!.userId,
usedAt: new Date(),
},
});
// 记录日志
await prisma.userLog.create({
data: {
userId: req.user!.userId,
action: 'VIP_ACTIVATE',
targetId: card.id,
ipAddress: req.ip,
},
});
res.json({
success: true,
isVip: true,
vipExpireAt: newExpireAt,
vipLevel: updatedUser.vipLevel,
message: `激活成功VIP 有效期延长 ${card.days}`,
});
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({
success: false,
message: '卡密格式无效',
});
}
console.error('Activate VIP error:', error);
res.status(500).json({ success: false, message: '激活失败' });
}
});
export default router;

64
src/utils/encryption.ts Normal file
View File

@@ -0,0 +1,64 @@
import crypto from 'crypto';
const ALGORITHM = 'aes-256-cbc';
const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY || '0123456789abcdef0123456789abcdef';
export interface EncryptedData {
encrypted: boolean;
iv: string;
data: string;
}
/**
* AES 加密
*/
export function encrypt(text: string): EncryptedData {
const iv = crypto.randomBytes(16);
const key = Buffer.from(ENCRYPTION_KEY, 'utf-8');
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
let encrypted = cipher.update(text, 'utf-8', 'hex');
encrypted += cipher.final('hex');
return {
encrypted: true,
iv: iv.toString('hex'),
data: encrypted,
};
}
/**
* AES 解密
*/
export function decrypt(ivHex: string, dataHex: string): string {
const iv = Buffer.from(ivHex, 'hex');
const encryptedData = Buffer.from(dataHex, 'hex');
const key = Buffer.from(ENCRYPTION_KEY, 'utf-8');
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
let decrypted = decipher.update(encryptedData, undefined, 'utf-8');
decrypted += decipher.final('utf-8');
return decrypted;
}
/**
* 密码哈希
*/
export function hashPassword(password: string): string {
return crypto.createHash('sha256').update(password).digest('hex');
}
/**
* 生成随机字符串
*/
export function randomString(length: number = 32): string {
return crypto.randomBytes(length).toString('hex').slice(0, length);
}
/**
* 生成设备哈希
*/
export function generateDeviceHash(data: string): string {
return crypto.createHash('sha256').update(data).digest('hex').slice(0, 64);
}

47
src/utils/jwt.ts Normal file
View File

@@ -0,0 +1,47 @@
import jwt from 'jsonwebtoken';
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '7d';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const jwtSign = jwt.sign as any;
export interface JwtPayload {
userId: string;
username: string;
email: string;
isVip: boolean;
vipLevel: number;
}
/**
* 生成 JWT Token
*/
export function generateToken(payload: JwtPayload): string {
return jwtSign(payload, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN });
}
/**
* 验证 JWT Token
*/
export function verifyToken(token: string): JwtPayload | null {
try {
return jwt.verify(token, JWT_SECRET) as JwtPayload;
} catch (error) {
return null;
}
}
/**
* 从请求头提取 Token
*/
export function extractToken(authHeader?: string): string | null {
if (!authHeader) return null;
const parts = authHeader.split(' ');
if (parts.length !== 2 || parts[0] !== 'Bearer') {
return null;
}
return parts[1];
}