惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

The GitHub Blog
The GitHub Blog
IT之家
IT之家
B
Blog RSS Feed
罗磊的独立博客
GbyAI
GbyAI
博客园 - Franky
Y
Y Combinator Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Google DeepMind News
Google DeepMind News
博客园 - 聂微东
N
Netflix TechBlog - Medium
博客园 - 三生石上(FineUI控件)
人人都是产品经理
人人都是产品经理
U
Unit 42
博客园 - 叶小钗
Jina AI
Jina AI
MyScale Blog
MyScale Blog
雷峰网
雷峰网
B
Blog
Hugging Face - Blog
Hugging Face - Blog
Blog — PlanetScale
Blog — PlanetScale
Recent Announcements
Recent Announcements
腾讯CDC
酷 壳 – CoolShell
酷 壳 – CoolShell

博客园 - 程序员李铁牛

赛事报名系统开发总管理中心规划 赛事报名系统开发:角色与权限体系设计 赛事系统报名接龙高并发技术细节处理浅析 档案数字化研发手记:PDF合并内存溢出踩坑 纸质档案数字化管理系统研发手记:法院诉讼档案单套制落地案例复盘——从纸质卷宗到电子卷宗 档案数字化管理系统开发手记:人事档案专项审核与数字化的系统支撑——"凡提必审"下的技术要点 档案数字化研发手记:我们的技术栈选择——档案系统前后端选型理由(含一次"炫技"的教训) 社群团购系统开发分享——工程化(下):资金支付测试策略——资金模块 100% 覆盖,其他 60% 就够 社群团购类快团团系统开发——RBAC 权限设计源码分析:当一个微信号有 4 种身份时怎么办 社群团购系统类快团团模式开发——微信小程序登录:code2Session 之后还有 5 件事要做 数字档案管理系统化研发手记:医院病历档案数字化——合规、病案首页 OCR 与调阅提速 数字档案系统研发手记:批量扫描任务调度——TWAIN/SANE 采集驱动的封装实践 档案数字化系统研发源码手记:置信度过滤——把 97% 识别率变成真正可交付的成果 档案数字化开发实例手记:盖章遮挡文字识别恢复 景区运营预约系统开发:景区会员体系怎么搭?3级会员模型+积分玩法 景区门票预约系统全渠道平台库存数据同步技术处理方式 系统宕机了怎么办?景区票务系统应急预案模板 景区门票预约系统开发:接入AI预测客流设计思路分析 景区预约系统开发总结:门票分时预约设计原理和落地方法 设备运维管理系统开发实战:用规则引擎实现可配置的设备告警策略自研轻量引擎 vs Drools 落地对比 设备维修保养系统预测性维护不用深度学习?设备健康度评分的务实实现方案 档案数字化管理系统研发手记:档案权限模型——全宗-门类-案卷-文件四级控制的设计与实践 档案管理信息化系统研发手记:数字水印方案对比——可见水印 vs 盲水印(档案借阅防泄露) 景区票务系统开发实例分析:上云还是本地部署?6个维度判断 档案数字化系统源码研发手记:老旧档案去污点——中值滤波与 inpainting 效果对比 一物一码防伪溯源系统开发全栈源码串联一次扫码的完整链路追踪代码走读 景区门票预约系统票务系统的6个核心模块 景区上线门票预约系统的5个常见翻车点 景区门票预约系统之人脸识别 vs 身份证核验 vs 扫码入园,哪种最适合你? 景区门票预约系统开发深度解析之定价策略功能设计
社群团购系统类快团团源码开发——工程化(上):一套让 5 人小...
程序员李铁牛 · 2026-09-07 · via 博客园 - 程序员李铁牛

规范的失败方式只有一种:靠人肉执行

上一篇我们把权限模型收了口。代码骨架、登录态、权限体系都齐了,接下来是一个大多数团队"知道重要、但从来做不好"的话题——开发规范。

先给结论:规范不是一份文档,是一组可执行的约束。 文档写再厚,只要"遵守"和"不遵守"的结果一样,它三个月后一定变成摆设。我见过太多团队的规范文档停在 v1.0,最后一条 commit 规范执行的记录是半年前。规范要活下来,只有一个办法:把每条规则变成机器卡点——编译期拦截、CI 流水线拦截、Code Review 强制清单,让"不合规的代码"根本进不了主干。

还有一个反直觉的事实要先摆出来:5 人小团队的事故,90% 不是高并发、不是架构问题,而是低级错误——金额用了 double、回调没做幂等、查询没校验数据归属(越权)、事务边界画错。这类错误有一个共同点:每一类都可以用一条机器规则拦住。所以这套规范的裁剪逻辑非常功利:只保留"违反即事故"的规则,其余一律砍掉。

本篇讲规范的前半部分:代码规范裁剪、目录纪律、Git 分支与 commit 规范、Code Review 清单、API 先行、CI 卡点。测试策略单独立成下一篇,因为篇幅和它的重要性都配得上独立一篇。


1. 规范裁剪:阿里 Java 规范的 30% 落地版

《阿里巴巴 Java 开发手册》是好东西,但全文 40+ 章节直接照搬到 5 人团队,等于没有规范。我们的做法是只裁出违反即事故的那部分,其余让 IDE 默认处理。

1.1 保留清单与裁剪清单

类别 保留(违反即事故) 裁剪(交给 IDE / 不值得卡)
金额 禁用 float/double 存金额;禁用 new BigDecimal(double);金额一律 long(分)或 BigDecimal 金额变量命名格式(priceSupply 已够用)
事务 @Transactional 必须显式指定 rollbackFor = Exception.class;事务方法内禁止远程调用 事务方法命名前缀约定
并发 禁止 SimpleDateFormat 静态共享;共享可变状态必须显式标注并发策略 线程池命名格式
SQL 禁止 SELECT *;UPDATE/DELETE 必须带 WHERE;批量操作必须限流分批 索引命名统一为 idx_/uk_(建表模板已固化)
安全 手机号等敏感字段禁止日志明文输出;SQL 必须参数化,禁字符串拼接 注释覆盖率
注释 公开的资金规则方法必须有说明"为什么这么算" 所有的 Javadoc 模板要求

裁剪的原则可以说成一句话:机器能拦的进 Checkstyle,机器拦不住的进 Review 清单,都拦不住的说明不值得立规矩。

1.2 Checkstyle 配置示例

只上真规则,配置越短越好维护。节选我们实际使用的 checkstyle.xml

<module name="Checker">
    <!-- 金额禁用 double/float:拦截字段声明与变量类型 -->
    <module name="RegexpSinglelineJava">
        <property name="format"
                  value="(?:float|double|Double|Float)\s+(?:(?!rate|ratio|percent)\w)*(?:amount|price|fee|commission)\w*"/>
        <property name="message"
                  value="金额字段禁止使用浮点类型,请使用 long(分为单位)或 BigDecimal"/>
    </module>

    <!-- 拦截 new BigDecimal(double) 的经典精度陷阱 -->
    <module name="RegexpSinglelineJava">
        <property name="format" value="new\s+BigDecimal\s*\(\s*[\d.]+"/>
        <property name="message" value="禁止 new BigDecimal(double),请使用 BigDecimal.valueOf() 或字符串构造"/>
    </module>

    <!-- 拦截 SimpleDateFormat 静态共享(线程安全) -->
    <module name="RegexpSinglelineJava">
        <property name="format" value="static.*SimpleDateFormat"/>
        <property name="message" value="SimpleDateFormat 非线程安全,禁止静态共享,请使用 DateTimeFormatter"/>
    </module>

    <!-- SQL 拦截:SELECT * -->
    <module name="RegexpSinglelineJava">
        <property name="format" value="select\s+\*\s+from"/>
        <property name="ignoreCase" value="true"/>
        <property name="message" value="禁止 SELECT *,请显式列出字段(配合 MyBatis-Plus 的字段缓存)"/>
    </module>
</module>

注意第一条正则里排除了 rate/ratio/percent——佣金比例确实用基数字(如 1500 表示 15%)存 int,但命名里带 rate 的字段多为比例而非金额,误报会摧毁团队对规范的信任。卡点规则的误报率必须控制在几乎为零,否则大家学会的第一件事是绕过卡点。


2. 目录纪律:把第 3 篇的模块纪律固化成代码

第 3 篇定过三条模块纪律:跨模块只走 Service 接口、资金规则只在 domain 层、依赖无环。口头约定守不住,我们用 ArchUnit 把它变成单元测试,跑在每次 CI 里:

@AnalyzeClasses(packages = "com.gbs")
class ArchitectureTest {

    // 纪律一:commission 模块禁止依赖 order 模块的内部实现,只准调对方暴露的 Service
    @ArchTest
    static final ArchRule crossModuleAccess =
        noClasses().that().resideInAPackage("..commission..")
            .should().dependOnClassesThat()
            .resideInAPackage("..order.domain..", "..order.mapper..");

    // 纪律二:资金规则只允许出现在 domain 层——controller/service/job 出现金额计算类即失败
    @ArchTest
    static final ArchRule moneyOnlyInDomain =
        classes().that().haveSimpleNameEndingWith("Calculator")
            .or().haveSimpleNameEndingWith("SettlementRule")
            .should().resideInAPackage("..domain..");

    // 纪律三:模块依赖无环
    @ArchTest
    static final ArchRule noPackageCycle =
        slices().matching("com.gbs.(**)").should().beFreeOfCycles();
}

这段测试的价值在第三条规则暴露得最充分:某次需求里,图省事的同事在 order 模块里 import 了 commission 的一个枚举,两周后 commission 又反过来引了 order 的工具类——环在没人察觉时闭合。ArchUnit 在 CI 里直接红了,重构成本是十分钟;如果等到要拆分模块时发现环,成本以月计。


3. Git 分支模型:小团队别照搬大厂

5 人团队上完整的 GitFlow(develop/release/hotfix 多长生命分支)是自杀行为——合并成本吃掉产能。我们的分支模型只有三条:

main            # 可发布,受保护,只接受 PR 合入
├── feature/GB-142-commission-snapshot   # 功能分支,生命周期 ≤ 3 天
└── fix/GB-155-idempotent-callback       # 修复分支,同上

三条纪律:

  1. 分支命名带任务号feature/xxx 里那个 GB-142 是任务系统编号——半年后排查"这行代码当时为什么这么写",commit 能反查到当时的讨论,这是唯一的原因追溯链;
  2. 功能分支生命周期不超过 3 天。超过 3 天说明任务切太大,强制拆分。长生命分支是合并冲突的唯一来源,小团队没有任何理由养它;
  3. main 分支保护 + 线上 hotfix 也走 fix 分支,禁止直接 push。发布打 tag(v1.4.0),回滚回滚到上一个 tag——部署细节留到第 25 篇展开。

commit message 用 Conventional Commits 的裁剪版,同样进 CI 卡点(commitlint):

// commitlint.config.js —— 只留 4 种类型,够用
module.exports = {
  rules: {
    'type-enum': [2, 'always', ['feat', 'fix', 'refactor', 'chore']],
    'subject-max-length': [2, 'always', 40],
    'subject-empty': [2, 'never'],
  },
};

只留 feat / fix / refactor / chore 四种类型。多团队协作时 type 用于自动生成 changelog 才有价值,5 个人生 changelog 不如看任务系统,所以规范服务的目标要清楚:commit 规范在我们这里只服务于"半年后看懂历史",不服务于流水线自动化。


4. Code Review:一张清单,一票否决

5 人团队的 Review 不是仪式,是事故的最后防线。我们的清单只有 12 条,按"资金 > 幂等 > 越权 > 其他"排序,前 5 条一票否决:

# 检查项 级别
1 金额计算是否全部落在 domain 层、且基于快照而非当前主数据? 一票否决
2 所有写操作是否有幂等保障(唯一索引 uk_biz_serial / 幂等 token)? 一票否决
3 查询与写操作是否校验了数据归属(帮卖只能看/改自己的单,防水平越权)? 一票否决
4 @Transactional 边界是否覆盖了"价格快照 + 佣金快照 + 订单落库"三件事? 一票否决
5 微信回调、定时任务重跑、消息重复消费,三条路径是否都重放了幂等? 一票否决
6 新增 SQL 是否走索引、是否会在大表上产生全表扫描? 必须修
7 外部调用(微信 API)是否包了防腐层、是否设置了超时与重试上限? 必须修
8 日志是否带上下文(订单号/traceId)、是否泄露敏感字段? 必须修
9 异常是否被吞掉(空 catch、catch 后只打日志不抛)? 必须修
10 新增状态是否进了状态机迁移表,而不是散落的 if/else? 必须修
11 命名是否与限界上下文一致(同一概念在两个模块里名字不同 = 边界在烂)? 建议修
12 是否存在"顺手优化"(无关文件的格式化、重命名)? 建议修(禁止混入)

第 12 条值得单独说:PR 里混入顺手优化,是 Review 质量崩坏的开始。 Reviewer 看到 500 行 diff,前 300 行是格式化,真正要审的 200 行反而没人细看。格式化要么单独提 PR,要么配 spotless 进 CI 自动做——总之不能混在功能变更里。


5. API 先行:OpenAPI 契约驱动联调

五端共用一个后端(第 3 篇的架构决定),前后端联调成本是这里的隐性大头。我们的做法是契约先行:接口先用 OpenAPI 文档定义并评审,前后端各自基于契约开发,后端用契约做接口测试。

流程上只有三步:

  1. 后端在任务系统里提交接口设计(OpenAPI YAML 片段),前端在一个工作日内提出字段异议,评审通过即冻结为契约;
  2. 前端基于 YAML 生成 TypeScript 类型与 mock 服务,不等后端;后端基于 YAML 实现接口;
  3. 后端用 springdoc 导出的文档与契约做 diff,字段不匹配 CI 直接失败。

一个团购创建接口的契约片段:

POST /api/leader/groupbuy/create:
  requestBody:
    required: true
    content:
      application/json:
        schema:
          type: object
          required: [title, endTime, stockMode]
          properties:
            title:
              type: string
              maxLength: 64
            endTime:
              type: string
              format: date-time
              description: 截单时间,必须晚于当前时间 1 小时
            stockMode:
              type: integer
              enum: [10, 20]
              description: "10=限量库存 20=预售不限量(截单后按订单量要货)"
  responses:
    "0":
      description: 返回团购 ID
    "41001":
      description: 截单时间非法

两个必须坚持的细节:错误码进契约(前端按 41001 这类业务码做交互分支,而不是解析中文 message);DTO 按端隔离/api/leader//api/c/ 的出入参对象禁止复用——第 3 篇讲过,这是"改一个小程序字段、PC 端莫名报错"事故的根源)。

契约的第三个细节是演进规则——契约冻结不等于永远不改,改要守兼容三原则:字段只增不改不删(改语义和删除都是破坏性变更,小程序端旧版本还在线上跑着,用户没有"全部升级"这回事);新增字段必须可缺省(旧客户端不传新参数,服务端要有默认行为,而不是报错);破坏性变更走新路径/api/v2/...),新旧并行一个审核周期后再下线。小程序的发布天然有微信审核期缓冲,这反而要求后端的兼容窗口比 Web 更长——Web 服务发完版客户端立刻是新的,小程序用户手里可能是三个月前的版本。这三原则写进了契约评审的 checklist,CI 的 contract-diff 会拦截删字段和改类型的 diff。


6. CI 卡点:把所有规则串成一条流水线

前面所有规则的执行者,是一条 6 步流水线。节选 GitHub Actions 配置(Jenkins 逻辑完全一致):

name: ci
on: [pull_request]
jobs:
  gate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build & Static Check   # 编译 + Checkstyle,红即止
        run: mvn -B verify -DskipTests
      - name: Unit & Integration Tests  # 含 ArchUnit 架构测试
        run: mvn -B test
      - name: Coverage Gate           # 资金模块 80%,其余 60%(下一篇展开分配逻辑)
        run: mvn -B jacoco:check -pl gbs-domain,gbs-commission
      - name: Commit Lint
        run: npx commitlint --from origin/main --to HEAD
      - name: OpenAPI Contract Diff   # 实现与契约比对
        run: ./scripts/contract-diff.sh

流水线的排序有讲究:越便宜的检查越靠前。Checkstyle 秒级,放最前面;集成测试分钟级,放中间;契约 diff 依赖环境,放后面。目标只有一个——开发者收到失败反馈的时间最短,修规则的意愿才最强。


7. 接口规范的两块基石:统一响应与错误码分段

CI 卡点管住"代码怎么写",还有一类事故藏在"接口怎么约定"里。五端共用一个后端(第 3 篇的架构),接口约定不统一,前端联调成本会指数级上升,排障时更会互相甩锅。两条必须提前定死的约定:

统一响应体:所有接口返回 { code, message, data } 三段结构,code = 0 成功,非 0 一律由前端按业务码分支处理。业务异常统一走 BizException,由全局异常处理器兜底转成结构化响应——前端永远拿不到裸 500 堆栈:

/** 统一业务异常,携带分段错误码;所有 Service 抛出,Controller 不 try-catch */
public class BizException extends RuntimeException {
    private final int code;
    public BizException(ErrorCode ec, Object... args) {
        super(ec.format(args));
        this.code = ec.getCode();
    }
}
// 抛出示例:库存不足
throw new BizException(ErrorCode.STOCK_NOT_ENOUGH, sku.getName());

错误码分段0 成功;4xxxx 客户端错误(参数非法、权限不足、库存不足);5xxxx 服务端错误;7xxxx 第三方错误(微信支付回调失败、内容安全审核超时等)。分段的价值在排障现场:值班同学凌晨看到告警里是 70xxx,第一反应是查微信侧状态而不是翻自己的代码——错误码分段本质上是把"故障域归属"编码进了错误信息,5 人团队没有专职运维,这个信息差就是处理速度。

顺带一条与日志相关的纪律,它属于"机器拦不住但必须立"的规矩:每个对外接口的异常日志必须带上下文(订单号、用户 ID、traceId),traceId 由网关生成、贯穿整条调用链。售后纠纷最经典的场景是"用户说付了钱没订单"——拿一个订单号查出 traceId,整条链路一屏拉完,这是唯一的救命稻草。我们把它做进了 Review 清单第 8 条,也做了正则卡点拦截"没有 MDC 上下文的 error 日志"。

最后回答一个常见质疑:"5 个人有必要搞这么重吗?"——这套流水线从写到稳定运行花了两天半,之后每天替团队拦下的低级错误以个位数计。规范的建设成本是一次性的,而低级事故的成本按次收,且复利。 真正重的是测试策略,那是下一篇的话题。


8. 本篇小结

  1. 规范 = 可执行的约束——文档保证不了规范存活,CI 卡点可以;裁剪原则是只保留"违反即事故"的规则;
  2. 阿里规范裁剪 30% 落地——金额禁浮点、事务显式 rollbackFor、SQL 禁 SELECT *,全部进 Checkstyle,且误报率必须趋近于零;
  3. 模块纪律用 ArchUnit 固化——跨模块只走接口、资金规则只在 domain、依赖无环,三条纪律在 CI 里自动验证;
  4. 分支模型做减法——main + 短生命功能分支,commit 规范服务于追溯历史而非自动化;
  5. Review 清单 12 条,前 5 条一票否决——资金、幂等、越权、事务边界、三条幂等重放路径;PR 禁止混入顺手优化;
  6. API 契约先行——OpenAPI 冻结后前后端并行,错误码进契约,DTO 按端隔离;
  7. 错误码分段 = 故障域归属编码——4xxxx/5xxxx/7xxxx 让值班 10 秒定位该查谁;异常日志必带订单号与 traceId。

规范立完了,但"不出事故"的另一半是测试。下一篇《工程化(下):测试策略——资金模块 100% 覆盖,其他 60% 就够》——讲清楚测试资源怎么分配、佣金计算的 12 个边界用例怎么写、以及为什么我们放弃了 H2 而用 Testcontainers。