文件
xionggeshop/addons/shopro/library/split/SplitCloseService.php
可乐加冰 6a654070ed ```
feat(split): 新增分账订单线下结案功能

- 添加 SplitCloseService 类用于处理订单结案逻辑
- 实现订单结案状态检查和标记功能
- 在汇付分账重试逻辑中加入结案状态验证,防止已结案订单继续重试
- 更新分账手动服务,添加结案操作类型
- 扩展钱包结算类,增加结案相关诊断方法
- 修改测试脚本,支持批量标记订单为已结案状态
- 添加数据库升级脚本,增加结案相关字段和菜单权限
```
2026-10-24 21:18:48 +08:00

203 行
6.7 KiB
PHP

<?php
namespace addons\shopro\library\split;
use addons\shopro\model\Order;
use addons\shopro\model\split\SplitManualLog;
use addons\shopro\model\split\SplitOrder;
use think\Db;
class SplitCloseService
{
/** 已结案(线下处理),不再重试汇付/微信分账 */
const STATUS_CLOSED = 5;
/**
* 订单是否已标记结案(分账 status=5 或仅有 close 日志)
*/
public function isClosed($orderId)
{
$orderId = intval($orderId);
if ($orderId <= 0) {
return false;
}
if ($this->hasCloseLog($orderId)) {
return true;
}
$row = SplitOrder::where('order_id', $orderId)
->where('channel', 'huifu')
->order('id', 'desc')
->find();
return $row && intval($row['status']) === self::STATUS_CLOSED;
}
public function hasCloseLog($orderId)
{
$orderId = intval($orderId);
if ($orderId <= 0) {
return false;
}
if (!$this->manualLogSupportsClose()) {
return false;
}
return (bool) SplitManualLog::where('order_id', $orderId)->where('action', 'close')->find();
}
/**
* @return int[] order_id 列表(仅有 close 日志、无 status=5 分账单时也视为结案)
*/
public function orderIdsWithCloseLog(array $orderIds)
{
$orderIds = array_values(array_unique(array_filter(array_map('intval', $orderIds))));
if (!$orderIds || !$this->manualLogSupportsClose()) {
return [];
}
return array_map('intval', SplitManualLog::where('order_id', 'in', $orderIds)
->where('action', 'close')
->group('order_id')
->column('order_id'));
}
/**
* 按订单号结案
*
* @return array{order_id:int, split_order_id:int|null, already:bool}
*/
public function closeByOrderSn($orderSn, $reason, $adminId = 0, $adminName = '')
{
$orderSn = trim((string) $orderSn);
if ($orderSn === '') {
throw new \InvalidArgumentException('订单号不能为空');
}
$reason = trim((string) $reason);
if ($reason === '') {
throw new \InvalidArgumentException('请填写结案原因');
}
$order = Order::where('order_sn', $orderSn)->find();
if (!$order && strlen($orderSn) < 24) {
$order = Order::where('order_sn', 'like', $orderSn . '%')->order('id', 'desc')->find();
}
if (!$order) {
throw new \RuntimeException('订单不存在: ' . $orderSn);
}
return $this->closeByOrderId(intval($order['id']), $reason, $adminId, $adminName);
}
/**
* @return array{order_id:int, split_order_id:int|null, already:bool}
*/
public function closeByOrderId($orderId, $reason, $adminId = 0, $adminName = '')
{
$orderId = intval($orderId);
$reason = trim((string) $reason);
if ($reason === '') {
throw new \InvalidArgumentException('请填写结案原因');
}
if ($this->isClosed($orderId)) {
return ['order_id' => $orderId, 'split_order_id' => null, 'already' => true];
}
$order = Order::get($orderId);
if (!$order) {
throw new \RuntimeException('订单不存在');
}
$split = SplitOrder::where('order_id', $orderId)
->where('channel', 'huifu')
->order('id', 'desc')
->find();
$config = new Config();
$retryMax = max(5, $config->getRetryMax());
Db::startTrans();
try {
$splitOrderId = null;
if ($split) {
$status = intval($split['status']);
if (in_array($status, [0, 1], true)) {
throw new \RuntimeException('分账进行中,请稍后再结案或先同步状态');
}
$splitOrderId = intval($split['id']);
$split->status = self::STATUS_CLOSED;
$split->retry_count = max(intval($split['retry_count']), $retryMax + 1);
$split->next_retry_time = null;
if ($this->splitOrderHasCloseColumns()) {
$split->close_reason = mb_substr($reason, 0, 255);
$split->close_admin_id = intval($adminId);
$split->closetime = time();
}
$note = "\n[已结案 " . date('Y-m-d H:i:s') . '] ' . $reason;
$split->wx_result = trim((string) ($split['wx_result'] ?? '')) . $note;
$split->save();
}
$this->writeCloseLog($orderId, (string) $order['order_sn'], $reason, $adminId, $adminName, $splitOrderId);
Db::commit();
return ['order_id' => $orderId, 'split_order_id' => $splitOrderId, 'already' => false];
} catch (\Exception $e) {
Db::rollback();
throw $e;
}
}
protected function writeCloseLog($orderId, $orderSn, $reason, $adminId, $adminName, $splitOrderId = null)
{
if (!$this->manualLogSupportsClose()) {
return;
}
SplitManualLog::create([
'order_id' => intval($orderId),
'order_sn' => $orderSn,
'action' => 'close',
'before_items' => [],
'after_items' => [],
'admin_id' => intval($adminId),
'admin_name' => $adminName ?: 'system',
'remark' => mb_substr($reason, 0, 255),
'retry_split_order_id'=> $splitOrderId ? intval($splitOrderId) : null,
'createtime' => time(),
]);
}
protected function splitOrderHasCloseColumns()
{
static $ok = null;
if ($ok !== null) {
return $ok;
}
$prefix = config('database.prefix');
try {
$rows = Db::query("SHOW COLUMNS FROM `{$prefix}shopro_split_order` LIKE 'close_reason'");
$ok = !empty($rows);
} catch (\Exception $e) {
$ok = false;
}
return $ok;
}
protected function manualLogSupportsClose()
{
static $ok = null;
if ($ok !== null) {
return $ok;
}
$prefix = config('database.prefix');
try {
$row = Db::query("SHOW COLUMNS FROM `{$prefix}shopro_split_manual_log` WHERE Field = 'action'");
if (empty($row[0]['Type'])) {
$ok = false;
return $ok;
}
$ok = strpos($row[0]['Type'], 'close') !== false;
} catch (\Exception $e) {
$ok = false;
}
return $ok;
}
}