1225 行
40 KiB
TypeScript
1225 行
40 KiB
TypeScript
// 已读取AGENTS.md
|
||
import express, { Request, Response } from 'express';
|
||
import cors from 'cors';
|
||
import path from 'path';
|
||
import fs from 'fs';
|
||
import multer from 'multer';
|
||
import { ActivityService, ActivityPayType } from './core/activity/activity.service';
|
||
import { CompanionOrderService } from './core/order/companion-order.service';
|
||
import { AuthRoleManager, UserRoleFlag, WorkMode } from './core/auth/auth-role.manager';
|
||
import { FinanceCalculator } from './core/finance/finance.calculator';
|
||
import { ServiceCategoryManager, CompanionTrack, ExpenseBurdenType } from './core/companion/service-category';
|
||
import { ContentSecurityService } from './core/security/content-security.service';
|
||
import { initDatabase } from './db/mysql';
|
||
import { repo } from './db/repository';
|
||
|
||
const app = express();
|
||
const port = process.env.PORT || 3000;
|
||
|
||
app.use(cors());
|
||
app.use(express.json());
|
||
|
||
// 静态文件与对象存储目录配置
|
||
const uploadDir = path.resolve(__dirname, '../public/uploads');
|
||
if (!fs.existsSync(uploadDir)) {
|
||
fs.mkdirSync(uploadDir, { recursive: true });
|
||
}
|
||
app.use('/uploads', express.static(uploadDir));
|
||
|
||
// Multer 文件上传引擎配置
|
||
const storage = multer.diskStorage({
|
||
destination: (_req, _file, cb) => {
|
||
cb(null, uploadDir);
|
||
},
|
||
filename: (_req, file, cb) => {
|
||
const ext = path.extname(file.originalname).toLowerCase() || '.png';
|
||
const uniqueSuffix = `${Date.now()}_${Math.round(Math.random() * 1e9)}`;
|
||
cb(null, `img_${uniqueSuffix}${ext}`);
|
||
}
|
||
});
|
||
|
||
const upload = multer({
|
||
storage,
|
||
limits: { fileSize: 10 * 1024 * 1024 }, // 限制单个文件最大 10MB
|
||
fileFilter: (_req, file, cb) => {
|
||
const allowed = ['image/jpeg', 'image/png', 'image/webp', 'image/gif', 'image/jpg'];
|
||
if (allowed.includes(file.mimetype)) {
|
||
cb(null, true);
|
||
} else {
|
||
cb(new Error('仅支持上传 JPG、PNG、WEBP、GIF 格式的图片文件'));
|
||
}
|
||
}
|
||
});
|
||
|
||
// 实例化领域服务(作为核心算法与计算引擎)
|
||
export const activityService = new ActivityService();
|
||
export const orderService = new CompanionOrderService();
|
||
export const authRoleManager = new AuthRoleManager();
|
||
export const financeCalculator = new FinanceCalculator();
|
||
export const categoryManager = new ServiceCategoryManager();
|
||
export const contentSecurityService = new ContentSecurityService();
|
||
|
||
// 1. 基础探活与健康检查
|
||
app.get('/', (_req: Request, res: Response) => {
|
||
res.json({
|
||
status: 'ok',
|
||
service: 'peiwan-server',
|
||
storage: 'MySQL 8.0 (peiwan database)',
|
||
description: '陪玩与同城生活陪伴服务核心API',
|
||
version: '1.0.0',
|
||
time: new Date().toISOString()
|
||
});
|
||
});
|
||
|
||
app.get('/api/health', (_req: Request, res: Response) => {
|
||
res.json({ status: 'healthy', database: 'connected', uptime: process.uptime() });
|
||
});
|
||
|
||
// 1.1 管理员鉴权与登录接口 (供 PC 运营管理后台生产环境调用)
|
||
app.post('/api/user/login', (req: Request, res: Response) => {
|
||
const { username, password } = req.body || {};
|
||
if (!username || !password) {
|
||
return res.status(400).json({ code: 50000, message: '用户名和密码不能为空' });
|
||
}
|
||
// 生产环境支持默认管理员凭证
|
||
if (username === 'admin' && (password === 'admin' || password === 'admin123')) {
|
||
return res.json({
|
||
code: 20000,
|
||
data: {
|
||
token: `token_${Date.now()}_peiwan_admin`,
|
||
},
|
||
message: '登录成功'
|
||
});
|
||
}
|
||
return res.status(401).json({ code: 50000, message: '账号或密码错误(默认账号: admin,密码: admin)' });
|
||
});
|
||
|
||
app.all('/api/user/info', (_req: Request, res: Response) => {
|
||
res.json({
|
||
code: 20000,
|
||
data: {
|
||
name: '超级管理员',
|
||
avatar: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><circle cx="50" cy="50" r="50" fill="%23165DFF"/><text x="50" y="62" font-size="36" text-anchor="middle" fill="white" font-family="sans-serif">AD</text></svg>',
|
||
email: 'admin@peiwan.com',
|
||
job: 'super_admin',
|
||
jobName: '全权运营总监',
|
||
organization: 'Platform',
|
||
organizationName: '同城陪伴运营总部',
|
||
location: 'beijing',
|
||
locationName: '北京',
|
||
introduction: '具备平台最高运营、风控与财务审核权限',
|
||
personalWebsite: 'https://peiww.gynhc.com',
|
||
phone: '188****8888',
|
||
registrationDate: '2026-09-01 10:00:00',
|
||
accountId: '10001',
|
||
certification: 1,
|
||
role: 'admin',
|
||
roles: ['admin'],
|
||
}
|
||
});
|
||
});
|
||
|
||
app.post('/api/user/logout', (_req: Request, res: Response) => {
|
||
res.json({
|
||
code: 20000,
|
||
data: null,
|
||
message: '登出成功'
|
||
});
|
||
});
|
||
|
||
app.all('/api/user/menu', (_req: Request, res: Response) => {
|
||
res.json({
|
||
code: 20000,
|
||
data: []
|
||
});
|
||
});
|
||
|
||
// 1.2 个人中心与管理员辅助接口 (用户设置与用户信息页)
|
||
app.all('/api/user/certification', (_req: Request, res: Response) => {
|
||
res.json({
|
||
code: 20000,
|
||
data: {
|
||
enterpriseInfo: {
|
||
accountType: 1,
|
||
status: 1,
|
||
time: '2026-09-01 10:00:00',
|
||
legalPerson: '平台管理员',
|
||
certificateType: '统一社会信用代码',
|
||
authenticationNumber: '91110108MA00000000',
|
||
enterpriseName: '同城生活陪伴运营服务中心',
|
||
enterpriseCertificateType: '营业执照',
|
||
organizationCode: 'MA000000-0',
|
||
},
|
||
record: [
|
||
{
|
||
certificationType: 1,
|
||
certificationContent: '企业实名认证及运营资质核准',
|
||
status: 1,
|
||
time: '2026-09-01 10:00:00',
|
||
}
|
||
]
|
||
}
|
||
});
|
||
});
|
||
|
||
app.all('/api/user/my-project/list', (_req: Request, res: Response) => {
|
||
res.json({
|
||
code: 20000,
|
||
data: [
|
||
{
|
||
id: 1,
|
||
name: '同城线下生活陪伴运营大盘',
|
||
description: '涵盖线下陪吃、陪喝、陪唱与线上游戏陪伴的综合履约与合规监管系统。',
|
||
peopleNumber: 12,
|
||
contributors: [
|
||
{ name: '超级管理员', email: 'admin@peiwan.com', avatar: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><circle cx="50" cy="50" r="50" fill="%23165DFF"/><text x="50" y="62" font-size="36" text-anchor="middle" fill="white" font-family="sans-serif">AD</text></svg>' }
|
||
]
|
||
},
|
||
{
|
||
id: 2,
|
||
name: '同城公共商圈合规与风控拦截',
|
||
description: '高频违规场所自动识别与阻断引擎,保障线下组局公开与人身安全。',
|
||
peopleNumber: 6,
|
||
contributors: [
|
||
{ name: '风控专员', email: 'sec@peiwan.com', avatar: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><circle cx="50" cy="50" r="50" fill="%2300B42A"/><text x="50" y="62" font-size="36" text-anchor="middle" fill="white" font-family="sans-serif">FK</text></svg>' }
|
||
]
|
||
}
|
||
]
|
||
});
|
||
});
|
||
|
||
app.all('/api/user/my-team/list', (_req: Request, res: Response) => {
|
||
res.json({
|
||
code: 20000,
|
||
data: [
|
||
{
|
||
id: 1,
|
||
avatar: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><circle cx="50" cy="50" r="50" fill="%23165DFF"/><text x="50" y="62" font-size="36" text-anchor="middle" fill="white" font-family="sans-serif">OP</text></svg>',
|
||
name: '同城活动运营组',
|
||
peopleNumber: 8
|
||
},
|
||
{
|
||
id: 2,
|
||
avatar: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><circle cx="50" cy="50" r="50" fill="%23F53F3F"/><text x="50" y="62" font-size="36" text-anchor="middle" fill="white" font-family="sans-serif">FN</text></svg>',
|
||
name: '财务与结算中心',
|
||
peopleNumber: 4
|
||
}
|
||
]
|
||
});
|
||
});
|
||
|
||
app.all('/api/user/latest-activity', (_req: Request, res: Response) => {
|
||
res.json({
|
||
code: 20000,
|
||
data: [
|
||
{
|
||
id: 1,
|
||
title: '完成了苏苏学姐的微信商户打款审核',
|
||
description: '提现单号 TX20260928001,金额 ¥1250.00,已打款至微信商户零钱。',
|
||
avatar: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><circle cx="50" cy="50" r="50" fill="%2300B42A"/><text x="50" y="62" font-size="36" text-anchor="middle" fill="white" font-family="sans-serif">CW</text></svg>'
|
||
},
|
||
{
|
||
id: 2,
|
||
title: '处理了工体纯KTV争议仲裁单',
|
||
description: '对涉事大神迟到提前离场进行举证判定,款项已全额退款用户。',
|
||
avatar: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><circle cx="50" cy="50" r="50" fill="%23FF7D00"/><text x="50" y="62" font-size="36" text-anchor="middle" fill="white" font-family="sans-serif">ZC</text></svg>'
|
||
}
|
||
]
|
||
});
|
||
});
|
||
|
||
app.all('/api/user/save-info', (_req: Request, res: Response) => {
|
||
res.json({ code: 20000, data: null, message: '个人设置保存成功' });
|
||
});
|
||
|
||
// 真实文件与图片对象存储上传接口 (支持单图上传并持久化至 public/uploads)
|
||
const handleFileUpload = (req: Request, res: Response) => {
|
||
upload.single('file')(req, res, (err: any) => {
|
||
if (err) {
|
||
return res.status(400).json({
|
||
code: 40000,
|
||
message: err.message || '文件上传失败'
|
||
});
|
||
}
|
||
if (!req.file) {
|
||
return res.status(400).json({
|
||
code: 40000,
|
||
message: '未接收到有效文件,请使用 file 字段提交'
|
||
});
|
||
}
|
||
|
||
// 微信内容安全多媒体合规性检测 (mediaCheckAsync)
|
||
const mediaCheck = contentSecurityService.checkImage(
|
||
req.file.originalname,
|
||
req.file.mimetype,
|
||
req.file.size
|
||
);
|
||
if (!mediaCheck.pass) {
|
||
// 若拦截到非法文件特征,立即删除已写入的临时文件
|
||
try {
|
||
fs.unlinkSync(req.file.path);
|
||
} catch (e) {}
|
||
return res.status(400).json({
|
||
code: mediaCheck.code || 87014,
|
||
message: mediaCheck.message
|
||
});
|
||
}
|
||
|
||
const publicUrl = `/uploads/${req.file.filename}`;
|
||
res.json({
|
||
code: 20000,
|
||
data: {
|
||
url: publicUrl,
|
||
filename: req.file.filename,
|
||
size: req.file.size,
|
||
mimetype: req.file.mimetype
|
||
},
|
||
message: '图片上传成功并已持久化存储'
|
||
});
|
||
});
|
||
};
|
||
|
||
app.post('/api/upload', handleFileUpload);
|
||
app.post('/api/user/upload', handleFileUpload);
|
||
|
||
// 微信官方内容安全文本检测接口 (对接 security.msgSecCheck)
|
||
app.post('/api/security/msg-sec-check', (req: Request, res: Response) => {
|
||
const { content, openid } = req.body || {};
|
||
const checkResult = contentSecurityService.checkText(content, openid);
|
||
if (!checkResult.pass) {
|
||
return res.status(400).json({
|
||
code: checkResult.code,
|
||
data: {
|
||
suggest: checkResult.suggest,
|
||
label: checkResult.label
|
||
},
|
||
message: checkResult.message
|
||
});
|
||
}
|
||
res.json({
|
||
code: 20000,
|
||
data: {
|
||
suggest: checkResult.suggest
|
||
},
|
||
message: '内容合规通过'
|
||
});
|
||
});
|
||
|
||
// 1.3 顶部通知中心接口
|
||
app.all('/api/message/list', (_req: Request, res: Response) => {
|
||
res.json({
|
||
code: 20000,
|
||
data: [
|
||
{
|
||
id: 1,
|
||
type: 'notice',
|
||
title: '系统通知',
|
||
subTitle: 'MySQL 数据库就绪',
|
||
avatar: '',
|
||
content: '生产级 MySQL 数据库与 10 张核心业务表已完成线上平滑就绪。',
|
||
time: '刚刚',
|
||
},
|
||
{
|
||
id: 2,
|
||
type: 'todo',
|
||
title: '待办预警',
|
||
subTitle: '大神入驻待审核',
|
||
avatar: '',
|
||
content: '新注册大神可可奶芙、夜风沉醉提交了入驻资质申请,请及时审核。',
|
||
time: '10分钟前',
|
||
}
|
||
]
|
||
});
|
||
});
|
||
|
||
app.all('/api/message/read', (_req: Request, res: Response) => {
|
||
res.json({ code: 20000, data: true });
|
||
});
|
||
|
||
app.all('/api/chat/list', (_req: Request, res: Response) => {
|
||
res.json({ code: 20000, data: [] });
|
||
});
|
||
|
||
// 2. 运营后台统计大盘接口 (真实读取 MySQL)
|
||
app.get('/api/dashboard/stats', async (_req: Request, res: Response) => {
|
||
try {
|
||
const stats = await repo.getDashboardStats();
|
||
res.json({
|
||
code: 20000,
|
||
data: stats
|
||
});
|
||
} catch (err: any) {
|
||
res.status(500).json({ code: 50000, message: err.message });
|
||
}
|
||
});
|
||
|
||
// 2.1 综合运营待办与预警中心接口 (真实读取 MySQL 待办项)
|
||
app.get('/api/dashboard/todos', async (_req: Request, res: Response) => {
|
||
try {
|
||
const todos = await repo.getDashboardTodos();
|
||
res.json({
|
||
code: 20000,
|
||
data: todos
|
||
});
|
||
} catch (err: any) {
|
||
res.status(500).json({ code: 50000, message: err.message });
|
||
}
|
||
});
|
||
|
||
// 3. 活动与4位核销码相关接口 (MySQL 持久化)
|
||
app.get('/api/activities', async (_req: Request, res: Response) => {
|
||
try {
|
||
const acts = await repo.getActivities();
|
||
res.json({
|
||
code: 20000,
|
||
data: acts
|
||
});
|
||
} catch (err: any) {
|
||
res.status(500).json({ code: 50000, message: err.message });
|
||
}
|
||
});
|
||
|
||
app.get('/api/activities/:id', async (req: Request, res: Response) => {
|
||
try {
|
||
const act = await repo.getActivityById(String(req.params.id));
|
||
if (!act) {
|
||
return res.status(404).json({ code: 40400, message: '活动不存在' });
|
||
}
|
||
res.json({ code: 20000, data: act });
|
||
} catch (err: any) {
|
||
res.status(500).json({ code: 50000, message: err.message });
|
||
}
|
||
});
|
||
|
||
app.post('/api/activities', async (req: Request, res: Response) => {
|
||
try {
|
||
const { title, location } = req.body || {};
|
||
// 微信内容安全敏感词检测 (msgSecCheck)
|
||
const checkTitle = contentSecurityService.checkText(title);
|
||
if (!checkTitle.pass) {
|
||
return res.status(400).json({ code: checkTitle.code, message: checkTitle.message });
|
||
}
|
||
const checkLoc = contentSecurityService.checkText(location);
|
||
if (!checkLoc.pass) {
|
||
return res.status(400).json({ code: checkLoc.code, message: checkLoc.message });
|
||
}
|
||
|
||
const act = await repo.createActivity(req.body);
|
||
res.json({ code: 20000, data: act, message: '活动创建成功并已写入MySQL' });
|
||
} catch (err: any) {
|
||
res.status(400).json({ code: 40000, message: err.message });
|
||
}
|
||
});
|
||
|
||
// 报名并生成4位数字核销码 (落盘 MySQL)
|
||
app.post('/api/activities/:id/join', async (req: Request, res: Response) => {
|
||
try {
|
||
const actId = String(req.params.id);
|
||
const { userId, payAmountCents } = req.body;
|
||
const participant = await repo.joinActivity(actId, {
|
||
userId: Number(userId),
|
||
payAmountCents: Number(payAmountCents || 0)
|
||
});
|
||
res.json({
|
||
code: 20000,
|
||
data: participant,
|
||
message: '报名成功,现场出示4位核销码签到核销'
|
||
});
|
||
} catch (err: any) {
|
||
res.status(400).json({ code: 40000, message: err.message });
|
||
}
|
||
});
|
||
|
||
// 房主/管理员核销 4 位核销码 (防重落盘 MySQL)
|
||
app.post('/api/activities/:id/verify', async (req: Request, res: Response) => {
|
||
try {
|
||
const actId = String(req.params.id);
|
||
const { operatorId, verifyCode } = req.body;
|
||
const result = await repo.verifyCheckin(actId, String(verifyCode), Number(operatorId || 1001));
|
||
res.json({ code: 20000, data: result, message: '核销成功,已完成现场到场签到' });
|
||
} catch (err: any) {
|
||
res.status(400).json({ code: 40000, message: err.message });
|
||
}
|
||
});
|
||
|
||
// 通用4位核销码反查信息 (支持组局活动与同城线下订单)
|
||
app.get('/api/verify/query/:code', async (req: Request, res: Response) => {
|
||
try {
|
||
const info = await repo.queryCheckinCodeInfo(String(req.params.code));
|
||
res.json({ code: 20000, data: info });
|
||
} catch (err: any) {
|
||
res.status(400).json({ code: 40000, message: err.message });
|
||
}
|
||
});
|
||
|
||
// 通用4位核销码核销接口 (支持大神接单工作台一键核验打卡)
|
||
app.post('/api/verify/universal', async (req: Request, res: Response) => {
|
||
try {
|
||
const { verifyCode, operatorId } = req.body;
|
||
const result = await repo.universalVerifyCheckin(String(verifyCode), Number(operatorId || 1001));
|
||
res.json({
|
||
code: 20000,
|
||
data: result,
|
||
message: '核销成功,已完成现场签到并打卡'
|
||
});
|
||
} catch (err: any) {
|
||
res.status(400).json({ code: 40000, message: err.message });
|
||
}
|
||
});
|
||
|
||
// 房主手动改价与追加赏金 (落盘 MySQL 并记录流水)
|
||
app.post('/api/activities/:id/adjust-price', async (req: Request, res: Response) => {
|
||
try {
|
||
const actId = String(req.params.id);
|
||
const { operatorId, newPriceCents } = req.body;
|
||
const updated = await repo.adjustPrice(actId, Number(newPriceCents), Number(operatorId || 1001));
|
||
res.json({ code: 20000, data: updated, message: '出价调整成功,老成员同享调价差额' });
|
||
} catch (err: any) {
|
||
res.status(400).json({ code: 40000, message: err.message });
|
||
}
|
||
});
|
||
|
||
// 房主取消/解散组局并触发全额原路退款
|
||
app.post('/api/activities/:id/cancel', async (req: Request, res: Response) => {
|
||
try {
|
||
const actId = String(req.params.id);
|
||
const { hostId, reason } = req.body;
|
||
const result = await repo.cancelActivity(actId, Number(hostId || 1001), reason);
|
||
res.json({
|
||
code: 20000,
|
||
data: result,
|
||
message: `组局已成功解散${result.refundedCount > 0 ? `,已原路全额退款 ${result.refundedCount} 位搭子` : ''}`
|
||
});
|
||
} catch (err: any) {
|
||
res.status(400).json({ code: 40000, message: err.message });
|
||
}
|
||
});
|
||
|
||
// 报名成员主动退席与退款
|
||
app.post('/api/activities/:id/leave', async (req: Request, res: Response) => {
|
||
try {
|
||
const actId = String(req.params.id);
|
||
const { userId } = req.body;
|
||
const result = await repo.leaveActivity(actId, Number(userId));
|
||
res.json({
|
||
code: 20000,
|
||
data: result,
|
||
message: '已成功退出组局,报名款已原路退回您的钱包'
|
||
});
|
||
} catch (err: any) {
|
||
res.status(400).json({ code: 40000, message: err.message });
|
||
}
|
||
});
|
||
|
||
// 用户“我的组局”(发起的 + 报名的)
|
||
app.get('/api/users/:id/my-activities', async (req: Request, res: Response) => {
|
||
try {
|
||
const userId = Number(req.params.id);
|
||
const data = await repo.getMyActivities(userId);
|
||
res.json({ code: 20000, data });
|
||
} catch (err: any) {
|
||
res.status(500).json({ code: 50000, message: err.message });
|
||
}
|
||
});
|
||
|
||
// 4. 陪玩与大神管理接口 (落盘 MySQL)
|
||
app.get('/api/companions', async (_req: Request, res: Response) => {
|
||
try {
|
||
const companions = await repo.getCompanions();
|
||
res.json({
|
||
code: 20000,
|
||
data: companions
|
||
});
|
||
} catch (err: any) {
|
||
res.status(500).json({ code: 50000, message: err.message });
|
||
}
|
||
});
|
||
|
||
app.get('/api/companions/:id', async (req: Request, res: Response) => {
|
||
try {
|
||
const companion = await repo.getCompanionById(String(req.params.id));
|
||
if (!companion) {
|
||
return res.status(404).json({ code: 40400, message: '未找到指定陪伴师资料' });
|
||
}
|
||
res.json({
|
||
code: 20000,
|
||
data: companion
|
||
});
|
||
} catch (err: any) {
|
||
res.status(500).json({ code: 50000, message: err.message });
|
||
}
|
||
});
|
||
|
||
app.post('/api/companions/:id/audit', async (req: Request, res: Response) => {
|
||
try {
|
||
const companionId = Number(req.params.id);
|
||
const { action, reason, operatorRole } = req.body; // action: 'approve' | 'reject'
|
||
const headerRole = req.headers['x-user-role'];
|
||
const callerRole = operatorRole || headerRole || 'admin';
|
||
if (callerRole === 'user' || callerRole === 'expert' || callerRole === 'PLAYER' || callerRole === 'EXPERT') {
|
||
return res.status(403).json({ code: 40300, message: '权限不足:买家与大神不可越权执行审核' });
|
||
}
|
||
const result = await repo.auditCompanion(companionId, action, reason);
|
||
res.json({ code: 20000, data: result, message: result.message });
|
||
} catch (err: any) {
|
||
res.status(400).json({ code: 40000, message: err.message });
|
||
}
|
||
});
|
||
|
||
// 5. 订单与仲裁接口 (落盘 MySQL)
|
||
app.get('/api/orders', async (_req: Request, res: Response) => {
|
||
try {
|
||
const orders = await repo.getOrders();
|
||
res.json({
|
||
code: 20000,
|
||
data: orders
|
||
});
|
||
} catch (err: any) {
|
||
res.status(500).json({ code: 50000, message: err.message });
|
||
}
|
||
});
|
||
|
||
// 获取即时可抢订单池 (落盘 MySQL)
|
||
app.get('/api/orders/pool', async (_req: Request, res: Response) => {
|
||
try {
|
||
const list = await repo.getOrdersPool();
|
||
res.json({ code: 20000, data: list });
|
||
} catch (err: any) {
|
||
res.status(500).json({ code: 50000, message: err.message });
|
||
}
|
||
});
|
||
|
||
// 大神抢单/接单认领 (落盘 MySQL)
|
||
app.post('/api/orders/:id/claim', async (req: Request, res: Response) => {
|
||
try {
|
||
const orderId = String(req.params.id);
|
||
const { isCertified, operatorRole } = req.body;
|
||
if (isCertified === false || operatorRole === 'user' || operatorRole === 'PLAYER') {
|
||
return res.status(403).json({ code: 40300, message: '未认证用户无法接单' });
|
||
}
|
||
const order = await repo.claimOrder(orderId, req.body);
|
||
res.json({ code: 20000, data: order, message: '抢单成功,服务已锁定为履约中' });
|
||
} catch (err: any) {
|
||
res.status(400).json({ code: 40000, message: err.message });
|
||
}
|
||
});
|
||
|
||
// 大神今日接单数据汇总 (落盘 MySQL)
|
||
app.get('/api/expert/stats/:id', async (req: Request, res: Response) => {
|
||
try {
|
||
const stats = await repo.getExpertStats(String(req.params.id));
|
||
res.json({ code: 20000, data: stats });
|
||
} catch (err: any) {
|
||
res.status(500).json({ code: 50000, message: err.message });
|
||
}
|
||
});
|
||
|
||
|
||
// 首页运营 Banner 列表 (小程序端读取启用状态)
|
||
app.get('/api/banners', async (_req: Request, res: Response) => {
|
||
try {
|
||
const banners = await repo.getBanners();
|
||
res.json({ code: 20000, data: banners });
|
||
} catch (err: any) {
|
||
res.status(500).json({ code: 50000, message: err.message });
|
||
}
|
||
});
|
||
|
||
// 管理后台: 获取全部 Banner 列表 (含停用状态)
|
||
app.get('/api/admin/banners', async (_req: Request, res: Response) => {
|
||
try {
|
||
const banners = await repo.getAdminBanners();
|
||
res.json({ code: 20000, data: banners });
|
||
} catch (err: any) {
|
||
res.status(500).json({ code: 50000, message: err.message });
|
||
}
|
||
});
|
||
|
||
// 管理后台: 新增 Banner
|
||
app.post('/api/admin/banners', async (req: Request, res: Response) => {
|
||
try {
|
||
const { title, subtitle, tag, badge, badgeType, bg, image, link, sortOrder, status } = req.body;
|
||
if (!title || !title.trim()) {
|
||
return res.status(400).json({ code: 40000, message: 'Banner 标题不能为空' });
|
||
}
|
||
|
||
// 微信内容安全检测
|
||
const secCheck = contentSecurityService.checkText(title + ' ' + (subtitle || ''));
|
||
if (!secCheck.pass) {
|
||
return res.status(400).json({ code: secCheck.code, message: secCheck.message });
|
||
}
|
||
|
||
const banner = await repo.createBanner({
|
||
title: title.trim(),
|
||
subtitle: subtitle ? subtitle.trim() : '',
|
||
tag,
|
||
badge,
|
||
badgeType,
|
||
bg,
|
||
image,
|
||
link,
|
||
sortOrder: sortOrder !== undefined ? Number(sortOrder) : 0,
|
||
status: status !== undefined ? Number(status) : 1
|
||
});
|
||
|
||
res.json({ code: 20000, data: banner, message: '轮播图创建成功' });
|
||
} catch (err: any) {
|
||
res.status(400).json({ code: 40000, message: err.message });
|
||
}
|
||
});
|
||
|
||
// 管理后台: 修改 Banner
|
||
app.put('/api/admin/banners/:id', async (req: Request, res: Response) => {
|
||
try {
|
||
const bannerId = req.params.id;
|
||
const { title, subtitle, tag, badge, badgeType, bg, image, link, sortOrder, status } = req.body;
|
||
|
||
if (title) {
|
||
const secCheck = contentSecurityService.checkText(title + ' ' + (subtitle || ''));
|
||
if (!secCheck.pass) {
|
||
return res.status(400).json({ code: secCheck.code, message: secCheck.message });
|
||
}
|
||
}
|
||
|
||
const updated = await repo.updateBanner(bannerId, {
|
||
title,
|
||
subtitle,
|
||
tag,
|
||
badge,
|
||
badgeType,
|
||
bg,
|
||
image,
|
||
link,
|
||
sortOrder: sortOrder !== undefined ? Number(sortOrder) : undefined,
|
||
status: status !== undefined ? Number(status) : undefined
|
||
});
|
||
|
||
if (!updated) {
|
||
return res.status(404).json({ code: 40400, message: '轮播图不存在或已被删除' });
|
||
}
|
||
|
||
res.json({ code: 20000, data: updated, message: '轮播图修改成功' });
|
||
} catch (err: any) {
|
||
res.status(400).json({ code: 40000, message: err.message });
|
||
}
|
||
});
|
||
|
||
// 管理后台: 切换 Banner 上架/下线状态
|
||
app.post('/api/admin/banners/:id/status', async (req: Request, res: Response) => {
|
||
try {
|
||
const bannerId = req.params.id;
|
||
const { status } = req.body;
|
||
if (status === undefined || (status !== 0 && status !== 1)) {
|
||
return res.status(400).json({ code: 40000, message: '状态参数必须为 0 或 1' });
|
||
}
|
||
|
||
const updated = await repo.updateBannerStatus(bannerId, Number(status));
|
||
if (!updated) {
|
||
return res.status(404).json({ code: 40400, message: '轮播图不存在' });
|
||
}
|
||
|
||
res.json({ code: 20000, data: updated, message: status === 1 ? '轮播图已上架生效' : '轮播图已下线隐藏' });
|
||
} catch (err: any) {
|
||
res.status(400).json({ code: 40000, message: err.message });
|
||
}
|
||
});
|
||
|
||
// 管理后台: 删除 Banner
|
||
app.delete('/api/admin/banners/:id', async (req: Request, res: Response) => {
|
||
try {
|
||
const bannerId = req.params.id;
|
||
const ok = await repo.deleteBanner(bannerId);
|
||
if (!ok) {
|
||
return res.status(404).json({ code: 40400, message: '轮播图不存在或已被删除' });
|
||
}
|
||
|
||
res.json({ code: 20000, data: null, message: '轮播图已删除' });
|
||
} catch (err: any) {
|
||
res.status(400).json({ code: 40000, message: err.message });
|
||
}
|
||
});
|
||
|
||
// 创建新订单 (落盘 MySQL)
|
||
app.post('/api/orders', async (req: Request, res: Response) => {
|
||
try {
|
||
const order = await repo.createOrder(req.body);
|
||
res.json({
|
||
code: 20000,
|
||
data: order,
|
||
message: '订单已创建并已写入MySQL'
|
||
});
|
||
} catch (err: any) {
|
||
res.status(400).json({ code: 40000, message: err.message });
|
||
}
|
||
});
|
||
|
||
// 取消订单
|
||
app.post('/api/orders/:id/cancel', async (req: Request, res: Response) => {
|
||
try {
|
||
const order = await repo.updateOrderStatus(String(req.params.id), '已取消');
|
||
res.json({ code: 20000, data: order, message: '订单已取消' });
|
||
} catch (err: any) {
|
||
res.status(400).json({ code: 40000, message: err.message });
|
||
}
|
||
});
|
||
|
||
// 支付订单
|
||
app.post('/api/orders/:id/pay', async (req: Request, res: Response) => {
|
||
try {
|
||
const order = await repo.updateOrderStatus(String(req.params.id), '履约中');
|
||
res.json({ code: 20000, data: order, message: '支付成功,服务已进入履约中' });
|
||
} catch (err: any) {
|
||
res.status(400).json({ code: 40000, message: err.message });
|
||
}
|
||
});
|
||
|
||
// 确认完成验收
|
||
app.post('/api/orders/:id/complete', async (req: Request, res: Response) => {
|
||
try {
|
||
const order = await repo.updateOrderStatus(String(req.params.id), '已完成');
|
||
res.json({ code: 20000, data: order, message: '订单已确认完成' });
|
||
} catch (err: any) {
|
||
res.status(400).json({ code: 40000, message: err.message });
|
||
}
|
||
});
|
||
|
||
// 申请退款/售后
|
||
app.post('/api/orders/:id/refund', async (req: Request, res: Response) => {
|
||
try {
|
||
const order = await repo.updateOrderStatus(String(req.params.id), '争议仲裁中');
|
||
res.json({ code: 20000, data: order, message: '退款申诉已受理,进入争议仲裁流程' });
|
||
} catch (err: any) {
|
||
res.status(400).json({ code: 40000, message: err.message });
|
||
}
|
||
});
|
||
|
||
app.get('/api/arbitrations', async (_req: Request, res: Response) => {
|
||
try {
|
||
const arbitrations = await repo.getArbitrations();
|
||
res.json({
|
||
code: 20000,
|
||
data: arbitrations
|
||
});
|
||
} catch (err: any) {
|
||
res.status(500).json({ code: 50000, message: err.message });
|
||
}
|
||
});
|
||
|
||
app.post('/api/arbitrations/:id/handle', async (req: Request, res: Response) => {
|
||
try {
|
||
const arbId = String(req.params.id);
|
||
const { decision, operatorRole } = req.body; // 'refund_full' | 'reject' | 'deduct_credit'
|
||
const headerRole = req.headers['x-user-role'];
|
||
const callerRole = operatorRole || headerRole || 'admin';
|
||
if (callerRole === 'user' || callerRole === 'expert' || callerRole === 'PLAYER' || callerRole === 'EXPERT') {
|
||
return res.status(403).json({ code: 40300, message: '权限不足:买家与大神不可越权执行管理员仲裁' });
|
||
}
|
||
const result = await repo.handleArbitration(arbId, decision);
|
||
res.json({ code: 20000, data: result, message: `仲裁处理完毕: ${result.status}` });
|
||
} catch (err: any) {
|
||
res.status(400).json({ code: 40000, message: err.message });
|
||
}
|
||
});
|
||
|
||
// 6. 财务与分销管理接口 (落盘 MySQL)
|
||
app.get('/api/finance/records', async (_req: Request, res: Response) => {
|
||
try {
|
||
const records = await repo.getFinanceRecords();
|
||
res.json({
|
||
code: 20000,
|
||
data: records
|
||
});
|
||
} catch (err: any) {
|
||
res.status(500).json({ code: 50000, message: err.message });
|
||
}
|
||
});
|
||
|
||
app.get('/api/finance/withdrawals', async (_req: Request, res: Response) => {
|
||
try {
|
||
const withdrawals = await repo.getWithdrawals();
|
||
res.json({
|
||
code: 20000,
|
||
data: withdrawals
|
||
});
|
||
} catch (err: any) {
|
||
res.status(500).json({ code: 50000, message: err.message });
|
||
}
|
||
});
|
||
|
||
app.post('/api/finance/withdrawals/:id/review', async (req: Request, res: Response) => {
|
||
try {
|
||
const wdId = String(req.params.id);
|
||
const { action, rejectReason } = req.body; // 'approve' | 'reject'
|
||
const result = await repo.reviewWithdrawal(wdId, action, rejectReason);
|
||
res.json({ code: 20000, data: result, message: action === 'approve' ? '提现申请审核通过,款项已通过微信商户零钱打款' : `提现驳回: ${rejectReason || '风控核验未通过'}` });
|
||
} catch (err: any) {
|
||
res.status(400).json({ code: 40000, message: err.message });
|
||
}
|
||
});
|
||
|
||
app.get('/api/finance/config', async (_req: Request, res: Response) => {
|
||
try {
|
||
const config = await repo.getFinanceConfig();
|
||
res.json({
|
||
code: 20000,
|
||
data: config
|
||
});
|
||
} catch (err: any) {
|
||
res.status(500).json({ code: 50000, message: err.message });
|
||
}
|
||
});
|
||
|
||
app.post('/api/finance/config', async (req: Request, res: Response) => {
|
||
try {
|
||
const config = await repo.updateFinanceConfig(req.body);
|
||
res.json({
|
||
code: 20000,
|
||
data: config,
|
||
message: '财务抽成与分销费率配置已更新并落盘存储'
|
||
});
|
||
} catch (err: any) {
|
||
res.status(400).json({ code: 40000, message: err.message });
|
||
}
|
||
});
|
||
|
||
// 7. 场所与风控接口 (落盘 MySQL)
|
||
app.get('/api/safety/places', async (_req: Request, res: Response) => {
|
||
try {
|
||
const places = await repo.getSafetyPlaces();
|
||
res.json({
|
||
code: 20000,
|
||
data: places
|
||
});
|
||
} catch (err: any) {
|
||
res.status(500).json({ code: 50000, message: err.message });
|
||
}
|
||
});
|
||
|
||
app.post('/api/safety/places', async (req: Request, res: Response) => {
|
||
try {
|
||
const place = await repo.createSafetyPlace(req.body);
|
||
res.json({
|
||
code: 20000,
|
||
data: place,
|
||
message: '合规场所准入录入成功并持久化'
|
||
});
|
||
} catch (err: any) {
|
||
res.status(400).json({ code: 40000, message: err.message });
|
||
}
|
||
});
|
||
|
||
app.get('/api/safety/audit-logs', async (_req: Request, res: Response) => {
|
||
try {
|
||
const logs = await repo.getSafetyAuditLogs();
|
||
res.json({
|
||
code: 20000,
|
||
data: logs
|
||
});
|
||
} catch (err: any) {
|
||
res.status(500).json({ code: 50000, message: err.message });
|
||
}
|
||
});
|
||
|
||
// 7.1 全平台用户管理接口 (真实读取与更新 MySQL)
|
||
app.get('/api/users', async (req: Request, res: Response) => {
|
||
try {
|
||
const { keyword, role, status } = req.query;
|
||
const users = await repo.getUsers({
|
||
keyword: keyword ? String(keyword) : undefined,
|
||
role: role ? String(role) : undefined,
|
||
status: status ? String(status) : undefined,
|
||
});
|
||
res.json({
|
||
code: 20000,
|
||
data: users
|
||
});
|
||
} catch (err: any) {
|
||
res.status(500).json({ code: 50000, message: err.message });
|
||
}
|
||
});
|
||
|
||
app.post('/api/users/:id/status', async (req: Request, res: Response) => {
|
||
try {
|
||
const userId = Number(req.params.id);
|
||
const { status } = req.body;
|
||
if (!status || !['正常', '已封禁'].includes(status)) {
|
||
return res.status(400).json({ code: 40000, message: '无效的状态参数' });
|
||
}
|
||
const updated = await repo.updateUserStatus(userId, status);
|
||
res.json({
|
||
code: 20000,
|
||
data: updated,
|
||
message: `用户状态已更新为: ${status}`
|
||
});
|
||
} catch (err: any) {
|
||
res.status(400).json({ code: 40000, message: err.message });
|
||
}
|
||
});
|
||
|
||
// 7.2 用户360度全景综合数据看板 (涵盖活动/核销码/订单/财务流水/风控评级)
|
||
app.get('/api/users/:id/detail', async (req: Request, res: Response) => {
|
||
try {
|
||
const userId = Number(req.params.id);
|
||
const detail = await repo.getUserDetail(userId);
|
||
if (!detail) {
|
||
return res.status(404).json({ code: 40400, message: '用户不存在' });
|
||
}
|
||
res.json({
|
||
code: 20000,
|
||
data: detail
|
||
});
|
||
} catch (err: any) {
|
||
res.status(500).json({ code: 50000, message: err.message });
|
||
}
|
||
});
|
||
|
||
// 7.2.1 移动端用户资产与基本信息
|
||
app.get('/api/users/:id', async (req: Request, res: Response) => {
|
||
try {
|
||
const userId = Number(req.params.id);
|
||
const user = await repo.getUserById(userId);
|
||
if (!user) {
|
||
return res.status(404).json({ code: 40400, message: '用户未找到' });
|
||
}
|
||
res.json({ code: 20000, data: user });
|
||
} catch (err: any) {
|
||
res.status(500).json({ code: 50000, message: err.message });
|
||
}
|
||
});
|
||
|
||
// 7.2.1 个人资料修改 (真实修改 users 表 nickname, avatar, phone)
|
||
app.post('/api/users/:id/profile', async (req: Request, res: Response) => {
|
||
try {
|
||
const userId = Number(req.params.id);
|
||
const { nickname, avatar, phone } = req.body;
|
||
const updated = await repo.updateUserProfile(userId, { nickname, avatar, phone });
|
||
res.json({
|
||
code: 20000,
|
||
data: updated,
|
||
message: '个人信息修改成功'
|
||
});
|
||
} catch (err: any) {
|
||
res.status(500).json({ code: 50000, message: err.message });
|
||
}
|
||
});
|
||
|
||
// 7.2.2 钱包充值接口 (真实落盘 balance 与 finance_records)
|
||
app.post('/api/users/:id/recharge', async (req: Request, res: Response) => {
|
||
try {
|
||
const userId = Number(req.params.id);
|
||
const { amount } = req.body;
|
||
const result = await repo.rechargeUserBalance(userId, Number(amount));
|
||
res.json({
|
||
code: 20000,
|
||
data: result,
|
||
message: `成功充值 ¥${Number(amount).toFixed(2)},余额已更新`
|
||
});
|
||
} catch (err: any) {
|
||
res.status(400).json({ code: 40000, message: err.message });
|
||
}
|
||
});
|
||
|
||
// 7.2.3 佣金提现申请接口 (真实扣减 commission 并落盘 withdrawals 与 finance_records)
|
||
app.post('/api/users/:id/withdraw', async (req: Request, res: Response) => {
|
||
try {
|
||
const userId = Number(req.params.id);
|
||
const { amount, channel } = req.body;
|
||
const result = await repo.withdrawUserCommission(userId, Number(amount), channel);
|
||
res.json({
|
||
code: 20000,
|
||
data: result,
|
||
message: '提现申请已提交至后台审核,将在1个工作日内到账'
|
||
});
|
||
} catch (err: any) {
|
||
res.status(400).json({ code: 40000, message: err.message });
|
||
}
|
||
});
|
||
|
||
// 7.2.4 用户个人财务流水列表
|
||
app.get('/api/users/:id/finance-records', async (req: Request, res: Response) => {
|
||
try {
|
||
const userId = Number(req.params.id);
|
||
const list = await repo.getUserFinanceRecords(userId);
|
||
res.json({ code: 20000, data: list });
|
||
} catch (err: any) {
|
||
res.status(500).json({ code: 50000, message: err.message });
|
||
}
|
||
});
|
||
|
||
|
||
// 7.3 友圈广场动态接口 (落盘 MySQL)
|
||
app.get('/api/posts', async (req: Request, res: Response) => {
|
||
try {
|
||
const category = req.query.category ? String(req.query.category) : undefined;
|
||
const posts = await repo.getPosts(category);
|
||
res.json({
|
||
code: 20000,
|
||
data: posts
|
||
});
|
||
} catch (err: any) {
|
||
res.status(500).json({ code: 50000, message: err.message });
|
||
}
|
||
});
|
||
|
||
app.post('/api/posts', async (req: Request, res: Response) => {
|
||
try {
|
||
const { content } = req.body || {};
|
||
// 微信内容安全检测
|
||
const secCheck = contentSecurityService.checkText(content);
|
||
if (!secCheck.pass) {
|
||
return res.status(400).json({ code: secCheck.code, message: secCheck.message });
|
||
}
|
||
|
||
const post = await repo.createPost(req.body);
|
||
res.json({
|
||
code: 20000,
|
||
data: post,
|
||
message: '动态发布成功并已写入MySQL'
|
||
});
|
||
} catch (err: any) {
|
||
res.status(400).json({ code: 40000, message: err.message });
|
||
}
|
||
});
|
||
|
||
app.post('/api/posts/:id/like', async (req: Request, res: Response) => {
|
||
try {
|
||
const result = await repo.likePost(String(req.params.id));
|
||
res.json({
|
||
code: 20000,
|
||
data: result,
|
||
message: '点赞成功'
|
||
});
|
||
} catch (err: any) {
|
||
res.status(400).json({ code: 40000, message: err.message });
|
||
}
|
||
});
|
||
|
||
// 7.4 订单评价与评分接口 (落盘 MySQL order_reviews)
|
||
app.post('/api/orders/:id/review', async (req: Request, res: Response) => {
|
||
try {
|
||
const orderId = String(req.params.id);
|
||
const { rating, tags, content, userId, userNickname, userAvatar, expertId } = req.body;
|
||
// 微信内容安全检测
|
||
const secCheck = contentSecurityService.checkText(content);
|
||
if (!secCheck.pass) {
|
||
return res.status(400).json({ code: secCheck.code, message: secCheck.message });
|
||
}
|
||
|
||
const review = await repo.createOrderReview({
|
||
orderId,
|
||
expertId: expertId ? Number(expertId) : undefined,
|
||
userId: userId ? Number(userId) : undefined,
|
||
userNickname,
|
||
userAvatar,
|
||
rating: Number(rating) || 5,
|
||
tags,
|
||
content
|
||
});
|
||
res.json({
|
||
code: 20000,
|
||
data: review,
|
||
message: '评价提交成功并已落盘入库'
|
||
});
|
||
} catch (err: any) {
|
||
res.status(400).json({ code: 40000, message: err.message });
|
||
}
|
||
});
|
||
|
||
// 7.5 大神历史真实评价列表接口
|
||
app.get('/api/companions/:id/reviews', async (req: Request, res: Response) => {
|
||
try {
|
||
const expertId = Number(req.params.id);
|
||
const reviews = await repo.getOrderReviews(expertId);
|
||
res.json({
|
||
code: 20000,
|
||
data: reviews
|
||
});
|
||
} catch (err: any) {
|
||
res.status(500).json({ code: 50000, message: err.message });
|
||
}
|
||
});
|
||
|
||
// 7.6 友圈动态互动评论区接口
|
||
app.get('/api/posts/:id/comments', async (req: Request, res: Response) => {
|
||
try {
|
||
const postId = String(req.params.id);
|
||
const comments = await repo.getPostComments(postId);
|
||
res.json({
|
||
code: 20000,
|
||
data: comments
|
||
});
|
||
} catch (err: any) {
|
||
res.status(500).json({ code: 50000, message: err.message });
|
||
}
|
||
});
|
||
|
||
app.post('/api/posts/:id/comments', async (req: Request, res: Response) => {
|
||
try {
|
||
const postId = String(req.params.id);
|
||
const { content, userId, author, avatar } = req.body;
|
||
if (!content || !content.trim()) {
|
||
return res.status(400).json({ code: 40000, message: '评论内容不能为空' });
|
||
}
|
||
|
||
// 微信内容安全检测
|
||
const secCheck = contentSecurityService.checkText(content);
|
||
if (!secCheck.pass) {
|
||
return res.status(400).json({ code: secCheck.code, message: secCheck.message });
|
||
}
|
||
|
||
const comment = await repo.createPostComment({
|
||
postId,
|
||
content: content.trim(),
|
||
userId: userId ? Number(userId) : undefined,
|
||
author,
|
||
avatar
|
||
});
|
||
res.json({
|
||
code: 20000,
|
||
data: comment,
|
||
message: '评论发表成功'
|
||
});
|
||
} catch (err: any) {
|
||
res.status(400).json({ code: 40000, message: err.message });
|
||
}
|
||
});
|
||
|
||
// 7.7 友圈搭子互动上车约伴申请接口
|
||
app.post('/api/posts/:id/apply', async (req: Request, res: Response) => {
|
||
try {
|
||
const postId = String(req.params.id);
|
||
const { applicantId, applicantName, applicantAvatar } = req.body;
|
||
const result = await repo.applyPostParty({
|
||
postId,
|
||
applicantId: applicantId ? Number(applicantId) : undefined,
|
||
applicantName,
|
||
applicantAvatar
|
||
});
|
||
res.json({
|
||
code: 20000,
|
||
data: result,
|
||
message: '搭子互动上车申请已送达'
|
||
});
|
||
} catch (err: any) {
|
||
res.status(400).json({ code: 40000, message: err.message });
|
||
}
|
||
});
|
||
|
||
// 8. 启动服务器并初始化数据库
|
||
async function bootstrap() {
|
||
try {
|
||
await initDatabase();
|
||
console.log('[Peiwan Server] MySQL 数据库核心表及种子数据已校验就绪');
|
||
} catch (err) {
|
||
console.error('[Peiwan Server] MySQL 初始化连接发生警告:', err);
|
||
}
|
||
|
||
app.listen(port, () => {
|
||
console.log(`[Peiwan Server] HTTP server is running on http://localhost:${port}`);
|
||
});
|
||
}
|
||
|
||
bootstrap();
|
||
|
||
export default app;
|