2026-05-09-night-market-commission-design.md 3.4 KB

夜市摊位抽成设计

背景

现有抽成模式:平台从商家(userType=1)每笔交易中按 commission 比例收取佣金,收入入商家个人钱包。

新增夜市层级后,交易主体变为摊位(PosStore, isStall=1),摊位归属夜市(InfoUser, userType=3)管理。摊位可能有多个摊位主,钱包是门店维度的,不是个人维度的。

业务规则

  • 平台从摊位每笔交易中收取佣金,比例由所属夜市统一设定
  • 夜市不参与交易抽成,只线下收取摊位费
  • 抽成比例存在夜市用户的 commission 字段上,下属所有摊位统一使用
  • 资金流向:交易额 - 平台抽成 = 摊位到手,平台直接扣除
  • 摊位收入进入摊位门店钱包(UserWallet.storeId 关联),不进摊位主个人钱包

技术设计

改动范围

改动 文件
抽成判断逻辑 PosOrderController.setSanghuBilling()
摊位钱包更新 WalletService 新增摊位钱包操作方法
账单关联摊位 UserBilling 新增 storeId 字段
数据库 user_billing 表新增 store_id 列

1. 抽成判断逻辑

修改 PosOrderController.setSanghuBilling():

PosStore store = posStoreService.getById(posOrder.getMdId())
if (store.getIsStall() == 1 && store.getNightMarketId() != null) {
    // 摊位订单:用夜市的抽成比例
    InfoUser nightMarket = infoUserService.getById(store.getNightMarketId())
    fcbili = nightMarket.getCommission() == null ? 0.00 : nightMarket.getCommission()
} else {
    // 普通商家:保持原逻辑
    InfoUser user = infoUserService.getById(posOrder.getShId())
    fcbili = user.getCommission() == null ? 0.00 : user.getCommission()
}

后续计算不变:chouc = fenc * fcbili,shfc = fenc - chouc。

2. 账单记录

user_billing 表新增 store_id 列(BIGINT, NULL, 默认NULL)。

摊位订单账单记录:

字段 值 说明
userId shId(夜市ID) 标识交易归属哪个夜市
mdId 摊位门店ID 标识哪个摊位的交易
storeId 摊位门店ID 关联摊位钱包(新增字段)
amount shfc 摊位到手金额
divvy chouc 平台抽成金额
divvyRate fcbili 抽成比例

普通商家订单不受影响,storeId 为 NULL。

3. 钱包更新

WalletService 新增摊位钱包操作方法 addStallBalance(Long storeId, BigDecimal amount, UserBilling billing):

1. 通过 storeId 查找 UserWallet(WHERE store_id = ?)
2. 加分布式锁
3. 乐观锁更新钱包余额(与现有 addBalance 逻辑相同)
4. 创建账单记录(billing.storeId 已设置)

setSanghuBilling() 中的调用变更:

if (isStall) {
    billing.setStoreId(store.getId())
    walletService.addStallBalance(store.getId(), amount, billing)
} else {
    walletService.addBalance(userId, amount, billing)  // 原逻辑不变
}

4. 数据模型

仅一个数据库变更:

ALTER TABLE user_billing ADD COLUMN store_id BIGINT DEFAULT NULL COMMENT '关联摊位ID,非摊位账单为NULL';

复用现有字段:

字段 所在实体 用途
commission InfoUser (夜市) 夜市设定的平台抽成比例
isStall PosStore 判断是否为摊位(1=摊位)
nightMarketId PosStore 摊位关联的夜市用户ID
storeId UserWallet 摊位钱包关联的门店ID(已有)