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) { $originalItems = $this->snapshotPlannedAutoItems($orderId); } else { $originalItems = $existing['original_items'] ?? []; } $storageItems = $this->enrichItemsForStorage($normalized); $itemsChanged = !$existing || !$this->itemsEqual($beforeItems, $storageItems); $logBefore = $existing ? $beforeItems : $originalItems; 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, ]); } if ($itemsChanged) { $this->appendLog($orderId, $action, $logBefore, $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' => [], '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']) : '-'; $before = is_array($data['before_items'] ?? null) ? $data['before_items'] : []; $after = is_array($data['after_items'] ?? null) ? $data['after_items'] : []; $data['before_summary'] = $this->formatItemsBrief($before); $data['after_summary'] = $this->formatItemsBrief($after); $data['change_summary'] = $this->buildChangeSummary($data['action'] ?? '', $before, $after); $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' => '保存并重试', 'close' => '线下结案', ]; return $map[$action] ?? $action; } protected function retryResultLabel($result) { if ($result === null || $result === '') { return ''; } $map = [ 'pending' => '重试中', 'success' => '成功', 'fail' => '失败', ]; return $map[$result] ?? $result; } /** * 首次改人时的「原系统计划」快照(含未进件推广员) */ protected function snapshotPlannedAutoItems($orderId) { $calculator = new SplitCalculator(); $auto = $calculator->calculateAuto($orderId); if (!empty($auto)) { return $this->enrichItemsForStorage($auto); } $items = []; $commissionRewards = CommissionReward::where('order_id', $orderId)->where('status', '>=', 0)->select(); foreach ($commissionRewards as $reward) { $userId = intval($reward['agent_id']); $amount = round(floatval($reward['commission']), 2); if ($userId <= 0 || $amount <= 0) { continue; } $items[] = $this->buildPlannedItem('promoter', $userId, $amount, '原推广员佣金'); } foreach ($this->getRegionalRewardsByOrderId($orderId) as $reward) { $userId = intval($reward['agent_user_id']); $amount = round(floatval($reward['commission']), 2); if ($userId <= 0 || $amount <= 0) { continue; } $items[] = $this->buildPlannedItem('regional_agent', $userId, $amount, '原区域公司佣金'); } return $items; } protected function getRegionalRewardsByOrderId($orderId) { try { return RegionalReward::where('order_id', $orderId)->where('status', '>=', 0)->select(); } catch (\Exception $e) { return RegionalReward::alias('rr') ->join('shopro_regional_agent_order ro', 'ro.id = rr.regional_order_id') ->where('ro.order_id', $orderId) ->where('rr.status', '>=', 0) ->field('rr.*') ->select(); } } protected function buildPlannedItem($roleType, $userId, $amount, $desc) { $receiver = Receiver::where('role_type', $roleType) ->where('role_id', $userId) ->where('status', 'normal') ->find(); $user = User::get($userId); $name = $receiver ? trim((string) ($receiver['receiver_name'] ?? '')) : ''; if ($name === '' && $user) { $name = trim((string) ($user['nickname'] ?? '')) ?: ('用户#' . $userId); } return [ 'receiver_id' => $receiver ? intval($receiver['id']) : 0, 'role_type' => $roleType, 'role_id' => $userId, 'receiver_account' => $receiver ? trim((string) ($receiver['receiver_account'] ?? '')) : '', 'receiver_name' => $name, 'amount' => $amount, 'description' => $desc, ]; } protected function buildChangeSummary($action, array $before, array $after) { if ($action === 'retry') { $afterText = $this->formatItemsBrief($after); return $afterText ? ('按以下接收方重新发起分账:' . $afterText) : '重新发起分账重试'; } if ($action === 'clear') { $beforeText = $this->formatItemsBrief($before); return $beforeText ? ('清除手动指定,恢复自动计算(原手动指定:' . $beforeText . ')') : '清除手动指定,恢复自动计算'; } $beforeText = $this->formatItemsBrief($before); $afterText = $this->formatItemsBrief($after); if ($beforeText === '' && $afterText !== '') { return '指定分账接收方为:' . $afterText; } if ($beforeText === $afterText) { return '分账接收方未变化:' . $afterText; } return '分账接收方:' . ($beforeText ?: '(无有效自动接收方)') . ' → ' . $afterText; } protected function formatItemsBrief(array $items) { if (empty($items)) { return ''; } $parts = []; foreach ($items as $item) { $parts[] = $this->formatOneItemBrief($item); } return implode(';', $parts); } protected function formatOneItemBrief(array $item) { $roleMap = [ 'platform' => '平台', 'promoter' => '推广员', 'regional_agent' => '区域公司', ]; $role = $roleMap[$item['role_type'] ?? ''] ?? ($item['role_type'] ?? '接收方'); $name = trim((string) ($item['receiver_name'] ?? '')); if ($name === '' && !empty($item['role_id'])) { $name = '用户#' . intval($item['role_id']); } $account = trim((string) ($item['receiver_account'] ?? '')); $accountPart = $account !== '' ? $account : '未进件'; $amt = number_format(floatval($item['amount'] ?? 0), 2, '.', ''); return $role . ' ' . $name . '(' . $accountPart . ')¥' . $amt; } protected function itemsEqual(array $before, array $after) { $norm = function (array $items) { $rows = []; foreach ($items as $item) { $rows[] = [ intval($item['receiver_id'] ?? 0), number_format(floatval($item['amount'] ?? 0), 2, '.', ''), trim((string) ($item['receiver_account'] ?? '')), ]; } usort($rows, function ($a, $b) { return ($a[0] <=> $b[0]) ?: strcmp($a[1], $b[1]); }); return $rows; }; return $norm($before) === $norm($after); } }