diff --git a/addons/shopro/Shopro.php b/addons/shopro/Shopro.php
index b41e0f0..f1272c6 100755
--- a/addons/shopro/Shopro.php
+++ b/addons/shopro/Shopro.php
@@ -112,6 +112,7 @@ class Shopro extends Addons
'addons\shopro\console\PartnerDemo',
'addons\shopro\console\PartnerAccept',
'addons\shopro\console\PartnerClean',
+ 'addons\shopro\console\PartnerTransferCommission',
]);
}
diff --git a/addons/shopro/console/PartnerInstall.php b/addons/shopro/console/PartnerInstall.php
index 080e7f5..ef573b6 100644
--- a/addons/shopro/console/PartnerInstall.php
+++ b/addons/shopro/console/PartnerInstall.php
@@ -227,24 +227,8 @@ class PartnerInstall extends Command
protected function backfillLastSelfOrder(Output $output)
{
- $rows = Db::name('shopro_order')
- ->where('status', 'in', ['paid', 'completed'])
- ->where('paid_time', '>', 0)
- ->field('user_id, max(paid_time) as last_paid')
- ->group('user_id')
- ->select();
- $count = 0;
- foreach ($rows as $row) {
- $agent = Db::name('shopro_commission_agent')->where('user_id', $row['user_id'])->find();
- if (!$agent || !empty($agent['last_self_order_time'])) {
- continue;
- }
- Db::name('shopro_commission_agent')->where('user_id', $row['user_id'])->update([
- 'last_self_order_time' => intval($row['last_paid']),
- ]);
- $count++;
- }
- $output->writeln("backfill last_self_order_time: {$count}");
+ $count = (new \addons\shopro\service\partner\ActiveService())->syncActiveTimes();
+ $output->writeln("backfill last_self_order_time (self + direct): {$count}");
}
protected function clearConfigCache(Output $output)
diff --git a/addons/shopro/console/PartnerTransferCommission.php b/addons/shopro/console/PartnerTransferCommission.php
new file mode 100644
index 0000000..d54316f
--- /dev/null
+++ b/addons/shopro/console/PartnerTransferCommission.php
@@ -0,0 +1,84 @@
+setName('shopro:partner:transfer-commission')
+ ->addOption('dry-run', 'd', Option::VALUE_NONE, '只读试算预览,不真实扣减与增加资金')
+ ->addOption('force', 'f', Option::VALUE_NONE, '强制执行,不提示二次确认')
+ ->setDescription('将所有会员账户已有佣金收益全量结转至商城余额');
+ }
+
+ protected function execute(Input $input, Output $output)
+ {
+ $dryRun = (bool) $input->getOption('dry-run');
+ $force = (bool) $input->getOption('force');
+
+ $output->writeln("========================================================");
+ $output->writeln(" 恒联数通 · 历史收益全量结转至商城余额工具");
+ $output->writeln("========================================================");
+
+ $service = new WalletMigrationService();
+ $preview = $service->preview();
+
+ $output->writeln("待处理会员总数: {$preview['total_users']} 人");
+ $output->writeln("待结转收益总金额: ¥{$preview['total_commission']} 元");
+
+ if ($preview['total_users'] === 0) {
+ $output->writeln("\n当前无任何待结转佣金余额,无需处理。");
+ return 0;
+ }
+
+ $output->writeln("\n部分待结转会员抽样(前 10 名):");
+ foreach (array_slice($preview['samples'], 0, 10) as $i => $s) {
+ $idx = $i + 1;
+ $output->writeln(" [{$idx}] 会员ID: {$s['id']} | 手机: {$s['mobile']} | 当前佣金: ¥{$s['current_commission']} -> 商城余额: ¥{$s['current_money']} (结转后预计: ¥{$s['target_money']})");
+ }
+
+ if ($dryRun) {
+ $output->writeln("\n【DRY-RUN 试算模式已结束】未对数据库发生任何资金变动。");
+ return 0;
+ }
+
+ if (!$force) {
+ $output->writeln("\n【安全警告】即将对全量 {$preview['total_users']} 名会员执行结转,将 ¥{$preview['total_commission']} 元佣金划入商城余额!");
+ $output->writeln("若确认执行,请增加参数 --force 或 -f,例如:");
+ $output->writeln("php think shopro:partner:transfer-commission --force");
+ return 1;
+ }
+
+ $output->writeln("\n>>> 开始执行资金划转...");
+ $startTime = microtime(true);
+
+ $res = $service->transferAll(false, function ($current, $total, $userId, $amount, $err) use ($output) {
+ if ($err) {
+ $output->writeln(" [{$current}/{$total}] 会员 {$userId} 结转失败: {$err}");
+ } else {
+ $output->writeln(" [{$current}/{$total}] 会员 {$userId} 成功结转 ¥" . number_format($amount, 2, '.', '') . " 元至商城余额");
+ }
+ });
+
+ $cost = round(microtime(true) - $startTime, 2);
+
+ $output->writeln("\n========================================================");
+ $output->writeln("结转完成!耗时: {$cost} 秒");
+ $output->writeln("成功处理: {$res['success_count']} 人");
+ $output->writeln("失败人数: {$res['fail_count']} 人");
+ $output->writeln("累计结转资金: ¥{$res['transferred_amount']} 元");
+ $output->writeln("========================================================");
+
+ return $res['fail_count'] > 0 ? 1 : 0;
+ }
+}
diff --git a/addons/shopro/controller/Pay.php b/addons/shopro/controller/Pay.php
index 530ea82..b790370 100644
--- a/addons/shopro/controller/Pay.php
+++ b/addons/shopro/controller/Pay.php
@@ -36,7 +36,7 @@ class Pay extends Common
$payment = $this->request->post('payment');
$openid = $this->request->post('openid', '');
$money = $this->request->post('money', 0);
- $money = $money > 0 ? $money : 0;
+ $money = floatval($money) > 0 ? round(floatval($money), 2) : 0;
$platform = $this->request->header('platform');
list($order, $order_type) = $this->getOrderInstance($order_sn);
@@ -65,30 +65,62 @@ class Pay extends Common
// pay 实例
$payOper = new PayOper();
- if ($money && $order_type == 'order') {
- // 余额混合支付
- $order = Db::transaction(function () use ($payOper, $order, $order_type, $money) {
+ $mixedPayRecord = null;
+ if ($money > 0 && $order_type == 'order') {
+ // 余额混合支付(行锁串行化保障并发资金安全)
+ $order = Db::transaction(function () use ($order, $order_type, $money, $user, &$mixedPayRecord) {
// 加锁读订单
$order = $order->lock(true)->find($order->id);
+ if (in_array($order->status, [$order::STATUS_PAID, $order::STATUS_COMPLETED])) {
+ $this->error('订单已支付');
+ }
- // 余额支付
- $order = $payOper->money($order, $money, $order_type);
+ // 事务内行级排他锁查验用户最新可用余额,杜绝并发快照双花
+ $lockedUser = \app\admin\model\shopro\user\User::where('id', $user->id)->lock(true)->find();
+ if (bccomp((string)$money, (string)$lockedUser->money, 2) === 1) {
+ $this->error('账户可用余额不足以进行本次抵扣');
+ }
+
+ $deductMoney = min($money, floatval($order->remain_pay_fee));
+ if ($deductMoney <= 0) {
+ return $order;
+ }
+
+ // 使用锁定的实时用户对象执行余额扣减
+ $lockedPayOper = new PayOper($lockedUser);
+ $order = $lockedPayOper->money($order, $deductMoney, $order_type);
return $order;
});
+ // 获取刚才产生的余额支付记录,用于异常情况冲正
+ $mixedPayRecord = PayModel::where('order_id', $order->id)
+ ->where('order_type', $order_type)
+ ->where('pay_type', 'money')
+ ->order('id', 'desc')
+ ->find();
+
if (in_array($order->status, [$order::STATUS_PAID, $order::STATUS_COMPLETED])) {
$this->success('订单支付成功', $order);
}
}
if ($payment == 'money' && $order_type == 'order') {
- // 余额支付
- $order = Db::transaction(function () use ($payOper, $order, $order_type) {
+ // 纯余额支付
+ $order = Db::transaction(function () use ($order, $order_type, $user) {
// 加锁读订单
$order = $order->lock(true)->find($order->id);
+ if (in_array($order->status, [$order::STATUS_PAID, $order::STATUS_COMPLETED])) {
+ $this->error('订单已支付');
+ }
- $order = $payOper->money($order, $order->remain_pay_fee, $order_type);
+ $lockedUser = \app\admin\model\shopro\user\User::where('id', $user->id)->lock(true)->find();
+ if (bccomp((string)$order->remain_pay_fee, (string)$lockedUser->money, 2) === 1) {
+ $this->error('账户可用余额不足以付清该订单');
+ }
+
+ $lockedPayOper = new PayOper($lockedUser);
+ $order = $lockedPayOper->money($order, $order->remain_pay_fee, $order_type);
return $order;
});
@@ -146,7 +178,10 @@ class Pay extends Common
}
if (empty($order_data['payer']['openid'])) {
- // 缺少 openid
+ // 缺少 openid 时,若前面预扣了余额则冲正
+ if ($mixedPayRecord) {
+ $this->rollbackMixedMoney($order, $mixedPayRecord, $user, $order_type);
+ }
$this->error('miss_openid', -1);
}
}
@@ -160,11 +195,18 @@ class Pay extends Common
try {
$result = $payService->pay($order_data);
- } catch (\Yansongda\Pay\Exception\Exception $e) {
- $this->error('支付失败' . (config('app_debug') ? ":" . $e->getMessage() : ',请重试'));
- } catch (HttpResponseException $e) {
- $data = $e->getResponse()->getData();
- $message = $data ? ($data['msg'] ?? '') : $e->getMessage();
+ } catch (\Exception $e) {
+ // 第三方预支付异常,安全冲正本次混合支付预扣款
+ if ($mixedPayRecord) {
+ $this->rollbackMixedMoney($order, $mixedPayRecord, $user, $order_type);
+ }
+
+ if ($e instanceof HttpResponseException) {
+ $data = $e->getResponse()->getData();
+ $message = $data ? ($data['msg'] ?? '') : $e->getMessage();
+ } else {
+ $message = $e->getMessage();
+ }
$this->error('支付失败' . (config('app_debug') ? ":" . $message : ',请重试'));
}
@@ -183,6 +225,39 @@ class Pay extends Common
]);
}
+ /**
+ * 混合支付预下单异常时,冲正退回预扣余额
+ *
+ * @param \think\Model $order
+ * @param \think\Model $mixedPayRecord
+ * @param \think\Model $user
+ * @param string $order_type
+ * @return void
+ */
+ protected function rollbackMixedMoney($order, $mixedPayRecord, $user, $order_type)
+ {
+ try {
+ Db::transaction(function () use ($order, $mixedPayRecord, $user, $order_type) {
+ \addons\shopro\service\Wallet::change($user, 'money', $mixedPayRecord->pay_fee, 'order_refund', [
+ 'order_id' => $order->id,
+ 'order_sn' => $order->order_sn,
+ 'order_type' => $order_type,
+ 'remark' => '混合支付预下单未完成,安全冲正退回'
+ ]);
+
+ $mixedPayRecord->status = PayModel::PAY_STATUS_REFUND;
+ $mixedPayRecord->refund_fee = $mixedPayRecord->pay_fee;
+ $mixedPayRecord->save();
+
+ $order = $order->lock(true)->find($order->id);
+ $order->remain_pay_fee = bcadd((string)$order->remain_pay_fee, (string)$mixedPayRecord->pay_fee, 2);
+ $order->save();
+ });
+ } catch (\Exception $e) {
+ Log::write('混合支付预扣款冲正异常: ' . $e->getMessage(), 'error');
+ }
+ }
+
/**
diff --git a/addons/shopro/controller/commission/Agent.php b/addons/shopro/controller/commission/Agent.php
index 6cb2fdd..be5d8f6 100644
--- a/addons/shopro/controller/commission/Agent.php
+++ b/addons/shopro/controller/commission/Agent.php
@@ -303,16 +303,18 @@ class Agent extends Commission
->column('id');
if (empty($teamUserIds)) {
+ $selfAgent = AgentModel::where('user_id', $agentId)->find();
+ $selfUser = UserModel::where('id', $agentId)->find();
return [
'total_count' => 0,
'active_count' => 0,
'expiring_count' => 0,
'inactive_count' => 0,
'partner_count' => 0,
- 'team_total_consume' => '0.00',
+ 'team_total_consume' => \addons\shopro\service\partner\IdentityService::formatMoney($selfUser->total_consume ?? 0),
'team_total_reward' => '0.00',
'team_month_reward' => '0.00',
- 'team_order_count' => 0,
+ 'team_order_count' => intval($selfAgent->child_order_count_0 ?? 0),
];
}
@@ -348,8 +350,17 @@ class Agent extends Commission
$inactiveCount += $missingAgentCount;
}
- // 1. 团队累计自购总业绩
- $teamTotalConsume = UserModel::whereIn('id', $teamUserIds)->sum('total_consume');
+ // 1. 团队累计自购总业绩与团队订单总数(统一使用包含本人自购的分销团队全口径)
+ $selfAgent = AgentModel::where('user_id', $agentId)->find();
+ $selfUser = UserModel::where('id', $agentId)->find();
+
+ $teamTotalConsume = isset($selfAgent->child_order_money_all) && floatval($selfAgent->child_order_money_all) > 0
+ ? floatval($selfAgent->child_order_money_all)
+ : (UserModel::whereIn('id', $teamUserIds)->sum('total_consume') + floatval($selfUser->total_consume ?? 0));
+
+ $teamOrderCount = isset($selfAgent->child_order_count_all) && intval($selfAgent->child_order_count_all) > 0
+ ? intval($selfAgent->child_order_count_all)
+ : ($teamOrderCount + intval($selfAgent->child_order_count_0 ?? 0));
// 2. 团队成员为当前用户贡献的累计收益与本月收益
$monthStart = strtotime(date('Y-m-01 00:00:00'));
diff --git a/addons/shopro/controller/partner/Index.php b/addons/shopro/controller/partner/Index.php
index 7913112..d075d43 100644
--- a/addons/shopro/controller/partner/Index.php
+++ b/addons/shopro/controller/partner/Index.php
@@ -28,8 +28,10 @@ class Index extends Commission
->where('createtime', '>=', $monthStart)
->count();
$agent = IdentityService::attachAgent($this->service->agent);
+ $totalIncome = $agent['total_income'] ?? '0.00';
$this->success('ok', [
+ 'total_income' => $totalIncome,
'month_income' => IdentityService::formatMoney($monthIncome),
'skip_count' => intval($skipCount),
'active_30d' => $agent['active_30d'] ?? true,
@@ -92,10 +94,21 @@ class Index extends Commission
}
}
+ $monthStart = strtotime(date('Y-m-01 00:00:00'));
+ $monthIncome = RewardModel::where('user_id', $userId)
+ ->where('status', 1)
+ ->where('createtime', '>=', $monthStart)
+ ->whereIn('reward_type', array_merge(PartnerConfig::instantTypes(), ['venue_share', 'county_share']))
+ ->sum('amount');
+ $agent = IdentityService::attachAgent($this->service->agent);
+ $totalIncome = $agent['total_income'] ?? '0.00';
+
$this->success('ok', [
'total' => $list->total(),
'per_page' => $list->listRows(),
'current_page' => $list->currentPage(),
+ 'total_income' => $totalIncome,
+ 'month_income' => IdentityService::formatMoney($monthIncome),
'data' => $rows,
'buyer_info' => $buyerInfo,
]);
@@ -127,6 +140,7 @@ class Index extends Commission
'madow' => $agent['madow'] ?? '0.00000000',
'madow_rate' => $agent['madow_rate'] ?? IdentityService::formatRate(null),
'madow_value' => $agent['madow_value'] ?? '0.00',
+ 'show_madow_rate' => PartnerConfig::showMadowRate(),
'list' => $list,
]);
}
diff --git a/addons/shopro/controller/user/User.php b/addons/shopro/controller/user/User.php
index ab299fc..8da608f 100644
--- a/addons/shopro/controller/user/User.php
+++ b/addons/shopro/controller/user/User.php
@@ -84,7 +84,20 @@ class User extends Common
$agent = \addons\shopro\service\partner\IdentityService::findAgent($user->id);
$partner = \addons\shopro\service\partner\IdentityService::attachProfile($agent);
+ if (empty($partner['total_income']) || $partner['total_income'] === '0.00') {
+ $userRewardCash = \app\admin\model\shopro\partner\Reward::where('user_id', $user->id)
+ ->where('status', 1)
+ ->where('amount', '>', 0)
+ ->sum('amount');
+ if ($userRewardCash > 0) {
+ $formatted = \addons\shopro\service\partner\IdentityService::formatMoney($userRewardCash);
+ $partner['total_income'] = $formatted;
+ $partner['income_total_cash'] = $formatted;
+ }
+ }
$data = array_merge($user->toArray(), $partner);
+ // 对齐用户余额字段:统一映射商城余额,避免前端不同字段读取出现口径偏差
+ $data['balance'] = $user->money;
$this->success('个人详情', $data);
}
diff --git a/addons/shopro/controller/user/WalletLog.php b/addons/shopro/controller/user/WalletLog.php
index 2be0a09..7ea7d6c 100644
--- a/addons/shopro/controller/user/WalletLog.php
+++ b/addons/shopro/controller/user/WalletLog.php
@@ -19,20 +19,102 @@ class WalletLog extends Common
$list_rows = $this->request->param('list_rows', 10);
$date = $this->request->param('date/a');
$user = auth_user();
- $where['user_id'] = $user->id;
+
+ $rewardEvents = [
+ 'push3', 'maker_diff', 'maker_peer', 'maker_overtaken',
+ 'team_venue', 'team_county', 'venue_peer', 'county_peer',
+ 'venue_share', 'county_share', 'reward_income', 'transfer_by_commission', 'transfer_to_money'
+ ];
+
+ $baseQuery = function () use ($user, $type, $rewardEvents) {
+ $model = UserWalletLogModel::where('user_id', $user->id);
+ if ($type === 'commission') {
+ // 收益记录:既包含历史 commission 流水,也对齐直入 money 的收益分成流水
+ $model->where(function ($query) use ($rewardEvents) {
+ $query->where('type', 'commission')
+ ->whereOr(function ($sub) use ($rewardEvents) {
+ $sub->where('type', 'money')->whereIn('event', $rewardEvents);
+ });
+ });
+ } else {
+ $model->where('type', $type);
+ }
+ return $model;
+ };
+
+ $filterQuery = $baseQuery();
+
+ if (!empty($date) && is_array($date) && count($date) === 2) {
+ $filterQuery->whereTime('createtime', 'between', $date);
+ }
switch ($tab) {
+ case 'reward':
+ $filterQuery->where('amount', '>', 0)->whereIn('event', $rewardEvents);
+ break;
case 'income':
- $where['amount'] = ['>', 0];
+ $filterQuery->where('amount', '>', 0);
break;
case 'expense':
- $where['amount'] = ['<', 0];
+ $filterQuery->where('amount', '<', 0);
break;
}
- $income = UserWalletLogModel::where('user_id', $user->id)->{$type}()->where('amount', '>', 0)->whereTime('createtime', 'between', $date)->sum('amount');
- $expense = UserWalletLogModel::where('user_id', $user->id)->{$type}()->where('amount', '<', 0)->whereTime('createtime', 'between', $date)->sum('amount');
- $logs = UserWalletLogModel::where($where)->{$type}()->whereTime('createtime', 'between', $date)->order('createtime', 'desc')->paginate($list_rows);
- $this->success('获取成功', ['list' => $logs, 'income' => $income, 'expense' => $expense]);
+ // 统计收入与支出(统一统计口径,避免内部互转重叠统计造成误导)
+ $incomeQuery = $baseQuery();
+ $expenseQuery = $baseQuery();
+ $rewardQuery = $baseQuery();
+
+ if (!empty($date) && is_array($date) && count($date) === 2) {
+ $incomeQuery->whereTime('createtime', 'between', $date);
+ $expenseQuery->whereTime('createtime', 'between', $date);
+ $rewardQuery->whereTime('createtime', 'between', $date);
+ }
+
+ $actualRewardEvents = [
+ 'push3', 'maker_diff', 'maker_peer', 'maker_overtaken',
+ 'team_venue', 'team_county', 'venue_peer', 'county_peer',
+ 'venue_share', 'county_share', 'reward_income'
+ ];
+
+ if ($type === 'commission') {
+ // 收益流水统计口径对齐:
+ // 1. 真实收益(总收入):排除内部互转入账事件 transfer_by_commission,避免佣金转入余额时重复计入收益
+ // 2. 真实扣减(总支出):排除内部结转转出事件 transfer_to_money 与 commission_transfer,避免误导用户认为产生了消费支出
+ $income = (float) $incomeQuery->where('amount', '>', 0)
+ ->where(function ($q) use ($actualRewardEvents) {
+ $q->where(function ($sub) use ($actualRewardEvents) {
+ $sub->where('type', 'money')->whereIn('event', $actualRewardEvents);
+ })->whereOr(function ($sub) {
+ $sub->where('type', 'commission')->whereNotIn('event', ['transfer_by_commission']);
+ });
+ })->sum('amount');
+
+ $expense = (float) $expenseQuery->where('amount', '<', 0)
+ ->whereNotIn('event', ['transfer_to_money', 'commission_transfer'])
+ ->sum('amount');
+
+ $rewardIncome = (float) $rewardQuery->where('amount', '>', 0)
+ ->where(function ($q) use ($actualRewardEvents) {
+ $q->where(function ($sub) use ($actualRewardEvents) {
+ $sub->where('type', 'money')->whereIn('event', $actualRewardEvents);
+ })->whereOr(function ($sub) {
+ $sub->where('type', 'commission')->whereNotIn('event', ['transfer_by_commission']);
+ });
+ })->sum('amount');
+ } else {
+ $income = (float) $incomeQuery->where('amount', '>', 0)->sum('amount');
+ $expense = (float) $expenseQuery->where('amount', '<', 0)->sum('amount');
+ $rewardIncome = (float) $rewardQuery->where('amount', '>', 0)->whereIn('event', $actualRewardEvents)->sum('amount');
+ }
+
+ $logs = $filterQuery->order('id', 'desc')->paginate($list_rows);
+
+ $this->success('获取成功', [
+ 'list' => $logs,
+ 'income' => $income,
+ 'expense' => $expense,
+ 'reward_income' => $rewardIncome,
+ ]);
}
}
diff --git a/addons/shopro/listener/Partner.php b/addons/shopro/listener/Partner.php
index 16af816..3ce0171 100644
--- a/addons/shopro/listener/Partner.php
+++ b/addons/shopro/listener/Partner.php
@@ -38,12 +38,9 @@ class Partner
\addons\shopro\service\partner\RateService::clearCache();
$userId = $order['user_id'] ?? ($params['user']['id'] ?? 0);
if ($userId) {
- $agent = new AgentService($userId);
- if (method_exists($agent, 'touchLastSelfOrderTime')) {
- $agent->touchLastSelfOrderTime();
- } else {
- $service->touchSelfOrder($userId);
- }
+ // 自购与直推订单双向保活(自购更新买家,直推更新一级推荐人)
+ $paidTime = !empty($order['paid_time']) ? intval($order['paid_time']) : time();
+ $service->touchOrderTimes($userId, $paidTime);
}
Queue::push('\addons\shopro\job\PartnerSettle@order', [
'order_id' => $order['id'],
diff --git a/addons/shopro/service/commission/Goods.php b/addons/shopro/service/commission/Goods.php
index 46745fb..c8ae903 100755
--- a/addons/shopro/service/commission/Goods.php
+++ b/addons/shopro/service/commission/Goods.php
@@ -38,7 +38,7 @@ class Goods
if ($this->commissionGoods) {
$commission_config = $this->commissionGoods->commission_config;
- if ($commission_config['status']) {
+ if (!empty($commission_config) && !empty($commission_config['status'])) {
$this->commissionConfig = $commission_config;
}
switch ($this->commissionGoods->self_rules) {
@@ -112,10 +112,12 @@ class Goods
$commissionRules = [];
foreach ($agentLevelRules as $agentLevel => $rule) {
$rule = json_decode($rule, true);
- foreach ($rule as $commission_level => $percent) {
- $commission_level = explode('_', $commission_level);
- $level = intval($commission_level[1]);
- $commissionRules[$agentLevel][$level] = ['rate' => $percent];
+ if (is_array($rule)) {
+ foreach ($rule as $commission_level => $percent) {
+ $commission_level = explode('_', $commission_level);
+ $level = intval($commission_level[1] ?? 1);
+ $commissionRules[$agentLevel][$level] = ['rate' => $percent];
+ }
}
}
return $commissionRules;
diff --git a/addons/shopro/service/commission/Order.php b/addons/shopro/service/commission/Order.php
index 34bf86c..c1d903b 100755
--- a/addons/shopro/service/commission/Order.php
+++ b/addons/shopro/service/commission/Order.php
@@ -71,8 +71,8 @@ class Order
return false;
}
- // 是否自购分销订单
- $this->selfBuy = $this->buyer->isAgentAvaliable() && $this->config->isSelfBuy();
+ // 是否自购分销订单(收益中心模式或开启自购时,分销商本人订单均记为自购订单以累积团队业绩)
+ $this->selfBuy = $this->buyer->isAgentAvaliable() && ($this->config->isSelfBuy() || boolval(sheep_config('shop.commission.partner_mode')));
// 未找到上级分销商且不是自购
if (!$this->buyer->getParentUserId() && !$this->selfBuy) {
@@ -202,7 +202,7 @@ class Order
'buyer_id' => $commissionOrder->buyer_id, // 购买人
'commission_order_id' => $commissionOrder->id, // 分销订单ID
'agent_id' => $currentAgent->user->id, // 待分佣分销商ID
- 'type' => 'commission', // 奖金类型
+ 'type' => \addons\shopro\service\partner\PartnerConfig::rewardWalletType(), // 奖金类型
'commission' => $commission, // 佣金
'status' => 0, // 佣金状态
'original_commission' => $commission, // 原始佣金
diff --git a/addons/shopro/service/partner/ActiveService.php b/addons/shopro/service/partner/ActiveService.php
index 5c00342..b29ea3a 100644
--- a/addons/shopro/service/partner/ActiveService.php
+++ b/addons/shopro/service/partner/ActiveService.php
@@ -36,11 +36,20 @@ class ActiveService
return $this->hasQualifiedOrder($userId, $start, $at);
}
+ /**
+ * 判断指定用户在时间窗口内是否有有效订单(本人自购或直推下级合格订单)
+ */
public function hasQualifiedOrder($userId, $start, $end)
{
+ if (!$userId) {
+ return false;
+ }
$rateService = new RateService();
+ $directChildIds = \think\Db::name('user')->where('parent_user_id', $userId)->column('id');
+ $candidateUserIds = array_merge([$userId], $directChildIds);
+
$order = $rateService->qualifiedOrderQuery($start, $end)
- ->where('o.user_id', $userId)
+ ->whereIn('o.user_id', $candidateUserIds)
->find();
return (bool) $order;
}
@@ -70,6 +79,7 @@ class ActiveService
if (!empty($nullUserIds)) {
$rateService = new RateService();
+ // 2.1 自购订单命中
$orderHits = $rateService->qualifiedOrderQuery($start, $end)
->whereIn('o.user_id', $nullUserIds)
->group('o.user_id')
@@ -79,8 +89,67 @@ class ActiveService
\think\Db::name('shopro_commission_agent')->where('user_id', $uId)->update(['last_self_order_time' => $lastTime]);
$hitUserIds[] = intval($uId);
}
+
+ // 2.2 直推下级订单命中(针对仍未命中的代理商)
+ $remainingNullIds = array_diff($nullUserIds, array_keys($orderHits));
+ if (!empty($remainingNullIds)) {
+ $userTable = \think\Db::name('user')->getTable();
+ $directHits = $rateService->qualifiedOrderQuery($start, $end)
+ ->join($userTable . ' u', 'u.id = o.user_id')
+ ->whereIn('u.parent_user_id', $remainingNullIds)
+ ->group('u.parent_user_id')
+ ->column('max(o.paid_time)', 'u.parent_user_id');
+
+ foreach ($directHits as $pId => $lastTime) {
+ \think\Db::name('shopro_commission_agent')->where('user_id', $pId)->update(['last_self_order_time' => $lastTime]);
+ $hitUserIds[] = intval($pId);
+ }
+ }
}
return array_values(array_unique(array_map('intval', $hitUserIds)));
}
+
+ /**
+ * 批量校准所有分销商的有效订单时间(自购及直推合格订单最大值回填)
+ */
+ public function syncActiveTimes($sinceTime = null)
+ {
+ $rateService = new RateService();
+ $userTable = \think\Db::name('user')->getTable();
+
+ // 1. 本人自购最新合格订单时间
+ $selfQuery = $rateService->qualifiedOrderQuery($sinceTime)
+ ->group('o.user_id')
+ ->column('max(o.paid_time)', 'o.user_id');
+
+ // 2. 直推下级最新合格订单时间
+ $directQuery = $rateService->qualifiedOrderQuery($sinceTime)
+ ->join($userTable . ' u', 'u.id = o.user_id')
+ ->where('u.parent_user_id', '>', 0)
+ ->group('u.parent_user_id')
+ ->column('max(o.paid_time)', 'u.parent_user_id');
+
+ // 3. 汇总取最大值
+ $allUserIds = array_unique(array_merge(array_keys($selfQuery), array_keys($directQuery)));
+ $updated = 0;
+
+ foreach ($allUserIds as $uId) {
+ $selfTime = intval($selfQuery[$uId] ?? 0);
+ $directTime = intval($directQuery[$uId] ?? 0);
+ $maxTime = max($selfTime, $directTime);
+
+ if ($maxTime > 0) {
+ $agent = \think\Db::name('shopro_commission_agent')->where('user_id', $uId)->find();
+ if ($agent && $maxTime > intval($agent['last_self_order_time'] ?? 0)) {
+ \think\Db::name('shopro_commission_agent')->where('user_id', $uId)->update([
+ 'last_self_order_time' => $maxTime,
+ ]);
+ $updated++;
+ }
+ }
+ }
+
+ return $updated;
+ }
}
diff --git a/addons/shopro/service/partner/DailySettleService.php b/addons/shopro/service/partner/DailySettleService.php
index 7edfe2b..5be3937 100644
--- a/addons/shopro/service/partner/DailySettleService.php
+++ b/addons/shopro/service/partner/DailySettleService.php
@@ -373,6 +373,7 @@ class DailySettleService
}
$each = function_exists('bcdiv') ? bcdiv((string) $pool, (string) $count, 2) : number_format($pool / $count, 2, '.', '');
$distributed = 0;
+ $walletType = PartnerConfig::rewardWalletType();
foreach ($userIds as $userId) {
PartnerRewardService::write([
'order_id' => $orderId,
@@ -382,11 +383,14 @@ class DailySettleService
'amount' => $each,
'base_amount' => $pool,
'settle_date' => $settleDate,
- 'ext' => ['formula' => "{$label}奖池 {$pool} / {$count} = {$each}", 'share_user_count' => $count],
+ 'ext' => [
+ 'formula' => "{$label}奖池 {$pool} / {$count} = {$each}",
+ 'share_user_count' => $count,
+ 'wallet_type' => $walletType,
+ ],
'status' => 1,
]);
if ((float) $each > 0) {
- $walletType = PartnerConfig::rewardWalletType();
Wallet::change($userId, $walletType, $each, $type, [
'settle_date' => $settleDate,
]);
@@ -404,7 +408,10 @@ class DailySettleService
'amount' => $tail,
'base_amount' => $pool,
'settle_date' => $settleDate,
- 'ext' => ['formula' => "{$label}尾差 {$tail} 进平台"],
+ 'ext' => [
+ 'formula' => "{$label}尾差 {$tail} 进平台",
+ 'wallet_type' => 'system',
+ ],
'status' => 1,
]);
}
@@ -434,7 +441,11 @@ class DailySettleService
'amount' => $pool,
'base_amount' => $pool,
'settle_date' => null,
- 'ext' => ['formula' => "{$label}分母为0,奖池 {$pool} 进平台", 'orders' => $orderIds],
+ 'ext' => [
+ 'formula' => "{$label}分母为0,奖池 {$pool} 进平台",
+ 'orders' => $orderIds,
+ 'wallet_type' => 'system',
+ ],
'status' => 1,
]);
return ['pool' => $pool, 'count' => 0, 'each' => '0.00'];
@@ -442,6 +453,7 @@ class DailySettleService
$each = function_exists('bcdiv') ? bcdiv((string) $pool, (string) $count, 2) : number_format($pool / $count, 2, '.', '');
$distributed = 0;
+ $walletType = PartnerConfig::rewardWalletType();
foreach ($userIds as $userId) {
PartnerRewardService::write([
@@ -453,11 +465,16 @@ class DailySettleService
'amount' => $each,
'base_amount' => $pool,
'settle_date' => null,
- 'ext' => ['formula' => "{$label}奖池 {$pool} / {$count} = {$each}", 'share_user_count' => $count, 'pool' => $pool, 'orders' => $orderIds],
+ 'ext' => [
+ 'formula' => "{$label}奖池 {$pool} / {$count} = {$each}",
+ 'share_user_count' => $count,
+ 'pool' => $pool,
+ 'orders' => $orderIds,
+ 'wallet_type' => $walletType,
+ ],
'status' => 1,
]);
if ((float) $each > 0) {
- $walletType = PartnerConfig::rewardWalletType();
Wallet::change($userId, $walletType, $each, $type, [
'minute_settle' => 1,
'orders' => $orderIds,
@@ -478,7 +495,11 @@ class DailySettleService
'amount' => $tail,
'base_amount' => $pool,
'settle_date' => null,
- 'ext' => ['formula' => "{$label}尾差 {$tail} 进平台", 'orders' => $orderIds],
+ 'ext' => [
+ 'formula' => "{$label}尾差 {$tail} 进平台",
+ 'orders' => $orderIds,
+ 'wallet_type' => 'system',
+ ],
'status' => 1,
]);
}
diff --git a/addons/shopro/service/partner/IdentityService.php b/addons/shopro/service/partner/IdentityService.php
index 0d5d4bf..6752fa8 100644
--- a/addons/shopro/service/partner/IdentityService.php
+++ b/addons/shopro/service/partner/IdentityService.php
@@ -153,7 +153,17 @@ class IdentityService
}
$data = is_object($agent) ? $agent->toArray() : $agent;
+ $userId = intval($data['user_id'] ?? 0);
+ $agentTotalIncome = floatval($data['total_income'] ?? 0);
+ $rewardCash = $userId > 0 ? \app\admin\model\shopro\partner\Reward::where('user_id', $userId)
+ ->where('status', 1)
+ ->where('amount', '>', 0)
+ ->sum('amount') : 0;
+ $unifiedTotalIncome = max($agentTotalIncome, floatval($rewardCash));
+
return array_merge($data, $identity, [
+ 'total_income' => self::formatMoney($unifiedTotalIncome),
+ 'income_total_cash' => self::formatMoney($unifiedTotalIncome),
'is_venue' => intval($data['is_venue'] ?? 0),
'is_county' => intval($data['is_county'] ?? 0),
'county_code' => (string) ($data['county_code'] ?? ''),
@@ -161,6 +171,7 @@ class IdentityService
'madow' => self::formatMadow($madow),
'madow_rate' => self::formatRate($rate),
'madow_value' => self::formatMoney((float) $madow * (float) $rate),
+ 'show_madow_rate' => PartnerConfig::showMadowRate(),
'voucher_total' => self::formatMoney($total),
'voucher_released' => self::formatMoney($released),
'voucher_used' => self::formatMoney($used),
@@ -190,6 +201,9 @@ class IdentityService
'madow' => '0',
'madow_rate' => self::formatRate($baseRate),
'madow_value' => '0.00',
+ 'show_madow_rate' => PartnerConfig::showMadowRate(),
+ 'total_income' => '0.00',
+ 'income_total_cash' => '0.00',
'voucher_available' => '0.00',
'active_30d' => true,
'active_status' => 'inactive',
@@ -211,9 +225,12 @@ class IdentityService
'maker_level_name' => $full['maker_level_name'],
'role_names' => $roleNames,
'custom_role_names' => $roleNames,
+ 'total_income' => $full['total_income'] ?? '0.00',
+ 'income_total_cash' => $full['income_total_cash'] ?? '0.00',
'madow' => $full['madow'],
'madow_rate' => $full['madow_rate'],
'madow_value' => $full['madow_value'],
+ 'show_madow_rate' => $full['show_madow_rate'] ?? PartnerConfig::showMadowRate(),
'voucher_available' => $full['voucher_available'],
'active_30d' => $full['active_30d'],
'active_status' => $full['active_status'] ?? 'inactive',
diff --git a/addons/shopro/service/partner/PartnerConfig.php b/addons/shopro/service/partner/PartnerConfig.php
index 06f4752..a10c901 100644
--- a/addons/shopro/service/partner/PartnerConfig.php
+++ b/addons/shopro/service/partner/PartnerConfig.php
@@ -40,6 +40,15 @@ class PartnerConfig
return $enable === null ? true : (bool) $enable;
}
+ public static function showMadowRate()
+ {
+ $show = self::get('partner_show_madow_rate', 1);
+ if ($show === null || $show === '') {
+ return 1;
+ }
+ return intval($show) ? 1 : 0;
+ }
+
public static function activeDays()
{
$days = intval(self::get('partner_active_days', 30));
@@ -97,18 +106,20 @@ class PartnerConfig
$maker = $roles['maker'];
$venue = $roles['venue'];
$county = $roles['county'];
+ $countyShort = in_array($county, ['区县代理', '区县']) ? '区县' : $county;
+ $countyAgentShort = in_array($county, ['区县代理', '区县']) ? '区县代' : $county;
return [
- 'push3' => '推荐开拓奖',
+ 'push3' => '推荐提成',
'maker_diff' => $maker . '进阶奖',
'maker_peer' => $maker . '协同奖',
'maker_overtaken' => $maker . '卓越奖',
- 'team_venue' => $venue . '运营奖',
- 'team_county' => $county . '统筹奖',
- 'venue_peer' => $venue . '协同奖',
- 'county_peer' => $county . '协同奖',
- 'venue_share' => $venue . '加权奖励',
- 'county_share' => $county . '加权奖励',
+ 'team_venue' => $venue . '管理奖',
+ 'team_county' => $countyShort . '管理奖',
+ 'venue_peer' => $venue . '平级奖',
+ 'county_peer' => $countyAgentShort . '平级奖',
+ 'venue_share' => $venue . '加权奖',
+ 'county_share' => $countyShort . '加权奖',
'madow_share' => '每日贡献马豆',
'admin_madow' => '管理员调整马豆',
'voucher_grant' => '消费补贴发放',
@@ -139,13 +150,13 @@ class PartnerConfig
'partner_off' => '结算未开启',
];
$user = [
- 'no_parent' => '暂无推荐人,不计提推荐开拓奖',
- 'self_buy' => '本人下单不计提推荐开拓奖',
+ 'no_parent' => '暂无推荐人,不计提推荐提成',
+ 'self_buy' => '本人下单不计提推荐提成',
'inactive_30d' => '需 30 日内有本人订单才能领取奖励',
'not_maker' => '',
'not_venue' => '',
'not_county' => '',
- 'cap_not_higher' => '同阶不计进阶差额,享受协同奖励',
+ 'cap_not_higher' => '同阶不计进阶差额,享受平级奖励',
'already_settled' => '',
'partner_goods_off' => '',
'partner_off' => '',
diff --git a/addons/shopro/service/partner/PartnerRewardService.php b/addons/shopro/service/partner/PartnerRewardService.php
index 405cad7..57aa1d1 100644
--- a/addons/shopro/service/partner/PartnerRewardService.php
+++ b/addons/shopro/service/partner/PartnerRewardService.php
@@ -310,6 +310,10 @@ class PartnerRewardService
public static function account($order, $buyerId, $userId, $type, $role, $amount, $base, $ext = [])
{
$orderId = is_array($order) || $order instanceof \think\Model ? ($order['id'] ?? 0) : intval($order);
+ $walletType = PartnerConfig::rewardWalletType();
+ if (!isset($ext['wallet_type'])) {
+ $ext['wallet_type'] = $walletType;
+ }
$row = self::write([
'order_id' => $orderId,
'buyer_id' => $buyerId,
@@ -322,7 +326,6 @@ class PartnerRewardService
'status' => 1,
]);
if ($row && self::$lastWriteCreated && (float) $amount > 0 && $userId > 0 && $row->status == 1 && in_array($type, PartnerConfig::instantTypes())) {
- $walletType = PartnerConfig::rewardWalletType();
Wallet::change($userId, $walletType, $amount, $type, [
'partner_reward_id' => $row->id,
'order_id' => $orderId,
diff --git a/addons/shopro/service/partner/PeerShareReward.php b/addons/shopro/service/partner/PeerShareReward.php
index e4fe64b..34be244 100644
--- a/addons/shopro/service/partner/PeerShareReward.php
+++ b/addons/shopro/service/partner/PeerShareReward.php
@@ -3,7 +3,7 @@
namespace addons\shopro\service\partner;
/**
- * 酒道馆 / 区县协同奖 10% 逐代递减级联,最低发放不能少于 0.10 元截断
+ * 酒道馆 / 区县代平级奖 10% 逐代递减级联,最低发放不能少于 0.10 元截断
*/
class PeerShareReward
{
@@ -66,7 +66,7 @@ class PeerShareReward
$type,
$role,
'inactive_30d',
- "{$label}协同(第{$gen}代) 30 日无自购"
+ "{$label}平级(第{$gen}代) 30 日无自购"
);
} else {
PartnerRewardService::account(
@@ -79,7 +79,7 @@ class PeerShareReward
$currentBase,
[
'generation' => $gen,
- 'formula' => "{$label}协同(第{$gen}代) {$currentBase} × " . ($rate * 100) . "% = {$amount}",
+ 'formula' => "{$label}平级(第{$gen}代) {$currentBase} × " . ($rate * 100) . "% = {$amount}",
'from_user_id' => $sourceUserId,
]
);
diff --git a/addons/shopro/service/partner/Push3Reward.php b/addons/shopro/service/partner/Push3Reward.php
index d2f0d3c..7e7b942 100644
--- a/addons/shopro/service/partner/Push3Reward.php
+++ b/addons/shopro/service/partner/Push3Reward.php
@@ -5,18 +5,18 @@ namespace addons\shopro\service\partner;
use app\admin\model\shopro\commission\Agent as AgentModel;
/**
- * 推荐开拓奖:398/400/600 循环,计到推荐人,自购不发
+ * 推荐提成:398/400/600 循环,计到推荐人,自购不发
*/
class Push3Reward
{
public function run($order, $buyerId, $parent, ActiveService $active, $rules = null)
{
if (!$parent) {
- PartnerRewardService::writeSkip($order, $buyerId, 0, 'push3', 'taster', 'no_parent', '暂无推荐人,不计提推荐开拓奖');
+ PartnerRewardService::writeSkip($order, $buyerId, 0, 'push3', 'taster', 'no_parent', '暂无推荐人,不计提推荐提成');
return;
}
if (intval($parent['user_id']) === intval($buyerId)) {
- PartnerRewardService::writeSkip($order, $buyerId, $parent['user_id'], 'push3', 'taster', 'self_buy', '本人下单不计提推荐开拓奖');
+ PartnerRewardService::writeSkip($order, $buyerId, $parent['user_id'], 'push3', 'taster', 'self_buy', '本人下单不计提推荐提成');
return;
}
if (intval($parent['level']) < 1) {
@@ -42,7 +42,7 @@ class Push3Reward
$amount = $amounts[($seq - 1) % 3];
PartnerRewardService::account($order, $buyerId, $agent->user_id, 'push3', 'taster', $amount, $amount, [
- 'formula' => "推荐开拓第{$seq}单,循环第" . ((($seq - 1) % 3) + 1) . "档 +{$amount}",
+ 'formula' => "推荐提成第{$seq}单,循环第" . ((($seq - 1) % 3) + 1) . "档 +{$amount}",
'seq' => $seq,
]);
}
diff --git a/addons/shopro/service/partner/TeamSplitReward.php b/addons/shopro/service/partner/TeamSplitReward.php
index 368a059..fe3c7c4 100644
--- a/addons/shopro/service/partner/TeamSplitReward.php
+++ b/addons/shopro/service/partner/TeamSplitReward.php
@@ -3,7 +3,7 @@
namespace addons\shopro\service\partner;
/**
- * 统筹运营奖励:无馆有县=100;有馆有县=50+50;有馆无县=50
+ * 渠道管理奖(馆/县拆分):无馆有县=100;有馆有县=50+50;有馆无县=50
*/
class TeamSplitReward
{
@@ -30,7 +30,7 @@ class TeamSplitReward
PartnerRewardService::writeSkip($order, $buyerId, $venue['user_id'], 'team_venue', 'venue', 'inactive_30d', '酒道馆 30 日无自购');
} else {
PartnerRewardService::account($order, $buyerId, $venue['user_id'], 'team_venue', 'venue', $venueSplit, $venueSplit, [
- 'formula' => "酒道馆运营奖 {$venueSplit}",
+ 'formula' => "酒道馆管理奖 {$venueSplit}",
]);
$result['venue'] = ['user_id' => $venue['user_id'], 'amount' => $venueSplit, 'index' => $venue['index']];
}
@@ -41,8 +41,11 @@ class TeamSplitReward
if ($county['inactive']) {
PartnerRewardService::writeSkip($order, $buyerId, $county['user_id'], 'team_county', 'county', 'inactive_30d', '区县 30 日无自购');
} else {
+ $formulaText = $venue
+ ? (($venue['user_id'] == $county['user_id']) ? "区县管理奖 {$amount}(馆兼县)" : "区县管理奖 {$amount}(馆上)")
+ : "区县管理奖 {$amount}(直达)";
PartnerRewardService::account($order, $buyerId, $county['user_id'], 'team_county', 'county', $amount, $amount, [
- 'formula' => $venue ? "区县统筹奖 {$amount}(馆上)" : "区县统筹奖 {$amount}(直达)",
+ 'formula' => $formulaText,
]);
$result['county'] = ['user_id' => $county['user_id'], 'amount' => $amount, 'index' => $county['index']];
}
@@ -71,7 +74,8 @@ class TeamSplitReward
protected function nearestCounty($chain, $venue, ActiveService $active)
{
- $start = $venue ? ($venue['index'] + 1) : 0;
+ // 若酒道馆节点本身兼任区县代理,允许从该酒道馆节点开始匹配(兼具双重身份场景);若未兼任,循环会自动检查其上一级
+ $start = $venue ? $venue['index'] : 0;
for ($i = $start; $i < count($chain); $i++) {
if (!intval($chain[$i]['is_county'] ?? 0)) {
continue;
diff --git a/addons/shopro/service/partner/WalletMigrationService.php b/addons/shopro/service/partner/WalletMigrationService.php
new file mode 100644
index 0000000..6ed7f9e
--- /dev/null
+++ b/addons/shopro/service/partner/WalletMigrationService.php
@@ -0,0 +1,141 @@
+', 0);
+ $totalCount = (int) $query->count();
+ $totalCommission = (float) $query->sum('commission');
+
+ $sampleList = UserModel::where('commission', '>', 0)
+ ->field('id,nickname,mobile,money,commission')
+ ->order('commission', 'desc')
+ ->limit(20)
+ ->select();
+
+ $samples = [];
+ foreach ($sampleList as $u) {
+ $samples[] = [
+ 'id' => (int) $u->id,
+ 'nickname' => (string) ($u->nickname ?: '用户' . $u->id),
+ 'mobile' => (string) ($u->mobile ?: ''),
+ 'current_money' => number_format((float) $u->money, 2, '.', ''),
+ 'current_commission' => number_format((float) $u->commission, 2, '.', ''),
+ 'target_money' => number_format((float) bcadd((string) $u->money, (string) $u->commission, 2), 2, '.', ''),
+ ];
+ }
+
+ return [
+ 'total_users' => $totalCount,
+ 'total_commission' => number_format($totalCommission, 2, '.', ''),
+ 'samples' => $samples,
+ ];
+ }
+
+ /**
+ * 执行全量结转:将所有会员的 commission 余额转入 money 钱包
+ *
+ * @param bool $isDryRun 是否只读模拟
+ * @param callable|null $progressCallback 进度回调
+ * @param array $operator 操作人上下文
+ * @return array
+ */
+ public function transferAll($isDryRun = false, $progressCallback = null, $operator = [])
+ {
+ if ($isDryRun) {
+ $preview = $this->preview();
+ return [
+ 'status' => 'dry_run',
+ 'dry_run' => true,
+ 'total_users' => $preview['total_users'],
+ 'transferred_amount' => $preview['total_commission'],
+ 'success_count' => 0,
+ 'fail_count' => 0,
+ 'errors' => [],
+ 'preview' => $preview,
+ ];
+ }
+
+ $userIds = UserModel::where('commission', '>', 0)->order('id', 'asc')->column('id');
+ $totalUsers = count($userIds);
+ $successCount = 0;
+ $failCount = 0;
+ $transferredAmount = '0.00';
+ $errors = [];
+
+ foreach ($userIds as $idx => $userId) {
+ try {
+ $transferResult = Db::transaction(function () use ($userId) {
+ $user = UserModel::where('id', $userId)->lock(true)->find();
+ if (!$user) {
+ return 0.0;
+ }
+
+ $amount = floatval($user->commission);
+ if ($amount <= 0) {
+ return 0.0;
+ }
+
+ // 1. 扣减佣金钱包
+ Wallet::change($user, 'commission', -$amount, 'transfer_to_money', [
+ 'batch_migration' => 1,
+ 'reason' => '全量历史收益结转至商城余额',
+ ], '收益结转至商城余额');
+
+ // 2. 增加商城余额
+ Wallet::change($user, 'money', $amount, 'transfer_by_commission', [
+ 'batch_migration' => 1,
+ 'reason' => '全量历史收益结转至商城余额',
+ ], '收益结转至商城余额');
+
+ return $amount;
+ });
+
+ if ($transferResult > 0) {
+ $successCount++;
+ $transferredAmount = bcadd($transferredAmount, (string) $transferResult, 2);
+ }
+
+ if (is_callable($progressCallback)) {
+ $progressCallback($idx + 1, $totalUsers, $userId, $transferResult, null);
+ }
+ } catch (\Throwable $e) {
+ $failCount++;
+ $errItem = [
+ 'user_id' => $userId,
+ 'error' => $e->getMessage(),
+ ];
+ $errors[] = $errItem;
+
+ if (is_callable($progressCallback)) {
+ $progressCallback($idx + 1, $totalUsers, $userId, 0.0, $e->getMessage());
+ }
+ }
+ }
+
+ return [
+ 'status' => 'completed',
+ 'dry_run' => false,
+ 'total_users' => $totalUsers,
+ 'success_count' => $successCount,
+ 'fail_count' => $failCount,
+ 'transferred_amount' => number_format((float) $transferredAmount, 2, '.', ''),
+ 'errors' => $errors,
+ ];
+ }
+}
diff --git a/addons/shopro/upgrade/partner_v2.sql b/addons/shopro/upgrade/partner_v2.sql
index e1204cc..e3dbc57 100644
--- a/addons/shopro/upgrade/partner_v2.sql
+++ b/addons/shopro/upgrade/partner_v2.sql
@@ -246,6 +246,7 @@ INSERT INTO `__PREFIX__shopro_config` (`code`, `parent_code`, `name`, `descripti
('shop.commission.partner_chain_max_depth', 'shop.commission', '关系链最大追溯层级', '向上寻找创客/酒道馆/区县时的最大关系链深度', 'int', '50', NULL, NULL, 84),
('shop.commission.partner_push3_amounts', 'shop.commission', '推三返一循环金额配置', '品酒师推荐直推合格单获得的阶梯奖励金额循环数组', 'array', '[398,400,600]', NULL, NULL, 85),
('shop.commission.partner_madow_mode', 'shop.commission', '每日GMV分润模式', 'madow=以马豆形式发放 cash=直接以佣金现金发放', 'string', 'madow', NULL, NULL, 86),
+('shop.commission.partner_show_madow_rate', 'shop.commission', '显示马币市值胶囊', '控制移动端我的马豆页面是否显示马币市值胶囊 1=显示 0=隐藏', 'int', '1', NULL, NULL, 86),
('shop.commission.partner_madow_base_rate', 'shop.commission', '马豆基准汇率', '大盘初始起步汇率 (元/马豆)', 'string', '0.00000075', NULL, NULL, 87),
('shop.commission.partner_madow_step_orders', 'shop.commission', '马豆汇率步进单数', '每达成指定合格订单数,马豆大盘汇率递增1个基数', 'int', '1', NULL, NULL, 88),
('shop.commission.partner_voucher_times', 'shop.commission', '买酒赠送兑换券倍数', '符合条件的订单实付金额倍数,默认1.5倍', 'float', '1.5', NULL, NULL, 89),
diff --git a/application/admin/controller/shopro/Config.php b/application/admin/controller/shopro/Config.php
index 97351e8..8415e02 100644
--- a/application/admin/controller/shopro/Config.php
+++ b/application/admin/controller/shopro/Config.php
@@ -194,6 +194,7 @@ class Config extends Common
$keys = [
'partner_mode', 'partner_enable', 'partner_count_type', 'partner_active_days',
'partner_chain_max_depth', 'partner_push3_amounts', 'partner_madow_mode',
+ 'partner_show_madow_rate',
'partner_madow_base_rate', 'partner_madow_step_orders', 'partner_voucher_times',
'partner_voucher_days', 'partner_maker_bonus', 'partner_maker_peer_rate',
'partner_maker_overtaken_rate', 'partner_team_total', 'partner_venue_split',
@@ -216,6 +217,7 @@ class Config extends Common
'partner_chain_max_depth' => 50,
'partner_push3_amounts' => [398, 400, 600],
'partner_madow_mode' => 'madow',
+ 'partner_show_madow_rate' => 1,
'partner_madow_base_rate' => '0.00000075',
'partner_madow_step_orders' => 1,
'partner_voucher_times' => 1.5,
@@ -241,6 +243,7 @@ class Config extends Common
$configs[$key] = $value;
}
}
+ $configs['partner_show_madow_rate'] = isset($configs['partner_show_madow_rate']) ? intval($configs['partner_show_madow_rate']) : 1;
} elseif ('POST' === $this->request->method()) {
check_env('commission');
$params = $this->request->param();
@@ -250,6 +253,22 @@ class Config extends Common
$save[$key] = $params[$key];
}
}
+ if (isset($save['partner_show_madow_rate'])) {
+ $save['partner_show_madow_rate'] = intval($save['partner_show_madow_rate']);
+ }
+ // 确保数据库存在 partner_show_madow_rate 配置行
+ $existConfig = ShoproConfig::where('code', 'shop.commission.partner_show_madow_rate')->find();
+ if (!$existConfig) {
+ ShoproConfig::create([
+ 'code' => 'shop.commission.partner_show_madow_rate',
+ 'parent_code' => 'shop.commission',
+ 'name' => '显示马币市值',
+ 'description' => '1=显示 0=隐藏',
+ 'type' => 'int',
+ 'value' => '1',
+ 'weigh' => 86
+ ]);
+ }
foreach (['partner_role_name_taster', 'partner_role_name_maker', 'partner_role_name_venue', 'partner_role_name_county', 'partner_role_name_partner'] as $roleKey) {
if (isset($save[$roleKey])) {
$save[$roleKey] = trim((string) $save[$roleKey]);
diff --git a/application/admin/controller/shopro/Dashboard.php b/application/admin/controller/shopro/Dashboard.php
index ff7234e..93162cf 100644
--- a/application/admin/controller/shopro/Dashboard.php
+++ b/application/admin/controller/shopro/Dashboard.php
@@ -749,7 +749,7 @@ class Dashboard extends Common
$structureMap = [
'push3' => [
'key' => 'push3',
- 'name' => '推荐开拓奖励',
+ 'name' => '推荐提成',
'types' => ['push3'],
'amount' => 0.00,
'count' => 0,
@@ -765,7 +765,7 @@ class Dashboard extends Common
],
'channel' => [
'key' => 'channel',
- 'name' => '实体统筹协同奖励',
+ 'name' => '渠道管理与平级奖',
'types' => ['team_venue', 'team_county', 'venue_peer', 'county_peer'],
'amount' => 0.00,
'count' => 0,
@@ -773,7 +773,7 @@ class Dashboard extends Common
],
'share' => [
'key' => 'share',
- 'name' => '全网加权贡献奖励',
+ 'name' => '全网加权分红奖',
'types' => ['venue_share', 'county_share'],
'amount' => 0.00,
'count' => 0,
diff --git a/application/admin/controller/shopro/commission/Agent.php b/application/admin/controller/shopro/commission/Agent.php
index a6bbf0d..ca412a7 100644
--- a/application/admin/controller/shopro/commission/Agent.php
+++ b/application/admin/controller/shopro/commission/Agent.php
@@ -93,10 +93,14 @@ class Agent extends Common
->where('reward_type', '<>', 'push3')
->sum('amount');
- $detail->income_total_cash = number_format((float) $totalCash, 2, '.', '');
+ $alignedIncome = number_format(max(floatval($detail->total_income ?? 0), (float) $totalCash), 2, '.', '');
+ $detail->income_total_cash = $alignedIncome;
+ $detail->total_income = $alignedIncome;
+ $detail->partner_cash_income = $alignedIncome;
$detail->push3_cash_income = number_format((float) $push3Cash, 2, '.', '');
$detail->partner_exclusive_cash = number_format((float) $partnerExclusiveCash, 2, '.', '');
- $detail->partner_cash_income = $detail->income_total_cash;
+ $detail->user_money = number_format(floatval($detail->user ? $detail->user->money : 0), 2, '.', '');
+ $detail->user_commission = number_format(floatval($detail->user ? $detail->user->commission : 0), 2, '.', '');
$detail->partner_mode = \addons\shopro\service\partner\PartnerConfig::isPartnerMode() ? 1 : 0;
$rate = (new \addons\shopro\service\partner\RateService())->currentRate();
$detail->madow_value = \addons\shopro\service\partner\IdentityService::formatMoney((float) ($detail->madow ?? 0) * (float) $rate);
diff --git a/application/admin/controller/shopro/commission/Reward.php b/application/admin/controller/shopro/commission/Reward.php
index 4429962..16eebbd 100644
--- a/application/admin/controller/shopro/commission/Reward.php
+++ b/application/admin/controller/shopro/commission/Reward.php
@@ -58,7 +58,7 @@ class Reward extends Common
$export = new \addons\shopro\library\Export();
$params = [
- 'file_name' => '佣金明细列表',
+ 'file_name' => '历史分销佣金列表',
'cell_titles' => $cellTitles,
'total' => $total,
'is_sub_cell' => false,
diff --git a/application/admin/controller/shopro/order/Order.php b/application/admin/controller/shopro/order/Order.php
index 04e9dbf..d756a51 100644
--- a/application/admin/controller/shopro/order/Order.php
+++ b/application/admin/controller/shopro/order/Order.php
@@ -242,7 +242,7 @@ class Order extends Common
// 友好展示金额
if ($type === 'voucher_grant') {
- $amountText = number_format($baseAmount, 2, '.', '') . ' 兑换券';
+ $amountText = number_format($baseAmount, 2, '.', '') . ' 个';
} elseif ($madow > 0) {
$amountText = \addons\shopro\service\partner\IdentityService::formatMadow($madow) . ' 马豆';
} elseif ($amount > 0) {
@@ -270,6 +270,8 @@ class Order extends Common
)),
'formula' => (string) ($row->formula ?? ''),
'skip_reason_text' => (string) ($row->skip_reason_text ?? ''),
+ 'wallet_type' => (string) ($row->wallet_type ?? ($amount > 0 ? \addons\shopro\service\partner\PartnerConfig::rewardWalletType() : 'system')),
+ 'wallet_type_text' => (string) ($row->wallet_type_text ?: ($amount > 0 ? (\addons\shopro\service\partner\PartnerConfig::rewardWalletType() === 'money' ? '商城余额' : '佣金钱包') : '平台留存')),
];
}
@@ -299,6 +301,8 @@ class Order extends Common
'base_amount' => '0.00',
'amount_text' => '¥' . number_format($cCommission, 2, '.', ''),
'madow' => '0.00000000',
+ 'wallet_type' => 'commission',
+ 'wallet_type_text' => '佣金钱包',
'status' => $cStatus,
'status_text' => (string) ($crow->status_text ?: ($cStatus === 1 ? '已入账' : '待入账')),
'formula' => '分销规则佣金',
@@ -393,6 +397,8 @@ class Order extends Common
'total_amount' => number_format($platAmount, 2, '.', ''),
'total_madow' => \addons\shopro\service\partner\IdentityService::formatMadow($platMadow),
'amount_text' => $amountText,
+ 'wallet_type' => 'system',
+ 'wallet_type_text' => '平台留存',
'settle_date' => $settleDate,
'status' => 1,
'status_text' => '已入账',
@@ -424,6 +430,8 @@ class Order extends Common
'total_amount' => number_format($totalAmount, 2, '.', ''),
'total_madow' => \addons\shopro\service\partner\IdentityService::formatMadow($totalMadow),
'amount_text' => $type === 'madow_share' ? ($eachMadow . ' 马豆/人') : ('¥' . $eachAmount . '/人'),
+ 'wallet_type' => $type === 'madow_share' ? 'madow' : \addons\shopro\service\partner\PartnerConfig::rewardWalletType(),
+ 'wallet_type_text' => $type === 'madow_share' ? '分红马豆' : (\addons\shopro\service\partner\PartnerConfig::rewardWalletType() === 'money' ? '商城余额' : '佣金钱包'),
'settle_date' => $settleDate,
'status' => 1,
'status_text' => '已入账',
diff --git a/application/admin/controller/shopro/partner/Reward.php b/application/admin/controller/shopro/partner/Reward.php
index 6fc88b0..ff94e24 100644
--- a/application/admin/controller/shopro/partner/Reward.php
+++ b/application/admin/controller/shopro/partner/Reward.php
@@ -50,6 +50,7 @@ class Reward extends Common
'reward_type_text' => '奖励类型',
'role_text' => '受益身份',
'amount' => '结算金额',
+ 'wallet_type_text' => '入账账户',
'madow' => '马豆',
'base_amount' => '基数',
'status_text' => '状态',
@@ -83,6 +84,7 @@ class Reward extends Common
'reward_type_text' => $reward['reward_type_text'],
'role_text' => $reward['role_text'] ?? $reward['role'],
'amount' => $reward['amount'],
+ 'wallet_type_text' => $reward['wallet_type_text'] ?? '商城余额',
'madow' => \addons\shopro\service\partner\IdentityService::formatMadow($reward['madow']),
'base_amount' => $reward['base_amount'],
'status_text' => $reward['status_text'],
@@ -102,6 +104,7 @@ class Reward extends Common
'reward_type_text' => '-',
'role_text' => '-',
'amount' => number_format($totalAmount, 2, '.', ''),
+ 'wallet_type_text' => '现金直入余额',
'madow' => $formattedTotalMadow,
'base_amount' => '-',
'status_text' => '已汇总',
@@ -138,7 +141,8 @@ class Reward extends Common
'ext' => ['formula' => '手工补发', 'memo' => $params['memo'], 'manual' => 1],
'status' => 1,
]);
- Wallet::change($params['user_id'], 'commission', $amount, 'reward_income', [
+ $walletType = PartnerConfig::rewardWalletType();
+ Wallet::change($params['user_id'], $walletType, $amount, 'reward_income', [
'partner_reward_id' => $row->id ?? 0,
'manual' => 1,
], $params['memo']);
@@ -162,7 +166,12 @@ class Reward extends Common
}
Db::transaction(function () use ($row, $memo) {
if ((float) $row->amount > 0 && $row->user_id > 0) {
- Wallet::change($row->user_id, 'commission', -$row->amount, 'reward_back', [
+ $walletLog = \app\admin\model\shopro\user\WalletLog::where('user_id', $row->user_id)
+ ->where('ext', 'like', '%"partner_reward_id":' . intval($row->id) . '%')
+ ->order('id', 'desc')
+ ->find();
+ $targetWallet = ($walletLog && $walletLog->type === 'commission') ? 'commission' : PartnerConfig::rewardWalletType();
+ Wallet::change($row->user_id, $targetWallet, -$row->amount, 'reward_back', [
'partner_reward_id' => $row->id,
], $memo);
}
@@ -217,6 +226,8 @@ class Reward extends Common
'user_nickname' => $r->user ? ($r->user->nickname ?: $r->user->mobile) : '平台/买家',
'role_text' => $r->role_text ?: $r->role,
'amount' => $r->amount,
+ 'wallet_type' => $r->wallet_type,
+ 'wallet_type_text' => $r->wallet_type_text,
'madow' => $r->madow,
'base_amount' => $r->base_amount,
'status' => $r->status,
@@ -259,4 +270,30 @@ class Reward extends Common
] : null,
]);
}
+
+ /**
+ * 全量结转收益预览试算
+ */
+ public function transferAllCommissionPreview()
+ {
+ $service = new \addons\shopro\service\partner\WalletMigrationService();
+ $preview = $service->preview();
+ $this->success('获取成功', null, $preview);
+ }
+
+ /**
+ * 执行全量结转收益至商城余额
+ */
+ public function transferAllCommission()
+ {
+ $isDryRun = (bool) $this->request->param('dry_run', false);
+ $confirm = (int) $this->request->param('confirm', 0);
+ if (!$isDryRun && $confirm !== 1) {
+ $this->error('操作属于高风险批量资金转移,请传递 confirm=1 确认执行');
+ }
+
+ $service = new \addons\shopro\service\partner\WalletMigrationService();
+ $result = $service->transferAll($isDryRun);
+ $this->success($isDryRun ? '试算成功' : '结转完成', null, $result);
+ }
}
diff --git a/application/admin/controller/shopro/user/User.php b/application/admin/controller/shopro/user/User.php
index 2274d7d..6fba0c7 100644
--- a/application/admin/controller/shopro/user/User.php
+++ b/application/admin/controller/shopro/user/User.php
@@ -142,11 +142,14 @@ class User extends Common
: ('已暂停领取' . ($ago !== null ? '(距今 ' . $ago . ' 天)' : '(从未下单)'));
}
- // 累计现金收益统计
+ // 累计现金收益统计(与 IdentityService 严格对齐,兼顾历史分销与收益中心)
+ $agentTotalIncome = floatval($agent->total_income ?? 0);
$totalCash = \app\admin\model\shopro\partner\Reward::where('user_id', $id)
->where('status', 1)
->where('amount', '>', 0)
->sum('amount');
+ $alignedTotalIncome = max($agentTotalIncome, (float) $totalCash);
+
$push3Cash = \app\admin\model\shopro\partner\Reward::where('user_id', $id)
->where('status', 1)
->where('amount', '>', 0)
@@ -158,9 +161,12 @@ class User extends Common
->where('reward_type', '<>', 'push3')
->sum('amount');
- $data['income_total_cash'] = number_format((float) $totalCash, 2, '.', '');
+ $data['income_total_cash'] = number_format($alignedTotalIncome, 2, '.', '');
+ $data['total_income'] = number_format($alignedTotalIncome, 2, '.', '');
$data['push3_cash_income'] = number_format((float) $push3Cash, 2, '.', '');
$data['partner_exclusive_cash'] = number_format((float) $partnerExclusiveCash, 2, '.', '');
+ $data['money'] = number_format(floatval($user->money ?? 0), 2, '.', '');
+ $data['commission'] = number_format(floatval($user->commission ?? 0), 2, '.', '');
// 兑换券计划资产统计
$voucherTotal = floatval($agent->voucher_total ?? 0);
diff --git a/application/admin/controller/shopro/user/WalletLog.php b/application/admin/controller/shopro/user/WalletLog.php
index ece320c..932082b 100644
--- a/application/admin/controller/shopro/user/WalletLog.php
+++ b/application/admin/controller/shopro/user/WalletLog.php
@@ -65,12 +65,26 @@ class WalletLog extends Common
}
/**
- * 佣金记录
+ * 佣金与收益记录
*/
public function commission($id)
{
$list_rows = $this->request->param('list_rows', 10);
- $walletLogs = WalletLogModel::where('user_id', $id)->commission()->order('id', 'desc')->paginate($list_rows);
+ $rewardEvents = [
+ 'push3', 'maker_diff', 'maker_peer', 'maker_overtaken',
+ 'team_venue', 'team_county', 'venue_peer', 'county_peer',
+ 'venue_share', 'county_share', 'reward_income', 'transfer_by_commission', 'transfer_to_money'
+ ];
+
+ $walletLogs = WalletLogModel::where('user_id', $id)
+ ->where(function ($query) use ($rewardEvents) {
+ $query->where('type', 'commission')
+ ->whereOr(function ($sub) use ($rewardEvents) {
+ $sub->where('type', 'money')->whereIn('event', $rewardEvents);
+ });
+ })
+ ->order('id', 'desc')
+ ->paginate($list_rows);
// 多态关联 oper
$morphs = [
diff --git a/application/admin/model/shopro/app/ScoreSkuPrice.php b/application/admin/model/shopro/app/ScoreSkuPrice.php
index 5ab1697..fb7b452 100644
--- a/application/admin/model/shopro/app/ScoreSkuPrice.php
+++ b/application/admin/model/shopro/app/ScoreSkuPrice.php
@@ -55,11 +55,11 @@ class ScoreSkuPrice extends Common
$priceNum = floatval($price);
if ($priceNum > 0 && $score > 0) {
- return '¥' . $price . '兑换券 + ' . $score . '积分';
+ return $price . ' 个兑换券 + ' . $score . ' 积分';
} elseif ($priceNum > 0) {
- return '¥' . $price . ' 兑换券';
+ return $price . ' 个兑换券';
} elseif ($score > 0) {
- return $score . '积分';
+ return $score . ' 积分';
}
return '免费兑换';
diff --git a/application/admin/model/shopro/partner/Reward.php b/application/admin/model/shopro/partner/Reward.php
index ebd4430..4aedc81 100644
--- a/application/admin/model/shopro/partner/Reward.php
+++ b/application/admin/model/shopro/partner/Reward.php
@@ -22,6 +22,8 @@ class Reward extends Common
'skip_reason',
'skip_reason_text',
'formula',
+ 'wallet_type',
+ 'wallet_type_text',
];
public function statusList()
@@ -91,6 +93,41 @@ class Reward extends Common
return \addons\shopro\service\partner\IdentityService::formatMadow($value);
}
+ public function getWalletTypeAttr($value, $data)
+ {
+ $ext = $data['ext'] ?? [];
+ if (is_string($ext)) {
+ $ext = json_decode($ext, true) ?: [];
+ }
+ if (!empty($ext['wallet_type'])) {
+ return $ext['wallet_type'];
+ }
+ $rewardType = $data['reward_type'] ?? '';
+ if (in_array($rewardType, ['voucher_grant', 'voucher_release'], true)) {
+ return 'voucher';
+ }
+ if ($rewardType === 'madow_share') {
+ return 'madow';
+ }
+ if (intval($data['user_id'] ?? 0) === 0) {
+ return 'system';
+ }
+ return PartnerConfig::rewardWalletType();
+ }
+
+ public function getWalletTypeTextAttr($value, $data)
+ {
+ $type = $this->getWalletTypeAttr(null, $data);
+ $map = [
+ 'money' => '商城余额',
+ 'commission' => '佣金钱包',
+ 'voucher' => '消费补贴券',
+ 'madow' => '分红马豆',
+ 'system' => '平台留存',
+ ];
+ return $map[$type] ?? '商城余额';
+ }
+
public function buyer()
{
return $this->belongsTo(UserModel::class, 'buyer_id', 'id')->field('id, nickname, avatar, mobile');
diff --git a/application/admin/model/shopro/user/WalletLog.php b/application/admin/model/shopro/user/WalletLog.php
index 9823a5e..b2d58ab 100644
--- a/application/admin/model/shopro/user/WalletLog.php
+++ b/application/admin/model/shopro/user/WalletLog.php
@@ -45,16 +45,16 @@ class WalletLog extends Common
'transfer_by_commission' => '服务收益-转入到余额',
'reward_income' => '收益分成-入账',
'reward_back' => '收益分成-退还',
- 'push3' => '推荐开拓奖',
+ 'push3' => '推荐提成',
'maker_diff' => '创客进阶奖',
'maker_peer' => '创客协同奖',
'maker_overtaken' => '创客卓越奖',
- 'team_venue' => '酒道馆运营奖',
- 'team_county' => '区县统筹奖',
- 'venue_peer' => '酒道馆协同奖',
- 'county_peer' => '区县协同奖',
- 'venue_share' => '酒道馆加权奖励',
- 'county_share' => '区县加权奖励'
+ 'team_venue' => '酒道馆管理奖',
+ 'team_county' => '区县管理奖',
+ 'venue_peer' => '酒道馆平级奖',
+ 'county_peer' => '区县代平级奖',
+ 'venue_share' => '酒道馆加权奖',
+ 'county_share' => '区县加权奖'
],
'commission' => [
'withdraw' => '提现',
@@ -62,16 +62,16 @@ class WalletLog extends Common
'reward_income' => '服务收益-入账',
'reward_back' => '服务收益-退还',
'transfer_to_money' => '服务收益-转出到余额',
- 'push3' => '推荐开拓奖',
+ 'push3' => '推荐提成',
'maker_diff' => '创客进阶奖',
'maker_peer' => '创客协同奖',
'maker_overtaken' => '创客卓越奖',
- 'team_venue' => '酒道馆运营奖',
- 'team_county' => '区县统筹奖',
- 'venue_peer' => '酒道馆协同奖',
- 'county_peer' => '区县协同奖',
- 'venue_share' => '酒道馆加权奖励',
- 'county_share' => '区县加权奖励'
+ 'team_venue' => '酒道馆管理奖',
+ 'team_county' => '区县管理奖',
+ 'venue_peer' => '酒道馆平级奖',
+ 'county_peer' => '区县代平级奖',
+ 'venue_share' => '酒道馆加权奖',
+ 'county_share' => '区县加权奖'
]
];
@@ -111,6 +111,6 @@ class WalletLog extends Common
$type = $data['type'] ?? '';
$event = $data['event'] ?? '';
$eventMap = $this->getEventMap();
- return $eventMap[$type][$event] ?? ($eventMap['commission'][$event] ?? '');
+ return $eventMap[$type][$event] ?? ($eventMap['money'][$event] ?? ($eventMap['commission'][$event] ?? ($event ?: '')));
}
}
diff --git a/application/admin/view/shopro/app/score_shop/add.html b/application/admin/view/shopro/app/score_shop/add.html
index 96457c8..0ec3c69 100644
--- a/application/admin/view/shopro/app/score_shop/add.html
+++ b/application/admin/view/shopro/app/score_shop/add.html
@@ -62,7 +62,7 @@
type="info"
:closable="false"
class="mb-4"
- description="1. 纯兑换券兑换:【所需兑换券】填入所需面额(如 100),【所需积分】填 0。用户在小程序下单时将自动使用可用兑换券 1:1 全额抵扣(实付 0 元)。
2. 兑换券+积分组合:同时填写对应所需兑换券额度与所需积分。
3. 若用户可用兑换券不足,系统自动支持抵扣部分可用券额,剩余差额可使用微信现金支付补齐。"
+ description="1. 纯兑换券兑换:【所需兑换券】填入所需数量(如 100 个),【所需积分】填 0。用户在小程序下单时将自动使用可用兑换券 1:1 全额抵扣(实付 0 元)。
2. 兑换券+积分组合:同时填写对应所需兑换券数量与所需积分。
3. 若用户可用兑换券不足,系统自动支持抵扣部分可用券额,剩余差额可使用微信现金支付补齐。"
show-icon>
@@ -73,7 +73,7 @@
是否参与 |
商品原价 |
可兑换数量 |
-
所需兑换券(元) |
+
所需兑换券(个) |
所需积分 |
diff --git a/application/admin/view/shopro/app/score_shop/index.html b/application/admin/view/shopro/app/score_shop/index.html
index 72d1e7b..67df600 100644
--- a/application/admin/view/shopro/app/score_shop/index.html
+++ b/application/admin/view/shopro/app/score_shop/index.html
@@ -125,7 +125,7 @@
- ¥{{ scope.row.score_price.price }} 兑换券
+ {{ scope.row.score_price.price }} 个兑换券
+
diff --git a/application/admin/view/shopro/commission/agent/detail.html b/application/admin/view/shopro/commission/agent/detail.html
index de92569..5f367af 100644
--- a/application/admin/view/shopro/commission/agent/detail.html
+++ b/application/admin/view/shopro/commission/agent/detail.html
@@ -304,7 +304,7 @@
第 {{ state.data.direct_order_seq || 0 }} 单,{{ state.data.next_push3_text || '' }}
-
+
{{ state.data.push3_cash_income || '0.00' }} 元
@@ -357,8 +357,8 @@
{{ state.data.madow || 0 }}(约合 ¥{{ state.data.madow_value || '0.00' }})
- 总额 {{ state.data.voucher_total || 0 }} / 已释放 {{ state.data.voucher_released || 0 }} /
- 已使用 {{ state.data.voucher_used || 0 }} / 可用 {{ state.data.voucher_available || 0 }}
+ 总额 {{ state.data.voucher_total || 0 }} 个 / 已释放 {{ state.data.voucher_released || 0 }} 个 /
+ 已使用 {{ state.data.voucher_used || 0 }} 个 / 可用 {{ state.data.voucher_available || 0 }} 个
@@ -382,9 +382,10 @@
- {{ state.data.income_total_cash || state.data.partner_cash_income || '0.00' }}
+ {{ state.data.total_income || state.data.income_total_cash || '0.00' }}
元
+ 直入商城余额
资产信息
-
- 累计佣金:
+
+ 累计收益/佣金:
{{ state.data.total_income }}
元
-
- 待入账佣金:
-
- {{ state.data.pending_reward }}
+
+ 商城可用余额:
+
+ {{ state.data.user?.money || state.data.user_money || '0.00' }}
元
-
+
+ 历史未转佣金:
+
+ {{ state.data.user?.commission || state.data.user_commission || '0.00' }}
+ 元
+
+
+
消费金额:
{{ state.data.user?.total_consume || 0 }}
@@ -646,7 +654,7 @@
-
+
diff --git a/application/admin/view/shopro/commission/agent/index.html b/application/admin/view/shopro/commission/agent/index.html
index 0558895..4bafbc7 100644
--- a/application/admin/view/shopro/commission/agent/index.html
+++ b/application/admin/view/shopro/commission/agent/index.html
@@ -87,7 +87,7 @@
- {{ scope.row.voucher_available || 0 }}
+ {{ scope.row.voucher_available || 0 }} 个
{{ scope.row.total_income }}元
diff --git a/application/admin/view/shopro/commission/goods/edit.html b/application/admin/view/shopro/commission/goods/edit.html
index 4f7729e..2702151 100644
--- a/application/admin/view/shopro/commission/goods/edit.html
+++ b/application/admin/view/shopro/commission/goods/edit.html
@@ -227,7 +227,7 @@
@@ -239,7 +239,7 @@
- 推荐开拓奖(推三返一):
+ 推荐提成(推三返一):
循环返现 ¥{{ state.defaultPartnerRules.push3_amounts?.[0] || 398 }} / ¥{{ state.defaultPartnerRules.push3_amounts?.[1] || 400 }} / ¥{{ state.defaultPartnerRules.push3_amounts?.[2] || 600 }} 元
@@ -251,11 +251,11 @@
同级协同 {{ ((state.defaultPartnerRules.maker_peer_rate || 0.10) * 100).toFixed(0) }}%,超越关怀 {{ ((state.defaultPartnerRules.maker_overtaken_rate || 0.05) * 100).toFixed(0) }}%
- 渠道运营统筹奖:
- 酒道馆 ¥{{ state.defaultPartnerRules.venue_split || 50 }},区县统筹 ¥{{ state.defaultPartnerRules.county_full || 100 }}(有馆时各50)
+ 实体渠道管理奖:
+ 酒道馆 ¥{{ state.defaultPartnerRules.venue_split || 50 }},区县管理 ¥{{ state.defaultPartnerRules.county_full || 100 }}(有馆时各50)
- 馆/县平级协同奖:
+ 馆/县平级奖:
酒道馆 {{ ((state.defaultPartnerRules.venue_peer_rate || 0.10) * 100).toFixed(0) }}%,区县代理 {{ ((state.defaultPartnerRules.county_peer_rate || 0.10) * 100).toFixed(0) }}%
@@ -300,10 +300,10 @@
-
+
-
+
-
区县统筹全额(无馆时独享)
+
区县管理全额(无馆时独享)
元
@@ -410,13 +410,13 @@
-
区县代理平级协同比例
+
区县代理平级比例
%
diff --git a/application/admin/view/shopro/commission/reward/index.html b/application/admin/view/shopro/commission/reward/index.html
index e5199f4..411086b 100644
--- a/application/admin/view/shopro/commission/reward/index.html
+++ b/application/admin/view/shopro/commission/reward/index.html
@@ -6,6 +6,7 @@
历史佣金
+
注:当前收益分成已全额直入商城余额,历史未转佣金可批量转入余额
diff --git a/application/admin/view/shopro/config/index.html b/application/admin/view/shopro/config/index.html
index 92daf41..ec788ec 100644
--- a/application/admin/view/shopro/config/index.html
+++ b/application/admin/view/shopro/config/index.html
@@ -919,7 +919,7 @@
-
推荐开拓奖
+
推荐提成
@@ -941,6 +941,10 @@
+
+
+ 开启后,移动端“我的马豆”资产卡片将显示“马币市值 1 马豆 ≈ xxxx 元 生态增值”指示胶囊;关闭则隐藏该胶囊。
+
兑换券
diff --git a/application/admin/view/shopro/dashboard/index.html b/application/admin/view/shopro/dashboard/index.html
index 7b2477d..e18cb66 100644
--- a/application/admin/view/shopro/dashboard/index.html
+++ b/application/admin/view/shopro/dashboard/index.html
@@ -414,14 +414,14 @@
- ¥{{ state.voucher.total_voucher }}
+ {{ state.voucher.total_voucher }} 个
- 已释放:¥{{ state.voucher.released_voucher }}
- 待释放补贴券:¥{{ state.voucher.pending_voucher }}
+ 已释放:{{ state.voucher.released_voucher }} 个
+ 待释放补贴券:{{ state.voucher.pending_voucher }} 个
diff --git a/application/admin/view/shopro/order/order/detail.html b/application/admin/view/shopro/order/order/detail.html
index cd0bd63..99fdea1 100644
--- a/application/admin/view/shopro/order/order/detail.html
+++ b/application/admin/view/shopro/order/order/detail.html
@@ -887,9 +887,10 @@
本单现金合计 ¥{{ state.detail.partner_split.total_cash }}
+ 直入商城余额
- 兑换券释放额度 {{ state.detail.partner_split.total_voucher }}
+ 兑换券释放额度 {{ state.detail.partner_split.total_voucher }} 个
均分日 {{ state.detail.partner_split.settle_date }}
@@ -941,7 +942,7 @@
- {{ scope.row.amount_text || (scope.row.base_amount + ' 兑换券') }}
+ {{ scope.row.amount_text || (scope.row.base_amount + ' 个') }}
¥{{ scope.row.amount }}
@@ -955,6 +956,16 @@
+
+
+
+ {{ scope.row.wallet_type_text || (Number(scope.row.amount) > 0 ? '商城余额' : (Number(scope.row.madow) > 0 ? '分红马豆' : '平台留存')) }}
+
+
+
昨日 GMV ¥ {{ state.kpi.yesterday?.gmv || '0.00' }}
昨日现金总拨发 ¥ {{ state.kpi.yesterday_cash || '0.00' }}
昨日综合拨比 {{ state.kpi.yesterday_payout_ratio || '0%' }}
- 兑换券待履约负债 ¥ {{ state.kpi.voucher_debt || '0.00' }}
+ 兑换券待履约存量 {{ state.kpi.voucher_debt || '0.00' }} 个
活跃馆/县 {{ state.kpi.venue_active_count || 0 }} 馆 / {{ state.kpi.county_active_count || 0 }} 县
今日累计现金 ¥ {{ state.kpi.today_cash || '0.00' }}
暂停领取 {{ state.kpi.inactive_count || 0 }} 人
@@ -38,7 +38,7 @@
-
+
@@ -112,7 +112,7 @@
-
酒道馆加权奖励池
+
酒道馆加权奖池
奖池总额:¥
{{ previewDialog.result.venue?.pool }}
合格人数:
{{ previewDialog.result.venue?.user_count }} 人
@@ -123,7 +123,7 @@
-
区县代理加权奖励池
+
区县加权奖池
奖池总额:¥
{{ previewDialog.result.county?.pool }}
合格人数:
{{ previewDialog.result.county?.user_count }} 人
@@ -146,7 +146,7 @@
- 📅 兑换券释放预估:当日按计划释放兑换券总额约 ¥ {{ previewDialog.result.voucher?.release_estimate }}(以365天逐日释放)。
+ 📅 兑换券释放预估:当日按计划释放兑换券总额约 {{ previewDialog.result.voucher?.release_estimate }} 个(以365天逐日释放)。
diff --git a/application/admin/view/shopro/partner/reward/index.html b/application/admin/view/shopro/partner/reward/index.html
index 4e1bc9a..71f794d 100644
--- a/application/admin/view/shopro/partner/reward/index.html
+++ b/application/admin/view/shopro/partner/reward/index.html
@@ -52,6 +52,16 @@
{{ scope.row.amount }}
+
+
+
+ {{ scope.row.wallet_type_text || (Number(scope.row.amount) > 0 ? '商城余额' : (Number(scope.row.madow) > 0 ? '分红马豆' : '消费补贴')) }}
+
+
+
{{ scope.row.madow }}
@@ -143,7 +153,7 @@
1.5倍消费补贴履约计划:
- 总额 ¥ {{ orderTreeDialog.data.voucher_plan.total_amount }},已释放 ¥ {{ orderTreeDialog.data.voucher_plan.released_amount }} (已释放 {{ orderTreeDialog.data.voucher_plan.released_days }}/365天),状态:{{ orderTreeDialog.data.voucher_plan.status_text }}
+ 总额 {{ orderTreeDialog.data.voucher_plan.total_amount }} 个,已释放 {{ orderTreeDialog.data.voucher_plan.released_amount }} 个 (已释放 {{ orderTreeDialog.data.voucher_plan.released_days }}/365天),状态:{{ orderTreeDialog.data.voucher_plan.status_text }}
@@ -161,6 +171,16 @@
¥ {{ scope.row.amount }}
+
+
+
+ {{ scope.row.wallet_type_text || '商城余额' }}
+
+
+
diff --git a/application/admin/view/shopro/partner/voucher/index.html b/application/admin/view/shopro/partner/voucher/index.html
index 63d215a..aa63559 100644
--- a/application/admin/view/shopro/partner/voucher/index.html
+++ b/application/admin/view/shopro/partner/voucher/index.html
@@ -25,15 +25,15 @@
{{ scope.row.order?.order_sn || scope.row.order_id }}
-
-
+
+
{{ scope.row.released_days }}/{{ scope.row.total_days }}
-
-
-
-
+
+
+
+
@@ -52,7 +52,7 @@
计划 #{{ logs.plan?.id }} · 用户 {{ logs.plan?.user_id }} · 订单 {{ logs.plan?.order_id }}
-
+
diff --git a/application/admin/view/shopro/user/user/detail.html b/application/admin/view/shopro/user/user/detail.html
index 10d3bdc..fe4dfb4 100644
--- a/application/admin/view/shopro/user/user/detail.html
+++ b/application/admin/view/shopro/user/user/detail.html
@@ -186,7 +186,7 @@
充值余额 (元)
充值马豆 (颗)
充值积分 (分)
- 充值兑换券 (元)
+ 充值兑换券 (个)
@@ -418,18 +418,18 @@
二开核心资产中枢
-
+
-
可提现收益 (钱包现金)
+
收益中心累计收益 (元)
- ¥{{ state.detail.commission || '0.00' }}
+ ¥{{ state.detail.total_income || state.detail.income_total_cash || '0.00' }}
- 累计总现金: ¥{{ state.detail.income_total_cash || '0.00' }}
+ 当前商城余额: ¥{{ state.detail.money || '0.00' }}
- 进阶加权奖: ¥{{ state.detail.partner_exclusive_cash || '0.00' }}
+ 历史未转佣金: ¥{{ state.detail.commission || '0.00' }}
@@ -465,13 +465,13 @@
{/if}
- ¥{{ state.detail.voucher_available || '0.00' }}
+ {{ state.detail.voucher_available || '0.00' }} 个
- 计划总额: ¥{{ state.detail.voucher_total || '0.00' }}
+ 计划总额: {{ state.detail.voucher_total || '0.00' }} 个
- 已释放: ¥{{ state.detail.voucher_released || '0.00' }} / 已用: ¥{{ state.detail.voucher_used || '0.00' }}
+ 已释放: {{ state.detail.voucher_released || '0.00' }} 个 / 已用: {{ state.detail.voucher_used || '0.00' }} 个
@@ -517,7 +517,7 @@
-
+
@@ -541,6 +541,13 @@
+
+
+
+ {{ scope.row.wallet_type_text || (Number(scope.row.amount) > 0 ? '商城余额' : (Number(scope.row.madow) > 0 ? '分红马豆' : '消费补贴')) }}
+
+
+
@@ -582,12 +589,12 @@
- ¥{{ scope.row.total_fee }}
+ {{ scope.row.total_fee }} 个
- ¥{{ scope.row.day_amount }}
+ {{ scope.row.day_amount }} 个
@@ -598,10 +605,14 @@
-
-
+
- ¥{{ scope.row.voucher_available }}
+ {{ scope.row.released_amount }} 个
+
+
+
+
+ {{ scope.row.voucher_available }} 个
@@ -651,6 +662,13 @@
+
+
+
+ {{ scope.row.type == 'money' ? '商城余额' : '佣金钱包' }}
+
+
+
diff --git a/application/admin/view/shopro/user/user/index.html b/application/admin/view/shopro/user/user/index.html
index 642cf2d..a308757 100644
--- a/application/admin/view/shopro/user/user/index.html
+++ b/application/admin/view/shopro/user/user/index.html
@@ -151,17 +151,17 @@
- 可提现收益: ¥{{ scope.row.commission || '0.00' }}
- 马豆: {{ scope.row.madow || 0 }} (约 ¥{{ scope.row.madow_value || '0.00' }})
- 可用券: ¥{{ scope.row.voucher_available || '0.00' }}
+ 累计收益: ¥{{ scope.row.total_income || scope.row.income_total_cash || '0.00' }}
+ 商城余额: ¥{{ scope.row.money || '0.00' }}
+ 未转佣金: ¥{{ scope.row.commission || '0.00' }}
-
+
- 余额: ¥{{ scope.row.money || '0.00' }}
- 积分: {{ scope.row.score || 0 }}
- 消费: ¥{{ scope.row.total_consume || '0.00' }}
+ 分红马豆: {{ scope.row.madow || 0 }} 颗
+ 马豆估值: ¥{{ scope.row.madow_value || '0.00' }}
+ 可用券: {{ scope.row.voucher_available || '0.00' }} 个
diff --git a/application/admin/view/shopro/user/user/recharge.html b/application/admin/view/shopro/user/user/recharge.html
index 230ea75..5ae574d 100644
--- a/application/admin/view/shopro/user/user/recharge.html
+++ b/application/admin/view/shopro/user/user/recharge.html
@@ -41,7 +41,7 @@
可用兑换券
-
¥{{ state.user.voucher_available || '0.00' }}
+
{{ state.user.voucher_available || '0.00' }} 个
@@ -54,7 +54,7 @@
余额 (元)
马豆 (颗)
积分 (分)
- 兑换券 (元)
+ 兑换券 (个)
@@ -63,7 +63,7 @@
¥{{ currentBalance }}
{{ currentBalance }} 颗 (实时估值 ¥{{ state.user ? state.user.madow_value : '0.00' }})
{{ currentBalance }} 积分
- ¥{{ currentBalance }} (计划总额 ¥{{ state.user ? state.user.voucher_total : '0.00' }})
+ {{ currentBalance }} 个 (计划总额 {{ state.user ? state.user.voucher_total : '0.00' }} 个)
diff --git a/public/assets/js/backend/shopro/config.js b/public/assets/js/backend/shopro/config.js
index 7da57f3..32279fc 100644
--- a/public/assets/js/backend/shopro/config.js
+++ b/public/assets/js/backend/shopro/config.js
@@ -88,6 +88,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
if (!form.model.partner_maker_bonus) {
form.model.partner_maker_bonus = { 2: 20, 3: 40, 4: 60, 5: 80, 6: 100, 7: 120 }
}
+ form.model.partner_show_madow_rate = form.model.partner_show_madow_rate !== undefined && form.model.partner_show_madow_rate !== null ? Number(form.model.partner_show_madow_rate) : 1;
}
if (state.tabActive == 'shopro/config/chat') {
diff --git a/public/assets/js/backend/shopro/partner/reward.js b/public/assets/js/backend/shopro/partner/reward.js
index 3d13712..02db951 100644
--- a/public/assets/js/backend/shopro/partner/reward.js
+++ b/public/assets/js/backend/shopro/partner/reward.js
@@ -52,16 +52,16 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
type: 'tselect', label: '奖励类型', value: '',
options: {
data: [
- { label: '推荐开拓奖', value: 'push3' },
+ { label: '推荐提成', value: 'push3' },
{ label: '创客进阶奖', value: 'maker_diff' },
{ label: '创客协同奖', value: 'maker_peer' },
{ label: '创客卓越奖', value: 'maker_overtaken' },
- { label: '酒道馆运营奖', value: 'team_venue' },
- { label: '区县统筹奖', value: 'team_county' },
- { label: '酒道馆协同奖', value: 'venue_peer' },
- { label: '区县协同奖', value: 'county_peer' },
- { label: '酒道馆加权奖励', value: 'venue_share' },
- { label: '区县加权奖励', value: 'county_share' },
+ { label: '酒道馆管理奖', value: 'team_venue' },
+ { label: '区县管理奖', value: 'team_county' },
+ { label: '酒道馆平级奖', value: 'venue_peer' },
+ { label: '区县代平级奖', value: 'county_peer' },
+ { label: '酒道馆加权奖', value: 'venue_share' },
+ { label: '区县加权奖', value: 'county_share' },
{ label: '每日贡献马豆', value: 'madow_share' },
{ label: '消费补贴发放', value: 'voucher_grant' },
{ label: '消费补贴到账', value: 'voucher_release' },
diff --git a/scripts/repair_order_12_county_reward.php b/scripts/repair_order_12_county_reward.php
new file mode 100644
index 0000000..62e4730
--- /dev/null
+++ b/scripts/repair_order_12_county_reward.php
@@ -0,0 +1,134 @@
+find();
+ if (!$order) {
+ echo " [✗ 错误] 目标订单不存在!\n";
+ return 1;
+ }
+ echo " [✓] 目标订单: ID {$order->id}, 单号 {$order->order_sn}, 实付 {$order->pay_fee} 元, 买家 {$order->user_id}\n";
+
+ // 2. 校验受益人分销商身份
+ $agent = AgentModel::where('user_id', self::TARGET_USER_ID)->find();
+ if (!$agent || !$agent->is_county) {
+ echo " [✗ 错误] 用户 " . self::TARGET_USER_ID . " 不是有效的区县代理!\n";
+ return 1;
+ }
+ echo " [✓] 受益区县代理: User ID {$agent->user_id}, 等级 V{$agent->level}, 酒道馆: {$agent->is_venue}, 区县代: {$agent->is_county}, 现有累计收益: {$agent->total_income}\n";
+
+ // 3. 幂等性核查:检查是否已经发放过
+ $existingWalletLog = WalletLog::where('user_id', self::TARGET_USER_ID)
+ ->where('event', 'team_county')
+ ->where('ext', 'like', '%"order_id":' . self::TARGET_ORDER_ID . '%')
+ ->find();
+ if ($existingWalletLog) {
+ echo " [!] 警告:钱包日志中已存在该订单的 team_county 记录 (Log ID: {$existingWalletLog->id}, 金额: {$existingWalletLog->amount}),无需重复补发!\n";
+ return 0;
+ }
+
+ // 4. 检查跳过的流水记录 ID 631
+ $skipReward = RewardModel::where('order_id', self::TARGET_ORDER_ID)
+ ->where('reward_type', 'team_county')
+ ->find();
+ if (!$skipReward) {
+ echo " [✗ 错误] 未找到订单 {$order->id} 的 team_county 流水记录!\n";
+ return 1;
+ }
+ echo " [✓] 找到流水记录: ID {$skipReward->id}, 当前状态: {$skipReward->status}, 金额: {$skipReward->amount}, 原归属: {$skipReward->user_id}\n";
+
+ if ($skipReward->status == 1 && (float)$skipReward->amount > 0 && $skipReward->user_id == self::TARGET_USER_ID) {
+ echo " [!] 流水记录已经是发放状态,跳过执行。\n";
+ return 0;
+ }
+
+ $walletType = PartnerConfig::rewardWalletType();
+ echo " [✓] 结算钱包类型: {$walletType}, 计划补发金额: ¥ " . number_format(self::REWARD_AMOUNT, 2) . "\n";
+
+ if (!$isCommit) {
+ echo "\n>>> 预检通过!若要正式执行并入账,请使用参数 --commit 执行。\n";
+ return 0;
+ }
+
+ // 5. 开启事务执行补发
+ try {
+ Db::startTrans();
+
+ // 5.1 更新流水记录为正常入账
+ $skipReward->user_id = self::TARGET_USER_ID;
+ $skipReward->role = 'county';
+ $skipReward->amount = self::REWARD_AMOUNT;
+ $skipReward->base_amount = self::REWARD_AMOUNT;
+ $skipReward->status = 1;
+ $extData = is_array($skipReward->ext) ? $skipReward->ext : (json_decode($skipReward->ext ?: '[]', true) ?: []);
+ $extData['original_skip_reason'] = $extData['skip_reason'] ?? 'not_county';
+ $extData['formula'] = '区县统筹奖 50(馆兼县补发)';
+ $extData['repaired_at'] = time();
+ $extData['repair_note'] = '修复双重身份查找起始索引缺陷后补发';
+ $skipReward->ext = $extData;
+ $skipReward->save();
+
+ // 5.2 变动钱包
+ Wallet::change(self::TARGET_USER_ID, $walletType, self::REWARD_AMOUNT, 'team_county', [
+ 'partner_reward_id' => (string) $skipReward->id,
+ 'order_id' => self::TARGET_ORDER_ID,
+ ], '区县统筹奖补发');
+
+ // 5.3 累计分销商收益
+ AgentModel::where('user_id', self::TARGET_USER_ID)->setInc('total_income', self::REWARD_AMOUNT);
+
+ Db::commit();
+ echo " [✓ SUCCESS] 事务提交成功!订单 " . self::TARGET_ORDER_ID . " 区县统筹奖 50 元已入账 user_id: " . self::TARGET_USER_ID . "\n";
+
+ // 输出更新后的数据
+ $updatedAgent = AgentModel::where('user_id', self::TARGET_USER_ID)->find();
+ echo " [✓] 更新后分销商累计收益: {$updatedAgent->total_income}\n";
+ return 0;
+ } catch (\Exception $e) {
+ Db::rollback();
+ echo " [✗ FAIL] 执行异常回滚: " . $e->getMessage() . "\n";
+ return 1;
+ }
+ }
+}
+
+// CLI 执行入口
+$isCommit = in_array('--commit', $argv ?? []);
+$repair = new RepairOrder12CountyReward();
+exit($repair->run($isCommit));
diff --git a/scripts/transfer_all_commission_to_money.php b/scripts/transfer_all_commission_to_money.php
new file mode 100644
index 0000000..1d6d786
--- /dev/null
+++ b/scripts/transfer_all_commission_to_money.php
@@ -0,0 +1,103 @@
+ $val) {
+ $name = 'PHP_' . strtoupper($key);
+ if (is_array($val)) {
+ foreach ($val as $k => $v) {
+ $item = $name . '_' . strtoupper($k);
+ putenv("$item=$v");
+ }
+ } else {
+ putenv("$name=$val");
+ }
+ }
+}
+
+require_once ROOT_PATH . 'application/common.php';
+
+\think\Config::set(\think\Config::load(APP_PATH . 'config.php'));
+\think\Config::set(include APP_PATH . 'database.php', 'database');
+\think\Loader::addNamespace('app', APP_PATH);
+\think\Loader::addNamespace('addons', ROOT_PATH . 'addons/');
+
+require_once ROOT_PATH . 'addons/shopro/helper/helper.php';
+
+use addons\shopro\service\partner\WalletMigrationService;
+
+echo "========================================================\n";
+echo " 恒联数通 · 历史收益全量结转至商城余额独立脚本\n";
+echo "========================================================\n";
+
+$args = $argv ?? [];
+$dryRun = in_array('--dry-run', $args) || in_array('-d', $args);
+$force = in_array('--force', $args) || in_array('-f', $args);
+
+$service = new WalletMigrationService();
+$preview = $service->preview();
+
+echo "待处理会员总数: {$preview['total_users']} 人\n";
+echo "待结转收益总金额: ¥{$preview['total_commission']} 元\n";
+
+if ($preview['total_users'] === 0) {
+ echo "\n当前无任何待结转佣金余额,无需处理。\n";
+ exit(0);
+}
+
+echo "\n待结转会员抽样(前 10 名):\n";
+foreach (array_slice($preview['samples'], 0, 10) as $i => $s) {
+ $idx = $i + 1;
+ echo " [{$idx}] 会员ID: {$s['id']} | 手机: {$s['mobile']} | 当前佣金: ¥{$s['current_commission']} -> 商城余额: ¥{$s['current_money']} (预计转后: ¥{$s['target_money']})\n";
+}
+
+if ($dryRun) {
+ echo "\n【DRY-RUN 试算模式结束】未修改数据库。\n";
+ exit(0);
+}
+
+if (!$force) {
+ echo "\n【安全警告】即将对 {$preview['total_users']} 名会员执行结转,将 ¥{$preview['total_commission']} 元转入商城余额!\n";
+ echo "若确认执行,请增加参数 --force 或 -f,例如:\n";
+ echo "php scripts/transfer_all_commission_to_money.php --force\n";
+ exit(1);
+}
+
+echo "\n>>> 开始执行资金划转...\n";
+$startTime = microtime(true);
+
+$res = $service->transferAll(false, function ($current, $total, $userId, $amount, $err) {
+ if ($err) {
+ echo " [{$current}/{$total}] 会员 {$userId} 结转失败: {$err}\n";
+ } else {
+ echo " [{$current}/{$total}] 会员 {$userId} 成功结转 ¥" . number_format($amount, 2, '.', '') . " 元至商城余额\n";
+ }
+});
+
+$cost = round(microtime(true) - $startTime, 2);
+
+echo "\n========================================================\n";
+echo "结转完成!耗时: {$cost} 秒\n";
+echo "成功处理: {$res['success_count']} 人\n";
+echo "失败人数: {$res['fail_count']} 人\n";
+echo "累计结转资金: ¥{$res['transferred_amount']} 元\n";
+echo "========================================================\n";
+
+exit($res['fail_count'] > 0 ? 1 : 0);
diff --git a/tests/cashier/verify_cashier_mixed_pay_spec.js b/tests/cashier/verify_cashier_mixed_pay_spec.js
new file mode 100644
index 0000000..f5cc2d7
--- /dev/null
+++ b/tests/cashier/verify_cashier_mixed_pay_spec.js
@@ -0,0 +1,297 @@
+/**
+ * 收银台余额混合支付功能验证测试用例
+ * 覆盖:
+ * 1. 前端计算引擎:订单总额、剩余待付、已付金额、余额充足判断、混合支付触发条件、抵扣金额与第三方补足金额联动;
+ * 2. 后端 prepay 控制器契约与逻辑测试:
+ * - 正常场景:混合支付成功拉起第三方支付(余额抵扣 + 微信/支付宝补足)
+ * - 正常场景:全额余额支付
+ * - 业务失败场景:抵扣金额超出账户可用余额(防超扣)
+ * - 业务失败场景:纯余额支付时余额不足付清整单
+ * - 业务失败场景:第三方预支付异常触发冲正退回余额
+ * - 参数错误场景:非法支付通道或空参数
+ */
+
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+
+console.log('=== [1] 正在进行收银台前端核心计算引擎验证 ===');
+
+// 模拟前端计算逻辑
+function cashierComputeEngine({ orderType, payFee, remainPayFee, userMoney, useMixedPay, payment }) {
+ const payTotalFee = parseFloat(payFee) || 0;
+ const currentRemain = remainPayFee !== undefined ? parseFloat(remainPayFee) : payTotalFee;
+ const alreadyPaidFee = Math.max(0, payTotalFee - currentRemain);
+ const userBalance = parseFloat(userMoney) || 0;
+
+ const isBalanceEnough = userBalance >= currentRemain && currentRemain > 0;
+ const isMixedPaySupported = (orderType === 'goods' && userBalance > 0 && userBalance < currentRemain && currentRemain > 0);
+
+ const deductMoney = (isMixedPaySupported && useMixedPay) ? Math.min(userBalance, currentRemain) : 0;
+ const thirdPayFee = Math.max(0, currentRemain - deductMoney);
+
+ const currentDueFee = (isMixedPaySupported && useMixedPay && deductMoney > 0) ? thirdPayFee : currentRemain;
+ const currentAmountLabel = (isMixedPaySupported && useMixedPay && deductMoney > 0)
+ ? '在线待付金额'
+ : (alreadyPaidFee > 0 ? '剩余应付金额' : '应付金额');
+
+ return {
+ payTotalFee,
+ currentRemain,
+ alreadyPaidFee,
+ userBalance,
+ isBalanceEnough,
+ isMixedPaySupported,
+ deductMoney,
+ thirdPayFee,
+ currentDueFee,
+ currentAmountLabel,
+ };
+}
+
+// Case 1.1: 余额不足(待付 100,余额 30,商品订单),支持混合支付
+{
+ const res = cashierComputeEngine({
+ orderType: 'goods',
+ payFee: 100.0,
+ remainPayFee: 100.0,
+ userMoney: 30.0,
+ useMixedPay: true,
+ payment: 'wechat',
+ });
+ assert.strictEqual(res.isBalanceEnough, false, '余额不足整笔订单');
+ assert.strictEqual(res.isMixedPaySupported, true, '应触发支持混合支付');
+ assert.strictEqual(res.deductMoney, 30.0, '抵扣金额应为全部余额 30 元');
+ assert.strictEqual(res.thirdPayFee, 70.0, '第三方通道应补足 70 元');
+ assert.strictEqual(res.currentDueFee, 70.0, '顶部展示金额应为 70 元');
+ assert.strictEqual(res.currentAmountLabel, '在线待付金额');
+ console.log(' ✓ Case 1.1: 余额不足且有余额时,成功触发混合支付并正确计算抵扣(30)与需补足金额(70)');
+}
+
+// Case 1.2: 用户关闭混合支付开关
+{
+ const res = cashierComputeEngine({
+ orderType: 'goods',
+ payFee: 100.0,
+ remainPayFee: 100.0,
+ userMoney: 30.0,
+ useMixedPay: false,
+ payment: 'wechat',
+ });
+ assert.strictEqual(res.isMixedPaySupported, true, '依然满足混合支付硬件条件');
+ assert.strictEqual(res.deductMoney, 0, '用户手动关闭混合支付时抵扣为 0');
+ assert.strictEqual(res.thirdPayFee, 100.0, '全额付 100 元');
+ assert.strictEqual(res.currentDueFee, 100.0, '顶部展示金额应为 100 元');
+ console.log(' ✓ Case 1.2: 用户手动关闭混合支付时,平滑降级为全额在线支付');
+}
+
+// Case 1.3: 余额充足(待付 50,余额 100)
+{
+ const res = cashierComputeEngine({
+ orderType: 'goods',
+ payFee: 50.0,
+ remainPayFee: 50.0,
+ userMoney: 100.0,
+ useMixedPay: true,
+ payment: 'money',
+ });
+ assert.strictEqual(res.isBalanceEnough, true, '余额充足');
+ assert.strictEqual(res.isMixedPaySupported, false, '余额充足无需混合支付');
+ assert.strictEqual(res.currentDueFee, 50.0, '待付 50 元');
+ console.log(' ✓ Case 1.3: 余额充足时,单选余额支付正常运作,不误触发混合支付');
+}
+
+// Case 1.4: 充值订单(不能用余额混合支付)
+{
+ const res = cashierComputeEngine({
+ orderType: 'recharge',
+ payFee: 100.0,
+ remainPayFee: 100.0,
+ userMoney: 30.0,
+ useMixedPay: true,
+ payment: 'wechat',
+ });
+ assert.strictEqual(res.isMixedPaySupported, false, '充值订单绝不能使用余额混合支付');
+ console.log(' ✓ Case 1.4: 充值订单安全拦截混合支付,仅允许全额第三方充值');
+}
+
+// Case 1.5: 历史已付部分款项订单(订单总额 100,剩余待付 80,已付 20)
+{
+ const res = cashierComputeEngine({
+ orderType: 'goods',
+ payFee: 100.0,
+ remainPayFee: 80.0,
+ userMoney: 15.0,
+ useMixedPay: true,
+ payment: 'wechat',
+ });
+ assert.strictEqual(res.alreadyPaidFee, 20.0, '已付金额为 20 元');
+ assert.strictEqual(res.deductMoney, 15.0, '本次继续抵扣 15 元余额');
+ assert.strictEqual(res.thirdPayFee, 65.0, '第三方需付 65 元');
+ console.log(' ✓ Case 1.5: 部分已支付订单支持基于 remain_pay_fee 继续混合抵扣');
+}
+
+console.log('\n=== [2] 正在进行后端 prepay 逻辑与异常冲正安全契约验证 ===');
+
+// 模拟后端 prepay 核心校验与处理引擎
+function mockPrepayService({
+ order,
+ user,
+ payment,
+ money,
+ platform,
+ mockThirdPartyFail = false,
+}) {
+ // 基础校验
+ if (!['wechat', 'alipay', 'money', 'offline'].includes(payment)) {
+ return { code: 1002, message: '支付类型不能为空' };
+ }
+
+ const parsedMoney = parseFloat(money) > 0 ? Math.round(parseFloat(money) * 100) / 100 : 0;
+ let mixedPayRecord = null;
+
+ // 混合支付分支
+ if (parsedMoney > 0 && order.type === 'order') {
+ if (parsedMoney > user.money) {
+ return { code: 3001, message: '账户可用余额不足以进行本次抵扣' };
+ }
+
+ const deduct = Math.min(parsedMoney, order.remain_pay_fee);
+ // 扣除余额并生成支付记录
+ user.money = Math.round((user.money - deduct) * 100) / 100;
+ order.remain_pay_fee = Math.round((order.remain_pay_fee - deduct) * 100) / 100;
+ mixedPayRecord = {
+ pay_type: 'money',
+ pay_fee: deduct,
+ status: 'paid',
+ };
+
+ if (order.remain_pay_fee <= 0) {
+ order.status = 'paid';
+ return { code: 0, message: '订单支付成功', data: order };
+ }
+ }
+
+ // 纯余额支付分支
+ if (payment === 'money' && order.type === 'order') {
+ if (order.remain_pay_fee > user.money) {
+ return { code: 3001, message: '账户可用余额不足以付清该订单' };
+ }
+ user.money = Math.round((user.money - order.remain_pay_fee) * 100) / 100;
+ order.remain_pay_fee = 0;
+ order.status = 'paid';
+ return { code: 0, message: '订单支付成功', data: order };
+ }
+
+ // 第三方发起分支
+ if (mockThirdPartyFail) {
+ // 触发冲正回滚
+ if (mixedPayRecord) {
+ user.money = Math.round((user.money + mixedPayRecord.pay_fee) * 100) / 100;
+ order.remain_pay_fee = Math.round((order.remain_pay_fee + mixedPayRecord.pay_fee) * 100) / 100;
+ mixedPayRecord.status = 'refund';
+ }
+ return { code: 5001, message: '支付失败:第三方商户服务异常,已安全冲正' };
+ }
+
+ return {
+ code: 0,
+ message: '操作成功',
+ data: {
+ pay_data: {
+ out_trade_no: 'P' + Date.now(),
+ total_amount: order.remain_pay_fee,
+ channel: payment,
+ },
+ },
+ };
+}
+
+// Case 2.1: 正常混合支付拉起微信
+{
+ const order = { type: 'order', remain_pay_fee: 100.0, status: 'unpaid' };
+ const user = { money: 30.0 };
+ const res = mockPrepayService({
+ order,
+ user,
+ payment: 'wechat',
+ money: 30.0,
+ platform: 'WechatMiniProgram',
+ });
+ assert.strictEqual(res.code, 0);
+ assert.strictEqual(user.money, 0, '余额扣除 30 元');
+ assert.strictEqual(order.remain_pay_fee, 70.0, '订单剩余需付 70 元');
+ assert.strictEqual(res.data.pay_data.total_amount, 70.0, '微信预下单金额为 70 元');
+ console.log(' ✓ Case 2.1: 正常混合支付成功扣减余额 30 并下发 70 元微信预支付凭证');
+}
+
+// Case 2.2: 抵扣金额超过用户可用余额(防超扣)
+{
+ const order = { type: 'order', remain_pay_fee: 100.0, status: 'unpaid' };
+ const user = { money: 20.0 };
+ const res = mockPrepayService({
+ order,
+ user,
+ payment: 'wechat',
+ money: 50.0,
+ platform: 'WechatMiniProgram',
+ });
+ assert.strictEqual(res.code, 3001);
+ assert.strictEqual(res.message, '账户可用余额不足以进行本次抵扣');
+ assert.strictEqual(user.money, 20.0, '用户余额未被扣除');
+ assert.strictEqual(order.remain_pay_fee, 100.0, '订单未被修改');
+ console.log(' ✓ Case 2.2: 余额超扣请求被严格拦截,资金零风险');
+}
+
+// Case 2.3: 第三方预支付异常时触发即时冲正
+{
+ const order = { type: 'order', remain_pay_fee: 100.0, status: 'unpaid' };
+ const user = { money: 30.0 };
+ const res = mockPrepayService({
+ order,
+ user,
+ payment: 'wechat',
+ money: 30.0,
+ platform: 'WechatMiniProgram',
+ mockThirdPartyFail: true,
+ });
+ assert.strictEqual(res.code, 5001);
+ assert.strictEqual(user.money, 30.0, '用户余额已自动冲正退回');
+ assert.strictEqual(order.remain_pay_fee, 100.0, '订单待付金额已恢复为 100 元');
+ console.log(' ✓ Case 2.3: 第三方接口异常时,预扣余额被即时冲正退还,彻底杜绝单边扣款');
+}
+
+// Case 2.4: 纯余额支付时余额不足付清
+{
+ const order = { type: 'order', remain_pay_fee: 100.0, status: 'unpaid' };
+ const user = { money: 30.0 };
+ const res = mockPrepayService({
+ order,
+ user,
+ payment: 'money',
+ money: 0,
+ platform: 'WechatMiniProgram',
+ });
+ assert.strictEqual(res.code, 3001);
+ assert.strictEqual(res.message, '账户可用余额不足以付清该订单');
+ assert.strictEqual(user.money, 30.0, '余额未动');
+ console.log(' ✓ Case 2.4: 纯余额支付在余额不足时精准拦截并提示');
+}
+
+console.log('\n=== [3] 正在验证代码文件一致性 ===');
+const payPhpContent = fs.readFileSync(path.join(__dirname, '../../addons/shopro/controller/Pay.php'), 'utf8');
+assert(payPhpContent.includes('rollbackMixedMoney'), 'Pay.php 必须包含 rollbackMixedMoney 冲正方法');
+assert(payPhpContent.includes('账户可用余额不足以进行本次抵扣'), 'Pay.php 必须包含余额超扣防范校验');
+
+const payJsContent = fs.readFileSync(path.join(__dirname, '../../uniapp/sheep/platform/pay.js'), 'utf8');
+assert(payJsContent.includes('Number(this.options.money) > 0'), 'pay.js 必须支持 options.money 透传');
+
+const indexVueContent = fs.readFileSync(path.join(__dirname, '../../uniapp/pages/pay/index.vue'), 'utf8');
+assert(indexVueContent.includes('mixed-pay-card'), 'index.vue 必须包含 mixed-pay-card 混合支付卡片');
+assert(indexVueContent.includes('isMixedPaySupported'), 'index.vue 必须包含 isMixedPaySupported 计算属性');
+assert(indexVueContent.includes('useMixedPay'), 'index.vue 必须包含 useMixedPay 状态');
+console.log(' ✓ Case 3.1: 前后端所有修改文件核心方法与关键标识均存在且格式合法');
+
+console.log('\n=============================================');
+console.log('🎉 所有 10 项测试用例全部通过!收银台混合支付功能验证成功!');
+console.log('=============================================\n');
diff --git a/tests/partner/verify_active_order_source_spec.js b/tests/partner/verify_active_order_source_spec.js
new file mode 100644
index 0000000..d560470
--- /dev/null
+++ b/tests/partner/verify_active_order_source_spec.js
@@ -0,0 +1,176 @@
+/**
+ * 自购与直推订单双向保活(收益有效期)专项测试
+ * 覆盖场景:
+ * 1. 静态代码结构检查:PartnerRewardService::touchOrderTimes, ActiveService::hasQualifiedOrder & syncActiveTimes, Partner.php 监听调用
+ * 2. 逻辑模拟:自购订单激活本人保活
+ * 3. 逻辑模拟:直推订单激活直属上级保活(二级及以上间接团队隔离,不激活)
+ * 4. 逻辑模拟:防时间倒退(较旧订单不覆盖较新订单时间戳)
+ * 5. 状态转换模拟:直推订单使 30 天未自购的上级从 inactive 转为 active
+ */
+
+const fs = require('fs');
+const path = require('path');
+const assert = require('assert');
+
+let passCount = 0;
+let failCount = 0;
+
+function it(desc, fn) {
+ try {
+ fn();
+ console.log(` [PASS] ${desc}`);
+ passCount++;
+ } catch (err) {
+ console.error(` [FAIL] ${desc}`);
+ console.error(` Error: ${err.message}`);
+ failCount++;
+ }
+}
+
+console.log('========================================================');
+console.log(' 自购与直推订单双向保活(收益有效期)专项测试');
+console.log('========================================================\n');
+
+const rewardServicePath = path.resolve(__dirname, '../../addons/shopro/service/partner/PartnerRewardService.php');
+const activeServicePath = path.resolve(__dirname, '../../addons/shopro/service/partner/ActiveService.php');
+const listenerPath = path.resolve(__dirname, '../../addons/shopro/listener/Partner.php');
+
+const rewardServiceContent = fs.readFileSync(rewardServicePath, 'utf8');
+const activeServiceContent = fs.readFileSync(activeServicePath, 'utf8');
+const listenerContent = fs.readFileSync(listenerPath, 'utf8');
+
+// 1. 代码规范与方法存在性检查
+it('PartnerRewardService 包含 touchOrderTimes 方法且更新买家本人与一级推荐人', () => {
+ assert(rewardServiceContent.includes('function touchOrderTimes'), '缺少 touchOrderTimes 方法');
+ assert(rewardServiceContent.includes('parent_user_id'), 'touchOrderTimes 未查询 parent_user_id');
+ assert(rewardServiceContent.includes('$this->updateAgentOrderTime($parentUserId'), '未更新直推上级订单时间');
+});
+
+it('PartnerRewardService 防时间倒退逻辑(新时间戳不被旧时间戳覆盖)', () => {
+ assert(rewardServiceContent.includes('$timestamp >= $current'), 'updateAgentOrderTime 缺少防时间倒退判断');
+});
+
+it('ActiveService::hasQualifiedOrder 支持自购与直推订单双向判定', () => {
+ assert(activeServiceContent.includes('parent_user_id'), 'hasQualifiedOrder 未关联直推下级 parent_user_id');
+ assert(activeServiceContent.includes('$candidateUserIds = array_merge([$userId], $directChildIds)'), '缺少买家本人与直推下级候选集合并');
+});
+
+it('ActiveService 包含 syncActiveTimes 全量校准同步方法', () => {
+ assert(activeServiceContent.includes('function syncActiveTimes'), '缺少 syncActiveTimes 方法');
+ assert(activeServiceContent.includes('max($selfTime, $directTime)'), 'syncActiveTimes 缺少自购与直推最大值聚合');
+});
+
+it('Partner 监听器 orderPaidAfter 调用 touchOrderTimes 触发双向保活', () => {
+ assert(listenerContent.includes('touchOrderTimes'), 'Partner.php 未调用 touchOrderTimes');
+});
+
+// 2. 模拟订单与层级保活流转模拟器
+class MockAgentSystem {
+ constructor() {
+ this.users = {}; // id -> { id, parent_user_id }
+ this.agents = {}; // user_id -> { last_self_order_time, level }
+ }
+
+ addUser(id, parentUserId, level = 1) {
+ this.users[id] = { id, parent_user_id: parentUserId };
+ this.agents[id] = { last_self_order_time: 0, level };
+ }
+
+ updateAgentOrderTime(userId, timestamp) {
+ const agent = this.agents[userId];
+ if (agent) {
+ if (timestamp >= agent.last_self_order_time) {
+ agent.last_self_order_time = timestamp;
+ }
+ }
+ }
+
+ touchOrderTimes(buyerId, time) {
+ const timestamp = time || Math.floor(Date.now() / 1000);
+ // 1. 买家本人(自购)
+ this.updateAgentOrderTime(buyerId, timestamp);
+
+ // 2. 直推一级上级(直推)
+ const user = this.users[buyerId];
+ if (user && user.parent_user_id && user.parent_user_id !== buyerId) {
+ this.updateAgentOrderTime(user.parent_user_id, timestamp);
+ }
+ }
+
+ isActive(userId, nowSec, windowDays = 30) {
+ const agent = this.agents[userId];
+ if (!agent || !agent.last_self_order_time) return false;
+ const windowStart = nowSec - windowDays * 86400;
+ return agent.last_self_order_time >= windowStart;
+ }
+}
+
+// 场景一:买家 A 自购,激活本人 30 天保活
+it('场景一:买家 A 自购 -> 激活本人保活,状态为 active', () => {
+ const sys = new MockAgentSystem();
+ sys.addUser(101, 0); // 无上级
+ const orderTime = 1700000000;
+
+ sys.touchOrderTimes(101, orderTime);
+ assert.strictEqual(sys.agents[101].last_self_order_time, orderTime, '买家 A 订单时间未更新');
+ assert.strictEqual(sys.isActive(101, orderTime + 86400), true, '自购后第 1 天应为 active');
+ assert.strictEqual(sys.isActive(101, orderTime + 31 * 86400), false, '自购后第 31 天应为 inactive');
+});
+
+// 场景二:直推下级 B 下单 -> 激活直推上级 A 保活
+it('场景二:直推下级 B 下单 -> 同时激活直推上级 A,A 即使无自购也变为 active', () => {
+ const sys = new MockAgentSystem();
+ sys.addUser(100, 0); // 上级 A(从未自购)
+ sys.addUser(200, 100); // 直推下级 B(推荐人是 A)
+
+ const orderTime = 1700000000;
+ sys.touchOrderTimes(200, orderTime);
+
+ // B 本人自购更新
+ assert.strictEqual(sys.agents[200].last_self_order_time, orderTime, '买家 B 未更新');
+ // A 直推获得保活更新
+ assert.strictEqual(sys.agents[100].last_self_order_time, orderTime, '直推上级 A 未被直推订单激活');
+ assert.strictEqual(sys.isActive(100, orderTime + 10 * 86400), true, '直推上级 A 获得 30 天有效分红资格');
+});
+
+// 场景三:间接下级 C(二级)下单 -> 仅激活 B,不激活上上级 A(严格限定直推与自购)
+it('场景三:二级下级 C 下单 -> 仅激活一级直推人 B,上上级 A 不受影响(层级隔离)', () => {
+ const sys = new MockAgentSystem();
+ sys.addUser(10, 0); // 上上级 A
+ sys.addUser(20, 10); // 上级 B
+ sys.addUser(30, 20); // 下级 C(买家)
+
+ const orderTime = 1700000000;
+ sys.touchOrderTimes(30, orderTime);
+
+ assert.strictEqual(sys.agents[30].last_self_order_time, orderTime, '买家 C 应更新');
+ assert.strictEqual(sys.agents[20].last_self_order_time, orderTime, '直属上级 B 应更新');
+ assert.strictEqual(sys.agents[10].last_self_order_time, 0, '上上级 A 绝不应被二级订单更新(严格限定自购与直推)');
+});
+
+// 场景四:防时间倒退机制(乱序异步事件不能覆盖更新的时间戳)
+it('场景四:防时间倒退 -> 乱序处理时,较旧的订单不会覆盖较新的保活时间戳', () => {
+ const sys = new MockAgentSystem();
+ sys.addUser(1, 0);
+ sys.addUser(2, 1);
+
+ const tNew = 1700050000;
+ const tOld = 1700010000;
+
+ sys.touchOrderTimes(2, tNew);
+ assert.strictEqual(sys.agents[1].last_self_order_time, tNew);
+
+ // 此时来了一单较早的补偿结算
+ sys.touchOrderTimes(2, tOld);
+ assert.strictEqual(sys.agents[1].last_self_order_time, tNew, '较旧的时间戳被错误覆盖了');
+});
+
+console.log(`\n--------------------------------------------------------`);
+console.log(`测试结果统计: 通过 ${passCount} 项, 失败 ${failCount} 项`);
+console.log(`========================================================\n`);
+
+if (failCount > 0) {
+ process.exit(1);
+} else {
+ process.exit(0);
+}
diff --git a/tests/partner/verify_admin_views_alignment_spec.js b/tests/partner/verify_admin_views_alignment_spec.js
new file mode 100644
index 0000000..676261f
--- /dev/null
+++ b/tests/partner/verify_admin_views_alignment_spec.js
@@ -0,0 +1,153 @@
+const fs = require('fs');
+const path = require('path');
+
+console.log('========================================================');
+console.log(' 管理后台全模块查询与展示对齐防误导专项测试');
+console.log('========================================================\n');
+
+let passCount = 0;
+let failCount = 0;
+
+function assert(condition, message) {
+ if (condition) {
+ console.log(` [\x1b[32mPASS\x1b[0m] ${message}`);
+ passCount++;
+ } else {
+ console.error(` [\x1b[31mFAIL\x1b[0m] ${message}`);
+ failCount++;
+ }
+}
+
+const rootDir = path.resolve(__dirname, '../..');
+
+// --- 1. Reward.php 模型追加属性与解析验证 ---
+console.log('--- 1. Reward.php 模型追加属性与入账账户解析验证 ---');
+const rewardModelPath = path.join(rootDir, 'application/admin/model/shopro/partner/Reward.php');
+assert(fs.existsSync(rewardModelPath), 'application/admin/model/shopro/partner/Reward.php 文件存在');
+const rewardModelContent = fs.readFileSync(rewardModelPath, 'utf8');
+assert(rewardModelContent.includes("'wallet_type'"), 'Reward 模型 $append 追加 wallet_type');
+assert(rewardModelContent.includes("'wallet_type_text'"), 'Reward 模型 $append 追加 wallet_type_text');
+assert(rewardModelContent.includes('function getWalletTypeAttr'), '包含 getWalletTypeAttr 属性获取器');
+assert(rewardModelContent.includes('function getWalletTypeTextAttr'), '包含 getWalletTypeTextAttr 属性获取器');
+assert(rewardModelContent.includes("PartnerConfig::rewardWalletType()"), '现金分润缺省联动 PartnerConfig::rewardWalletType()');
+assert(rewardModelContent.includes("'商城余额'"), 'wallet_type_text 映射包含 商城余额');
+assert(rewardModelContent.includes("'佣金钱包'"), 'wallet_type_text 映射包含 佣金钱包');
+assert(rewardModelContent.includes("'分红马豆'"), 'wallet_type_text 映射包含 分红马豆');
+
+// --- 2. Order.php 订单分成构建与入账账户对齐验证 ---
+console.log('\n--- 2. Order.php 订单分成构建与入账账户对齐验证 ---');
+const orderControllerPath = path.join(rootDir, 'application/admin/controller/shopro/order/Order.php');
+assert(fs.existsSync(orderControllerPath), 'Order.php 文件存在');
+const orderControllerContent = fs.readFileSync(orderControllerPath, 'utf8');
+assert(orderControllerContent.includes("'wallet_type' => (string) ($row->wallet_type"), '即时奖励数据行注入 wallet_type');
+assert(orderControllerContent.includes("'wallet_type_text' => (string) ($row->wallet_type_text"), '即时奖励数据行注入 wallet_type_text');
+assert(orderControllerContent.includes("'wallet_type' => 'commission'"), '传统分销数据行标识为 commission');
+assert(orderControllerContent.includes("'wallet_type_text' => '佣金钱包'"), '传统分销数据行标识为 佣金钱包');
+assert(orderControllerContent.includes("'wallet_type' => 'system'"), '平台留存数据行标识为 system');
+assert(orderControllerContent.includes("'wallet_type_text' => '平台留存'"), '平台留存数据行标识为 平台留存');
+assert(orderControllerContent.includes("PartnerConfig::rewardWalletType()"), '日结均分奖池注入动态入账钱包');
+
+// --- 3. 订单详情视图 (order/detail.html) 验证 ---
+console.log('\n--- 3. 订单详情视图 (order/detail.html) 验证 ---');
+const orderDetailViewPath = path.join(rootDir, 'application/admin/view/shopro/order/order/detail.html');
+assert(fs.existsSync(orderDetailViewPath), 'order/detail.html 文件存在');
+const orderDetailViewContent = fs.readFileSync(orderDetailViewPath, 'utf8');
+assert(orderDetailViewContent.includes('直入商城余额'), '分成顶部横幅包含「直入商城余额」专属 Tag 提示');
+assert(orderDetailViewContent.includes('label="入账账户"'), '分成明细表格包含「入账账户」列');
+assert(orderDetailViewContent.includes('scope.row.wallet_type_text'), '入账账户列展示 scope.row.wallet_type_text');
+
+// --- 4. 收益明细控制器与视图 (partner/reward) 验证 ---
+console.log('\n--- 4. 收益明细控制器与视图 (partner/reward) 验证 ---');
+const partnerRewardControllerPath = path.join(rootDir, 'application/admin/controller/shopro/partner/Reward.php');
+assert(fs.existsSync(partnerRewardControllerPath), 'partner/Reward.php 文件存在');
+const partnerRewardControllerContent = fs.readFileSync(partnerRewardControllerPath, 'utf8');
+assert(partnerRewardControllerContent.includes("'wallet_type_text' => '入账账户'"), '导出表头新增「入账账户」列');
+assert(partnerRewardControllerContent.includes("'wallet_type' => $r->wallet_type"), 'orderTree 订单透视树注入 wallet_type');
+assert(partnerRewardControllerContent.includes("'wallet_type_text' => $r->wallet_type_text"), 'orderTree 订单透视树注入 wallet_type_text');
+
+const partnerRewardViewPath = path.join(rootDir, 'application/admin/view/shopro/partner/reward/index.html');
+assert(fs.existsSync(partnerRewardViewPath), 'partner/reward/index.html 文件存在');
+const partnerRewardViewContent = fs.readFileSync(partnerRewardViewPath, 'utf8');
+assert(partnerRewardViewContent.includes('label="入账账户"'), '收益流水主列表包含「入账账户」列');
+assert(partnerRewardViewContent.includes('orderTreeDialog'), '包含订单透视弹窗');
+assert(partnerRewardViewContent.match(/orderTreeDialog[\s\S]*label="入账账户"/), '订单透视明细树表格包含「入账账户」列');
+
+// --- 5. 会员详情与会员列表 (user/user) 验证 ---
+console.log('\n--- 5. 会员详情与会员列表 (user/user) 验证 ---');
+const userControllerPath = path.join(rootDir, 'application/admin/controller/shopro/user/User.php');
+assert(fs.existsSync(userControllerPath), 'User.php 文件存在');
+const userControllerContent = fs.readFileSync(userControllerPath, 'utf8');
+assert(userControllerContent.includes('alignedTotalIncome = max($agentTotalIncome, (float) $totalCash)'), 'User.php 累计收益取分销与二开收益中心最大值对齐');
+assert(userControllerContent.includes("$data['total_income'] = number_format($alignedTotalIncome"), 'User.php 输出格式化 total_income');
+assert(userControllerContent.includes("$data['income_total_cash'] = number_format($alignedTotalIncome"), 'User.php 输出格式化 income_total_cash');
+assert(userControllerContent.includes("$data['money'] = number_format"), 'User.php 输出格式化 money');
+assert(userControllerContent.includes("$data['commission'] = number_format"), 'User.php 输出格式化 commission');
+
+const userDetailViewPath = path.join(rootDir, 'application/admin/view/shopro/user/user/detail.html');
+assert(fs.existsSync(userDetailViewPath), 'user/detail.html 文件存在');
+const userDetailViewContent = fs.readFileSync(userDetailViewPath, 'utf8');
+assert(userDetailViewContent.includes('当前商城余额: ¥{{ state.detail.money'), '会员详情卡片1明确为「当前商城余额」');
+assert(userDetailViewContent.includes('历史未转佣金: ¥{{ state.detail.commission'), '会员详情卡片1明确为「历史未转佣金」');
+assert(userDetailViewContent.includes('state.detail.total_income || state.detail.income_total_cash'), '卡片1优先绑定统一对齐的 total_income');
+assert(userDetailViewContent.match(/log\.tabActive == 'partner_reward'[\s\S]*label="入账账户"/), 'Tab 1 收益中心流水包含「入账账户」列');
+
+const userIndexViewPath = path.join(rootDir, 'application/admin/view/shopro/user/user/index.html');
+assert(fs.existsSync(userIndexViewPath), 'user/index.html 文件存在');
+const userIndexViewContent = fs.readFileSync(userIndexViewPath, 'utf8');
+assert(!userIndexViewContent.includes('可提现收益:'), '会员列表已彻底移除误导性文案「可提现收益」');
+assert(userIndexViewContent.includes('累计收益:'), '会员列表展示「累计收益」');
+assert(userIndexViewContent.includes('商城余额: ¥{{ scope.row.money'), '会员列表展示「商城余额」');
+assert(userIndexViewContent.includes('未转佣金: ¥{{ scope.row.commission'), '会员列表展示「未转佣金」');
+
+// --- 6. 分销商详情与佣金明细 (commission) 验证 ---
+console.log('\n--- 6. 分销商详情与佣金明细 (commission) 验证 ---');
+const agentControllerPath = path.join(rootDir, 'application/admin/controller/shopro/commission/Agent.php');
+assert(fs.existsSync(agentControllerPath), 'commission/Agent.php 文件存在');
+const agentControllerContent = fs.readFileSync(agentControllerPath, 'utf8');
+assert(agentControllerContent.includes('alignedIncome = number_format(max(floatval($detail->total_income ?? 0), (float) $totalCash)'), 'Agent.php detail 累计收益严格对齐收益中心');
+assert(agentControllerContent.includes('$detail->user_money = number_format'), 'Agent.php 注入 user_money');
+assert(agentControllerContent.includes('$detail->user_commission = number_format'), 'Agent.php 注入 user_commission');
+
+const agentDetailViewPath = path.join(rootDir, 'application/admin/view/shopro/commission/agent/detail.html');
+assert(fs.existsSync(agentDetailViewPath), 'commission/agent/detail.html 文件存在');
+const agentDetailViewContent = fs.readFileSync(agentDetailViewPath, 'utf8');
+assert(agentDetailViewContent.includes('直入商城余额'), '分销商详情收益卡片带有「直入商城余额」Tag');
+assert(agentDetailViewContent.includes('商城可用余额:'), '分销商资产信息包含「商城可用余额」');
+assert(agentDetailViewContent.includes('历史未转佣金:'), '分销商资产信息包含「历史未转佣金」');
+assert(agentDetailViewContent.includes('label="历史分销佣金"'), 'Tab 3 明确标注为「历史分销佣金」');
+
+const commRewardViewPath = path.join(rootDir, 'application/admin/view/shopro/commission/reward/index.html');
+assert(fs.existsSync(commRewardViewPath), 'commission/reward/index.html 文件存在');
+const commRewardViewContent = fs.readFileSync(commRewardViewPath, 'utf8');
+assert(commRewardViewContent.includes('当前收益分成已全额直入商城余额'), '历史佣金明细顶部包含直入商城余额提示说明');
+
+const commRewardControllerPath = path.join(rootDir, 'application/admin/controller/shopro/commission/Reward.php');
+assert(fs.existsSync(commRewardControllerPath), 'commission/Reward.php 文件存在');
+const commRewardControllerContent = fs.readFileSync(commRewardControllerPath, 'utf8');
+assert(commRewardControllerContent.includes('历史分销佣金列表'), '导出文件名称更新为「历史分销佣金列表」');
+
+// --- 7. 底层分润服务 ext.wallet_type 与日志兜底防误导验证 ---
+console.log('\n--- 7. 底层分润服务 ext.wallet_type 与日志兜底防误导验证 ---');
+const partnerRewardServicePath = path.join(rootDir, 'addons/shopro/service/partner/PartnerRewardService.php');
+assert(fs.existsSync(partnerRewardServicePath), 'PartnerRewardService.php 文件存在');
+const partnerRewardServiceContent = fs.readFileSync(partnerRewardServicePath, 'utf8');
+assert(partnerRewardServiceContent.includes("$ext['wallet_type'] = $walletType;"), 'PartnerRewardService::account 显式持久化 ext.wallet_type 确保自说明性');
+
+const dailySettleServicePath = path.join(rootDir, 'addons/shopro/service/partner/DailySettleService.php');
+assert(fs.existsSync(dailySettleServicePath), 'DailySettleService.php 文件存在');
+const dailySettleServiceContent = fs.readFileSync(dailySettleServicePath, 'utf8');
+assert(dailySettleServiceContent.includes("'wallet_type' => $walletType,"), 'DailySettleService 均分分润显式持久化 ext.wallet_type');
+assert(dailySettleServiceContent.includes("'wallet_type' => 'system',"), 'DailySettleService 平台尾差与分母为0显式持久化 ext.wallet_type 为 system');
+
+const walletLogModelPath = path.join(rootDir, 'application/admin/model/shopro/user/WalletLog.php');
+assert(fs.existsSync(walletLogModelPath), 'WalletLog.php 文件存在');
+const walletLogModelContent = fs.readFileSync(walletLogModelPath, 'utf8');
+assert(walletLogModelContent.includes("$eventMap['money'][$event] ?? ($eventMap['commission'][$event] ?? ($event ?: ''))"), 'WalletLog::getEventTextAttr 包含跨钱包与事件编码三级非空兜底保护');
+
+console.log('\n========================================================');
+console.log(`测试完成!通过: ${passCount}, 失败: ${failCount}`);
+console.log('========================================================');
+
+if (failCount > 0) {
+ process.exit(1);
+}
diff --git a/tests/partner/verify_agent_upgrade_php8_spec.php b/tests/partner/verify_agent_upgrade_php8_spec.php
new file mode 100644
index 0000000..72cbf23
--- /dev/null
+++ b/tests/partner/verify_agent_upgrade_php8_spec.php
@@ -0,0 +1,87 @@
+testGoodsCommissionConfigNullSafe();
+ $this->testUserUpgradeLevelMatch();
+
+ echo "\n--------------------------------------------------------\n";
+ echo "测试执行结果: 通过 {$this->passed} 项, 失败 {$this->failed} 项\n";
+ echo "========================================================\n";
+ return $this->failed === 0 ? 0 : 1;
+ }
+
+ private function assert($condition, $testName, $detail = '')
+ {
+ if ($condition) {
+ $this->passed++;
+ echo " [✓ PASS] {$testName}\n";
+ } else {
+ $this->failed++;
+ echo " [✗ FAIL] {$testName} - {$detail}\n";
+ }
+ }
+
+ private function testGoodsCommissionConfigNullSafe()
+ {
+ try {
+ // 模拟一个 commission_config 为 null 的商品对象
+ $fakeGoods = new \stdClass();
+ $fakeGoods->commission_config = null;
+ $fakeGoods->self_rules = 0;
+ $fakeGoods->commission_rules = null;
+
+ $goodsService = new GoodsCommissionService($fakeGoods, 0);
+ $this->assert($goodsService !== null, 'GoodsCommissionService 空配置对象构造兼容测试');
+ } catch (\Throwable $e) {
+ $this->assert(false, 'GoodsCommissionService 空配置对象构造兼容测试', $e->getMessage());
+ }
+ }
+
+ private function testUserUpgradeLevelMatch()
+ {
+ try {
+ $levels = LevelModel::where('level', '>=', 2)->select();
+ $allStrict = true;
+ foreach ($levels as $l) {
+ if ($l->upgrade_type !== 1) {
+ $allStrict = false;
+ }
+ }
+ $this->assert($allStrict, '创客等级全部条件升级模式校验 (upgrade_type === 1)');
+ } catch (\Throwable $e) {
+ $this->assert(false, '创客等级全部条件升级模式校验', $e->getMessage());
+ }
+ }
+}
+
+if (PHP_SAPI === 'cli') {
+ $tester = new VerifyAgentUpgradePhp8Spec();
+ exit($tester->run());
+}
diff --git a/tests/partner/verify_api_profile_madow_online.php b/tests/partner/verify_api_profile_madow_online.php
new file mode 100644
index 0000000..85cd297
--- /dev/null
+++ b/tests/partner/verify_api_profile_madow_online.php
@@ -0,0 +1,25 @@
+direct(10);
+echo "User 10 logged in, id: " . $auth->id . PHP_EOL;
+
+$agent = \app\admin\model\shopro\commission\Agent::where('user_id', 10)->find();
+$res = \addons\shopro\service\partner\IdentityService::attachAgent($agent);
+echo "show_madow_rate in attachAgent: " . ($res['show_madow_rate'] ?? 'NOT_FOUND') . PHP_EOL;
+
+$prof = \addons\shopro\service\partner\IdentityService::attachProfile($agent);
+echo "show_madow_rate in attachProfile: " . ($prof['show_madow_rate'] ?? 'NOT_FOUND') . PHP_EOL;
+
+$rate = \addons\shopro\service\partner\PartnerConfig::showMadowRate();
+echo "PartnerConfig::showMadowRate: " . $rate . PHP_EOL;
+echo "ALL OK!" . PHP_EOL;
diff --git a/tests/partner/verify_income_criteria_alignment_spec.js b/tests/partner/verify_income_criteria_alignment_spec.js
new file mode 100644
index 0000000..1cd6d6b
--- /dev/null
+++ b/tests/partner/verify_income_criteria_alignment_spec.js
@@ -0,0 +1,194 @@
+/**
+ * 累计收益统计口径与用户商城余额统一专项测试
+ * 覆盖范围:
+ * 1. IdentityService attachAgent / attachProfile 累计收益口径统一与兜底
+ * 2. User profile 接口统一映射 balance = money 及累计收益补偿
+ * 3. partner/Index index / rewards 接口统一返回 total_income 与 month_income
+ * 4. WalletLog 收益统计扣除互转重复项,真实反映收益与扣减(防误导)
+ * 5. uniapp 视图端全面对齐:
+ * - rewards.vue 累计已结算收益卡片与本月统计
+ * - account-info.vue 累计收益与商城余额
+ * - commission.vue 累计总收益、周期收益与商城可用余额
+ * - withdraw.vue 商城可用余额提示与收益入账说明
+ * 6. 算法逻辑模拟:无重复统计、真实收益判定与资金守恒
+ */
+
+const fs = require('fs');
+const path = require('path');
+
+let passCount = 0;
+let failCount = 0;
+
+function assert(condition, message) {
+ if (condition) {
+ console.log(` \x1b[32m[PASS]\x1b[0m ${message}`);
+ passCount++;
+ } else {
+ console.error(` \x1b[31m[FAIL]\x1b[0m ${message}`);
+ failCount++;
+ }
+}
+
+console.log('========================================================');
+console.log(' 累计收益统计口径与用户商城余额统一专项测试');
+console.log('========================================================\n');
+
+// ----------------------------------------------------
+// 1. IdentityService 累计收益与资产聚合验证
+// ----------------------------------------------------
+console.log('--- 1. IdentityService 累计收益口径与资产聚合验证 ---');
+const identityServicePath = path.resolve(__dirname, '../../addons/shopro/service/partner/IdentityService.php');
+assert(fs.existsSync(identityServicePath), 'IdentityService.php 文件存在');
+if (fs.existsSync(identityServicePath)) {
+ const content = fs.readFileSync(identityServicePath, 'utf-8');
+ assert(content.includes('max($agentTotalIncome, floatval($rewardCash))'), 'attachAgent 取分销商总收益与收益中心奖励最大值对齐');
+ assert(content.includes("'total_income' => self::formatMoney($unifiedTotalIncome)"), 'attachAgent 返回格式化 total_income');
+ assert(content.includes("'income_total_cash' => self::formatMoney($unifiedTotalIncome)"), 'attachAgent 返回格式化 income_total_cash');
+ assert(content.includes("'total_income' => $full['total_income'] ?? '0.00'"), 'attachProfile 包含 total_income');
+ assert(content.includes("'income_total_cash' => $full['income_total_cash'] ?? '0.00'"), 'attachProfile 包含 income_total_cash');
+}
+
+// ----------------------------------------------------
+// 2. User profile 接口验证
+// ----------------------------------------------------
+console.log('\n--- 2. User.php 个人详情接口 balance 与收益对齐验证 ---');
+const userControllerPath = path.resolve(__dirname, '../../addons/shopro/controller/user/User.php');
+assert(fs.existsSync(userControllerPath), 'addons/shopro/controller/user/User.php 文件存在');
+if (fs.existsSync(userControllerPath)) {
+ const content = fs.readFileSync(userControllerPath, 'utf-8');
+ assert(content.includes("$data['balance'] = $user->money"), 'profile 明确将 balance 映射为商城余额 money');
+ assert(content.includes('$userRewardCash > 0'), '非分销商身份但有分成收益时,执行累计收益兜底统计');
+}
+
+// ----------------------------------------------------
+// 3. partner/Index 接口对齐验证
+// ----------------------------------------------------
+console.log('\n--- 3. partner/Index 概览与明细接口 total_income 对齐验证 ---');
+const partnerIndexPath = path.resolve(__dirname, '../../addons/shopro/controller/partner/Index.php');
+assert(fs.existsSync(partnerIndexPath), 'partner/Index.php 文件存在');
+if (fs.existsSync(partnerIndexPath)) {
+ const content = fs.readFileSync(partnerIndexPath, 'utf-8');
+ assert(content.includes("'total_income' => $totalIncome"), 'index 概览接口返回 total_income');
+ assert(content.includes("'month_income' => IdentityService::formatMoney($monthIncome)"), 'index 概览接口返回本月收益 month_income');
+ assert(content.includes("'total_income' => $totalIncome"), 'rewards 明细接口返回 total_income');
+}
+
+// ----------------------------------------------------
+// 4. WalletLog 收益统计防误导口径验证
+// ----------------------------------------------------
+console.log('\n--- 4. WalletLog 收入与支出统计口径防误导验证 ---');
+const walletLogPath = path.resolve(__dirname, '../../addons/shopro/controller/user/WalletLog.php');
+assert(fs.existsSync(walletLogPath), 'WalletLog.php 文件存在');
+if (fs.existsSync(walletLogPath)) {
+ const content = fs.readFileSync(walletLogPath, 'utf-8');
+ assert(content.includes("whereNotIn('event', ['transfer_by_commission'])"), 'commission 收入统计排除了转余额入账 transfer_by_commission (防重复计算)');
+ assert(content.includes("whereNotIn('event', ['transfer_to_money', 'commission_transfer'])"), 'commission 支出统计排除了结转至余额 transfer_to_money (防伪支出误导)');
+ assert(content.includes('$actualRewardEvents'), '定义了真实业务奖励事件清单 actualRewardEvents');
+}
+
+// ----------------------------------------------------
+// 5. uniapp 页面端到端对齐验证
+// ----------------------------------------------------
+console.log('\n--- 5. uniapp 视图端全面对齐验证 ---');
+
+// 5.1 rewards.vue
+const rewardsVuePath = path.resolve(__dirname, '../../uniapp/pages/commission/rewards.vue');
+assert(fs.existsSync(rewardsVuePath), 'rewards.vue 文件存在');
+if (fs.existsSync(rewardsVuePath)) {
+ const content = fs.readFileSync(rewardsVuePath, 'utf-8');
+ assert(content.includes('累计已结算收益(元)'), 'rewards.vue 卡片主标题变更为「累计已结算收益(元)」');
+ assert(content.includes('displayTotalIncome'), 'rewards.vue 卡片主金额使用 displayTotalIncome 绑定');
+ assert(content.includes('本月已结算: ¥{{ state.overview.month_income'), '副指标清晰标注本月已结算');
+ assert(content.includes('state.overview.total_income'), 'overview 状态机与接口对齐包含 total_income');
+}
+
+// 5.2 account-info.vue
+const accountInfoVuePath = path.resolve(__dirname, '../../uniapp/pages/commission/components/account-info.vue');
+assert(fs.existsSync(accountInfoVuePath), 'account-info.vue 文件存在');
+if (fs.existsSync(accountInfoVuePath)) {
+ const content = fs.readFileSync(accountInfoVuePath, 'utf-8');
+ assert(content.includes("agentInfo?.total_income || userInfo?.total_income"), '累计收益具备 agentInfo 与 userInfo 双重对齐兜底');
+ assert(content.includes("userInfo?.money || '0.00'"), '商城余额绑定 userInfo.money');
+}
+
+// 5.3 commission.vue
+const commissionVuePath = path.resolve(__dirname, '../../uniapp/pages/user/wallet/commission.vue');
+assert(fs.existsSync(commissionVuePath), 'commission.vue 文件存在');
+if (fs.existsSync(commissionVuePath)) {
+ const content = fs.readFileSync(commissionVuePath, 'utf-8');
+ assert(content.includes("agentInfo.total_income || userInfo.total_income"), '累计总收益具备双重兜底');
+ assert(content.includes('周期收益¥'), '日期区间统计描述对齐为「周期收益」,明确收益口径');
+}
+
+// 5.4 withdraw.vue
+const withdrawVuePath = path.resolve(__dirname, '../../uniapp/pages/pay/withdraw.vue');
+assert(fs.existsSync(withdrawVuePath), 'withdraw.vue 文件存在');
+if (fs.existsSync(withdrawVuePath)) {
+ const content = fs.readFileSync(withdrawVuePath, 'utf-8');
+ assert(content.includes('商城可用余额:¥{{ userInfo.money'), 'withdraw.vue 同步展示商城可用余额');
+ assert(content.includes('收益分成已全额直接入账商城余额'), 'withdraw.vue 显著提示收益已全额直接入账商城余额');
+}
+
+// ----------------------------------------------------
+// 6. 统计口径算法精准性断言
+// ----------------------------------------------------
+console.log('\n--- 6. 统计口径算法精准性模拟断言 ---');
+
+const actualRewardEvents = [
+ 'push3', 'maker_diff', 'maker_peer', 'maker_overtaken',
+ 'team_venue', 'team_county', 'venue_peer', 'county_peer',
+ 'venue_share', 'county_share', 'reward_income'
+];
+
+// 模拟典型用户流水:
+// 1) 历史获得推三返一:+398 (commission)
+// 2) 结转转出至余额:-398 (commission, event=transfer_to_money)
+// 3) 结转入账至余额:+398 (money, event=transfer_by_commission)
+// 4) 产生新订单直入余额奖:+50 (money, event=team_venue)
+// 5) 新订单发生售后退回:-50 (money, event=team_venue_back)
+const userTestLogs = [
+ { type: 'commission', event: 'push3', amount: 398.00 },
+ { type: 'commission', event: 'transfer_to_money', amount: -398.00 },
+ { type: 'money', event: 'transfer_by_commission', amount: 398.00 },
+ { type: 'money', event: 'team_venue', amount: 50.00 },
+ { type: 'money', event: 'team_venue_back', amount: -50.00 },
+];
+
+function calculateAlignedIncomeAndExpense(logs) {
+ let income = 0;
+ let expense = 0;
+
+ for (const log of logs) {
+ if (log.amount > 0) {
+ // 排除互转入账,只统计真正创造的收益
+ if (log.type === 'money' && actualRewardEvents.includes(log.event)) {
+ income += log.amount;
+ } else if (log.type === 'commission' && log.event !== 'transfer_by_commission') {
+ income += log.amount;
+ }
+ } else if (log.amount < 0) {
+ // 排除结转内部转出,只统计真实的业务扣减与退款
+ if (!['transfer_to_money', 'commission_transfer'].includes(log.event)) {
+ expense += log.amount;
+ }
+ }
+ }
+ return { income, expense };
+}
+
+const calculated = calculateAlignedIncomeAndExpense(userTestLogs);
+assert(calculated.income === (398.00 + 50.00), `总收益收入精准为 ¥${calculated.income} (398+50=448,未被 398 结转重复统计为 846)`);
+assert(calculated.expense === -50.00, `总支出精准为 ¥${calculated.expense} (仅扣款-50,未被 -398 结转误统计为 -448)`);
+
+const netGain = calculated.income + calculated.expense;
+assert(netGain === 398.00, `实际净收益精准守恒为 ¥${netGain}`);
+
+console.log('\n========================================================');
+console.log(`测试完成!通过: \x1b[32m${passCount}\x1b[0m, 失败: \x1b[31m${failCount}\x1b[0m`);
+console.log('========================================================');
+
+if (failCount > 0) {
+ process.exit(1);
+} else {
+ process.exit(0);
+}
diff --git a/tests/partner/verify_madow_switch_spec.js b/tests/partner/verify_madow_switch_spec.js
new file mode 100644
index 0000000..9a65282
--- /dev/null
+++ b/tests/partner/verify_madow_switch_spec.js
@@ -0,0 +1,163 @@
+/**
+ * tests/partner/verify_madow_switch_spec.js
+ *
+ * 验证“我的马豆”页面马币市值指示胶囊后台开关:
+ * 1. 后端配置读取与默认值向后兼容逻辑验证
+ * 2. 移动端前台显隐条件判定逻辑(开启、关闭、未配置)验证
+ * 3. 接口数据契约与统一返回结构校验
+ * 4. 关键文件代码变更完整性静态检查
+ */
+
+const fs = require('fs');
+const path = require('path');
+const assert = require('assert');
+
+console.log('====================================================');
+console.log(' 开始验证:“我的马豆”马币市值指示胶囊后台显隐开关');
+console.log('====================================================\n');
+
+let passCount = 0;
+let failCount = 0;
+
+function runTest(title, fn) {
+ try {
+ fn();
+ console.log(`[PASS] ${title}`);
+ passCount++;
+ } catch (err) {
+ console.error(`[FAIL] ${title}`);
+ console.error(` 错误原因: ${err.message}`);
+ failCount++;
+ }
+}
+
+// ----------------------------------------------------
+// 1. 测试后端 showMadowRate 配置降级与逻辑映射
+// ----------------------------------------------------
+function mockShowMadowRate(configMap) {
+ const get = (key, defaultVal) => (configMap[key] !== undefined ? configMap[key] : defaultVal);
+ const show = get('partner_show_madow_rate', 1);
+ if (show === null || show === '') {
+ return 1;
+ }
+ return parseInt(show, 10) ? 1 : 0;
+}
+
+runTest('配置未设置或为 null/空字符时,向后兼容默认返回 1 (显示)', () => {
+ assert.strictEqual(mockShowMadowRate({}), 1);
+ assert.strictEqual(mockShowMadowRate({ partner_show_madow_rate: null }), 1);
+ assert.strictEqual(mockShowMadowRate({ partner_show_madow_rate: '' }), 1);
+});
+
+runTest('配置显式设置为 0 时,返回 0 (隐藏)', () => {
+ assert.strictEqual(mockShowMadowRate({ partner_show_madow_rate: 0 }), 0);
+ assert.strictEqual(mockShowMadowRate({ partner_show_madow_rate: '0' }), 0);
+});
+
+runTest('配置显式设置为 1 时,返回 1 (显示)', () => {
+ assert.strictEqual(mockShowMadowRate({ partner_show_madow_rate: 1 }), 1);
+ assert.strictEqual(mockShowMadowRate({ partner_show_madow_rate: '1' }), 1);
+});
+
+// ----------------------------------------------------
+// 2. 测试移动端 madow.vue 显隐条件判定
+// ----------------------------------------------------
+function evalRateCapsuleVisible(showMadowRate) {
+ return showMadowRate !== false && showMadowRate !== 0 && showMadowRate !== '0';
+}
+
+runTest('移动端当 show_madow_rate 为 1 / "1" / true 时判定展示', () => {
+ assert.strictEqual(evalRateCapsuleVisible(1), true);
+ assert.strictEqual(evalRateCapsuleVisible('1'), true);
+ assert.strictEqual(evalRateCapsuleVisible(true), true);
+});
+
+runTest('移动端当 show_madow_rate 为 0 / "0" / false 时判定隐藏', () => {
+ assert.strictEqual(evalRateCapsuleVisible(0), false);
+ assert.strictEqual(evalRateCapsuleVisible('0'), false);
+ assert.strictEqual(evalRateCapsuleVisible(false), false);
+});
+
+runTest('移动端当 show_madow_rate 为 undefined / null 时保底默认展示', () => {
+ assert.strictEqual(evalRateCapsuleVisible(undefined), true);
+ assert.strictEqual(evalRateCapsuleVisible(null), true);
+});
+
+// ----------------------------------------------------
+// 3. 接口数据返回结构校验
+// ----------------------------------------------------
+runTest('madow 接口返回 payload 需包含 show_madow_rate 字段', () => {
+ const mockApiResponse = {
+ code: 1,
+ msg: 'ok',
+ data: {
+ madow: '100.0000',
+ madow_rate: '0.000007865',
+ madow_value: '0.00',
+ show_madow_rate: 1,
+ list: []
+ }
+ };
+ assert.strictEqual(mockApiResponse.code, 1);
+ assert.strictEqual(typeof mockApiResponse.data.show_madow_rate, 'number');
+ assert.strictEqual(mockApiResponse.data.show_madow_rate, 1);
+ assert.strictEqual(evalRateCapsuleVisible(mockApiResponse.data.show_madow_rate), true);
+});
+
+// ----------------------------------------------------
+// 4. 源码静态变更完整性检查
+// ----------------------------------------------------
+const rootDir = path.resolve(__dirname, '../../');
+
+runTest('检查 application/admin/controller/shopro/Config.php 包含 partner_show_madow_rate', () => {
+ const file = path.join(rootDir, 'application/admin/controller/shopro/Config.php');
+ const content = fs.readFileSync(file, 'utf-8');
+ assert.ok(content.includes('partner_show_madow_rate'), 'Config.php 必须包含 partner_show_madow_rate 字段');
+ assert.ok(content.includes("ShoproConfig::where('code', 'shop.commission.partner_show_madow_rate')"), 'Config.php 必须包含自愈检查逻辑');
+});
+
+runTest('检查 application/admin/view/shopro/config/index.html 包含开关组件', () => {
+ const file = path.join(rootDir, 'application/admin/view/shopro/config/index.html');
+ const content = fs.readFileSync(file, 'utf-8');
+ assert.ok(content.includes('form.model.partner_show_madow_rate'), 'index.html 必须绑定 partner_show_madow_rate');
+ assert.ok(content.includes('显示马币市值'), 'index.html 必须包含表单中文标签');
+});
+
+runTest('检查 public/assets/js/backend/shopro/config.js 包含初始化', () => {
+ const file = path.join(rootDir, 'public/assets/js/backend/shopro/config.js');
+ const content = fs.readFileSync(file, 'utf-8');
+ assert.ok(content.includes('form.model.partner_show_madow_rate'), 'config.js 必须包含 partner_show_madow_rate 初始化');
+});
+
+runTest('检查 addons/shopro/service/partner/PartnerConfig.php 包含 showMadowRate 方法', () => {
+ const file = path.join(rootDir, 'addons/shopro/service/partner/PartnerConfig.php');
+ const content = fs.readFileSync(file, 'utf-8');
+ assert.ok(content.includes('public static function showMadowRate()'), 'PartnerConfig 必须包含 showMadowRate 方法');
+});
+
+runTest('检查 addons/shopro/controller/partner/Index.php 包含 show_madow_rate 输出', () => {
+ const file = path.join(rootDir, 'addons/shopro/controller/partner/Index.php');
+ const content = fs.readFileSync(file, 'utf-8');
+ assert.ok(content.includes("'show_madow_rate' => PartnerConfig::showMadowRate()"), 'Index.php 必须在 madow 中返回 show_madow_rate');
+});
+
+runTest('检查 uniapp/pages/commission/madow.vue 包含条件渲染与状态绑定', () => {
+ const file = path.join(rootDir, 'uniapp/pages/commission/madow.vue');
+ const content = fs.readFileSync(file, 'utf-8');
+ assert.ok(content.includes('state.info.show_madow_rate'), 'madow.vue 必须使用 state.info.show_madow_rate');
+ assert.ok(content.includes('v-if="state.info.show_madow_rate'), 'madow.vue 的 rate-capsule 必须绑定 v-if');
+});
+
+runTest('检查 addons/shopro/upgrade/partner_v2.sql 包含 SQL 种子', () => {
+ const file = path.join(rootDir, 'addons/shopro/upgrade/partner_v2.sql');
+ const content = fs.readFileSync(file, 'utf-8');
+ assert.ok(content.includes('shop.commission.partner_show_madow_rate'), 'partner_v2.sql 必须包含配置初始行');
+});
+
+console.log('\n====================================================');
+console.log(`测试完成: 成功 ${passCount} 项, 失败 ${failCount} 项`);
+console.log('====================================================');
+
+if (failCount > 0) {
+ process.exit(1);
+}
diff --git a/tests/partner/verify_partner_reward_to_money_spec.js b/tests/partner/verify_partner_reward_to_money_spec.js
new file mode 100644
index 0000000..d6b62f5
--- /dev/null
+++ b/tests/partner/verify_partner_reward_to_money_spec.js
@@ -0,0 +1,191 @@
+/**
+ * 收益分成直接加到商城余额专项验证测试
+ * 覆盖范围:
+ * 1. PartnerConfig (rewardWalletType 默认 money)
+ * 2. PartnerRewardService (account 走 money 入账,refund 智能回滚)
+ * 3. DailySettleService (cashShare 走 money 入账,clearDaily 智能回滚)
+ * 4. 后台 Reward 控制器 (add 走 money 入账,back 智能回滚)
+ * 5. WalletLog 模型 (eventMap['money'] 补全收益事件,getEventTextAttr 动态解析)
+ * 6. 用户端组件 account-info.vue (展示商城余额并跳转 /pages/user/wallet/money)
+ * 7. 业务逻辑与回滚算法行为模拟断言
+ */
+
+const fs = require('fs');
+const path = require('path');
+
+let passCount = 0;
+let failCount = 0;
+
+function assert(condition, message) {
+ if (condition) {
+ console.log(` \x1b[32m[PASS]\x1b[0m ${message}`);
+ passCount++;
+ } else {
+ console.error(` \x1b[31m[FAIL]\x1b[0m ${message}`);
+ failCount++;
+ }
+}
+
+console.log('========================================================');
+console.log(' 收益分成直接加到商城余额专项测试');
+console.log('========================================================\n');
+
+// ----------------------------------------------------
+// 1. PartnerConfig.php 验证
+// ----------------------------------------------------
+console.log('--- 1. PartnerConfig.php 规范验证 ---');
+const partnerConfigPath = path.resolve(__dirname, '../../addons/shopro/service/partner/PartnerConfig.php');
+assert(fs.existsSync(partnerConfigPath), 'PartnerConfig.php 文件存在');
+if (fs.existsSync(partnerConfigPath)) {
+ const content = fs.readFileSync(partnerConfigPath, 'utf-8');
+ assert(content.includes('public static function rewardWalletType()'), '包含 rewardWalletType() 收益入账钱包解析方法');
+ assert(content.includes("self::get('partner_reward_wallet', 'money')"), '默认收益入账钱包缺省为 money (商城余额)');
+ assert(content.includes("in_array($type, ['money', 'commission'])"), '安全限定钱包类型在合法区间');
+}
+
+// ----------------------------------------------------
+// 2. PartnerRewardService.php 验证
+// ----------------------------------------------------
+console.log('\n--- 2. PartnerRewardService.php 调度验证 ---');
+const partnerRewardServicePath = path.resolve(__dirname, '../../addons/shopro/service/partner/PartnerRewardService.php');
+assert(fs.existsSync(partnerRewardServicePath), 'PartnerRewardService.php 文件存在');
+if (fs.existsSync(partnerRewardServicePath)) {
+ const content = fs.readFileSync(partnerRewardServicePath, 'utf-8');
+ assert(content.includes('$walletType = PartnerConfig::rewardWalletType();'), 'account() 动态获取收益入账钱包类型');
+ assert(content.includes('Wallet::change($userId, $walletType, $amount, $type,'), 'account() 使用动态钱包入账(直接入商城余额)');
+ assert(content.includes("AgentModel::where('user_id', $userId)->setInc('total_income', $amount);"), '保留分销商累计收益 total_income 累加');
+ assert(content.includes('$walletLog = \\app\\admin\\model\\shopro\\user\\WalletLog::where('), 'refund() 退款时查询原入账钱包记录');
+ assert(content.includes("$targetWallet = ($walletLog && $walletLog->type === 'commission') ? 'commission' : PartnerConfig::rewardWalletType();"), 'refund() 智能根据原入账类型回滚对应钱包');
+ assert(content.includes("Wallet::change($reward->user_id, $targetWallet, -$reward->amount, 'reward_back'"), 'refund() 精准回滚目标钱包');
+}
+
+// ----------------------------------------------------
+// 3. DailySettleService.php 验证
+// ----------------------------------------------------
+console.log('\n--- 3. DailySettleService.php 日结加权分润验证 ---');
+const dailySettleServicePath = path.resolve(__dirname, '../../addons/shopro/service/partner/DailySettleService.php');
+assert(fs.existsSync(dailySettleServicePath), 'DailySettleService.php 文件存在');
+if (fs.existsSync(dailySettleServicePath)) {
+ const content = fs.readFileSync(dailySettleServicePath, 'utf-8');
+ assert(content.includes('$walletType = PartnerConfig::rewardWalletType();'), 'cashShare() 获取收益入账钱包类型');
+ assert(content.includes('Wallet::change($userId, $walletType, $each, $type,'), 'cashShare() 日结酒道馆/区县分成直接加到商城余额');
+ assert(content.includes("AgentModel::where('user_id', $userId)->setInc('total_income', $each);"), 'cashShare() 累计更新 total_income');
+ assert(content.includes("$targetWallet = ($walletLog && $walletLog->type === 'commission') ? 'commission' : PartnerConfig::rewardWalletType();"), 'clearDaily() 重跑日结时智能识别并回滚对应钱包');
+}
+
+// ----------------------------------------------------
+// 4. 后台 Reward 控制器验证
+// ----------------------------------------------------
+console.log('\n--- 4. 后台 Reward.php 控制器验证 ---');
+const rewardCtrlPath = path.resolve(__dirname, '../../application/admin/controller/shopro/partner/Reward.php');
+assert(fs.existsSync(rewardCtrlPath), 'Reward.php 文件存在');
+if (fs.existsSync(rewardCtrlPath)) {
+ const content = fs.readFileSync(rewardCtrlPath, 'utf-8');
+ assert(content.includes('$walletType = PartnerConfig::rewardWalletType();'), 'add() 手工补发获取收益入账钱包类型');
+ assert(content.includes('Wallet::change($params[\'user_id\'], $walletType, $amount, \'reward_income\''), 'add() 手工补发直接加到商城余额');
+ assert(content.includes("$targetWallet = ($walletLog && $walletLog->type === 'commission') ? 'commission' : PartnerConfig::rewardWalletType();"), 'back() 手工撤销智能回滚对应钱包');
+}
+
+// ----------------------------------------------------
+// 5. WalletLog.php 模型验证
+// ----------------------------------------------------
+console.log('\n--- 5. WalletLog.php 模型事件映射验证 ---');
+const walletLogPath = path.resolve(__dirname, '../../application/admin/model/shopro/user/WalletLog.php');
+assert(fs.existsSync(walletLogPath), 'WalletLog.php 文件存在');
+if (fs.existsSync(walletLogPath)) {
+ const content = fs.readFileSync(walletLogPath, 'utf-8');
+ assert(content.includes("'push3' => '推荐提成'"), 'money 事件映射包含 push3 (推荐提成)');
+ assert(content.includes("'maker_diff' => '创客进阶奖'"), 'money 事件映射包含 maker_diff (创客进阶奖)');
+ assert(content.includes("'maker_peer' => '创客协同奖'"), 'money 事件映射包含 maker_peer (创客协同奖)');
+ assert(content.includes("'maker_overtaken' => '创客卓越奖'"), 'money 事件映射包含 maker_overtaken (创客卓越奖)');
+ assert(content.includes("'team_venue' => '酒道馆管理奖'"), 'money 事件映射包含 team_venue (酒道馆管理奖)');
+ assert(content.includes("'team_county' => '区县管理奖'"), 'money 事件映射包含 team_county (区县管理奖)');
+ assert(content.includes("'venue_peer' => '酒道馆平级奖'"), 'money 事件映射包含 venue_peer (酒道馆平级奖)');
+ assert(content.includes("'county_peer' => '区县代平级奖'"), 'money 事件映射包含 county_peer (区县代平级奖)');
+ assert(content.includes("'venue_share' => '酒道馆加权奖'"), 'money 事件映射包含 venue_share (酒道馆加权奖)');
+ assert(content.includes("'county_share' => '区县加权奖'"), 'money 事件映射包含 county_share (区县加权奖)');
+ assert(content.includes("'reward_income' => '收益分成-入账'"), 'money 事件映射包含 reward_income (收益分成-入账)');
+ assert(content.includes("'reward_back' => '收益分成-退还'"), 'money 事件映射包含 reward_back (收益分成-退还)');
+ assert(content.includes('PartnerConfig::rewardTypeMap()'), 'WalletLog 支持联动 PartnerConfig 动态解析文案');
+}
+
+// ----------------------------------------------------
+// 6. 前端视图 account-info.vue 验证
+// ----------------------------------------------------
+console.log('\n--- 6. 前端视图 account-info.vue 验证 ---');
+const accountInfoPath = path.resolve(__dirname, '../../uniapp/pages/commission/components/account-info.vue');
+assert(fs.existsSync(accountInfoPath), 'account-info.vue 文件存在');
+if (fs.existsSync(accountInfoPath)) {
+ const content = fs.readFileSync(accountInfoPath, 'utf-8');
+ assert(content.includes('商城余额(元)'), '卡片标签已变更为「商城余额(元)」');
+ assert(content.includes("userInfo?.money || '0.00'"), '读取 userInfo.money 商城余额');
+ assert(content.includes("sheep.$router.go('/pages/user/wallet/money')"), '点击商城余额卡片直达 /pages/user/wallet/money');
+ assert(content.includes("sheep.$router.go('/pages/commission/rewards')"), '查看明细直达收益明细');
+}
+
+// ----------------------------------------------------
+// 7. 模拟行为逻辑断言
+// ----------------------------------------------------
+console.log('\n--- 7. 模拟算法与资金业务流断言 ---');
+
+// 模拟场景 A: 默认配置读取
+function mockGetWalletType(config) {
+ const val = config['partner_reward_wallet'] || 'money';
+ return ['money', 'commission'].includes(val) ? val : 'money';
+}
+assert(mockGetWalletType({}) === 'money', '未配置 partner_reward_wallet 时,默认返回 money');
+assert(mockGetWalletType({ partner_reward_wallet: 'commission' }) === 'commission', '配置 commission 时,支持切换为 commission');
+assert(mockGetWalletType({ partner_reward_wallet: 'invalid_type' }) === 'money', '配置非法类型时,安全回退至 money');
+
+// 模拟场景 B: 收益入账模拟
+function mockAccountReward(user, agent, amount, event) {
+ const walletType = mockGetWalletType({});
+ const beforeMoney = user[walletType];
+ user[walletType] = (parseFloat(user[walletType]) + amount).toFixed(2);
+ agent.total_income = (parseFloat(agent.total_income) + amount).toFixed(2);
+
+ const walletLog = {
+ user_id: user.id,
+ type: walletType,
+ amount: amount,
+ before: beforeMoney,
+ after: user[walletType],
+ event: event,
+ };
+ return walletLog;
+}
+
+const mockUser = { id: 10, money: '100.00', commission: '0.00' };
+const mockAgent = { user_id: 10, total_income: '500.00' };
+const log1 = mockAccountReward(mockUser, mockAgent, 398.00, 'push3');
+
+assert(mockUser.money === '498.00', '模拟即时奖入账:用户商城余额从 100 增至 498.00');
+assert(mockUser.commission === '0.00', '模拟即时奖入账:佣金钱包维持 0.00 未动');
+assert(mockAgent.total_income === '898.00', '模拟即时奖入账:分销商累计收益从 500 累加至 898.00');
+assert(log1.type === 'money' && log1.event === 'push3', '产生正确的 money 钱包变更流水');
+
+// 模拟场景 C: 退款智能回滚判断
+function mockDetermineRefundWallet(walletLogs, rewardId) {
+ const found = walletLogs.find(l => l.partner_reward_id === rewardId);
+ if (found && found.type === 'commission') {
+ return 'commission';
+ }
+ return 'money';
+}
+
+const testLogs = [
+ { id: 1, type: 'commission', partner_reward_id: 101 }, // 历史老订单
+ { id: 2, type: 'money', partner_reward_id: 102 }, // 新订单
+];
+
+assert(mockDetermineRefundWallet(testLogs, 101) === 'commission', '历史老订单收益退款:精准定位回滚 commission 钱包');
+assert(mockDetermineRefundWallet(testLogs, 102) === 'money', '新订单收益退款:精准定位回滚 money 钱包');
+assert(mockDetermineRefundWallet(testLogs, 999) === 'money', '无流水记录极端情况:默认回退扣减当前生效的 money 钱包');
+
+console.log('\n========================================================');
+console.log(`测试执行结果: 通过 ${passCount} 项, 失败 ${failCount} 项`);
+console.log('========================================================');
+
+if (failCount > 0) {
+ process.exit(1);
+}
diff --git a/tests/partner/verify_reward_names_rename_spec.js b/tests/partner/verify_reward_names_rename_spec.js
new file mode 100644
index 0000000..c4353c3
--- /dev/null
+++ b/tests/partner/verify_reward_names_rename_spec.js
@@ -0,0 +1,137 @@
+/**
+ * 奖项名称全局规范重命名专项验证测试套件
+ *
+ * 验证目标:
+ * 1. 区县统筹奖 -> 区县管理奖
+ * 2. 区县加权奖励 -> 区县加权奖
+ * 3. 酒道馆加权奖励 -> 酒道馆加权奖
+ * 4. 酒道馆运营奖 -> 酒道馆管理奖
+ * 5. 酒道馆协同奖 -> 酒道馆平级奖(区县代为区县代平级奖)
+ * 6. 推荐开拓奖 -> 推荐提成
+ */
+
+const fs = require('fs');
+const path = require('path');
+
+let passCount = 0;
+let failCount = 0;
+
+function assert(condition, message) {
+ if (condition) {
+ console.log(` \x1b[32m[PASS]\x1b[0m ${message}`);
+ passCount++;
+ } else {
+ console.error(` \x1b[31m[FAIL]\x1b[0m ${message}`);
+ failCount++;
+ }
+}
+
+const rootDir = path.resolve(__dirname, '../../');
+
+console.log('========================================================');
+console.log(' 奖项名称全局规范重命名专项验证');
+console.log('========================================================\n');
+
+// 1. PartnerConfig.php 验证
+console.log('--- 1. PartnerConfig.php 核心映射与提示文案验证 ---');
+const partnerConfigPath = path.join(rootDir, 'addons/shopro/service/partner/PartnerConfig.php');
+assert(fs.existsSync(partnerConfigPath), 'PartnerConfig.php 文件存在');
+const partnerConfigContent = fs.readFileSync(partnerConfigPath, 'utf8');
+assert(partnerConfigContent.includes("'push3' => '推荐提成'"), 'rewardTypeMap 包含 push3: 推荐提成');
+assert(partnerConfigContent.includes("$venue . '管理奖'"), 'rewardTypeMap 包含 team_venue: 酒道馆管理奖');
+assert(partnerConfigContent.includes("$countyShort . '管理奖'"), 'rewardTypeMap 包含 team_county: 区县管理奖');
+assert(partnerConfigContent.includes("$venue . '平级奖'"), 'rewardTypeMap 包含 venue_peer: 酒道馆平级奖');
+assert(partnerConfigContent.includes("$countyAgentShort . '平级奖'"), 'rewardTypeMap 包含 county_peer: 区县代平级奖');
+assert(partnerConfigContent.includes("$venue . '加权奖'"), 'rewardTypeMap 包含 venue_share: 酒道馆加权奖');
+assert(partnerConfigContent.includes("$countyShort . '加权奖'"), 'rewardTypeMap 包含 county_share: 区县加权奖');
+assert(partnerConfigContent.includes("'no_parent' => '暂无推荐人,不计提推荐提成'"), 'skipReasonText 包含不计提推荐提成');
+assert(partnerConfigContent.includes("'self_buy' => '本人下单不计提推荐提成'"), 'skipReasonText 包含本人下单不计提推荐提成');
+
+// 2. 结算服务公式文案验证
+console.log('\n--- 2. 核心结算服务公式与提示文案验证 ---');
+const push3ServicePath = path.join(rootDir, 'addons/shopro/service/partner/Push3Reward.php');
+assert(fs.existsSync(push3ServicePath), 'Push3Reward.php 文件存在');
+const push3ServiceContent = fs.readFileSync(push3ServicePath, 'utf8');
+assert(push3ServiceContent.includes('不计提推荐提成'), 'Push3Reward 跳过提示更新为推荐提成');
+assert(push3ServiceContent.includes('推荐提成第{$seq}单'), 'Push3Reward 收益公式更新为推荐提成');
+
+const teamSplitPath = path.join(rootDir, 'addons/shopro/service/partner/TeamSplitReward.php');
+assert(fs.existsSync(teamSplitPath), 'TeamSplitReward.php 文件存在');
+const teamSplitContent = fs.readFileSync(teamSplitPath, 'utf8');
+assert(teamSplitContent.includes('酒道馆管理奖 {$venueSplit}'), 'TeamSplitReward 包含酒道馆管理奖公式');
+assert(teamSplitContent.includes('区县管理奖 {$amount}'), 'TeamSplitReward 包含区县管理奖公式');
+
+const peerSharePath = path.join(rootDir, 'addons/shopro/service/partner/PeerShareReward.php');
+assert(fs.existsSync(peerSharePath), 'PeerShareReward.php 文件存在');
+const peerShareContent = fs.readFileSync(peerSharePath, 'utf8');
+assert(peerShareContent.includes('{$label}平级(第{$gen}代)'), 'PeerShareReward 包含平级公式');
+assert(peerShareContent.includes('{$label}平级(第{$gen}代) 30 日无自购'), 'PeerShareReward 包含平级跳过提示');
+
+// 3. WalletLog 模型字典映射验证
+console.log('\n--- 3. WalletLog 模型事件字典映射验证 ---');
+const walletLogPath = path.join(rootDir, 'application/admin/model/shopro/user/WalletLog.php');
+assert(fs.existsSync(walletLogPath), 'WalletLog.php 文件存在');
+const walletLogContent = fs.readFileSync(walletLogPath, 'utf8');
+assert(walletLogContent.includes("'push3' => '推荐提成'"), 'WalletLog 包含推荐提成');
+assert(walletLogContent.includes("'team_venue' => '酒道馆管理奖'"), 'WalletLog 包含酒道馆管理奖');
+assert(walletLogContent.includes("'team_county' => '区县管理奖'"), 'WalletLog 包含区县管理奖');
+assert(walletLogContent.includes("'venue_peer' => '酒道馆平级奖'"), 'WalletLog 包含酒道馆平级奖');
+assert(walletLogContent.includes("'county_peer' => '区县代平级奖'"), 'WalletLog 包含区县代平级奖');
+assert(walletLogContent.includes("'venue_share' => '酒道馆加权奖'"), 'WalletLog 包含酒道馆加权奖');
+assert(walletLogContent.includes("'county_share' => '区县加权奖'"), 'WalletLog 包含区县加权奖');
+
+// 4. 后台视图与筛选菜单验证
+console.log('\n--- 4. 后台管理视图与筛选菜单验证 ---');
+const rewardJsPath = path.join(rootDir, 'public/assets/js/backend/shopro/partner/reward.js');
+assert(fs.existsSync(rewardJsPath), 'reward.js 文件存在');
+const rewardJsContent = fs.readFileSync(rewardJsPath, 'utf8');
+assert(rewardJsContent.includes("{ label: '推荐提成', value: 'push3' }"), 'reward.js 筛选包含推荐提成');
+assert(rewardJsContent.includes("{ label: '酒道馆管理奖', value: 'team_venue' }"), 'reward.js 筛选包含酒道馆管理奖');
+assert(rewardJsContent.includes("{ label: '区县管理奖', value: 'team_county' }"), 'reward.js 筛选包含区县管理奖');
+assert(rewardJsContent.includes("{ label: '酒道馆平级奖', value: 'venue_peer' }"), 'reward.js 筛选包含酒道馆平级奖');
+assert(rewardJsContent.includes("{ label: '区县代平级奖', value: 'county_peer' }"), 'reward.js 筛选包含区县代平级奖');
+assert(rewardJsContent.includes("{ label: '酒道馆加权奖', value: 'venue_share' }"), 'reward.js 筛选包含酒道馆加权奖');
+assert(rewardJsContent.includes("{ label: '区县加权奖', value: 'county_share' }"), 'reward.js 筛选包含区县加权奖');
+
+const configViewPath = path.join(rootDir, 'application/admin/view/shopro/config/index.html');
+assert(fs.existsSync(configViewPath), 'config/index.html 文件存在');
+const configViewContent = fs.readFileSync(configViewPath, 'utf8');
+assert(configViewContent.includes('推荐提成
'), 'config/index.html 配置卡片更新为推荐提成');
+
+const goodsEditPath = path.join(rootDir, 'application/admin/view/shopro/commission/goods/edit.html');
+assert(fs.existsSync(goodsEditPath), 'commission/goods/edit.html 文件存在');
+const goodsEditContent = fs.readFileSync(goodsEditPath, 'utf8');
+assert(goodsEditContent.includes('推荐提成(推三返一)'), 'goods/edit.html 预览卡片更新为推荐提成');
+assert(goodsEditContent.includes('实体渠道管理奖'), 'goods/edit.html 预览卡片更新为实体渠道管理奖');
+assert(goodsEditContent.includes('馆/县平级奖'), 'goods/edit.html 预览卡片更新为馆/县平级奖');
+assert(goodsEditContent.includes('1. 推荐提成(推三返一循环返现)'), 'goods/edit.html section 1 标题更新为推荐提成');
+assert(goodsEditContent.includes('酒道馆管理奖基准'), 'goods/edit.html 字段更新为酒道馆管理奖基准');
+assert(goodsEditContent.includes('区县管理全额'), 'goods/edit.html 字段更新为区县管理全额');
+
+const dailyViewPath = path.join(rootDir, 'application/admin/view/shopro/partner/daily/index.html');
+assert(fs.existsSync(dailyViewPath), 'partner/daily/index.html 文件存在');
+const dailyViewContent = fs.readFileSync(dailyViewPath, 'utf8');
+assert(dailyViewContent.includes('酒道馆加权奖池'), 'daily/index.html 包含酒道馆加权奖池');
+assert(dailyViewContent.includes('区县加权奖池'), 'daily/index.html 包含区县加权奖池');
+
+// 5. Uniapp 用户端验证
+console.log('\n--- 5. Uniapp 用户端分类与文案验证 ---');
+const rewardsVuePath = path.join(rootDir, 'uniapp/pages/commission/rewards.vue');
+assert(fs.existsSync(rewardsVuePath), 'rewards.vue 文件存在');
+const rewardsVueContent = fs.readFileSync(rewardsVuePath, 'utf8');
+assert(rewardsVueContent.includes("{ name: '推荐提成', value: 'push3' }"), 'rewards.vue 包含推荐提成筛选Tab');
+assert(rewardsVueContent.includes("{ name: '加权奖', value: 'share' }"), 'rewards.vue 包含加权奖筛选Tab');
+
+const teamVuePath = path.join(rootDir, 'uniapp/pages/commission/team.vue');
+assert(fs.existsSync(teamVuePath), 'team.vue 文件存在');
+const teamVueContent = fs.readFileSync(teamVuePath, 'utf8');
+assert(teamVueContent.includes('提成循环序号'), 'team.vue 包含提成循环序号指标');
+assert(teamVueContent.includes('推荐提成序号:'), 'team.vue 包含推荐提成序号说明');
+
+console.log('\n========================================================');
+console.log(`测试完成!通过: ${passCount}, 失败: ${failCount}`);
+console.log('========================================================');
+
+if (failCount > 0) {
+ process.exit(1);
+}
diff --git a/tests/partner/verify_team_county_dual_identity_spec.js b/tests/partner/verify_team_county_dual_identity_spec.js
new file mode 100644
index 0000000..bddbd73
--- /dev/null
+++ b/tests/partner/verify_team_county_dual_identity_spec.js
@@ -0,0 +1,193 @@
+/**
+ * 验证酒道馆与区县代理双重身份及全拓扑场景下的团队统筹拆分奖专项测试
+ */
+
+const fs = require('fs');
+const path = require('path');
+
+let passed = 0;
+let failed = 0;
+
+function assert(condition, message) {
+ if (condition) {
+ passed++;
+ console.log(` [PASS] ${message}`);
+ } else {
+ failed++;
+ console.error(` [FAIL] ${message}`);
+ }
+}
+
+console.log('========================================================');
+console.log(' 酒道馆与区县代理双重身份及全拓扑场景团队奖专项测试');
+console.log('========================================================\n');
+
+// 1. 静态代码分析
+const teamSplitFile = path.resolve(__dirname, '../../addons/shopro/service/partner/TeamSplitReward.php');
+assert(fs.existsSync(teamSplitFile), 'TeamSplitReward.php 文件存在');
+const code = fs.readFileSync(teamSplitFile, 'utf8');
+
+assert(code.includes('$start = $venue ? $venue[\'index\'] : 0;'), 'nearestCounty 起始查找索引支持从当前酒道馆节点匹配兼职区县代理');
+assert(code.includes('馆兼县'), '支持「馆兼县」专属流水明细公式标识');
+
+// 2. 模拟 TeamSplitReward 核心算法逻辑
+class MockActiveService {
+ isActive(node) {
+ return node.is_active !== false;
+ }
+}
+
+class MockTeamSplitReward {
+ run(order, buyerId, chain, active, rules = null) {
+ const venueSplit = (rules && typeof rules.venue_split === 'number') ? rules.venue_split : 50;
+ const countyFull = (rules && typeof rules.county_full === 'number') ? rules.county_full : 100;
+ const result = { venue: null, county: null, skips: [] };
+
+ const venue = this.nearest(chain, 'is_venue', active);
+ const county = this.nearestCounty(chain, venue, active);
+
+ if (!venue && !county) {
+ result.skips.push({ type: 'team_venue', reason: 'not_venue', formula: '链上无酒道馆且无区县' });
+ return result;
+ }
+
+ if (venue) {
+ if (venue.inactive) {
+ result.skips.push({ type: 'team_venue', user_id: venue.user_id, reason: 'inactive_30d', formula: '酒道馆 30 日无自购' });
+ } else {
+ result.venue = { user_id: venue.user_id, amount: venueSplit, index: venue.index };
+ }
+ }
+
+ if (county) {
+ const amount = venue ? venueSplit : countyFull;
+ if (county.inactive) {
+ result.skips.push({ type: 'team_county', user_id: county.user_id, reason: 'inactive_30d', formula: '区县 30 日无自购' });
+ } else {
+ const formulaText = venue
+ ? (venue.user_id === county.user_id ? `区县管理奖 ${amount}(馆兼县)` : `区县管理奖 ${amount}(馆上)`)
+ : `区县管理奖 ${amount}(直达)`;
+ result.county = { user_id: county.user_id, amount, index: county.index, formula: formulaText };
+ }
+ } else if (venue && !county) {
+ result.skips.push({ type: 'team_county', user_id: 0, reason: 'not_county', formula: '有馆无区县' });
+ }
+
+ return result;
+ }
+
+ nearest(chain, flag, active) {
+ for (let i = 0; i < chain.length; i++) {
+ if (!chain[i][flag]) continue;
+ return {
+ user_id: chain[i].user_id,
+ index: i,
+ inactive: !active.isActive(chain[i]),
+ node: chain[i]
+ };
+ }
+ return null;
+ }
+
+ nearestCounty(chain, venue, active) {
+ const start = venue ? venue.index : 0;
+ for (let i = start; i < chain.length; i++) {
+ if (!chain[i].is_county) continue;
+ return {
+ user_id: chain[i].user_id,
+ index: i,
+ inactive: !active.isActive(chain[i]),
+ node: chain[i]
+ };
+ }
+ if (!venue) {
+ return this.nearest(chain, 'is_county', active);
+ }
+ return null;
+ }
+}
+
+const runner = new MockTeamSplitReward();
+const active = new MockActiveService();
+
+// 场景 1:同一人兼任酒道馆与区县代理(买家25 -> 上级20兼具馆+县,即本Bug场景)
+{
+ const chain = [
+ { user_id: 25, is_venue: 0, is_county: 0 },
+ { user_id: 20, is_venue: 1, is_county: 1, is_active: true }
+ ];
+ const res = runner.run({ id: 12 }, 25, chain, active);
+ assert(res.venue !== null && res.venue.user_id === 20 && res.venue.amount === 50, '场景1: 上级20号成功获得酒道馆奖 50 元');
+ assert(res.county !== null && res.county.user_id === 20 && res.county.amount === 50, '场景1: 上级20号兼任区县代理,成功获得区县管理奖 50 元');
+ assert(res.county.formula.includes('馆兼县'), '场景1: 公式正确标识为「区县管理奖 50(馆兼县)」');
+ assert(res.venue.amount + res.county.amount === 100, '场景1: 团队拆分总额严格守恒为 100 元 (50 + 50)');
+ assert(res.skips.length === 0, '场景1: 无错误跳过记录');
+}
+
+// 场景 2:酒道馆与区县代理为不同上级(买家25 -> 馆20 -> 县30)
+{
+ const chain = [
+ { user_id: 25, is_venue: 0, is_county: 0 },
+ { user_id: 20, is_venue: 1, is_county: 0, is_active: true },
+ { user_id: 30, is_venue: 0, is_county: 1, is_active: true }
+ ];
+ const res = runner.run({ id: 13 }, 25, chain, active);
+ assert(res.venue !== null && res.venue.user_id === 20 && res.venue.amount === 50, '场景2: 独立酒道馆20号获得 50 元');
+ assert(res.county !== null && res.county.user_id === 30 && res.county.amount === 50, '场景2: 上层区县代理30号获得 50 元');
+ assert(res.county.formula.includes('馆上'), '场景2: 公式正确标识为「区县管理奖 50(馆上)」');
+ assert(res.venue.amount + res.county.amount === 100, '场景2: 团队拆分总额严格守恒为 100 元');
+}
+
+// 场景 3:无馆有县(买家25 -> 县20)
+{
+ const chain = [
+ { user_id: 25, is_venue: 0, is_county: 0 },
+ { user_id: 20, is_venue: 0, is_county: 1, is_active: true }
+ ];
+ const res = runner.run({ id: 14 }, 25, chain, active);
+ assert(res.venue === null, '场景3: 无酒道馆');
+ assert(res.county !== null && res.county.user_id === 20 && res.county.amount === 100, '场景3: 无馆区县独享 100 元全额');
+ assert(res.county.formula.includes('直达'), '场景3: 公式正确标识为「区县管理奖 100(直达)」');
+}
+
+// 场景 4:有馆无县(买家25 -> 馆20,上方无区县)
+{
+ const chain = [
+ { user_id: 25, is_venue: 0, is_county: 0 },
+ { user_id: 20, is_venue: 1, is_county: 0, is_active: true }
+ ];
+ const res = runner.run({ id: 15 }, 25, chain, active);
+ assert(res.venue !== null && res.venue.user_id === 20 && res.venue.amount === 50, '场景4: 馆20号获得 50 元');
+ assert(res.county === null, '场景4: 无区县,跳过发奖');
+ assert(res.skips.length === 1 && res.skips[0].reason === 'not_county', '场景4: 产生「有馆无区县」跳过记录');
+}
+
+// 场景 5:馆与县均不存在
+{
+ const chain = [
+ { user_id: 25, is_venue: 0, is_county: 0 },
+ { user_id: 20, is_venue: 0, is_county: 0, is_active: true }
+ ];
+ const res = runner.run({ id: 16 }, 25, chain, active);
+ assert(res.venue === null && res.county === null, '场景5: 均不存在');
+ assert(res.skips.length === 1 && res.skips[0].reason === 'not_venue', '场景5: 记录链上无酒道馆且无区县');
+}
+
+// 场景 6:买家本人自购节点兼任馆或县(自购保活与自购分成合法性)
+{
+ const chain = [
+ { user_id: 20, is_venue: 1, is_county: 1, is_active: true }
+ ];
+ const res = runner.run({ id: 17 }, 20, chain, active);
+ assert(res.venue !== null && res.venue.user_id === 20 && res.venue.amount === 50, '场景6: 自购节点获得酒道馆运营奖 50 元');
+ assert(res.county !== null && res.county.user_id === 20 && res.county.amount === 50, '场景6: 自购节点获得区县管理奖 50 元');
+ assert(res.venue.amount + res.county.amount === 100, '场景6: 自购节点拆分总额 100 元');
+}
+
+console.log('\n--------------------------------------------------------');
+console.log(`测试执行结果: 通过 ${passed} 项, 失败 ${failed} 项`);
+console.log('========================================================\n');
+
+if (failed > 0) {
+ process.exit(1);
+}
diff --git a/tests/partner/verify_team_self_order_spec.php b/tests/partner/verify_team_self_order_spec.php
new file mode 100644
index 0000000..2ab1211
--- /dev/null
+++ b/tests/partner/verify_team_self_order_spec.php
@@ -0,0 +1,101 @@
+testAgentTeamStatsIncludeSelf();
+ $this->testTeamHealthSummaryIncludeSelf();
+
+ echo "\n--------------------------------------------------------\n";
+ echo "测试执行结果: 通过 {$this->passed} 项, 失败 {$this->failed} 项\n";
+ echo "========================================================\n";
+ return $this->failed === 0 ? 0 : 1;
+ }
+
+ private function assert($condition, $testName, $detail = '')
+ {
+ if ($condition) {
+ $this->passed++;
+ echo " [✓ PASS] {$testName}\n";
+ } else {
+ $this->failed++;
+ echo " [✗ FAIL] {$testName} - {$detail}\n";
+ }
+ }
+
+ private function testAgentTeamStatsIncludeSelf()
+ {
+ try {
+ $agent = new AgentService(20);
+ $agent->runAgentUpgradePlan(false);
+ $agentRow = AgentModel::get(20);
+
+ // 自购单数 >= 1
+ $this->assert($agentRow->child_order_count_0 >= 1, '分销商自购订单数已记录 (child_order_count_0 >= 1)');
+
+ // 团队总单数 = 自购单数 + 直推单数 + 伞下单数 (当前为 1自购 + 3直推 + 1伞下 = 5单)
+ $this->assert($agentRow->child_order_count_all >= 5,
+ "团队总单数包含自购订单 (当前团队总单数: {$agentRow->child_order_count_all}, 包含自购 {$agentRow->child_order_count_0} 单)");
+
+ // 团队总金额包含自购金额
+ $this->assert(floatval($agentRow->child_order_money_all) >= 6990.00,
+ "团队总金额包含自购金额 (当前团队总金额: {$agentRow->child_order_money_all})");
+ } catch (\Throwable $e) {
+ $this->assert(false, '分销商团队数据统计包含自购测试', $e->getMessage());
+ }
+ }
+
+ private function testTeamHealthSummaryIncludeSelf()
+ {
+ try {
+ $ref = new \ReflectionClass(AgentController::class);
+ $controller = $ref->newInstanceWithoutConstructor();
+
+ $summaryMethod = $ref->getMethod('getTeamHealthSummary');
+ $summaryMethod->setAccessible(true);
+ $window = time() - 30 * 86400;
+ $expiring = time() - 23 * 86400;
+ $summary = $summaryMethod->invoke($controller, 20, [21, 22, 23], $window, $expiring);
+
+ // 团队累计订单数应为 5 笔(包含下级与本人自购)
+ $this->assert(isset($summary['team_order_count']) && $summary['team_order_count'] >= 5,
+ "团队健康度汇总 team_order_count 应为 5 笔 (实测: " . ($summary['team_order_count'] ?? 0) . ")");
+
+ // 团队累计自购总业绩应为 6990.00(包含下级与本人自购)
+ $this->assert(isset($summary['team_total_consume']) && floatval($summary['team_total_consume']) >= 6990.00,
+ "团队累计总业绩 team_total_consume 应为 6990.00 (实测: " . ($summary['team_total_consume'] ?? '0.00') . ")");
+ } catch (\Throwable $e) {
+ $this->assert(false, '团队健康度汇总包含本人自购测试', $e->getMessage());
+ }
+ }
+}
+
+if (PHP_SAPI === 'cli') {
+ $tester = new VerifyTeamSelfOrderSpec();
+ exit($tester->run());
+}
diff --git a/tests/partner/verify_wallet_migration_spec.js b/tests/partner/verify_wallet_migration_spec.js
new file mode 100644
index 0000000..c18f793
--- /dev/null
+++ b/tests/partner/verify_wallet_migration_spec.js
@@ -0,0 +1,193 @@
+/**
+ * 历史收益全量结转至商城余额专项测试
+ * 覆盖范围:
+ * 1. WalletMigrationService 业务服务契约与原子性逻辑
+ * 2. PartnerTransferCommission 控制台命令
+ * 3. Shopro.php 注册命令配置
+ * 4. transfer_all_commission_to_money.php 生产独立运维脚本
+ * 5. Reward.php 后台接口与防误触确认校验
+ * 6. 算法行为模拟:多用户结转、资金守恒、零余额跳过、幂等重入、单用户异常容灾隔离
+ */
+
+const fs = require('fs');
+const path = require('path');
+
+let passCount = 0;
+let failCount = 0;
+
+function assert(condition, message) {
+ if (condition) {
+ console.log(` \x1b[32m[PASS]\x1b[0m ${message}`);
+ passCount++;
+ } else {
+ console.error(` \x1b[31m[FAIL]\x1b[0m ${message}`);
+ failCount++;
+ }
+}
+
+console.log('========================================================');
+console.log(' 历史收益全量结转至商城余额专项测试');
+console.log('========================================================\n');
+
+// ----------------------------------------------------
+// 1. WalletMigrationService.php 验证
+// ----------------------------------------------------
+console.log('--- 1. WalletMigrationService.php 业务规范验证 ---');
+const migrationServicePath = path.resolve(__dirname, '../../addons/shopro/service/partner/WalletMigrationService.php');
+assert(fs.existsSync(migrationServicePath), 'WalletMigrationService.php 文件存在');
+if (fs.existsSync(migrationServicePath)) {
+ const content = fs.readFileSync(migrationServicePath, 'utf-8');
+ assert(content.includes('public function preview()'), '包含 preview() 试算预览方法');
+ assert(content.includes('public function transferAll('), '包含 transferAll() 全量结转执行方法');
+ assert(content.includes('Db::transaction(function ()'), '结转过程使用数据库事务');
+ assert(content.includes('lock(true)'), '读取用户使用排他行锁 lock(true) 防并发脏读');
+ assert(content.includes("Wallet::change($user, 'commission', -$amount, 'transfer_to_money'"), '调用 Wallet::change 扣减佣金钱包');
+ assert(content.includes("Wallet::change($user, 'money', $amount, 'transfer_by_commission'"), '调用 Wallet::change 增加商城余额');
+ assert(content.includes('bcadd('), '资金累计采用高精度 bcadd 避免浮点误差');
+}
+
+// ----------------------------------------------------
+// 2. PartnerTransferCommission.php 控制台命令验证
+// ----------------------------------------------------
+console.log('\n--- 2. PartnerTransferCommission.php 控制台命令验证 ---');
+const consoleCmdPath = path.resolve(__dirname, '../../addons/shopro/console/PartnerTransferCommission.php');
+assert(fs.existsSync(consoleCmdPath), 'PartnerTransferCommission.php 文件存在');
+if (fs.existsSync(consoleCmdPath)) {
+ const content = fs.readFileSync(consoleCmdPath, 'utf-8');
+ assert(content.includes("'shopro:partner:transfer-commission'"), '命令名称定义为 shopro:partner:transfer-commission');
+ assert(content.includes("'dry-run'"), '命令支持 --dry-run 试算选项');
+ assert(content.includes("'force'"), '命令支持 --force 强制执行选项');
+ assert(content.includes('WalletMigrationService'), '命令调用 WalletMigrationService 服务');
+}
+
+// ----------------------------------------------------
+// 3. Shopro.php 自动命令注册验证
+// ----------------------------------------------------
+console.log('\n--- 3. Shopro.php 注册命令验证 ---');
+const shoproPath = path.resolve(__dirname, '../../addons/shopro/Shopro.php');
+assert(fs.existsSync(shoproPath), 'Shopro.php 文件存在');
+if (fs.existsSync(shoproPath)) {
+ const content = fs.readFileSync(shoproPath, 'utf-8');
+ assert(content.includes("'addons\\shopro\\console\\PartnerTransferCommission'"), 'Shopro.php addDefaultCommands 已注册 PartnerTransferCommission');
+}
+
+// ----------------------------------------------------
+// 4. transfer_all_commission_to_money.php 生产脚本验证
+// ----------------------------------------------------
+console.log('\n--- 4. transfer_all_commission_to_money.php 运维脚本验证 ---');
+const scriptPath = path.resolve(__dirname, '../../scripts/transfer_all_commission_to_money.php');
+assert(fs.existsSync(scriptPath), 'transfer_all_commission_to_money.php 脚本存在');
+if (fs.existsSync(scriptPath)) {
+ const content = fs.readFileSync(scriptPath, 'utf-8');
+ assert(content.includes('WalletMigrationService'), '脚本引入 WalletMigrationService 执行');
+ assert(content.includes('--dry-run'), '脚本支持 --dry-run');
+ assert(content.includes('--force'), '脚本安全防御支持 --force');
+}
+
+// ----------------------------------------------------
+// 5. Reward.php 后台接口验证
+// ----------------------------------------------------
+console.log('\n--- 5. Reward.php 后台接口验证 ---');
+const rewardPath = path.resolve(__dirname, '../../application/admin/controller/shopro/partner/Reward.php');
+assert(fs.existsSync(rewardPath), 'Reward.php 文件存在');
+if (fs.existsSync(rewardPath)) {
+ const content = fs.readFileSync(rewardPath, 'utf-8');
+ assert(content.includes('public function transferAllCommissionPreview()'), '包含 transferAllCommissionPreview 后台预览接口');
+ assert(content.includes('public function transferAllCommission()'), '包含 transferAllCommission 后台执行接口');
+ assert(content.includes('confirm !== 1'), '正式结转接口必须传递 confirm=1 进行二次安全防御');
+}
+
+// ----------------------------------------------------
+// 6. 算法行为模拟断言
+// ----------------------------------------------------
+console.log('\n--- 6. 资金结转算法与守恒性模拟断言 ---');
+
+function mockTransferService(users) {
+ let successCount = 0;
+ let failCount = 0;
+ let totalTransferred = 0;
+ const logs = [];
+
+ users.forEach(u => {
+ if (u.simulateError) {
+ failCount++;
+ return;
+ }
+ const comm = parseFloat(u.commission);
+ if (comm <= 0) {
+ return; // 跳过
+ }
+ const money = parseFloat(u.money);
+ // 执行原子互转
+ u.commission = '0.00';
+ u.money = (money + comm).toFixed(2);
+ totalTransferred += comm;
+ successCount++;
+
+ logs.push({
+ user_id: u.id,
+ type: 'commission',
+ amount: -comm,
+ event: 'transfer_to_money',
+ });
+ logs.push({
+ user_id: u.id,
+ type: 'money',
+ amount: comm,
+ event: 'transfer_by_commission',
+ });
+ });
+
+ return {
+ successCount,
+ failCount,
+ totalTransferred: totalTransferred.toFixed(2),
+ logs,
+ };
+}
+
+// 测试数据集
+const initialUsers = [
+ { id: 1, nickname: '张三', money: '100.00', commission: '250.50' },
+ { id: 2, nickname: '李四', money: '0.00', commission: '398.00' },
+ { id: 3, nickname: '王五', money: '50.00', commission: '0.00' }, // 佣金为0
+ { id: 4, nickname: '赵六', money: '80.00', commission: '120.00', simulateError: true }, // 模拟异常
+];
+
+// 计算初始总资金
+const initialTotalAssets = initialUsers.reduce((sum, u) => {
+ return sum + parseFloat(u.money) + parseFloat(u.commission);
+}, 0);
+
+const result = mockTransferService(initialUsers);
+
+assert(result.successCount === 2, '成功结转 2 位有佣金的会员');
+assert(result.failCount === 1, '异常会员被安全隔离统计为 1 笔失败');
+assert(result.totalTransferred === '648.50', '结转总金额精确匹配 250.50 + 398.00 = 648.50');
+
+// 校验各个用户结转后的状态
+assert(initialUsers[0].commission === '0.00', '张三佣金归零');
+assert(initialUsers[0].money === '350.50', '张三商城余额从 100 增至 350.50');
+assert(initialUsers[1].commission === '0.00', '李四佣金归零');
+assert(initialUsers[1].money === '398.00', '李四商城余额从 0 增至 398.00');
+assert(initialUsers[2].commission === '0.00' && initialUsers[2].money === '50.00', '王五佣金本为0,保持不变');
+assert(initialUsers[3].commission === '120.00' && initialUsers[3].money === '80.00', '赵六发生异常,资金状态未被污染');
+
+// 资金守恒定理断言:结转前后总资产必须完全相等
+const finalTotalAssets = initialUsers.reduce((sum, u) => {
+ return sum + parseFloat(u.money) + parseFloat(u.commission);
+}, 0);
+assert(Math.abs(initialTotalAssets - finalTotalAssets) < 0.0001, '资金守恒断言:结转前后系统总资产完全守恒');
+
+// 幂等性测试断言:再次运行
+const result2 = mockTransferService(initialUsers.filter(u => !u.simulateError));
+assert(result2.successCount === 0, '幂等性断言:再次运行成功数为 0');
+assert(result2.totalTransferred === '0.00', '幂等性断言:再次运行结转金额为 0.00');
+
+console.log('\n========================================================');
+console.log(`测试执行结果: 通过 ${passCount} 项, 失败 ${failCount} 项`);
+console.log('========================================================');
+
+if (failCount > 0) {
+ process.exit(1);
+}
diff --git a/tests/partner/verify_wallet_records_alignment_spec.js b/tests/partner/verify_wallet_records_alignment_spec.js
new file mode 100644
index 0000000..c255b19
--- /dev/null
+++ b/tests/partner/verify_wallet_records_alignment_spec.js
@@ -0,0 +1,193 @@
+/**
+ * 钱包收益记录与端到端UI对齐专项测试
+ * 覆盖范围:
+ * 1. addons/shopro/controller/user/WalletLog.php 收益流水聚合及 tab=reward 筛选
+ * 2. application/admin/controller/shopro/user/WalletLog.php 后台佣金与收益流水联查
+ * 3. application/admin/view/shopro/user/user/detail.html 资产卡片与明细表格对齐
+ * 4. uniapp/pages/user/wallet/commission.vue 用户收益页面卡片/按钮/标签/提示文案对齐
+ * 5. uniapp/pages/user/wallet/money.vue 商城余额明细页面 tab 筛选与动态指示胶囊对齐
+ * 6. 核心行为算法模拟:历史流水+直入余额流水联查、收益分成专属过滤、收入支出统计守恒
+ */
+
+const fs = require('fs');
+const path = require('path');
+
+let passCount = 0;
+let failCount = 0;
+
+function assert(condition, message) {
+ if (condition) {
+ console.log(` \x1b[32m[PASS]\x1b[0m ${message}`);
+ passCount++;
+ } else {
+ console.error(` \x1b[31m[FAIL]\x1b[0m ${message}`);
+ failCount++;
+ }
+}
+
+console.log('========================================================');
+console.log(' 钱包收益记录与端到端UI对齐专项测试');
+console.log('========================================================\n');
+
+// ----------------------------------------------------
+// 1. 用户端 API: addons/shopro/controller/user/WalletLog.php
+// ----------------------------------------------------
+console.log('--- 1. 用户端 WalletLog 控制器聚合与对齐验证 ---');
+const userWalletLogPath = path.resolve(__dirname, '../../addons/shopro/controller/user/WalletLog.php');
+assert(fs.existsSync(userWalletLogPath), 'addons/shopro/controller/user/WalletLog.php 文件存在');
+if (fs.existsSync(userWalletLogPath)) {
+ const content = fs.readFileSync(userWalletLogPath, 'utf-8');
+ assert(content.includes("$type === 'commission'"), '包含对 commission 类型的特殊聚合处理');
+ assert(content.includes("where('type', 'commission')"), '查询条件包含历史佣金流水');
+ assert(content.includes("where('type', 'money')->whereIn('event', $rewardEvents)"), '查询条件联查直入余额的收益分成流水');
+ assert(content.includes("'transfer_by_commission'"), 'rewardEvents 包含转入商城余额事件 transfer_by_commission');
+ assert(content.includes("'transfer_to_money'"), 'rewardEvents 包含结转事件 transfer_to_money');
+ assert(content.includes("case 'reward':"), '支持 tab=reward 收益分成专属筛选');
+ assert(content.includes("'reward_income' => $rewardIncome"), '接口返回收益分成累计金额统计');
+}
+
+// ----------------------------------------------------
+// 2. 后台 API: application/admin/controller/shopro/user/WalletLog.php
+// ----------------------------------------------------
+console.log('\n--- 2. 后台 WalletLog 控制器佣金流水联查验证 ---');
+const adminWalletLogPath = path.resolve(__dirname, '../../application/admin/controller/shopro/user/WalletLog.php');
+assert(fs.existsSync(adminWalletLogPath), 'application/admin/controller/shopro/user/WalletLog.php 文件存在');
+if (fs.existsSync(adminWalletLogPath)) {
+ const content = fs.readFileSync(adminWalletLogPath, 'utf-8');
+ assert(content.includes('public function commission($id)'), '后台包含 commission($id) 接口');
+ assert(content.includes("where('type', 'commission')"), '后台查询历史佣金');
+ assert(content.includes("where('type', 'money')->whereIn('event', $rewardEvents)"), '后台同时联查收益分成直入余额记录');
+ assert(content.includes("'transfer_to_money'"), '后台 rewardEvents 包含 transfer_to_money');
+}
+
+// ----------------------------------------------------
+// 3. 后台详情视图: application/admin/view/shopro/user/user/detail.html
+// ----------------------------------------------------
+console.log('\n--- 3. 后台用户详情 detail.html 资产看板与明细对齐验证 ---');
+const detailHtmlPath = path.resolve(__dirname, '../../application/admin/view/shopro/user/user/detail.html');
+assert(fs.existsSync(detailHtmlPath), 'detail.html 文件存在');
+if (fs.existsSync(detailHtmlPath)) {
+ const content = fs.readFileSync(detailHtmlPath, 'utf-8');
+ assert(content.includes('收益中心累计收益 (元)'), '资产卡片1标题更新为「收益中心累计收益 (元)」');
+ assert(content.includes('当前商城余额: ¥{{ state.detail.money') || content.includes('已直入商城余额: ¥{{ state.detail.money'), '展示当前商城余额(已直入余额)字段');
+ assert(content.includes('历史未转佣金: ¥{{ state.detail.commission'), '展示历史未转佣金字段');
+ assert(content.includes('name="commission">佣金与收益流水') || content.includes('label="佣金与收益流水" name="commission"'), 'Tab 4 名称更新为「佣金与收益流水」');
+ assert(content.includes("label=\"钱包类别\" min-width=\"110\" v-if=\"log.tabActive=='commission'\""), '明细表格包含钱包类别列');
+ assert(content.includes("scope.row.type == 'money' ? '商城余额' : '佣金钱包'"), '钱包类别使用 Tag 区分商城余额与佣金钱包');
+}
+
+// ----------------------------------------------------
+// 4. uniapp 我的收益: uniapp/pages/user/wallet/commission.vue
+// ----------------------------------------------------
+console.log('\n--- 4. uniapp 我的收益页面 (commission.vue) 对齐验证 ---');
+const commissionVuePath = path.resolve(__dirname, '../../uniapp/pages/user/wallet/commission.vue');
+assert(fs.existsSync(commissionVuePath), 'commission.vue 文件存在');
+if (fs.existsSync(commissionVuePath)) {
+ const content = fs.readFileSync(commissionVuePath, 'utf-8');
+ assert(content.includes('商城可用余额(元)'), '顶部卡片主金额标题变更为「商城可用余额(元)」');
+ assert(content.includes('userInfo.money'), '顶部卡片主金额绑定为 userInfo.money');
+ assert(content.includes('/pages/goods/list'), '卡片操作区提供「去消费」直达按钮');
+ assert(content.includes('Number(userInfo.commission || 0) > 0'), '历史佣金大于0时才显示「转余额」操作');
+ assert(content.includes('累计总收益'), '次要指标区展示「累计总收益」');
+ assert(content.includes('待结算收益'), '次要指标区展示「待结算收益」');
+ assert(content.includes("name: '收益分成'") && content.includes("value: 'reward'"), 'tabMaps 增加「收益分成」(value: reward) 选项');
+ assert(content.includes("item.type === 'money'"), '流水记录列表对直入余额项进行识别');
+ assert(content.includes('已入余额'), '直入余额项显示「已入余额」专属胶囊标签');
+ assert(content.includes('收益分成已全额直接入账商城可用余额'), '底部提示文案明确说明收益直接入账商城余额');
+}
+
+// ----------------------------------------------------
+// 5. uniapp 钱包余额: uniapp/pages/user/wallet/money.vue
+// ----------------------------------------------------
+console.log('\n--- 5. uniapp 钱包余额页面 (money.vue) 对齐验证 ---');
+const moneyVuePath = path.resolve(__dirname, '../../uniapp/pages/user/wallet/money.vue');
+assert(fs.existsSync(moneyVuePath), 'money.vue 文件存在');
+if (fs.existsSync(moneyVuePath)) {
+ const content = fs.readFileSync(moneyVuePath, 'utf-8');
+ assert(content.includes("name: '收益分成'") && content.includes("value: 'reward'"), 'money.vue 的 tabMaps 增加「收益分成」筛选');
+ assert(content.includes('收益分成全额入账'), '动态指示胶囊说明收益分成全额入账可直接抵扣');
+}
+
+// ----------------------------------------------------
+// 6. 流水联查与筛选算法逻辑模拟
+// ----------------------------------------------------
+console.log('\n--- 6. 流水联查与聚合筛选算法模拟验证 ---');
+const rewardEvents = [
+ 'push3', 'maker_diff', 'maker_peer', 'maker_overtaken',
+ 'team_venue', 'team_county', 'venue_peer', 'county_peer',
+ 'venue_share', 'county_share', 'reward_income', 'transfer_by_commission', 'transfer_to_money'
+];
+
+// 模拟钱包日志混合数据池
+const mockLogs = [
+ { id: 1, type: 'commission', event: 'push3', amount: 398.00, memo: '推三返一' },
+ { id: 2, type: 'commission', event: 'transfer_to_money', amount: -398.00, memo: '结转至商城余额' },
+ { id: 3, type: 'money', event: 'transfer_by_commission', amount: 398.00, memo: '收益结转入账' },
+ { id: 4, type: 'money', event: 'recharge', amount: 100.00, memo: '微信充值' },
+ { id: 5, type: 'money', event: 'team_venue', amount: 50.00, memo: '酒道馆订单奖' },
+ { id: 6, type: 'money', event: 'consume', amount: -120.00, memo: '商城订单支付' },
+ { id: 7, type: 'score', event: 'sign', amount: 10, memo: '每日签到' },
+];
+
+function queryWalletLogs(type, tab) {
+ // 模拟 WalletLog 控制器 index 逻辑
+ return mockLogs.filter(log => {
+ // 1. 类型过滤 / commission 聚合
+ if (type === 'commission') {
+ const isCommission = log.type === 'commission';
+ const isMoneyReward = log.type === 'money' && rewardEvents.includes(log.event);
+ if (!isCommission && !isMoneyReward) return false;
+ } else {
+ if (log.type !== type) return false;
+ }
+
+ // 2. tab 筛选
+ if (tab === 'reward') {
+ return log.amount > 0 && rewardEvents.includes(log.event);
+ } else if (tab === 'income') {
+ return log.amount > 0;
+ } else if (tab === 'expense') {
+ return log.amount < 0;
+ }
+ return true;
+ });
+}
+
+// 6.1 在 commission 页面查询全部
+const commissionAll = queryWalletLogs('commission', 'all');
+assert(commissionAll.length === 4, `commission 页面查全部包含 4 条记录 (实际: ${commissionAll.length})`);
+assert(commissionAll.some(l => l.id === 1 && l.type === 'commission'), '包含历史佣金记录 ID=1');
+assert(commissionAll.some(l => l.id === 2 && l.type === 'commission'), '包含佣金转出记录 ID=2');
+assert(commissionAll.some(l => l.id === 3 && l.type === 'money'), '包含收益转余额记录 ID=3');
+assert(commissionAll.some(l => l.id === 5 && l.type === 'money'), '包含直入余额收益记录 ID=5');
+assert(!commissionAll.some(l => l.id === 4), '不包含普通微信充值记录 ID=4');
+assert(!commissionAll.some(l => l.id === 6), '不包含普通消费扣款记录 ID=6');
+
+// 6.2 在 commission 页面筛选收益分成 (tab=reward)
+const commissionReward = queryWalletLogs('commission', 'reward');
+assert(commissionReward.length === 3, `commission 页面筛选收益分成包含 3 条记录 (实际: ${commissionReward.length})`);
+assert(commissionReward.every(l => l.amount > 0), '收益分成记录金额全部为正');
+assert(!commissionReward.some(l => l.id === 2), '不包含负向转出记录');
+
+// 6.3 在 money 页面筛选收益分成 (tab=reward)
+const moneyReward = queryWalletLogs('money', 'reward');
+assert(moneyReward.length === 2, `money 页面筛选收益分成包含 2 条直入余额收益记录 (实际: ${moneyReward.length})`);
+assert(moneyReward.some(l => l.id === 3), '包含转余额入账 ID=3');
+assert(moneyReward.some(l => l.id === 5), '包含直推酒道馆收益 ID=5');
+assert(!moneyReward.some(l => l.id === 4), '不包含充值 ID=4');
+
+// 6.4 收入支出统计
+const totalIncome = commissionAll.filter(l => l.amount > 0).reduce((sum, l) => sum + l.amount, 0);
+const totalExpense = commissionAll.filter(l => l.amount < 0).reduce((sum, l) => sum + l.amount, 0);
+assert(totalIncome === (398.00 + 398.00 + 50.00), `收益中心统计总收入正确 (¥${totalIncome})`);
+assert(totalExpense === -398.00, `收益中心统计总支出正确 (¥${totalExpense})`);
+
+console.log('\n========================================================');
+console.log(`测试完成!通过: \x1b[32m${passCount}\x1b[0m, 失败: \x1b[31m${failCount}\x1b[0m`);
+console.log('========================================================');
+
+if (failCount > 0) {
+ process.exit(1);
+} else {
+ process.exit(0);
+}
diff --git a/tests/verify_custom_role_names_spec.js b/tests/verify_custom_role_names_spec.js
index bfd20ff..3a32ef4 100644
--- a/tests/verify_custom_role_names_spec.js
+++ b/tests/verify_custom_role_names_spec.js
@@ -35,8 +35,12 @@ if (fs.existsSync(partnerConfigPath)) {
assert(content.includes('partner_role_name_county'), 'roleNames() 包含区县别名读取');
assert(content.includes('partner_role_name_partner'), 'roleNames() 包含合伙股东别名读取');
assert(content.includes("$maker . '进阶奖'"), 'rewardTypeMap() 动态拼接创客进阶奖');
- assert(content.includes("$venue . '运营奖'"), 'rewardTypeMap() 动态拼接酒道馆运营奖');
- assert(content.includes("$county . '统筹奖'"), 'rewardTypeMap() 动态拼接区县统筹奖');
+ assert(content.includes("$venue . '管理奖'"), 'rewardTypeMap() 动态拼接酒道馆管理奖');
+ assert(content.includes("$countyShort . '管理奖'"), 'rewardTypeMap() 动态拼接区县管理奖');
+ assert(content.includes("$venue . '平级奖'"), 'rewardTypeMap() 动态拼接酒道馆平级奖');
+ assert(content.includes("$countyAgentShort . '平级奖'"), 'rewardTypeMap() 动态拼接区县代平级奖');
+ assert(content.includes("$venue . '加权奖'"), 'rewardTypeMap() 动态拼接酒道馆加权奖');
+ assert(content.includes("$countyShort . '加权奖'"), 'rewardTypeMap() 动态拼接区县加权奖');
}
// 2. IdentityService.php 验证
diff --git a/tests/verify_score_shop_voucher_integration_spec.js b/tests/verify_score_shop_voucher_integration_spec.js
index e65fb0f..e1666b2 100644
--- a/tests/verify_score_shop_voucher_integration_spec.js
+++ b/tests/verify_score_shop_voucher_integration_spec.js
@@ -117,7 +117,7 @@ const addHtmlPath = path.resolve(__dirname, '../application/admin/view/shopro/ap
assert(fs.existsSync(addHtmlPath), 'add.html 文件存在');
if (fs.existsSync(addHtmlPath)) {
const content = fs.readFileSync(addHtmlPath, 'utf-8');
- assert(content.includes('所需兑换券(元)'), 'add.html 包含所需兑换券表头');
+ assert(content.includes('所需兑换券(个)') || content.includes('所需兑换券(元)'), 'add.html 包含所需兑换券表头');
assert(content.includes('所需积分'), 'add.html 包含所需积分表头');
}
diff --git a/tests/verify_voucher_unit_ge_spec.js b/tests/verify_voucher_unit_ge_spec.js
new file mode 100644
index 0000000..ac32496
--- /dev/null
+++ b/tests/verify_voucher_unit_ge_spec.js
@@ -0,0 +1,132 @@
+const fs = require('fs');
+const path = require('path');
+
+let passed = 0;
+let failed = 0;
+
+function assert(condition, message, detail = '') {
+ if (condition) {
+ console.log(` [PASS] ${message}`);
+ passed++;
+ } else {
+ console.error(` [FAIL] ${message} ${detail ? '-> ' + detail : ''}`);
+ failed++;
+ }
+}
+
+console.log("========================================================");
+console.log(" 兑换券单位全面更名为「个」(UniApp + 后台系统)测试");
+console.log("========================================================\n");
+
+// 1. UniApp - s-wallet-card.vue
+const walletCardPath = path.resolve(__dirname, '../uniapp/sheep/components/s-wallet-card/s-wallet-card.vue');
+assert(fs.existsSync(walletCardPath), 's-wallet-card.vue 存在');
+if (fs.existsSync(walletCardPath)) {
+ const content = fs.readFileSync(walletCardPath, 'utf-8');
+ assert(content.includes("voucher_available") && content.includes('个'), 's-wallet-card.vue 兑换券单位为「个」');
+ assert(!content.includes("userInfo?.voucher_available || '0.00' }}\n 元"), 's-wallet-card.vue 兑换券不再展示「元」');
+}
+
+// 2. UniApp - account-info.vue
+const accountInfoPath = path.resolve(__dirname, '../uniapp/pages/commission/components/account-info.vue');
+assert(fs.existsSync(accountInfoPath), 'account-info.vue 存在');
+if (fs.existsSync(accountInfoPath)) {
+ const content = fs.readFileSync(accountInfoPath, 'utf-8');
+ assert(content.includes('可用兑换券(个)'), 'account-info.vue 收益中心卡片展示「可用兑换券(个)」');
+ assert(!content.includes('可用兑换券(元)'), 'account-info.vue 不再包含「可用兑换券(元)」');
+}
+
+// 3. UniApp - voucher.vue
+const voucherVuePath = path.resolve(__dirname, '../uniapp/pages/commission/voucher.vue');
+assert(fs.existsSync(voucherVuePath), 'voucher.vue 存在');
+if (fs.existsSync(voucherVuePath)) {
+ const content = fs.readFileSync(voucherVuePath, 'utf-8');
+ assert(content.includes('个'), 'voucher.vue 核心大字数单位展示为「个」');
+ assert(!content.includes('¥'), 'voucher.vue 核心大字数移除「¥」符号');
+ assert(content.includes('个'), 'voucher.vue 底部统计数据栏使用「个」单位');
+ assert(content.includes("{{ item.total_amount || item.total || item.voucher_total || '0.00' }} 个"), 'voucher.vue 计划列表卡片使用「个」单位');
+}
+
+// 4. UniApp - confirm.vue
+const confirmVuePath = path.resolve(__dirname, '../uniapp/pages/order/confirm.vue');
+assert(fs.existsSync(confirmVuePath), 'confirm.vue 存在');
+if (fs.existsSync(confirmVuePath)) {
+ const content = fs.readFileSync(confirmVuePath, 'utf-8');
+ assert(content.includes('{{ state.orderInfo.voucher_available || \'0.00\' }} 个'), 'confirm.vue 抵扣弹窗展示可用数量「个」');
+ assert(content.includes('确定使用 ${voucherFee} 个兑换券'), 'confirm.vue 兑换确认弹窗提示为「个兑换券」');
+ assert(!content.includes('元兑换券'), 'confirm.vue 不再出现「元兑换券」字样');
+}
+
+// 5. UniApp - score-shop.vue
+const scoreShopVuePath = path.resolve(__dirname, '../uniapp/pages/app/score-shop.vue');
+assert(fs.existsSync(scoreShopVuePath), 'score-shop.vue 存在');
+if (fs.existsSync(scoreShopVuePath)) {
+ const content = fs.readFileSync(scoreShopVuePath, 'utf-8');
+ assert(content.includes('{{ userVoucherAvailable }} 个'), 'score-shop.vue 资产看板兑换券单位为「个」');
+ assert(!content.includes('¥ {{ userVoucherAvailable }}'), 'score-shop.vue 资产看板不带「¥」');
+}
+
+// 6. 后台 - dashboard/index.html
+const dashboardPath = path.resolve(__dirname, '../application/admin/view/shopro/dashboard/index.html');
+assert(fs.existsSync(dashboardPath), 'dashboard/index.html 存在');
+if (fs.existsSync(dashboardPath)) {
+ const content = fs.readFileSync(dashboardPath, 'utf-8');
+ assert(content.includes('{{ state.voucher.total_voucher }} 个'), 'dashboard/index.html 总补贴券单位为「个」');
+ assert(content.includes('{{ state.voucher.released_voucher }} 个'), 'dashboard/index.html 已释放补贴券单位为「个」');
+ assert(content.includes('{{ state.voucher.pending_voucher }} 个'), 'dashboard/index.html 待释放补贴券单位为「个」');
+ assert(!content.includes('¥{{ state.voucher.total_voucher }}'), 'dashboard/index.html 补贴券不再展示「¥」');
+}
+
+// 7. 后台 - user/recharge.html
+const rechargePath = path.resolve(__dirname, '../application/admin/view/shopro/user/user/recharge.html');
+assert(fs.existsSync(rechargePath), 'recharge.html 存在');
+if (fs.existsSync(rechargePath)) {
+ const content = fs.readFileSync(rechargePath, 'utf-8');
+ assert(content.includes('兑换券 (个)'), 'recharge.html 单选类型为「兑换券 (个)」');
+ assert(!content.includes('兑换券 (元)'), 'recharge.html 不再包含「兑换券 (元)」');
+ assert(content.includes('{{ state.user.voucher_available || \'0.00\' }} 个'), 'recharge.html 可用兑换券为「个」');
+}
+
+// 8. 后台 - user/detail.html
+const userDetailPath = path.resolve(__dirname, '../application/admin/view/shopro/user/user/detail.html');
+assert(fs.existsSync(userDetailPath), 'user/detail.html 存在');
+if (fs.existsSync(userDetailPath)) {
+ const content = fs.readFileSync(userDetailPath, 'utf-8');
+ assert(content.includes('充值兑换券 (个)'), 'user/detail.html 充值下拉菜单为「充值兑换券 (个)」');
+ assert(!content.includes('充值兑换券 (元)'), 'user/detail.html 不再包含「充值兑换券 (元)」');
+ assert(content.includes('{{ state.detail.voucher_available || \'0.00\' }} 个'), 'user/detail.html 商城可用兑换券卡片单位为「个」');
+ assert(content.includes('{{ scope.row.total_fee }} 个'), 'user/detail.html 计划总额列使用「个」');
+}
+
+// 9. 后台 - partner/voucher/index.html
+const partnerVoucherPath = path.resolve(__dirname, '../application/admin/view/shopro/partner/voucher/index.html');
+assert(fs.existsSync(partnerVoucherPath), 'partner/voucher/index.html 存在');
+if (fs.existsSync(partnerVoucherPath)) {
+ const content = fs.readFileSync(partnerVoucherPath, 'utf-8');
+ assert(content.includes('label="总额(个)"'), 'partner/voucher/index.html 包含「总额(个)」表头');
+ assert(content.includes('label="每天释放(个)"'), 'partner/voucher/index.html 包含「每天释放(个)」表头');
+ assert(content.includes('label="释放数量(个)"'), 'partner/voucher/index.html 包含「释放数量(个)」明细表头');
+}
+
+// 10. 后台 - app/score_shop/add.html & ScoreSkuPrice.php
+const scoreShopAddPath = path.resolve(__dirname, '../application/admin/view/shopro/app/score_shop/add.html');
+assert(fs.existsSync(scoreShopAddPath), 'score_shop/add.html 存在');
+if (fs.existsSync(scoreShopAddPath)) {
+ const content = fs.readFileSync(scoreShopAddPath, 'utf-8');
+ assert(content.includes('所需兑换券(个)'), 'score_shop/add.html 包含「所需兑换券(个)」表头');
+ assert(!content.includes('所需兑换券(元)'), 'score_shop/add.html 不再包含「所需兑换券(元)」');
+}
+
+const scoreSkuPricePath = path.resolve(__dirname, '../application/admin/model/shopro/app/ScoreSkuPrice.php');
+assert(fs.existsSync(scoreSkuPricePath), 'ScoreSkuPrice.php 存在');
+if (fs.existsSync(scoreSkuPricePath)) {
+ const content = fs.readFileSync(scoreSkuPricePath, 'utf-8');
+ assert(content.includes("return $price . ' 个兑换券'"), 'ScoreSkuPrice.php 输出格式为「个兑换券」');
+ assert(!content.includes("return '¥' . $price . ' 兑换券'"), 'ScoreSkuPrice.php 不再输出「¥」');
+}
+
+console.log("\n--------------------------------------------------------");
+console.log(`测试执行结果: 通过 ${passed} 项, 失败 ${failed} 项`);
+console.log("========================================================");
+
+process.exit(failed === 0 ? 0 : 1);
diff --git a/uniapp/pages/app/score-shop.vue b/uniapp/pages/app/score-shop.vue
index 3e05b83..4779402 100644
--- a/uniapp/pages/app/score-shop.vue
+++ b/uniapp/pages/app/score-shop.vue
@@ -10,7 +10,7 @@
可用兑换券
- ¥ {{ userVoucherAvailable }}
+ {{ userVoucherAvailable }} 个
-
+
@@ -26,20 +26,20 @@
累计收益(元)
- {{ state.showMoney ? agentInfo?.total_income || '0.00' : '***' }}
+ {{ state.showMoney ? agentInfo?.total_income || userInfo?.total_income || '0.00' : '***' }}
- 可提现(元)
+ 商城余额(元)
- {{ state.showMoney ? userInfo?.commission || '0.00' : '***' }}
+ {{ state.showMoney ? userInfo?.money || '0.00' : '***' }}
@@ -61,7 +61,7 @@
class="ss-flex-1 ss-flex-col ss-col-center metric-item"
@tap="sheep.$router.go('/pages/commission/voucher')"
>
- 可用兑换券(元)
+ 可用兑换券(个)
{{
state.showMoney
diff --git a/uniapp/pages/commission/index.vue b/uniapp/pages/commission/index.vue
index 1c9e107..96e1764 100644
--- a/uniapp/pages/commission/index.vue
+++ b/uniapp/pages/commission/index.vue
@@ -143,8 +143,8 @@
title: '收益奖励已暂停',
badge: '暂缓计奖',
btnText: '去选购恢复',
- tip: '次日完成任意一笔本人自购订单即可自动恢复保活',
- lastOrderText: lastTime > 0 ? `上次自购: ${dayjs(lastTime * 1000).format('MM-DD HH:mm')}` : '暂无自购记录',
+ tip: '完成任意一笔自购或直推订单即可自动恢复保活',
+ lastOrderText: lastTime > 0 ? `上次有效订单: ${dayjs(lastTime * 1000).format('MM-DD HH:mm')}` : '暂无有效订单记录',
};
}
@@ -164,8 +164,8 @@
title: '收益奖励已暂停',
badge: '暂缓计奖',
btnText: '去选购恢复',
- tip: '近30日暂无本人自购,次日下单后即可自动激活',
- lastOrderText: lastTime > 0 ? `上次自购: ${dayjs(lastTime * 1000).format('MM-DD HH:mm')}` : '',
+ tip: '近30日暂无自购或直推订单,完成下单后即可自动激活',
+ lastOrderText: lastTime > 0 ? `上次有效订单: ${dayjs(lastTime * 1000).format('MM-DD HH:mm')}` : '',
};
}
@@ -175,8 +175,8 @@
title: '30天保活临期预警',
badge: `仅剩 ${remainingDays} 天`,
btnText: '立即自购保活',
- tip: '即将暂停分润!请在倒计时结束前完成自购续期',
- lastOrderText: lastTime > 0 ? `上次自购: ${dayjs(lastTime * 1000).format('MM-DD HH:mm')}` : '',
+ tip: '即将暂停分润!请在倒计时结束前完成自购或直推续期',
+ lastOrderText: lastTime > 0 ? `上次有效订单: ${dayjs(lastTime * 1000).format('MM-DD HH:mm')}` : '',
};
}
@@ -185,8 +185,8 @@
title: '30天收益资格生效中',
badge: '正常活跃',
btnText: '去自购续期',
- tip: '已满足30日保活,享有全部团队分红与奖项',
- lastOrderText: lastTime > 0 ? `上次自购: ${dayjs(lastTime * 1000).format('MM-DD HH:mm')}` : '',
+ tip: '已满足30日保活(自购/直推有效),享有全部团队分红与奖项',
+ lastOrderText: lastTime > 0 ? `上次有效订单: ${dayjs(lastTime * 1000).format('MM-DD HH:mm')}` : '',
};
});
diff --git a/uniapp/pages/commission/madow.vue b/uniapp/pages/commission/madow.vue
index 71a8877..cacdb66 100644
--- a/uniapp/pages/commission/madow.vue
+++ b/uniapp/pages/commission/madow.vue
@@ -22,7 +22,10 @@
-
+
马币市值 1 马豆 ≈ {{ state.info.madow_rate || '0' }} 元
@@ -178,6 +181,7 @@
madow: sheep.$store('user').userInfo?.madow || '0',
madow_rate: sheep.$store('user').userInfo?.madow_rate || '0',
madow_value: sheep.$store('user').userInfo?.madow_value || '0.00',
+ show_madow_rate: sheep.$store('user').userInfo?.show_madow_rate ?? 1,
},
list: [],
inited: false,
@@ -214,6 +218,9 @@
state.info.madow = data.madow ?? state.info.madow;
state.info.madow_rate = data.madow_rate ?? state.info.madow_rate;
state.info.madow_value = data.madow_value ?? state.info.madow_value;
+ if (data.show_madow_rate !== undefined) {
+ state.info.show_madow_rate = data.show_madow_rate;
+ }
const list = pickList(data);
state.list = page === 1 ? list : _.concat(state.list, list);
state.page = data.list?.current_page || data.current_page || page;
diff --git a/uniapp/pages/commission/rewards.vue b/uniapp/pages/commission/rewards.vue
index e27ec2e..1fbf544 100644
--- a/uniapp/pages/commission/rewards.vue
+++ b/uniapp/pages/commission/rewards.vue
@@ -3,11 +3,12 @@
@@ -472,10 +472,10 @@
-
+
- 30天自购保活状态
+ 30天收益保活状态
- 最近本人下单时间:
+ 最近有效订单时间:
{{ formatLastOrderTime(state.currentMember.agent?.last_self_order_time) }}
- 推荐开拓序号:
+ 推荐提成序号:
{{ state.currentMember.agent?.direct_order_seq !== undefined ? '当前第 ' + state.currentMember.agent.direct_order_seq + ' 单' : '0 单' }}
diff --git a/uniapp/pages/commission/voucher.vue b/uniapp/pages/commission/voucher.vue
index d7133f4..0f1eeca 100644
--- a/uniapp/pages/commission/voucher.vue
+++ b/uniapp/pages/commission/voucher.vue
@@ -18,9 +18,8 @@
- ¥
{{ state.info.voucher_available || '0.00' }}
- 元
+ 个
@@ -102,7 +101,7 @@
- ¥{{ item.total_amount || item.total || item.voucher_total || '0.00' }}
+ {{ item.total_amount || item.total || item.voucher_total || '0.00' }} 个
{{ item.status_text || (item.status === 2 ? '已完成' : '释放中') }}
@@ -123,13 +122,13 @@
- 已释放 ¥{{ item.released_amount || item.released || '0.00' }}
+ 已释放 {{ item.released_amount || item.released || '0.00' }} 个
- 每日释放 ¥{{ item.daily_amount }}
+ 每日释放 {{ item.daily_amount }} 个
- 待释放 ¥{{ calcRemain(item) }}
+ 待释放 {{ calcRemain(item) }} 个
@@ -448,6 +447,14 @@
color: #f4d397;
font-family: OPPOSANS, DIN, -apple-system, sans-serif;
}
+
+ .metric-unit {
+ font-size: 22rpx;
+ color: #f4d397;
+ font-weight: 500;
+ margin-left: 4rpx;
+ margin-bottom: 2rpx;
+ }
}
}
}
diff --git a/uniapp/pages/order/confirm.vue b/uniapp/pages/order/confirm.vue
index b684373..2a18de9 100644
--- a/uniapp/pages/order/confirm.vue
+++ b/uniapp/pages/order/confirm.vue
@@ -101,7 +101,7 @@
兑换券
-¥{{ state.orderInfo.voucher_fee || state.orderPayload.voucher_fee }}-{{ state.orderInfo.voucher_fee || state.orderPayload.voucher_fee }} 个
{{
Number(state.orderInfo.voucher_available) > 0
- ? '可用 ¥' + (state.orderInfo.voucher_available || '0.00')
+ ? '可用 ' + (state.orderInfo.voucher_available || '0.00') + ' 个'
: '暂无可用兑换券'
}}
@@ -169,17 +169,17 @@
兑换券抵扣
- 可用 ¥{{ state.orderInfo.voucher_available || '0.00' }},先用商城优惠券再用兑换券
+ 可用 {{ state.orderInfo.voucher_available || '0.00' }} 个,先用商城优惠券再用兑换券
- ¥
+ 个
+
+
+
+
+
+
+
+
+ 余额混合支付
+ 组合立省
+
+
+ 可用余额 ¥{{ userBalance.toFixed(2) }},本单可抵扣 ¥{{ deductMoney.toFixed(2) }}
+
+
+
+
+
+
+
+
推荐
+
+
+ 需付 ¥{{ thirdPayFee.toFixed(2) }}
+
@@ -140,6 +196,15 @@
余额充足
+
+
+
+ 已启用抵扣
+
+
去充值
+
+
+
+ 开启混合支付
+
+
支付后剩余 ¥{{ remainingBalance }}
@@ -254,29 +329,85 @@ const userBalance = computed(() => {
return isNaN(m) ? 0 : m;
});
-const payFee = computed(() => {
+// 订单原始总应付金额
+const payTotalFee = computed(() => {
const f = parseFloat(state.orderInfo?.pay_fee);
return isNaN(f) ? 0 : f;
});
-// 余额是否充足
-const isBalanceEnough = computed(() => {
- return userBalance.value >= payFee.value && payFee.value > 0;
+// 订单当前剩余应付金额
+const remainPayFee = computed(() => {
+ const rf =
+ state.orderInfo?.remain_pay_fee !== undefined
+ ? parseFloat(state.orderInfo.remain_pay_fee)
+ : parseFloat(state.orderInfo?.pay_fee);
+ return isNaN(rf) ? 0 : Math.max(0, rf);
});
-// 扣减后剩余余额
+// 历史已付金额
+const alreadyPaidFee = computed(() => {
+ const diff = payTotalFee.value - remainPayFee.value;
+ return diff > 0 ? diff : 0;
+});
+
+// 余额是否充足(是否可全额支付当前剩余待付金额)
+const isBalanceEnough = computed(() => {
+ return userBalance.value >= remainPayFee.value && remainPayFee.value > 0;
+});
+
+// 扣减后剩余余额(若全额使用余额支付)
const remainingBalance = computed(() => {
- const rem = userBalance.value - payFee.value;
+ const rem = userBalance.value - remainPayFee.value;
return rem >= 0 ? rem.toFixed(2) : "0.00";
});
+// 是否支持混合支付:商品订单、有可用余额但不足以全额付清、且剩余待付款大于0
+const isMixedPaySupported = computed(() => {
+ return (
+ state.orderType === "goods" &&
+ userBalance.value > 0 &&
+ userBalance.value < remainPayFee.value &&
+ remainPayFee.value > 0
+ );
+});
+
+// 用户是否启用混合支付开关(满足条件时默认开启)
+const useMixedPay = ref(true);
+
+// 混合支付下抵扣的余额金额
+const deductMoney = computed(() => {
+ if (!isMixedPaySupported.value || !useMixedPay.value) return 0;
+ return Math.min(userBalance.value, remainPayFee.value);
+});
+
+// 混合支付下第三方渠道需补足支付的金额
+const thirdPayFee = computed(() => {
+ const diff = remainPayFee.value - deductMoney.value;
+ return diff > 0 ? diff : 0;
+});
+
+// 顶部卡片展示的当前待付金额
+const currentDueFee = computed(() => {
+ if (isMixedPaySupported.value && useMixedPay.value && deductMoney.value > 0) {
+ return thirdPayFee.value;
+ }
+ return remainPayFee.value;
+});
+
+// 顶部金额标题
+const currentAmountLabel = computed(() => {
+ if (isMixedPaySupported.value && useMixedPay.value && deductMoney.value > 0) {
+ return "在线待付金额";
+ }
+ if (alreadyPaidFee.value > 0) {
+ return "剩余应付金额";
+ }
+ return "应付金额";
+});
+
// 金额拆分整数与小数
const priceParts = computed(() => {
- const fee = state.orderInfo?.pay_fee;
- if (fee === undefined || fee === null || fee === "") {
- return { integer: "0", decimal: "00" };
- }
- const num = parseFloat(fee);
+ const num = currentDueFee.value;
if (isNaN(num)) return { integer: "0", decimal: "00" };
const str = num.toFixed(2);
const parts = str.split(".");
@@ -382,6 +513,18 @@ const orderSubject = computed(() => {
return state.orderInfo.order_title || "商城商品订单";
});
+// 支付方式名称映射
+function getMethodTitle(val) {
+ const map = {
+ wechat: "微信支付",
+ alipay: "支付宝",
+ money: "余额支付",
+ apple: "Apple Pay",
+ offline: "货到付款",
+ };
+ return map[val] || "在线支付";
+}
+
// 3. 实时平滑倒计时逻辑
let countdownTimer = null;
const countdownStr = ref("");
@@ -442,9 +585,39 @@ function autoSelectPayment() {
return;
}
+ // 若开启了混合支付,优先确保选择有效的第三方通道(非 money)
+ if (isMixedPaySupported.value && useMixedPay.value) {
+ if (
+ state.payment &&
+ state.payment !== "money" &&
+ list.some((item) => item.value === state.payment)
+ ) {
+ return;
+ }
+
+ if (isWechatEnv.value) {
+ const wechatItem = list.find((item) => item.value === "wechat");
+ if (wechatItem) {
+ state.payment = "wechat";
+ return;
+ }
+ }
+
+ const alipayItem = list.find((item) => item.value === "alipay");
+ if (alipayItem) {
+ state.payment = "alipay";
+ return;
+ }
+
+ const nonMoneyItem = list.find((item) => item.value !== "money");
+ if (nonMoneyItem) {
+ state.payment = nonMoneyItem.value;
+ return;
+ }
+ }
+
// 若当前已选中的支付方式依然可用且非禁用
if (state.payment && list.some((item) => item.value === state.payment)) {
- // 若当前选中的是余额支付但余额不足,尝试智能自动切换至微信支付
if (state.payment === "money" && !isBalanceEnough.value) {
const wechatItem = list.find((item) => item.value === "wechat");
if (wechatItem) {
@@ -500,7 +673,47 @@ function autoSelectPayment() {
state.payment = list[0].value;
}
-// 5. 点击切换支付方式
+// 5. 混合支付开关与交互
+function onToggleMixedPay(e) {
+ useMixedPay.value = e.detail.value;
+ if (useMixedPay.value) {
+ if (state.payment === "money") {
+ const nonMoney = visiblePayMethods.value.find(
+ (i) => !i.disabled && i.value !== "money",
+ );
+ if (nonMoney) {
+ state.payment =
+ isWechatEnv.value &&
+ visiblePayMethods.value.some((i) => i.value === "wechat")
+ ? "wechat"
+ : nonMoney.value;
+ }
+ }
+ } else {
+ if (state.payment === "money" && !isBalanceEnough.value) {
+ sheep.$helper.toast("已关闭混合支付,当前可用余额不足以全额付款");
+ }
+ }
+}
+
+function onEnableMixedPay() {
+ useMixedPay.value = true;
+ if (state.payment === "money") {
+ const nonMoney = visiblePayMethods.value.find(
+ (i) => !i.disabled && i.value !== "money",
+ );
+ if (nonMoney) {
+ state.payment =
+ isWechatEnv.value &&
+ visiblePayMethods.value.some((i) => i.value === "wechat")
+ ? "wechat"
+ : nonMoney.value;
+ }
+ }
+ sheep.$helper.toast("已开启余额混合支付");
+}
+
+// 6. 点击切换支付方式
function onSelectPay(item) {
if (item.disabled) {
if (item.value === "offline") {
@@ -508,26 +721,55 @@ function onSelectPay(item) {
}
return;
}
- state.payment = item.value;
- if (item.value === "money" && !isBalanceEnough.value) {
+ if (item.value === "money") {
+ if (isBalanceEnough.value) {
+ state.payment = "money";
+ return;
+ }
+
+ if (isMixedPaySupported.value) {
+ if (!useMixedPay.value) {
+ uni.showModal({
+ title: "混合支付推荐",
+ content: `当前可用余额 ¥${userBalance.value.toFixed(2)} 不足全额支付该订单(待付 ¥${remainPayFee.value.toFixed(2)})。是否启用【余额混合支付】抵扣 ¥${deductMoney.value.toFixed(2)},剩余 ¥${(remainPayFee.value - deductMoney.value).toFixed(2)} 通过微信/支付宝支付?`,
+ confirmText: "启用混合支付",
+ cancelText: "去充值",
+ confirmColor: "var(--ui-BG-Main, #ff3000)",
+ success: (res) => {
+ if (res.confirm) {
+ onEnableMixedPay();
+ } else {
+ onGoRecharge();
+ }
+ },
+ });
+ } else {
+ sheep.$helper.toast("已启用余额抵扣,请选择补足方式(如微信或支付宝)");
+ }
+ return;
+ }
+
sheep.$helper.toast("可用余额不足,可前往充值或切换支付方式");
+ return;
}
+
+ state.payment = item.value;
}
-// 6. 复制订单号
+// 7. 复制订单号
function onCopyOrderSn() {
if (!state.orderInfo?.order_sn) return;
sheep.$helper.copyText(state.orderInfo.order_sn);
sheep.$helper.toast("订单号已复制");
}
-// 7. 前往充值页面
+// 8. 前往充值页面
function onGoRecharge() {
sheep.$router.go("/pages/pay/recharge");
}
-// 8. 检查订单支付状态
+// 9. 检查订单支付状态
function checkPayStatus() {
if (state.orderInfo.status === "unpaid") {
state.payStatus = 1;
@@ -547,7 +789,7 @@ function checkPayStatus() {
state.payStatus = 2;
}
-// 9. 获取充值订单信息
+// 10. 获取充值订单信息
async function setRechargeOrder(id) {
state.loading = true;
try {
@@ -567,7 +809,7 @@ async function setRechargeOrder(id) {
}
}
-// 10. 获取商品订单信息
+// 11. 获取商品订单信息
async function setGoodsOrder(id) {
state.loading = true;
try {
@@ -598,7 +840,7 @@ async function setGoodsOrder(id) {
}
}
-// 11. 底部按钮文案与可用性计算
+// 12. 底部按钮文案与可用性计算
const isButtonDisabled = computed(() => {
if (state.isSubmitting) return true;
if (state.payStatus !== 1) return true;
@@ -624,13 +866,19 @@ const payButtonText = computed(() => {
// 选中余额支付且余额不足时
if (state.payment === "money" && !isBalanceEnough.value) {
- return "余额不足 · 去充值";
+ return isMixedPaySupported.value ? "余额不足 · 开启混合支付" : "余额不足 · 去充值";
}
- return `立即支付 ¥${payFee.value.toFixed(2)}`;
+ // 混合支付模式
+ if (isMixedPaySupported.value && useMixedPay.value && deductMoney.value > 0) {
+ const title = getMethodTitle(state.payment);
+ return `混合支付 · ${title}付 ¥${thirdPayFee.value.toFixed(2)} (已抵余额¥${deductMoney.value.toFixed(2)})`;
+ }
+
+ return `立即支付 ¥${remainPayFee.value.toFixed(2)}`;
});
-// 12. 支付流程核心入口
+// 13. 支付流程核心入口
async function onPay() {
if (state.payStatus !== 1 || state.isSubmitting) return;
@@ -639,27 +887,58 @@ async function onPay() {
return;
}
- // 余额支付流程
+ // 混合支付流程
+ if (isMixedPaySupported.value && useMixedPay.value && deductMoney.value > 0) {
+ if (state.payment === "money") {
+ sheep.$helper.toast("请选择补足剩余款项的支付通道(微信或支付宝)");
+ return;
+ }
+ if (state.payment === "offline") {
+ sheep.$helper.toast("货到付款不支持混合支付");
+ return;
+ }
+
+ const title = getMethodTitle(state.payment);
+ uni.showModal({
+ title: "混合支付确认",
+ content: `本次将扣除账户可用余额 ¥${deductMoney.value.toFixed(2)},并通过 ${title} 支付剩余 ¥${thirdPayFee.value.toFixed(2)}。确定发起支付吗?`,
+ confirmText: "确认支付",
+ cancelText: "取消",
+ confirmColor: "var(--ui-BG-Main, #ff3000)",
+ success: (res) => {
+ if (res.confirm) {
+ submitPayment({ money: deductMoney.value });
+ }
+ },
+ });
+ return;
+ }
+
+ // 纯余额支付流程
if (state.payment === "money") {
if (!isBalanceEnough.value) {
- uni.showModal({
- title: "余额不足",
- content: `当前可用余额为 ¥${userBalance.value.toFixed(2)},不足以支付该订单(¥${payFee.value.toFixed(2)}),是否前往充值?`,
- confirmText: "去充值",
- cancelText: "取消",
- confirmColor: "var(--ui-BG-Main, #ff3000)",
- success: (res) => {
- if (res.confirm) {
- onGoRecharge();
- }
- },
- });
+ if (isMixedPaySupported.value) {
+ onEnableMixedPay();
+ } else {
+ uni.showModal({
+ title: "余额不足",
+ content: `当前可用余额为 ¥${userBalance.value.toFixed(2)},不足以支付该订单(待付 ¥${remainPayFee.value.toFixed(2)}),是否前往充值?`,
+ confirmText: "去充值",
+ cancelText: "取消",
+ confirmColor: "var(--ui-BG-Main, #ff3000)",
+ success: (res) => {
+ if (res.confirm) {
+ onGoRecharge();
+ }
+ },
+ });
+ }
return;
}
uni.showModal({
title: "余额支付确认",
- content: `本次将扣除 ¥${payFee.value.toFixed(2)},支付后账户剩余 ¥${remainingBalance.value}。确定支付吗?`,
+ content: `本次将扣除 ¥${remainPayFee.value.toFixed(2)},支付后账户剩余 ¥${remainingBalance.value}。确定支付吗?`,
confirmText: "确认支付",
cancelText: "取消",
confirmColor: "var(--ui-BG-Main, #ff3000)",
@@ -689,18 +968,19 @@ async function onPay() {
return;
}
- // 微信支付、支付宝支付等常规通道
+ // 微信支付、支付宝支付等常规全额通道
submitPayment();
}
// 提交支付
-async function submitPayment() {
+async function submitPayment(options = {}) {
state.isSubmitting = true;
try {
await sheep.$platform.pay(
state.payment,
state.orderType,
state.orderInfo.order_sn,
+ options,
);
} catch (err) {
console.error("支付发起异常:", err);
@@ -709,7 +989,7 @@ async function submitPayment() {
}
}
-// 13. 生命周期钩子
+// 14. 生命周期钩子
onLoad((options) => {
// 微信公众号 iOS 路由刷新边缘兼容
if (
@@ -864,6 +1144,18 @@ onUnmounted(() => {
color: #333333;
max-width: 440rpx;
text-align: right;
+
+ &.text-success {
+ color: #52c41a;
+ }
+
+ &.text-orange {
+ color: #fa8c16;
+ }
+
+ &.font-bold {
+ font-weight: 600;
+ }
}
.copy-btn {
@@ -925,6 +1217,63 @@ onUnmounted(() => {
}
}
+ // 专属混合支付卡片
+ .mixed-pay-card {
+ background: linear-gradient(135deg, #fff9f6 0%, #fff4ef 100%);
+ border: 2rpx solid rgba(255, 87, 34, 0.22);
+ border-radius: 18rpx;
+ padding: 20rpx 22rpx;
+ box-sizing: border-box;
+
+ .mixed-left {
+ flex: 1;
+
+ .mixed-icon-wrap {
+ width: 56rpx;
+ height: 56rpx;
+ background: #ffffff;
+ border-radius: 50%;
+ margin-right: 18rpx;
+ box-shadow: 0 2rpx 8rpx rgba(255, 87, 34, 0.12);
+
+ .mixed-icon-img {
+ width: 38rpx;
+ height: 38rpx;
+ }
+ }
+
+ .mixed-info {
+ flex: 1;
+
+ .mixed-title-row {
+ .mixed-title {
+ font-size: 28rpx;
+ font-weight: bold;
+ color: #333333;
+ }
+
+ .mixed-tag {
+ font-size: 20rpx;
+ font-weight: 500;
+ color: #ffffff;
+ background: linear-gradient(90deg, #ff7a45, #ff4d4f);
+ padding: 2rpx 10rpx;
+ border-radius: 6rpx;
+ margin-left: 12rpx;
+ line-height: 28rpx;
+ }
+ }
+
+ .mixed-desc {
+ font-size: 22rpx;
+ color: #777777;
+ margin-top: 6rpx;
+ font-family: OPPOSANS, sans-serif;
+ }
+ }
+ }
+ }
+
.payment-list {
.pay-option-item {
padding: 28rpx 0;
@@ -972,6 +1321,13 @@ onUnmounted(() => {
color: #1890ff;
border: 1rpx solid #91d5ff;
}
+
+ &.mixed-amount-tag {
+ background: #fff7e6;
+ color: #fa8c16;
+ border: 1rpx solid #ffd591;
+ font-weight: 600;
+ }
}
}
@@ -1008,6 +1364,13 @@ onUnmounted(() => {
border: 1rpx solid #b7eb8f;
}
+ &.mixed {
+ background: #e6f7ff;
+ color: #1890ff;
+ border: 1rpx solid #91d5ff;
+ font-weight: 500;
+ }
+
&.danger {
background: #fff2f0;
color: #ff4d4f;
@@ -1028,6 +1391,20 @@ onUnmounted(() => {
margin-left: 2rpx;
}
}
+
+ .mixed-link-btn {
+ font-size: 22rpx;
+ color: #fa8c16;
+ font-weight: 600;
+ background: rgba(250, 140, 22, 0.1);
+ padding: 4rpx 12rpx;
+ border-radius: 16rpx;
+
+ .cicon-forward {
+ font-size: 18rpx;
+ margin-left: 2rpx;
+ }
+ }
}
.balance-deduct-tip {
diff --git a/uniapp/pages/pay/withdraw.vue b/uniapp/pages/pay/withdraw.vue
index f416126..84a02f3 100644
--- a/uniapp/pages/pay/withdraw.vue
+++ b/uniapp/pages/pay/withdraw.vue
@@ -11,8 +11,11 @@
]"
>
- 可提现金额(元)
+ 可提现佣金(元)
{{ userInfo.commission || '0.00' }}
+
+ 商城可用余额:¥{{ userInfo.money || '0.00' }}
+
- 提现说明
+ 提现说明
+
+ 💡 提示:商城收益分成已全额直接入账商城余额,可用于下单直接抵扣。历史存量佣金可转入商城余额消费。
+
{{ index + 1 }}.{{ rule }}
diff --git a/uniapp/pages/user/wallet/commission.vue b/uniapp/pages/user/wallet/commission.vue
index 5be9111..3b8a902 100644
--- a/uniapp/pages/user/wallet/commission.vue
+++ b/uniapp/pages/user/wallet/commission.vue
@@ -4,7 +4,7 @@
@@ -172,6 +186,10 @@
name: '全部',
value: 'all',
},
+ {
+ name: '收益分成',
+ value: 'reward',
+ },
{
name: '收入',
value: 'income',
diff --git a/uniapp/pages/user/wallet/money.vue b/uniapp/pages/user/wallet/money.vue
index 27e47ca..4f4ff31 100644
--- a/uniapp/pages/user/wallet/money.vue
+++ b/uniapp/pages/user/wallet/money.vue
@@ -41,7 +41,7 @@
- 线上充值与消费余额 · 下单直接抵扣
+ 线上充值与收益分成全额入账 · 下单直接抵扣
即时可用
@@ -185,6 +185,10 @@
name: '全部',
value: 'all',
},
+ {
+ name: '收益分成',
+ value: 'reward',
+ },
{
name: '收入',
value: 'income',
diff --git a/uniapp/sheep/components/s-wallet-card/s-wallet-card.vue b/uniapp/sheep/components/s-wallet-card/s-wallet-card.vue
index 29b718a..7f2f183 100644
--- a/uniapp/sheep/components/s-wallet-card/s-wallet-card.vue
+++ b/uniapp/sheep/components/s-wallet-card/s-wallet-card.vue
@@ -18,8 +18,7 @@
{{ displayMadow }}
- 约¥{{ userInfo?.madow_value || '0.00' }}
-
+
diff --git a/uniapp/sheep/platform/index.js b/uniapp/sheep/platform/index.js
index 2fa2fb6..366939e 100644
--- a/uniapp/sheep/platform/index.js
+++ b/uniapp/sheep/platform/index.js
@@ -75,8 +75,8 @@ const useProvider = (_provider = '') => {
};
// 支付服务转发
-const pay = (payment, orderType, orderSN) => {
- return new Pay(payment, orderType, orderSN);
+const pay = (payment, orderType, orderSN, options = {}) => {
+ return new Pay(payment, orderType, orderSN, options);
};
/**
diff --git a/uniapp/sheep/platform/pay.js b/uniapp/sheep/platform/pay.js
index b97d117..7b82401 100644
--- a/uniapp/sheep/platform/pay.js
+++ b/uniapp/sheep/platform/pay.js
@@ -15,10 +15,11 @@ import {
*/
export default class SheepPay {
- constructor(payment, orderType, orderSN) {
+ constructor(payment, orderType, orderSN, options = {}) {
this.payment = payment;
this.orderSN = orderSN;
this.orderType = orderType;
+ this.options = options || {};
this.payAction();
}
@@ -91,22 +92,31 @@ export default class SheepPay {
order_sn: this.orderSN,
payment: this.payment,
};
+ if (this.options && Number(this.options.money) > 0) {
+ data.money = Number(this.options.money);
+ }
if (uni.getStorageSync('openid')) {
data.openid = uni.getStorageSync('openid');
}
sheep.$api.pay.prepay(data).then((res) => {
- res.code === 1 && resolve(res);
- if (res.data === -1 && res.msg === 'miss_openid') {
- uni.showModal({
- title: '微信支付',
- content: '请先绑定微信再使用微信支付',
- success: function (res) {
- if (res.confirm) {
- sheep.$platform.useProvider('wechat').bind();
- }
- },
- });
+ if (res && res.code === 1) {
+ resolve(res);
+ } else {
+ if (res && res.data === -1 && res.msg === 'miss_openid') {
+ uni.showModal({
+ title: '微信支付',
+ content: '请先绑定微信再使用微信支付',
+ success: function (res) {
+ if (res.confirm) {
+ sheep.$platform.useProvider('wechat').bind();
+ }
+ },
+ });
+ }
+ reject(res);
}
+ }).catch((err) => {
+ reject(err);
});
});
}
@@ -114,51 +124,75 @@ export default class SheepPay {
// 微信公众号JSSDK支付
async wechatOfficialAccountPay() {
let that = this;
- let {
- code,
- data,
- msg
- } = await this.prepay();
- if (code !== 1) {
- console.log('支付错误', msg);
- return;
- }
- $wxsdk.wxpay(data.pay_data, {
- success: () => {
+ try {
+ let {
+ code,
+ data,
+ msg
+ } = await this.prepay();
+ if (code !== 1) {
+ console.log('支付错误', msg);
+ return;
+ }
+ if (!data?.pay_data) {
that.payResult('success');
- },
- cancel: () => {
- sheep.$helper.toast('支付已手动取消');
- },
- fail: () => {
- that.payResult('fail');
- },
- });
+ return;
+ }
+ $wxsdk.wxpay(data.pay_data, {
+ success: () => {
+ that.payResult('success');
+ },
+ cancel: () => {
+ sheep.$helper.toast('支付已手动取消');
+ },
+ fail: () => {
+ that.payResult('fail');
+ },
+ });
+ } catch (e) {
+ console.log('预支付未完成:', e);
+ }
}
//浏览器微信H5支付
async wechatWapPay() {
- const {
- code,
- data
- } = await this.prepay();
- if (code === 1) {
- const redirect_url = `${getRootUrl()}pages/pay/result?orderSN=${this.orderSN}&payment=${this.payment
- }&orderType=${this.orderType}`;
- location.href = `${data.pay_data.h5_url}&redirect_url=${encodeURIComponent(redirect_url)}`;
+ try {
+ const {
+ code,
+ data
+ } = await this.prepay();
+ if (code === 1) {
+ if (!data?.pay_data) {
+ this.payResult('success');
+ return;
+ }
+ const redirect_url = `${getRootUrl()}pages/pay/result?orderSN=${this.orderSN}&payment=${this.payment
+ }&orderType=${this.orderType}`;
+ location.href = `${data.pay_data.h5_url}&redirect_url=${encodeURIComponent(redirect_url)}`;
+ }
+ } catch (e) {
+ console.log('预支付未完成:', e);
}
}
// 支付链接
async redirectPay() {
- let {
- code,
- data
- } = await this.prepay();
- if (code === 1) {
- const redirect_url = `${getRootUrl()}pages/pay/result?orderSN=${this.orderSN}&payment=${this.payment
- }&orderType=${this.orderType}`;
- location.href = data.pay_data + encodeURIComponent(redirect_url);
+ try {
+ let {
+ code,
+ data
+ } = await this.prepay();
+ if (code === 1) {
+ if (!data?.pay_data) {
+ this.payResult('success');
+ return;
+ }
+ const redirect_url = `${getRootUrl()}pages/pay/result?orderSN=${this.orderSN}&payment=${this.payment
+ }&orderType=${this.orderType}`;
+ location.href = data.pay_data + encodeURIComponent(redirect_url);
+ }
+ } catch (e) {
+ console.log('预支付未完成:', e);
}
}
@@ -167,104 +201,145 @@ export default class SheepPay {
// 微信小程序支付
async wechatMiniProgramPay() {
let that = this;
- let result = await this.prepay();
- uni.requestPayment({
- provider: 'wxpay',
- ...result.data.pay_data,
- success: (res) => {
+ try {
+ let result = await this.prepay();
+ if (result.code !== 1) return;
+ if (!result.data?.pay_data) {
that.payResult('success');
- },
- fail: (err) => {
- if (err.errMsg === 'requestPayment:fail cancel') {
- sheep.$helper.toast('支付已手动取消');
- } else {
- that.payResult('fail');
- }
- },
- });
- }
-
- // 余额支付
- async moneyPay() {
- const {
- code
- } = await this.prepay();
- code === 1 && this.payResult('success');
- }
-
- // 货到付款
- async offlinePay() {
- const {
- code
- } = await this.prepay();
- code === 1 && this.payResult('success');
- }
-
- // 支付宝复制链接支付
- async copyPayLink() {
- let that = this;
- let {
- code,
- data
- } = await this.prepay();
- if (code === 1) {
- // 引入showModal 点击确认 复制链接;
- uni.showModal({
- title: '支付宝支付',
- content: '复制链接到外部浏览器',
- confirmText: '复制链接',
- success: (res) => {
- if (res.confirm) {
- sheep.$helper.copyText(data.pay_data);
- }
- },
- });
- }
- }
-
- // 支付宝支付
- async alipay() {
- let that = this;
- const {
- code,
- data
- } = await this.prepay();
- if (code === 1) {
+ return;
+ }
uni.requestPayment({
- provider: 'alipay',
- orderInfo: data.pay_data, //支付宝订单数据
+ provider: 'wxpay',
+ ...result.data.pay_data,
success: (res) => {
that.payResult('success');
},
fail: (err) => {
- if (err.errMsg === 'requestPayment:fail [paymentAlipay:62001]user cancel') {
+ if (err.errMsg === 'requestPayment:fail cancel') {
sheep.$helper.toast('支付已手动取消');
} else {
that.payResult('fail');
}
},
});
+ } catch (e) {
+ console.log('预支付未完成:', e);
+ }
+ }
+
+ // 余额支付
+ async moneyPay() {
+ try {
+ const {
+ code
+ } = await this.prepay();
+ code === 1 && this.payResult('success');
+ } catch (e) {
+ console.log('余额支付未完成:', e);
+ }
+ }
+
+ // 货到付款
+ async offlinePay() {
+ try {
+ const {
+ code
+ } = await this.prepay();
+ code === 1 && this.payResult('success');
+ } catch (e) {
+ console.log('货到付款未完成:', e);
+ }
+ }
+
+ // 支付宝复制链接支付
+ async copyPayLink() {
+ let that = this;
+ try {
+ let {
+ code,
+ data
+ } = await this.prepay();
+ if (code === 1) {
+ if (!data?.pay_data) {
+ that.payResult('success');
+ return;
+ }
+ // 引入showModal 点击确认 复制链接;
+ uni.showModal({
+ title: '支付宝支付',
+ content: '复制链接到外部浏览器',
+ confirmText: '复制链接',
+ success: (res) => {
+ if (res.confirm) {
+ sheep.$helper.copyText(data.pay_data);
+ }
+ },
+ });
+ }
+ } catch (e) {
+ console.log('复制支付链接未完成:', e);
+ }
+ }
+
+ // 支付宝支付
+ async alipay() {
+ let that = this;
+ try {
+ const {
+ code,
+ data
+ } = await this.prepay();
+ if (code === 1) {
+ if (!data?.pay_data) {
+ that.payResult('success');
+ return;
+ }
+ uni.requestPayment({
+ provider: 'alipay',
+ orderInfo: data.pay_data, //支付宝订单数据
+ success: (res) => {
+ that.payResult('success');
+ },
+ fail: (err) => {
+ if (err.errMsg === 'requestPayment:fail [paymentAlipay:62001]user cancel') {
+ sheep.$helper.toast('支付已手动取消');
+ } else {
+ that.payResult('fail');
+ }
+ },
+ });
+ }
+ } catch (e) {
+ console.log('支付宝预支付未完成:', e);
}
}
// 微信支付
async wechatAppPay() {
let that = this;
- let {
- code,
- data
- } = await this.prepay();
- if (code === 1) {
- uni.requestPayment({
- provider: 'wxpay',
- orderInfo: data.pay_data, //微信订单数据(官方说是string。实测为object)
- success: (res) => {
+ try {
+ let {
+ code,
+ data
+ } = await this.prepay();
+ if (code === 1) {
+ if (!data?.pay_data) {
that.payResult('success');
- },
- fail: (err) => {
- err.errMsg !== 'requestPayment:fail cancel' && that.payResult('fail');
- },
- });
+ return;
+ }
+ uni.requestPayment({
+ provider: 'wxpay',
+ orderInfo: data.pay_data, //微信订单数据(官方说是string。实测为object)
+ success: (res) => {
+ that.payResult('success');
+ },
+ fail: (err) => {
+ err.errMsg !== 'requestPayment:fail cancel' && that.payResult('fail');
+ },
+ });
+ }
+ } catch (e) {
+ console.log('微信App预支付未完成:', e);
}
}
diff --git a/uniapp/sheep/store/user.js b/uniapp/sheep/store/user.js
index 3ad5810..08c5f2b 100644
--- a/uniapp/sheep/store/user.js
+++ b/uniapp/sheep/store/user.js
@@ -24,6 +24,7 @@ const defaultUserInfo = {
madow: '', // 马豆数量
madow_rate: '', // 马豆汇率
madow_value: '', // 马豆约合
+ show_madow_rate: 1, // 是否显示马豆市值
voucher_available: '', // 可用兑换券
active_30d: true, // 近 30 日是否有本人订单
agent_form: false,