feat(split): 添加批量查询手动分账覆盖和变更历史功能 - 新增 SplitManualService::orderIdsWithOverride 方法用于批量查询存在手动覆盖的订单 - 新增 SplitManualService::orderIdsWithLog 方法用于批量查询存在变更历史的订单 - 在订单列表页面优化性能,批量查询订单的手动覆盖和变更历史状态 - 添加 can_view_manual_history 权限控制,允许成功/失败后查看变更历史 - 更新前端界面显示逻辑,优化变更历史查看权限判断 - 增强操作提示文案,说明变更历史查看功能 ```
582 行
20 KiB
PHP
582 行
20 KiB
PHP
<?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;
|
||
}
|
||
}
|