一、编码写死在代码里,改一次伤一次
做资产管理系统的团队迟早会遇到这个需求变更:原来资产编号是"类别码+流水号",新来的财务总监要求加上年度段,行政部又希望编号里能看出资产在哪个部门。如果编码规则是写死在代码里的一行拼接逻辑,每次调整都是一次发版。
我们的做法是把编码规则做成可配置的引擎:规则本身是一条数据库记录,由若干"段"拼成,段类型支持固定前缀、类别码、日期、部门码、流水号。运营在界面上拖几个段、填个分隔符,新规则即时生效,已经在跑的旧编号不受影响。这篇文章讲这个引擎的两层设计:规则怎么建模,流水号在并发下怎么保证不重不漏。
二、规则建模:一段一行,顺序即语义
编码规则的核心表就两张:规则主表存编码用途(资产编码、入库单编码、报废单编码各一条)和分隔符;段表存每一段的类型、参数和顺序。以资产编码为例,"固定前缀 ASSET + 类别码 + 年月 + 4位流水"存进去是这样:
SEGMENTS = [
{
"type": "prefix", "value": "ASSET"},
{
"type": "category"}, # 类别码,取资产分类表映射
{
"type": "date", "fmt": "%Y%m"},
{
"type": "serial", "width": 4}, # 流水号,按"前缀+类别+年月"分组重计
]
三个容易踩的坑,都是真踩过才记下来的:
- 段定义要带版本。规则改动后,当天已生成的编号必须还能被解析,所以每次修改规则生成一个新版本号,编码本体不变,解析时按生成时间匹配对应版本。
- 日期段用业务日期不用系统日期。补录上月入库的资产,编号里的年月应该是业务发生月,否则对账时编号日期和单据日期对不上。
- 流水号分组键要和段定义一起配。按"类别+年月"分组还是全局分组,决定编号回收语义,两处配置必须出自同一条规则记录,不能各自维护。
三、流水号并发:号段模式,一次取一百个
流水号最怕两件事:重号和跳号被追责。用 SELECT MAX 加一的做法在并发下必撞车,靠数据库唯一索引兜底重试又把吞吐拖垮。我们用的是号段模式:服务启动时向号段表一次性申请一段(比如 100 个),本地原子递增消耗,用完再申请:
def next_serial(group_key, db):
seg = db.serial_pool.find_one_and_update(
{
"group": group_key},
{
"$inc": {
"cursor": POOL_SIZE}},
upsert=True,
)
start = seg["cursor"] - POOL_SIZE + 1
return start, start + POOL_SIZE - 1 # 本地循环使用,用完再取
代价是服务重启会丢弃号段尾部,产生跳号。我们的取舍是明确接受跳号、绝不接受重号——编号的唯一性靠号段表行级锁保证,跳号在编码规则里注明即可。资产编号毕竟不是发票号,连续性没有审计硬要求,吞吐和唯一性优先。另外号段表建议按 group 分区建索引,分组键只有十几个取值,表会一直很小,行级锁竞争天然分散,不需要额外的分布式锁组件。
四、入库即绑定:编号生成只是起点
编码引擎挂接入库流程的审核通过事件:入库单审核后批量生成资产编号,同步写入对应的 RFID 号(EPC 区),编号与 RFID 号一对一绑定。之后标签打印服务按资产类别取模板——固定资产打二维码标签,机房设备打 RFID 标签——模板里就是编码变量的占位渲染,一套引擎同时服务三类标签。
打印记录回传台账时走统一的适配器装配,按标签类型分发:
SINKS = {
"qrcode": LedgerAdapter("首码资产管理系统"),
"rfid": RfidSink(epc_region="EPC-BANK-2"),
}
绑定关系落库后,后续的盘点、调拨、报废全部以编号为主键串联,标签只是编号的物理载体。这一层关系定了,换标签方案就不用动台账。
五、上线半年,变更成本从发版降到分钟级
引擎上线后经历了三次规则调整:加年度段、换类别码映射、报废单增加审批段,三次都是运营在界面改配置完成,没有一次动代码。回过头看,设计上最值的决定是把"规则版本"和"业务日期段"纳入模型——前者保住了存量编号的可解析性,后者避免了对账时永远差一个月的诡异现象。编码是资产系统里最小的一块,却是被引用最多的一块,值得单独认真设计。