五个包各司其职、依赖单向:controller 只做协议转换,service 承载全部业务决策,mapper 隔离存储,pojo 按「入参 / 出参 / 实体」三界分型——类型即边界。
选四个「问题真实存在、决策可被讨论、结果有测试守住」的样本——好设计不是玄学,是把一类问题挡在一层里。
问题:上游错误体可能带真实模型名、路由细节,且同一故障在不同上游表现不同(网关瞬断返回 400、欠费伪装成 401)——原样透传,轻则吓到用户,重则把「该重试的」标成「立即失败」。
决策:服务端设一道归一化层——上游错误一律先分类归一为有限的语义集合(请求错误 / 欠费 / 认证 / 限流 / 上游不可用),再按「可重试 / 不可重试」两级分发给调用方;上游原文只进日志、永不进响应。
守住它的测试:真实套件 44 个用例、8 个嵌套分组,把每一类伪装形态(欠费返回 401、限流返回 429、超限返回 413……)逐一断言归一结果。
问题:内部规则与提示词不允许逐字流到用户侧,但规则会持续更新——把拦截特征写死在代码里,每次规则变更都要改代码发版。
决策:拦截指纹完全从当前规则文本动态派生——服务启动时扫描全部规则生成指纹集,规则更新重启即自动重建,代码里没有一个写死的指纹;同时以「正常对话零误杀」为硬约束选窗口参数(参数取值见源码,此处不展开)。
守住它的测试:真实套件 29 个用例——复述命中即截断、正常对话零误杀、跨数据块边界同样命中。
问题:反馈提交后自动分类依赖外部模型服务——超时、返回 HTML 错误页、返回非法结构、未携带凭证,四种坏法任何一种都不该让用户的反馈消失。
决策:分类永远是「尽力而为」——失败一律降级为未分类、任务保留可人工补选;降级路径与正常路径同级对待,都有显式测试用例(真实套件 7 用例对应四种坏法全覆盖)。同款决策也用在更新策略上:外部策略文件损坏时告警并回落内置默认(5 用例守住)。
问题:反馈工单在多人之间流转(提交 → 认领 → 解决)——若状态合法性只靠前端按钮显隐,绕过界面的调用(脚本、并发双开)会把状态改乱。
决策:全部流转在服务层裁决——未认领不可解决、重复认领冲突拒绝、解决时一次性落处理说明与时间;客户端拿到的永远是裁决后的结果。数据库枚举与服务端枚举逐值对齐,非法值写入即被库层拦截。
守住它的测试:服务层 16 用例 + 全链路集成 9 用例(真实起服务跑完整流转)。
设计落到日常,是每处代码都直读意图(真实代码节选):
// 常量名即文档:为什么是这个值,写在名字与注释里 private static final int MAX_TRANSCRIPT_SIZE = 15 * 1024 * 1024; // 源会话转写上限 15MB // 测试名即场景:失败时看名字就知道坏了什么 @DisplayName("上游 5xx → 归一为可重试")