From 0fdb813c168fccdbda01d5161ae2b05aaae753f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=AF=E4=B9=90=E5=8A=A0=E5=86=B0?= <11869599+addie66@user.noreply.gitee.com> Date: Mon, 5 Oct 2026 12:43:27 +0800 Subject: [PATCH] =?UTF-8?q?```=20feat(split):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E5=88=86=E8=B4=A6=E6=8E=A5=E6=94=B6=E6=96=B9=E6=98=8E=E7=BB=86?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 SplitOrderHelper 类用于同步分账接收方明细数据,包括接收方角色、 姓名、账户和金额等信息。优化管理后台分账订单列表页面,显示详细的 接收方信息和失败原因,提升分账流程的可视化和可追溯性。 ``` --- addons/shopro/library/split/SplitExecutor.php | 2 + .../shopro/library/split/SplitOrderHelper.php | 39 ++++ .../shopro/model/split/SplitOrderReceiver.php | 5 + .../library/HuifuSplitService.php | 4 +- .../admin/controller/shopro/split/Order.php | 181 +++++++++++++++++- .../admin/view/shopro/split/order/index.html | 130 ++++++++++--- 6 files changed, 324 insertions(+), 37 deletions(-) create mode 100644 addons/shopro/library/split/SplitOrderHelper.php diff --git a/addons/shopro/library/split/SplitExecutor.php b/addons/shopro/library/split/SplitExecutor.php index 741c95b..9f0d5e9 100644 --- a/addons/shopro/library/split/SplitExecutor.php +++ b/addons/shopro/library/split/SplitExecutor.php @@ -3,6 +3,7 @@ namespace addons\shopro\library\split; use addons\shopro\model\Order; +use addons\shopro\library\split\SplitOrderHelper; use addons\shopro\model\split\SplitOrder; use addons\shopro\model\split\SplitOrderReceiver; use think\Db; @@ -224,5 +225,6 @@ class SplitExecutor $splitOrder->next_retry_time = time() + $this->config->getRetryInterval(); $splitOrder->save(); } + SplitOrderHelper::syncReceivers($splitOrder->id, $items); } } diff --git a/addons/shopro/library/split/SplitOrderHelper.php b/addons/shopro/library/split/SplitOrderHelper.php new file mode 100644 index 0000000..c57f6fc --- /dev/null +++ b/addons/shopro/library/split/SplitOrderHelper.php @@ -0,0 +1,39 @@ +delete(); + } + $exists = SplitOrderReceiver::where('split_order_id', $splitOrderId)->count(); + if ($exists > 0 && !$replace) { + return; + } + foreach ($items as $item) { + $amount = floatval($item['amount'] ?? 0); + if ($amount <= 0) { + continue; + } + SplitOrderReceiver::create([ + 'split_order_id' => $splitOrderId, + 'receiver_id' => intval($item['receiver_id'] ?? 0), + 'role_type' => $item['role_type'] ?? '', + 'amount' => round($amount, 2), + 'description' => $item['description'] ?? '', + 'status' => 0, + ]); + } + } +} diff --git a/addons/shopro/model/split/SplitOrderReceiver.php b/addons/shopro/model/split/SplitOrderReceiver.php index 762fa12..f9013f4 100644 --- a/addons/shopro/model/split/SplitOrderReceiver.php +++ b/addons/shopro/model/split/SplitOrderReceiver.php @@ -8,4 +8,9 @@ class SplitOrderReceiver extends Model { protected $name = 'shopro_split_order_receiver'; protected $autoWriteTimestamp = false; + + public function receiverConfig() + { + return $this->belongsTo(Receiver::class, 'receiver_id', 'id'); + } } diff --git a/addons/shopro_huifu/library/HuifuSplitService.php b/addons/shopro_huifu/library/HuifuSplitService.php index 8e79ffc..b3ab01a 100644 --- a/addons/shopro_huifu/library/HuifuSplitService.php +++ b/addons/shopro_huifu/library/HuifuSplitService.php @@ -3,6 +3,7 @@ namespace addons\shopro_huifu\library; use addons\shopro\library\split\SplitCalculator; +use addons\shopro\library\split\SplitOrderHelper; use addons\shopro\library\split\Config as SplitConfig; use addons\shopro\model\Order; use addons\shopro\model\split\SplitOrder; @@ -469,7 +470,7 @@ class HuifuSplitService $splitOrder = SplitOrder::where('order_id', $orderId)->where('channel', 'huifu')->find(); $splitConfig = new SplitConfig(); if (!$splitOrder) { - SplitOrder::create([ + $splitOrder = SplitOrder::create([ 'order_id' => $orderId, 'order_sn' => $order['order_sn'], 'channel' => 'huifu', @@ -489,5 +490,6 @@ class HuifuSplitService $splitOrder->next_retry_time = time() + $splitConfig->getRetryInterval(); $splitOrder->save(); } + SplitOrderHelper::syncReceivers($splitOrder->id, $items); } } diff --git a/application/admin/controller/shopro/split/Order.php b/application/admin/controller/shopro/split/Order.php index 3b124fe..cbb418a 100644 --- a/application/admin/controller/shopro/split/Order.php +++ b/application/admin/controller/shopro/split/Order.php @@ -4,6 +4,7 @@ namespace app\admin\controller\shopro\split; use app\admin\controller\shopro\Base; use addons\shopro\library\split\Config as SplitConfig; +use addons\shopro\library\split\SplitCalculator; use addons\shopro\library\split\SplitExecutor; use addons\shopro\library\split\ProfitSharingService; use app\admin\model\shopro\order\Order as ShopOrder; @@ -38,7 +39,9 @@ class Order extends Base $total = $query->where($where)->count(); $list = $this->buildSearch() ->with([ - 'receivers', + 'receivers' => function ($q) { + $q->with('receiverConfig'); + }, 'order' => function ($q) { $q->field('id,order_sn,user_id,pay_fee,paytime,status') ->with(['user' => function ($uq) { @@ -110,14 +113,18 @@ class Order extends Base $buyerNickname = $user['nickname'] ?? ''; $buyerMobile = $user['mobile'] ?? ''; + $orderId = intval($data['order_id'] ?? 0); $receivers = $data['receivers'] ?? []; - $lines = []; - foreach ($receivers as $recv) { - $r = is_array($recv) ? $recv : (method_exists($recv, 'toArray') ? $recv->toArray() : []); - $lines[] = sprintf( - '%s ¥%s', - $this->roleTypeLabel($r['role_type'] ?? ''), - number_format(floatval($r['amount'] ?? 0), 2, '.', '') + $receiverDetails = $this->buildReceiverDetails($orderId, $receivers); + $receiverPreview = empty($receivers) && !empty($receiverDetails); + + $summaryLines = []; + foreach ($receiverDetails as $detail) { + $summaryLines[] = sprintf( + '%s %s ¥%s', + $detail['role_label'], + $detail['display_name'], + number_format(floatval($detail['amount']), 2, '.', '') ); } @@ -136,15 +143,169 @@ class Order extends Base $data['order_paytime_text'] = (is_array($order) && !empty($order['paytime'])) ? date('Y-m-d H:i:s', $order['paytime']) : '-'; - $data['receiver_summary'] = $lines ? implode(';', $lines) : '—'; + $data['receiver_details'] = $receiverDetails; + $data['receiver_preview'] = $receiverPreview; + $data['receiver_summary'] = $summaryLines ? implode(';', $summaryLines) : '—'; + $data['fail_reason'] = $this->formatFailReason($data['wx_result'] ?? '', $status); - unset($data['order']); + unset($data['order'], $data['receivers']); $rows[] = $data; } return $rows; } + protected function buildReceiverDetails($orderId, $receivers) + { + $details = []; + if (!empty($receivers)) { + $userIds = []; + foreach ($receivers as $recv) { + $r = is_array($recv) ? $recv : (method_exists($recv, 'toArray') ? $recv->toArray() : []); + $cfg = $r['receiver_config'] ?? null; + if (is_object($cfg) && method_exists($cfg, 'toArray')) { + $cfg = $cfg->toArray(); + } + $roleId = intval(is_array($cfg) ? ($cfg['role_id'] ?? 0) : 0); + if ($roleId > 0) { + $userIds[] = $roleId; + } + } + $users = $userIds ? $this->loadUsersByIds($userIds) : []; + + foreach ($receivers as $recv) { + $r = is_array($recv) ? $recv : (method_exists($recv, 'toArray') ? $recv->toArray() : []); + $cfg = $r['receiver_config'] ?? null; + if (is_object($cfg) && method_exists($cfg, 'toArray')) { + $cfg = $cfg->toArray(); + } + $cfg = is_array($cfg) ? $cfg : []; + $roleId = intval($cfg['role_id'] ?? 0); + $account = trim((string) ($cfg['receiver_account'] ?? '')); + $recvName = trim((string) ($cfg['receiver_name'] ?? '')); + $user = $roleId && isset($users[$roleId]) ? $users[$roleId] : null; + $details[] = [ + 'role_label' => $this->roleTypeLabel($r['role_type'] ?? ($cfg['role_type'] ?? '')), + 'display_name' => $this->formatReceiverDisplayName($roleId, $recvName, $user), + 'account' => $account, + 'amount' => floatval($r['amount'] ?? 0), + 'status' => intval($r['status'] ?? 0), + ]; + } + return $details; + } + + if ($orderId <= 0) { + return []; + } + + try { + $items = (new SplitCalculator())->calculate($orderId); + } catch (\Throwable $e) { + return []; + } + if (empty($items)) { + return []; + } + + $roleIds = []; + foreach ($items as $item) { + $rid = intval($item['role_id'] ?? 0); + if ($rid > 0) { + $roleIds[] = $rid; + } + } + $users = $roleIds ? $this->loadUsersByIds($roleIds) : []; + + foreach ($items as $item) { + $roleId = intval($item['role_id'] ?? 0); + $user = $roleId && isset($users[$roleId]) ? $users[$roleId] : null; + $details[] = [ + 'role_label' => $this->roleTypeLabel($item['role_type'] ?? ''), + 'display_name' => $this->formatReceiverDisplayName( + $roleId, + trim((string) ($item['receiver_name'] ?? '')), + $user + ), + 'account' => trim((string) ($item['receiver_account'] ?? '')), + 'amount' => floatval($item['amount'] ?? 0), + 'status' => -1, + ]; + } + + return $details; + } + + protected function loadUsersByIds(array $userIds) + { + $userIds = array_values(array_unique(array_filter(array_map('intval', $userIds)))); + if (!$userIds) { + return []; + } + $rows = ShopUser::where('id', 'in', $userIds)->field('id,nickname,mobile')->select(); + $map = []; + foreach ($rows as $row) { + $item = is_array($row) ? $row : $row->toArray(); + $map[intval($item['id'])] = $item; + } + return $map; + } + + protected function formatReceiverDisplayName($roleId, $receiverName, $user) + { + $nickname = is_array($user) ? trim((string) ($user['nickname'] ?? '')) : ''; + $mobile = is_array($user) ? trim((string) ($user['mobile'] ?? '')) : ''; + if ($nickname !== '') { + $label = $nickname; + if ($roleId > 0) { + $label .= '#' . $roleId; + } + if ($mobile !== '') { + $label .= ' ' . $this->maskMobile($mobile); + } + return $label; + } + if ($receiverName !== '') { + return $roleId > 0 ? ($receiverName . '#' . $roleId) : $receiverName; + } + return $roleId > 0 ? ('用户#' . $roleId) : '通用接收方'; + } + + protected function maskMobile($mobile) + { + $mobile = trim((string) $mobile); + if (strlen($mobile) >= 11) { + return substr($mobile, 0, 3) . '****' . substr($mobile, -4); + } + return $mobile; + } + + protected function formatFailReason($wxResult, $status) + { + if ($status !== 4 || $wxResult === '' || $wxResult === null) { + return ''; + } + $raw = is_string($wxResult) ? $wxResult : json_encode($wxResult, JSON_UNESCAPED_UNICODE); + if ($raw === '') { + return ''; + } + if (strpos($raw, 'fund_freeze_stat=FREEZE') !== false) { + return '微信资金冻结,需发货/确认收货后再分账'; + } + $decoded = json_decode($raw, true); + if (is_array($decoded)) { + $parts = array_filter([ + $decoded['resp_desc'] ?? '', + $decoded['resp_code'] ?? '', + $decoded['bank_message'] ?? '', + ]); + if ($parts) { + return implode(' ', $parts); + } + } + return mb_strlen($raw) > 120 ? (mb_substr($raw, 0, 120) . '…') : $raw; + } + protected function formatBuyerDisplay($nickname, $mobile, $userId) { $nickname = trim((string) $nickname); diff --git a/application/admin/view/shopro/split/order/index.html b/application/admin/view/shopro/split/order/index.html index f89c967..5b9fdd4 100644 --- a/application/admin/view/shopro/split/order/index.html +++ b/application/admin/view/shopro/split/order/index.html @@ -26,17 +26,28 @@ gap: 10px; align-items: center; } - .source-main { + .cell-main { font-size: 13px; - color: #333; - font-weight: 500; + color: #303133; line-height: 1.5; + word-break: keep-all; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } - .source-sub { + .cell-sub { font-size: 12px; color: #909399; - line-height: 1.6; - margin-top: 2px; + line-height: 1.5; + margin-top: 4px; + word-break: keep-all; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + .mono-text { + font-family: Consolas, Monaco, "Courier New", monospace; + letter-spacing: 0.2px; } .link-text { color: #7438D5; @@ -44,22 +55,76 @@ } .link-text:hover { opacity: 0.85; } .money-text { color: #e1212b; font-weight: 600; } + .channel-tag { + display: inline-block; + margin-left: 6px; + padding: 0 6px; + font-size: 11px; + line-height: 18px; + border-radius: 4px; + background: #f4f4f5; + color: #606266; + vertical-align: middle; + } .status-tag { display: inline-block; padding: 2px 10px; border-radius: 12px; font-size: 12px; + white-space: nowrap; } .status-tag.s0 { background: #f4f4f5; color: #909399; } .status-tag.s1 { background: #ecf5ff; color: #409eff; } .status-tag.s2 { background: #f0f9eb; color: #67c23a; } .status-tag.s3 { background: #fdf6ec; color: #e6a23c; } .status-tag.s4 { background: #fef0f0; color: #f56c6c; } + .recv-list { display: flex; flex-direction: column; gap: 6px; } + .recv-row { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 6px; + font-size: 12px; + line-height: 1.5; + } + .recv-role { + display: inline-block; + padding: 0 6px; + border-radius: 4px; + background: #f0ebfa; + color: #7438D5; + white-space: nowrap; + } + .recv-name { color: #303133; } + .recv-account { + color: #909399; + font-family: Consolas, Monaco, "Courier New", monospace; + font-size: 11px; + } + .recv-amt { color: #e1212b; font-weight: 600; margin-left: auto; white-space: nowrap; } + .recv-preview-hint { + font-size: 11px; + color: #c0c4cc; + margin-top: 2px; + } + .fail-reason { + font-size: 12px; + color: #f56c6c; + line-height: 1.5; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + word-break: break-all; + } .op-hint { font-size: 12px; color: #909399; margin-left: 4px; } + #splitOrderIndex .el-table .cell { + line-height: 1.5; + }
@@ -92,44 +157,57 @@
- - + + - + - + - + - + - - + + + + + @@ -146,7 +224,7 @@

- 「重试分账」仅对失败记录有效,会向通道重新发起分账;「同步通道状态」只拉取最新结果并更新本页,不会重复扣款。 + 「重试分账」仅对失败记录有效,会向通道重新发起分账;「同步状态」只拉取最新结果并更新本页,不会重复扣款。