198 行
7.2 KiB
PHP
198 行
7.2 KiB
PHP
<?php
|
||
|
||
namespace addons\shopro\service\partner;
|
||
|
||
use think\Db;
|
||
use app\admin\model\shopro\order\Order as OrderModel;
|
||
use app\admin\model\shopro\partner\Reward as RewardModel;
|
||
use app\admin\model\shopro\commission\CommissionGoods;
|
||
|
||
/**
|
||
* 恒联数通 · 订单收益自动对账与补偿服务 (Production-Grade Auto-Compensation)
|
||
*
|
||
* 职责:
|
||
* 1. 扫描线上已支付但由于队列崩溃、丢单、重试上限或Redis故障未完成收益结算的合格订单;
|
||
* 2. 具备绝对幂等性安全补偿发放,杜绝漏发与重复发放;
|
||
* 3. 提供收益中心健康度诊断与对账审计报表。
|
||
*/
|
||
class PartnerCompensateService
|
||
{
|
||
/**
|
||
* 查询未结算收益的合格订单
|
||
*
|
||
* @param int $limit 查询数量上限
|
||
* @param int|null $startTime 起始支付时间戳
|
||
* @param int|null $endTime 结束支付时间戳
|
||
* @return \think\Collection
|
||
*/
|
||
public function findUnsettledOrders($limit = 100, $startTime = null, $endTime = null)
|
||
{
|
||
$rateService = new RateService();
|
||
$query = $rateService->qualifiedOrderQuery($startTime, $endTime);
|
||
|
||
$orderTable = (new OrderModel())->getQuery()->getTable();
|
||
$rewardTable = (new RewardModel())->getQuery()->getTable();
|
||
|
||
// 核心排除:在 shopro_partner_reward 表中已存在该订单的有效流水或标记
|
||
$query->whereNotExists(function ($q) use ($rewardTable) {
|
||
$q->table($rewardTable)->alias('pr')
|
||
->whereRaw('pr.order_id = o.id')
|
||
->where('pr.order_id', '>', 0);
|
||
});
|
||
|
||
return $query->order('o.id', 'asc')->limit($limit)->select();
|
||
}
|
||
|
||
/**
|
||
* 查询结算标记失败的异常订单
|
||
*
|
||
* @param int $limit
|
||
* @return \think\Collection
|
||
*/
|
||
public function findFailedMarkedOrders($limit = 50)
|
||
{
|
||
return OrderModel::alias('o')
|
||
->where('o.type', '<>', 'score')
|
||
->where('o.pay_fee', '>', 0)
|
||
->whereIn('o.status', [OrderModel::STATUS_PAID, OrderModel::STATUS_COMPLETED])
|
||
->where('o.ext', 'like', '%"reward_settle_failed":1%')
|
||
->order('o.id', 'asc')
|
||
->limit($limit)
|
||
->select();
|
||
}
|
||
|
||
/**
|
||
* 执行自动对账与收益安全补偿
|
||
*
|
||
* @param int $limit 单次最大补偿订单数
|
||
* @param bool $dryRun 是否仅试算对账不写入
|
||
* @return array 对账与补偿执行报表
|
||
*/
|
||
public function compensateAll($limit = 100, $dryRun = false)
|
||
{
|
||
$unsettled = $this->findUnsettledOrders($limit);
|
||
$rewardService = new PartnerRewardService();
|
||
|
||
$report = [
|
||
'total_found' => count($unsettled),
|
||
'dry_run' => $dryRun,
|
||
'success_count' => 0,
|
||
'failed_count' => 0,
|
||
'skipped_count' => 0,
|
||
'details' => [],
|
||
'time' => date('Y-m-d H:i:s'),
|
||
];
|
||
|
||
foreach ($unsettled as $order) {
|
||
$orderId = intval($order->id);
|
||
$orderSn = $order->order_sn;
|
||
$payFee = (float) $order->pay_fee;
|
||
|
||
if ($dryRun) {
|
||
$report['details'][] = [
|
||
'order_id' => $orderId,
|
||
'order_sn' => $orderSn,
|
||
'pay_fee' => $payFee,
|
||
'status' => 'preview',
|
||
'message' => '待补偿',
|
||
];
|
||
continue;
|
||
}
|
||
|
||
try {
|
||
$result = Db::transaction(function () use ($rewardService, $orderId) {
|
||
return $rewardService->settleOrder($orderId);
|
||
});
|
||
|
||
if ($result) {
|
||
$report['success_count']++;
|
||
$rewards = RewardModel::where('order_id', $orderId)->where('order_id', '>', 0)->select();
|
||
$cashTotal = 0;
|
||
$types = [];
|
||
foreach ($rewards as $r) {
|
||
if ((float) $r->amount > 0 && $r->status == 1) {
|
||
$cashTotal = bcadd((string)$cashTotal, (string)$r->amount, 2);
|
||
}
|
||
$types[] = $r->reward_type;
|
||
}
|
||
|
||
$report['details'][] = [
|
||
'order_id' => $orderId,
|
||
'order_sn' => $orderSn,
|
||
'pay_fee' => $payFee,
|
||
'status' => 'success',
|
||
'cash_distributed' => $cashTotal,
|
||
'reward_types' => array_values(array_unique($types)),
|
||
];
|
||
} else {
|
||
$report['skipped_count']++;
|
||
$report['details'][] = [
|
||
'order_id' => $orderId,
|
||
'order_sn' => $orderSn,
|
||
'pay_fee' => $payFee,
|
||
'status' => 'skipped',
|
||
'message' => '订单不满足计奖条件或已结',
|
||
];
|
||
}
|
||
} catch (\Throwable $e) {
|
||
$report['failed_count']++;
|
||
$errorMsg = $e->getMessage();
|
||
\think\Log::record("[PartnerCompensate] 订单 ID:{$orderId} 补偿执行异常: {$errorMsg}", 'error');
|
||
$report['details'][] = [
|
||
'order_id' => $orderId,
|
||
'order_sn' => $orderSn,
|
||
'pay_fee' => $payFee,
|
||
'status' => 'failed',
|
||
'error' => $errorMsg,
|
||
];
|
||
}
|
||
}
|
||
|
||
return $report;
|
||
}
|
||
|
||
/**
|
||
* 收益中心与队列运行健康度全景诊断
|
||
*
|
||
* @return array
|
||
*/
|
||
public function checkHealth()
|
||
{
|
||
$queueConfig = \think\Config::get('queue');
|
||
$expire = $queueConfig['expire'] ?? null;
|
||
$selectDb = $queueConfig['select'] ?? null;
|
||
|
||
$unsettledCount = count($this->findUnsettledOrders(500));
|
||
$failedMarkCount = count($this->findFailedMarkedOrders(500));
|
||
|
||
$warnings = [];
|
||
if ($expire === 0) {
|
||
$warnings[] = "致命配置警告:queue.expire 为 0,会导致队列任务被反复超时重放与并发重复消费!";
|
||
} elseif ($expire === null || $expire < 60) {
|
||
$warnings[] = "配置建议:queue.expire 应设置为 >= 60 秒(推荐 180 秒)。";
|
||
}
|
||
|
||
if ($unsettledCount > 0) {
|
||
$warnings[] = "发现 {$unsettledCount} 笔已支付但未进行收益结算的订单,建议执行补偿操作。";
|
||
}
|
||
|
||
if ($failedMarkCount > 0) {
|
||
$warnings[] = "发现 {$failedMarkCount} 笔标记为结算重试失败的订单,需重点关注。";
|
||
}
|
||
|
||
return [
|
||
'status' => empty($warnings) ? 'healthy' : ($expire === 0 ? 'critical' : 'warning'),
|
||
'queue' => [
|
||
'connector' => $queueConfig['connector'] ?? 'unknown',
|
||
'expire_seconds' => $expire,
|
||
'redis_db' => $selectDb,
|
||
'default_queue' => $queueConfig['default'] ?? 'default',
|
||
],
|
||
'unsettled_orders_count' => $unsettledCount,
|
||
'failed_marked_orders_count' => $failedMarkCount,
|
||
'warnings' => $warnings,
|
||
'check_time' => date('Y-m-d H:i:s'),
|
||
];
|
||
}
|
||
}
|