feat(split): 新增分账订单线下结案功能 - 添加 SplitCloseService 类用于处理订单结案逻辑 - 实现订单结案状态检查和标记功能 - 在汇付分账重试逻辑中加入结案状态验证,防止已结案订单继续重试 - 更新分账手动服务,添加结案操作类型 - 扩展钱包结算类,增加结案相关诊断方法 - 修改测试脚本,支持批量标记订单为已结案状态 - 添加数据库升级脚本,增加结案相关字段和菜单权限 ```
203 行
6.7 KiB
PHP
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;
|
|
}
|
|
}
|