chore: mqsrv backend
This commit is contained in:
212
src/index.ts
Normal file
212
src/index.ts
Normal 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
114
src/middleware/auth.ts
Normal 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
269
src/routes/auth.ts
Normal 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
167
src/routes/favorites.ts
Normal 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
135
src/routes/filters.ts
Normal 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
278
src/routes/schemes.ts
Normal 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
122
src/routes/schemesAob.ts
Normal 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
337
src/routes/user.ts
Normal 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
112
src/routes/vip.ts
Normal 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
64
src/utils/encryption.ts
Normal 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
47
src/utils/jwt.ts
Normal 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];
|
||||
}
|
||||
Reference in New Issue
Block a user