多租户自助售卖机如何支持刷脸支付、付款码支付与分账
明确结论
可以实现,但不能把”直付通”简单替换为另一个支付接口。更合适的是采用支付机构或银行收单机构提供的”服务商/平台型收单”模式:
- 平台以服务商或平台方身份接入支付渠道;
- 每个租户独立完成商户进件,取得自己的子商户号;
- 售卖机发起交易时,系统根据设备所属租户指定对应的子商户;
- 刷脸支付和付款码支付均由该子商户收款;
- 支付成功后,通过渠道分账、交易结算分润或手续费返佣,将平台佣金划给平台;
- 其余资金直接结算到租户绑定的银行账户。
各租户可以共用一个小程序和同一套售卖机程序。资金最终归属于谁,不由小程序 appid 决定,而取决于每笔支付请求中的商户、子商户、门店、设备及订单归属信息。
不同支付机构的产品名称、接口和准入条件并不相同。刷脸支付、付款码支付和分账能力能否同时开通,通常还取决于行业、设备认证、租户资质和服务商权限。因此,技术开发开始前,应先取得渠道方的书面确认。
推荐的整体架构
系统可以分为三个层次:
统一小程序 / 售卖机客户端
│
▼
平台交易系统
├─ 识别设备所属租户
├─ 创建平台订单
├─ 选择支付通道
├─ 计算佣金与租户应得金额
└─ 保存支付、分账和退款状态
│
▼
服务商型支付通道
├─ 平台服务商号
├─ 租户 A 子商户号 → 租户 A 结算账户
├─ 租户 B 子商户号 → 租户 B 结算账户
└─ 平台分账接收方 → 平台佣金账户
例如,一笔订单的支付金额为 100 元,平台佣金为 5 元:
消费者支付:100 元
平台佣金: 5 元
租户所得: 95 元
资金可以采用以下两种处理方式:
- 消费者向租户子商户支付 100 元,支付成功后再将其中的 5 元分给平台;
- 渠道在清算时直接计算平台服务费,将 95 元结算给租户,5 元结算给平台。
并非所有渠道都提供第二种方式。如果渠道只支持交易后分账,就采用第一种方式。
为什么不能只用一个普通商户号收款
如果所有订单都先进入平台自己的普通商户号,再由平台定期向租户转账,技术实现看起来更简单,但会产生一系列问题:
- 平台可能被认定为代收代付;
- 租户资金会先进入平台账户,无法称为”直接结算”;
- 退款、投诉、税务和交易对账都会落到平台主体上;
- 银行流水中的交易商户与实际经营者不一致;
- 可能触发支付机构的风控、限额,甚至导致账户被冻结;
- 平台自行保管和划拨商户资金,可能涉及支付合规问题。
因此,每个租户都应作为支付渠道认可的商户或子商户完成实名进件。平台负责技术服务、订单路由和合规分润,不自行沉淀租户资金。
各租户共用一个小程序如何区分收款主体
小程序可以保持统一,但平台必须建立可靠的租户路由关系:
device_id → tenant_id → sub_merchant_id → settlement_account
售卖机只提交设备编号、商品和支付凭证,不能自行指定子商户号。后端根据已经绑定的设备信息确定收款商户,防止客户端篡改资金流向。
示例数据结构:
CREATE TABLE tenant_payment_account (
tenant_id BIGINT PRIMARY KEY,
channel VARCHAR(32) NOT NULL,
sub_merchant_id VARCHAR(64) NOT NULL,
account_status VARCHAR(32) NOT NULL,
profit_share_status VARCHAR(32) NOT NULL
);
CREATE TABLE vending_device (
device_id VARCHAR(64) PRIMARY KEY,
tenant_id BIGINT NOT NULL,
store_id VARCHAR(64) NOT NULL
);
创建订单时,由服务端完成路由:
public PaymentOrder createOrder(CreateOrderRequest request) {
Device device = deviceRepository.findEnabled(request.deviceId());
TenantPaymentAccount account =
paymentAccountRepository.findAvailable(device.tenantId());
Money totalAmount = pricingService.calculate(
device.tenantId(),
request.items()
);
Money commission = commissionService.calculate(
device.tenantId(),
totalAmount
);
return orderRepository.create(
device.tenantId(),
device.deviceId(),
account.subMerchantId(),
totalAmount,
commission
);
}
sub_merchant_id 必须由后端查询,不能信任客户端上传的值。
付款码支付的实现方式
付款码支付通常采用售卖机扫描消费者付款码的模式:
- 消费者在支付 App 中展示付款码;
- 售卖机扫码器读取付款码;
- 售卖机将付款码和平台订单号提交到平台后端;
- 后端根据设备查找租户子商户号;
- 后端以该子商户身份调用付款码支付接口;
- 支付成功后出货;
- 平台异步执行或确认分账。
以下示例只用于说明业务结构,具体字段和接口名称应以所接支付渠道的当前文档为准:
{
"order_no": "VM202609100001",
"device_id": "DEVICE-10086",
"auth_code": "消费者付款码",
"amount": 1000,
"currency": "CNY"
}
服务端组装渠道请求:
CodePayRequest payRequest = new CodePayRequest();
payRequest.setOutTradeNo(order.getOrderNo());
payRequest.setSubMerchantId(order.getSubMerchantId());
payRequest.setAmount(order.getTotalAmount().toCent());
payRequest.setAuthCode(request.authCode());
payRequest.setDeviceId(order.getDeviceId());
payRequest.setDescription(order.getDescription());
CodePayResult result = paymentChannel.codePay(payRequest);
付款码是高敏感、短时有效的支付凭证,处理时应满足以下要求:
- 全程使用 TLS;
- 不写入业务日志;
- 不在客户端持久化;
- 不保存完整付款码;
- 严格限制接口调用来源;
- 对设备进行身份认证并校验请求签名;
- 防止同一付款码被重复提交。
付款码交易还可能返回”处理中”状态。遇到这种情况,不能立即判定交易失败,也不能重复创建新订单,而应查询原订单的最终状态。
刷脸支付的实现方式
刷脸支付通常不能通过一个普通 HTTP 接口直接完成。人脸采集和身份核验需要使用支付渠道认可的设备、刷脸组件或 SDK。
典型流程如下:
- 售卖机使用渠道认证的刷脸设备和 SDK;
- SDK 完成人脸识别,返回一次性支付凭证;
- 售卖机将凭证提交到平台后端;
- 后端根据
device_id获取对应租户的子商户号; - 后端以该子商户身份发起刷脸支付;
- 确认支付结果后出货;
- 平台按照订单执行分账。
业务请求可以抽象为:
{
"order_no": "VM202609100002",
"device_id": "DEVICE-10086",
"face_token": "一次性刷脸支付凭证",
"amount": 1000
}
后端处理示例:
public PayResult facePay(FacePayCommand command) {
PaymentOrder order = orderRepository.find(command.orderNo());
deviceSecurityService.verifyBinding(
command.deviceId(),
order.getTenantId()
);
FacePayRequest request = new FacePayRequest();
request.setOutTradeNo(order.getOrderNo());
request.setSubMerchantId(order.getSubMerchantId());
request.setFaceToken(command.faceToken());
request.setAmount(order.getTotalAmount().toCent());
request.setDeviceId(command.deviceId());
return paymentChannel.facePay(request);
}
face_token 只是一个抽象名称。不同渠道使用的凭证名称、SDK 流程和加密方式可能完全不同,在确认渠道文档前不应写死。
平台也不能将人脸照片或视频上传到自己的后端进行识别,再把识别结果作为支付授权。支付身份核验应由合规支付渠道及其认证组件完成,平台不应保存消费者的人脸原始图像或生物特征模板。
分账与佣金抽取
支付成功后,需要为订单生成不可变的分账快照:
{
"order_no": "VM202609100001",
"paid_amount": 10000,
"platform_commission": 500,
"tenant_amount": 9500,
"currency": "CNY"
}
金额应统一使用最小货币单位,例如人民币的”分”,不要使用浮点数:
long paidAmount = 10000L;
long platformCommission = 500L;
long tenantAmount = Math.subtractExact(
paidAmount,
platformCommission
);
如果渠道采用”租户先收款、平台后分账”的模式,处理逻辑可以写成:
public void handlePaymentSucceeded(PaymentOrder order) {
if (!order.tryMarkPaid()) {
return;
}
ProfitShareRequest request = new ProfitShareRequest();
request.setTransactionId(order.getChannelTransactionId());
request.setOutOrderNo("PS-" + order.getOrderNo());
request.setSubMerchantId(order.getSubMerchantId());
request.addReceiver(
platformPaymentAccount.getReceiverId(),
order.getCommissionAmount().toCent(),
"平台技术服务费"
);
profitShareService.submit(request);
}
分账请求必须支持幂等。可以使用平台分账单号配合数据库唯一索引,确保同一订单不会被重复分账:
CREATE UNIQUE INDEX uk_profit_share_order
ON profit_share_order(channel, out_order_no);
平台还应分别保存以下信息:
- 平台订单号;
- 渠道交易号;
- 租户子商户号;
- 实付金额;
- 平台佣金;
- 分账单号;
- 支付状态;
- 分账状态;
- 退款状态;
- 渠道通知原文摘要及验签结果。
可选择的接入路径
支付机构官方服务商模式
分别申请微信支付、支付宝等渠道的服务商能力,并为每个租户创建或绑定子商户。
这种方式的交易链路直接,各项能力也比较明确。不过,平台需要分别处理进件、支付、分账、退款和对账,刷脸设备还可能需要针对不同渠道进行适配。
银行或持牌收单机构的平台收单方案
由银行或持牌收单机构提供聚合收单、子商户进件、设备管理及分润服务。
这类方案更有可能同时覆盖刷脸支付、付款码支付和多渠道收单,但接入前必须确认:
- 是否真正按照子商户结算;
- 是否支持平台分润;
- 是否支持交易级分账;
- 刷脸设备是否已经获得支付渠道认证;
- 是否允许一个统一应用服务多个子商户;
- 退款后能否自动退回已经分账的佣金;
- 平台是否需要承担垫资或资金清算责任。
聚合支付服务商
平台也可以接入具备相应资质和渠道授权的聚合支付服务商,减少多渠道开发工作。
不能只依据”支持刷脸”和”支持分账”等宣传信息作出判断。需要确认这两项能力能否在同一产品、同一交易链路及同一子商户模型中组合使用。
小程序支付与售卖机支付应分开建模
统一小程序可以用于选品、登录、领取优惠券和查看订单,但支付场景需要按照实际渠道区分:
小程序内支付 → 小程序支付接口
售卖机扫付款码 → 付款码支付接口
售卖机刷脸 → 刷脸设备 SDK + 刷脸支付接口
这三种支付方式可以共用一套平台订单体系,但不能假设它们使用相同的支付 API。
可以定义统一的内部支付接口:
public interface PaymentChannel {
PayResult miniProgramPay(MiniProgramPayRequest request);
PayResult codePay(CodePayRequest request);
PayResult facePay(FacePayRequest request);
ProfitShareResult profitShare(ProfitShareRequest request);
RefundResult refund(RefundRequest request);
PaymentQueryResult query(String outTradeNo);
}
租户路由、佣金计算和订单状态管理由平台层统一处理,各渠道的字段差异则封装在对应的适配器中。
退款必须和分账一起设计
支付和分账完成后,如果订单发生退款,除了退还消费者款项,还要处理平台已经取得的佣金。
以支付金额 100 元、平台佣金 5 元的订单为例:
- 全额退款时,平台通常需要退回 5 元佣金;
- 部分退款时,应按照合同规则计算平台需要退回的佣金;
- 如果平台分账余额不足,退款可能失败,也可能需要平台补足资金;
- 渠道是否自动回退分账,需要根据实际产品确认。
佣金规则应在创建订单时固化,避免退款时使用已经变更的新费率:
long calculateCommissionRefund(
long originalCommission,
long originalPaidAmount,
long refundAmount
) {
if (refundAmount == originalPaidAmount) {
return originalCommission;
}
return BigDecimal.valueOf(originalCommission)
.multiply(BigDecimal.valueOf(refundAmount))
.divide(
BigDecimal.valueOf(originalPaidAmount),
0,
RoundingMode.DOWN
)
.longValueExact();
}
具体的取整方式应写入租户协议,并与支付渠道允许的分账及退款规则保持一致。
上线前需要向渠道确认的问题
确定替代方案前,应把完整业务链路交给支付渠道或收单机构确认,而不是分开询问单个功能:
- 平台是否可以作为服务商接入多个租户?
- 每个租户是否拥有独立的子商户号和结算账户?
- 同一个小程序或应用是否可以服务这些子商户?
- 子商户是否同时支持付款码支付和刷脸支付?
- 刷脸支付支持哪些设备型号和 SDK?
- 平台佣金采用实时分账、延迟分账还是清算分润?
- 平台可分账比例是否有限制?
- 租户和平台分别需要签署哪些协议?
- 退款、撤销和投诉发生时,分账如何回退?
- 交易账单、分账账单和结算账单是否可以分别下载?
- 租户主体、门店、设备和子商户号是否必须一一绑定?
- 平台是否会接触或沉淀租户资金?
只有确认这些问题,才能判断某个方案能否真正替代直付通。
实施建议
较为稳妥的落地方式是:
- 选择支持服务商型收单的支付渠道或持牌收单机构;
- 让所有租户独立完成子商户进件;
- 将每台售卖机绑定到唯一的租户和门店;
- 由后端根据设备确定子商户,禁止客户端指定收款方;
- 分别接入付款码支付和经过认证的刷脸支付能力;
- 支付成功后,通过渠道执行分账或清算分润;
- 建立支付、分账、退款和结算四套对账机制;
- 正式开发前,使用真实子商户完成小额支付、分账、部分退款和全额退款的闭环验证。
替代直付通的重点并不是寻找一个完全相同的接口,而是找到同时支持”子商户收单、刷脸/付款码支付、平台分润、租户独立结算”的服务商型支付产品。只要每笔支付请求都能路由到正确的子商户,共用一个小程序就不会影响多租户收款和平台佣金分账。
备注:内容仅供参考。
