fix: 优化生产环境收益稳定发放与队列可靠性,支持自动对账补偿与幂等防重

这个提交包含在:
chen
2026-09-19 16:12:08 +08:00
父节点 aff03bff44
当前提交 7cd54ac592
共有 10 个文件被更改,包括 823 次插入 和 72 次删除

查看文件

@@ -0,0 +1,145 @@
const fs = require('fs');
const path = require('path');
const assert = require('assert');
console.log('================================================================');
console.log(' 恒联数通 · 生产环境收益中心与队列可靠性规范自动化验证规范');
console.log('================================================================\n');
let passCount = 0;
function test(name, fn) {
try {
fn();
console.log(` [PASS] ${name}`);
passCount++;
} catch (err) {
console.error(` [FAIL] ${name}: ${err.message}`);
process.exit(1);
}
}
const rootDir = path.resolve(__dirname, '../..');
// 1. 队列配置验证 (application/extra/queue.php)
const queueConfigPath = path.join(rootDir, 'application/extra/queue.php');
assert(fs.existsSync(queueConfigPath), 'queue.php 必须存在');
const queueConfigContent = fs.readFileSync(queueConfigPath, 'utf8');
test('queue.php 中 expire 严禁设置为 0 (消除任务弹出即超时死循环)', () => {
assert(!queueConfigContent.includes("'expire' => 0,"), "严禁包含 'expire' => 0");
assert(!queueConfigContent.includes("'expire' => 0,"), "严禁包含 'expire' => 0");
});
test('queue.php 中 expire 正确配置为 180 秒安全窗口', () => {
assert(queueConfigContent.includes("'expire' => 180,"), "必须配置 'expire' => 180");
});
test('queue.php 锁定 Redis 隔离库 DB3', () => {
assert(queueConfigContent.includes("'redis.select', 3)"), "必须锁定 DB3 隔离多站点");
});
// 2. 队列任务异常处理与重试保护 (addons/shopro/job/PartnerSettle.php)
const partnerSettleJobPath = path.join(rootDir, 'addons/shopro/job/PartnerSettle.php');
assert(fs.existsSync(partnerSettleJobPath), 'PartnerSettle.php 必须存在');
const partnerSettleJobContent = fs.readFileSync(partnerSettleJobPath, 'utf8');
test('PartnerSettle 包含 handleFailure 阶梯重试与上限保护机制', () => {
assert(partnerSettleJobContent.includes('function handleFailure('), '必须包含 handleFailure 方法');
assert(partnerSettleJobContent.includes('$maxAttempts = 5;'), '重试最大次数应提高至 5 次');
assert(partnerSettleJobContent.includes('$delay = min(60, $attempts * 5);'), '必须采用阶梯式延迟重试');
});
test('PartnerSettle 达到重试上限时持久化标记到订单 ext 并写入告警,禁止静默丢弃', () => {
assert(partnerSettleJobContent.includes("'reward_settle_failed'] = 1"), '达到上限必须记录 reward_settle_failed 标记');
assert(partnerSettleJobContent.includes('[PartnerSettle CRITICAL]'), '达到上限必须写入 CRITICAL 系统告警');
assert(partnerSettleJobContent.includes('$this->failed('), '必须调用 $this->failed() 触发失败回调');
});
// 3. 支付后队列任务幂等防超扣 (addons/shopro/job/OrderPaid.php)
const orderPaidJobPath = path.join(rootDir, 'addons/shopro/job/OrderPaid.php');
assert(fs.existsSync(orderPaidJobPath), 'OrderPaid.php 必须存在');
const orderPaidJobContent = fs.readFileSync(orderPaidJobPath, 'utf8');
test('OrderPaid@paid 增加 paid_forward_processed 幂等防重扣库存控制', () => {
assert(orderPaidJobContent.includes("empty($ext['paid_forward_processed'])"), '必须检查 paid_forward_processed 标记');
assert(orderPaidJobContent.includes("$ext['paid_forward_processed'] = time();"), '处理后必须记录 paid_forward_processed');
});
test('OrderPaid@offline 增加 offline_forward_processed 幂等防重控制', () => {
assert(orderPaidJobContent.includes("empty($ext['offline_forward_processed'])"), '必须检查 offline_forward_processed 标记');
assert(orderPaidJobContent.includes("$ext['offline_forward_processed'] = time();"), '处理后必须记录 offline_forward_processed');
});
test('OrderPaid 包含 handleFailure 重试与失败持久化处理', () => {
assert(orderPaidJobContent.includes('function handleFailure('), 'OrderPaid 必须包含 handleFailure');
assert(orderPaidJobContent.includes('[OrderPaid CRITICAL]'), 'OrderPaid 必须记录严重告警日志');
});
// 4. 收益中心核心结算逻辑增强 (addons/shopro/service/partner/PartnerRewardService.php)
const rewardServicePath = path.join(rootDir, 'addons/shopro/service/partner/PartnerRewardService.php');
assert(fs.existsSync(rewardServicePath), 'PartnerRewardService.php 必须存在');
const rewardServiceContent = fs.readFileSync(rewardServicePath, 'utf8');
test('PartnerRewardService settleOrder 内置 touchOrderTimes 兜底保活', () => {
assert(rewardServiceContent.includes('$this->touchOrderTimes($buyerId,'), 'settleOrder 必须调用 touchOrderTimes 保障自购直推保活');
});
test('PartnerRewardService settleOrder 成功后清理历史失败重试标记', () => {
assert(rewardServiceContent.includes("unset($ext['reward_settle_failed']"), '结算成功后必须清理历史失败标记');
});
test('PartnerRewardService write/account 显式标识新生成记录,双重保障加钱包幂等', () => {
assert(rewardServiceContent.includes('$row->_isNewCreated = true;'), '新建流水标记 _isNewCreated = true');
assert(rewardServiceContent.includes('$existing->_isNewCreated = false;'), '重复流水标记 _isNewCreated = false');
assert(rewardServiceContent.includes('$isNew = ($row && !empty($row->_isNewCreated))'), 'account 结算加款前必须检查 _isNew');
});
// 5. 自动对账与补偿服务 (addons/shopro/service/partner/PartnerCompensateService.php)
const compensateServicePath = path.join(rootDir, 'addons/shopro/service/partner/PartnerCompensateService.php');
assert(fs.existsSync(compensateServicePath), 'PartnerCompensateService.php 必须存在');
const compensateServiceContent = fs.readFileSync(compensateServicePath, 'utf8');
test('PartnerCompensateService 包含精准扫描未结算订单能力 (findUnsettledOrders)', () => {
assert(compensateServiceContent.includes('function findUnsettledOrders('), '必须包含 findUnsettledOrders');
assert(compensateServiceContent.includes('whereNotExists('), '必须通过 whereNotExists 排除已结流水');
assert(compensateServiceContent.includes("where('pr.order_id', '>', 0)"), '必须精准关联已发放订单流水');
});
test('PartnerCompensateService 包含自动安全补偿执行入口 (compensateAll)', () => {
assert(compensateServiceContent.includes('function compensateAll('), '必须包含 compensateAll');
assert(compensateServiceContent.includes('$dryRun = false'), '必须支持 dryRun 参数');
assert(compensateServiceContent.includes('$rewardService->settleOrder($orderId)'), '必须调用 settleOrder 进行安全幂等补偿');
});
test('PartnerCompensateService 包含系统健康度与队列参数体检诊断 (checkHealth)', () => {
assert(compensateServiceContent.includes('function checkHealth()'), '必须包含 checkHealth 方法');
assert(compensateServiceContent.includes("'queue' => ["), '必须诊断输出队列核心配置');
assert(compensateServiceContent.includes('unsettled_orders_count'), '必须诊断输出未结算单量');
});
// 6. 控制台命令扩展 (addons/shopro/console/PartnerSettle.php)
const partnerSettleCmdPath = path.join(rootDir, 'addons/shopro/console/PartnerSettle.php');
assert(fs.existsSync(partnerSettleCmdPath), 'PartnerSettle.php console command 必须存在');
const partnerSettleCmdContent = fs.readFileSync(partnerSettleCmdPath, 'utf8');
test('PartnerSettle 命令行集成 --compensate 与 --check', () => {
assert(partnerSettleCmdContent.includes("'compensate', 'c'"), '必须支持 -c/--compensate 参数');
assert(partnerSettleCmdContent.includes("'check'"), '必须支持 --check 参数');
});
test('PartnerSettle 日结前置自动执行对账补偿,确保日结无漏单', () => {
assert(partnerSettleCmdContent.includes('compensateAll(100)'), '传统日结前必须自动触发补偿对账');
});
// 7. BaseJob 失败日志
const baseJobPath = path.join(rootDir, 'addons/shopro/job/BaseJob.php');
assert(fs.existsSync(baseJobPath), 'BaseJob.php 必须存在');
const baseJobContent = fs.readFileSync(baseJobPath, 'utf8');
test('BaseJob 失败回调显式记录错误日志', () => {
assert(baseJobContent.includes("Log::record('[QUEUE FAILED JOB]"), 'failed 回调必须记录 Log::record');
});
console.log('\n----------------------------------------------------------------');
console.log(`测试执行结果: 通过 ${passCount} 项, 失败 0 项`);
console.log('================================================================\n');

查看文件

@@ -0,0 +1,184 @@
<?php
namespace tests\partner;
define('APP_PATH', __DIR__ . '/../../application/');
define('ROOT_PATH', dirname(realpath(APP_PATH)) . '/');
require_once __DIR__ . '/../../thinkphp/base.php';
require_once __DIR__ . '/../../thinkphp/helper.php';
require_once __DIR__ . '/../../application/common.php';
// 加载环境变量与配置
if (is_file(ROOT_PATH . '.env')) {
$env = parse_ini_file(ROOT_PATH . '.env', true);
foreach ($env as $key => $val) {
$name = 'PHP_' . strtoupper($key);
if (is_array($val)) {
foreach ($val as $k => $v) {
putenv("{$name}_" . strtoupper($k) . "=$v");
}
} else {
putenv("$name=$val");
}
}
}
\think\Config::set(\think\Config::load(APP_PATH . 'config.php'));
\think\Config::set(include APP_PATH . 'database.php', 'database');
\think\Config::set(include APP_PATH . 'extra/queue.php', 'queue');
\think\Loader::addNamespace('app', APP_PATH);
\think\Loader::addNamespace('addons', ROOT_PATH . 'addons/');
require_once __DIR__ . '/../../addons/shopro/helper/helper.php';
use think\Db;
use addons\shopro\service\partner\PartnerConfig;
use addons\shopro\service\partner\PartnerRewardService;
use addons\shopro\service\partner\PartnerCompensateService;
use app\admin\model\shopro\order\Order as OrderModel;
use app\admin\model\shopro\partner\Reward as RewardModel;
use app\admin\model\shopro\user\User as UserModel;
class VerifyProductionQueueAndCompensationSpec
{
private $passed = 0;
private $failed = 0;
public function run()
{
echo "========================================================\n";
echo " 恒联数通 · 生产环境收益中心与队列可靠性自动化验证规范\n";
echo "========================================================\n\n";
$this->testQueueConfigExpire();
$this->testCompensateServiceHealth();
$this->testOrderSettleIdempotency();
$this->testCompensateDryRun();
$this->testOrderPaidIdempotentExt();
$this->testPartnerWriteDuplicateSafety();
echo "\n--------------------------------------------------------\n";
echo "测试执行结果: 通过 {$this->passed} 项, 失败 {$this->failed} 项\n";
echo "========================================================\n";
return $this->failed === 0 ? 0 : 1;
}
private function assert($condition, $name, $detail = '')
{
if ($condition) {
echo " [PASS] {$name}\n";
$this->passed++;
} else {
echo " [FAIL] {$name}" . ($detail ? " -> {$detail}" : '') . "\n";
$this->failed++;
}
}
/**
* 1. 验证队列配置 expire 严禁为 0,且超时窗口充足
*/
private function testQueueConfigExpire()
{
$queueConfig = \think\Config::get('queue');
$expire = $queueConfig['expire'] ?? null;
$this->assert($expire !== 0, '队列 expire 必须严禁为 0 (消除任务弹出即超时死循环)', "当前值: " . var_export($expire, true));
$this->assert($expire >= 60, '队列 expire 必须 >= 60 秒 (提供充足任务执行窗口)', "当前值: {$expire}");
$this->assert(isset($queueConfig['select']) && $queueConfig['select'] === 3, 'Redis 队列库必须隔离锁定在 DB3', "当前值: " . ($queueConfig['select'] ?? 'null'));
}
/**
* 2. 验证系统健康度体检诊断
*/
private function testCompensateServiceHealth()
{
$compensate = new PartnerCompensateService();
$health = $compensate->checkHealth();
$this->assert(in_array($health['status'], ['healthy', 'warning']), '系统健康度状态必须正常或受控警告 (非 critical)', "当前状态: {$health['status']}");
$this->assert(isset($health['unsettled_orders_count']), '诊断输出必须包含未结算订单统计');
$this->assert($health['queue']['expire_seconds'] >= 60, '健康体检排查确认队列过期参数安全');
}
/**
* 3. 验证订单结算接口针对已结算订单具备绝对幂等(不重复发放)
*/
private function testOrderSettleIdempotency()
{
$service = new PartnerRewardService();
// 查询一个已有收益记录的历史合格订单
$existingReward = RewardModel::where('order_id', '>', 0)->where('status', 1)->order('id', 'desc')->find();
if ($existingReward) {
$orderId = $existingReward->order_id;
$countBefore = RewardModel::where('order_id', $orderId)->count();
// 模拟队列重复投递或重启后重放,连续调用 2 次
$res1 = $service->settleOrder($orderId);
$res2 = $service->settleOrder($orderId);
$countAfter = RewardModel::where('order_id', $orderId)->count();
$this->assert($res1 === true && $res2 === true, "已结算订单重复调用 settleOrder({$orderId}) 均必须幂等返回 true");
$this->assert($countBefore === $countAfter, "重复执行 settleOrder 流水记录条数绝不增加 (前: {$countBefore}, 后: {$countAfter})");
} else {
$this->assert(true, '无历史结算订单,跳过存量单防重测试');
}
}
/**
* 4. 验证对账补偿服务 DryRun 试算能力
*/
private function testCompensateDryRun()
{
$compensate = new PartnerCompensateService();
$report = $compensate->compensateAll(10, true);
$this->assert($report['dry_run'] === true, '对账补偿服务必须支持 DryRun 试算');
$this->assert(isset($report['total_found']), '对账报表必须包含扫描到的待结算单量');
$this->assert(is_array($report['details']), '对账报表必须包含对账明细列表');
}
/**
* 5. 验证 OrderPaid 幂等标记机制
*/
private function testOrderPaidIdempotentExt()
{
$order = OrderModel::where('status', 'in', [OrderModel::STATUS_PAID, OrderModel::STATUS_COMPLETED])->order('id', 'desc')->find();
if ($order) {
$ext = is_array($order->ext) ? $order->ext : (json_decode($order->ext ?? '', true) ?: []);
// 验证 ext 结构能够正常承载 paid_forward_processed 字段
$ext['paid_forward_processed'] = time();
$this->assert(!empty($ext['paid_forward_processed']), 'OrderPaid 幂等防重标记 paid_forward_processed 支持正常写入与读取');
} else {
$this->assert(true, '无订单数据,跳过标记字段校验');
}
}
/**
* 6. 验证 PartnerRewardService 写入防重与重复插入安全
*/
private function testPartnerWriteDuplicateSafety()
{
// 验证静态变量与重复流水状态
$existingReward = RewardModel::where('order_id', '>', 0)->find();
if ($existingReward) {
// 尝试对已存在的同一 (order_id, user_id, reward_type) 再次调用 write
$row = PartnerRewardService::write([
'order_id' => $existingReward->order_id,
'buyer_id' => $existingReward->buyer_id,
'user_id' => $existingReward->user_id,
'role' => $existingReward->role,
'reward_type' => $existingReward->reward_type,
'amount' => 10.00,
]);
$this->assert($row !== null, '重复写入必须安全捕获唯一键冲突并返回已存流水对象');
$this->assert(empty($row->_isNewCreated), '重复流水 _isNewCreated 必须为 false (禁止触发钱包重复加款)');
} else {
$this->assert(true, '无流水数据,跳过底层写入防重测试');
}
}
}
// 执行测试
$test = new VerifyProductionQueueAndCompensationSpec();
exit($test->run());