```
feat(split): 新增分账订单线下结案功能 - 添加 SplitCloseService 类用于处理订单结案逻辑 - 实现订单结案状态检查和标记功能 - 在汇付分账重试逻辑中加入结案状态验证,防止已结案订单继续重试 - 更新分账手动服务,添加结案操作类型 - 扩展钱包结算类,增加结案相关诊断方法 - 修改测试脚本,支持批量标记订单为已结案状态 - 添加数据库升级脚本,增加结案相关字段和菜单权限 ```
这个提交包含在:
@@ -0,0 +1,202 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\library\split;
|
||||
|
||||
use addons\shopro\model\Order;
|
||||
use addons\shopro\model\split\SplitManualLog;
|
||||
use addons\shopro\model\split\SplitOrder;
|
||||
use think\Db;
|
||||
|
||||
class SplitCloseService
|
||||
{
|
||||
/** 已结案(线下处理),不再重试汇付/微信分账 */
|
||||
const STATUS_CLOSED = 5;
|
||||
|
||||
/**
|
||||
* 订单是否已标记结案(分账 status=5 或仅有 close 日志)
|
||||
*/
|
||||
public function isClosed($orderId)
|
||||
{
|
||||
$orderId = intval($orderId);
|
||||
if ($orderId <= 0) {
|
||||
return false;
|
||||
}
|
||||
if ($this->hasCloseLog($orderId)) {
|
||||
return true;
|
||||
}
|
||||
$row = SplitOrder::where('order_id', $orderId)
|
||||
->where('channel', 'huifu')
|
||||
->order('id', 'desc')
|
||||
->find();
|
||||
return $row && intval($row['status']) === self::STATUS_CLOSED;
|
||||
}
|
||||
|
||||
public function hasCloseLog($orderId)
|
||||
{
|
||||
$orderId = intval($orderId);
|
||||
if ($orderId <= 0) {
|
||||
return false;
|
||||
}
|
||||
if (!$this->manualLogSupportsClose()) {
|
||||
return false;
|
||||
}
|
||||
return (bool) SplitManualLog::where('order_id', $orderId)->where('action', 'close')->find();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int[] order_id 列表(仅有 close 日志、无 status=5 分账单时也视为结案)
|
||||
*/
|
||||
public function orderIdsWithCloseLog(array $orderIds)
|
||||
{
|
||||
$orderIds = array_values(array_unique(array_filter(array_map('intval', $orderIds))));
|
||||
if (!$orderIds || !$this->manualLogSupportsClose()) {
|
||||
return [];
|
||||
}
|
||||
return array_map('intval', SplitManualLog::where('order_id', 'in', $orderIds)
|
||||
->where('action', 'close')
|
||||
->group('order_id')
|
||||
->column('order_id'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 按订单号结案
|
||||
*
|
||||
* @return array{order_id:int, split_order_id:int|null, already:bool}
|
||||
*/
|
||||
public function closeByOrderSn($orderSn, $reason, $adminId = 0, $adminName = '')
|
||||
{
|
||||
$orderSn = trim((string) $orderSn);
|
||||
if ($orderSn === '') {
|
||||
throw new \InvalidArgumentException('订单号不能为空');
|
||||
}
|
||||
$reason = trim((string) $reason);
|
||||
if ($reason === '') {
|
||||
throw new \InvalidArgumentException('请填写结案原因');
|
||||
}
|
||||
|
||||
$order = Order::where('order_sn', $orderSn)->find();
|
||||
if (!$order && strlen($orderSn) < 24) {
|
||||
$order = Order::where('order_sn', 'like', $orderSn . '%')->order('id', 'desc')->find();
|
||||
}
|
||||
if (!$order) {
|
||||
throw new \RuntimeException('订单不存在: ' . $orderSn);
|
||||
}
|
||||
|
||||
return $this->closeByOrderId(intval($order['id']), $reason, $adminId, $adminName);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{order_id:int, split_order_id:int|null, already:bool}
|
||||
*/
|
||||
public function closeByOrderId($orderId, $reason, $adminId = 0, $adminName = '')
|
||||
{
|
||||
$orderId = intval($orderId);
|
||||
$reason = trim((string) $reason);
|
||||
if ($reason === '') {
|
||||
throw new \InvalidArgumentException('请填写结案原因');
|
||||
}
|
||||
|
||||
if ($this->isClosed($orderId)) {
|
||||
return ['order_id' => $orderId, 'split_order_id' => null, 'already' => true];
|
||||
}
|
||||
|
||||
$order = Order::get($orderId);
|
||||
if (!$order) {
|
||||
throw new \RuntimeException('订单不存在');
|
||||
}
|
||||
|
||||
$split = SplitOrder::where('order_id', $orderId)
|
||||
->where('channel', 'huifu')
|
||||
->order('id', 'desc')
|
||||
->find();
|
||||
|
||||
$config = new Config();
|
||||
$retryMax = max(5, $config->getRetryMax());
|
||||
|
||||
Db::startTrans();
|
||||
try {
|
||||
$splitOrderId = null;
|
||||
if ($split) {
|
||||
$status = intval($split['status']);
|
||||
if (in_array($status, [0, 1], true)) {
|
||||
throw new \RuntimeException('分账进行中,请稍后再结案或先同步状态');
|
||||
}
|
||||
$splitOrderId = intval($split['id']);
|
||||
$split->status = self::STATUS_CLOSED;
|
||||
$split->retry_count = max(intval($split['retry_count']), $retryMax + 1);
|
||||
$split->next_retry_time = null;
|
||||
if ($this->splitOrderHasCloseColumns()) {
|
||||
$split->close_reason = mb_substr($reason, 0, 255);
|
||||
$split->close_admin_id = intval($adminId);
|
||||
$split->closetime = time();
|
||||
}
|
||||
$note = "\n[已结案 " . date('Y-m-d H:i:s') . '] ' . $reason;
|
||||
$split->wx_result = trim((string) ($split['wx_result'] ?? '')) . $note;
|
||||
$split->save();
|
||||
}
|
||||
|
||||
$this->writeCloseLog($orderId, (string) $order['order_sn'], $reason, $adminId, $adminName, $splitOrderId);
|
||||
|
||||
Db::commit();
|
||||
return ['order_id' => $orderId, 'split_order_id' => $splitOrderId, 'already' => false];
|
||||
} catch (\Exception $e) {
|
||||
Db::rollback();
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
protected function writeCloseLog($orderId, $orderSn, $reason, $adminId, $adminName, $splitOrderId = null)
|
||||
{
|
||||
if (!$this->manualLogSupportsClose()) {
|
||||
return;
|
||||
}
|
||||
SplitManualLog::create([
|
||||
'order_id' => intval($orderId),
|
||||
'order_sn' => $orderSn,
|
||||
'action' => 'close',
|
||||
'before_items' => [],
|
||||
'after_items' => [],
|
||||
'admin_id' => intval($adminId),
|
||||
'admin_name' => $adminName ?: 'system',
|
||||
'remark' => mb_substr($reason, 0, 255),
|
||||
'retry_split_order_id'=> $splitOrderId ? intval($splitOrderId) : null,
|
||||
'createtime' => time(),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function splitOrderHasCloseColumns()
|
||||
{
|
||||
static $ok = null;
|
||||
if ($ok !== null) {
|
||||
return $ok;
|
||||
}
|
||||
$prefix = config('database.prefix');
|
||||
try {
|
||||
$rows = Db::query("SHOW COLUMNS FROM `{$prefix}shopro_split_order` LIKE 'close_reason'");
|
||||
$ok = !empty($rows);
|
||||
} catch (\Exception $e) {
|
||||
$ok = false;
|
||||
}
|
||||
return $ok;
|
||||
}
|
||||
|
||||
protected function manualLogSupportsClose()
|
||||
{
|
||||
static $ok = null;
|
||||
if ($ok !== null) {
|
||||
return $ok;
|
||||
}
|
||||
$prefix = config('database.prefix');
|
||||
try {
|
||||
$row = Db::query("SHOW COLUMNS FROM `{$prefix}shopro_split_manual_log` WHERE Field = 'action'");
|
||||
if (empty($row[0]['Type'])) {
|
||||
$ok = false;
|
||||
return $ok;
|
||||
}
|
||||
$ok = strpos($row[0]['Type'], 'close') !== false;
|
||||
} catch (\Exception $e) {
|
||||
$ok = false;
|
||||
}
|
||||
return $ok;
|
||||
}
|
||||
}
|
||||
@@ -133,6 +133,13 @@ class SplitExecutor
|
||||
return false;
|
||||
}
|
||||
|
||||
if (intval($splitOrder['status']) === SplitCloseService::STATUS_CLOSED) {
|
||||
throw new \RuntimeException('该订单已标记线下结案,不可重试');
|
||||
}
|
||||
if ((new SplitCloseService())->isClosed((int) $splitOrder['order_id'])) {
|
||||
throw new \RuntimeException('该订单已标记线下结案,不可重试');
|
||||
}
|
||||
|
||||
if (($splitOrder['channel'] ?? 'wechat') === 'huifu' && class_exists('\\addons\\shopro_huifu\\library\\HuifuSplitService')) {
|
||||
return (new \addons\shopro_huifu\library\HuifuSplitService())->retry($splitOrderId);
|
||||
}
|
||||
|
||||
@@ -572,6 +572,7 @@ class SplitManualService
|
||||
'update' => '修改',
|
||||
'clear' => '清除',
|
||||
'retry' => '保存并重试',
|
||||
'close' => '线下结案',
|
||||
];
|
||||
return $map[$action] ?? $action;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ use addons\shopro\model\regional_agent\Reward as RegionalRewardModel;
|
||||
use addons\shopro\model\split\Receiver;
|
||||
use addons\shopro\model\split\SplitOrder;
|
||||
use addons\shopro\model\split\SplitOrderReceiver;
|
||||
use think\Db;
|
||||
|
||||
/**
|
||||
* 支付通道分账与钱包佣金:汇付可按人混合(已进件走通道、未进件进钱包);其它通道整单成功则不入余额
|
||||
@@ -174,18 +175,16 @@ class SplitWalletSettlement
|
||||
->where('commission_reward_status', CommissionRewardLibrary::COMMISSION_REWARD_STATUS_WAITING)
|
||||
->select();
|
||||
foreach ($commissionOrders as $commissionOrder) {
|
||||
if ($commissionOrder['commission_event'] !== $event && $event !== 'admin') {
|
||||
continue;
|
||||
}
|
||||
$settleEvent = (string) ($commissionOrder['commission_event'] ?? $event);
|
||||
$rewards = CommissionRewardModel::where('commission_order_id', $commissionOrder['id'])
|
||||
->where('status', CommissionRewardLibrary::COMMISSION_REWARD_STATUS_WAITING)
|
||||
->select();
|
||||
foreach ($rewards as $reward) {
|
||||
$userId = (int) $reward['agent_id'];
|
||||
if ($userId > 0 && in_array($userId, $gateway['promoter'], true)) {
|
||||
$commissionLib->runCommissionRewardWithoutWallet($event, $reward);
|
||||
$commissionLib->runCommissionRewardWithoutWallet($settleEvent, $reward);
|
||||
} else {
|
||||
$commissionLib->runCommissionReward($event, $reward);
|
||||
$commissionLib->runCommissionReward($settleEvent, $reward);
|
||||
}
|
||||
}
|
||||
self::markCommissionOrderSettledIfDone($commissionOrder);
|
||||
@@ -196,18 +195,16 @@ class SplitWalletSettlement
|
||||
->where('commission_reward_status', RegionalRewardLibrary::STATUS_WAITING)
|
||||
->select();
|
||||
foreach ($regionalOrders as $regionalOrder) {
|
||||
if ($regionalOrder['commission_event'] !== $event) {
|
||||
continue;
|
||||
}
|
||||
$settleEvent = (string) ($regionalOrder['commission_event'] ?? $event);
|
||||
$rewards = RegionalRewardModel::where('regional_order_id', $regionalOrder['id'])
|
||||
->where('status', RegionalRewardLibrary::STATUS_WAITING)
|
||||
->select();
|
||||
foreach ($rewards as $reward) {
|
||||
$userId = (int) $reward['agent_user_id'];
|
||||
if ($userId > 0 && in_array($userId, $gateway['regional_agent'], true)) {
|
||||
$regionalLib->settleRewardWithoutWallet($reward, $event);
|
||||
$regionalLib->settleRewardWithoutWallet($reward, $settleEvent);
|
||||
} else {
|
||||
$regionalLib->settleReward($reward, $event);
|
||||
$regionalLib->settleReward($reward, $settleEvent);
|
||||
}
|
||||
}
|
||||
self::markRegionalOrderSettledIfDone($regionalOrder);
|
||||
@@ -310,9 +307,8 @@ class SplitWalletSettlement
|
||||
->where('commission_reward_status', CommissionRewardLibrary::COMMISSION_REWARD_STATUS_WAITING)
|
||||
->select();
|
||||
foreach ($commissionOrders as $commissionOrder) {
|
||||
if ($commissionOrder['commission_event'] === $event || $event === 'admin') {
|
||||
$commissionLib->runCommissionRewardByOrderWithoutWallet($event, $commissionOrder);
|
||||
}
|
||||
$settleEvent = (string) ($commissionOrder['commission_event'] ?? $event);
|
||||
$commissionLib->runCommissionRewardByOrderWithoutWallet($settleEvent, $commissionOrder);
|
||||
}
|
||||
|
||||
$regionalLib = new RegionalRewardLibrary();
|
||||
@@ -320,9 +316,8 @@ class SplitWalletSettlement
|
||||
->where('commission_reward_status', RegionalRewardLibrary::STATUS_WAITING)
|
||||
->select();
|
||||
foreach ($regionalOrders as $regionalOrder) {
|
||||
if ($regionalOrder['commission_event'] === $event) {
|
||||
$regionalLib->settleByRegionalOrderWithoutWallet($regionalOrder->id, $event);
|
||||
}
|
||||
$settleEvent = (string) ($regionalOrder['commission_event'] ?? $event);
|
||||
$regionalLib->settleByRegionalOrderWithoutWallet($regionalOrder->id, $settleEvent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -333,9 +328,8 @@ class SplitWalletSettlement
|
||||
->where('commission_reward_status', CommissionRewardLibrary::COMMISSION_REWARD_STATUS_WAITING)
|
||||
->select();
|
||||
foreach ($commissionOrders as $commissionOrder) {
|
||||
if ($commissionOrder['commission_event'] === $event || $event === 'admin') {
|
||||
$commissionLib->runCommissionRewardByOrder($event, $commissionOrder);
|
||||
}
|
||||
$settleEvent = (string) ($commissionOrder['commission_event'] ?? $event);
|
||||
$commissionLib->runCommissionRewardByOrder($settleEvent, $commissionOrder);
|
||||
}
|
||||
|
||||
$regionalLib = new RegionalRewardLibrary();
|
||||
@@ -343,9 +337,106 @@ class SplitWalletSettlement
|
||||
->where('commission_reward_status', RegionalRewardLibrary::STATUS_WAITING)
|
||||
->select();
|
||||
foreach ($regionalOrders as $regionalOrder) {
|
||||
if ($regionalOrder['commission_event'] === $event) {
|
||||
$regionalLib->settleByRegionalOrder($regionalOrder->id, $event);
|
||||
}
|
||||
$settleEvent = (string) ($regionalOrder['commission_event'] ?? $event);
|
||||
$regionalLib->settleByRegionalOrder($regionalOrder->id, $settleEvent);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 汇付 confirm 重试前:佣金已入平台钱包则禁止再次分账(防双发)
|
||||
*/
|
||||
public static function getHuifuRetryBlockReason($orderId)
|
||||
{
|
||||
$orderId = intval($orderId);
|
||||
if ($orderId <= 0) {
|
||||
return null;
|
||||
}
|
||||
if (class_exists('\\addons\\shopro\\library\\split\\SplitCloseService')
|
||||
&& (new SplitCloseService())->isClosed($orderId)) {
|
||||
return '该订单已标记线下结案,不可重试汇付分账';
|
||||
}
|
||||
$wallet = self::sumOrderWalletCommission($orderId);
|
||||
if (bccomp($wallet, '0.00', 2) <= 0) {
|
||||
return null;
|
||||
}
|
||||
$splitOk = self::isSplitSuccessful($orderId);
|
||||
if ($splitOk) {
|
||||
return '网关分账已成功且存在钱包佣金流水,禁止重试(疑似双轨),请标记结案或人工对账';
|
||||
}
|
||||
return '佣金已入平台钱包兜底,禁止再次汇付 confirm(防双发);若需通道分账请先对账并标记结案';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{settlement_source:string,settlement_source_text:string,wallet_sum:string,split_success:bool,retry_blocked:bool,retry_block_reason:string}
|
||||
*/
|
||||
public static function diagnoseOrderSettlement($orderId)
|
||||
{
|
||||
$orderId = intval($orderId);
|
||||
$walletSum = self::sumOrderWalletCommission($orderId);
|
||||
$splitOk = self::isSplitSuccessful($orderId);
|
||||
$retryReason = self::getHuifuRetryBlockReason($orderId);
|
||||
|
||||
$source = 'pending';
|
||||
if ($splitOk && bccomp($walletSum, '0.00', 2) > 0) {
|
||||
$source = 'hybrid';
|
||||
} elseif ($splitOk) {
|
||||
$source = 'gateway';
|
||||
} elseif (bccomp($walletSum, '0.00', 2) > 0) {
|
||||
$source = 'wallet';
|
||||
}
|
||||
|
||||
$textMap = [
|
||||
'pending' => '待结算/无钱包流水',
|
||||
'gateway' => '汇付通道(未入平台钱包)',
|
||||
'wallet' => '平台钱包兜底',
|
||||
'hybrid' => '通道+钱包混合(请核对)',
|
||||
];
|
||||
|
||||
return [
|
||||
'settlement_source' => $source,
|
||||
'settlement_source_text' => $textMap[$source] ?? $source,
|
||||
'wallet_sum' => $walletSum,
|
||||
'split_success' => $splitOk,
|
||||
'retry_blocked' => $retryReason !== null,
|
||||
'retry_block_reason' => $retryReason ?: '',
|
||||
];
|
||||
}
|
||||
|
||||
protected static function sumOrderWalletCommission($orderId)
|
||||
{
|
||||
$orderId = intval($orderId);
|
||||
if ($orderId <= 0) {
|
||||
return '0.00';
|
||||
}
|
||||
$sum = '0.00';
|
||||
|
||||
$crIds = CommissionRewardModel::where('order_id', $orderId)
|
||||
->where('status', CommissionRewardLibrary::COMMISSION_REWARD_STATUS_ACCOUNTED)
|
||||
->column('id');
|
||||
if ($crIds) {
|
||||
$part = Db::name('shopro_user_wallet_log')
|
||||
->where('item_id', 'in', $crIds)
|
||||
->where('wallet_type', 'money')
|
||||
->where('type', 'commission_income')
|
||||
->sum('wallet');
|
||||
$sum = bcadd($sum, number_format((float) $part, 2, '.', ''), 2);
|
||||
}
|
||||
|
||||
$regionalOrderIds = RegionalOrderModel::where('order_id', $orderId)->column('id');
|
||||
if ($regionalOrderIds) {
|
||||
$rrIds = RegionalRewardModel::where('regional_order_id', 'in', $regionalOrderIds)
|
||||
->where('status', RegionalRewardLibrary::STATUS_ACCOUNTED)
|
||||
->column('id');
|
||||
if ($rrIds) {
|
||||
$part = Db::name('shopro_user_wallet_log')
|
||||
->where('item_id', 'in', $rrIds)
|
||||
->where('wallet_type', 'money')
|
||||
->where('type', 'regional_agent_income')
|
||||
->sum('wallet');
|
||||
$sum = bcadd($sum, number_format((float) $part, 2, '.', ''), 2);
|
||||
}
|
||||
}
|
||||
|
||||
return $sum;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
* php addons/shopro/tests/split_commission_verify.php try-split {order_sn} --execute # 补跑汇付分账(慎用)
|
||||
* php addons/shopro/tests/split_commission_verify.php try-split {order_sn} --confirm-only # 仅延时分账确认、不分账(排查22000002)
|
||||
* php addons/shopro/tests/split_commission_verify.php audit-delay [limit=30] [--live] # 延迟户对账:本地+可选汇付查单 unconfirm_amt
|
||||
* php addons/shopro/tests/split_commission_verify.php mark-closed {order_sn} --reason="财务线下处理"
|
||||
* php addons/shopro/tests/split_commission_verify.php mark-closed-batch --file=orders.txt --reason="..."
|
||||
* php addons/shopro/tests/split_commission_verify.php mark-closed-batch --from-anomalies --reason="..." [--yes]
|
||||
*/
|
||||
|
||||
if (PHP_SAPI !== 'cli') {
|
||||
@@ -68,12 +71,209 @@ switch ($command) {
|
||||
$live = in_array('--live', $argv, true);
|
||||
auditDelayUnconfirm($limit, $live);
|
||||
break;
|
||||
case 'mark-closed':
|
||||
$sn = $argv[2] ?? '';
|
||||
$reason = parseCliOptionValue($argv, '--reason') ?: '财务已线下处理,停止汇付重试';
|
||||
runMarkClosedSingle($sn, $reason);
|
||||
break;
|
||||
case 'mark-closed-batch':
|
||||
$reason = parseCliOptionValue($argv, '--reason') ?: '财务已线下处理,停止汇付重试';
|
||||
$yes = in_array('--yes', $argv, true);
|
||||
$file = parseCliOptionValue($argv, '--file');
|
||||
$fromAnomalies = in_array('--from-anomalies', $argv, true);
|
||||
runMarkClosedBatch($reason, $file, $fromAnomalies, $yes);
|
||||
break;
|
||||
default:
|
||||
echo "未知命令: {$command}\n";
|
||||
echo "可用: summary | anomalies | order {order_sn} | recent-success [limit] | diagnose-missing [limit] | try-split {order_sn} [--execute|--confirm-only] | audit-delay [limit] [--live]\n";
|
||||
echo "可用: summary | anomalies | order {order_sn} | recent-success [limit] | diagnose-missing [limit]\n";
|
||||
echo " try-split {order_sn} [--execute|--confirm-only] | audit-delay [limit] [--live]\n";
|
||||
echo " mark-closed {order_sn} [--reason=...] | mark-closed-batch [--file=...|--from-anomalies] [--reason=...] [--yes]\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
function parseCliOptionValue(array $argv, $flag)
|
||||
{
|
||||
$prefix = $flag . '=';
|
||||
foreach ($argv as $arg) {
|
||||
if (strpos($arg, $prefix) === 0) {
|
||||
return substr($arg, strlen($prefix));
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function splitCloseService()
|
||||
{
|
||||
if (!class_exists('\\addons\\shopro\\library\\split\\SplitCloseService')) {
|
||||
fwrite(STDERR, "SplitCloseService 未部署\n");
|
||||
exit(1);
|
||||
}
|
||||
return new \addons\shopro\library\split\SplitCloseService();
|
||||
}
|
||||
|
||||
/** @return int[] */
|
||||
function fetchOrderIdsWithCloseLog(array $orderIds)
|
||||
{
|
||||
if (!$orderIds) {
|
||||
return [];
|
||||
}
|
||||
return splitCloseService()->orderIdsWithCloseLog($orderIds);
|
||||
}
|
||||
|
||||
function sqlExcludeClosedOrdersSubquery($orderAlias = 'o')
|
||||
{
|
||||
$closedStatus = \addons\shopro\library\split\SplitCloseService::STATUS_CLOSED;
|
||||
$pfx = config('database.prefix');
|
||||
if (!tableHasColumn('shopro_split_manual_log', 'action')) {
|
||||
return "NOT EXISTS (
|
||||
SELECT 1 FROM {$pfx}shopro_split_order so_c
|
||||
WHERE so_c.order_id = {$orderAlias}.id AND so_c.channel = 'huifu' AND so_c.status = {$closedStatus}
|
||||
)";
|
||||
}
|
||||
return "NOT EXISTS (
|
||||
SELECT 1 FROM {$pfx}shopro_split_order so_c
|
||||
WHERE so_c.order_id = {$orderAlias}.id AND so_c.channel = 'huifu' AND so_c.status = {$closedStatus}
|
||||
) AND NOT EXISTS (
|
||||
SELECT 1 FROM {$pfx}shopro_split_manual_log ml
|
||||
WHERE ml.order_id = {$orderAlias}.id AND ml.action = 'close'
|
||||
)";
|
||||
}
|
||||
|
||||
function runMarkClosedSingle($orderSn, $reason)
|
||||
{
|
||||
if ($orderSn === '') {
|
||||
fwrite(STDERR, "用法: mark-closed {order_sn} [--reason=...]\n");
|
||||
exit(1);
|
||||
}
|
||||
try {
|
||||
$result = splitCloseService()->closeByOrderSn($orderSn, $reason, 0, 'CLI');
|
||||
} catch (\Throwable $e) {
|
||||
fwrite(STDERR, '结案失败: ' . $e->getMessage() . "\n");
|
||||
exit(1);
|
||||
}
|
||||
if (!empty($result['already'])) {
|
||||
echo "订单 id={$result['order_id']} 此前已结案\n";
|
||||
return;
|
||||
}
|
||||
echo "已结案 order_id={$result['order_id']}";
|
||||
if (!empty($result['split_order_id'])) {
|
||||
echo " split_order_id={$result['split_order_id']}";
|
||||
}
|
||||
echo "\n";
|
||||
}
|
||||
|
||||
function runMarkClosedBatch($reason, $file, $fromAnomalies, $yes)
|
||||
{
|
||||
$sns = [];
|
||||
if ($file !== '') {
|
||||
if (!is_readable($file)) {
|
||||
fwrite(STDERR, "无法读取文件: {$file}\n");
|
||||
exit(1);
|
||||
}
|
||||
$lines = file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line);
|
||||
if ($line === '' || $line[0] === '#') {
|
||||
continue;
|
||||
}
|
||||
$sns[] = preg_split('/[\s,,]+/', $line)[0];
|
||||
}
|
||||
} elseif ($fromAnomalies) {
|
||||
$sns = collectAnomalyOrderSns(90);
|
||||
} else {
|
||||
fwrite(STDERR, "请指定 --file=orders.txt 或 --from-anomalies\n");
|
||||
exit(1);
|
||||
}
|
||||
$sns = array_values(array_unique(array_filter(array_map('trim', $sns))));
|
||||
if (!$sns) {
|
||||
echo "无订单号可处理\n";
|
||||
return;
|
||||
}
|
||||
echo "将结案 " . count($sns) . " 笔订单,原因: {$reason}\n";
|
||||
foreach ($sns as $sn) {
|
||||
echo " · {$sn}\n";
|
||||
}
|
||||
if (!$yes) {
|
||||
echo "\n加 --yes 确认执行\n";
|
||||
return;
|
||||
}
|
||||
$ok = 0;
|
||||
$skip = 0;
|
||||
$fail = 0;
|
||||
$svc = splitCloseService();
|
||||
foreach ($sns as $sn) {
|
||||
try {
|
||||
$result = $svc->closeByOrderSn($sn, $reason, 0, 'CLI-batch');
|
||||
if (!empty($result['already'])) {
|
||||
$skip++;
|
||||
} else {
|
||||
$ok++;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$fail++;
|
||||
echo "[FAIL] {$sn}: " . $e->getMessage() . "\n";
|
||||
}
|
||||
}
|
||||
echo "完成:新结案 {$ok},已结案跳过 {$skip},失败 {$fail}\n";
|
||||
}
|
||||
|
||||
/** @return string[] */
|
||||
function collectAnomalyOrderSns($days = 90)
|
||||
{
|
||||
$since = time() - intval($days) * 86400;
|
||||
$pfx = config('database.prefix');
|
||||
$closed = \addons\shopro\library\split\SplitCloseService::STATUS_CLOSED;
|
||||
$sns = [];
|
||||
|
||||
$double = Db::query("
|
||||
SELECT so.order_sn
|
||||
FROM {$pfx}shopro_split_order so
|
||||
INNER JOIN {$pfx}shopro_order o ON o.id = so.order_id
|
||||
INNER JOIN (
|
||||
SELECT order_id, SUM(wallet_sum) AS wallet_sum FROM (
|
||||
SELECT cr.order_id, SUM(wl.wallet) AS wallet_sum
|
||||
FROM {$pfx}shopro_commission_reward cr
|
||||
INNER JOIN {$pfx}shopro_user_wallet_log wl
|
||||
ON wl.item_id = cr.id AND wl.type = 'commission_income' AND wl.wallet_type = 'money'
|
||||
WHERE cr.status = 1
|
||||
GROUP BY cr.order_id
|
||||
UNION ALL
|
||||
SELECT rr.order_id, SUM(wl.wallet)
|
||||
FROM {$pfx}shopro_regional_agent_reward rr
|
||||
INNER JOIN {$pfx}shopro_user_wallet_log wl
|
||||
ON wl.item_id = rr.id AND wl.type = 'regional_agent_income' AND wl.wallet_type = 'money'
|
||||
WHERE rr.status = 1
|
||||
GROUP BY rr.order_id
|
||||
) t GROUP BY order_id
|
||||
) wl ON wl.order_id = so.order_id
|
||||
WHERE so.status IN (2,3) AND so.split_amount > 0 AND wl.wallet_sum > 0
|
||||
AND so.status <> {$closed}
|
||||
AND o.paytime >= ?
|
||||
ORDER BY so.id DESC
|
||||
LIMIT 100
|
||||
", [$since]);
|
||||
foreach ($double as $r) {
|
||||
$sns[] = $r['order_sn'];
|
||||
}
|
||||
|
||||
$failed = Db::name('shopro_split_order')->alias('so')
|
||||
->join('shopro_order o', 'o.id = so.order_id')
|
||||
->where('so.channel', 'huifu')
|
||||
->where('so.status', 4)
|
||||
->where('o.paytime', '>=', $since)
|
||||
->column('so.order_sn');
|
||||
|
||||
foreach ($failed as $sn) {
|
||||
$sns[] = $sn;
|
||||
}
|
||||
|
||||
foreach (fetchMissingSplitOrders($since, 100) as $row) {
|
||||
$sns[] = $row['order_sn'];
|
||||
}
|
||||
|
||||
return array_values(array_unique(array_filter($sns)));
|
||||
}
|
||||
|
||||
function tableHasColumn($table, $column)
|
||||
{
|
||||
static $cache = [];
|
||||
@@ -255,6 +455,10 @@ function printAnomalies()
|
||||
) wl ON wl.order_id = so.order_id
|
||||
WHERE so.status IN (2,3) AND so.split_amount > 0 AND wl.wallet_sum > 0
|
||||
AND o.paytime >= ?
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM {$pfx}shopro_split_manual_log ml
|
||||
WHERE ml.order_id = so.order_id AND ml.action = 'close'
|
||||
)
|
||||
ORDER BY so.id DESC
|
||||
LIMIT 30
|
||||
", [$since]);
|
||||
@@ -290,6 +494,7 @@ function printAnomalies()
|
||||
->where('so.channel', 'huifu')
|
||||
->where('so.status', 'in', [1, 2, 3]);
|
||||
})
|
||||
->whereRaw(sqlExcludeClosedOrdersSubquery('o'))
|
||||
->whereExists(function ($q) {
|
||||
$q->name('shopro_commission_reward')->alias('cr')
|
||||
->whereRaw('cr.order_id = o.id')
|
||||
@@ -315,6 +520,7 @@ function printAnomalies()
|
||||
*/
|
||||
function fetchMissingSplitOrders($since, $limit = 20)
|
||||
{
|
||||
$closedStatus = \addons\shopro\library\split\SplitCloseService::STATUS_CLOSED;
|
||||
return Db::name('shopro_order')->alias('o')
|
||||
->where('o.pay_type', 'huifu_wechat')
|
||||
->where('o.paytime', '>=', $since)
|
||||
@@ -324,6 +530,13 @@ function fetchMissingSplitOrders($since, $limit = 20)
|
||||
->where('so.channel', 'huifu')
|
||||
->where('so.status', 'in', [1, 2, 3]);
|
||||
})
|
||||
->whereNotExists(function ($q) use ($closedStatus) {
|
||||
$q->name('shopro_split_order')->alias('so5')
|
||||
->whereRaw('so5.order_id = o.id')
|
||||
->where('so5.channel', 'huifu')
|
||||
->where('so5.status', $closedStatus);
|
||||
})
|
||||
->whereRaw(sqlExcludeClosedOrdersSubquery('o'))
|
||||
->whereRaw(sqlOrderHasAnyReward('o.id'))
|
||||
->field('o.id, o.order_sn, o.pay_fee, o.pay_type, o.paytime, o.transaction_id')
|
||||
->order('o.id desc')
|
||||
@@ -338,6 +551,10 @@ function diagnoseOrderSplitBlockers($orderId)
|
||||
if (!$order) {
|
||||
return ['订单不存在'];
|
||||
}
|
||||
if (class_exists('\\addons\\shopro\\library\\split\\SplitCloseService')
|
||||
&& (new \addons\shopro\library\split\SplitCloseService())->isClosed($orderId)) {
|
||||
$reasons[] = '已标记线下结案,不再重试汇付分账';
|
||||
}
|
||||
if ($order['pay_type'] !== 'huifu_wechat') {
|
||||
$reasons[] = "pay_type={$order['pay_type']},非 huifu_wechat,汇付分账不会执行";
|
||||
}
|
||||
@@ -586,6 +803,11 @@ function trySplitOrder($orderSn, $execute, $confirmOnly = false)
|
||||
fwrite(STDERR, "订单不存在\n");
|
||||
exit(1);
|
||||
}
|
||||
if (class_exists('\\addons\\shopro\\library\\split\\SplitCloseService')
|
||||
&& (new \addons\shopro\library\split\SplitCloseService())->isClosed($order['id'])) {
|
||||
echo "该订单已标记线下结案,禁止 try-split --execute / --confirm-only\n";
|
||||
exit($execute || $confirmOnly ? 1 : 0);
|
||||
}
|
||||
echo "诊断:\n";
|
||||
foreach (diagnoseOrderSplitBlockers($order['id']) as $line) {
|
||||
echo " · {$line}\n";
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
-- 分账订单「已结案(线下处理)」:停止重试、后台与核验脚本可识别
|
||||
-- 执行前请将 fa_ 替换为实际表前缀
|
||||
|
||||
ALTER TABLE `fa_shopro_split_order`
|
||||
MODIFY COLUMN `status` tinyint(1) NOT NULL DEFAULT 0
|
||||
COMMENT '0=待分账,1=分账中,2=成功,3=部分成功,4=失败,5=已结案(线下处理)';
|
||||
|
||||
ALTER TABLE `fa_shopro_split_order`
|
||||
ADD COLUMN `close_reason` varchar(255) DEFAULT NULL COMMENT '结案原因' AFTER `wx_result`,
|
||||
ADD COLUMN `close_admin_id` int(11) NOT NULL DEFAULT 0 COMMENT '结案操作管理员' AFTER `close_reason`,
|
||||
ADD COLUMN `closetime` int(10) DEFAULT NULL COMMENT '结案时间' AFTER `close_admin_id`;
|
||||
|
||||
ALTER TABLE `fa_shopro_split_manual_log`
|
||||
MODIFY COLUMN `action` enum('create','update','clear','retry','close') NOT NULL DEFAULT 'update' COMMENT '操作类型';
|
||||
@@ -0,0 +1,6 @@
|
||||
-- 分账记录:标记已线下处理
|
||||
SET @now = UNIX_TIMESTAMP();
|
||||
SET @ord = (SELECT id FROM `fa_auth_rule` WHERE `name` = 'shopro/split/order' LIMIT 1);
|
||||
|
||||
INSERT IGNORE INTO `fa_auth_rule` (`type`,`pid`,`name`,`title`,`icon`,`url`,`condition`,`remark`,`ismenu`,`extend`,`createtime`,`updatetime`,`weigh`,`status`) VALUES
|
||||
('file', @ord, 'shopro/split/order/markClosed', '标记已线下处理', 'fa fa-circle-o', '', '', '', 0, '', @now, @now, 0, 'normal');
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace addons\shopro_huifu\command;
|
||||
|
||||
use addons\shopro\library\split\Config;
|
||||
use addons\shopro\library\split\SplitCloseService;
|
||||
use addons\shopro\model\split\SplitOrder;
|
||||
use addons\shopro_huifu\library\HuifuSplitService;
|
||||
use think\console\Command;
|
||||
@@ -60,6 +61,11 @@ class SplitRetry extends Command
|
||||
$output->writeln("[OK] 已分账成功: {$sn}");
|
||||
continue;
|
||||
}
|
||||
if ((int) $split['status'] === SplitCloseService::STATUS_CLOSED
|
||||
|| (new SplitCloseService())->isClosed((int) $order['id'])) {
|
||||
$output->writeln("[SKIP] 已线下结案: {$sn}");
|
||||
continue;
|
||||
}
|
||||
$rows[] = $split;
|
||||
}
|
||||
} else {
|
||||
@@ -79,7 +85,13 @@ class SplitRetry extends Command
|
||||
|
||||
$ok = 0;
|
||||
$fail = 0;
|
||||
$closeService = new SplitCloseService();
|
||||
foreach ($rows as $row) {
|
||||
if ((int) $row['status'] === SplitCloseService::STATUS_CLOSED
|
||||
|| $closeService->isClosed((int) $row['order_id'])) {
|
||||
$output->writeln('[SKIP] 已线下结案 order_sn=' . ($row['order_sn'] ?? ''));
|
||||
continue;
|
||||
}
|
||||
$output->writeln('retry split#' . $row['id'] . ' order_sn=' . ($row['order_sn'] ?? ''));
|
||||
$result = $service->retry($row['id']);
|
||||
if ($result) {
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
namespace addons\shopro_huifu\library;
|
||||
|
||||
use addons\shopro\library\split\SplitCalculator;
|
||||
use addons\shopro\library\split\SplitCloseService;
|
||||
use addons\shopro\library\split\SplitManualService;
|
||||
use addons\shopro\library\split\SplitWalletSettlement;
|
||||
use addons\shopro\library\split\SplitOrderHelper;
|
||||
use addons\shopro\library\split\Config as SplitConfig;
|
||||
use addons\shopro\model\Order;
|
||||
@@ -29,6 +31,16 @@ class HuifuSplitService
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((new SplitCloseService())->isClosed($orderId)) {
|
||||
\think\Log::info('[shopro_huifu] split skipped order_id=' . $orderId . ' reason=closed_offline');
|
||||
return false;
|
||||
}
|
||||
|
||||
$retryBlock = SplitWalletSettlement::getHuifuRetryBlockReason($orderId);
|
||||
if ($retryBlock !== null) {
|
||||
throw new \RuntimeException($retryBlock);
|
||||
}
|
||||
|
||||
$done = SplitOrder::where('order_id', $orderId)->where('channel', 'huifu')->where('status', 2)->find();
|
||||
if ($done) {
|
||||
return true;
|
||||
@@ -150,7 +162,19 @@ class HuifuSplitService
|
||||
if (!$splitOrder || $splitOrder['channel'] !== 'huifu') {
|
||||
return false;
|
||||
}
|
||||
return $this->executeByOrderId((int) $splitOrder['order_id']);
|
||||
if (intval($splitOrder['status']) === SplitCloseService::STATUS_CLOSED) {
|
||||
throw new \RuntimeException('该订单已标记线下结案,不可重试');
|
||||
}
|
||||
if ((new SplitCloseService())->isClosed((int) $splitOrder['order_id'])) {
|
||||
throw new \RuntimeException('该订单已标记线下结案,不可重试');
|
||||
}
|
||||
$orderId = (int) $splitOrder['order_id'];
|
||||
$ok = $this->executeByOrderId($orderId);
|
||||
if ($ok) {
|
||||
$event = (new SplitConfig())->getTiming();
|
||||
SplitWalletSettlement::finalizeAfterSplitAttempt($orderId, $event, true);
|
||||
}
|
||||
return $ok;
|
||||
}
|
||||
|
||||
public function retryByOrderId($orderId)
|
||||
@@ -159,7 +183,11 @@ class HuifuSplitService
|
||||
if ($orderId <= 0) {
|
||||
return false;
|
||||
}
|
||||
return $this->executeByOrderId($orderId);
|
||||
$latest = SplitOrder::where('order_id', $orderId)->where('channel', 'huifu')->order('id', 'desc')->find();
|
||||
if (!$latest) {
|
||||
return $this->executeByOrderId($orderId);
|
||||
}
|
||||
return $this->retry((int) $latest['id']);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,8 +5,10 @@ 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\SplitCloseService;
|
||||
use addons\shopro\library\split\SplitExecutor;
|
||||
use addons\shopro\library\split\SplitManualService;
|
||||
use addons\shopro\library\split\SplitWalletSettlement;
|
||||
use addons\shopro\library\split\ProfitSharingService;
|
||||
use app\admin\model\shopro\order\Order as ShopOrder;
|
||||
use app\admin\model\shopro\user\User as ShopUser;
|
||||
@@ -22,6 +24,7 @@ class Order extends Base
|
||||
2 => '成功',
|
||||
3 => '部分成功',
|
||||
4 => '失败',
|
||||
5 => '已结案',
|
||||
];
|
||||
|
||||
public function _initialize()
|
||||
@@ -136,6 +139,7 @@ class Order extends Base
|
||||
{
|
||||
$rows = [];
|
||||
$manualService = new SplitManualService();
|
||||
$closeService = new SplitCloseService();
|
||||
$orderIds = [];
|
||||
foreach ($list as $item) {
|
||||
$data = is_array($item) ? $item : $item->toArray();
|
||||
@@ -146,6 +150,7 @@ class Order extends Base
|
||||
}
|
||||
$overrideOrderIds = array_flip($manualService->orderIdsWithOverride($orderIds));
|
||||
$logOrderIds = array_flip($manualService->orderIdsWithLog($orderIds));
|
||||
$closedLogOrderIds = array_flip($closeService->orderIdsWithCloseLog($orderIds));
|
||||
|
||||
foreach ($list as $item) {
|
||||
$data = is_array($item) ? $item : $item->toArray();
|
||||
@@ -209,10 +214,42 @@ class Order extends Base
|
||||
$hasManualLog = isset($logOrderIds[$orderId]);
|
||||
$data['has_manual_override'] = $hasOverride;
|
||||
$data['has_manual_log'] = $hasManualLog;
|
||||
$data['can_edit_manual'] = ($status === 4 && $channel === 'huifu');
|
||||
$settleDiag = SplitWalletSettlement::diagnoseOrderSettlement($orderId);
|
||||
$data['settlement_source'] = $settleDiag['settlement_source'];
|
||||
$data['settlement_source_text'] = $settleDiag['settlement_source_text'];
|
||||
$data['wallet_settlement_sum'] = $settleDiag['wallet_sum'];
|
||||
$data['retry_blocked'] = $settleDiag['retry_blocked'];
|
||||
$data['retry_block_reason'] = $settleDiag['retry_block_reason'];
|
||||
|
||||
$data['can_edit_manual'] = ($status === 4 && $channel === 'huifu')
|
||||
&& !$settleDiag['retry_blocked'];
|
||||
$data['can_retry_split'] = ($status === 4 && $channel === 'huifu')
|
||||
&& !$settleDiag['retry_blocked'];
|
||||
// 变更历史:成功/失败后均可查看,不限于 status=4
|
||||
$data['can_view_manual_history'] = ($channel === 'huifu')
|
||||
&& ($hasOverride || $hasManualLog || $orderSplitSource === 'manual');
|
||||
&& ($hasOverride || $hasManualLog || $orderSplitSource === 'manual' || isset($closedLogOrderIds[$orderId]));
|
||||
|
||||
$isClosed = ($status === SplitCloseService::STATUS_CLOSED) || isset($closedLogOrderIds[$orderId]);
|
||||
$data['is_closed'] = $isClosed;
|
||||
$data['close_reason'] = trim((string) ($data['close_reason'] ?? ''));
|
||||
$data['closetime_text'] = !empty($data['closetime'])
|
||||
? date('Y-m-d H:i:s', $data['closetime'])
|
||||
: '';
|
||||
$data['can_mark_closed'] = ($channel === 'huifu')
|
||||
&& in_array($status, [2, 3, 4], true)
|
||||
&& !$isClosed;
|
||||
if ($settleDiag['retry_blocked'] && $status === 4 && $data['fail_reason_full'] === '') {
|
||||
$data['fail_reason_full'] = $settleDiag['retry_block_reason'];
|
||||
$data['fail_reason'] = mb_strlen($settleDiag['retry_block_reason']) > 72
|
||||
? (mb_substr($settleDiag['retry_block_reason'], 0, 72) . '…')
|
||||
: $settleDiag['retry_block_reason'];
|
||||
}
|
||||
if ($status === SplitCloseService::STATUS_CLOSED && $data['close_reason'] !== '') {
|
||||
$data['fail_reason_full'] = '线下结案:' . $data['close_reason'];
|
||||
$data['fail_reason'] = mb_strlen($data['close_reason']) > 72
|
||||
? (mb_substr($data['close_reason'], 0, 72) . '…')
|
||||
: $data['close_reason'];
|
||||
}
|
||||
|
||||
unset($data['order'], $data['receivers']);
|
||||
$rows[] = $data;
|
||||
@@ -630,6 +667,37 @@ class Order extends Base
|
||||
$this->success('操作成功', null, $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记已线下处理(停止重试,status=5)
|
||||
*/
|
||||
public function markClosed()
|
||||
{
|
||||
$splitOrderId = intval($this->request->post('split_order_id', 0));
|
||||
$reason = trim((string) $this->request->post('reason', ''));
|
||||
|
||||
$row = $this->model->get($splitOrderId);
|
||||
if (!$row) {
|
||||
$this->error('分账记录不存在');
|
||||
}
|
||||
if (($row['channel'] ?? '') !== 'huifu') {
|
||||
$this->error('仅汇付分账记录可标记结案');
|
||||
}
|
||||
try {
|
||||
$result = (new SplitCloseService())->closeByOrderId(
|
||||
intval($row['order_id']),
|
||||
$reason,
|
||||
intval($this->auth->id),
|
||||
(string) ($this->auth->nickname ?? $this->auth->username ?? '')
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
$this->error($e->getMessage());
|
||||
}
|
||||
if (!empty($result['already'])) {
|
||||
$this->success('该订单此前已标记结案');
|
||||
}
|
||||
$this->success('已标记线下结案,将不再自动重试');
|
||||
}
|
||||
|
||||
public function retry($ids = null)
|
||||
{
|
||||
$id = $ids ?: $this->request->post('id');
|
||||
@@ -637,6 +705,14 @@ class Order extends Base
|
||||
if (!$row) {
|
||||
$this->error('分账记录不存在,可能已被上次重试替换。请刷新列表后,对最新一条失败记录重试');
|
||||
}
|
||||
if (intval($row['status']) === SplitCloseService::STATUS_CLOSED
|
||||
|| (new SplitCloseService())->isClosed((int) $row['order_id'])) {
|
||||
$this->error('该订单已标记线下结案,不可重试');
|
||||
}
|
||||
$block = SplitWalletSettlement::getHuifuRetryBlockReason((int) $row['order_id']);
|
||||
if ($block !== null) {
|
||||
$this->error($block);
|
||||
}
|
||||
$orderId = (int) $row['order_id'];
|
||||
$channel = $row['channel'] ?? 'wechat';
|
||||
try {
|
||||
|
||||
@@ -79,6 +79,7 @@
|
||||
.status-tag.s2 { background: #f0f9eb; color: #67c23a; }
|
||||
.status-tag.s3 { background: #fdf6ec; color: #e6a23c; }
|
||||
.status-tag.s4 { background: #fef0f0; color: #f56c6c; }
|
||||
.status-tag.s5 { background: #f4f4f5; color: #606266; }
|
||||
.money-num {
|
||||
display: block;
|
||||
text-align: right;
|
||||
@@ -279,6 +280,7 @@
|
||||
<el-option label="成功" value="2"></el-option>
|
||||
<el-option label="部分成功" value="3"></el-option>
|
||||
<el-option label="失败" value="4"></el-option>
|
||||
<el-option label="已结案" value="5"></el-option>
|
||||
</el-select>
|
||||
<el-select v-model="filters.channel" placeholder="通道" size="small" style="width:100px;" clearable>
|
||||
<el-option label="汇付" value="huifu"></el-option>
|
||||
@@ -322,8 +324,13 @@
|
||||
<span class="money-num highlight">¥{{ formatMoney(s.row.split_amount) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="分账来源" class-name="col-source" width="130" align="center">
|
||||
<el-table-column label="分账来源" class-name="col-source" width="148" align="center">
|
||||
<template slot-scope="s">
|
||||
<div v-if="s.row.settlement_source_text" style="font-size:11px;color:#606266;margin-bottom:4px;">
|
||||
结算:{{ s.row.settlement_source_text }}
|
||||
<span v-if="parseFloat(s.row.wallet_settlement_sum) > 0">(钱包 ¥{{ formatMoney(s.row.wallet_settlement_sum) }})</span>
|
||||
</div>
|
||||
<div v-if="s.row.retry_blocked && s.row.status == 4" style="font-size:11px;color:#f56c6c;margin-bottom:4px;">不可重试</div>
|
||||
<div class="source-cell" v-if="s.row.split_source === 'manual' || s.row.has_manual_override || s.row.calc_source">
|
||||
<span class="source-tag manual" v-if="s.row.split_source === 'manual' || s.row.has_manual_override">手动</span>
|
||||
<span v-else-if="s.row.calc_source" :class="'source-tag ' + (s.row.calc_source === 'commission' ? 'commission' : 'rule')">
|
||||
@@ -388,7 +395,8 @@
|
||||
<template slot-scope="s">
|
||||
<el-button type="text" size="small" v-if="s.row.can_edit_manual" @click="openManualDialog(s.row)">指定接收方</el-button>
|
||||
<el-button type="text" size="small" v-if="s.row.can_view_manual_history" @click="openHistoryDrawer(s.row)">变更历史</el-button>
|
||||
<el-button type="text" size="small" v-if="s.row.status == 4" @click="retry(s.row)">重试</el-button>
|
||||
<el-button type="text" size="small" v-if="s.row.can_retry_split && !s.row.is_closed" @click="retry(s.row)">重试</el-button>
|
||||
<el-button type="text" size="small" v-if="s.row.can_mark_closed" @click="markClosed(s.row)">标记结案</el-button>
|
||||
<el-button type="text" size="small" @click="query(s.row.id)">同步</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -405,7 +413,7 @@
|
||||
@size-change="onSizeChange">
|
||||
</el-pagination>
|
||||
</div>
|
||||
<p class="op-hint">失败汇付单可「指定接收方」改人改金额后重试;分账成功或失败后均可点「变更历史」查看每次改人记录。「同步」只更新状态,不会重复扣款。</p>
|
||||
<p class="op-hint">失败汇付单可「指定接收方」改人改金额后重试;财务已线下处理的异常单请「标记结案」以停止自动重试。分账成功或失败后均可点「变更历史」查看记录。「同步」只更新状态,不会重复扣款。</p>
|
||||
</div>
|
||||
|
||||
<!-- 指定接收方弹窗 -->
|
||||
|
||||
@@ -295,6 +295,29 @@ define(['jquery', 'bootstrap', 'backend', 'form'], function ($, undefined, Backe
|
||||
}
|
||||
);
|
||||
},
|
||||
markClosed(row) {
|
||||
var self = this;
|
||||
Layer.prompt({
|
||||
title: '标记已线下处理(停止重试)',
|
||||
formType: 2,
|
||||
value: '财务已线下对账/补款,停止汇付分账重试',
|
||||
area: ['420px', '160px']
|
||||
}, function (value, index) {
|
||||
value = (value || '').trim();
|
||||
if (!value) {
|
||||
Layer.msg('请填写结案原因');
|
||||
return;
|
||||
}
|
||||
Layer.close(index);
|
||||
Fast.api.ajax({
|
||||
url: 'shopro/split/order/markClosed',
|
||||
type: 'POST',
|
||||
data: { split_order_id: row.id, reason: value }
|
||||
}, () => {
|
||||
self.getList();
|
||||
});
|
||||
});
|
||||
},
|
||||
query(id) {
|
||||
Fast.api.ajax({ url: 'shopro/split/order/query/ids/' + id, type: 'POST' }, (ret, res) => {
|
||||
var hint = (res && res.msg) ? res.msg : '已同步通道状态并更新记录';
|
||||
|
||||
59
xgsc.gynhc.com_wx/package-lock.json
自动生成的
59
xgsc.gynhc.com_wx/package-lock.json
自动生成的
@@ -1,57 +1,84 @@
|
||||
{
|
||||
"name": "shopro-plus",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 1,
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": {
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "shopro-plus",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"jweixin-module": "^1.6.0",
|
||||
"uni-read-pages": "^1.0.5",
|
||||
"uni-simple-router": "^2.0.6",
|
||||
"vconsole": "^3.7.0"
|
||||
},
|
||||
"devDependencies": {}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.0.tgz",
|
||||
"integrity": "sha512-NDYdls71fTXoU8TZHfbBWg7DiZfNzClcKui/+kyi6ppD2L1qnWW3VV6CjtaBXSUGGhiTWJ6ereOIkUvenif66Q==",
|
||||
"requires": {
|
||||
"dependencies": {
|
||||
"regenerator-runtime": "^0.13.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"copy-text-to-clipboard": {
|
||||
"node_modules/copy-text-to-clipboard": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/copy-text-to-clipboard/-/copy-text-to-clipboard-3.0.1.tgz",
|
||||
"integrity": "sha512-rvVsHrpFcL4F2P8ihsoLdFHmd404+CMg71S756oRSeQgqk51U3kicGdnvfkrxva0xXH92SjGS62B0XIJsbh+9Q=="
|
||||
"integrity": "sha512-rvVsHrpFcL4F2P8ihsoLdFHmd404+CMg71S756oRSeQgqk51U3kicGdnvfkrxva0xXH92SjGS62B0XIJsbh+9Q==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"core-js": {
|
||||
"node_modules/core-js": {
|
||||
"version": "3.26.0",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.26.0.tgz",
|
||||
"integrity": "sha512-+DkDrhoR4Y0PxDz6rurahuB+I45OsEUv8E1maPTB6OuHRohMMcznBq9TMpdpDMm/hUPob/mJJS3PqgbHpMTQgw=="
|
||||
"integrity": "sha512-+DkDrhoR4Y0PxDz6rurahuB+I45OsEUv8E1maPTB6OuHRohMMcznBq9TMpdpDMm/hUPob/mJJS3PqgbHpMTQgw==",
|
||||
"hasInstallScript": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/core-js"
|
||||
}
|
||||
},
|
||||
"jweixin-module": {
|
||||
"node_modules/jweixin-module": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/jweixin-module/-/jweixin-module-1.6.0.tgz",
|
||||
"integrity": "sha512-dGk9cf+ipipHmtzYmKZs5B2toX+p4hLyllGLF6xuC8t+B05oYxd8fYoaRz0T30U2n3RUv8a4iwvjhA+OcYz52w=="
|
||||
},
|
||||
"mutation-observer": {
|
||||
"node_modules/mutation-observer": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/mutation-observer/-/mutation-observer-1.0.3.tgz",
|
||||
"integrity": "sha512-M/O/4rF2h776hV7qGMZUH3utZLO/jK7p8rnNgGkjKUw8zCGjRQPxB8z6+5l8+VjRUQ3dNYu4vjqXYLr+U8ZVNA=="
|
||||
},
|
||||
"regenerator-runtime": {
|
||||
"node_modules/regenerator-runtime": {
|
||||
"version": "0.13.10",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.10.tgz",
|
||||
"integrity": "sha512-KepLsg4dU12hryUO7bp/axHAKvwGOCV0sGloQtpagJ12ai+ojVDqkeGSiRX1zlq+kjIMZ1t7gpze+26QqtdGqw=="
|
||||
},
|
||||
"uni-read-pages": {
|
||||
"node_modules/uni-read-pages": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/uni-read-pages/-/uni-read-pages-1.0.5.tgz",
|
||||
"integrity": "sha512-GkrrZ0LX0vn9R5k6RKEi0Ez3Q3e2vUpjXQ8Z6/K/d28KudI9ajqgt8WEjQFlG5EPm1K6uTArN8LlqmZTEixDUA=="
|
||||
"integrity": "sha512-GkrrZ0LX0vn9R5k6RKEi0Ez3Q3e2vUpjXQ8Z6/K/d28KudI9ajqgt8WEjQFlG5EPm1K6uTArN8LlqmZTEixDUA==",
|
||||
"hasInstallScript": true
|
||||
},
|
||||
"uni-simple-router": {
|
||||
"node_modules/uni-simple-router": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/uni-simple-router/-/uni-simple-router-2.0.7.tgz",
|
||||
"integrity": "sha512-8FKv5dw7Eoonm0gkO8udprrxzin0fNUI0+AvIphFkFRH5ZmP5ZWJ2pvnWzb2NiiqQSECTSU5VSB7HhvOSwD5eA=="
|
||||
},
|
||||
"vconsole": {
|
||||
"node_modules/vconsole": {
|
||||
"version": "3.14.7",
|
||||
"resolved": "https://registry.npmjs.org/vconsole/-/vconsole-3.14.7.tgz",
|
||||
"integrity": "sha512-48byrra1sZk0zJfUM8HFN/ZKtMzvd7Sirt2h5Hfnf7txKOxL9JfoFKB2uLixmOF3jjxCpwnLLb1tsHlg4YzoyA==",
|
||||
"requires": {
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.17.2",
|
||||
"copy-text-to-clipboard": "^3.0.1",
|
||||
"core-js": "^3.11.0",
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
v-if="userHeadData && userHeadData.style"
|
||||
:scrollTop="scrollTop"
|
||||
:detail="userHeadData"
|
||||
:refreshKey="incomeRefreshKey"
|
||||
@onShare="onShare"
|
||||
></userinfo-card>
|
||||
|
||||
@@ -109,7 +110,8 @@ export default {
|
||||
scrollTop: 0,
|
||||
showShare: false,
|
||||
enable: false,
|
||||
userData: {}
|
||||
userData: {},
|
||||
incomeRefreshKey: 0
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -150,6 +152,7 @@ export default {
|
||||
this.init();
|
||||
this.getUserData();
|
||||
this.getAgent();
|
||||
this.incomeRefreshKey += 1;
|
||||
}
|
||||
this.enable = true;
|
||||
},
|
||||
|
||||
@@ -52,21 +52,91 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="asset-bar u-flex">
|
||||
<view class="asset-item u-flex-1 u-flex-col u-col-center" @tap="jump('/pages/user/wallet/index')">
|
||||
<text class="asset-value font-OPPOSANS">{{ userInfo.money || '0.00' }}</text>
|
||||
<text class="asset-label">余额(元)</text>
|
||||
<!-- 普通用户:余额 / 积分 / 优惠券 -->
|
||||
<view class="asset-bar u-flex" v-if="!showIncomePanel">
|
||||
<view
|
||||
class="asset-item u-flex-1 u-flex-col u-col-center"
|
||||
v-for="item in walletItems"
|
||||
:key="item.key"
|
||||
@tap="onAssetTap(item)"
|
||||
>
|
||||
<text class="asset-value font-OPPOSANS">{{ item.displayValue }}</text>
|
||||
<text class="asset-label">{{ item.label }}</text>
|
||||
</view>
|
||||
<view class="asset-divider"></view>
|
||||
<view class="asset-item u-flex-1 u-flex-col u-col-center" @tap="jump('/pages/user/wallet/score-balance')">
|
||||
<text class="asset-value font-OPPOSANS">{{ userInfo.score || '0' }}</text>
|
||||
<text class="asset-label">积分</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 分销商 / 区域代理:汇付分账收益面板 -->
|
||||
<view class="split-income-card brand-card" v-if="showIncomePanel">
|
||||
<view class="split-income-card__head u-flex u-row-between u-col-center">
|
||||
<view>
|
||||
<text class="split-income-card__title">分账收益</text>
|
||||
<text class="split-income-card__sub">平台钱包可提现 · 汇付已进件走子户</text>
|
||||
</view>
|
||||
<view class="asset-divider"></view>
|
||||
<view class="asset-item u-flex-1 u-flex-col u-col-center" @tap="jump('/pages/app/coupon/list')">
|
||||
<text class="asset-value font-OPPOSANS">{{ userOtherData.coupons_num || '0' }}</text>
|
||||
<text class="asset-label">优惠券</text>
|
||||
<text class="split-income-card__role">{{ incomeRoleText }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 平台钱包余额(兜底入账部分) -->
|
||||
<view class="split-wallet" @tap="onAssetTap(balanceItem)">
|
||||
<view class="split-wallet__top u-flex u-row-between u-col-center">
|
||||
<text class="split-wallet__label">平台钱包 · 可提现(元)</text>
|
||||
<text class="split-wallet__state">余额</text>
|
||||
</view>
|
||||
<text class="split-wallet__amount font-OPPOSANS">¥{{ balanceItem.displayValue }}</text>
|
||||
<view class="split-wallet__foot u-flex u-row-between u-col-center">
|
||||
<text class="split-wallet__tip">含未进件/分账失败的兜底入账;已进件部分直达汇付子户</text>
|
||||
<text class="u-iconfont uicon-arrow-right split-wallet__arrow"></text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 已结算:累计收益(非钱包余额) -->
|
||||
<view class="split-settled u-flex" v-if="showCommissionMetric || showRegionalMetric">
|
||||
<view
|
||||
class="split-settled__item u-flex-1"
|
||||
v-if="showCommissionMetric"
|
||||
@tap="onAssetTap(commissionItem)"
|
||||
>
|
||||
<text class="split-settled__label">分销收益</text>
|
||||
<text class="split-settled__value font-OPPOSANS">¥{{ commissionItem.displayValue }}</text>
|
||||
<text class="split-settled__hint">累计已结算(含汇付子户,非可提现余额)</text>
|
||||
</view>
|
||||
<view
|
||||
class="split-settled__item u-flex-1"
|
||||
v-if="showRegionalMetric"
|
||||
:class="{ 'split-settled__item--solo': !showCommissionMetric }"
|
||||
@tap="onAssetTap(regionalItem)"
|
||||
>
|
||||
<text class="split-settled__label">区县收益</text>
|
||||
<text class="split-settled__value font-OPPOSANS">¥{{ regionalItem.displayValue }}</text>
|
||||
<text class="split-settled__hint">累计已结算(含汇付子户)</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 非终态:分账待到账 -->
|
||||
<view class="split-pending u-flex u-row-between u-col-center" @tap="onAssetTap(pendingItem)">
|
||||
<view class="split-pending__main">
|
||||
<view class="split-pending__title-row u-flex u-col-center">
|
||||
<text class="split-pending__dot"></text>
|
||||
<text class="split-pending__title">待入账收益</text>
|
||||
</view>
|
||||
<text class="split-pending__desc">订单已产生,等待结算或汇付分账确认</text>
|
||||
</view>
|
||||
<view class="split-pending__right u-flex u-col-center">
|
||||
<text class="split-pending__amount font-OPPOSANS">¥{{ pendingItem.displayValue }}</text>
|
||||
<text class="u-iconfont uicon-arrow-right split-pending__arrow"></text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="split-onboard-nudge u-flex u-row-between u-col-center"
|
||||
v-if="showOnboardNudge"
|
||||
@tap="jump('/pages/user/wallet/onboard-bind')"
|
||||
>
|
||||
<view class="split-onboard-nudge__text">
|
||||
<text class="split-onboard-nudge__title">有待入账收益,建议完成汇付进件</text>
|
||||
<text class="split-onboard-nudge__sub">进件通过后新单收益直达汇付子户;未完成时走平台钱包兜底</text>
|
||||
</view>
|
||||
<text class="split-onboard-nudge__link">去进件 ›</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -125,15 +195,28 @@
|
||||
isFixed: false,
|
||||
navBackground: {
|
||||
background: 'none'
|
||||
}
|
||||
},
|
||||
regionalIncome: '0.00',
|
||||
regionalDelayMoney: '0.00',
|
||||
hasRegionalAgent: false,
|
||||
huifuOnboard: null
|
||||
};
|
||||
},
|
||||
mounted() {},
|
||||
mounted() {
|
||||
if (this.isLogin) {
|
||||
this.loadRegionalIncome();
|
||||
this.loadHuifuOnboardHint();
|
||||
}
|
||||
},
|
||||
props: {
|
||||
detail: {},
|
||||
scrollTop: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
refreshKey: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -142,6 +225,23 @@
|
||||
this.navBackground.background = val > 50
|
||||
? `linear-gradient(180deg, ${theme.navy} 0%, ${theme.navyLight} 100%)`
|
||||
: 'none';
|
||||
},
|
||||
isLogin(val) {
|
||||
if (val) {
|
||||
this.loadRegionalIncome();
|
||||
this.loadHuifuOnboardHint();
|
||||
} else {
|
||||
this.regionalIncome = '0.00';
|
||||
this.regionalDelayMoney = '0.00';
|
||||
this.hasRegionalAgent = false;
|
||||
this.huifuOnboard = null;
|
||||
}
|
||||
},
|
||||
refreshKey() {
|
||||
if (this.isLogin) {
|
||||
this.loadRegionalIncome();
|
||||
this.loadHuifuOnboardHint();
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -202,10 +302,168 @@
|
||||
tags.push(this.agentInfo.agent_level.name);
|
||||
}
|
||||
return tags;
|
||||
},
|
||||
isCommissionAgent() {
|
||||
return !!(this.agentInfo && this.agentInfo.agent_level);
|
||||
},
|
||||
showIncomePanel() {
|
||||
return this.isLogin && (this.isCommissionAgent || this.hasRegionalAgent);
|
||||
},
|
||||
incomeRoleText() {
|
||||
if (this.isCommissionAgent && this.hasRegionalAgent) {
|
||||
return '分销商 · 区域代理';
|
||||
}
|
||||
if (this.isCommissionAgent) {
|
||||
return '分销商';
|
||||
}
|
||||
if (this.hasRegionalAgent) {
|
||||
return '区域代理';
|
||||
}
|
||||
return '';
|
||||
},
|
||||
walletItems() {
|
||||
return [{
|
||||
key: 'money',
|
||||
label: '余额(元)',
|
||||
displayValue: this.formatMoney(this.userInfo.money),
|
||||
path: '/pages/user/wallet/index'
|
||||
}, {
|
||||
key: 'score',
|
||||
label: '积分',
|
||||
displayValue: String(this.userInfo.score || '0'),
|
||||
path: '/pages/user/wallet/score-balance'
|
||||
}, {
|
||||
key: 'coupon',
|
||||
label: '优惠券',
|
||||
displayValue: String(this.userOtherData.coupons_num || '0'),
|
||||
path: '/pages/app/coupon/list'
|
||||
}];
|
||||
},
|
||||
pendingIncome() {
|
||||
const commissionPending = parseFloat(this.agentInfo?.delay_money || 0) || 0;
|
||||
const regionalPending = parseFloat(this.regionalDelayMoney || 0) || 0;
|
||||
return this.formatMoney(commissionPending + regionalPending);
|
||||
},
|
||||
showCommissionMetric() {
|
||||
return this.isCommissionAgent;
|
||||
},
|
||||
showRegionalMetric() {
|
||||
return this.hasRegionalAgent;
|
||||
},
|
||||
balanceItem() {
|
||||
return {
|
||||
key: 'balance',
|
||||
displayValue: this.formatMoney(this.userInfo.money),
|
||||
path: '/pages/user/wallet/index'
|
||||
};
|
||||
},
|
||||
commissionItem() {
|
||||
return {
|
||||
key: 'commission',
|
||||
displayValue: this.formatMoney(this.agentInfo?.total_income),
|
||||
path: '/pages/app/commission/index'
|
||||
};
|
||||
},
|
||||
regionalItem() {
|
||||
return {
|
||||
key: 'regional',
|
||||
displayValue: this.formatMoney(this.regionalIncome),
|
||||
path: '/pages/app/regional-agent/index'
|
||||
};
|
||||
},
|
||||
pendingItem() {
|
||||
return {
|
||||
key: 'pending',
|
||||
displayValue: this.pendingIncome,
|
||||
path: this.pendingIncomePath
|
||||
};
|
||||
},
|
||||
pendingIncomePath() {
|
||||
const commissionPending = parseFloat(this.agentInfo?.delay_money || 0) || 0;
|
||||
const regionalPending = parseFloat(this.regionalDelayMoney || 0) || 0;
|
||||
if (regionalPending > 0 && commissionPending <= 0) {
|
||||
return '/pages/app/regional-agent/rewards';
|
||||
}
|
||||
return '/pages/app/commission/commission-log';
|
||||
},
|
||||
showOnboardNudge() {
|
||||
if (!this.showIncomePanel || !this.isCommissionAgent) {
|
||||
return false;
|
||||
}
|
||||
const pending = parseFloat(this.pendingIncome) || 0;
|
||||
if (pending <= 0) {
|
||||
return false;
|
||||
}
|
||||
const h = this.huifuOnboard;
|
||||
if (!h || !h.required) {
|
||||
return false;
|
||||
}
|
||||
if (h.status === 'approved') {
|
||||
return false;
|
||||
}
|
||||
return !!h.can_submit || !h.status;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['getUserInfo', 'showAuthModal']),
|
||||
formatMoney(value) {
|
||||
const num = parseFloat(value);
|
||||
if (Number.isNaN(num)) {
|
||||
return '0.00';
|
||||
}
|
||||
return num.toFixed(2);
|
||||
},
|
||||
loadHuifuOnboardHint() {
|
||||
if (!this.isCommissionAgent) {
|
||||
this.huifuOnboard = null;
|
||||
return;
|
||||
}
|
||||
this.$http('commission.auth').then(res => {
|
||||
if (res.code === 1 && res.data) {
|
||||
this.huifuOnboard = res.data.huifu_onboard || null;
|
||||
}
|
||||
}).catch(() => {});
|
||||
},
|
||||
loadRegionalIncome() {
|
||||
this.$http('regionalAgent.info').then(res => {
|
||||
if (res.code !== 1 || !res.data || res.data.status === 'close') {
|
||||
this.regionalIncome = '0.00';
|
||||
this.regionalDelayMoney = '0.00';
|
||||
this.hasRegionalAgent = false;
|
||||
return;
|
||||
}
|
||||
this.regionalDelayMoney = res.data.delay_money || '0.00';
|
||||
const agents = Array.isArray(res.data.agents) ? res.data.agents : [];
|
||||
this.hasRegionalAgent = agents.some(item => item.status === 'normal');
|
||||
const total = agents.reduce((sum, item) => {
|
||||
return sum + (parseFloat(item.total_income) || 0);
|
||||
}, 0);
|
||||
if (total > 0) {
|
||||
this.regionalIncome = total.toFixed(2);
|
||||
return;
|
||||
}
|
||||
this.loadRegionalSummary();
|
||||
}).catch(() => {
|
||||
this.regionalIncome = '0.00';
|
||||
this.regionalDelayMoney = '0.00';
|
||||
this.hasRegionalAgent = false;
|
||||
});
|
||||
},
|
||||
loadRegionalSummary() {
|
||||
this.$http('regionalAgent.stats').then(res => {
|
||||
if (res.code === 1) {
|
||||
const summary = (res.data && res.data.summary) || {};
|
||||
this.regionalIncome = this.formatMoney(summary.total_income);
|
||||
}
|
||||
}).catch(() => {});
|
||||
},
|
||||
onAssetTap(item) {
|
||||
if (!this.isLogin) {
|
||||
this.showAuthModal('accountLogin');
|
||||
return;
|
||||
}
|
||||
this.jump(item.path);
|
||||
},
|
||||
onProfileTap() {
|
||||
if (this.isLogin) {
|
||||
this.$Router.push('/pages/user/info');
|
||||
@@ -288,14 +546,276 @@
|
||||
|
||||
.sh-userinfo-box {
|
||||
position: relative;
|
||||
padding-bottom: 0;
|
||||
padding-bottom: 8rpx;
|
||||
|
||||
.head-wrap {
|
||||
width: 100%;
|
||||
padding: 0 $brand-page-gutter 28rpx;
|
||||
padding: 0 $brand-page-gutter 20rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.asset-bar {
|
||||
margin-top: 28rpx;
|
||||
padding: 8rpx 0 4rpx;
|
||||
}
|
||||
|
||||
.asset-item {
|
||||
min-width: 0;
|
||||
|
||||
& + .asset-item {
|
||||
border-left: 1rpx solid rgba(255, 255, 255, 0.28);
|
||||
}
|
||||
}
|
||||
|
||||
.asset-value {
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.asset-label {
|
||||
margin-top: 8rpx;
|
||||
font-size: 22rpx;
|
||||
color: rgba(255, 255, 255, 0.88);
|
||||
}
|
||||
|
||||
.split-income-card {
|
||||
margin: 16rpx $brand-page-gutter 0;
|
||||
padding: 28rpx 24rpx 24rpx;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 12rpx 40rpx rgba(10, 36, 99, 0.12);
|
||||
}
|
||||
|
||||
.split-income-card__head {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.split-income-card__title {
|
||||
display: block;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #1D2129;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.split-income-card__sub {
|
||||
display: block;
|
||||
margin-top: 6rpx;
|
||||
font-size: 22rpx;
|
||||
color: #86909C;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.split-income-card__role {
|
||||
flex-shrink: 0;
|
||||
padding: 6rpx 16rpx;
|
||||
border-radius: 20rpx;
|
||||
font-size: 22rpx;
|
||||
font-weight: 500;
|
||||
color: #1677FF;
|
||||
background: #E8F3FF;
|
||||
}
|
||||
|
||||
.split-wallet {
|
||||
padding: 28rpx;
|
||||
border-radius: 20rpx;
|
||||
background: $brand-primary-gradient;
|
||||
margin-bottom: 20rpx;
|
||||
|
||||
&:active {
|
||||
opacity: 0.92;
|
||||
}
|
||||
}
|
||||
|
||||
.split-wallet__label {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.88);
|
||||
}
|
||||
|
||||
.split-wallet__state {
|
||||
padding: 4rpx 14rpx;
|
||||
border-radius: 16rpx;
|
||||
font-size: 20rpx;
|
||||
font-weight: 500;
|
||||
color: #1677FF;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
}
|
||||
|
||||
.split-wallet__amount {
|
||||
display: block;
|
||||
margin-top: 16rpx;
|
||||
font-size: 56rpx;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.split-wallet__foot {
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.split-wallet__tip {
|
||||
font-size: 22rpx;
|
||||
color: rgba(255, 255, 255, 0.78);
|
||||
line-height: 1.4;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.split-wallet__arrow {
|
||||
font-size: 22rpx;
|
||||
color: rgba(255, 255, 255, 0.72);
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
|
||||
.split-settled {
|
||||
margin-bottom: 20rpx;
|
||||
border: 1rpx solid #E5E7EB;
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
background: #F7F8FA;
|
||||
}
|
||||
|
||||
.split-settled__item {
|
||||
min-width: 0;
|
||||
padding: 24rpx 20rpx;
|
||||
text-align: center;
|
||||
background: #ffffff;
|
||||
|
||||
& + .split-settled__item {
|
||||
border-left: 1rpx solid #E5E7EB;
|
||||
}
|
||||
|
||||
&--solo {
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: #F7F8FA;
|
||||
}
|
||||
}
|
||||
|
||||
.split-settled__label {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
color: #1D2129;
|
||||
}
|
||||
|
||||
.split-settled__value {
|
||||
display: block;
|
||||
margin-top: 12rpx;
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: #1677FF;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.split-settled__hint {
|
||||
display: block;
|
||||
margin-top: 8rpx;
|
||||
font-size: 20rpx;
|
||||
color: #86909C;
|
||||
}
|
||||
|
||||
.split-pending {
|
||||
padding: 24rpx;
|
||||
border-radius: 16rpx;
|
||||
background: linear-gradient(135deg, #FFF7E6 0%, #FFFBF0 100%);
|
||||
border: 1rpx solid rgba(212, 107, 8, 0.22);
|
||||
|
||||
&:active {
|
||||
opacity: 0.92;
|
||||
}
|
||||
}
|
||||
|
||||
.split-pending__main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding-right: 16rpx;
|
||||
}
|
||||
|
||||
.split-pending__title-row {
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.split-pending__dot {
|
||||
width: 12rpx;
|
||||
height: 12rpx;
|
||||
border-radius: 50%;
|
||||
background: #D46B08;
|
||||
margin-right: 10rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.split-pending__title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #1D2129;
|
||||
}
|
||||
|
||||
.split-pending__desc {
|
||||
display: block;
|
||||
font-size: 22rpx;
|
||||
color: #86909C;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.split-pending__right {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.split-pending__amount {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: #D46B08;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.split-pending__arrow {
|
||||
font-size: 22rpx;
|
||||
color: rgba(212, 107, 8, 0.55);
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
|
||||
.split-onboard-nudge {
|
||||
margin-top: 20rpx;
|
||||
padding: 20rpx 24rpx;
|
||||
border-radius: 12rpx;
|
||||
background: rgba(116, 56, 213, 0.08);
|
||||
border: 1rpx solid rgba(116, 56, 213, 0.18);
|
||||
}
|
||||
|
||||
.split-onboard-nudge__text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding-right: 16rpx;
|
||||
}
|
||||
|
||||
.split-onboard-nudge__title {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: #1D2129;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.split-onboard-nudge__sub {
|
||||
display: block;
|
||||
margin-top: 6rpx;
|
||||
font-size: 22rpx;
|
||||
color: #86909C;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.split-onboard-nudge__link {
|
||||
flex-shrink: 0;
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: #7438D5;
|
||||
}
|
||||
|
||||
.user-head {
|
||||
width: 100%;
|
||||
padding-top: 8rpx;
|
||||
@@ -440,34 +960,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.asset-bar {
|
||||
margin-top: 28rpx;
|
||||
padding: 8rpx 0 4rpx;
|
||||
}
|
||||
|
||||
.asset-item {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.asset-value {
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.asset-label {
|
||||
margin-top: 8rpx;
|
||||
font-size: 22rpx;
|
||||
color: rgba(255, 255, 255, 0.72);
|
||||
}
|
||||
|
||||
.asset-divider {
|
||||
width: 1rpx;
|
||||
height: 56rpx;
|
||||
background: rgba(255, 255, 255, 0.22);
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
|
||||
// 绑定微信公众号
|
||||
@@ -497,9 +989,6 @@
|
||||
}
|
||||
}
|
||||
</style>
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -7,8 +7,9 @@
|
||||
<!-- 可提现 -->
|
||||
<view class="wallet-num-box u-flex u-col-center u-row-between">
|
||||
<view class="">
|
||||
<view class="num-title">可提现金额(元)</view>
|
||||
<view class="num-title">平台钱包 · 可提现(元)</view>
|
||||
<view class="wallet-num">{{ userInfo.money || '0.00' }}</view>
|
||||
<view class="num-tip">不含已进件推广员走汇付子户到账的部分,子户资金请在汇付侧结算/提现</view>
|
||||
</view>
|
||||
<button class="u-reset-button log-btn"
|
||||
@tap="$Router.push({ path: '/pages/user/wallet/withdraw-log' })">提现记录</button>
|
||||
@@ -358,7 +359,15 @@
|
||||
font-size: 26rpx;
|
||||
font-weight: 500;
|
||||
color: #ffffff;
|
||||
margin-bottom: 20rpx;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.num-tip {
|
||||
font-size: 22rpx;
|
||||
line-height: 1.45;
|
||||
color: rgba(255, 255, 255, 0.82);
|
||||
margin-bottom: 16rpx;
|
||||
max-width: 520rpx;
|
||||
}
|
||||
|
||||
.wallet-num {
|
||||
|
||||
在新工单中引用
屏蔽一个用户