feat(core): 按照严格TDD模式完成活动项目(4位核销码)、订单状态机、财务分销与双身份切换核心领域服务及30项单元测试

这个提交包含在:
wikiboss
2026-09-16 15:30:23 +08:00
父节点 7f9da8aec3
当前提交 0708f4736a
共有 12 个文件被更改,包括 3386 次插入 和 0 次删除

查看文件

@@ -0,0 +1,133 @@
export enum ActivityPayType {
AA_PAY = 1, // AA制付费
PROJECT_PAY = 2, // 项目制付费
}
export interface Activity {
id: string;
creatorId: number;
title: string;
gameCategory: string;
payType: ActivityPayType;
totalFeeCents?: number;
aaFeeCents?: number;
maxParticipants: number;
currentParticipants: number;
}
export interface Participant {
id: string;
activityId: string;
userId: number;
payAmountCents: number;
verifyCode: string; // 4位纯数字核销码
checkinStatus: boolean;
verifiedAt?: Date;
verifiedBy?: number;
}
export class ActivityService {
private activities = new Map<string, Activity>();
private participants = new Map<string, Participant[]>(); // activityId -> participants
createActivity(params: {
creatorId: number;
title: string;
gameCategory: string;
payType: ActivityPayType;
totalFeeCents?: number;
aaFeeCents?: number;
maxParticipants: number;
}): Activity {
const id = `act_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
const activity: Activity = {
id,
creatorId: params.creatorId,
title: params.title,
gameCategory: params.gameCategory,
payType: params.payType,
totalFeeCents: params.totalFeeCents,
aaFeeCents: params.aaFeeCents,
maxParticipants: params.maxParticipants,
currentParticipants: 0,
};
this.activities.set(id, activity);
this.participants.set(id, []);
return activity;
}
joinActivity(activityId: string, params: { userId: number; payAmountCents: number }): Participant {
const activity = this.activities.get(activityId);
if (!activity) {
throw new Error('活动不存在');
}
const currentList = this.participants.get(activityId) || [];
if (activity.maxParticipants > 0 && currentList.length >= activity.maxParticipants) {
throw new Error('活动报名人数已满');
}
const alreadyJoined = currentList.some((p) => p.userId === params.userId);
if (alreadyJoined) {
throw new Error('用户已报名该活动');
}
// 生成当前活动内唯一的4位数字核销码 (1000-9999)
const existingCodes = new Set(currentList.map((p) => p.verifyCode));
let verifyCode = '';
for (let i = 0; i < 100; i++) {
const code = String(Math.floor(1000 + Math.random() * 9000));
if (!existingCodes.has(code)) {
verifyCode = code;
break;
}
}
if (!verifyCode) {
throw new Error('核销码生成失败,池已满');
}
const participant: Participant = {
id: `p_${Date.now()}_${Math.floor(Math.random() * 1000)}`,
activityId,
userId: params.userId,
payAmountCents: params.payAmountCents,
verifyCode,
checkinStatus: false,
};
currentList.push(participant);
activity.currentParticipants = currentList.length;
return participant;
}
verifyCheckin(params: {
activityId: string;
operatorId: number;
verifyCode: string;
}): { success: boolean; participant: Participant } {
const activity = this.activities.get(params.activityId);
if (!activity) {
throw new Error('活动不存在');
}
if (activity.creatorId !== params.operatorId) {
throw new Error('无权核销该活动的核销码');
}
const currentList = this.participants.get(params.activityId) || [];
const participant = currentList.find((p) => p.verifyCode === params.verifyCode);
if (!participant) {
throw new Error('无效或不存在的核销码');
}
if (participant.checkinStatus) {
throw new Error('该核销码已被核销,不可重复使用');
}
participant.checkinStatus = true;
participant.verifiedAt = new Date();
participant.verifiedBy = params.operatorId;
return { success: true, participant };
}
}

查看文件

@@ -0,0 +1,80 @@
export enum UserRoleFlag {
NONE = 0,
PLAYER = 1 << 0, // 1
EXPERT = 1 << 1, // 2
}
export enum WorkMode {
PLAYER = 'PLAYER',
EXPERT = 'EXPERT',
}
export enum CertStatus {
NONE = 0, // 未提交
PENDING = 1, // 待审核
APPROVED = 2, // 审核通过
REJECTED = 3, // 驳回
}
export interface UserAccount {
id: number;
openid: string;
nickname: string;
roleFlags: number;
currentMode: WorkMode;
}
export class AuthRoleManager {
private users = new Map<number, UserAccount>();
private certs = new Map<number, { status: CertStatus; realName?: string; idCardNo?: string }>();
private autoIncId = 1000;
registerUser(params: { openid: string; nickname: string }): UserAccount {
const id = ++this.autoIncId;
const user: UserAccount = {
id,
openid: params.openid,
nickname: params.nickname,
roleFlags: UserRoleFlag.PLAYER,
currentMode: WorkMode.PLAYER,
};
this.users.set(id, user);
this.certs.set(id, { status: CertStatus.NONE });
return user;
}
getUser(userId: number): UserAccount {
const user = this.users.get(userId);
if (!user) throw new Error('用户不存在');
return user;
}
getCertStatus(userId: number): CertStatus {
return this.certs.get(userId)?.status ?? CertStatus.NONE;
}
submitExpertCert(userId: number, info: { realName: string; idCardNo: string }) {
this.certs.set(userId, {
status: CertStatus.PENDING,
realName: info.realName,
idCardNo: info.idCardNo,
});
}
approveExpertCert(userId: number) {
const cert = this.certs.get(userId);
if (!cert) throw new Error('未找到认证申请');
cert.status = CertStatus.APPROVED;
const user = this.getUser(userId);
user.roleFlags |= UserRoleFlag.EXPERT;
}
switchMode(userId: number, targetMode: WorkMode) {
const user = this.getUser(userId);
if (targetMode === WorkMode.EXPERT && !(user.roleFlags & UserRoleFlag.EXPERT)) {
throw new Error('尚未开通或通过大神认证,无法切换至接单模式');
}
user.currentMode = targetMode;
}
}

查看文件

@@ -0,0 +1,70 @@
export class FinanceCalculator {
/**
* 计算平台抽成与大神实际分润所得
*/
splitOrderIncome(params: {
orderAmountCents: number;
platformRatePercent: number;
}): { platformFeeCents: number; expertIncomeCents: number } {
const platformFeeCents = Math.floor((params.orderAmountCents * params.platformRatePercent) / 100);
const expertIncomeCents = params.orderAmountCents - platformFeeCents;
return { platformFeeCents, expertIncomeCents };
}
/**
* 计算二级分销佣金
*/
calculateDistribution(params: {
orderAmountCents: number;
level1Percent: number;
level2Percent: number;
hasLevel1: boolean;
hasLevel2: boolean;
}): { level1Cents: number; level2Cents: number; totalCommissionCents: number } {
const level1Cents = params.hasLevel1
? Math.floor((params.orderAmountCents * params.level1Percent) / 100)
: 0;
const level2Cents = params.hasLevel2
? Math.floor((params.orderAmountCents * params.level2Percent) / 100)
: 0;
return {
level1Cents,
level2Cents,
totalCommissionCents: level1Cents + level2Cents,
};
}
/**
* 应用优惠券抵扣
*/
applyCoupon(params: {
orderAmountCents: number;
couponDiscountCents: number;
minOrderThresholdCents: number;
}): number {
if (params.orderAmountCents < params.minOrderThresholdCents) {
throw new Error('订单未达到优惠券使用门槛');
}
const payAmount = params.orderAmountCents - params.couponDiscountCents;
return Math.max(0, payAmount);
}
/**
* 提现校验与实际到账金额计算
*/
calculateWithdrawal(params: {
withdrawAmountCents: number;
minLimitCents: number;
feePercent: number;
}): { feeCents: number; actualArrivalCents: number } {
if (params.withdrawAmountCents < params.minLimitCents) {
throw new Error('提现金额低于平台最低提现限额');
}
const feeCents = Math.floor((params.withdrawAmountCents * params.feePercent) / 100);
const actualArrivalCents = params.withdrawAmountCents - feeCents;
return { feeCents, actualArrivalCents };
}
}

查看文件

@@ -0,0 +1,61 @@
export enum OrderStatus {
PENDING_PAYMENT = 'PENDING_PAYMENT', // 待付款
PENDING_ACCEPT = 'PENDING_ACCEPT', // 待接单
IN_SERVICE = 'IN_SERVICE', // 服务中
PENDING_CONFIRM = 'PENDING_CONFIRM', // 待确认
COMPLETED = 'COMPLETED', // 已完成
CANCELLED = 'CANCELLED', // 已取消
DISPUTED = 'DISPUTED', // 仲裁中
REFUNDED = 'REFUNDED', // 已退款
}
export enum OrderEvent {
PAY_SUCCESS = 'PAY_SUCCESS', // 付款成功
CANCEL = 'CANCEL', // 取消订单
EXPERT_ACCEPT = 'EXPERT_ACCEPT', // 大神接单
EXPERT_REJECT = 'EXPERT_REJECT', // 大神拒单或超时退款
EXPERT_APPLY_FINISH = 'EXPERT_APPLY_FINISH', // 大神申请完成
BUYER_CONFIRM = 'BUYER_CONFIRM', // 买家确认完成
BUYER_DISPUTE = 'BUYER_DISPUTE', // 买家申请仲裁
ARBITRATE_REFUND = 'ARBITRATE_REFUND', // 仲裁退款
ARBITRATE_RELEASE = 'ARBITRATE_RELEASE', // 仲裁结单打款
}
export class OrderStateMachine {
// 状态转移表
private transitions: Record<OrderStatus, Partial<Record<OrderEvent, OrderStatus>>> = {
[OrderStatus.PENDING_PAYMENT]: {
[OrderEvent.PAY_SUCCESS]: OrderStatus.PENDING_ACCEPT,
[OrderEvent.CANCEL]: OrderStatus.CANCELLED,
},
[OrderStatus.PENDING_ACCEPT]: {
[OrderEvent.EXPERT_ACCEPT]: OrderStatus.IN_SERVICE,
[OrderEvent.EXPERT_REJECT]: OrderStatus.REFUNDED,
},
[OrderStatus.IN_SERVICE]: {
[OrderEvent.EXPERT_APPLY_FINISH]: OrderStatus.PENDING_CONFIRM,
},
[OrderStatus.PENDING_CONFIRM]: {
[OrderEvent.BUYER_CONFIRM]: OrderStatus.COMPLETED,
[OrderEvent.BUYER_DISPUTE]: OrderStatus.DISPUTED,
},
[OrderStatus.DISPUTED]: {
[OrderEvent.ARBITRATE_REFUND]: OrderStatus.REFUNDED,
[OrderEvent.ARBITRATE_RELEASE]: OrderStatus.COMPLETED,
},
[OrderStatus.COMPLETED]: {},
[OrderStatus.CANCELLED]: {},
[OrderStatus.REFUNDED]: {},
};
/**
* 执行状态迁移
*/
transition(currentState: OrderStatus, event: OrderEvent): OrderStatus {
const validNextState = this.transitions[currentState]?.[event];
if (!validNextState) {
throw new Error(`非法状态流转: 当前状态 [${currentState}] 无法执行事件 [${event}]`);
}
return validNextState;
}
}