文件
shanghaipeiwan/server/src/index.ts

1225 行
40 KiB
TypeScript

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

// 已读取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;