羽球搭子 HarmonyOS 实战(13):接龙文本解析与费用计算模型
一、接龙名单和费用表是同一条数据链群聊接龙常混有活动标题、日期、地点、编号、标点和“张三 2”这类同行者写法。直接按换行切割会把噪声当成人名也可能把重复报名计算两次。费用页面若再手工录入人数名单人数与费用人数还可能不一致。更稳妥的链路是先把文本解析成去重名单再由操作人确认男女人数最后进入费用模型。解析器只负责姓名结构不猜测性别费用服务只接收明确数字不读取 UI 字符串。两个边界通过“总人数一致”这条校验连接。二、逐行清洗并保持首次出现顺序解析入口逐行调用清洗函数把同一行可能产生的多个姓名展开然后按首次出现顺序去重。使用数组顺序能让页面显示与原接龙基本一致方便人工核对。export class ChainTextParser { static parse(text: string): string[] { const result: string[] [] const lines text.split(\n) lines.forEach((line: string) { const names this.cleanLine(line) names.forEach((name: string) { if (name.length 0 result.indexOf(name) 0) { result.push(name) } }) }) return result } }这里不使用无序集合直接返回结果是因为报名顺序往往仍有业务含义例如候补顺序或临时分组参考。三、编号、标点和同行者要分步处理清洗顺序会影响结果。先移除开头编号再识别末尾N随后清除固定业务词和标点最后判断是否为噪声行。若先删除所有数字就会丢失同行者数量。static cleanLine(line: string): string[] { let value line.trim() if (value.length 0 || value.indexOf(#) 0) return [] value value.replace(/^\d(?:\.(?!\d)|[、\)])\s*/, ) const plusMatch value.match(/[]\s*(\d)\s*$/) let extraCount 0 if (plusMatch ! null) { extraCount Number(plusMatch[1]) value value.replace(/\s*[]\s*\d\s*$/, ) } value value.replace(/报名|接龙|羽毛球|名单|参加|人员/g, ) value value.replace(/[:,。;]/g, ) if (this.isNoiseLine(value)) return [] const baseName value.trim() if (baseName.length 0) return [] const names: string[] [baseName] for (let index 1; index extraCount; index) { names.push(${baseName}(${index})) } return names }张三 2被展开为张三、张三(1)、张三(2)。占位姓名让总人数正确同时提醒操作人后续可以替换成真实姓名。四、噪声识别要保守日期、星期、时段、地点和活动说明通常不是姓名。噪声规则应优先过滤高置信特征不能用“包含数字”这种过宽条件否则会误删“球友1”一类合法名称。static isNoiseLine(value: string): boolean { if (value.length 0 || value.indexOf(#) 0) { return true } const schedule /(\d{1,2}[\.月号日]|周[一二三四五六日天])/ const period /(晚上|下午|上午|点)/ const activity /(组局|球局|可延|地址|地点)/ return schedule.test(value) || period.test(value) || activity.test(value) }原始行解析结果原因1. 张三张三去掉编号李四 2李四及两个占位同行者展开人数周六晚上 7 点忽略时间噪声地点体育馆忽略地址噪声2. 张三不重复加入全局去重五、费用输入使用数值模型而不是表单字符串费用模型要完整保留场地费、充值与赠送、球费明细、男女人数和折扣。页面负责把输入字符串转换为非负数服务层只接收规范化数据。export interface FeeShuttleItem { id: string name: string bucketPrice: number shuttleCount: number subtotal: number } export interface FeeInput { courtFee: number rechargeAmount: number bonusAmount: number shuttleFee: number shuttleItems: FeeShuttleItem[] maleCount: number femaleCount: number femaleDiscount: number } function parseNonNegative(value: string): number { const parsed Number(value) return Number.isFinite(parsed) parsed 0 ? parsed : 0 }球费既允许直接填写总额也允许按“单桶价格 ÷ 12 × 消耗球数”生成明细两种来源相加得到总球费。明细保留球品和小计结算结果才能追溯。输入草稿与最终结果应分开持久化。编辑过程中保存草稿可以在系统回收页面后恢复未完成的场地费和球具明细成功计算后清除旧草稿、保留只读结果返回输入页时不会把上一次结算误当成待提交数据。反序列化失败时回退到零值模型不能让一条损坏缓存阻断整个费用入口。六、场地费与球费采用不同分摊权重充值赠送会降低场地实付比例充值 1000 元赠送 200 元时实际折扣率为1000 / 1200。场地费按总人数平均球费可以让女生使用单独折扣权重再分别计算男女应付金额。static calculate(input: FeeInput): FeeResult { const totalPeople input.maleCount input.femaleCount const rechargeTotal input.rechargeAmount input.bonusAmount const courtRate rechargeTotal 0 ? input.rechargeAmount / rechargeTotal : 1 const actualCourtFee this.round2(input.courtFee * courtRate) const perCourtFee totalPeople 0 ? this.round2(actualCourtFee / totalPeople) : 0 const femaleRate input.femaleDiscount / 10 const weightedPeople input.maleCount input.femaleCount * femaleRate const maleShuttleFee weightedPeople 0 ? this.round2(input.shuttleFee / weightedPeople) : 0 const femaleShuttleFee this.round2(maleShuttleFee * femaleRate) return { input, actualCourtFee, courtDiscountRate: this.round2(courtRate * 10), perCourtFee, maleShuttleFee, femaleShuttleFee, maleTotalFee: this.round2(perCourtFee maleShuttleFee), femaleTotalFee: this.round2(perCourtFee femaleShuttleFee), totalPeople, detailText: } }所有金额统一保留两位小数避免界面各自取整导致合计难以解释。分摊结果各人金额相加后可能与总费用存在几分钱差异这是逐人四舍五入造成的。若业务需要严格对账可以把差额分配给最后一位参与者或全程使用“分”为整数单位最后再格式化为元。当前模型强调展示一致和公式可读验收时应同时记录原始总额、权重和舍入后的差值避免把正常精度误差判断成公式错误。七、名单与费用之间增加一致性校验解析器不能自动判断姓名对应性别因此确认名单后仍需人工填写男女数量。但提交费用计算前可以检查两者之和是否等于名单数量把录入偏差暴露在结果生成前。type CountCheck { rosterCount: number maleCount: number femaleCount: number valid: boolean message: string } function verifyCounts( roster: string[], maleCount: number, femaleCount: number ): CountCheck { const entered maleCount femaleCount const valid roster.length entered entered 0 return { rosterCount: roster.length, maleCount, femaleCount, valid, message: valid ? 人数一致 : 名单 ${roster.length} 人费用人数 ${entered} 人 } }边界处理方式结果名单为空阻止进入费用确认不产生零人账单费用人数不一致显示差异并要求确认避免漏算或多算充值与赠送都为 0场地折扣率按 1不出现除零女生折扣为 10男女球费权重一致等额分摊球费为 0只分摊场地费金额仍可生成八、用具体数字回归公式和解析器测试应同时覆盖解析和费用。解析测试关注输入变体费用测试关注零值、折扣和舍入。一组可复核样例是场地费 240 元充值 1000 赠送 200球费 96 元6 男 2 女女生 8 折。先计算实付场地费再按八人均分球费则按6 2 × 0.8的权重分配。用例输入重点断言标准接龙编号、标题、8 个姓名顺序保留且无标题同行者姓名 2总人数增加 3重复报名同一姓名出现两次只保留首次零人数男女均为 0各人费用为 0不出现异常数值充值优惠1000 赠 200实付场地费为原价的 5/6折扣球费女生 8 折女生球费为男生球费的 0.8页面表单的响应式状态传递可参考 HarmonyOS 状态管理概述。表单草稿与计算结果应使用不同键保存完成计算后清理草稿避免返回页面时把旧结果误当新输入。九、总结接龙解析的关键是按顺序清洗、保守识别噪声、展开同行者并去重费用模型的关键是规范化数值、分离场地费与球费权重、保留可追溯明细。两者通过人数一致性校验连接后从群文本到分摊结果就形成了一条可解释的数据链异常输入也能在生成账单前被发现。