'待分账', 1 => '分账中', 2 => '成功', 3 => '部分成功', 4 => '失败', ]; public function _initialize() { parent::_initialize(); $this->model = new \addons\shopro\model\split\SplitOrder; } public function index() { $this->request->filter(['strip_tags']); if ($this->request->isAjax()) { $nobuildfields = ['buyer_keyword', 'order_sn', 'status', 'channel', 'createtime']; list($where, $sort, $order, $offset, $limit) = $this->custombuildparams(null, $nobuildfields); $query = $this->buildSearch(); $total = $query->where($where)->count(); $list = $this->buildSearch() ->with([ 'receivers' => function ($q) { $q->with('receiverConfig'); }, 'order' => function ($q) { $q->field('id,order_sn,user_id,pay_fee,paytime,status') ->with(['user' => function ($uq) { $uq->field('id,nickname,mobile'); }]); }, ]) ->where($where) ->order($sort, $order) ->limit($offset, $limit) ->select(); $list = $this->formatSplitOrderList($list); return $this->success('操作成功', null, ['total' => $total, 'rows' => $list]); } $this->assignconfig('splitHuifuOnly', (new SplitConfig())->isHuifuMode()); return $this->view->fetch(); } protected function buildSearch() { $filter = (array) json_decode($this->request->get('filter', ''), true); $query = $this->model; if (!empty($filter['order_sn'])) { $orderIds = ShopOrder::where('order_sn', 'like', '%' . trim($filter['order_sn']) . '%')->column('id'); $query = $query->where('order_id', 'in', $orderIds ?: [0]); } if (!empty($filter['buyer_keyword'])) { $kw = trim($filter['buyer_keyword']); $userIds = ShopUser::where('nickname|mobile', 'like', '%' . $kw . '%')->column('id'); $orderIds = $userIds ? ShopOrder::where('user_id', 'in', $userIds)->column('id') : []; $query = $query->where('order_id', 'in', $orderIds ?: [0]); } if (isset($filter['status']) && $filter['status'] !== '') { $query = $query->where('status', intval($filter['status'])); } if (!empty($filter['channel'])) { $query = $query->where('channel', $filter['channel']); } if (!empty($filter['createtime'])) { $parts = explode(' - ', $filter['createtime']); if (count($parts) === 2) { $start = strtotime($parts[0]); $end = strtotime($parts[1] . ' 23:59:59'); if ($start && $end) { $query = $query->where('createtime', 'between', [$start, $end]); } } } return $query; } protected function formatSplitOrderList($list) { $rows = []; foreach ($list as $item) { $data = is_array($item) ? $item : $item->toArray(); $order = $data['order'] ?? null; $user = is_array($order) ? ($order['user'] ?? null) : null; $status = intval($data['status'] ?? 0); $channel = $data['channel'] ?? 'wechat'; $buyerNickname = $user['nickname'] ?? ''; $buyerMobile = $user['mobile'] ?? ''; $orderId = intval($data['order_id'] ?? 0); $receivers = $data['receivers'] ?? []; $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, '.', '') ); } $data['buyer_nickname'] = $buyerNickname; $data['buyer_mobile'] = $buyerMobile; $data['buyer_display'] = $this->formatBuyerDisplay( $buyerNickname, $buyerMobile, is_array($order) ? intval($order['user_id'] ?? 0) : 0 ); $data['status_text'] = $this->statusMap[$status] ?? '-'; $data['channel_text'] = $channel === 'huifu' ? '汇付' : '微信'; $data['createtime_text'] = !empty($data['createtime']) ? date('Y-m-d H:i:s', $data['createtime']) : '-'; $data['order_paytime_text'] = (is_array($order) && !empty($order['paytime'])) ? date('Y-m-d H:i:s', $order['paytime']) : '-'; $data['receiver_details'] = $receiverDetails; $data['receiver_preview'] = $receiverPreview; $data['receiver_summary'] = $summaryLines ? implode(';', $summaryLines) : '—'; $data['fail_reason'] = $this->formatFailReason($data['wx_result'] ?? '', $status); $data['fail_reason_full'] = $this->formatFailReasonFull($data['wx_result'] ?? '', $status); $diag = (new SplitCalculator())->diagnose($orderId); $data['split_source'] = $diag['final_source']; $data['split_source_text'] = $this->formatSplitSourceText($diag); $data['split_diagnose'] = $diag['issues']; $data['commission_total'] = $diag['commission_total']; $data['rule_total'] = $diag['rule_total']; 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 && (string) $nickname !== (string) $roleId) { $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 formatSplitSourceText(array $diag) { $map = [ 'commission' => '按佣金分账', 'rule' => '按规则分账(兜底)', 'none' => '未算出明细', ]; $text = $map[$diag['final_source'] ?? ''] ?? '-'; if (($diag['final_source'] ?? '') === 'commission' && ($diag['commission_total'] ?? 0) > 0) { $text .= ' ¥' . number_format(floatval($diag['commission_total']), 2, '.', ''); } elseif (($diag['final_source'] ?? '') === 'rule' && ($diag['rule_total'] ?? 0) > 0) { $text .= ' ¥' . number_format(floatval($diag['rule_total']), 2, '.', ''); } return $text; } 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); } } $full = $this->formatFailReasonFull($wxResult, $status); if ($full === '') { return ''; } return mb_strlen($full) > 48 ? (mb_substr($full, 0, 48) . '…') : $full; } protected function formatFailReasonFull($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'] ?? '', isset($decoded['resp_code']) ? ('resp_code=' . $decoded['resp_code']) : '', $decoded['bank_message'] ?? '', $decoded['trans_stat'] ?? '', ]); if ($parts) { return implode(',', $parts); } } return $raw; } protected function formatBuyerDisplay($nickname, $mobile, $userId) { $nickname = trim((string) $nickname); $mobile = trim((string) $mobile); if ($nickname !== '') { return $mobile !== '' ? ($nickname . '(' . $mobile . ')') : $nickname; } if ($mobile !== '') { return $mobile; } return $userId ? ('用户#' . $userId) : '—'; } protected function roleTypeLabel($type) { $map = [ 'platform' => '平台', 'promoter' => '推广员', 'regional_agent' => '区域公司', ]; return $map[$type] ?? ($type ?: '接收方'); } public function retry($ids = null) { $id = $ids ?: $this->request->post('id'); $row = $this->model->get($id); if (!$row) { $this->error('分账记录不存在,可能已被上次重试替换。请刷新列表后,对最新一条失败记录重试'); } $orderId = (int) $row['order_id']; $channel = $row['channel'] ?? 'wechat'; try { $result = (new SplitExecutor())->retry($id); } catch (\Throwable $e) { $this->error('重试失败:' . $e->getMessage()); } if ($result) { $this->success('重试成功'); } $hint = $this->formatSplitWxResultHint($orderId, $channel); if ($hint === '') { $hint = ':请确认已部署最新 HuifuSplitService.php;或执行 php addons/shopro/tests/split_commission_verify.php try-split ' . $row['order_sn'] . ' --execute 查看详情'; } $this->error('重试失败' . $hint); } protected function formatSplitWxResultHint($orderId, $channel) { $latest = $this->model->where('order_id', $orderId) ->where('channel', $channel) ->order('id', 'desc') ->find(); if (!$latest || empty($latest['wx_result'])) { return ''; } $raw = is_string($latest['wx_result']) ? $latest['wx_result'] : json_encode($latest['wx_result'], JSON_UNESCAPED_UNICODE); if ($raw !== '' && strpos($raw, 'fund_freeze_stat=FREEZE') !== false) { return ':' . mb_substr($raw, 0, 500); } $decoded = json_decode($raw, true); if (is_array($decoded)) { $parts = array_filter([ $decoded['resp_desc'] ?? '', $decoded['resp_code'] ?? '', $decoded['bank_message'] ?? '', ]); return $parts ? (':' . implode(' ', $parts)) : (':' . mb_substr($raw, 0, 300)); } return ':' . mb_substr($raw, 0, 300); } public function query($ids = null) { $row = $this->model->get($ids); if (!$row) { $this->error('记录不存在'); } try { if (($row['channel'] ?? 'wechat') === 'huifu' && class_exists('\\addons\\shopro_huifu\\library\\HuifuSplitService')) { $result = $this->queryHuifuChannelStatus($row); } elseif ((new SplitConfig())->isHuifuMode()) { $this->error('当前为汇付分账,请对 channel=huifu 的分账记录查询,或到汇付控台查看分账结果'); } else { $service = new ProfitSharingService(); $result = $service->queryOrder([ 'out_order_no' => $row['out_order_no'], 'transaction_id' => $row['transaction_id'], ]); } } catch (\think\exception\HttpResponseException $e) { throw $e; } catch (\Throwable $e) { $this->error('查询失败:' . ($e->getMessage() ?: '未知错误')); } $row->wx_result = json_encode($result, JSON_UNESCAPED_UNICODE); $row->save(); $this->success('查询成功', null, $result); } /** * 汇付:未 confirm 成功时用支付查单;已有 confirm_hf_seq_id 时再查分账结果 */ protected function queryHuifuChannelStatus($row) { $payload = []; if (class_exists('\\addons\\shopro_huifu\\library\\HuifuQueryService')) { $payload['pay_query'] = (new \addons\shopro_huifu\library\HuifuQueryService())->queryByOrderSn($row['order_sn']); } $confirmSeq = trim((string) ($row['confirm_hf_seq_id'] ?? '')); if ($confirmSeq !== '') { $service = new \addons\shopro_huifu\library\HuifuSplitService(); $payload['split_query'] = $service->querySplit($confirmSeq); } if (empty($payload)) { throw new \RuntimeException('汇付查询组件不可用'); } return $payload; } }