feat(companion): 全面扩展同城生活陪伴体系(陪吃·陪喝·陪玩),完成TDD测试与安全核销闭环

这个提交包含在:
wikiboss
2026-09-16 20:12:54 +08:00
父节点 a90cd3c0d9
当前提交 341023c77b
共有 15 个文件被更改,包括 972 次插入 和 23 次删除

查看文件

@@ -0,0 +1,94 @@
/**
* 陪伴服务赛道与品类管理核心
* 支持【线上游戏陪玩】与【同城线下陪玩·陪吃·陪喝】多维服务形态
*/
export enum CompanionTrack {
ONLINE_GAME = 'ONLINE_GAME', // 线上游戏陪玩 (王者/和平/LOL等)
OFFLINE_PLAY = 'OFFLINE_PLAY', // 线下同城陪玩 (桌游/剧本杀/运动/漫展)
OFFLINE_MEAL = 'OFFLINE_MEAL', // 线下同城陪吃 (网红探店/火锅/餐厅饭搭子)
OFFLINE_DRINK = 'OFFLINE_DRINK', // 线下同城陪喝 (咖啡下午茶/茶室清吧)
}
export enum ExpenseBurdenType {
AA_SHARE = 'AA_SHARE', // 现场消费 AA 制各自付
BUYER_TREAT = 'BUYER_TREAT', // 客户全包请客
SELF_CARE = 'SELF_CARE', // 大神自理
}
export interface ServiceItem {
id: string;
track: CompanionTrack;
title: string;
gameName?: string;
city?: string;
venueType?: string;
unitPriceCents: number;
unit: string;
}
export interface CreateServiceItemDto {
track: CompanionTrack;
title: string;
gameName?: string;
city?: string;
venueType?: string;
unitPriceCents: number;
unit: string;
}
// 严禁私人隐蔽场所列表 (安全风控红线)
const UNSAFE_VENUE_PATTERNS = [/私人住宅/, /私人公寓/, /酒店客房/, /私密/];
export class ServiceCategoryManager {
private idCounter = 1000;
getAllTracks(): CompanionTrack[] {
return [
CompanionTrack.ONLINE_GAME,
CompanionTrack.OFFLINE_PLAY,
CompanionTrack.OFFLINE_MEAL,
CompanionTrack.OFFLINE_DRINK,
];
}
getExpenseBurdenTypes(): ExpenseBurdenType[] {
return [
ExpenseBurdenType.AA_SHARE,
ExpenseBurdenType.BUYER_TREAT,
ExpenseBurdenType.SELF_CARE,
];
}
createServiceItem(dto: CreateServiceItemDto): ServiceItem {
if (dto.track === CompanionTrack.ONLINE_GAME) {
if (!dto.gameName || dto.gameName.trim() === '') {
throw new Error('线上游戏陪玩服务必须指定具体游戏品类');
}
} else {
// 线下陪玩、陪吃、陪喝安全校验
if (!dto.city || dto.city.trim() === '') {
throw new Error('线下陪伴服务必须指定服务城市');
}
if (dto.venueType) {
for (const pattern of UNSAFE_VENUE_PATTERNS) {
if (pattern.test(dto.venueType)) {
throw new Error('安全风控触发: 线下陪伴服务仅限在合法的公开商业场所履约,禁止私人住宅或私密空间');
}
}
}
}
return {
id: `srv_${++this.idCounter}`,
track: dto.track,
title: dto.title,
gameName: dto.gameName,
city: dto.city,
venueType: dto.venueType,
unitPriceCents: dto.unitPriceCents,
unit: dto.unit,
};
}
}