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

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

查看文件

@@ -0,0 +1,167 @@
import { ActivityService, ActivityPayType, Activity, Participant } from '../src/core/activity/activity.service';
describe('ActivityService (TDD 测试套件: 活动项目与4位核销码)', () => {
let activityService: ActivityService;
beforeEach(() => {
activityService = new ActivityService();
});
describe('1. 发起活动 (AA制与项目制)', () => {
it('应成功创建AA制活动并正确初始化', () => {
const activity = activityService.createActivity({
creatorId: 1001,
title: '王者荣耀荣耀王者五排冲星车队',
gameCategory: '王者荣耀',
payType: ActivityPayType.AA_PAY,
aaFeeCents: 3000, // 每人30元
maxParticipants: 4, // 限制4人,0表示不限制
});
expect(activity.id).toBeDefined();
expect(activity.payType).toBe(ActivityPayType.AA_PAY);
expect(activity.aaFeeCents).toBe(3000);
expect(activity.maxParticipants).toBe(4);
expect(activity.currentParticipants).toBe(0);
});
it('应成功创建项目制活动(不限制报名人数)', () => {
const activity = activityService.createActivity({
creatorId: 1002,
title: '水友粉丝狂欢内战赛',
gameCategory: '英雄联盟',
payType: ActivityPayType.PROJECT_PAY,
totalFeeCents: 100000, // 发起人项目总包1000元
maxParticipants: 0, // 不限制报名人数
});
expect(activity.payType).toBe(ActivityPayType.PROJECT_PAY);
expect(activity.totalFeeCents).toBe(100000);
expect(activity.maxParticipants).toBe(0);
});
});
describe('2. 报名与生成 4 位纯数字核销码', () => {
let aaActivity: Activity;
beforeEach(() => {
aaActivity = activityService.createActivity({
creatorId: 1001,
title: '和平精英四排吃鸡车队',
gameCategory: '和平精英',
payType: ActivityPayType.AA_PAY,
aaFeeCents: 2500,
maxParticipants: 3,
});
});
it('报名成功后应生成合法的 4 位纯数字核销码 (1000-9999)', () => {
const participant = activityService.joinActivity(aaActivity.id, {
userId: 2001,
payAmountCents: 2500,
});
expect(participant.verifyCode).toMatch(/^\d{4}$/);
const codeNum = parseInt(participant.verifyCode, 10);
expect(codeNum).toBeGreaterThanOrEqual(1000);
expect(codeNum).toBeLessThanOrEqual(9999);
expect(participant.checkinStatus).toBe(false);
});
it('单活动内多个参与者的 4 位核销码不可重复碰撞', () => {
const p1 = activityService.joinActivity(aaActivity.id, { userId: 2001, payAmountCents: 2500 });
const p2 = activityService.joinActivity(aaActivity.id, { userId: 2002, payAmountCents: 2500 });
const p3 = activityService.joinActivity(aaActivity.id, { userId: 2003, payAmountCents: 2500 });
const codes = new Set([p1.verifyCode, p2.verifyCode, p3.verifyCode]);
expect(codes.size).toBe(3);
});
it('同一用户不可重复报名同一活动', () => {
activityService.joinActivity(aaActivity.id, { userId: 2001, payAmountCents: 2500 });
expect(() => {
activityService.joinActivity(aaActivity.id, { userId: 2001, payAmountCents: 2500 });
}).toThrow('用户已报名该活动');
});
it('超出人数限制时应阻止报名', () => {
activityService.joinActivity(aaActivity.id, { userId: 2001, payAmountCents: 2500 });
activityService.joinActivity(aaActivity.id, { userId: 2002, payAmountCents: 2500 });
activityService.joinActivity(aaActivity.id, { userId: 2003, payAmountCents: 2500 });
expect(() => {
activityService.joinActivity(aaActivity.id, { userId: 2004, payAmountCents: 2500 });
}).toThrow('活动报名人数已满');
});
});
describe('3. 4位核销码签到核销与防重控制', () => {
let testActivity: Activity;
let participant: Participant;
beforeEach(() => {
testActivity = activityService.createActivity({
creatorId: 1001,
title: '双排上分活动',
gameCategory: '王者荣耀',
payType: ActivityPayType.AA_PAY,
aaFeeCents: 2000,
maxParticipants: 1,
});
participant = activityService.joinActivity(testActivity.id, {
userId: 3001,
payAmountCents: 2000,
});
});
it('发起人使用正确的 4 位核销码应核销成功', () => {
const result = activityService.verifyCheckin({
activityId: testActivity.id,
operatorId: 1001, // 发起人操作
verifyCode: participant.verifyCode,
});
expect(result.success).toBe(true);
expect(result.participant.checkinStatus).toBe(true);
expect(result.participant.verifiedAt).toBeDefined();
});
it('同一 4 位核销码不可重复核销(防重签到)', () => {
activityService.verifyCheckin({
activityId: testActivity.id,
operatorId: 1001,
verifyCode: participant.verifyCode,
});
expect(() => {
activityService.verifyCheckin({
activityId: testActivity.id,
operatorId: 1001,
verifyCode: participant.verifyCode,
});
}).toThrow('该核销码已被核销,不可重复使用');
});
it('输入错误的 4 位核销码应报错拒绝', () => {
expect(() => {
activityService.verifyCheckin({
activityId: testActivity.id,
operatorId: 1001,
verifyCode: '0000', // 不存在的核销码
});
}).toThrow('无效或不存在的核销码');
});
it('非发起人或未授权人员核销应被拒绝', () => {
expect(() => {
activityService.verifyCheckin({
activityId: testActivity.id,
operatorId: 9999, // 非法第三方操作者
verifyCode: participant.verifyCode,
});
}).toThrow('无权核销该活动的核销码');
});
});
});

查看文件

@@ -0,0 +1,52 @@
import { AuthRoleManager, UserRoleFlag, WorkMode, CertStatus } from '../src/core/auth/auth-role.manager';
describe('AuthRoleManager (TDD 测试套件: 用户与大神双身份平滑切换)', () => {
let roleManager: AuthRoleManager;
beforeEach(() => {
roleManager = new AuthRoleManager();
});
describe('1. 玩家与大神身份校验', () => {
it('普通注册用户默认仅具备玩家身份', () => {
const user = roleManager.registerUser({ openid: 'wx_123456', nickname: '新手玩家' });
expect(user.roleFlags).toBe(UserRoleFlag.PLAYER);
expect(user.currentMode).toBe(WorkMode.PLAYER);
});
it('普通玩家尝试直接切换至大神接单模式应被拒绝', () => {
const user = roleManager.registerUser({ openid: 'wx_123456', nickname: '新手玩家' });
expect(() => {
roleManager.switchMode(user.id, WorkMode.EXPERT);
}).toThrow('尚未开通或通过大神认证,无法切换至接单模式');
});
});
describe('2. 认证开通与双身份平滑切换', () => {
it('完成实名认证审核通过后,赋予双身份并允许自由切换', () => {
const user = roleManager.registerUser({ openid: 'wx_expert_001', nickname: '电竞大神' });
// 提交实名认证
roleManager.submitExpertCert(user.id, { realName: '张三', idCardNo: '520100...' });
expect(roleManager.getCertStatus(user.id)).toBe(CertStatus.PENDING);
// 后台审核通过
roleManager.approveExpertCert(user.id);
expect(roleManager.getCertStatus(user.id)).toBe(CertStatus.APPROVED);
// 检查角色位图已升级为 BOTH (玩家+大神)
const updatedUser = roleManager.getUser(user.id);
expect(updatedUser.roleFlags).toBe(UserRoleFlag.PLAYER | UserRoleFlag.EXPERT);
// 切换至大神模式
roleManager.switchMode(user.id, WorkMode.EXPERT);
expect(roleManager.getUser(user.id).currentMode).toBe(WorkMode.EXPERT);
// 一键切回玩家模式
roleManager.switchMode(user.id, WorkMode.PLAYER);
expect(roleManager.getUser(user.id).currentMode).toBe(WorkMode.PLAYER);
});
});
});

118
server/tests/finance.spec.ts 普通文件
查看文件

@@ -0,0 +1,118 @@
import { FinanceCalculator } from '../src/core/finance/finance.calculator';
describe('FinanceCalculator (TDD 测试套件: 财务与分销计算引擎)', () => {
let calculator: FinanceCalculator;
beforeEach(() => {
calculator = new FinanceCalculator();
});
describe('1. 平台抽成与大神分润计算', () => {
it('100元订单按10%抽成,平台得10元,大神得90元', () => {
const result = calculator.splitOrderIncome({
orderAmountCents: 10000, // 100.00 元
platformRatePercent: 10,
});
expect(result.platformFeeCents).toBe(1000); // 10.00 元
expect(result.expertIncomeCents).toBe(9000); // 90.00 元
expect(result.platformFeeCents + result.expertIncomeCents).toBe(10000);
});
it('出现小数分位时,向下取整平台抽成,保证资金平衡无漂移', () => {
const result = calculator.splitOrderIncome({
orderAmountCents: 3333, // 33.33 元
platformRatePercent: 15, // 15% -> 499.95 分
});
expect(result.platformFeeCents).toBe(499);
expect(result.expertIncomeCents).toBe(2834);
expect(result.platformFeeCents + result.expertIncomeCents).toBe(3333);
});
});
describe('2. 二级分销佣金分润计算', () => {
it('100元订单,一级分销商得5元,二级分销商得2元', () => {
const comm = calculator.calculateDistribution({
orderAmountCents: 10000,
level1Percent: 5,
level2Percent: 2,
hasLevel1: true,
hasLevel2: true,
});
expect(comm.level1Cents).toBe(500); // 5.00 元
expect(comm.level2Cents).toBe(200); // 2.00 元
expect(comm.totalCommissionCents).toBe(700);
});
it('只有一级推荐人时,仅发放一级佣金', () => {
const comm = calculator.calculateDistribution({
orderAmountCents: 20000,
level1Percent: 5,
level2Percent: 2,
hasLevel1: true,
hasLevel2: false,
});
expect(comm.level1Cents).toBe(1000);
expect(comm.level2Cents).toBe(0);
expect(comm.totalCommissionCents).toBe(1000);
});
});
describe('3. 优惠券抵扣计算', () => {
it('订单满100减20,实付80元', () => {
const payAmount = calculator.applyCoupon({
orderAmountCents: 10000,
couponDiscountCents: 2000,
minOrderThresholdCents: 8000,
});
expect(payAmount).toBe(8000);
});
it('未达到优惠券门槛时应抛出异常', () => {
expect(() => {
calculator.applyCoupon({
orderAmountCents: 5000, // 50元
couponDiscountCents: 2000,
minOrderThresholdCents: 8000, // 需满80元
});
}).toThrow('订单未达到优惠券使用门槛');
});
it('优惠券减免额大于订单总额时,实付金额应为0而不是负数', () => {
const payAmount = calculator.applyCoupon({
orderAmountCents: 3000,
couponDiscountCents: 5000,
minOrderThresholdCents: 0,
});
expect(payAmount).toBe(0);
});
});
describe('4. 提现门槛与手续费校验', () => {
it('低于最低提现额度(如50元)应抛出异常', () => {
expect(() => {
calculator.calculateWithdrawal({
withdrawAmountCents: 3000, // 30元
minLimitCents: 5000, // 最低50元
feePercent: 1,
});
}).toThrow('提现金额低于平台最低提现限额');
});
it('提现100元,手续费1%,实际打款99元', () => {
const res = calculator.calculateWithdrawal({
withdrawAmountCents: 10000,
minLimitCents: 5000,
feePercent: 1,
});
expect(res.feeCents).toBe(100);
expect(res.actualArrivalCents).toBe(9900);
});
});
});

查看文件

@@ -0,0 +1,70 @@
import { OrderStateMachine, OrderStatus, OrderEvent } from '../src/core/order/order-state-machine';
describe('OrderStateMachine (TDD 测试套件: 陪玩订单状态机驱动)', () => {
let stateMachine: OrderStateMachine;
beforeEach(() => {
stateMachine = new OrderStateMachine();
});
describe('1. 正向订单履约流转', () => {
it('正常流程: 待付款 -> 待接单 -> 服务中 -> 待确认 -> 已完成', () => {
let state = OrderStatus.PENDING_PAYMENT;
state = stateMachine.transition(state, OrderEvent.PAY_SUCCESS);
expect(state).toBe(OrderStatus.PENDING_ACCEPT);
state = stateMachine.transition(state, OrderEvent.EXPERT_ACCEPT);
expect(state).toBe(OrderStatus.IN_SERVICE);
state = stateMachine.transition(state, OrderEvent.EXPERT_APPLY_FINISH);
expect(state).toBe(OrderStatus.PENDING_CONFIRM);
state = stateMachine.transition(state, OrderEvent.BUYER_CONFIRM);
expect(state).toBe(OrderStatus.COMPLETED);
});
});
describe('2. 取消与拒单退款流转', () => {
it('待付款状态下买家取消,变为已取消', () => {
const state = stateMachine.transition(OrderStatus.PENDING_PAYMENT, OrderEvent.CANCEL);
expect(state).toBe(OrderStatus.CANCELLED);
});
it('待接单状态下大神拒单或超时,变为已退款', () => {
const state = stateMachine.transition(OrderStatus.PENDING_ACCEPT, OrderEvent.EXPERT_REJECT);
expect(state).toBe(OrderStatus.REFUNDED);
});
});
describe('3. 售后与仲裁介入流转', () => {
it('待确认状态下买家发起申诉仲裁,进入仲裁中', () => {
const state = stateMachine.transition(OrderStatus.PENDING_CONFIRM, OrderEvent.BUYER_DISPUTE);
expect(state).toBe(OrderStatus.DISPUTED);
});
it('仲裁判定买家胜诉,转为已退款', () => {
const state = stateMachine.transition(OrderStatus.DISPUTED, OrderEvent.ARBITRATE_REFUND);
expect(state).toBe(OrderStatus.REFUNDED);
});
it('仲裁驳回申诉,转为已完成', () => {
const state = stateMachine.transition(OrderStatus.DISPUTED, OrderEvent.ARBITRATE_RELEASE);
expect(state).toBe(OrderStatus.COMPLETED);
});
});
describe('4. 非法状态跃迁防御', () => {
it('待付款状态直接跳至已完成应抛出非法操作异常', () => {
expect(() => {
stateMachine.transition(OrderStatus.PENDING_PAYMENT, OrderEvent.BUYER_CONFIRM);
}).toThrow('非法状态流转');
});
it('已完成订单不可再次发起退款或取消', () => {
expect(() => {
stateMachine.transition(OrderStatus.COMPLETED, OrderEvent.CANCEL);
}).toThrow('非法状态流转');
});
});
});