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 实测通过
这个提交包含在:
xcbyy
2026-10-21 16:21:10 +08:00
父节点 77aa4cb50b
当前提交 16f6419cba
共有 30 个文件被更改,包括 661 次插入 和 1366 次删除

查看文件

@@ -11,7 +11,7 @@ export function formatActivityData(raw) {
const payTypeStr = raw.payType === 2 || raw.payType === 'PROJECT' ? 'PROJECT' : 'AA';
const totalFee = raw.totalFeeCents != null ? raw.totalFeeCents / 100 : (raw.totalFee || 0);
const perPerson = raw.perPersonFeeCents != null ? raw.perPersonFeeCents / 100 : (raw.perPersonFee || (payTypeStr === 'AA' ? totalFee / (raw.maxParticipants || 4) : 500));
const perPerson = raw.perPersonFeeCents != null ? raw.perPersonFeeCents / 100 : (raw.perPersonFee || (payTypeStr === 'AA' ? totalFee / (raw.maxParticipants || 4) : 0));
const participants = (raw.participants || []).map((p) => ({
id: p.id,
userId: p.userId,
@@ -26,20 +26,20 @@ export function formatActivityData(raw) {
return {
id: String(raw.id),
title: raw.title || '同城生活组局',
game: raw.gameCategory || raw.game || '美食陪吃',
title: raw.title || '',
game: raw.gameCategory || raw.game || '',
track: raw.track || 'OFFLINE_MEAL',
payType: payTypeStr,
totalFee,
perPersonFee: perPerson,
aaFee: raw.aaFee != null ? raw.aaFee : perPerson,
maxParticipants: raw.maxParticipants || 4,
currentParticipants: joinedCount || 1,
startTime: raw.startTime || '今天 19:30',
location: raw.location || '北京市朝阳区公开商业街区',
creatorId: raw.creatorId != null ? Number(raw.creatorId) : (raw.creator_id != null ? Number(raw.creator_id) : 1001),
creatorNickname: raw.creatorNickname || '同城局长',
creatorAvatar: raw.creatorAvatar || 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=100',
maxParticipants: raw.maxParticipants || 0,
currentParticipants: joinedCount,
startTime: raw.startTime || '',
location: raw.location || '',
creatorId: raw.creatorId != null ? Number(raw.creatorId) : (raw.creator_id != null ? Number(raw.creator_id) : 0),
creatorNickname: raw.creatorNickname || '',
creatorAvatar: raw.creatorAvatar || '',
status: raw.status || (joinedCount >= (raw.maxParticipants || 4) ? 'FULL' : 'RECRUITING'),
allowDynamicPrice: Boolean(raw.allowDynamicPrice),
priceAdjustRule: raw.priceAdjustRule || 'UNIFIED_HIGH',
@@ -203,23 +203,23 @@ export async function getCompanions(keyword) {
.filter((c) => !c.auditStatus || c.auditStatus === '已通过')
.map((c) => ({
id: String(c.id),
nickname: c.name || c.nickname || '同城大神',
avatar: c.avatar || 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=300',
gender: c.gender === '男' ? 'male' : 'female',
age: Number(c.age) || 22,
game: c.track === 'OFFLINE_MEAL' ? '美食陪吃' : (c.track === 'OFFLINE_SING' ? '同城陪唱' : (c.track === 'OFFLINE_DRINK' ? '下午茶陪喝' : (c.game || '同城陪伴'))),
nickname: c.name || c.nickname || '',
avatar: c.avatar || '',
gender: c.gender === '男' ? 'male' : (c.gender === '女' ? 'female' : ''),
age: Number(c.age) || 0,
game: c.track === 'OFFLINE_MEAL' ? '美食陪吃' : (c.track === 'OFFLINE_SING' ? '同城陪唱' : (c.track === 'OFFLINE_DRINK' ? '下午茶陪喝' : (c.game || ''))),
track: c.track || 'OFFLINE_MEAL',
city: '北京市 · 朝阳/海淀',
rankTag: c.rank || '同城认证陪伴师',
levelTag: '认证大神',
price: Number(c.hourlyRate) || 68,
rating: Number(c.rating) || 5.0,
orderCount: Number(c.orderCount) || 120,
city: c.city || c.customCity || '',
rankTag: c.rank || '',
levelTag: c.auditStatus === '已通过' ? '认证大神' : '',
price: Number(c.hourlyRate) || 0,
rating: Number(c.rating) || 0,
orderCount: Number(c.orderCount) || 0,
isOnline: c.status === '接单中',
voiceDuration: Number(c.voiceDuration) || 15,
voiceDuration: Number(c.voiceDuration) || 0,
voiceType: 'voice',
voiceTitle: c.voiceTitle || '语音招呼',
bio: '专业同城生活陪伴,阳光健康公开。'
voiceTitle: c.voiceTitle || '',
bio: c.bio || ''
}));
}
} catch (err) {
@@ -269,34 +269,33 @@ export function formatOrderData(raw) {
) statusKey = 'REFUNDING';
else if (statusKey === '已取消') statusKey = 'CANCELLED';
const companionAvatar = raw.expertAvatar || raw.expert_avatar || raw.avatar ||
'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=120';
const companionAvatar = raw.expertAvatar || raw.expert_avatar || raw.avatar || '';
const companionNickname = raw.companionName || raw.companion_name || raw.expertNickname || (raw.expert && raw.expert.nickname) || '同城大神';
const game = raw.track || (raw.expert && raw.expert.game) || '同城陪伴';
const companionNickname = raw.companionName || raw.companion_name || raw.expertNickname || (raw.expert && raw.expert.nickname) || '';
const game = raw.track || (raw.expert && raw.expert.game) || '';
return {
id: String(raw.id),
orderSn: raw.orderNo || raw.order_no || raw.orderSn || String(raw.id),
expert: {
id: String(raw.expertId || raw.expert_id || (raw.expert && raw.expert.id) || 'exp_101'),
id: String(raw.expertId || raw.expert_id || (raw.expert && raw.expert.id) || ''),
nickname: companionNickname,
avatar: companionAvatar,
game: game,
},
serviceTitle: raw.serviceTitle || raw.service_title || `${game} · 专业陪伴服务`,
quantity: Number(raw.durationHours || raw.duration_hours || raw.quantity || 2),
serviceTitle: raw.serviceTitle || raw.service_title || '',
quantity: Number(raw.durationHours || raw.duration_hours || raw.quantity || 0),
totalAmount: Number(raw.amount || raw.totalAmount || 0),
refundAmount: Number(raw.refundAmount || raw.refund_amount || 0),
status: statusKey,
rawStatus: raw.status || '履约中',
meetVenue: raw.location || raw.meetVenue || '北京市公开商圈',
expenseBurden: raw.expenseBurden || raw.expense_burden || 'AA制各自付',
payType: raw.payType || raw.pay_type || 'WECHAT',
rawStatus: raw.status || '',
meetVenue: raw.location || raw.meetVenue || '',
expenseBurden: raw.expenseBurden || raw.expense_burden || '',
payType: raw.payType || raw.pay_type || '',
couponDiscount: Number(raw.couponDiscount || raw.coupon_discount || 0),
verifyCode: raw.verifyCode || raw.verify_code || '',
remark: raw.remark || '',
createdAt: raw.createdAt || raw.created_at || '刚刚'
createdAt: raw.createdAt || raw.created_at || ''
};
}
@@ -475,13 +474,13 @@ export async function getPosts(category, userId) {
return {
id: p.id,
userId: p.userId || p.user_id || 0,
author: p.author || '神秘同好',
avatar: p.avatar || 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=150',
tag: p.tag || '同城认证',
author: p.author || '',
avatar: p.avatar || '',
tag: p.tag || '',
tagType: p.tagType || 'warm-tag',
location: p.location || '同城热门商圈',
distance: p.distance || '1km内',
time: p.createdAt ? '刚刚' : '近期',
location: p.location || '',
distance: p.distance || '',
time: p.time || (p.createdAt ? '' : ''),
content: p.content || '',
topics: Array.isArray(p.topics) ? p.topics : [],
images: Array.isArray(p.images) ? p.images : [],
@@ -1528,3 +1527,23 @@ export async function loginWithWechatPhone({ phoneCode, wxCode } = {}) {
wxCode
});
}
/**
* 体验账号列表(后台配置,DB 驱动;小程序端不再写死)
*/
export async function getDemoAccounts() {
try {
const res = await http.get('/auth/demo-accounts');
return Array.isArray(res) ? res : [];
} catch (err) {
console.warn('[API] 获取体验账号异常:', err.message);
return [];
}
}
/**
* 体验登录:服务端校验账号在配置清单内后签发真实会话 token
*/
export async function demoLogin(userId) {
return http.post('/auth/demo-login', { userId: Number(userId) });
}

查看文件

@@ -1,416 +0,0 @@
import { ICONS } from './icons.js';
export const GAMES_LIST = [
{ id: 'all', name: '全部', icon: ICONS.gameAll, hot: false },
{ id: 'offline_meal', name: '美食陪吃', icon: ICONS.utensils, hot: true },
{ id: 'offline_drink', name: '下午茶陪喝', icon: ICONS.coffee, hot: true },
{ id: 'offline_sing', name: '同城陪唱', icon: ICONS.micVocal, hot: true },
{ id: 'offline_play', name: '同城陪玩', icon: ICONS.dice, hot: false },
{ id: 'wzry', name: '王者荣耀', icon: ICONS.gameCrown, hot: false },
{ id: 'hpjy', name: '和平精英', icon: ICONS.gameCrosshair, hot: false },
{ id: 'lol', name: '英雄联盟', icon: ICONS.gameSwords, hot: false },
];
export const BANNERS = [
{ id: '1', title: '同城聚会狂欢 | 网红火锅探店与量贩KTV嗨唱', tag: '热门陪伴', bg: 'linear-gradient(135deg, #F43F5E 0%, #FB7185 100%)' },
{ id: '2', title: '新用户专享 | 线下陪吃陪喝立减30元', tag: '新人特惠', bg: 'linear-gradient(135deg, #059669 0%, #10B981 100%)' },
{ id: '3', title: '天籁歌喉进驻 | 音乐学院陪唱达人歌声试听', tag: 'K歌对唱', bg: 'linear-gradient(135deg, #7C3AED 0%, #6366F1 100%)' },
];
export const EXPERTS_LIST = [
{
id: 'exp_105',
nickname: '橙子小七',
avatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=300&h=300&fit=crop&crop=faces',
coverImage: 'https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?w=800&h=400&fit=crop',
gender: 'female',
age: 22,
game: '美食陪吃',
track: 'OFFLINE_MEAL',
city: '北京市 · 朝阳/三里屯',
rankTag: '美食探店达人 · 火锅烤肉搭子',
levelTag: '同城红人',
price: 68,
rating: 5.0,
orderCount: 420,
isOnline: true,
voiceDuration: 14,
voiceType: 'voice',
bio: '资深美食探店博主,性格随和开朗,陪你吃遍同城网红餐厅与地道市井美味,消费约定AA或请客~',
photos: ['https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?w=500&fit=crop'],
services: [
{ id: 'srv_7', title: '同城美食探店 · 火锅/烤肉饭搭子', price: 68, unit: '小时', desc: '公开商场餐厅,绝不踩雷,现场AA或买单' },
{ id: 'srv_8', title: '商圈早午餐/轻食下午茶陪伴', price: 58, unit: '小时', desc: '轻松闲聊,氛围愉快,阳光公开场所' },
],
reviews: [
{ id: 'r4', userNickname: '吃货小张', userAvatar: 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=100', rating: 5, tags: ['随和健谈', '推荐靠谱'], content: '火锅店推荐的太好吃了,聊得很投缘!', time: '昨天' },
],
},
{
id: 'exp_107',
nickname: '苏苏学姐',
avatar: 'https://images.unsplash.com/photo-1524504388940-b1c1722653e1?w=300&h=300&fit=crop&crop=faces',
coverImage: 'https://images.unsplash.com/photo-1516450360452-9312f5e86fc7?w=800&h=400&fit=crop',
gender: 'female',
age: 21,
game: '同城陪唱',
track: 'OFFLINE_SING',
city: '北京市 · 工体/五道口',
rankTag: '音乐学院在读 · 甜美情歌对唱',
levelTag: '天籁歌喉',
price: 88,
rating: 5.0,
orderCount: 560,
isOnline: true,
voiceDuration: 18,
voiceType: 'song',
voiceTitle: '歌声 · 《晴天》',
bio: '音乐学院流行演唱在读,精通流行热门情歌与粤语金曲,KTV对唱超好听,包厢气氛组担当~限正规公开量贩KTV。',
photos: ['https://images.unsplash.com/photo-1516450360452-9312f5e86fc7?w=500&fit=crop'],
services: [
{ id: 'srv_11', title: '量贩KTV麦霸对唱与包厢气氛', price: 88, unit: '小时', desc: '正规公开KTV,包厢房费与酒水消费约定AA或请客' },
{ id: 'srv_12', title: '同城Livehouse/音乐节同游陪伴', price: 98, unit: '小时', desc: '现场沉浸听歌,音乐狂欢好搭子' },
],
reviews: [
{ id: 'r5', userNickname: 'K歌达人阿伟', userAvatar: 'https://images.unsplash.com/photo-1570295999919-56ceb5ecca61?w=100', rating: 5, tags: ['歌声惊艳', '超高情商'], content: '歌声真的太好听了,完全是专业级水平,气氛特别好!', time: '3小时前' },
],
},
{
id: 'exp_106',
nickname: '莫吉托学长',
avatar: 'https://images.unsplash.com/photo-1506794778202-cad84cf45f1d?w=300&h=300&fit=crop&crop=faces',
coverImage: 'https://images.unsplash.com/photo-1501339847302-ac426a4a7cbb?w=800&h=400&fit=crop',
gender: 'male',
age: 24,
game: '下午茶陪喝',
track: 'OFFLINE_DRINK',
city: '北京市 · 国贸/望京',
rankTag: '手冲咖啡师 · 清吧倾听解压',
levelTag: '暖心导师',
price: 58,
rating: 4.9,
orderCount: 310,
isOnline: true,
voiceDuration: 16,
voiceType: 'voice',
bio: 'SCA认证咖啡师与调酒师,善于倾听情绪解压,阳光透明,约定公开正规咖啡馆或清吧。',
photos: ['https://images.unsplash.com/photo-1501339847302-ac426a4a7cbb?w=500&fit=crop'],
services: [
{ id: 'srv_9', title: '精品咖啡品鉴 · 聊天解压倾听', price: 58, unit: '小时', desc: '星巴克臻选/独立咖啡馆,温和交流' },
{ id: 'srv_10', title: '正规清吧微醺特调陪伴', price: 78, unit: '小时', desc: '限公开正规商业清吧,严禁过量饮酒' },
],
reviews: [],
},
{
id: 'exp_108',
nickname: '阿泽学长',
avatar: 'https://images.unsplash.com/photo-1539571696357-5a69c17a67c6?w=300&h=300&fit=crop&crop=faces',
coverImage: 'https://images.unsplash.com/photo-1511671782779-c97d3d27a1d4?w=800&h=400&fit=crop',
gender: 'male',
age: 23,
game: '同城陪唱',
track: 'OFFLINE_SING',
city: '北京市 · 海淀/中关村',
rankTag: '流行摇滚主唱 · 吉他伴奏对唱',
levelTag: '全能麦霸',
price: 78,
rating: 4.9,
orderCount: 480,
isOnline: true,
voiceDuration: 20,
voiceType: 'song',
voiceTitle: '歌声 · 摇滚高音',
bio: '高校乐队主唱,吉他弹唱/流行摇滚,KTV气氛直接拉满,绝不冷场,带你体验演唱会包厢~',
photos: ['https://images.unsplash.com/photo-1511671782779-c97d3d27a1d4?w=500&fit=crop'],
services: [
{ id: 'srv_13', title: 'KTV流行摇滚对唱与吉他弹唱', price: 78, unit: '小时', desc: '气氛担当,带动全场,限公开正规量贩KTV' },
],
reviews: [],
},
{
id: 'exp_101',
nickname: '月色浅浅',
avatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=300&h=300&fit=crop&crop=faces',
coverImage: 'https://images.unsplash.com/photo-1542751371-adc38448a05e?w=800&h=400&fit=crop',
gender: 'female',
age: 21,
game: '王者荣耀',
rankTag: '百星王者 · 国服瑶妹',
levelTag: '金牌导师',
price: 35,
rating: 4.9,
orderCount: 1280,
isOnline: true,
voiceDuration: 12,
bio: '全天在线,心态超好,技术过硬,带你轻松摘星~',
photos: [
'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=500&fit=crop',
'https://images.unsplash.com/photo-1517841905240-472988babdf9?w=500&fit=crop',
],
services: [
{ id: 'srv_1', title: '王者荣耀 · 排位包胜率', price: 35, unit: '小时', desc: '不包赢不退,高效稳定冲星' },
{ id: 'srv_2', title: '王者荣耀 · 娱乐开黑陪练', price: 28, unit: '小时', desc: '温柔互动,欢乐开黑' },
],
reviews: [
{ id: 'r1', userNickname: '电竞刺客', userAvatar: 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=100', rating: 5, tags: ['声音甜美', '技术过硬'], content: '小姐姐声音太好听了,操作也很稳,连赢5把!', time: '2小时前' },
{ id: 'r2', userNickname: '峡谷追梦人', userAvatar: 'https://images.unsplash.com/photo-1570295999919-56ceb5ecca61?w=100', rating: 5, tags: ['准时上号', '耐心教学'], content: '很耐心教辅助意识,体验绝佳。', time: '昨天' },
],
},
{
id: 'exp_102',
nickname: '夜鹰狙神',
avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=300&h=300&fit=crop&crop=faces',
coverImage: 'https://images.unsplash.com/photo-1511512578047-dfb367046420?w=800&h=400&fit=crop',
gender: 'male',
age: 23,
game: '和平精英',
rankTag: '无敌战神 · 15KD',
levelTag: '顶流大神',
price: 45,
rating: 5.0,
orderCount: 2360,
isOnline: true,
voiceDuration: 18,
bio: '前职业青训选手,意识拉满,带粉乱杀,绝不白给!',
photos: [
'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=500&fit=crop',
'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=500&fit=crop',
],
services: [
{ id: 'srv_3', title: '和平精英 · 战神四排车队', price: 45, unit: '小时', desc: '战神指引,全程高能护航' },
{ id: 'srv_4', title: '吃鸡指导 · 枪法与身法特训', price: 60, unit: '小时', desc: '1对1定制身法特训' },
],
reviews: [
{ id: 'r3', userNickname: 'AWM战狂', userAvatar: 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=100', rating: 5, tags: ['枪法如神', '落地包稳'], content: '真的太猛了,连吃三把鸡,KD直接飙升!', time: '前天' },
],
},
{
id: 'exp_103',
nickname: '软软喵',
avatar: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=300&h=300&fit=crop&crop=faces',
coverImage: 'https://images.unsplash.com/photo-1538481199705-c710c4e965fc?w=800&h=400&fit=crop',
gender: 'female',
age: 20,
game: '英雄联盟',
rankTag: '傲世宗师 · 中单法王',
levelTag: '新星大神',
price: 30,
rating: 4.8,
orderCount: 650,
isOnline: false,
voiceDuration: 9,
bio: '峡谷高分妹子,阿狸/辛德拉本命,能C能辅,幽默开心果~',
photos: ['https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=500&fit=crop'],
services: [
{ id: 'srv_5', title: 'LOL峡谷排位双排', price: 30, unit: '小时', desc: '双排上分,稳定Carry' },
],
reviews: [],
},
{
id: 'exp_104',
nickname: '瞬移者雷克',
avatar: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=300&h=300&fit=crop&crop=faces',
coverImage: 'https://images.unsplash.com/photo-1550745165-9bc0b252726f?w=800&h=400&fit=crop',
gender: 'male',
age: 22,
game: '无畏契约',
rankTag: '赋能战魂 · 芮娜绝活',
levelTag: '优质陪练',
price: 40,
rating: 4.9,
orderCount: 890,
isOnline: true,
voiceDuration: 15,
bio: '瓦罗兰特高分车头,定位准身法快,报点清晰,带你上神话!',
photos: ['https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=500&fit=crop'],
services: [
{ id: 'srv_6', title: '无畏契约冲分排位', price: 40, unit: '小时', desc: '带队突破,绝不拖泥带水' },
],
reviews: [],
},
];
export const ACTIVITIES_LIST = [
{
id: 'act_101',
title: '【陪吃·铜锅涮肉】周末三里屯地道老北京火锅探店AA局',
game: '美食陪吃',
track: 'OFFLINE_MEAL',
payType: 'AA',
totalFee: 240,
aaFee: 60,
maxParticipants: 4,
currentParticipants: 3,
startTime: '周六 18:30',
location: '北京市朝阳区三里屯聚宝源铜锅涮肉',
creatorNickname: '橙子小七',
creatorAvatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=100',
status: 'RECRUITING',
participants: [
{ id: 'p1', nickname: '涮肉小行家', avatar: 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=100', verifyCode: '8392', checkedIn: false },
{ id: 'p2', nickname: '快乐干饭人', avatar: 'https://images.unsplash.com/photo-1570295999919-56ceb5ecca61?w=100', verifyCode: '6147', checkedIn: true },
{ id: 'p3', nickname: '胡同老饕', avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=100', verifyCode: '9025', checkedIn: false },
],
},
{
id: 'act_102',
title: '【同城组队·高额悬赏】工体欢唱减压局·急招欢唱搭子',
game: '同城陪唱',
track: 'OFFLINE_SING',
payType: 'PROJECT',
pricingMode: 'PER_PERSON',
perPersonFee: 500,
totalFee: 2000,
aaFee: 0,
maxParticipants: 4,
currentParticipants: 3,
allowDynamicPrice: true,
priceAdjustRule: 'UNIFIED_HIGH',
priceAdjustHistory: [],
startTime: '周五 20:00',
location: '北京市朝阳区工人体育场纯K 888豪华大包',
creatorNickname: '苏苏学姐',
creatorAvatar: 'https://images.unsplash.com/photo-1524504388940-b1c1722653e1?w=100',
status: 'RECRUITING',
participants: [
{ id: 'p4', nickname: '情歌王子', avatar: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=100', verifyCode: '2481', checkedIn: true, priceAtJoin: 500, diffCompensated: 0 },
{ id: 'p5', nickname: '高音小王子', avatar: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=100', verifyCode: '7390', checkedIn: false, priceAtJoin: 500, diffCompensated: 0 },
],
},
{
id: 'act_103',
title: '【陪喝·高空下午茶】国贸云端景观双人下午茶拍照闲聊',
game: '下午茶陪喝',
track: 'OFFLINE_DRINK',
payType: 'AA',
totalFee: 196,
aaFee: 98,
maxParticipants: 2,
currentParticipants: 1,
startTime: '周日 14:30',
location: '北京市朝阳区国贸商城高空云端景观咖啡',
creatorNickname: '清芷',
creatorAvatar: 'https://images.unsplash.com/photo-1517841905240-472988babdf9?w=100',
status: 'RECRUITING',
participants: [
{ id: 'p6', nickname: '拿铁爱好者', avatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=100', verifyCode: '5821', checkedIn: false },
],
},
{
id: 'act_104',
title: '【陪吃·炭火烤肉】望京韩国街地道厚切五花肉深夜食堂搭子',
game: '美食陪吃',
track: 'OFFLINE_MEAL',
payType: 'AA',
totalFee: 260,
aaFee: 65,
maxParticipants: 4,
currentParticipants: 2,
startTime: '周六 19:00',
location: '北京市朝阳区望京新天地地道炭火烤肉',
creatorNickname: '桃子汽水',
creatorAvatar: 'https://images.unsplash.com/photo-1544005313-94ddf0286df2?w=100',
status: 'RECRUITING',
participants: [
{ id: 'p7', nickname: '烤肉达人', avatar: 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=100', verifyCode: '3192', checkedIn: false },
],
},
{
id: 'act_105',
title: '【陪喝·精酿微醺】五道营胡同屋顶露台精酿清吧品酒局',
game: '同城微醺',
track: 'OFFLINE_DRINK',
payType: 'PROJECT',
totalFee: 480,
aaFee: 0,
maxParticipants: 4,
currentParticipants: 3,
startTime: '周六 21:00',
location: '北京市东城区五道营胡同露台精酿餐吧',
creatorNickname: '夜鹰',
creatorAvatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=100',
status: 'RECRUITING',
participants: [
{ id: 'p8', nickname: '微醺客', avatar: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=100', verifyCode: '4912', checkedIn: true },
],
},
{
id: 'act_106',
title: '【陪唱·音乐清吧】五道口民谣现场驻唱与合唱拼桌',
game: '同城陪唱',
track: 'OFFLINE_SING',
payType: 'AA',
totalFee: 200,
aaFee: 50,
maxParticipants: 4,
currentParticipants: 4,
startTime: '周日 20:30',
location: '北京市海淀区五道口民谣音乐清吧',
creatorNickname: '月色浅浅',
creatorAvatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=100',
status: 'RUNNING',
participants: [
{ id: 'p9', nickname: '吉他手阿杰', avatar: 'https://images.unsplash.com/photo-1570295999919-56ceb5ecca61?w=100', verifyCode: '7721', checkedIn: true },
{ id: 'p10', nickname: '民谣小调', avatar: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=100', verifyCode: '8832', checkedIn: true },
],
},
];
export const ORDERS_LIST = [
{
id: 'ord_1',
orderSn: 'PW20260916001',
expert: { id: 'exp_101', nickname: '月色浅浅', avatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=100', game: '王者荣耀' },
serviceTitle: '王者荣耀 · 排位包胜率',
quantity: 2,
totalAmount: 70,
status: 'IN_SERVICE',
createdAt: '2026-09-16 15:30',
},
{
id: 'ord_4',
orderSn: 'PW20260916099',
expert: { id: 'exp_105', nickname: '橙子小七', avatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=100', game: '美食陪吃' },
serviceTitle: '同城美食探店 · 火锅/烤肉饭搭子',
quantity: 2,
totalAmount: 136,
status: 'IN_SERVICE',
meetVenue: '朝阳大悦城海底捞',
expenseBurden: 'AA制各自付',
verifyCode: '6829',
createdAt: '2026-09-16 18:00',
},
{
id: 'ord_2',
orderSn: 'PW20260915088',
expert: { id: 'exp_102', nickname: '夜鹰狙神', avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=100', game: '和平精英' },
serviceTitle: '和平精英 · 战神四排车队',
quantity: 1,
totalAmount: 45,
status: 'COMPLETED',
createdAt: '2026-09-15 19:00',
},
{
id: 'ord_3',
orderSn: 'PW20260914032',
expert: { id: 'exp_103', nickname: '软软喵', avatar: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=100', game: '英雄联盟' },
serviceTitle: 'LOL峡谷排位双排',
quantity: 3,
totalAmount: 90,
status: 'PENDING_PAY',
createdAt: '2026-09-14 21:10',
},
];
export const USER_PROFILE = {
id: 'u_888',
nickname: '超能电竞玩家',
avatar: 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=200&h=200&fit=crop&crop=faces',
gender: 'male',
role: 'PLAYER',
balance: 380.5,
frozenBalance: 0,
commission: 128.0,
invitedCount: 16,
isCertifiedExpert: false,
};

查看文件

@@ -491,20 +491,13 @@ const handleChooseLocation = () => {
},
fail: (err) => {
console.warn('[ActivityCreate] 地图选点取消或未授权:', err);
// 若未授权或在H5/开发环境,平滑引导点选推荐公开商圈
if (recommendPublicZones.value.length > 0) {
selectZone(recommendPublicZones.value[0]);
uni.showToast({ title: '已填入推荐商圈', icon: 'none' });
}
uni.showToast({ title: '未选择地点,可手动填写', icon: 'none' });
}
});
// #endif
// #ifndef MP-WEIXIN || APP-PLUS
if (recommendPublicZones.value.length > 0) {
selectZone(recommendPublicZones.value[0]);
uni.showToast({ title: '已为您填入示范商圈', icon: 'none' });
}
uni.showToast({ title: '当前环境不支持地图选点,请手动填写', icon: 'none' });
// #endif
};
@@ -659,7 +652,7 @@ const handleSubmit = async () => {
location: trimmedLocation,
latitude: form.value.latitude,
longitude: form.value.longitude,
creatorNickname: cachedUser.nickname || '同城局长',
creatorNickname: cachedUser.nickname || '同城用户',
creatorAvatar: cachedUser.avatar || 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=150&h=150&fit=crop',
};

查看文件

@@ -52,7 +52,7 @@
</text>
</view>
<text class="dp-status-text">
当前单人出价 ¥{{ activity.perPersonFee || 500 }}/人 · 招募{{ activity.maxParticipants }}人 · 已报名{{ joinedCount }}人
当前单人出价 ¥{{ activity.perPersonFee || 0 }}/人 · 招募{{ activity.maxParticipants }}人 · 已报名{{ joinedCount }}人
</text>
</view>
<view class="dp-btn-group">
@@ -76,9 +76,9 @@
<view class="creator-info btn-active" @click="openHostProfile">
<view class="creator-name-row">
<text class="creator-name">{{ activity.creatorNickname }}</text>
<text class="host-badge">同城局长</text>
<text class="host-badge">发起人</text>
</view>
<text class="creator-sub">同城组局召集人 · 履约守约率 100%</text>
<text class="creator-sub">同城组局召集人</text>
</view>
<button class="btn-chat btn-active" @click.stop="chatWithHost">私聊</button>
</view>
@@ -418,7 +418,8 @@ import {
adjustActivityPrice,
verifyActivityCheckin,
cancelActivity,
leaveActivity
leaveActivity,
normalizeUserId
} from '@/common/api.js';
import { ICONS } from '@/common/icons.js';
import { getCurrentThemeKey, applyThemeToNativeUI } from '@/common/theme.js';
@@ -489,18 +490,18 @@ const stopPropagation = (e) => {
const activity = ref({
id: '',
title: '同城生活组局',
game: '同城陪伴',
title: '',
game: '',
payType: 'AA',
totalFee: 0,
perPersonFee: 0,
maxParticipants: 4,
currentParticipants: 1,
startTime: '根据预约安排',
location: '同城公开商业场所',
creatorId: 1001,
creatorNickname: '局长',
creatorAvatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=100',
maxParticipants: 0,
currentParticipants: 0,
startTime: '',
location: '',
creatorId: 0,
creatorNickname: '',
creatorAvatar: '',
participants: [],
priceAdjustHistory: []
});
@@ -510,9 +511,12 @@ const loadLiveDetail = async (id) => {
const detail = await getActivityDetail(id);
if (detail) {
activity.value = detail;
} else {
uni.showToast({ title: '组局不存在或已下架', icon: 'none' });
}
} catch (err) {
console.warn('[ActivityDetail] 获取详情失败:', err);
uni.showToast({ title: err.message || '组局加载失败', icon: 'none' });
}
};
@@ -559,14 +563,14 @@ const setQuickPrice = (val) => {
};
const joinedCount = computed(() => {
if (activity.value && activity.value.participants) {
if (activity.value && Array.isArray(activity.value.participants)) {
return activity.value.participants.length;
}
return 2;
return 0;
});
const currentPerPerson = computed(() => {
return Number(activity.value.perPersonFee) || 500;
return Number(activity.value.perPersonFee) || 0;
});
const appendQuickOptions = computed(() => {
@@ -631,7 +635,8 @@ const confirmAdjustPrice = async () => {
uni.hideLoading();
} catch (err) {
uni.hideLoading();
console.warn('[ActivityDetail] 远程调价异常,执行本地同步:', err.message);
uni.showToast({ title: err.message || '调价失败,请稍后重试', icon: 'none' });
return;
}
activity.value.perPersonFee = newP;
@@ -686,44 +691,45 @@ const copyLocation = () => {
const openHostProfile = () => {
const hostId = activity.value.creatorId || 1001;
const hostName = activity.value.creatorNickname || '同城局长';
const hostName = activity.value.creatorNickname || '同城用户';
const hostAvatar = activity.value.creatorAvatar || 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=150';
const hostSnapshot = {
id: hostId,
userId: hostId,
nickname: hostName,
avatar: hostAvatar,
rankTag: '同城局长 · 守约率100%',
levelTag: '实名认证',
game: activity.value.game || '同城陪伴',
city: activity.value.location || '北京市 · 朝阳商圈',
bio: `【${activity.value.title || '同城生活组局'}】发起人,历史组局履约守约率 100%,热情好客,欢迎同城搭子一起加入!`,
photos: [hostAvatar]
rankTag: '发起人',
levelTag: '',
game: activity.value.game || '',
city: activity.value.location || '',
bio: activity.value.title ? `【${activity.value.title}】发起人` : '',
photos: hostAvatar ? [hostAvatar] : []
};
uni.setStorageSync('ACTIVE_AUTHOR_PROFILE', hostSnapshot);
uni.navigateTo({
url: `/pages/expert/detail?id=${encodeURIComponent(String(hostId))}&name=${encodeURIComponent(hostName)}&avatar=${encodeURIComponent(hostAvatar)}&tag=${encodeURIComponent('同城局长 · 守约100%')}&city=${encodeURIComponent(hostSnapshot.city)}`
url: `/pages/expert/detail?id=${encodeURIComponent(String(hostId))}&name=${encodeURIComponent(hostName)}&avatar=${encodeURIComponent(hostAvatar)}&tag=${encodeURIComponent('发起人')}&city=${encodeURIComponent(hostSnapshot.city)}`
});
};
const chatWithHost = () => {
uni.showActionSheet({
itemList: [
'复制组局联络专属口令',
'查看局长主页及守约信誉'
'复制组局信息',
'查看发起人主页'
],
success: (res) => {
if (res.tapIndex === 0) {
const hidePhone = uni.getStorageSync('privacy_hide_phone') === '1';
const rawPhone = activity.value.contactPhone || '13888886666';
const displayContact = hidePhone
? rawPhone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2') + '(隐私保护已脱敏)'
: rawPhone;
const tokenText = `【同城组局联络口令】${activity.value.title || '同城组局'} | 时间:${activity.value.startTime || '今天 19:30'} | 地点:${activity.value.location || '公开商圈'} | 联络方式:${displayContact} | 专属核验口令:#JU-${activity.value.id || '101'}-PW`;
const parts = [
`【组局】${activity.value.title || ''}`,
`时间:${activity.value.startTime || '待定'}`,
`地点:${activity.value.location || '待定'}`
];
const contact = String(activity.value.contactPhone || '').trim();
if (contact) parts.push(`联络方式:${contact}`);
uni.setClipboardData({
data: tokenText,
data: parts.join(' | '),
success: () => {
uni.showToast({ title: '专属组局口令已复制', icon: 'success' });
uni.showToast({ title: '组局信息已复制', icon: 'success' });
}
});
} else if (res.tapIndex === 1) {
@@ -737,9 +743,9 @@ const goToMyTicket = (code) => {
if (!ensureLoginOrRedirect(`/pages/activity/detail?id=${currentActId.value}`)) {
return;
}
const c = code || (activity.value.participants && activity.value.participants[0] ? activity.value.participants[0].verifyCode : '8392');
const c = code || ((activity.value.participants || []).find(p => Number(p.userId) === Number(currentUserId.value)) || {}).verifyCode || '';
uni.navigateTo({
url: `/pages/activity/my-ticket?id=${activity.value.id}&code=${c}`
url: `/pages/activity/my-ticket?id=${activity.value.id}${c ? `&code=${encodeURIComponent(c)}` : ''}`
});
};
@@ -851,19 +857,17 @@ const handleJoin = () => {
// 报名前请求一次性订阅授权(组局报名通知)
requestSubscribeFor('activity');
uni.showLoading({ title: '正在报名生成核销码...' });
let code = '8392';
let code = '';
try {
const fee = activity.value.payType === 'AA' ? activity.value.aaFee : 0;
const activeUid = currentUserId.value || Number(uni.getStorageSync('userId'));
const activeUid = normalizeUserId();
const joinRes = await joinActivity(activity.value.id, {
userId: activeUid,
payAmount: fee,
});
if (joinRes && joinRes.verifyCode) {
code = joinRes.verifyCode;
}
code = joinRes && joinRes.verifyCode ? joinRes.verifyCode : '';
uni.hideLoading();
uni.showToast({ title: '报名成功!已生成4位核销码', icon: 'success' });
uni.showToast({ title: '报名成功', icon: 'success' });
setTimeout(() => {
goToMyTicket(code);
}, 1000);

查看文件

@@ -176,12 +176,12 @@ import { ref, computed, onMounted, onUnmounted } from 'vue';
import { onShow, onLoad } from '@dcloudio/uni-app';
import AppTabBar from '@/components/CustomTabBar.vue';
import SkeletonScreen from '@/components/SkeletonScreen.vue';
import { getActivities, getNavCategories } from '@/common/api.js';
import { getActivities, getNavCategories, normalizeUserId } from '@/common/api.js';
import { ICONS } from '@/common/icons.js';
import { getCurrentThemeKey, applyThemeToNativeUI } from '@/common/theme.js';
import { getNavBarInfo } from '@/common/nav.js';
const CURRENT_USER_ID = 1001;
const CURRENT_USER_ID = normalizeUserId();
const isLoading = ref(true);
const activities = ref([]);
@@ -335,7 +335,7 @@ const filteredActivities = computed(() => {
// 2. 支付模式或发单人筛选
let matchTab = true;
if (currentTab.value === 'MINE') {
matchTab = Number(a.creatorId) === CURRENT_USER_ID || a.creatorNickname === '同城局长';
matchTab = Number(a.creatorId) === Number(CURRENT_USER_ID);
} else if (currentTab.value !== 'ALL') {
matchTab = a.payType === currentTab.value;
}

查看文件

@@ -1,6 +1,6 @@
<template>
<view class="ticket-container" :class="['theme-' + currentThemeKey]">
<view class="ticket-card">
<view class="ticket-card" v-if="hasTicket">
<!-- 票据头部 -->
<view class="ticket-header">
<view class="game-badge">{{ ticketData.game }}</view>
@@ -107,22 +107,25 @@
</view>
<!-- 门票操作按钮 -->
<view class="action-wrap">
<view class="action-wrap" v-if="hasTicket">
<button class="btn-cancel-ticket btn-active" v-if="!isCheckedIn" @click="handleCancelTicket">
申请退票退款 (释放席位)
</button>
<button class="btn-verify-demo btn-active" @click="toggleCheckIn">
{{ isCheckedIn ? '重置为待核销状态' : '局长现场核销此门票' }}
</button>
<button class="btn-back btn-active" @click="goBack">返回活动详情</button>
</view>
<view class="ticket-empty" v-else>
<text class="ticket-empty-title">暂无可用门票</text>
<text class="ticket-empty-desc">报名组局后,专属入场核销码会出现在这里</text>
<button class="btn-back btn-active" @click="goBack">去逛同城组局</button>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
import { getActivityDetail, verifyActivityCheckin, getMyActivities, leaveActivity } from '@/common/api.js';
import { getActivityDetail, getMyActivities, leaveActivity, normalizeUserId } from '@/common/api.js';
import { encodeCode128B } from '@/common/barcode.js';
import { generateQRCodeMatrix } from '@/common/qrcode.js';
import { getCurrentThemeKey } from '@/common/theme.js';
@@ -132,28 +135,24 @@ const currentThemeKey = ref(getCurrentThemeKey());
const activeCodeType = ref('BARCODE');
const ticketData = ref({
id: '',
title: '同城组局现场门票',
game: '同城陪伴',
startTime: '根据预约到场',
location: '同城商业街区',
creatorNickname: '组局房主'
title: '',
game: '',
startTime: '',
location: '',
creatorNickname: ''
});
const verifyCode = ref('8392');
const verifyCode = ref('');
const isCheckedIn = ref(false);
const currentTime = ref('');
const hasTicket = ref(true);
const hasTicket = ref(false);
const currentUserId = computed(() => {
try {
return Number(uni.getStorageSync('userId')) || 1001;
} catch (_) {
return 1001;
}
});
const currentUserId = computed(() => normalizeUserId());
const ticketSn = computed(() => {
const actId = ticketData.value && ticketData.value.id ? ticketData.value.id : '101';
return `TICKET-${actId}-${verifyCode.value}`;
const actId = ticketData.value && ticketData.value.id ? ticketData.value.id : '';
const code = String(verifyCode.value || '');
if (!actId || !code) return '';
return `TICKET-${actId}-${code}`;
});
const barcodeSegments = computed(() => {
@@ -184,9 +183,8 @@ const updateTime = () => {
let savedQuery = null;
const loadTicketData = async (query = savedQuery) => {
if (query && query.code) {
verifyCode.value = String(query.code);
}
hasTicket.value = false;
verifyCode.value = query && query.code ? String(query.code) : '';
// 1. 若有指定活动ID,直接拉取该活动详情
if (query && query.id) {
@@ -194,15 +192,13 @@ const loadTicketData = async (query = savedQuery) => {
const detail = await getActivityDetail(query.id);
if (detail) {
ticketData.value = detail;
if (detail.participants && detail.participants.length > 0) {
const found = detail.participants.find(p => p.verifyCode === verifyCode.value);
if (found) {
isCheckedIn.value = Boolean(found.checkedIn);
} else if (!query.code) {
verifyCode.value = detail.participants[0].verifyCode;
isCheckedIn.value = Boolean(detail.participants[0].checkedIn);
}
const uid = Number(currentUserId.value);
const mine = (detail.participants || []).find(p => Number(p.userId) === uid);
if (mine) {
if (mine.verifyCode) verifyCode.value = String(mine.verifyCode);
isCheckedIn.value = Boolean(mine.checkedIn);
}
hasTicket.value = Boolean(detail.id && verifyCode.value);
return;
}
} catch (err) {
@@ -212,43 +208,27 @@ const loadTicketData = async (query = savedQuery) => {
// 2. 若无传参,真实拉取当前用户的已报名组局列表
try {
const uid = currentUserId.value;
const uid = Number(currentUserId.value);
const myActs = await getMyActivities(uid);
const joinedList = myActs.joined || [];
if (joinedList.length > 0) {
let targetAct = joinedList.find(act => {
const myP = (act.participants || []).find(p => Number(p.userId) === uid || p.nickname === '同城局长' || p.verifyCode);
return myP && !myP.checkedIn;
}) || joinedList[0];
const joinedList = (myActs && myActs.joined) || [];
const withTicket = joinedList.filter((act) => {
const mine = (act.participants || []).find(p => Number(p.userId) === uid && p.verifyCode);
return Boolean(mine);
});
if (withTicket.length > 0) {
const targetAct = withTicket.find((act) => {
const mine = (act.participants || []).find(p => Number(p.userId) === uid);
return mine && !mine.checkedIn;
}) || withTicket[0];
const mine = (targetAct.participants || []).find(p => Number(p.userId) === uid) || {};
ticketData.value = targetAct;
if (targetAct.participants && targetAct.participants.length > 0) {
const myP = targetAct.participants.find(p => Number(p.userId) === uid || p.verifyCode);
if (myP && myP.verifyCode) {
verifyCode.value = String(myP.verifyCode);
isCheckedIn.value = Boolean(myP.checkedIn);
} else {
verifyCode.value = String(targetAct.participants[0].verifyCode || '8392');
isCheckedIn.value = Boolean(targetAct.participants[0].checkedIn);
}
}
return;
verifyCode.value = String(mine.verifyCode || '');
isCheckedIn.value = Boolean(mine.checkedIn);
hasTicket.value = Boolean(targetAct.id && verifyCode.value);
}
} catch (err) {
console.warn('[MyTicket] 获取用户报名门票异常:', err);
}
// 3. 兜底拉取线上第一个活动
try {
const defaultDetail = await getActivityDetail('act_101');
if (defaultDetail) {
ticketData.value = defaultDetail;
if (defaultDetail.participants && defaultDetail.participants.length > 0) {
verifyCode.value = String(defaultDetail.participants[0].verifyCode || '8392');
isCheckedIn.value = Boolean(defaultDetail.participants[0].checkedIn);
}
}
} catch (_) {}
};
onLoad(async (query) => {
@@ -270,38 +250,6 @@ onUnmounted(() => {
if (timer) clearInterval(timer);
});
const toggleCheckIn = async () => {
if (!isCheckedIn.value) {
try {
uni.showLoading({ title: '正在核销签到...' });
await verifyActivityCheckin(ticketData.value.id, verifyCode.value, currentUserId.value);
uni.hideLoading();
isCheckedIn.value = true;
uni.showToast({
title: '4位核销码核销成功,已完成现场签到!',
icon: 'success',
duration: 2000
});
} catch (err) {
uni.hideLoading();
// 若线上返回已核销或网络异常,同步状态
isCheckedIn.value = true;
uni.showToast({
title: err.message || '核销完成',
icon: 'none',
duration: 2000
});
}
} else {
isCheckedIn.value = false;
uni.showToast({
title: '状态已重置为待核销',
icon: 'none',
duration: 1500
});
}
};
const handleCancelTicket = () => {
uni.showModal({
title: '申请退票并退款?',
@@ -313,7 +261,12 @@ const handleCancelTicket = () => {
if (res.confirm) {
try {
uni.showLoading({ title: '正在办理退票...' });
const actId = ticketData.value && ticketData.value.id ? ticketData.value.id : 'act_101';
const actId = ticketData.value && ticketData.value.id;
if (!actId) {
uni.hideLoading();
uni.showToast({ title: '门票信息缺失,无法退票', icon: 'none' });
return;
}
await leaveActivity(actId, currentUserId.value);
uni.hideLoading();
uni.showToast({ title: '退票成功,款项已退回钱包', icon: 'success' });
@@ -624,6 +577,34 @@ const goBack = () => {
}
}
.ticket-empty {
margin-top: 80rpx;
display: flex;
flex-direction: column;
align-items: center;
gap: 12rpx;
padding: 60rpx 32rpx;
border-radius: 24rpx;
background: var(--bg-surface, #151D30);
border: 1rpx dashed rgba(148, 163, 184, 0.35);
.ticket-empty-title {
font-size: 30rpx;
font-weight: 600;
color: var(--text-primary, #F8FAFC);
}
.ticket-empty-desc {
font-size: 24rpx;
color: var(--text-muted, #94A3B8);
text-align: center;
}
.btn-back {
margin-top: 20rpx;
}
}
.action-wrap {
margin-top: 48rpx;
display: flex;

查看文件

@@ -23,7 +23,7 @@
</view>
<!-- 多角色体验账号矩阵 (局长/买家/金牌大神) -->
<view class="demo-accounts-card">
<view class="demo-accounts-card" v-if="demoAccounts.length > 0">
<view class="demo-card-head">
<text class="demo-title">多角色体验账号直登</text>
<text class="demo-sub">评审与体验专用 · 一键切换角色</text>
@@ -31,12 +31,12 @@
<view class="demo-list">
<view
v-for="acc in DEMO_ACCOUNTS"
:key="acc.id"
v-for="acc in demoAccounts"
:key="acc.userId"
class="demo-item btn-active"
@click="loginWithDemo(acc)"
>
<image class="demo-avatar" :src="acc.avatar" mode="aspectFill" />
<image class="demo-avatar" :src="acc.avatar || DEFAULT_AVATAR" mode="aspectFill" />
<view class="demo-info">
<view class="demo-name-row">
<text class="demo-name">{{ acc.nickname }}</text>
@@ -69,7 +69,7 @@ import { ref, onMounted } from 'vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
import { ICONS } from '@/common/icons.js';
import { getCurrentThemeKey, applyThemeToNativeUI } from '@/common/theme.js';
import { getPlatformConfigs, loginWithWechatPhone } from '@/common/api.js';
import { getPlatformConfigs, loginWithWechatPhone, getDemoAccounts, demoLogin } from '@/common/api.js';
const APP_LOGO = 'https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?w=200';
const DEFAULT_AVATAR = 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=200';
@@ -107,32 +107,18 @@ const loadProtocolConfig = async () => {
}
};
const DEMO_ACCOUNTS = [
{
id: 1001,
nickname: '晨曦微光局长',
roleTag: 'host',
roleDesc: '房主/局长',
desc: '余额充足 · 发起项目制/AA组局',
avatar: 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=200'
},
{
id: 2001,
nickname: '快乐小买家',
roleTag: 'buyer',
roleDesc: '普通买家',
desc: '搭子报名 · 预约陪伴 · 核销码签到',
avatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=200'
},
{
id: 2006,
nickname: '苏苏学姐',
roleTag: 'expert',
roleDesc: '金牌陪伴师',
desc: '工作台接单 · 雷达抢单 · 现场核销',
avatar: 'https://images.unsplash.com/photo-1524504388940-b1c1722653e1?w=200'
const demoAccounts = ref([]);
const loadingDemoAccounts = ref(false);
const loadDemoAccounts = async () => {
if (loadingDemoAccounts.value) return;
loadingDemoAccounts.value = true;
try {
demoAccounts.value = await getDemoAccounts();
} finally {
loadingDemoAccounts.value = false;
}
];
};
onLoad((options) => {
if (options && options.redirect) {
@@ -148,6 +134,7 @@ onMounted(() => {
currentThemeKey.value = getCurrentThemeKey();
applyThemeToNativeUI(currentThemeKey.value);
loadProtocolConfig();
loadDemoAccounts();
});
onShow(() => {
@@ -281,7 +268,7 @@ const handleGetPhoneNumber = async (e) => {
confirmColor: '#10B981',
success: (mRes) => {
if (mRes.confirm) {
completeLogin(1001);
loginFirstDemoAccount();
}
}
});
@@ -289,10 +276,7 @@ const handleGetPhoneNumber = async (e) => {
}
}
completeLogin(1001, {
nickname: '微信用户',
avatar: DEFAULT_AVATAR
});
loginFirstDemoAccount();
} else {
console.warn('[WeChat Login] 未授权手机号或环境受限:', e?.detail?.errMsg);
uni.showModal({
@@ -303,7 +287,7 @@ const handleGetPhoneNumber = async (e) => {
confirmColor: '#10B981',
success: (res) => {
if (res.confirm) {
completeLogin(1001);
loginFirstDemoAccount();
}
}
});
@@ -311,7 +295,7 @@ const handleGetPhoneNumber = async (e) => {
// #endif
// #ifndef MP-WEIXIN
completeLogin(1001);
loginFirstDemoAccount();
// #endif
};
@@ -321,19 +305,34 @@ const handleFallbackLogin = () => {
uni.showToast({ title: '请先阅读并勾选服务协议', icon: 'none' });
return;
}
completeLogin(1001);
loginFirstDemoAccount();
// #endif
};
const loginWithDemo = (acc) => {
const loginWithDemo = async (acc) => {
if (!agreedProtocol.value) {
uni.showToast({ title: '请先阅读并勾选服务协议', icon: 'none' });
return;
}
completeLogin(acc.id, {
nickname: acc.nickname,
avatar: acc.avatar
});
uni.showLoading({ title: '正在登录...' });
try {
const res = await demoLogin(acc.userId);
uni.hideLoading();
completeLogin(res.userId, res.user || {}, res.userToken || '');
} catch (e) {
uni.hideLoading();
uni.showToast({ title: (e && e.message) || '体验登录失败', icon: 'none' });
}
};
const loginFirstDemoAccount = async () => {
if (!demoAccounts.value.length) await loadDemoAccounts();
const first = demoAccounts.value[0];
if (!first) {
uni.showToast({ title: '体验账号未配置,请联系管理员', icon: 'none' });
return;
}
await loginWithDemo(first);
};
const openUserAgreement = () => {

查看文件

@@ -245,7 +245,7 @@ const normalizeCommentItem = (item) => {
...item,
author: (isMyComment && cur.nickname)
? cur.nickname
: ((item.author && item.author !== '电竞玩家 (我)') ? item.author : (cur.nickname || '同城局长')),
: ((item.author && item.author !== '电竞玩家 (我)') ? item.author : (cur.nickname || '同城用户')),
avatar: (isMyComment && cur.avatar)
? cur.avatar
: ((item.avatar && !item.avatar.includes('photo-1535713875002-d1d0cf377fde'))
@@ -342,19 +342,7 @@ const loadComments = async (id) => {
loadingComments.value = true;
try {
const list = await getPostComments(id);
if (list && list.length > 0) {
commentsList.value = list.map(normalizeCommentItem);
} else {
commentsList.value = [
{
id: `cmt_seed_${id}`,
author: (post.value && post.value.author) || '小甜橙',
avatar: (post.value && post.value.avatar) || 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=120',
content: '欢迎各位小伙伴在评论区留言约伴!一起开黑/探店~',
createdAt: '刚刚'
}
];
}
commentsList.value = Array.isArray(list) ? list.map(normalizeCommentItem) : [];
} catch (_) {
commentsList.value = [];
} finally {
@@ -378,10 +366,8 @@ const openAuthorProfile = (target) => {
const authorName = target.author || target.nickname || '同城达人';
let targetId = target.expertId || target.companionId || target.userId;
if (!targetId) {
if (authorName === '小甜橙') targetId = 2005;
else if (authorName.includes('暗影刺客')) targetId = 2004;
else if (authorName === '苏苏学姐') targetId = 2006;
else targetId = target.id || 101;
uni.showToast({ title: '该用户信息不完整', icon: 'none' });
return;
}
const authorSnapshot = {
id: targetId,
@@ -391,8 +377,8 @@ const openAuthorProfile = (target) => {
rankTag: target.tag || '同城认证达人',
levelTag: '实名认证',
game: target.category === 'game' ? '车队开黑' : (target.category === 'highlight' ? '战绩高光' : '同城搭子'),
city: target.location || '北京市 · 朝阳/海淀',
bio: target.content || '热爱同城探店与组局开黑,欢迎一起约伴同行!',
city: target.location || '',
bio: target.content || '',
photos: Array.isArray(target.images) && target.images.length > 0 ? target.images : [target.avatar || DEFAULT_USER_AVATAR]
};
uni.setStorageSync('ACTIVE_AUTHOR_PROFILE', authorSnapshot);
@@ -481,7 +467,7 @@ const sendComment = async () => {
? currentUserProfile.value
: await syncCurrentUserProfile();
const uid = Number(profile.id || uni.getStorageSync('userId')) || 1001;
const authorName = profile.nickname || '同城局长';
const authorName = profile.nickname || '同城用户';
const authorAvatar = profile.avatar || DEFAULT_USER_AVATAR;
try {
@@ -537,7 +523,7 @@ const handleJoinParty = () => {
? currentUserProfile.value
: await syncCurrentUserProfile();
const uid = Number(profile.id || uni.getStorageSync('userId')) || 1001;
const applicantName = profile.nickname || '同城局长';
const applicantName = profile.nickname || '同城用户';
const applicantAvatar = profile.avatar || DEFAULT_USER_AVATAR;
try {
await applyPostParty(post.value.id, {

查看文件

@@ -59,6 +59,10 @@
<!-- 动态信息流列表 -->
<view class="posts-list">
<view v-if="filteredPosts.length === 0" class="posts-empty">
<text class="posts-empty-title">还没有动态</text>
<text class="posts-empty-desc">发第一条同城搭子动态,约上附近的小伙伴</text>
</view>
<view
v-for="(post, index) in filteredPosts"
:key="post.id"
@@ -399,7 +403,7 @@ import { ICONS } from '@/common/icons.js';
import { getCurrentThemeKey, applyThemeToNativeUI } from '@/common/theme.js';
import { ensureLoginOrRedirect } from '@/common/role.js';
import { getNavBarInfo } from '@/common/nav.js';
import { getPosts, createPost, deletePost, likePost, viewPost, getPostComments, addPostComment, applyPostParty, getPostApplies, handlePostApply, uploadImage, getPlatformConfigs, getUserProfile } from '@/common/api.js';
import { getPosts, createPost, deletePost, likePost, viewPost, getPostComments, addPostComment, applyPostParty, getPostApplies, handlePostApply, uploadImage, getPlatformConfigs, getUserProfile, normalizeUserId } from '@/common/api.js';
const DEFAULT_USER_AVATAR = 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=120';
const isLoading = ref(true);
@@ -529,7 +533,7 @@ onUnmounted(() => {
const loadRemotePosts = async () => {
try {
const list = await getPosts(currentTab.value);
if (list && list.length > 0) {
{
let localLikedIds = uni.getStorageSync('COMMUNITY_LIKED_POST_IDS') || [];
if (!Array.isArray(localLikedIds)) localLikedIds = [];
let localAppliedIds = uni.getStorageSync('COMMUNITY_APPLIED_POST_IDS') || [];
@@ -537,7 +541,7 @@ const loadRemotePosts = async () => {
let viewsMap = uni.getStorageSync('COMMUNITY_POST_VIEWS_MAP') || {};
if (typeof viewsMap !== 'object' || viewsMap === null) viewsMap = {};
posts.value = list.map(item => {
posts.value = (list || []).map(item => {
const strId = String(item.id);
const serverStatus = item.myApplyStatus || null;
const isApplied = serverStatus ? (serverStatus !== 'REJECTED') : localAppliedIds.includes(strId);
@@ -610,75 +614,8 @@ const syncInitialPostStates = (rawPosts) => {
};
const syncInitialLikedState = syncInitialPostStates;
const posts = ref(syncInitialPostStates([
{
id: 1,
userId: 2005,
author: '小甜橙',
avatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=150',
tag: '火锅达人',
tagType: 'warm-tag',
location: '朝阳区 · 三里屯太古里',
distance: '800m',
time: '15分钟前',
content: '今晚7点半有没有在三里屯附近的小伙伴一起去吃铜锅涮肉?已预订包间,目前已有2人,再来2个饭搭子AA!吃完还可以顺路去清吧听歌放松~',
topics: ['三里屯探店', '铜锅涮肉AA', '饭搭子招募'],
images: [
'https://images.unsplash.com/photo-1555396273-367ea4eb4db5?w=500',
'https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?w=500'
],
actionText: '加入车队',
likes: 42,
liked: false,
comments: 8,
views: 310,
category: 'partner'
},
{
id: 2,
userId: 2004,
author: '暗影刺客 (省一李白)',
avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=150',
tag: '国服大神',
tagType: 'cool-tag',
location: '海淀区 · 五道口',
distance: '3.5km',
time: '40分钟前',
content: '今晚王者荣耀荣耀王者冲分车队!目前缺一个听指挥的中单或者软辅妹子,全程语音沟通包赢不保段包赔,想上星的速速滴我!',
topics: ['王者荣耀', '荣耀车队', '连胜冲分'],
images: [
'https://images.unsplash.com/photo-1542751371-adc38448a05e?w=500'
],
actionText: '立即上车',
likes: 76,
liked: true,
comments: 15,
views: 680,
category: 'game'
},
{
id: 3,
userId: 2006,
author: '苏苏学姐',
avatar: 'https://images.unsplash.com/photo-1517841905240-472988babdf9?w=150',
tag: '金牌麦霸',
tagType: 'gold-tag',
location: '朝阳区 · 工体商业街',
distance: '1.2km',
time: '1小时前',
content: '工体纯K量贩欢唱局!甜美情歌、粤语老歌、摇滚气氛拉满。现场备有精美果盘与特饮,今晚欢唱不嗨不散,还有专属歌声试听哦!',
topics: ['KTV欢唱', '量贩陪唱', '同城聚会'],
images: [
'https://images.unsplash.com/photo-1516450360452-9312f5e86fc7?w=500'
],
actionText: '报名同去',
likes: 58,
liked: false,
comments: 11,
views: 520,
category: 'partner'
}
]));
const posts = ref([]);
const filteredPosts = computed(() => {
if (currentTab.value === 'all') return posts.value;
@@ -761,19 +698,7 @@ const loadPostCommentsList = async (post) => {
loadingComments.value = true;
try {
const list = await getPostComments(post.id);
if (list && list.length > 0) {
commentsList.value = list;
} else {
commentsList.value = [
{
id: `cmt_seed_${post.id}`,
author: post.author || '小甜橙',
avatar: post.avatar || 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=120',
content: '欢迎各位小伙伴在评论区留言约伴!一起开黑/探店~',
createdAt: '刚刚'
}
];
}
commentsList.value = Array.isArray(list) ? list : [];
} catch (_) {
commentsList.value = [];
} finally {
@@ -786,10 +711,8 @@ const openAuthorProfile = (target) => {
const authorName = target.author || target.nickname || '同城达人';
let targetId = target.expertId || target.companionId || target.userId;
if (!targetId) {
if (authorName === '小甜橙') targetId = 2005;
else if (authorName.includes('暗影刺客')) targetId = 2004;
else if (authorName === '苏苏学姐') targetId = 2006;
else targetId = target.id || 101;
uni.showToast({ title: '该用户信息不完整', icon: 'none' });
return;
}
const authorSnapshot = {
id: targetId,
@@ -799,8 +722,8 @@ const openAuthorProfile = (target) => {
rankTag: target.tag || '同城认证达人',
levelTag: '实名认证',
game: target.category === 'game' ? '车队开黑' : (target.category === 'highlight' ? '战绩高光' : '同城搭子'),
city: target.location || '北京市 · 朝阳/海淀',
bio: target.content || '热爱同城探店与组局开黑,欢迎一起约伴同行!',
city: target.location || '',
bio: target.content || '',
photos: Array.isArray(target.images) && target.images.length > 0 ? target.images : [target.avatar || DEFAULT_USER_AVATAR]
};
uni.setStorageSync('ACTIVE_AUTHOR_PROFILE', authorSnapshot);
@@ -866,7 +789,7 @@ const sendComment = async () => {
? currentUserProfile.value
: await syncCurrentUserProfile();
const uid = Number(profile.id || uni.getStorageSync('userId')) || 1001;
const authorName = profile.nickname || '同城局长';
const authorName = profile.nickname || '同城用户';
const authorAvatar = profile.avatar || DEFAULT_USER_AVATAR;
try {
@@ -1010,7 +933,7 @@ const handleJoinParty = (post) => {
? currentUserProfile.value
: await syncCurrentUserProfile();
const uid = Number(profile.id || uni.getStorageSync('userId')) || 1001;
const applicantName = profile.nickname || '同城局长';
const applicantName = profile.nickname || '同城用户';
const applicantAvatar = profile.avatar || DEFAULT_USER_AVATAR;
try {
await applyPostParty(post.id, {
@@ -1063,9 +986,9 @@ const publishCategories = [
const newPostForm = ref({
category: 'partner',
content: '',
selectedTopics: ['三里屯探店'],
selectedTopics: [],
images: [],
location: '朝阳区 · 三里屯太古里'
location: ''
});
const contentPlaceholder = computed(() => {
@@ -1173,9 +1096,9 @@ const submitPublishPost = async () => {
const profile = currentUserProfile.value.nickname
? currentUserProfile.value
: await syncCurrentUserProfile();
const uid = Number(profile.id || uni.getStorageSync('userId')) || 1001;
const authorName = profile.nickname || '同城局长';
const authorAvatar = profile.avatar || DEFAULT_USER_AVATAR;
const uid = normalizeUserId();
const authorName = profile.nickname || '同城用户';
const authorAvatar = profile.avatar || '';
let finalImages = [];
try {
@@ -1183,14 +1106,12 @@ const submitPublishPost = async () => {
finalImages = await Promise.all(
newPostForm.value.images.map(img => uploadImage(img))
);
} else {
finalImages = ['https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?w=500'];
}
} catch (uploadErr) {
console.warn('[Community] 图片上传异常,保留原地址:', uploadErr);
finalImages = newPostForm.value.images.length > 0
? newPostForm.value.images
: ['https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?w=500'];
console.warn('[Community] 图片上传失败:', uploadErr);
uni.hideLoading();
uni.showToast({ title: '图片上传失败,请重试后再发布', icon: 'none' });
return;
}
const postPayload = {
@@ -1209,15 +1130,18 @@ const submitPublishPost = async () => {
try {
const res = await createPost(postPayload);
if (!res || !res.id) {
throw new Error('发布失败:服务端未返回动态编号');
}
const createdPost = {
id: res ? res.id : Date.now(),
id: res.id,
userId: uid,
author: (res && res.author) || postPayload.author,
avatar: (res && res.avatar) || postPayload.avatar,
tag: currentMeta.text,
tagType: currentMeta.type,
location: postPayload.location,
distance: '100m内',
distance: '',
time: '刚刚',
content: content,
topics: postPayload.topics,
@@ -1706,6 +1630,29 @@ const submitPublishPost = async () => {
flex-direction: column;
gap: 24rpx;
.posts-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10rpx;
padding: 80rpx 24rpx;
border-radius: 24rpx;
border: 1rpx dashed rgba(255, 255, 255, 0.12);
background: rgba(255, 255, 255, 0.03);
.posts-empty-title {
font-size: 28rpx;
font-weight: 600;
color: #E2E8F0;
}
.posts-empty-desc {
font-size: 24rpx;
color: #94A3B8;
}
}
.post-card {
background: #151D30;
border-radius: 24rpx;

查看文件

@@ -291,45 +291,24 @@ const stopPropagation = (e) => {
const isLoading = ref(true);
const isFav = ref(false);
const buildDefaultServices = (gameTitle = '同城陪伴', basePrice = 68) => [
{
id: 'srv_1',
name: `${gameTitle} · 基础陪伴 (2小时)`,
title: `${gameTitle} · 基础陪伴 (2小时)`,
price: basePrice,
unit: '小时',
desc: '正规公开商圈同城陪伴,AA制或协商消费'
},
{
id: 'srv_2',
name: `${gameTitle} · 半日畅享 (3小时)`,
title: `${gameTitle} · 半日畅享 (3小时)`,
price: Math.round(basePrice * 2.8),
unit: '次',
desc: '深度互动与陪伴交流,赠送专属点歌或语音祝福'
}
];
const expert = ref({
id: '101',
nickname: '同城陪伴师',
avatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=300',
gender: 'female',
id: '',
nickname: '',
avatar: '',
gender: '',
levelTag: '',
game: '同城陪伴',
game: '',
rankTag: '',
city: '',
rating: 0,
orderCount: 0,
isOnline: false,
isOnline: false,
bio: '',
voiceDuration: 0,
price: 0,
services: [],
photos: [
'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=400',
'https://images.unsplash.com/photo-1517841905240-472988babdf9?w=400'
],
photos: [],
reviews: []
});
// 是否为真实大神档案(非合成的普通用户资料)
@@ -374,31 +353,32 @@ const toggleFavorite = async () => {
};
const handleReplyReview = (rev) => {
const targetId = Number(expert.value.id || currentExpertTargetId || 101);
const targetId = Number(expert.value.id || currentExpertTargetId || 0);
if (!ensureLoginOrRedirect(`/pages/expert/detail?id=${targetId}`)) {
return;
}
const defaultReplies = [
'感谢老板的支持与五星好评!开黑配合非常默契,期待下次一起上大分!',
'谢谢老板认可!非常准时礼貌的高素质玩家老板,下次继续开黑走起~',
'多谢老板认可!服务过程非常愉快,有空随时来约局呀!'
];
uni.showActionSheet({
itemList: defaultReplies,
success: async (res) => {
const selectedReply = defaultReplies[res.tapIndex] || defaultReplies[0];
uni.showModal({
title: '回复评价',
editable: true,
placeholderText: '请输入回复内容',
confirmText: '提交回复',
success: async (modalRes) => {
const reply = String(modalRes.content || '').trim();
if (!modalRes.confirm) return;
if (!reply) {
uni.showToast({ title: '回复内容不能为空', icon: 'none' });
return;
}
try {
uni.showLoading({ title: '正在提交回复...' });
await replyOrderReview(rev.id, selectedReply);
rev.replyContent = selectedReply;
await replyOrderReview(rev.id, reply);
rev.replyContent = reply;
rev.replyTime = '刚刚';
uni.hideLoading();
uni.showToast({ title: '感谢回复已送达', icon: 'success' });
uni.showToast({ title: '回复已提交', icon: 'success' });
} catch (err) {
uni.hideLoading();
rev.replyContent = selectedReply;
rev.replyTime = '刚刚';
uni.showToast({ title: '感谢回复已送达', icon: 'success' });
uni.showToast({ title: err.message || '回复提交失败', icon: 'none' });
}
}
});
@@ -431,12 +411,12 @@ onLoad(async (query) => {
const snapAvatar = queryAvatar || (matchedSnapshot && matchedSnapshot.avatar) || expert.value.avatar;
const snapTag = queryTag || (matchedSnapshot && matchedSnapshot.rankTag) || expert.value.rankTag;
const snapCity = queryCity || (matchedSnapshot && matchedSnapshot.city) || expert.value.city;
const snapGame = (matchedSnapshot && matchedSnapshot.game) || '同城搭子';
const snapGame = (matchedSnapshot && matchedSnapshot.game) || '';
const snapBio = (matchedSnapshot && matchedSnapshot.bio) || expert.value.bio;
const snapPhotos = (matchedSnapshot && Array.isArray(matchedSnapshot.photos) && matchedSnapshot.photos.length > 0)
? matchedSnapshot.photos
: [snapAvatar];
const snapPrice = snapGame === '车队开黑' ? 88 : 128;
: (snapAvatar ? [snapAvatar] : []);
const snapPrice = Number(expert.value.price) || 0;
expert.value = {
...expert.value,
@@ -445,13 +425,13 @@ onLoad(async (query) => {
avatar: snapAvatar,
coverImage: snapPhotos[0] || snapAvatar,
rankTag: snapTag,
levelTag: '实名认证',
levelTag: '',
game: snapGame,
city: snapCity,
bio: snapBio,
price: snapPrice,
photos: snapPhotos,
services: buildDefaultServices(snapGame, snapPrice)
services: []
};
if (expert.value.services && expert.value.services.length > 0) {
selectedServiceId.value = expert.value.services[0].id;
@@ -553,19 +533,9 @@ const toggleVoice = () => {
const chatOnline = () => {
uni.showActionSheet({
itemList: [
`复制大神游戏开黑专属房号`,
`联系 7x24h 官方在线客服`
],
itemList: [`联系 7x24h 官方在线客服`],
success: (res) => {
if (res.tapIndex === 0) {
uni.setClipboardData({
data: `PW-${expert.value.id}-${Math.floor(1000 + Math.random() * 9000)}`,
success: () => {
uni.showToast({ title: '专属房号已复制', icon: 'success' });
}
});
} else if (res.tapIndex === 1) {
uni.showActionSheet({
itemList: [
'拨打官方服务热线 (400-888-6666)',

查看文件

@@ -9,7 +9,7 @@
<text class="expert-name">{{ expert.nickname }}</text>
<text class="pro-badge" :class="{ offline: !isOnline }">{{ isOnline ? '接单中' : '已打烊' }}</text>
</view>
<text class="rating-sub">评分 {{ expert.rating || '5.0' }} · 接单率 {{ expert.orderRate || '99.2%' }}</text>
<text class="rating-sub">评分 {{ expert.rating || '暂无' }} · 接单率 {{ expert.orderRate || '—' }}</text>
</view>
</view>
@@ -243,14 +243,13 @@ const getInitialExpert = () => {
try {
cached = uni.getStorageSync('PW_USER_INFO') || uni.getStorageSync('userInfo');
} catch (_) {}
let uid = (cached && cached.id) || uni.getStorageSync('userId') || 2006;
if (uid == 101) uid = 2006;
const uid = (cached && cached.id) || Number(uni.getStorageSync('userId')) || 0;
return {
id: uid,
nickname: (cached && cached.nickname) || '接单大神',
avatar: (cached && cached.avatar) || DEFAULT_EXPERT_AVATAR,
rating: (cached && cached.rating) || '5.0',
orderRate: (cached && cached.orderRate) || '99.2%'
nickname: (cached && cached.nickname) || '',
avatar: (cached && cached.avatar) || '',
rating: (cached && cached.rating) || '',
orderRate: (cached && cached.orderRate) || ''
};
};
@@ -272,9 +271,9 @@ const navStyle = computed(() => {
});
const todayStats = ref({
income: '380.00',
orders: 4,
waitingVerify: 1
income: '0.00',
orders: 0,
waitingVerify: 0
});
const onThemeChanged = (newTheme) => {

查看文件

@@ -265,9 +265,6 @@
</view>
</view>
<view class="quick-demo" @click="fillDemoCode">
<text>点击自动填入演示码 [8392] 并触发核销</text>
</view>
</view>
</view>
@@ -497,9 +494,9 @@ import {
const currentThemeKey = ref(getCurrentThemeKey());
const expert = ref({
id: 101,
nickname: '认证大神',
avatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=300'
id: 0,
nickname: '',
avatar: ''
});
const isReceivingOrders = ref(uni.getStorageSync('EXPERT_RECEIVING_STATUS') !== '0');
const todayIncome = ref('0.00');
@@ -519,35 +516,7 @@ const rawTrendsData = ref({ daily: [], weekly: [], monthly: [] });
const activeTooltip = ref(null);
const currentTrendsList = computed(() => {
const list = rawTrendsData.value[trendPeriod.value] || [];
if (list.length > 0) return list;
if (trendPeriod.value === 'daily') {
return [
{ label: '10-07', income: 120, orders: 3, avgPrice: 40 },
{ label: '10-08', income: 85, orders: 2, avgPrice: 42.5 },
{ label: '10-09', income: 160, orders: 4, avgPrice: 40 },
{ label: '10-10', income: 210, orders: 5, avgPrice: 42 },
{ label: '10-11', income: 140, orders: 3, avgPrice: 46.7 },
{ label: '10-12', income: 290, orders: 6, avgPrice: 48.3 },
{ label: '10-13', income: 180, orders: 4, avgPrice: 45 }
];
} else if (trendPeriod.value === 'weekly') {
return [
{ label: '3周前', income: 950, orders: 22, avgPrice: 43.2 },
{ label: '2周前', income: 1120, orders: 25, avgPrice: 44.8 },
{ label: '上周', income: 1350, orders: 28, avgPrice: 48.2 },
{ label: '本周', income: 890, orders: 19, avgPrice: 46.8 }
];
} else {
return [
{ label: '5月', income: 3200, orders: 75, avgPrice: 42.7 },
{ label: '6月', income: 3800, orders: 86, avgPrice: 44.2 },
{ label: '7月', income: 4500, orders: 98, avgPrice: 45.9 },
{ label: '8月', income: 4200, orders: 90, avgPrice: 46.7 },
{ label: '9月', income: 5100, orders: 105, avgPrice: 48.6 },
{ label: '10月', income: 2800, orders: 58, avgPrice: 48.3 }
];
}
return rawTrendsData.value[trendPeriod.value] || [];
});
const currentPeriodSummary = computed(() => {
@@ -813,63 +782,22 @@ const showConfirmModal = ref(false);
const verifiedParticipant = ref({
nickname: '电竞萌新小可爱',
avatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=150&h=150&fit=crop',
activityTitle: 'AA制五排王者百星车队',
code: '8392',
fee: '25.00'
nickname: '',
avatar: '',
activityTitle: '',
code: '',
fee: '0.00'
});
const poolOrders = ref([
{
id: 'pool_101',
game: '王者荣耀',
price: 35,
desc: 'QQ区/星耀1晋级赛冲王者/需开黑配合/射手或辅助',
countdown: 48
},
{
id: 'pool_102',
game: '和平精英',
price: 45,
desc: '微信区/四排海岛/刚枪上分/kd保4以上',
countdown: 32
},
{
id: 'pool_103',
game: '美食陪吃',
price: 68,
desc: '朝阳大悦城海底捞/周末饭搭子/消费现场AA/需守时礼貌',
countdown: 56
}
]);
const poolOrders = ref([]);
const activeOrders = ref([
{
id: 'ord_active_1',
buyerName: '超能电竞玩家',
buyerAvatar: 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=150&h=150&fit=crop',
serviceTitle: '排位包胜率',
hours: 2,
totalAmount: 70,
statusText: '正在开黑对局履约中'
},
{
id: 'ord_active_2',
buyerName: '北京吃货小张',
buyerAvatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=150&h=150&fit=crop',
serviceTitle: '火锅探店饭搭子',
hours: 2,
totalAmount: 136,
statusText: '已核销4位码入座,服务中'
}
]);
const activeOrders = ref([]);
const toggleReceivingStatus = async () => {
const next = !isReceivingOrders.value;
const newStatusStr = next ? '接单中' : '休息中';
try {
await updateCompanionProfile(expert.value.id || 101, { status: newStatusStr });
await updateCompanionProfile(currentExpertId(), { status: newStatusStr });
} catch (err) {
uni.showToast({
title: (err && err.message) || '接单状态切换失败,请稍后重试',
@@ -905,16 +833,12 @@ const clearCode = () => {
isCodeError.value = false;
};
const fillDemoCode = () => {
inputDigits.value = [8, 3, 9, 2];
verifyInputCode();
};
const loadWorkspaceOrders = async () => {
try {
// 先解析真实大神档案,避免工作台使用演示 id
await loadExpertProfile();
const eid = currentExpertId() || 101;
const eid = currentExpertId();
if (!eid) return;
const stats = await getExpertStats(eid);
if (stats) {
if (stats.income !== undefined) todayIncome.value = stats.income;
@@ -950,7 +874,7 @@ const loadWorkspaceOrders = async () => {
}
try {
const trends = await getExpertRevenueTrends(currentExpertId() || 101);
const trends = await getExpertRevenueTrends(currentExpertId());
if (trends && (trends.daily || trends.weekly || trends.monthly)) {
rawTrendsData.value = trends;
}
@@ -1002,11 +926,11 @@ const verifyInputCode = async () => {
}
verifiedParticipant.value = {
id: info.id,
nickname: info.nickname || '同城搭子',
avatar: info.avatar || 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=150',
activityTitle: info.title || '同城生活组局',
nickname: info.nickname || '同城用户',
avatar: info.avatar || '',
activityTitle: info.title || '',
code: info.code,
fee: info.fee || '50.00',
fee: info.fee !== undefined && info.fee !== null ? String(info.fee) : '0.00',
};
isCodeError.value = false;
showConfirmModal.value = true;
@@ -1025,7 +949,7 @@ const confirmCheckin = async () => {
const codeStr = verifiedParticipant.value.code;
uni.showLoading({ title: '确认核销中...' });
try {
await universalVerifyCheckin(codeStr, 1001);
await universalVerifyCheckin(codeStr, normalizeUserId());
uni.hideLoading();
showConfirmModal.value = false;
const gainedFee = Number(verifiedParticipant.value.fee) || 25;
@@ -1048,7 +972,7 @@ const confirmCheckin = async () => {
const grabOrder = async (order) => {
try {
await claimOrder(order.id, {
expertId: currentExpertId() || 101,
expertId: currentExpertId(),
companionName: '我 (接单大神)',
expertAvatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=150'
});
@@ -1077,7 +1001,7 @@ const handleRejectOrder = (order) => {
if (res.confirm) {
try {
await rejectOrder(order.id, {
expertId: expert.value.id || 101,
expertId: currentExpertId(),
reason: '大神当前档期冲突拒单'
});
} catch (err) {
@@ -1102,19 +1026,9 @@ const handleRejectOrder = (order) => {
const chatWithBuyer = () => {
uni.showActionSheet({
itemList: [
'复制开黑联络码',
'联系 7x24h 官方客服'
],
itemList: ['联系 7x24h 官方客服'],
success: (res) => {
if (res.tapIndex === 0) {
uni.setClipboardData({
data: `BUYER-${Math.floor(100000 + Math.random() * 900000)}`,
success: () => {
uni.showToast({ title: '联络码已复制', icon: 'success' });
}
});
} else if (res.tapIndex === 1) {
uni.showActionSheet({
itemList: [
'拨打官方服务热线 (400-888-6666)',
@@ -1151,7 +1065,7 @@ const completeOrder = (id) => {
success: async (res) => {
if (res.confirm) {
try {
await applyCompleteOrder(id, { expertId: expert.value.id || 101 });
await applyCompleteOrder(id, { expertId: currentExpertId() });
} catch (err) {
uni.showToast({
title: (err && err.message) || '提交失败,请稍后重试',

查看文件

@@ -149,7 +149,7 @@
<view class="rank-badge" v-else>{{ expert.rankTag }}</view>
<!-- 语音/歌声试听条 (矢量SVG播放图标,带播放高亮光晕) -->
<view class="voice-bar btn-active" :class="{ playing: playingId === expert.id, 'song-bar': expert.voiceType === 'song' }" @click.stop="toggleVoice(expert.id)">
<view class="voice-bar btn-active" :class="{ playing: playingId === expert.id, 'song-bar': expert.voiceType === 'song' }" @click.stop="toggleVoice(expert)">
<image class="voice-icon-svg" :src="playingId === expert.id ? ICONS.pause : (expert.voiceType === 'song' ? ICONS.micVocal : ICONS.play)" mode="aspectFit" />
<text class="voice-text">{{ playingId === expert.id ? '播放中...' : (expert.voiceTitle || expert.voiceDuration + 's 原声试听') }}</text>
<view class="wave-lines" v-if="playingId === expert.id">
@@ -468,6 +468,7 @@ onShow(() => {
onUnmounted(() => {
uni.$off('theme-change', onThemeChanged);
try { if (homeVoiceAudio) { homeVoiceAudio.destroy(); homeVoiceAudio = null; } } catch (_) {}
});
const filteredExperts = computed(() => {
@@ -584,16 +585,19 @@ const startInstantMatch = () => {
if (matchTimer) clearTimeout(matchTimer);
matchTimer = setTimeout(() => {
isMatching.value = false;
// 优先匹配当前列表中在线的大神,若无则取首位
// 按真实在线状态与距离推荐(有 distanceKm 时取最近),不做随机抽取
const onlineList = experts.value.filter(e => e.isOnline);
if (onlineList.length > 0) {
matchedExpert.value = onlineList[Math.floor(Math.random() * onlineList.length)];
} else if (experts.value.length > 0) {
matchedExpert.value = experts.value[0];
} else {
matchedExpert.value = null;
}
}, 1200);
const candidates = onlineList.length > 0 ? onlineList : experts.value;
const sorted = [...candidates].sort((a, b) => {
const da = Number(a.distanceKm);
const db = Number(b.distanceKm);
if (Number.isFinite(da) && Number.isFinite(db)) return da - db;
if (Number.isFinite(da)) return -1;
if (Number.isFinite(db)) return 1;
return 0;
});
matchedExpert.value = sorted[0] || null;
}, 600);
};
const closeMatchModal = () => {
@@ -613,16 +617,31 @@ const confirmMatchedOrder = () => {
}
};
const toggleVoice = (id) => {
if (playingId.value === id) {
playingId.value = null;
} else {
playingId.value = id;
uni.showToast({ title: '正在试听打招呼语音', icon: 'none' });
setTimeout(() => {
if (playingId.value === id) playingId.value = null;
}, 4000);
let homeVoiceAudio = null;
const toggleVoice = (expertItem) => {
const id = expertItem && expertItem.id !== undefined ? expertItem.id : expertItem;
const url = expertItem && expertItem.voiceIntroUrl;
if (!url) {
uni.showToast({ title: '该大神还未上传语音介绍', icon: 'none' });
return;
}
if (playingId.value === id) {
try { homeVoiceAudio && homeVoiceAudio.stop(); } catch (_) {}
playingId.value = null;
return;
}
try { homeVoiceAudio && homeVoiceAudio.stop(); } catch (_) {}
if (!homeVoiceAudio) {
homeVoiceAudio = uni.createInnerAudioContext();
homeVoiceAudio.onEnded(() => { playingId.value = null; });
homeVoiceAudio.onError(() => {
playingId.value = null;
uni.showToast({ title: '语音播放失败', icon: 'none' });
});
}
homeVoiceAudio.src = url;
homeVoiceAudio.play();
playingId.value = id;
};
const goToExpert = (id) => {

查看文件

@@ -286,7 +286,7 @@ import { ensureLoginOrRedirect } from '@/common/role.js';
const currentThemeKey = ref(getCurrentThemeKey());
const currentUserId = ref(Number(uni.getStorageSync('userId') || 1001));
const currentUserName = ref((uni.getStorageSync('userInfo') && uni.getStorageSync('userInfo').nickname) || '超能电竞玩家');
const currentUserName = ref((uni.getStorageSync('userInfo') && uni.getStorageSync('userInfo').nickname) || '同城用户');
const userBalance = ref('0.00');
const payMethod = ref('WECHAT'); // 'WECHAT' | 'BALANCE'
@@ -346,25 +346,25 @@ const loadUserWallet = async () => {
const expert = ref({
id: '',
nickname: '同城陪伴师',
avatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=150',
game: '同城陪伴',
price: 68,
nickname: '',
avatar: '',
game: '',
price: 0,
services: []
});
const selectedService = ref(null);
// 模板安全视图:即便服务包尚未加载完成,也不会出现 null.title 崩溃
// 模板安全视图:服务包未加载时展示中性占位,不伪造价格/服务名
const serviceView = computed(() => selectedService.value || {
id: 'base',
title: '同城陪伴基础服务',
price: Number(expert.value && expert.value.price) || 35,
id: '',
title: '暂未选择服务',
price: 0,
unit: '小时'
});
const hours = ref(2);
const timeType = ref('NOW');
const discount = ref(0);
const remark = ref('');
const meetVenue = ref('朝阳大悦城海底捞');
const meetVenue = ref('');
const meetCoords = ref({ latitude: null, longitude: null });
// 下单模式:EXPERT=指定大神,PLATFORM=平台派单(发布需求,附近大神抢单)
@@ -425,7 +425,13 @@ const isOffline = computed(() => {
});
onLoad(async (query) => {
const targetId = (query && query.expertId) ? query.expertId : '1';
const targetId = (query && query.expertId) ? String(query.expertId) : '';
if (!targetId) {
// 未指定大神:直接进入平台派单模式
orderMode.value = 'PLATFORM';
await loadLiveCoupons();
return;
}
try {
const liveExpert = await getCompanionDetail(targetId);
if (liveExpert) {
@@ -440,33 +446,11 @@ onLoad(async (query) => {
if (!selectedService.value && services.length > 0) {
selectedService.value = services[0];
}
// 兜底:大神尚未配置服务包时,按其基础时薪提供"基础陪伴服务",保证仍可下单
// (价格仍以服务端大神档案价为准,createOrder 会二次校验)
if (!selectedService.value) {
selectedService.value = {
id: 'base',
title: `${liveExpert.game || '同城陪伴'}基础陪伴服务`,
price: Number(liveExpert.price) || 35,
unit: '小时',
desc: '大神暂未配置服务包,按基础时薪计费'
};
}
}
} catch (err) {
console.warn('[OrderCreate] 获取线上大神详情异常:', err);
}
// 极端兜底:详情获取失败时也要保证不会因空服务崩溃
if (!selectedService.value) {
selectedService.value = {
id: 'base',
title: '同城陪伴基础服务',
price: Number(expert.value.price) || 35,
unit: '小时',
desc: ''
};
}
// 加载后台真实可用优惠券
await loadLiveCoupons();
});
@@ -579,6 +563,10 @@ const handlePay = async () => {
if (!ensureLoginOrRedirect('/pages/order/create')) {
return;
}
if (!isPlatformMode.value && !selectedService.value) {
uni.showToast({ title: '该大神暂未配置服务包,无法下单', icon: 'none' });
return;
}
// 平台派单校验:品类预算与碰面地点
if (isPlatformMode.value) {
if (!(Number(platformBudget.value) > 0)) {
@@ -605,7 +593,7 @@ const handlePay = async () => {
const usingUserCoupon = currentCoupon.value && currentCoupon.value.id && currentCoupon.value.id !== 'c_none';
const basePayload = {
buyerId: uid,
clientName: currentUserName.value || '超能电竞玩家',
clientName: currentUserName.value || '同城用户',
expertId: isPlatform ? 0 : expert.value.id,
companionName: isPlatform ? '平台派单' : expert.value.nickname,
expertAvatar: isPlatform ? '' : expert.value.avatar,

查看文件

@@ -355,7 +355,8 @@ import {
submitOrderReview as submitOrderReviewApi,
submitReport as submitReportApi,
requestWechatPayment,
uploadImage
uploadImage,
normalizeUserId
} from '@/common/api.js';
import { ICONS } from '@/common/icons.js';
import { getCurrentThemeKey, applyThemeToNativeUI } from '@/common/theme.js';
@@ -547,16 +548,9 @@ const payOrder = async (id) => {
const contactExpert = (name) => {
uni.showActionSheet({
itemList: [`复制【${name}】陪玩开黑房间号`, '联系 7x24h 官方电竞客服'],
itemList: ['联系 7x24h 官方电竞客服'],
success: (res) => {
if (res.tapIndex === 0) {
uni.setClipboardData({
data: `PW-${Math.floor(100000 + Math.random() * 900000)}`,
success: () => {
uni.showToast({ title: '开黑房号已复制', icon: 'success' });
}
});
} else {
uni.showActionSheet({
itemList: [
'拨打官方服务热线 (400-888-6666)',
@@ -593,7 +587,7 @@ const getOrderCountdown = (order) => {
if (!isNaN(parsed)) createdAtTs = parsed;
}
if (!createdAtTs) {
createdAtTs = Date.now();
return '待支付';
}
const expireTs = createdAtTs + 15 * 60 * 1000;
const diffSec = Math.max(0, Math.floor((expireTs - nowTick.value) / 1000));
@@ -885,17 +879,23 @@ const toggleTag = (tag) => {
const submitReview = async () => {
if (!targetReviewOrder.value) return;
const content = reviewForm.value.content.trim();
if (!content) {
uni.showToast({ title: '请填写评价内容', icon: 'none' });
return;
}
submittingReview.value = true;
try {
const order = targetReviewOrder.value;
const expertId = order.expert ? order.expert.id : (order.expertId || 101);
const expertId = order.expert ? order.expert.id : (order.expertId || 0);
const cachedUser = uni.getStorageSync('userInfo') || {};
await submitOrderReviewApi(order.id, {
expertId,
userId: Number(uni.getStorageSync('userId') || 1001),
userNickname: '电竞玩家 (我)',
userId: normalizeUserId(),
userNickname: cachedUser.nickname || '',
rating: reviewForm.value.rating,
tags: reviewForm.value.selectedTags,
content: reviewForm.value.content.trim() || '大神技术很棒,履约准时,体验绝佳!'
content
});
order.isReviewed = true;

查看文件

@@ -429,7 +429,7 @@ const loadCouponUsable = async () => {
couponUsable.value = 0;
}
};
const cacheSizeText = ref('1.25 MB');
const cacheSizeText = ref('0 KB');
// 分销推广管理
const showDistributionModal = ref(false);
@@ -446,32 +446,7 @@ const distributionRecords = ref([]);
const distWithdrawAmount = ref('');
const submittingDistWithdraw = ref(false);
const DEMO_ACCOUNTS = [
{
id: 1001,
nickname: '同城局长',
roleTag: 'host',
roleDesc: '房主/局长',
desc: '余额充足 · 组局发单',
avatar: 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=200'
},
{
id: 2001,
nickname: '超能电竞玩家',
roleTag: 'buyer',
roleDesc: '普通买家',
desc: '搭子报名 · 预约下单',
avatar: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=200'
},
{
id: 2006,
nickname: '苏苏学姐',
roleTag: 'expert',
roleDesc: '金牌陪伴师',
desc: '接单工作台 · 4位核销',
avatar: 'https://images.unsplash.com/photo-1524504388940-b1c1722653e1?w=200'
}
];
const user = ref({
id: initialUserId || '',
@@ -1075,7 +1050,7 @@ const updateCacheSize = () => {
cacheSizeText.value = `${kb} KB`;
}
} catch (_) {
cacheSizeText.value = '1.25 MB';
cacheSizeText.value = '0 KB';
}
};
@@ -1150,7 +1125,18 @@ const copyInviteLink = () => {
};
const savePosterImage = () => {
uni.showToast({ title: '海报已保存至相册', icon: 'success' });
const uid = normalizeUserId();
if (!uid) {
uni.showToast({ title: '请先登录后再分享', icon: 'none' });
return;
}
const link = `https://peiww.gynhc.com/invite?code=PW${uid}`;
uni.setClipboardData({
data: link,
success: () => {
uni.showToast({ title: '邀请链接已复制,可粘贴到微信分享', icon: 'none', duration: 2200 });
}
});
};
const submitDistWithdraw = async () => {

查看文件

@@ -37,7 +37,7 @@
<view class="dist-stat-divider"></view>
<view class="dist-stat-col">
<view class="code-box btn-active" @click="copyInviteCode">
<text class="code-txt">PW{{ user.id || 2006 }}</text>
<text class="code-txt">PW{{ user.id || '' }}</text>
<text class="copy-tag">一键复制</text>
</view>
<text class="dist-stat-lbl">专属邀请码</text>
@@ -77,7 +77,7 @@
<view class="poster-footer-code">
<text class="lbl">邀请码:</text>
<text class="val">PW{{ user.id || 2006 }}</text>
<text class="val">PW{{ user.id || '' }}</text>
</view>
</view>
@@ -218,7 +218,8 @@ import {
getDistributionTeam,
getDistributionRecords,
withdrawCommission,
bindInviteCode
bindInviteCode,
normalizeUserId
} from '@/common/api.js';
import { ICONS } from '@/common/icons.js';
import { getCurrentThemeKey, applyThemeToNativeUI } from '@/common/theme.js';
@@ -232,11 +233,11 @@ const bindCodeInput = ref('');
const defaultAvatar = 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=200';
const user = ref({
id: 2006,
nickname: '苏苏学姐',
id: 0,
nickname: '',
avatar: defaultAvatar,
commission: '0.00',
invitedCount: 5
invitedCount: 0
});
const distActiveTab = ref('poster');
@@ -250,25 +251,14 @@ const tabs = [
];
const distributionTeam = ref({
directCount: 3,
indirectCount: 2,
totalTeamCount: 5,
directList: [
{ id: 3001, nickname: '晨光玩家', avatar: 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=200', level: 1, createdAt: '2026-10-08 14:20', totalContribute: '48.50' },
{ id: 3002, nickname: '风暴骑士', avatar: 'https://images.unsplash.com/photo-1570295999919-56ceb5ecca61?w=200', level: 1, createdAt: '2026-10-09 11:35', totalContribute: '32.00' },
{ id: 3003, nickname: '月下独酌', avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=200', level: 1, createdAt: '2026-10-10 09:12', totalContribute: '19.80' }
],
indirectList: [
{ id: 4001, nickname: '极光闪电', avatar: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=200', level: 2, createdAt: '2026-10-11 16:45', totalContribute: '12.60' },
{ id: 4002, nickname: '晴空万里', avatar: 'https://images.unsplash.com/photo-1522075469751-3a6694fb2f61?w=200', level: 2, createdAt: '2026-10-12 18:02', totalContribute: '8.40' }
]
directCount: 0,
indirectCount: 0,
totalTeamCount: 0,
directList: [],
indirectList: []
});
const distributionRecords = ref([
{ id: 'REC_101', orderNo: 'ORD_2026101201', fromUserName: '晨光玩家', level: 1, ratePercent: 5, commission: '25.00', orderAmount: '500.00', status: '已入账', createdAt: '2026-10-12 20:30' },
{ id: 'REC_102', orderNo: 'ORD_2026101102', fromUserName: '风暴骑士', level: 1, ratePercent: 5, commission: '16.00', orderAmount: '320.00', status: '已入账', createdAt: '2026-10-11 19:15' },
{ id: 'REC_103', orderNo: 'ORD_2026101003', fromUserName: '极光闪电', level: 2, ratePercent: 2, commission: '6.40', orderAmount: '320.00', status: '已入账', createdAt: '2026-10-10 15:00' }
]);
const distributionRecords = ref([]);
const distWithdrawAmount = ref('');
const submittingDistWithdraw = ref(false);
@@ -311,7 +301,7 @@ const handleBack = () => {
};
const copyInviteCode = () => {
const code = `PW${user.value.id || 2006}`;
const code = `PW${normalizeUserId()}`;
uni.setClipboardData({
data: code,
success: () => {
@@ -321,7 +311,7 @@ const copyInviteCode = () => {
};
const copyInviteLink = () => {
const link = `https://peiww.gynhc.com/invite?code=PW${user.value.id || 2006}`;
const link = `https://peiww.gynhc.com/invite?code=PW${normalizeUserId()}`;
uni.setClipboardData({
data: link,
success: () => {
@@ -331,7 +321,18 @@ const copyInviteLink = () => {
};
const savePosterImage = () => {
uni.showToast({ title: '海报已保存至相册', icon: 'success' });
const uid = normalizeUserId();
if (!uid) {
uni.showToast({ title: '请先登录后再分享', icon: 'none' });
return;
}
const link = `https://peiww.gynhc.com/invite?code=PW${uid}`;
uni.setClipboardData({
data: link,
success: () => {
uni.showToast({ title: '邀请链接已复制,可粘贴到微信分享', icon: 'none', duration: 2200 });
}
});
};
const handleBindInviteCode = async () => {
@@ -341,7 +342,7 @@ const handleBindInviteCode = async () => {
return;
}
try {
const uid = user.value.id || 2006;
const uid = normalizeUserId();
const res = await bindInviteCode(uid, code);
user.value.inviterId = res.inviterId;
bindCodeInput.value = '';
@@ -381,7 +382,7 @@ const submitDistWithdraw = async () => {
submittingDistWithdraw.value = true;
uni.showLoading({ title: '正在提交申请...' });
try {
const uid = user.value.id || 2006;
const uid = normalizeUserId();
await withdrawCommission(uid, amount, { channel: 'WECHAT' });
user.value.commission = (currentComm - amount).toFixed(2);
distWithdrawAmount.value = '';
@@ -397,7 +398,7 @@ const submitDistWithdraw = async () => {
};
const loadData = async () => {
const uid = uni.getStorageSync('userId') || 2006;
const uid = normalizeUserId();
try {
const [profile, teamData, records] = await Promise.all([
getUserProfile(uid).catch(() => null),

查看文件

@@ -219,26 +219,17 @@ import { onShow } from '@dcloudio/uni-app';
import { ICONS } from '@/common/icons.js';
import { getCurrentThemeKey, applyThemeToNativeUI } from '@/common/theme.js';
import { getNavBarInfo } from '@/common/nav.js';
import { getUserProfile, rechargeBalance, getRechargeTiers, requestWechatPayment } from '@/common/api.js';
import { getUserProfile, rechargeBalance, getRechargeTiers, requestWechatPayment, normalizeUserId } from '@/common/api.js';
const currentThemeKey = ref(getCurrentThemeKey());
const navInfo = ref(getNavBarInfo());
const currentUserId = ref(1001);
const currentUserId = ref(0);
const userBalance = ref('0.00');
// 预设阶梯列表 (自带高质量官方预置,支持后台动态更新)
const DEFAULT_TIERS = [
{ id: 1, amount: 50, bonusAmount: 0, tag: '基础充值' },
{ id: 2, amount: 100, bonusAmount: 5, tag: '热门畅享' },
{ id: 3, amount: 200, bonusAmount: 15, tag: '超值加赠' },
{ id: 4, amount: 500, bonusAmount: 50, tag: '尊享特惠' },
{ id: 5, amount: 1000, bonusAmount: 120, tag: '土豪专享' },
{ id: 6, amount: 2000, bonusAmount: 300, tag: '超神狂欢' }
];
const tierList = ref([...DEFAULT_TIERS]);
const selectedTierIndex = ref(1); // 默认选中 100 元 (热门畅享)
// 充值档位由后台配置(recharge_tiers),小程序端不写死
const tierList = ref([]);
const selectedTierIndex = ref(0);
const isCustomMode = ref(false);
const customAmountText = ref('');
const isInputFocused = ref(false);
@@ -352,7 +343,7 @@ const clearCustomAmount = () => {
// 加载充值档位与用户数据
const loadData = async () => {
try {
const uid = uni.getStorageSync('userId') || 1001;
const uid = normalizeUserId();
currentUserId.value = uid;
const profile = await getUserProfile(uid);
if (profile && profile.balance !== undefined) {
@@ -405,7 +396,7 @@ const handleConfirmRecharge = async () => {
uni.showLoading({ title: '正在调起微信支付...', mask: true });
try {
const uid = currentUserId.value || uni.getStorageSync('userId') || 1001;
const uid = currentUserId.value || normalizeUserId();
await requestWechatPayment({
amount: payAmount,
title: `钱包余额充值 ¥${payAmount}`,
@@ -416,12 +407,14 @@ const handleConfirmRecharge = async () => {
const res = await rechargeBalance(uid, payAmount, bonus);
uni.hideLoading();
// 更新本地资产与全局缓存
// 余额以服务端返回为准;未返回则重新拉取,不在本地伪造入账
if (res && res.balance !== undefined) {
userBalance.value = String(res.balance);
} else {
const cur = parseFloat(userBalance.value) || 0;
userBalance.value = (cur + payAmount + bonus).toFixed(2);
const fresh = await getUserProfile(uid).catch(() => null);
if (fresh && fresh.balance !== undefined) {
userBalance.value = String(fresh.balance);
}
}
const userInfo = uni.getStorageSync('userInfo') || {};

查看文件

@@ -266,13 +266,13 @@ const getFeedbackText = (rpt) => {
if (rpt.handleNote || rpt.handleResult) {
return rpt.handleNote || rpt.handleResult;
}
if (rpt.status === '已核实处罚' || rpt.status === '已处置') {
return '平台已核实违规事实并对涉事账号或内容采取限制处罚措施,感谢您共同维护社区环境。';
}
if (rpt.status === '已驳回') {
return '经安全专员核查,暂未发现明显违规行为;若有补充凭证可随时联系官方在线客服申诉。';
return '该举报未被采纳';
}
return '平台安全专员正在核实处理中,预计 24 小时内同步处置结果。';
if (rpt.status === '已核实处罚' || rpt.status === '已处置') {
return '已处理';
}
return '处理中';
};
const handleCopyReportNo = (id) => {

查看文件

@@ -144,7 +144,7 @@
<view class="reject-title-row">
<text class="reject-title">审核未通过说明:</text>
</view>
<text class="reject-reason">{{ item.reason || '收款信息不完整或账户异常,请核对后重试。' }}</text>
<text class="reject-reason">{{ item.reason || '未填写驳回原因' }}</text>
<button class="btn-re-withdraw btn-active" @click="goToWithdraw">重新发起申请</button>
</view>
</view>

查看文件

@@ -352,7 +352,7 @@ const handleSubmitWithdraw = async () => {
channel: 'WECHAT'
};
const res = await withdrawCommission(1001, amt, payload);
const res = await withdrawCommission(normalizeUserId(), amt, payload);
uni.hideLoading();
isSubmitting.value = false;

查看文件

@@ -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);

查看文件

@@ -1,119 +1,37 @@
// 已读取AGENTS.md
import * as fs from 'fs';
import * as path from 'path';
describe('ActivityListContent (TDD 测试套件: 活动大厅陪吃·陪唱·陪喝内容与零Emoji校验)', () => {
const rootDir = path.resolve(__dirname, '../../');
const mockFilePath = path.join(rootDir, 'pwuniapp', 'common', 'mock.js');
const listVuePath = path.join(rootDir, 'pwuniapp', 'pages', 'activity', 'list.vue');
const emojiRegex = /(?:\p{Extended_Pictographic}|\p{Emoji_Presentation}|[\u2600-\u27BF])/u;
/**
* 活动大厅内容规范(第九十六批改写)
* 背景:演示用 mock.js 已删除,活动数据统一来自数据库(GET /api/activities)。
* 本套件改为校验:不再内置 mock、页面真实取数、无 Emoji、关键字段展示完整。
*/
describe('ActivityListContent(活动大厅真实数据与内容规范)', () => {
const root = path.resolve(__dirname, '../../');
const pagePath = path.join(root, 'pwuniapp', 'pages', 'activity', 'list.vue');
const page = fs.readFileSync(pagePath, 'utf-8');
const mockPath = path.join(root, 'pwuniapp', 'common', 'mock.js');
describe('1. mock.js 中 ACTIVITIES_LIST 内容规范校验', () => {
it('活动列表中陪吃·陪唱·陪喝三大核心品类应占主导地位 (>= 80%)', () => {
const mockContent = fs.readFileSync(mockFilePath, 'utf-8');
// 提取 ACTIVITIES_LIST 数组对象文本
const listMatch = mockContent.match(/export const ACTIVITIES_LIST\s*=\s*(\[[\s\S]*?\n\];)/);
expect(listMatch).not.toBeNull();
// 解析执行提取对象
const evalList = new Function(`return ${listMatch![1].replace(/;\s*$/, '')}`)();
expect(Array.isArray(evalList)).toBe(true);
expect(evalList.length).toBeGreaterThanOrEqual(4);
const mainCategories = ['美食陪吃', '同城陪唱', '下午茶陪喝', '同城微醺'];
const mainCategoryCount = evalList.filter((item: any) =>
mainCategories.includes(item.game || item.category)
).length;
const mainRatio = mainCategoryCount / evalList.length;
expect(mainRatio).toBeGreaterThanOrEqual(0.8);
});
it('所有活动必须包含合法公开场所、时间与发起人信息', () => {
const mockContent = fs.readFileSync(mockFilePath, 'utf-8');
const listMatch = mockContent.match(/export const ACTIVITIES_LIST\s*=\s*(\[[\s\S]*?\n\];)/);
const evalList = new Function(`return ${listMatch![1].replace(/;\s*$/, '')}`)();
for (const item of evalList) {
expect(item.id).toBeDefined();
expect(item.title).toBeDefined();
expect(item.location).toBeDefined();
expect(item.startTime).toBeDefined();
expect(item.creatorNickname).toBeDefined();
expect(item.creatorAvatar).toBeDefined();
expect(['AA', 'PROJECT']).toContain(item.payType);
// 验证地点均为公开合法商圈/门店,不能有私人住宅等违规词
expect(item.location).not.toMatch(/(酒店客房|私人住宅|私密公寓)/);
}
});
it('ACTIVITIES_LIST 数据中严禁包含任何系统 Emoji 字符', () => {
const mockContent = fs.readFileSync(mockFilePath, 'utf-8');
const listMatch = mockContent.match(/export const ACTIVITIES_LIST\s*=\s*(\[[\s\S]*?\n\];)/);
expect(listMatch).not.toBeNull();
const listContent = listMatch![1];
const lines = listContent.split('\n');
const violations: string[] = [];
lines.forEach((line, idx) => {
if (emojiRegex.test(line)) {
violations.push(`Line ${idx + 1}: ${line.trim()}`);
}
});
expect(violations).toEqual([]);
});
it('不得再内置活动 mock 数据', () => {
expect(fs.existsSync(mockPath)).toBe(false);
expect(page).not.toContain('ACTIVITIES_LIST');
expect(page).not.toContain("from '@/common/mock");
});
describe('2. pages/activity/list.vue 页面规范校验', () => {
it('活动列表页面模板与样式中严禁包含任何 Emoji 字符', () => {
const content = fs.readFileSync(listVuePath, 'utf-8');
const lines = content.split('\n');
const violations: string[] = [];
lines.forEach((line, idx) => {
if (emojiRegex.test(line)) {
violations.push(`Line ${idx + 1}: ${line.trim()}`);
}
});
expect(violations).toEqual([]);
});
it('页面应包含陪吃·陪唱·陪喝相关的分类快捷切换或展示逻辑', () => {
const content = fs.readFileSync(listVuePath, 'utf-8');
// 验证包含美食陪吃、同城陪唱等核心品类关键词
expect(content).toMatch(/(美食陪吃|陪吃)/);
expect(content).toMatch(/(同城陪唱|陪唱)/);
expect(content).toMatch(/(下午茶陪喝|陪喝)/);
});
it('活动列表必须从服务端取数', () => {
expect(page).toContain('getActivities');
expect(page).toContain('activities.value = list');
});
describe('3. 活动大厅综合搜索框功能与双主题规范门禁 (TDD)', () => {
it('页面必须包含顶部综合搜索框组件并支持关键词双向绑定', () => {
const content = fs.readFileSync(listVuePath, 'utf-8');
expect(content).toContain('header-search');
expect(content).toContain('search-input');
expect(content).toContain('searchKeyword');
});
it('活动卡片展示地点、开始时间与发起人字段', () => {
expect(page).toContain('location');
expect(page).toContain('startTime');
expect(page).toContain('creatorNickname');
});
it('搜索过滤逻辑必须同时支持标题、商圈地点、品类与局长多维度匹配', () => {
const content = fs.readFileSync(listVuePath, 'utf-8');
// 必须包含基于 searchKeyword 的模糊匹配逻辑
expect(content).toContain('matchKeyword');
expect(content).toMatch(/a\.title.*includes/);
expect(content).toMatch(/a\.location.*includes/);
expect(content).toMatch(/a\.creatorNickname.*includes/);
});
it('搜索框在 theme-warm 浅色暖阳晨曦模式下必须适配纯白背景与深色文本', () => {
const content = fs.readFileSync(listVuePath, 'utf-8');
const warmSection = content.split('.activity-container.theme-warm')[1] || '';
expect(warmSection).toContain('.header-search');
expect(warmSection).toContain('.search-box');
expect(warmSection).toContain('#FFFFFF');
});
it('页面严禁包含任何系统 Emoji 字符', () => {
const emojiRegex = /(?:\p{Extended_Pictographic}|\p{Emoji_Presentation}|[\u2600-\u27BF])/u;
expect(emojiRegex.test(page)).toBe(false);
});
});

查看文件

@@ -517,7 +517,8 @@ describe('ContentSecurityService & Compliance Architecture (TDD 微信内容安
expect(content).toContain('open-type="getPhoneNumber"');
expect(content).toContain('@getphonenumber="handleGetPhoneNumber"');
expect(content).toContain('一键授权快捷登录');
expect(content).toContain('DEMO_ACCOUNTS');
expect(content).toContain('getDemoAccounts');
expect(content).toContain('demoLogin');
expect(content).toContain('用户服务协议');
expect(content).toContain('隐私保护指引');
// 验证已彻底移除登录大门前强制填写的头像昵称卡片,保障成熟商业极速登录体验
@@ -819,15 +820,16 @@ describe('ContentSecurityService & Compliance Architecture (TDD 微信内容安
expect(content).toContain('WHERE id = ?');
});
it('仓储层与路由必须针对预设用户 2006 与 101 提供兜底配置防止出现 500/404', () => {
it('严禁在服务端内置演示用户兜底(测试数据必须落库)', () => {
const repoPath = path.join(rootDir, 'server', 'src', 'db', 'repository.ts');
const repoContent = fs.readFileSync(repoPath, 'utf-8');
expect(repoContent).toContain('if (id === 2006)');
expect(repoContent).toContain('苏苏学姐');
expect(repoContent).not.toContain('if (id === 2006)');
expect(repoContent).not.toContain('糖心小糯米');
const serverIndexPath = path.join(rootDir, 'server', 'src', 'index.ts');
const serverContent = fs.readFileSync(serverIndexPath, 'utf-8');
expect(serverContent).toContain('userId === 2006');
expect(serverContent).not.toContain('userId === 2006');
expect(serverContent).not.toContain('userId === 101');
});
it('相关文件严禁包含任何系统 Emoji 违规字符', () => {

查看文件

@@ -102,14 +102,11 @@ describe('UI/UX 规范门禁 (TDD 测试套件: 全面杜绝系统 Emoji)', () =
expect(textStyle).toContain('text-overflow: ellipsis');
});
it('mock.js 中试听标题长度必须精简且不得超过12个字,防止在小屏过早被截断', () => {
it('演示数据必须来自数据库,禁止在小程序内置 mock.js', () => {
const mockJsPath = path.join(rootDir, 'pwuniapp', 'common', 'mock.js');
const mockContent = fs.readFileSync(mockJsPath, 'utf-8');
const voiceTitleMatches = mockContent.matchAll(/voiceTitle:\s*['"]([^'"]+)['"]/g);
for (const match of voiceTitleMatches) {
const title = match[1];
expect(title.length).toBeLessThanOrEqual(12);
}
expect(fs.existsSync(mockJsPath)).toBe(false);
const apiContent = fs.readFileSync(path.join(rootDir, 'pwuniapp', 'common', 'api.js'), 'utf-8');
expect(apiContent).not.toContain("common/mock");
});
});

查看文件

@@ -22,9 +22,9 @@ describe('第八十二批服务包空值防护闸门', () => {
expect(createPage).not.toContain('selectedService.unit');
});
it('下单页在无服务包时按大神基础时薪兜底,保证仍可下单', () => {
expect(createPage).toContain("id: 'base'");
expect(createPage).toContain('基础陪伴服务');
it('下单页在无服务包时必须阻止下单,不得伪造基础服务', () => {
expect(createPage).not.toContain("id: 'base'");
expect(createPage).toContain('该大神暂未配置服务包,无法下单');
expect(createPage).toContain('Array.isArray(liveExpert.services) ? liveExpert.services : []');
});

查看文件

@@ -91,7 +91,7 @@ describe('订单与活动资金一致性护栏 (报名超卖 / 抢单 CAS / 评
it('小程序评价必须提交真实登录用户ID,严禁硬编码 1001 绕过买家校验', () => {
const vue = fs.readFileSync(pwuniappOrderListPath, 'utf-8');
expect(vue).toContain("userId: Number(uni.getStorageSync('userId') || 1001),");
expect(vue).toContain('userId: normalizeUserId(),');
});
});
});

查看文件

@@ -11,7 +11,7 @@ describe('第七十三批公开配置隐私闸门', () => {
const repo = fs.readFileSync(path.join(root, 'server/src/db/repository.ts'), 'utf-8');
it('公开配置排除风控词库与地图密钥配置', () => {
expect(repo).toContain("const PUBLIC_EXCLUDED_KEYS = new Set(['security_words', 'map_location_config'])");
expect(repo).toContain("const PUBLIC_EXCLUDED_KEYS = new Set(['security_words', 'map_location_config', 'demo_accounts'])");
expect(repo).toContain('if (PUBLIC_EXCLUDED_KEYS.has(r.configKey)) continue;');
});