地级市综合运营商做本地生活服务系统,技术评审里除了问「有哪些模块」,更该问「模块边界怎么划、结算字段放哪张表」。若外卖、跑腿、同城团购各维护独立结算导出逻辑,财务对账就要人工拼;加模块时结算规则又要重写一遍,专项定制预算很快烧在返工上。
下文从模块边界、结算表结构、Service 伪代码说明「统一结算中枢 + 业态扩展字段」怎么拆。示例为教学示意,以光合同城当期交付为准。
痛点:结算返工根因往往是字段散落各模块
早期拼装方案常见结构:
- 外卖模块自带
wm_settle_detail,跑腿模块自带errand_settle_detail - 各模块独立导出 CSV,列名、精度、状态枚举各不一致
- 加第三业态时,财务要求的核对列又要重新开发
后果很直观:周会前要对结算,运营从三个后台各导一份表再拼;改一条商务规则,要在多个系统各改一遍。本地生活服务系统若走统一后台一体化,架构上应先收敛结算写路径与导出配置,再谈 UI 有多少菜单。
地级市评审时可先问:结算明细是否全业态共用主表?导出列是否可配置?加模块时结算规则是否可继承?答不上来,后面多半要反复花钱改字段。
模块边界:订单中枢、结算中枢、业态插件
local-life-service/ ├── apps/ │ ├── user-app/ │ ├── merchant-app/ │ ├── rider-app/ │ └── admin-console/ # 统一运营后台 ├── order-hub/ # 订单中枢(唯一写入口) │ ├── command-api/ │ ├── state-machine/ │ └── read-model/ ├── settlement-hub/ # 结算中枢(唯一写入口) │ ├── settle-command/ │ ├── export-config/ │ ├── reconcile-read/ │ └── field-mapping.yaml ├── biz-plugins/ │ ├── takeout/ │ ├── errand/ │ └── group-buy/ ├── mid-shared/ │ ├── user-master/ │ ├── merchant-master/ │ └── marketing-core/ └── ops/ ├── settle-export-columns.yaml └── biz-settle-rules.yaml边界原则:
order-hub管订单状态,settlement-hub管结算明细与导出,业态插件禁止直连 UPDATE 结算主表。- 各
biz-plugin只提供业态扩展字段与合法状态分支,结算通用字段在settlement-hub统一维护。 - Admin 控制台结算导出走
export-config,列定义来自 YAML,而非各模块硬编码。
结算主表:通用字段 + 业态扩展
-- 结算明细主表:全业态共用CREATETABLEst_settle_detail(idBIGINTPRIMARYKEYAUTO_INCREMENT,order_idBIGINTNOTNULL,order_noVARCHAR(32)NOTNULL,biz_typeVARCHAR(16)NOTNULLCOMMENT'takeout|errand|group_buy|...',merchant_idBIGINTNOTNULL,user_idBIGINTNOTNULL,city_codeVARCHAR(12)NOTNULL,order_amountDECIMAL(12,2)NOTNULLCOMMENT'订单原价',discount_amountDECIMAL(12,2)NOTNULLDEFAULT0COMMENT'优惠抵扣',platform_feeDECIMAL(12,2)NOTNULLDEFAULT0COMMENT'平台服务费(客户自定规则)',merchant_incomeDECIMAL(12,2)NOTNULLCOMMENT'商家应得',rider_feeDECIMAL(12,2)NULLCOMMENT'配送费(如有)',settle_statusVARCHAR(16)NOTNULLCOMMENT'pending|confirmed|paid|disputed',settle_batch_noVARCHAR(32)NULLCOMMENT'结算批次号',created_atDATETIMENOTNULL,updated_atDATETIMENOTNULL,INDEXidx_merchant_batch(merchant_id,settle_batch_no),INDEXidx_biz_status(biz_type,settle_status))COMMENT='本地生活统一结算明细';-- 外卖结算扩展CREATETABLEst_settle_takeout_ext(settle_idBIGINTPRIMARYKEY,delivery_subsidyDECIMAL(12,2)NULLCOMMENT'配送补贴',packaging_feeDECIMAL(12,2)NULL,CONSTRAINTfk_takeout_settleFOREIGNKEY(settle_id)REFERENCESst_settle_detail(id));-- 跑腿结算扩展CREATETABLEst_settle_errand_ext(settle_idBIGINTPRIMARYKEY,distance_kmDECIMAL(8,2)NULL,weight_surchargeDECIMAL(12,2)NULL,CONSTRAINTfk_errand_settleFOREIGNKEY(settle_id)REFERENCESst_settle_detail(id));-- 导出配置:列定义可配置,避免各模块硬编码CREATETABLEst_export_column_def(idINTPRIMARYKEYAUTO_INCREMENT,column_keyVARCHAR(32)NOTNULLUNIQUE,column_labelVARCHAR(64)NOTNULL,source_tableVARCHAR(64)NOTNULLCOMMENT'st_settle_detail|ext',source_fieldVARCHAR(64)NOTNULL,sort_orderINTNOTNULLDEFAULT0,enabledTINYINTNOTNULLDEFAULT1);设计约束:
merchant_income、platform_fee等通用字段只在st_settle_detail维护,禁止各模块复制结算表。- 业态差异放
st_settle_*_ext,导出时 JOIN 主表与扩展表。 - 商务结算规则由客户确定;系统提供可配置核对字段与导出项,不作收益承诺。
导出列配置:YAML 驱动,少返工
# ops/settle-export-columns.yaml(示意)default_export:columns:-key:order_nolabel:订单号source:st_settle_detail.order_no-key:biz_typelabel:业态source:st_settle_detail.biz_type-key:merchant_namelabel:商家名称source:md_merchant.namejoin:merchant_id-key:order_amountlabel:订单金额source:st_settle_detail.order_amount-key:discount_amountlabel:优惠抵扣source:st_settle_detail.discount_amount-key:platform_feelabel:平台服务费source:st_settle_detail.platform_fee-key:merchant_incomelabel:商家应得source:st_settle_detail.merchant_income-key:settle_statuslabel:结算状态source:st_settle_detail.settle_status-key:settle_batch_nolabel:结算批次source:st_settle_detail.settle_batch_notakeout_extra:-key:delivery_subsidylabel:配送补贴source:st_settle_takeout_ext.delivery_subsidywhen_biz:takeouterrand_extra:-key:distance_kmlabel:配送公里source:st_settle_errand_ext.distance_kmwhen_biz:errand财务临时要求加一列核对项时,优先在 YAML 和st_export_column_def配置,而非各模块改硬编码导出逻辑。这是减少本地生活服务系统后期返工的关键。
业态结算规则:分支在配置,不在代码复制
# ops/biz-settle-rules.yaml(示意)common:platform_fee_mode:percent# percent|fixed|tiered(客户配置)platform_fee_rate:0.05# 示意值,客户自定merchant_income_formula:"order_amount - discount_amount - platform_fee"takeout:inherit:commonextra_fields:-packaging_fee-delivery_subsidyrider_fee_source:order_ext.rider_feeerrand:inherit:commonextra_fields:-distance_km-weight_surchargerider_fee_source:calculated_by_distancegroup_buy:inherit:commonextra_fields:-verify_code-verify_time加第三业态时,只需在 YAML 增加inherit: common与扩展字段清单,结算主表结构不变。
Service 伪代码:结算唯一写入口
@ServicepublicclassSettlementCommandService{publicSettleDetailconfirmSettle(ConfirmSettleCmdcmd,Operatorop){Orderorder=orderRepo.findById(cmd.getOrderId());SettleRulerule=ruleLoader.load(order.getBizType());SettleDetaildetail=newSettleDetail();detail.setOrderId(order.getId());detail.setOrderNo(order.getOrderNo());detail.setBizType(order.getBizType());detail.setMerchantId(order.getMerchantId());detail.setOrderAmount(order.getPayAmount());detail.setDiscountAmount(calcDiscount(order));detail.setPlatformFee(calcPlatformFee(order,rule));detail.setMerchantIncome(calcMerchantIncome(order,rule));detail.setSettleStatus("pending");settleRepo.save(detail);saveBizExtIfNeeded(detail,order,rule);auditLog.append("settle_confirmed",detail.getId(),op);returndetail;}privateBigDecimalcalcPlatformFee(Orderorder,SettleRulerule){if("percent".equals(rule.getPlatformFeeMode())){returnorder.getPayAmount().multiply(rule.getPlatformFeeRate()).setScale(2,RoundingMode.HALF_UP);}// fixed / tiered 分支略returnBigDecimal.ZERO;}}@ServicepublicclassSettlementExportService{publicExportFileexportByBatch(StringbatchNo,ExportProfileprofile){List<ColumnDef>columns=columnDefRepo.load(profile);List<Map<String,Object>>rows=reconcileRead.query(batchNo,columns);returncsvWriter.write(rows,columns);}}所有结算明细生成只经SettlementCommandService;导出只经SettlementExportService,Admin 控制台不得各模块各自写导出 SQL。
与统一后台的关系
结算中枢与订单中枢、用户核心资料同属统一后台层:
- 统一后台:结算导出、批次确认、争议标记在同一 Admin 入口,按
biz_type筛选。 - 数据互通:结算明细关联统一
order_id、merchant_id、user_id,禁止各模块独立结算副本。 - 能力共享:新增导出列、调整核对字段,在
settlement-hub/export-config一处变更,各业态同步生效。
光合同城国内综合形态走统一后台一体化路线:内置多业务模块,模块数据互通、后台统一管理。成品可直接部署;支持私有化源码与按需定制,商务结算规则由客户自行确定。
验收清单(技术评审可用)
- 结算主表是否全业态共用?禁止各模块复制
settle_detail。 - 导出列是否 YAML 可配置?财务临时加列是否不必改各模块代码?
- 加第三业态时,结算规则是否
inherit: common即可? - 结算写路径是否唯一?禁止业态插件直连 UPDATE 结算表。
- 统一 Admin 是否一处导出,而非每个业态各导一份再拼?
本地生活服务系统里,模块边界与结算字段怎么拆,决定了后期是改配置还是反复返工。先把结算中枢和导出配置收敛,再按节奏叠业态插件,比每个模块各存各的结算逻辑,往往更稳。