01 architecting-sea-payment-and-offline-licensing-pipelines (EN+ZH) - rewrite toyyibPay callback: hash = MD5(secretKey+status+order_id+refno+"ok") with hash_equals, transBegin + lockForUpdateByBillCode (no TOCTOU), integer-cents money comparison, fulfilment queued after commit so a gateway outage cannot roll back a paid order - abstract hardware fingerprint to multi-dimensional immutable registers; add k-of-n (4 choose 3) threshold matching section - drop unmeasured metrics (99.8% / 1.8s / 70%); fix duplicated 离线离线 typo 03 traefik-yaegi-plugin-package-name-trap (EN+ZH) - title: drop second-person "Your" - fix YAIEGI -> YAEGI code comment typo - add Go-spec root cause (hyphen is subtraction, go.mod decoupling vs Yaegi) - add .traefik.yml manifest contract paragraph - sanitize YAML thresholds as illustrative; add "where Yaegi stops being the right tool" boundary section - de-second-person the closing paragraph Consultant review pack v1.1 shipped separately (docx 05 = response ledger).
This commit is contained in:
1 parent
c0ea7ea066
commit
a7077a3a1d
4 files changed
+209
-46
No files matched your search
@@ -13,10 +13,10 @@ summary:
|
||||
audience: "金融科技工程师、SaaS 架构师与技术业务负责人"
|
||||
painPoint: "在东南亚地区销售桌面客户端软件时,直接采用西方主流支付(Stripe/PayPal)会因极低的信用卡普及率导致弃单率高企,且原始二进制极易遭遇跨机器盗版传播。"
|
||||
impact:
|
||||
- metric: "99.8% 本地支付成功率"
|
||||
detail: "原生对接 toyyibPay 聚合网关,覆盖马来西亚 FPX 本地在线网银与 DuitNow QR 直付通道,消解信用卡结算门槛。"
|
||||
- metric: "零信用卡收银通道"
|
||||
detail: "原生对接 toyyibPay 聚合网关,覆盖马来西亚 FPX 本地在线网银与 DuitNow QR 直付通道,从根上移除信用卡结算门槛,而不是在卡片表单上做优化。"
|
||||
- metric: "机器码硬件级防盗"
|
||||
detail: "通过 Keygate 将软件授权与 CPU/主板硬件指纹强绑定并签名,彻底杜绝一码多用,同时支持内网离线离线验活。"
|
||||
detail: "通过 Keygate 将软件授权与 CPU/主板硬件指纹强绑定并签名,彻底杜绝一码多用,同时支持内网纯离线验活。"
|
||||
- metric: "100% 幂等状态机履约"
|
||||
detail: "设计带分布式锁的 Webhook 消费引擎,确保在银行网络波动与重试风暴下绝不发生重复发码与状态覆盖。"
|
||||
stack: ["toyyibPay API", "Keygate 授权服务", "PHP/CodeIgniter", "PostgreSQL", "Telegram 自动化"]
|
||||
@@ -32,7 +32,7 @@ dynamicCta: true
|
||||
在面向东南亚区域(尤其是马来西亚、新加坡和印尼)的企业客户与独立专业人员交付桌面客户端工具时,直接套用欧美的常规 SaaS 支付架构往往会在上线第一天遭遇滑铁卢。
|
||||
|
||||
如果在结账页面上直接套用常规的 Stripe Elements 或 PayPal,系统会立即撞上两堵现实高墙:
|
||||
1. **信用卡渗透率低下**:在马来西亚本地企业主与从业者中,超过 **70% 的线上交易严重依赖 FPX(Financial Process Exchange)本地在线网银直连或 DuitNow QR 扫码支付**,绝大多数人不习惯或根本没有可用于扣费的企业信用卡。
|
||||
1. **信用卡渗透率低下**:在马来西亚本地企业主与从业者中,相当大比例的马来西亚线上零售结算走的是 **FPX(Financial Process Exchange)本地在线网银直连或 DuitNow QR 扫码**,而非 Visa / Mastercard。只渲染信用卡表单的结账页不会慢慢变差——它在第一屏就直接失效。
|
||||
2. **离线桌面软件的盗版风险**:如果是纯离线运行的高性能工具(例如批量视频转码器),二进制包一旦交付,如果缺乏基于物理硬件的授权签名机制,极易在团队内部被无限制复制扩散。
|
||||
|
||||
为了解决一款商业级媒体转码桌面软件的商业变现与履约闭环,我主导设计并落地了一套全自动化的计费与授权分发架构。该架构深度整合了本地 FPX 网银网关(`toyyibPay`)、多通道自动熔断降级、机器硬件绑定的离线授权系统(`Keygate`),以及联动 Telegram 与事务邮件的全闭环即时交付通知。
|
||||
@@ -68,35 +68,91 @@ dynamicCta: true
|
||||
```php
|
||||
public function callback(): ResponseInterface
|
||||
{
|
||||
$refNo = $this->request->getPost('refno');
|
||||
$status = $this->request->getPost('status');
|
||||
$billId = $this->request->getPost('billcode');
|
||||
$amount = $this->request->getPost('amount');
|
||||
$refNo = (string) $this->request->getPost('refno');
|
||||
$status = (string) $this->request->getPost('status'); // 1=成功 2=处理中 3=失败
|
||||
$billId = (string) $this->request->getPost('billcode');
|
||||
$orderId = (string) $this->request->getPost('order_id');
|
||||
$amount = (string) $this->request->getPost('amount');
|
||||
$hash = (string) $this->request->getPost('hash');
|
||||
|
||||
// 1. 幂等拦截:如果订单已处于履约完成状态,直接返回 200 终止重试风暴
|
||||
$order = $this->orderModel->findByBillCode($billId);
|
||||
if (!$order || $order['status'] === 'PAID') {
|
||||
return $this->response->setStatusCode(200)->setBody('OK');
|
||||
// 1. 网关校验和。toyyibPay 不提供签名头,回调里携带的是
|
||||
// hash = MD5(userSecretKey + status + order_id + refno + "ok")
|
||||
$expected = md5($this->gateway->secretKey() . $status . $orderId . $refNo . 'ok');
|
||||
if (!hash_equals($expected, $hash)) {
|
||||
$this->logger->warning('toyyibpay.callback.bad_hash', ['bill' => $billId]);
|
||||
return $this->response->setStatusCode(401)->setBody('INVALID_HASH');
|
||||
}
|
||||
|
||||
// 2. 严格核验状态码与应收金额(防止中间人篡改金额参数)
|
||||
if ($status === '1' && (float)$amount === (float)$order['expected_amount']) {
|
||||
$this->fulfillmentService->fulfillOrder($order['id']);
|
||||
return $this->response->setStatusCode(200)->setBody('OK');
|
||||
// 非成功状态一律不改动订单:2=处理中,3=失败
|
||||
if ($status !== '1') {
|
||||
return $this->response->setStatusCode(200)->setBody('IGNORED');
|
||||
}
|
||||
|
||||
return $this->response->setStatusCode(400)->setBody('INVALID_PAYMENT');
|
||||
// 2. 一个事务、一把行级排他锁:读取与状态扭转属于同一个临界区,
|
||||
// 因此不存在 TOCTOU 竞态窗口。
|
||||
$this->db->transBegin();
|
||||
try {
|
||||
$order = $this->orderModel->lockForUpdateByBillCode($billId); // SELECT ... FOR UPDATE
|
||||
if (!$order) {
|
||||
$this->db->transRollback();
|
||||
return $this->response->setStatusCode(404)->setBody('UNKNOWN_BILL');
|
||||
}
|
||||
if ($order['status'] === 'FULFILLED') {
|
||||
$this->db->transCommit(); // 重放风暴:吸收掉,绝不二次发码
|
||||
return $this->response->setStatusCode(200)->setBody('OK_IDEMPOTENT');
|
||||
}
|
||||
|
||||
// 3. 金额永不使用浮点比对,双方统一为整型分(cents)
|
||||
if ($this->toCents($amount) !== (int) $order['expected_amount_cents']) {
|
||||
$this->db->transRollback();
|
||||
$this->logger->error('toyyibpay.callback.amount_mismatch', [
|
||||
'bill' => $billId, 'got' => $amount,
|
||||
'expected_cents' => $order['expected_amount_cents'],
|
||||
]);
|
||||
return $this->response->setStatusCode(409)->setBody('AMOUNT_MISMATCH');
|
||||
}
|
||||
|
||||
$this->orderModel->updateStatus($order['id'], 'PROCESSING');
|
||||
$this->db->transCommit();
|
||||
} catch (Throwable $e) {
|
||||
$this->db->transRollback();
|
||||
throw $e;
|
||||
}
|
||||
|
||||
// 4. 履约在事务提交之后入队,绝不在锁内执行:Keygate 或 SMTP 故障既不能占住行锁,
|
||||
// 也绝不能让一笔已付款订单被回滚。这才让 [PROCESSING] → [RETRY_QUEUE] 成为安全状态。
|
||||
$this->fulfillmentQueue->push($order['id']);
|
||||
|
||||
return $this->response->setStatusCode(200)->setBody('OK');
|
||||
}
|
||||
|
||||
/** "12.40" -> 1240,纯整数运算,浮点永不进入金额链路 */
|
||||
private function toCents(string $amount): int
|
||||
{
|
||||
[$ringgit, $sen] = array_pad(explode('.', trim($amount), 2), 2, '0');
|
||||
return ((int) $ringgit) * 100 + (int) str_pad(substr($sen, 0, 2), 2, '0');
|
||||
}
|
||||
```
|
||||
|
||||
校验和只能证明回调来自网关,不能证明回调确实到达。因此 `[PROCESSING]` 状态额外挂了一个
|
||||
**主动对账 Worker**:任何停留在 `PROCESSING` 超过结算窗口的订单,都会以服务端身份调用
|
||||
`getBillTransactions` 向 toyyibPay 反查权威账单记录并据此结算。被动 Webhook 与主动对账
|
||||
是两条独立汇入同一个幂等状态转移的路径——这才是「回调丢失」不至于变成「客户付了钱没拿到码」的原因。
|
||||
|
||||
### 2. 基于 Keygate 的硬件指纹绑定与非对称离线验活
|
||||
订单扣款成功后,系统不会生成毫无约束的通用明文激活码,而是即时调用授权中心 API,为买家签发绑定其特定计算机硬件特征的专属 License:
|
||||
|
||||
* 授权码内部编码了功能等级(`standard` / `pro` / `enterprise`)、过期时间戳与特征权限集。
|
||||
* 桌面端首次运行时,自动化采集 CPU 序列号、主板 UUID 与操作系统 GUID 进行单向加盐哈希,生成 256 位不可伪造的硬件指纹。
|
||||
* Keygate 服务端使用 **Ed25519 私钥** 对机器指纹与授权元数据执行数字签名。
|
||||
* 桌面端首次运行时,读取一组**多维不可变硬件寄存器**——SMBIOS 主板标识、处理器架构签名与存储控制器 UUID——并以服务端下发的盐值派生逐组件摘要。具体取哪些寄存器、盐值如何派生属于私有实现,本文有意不公开:公开它等于直接告诉逆向者该 Hook 哪几个系统调用。
|
||||
* Keygate 服务端使用 **Ed25519 私钥** 对逐组件摘要与授权元数据执行数字签名。
|
||||
* 桌面客户端仅内置对应的 **Ed25519 公钥**。此后每次冷启动均在本地直接进行离线数学验签,即使客户将工作站置于完全隔离的物理内网机房,软件依然能零请求离线验证,逆向者也无法通过篡改本地内存字符串实现绕过。
|
||||
|
||||
**逐组件摘要的存在,是为了让授权能扛住合法的硬件变更。** 把所有寄存器拼成一个字符串单次哈希
|
||||
是经典错误:加一条内存或重装系统就改变一个输入,整个摘要雪崩式失效,把付了钱的客户锁在门外。
|
||||
正确做法是让签名负载分别携带各组件摘要,客户端执行 **k-of-n 阈值匹配**——四项中至少命中三项,
|
||||
且两项机身锚定组件(主板或 CPU)至少命中其一,即判定为合法硬件微调并放行。换硬盘、重装系统
|
||||
属于正常升级;把授权移植到另一台机器会同时改变主板与 CPU,直接卡在阈值之外。
|
||||
|
||||
---
|
||||
|
||||
## 一键极速直下与 CDN 动态容灾
|
||||
@@ -156,6 +212,6 @@ def notify_fulfillment(order_id, customer_email, bill_code, license_key, downloa
|
||||
## 交付结果
|
||||
|
||||
该全自动计费与分发流水线投产后:
|
||||
* **全流程零人工干预**:处理数百笔商业交易,从用户网银扣款成功到系统完成机器授权签发并通过邮件送达,平均仅耗时 **1.8 秒**。
|
||||
* **全流程零人工干预**:交易从网银扣款成功到授权签发并邮件送达全流程无人值守;履约以「秒」为单位计,而网关故障只会把订单推进重试队列,不会把它丢掉。
|
||||
* **杜绝私下窜码扩散**:依托硬件级 Keygate 指纹绑定,彻底终结了盗版秘钥在多台设备上的随意复制。
|
||||
* **高可用可观测性**:全链路运行日志与 Telegram 即时通知紧密联动,任何网关故障或 SMTP 队列堆积均能在 30 秒内被快速发现并处理。
|
||||
Reference in new issue
Block a user