文件
xionggeshop/addons/shopro/library/split/SplitManualService.php
可乐加冰 d2f22d7ecf ```
feat(split): 添加批量查询手动分账覆盖和变更历史功能

- 新增 SplitManualService::orderIdsWithOverride 方法用于批量查询存在手动覆盖的订单
- 新增 SplitManualService::orderIdsWithLog 方法用于批量查询存在变更历史的订单
- 在订单列表页面优化性能,批量查询订单的手动覆盖和变更历史状态
- 添加 can_view_manual_history 权限控制,允许成功/失败后查看变更历史
- 更新前端界面显示逻辑,优化变更历史查看权限判断
- 增强操作提示文案,说明变更历史查看功能
```
2026-10-23 00:15:38 +08:00

582 行
20 KiB
PHP

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

<?php
namespace addons\shopro\library\split;
use addons\shopro\model\Order;
use addons\shopro\model\split\Receiver;
use addons\shopro\model\split\SplitManual;
use addons\shopro\model\split\SplitManualLog;
use addons\shopro\model\split\SplitOrder;
use think\Db;
class SplitManualService
{
protected $config;
public function __construct()
{
$this->config = new Config();
}
/**
* 是否存在手动覆盖
*/
public function hasOverride($orderId)
{
return (bool) SplitManual::where('order_id', intval($orderId))->find();
}
/**
* 批量:存在手动覆盖的 order_id
* @param int[] $orderIds
* @return int[]
*/
public function orderIdsWithOverride(array $orderIds)
{
$orderIds = array_values(array_unique(array_filter(array_map('intval', $orderIds))));
if (!$orderIds) {
return [];
}
return array_map('intval', SplitManual::where('order_id', 'in', $orderIds)->column('order_id'));
}
/**
* 批量:存在变更历史的 order_id
* @param int[] $orderIds
* @return int[]
*/
public function orderIdsWithLog(array $orderIds)
{
$orderIds = array_values(array_unique(array_filter(array_map('intval', $orderIds))));
if (!$orderIds) {
return [];
}
return array_map('intval', SplitManualLog::where('order_id', 'in', $orderIds)->group('order_id')->column('order_id'));
}
/**
* 供 SplitCalculator 使用的明细
* @return array
*/
public function getCalculateItems($orderId)
{
$manual = SplitManual::where('order_id', intval($orderId))->find();
if (!$manual) {
return [];
}
$items = $manual['items'] ?? [];
if (!is_array($items) || empty($items)) {
return [];
}
return $this->normalizeCalculateItems($items);
}
/**
* 保存手动覆盖
* @param int $orderId
* @param array $items [{receiver_id, role_type, amount, description?}]
* @param int $adminId
* @param string $adminName
* @param string $remark
* @return SplitManual
*/
public function save($orderId, array $items, $adminId = 0, $adminName = '', $remark = '')
{
$orderId = intval($orderId);
$this->assertEditableSplitOrder($orderId);
$normalized = $this->validateAndNormalizeItems($orderId, $items);
$existing = SplitManual::where('order_id', $orderId)->find();
$beforeItems = $existing ? ($existing['items'] ?? []) : [];
$action = $existing ? 'update' : 'create';
$originalItems = [];
if (!$existing) {
$calculator = new SplitCalculator();
$originalItems = $calculator->calculateAuto($orderId);
$originalItems = $this->enrichItemsForStorage($originalItems);
} else {
$originalItems = $existing['original_items'] ?? [];
}
$storageItems = $this->enrichItemsForStorage($normalized);
Db::startTrans();
try {
if ($existing) {
$existing->items = $storageItems;
$existing->admin_id = intval($adminId);
$existing->remark = $remark;
$existing->save();
$manual = $existing;
} else {
$manual = SplitManual::create([
'order_id' => $orderId,
'items' => $storageItems,
'original_items' => $originalItems,
'admin_id' => intval($adminId),
'remark' => $remark,
]);
}
$this->appendLog($orderId, $action, $beforeItems, $storageItems, $adminId, $adminName, $remark);
Db::commit();
return $manual;
} catch (\Exception $e) {
Db::rollback();
throw $e;
}
}
/**
* 清除手动覆盖
*/
public function clear($orderId, $adminId = 0, $adminName = '', $remark = '')
{
$orderId = intval($orderId);
$this->assertEditableSplitOrder($orderId);
$existing = SplitManual::where('order_id', $orderId)->find();
if (!$existing) {
return true;
}
$beforeItems = $existing['items'] ?? [];
Db::startTrans();
try {
$existing->delete();
$this->appendLog($orderId, 'clear', $beforeItems, [], $adminId, $adminName, $remark);
Db::commit();
return true;
} catch (\Exception $e) {
Db::rollback();
throw $e;
}
}
/**
* 保存并重试前标记 pending 日志
* @return int log id
*/
public function markPendingRetryLog($orderId, $adminId = 0, $adminName = '', $remark = '')
{
$orderId = intval($orderId);
$manual = SplitManual::where('order_id', $orderId)->find();
if (!$manual) {
throw new \RuntimeException('请先保存手动指定接收方');
}
$order = Order::get($orderId);
$orderSn = $order ? (string) $order['order_sn'] : '';
$log = SplitManualLog::create([
'order_id' => $orderId,
'order_sn' => $orderSn,
'action' => 'retry',
'before_items' => $manual['items'] ?? [],
'after_items' => $manual['items'] ?? [],
'admin_id' => intval($adminId),
'admin_name' => $adminName,
'remark' => $remark,
'retry_result' => 'pending',
'createtime' => time(),
]);
return intval($log->id);
}
/**
* 重试完成后更新日志
*/
public function finishPendingRetryLog($orderId, $success, $message = '', $splitOrderId = null)
{
$orderId = intval($orderId);
$log = SplitManualLog::where('order_id', $orderId)
->where('action', 'retry')
->where('retry_result', 'pending')
->order('id', 'desc')
->find();
if (!$log) {
return;
}
$log->retry_result = $success ? 'success' : 'fail';
$log->retry_message = $message;
if ($splitOrderId) {
$log->retry_split_order_id = intval($splitOrderId);
} else {
$latest = SplitOrder::where('order_id', $orderId)->where('channel', 'huifu')->order('id', 'desc')->find();
if ($latest) {
$log->retry_split_order_id = intval($latest['id']);
}
}
$log->save();
}
/**
* 变更历史
*/
public function getHistory($orderId, $offset = 0, $limit = 20)
{
$orderId = intval($orderId);
$query = SplitManualLog::where('order_id', $orderId);
$total = $query->count();
$rows = SplitManualLog::where('order_id', $orderId)
->order('id', 'desc')
->limit($offset, $limit)
->select();
$list = [];
foreach ($rows as $row) {
$data = is_array($row) ? $row : $row->toArray();
$data['action_text'] = $this->actionLabel($data['action'] ?? '');
$data['retry_result_text'] = $this->retryResultLabel($data['retry_result'] ?? '');
$data['createtime_text'] = !empty($data['createtime'])
? date('Y-m-d H:i:s', $data['createtime'])
: '-';
$list[] = $data;
}
return ['total' => $total, 'rows' => $list];
}
/**
* 汇付接收方下拉选项
*/
public function listHuifuReceiverOptions($keyword = '')
{
$query = Receiver::where('status', 'normal')
->where(function ($q) {
$q->where('channel', 'huifu')->whereOr('channel', null)->whereOr('channel', '');
})
->where('receiver_account', '<>', '');
$keyword = trim((string) $keyword);
if ($keyword !== '') {
$query->where(function ($q) use ($keyword) {
$like = '%' . $keyword . '%';
$q->where('receiver_name', 'like', $like)
->whereOr('receiver_account', 'like', $like);
});
}
$rows = $query->order('id', 'desc')->limit(200)->select();
$options = [];
foreach ($rows as $row) {
$data = is_array($row) ? $row : $row->toArray();
$account = trim((string) ($data['receiver_account'] ?? ''));
if ($account === '') {
continue;
}
$options[] = [
'id' => intval($data['id']),
'role_type' => $data['role_type'] ?? '',
'role_id' => intval($data['role_id'] ?? 0),
'receiver_account' => $account,
'receiver_name' => trim((string) ($data['receiver_name'] ?? '')),
'receiver_type' => $data['receiver_type'] ?? 'HUIFU_ID',
'label' => $this->formatReceiverOptionLabel($data),
];
}
return $options;
}
/**
* 加载指定失败单的编辑上下文
*/
public function getManualContext($splitOrderId)
{
$splitOrder = SplitOrder::get(intval($splitOrderId));
if (!$splitOrder) {
throw new \RuntimeException('分账记录不存在');
}
$this->assertEditableSplitRow($splitOrder);
$orderId = intval($splitOrder['order_id']);
$order = Order::get($orderId);
$manual = SplitManual::where('order_id', $orderId)->find();
$calculator = new SplitCalculator();
$autoItems = $calculator->calculateAuto($orderId);
$autoItems = $this->enrichItemsForStorage($autoItems);
$currentItems = $manual ? ($manual['items'] ?? []) : $autoItems;
$originalItems = $manual ? ($manual['original_items'] ?? []) : $autoItems;
$confirmBase = null;
if (class_exists('\\addons\\shopro_huifu\\library\\HuifuSplitService') && $order) {
try {
$svc = new \addons\shopro_huifu\library\HuifuSplitService();
$confirmBase = $svc->previewConfirmPayFee($order['order_sn'], $order['pay_fee']);
} catch (\Throwable $e) {
$confirmBase = floatval($order['pay_fee'] ?? 0);
}
}
return [
'split_order_id' => intval($splitOrder['id']),
'order_id' => $orderId,
'order_sn' => $splitOrder['order_sn'] ?? ($order['order_sn'] ?? ''),
'pay_fee' => floatval($order['pay_fee'] ?? 0),
'confirm_base' => floatval($confirmBase ?? ($order['pay_fee'] ?? 0)),
'max_rate' => $this->config->getMaxRate(),
'max_split_amount'=> round(floatval($order['pay_fee'] ?? 0) * $this->config->getMaxRate() / 100, 2),
'has_manual' => (bool) $manual,
'current_items' => $currentItems,
'original_items' => $originalItems,
'auto_items' => $autoItems,
'receiver_options'=> $this->listHuifuReceiverOptions(),
];
}
protected function appendLog($orderId, $action, $beforeItems, $afterItems, $adminId, $adminName, $remark)
{
$order = Order::get($orderId);
SplitManualLog::create([
'order_id' => $orderId,
'order_sn' => $order ? (string) $order['order_sn'] : '',
'action' => $action,
'before_items' => is_array($beforeItems) ? $beforeItems : [],
'after_items' => is_array($afterItems) ? $afterItems : [],
'admin_id' => intval($adminId),
'admin_name' => $adminName,
'remark' => $remark,
'createtime' => time(),
]);
}
protected function assertEditableSplitOrder($orderId)
{
$row = SplitOrder::where('order_id', intval($orderId))
->where('channel', 'huifu')
->where('status', 4)
->order('id', 'desc')
->find();
if (!$row) {
throw new \RuntimeException('仅失败状态的汇付分账记录可指定接收方');
}
}
protected function assertEditableSplitRow($splitOrder)
{
$data = is_array($splitOrder) ? $splitOrder : $splitOrder->toArray();
if (($data['channel'] ?? '') !== 'huifu') {
throw new \RuntimeException('仅汇付通道分账记录可指定接收方');
}
if (intval($data['status'] ?? 0) !== 4) {
throw new \RuntimeException('仅失败状态的分账记录可指定接收方');
}
}
protected function validateAndNormalizeItems($orderId, array $items)
{
if (empty($items)) {
throw new \RuntimeException('请至少添加一行分账明细');
}
$order = Order::get($orderId);
if (!$order) {
throw new \RuntimeException('订单不存在');
}
$payFee = floatval($order['pay_fee']);
$maxSplit = round($payFee * $this->config->getMaxRate() / 100, 2);
$total = 0.0;
$normalized = [];
$paymentHuifuId = $this->getPaymentHuifuId();
foreach ($items as $item) {
$receiverId = intval($item['receiver_id'] ?? 0);
$amount = round(floatval($item['amount'] ?? 0), 2);
if ($receiverId <= 0) {
throw new \RuntimeException('请选择有效的分账接收方');
}
if ($amount <= 0) {
throw new \RuntimeException('分账金额须大于 0');
}
$receiver = Receiver::where('id', $receiverId)->where('status', 'normal')->find();
if (!$receiver) {
throw new \RuntimeException('接收方不存在或已禁用');
}
$account = trim((string) ($receiver['receiver_account'] ?? ''));
if ($account === '') {
throw new \RuntimeException('接收方未配置 huifu_id');
}
if ($paymentHuifuId !== '' && $account === $paymentHuifuId) {
throw new \RuntimeException('不得仅分给收款主商户(' . $paymentHuifuId . ')');
}
$total += $amount;
$normalized[] = [
'receiver_id' => $receiverId,
'role_type' => $item['role_type'] ?? ($receiver['role_type'] ?? 'promoter'),
'role_id' => intval($receiver['role_id'] ?? 0),
'receiver_account' => $account,
'receiver_type' => $receiver['receiver_type'] ?? 'HUIFU_ID',
'receiver_name' => trim((string) ($receiver['receiver_name'] ?? '')),
'amount' => $amount,
'description' => trim((string) ($item['description'] ?? '手动指定')),
];
}
if ($total > $maxSplit + 0.001) {
throw new \RuntimeException(sprintf(
'分账合计 ¥%s 超过上限 ¥%s(订单金额 × %s%%)',
number_format($total, 2, '.', ''),
number_format($maxSplit, 2, '.', ''),
$this->config->getMaxRate()
));
}
$confirmBase = $payFee;
if (class_exists('\\addons\\shopro_huifu\\library\\HuifuSplitService')) {
try {
$svc = new \addons\shopro_huifu\library\HuifuSplitService();
$confirmBase = floatval($svc->previewConfirmPayFee($order['order_sn'], $payFee));
} catch (\Throwable $e) {
// use pay_fee
}
}
if ($total > $confirmBase + 0.001) {
throw new \RuntimeException(sprintf(
'分账合计 ¥%s 超过汇付确认基数 ¥%s',
number_format($total, 2, '.', ''),
number_format($confirmBase, 2, '.', '')
));
}
return $normalized;
}
protected function normalizeCalculateItems(array $items)
{
$result = [];
foreach ($items as $item) {
$amount = round(floatval($item['amount'] ?? 0), 2);
if ($amount <= 0) {
continue;
}
$account = trim((string) ($item['receiver_account'] ?? ''));
if ($account === '') {
$receiverId = intval($item['receiver_id'] ?? 0);
if ($receiverId > 0) {
$receiver = Receiver::get($receiverId);
if ($receiver) {
$account = trim((string) ($receiver['receiver_account'] ?? ''));
}
}
}
if ($account === '') {
continue;
}
$result[] = [
'role_type' => $item['role_type'] ?? 'promoter',
'role_id' => intval($item['role_id'] ?? 0),
'receiver_id' => intval($item['receiver_id'] ?? 0),
'receiver_account' => $account,
'receiver_type' => $item['receiver_type'] ?? 'HUIFU_ID',
'receiver_name' => trim((string) ($item['receiver_name'] ?? '')),
'amount' => $amount,
'description' => trim((string) ($item['description'] ?? '手动指定')),
];
}
return $result;
}
protected function enrichItemsForStorage(array $items)
{
$result = [];
foreach ($items as $item) {
$receiverId = intval($item['receiver_id'] ?? 0);
$account = trim((string) ($item['receiver_account'] ?? ''));
$name = trim((string) ($item['receiver_name'] ?? ''));
$roleType = $item['role_type'] ?? 'promoter';
$roleId = intval($item['role_id'] ?? 0);
if ($receiverId > 0) {
$receiver = Receiver::get($receiverId);
if ($receiver) {
if ($account === '') {
$account = trim((string) ($receiver['receiver_account'] ?? ''));
}
if ($name === '') {
$name = trim((string) ($receiver['receiver_name'] ?? ''));
}
if (!$roleType) {
$roleType = $receiver['role_type'] ?? 'promoter';
}
if (!$roleId) {
$roleId = intval($receiver['role_id'] ?? 0);
}
}
}
$result[] = [
'receiver_id' => $receiverId,
'role_type' => $roleType,
'role_id' => $roleId,
'receiver_account' => $account,
'receiver_type' => $item['receiver_type'] ?? 'HUIFU_ID',
'receiver_name' => $name,
'amount' => round(floatval($item['amount'] ?? 0), 2),
'description' => trim((string) ($item['description'] ?? '')),
];
}
return $result;
}
protected function getPaymentHuifuId()
{
if (!class_exists('\\addons\\shopro_huifu\\library\\HuifuConfig')) {
return '';
}
try {
return trim((string) \addons\shopro_huifu\library\HuifuConfig::getInstance()->get('huifu_id'));
} catch (\Throwable $e) {
return '';
}
}
protected function formatReceiverOptionLabel(array $data)
{
$name = trim((string) ($data['receiver_name'] ?? ''));
$account = trim((string) ($data['receiver_account'] ?? ''));
$roleType = $data['role_type'] ?? '';
$roleMap = [
'platform' => '平台',
'promoter' => '推广员',
'regional_agent' => '区域公司',
];
$roleLabel = $roleMap[$roleType] ?? $roleType;
$parts = array_filter([$roleLabel, $name ?: ('#' . ($data['role_id'] ?? 0)), $account]);
return implode(' · ', $parts);
}
protected function actionLabel($action)
{
$map = [
'create' => '首次指定',
'update' => '修改',
'clear' => '清除',
'retry' => '保存并重试',
];
return $map[$action] ?? $action;
}
protected function retryResultLabel($result)
{
if ($result === null || $result === '') {
return '';
}
$map = [
'pending' => '重试中',
'success' => '成功',
'fail' => '失败',
];
return $map[$result] ?? $result;
}
}