背景:
最近在给一个 CRM 系统做升级,新增了一个字段:发票方向(invoiceDirection)。
一开始系统里的发票没区分红冲还是正常,只有一个字段:发票金额。
后来有红冲场景了,我那就简单的说一下红冲:
红冲 = 用一张「负数发票」来冲销一张已经开的正数发票,红冲是财务概念,这里不展开,知道它会导致金额为负就够了。
-
正常开票
-
红冲发票(简单理解为:用负数金额冲销原发票)
方案设计:
我最开始的方案(两行代码解决)
我当时的方案非常简单:
-
金额 > 0:绿色
-
金额 < 0:红色
前端区分一下展示就行了,两行代码就解决了。
老数据 0 影响,后端 0 改表
说实话,这个方案:简单、直接、完全够用。
if (column.property === 'invoiceMoney') {
const money = Number(row.invoiceMoney || 0)
return money > 0 ? 'xr-money green' : money === 0 ? 'xr-money gray' : 'xr-money red'
}
我到现在也觉得:在这个场景下,其实是 OK 的可能是我经验不足吧。
老大的原话大概意思是:
"颜色只是展示,必须加个字段必须明确区分是不是红冲。加字段。"这不是增加复杂度吗......
但没办法,谁让人家是老大呢 而且你还真不好反驳。
遇到的问题:
字段已经确定要加了,那真正的问题变成:
-
这是一个 新增字段
-
历史数据已经存在
-
不可能全表 update
-
也不能随便给默认值
如果处理不好,比不加还麻烦
最终方案:我用到了枚举了+兜底处理来处理就数据
-
用枚举定义业务边界:
-
读数据:不信任数据库,必须兜底
-
写数据:通过金额统一推导方向

这样做的好处:用枚举把"发票方向"的业务含义和判断规则集中管理,同时通过兜底逻辑兼容历史数据,避免魔法值扩散,让老系统升级更安全、更可维护。
最后我们在查询的时候做个兜底处理,因为老数据新的字段都是null 就根据发票金额做个兜底是正常开票还是红冲。
private void fillInvoiceDirectionIfAbsent(Map<String, Object> row) {
// 新字段已有值,直接使用
if (row.get("invoiceDirection") != null) {
return;
}
BigDecimal money = Convert.toBigDecimal(row.get("invoiceMoney"), BigDecimal.ZERO);
int direction = InvoiceDirectionEnum.fromMoney(money);
row.put("invoiceDirection", direction);
}
实现的结果:

结语:
这个字段本身,是不是一定非加不可?说实话,我现在依然觉得:不一定。用金额正负 + 前端颜色,
在当时的业务复杂度下,完全可以跑得通。但现实开发里经常是这样:
-
需求不一定最优
-
但你得让系统稳住
-
让历史数据不受影响
-
让后面接手的人少踩坑
所以最终选择了:
-
用枚举把业务含义说清楚
-
用兜底逻辑兼容老数据。