1. 业务目标与总体架构
1.1 商业目标与范围
在PHP酒店预订系统中实现稳定、可追溯的佣金计算与结算能力,是提升合作方积极性和平台 GMP 的关键。该模块需要覆盖不同来源的佣金规则、退款对佣金的回滚、以及多币种/多渠道的对账能力,确保每一笔订单在结算时的金额与发放给合作方的金额一致无误。完整性、可维护性与 可测试性 是设计的核心。
为了实现可扩展的佣金策略和稳定的对账流程,设计应将业务规则、数据持久化和结算落地解耦,确保后续可以灵活调整佣金率、封顶、分配策略等参数,而不改变核心代码。解耦设计和 可观测性是实现高可靠性的基础。
1.2 技术选型与模块划分
模块化设计将佣金相关逻辑分为若干子模块:规则管理、计算引擎、对账与结算、以及 退款与异常处理。系统应提供清晰的 API 边界,方便前端与第三方系统对接,同时具备完善的日志与审计能力以支撑问题排查。模块独立性与 接口稳定性是长期运维的关键。
2. 数据模型与表结构设计
2.1 核心表设计
设计中应包含以下核心表:affiliates(合作方)、commission_rules(佣金规则)、bookings(订单)、commissions(已计算佣金记录)、settlements(结算批次)、以及必要的日志表。通过这些表,可以实现从订单触发到结算落地的全链路追踪。数据一致性与 变更追踪是重点要考虑的方面。
下方给出一个简化的 SQL 设计草案,用于表达字段核心含义与表之间的关系。实际生产中请结合业务需求进行调整。示例仅供参考。
-- 合作方信息
CREATE TABLE affiliates (id BIGINT PRIMARY KEY,name VARCHAR(100) NOT NULL,currency VARCHAR(3) DEFAULT 'USD',payout_account VARCHAR(128),created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);-- 佣金规则:按渠道、订单金额段、或单笔订单的规则
CREATE TABLE commission_rules (id BIGINT PRIMARY KEY,affiliate_id BIGINT NOT NULL,rule_name VARCHAR(100),min_amount DECIMAL(12,2) DEFAULT 0,max_amount DECIMAL(12,2) DEFAULT NULL,rate DECIMAL(5,4) NOT NULL, -- 佣金率,如 0.10 表示 10%cap DECIMAL(12,2) DEFAULT NULL, -- 封顶金额currency VARCHAR(3) DEFAULT 'USD',active BOOLEAN DEFAULT TRUE,created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,FOREIGN KEY (affiliate_id) REFERENCES affiliates(id)
);-- 订单/预订记录
CREATE TABLE bookings (id BIGINT PRIMARY KEY,order_no VARCHAR(50) UNIQUE NOT NULL,affiliate_id BIGINT,amount DECIMAL(12,2) NOT NULL,currency VARCHAR(3) DEFAULT 'USD',status VARCHAR(20),created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,FOREIGN KEY (affiliate_id) REFERENCES affiliates(id)
);-- 已计算的佣金记录
CREATE TABLE commissions (id BIGINT PRIMARY KEY,booking_id BIGINT,affiliate_id BIGINT,rule_id BIGINT,commission_amount DECIMAL(12,2),base_amount DECIMAL(12,2),currency VARCHAR(3),settled BOOLEAN DEFAULT FALSE,settled_at TIMESTAMP NULL,created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,FOREIGN KEY (booking_id) REFERENCES bookings(id),FOREIGN KEY (affiliate_id) REFERENCES affiliates(id),FOREIGN KEY (rule_id) REFERENCES commission_rules(id)
);-- 结算批次
CREATE TABLE settlements (id BIGINT PRIMARY KEY,affiliate_id BIGINT,batch_no VARCHAR(50) UNIQUE,total_amount DECIMAL(12,2),currency VARCHAR(3),status VARCHAR(20), -- PENDING / PAID / FAILEDcreated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,paid_at TIMESTAMP NULL,FOREIGN KEY (affiliate_id) REFERENCES affiliates(id)
);
2.2 关键字段说明
在上面的设计中,affiliate_id 指向合作方,rule_id 对应具体的佣金规则,commission_amount 保存实际划拨金额,settled 指示该笔佣金是否已进入结算。min_amount、max_amount 与 rate 构成了规则核心,cap 防止单笔金额过大导致超出预期。对账与审计依赖于 created_at 与 settled_at 的时间戳。
3. 佣金规则与计算核心
3.1 公式与规则
佣金计算的核心公式通常是:commission = base_amount × rate,其中 base_amount 可以是订单金额、扣除税费后的净额,或按特定策略计算的金额。分段规则、封顶、以及 最高优先级规则 需要在计算时进行优先级判定,以确保边界情况正确处理。准确性是对接商家与平台的双重保障。
在涉及退款或取消的场景下,需考虑 回滚规则,确保退款金额能够对应到已产生的佣金。幂等性与 幂等处理 能避免重复计算或重复结算的问题。
3.2 考虑因素与细化要点
多币种支持时,汇率换算与 币种一致性是关键。对接方可能有不同的结算周期,需实现 批次化结算、对账单生成 与 支付接口 的协同。系统应对边缘案例保持鲁棒性,如 黑白名单订单、高并发下的锁机制 等。
4. 实现示例:完整方法与代码
4.1 计算服务的结构设计
实现应包含一个计算引擎,负责载入规则、解析订单、计算佣金并写入 commissions 表,同时提供一个对账接口以导出结算数据。以下示例展示了一个简化但可扩展的设计要点:模块化、易测试、可扩展。
核心思想是基于订单信息与规则进行自上而下的匹配,先确定适用的规则,再应用相应的费率与封顶,最后落地到数据库。可追溯性与 可变更历史是实现的基础。
db = $db;$this->rulesRepo = $rulesRepo;}// 根据订单与活跃规则计算佣金public function calculateCommissionForBooking(array $booking): array{// 1) 载入当前合作方的有效规则$rules = $this->rulesRepo->getActiveRulesForAffiliate((int)$booking['affiliate_id']);// 2) 选择最匹配的规则(简单示例:金额段匹配)$applied = null;foreach ($rules as $rule) {$min = $rule['min_amount'] ?? 0;$max = $rule['max_amount'] ?? PHP_INT_MAX;if ($booking['amount'] >= $min && $booking['amount'] <= $max) {$applied = $rule;break;}}// 3) 计算佣金if (!$applied) {// 没有匹配规则时,返回零佣金return ['commission_amount' => 0.0,'base_amount' => (float)$booking['amount'],'rule_id' => null];}$rate = (float) $applied['rate'];$base = (float) $booking['amount'];$commission = $base * $rate;// 4) 应用封顶if (!empty($applied['cap']) && $commission > (float)$applied['cap']) {$commission = (float)$applied['cap'];}return ['commission_amount' => $commission,'base_amount' => $base,'rule_id' => (int)$applied['id']];}// 将计算结果落地到 commissions 表public function persistCommission(array $booking, array $calcResult): void{$stmt = $this->db->prepare("INSERT INTO commissions (booking_id, affiliate_id, rule_id,commission_amount, base_amount, currency, created_at) VALUES (:booking_id, :affiliate_id, :rule_id,:commission_amount, :base_amount, :currency, NOW())");$stmt->execute([':booking_id' => $booking['id'],':affiliate_id' => $booking['affiliate_id'],':rule_id' => $calcResult['rule_id'],':commission_amount' => $calcResult['commission_amount'],':base_amount' => $calcResult['base_amount'],':currency' => $booking['currency']]);}
}
?>4.2 结算落地与对账示例
以下示例演示如何将未结算的佣金聚合成一个结算批次,并标记为已支付,方便后续对账。批次聚合和 状态管理是结算的核心。
prepare("SELECT SUM(commission_amount) AS totalFROM commissionsWHERE affiliate_id = :affiliate_id AND settled = FALSE AND currency = :currency");$stmt->execute([':affiliate_id' => $affiliateId, ':currency' => $currency]);$row = $stmt->fetch(PDO::FETCH_ASSOC);$total = $row ? (float)$row['total'] : 0.0;if ($total <= 0.0) {return ['batch_no' => '', 'total' => 0.0];}// 2) 创建结算批次纪录$batchNo = 'SB' . strtoupper(substr(md5(uniqid()), 0, 8));$ins = $db->prepare("INSERT INTO settlements (affiliate_id, batch_no, total_amount, currency, status, created_at)VALUES (:affiliate_id, :batch_no, :total_amount, :currency, 'PENDING', NOW())");$ins->execute([':affiliate_id' => $affiliateId,':batch_no' => $batchNo,':total_amount' => $total,':currency' => $currency]);// 3) 标记相关佣金记录为已结算(但需要在支付成功时再改为 PAID)$update = $db->prepare("UPDATE commissionsSET settled = TRUE, settled_at = NOW()WHERE affiliate_id = :affiliate_id AND settled = FALSE AND currency = :currencyAND booking_id IN (SELECT id FROM bookings WHERE affiliate_id = :affiliate_id)");$update->execute([':affiliate_id' => $affiliateId, ':currency' => $currency]);return ['batch_no' => $batchNo, 'total' => $total];
}
?> 5. 退款与异常处理
5.1 退款对佣金的影响
当订单发生退款时,相关佣金通常需要回滚或冲销。实现中应具备 回滚策略、对账单的和解能力,以及对退款金额的分摊规则。这可以通过在 commissions 表中维护一个 refunded_amount 字段,或在计算阶段重新评估并写入新的记录来实现。一致性与 可追溯性是实现的核心。
退款处理应具备幂等性,确保同一笔退款不会重复影响佣金。日志记录与审计字段应覆盖退款时间、原因及相关订单信息,便于后续对账。
5.2 异常场景与容错设计
并发冲突、规则变更、以及外部支付接口失败都可能影响佣金结算的正确性。系统应具备 乐观/悲观锁策略、幂等接口、以及 重试机制,以降低金钱相关操作的风险。可观测性与 故障自愈能力是生产环境的关键。
6. 接口设计、安全性与测试
6.1 API 设计与鉴权
对外暴露的接口应具备严格的鉴权、参数校验与日志记录,避免越权访问和数据泄露。OAuth2/JWT、输入校验、以及 请求幂等性策略应在实现中体现。对账数据导出、结算请求等操作应具有完善的审计轨迹。安全性是高可用系统的底线。
测试方面,应覆盖单位测试、集成测试以及端到端的对账场景。通过 测试用例覆盖率、回滚演练、和 异常场景的模拟,确保在上线前发现潜在问题。

附加:常见错误与排查要点
排查点1:规则未命中或冲突
若出现佣金为零的情况,优先检查是否存在活动规则、金额区间设置、以及币种不一致等问题。规则命中率和 日志中的规则ID是排查的关键线索。
排查点2:结算批次状态异常
如结算显示为 PAID 却未实际打款,需要对接支付网关及批次汇总逻辑进行审计;同样,重复结算需防止,通过幂等键和数据库唯一性约束来防护。
排查点3:退款回滚未生效
退款涉及的佣金回滚要与订单状态同步,确保 已发放佣金 与 实际结算金额 一致。对账时比对订单号、退款金额、及结算金额是有效的排查手段。
以上内容围绕“在 PHP 酒店预订系统中实现佣金计算与结算的完整方法与代码示例”的核心需求展开,包含数据模型设计、规则与计算、落地实现、结算与对账,以及退款与异常处理等全流程,辅以可执行的示例代码与 SQL 结构,帮助开发者在实际项目中快速落地并保障可维护性与可追溯性。


