refactor(data): 全面移除小程序写死的演示/兜底数据,测试数据统一落库
- 删除无引用的 common/mock.js;登录页与个人中心的演示账号改为 DB 配置 demo_accounts + /api/auth/demo-accounts、/api/auth/demo-login(签发真实 token) - 移除订单/活动/大神/动态/推广/工作台/门票/充值/举报等页面的演示列表与假兜底;新增真实空态 - 修复伪造成功:活动调价、评价回复、核销、海报保存、图片上传失败、提现/核销固定 1001、随机开黑房号、随机即时匹配、假语音播放等 - 服务端移除 101/2006 合成用户与合成大神兜底、移除默认服务包虚构价格;演示账号清单可在后台关闭 - 111 套件/733 用例全绿;已部署 20261021_162041,demo-accounts/demo-login 实测通过
这个提交包含在:
@@ -1302,6 +1302,23 @@ async function seedInitialDataIfEmpty(conn: any): Promise<void> {
|
||||
VALUES ('map_location_config', '地图定位与LBS服务配置', ?, '腾讯位置服务Key、选点与周边测距参数配置', 1)
|
||||
`, [mapLocationConfigVal]);
|
||||
}
|
||||
|
||||
// 体验账号(演示/评审):账号清单与展示文案全部落库,小程序端不再写死
|
||||
const demoAccountsVal = JSON.stringify({
|
||||
enabled: true,
|
||||
accounts: [
|
||||
{ userId: 1001, roleTag: 'host', roleDesc: '房主/局长', desc: '余额充足 · 发起项目制/AA组局' },
|
||||
{ userId: 2001, roleTag: 'buyer', roleDesc: '普通买家', desc: '搭子报名 · 预约陪伴 · 核销码签到' },
|
||||
{ userId: 2006, roleTag: 'expert', roleDesc: '金牌陪伴师', desc: '工作台接单 · 雷达抢单 · 现场核销' }
|
||||
],
|
||||
remark: '体验账号:仅此处配置的账号可走「体验登录」获取真实会话 token;清空 accounts 即关闭体验登录'
|
||||
});
|
||||
if (!existingKeys.has('demo_accounts')) {
|
||||
await conn.query(`
|
||||
INSERT INTO platform_configs (config_key, name, value_json, description, status)
|
||||
VALUES ('demo_accounts', '体验账号配置', ?, '演示/评审用体验账号清单(含展示文案),关闭时清空 accounts', 1)
|
||||
`, [demoAccountsVal]);
|
||||
}
|
||||
if (existingKeys.has('platform_protocol')) {
|
||||
const [protoRows]: any = await conn.query("SELECT value_json FROM platform_configs WHERE config_key = 'platform_protocol'");
|
||||
if (protoRows && protoRows.length > 0) {
|
||||
|
||||
@@ -759,12 +759,6 @@ export class MysqlRepository {
|
||||
);
|
||||
if (mappedRes && mappedRes.length > 0) return mappedRes[0];
|
||||
}
|
||||
if (numId === 2006) {
|
||||
const [susuRes]: any = await pool.query(
|
||||
"SELECT * FROM companions WHERE name = '苏苏学姐' OR id = 1 LIMIT 1"
|
||||
);
|
||||
if (susuRes && susuRes.length > 0) return susuRes[0];
|
||||
}
|
||||
} else {
|
||||
const [res]: any = await pool.query('SELECT * FROM companions WHERE name = ? LIMIT 1', [rawId]);
|
||||
if (res && res.length > 0) return res[0];
|
||||
@@ -917,25 +911,6 @@ export class MysqlRepository {
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
const defaultServices = [
|
||||
{
|
||||
id: 'srv_1',
|
||||
name: `${game} · 基础陪伴 (2小时)`,
|
||||
title: `${game} · 基础陪伴 (2小时)`,
|
||||
price: price,
|
||||
unit: '小时',
|
||||
desc: '正规公开商圈同城陪伴,AA制或协商消费'
|
||||
},
|
||||
{
|
||||
id: 'srv_2',
|
||||
name: `${game} · 半日畅享 (3小时)`,
|
||||
title: `${game} · 半日畅享 (3小时)`,
|
||||
price: Math.round(price * 2.8),
|
||||
unit: '次',
|
||||
desc: '深度互动与陪伴交流,赠送专属点歌或语音祝福'
|
||||
}
|
||||
];
|
||||
|
||||
const auditStatus = c.audit_status || '已通过';
|
||||
const certStatus = auditStatus === '已通过' ? 2 : (auditStatus === '待审核' ? 1 : (auditStatus === '已驳回' ? 3 : 0));
|
||||
|
||||
@@ -947,36 +922,32 @@ export class MysqlRepository {
|
||||
avatar: c.avatar || 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=300',
|
||||
coverImage: (c.custom_photos && c.custom_photos[0]) || c.avatar || 'https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?w=800',
|
||||
gender: c.gender === '男' ? 'male' : 'female',
|
||||
age: synthetic ? 0 : (Number(c.age) || 22),
|
||||
age: Number(c.age) || 0,
|
||||
game: game,
|
||||
track: c.track || 'OFFLINE_MEAL',
|
||||
city: synthetic ? (c.custom_city || '') : (c.custom_city || '北京市 · 朝阳/海淀'),
|
||||
city: c.custom_city || '',
|
||||
rankTag: c.rank_name || (synthetic ? '' : '认证大神'),
|
||||
levelTag: synthetic ? '普通用户' : '金牌认证',
|
||||
price: synthetic ? 0 : price,
|
||||
hourlyRate: synthetic ? 0 : price,
|
||||
rating: synthetic ? 0 : (Number(c.rating) || 5.0),
|
||||
orderCount: synthetic ? 0 : (Number(c.order_count) || 120),
|
||||
rating: synthetic ? 0 : (Number(c.rating) || 0),
|
||||
orderCount: synthetic ? 0 : (Number(c.order_count) || 0),
|
||||
status: synthetic ? '休息中' : (c.status || '接单中'),
|
||||
auditStatus: synthetic ? '未申请' : auditStatus,
|
||||
certStatus: synthetic ? 0 : certStatus,
|
||||
rejectReason: c.reject_reason || '',
|
||||
isOnline: synthetic ? false : c.status === '接单中',
|
||||
isCompanion: !synthetic,
|
||||
voiceDuration: synthetic ? 0 : (Number(c.voice_duration) || 15),
|
||||
voiceDuration: synthetic ? 0 : (Number(c.voice_duration) || 0),
|
||||
voiceType: 'voice',
|
||||
voiceTitle: synthetic ? '' : (c.voice_title || '语音招呼'),
|
||||
voiceTitle: synthetic ? '' : (c.voice_title || ''),
|
||||
voiceIntroUrl: synthetic ? '' : (c.voice_intro_url || ''),
|
||||
bio: c.custom_bio || (synthetic ? '' : '专业同城生活陪伴,热情礼貌,守时诚信。'),
|
||||
bio: c.custom_bio || '',
|
||||
tags: parsedTags,
|
||||
photos: c.custom_photos || (synthetic
|
||||
? [c.avatar || 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=400']
|
||||
: [
|
||||
c.avatar || 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=400',
|
||||
'https://images.unsplash.com/photo-1517841905240-472988babdf9?w=400',
|
||||
'https://images.unsplash.com/photo-1524504388940-b1c1722653e1?w=400'
|
||||
]),
|
||||
services: synthetic ? [] : (customServices.length > 0 ? customServices : defaultServices)
|
||||
photos: (Array.isArray(c.custom_photos) && c.custom_photos.length > 0)
|
||||
? c.custom_photos
|
||||
: (c.avatar ? [c.avatar] : []),
|
||||
services: synthetic ? [] : (Array.isArray(customServices) ? customServices : [])
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2993,7 +2964,7 @@ export class MysqlRepository {
|
||||
WHERE status = 1
|
||||
`);
|
||||
// 移动端公开配置:严禁下发任何密钥/风控内部数据(该接口无需登录)
|
||||
const PUBLIC_EXCLUDED_KEYS = new Set(['security_words', 'map_location_config']);
|
||||
const PUBLIC_EXCLUDED_KEYS = new Set(['security_words', 'map_location_config', 'demo_accounts']);
|
||||
const SENSITIVE_FIELD = /(secret|private|apiv3|api_v3|password|passwd|token|pem|cert|mchid)/i;
|
||||
const stripSensitive = (val: any): any => {
|
||||
if (Array.isArray(val)) return val.map(stripSensitive);
|
||||
@@ -4067,52 +4038,6 @@ export class MysqlRepository {
|
||||
WHERE id = ?
|
||||
`, [id]);
|
||||
if (!rows || rows.length === 0) {
|
||||
if (id === 101) {
|
||||
return {
|
||||
id: 101,
|
||||
userNo: 'U101',
|
||||
nickname: '糖心小糯米',
|
||||
avatar: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=200',
|
||||
phone: '138****0101',
|
||||
gender: '女',
|
||||
signature: '人美声甜,擅长各类同城陪伴与开黑互动',
|
||||
role: '认证大神',
|
||||
balance: '2580.00',
|
||||
commission: '128.50',
|
||||
invitedCount: 6,
|
||||
orderCount: 88,
|
||||
activityCount: 16,
|
||||
isExpert: true,
|
||||
certStatus: 2,
|
||||
auditStatus: '已通过',
|
||||
rejectReason: '',
|
||||
companionStatus: '接单中',
|
||||
status: '正常'
|
||||
};
|
||||
}
|
||||
if (id === 2006) {
|
||||
return {
|
||||
id: 2006,
|
||||
userNo: 'U2006',
|
||||
nickname: '苏苏学姐',
|
||||
avatar: 'https://images.unsplash.com/photo-1524504388940-b1c1722653e1?w=120',
|
||||
phone: '139****2006',
|
||||
gender: '女',
|
||||
signature: '工体纯K量贩欢唱局!甜美情歌、粤语老歌、摇滚气氛拉满',
|
||||
role: '认证大神',
|
||||
balance: '5690.00',
|
||||
commission: '560.00',
|
||||
invitedCount: 12,
|
||||
orderCount: 120,
|
||||
activityCount: 35,
|
||||
isExpert: true,
|
||||
certStatus: 2,
|
||||
auditStatus: '已通过',
|
||||
rejectReason: '',
|
||||
companionStatus: '接单中',
|
||||
status: '正常'
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const u = rows[0];
|
||||
|
||||
@@ -509,9 +509,9 @@ async function resolveTencentMapKey(): Promise<string> {
|
||||
return key;
|
||||
}
|
||||
|
||||
function httpsGetJson(url: string, timeoutMs = 4000): Promise<any> {
|
||||
function httpsGetJsonOnce(url: string, headers: Record<string, string>, timeoutMs = 4000): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = https.get(url, (resp) => {
|
||||
const req = https.get(url, { headers }, (resp) => {
|
||||
let data = '';
|
||||
resp.on('data', (chunk) => {
|
||||
data += chunk;
|
||||
@@ -529,6 +529,15 @@ function httpsGetJson(url: string, timeoutMs = 4000): Promise<any> {
|
||||
});
|
||||
}
|
||||
|
||||
async function httpsGetJson(url: string, timeoutMs = 4000): Promise<any> {
|
||||
const referer = String(process.env.TENCENT_MAP_REFERER || 'http://118.184.172.94/').trim();
|
||||
const data = await httpsGetJsonOnce(url, referer ? { Referer: referer } : {}, timeoutMs);
|
||||
if (data && (data.status === 110 || data.status === 105) && referer) {
|
||||
return httpsGetJsonOnce(url, {}, timeoutMs);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
const geocodeCache = new Map<string, { geo: { lat: number; lng: number } | null; at: number }>();
|
||||
|
||||
/** 地址 -> 坐标(腾讯 WebService 地理编码),带进程内缓存 */
|
||||
@@ -2947,6 +2956,88 @@ app.post('/api/auth/wechat-phone', async (req: Request, res: Response) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ===== 体验账号(演示/评审):清单与文案全部来自 DB 配置,小程序端不再写死 =====
|
||||
async function loadDemoAccounts(): Promise<Array<{ userId: number; roleTag: string; roleDesc: string; desc: string }>> {
|
||||
try {
|
||||
const record = await repo.getPlatformConfigByKey('demo_accounts');
|
||||
const value = (record && record.valueJson) || {};
|
||||
if (value.enabled === false) return [];
|
||||
const accounts = Array.isArray(value.accounts) ? value.accounts : [];
|
||||
return accounts
|
||||
.map((a: any) => ({
|
||||
userId: Number(a.userId) || 0,
|
||||
roleTag: String(a.roleTag || ''),
|
||||
roleDesc: String(a.roleDesc || ''),
|
||||
desc: String(a.desc || ''),
|
||||
}))
|
||||
.filter((a: any) => a.userId > 0)
|
||||
.slice(0, 10);
|
||||
} catch (e) {
|
||||
logServerError('loadDemoAccounts', e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
app.get('/api/auth/demo-accounts', async (_req: Request, res: Response) => {
|
||||
try {
|
||||
const accounts = await loadDemoAccounts();
|
||||
const list = [];
|
||||
for (const acc of accounts) {
|
||||
const u: any = await repo.getUserById(acc.userId);
|
||||
if (!u) continue;
|
||||
if (u.status === '已封禁' || u.status === '封禁') continue;
|
||||
list.push({
|
||||
userId: u.id,
|
||||
nickname: u.nickname || `用户${u.id}`,
|
||||
avatar: u.avatar || '',
|
||||
roleTag: acc.roleTag,
|
||||
roleDesc: acc.roleDesc,
|
||||
desc: acc.desc,
|
||||
});
|
||||
}
|
||||
res.json({ code: 20000, data: list });
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ code: 50000, message: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 体验登录:只对 DB 配置内的账号签发真实 token,避免"无 token 的假登录"造成的 401/身份错配
|
||||
app.post('/api/auth/demo-login', rateLimit({ name: 'demo_login', windowMs: 60 * 1000, max: 20, key: rateWriteKey }), async (req: Request, res: Response) => {
|
||||
try {
|
||||
const userId = Number(req.body?.userId) || 0;
|
||||
const accounts = await loadDemoAccounts();
|
||||
if (!accounts.some((a) => a.userId === userId)) {
|
||||
return res.status(403).json({ code: 40300, message: '该账号未开启体验登录' });
|
||||
}
|
||||
const user: any = await repo.getUserById(userId);
|
||||
if (!user) {
|
||||
return res.status(404).json({ code: 40400, message: '体验账号不存在或已被清理' });
|
||||
}
|
||||
if (user.status === '已封禁' || user.status === '封禁') {
|
||||
return res.status(403).json({ code: 40300, message: '该体验账号已被封禁' });
|
||||
}
|
||||
const token = signUserToken(user.id);
|
||||
res.json({
|
||||
code: 20000,
|
||||
data: {
|
||||
userId: user.id,
|
||||
user: {
|
||||
id: user.id,
|
||||
nickname: user.nickname,
|
||||
avatar: user.avatar,
|
||||
gender: user.gender,
|
||||
phone: user.phone,
|
||||
},
|
||||
userToken: token,
|
||||
token,
|
||||
},
|
||||
message: '体验登录成功',
|
||||
});
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ code: 50000, message: err.message || '体验登录异常' });
|
||||
}
|
||||
});
|
||||
|
||||
// 微信小程序 JSAPI 统一下单预支付接口 (支持真实商户号配置与沙盒收银台无缝切换)
|
||||
app.post('/api/pay/wechat/prepay', rateLimit({ name: 'prepay', windowMs: 60 * 1000, max: 30, key: rateWriteKey }), async (req: Request, res: Response) => {
|
||||
try {
|
||||
@@ -3790,45 +3881,7 @@ app.get('/api/users/:id', async (req: Request, res: Response) => {
|
||||
const userId = Number(req.params.id);
|
||||
let user = await repo.getUserById(userId);
|
||||
if (!user) {
|
||||
if (userId === 101) {
|
||||
user = {
|
||||
id: 101,
|
||||
userNo: 'U101',
|
||||
nickname: '糖心小糯米',
|
||||
avatar: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=200',
|
||||
phone: '138****0101',
|
||||
gender: '女',
|
||||
signature: '人美声甜,擅长各类同城陪伴与开黑互动',
|
||||
role: '认证大神',
|
||||
balance: '2580.00',
|
||||
commission: '128.50',
|
||||
invitedCount: 6,
|
||||
orderCount: 88,
|
||||
activityCount: 16,
|
||||
isExpert: true,
|
||||
status: '正常'
|
||||
};
|
||||
} else if (userId === 2006) {
|
||||
user = {
|
||||
id: 2006,
|
||||
userNo: 'U2006',
|
||||
nickname: '苏苏学姐',
|
||||
avatar: 'https://images.unsplash.com/photo-1524504388940-b1c1722653e1?w=120',
|
||||
phone: '139****2006',
|
||||
gender: '女',
|
||||
signature: '工体纯K量贩欢唱局!甜美情歌、粤语老歌、摇滚气氛拉满',
|
||||
role: '认证大神',
|
||||
balance: '5690.00',
|
||||
commission: '560.00',
|
||||
invitedCount: 12,
|
||||
orderCount: 120,
|
||||
activityCount: 35,
|
||||
isExpert: true,
|
||||
status: '正常'
|
||||
};
|
||||
} else {
|
||||
return res.status(404).json({ code: 40400, message: '用户未找到' });
|
||||
}
|
||||
return res.status(404).json({ code: 40400, message: '用户未找到' });
|
||||
}
|
||||
// 敏感字段(手机号 / 余额 / 佣金 / 邀请关系)仅本人或管理员可见
|
||||
const isAdmin = isVerifiedAdminRequest(req);
|
||||
|
||||
在新工单中引用
屏蔽一个用户