feat(partner): 完善马豆实时动态汇率与精度自适应,支持交易数据清理与汇率重置
@@ -111,6 +111,7 @@ class Shopro extends Addons
|
||||
'addons\shopro\console\PartnerInstall',
|
||||
'addons\shopro\console\PartnerDemo',
|
||||
'addons\shopro\console\PartnerAccept',
|
||||
'addons\shopro\console\PartnerClean',
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\console;
|
||||
|
||||
use think\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\Output;
|
||||
use think\console\input\Option;
|
||||
use think\Db;
|
||||
|
||||
/**
|
||||
* 清理订单数据、分销分润记录、日结快照日志,并将马豆汇率回归初始状态
|
||||
*/
|
||||
class PartnerClean extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
$this->setName('shopro:partner:clean')
|
||||
->addOption('force', 'f', Option::VALUE_NONE, '强制执行,不提示确认')
|
||||
->addOption('with-wallet-log', 'w', Option::VALUE_NONE, '同时清理分佣与订单相关的佣金钱包流水日志')
|
||||
->setDescription('清理所有订单、分销分润、日结日志,重置马豆与汇率回初始值');
|
||||
}
|
||||
|
||||
protected function execute(Input $input, Output $output)
|
||||
{
|
||||
$force = (bool) $input->getOption('force');
|
||||
$withWallet = (bool) $input->getOption('with-wallet-log');
|
||||
|
||||
$output->writeln("========================================================");
|
||||
$output->writeln(" 收益中心交易数据清理与马豆汇率重置程序");
|
||||
$output->writeln("========================================================");
|
||||
|
||||
if (!$force) {
|
||||
$output->writeln("【警告】此操作将清空所有商城订单、分销记录、分润流水并重置马豆汇率!");
|
||||
$output->writeln("如需执行,请增加参数 --force 或 -f,例如:");
|
||||
$output->writeln("php think shopro:partner:clean --force");
|
||||
return 1;
|
||||
}
|
||||
|
||||
$prefix = config('database.prefix') ?: 'fa_';
|
||||
$output->writeln("当前数据库前缀: [{$prefix}]");
|
||||
|
||||
Db::startTrans();
|
||||
try {
|
||||
// 1. 清理商城订单及关联子表
|
||||
$orderTables = [
|
||||
'shopro_order',
|
||||
'shopro_order_item',
|
||||
'shopro_order_action',
|
||||
'shopro_order_aftersale',
|
||||
'shopro_order_aftersale_log',
|
||||
'shopro_order_express',
|
||||
'shopro_order_invoice',
|
||||
'shopro_order_item_stock',
|
||||
];
|
||||
|
||||
$output->writeln("\n>>> 1. 正在清理商城订单及交易明细...");
|
||||
foreach ($orderTables as $tbl) {
|
||||
$fullTable = $prefix . $tbl;
|
||||
if ($this->tableExists($fullTable)) {
|
||||
Db::execute("TRUNCATE TABLE `{$fullTable}`");
|
||||
$output->writeln(" [OK] 清空表 `{$fullTable}`");
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 清理分销与合伙人收益流水表
|
||||
$rewardTables = [
|
||||
'shopro_partner_reward', // 二开收益中心全量分润流水
|
||||
'shopro_commission_order', // 分销订单关联表
|
||||
'shopro_commission_reward', // 原分销奖励明细
|
||||
'shopro_commission_log', // 分销变动日志
|
||||
];
|
||||
|
||||
$output->writeln("\n>>> 2. 正在清理分销订单与收益分润记录...");
|
||||
foreach ($rewardTables as $tbl) {
|
||||
$fullTable = $prefix . $tbl;
|
||||
if ($this->tableExists($fullTable)) {
|
||||
Db::execute("TRUNCATE TABLE `{$fullTable}`");
|
||||
$output->writeln(" [OK] 清空表 `{$fullTable}`");
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 清理日结对账、马豆大盘汇率快照与兑换券释放计划
|
||||
$dailyTables = [
|
||||
'shopro_partner_rate_log', // 马豆大盘汇率历史快照日志(清空后大盘汇率无历史,自动按 0 单重算)
|
||||
'shopro_partner_daily_report', // 每日日结对账日报表
|
||||
'shopro_partner_voucher_plan', // 兑换券释放计划表
|
||||
'shopro_partner_voucher_log', // 兑换券释放明细流水
|
||||
];
|
||||
|
||||
$output->writeln("\n>>> 3. 正在清理日结对账、汇率快照与兑换券计划...");
|
||||
foreach ($dailyTables as $tbl) {
|
||||
$fullTable = $prefix . $tbl;
|
||||
if ($this->tableExists($fullTable)) {
|
||||
Db::execute("TRUNCATE TABLE `{$fullTable}`");
|
||||
$output->writeln(" [OK] 清空表 `{$fullTable}`");
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 重置分销商资产状态(马豆归零、推三返一序号归零、兑换券归零、收益归零)
|
||||
$output->writeln("\n>>> 4. 正在重置分销商(Agent)资产指标...");
|
||||
$agentTable = $prefix . 'shopro_commission_agent';
|
||||
if ($this->tableExists($agentTable)) {
|
||||
Db::execute("UPDATE `{$agentTable}` SET
|
||||
`madow` = 0.00000000,
|
||||
`direct_order_seq` = 0,
|
||||
`voucher_total` = 0.00,
|
||||
`voucher_released` = 0.00,
|
||||
`voucher_used` = 0.00,
|
||||
`total_income` = 0.00,
|
||||
`commission` = 0.00,
|
||||
`total_consume` = 0.00,
|
||||
`last_self_order_time` = NULL");
|
||||
$output->writeln(" [OK] 已将所有分销商持仓马豆、推三返一序号、兑换券及累计收益重置归零");
|
||||
}
|
||||
|
||||
// 5. 重置用户表钱包分销佣金与累计消费额
|
||||
$output->writeln("\n>>> 5. 正在重置会员用户(User)交易与佣金数据...");
|
||||
$userTable = $prefix . 'user';
|
||||
if ($this->tableExists($userTable)) {
|
||||
Db::execute("UPDATE `{$userTable}` SET
|
||||
`commission` = 0.00,
|
||||
`total_consume` = 0.00,
|
||||
`order_count` = 0");
|
||||
$output->writeln(" [OK] 已将所有会员钱包佣金、累计消费、订单计数重置归零");
|
||||
}
|
||||
|
||||
// 6. 可选清理钱包流水表中佣金相关的记录
|
||||
if ($withWallet) {
|
||||
$walletLogTable = $prefix . 'shopro_user_wallet_log';
|
||||
if ($this->tableExists($walletLogTable)) {
|
||||
Db::execute("DELETE FROM `{$walletLogTable}` WHERE `wallet_type` = 'commission' OR `event` IN ('reward_income', 'reward_back', 'order_pay', 'order_refund')");
|
||||
$output->writeln(" [OK] 已清理佣金钱包及订单相关流水日志");
|
||||
}
|
||||
}
|
||||
|
||||
Db::commit();
|
||||
$output->writeln("\n[SUCCESS] 数据库交易与分润记录清理完毕!事务已成功提交。");
|
||||
} catch (\Exception $e) {
|
||||
Db::rollback();
|
||||
$output->writeln("\n[FAIL] 清理过程中发生异常,事务已回滚: " . $e->getMessage());
|
||||
return 1;
|
||||
}
|
||||
|
||||
// 7. 清理缓存中汇率与收益中心配置
|
||||
$output->writeln("\n>>> 6. 正在清除马豆汇率缓存与系统配置缓存...");
|
||||
cache('partner_madow_current_rate', null);
|
||||
cache('partner_rules', null);
|
||||
if (function_exists('rmdirs')) {
|
||||
@rmdirs(RUNTIME_PATH . 'cache');
|
||||
@rmdirs(RUNTIME_PATH . 'temp');
|
||||
}
|
||||
$output->writeln(" [OK] 马豆即时汇率缓存已清理");
|
||||
|
||||
// 8. 校验回归初始汇率
|
||||
$rateService = new \addons\shopro\service\partner\RateService();
|
||||
$resetRate = $rateService->currentRate();
|
||||
$resetN = $rateService->countN();
|
||||
$formattedRate = number_format($resetRate, 8, '.', '');
|
||||
|
||||
$output->writeln("\n========================================================");
|
||||
$output->writeln(" 重置后指标核对:");
|
||||
$output->writeln(" 合格订单总数 N : {$resetN} 单 (应为 0)");
|
||||
$output->writeln(" 当前大盘汇率 : {$formattedRate} 元/豆 (回归初始基准值 0.00000075)");
|
||||
$output->writeln("========================================================\n");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
protected function tableExists($table)
|
||||
{
|
||||
try {
|
||||
$check = Db::query("SHOW TABLES LIKE '{$table}'");
|
||||
return !empty($check);
|
||||
} catch (\Exception $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -56,43 +56,47 @@ class Commission
|
||||
*/
|
||||
public function orderPaidAfter($params)
|
||||
{
|
||||
// 订单支付成功
|
||||
$order = $params['order'];
|
||||
try {
|
||||
// 订单支付成功
|
||||
$order = $params['order'];
|
||||
|
||||
// 积分商品订单不参与分销
|
||||
if ($order['type'] === 'score') return;
|
||||
// 积分商品订单不参与分销
|
||||
if ($order['type'] === 'score') return;
|
||||
|
||||
$user = $params['user'];
|
||||
$user = $params['user'];
|
||||
|
||||
$agent = new AgentService($user);
|
||||
$agent = new AgentService($user);
|
||||
|
||||
// 绑定用户关系
|
||||
$agent->bindUserRelation('pay');
|
||||
// 绑定用户关系
|
||||
$agent->bindUserRelation('pay');
|
||||
|
||||
// 先记录分佣 再处理记录业绩、升级等情况
|
||||
$items = $order ? $order['items'] : [];
|
||||
// 先记录分佣 再处理记录业绩、升级等情况
|
||||
$items = $order ? $order['items'] : [];
|
||||
|
||||
foreach ($items as $item) {
|
||||
if (isset($item['ext']['is_commission']) && !$item['ext']['is_commission']) continue;
|
||||
foreach ($items as $item) {
|
||||
if (isset($item['ext']['is_commission']) && !$item['ext']['is_commission']) continue;
|
||||
|
||||
$commission = new OrderService($item);
|
||||
// 检查能否分销
|
||||
if (!$commission->checkAndSetCommission()) continue;
|
||||
// 添加分销订单
|
||||
$commissionOrder = $commission->createCommissionOrder();
|
||||
$commission = new OrderService($item);
|
||||
// 检查能否分销
|
||||
if (!$commission->checkAndSetCommission()) continue;
|
||||
// 添加分销订单
|
||||
$commissionOrder = $commission->createCommissionOrder();
|
||||
|
||||
if (!$commissionOrder) continue;
|
||||
if (!$commissionOrder) continue;
|
||||
|
||||
// 执行佣金计划
|
||||
$commission->runCommissionPlan($commissionOrder);
|
||||
// 支付后拨款
|
||||
(new RewardService('paid'))->runCommissionRewardByOrder($commissionOrder);
|
||||
// 执行佣金计划
|
||||
$commission->runCommissionPlan($commissionOrder);
|
||||
// 支付后拨款
|
||||
(new RewardService('paid'))->runCommissionRewardByOrder($commissionOrder);
|
||||
}
|
||||
|
||||
// 创建分销商
|
||||
$agent->createNewAgent();
|
||||
// 分销商数据统计&升级(异步执行)
|
||||
$agent->createAsyncAgentUpgrade();
|
||||
} catch (\Throwable $e) {
|
||||
format_log_error($e, 'Commission.orderPaidAfter', '原生分销监听处理异常: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
// 创建分销商
|
||||
$agent->createNewAgent();
|
||||
// 分销商数据统计&升级(异步执行)
|
||||
$agent->createAsyncAgentUpgrade();
|
||||
}
|
||||
|
||||
// 线下付款后
|
||||
|
||||
@@ -35,6 +35,7 @@ class Partner
|
||||
|
||||
$service = new PartnerRewardService();
|
||||
if ($service->isQualified($order)) {
|
||||
\addons\shopro\service\partner\RateService::clearCache();
|
||||
$userId = $order['user_id'] ?? ($params['user']['id'] ?? 0);
|
||||
if ($userId) {
|
||||
$agent = new AgentService($userId);
|
||||
|
||||
@@ -84,6 +84,11 @@ class IdentityService
|
||||
|
||||
public static function formatMadow($value)
|
||||
{
|
||||
$floatVal = (float) $value;
|
||||
// 当存在小于 1 豆且大于 0 的微量马豆时,自适应保留有效小数(最多4位),避免直接抹零显示为 0 豆
|
||||
if ($floatVal > 0 && $floatVal < 1) {
|
||||
return rtrim(rtrim(number_format($floatVal, 4, '.', ''), '0'), '.');
|
||||
}
|
||||
return number_format((float) $value, 0, '.', '');
|
||||
}
|
||||
|
||||
|
||||
@@ -104,10 +104,12 @@ class PartnerConfig
|
||||
'venue_share' => $venue . '加权奖励',
|
||||
'county_share' => $county . '加权奖励',
|
||||
'madow_share' => '每日贡献马豆',
|
||||
'admin_madow' => '管理员调整马豆',
|
||||
'voucher_grant' => '消费补贴发放',
|
||||
'voucher_release' => '消费补贴到账',
|
||||
'voucher_consume' => '消费券抵扣',
|
||||
'voucher_consume_refund' => '消费券退款退还',
|
||||
'admin_voucher' => '管理员调整兑换券',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -11,21 +11,33 @@ use app\admin\model\shopro\commission\CommissionGoods;
|
||||
*/
|
||||
class RateService
|
||||
{
|
||||
public function currentRate()
|
||||
/**
|
||||
* 获取大盘当前即时生效汇率(基于全网合格订单 N 实时动态计算,短缓存 120 秒保障高并发读性能)
|
||||
*
|
||||
* @param bool $forceRefresh 是否强制穿透缓存实时重算
|
||||
* @return float
|
||||
*/
|
||||
public function currentRate($forceRefresh = false)
|
||||
{
|
||||
$latest = RateLog::order('id', 'desc')->find();
|
||||
if ($latest && (float) $latest->rate > 0) {
|
||||
return (float) $latest->rate;
|
||||
}
|
||||
$cached = cache('partner_madow_current_rate');
|
||||
if ($cached !== null && $cached !== false && (float) $cached > 0) {
|
||||
return (float) $cached;
|
||||
if (!$forceRefresh) {
|
||||
$cached = cache('partner_madow_current_rate');
|
||||
if ($cached !== null && $cached !== false && (float) $cached > 0) {
|
||||
return (float) $cached;
|
||||
}
|
||||
}
|
||||
$rate = $this->calcRate($this->countN());
|
||||
cache('partner_madow_current_rate', $rate, 300);
|
||||
cache('partner_madow_current_rate', $rate, 120);
|
||||
return $rate;
|
||||
}
|
||||
|
||||
/**
|
||||
* 主动清除当前马豆汇率缓存(订单支付、退款或后台调整时调用)
|
||||
*/
|
||||
public static function clearCache()
|
||||
{
|
||||
cache('partner_madow_current_rate', null);
|
||||
}
|
||||
|
||||
public function calcRate($n = null)
|
||||
{
|
||||
$n = $n === null ? $this->countN() : intval($n);
|
||||
|
||||
@@ -198,7 +198,7 @@ class User extends Common
|
||||
->where('user_id', $id);
|
||||
if (!$showSkip) {
|
||||
$query->where(function ($q) {
|
||||
$q->where('amount', '>', 0)->whereOr('madow', '>', 0);
|
||||
$q->where('amount', '<>', 0)->whereOr('madow', '<>', 0);
|
||||
});
|
||||
}
|
||||
$list = $query->order('id', 'desc')->paginate($this->request->param('list_rows', 10));
|
||||
@@ -312,17 +312,182 @@ class User extends Common
|
||||
}
|
||||
|
||||
$params = $this->request->only(['id', 'type', 'amount', 'memo']);
|
||||
if (!in_array($params['type'], ['money', 'score'])) {
|
||||
error_stop('参数错误');
|
||||
$userId = intval($params['id'] ?? 0);
|
||||
$type = (string) ($params['type'] ?? 'money');
|
||||
$amount = trim((string) ($params['amount'] ?? ''));
|
||||
$memo = trim((string) ($params['memo'] ?? ''));
|
||||
|
||||
if (!$userId || !is_numeric($amount) || (float) $amount == 0) {
|
||||
$this->error('请输入合法的变更数值');
|
||||
}
|
||||
|
||||
$result = Db::transaction(function () use ($params) {
|
||||
return WalletService::change($params['id'], $params['type'], $params['amount'], 'admin_recharge', [], $params['memo']);
|
||||
});
|
||||
if ($result) {
|
||||
$this->success('充值成功');
|
||||
$allowedTypes = ['money', 'score', 'madow', 'voucher'];
|
||||
if (!in_array($type, $allowedTypes)) {
|
||||
$this->error('不支持的资产类型');
|
||||
}
|
||||
$this->error('充值失败');
|
||||
|
||||
try {
|
||||
$result = Db::transaction(function () use ($userId, $type, $amount, $memo) {
|
||||
$user = UserModel::where('id', $userId)->lock(true)->find();
|
||||
if (!$user) {
|
||||
error_stop('会员不存在');
|
||||
}
|
||||
|
||||
$oper = \addons\shopro\library\Operator::get();
|
||||
$operDesc = ($oper['type'] ?? 'admin') . ($oper['id'] ? '#' . $oper['id'] : '');
|
||||
|
||||
if ($type === 'money') {
|
||||
// 余额充值或扣除 (保留2位小数)
|
||||
$amount = round((float) $amount, 2);
|
||||
if ($amount < 0 && bcadd((string) $user->money, (string) $amount, 2) < 0) {
|
||||
error_stop('当前余额不足,无法扣除');
|
||||
}
|
||||
return WalletService::change($userId, 'money', $amount, 'admin_recharge', [], $memo ?: '后台管理调整余额');
|
||||
}
|
||||
|
||||
if ($type === 'score') {
|
||||
// 积分充值或扣除 (整数)
|
||||
$amount = intval($amount);
|
||||
if ($amount == 0) {
|
||||
error_stop('请输入有效的积分数值');
|
||||
}
|
||||
if ($amount < 0 && ($user->score + $amount) < 0) {
|
||||
error_stop('当前积分不足,无法扣除');
|
||||
}
|
||||
return WalletService::change($userId, 'score', $amount, 'admin_recharge', [], $memo ?: '后台管理调整积分');
|
||||
}
|
||||
|
||||
// 对于马豆 (madow) 与兑换券 (voucher),属于合伙人二开资产体系
|
||||
$agent = \app\admin\model\shopro\commission\Agent::where('user_id', $userId)->lock(true)->find();
|
||||
if (!$agent) {
|
||||
// 自动为用户初始化分销商记录,以便承载二开资产体系
|
||||
$agent = (new \addons\shopro\service\commission\Agent($userId))->createNewAgent();
|
||||
$agent = \app\admin\model\shopro\commission\Agent::where('user_id', $userId)->lock(true)->find();
|
||||
if (!$agent) {
|
||||
$now = time();
|
||||
\think\Db::name('shopro_commission_agent')->insert([
|
||||
'user_id' => $userId,
|
||||
'level' => 1,
|
||||
'status' => 'normal',
|
||||
'madow' => '0.00000000',
|
||||
'voucher_total' => '0.00',
|
||||
'voucher_released' => '0.00',
|
||||
'voucher_used' => '0.00',
|
||||
'createtime' => $now,
|
||||
'updatetime' => $now,
|
||||
]);
|
||||
$agent = \app\admin\model\shopro\commission\Agent::where('user_id', $userId)->lock(true)->find();
|
||||
}
|
||||
}
|
||||
|
||||
// 生成全局唯一的负数 order_id(防止与商城真实订单冲突,并避免 uk_order_user_type 唯一索引冲突)
|
||||
$randOrderId = -intval(substr(time(), -6) . mt_rand(100, 999));
|
||||
$identity = \addons\shopro\service\partner\IdentityService::build($agent);
|
||||
$role = !empty($identity['identities']) ? end($identity['identities']) : 'taster';
|
||||
|
||||
if ($type === 'madow') {
|
||||
// 马豆充值或扣除 (8位小数精度)
|
||||
$currentMadow = (string) ($agent->madow ?? '0.00000000');
|
||||
$amountMadow = function_exists('bcadd') ? bcadd((string) $amount, '0', 8) : number_format((float) $amount, 8, '.', '');
|
||||
$afterMadow = function_exists('bcadd') ? bcadd($currentMadow, $amountMadow, 8) : ($currentMadow + $amountMadow);
|
||||
|
||||
if ((float) $afterMadow < 0) {
|
||||
error_stop('当前持仓马豆不足,无法扣除');
|
||||
}
|
||||
|
||||
$rate = (new \addons\shopro\service\partner\RateService())->currentRate();
|
||||
$agent->madow = $afterMadow;
|
||||
$agent->save();
|
||||
|
||||
\addons\shopro\service\partner\PartnerRewardService::write([
|
||||
'order_id' => $randOrderId,
|
||||
'buyer_id' => $userId,
|
||||
'user_id' => $userId,
|
||||
'role' => $role,
|
||||
'reward_type' => 'admin_madow',
|
||||
'amount' => 0,
|
||||
'madow' => $amountMadow,
|
||||
'base_amount' => 0,
|
||||
'rate' => $rate,
|
||||
'settle_date' => null, // 设为 null,避免与每日均分的 uk_daily_user_type 发生索引冲突
|
||||
'ext' => [
|
||||
'formula' => ($amount > 0 ? '管理员充值马豆: +' : '管理员扣除马豆: ') . $amountMadow,
|
||||
'memo' => $memo ?: ($amount > 0 ? '管理员充值马豆' : '管理员扣除马豆'),
|
||||
'operator' => $operDesc,
|
||||
'before' => $currentMadow,
|
||||
'after' => $afterMadow,
|
||||
],
|
||||
'status' => 1,
|
||||
]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($type === 'voucher') {
|
||||
// 兑换券充值或扣除 (2位小数精度)
|
||||
$amountVoucher = round((float) $amount, 2);
|
||||
$currentReleased = (float) ($agent->voucher_released ?? 0);
|
||||
$currentUsed = (float) ($agent->voucher_used ?? 0);
|
||||
$currentTotal = (float) ($agent->voucher_total ?? 0);
|
||||
$available = max(0, round($currentReleased - $currentUsed, 2));
|
||||
|
||||
if ($amountVoucher < 0 && bccomp((string) $available, (string) abs($amountVoucher), 2) < 0) {
|
||||
error_stop('当前可用兑换券不足,无法扣除');
|
||||
}
|
||||
|
||||
if ($amountVoucher > 0) {
|
||||
// 充值增加可用兑换券:同时增加 released 与 total
|
||||
$newReleased = round($currentReleased + $amountVoucher, 2);
|
||||
$newTotal = round($currentTotal + $amountVoucher, 2);
|
||||
} else {
|
||||
// 扣除可用兑换券:减少 released 与 total
|
||||
$deduct = abs($amountVoucher);
|
||||
$newReleased = max(0, round($currentReleased - $deduct, 2));
|
||||
$newTotal = max(0, round($currentTotal - $deduct, 2));
|
||||
}
|
||||
|
||||
$agent->voucher_released = $newReleased;
|
||||
$agent->voucher_total = $newTotal;
|
||||
$agent->save();
|
||||
|
||||
\addons\shopro\service\partner\PartnerRewardService::write([
|
||||
'order_id' => $randOrderId,
|
||||
'buyer_id' => $userId,
|
||||
'user_id' => $userId,
|
||||
'role' => $role,
|
||||
'reward_type' => 'admin_voucher',
|
||||
'amount' => $amountVoucher,
|
||||
'madow' => 0,
|
||||
'base_amount' => abs($amountVoucher),
|
||||
'rate' => 0,
|
||||
'settle_date' => null,
|
||||
'ext' => [
|
||||
'formula' => ($amountVoucher > 0 ? '管理员充值兑换券: +' : '管理员扣除兑换券: ') . $amountVoucher,
|
||||
'memo' => $memo ?: ($amountVoucher > 0 ? '管理员充值兑换券' : '管理员扣除兑换券'),
|
||||
'operator' => $operDesc,
|
||||
'before_available' => number_format($available, 2, '.', ''),
|
||||
'after_available' => number_format(max(0, $newReleased - $currentUsed), 2, '.', ''),
|
||||
],
|
||||
'status' => 1,
|
||||
]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
} catch (\think\exception\HttpResponseException $e) {
|
||||
$data = $e->getResponse()->getData();
|
||||
$msg = $data ? ($data['msg'] ?? '') : $e->getMessage();
|
||||
$this->error($msg ?: '操作失败');
|
||||
} catch (\Exception $e) {
|
||||
$this->error($e->getMessage() ?: '系统异常');
|
||||
}
|
||||
|
||||
if ($result) {
|
||||
$this->success('操作成功');
|
||||
}
|
||||
$this->error('操作失败');
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ class User extends Validate
|
||||
'username.alphaDash' => '用户名只能包含字母,数字,_和-',
|
||||
'username.length' => '用户名长度必须在 5-12 位',
|
||||
'username.unique' => '用户名已被占用',
|
||||
'username.regx' => '用户名需以字母开头',
|
||||
'username.regex' => '用户名需以字母开头',
|
||||
|
||||
'nickname.require' => '昵称必须填写',
|
||||
'nickname.chsDash' => '昵称只能包含汉字,字母,数字,_和-',
|
||||
@@ -45,13 +45,13 @@ class User extends Validate
|
||||
|
||||
'password.require' => '请填写密码',
|
||||
'password.length' => '密码长度必须在 6-16 位',
|
||||
'password.regx' => '密码必须包含字母和数字',
|
||||
'password.regex' => '密码必须包含字母和数字',
|
||||
|
||||
'oldPassword.require' => '请填写旧密码',
|
||||
|
||||
'newPassword.require' => '请填写新密码',
|
||||
'newPassword.length' => '密码长度必须在 6-16 位',
|
||||
'newPassword.regx' => '密码必须包含字母和数字',
|
||||
'newPassword.regex' => '密码必须包含字母和数字',
|
||||
|
||||
'avatar.require' => '头像必须上传',
|
||||
|
||||
|
||||
@@ -177,7 +177,19 @@
|
||||
收益中心分销商档案
|
||||
</el-button>
|
||||
{if $auth->check('shopro/user/user/recharge')}
|
||||
<el-button type="warning" @click="onRecharge('money')">账户充值</el-button>
|
||||
<el-dropdown trigger="click" @command="onRecharge">
|
||||
<el-button type="warning">
|
||||
账户充值 <el-icon class="el-icon--right"><arrow-down /></el-icon>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="money">充值余额 (元)</el-dropdown-item>
|
||||
<el-dropdown-item command="madow">充值马豆 (颗)</el-dropdown-item>
|
||||
<el-dropdown-item command="score">充值积分 (分)</el-dropdown-item>
|
||||
<el-dropdown-item command="voucher">充值兑换券 (元)</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
{/if}
|
||||
<el-button class="sa-button-refresh ml-2" icon="RefreshRight" @click="getDetail"></el-button>
|
||||
</div>
|
||||
@@ -425,7 +437,12 @@
|
||||
<!-- 资产 2: 分红马豆 -->
|
||||
<el-col :xs="24" :sm="12" :md="6">
|
||||
<div class="asset-card-box">
|
||||
<div class="asset-card-label">分红马豆 (GMV 1:1 分红)</div>
|
||||
<div class="asset-card-label sa-flex sa-row-between" style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<span>分红马豆</span>
|
||||
{if $auth->check('shopro/user/user/recharge')}
|
||||
<el-button type="success" link size="small" @click="onRecharge('madow')">充值/扣除</el-button>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="asset-card-num" style="color: #67c23a;">
|
||||
{{ state.detail.madow || 0 }} <span style="font-size: 13px;">颗</span>
|
||||
</div>
|
||||
@@ -441,7 +458,12 @@
|
||||
<!-- 资产 3: 商城兑换券 -->
|
||||
<el-col :xs="24" :sm="12" :md="6">
|
||||
<div class="asset-card-box">
|
||||
<div class="asset-card-label">商城可用兑换券</div>
|
||||
<div class="asset-card-label sa-flex sa-row-between" style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<span>商城可用兑换券</span>
|
||||
{if $auth->check('shopro/user/user/recharge')}
|
||||
<el-button type="warning" link size="small" @click="onRecharge('voucher')">充值/扣除</el-button>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="asset-card-num" style="color: #e6a23c;">
|
||||
¥{{ state.detail.voucher_available || '0.00' }}
|
||||
</div>
|
||||
@@ -457,7 +479,15 @@
|
||||
<!-- 资产 4: 基础钱包与消费 -->
|
||||
<el-col :xs="24" :sm="12" :md="6">
|
||||
<div class="asset-card-box">
|
||||
<div class="asset-card-label">商城余额 / 积分</div>
|
||||
<div class="asset-card-label sa-flex sa-row-between" style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<span>商城余额 / 积分</span>
|
||||
{if $auth->check('shopro/user/user/recharge')}
|
||||
<div>
|
||||
<el-button type="primary" link size="small" @click="onRecharge('money')">充余额</el-button>
|
||||
<el-button type="primary" link size="small" @click="onRecharge('score')">充积分</el-button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="asset-card-num" style="color: #303133;">
|
||||
¥{{ state.detail.money || '0.00' }}
|
||||
</div>
|
||||
|
||||
@@ -1,19 +1,104 @@
|
||||
{include file="/shopro/common/script" /}
|
||||
|
||||
<div id="recharge" class="user-recharge" v-cloak>
|
||||
<el-container class="panel-block">
|
||||
<el-main>
|
||||
<el-form ref="formRef" :model="form.model" :rules="form.rules" label-width="100px">
|
||||
<el-form-item :label="`充值${state.type == 'money' ? '金额' : '积分'}`" prop="amount">
|
||||
<el-input v-model="form.model.amount" type="number" placeholder="负数则为扣除" />
|
||||
<div id="recharge" class="user-recharge" v-cloak style="padding: 16px;">
|
||||
<el-container class="panel-block" v-loading="state.loading">
|
||||
<el-main style="padding: 0 8px 16px 8px;">
|
||||
<!-- 会员简要信息与四大资产全景卡片 -->
|
||||
<div v-if="state.user" style="background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; padding: 12px 16px; margin-bottom: 20px;">
|
||||
<div class="sa-flex sa-row-between mb-3" style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<div class="sa-flex" style="display: flex; align-items: center;">
|
||||
<sa-image :url="state.user.avatar" size="42" radius="21"></sa-image>
|
||||
<div style="margin-left: 10px;">
|
||||
<div style="font-weight: 600; font-size: 14px; color: #1e293b;">{{ state.user.nickname || '未命名会员' }}</div>
|
||||
<div style="font-size: 12px; color: #64748b;">会员ID: #{{ state.user.id }} <span v-if="state.user.mobile">({{ state.user.mobile }})</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<el-tag v-if="state.user.identity_text" type="success" size="small">{{ state.user.identity_text }}</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 四大资产当前存量栅格展示 -->
|
||||
<el-row :gutter="8">
|
||||
<el-col :span="6">
|
||||
<div style="background: #ffffff; padding: 8px 10px; border-radius: 6px; border: 1px solid #edf2f7; text-align: center;">
|
||||
<div style="font-size: 11px; color: #64748b;">商城余额</div>
|
||||
<div style="font-size: 13px; font-weight: 600; color: #2563eb; margin-top: 2px;">¥{{ state.user.money || '0.00' }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div style="background: #ffffff; padding: 8px 10px; border-radius: 6px; border: 1px solid #edf2f7; text-align: center;">
|
||||
<div style="font-size: 11px; color: #64748b;">分红马豆</div>
|
||||
<div style="font-size: 13px; font-weight: 600; color: #16a34a; margin-top: 2px;">{{ state.user.madow || '0' }} <span style="font-size: 10px;">颗</span></div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div style="background: #ffffff; padding: 8px 10px; border-radius: 6px; border: 1px solid #edf2f7; text-align: center;">
|
||||
<div style="font-size: 11px; color: #64748b;">商城积分</div>
|
||||
<div style="font-size: 13px; font-weight: 600; color: #d97706; margin-top: 2px;">{{ state.user.score || 0 }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div style="background: #ffffff; padding: 8px 10px; border-radius: 6px; border: 1px solid #edf2f7; text-align: center;">
|
||||
<div style="font-size: 11px; color: #64748b;">可用兑换券</div>
|
||||
<div style="font-size: 13px; font-weight: 600; color: #ea580c; margin-top: 2px;">¥{{ state.user.voucher_available || '0.00' }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<!-- 充值操作表单 -->
|
||||
<el-form ref="formRef" :model="form.model" :rules="form.rules" label-width="110px" label-position="right">
|
||||
<el-form-item label="充值资产类型" prop="type">
|
||||
<el-radio-group v-model="form.model.type" @change="onTypeChange">
|
||||
<el-radio-button label="money">余额 (元)</el-radio-button>
|
||||
<el-radio-button label="madow">马豆 (颗)</el-radio-button>
|
||||
<el-radio-button label="score">积分 (分)</el-radio-button>
|
||||
<el-radio-button label="voucher">兑换券 (元)</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="form.model.memo"></el-input>
|
||||
|
||||
<el-form-item label="当前可用持有">
|
||||
<div style="font-size: 15px; font-weight: 600; color: #334155;">
|
||||
<span v-if="form.model.type === 'money'">¥{{ currentBalance }}</span>
|
||||
<span v-else-if="form.model.type === 'madow'">{{ currentBalance }} 颗 (实时估值 ¥{{ state.user ? state.user.madow_value : '0.00' }})</span>
|
||||
<span v-else-if="form.model.type === 'score'">{{ currentBalance }} 积分</span>
|
||||
<span v-else-if="form.model.type === 'voucher'">¥{{ currentBalance }} (计划总额 ¥{{ state.user ? state.user.voucher_total : '0.00' }})</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="`变动${assetLabel}`" prop="amount">
|
||||
<el-input
|
||||
v-model="form.model.amount"
|
||||
type="number"
|
||||
:step="form.model.type === 'madow' ? '0.00000001' : (form.model.type === 'score' ? '1' : '0.01')"
|
||||
:placeholder="form.model.type === 'madow' ? '正数为充值,负数为扣除 (支持8位小数)' : '正数为充值,负数为扣除'"
|
||||
clearable>
|
||||
<template #append>{{ assetUnit }}</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 动态变动试算与防呆预警 -->
|
||||
<el-form-item v-if="previewBalance !== null" label="变动试算">
|
||||
<div style="width: 100%; padding: 8px 12px; border-radius: 6px;" :style="{ background: isOverdrawn ? '#fef2f2' : '#f0fdf4', border: isOverdrawn ? '1px solid #fecaca' : '1px solid #bbf7d0' }">
|
||||
<div style="font-size: 13px;" :style="{ color: isOverdrawn ? '#dc2626' : '#15803d' }">
|
||||
<span>变动后预计持有:<b>{{ previewBalance }} {{ assetUnit }}</b></span>
|
||||
<span v-if="isOverdrawn" style="margin-left: 8px; font-weight: bold;">(⚠️ 扣除超出当前持有量,提交将被拦截)</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="操作备注" prop="memo">
|
||||
<el-input v-model="form.model.memo" placeholder="请输入充值或扣除原因(可追溯审计)" maxlength="100" show-word-limit></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-main>
|
||||
<el-footer class="sa-footer--submit sa-flex sa-row-right">
|
||||
<el-button type="primary" @click="onConfirm">确定</el-button>
|
||||
|
||||
<el-footer class="sa-footer--submit sa-flex sa-row-right" style="display: flex; justify-content: flex-end; padding-top: 10px; border-top: 1px solid #f1f5f9;">
|
||||
<el-button @click="onCancel">取消</el-button>
|
||||
<el-button type="primary" :loading="state.submitting" :disabled="isOverdrawn" @click="onConfirm">
|
||||
确认{{ form.model.amount && parseFloat(form.model.amount) < 0 ? '扣除' : '充值' }}
|
||||
</el-button>
|
||||
</el-footer>
|
||||
</el-container>
|
||||
</div>
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
// 公共助手函数
|
||||
|
||||
// 全局设置文件掩码,确保 CLI / Web / 队列生成的文件和目录具备全读写权限,杜绝权限冲突
|
||||
umask(0000);
|
||||
|
||||
use think\exception\HttpResponseException;
|
||||
use think\Response;
|
||||
|
||||
|
||||
|
之后 宽度: | 高度: | 大小: 440 B |
|
之后 宽度: | 高度: | 大小: 1.4 KiB |
|
之后 宽度: | 高度: | 大小: 5.8 KiB |
|
之后 宽度: | 高度: | 大小: 5.7 KiB |
|
之后 宽度: | 高度: | 大小: 5.5 KiB |
|
之前 宽度: | 高度: | 大小: 2.5 KiB 之后 宽度: | 高度: | 大小: 2.5 KiB |
|
之前 宽度: | 高度: | 大小: 2.5 KiB 之后 宽度: | 高度: | 大小: 2.5 KiB |
|
之前 宽度: | 高度: | 大小: 2.4 KiB 之后 宽度: | 高度: | 大小: 2.4 KiB |
|
之前 宽度: | 高度: | 大小: 3.8 KiB 之后 宽度: | 高度: | 大小: 3.8 KiB |
|
之前 宽度: | 高度: | 大小: 52 KiB 之后 宽度: | 高度: | 大小: 52 KiB |
|
之前 宽度: | 高度: | 大小: 46 KiB 之后 宽度: | 高度: | 大小: 46 KiB |
|
之前 宽度: | 高度: | 大小: 4.1 KiB 之后 宽度: | 高度: | 大小: 4.1 KiB |
|
之前 宽度: | 高度: | 大小: 96 KiB 之后 宽度: | 高度: | 大小: 96 KiB |
|
之前 宽度: | 高度: | 大小: 1.4 KiB 之后 宽度: | 高度: | 大小: 1.4 KiB |
|
之前 宽度: | 高度: | 大小: 1.1 KiB 之后 宽度: | 高度: | 大小: 1.1 KiB |
|
之前 宽度: | 高度: | 大小: 1010 B 之后 宽度: | 高度: | 大小: 1010 B |
|
之前 宽度: | 高度: | 大小: 1.2 KiB 之后 宽度: | 高度: | 大小: 1.2 KiB |
|
之前 宽度: | 高度: | 大小: 1.3 KiB 之后 宽度: | 高度: | 大小: 1.3 KiB |
|
之前 宽度: | 高度: | 大小: 1.4 KiB 之后 宽度: | 高度: | 大小: 1.4 KiB |
|
之前 宽度: | 高度: | 大小: 1.3 KiB 之后 宽度: | 高度: | 大小: 1.3 KiB |
|
之前 宽度: | 高度: | 大小: 1.1 KiB 之后 宽度: | 高度: | 大小: 1.1 KiB |
|
之前 宽度: | 高度: | 大小: 2.7 KiB 之后 宽度: | 高度: | 大小: 2.7 KiB |
|
之前 宽度: | 高度: | 大小: 4.3 KiB 之后 宽度: | 高度: | 大小: 4.3 KiB |
|
之前 宽度: | 高度: | 大小: 4.7 KiB 之后 宽度: | 高度: | 大小: 4.7 KiB |
|
之前 宽度: | 高度: | 大小: 4.6 KiB 之后 宽度: | 高度: | 大小: 4.6 KiB |
|
之前 宽度: | 高度: | 大小: 4.6 KiB 之后 宽度: | 高度: | 大小: 4.6 KiB |
|
之前 宽度: | 高度: | 大小: 14 KiB 之后 宽度: | 高度: | 大小: 14 KiB |
|
之前 宽度: | 高度: | 大小: 15 KiB 之后 宽度: | 高度: | 大小: 15 KiB |
|
之后 宽度: | 高度: | 大小: 5.1 KiB |
|
之后 宽度: | 高度: | 大小: 4.9 KiB |
|
之后 宽度: | 高度: | 大小: 3.1 KiB |
|
之后 宽度: | 高度: | 大小: 6.8 KiB |
|
之前 宽度: | 高度: | 大小: 77 KiB 之后 宽度: | 高度: | 大小: 77 KiB |
二进制
docs/archive_static_backup/logo.png
普通文件
|
之后 宽度: | 高度: | 大小: 160 KiB |
|
之前 宽度: | 高度: | 大小: 184 KiB 之后 宽度: | 高度: | 大小: 184 KiB |
|
之后 宽度: | 高度: | 大小: 4.3 KiB |
|
之后 宽度: | 高度: | 大小: 6.8 KiB |
@@ -0,0 +1,87 @@
|
||||
-- ==============================================================================
|
||||
-- 恒联数通 · 收益中心交易数据清理与马豆汇率回归初始值 SQL 归档脚本
|
||||
-- 适用场景:测试环境/上线前数据重置,清空全部历史订单、分销分润与日结记录,
|
||||
-- 分销商持仓马豆归零,大盘汇率完全重置为初始值 0.00000075 元/豆。
|
||||
--
|
||||
-- 【安全保留数据(绝对不清理)】:
|
||||
-- 1. 用户账号体系 (fa_user):账号、密码、手机号、推荐上下级关系树完整保留
|
||||
-- 2. 分销等级与身份 (fa_shopro_commission_agent):创客等级(V1-V6)、酒道馆、区县代理身份结构完整保留
|
||||
-- 3. 商城商品基座 (fa_shopro_goods 等):商品、规格、分类、分销计奖规则配置完整保留
|
||||
-- 4. 系统全局规则 (fa_shopro_config):收益中心各项参数、基础汇率 0.00000075 等完整保留
|
||||
-- ==============================================================================
|
||||
|
||||
-- 禁用外键约束检查
|
||||
SET FOREIGN_KEY_CHECKS = 0;
|
||||
|
||||
START TRANSACTION;
|
||||
|
||||
-- ------------------------------------------------------------------------------
|
||||
-- 1. 清理商城订单及附属明细数据 (TRUNCATE)
|
||||
-- ------------------------------------------------------------------------------
|
||||
TRUNCATE TABLE `fa_shopro_order`;
|
||||
TRUNCATE TABLE `fa_shopro_order_item`;
|
||||
TRUNCATE TABLE `fa_shopro_order_action`;
|
||||
TRUNCATE TABLE `fa_shopro_order_aftersale`;
|
||||
TRUNCATE TABLE `fa_shopro_order_aftersale_log`;
|
||||
TRUNCATE TABLE `fa_shopro_order_express`;
|
||||
TRUNCATE TABLE `fa_shopro_order_invoice`;
|
||||
|
||||
-- ------------------------------------------------------------------------------
|
||||
-- 2. 清理分销与合伙人收益流水数据 (TRUNCATE)
|
||||
-- ------------------------------------------------------------------------------
|
||||
TRUNCATE TABLE `fa_shopro_partner_reward`; -- 二开合伙人推三返一/极差/均分/马豆流水
|
||||
TRUNCATE TABLE `fa_shopro_commission_order`; -- 分销订单表
|
||||
TRUNCATE TABLE `fa_shopro_commission_reward`; -- 分销分佣表
|
||||
TRUNCATE TABLE `fa_shopro_commission_log`; -- 分销日志表
|
||||
|
||||
-- ------------------------------------------------------------------------------
|
||||
-- 3. 清理日结对账、马豆汇率快照与兑换券释放计划 (TRUNCATE)
|
||||
-- 清空后,因历史合格单数 N = 0 且快照为空,RateService 自动回到基准汇率 0.00000075
|
||||
-- ------------------------------------------------------------------------------
|
||||
TRUNCATE TABLE `fa_shopro_partner_rate_log`; -- 马豆汇率历史快照日志
|
||||
TRUNCATE TABLE `fa_shopro_partner_daily_report`; -- 每日日结对账报表
|
||||
TRUNCATE TABLE `fa_shopro_partner_voucher_plan`; -- 消费兑换券释放计划表
|
||||
TRUNCATE TABLE `fa_shopro_partner_voucher_log`; -- 消费兑换券释放日志明细
|
||||
|
||||
-- ------------------------------------------------------------------------------
|
||||
-- 4. 重置分销商 (Agent) 资产与进度指标 (UPDATE)
|
||||
-- 保留身份(is_venue/is_county/level/parent),将持仓马豆、序号、券资产归零
|
||||
-- ------------------------------------------------------------------------------
|
||||
UPDATE `fa_shopro_commission_agent` SET
|
||||
`madow` = 0.00000000,
|
||||
`direct_order_seq` = 0,
|
||||
`voucher_total` = 0.00,
|
||||
`voucher_released` = 0.00,
|
||||
`voucher_used` = 0.00,
|
||||
`total_income` = 0.00,
|
||||
`commission` = 0.00,
|
||||
`total_consume` = 0.00,
|
||||
`last_self_order_time` = NULL;
|
||||
|
||||
-- ------------------------------------------------------------------------------
|
||||
-- 5. 重置会员 (User) 钱包分销佣金与交易统计 (UPDATE)
|
||||
-- 清空佣金钱包余额、累计消费、订单总数
|
||||
-- ------------------------------------------------------------------------------
|
||||
UPDATE `fa_user` SET
|
||||
`commission` = 0.00,
|
||||
`total_consume` = 0.00,
|
||||
`order_count` = 0;
|
||||
|
||||
-- ------------------------------------------------------------------------------
|
||||
-- 6. 可选:清理钱包流水中关于佣金与订单相关的历史记录 (DELETE)
|
||||
-- ------------------------------------------------------------------------------
|
||||
DELETE FROM `fa_shopro_user_wallet_log`
|
||||
WHERE `wallet_type` = 'commission'
|
||||
OR `event` IN ('reward_income', 'reward_back', 'order_pay', 'order_refund');
|
||||
|
||||
COMMIT;
|
||||
|
||||
-- 恢复外键约束检查
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
|
||||
-- ==============================================================================
|
||||
-- 执行后提示:
|
||||
-- 1. 请在服务器端执行清空缓存命令: php think clear
|
||||
-- 2. 或进入 FastAdmin 后台 -> 右上角 -> 清除缓存
|
||||
-- 3. 此时打开后台【控制台仪表盘】及移动端【我的马豆】,汇率将显示为 0.00000075,持豆量与市值归零。
|
||||
-- ==============================================================================
|
||||
@@ -484,14 +484,21 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
|
||||
createApp('detail', detail);
|
||||
},
|
||||
recharge: () => {
|
||||
const { reactive, getCurrentInstance } = Vue
|
||||
const { reactive, getCurrentInstance, onMounted, computed } = Vue
|
||||
const recharge = {
|
||||
setup() {
|
||||
const { proxy } = getCurrentInstance();
|
||||
|
||||
const initialType = new URLSearchParams(location.search).get('type') || 'money';
|
||||
const validTypes = ['money', 'score', 'madow', 'voucher'];
|
||||
const defaultType = validTypes.includes(initialType) ? initialType : 'money';
|
||||
|
||||
const state = reactive({
|
||||
type: new URLSearchParams(location.search).get('type'),
|
||||
id: new URLSearchParams(location.search).get('id')
|
||||
type: defaultType,
|
||||
id: new URLSearchParams(location.search).get('id'),
|
||||
user: null,
|
||||
loading: false,
|
||||
submitting: false
|
||||
})
|
||||
|
||||
const form = reactive({
|
||||
@@ -502,27 +509,146 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
|
||||
memo: '',
|
||||
},
|
||||
rules: {
|
||||
amount: [{ required: true, message: '请输入', trigger: 'blur' }],
|
||||
type: [{ required: true, message: '请选择资产类型', trigger: 'change' }],
|
||||
amount: [
|
||||
{ required: true, message: '请输入变更数值', trigger: 'blur' },
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
if (value === '' || isNaN(value) || parseFloat(value) === 0) {
|
||||
callback(new Error('请输入非零有效数值'));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
// 获取会员详情与当前四大资产快照
|
||||
function getUserInfo() {
|
||||
if (!state.id) return;
|
||||
state.loading = true;
|
||||
Fast.api.ajax({
|
||||
url: `shopro/user/user/detail?id=${state.id}`,
|
||||
loading: false
|
||||
}, function (ret, res) {
|
||||
state.loading = false;
|
||||
state.user = res.data || null;
|
||||
}, function (ret, res) {
|
||||
state.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
// 切换资产类型
|
||||
function onTypeChange(val) {
|
||||
state.type = val;
|
||||
form.model.type = val;
|
||||
}
|
||||
|
||||
// 资产单位与名称
|
||||
const assetLabel = computed(() => {
|
||||
const map = {
|
||||
money: '金额',
|
||||
madow: '马豆',
|
||||
score: '积分',
|
||||
voucher: '兑换券'
|
||||
};
|
||||
return map[form.model.type] || '数量';
|
||||
});
|
||||
|
||||
const assetUnit = computed(() => {
|
||||
const map = {
|
||||
money: '元',
|
||||
madow: '颗',
|
||||
score: '积分',
|
||||
voucher: '元'
|
||||
};
|
||||
return map[form.model.type] || '';
|
||||
});
|
||||
|
||||
// 当前选中资产的当前持有值
|
||||
const currentBalance = computed(() => {
|
||||
if (!state.user) return '0';
|
||||
switch (form.model.type) {
|
||||
case 'money':
|
||||
return state.user.money || '0.00';
|
||||
case 'score':
|
||||
return state.user.score || 0;
|
||||
case 'madow':
|
||||
return state.user.madow || '0.00000000';
|
||||
case 'voucher':
|
||||
return state.user.voucher_available || '0.00';
|
||||
default:
|
||||
return '0';
|
||||
}
|
||||
});
|
||||
|
||||
// 预计变动后数值
|
||||
const previewBalance = computed(() => {
|
||||
const val = parseFloat(form.model.amount);
|
||||
if (isNaN(val) || val === 0) {
|
||||
return null;
|
||||
}
|
||||
const cur = parseFloat(currentBalance.value) || 0;
|
||||
const res = cur + val;
|
||||
if (form.model.type === 'madow') {
|
||||
return res.toFixed(8);
|
||||
} else if (form.model.type === 'score') {
|
||||
return Math.floor(res);
|
||||
} else {
|
||||
return res.toFixed(2);
|
||||
}
|
||||
});
|
||||
|
||||
// 是否超额扣除
|
||||
const isOverdrawn = computed(() => {
|
||||
if (previewBalance.value === null) return false;
|
||||
return parseFloat(previewBalance.value) < 0;
|
||||
});
|
||||
|
||||
function onCancel() {
|
||||
Fast.api.close();
|
||||
}
|
||||
|
||||
function onConfirm() {
|
||||
proxy.$refs['formRef'].validate((valid) => {
|
||||
if (valid) {
|
||||
if (isOverdrawn.value) {
|
||||
Fast.api.msg('扣除数额已超出当前持有量,请核对后重试');
|
||||
return;
|
||||
}
|
||||
state.submitting = true;
|
||||
Fast.api.ajax({
|
||||
url: `shopro/user/user/recharge`,
|
||||
type: 'POST',
|
||||
data: form.model,
|
||||
loading: true
|
||||
}, function (ret, res) {
|
||||
Fast.api.close()
|
||||
}, function (ret, res) { })
|
||||
state.submitting = false;
|
||||
Fast.api.close();
|
||||
}, function (ret, res) {
|
||||
state.submitting = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getUserInfo();
|
||||
})
|
||||
|
||||
return {
|
||||
state,
|
||||
form,
|
||||
assetLabel,
|
||||
assetUnit,
|
||||
currentBalance,
|
||||
previewBalance,
|
||||
isOverdrawn,
|
||||
onTypeChange,
|
||||
onCancel,
|
||||
onConfirm
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,375 @@
|
||||
<?php
|
||||
|
||||
namespace tests\partner;
|
||||
|
||||
@ini_set('session.use_cookies', '0');
|
||||
define('APP_PATH', __DIR__ . '/../../application/');
|
||||
define('ROOT_PATH', dirname(realpath(APP_PATH)) . '/');
|
||||
require_once __DIR__ . '/../../thinkphp/base.php';
|
||||
require_once __DIR__ . '/../../thinkphp/helper.php';
|
||||
if (is_file(ROOT_PATH . '.env')) {
|
||||
$env = parse_ini_file(ROOT_PATH . '.env', true);
|
||||
foreach ($env as $key => $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 __DIR__ . '/../../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);
|
||||
require_once __DIR__ . '/../../addons/shopro/helper/helper.php';
|
||||
|
||||
// 初始化 CLI 下的 Session 并注入合法的 Admin 账号及 Auth 登录状态
|
||||
\think\Session::init();
|
||||
$auth = \app\admin\library\Auth::instance();
|
||||
$refAuth = new \ReflectionClass($auth);
|
||||
if ($refAuth->hasProperty('logined')) {
|
||||
$p = $refAuth->getProperty('logined');
|
||||
$p->setAccessible(true);
|
||||
$p->setValue($auth, true);
|
||||
}
|
||||
if ($refAuth->hasProperty('rules')) {
|
||||
$p = $refAuth->getProperty('rules');
|
||||
$p->setAccessible(true);
|
||||
$p->setValue($auth, ['*']);
|
||||
}
|
||||
|
||||
use app\admin\controller\shopro\user\User;
|
||||
use app\admin\model\shopro\user\User as UserModel;
|
||||
use app\admin\model\shopro\commission\Agent as AgentModel;
|
||||
use app\admin\model\shopro\partner\Reward as RewardModel;
|
||||
use think\Request;
|
||||
use think\Db;
|
||||
|
||||
class TestUserController extends User
|
||||
{
|
||||
public function _initialize()
|
||||
{
|
||||
$this->model = new UserModel();
|
||||
}
|
||||
}
|
||||
|
||||
class MultiAssetRechargeTest
|
||||
{
|
||||
private $passed = 0;
|
||||
private $failed = 0;
|
||||
private $testUserId = 0;
|
||||
|
||||
public function run()
|
||||
{
|
||||
echo "========================================================\n";
|
||||
echo " 会员多资产(余额/积分/马豆/兑换券)充值业务测试 \n";
|
||||
echo "========================================================\n\n";
|
||||
|
||||
$this->setUp();
|
||||
|
||||
try {
|
||||
$this->testInvalidAssetType();
|
||||
$this->testMoneyRechargeAndDeduct();
|
||||
$this->testMoneyOverdrawn();
|
||||
$this->testScoreRechargeAndDeduct();
|
||||
$this->testScoreOverdrawn();
|
||||
$this->testMadowRechargeAndDeduct();
|
||||
$this->testMadowOverdrawn();
|
||||
$this->testVoucherRechargeAndDeduct();
|
||||
$this->testVoucherOverdrawn();
|
||||
} finally {
|
||||
$this->tearDown();
|
||||
}
|
||||
|
||||
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) {
|
||||
echo " [PASS] {$testName}\n";
|
||||
$this->passed++;
|
||||
} else {
|
||||
echo " [FAIL] {$testName} - {$detail}\n";
|
||||
$this->failed++;
|
||||
}
|
||||
}
|
||||
|
||||
private function setUp()
|
||||
{
|
||||
// 查找或创建一个测试用户
|
||||
$user = UserModel::where('username', 'test_recharge_user')->find();
|
||||
if (!$user) {
|
||||
$now = time();
|
||||
$id = Db::name('user')->insertGetId([
|
||||
'username' => 'test_recharge_user',
|
||||
'nickname' => '充值测试员',
|
||||
'password' => md5('123456'),
|
||||
'salt' => '123456',
|
||||
'mobile' => '13988889999',
|
||||
'money' => 100.00,
|
||||
'score' => 500,
|
||||
'status' => 'normal',
|
||||
'createtime' => $now,
|
||||
'updatetime' => $now,
|
||||
]);
|
||||
$this->testUserId = $id;
|
||||
} else {
|
||||
$this->testUserId = $user->id;
|
||||
Db::name('user')->where('id', $this->testUserId)->update([
|
||||
'money' => 100.00,
|
||||
'score' => 500,
|
||||
]);
|
||||
}
|
||||
|
||||
// 初始化 agent 记录
|
||||
$agent = Db::name('shopro_commission_agent')->where('user_id', $this->testUserId)->find();
|
||||
if (!$agent) {
|
||||
Db::name('shopro_commission_agent')->insert([
|
||||
'user_id' => $this->testUserId,
|
||||
'level' => 1,
|
||||
'status' => 'normal',
|
||||
'madow' => '10.00000000',
|
||||
'voucher_total' => '200.00',
|
||||
'voucher_released' => '200.00',
|
||||
'voucher_used' => '50.00',
|
||||
'createtime' => time(),
|
||||
'updatetime' => time(),
|
||||
]);
|
||||
} else {
|
||||
Db::name('shopro_commission_agent')->where('user_id', $this->testUserId)->update([
|
||||
'madow' => '10.00000000',
|
||||
'voucher_total' => '200.00',
|
||||
'voucher_released' => '200.00',
|
||||
'voucher_used' => '50.00',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function tearDown()
|
||||
{
|
||||
if ($this->testUserId) {
|
||||
try {
|
||||
Db::name('user')->where('id', $this->testUserId)->delete();
|
||||
Db::name('shopro_commission_agent')->where('user_id', $this->testUserId)->delete();
|
||||
Db::name('shopro_partner_reward')->where('user_id', $this->testUserId)->delete();
|
||||
Db::name('shopro_user_wallet_log')->where('user_id', $this->testUserId)->delete();
|
||||
Db::name('user_money_log')->where('user_id', $this->testUserId)->delete();
|
||||
Db::name('user_score_log')->where('user_id', $this->testUserId)->delete();
|
||||
} catch (\Exception $e) {
|
||||
// 忽略非关键日志表删除错误
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function invokeRecharge($params)
|
||||
{
|
||||
$controller = new TestUserController();
|
||||
$request = Request::create('shopro/user/user/recharge', 'POST', $params, [], [], [
|
||||
'HTTP_X_REQUESTED_WITH' => 'xmlhttprequest',
|
||||
'HTTP_ACCEPT' => 'application/json',
|
||||
]);
|
||||
|
||||
// 绑定 request 到 控制器
|
||||
$ref = new \ReflectionClass($controller);
|
||||
$reqProp = $ref->getProperty('request');
|
||||
$reqProp->setAccessible(true);
|
||||
$reqProp->setValue($controller, $request);
|
||||
|
||||
// 设置 Request 全局单例
|
||||
$refInst = new \ReflectionClass(Request::class);
|
||||
$instProp = $refInst->getProperty('instance');
|
||||
$instProp->setAccessible(true);
|
||||
$instProp->setValue(null, $request);
|
||||
|
||||
try {
|
||||
$controller->recharge();
|
||||
} catch (\think\exception\HttpResponseException $e) {
|
||||
$data = $e->getResponse()->getData();
|
||||
if (is_string($data)) {
|
||||
$decoded = json_decode($data, true);
|
||||
if (is_array($decoded)) {
|
||||
return $decoded;
|
||||
}
|
||||
}
|
||||
if (!is_array($data)) {
|
||||
echo "[DEBUG RAW DATA] " . substr((string)$data, 0, 200) . "\n";
|
||||
}
|
||||
return is_array($data) ? $data : ['code' => 0, 'msg' => (string) $data];
|
||||
}
|
||||
return ['code' => 0, 'msg' => '未知响应'];
|
||||
}
|
||||
|
||||
public function testInvalidAssetType()
|
||||
{
|
||||
$res = $this->invokeRecharge([
|
||||
'id' => $this->testUserId,
|
||||
'type' => 'invalid_coin',
|
||||
'amount' => 10,
|
||||
'memo' => '测试非法资产类型',
|
||||
]);
|
||||
$this->assert($res['code'] === 0 && strpos($res['msg'], '不支持') !== false, '非法资产类型校验拦截', $res['msg'] ?? '');
|
||||
}
|
||||
|
||||
public function testMoneyRechargeAndDeduct()
|
||||
{
|
||||
// 充值 50 元
|
||||
$res1 = $this->invokeRecharge([
|
||||
'id' => $this->testUserId,
|
||||
'type' => 'money',
|
||||
'amount' => 50,
|
||||
'memo' => '充值50元',
|
||||
]);
|
||||
$u1 = UserModel::get($this->testUserId);
|
||||
$this->assert($res1['code'] === 1 && (float)$u1->money == 150.00, '余额充值成功且到账', "当前余额: {$u1->money}");
|
||||
|
||||
// 扣除 30 元
|
||||
$res2 = $this->invokeRecharge([
|
||||
'id' => $this->testUserId,
|
||||
'type' => 'money',
|
||||
'amount' => -30,
|
||||
'memo' => '扣除30元',
|
||||
]);
|
||||
$u2 = UserModel::get($this->testUserId);
|
||||
$this->assert($res2['code'] === 1 && (float)$u2->money == 120.00, '余额扣除成功且准确', "当前余额: {$u2->money}");
|
||||
}
|
||||
|
||||
public function testMoneyOverdrawn()
|
||||
{
|
||||
// 尝试扣除超过当前余额 (120元)
|
||||
$res = $this->invokeRecharge([
|
||||
'id' => $this->testUserId,
|
||||
'type' => 'money',
|
||||
'amount' => -200,
|
||||
'memo' => '超额扣除余额',
|
||||
]);
|
||||
$this->assert($res['code'] === 0 && strpos($res['msg'], '不足') !== false, '余额不足拦截超扣', $res['msg'] ?? '');
|
||||
}
|
||||
|
||||
public function testScoreRechargeAndDeduct()
|
||||
{
|
||||
// 充值 100 积分
|
||||
$res1 = $this->invokeRecharge([
|
||||
'id' => $this->testUserId,
|
||||
'type' => 'score',
|
||||
'amount' => 100,
|
||||
'memo' => '充值100积分',
|
||||
]);
|
||||
$u1 = UserModel::get($this->testUserId);
|
||||
$this->assert($res1['code'] === 1 && $u1->score == 600, '积分充值成功且到账', "当前积分: {$u1->score}");
|
||||
|
||||
// 扣除 200 积分
|
||||
$res2 = $this->invokeRecharge([
|
||||
'id' => $this->testUserId,
|
||||
'type' => 'score',
|
||||
'amount' => -200,
|
||||
'memo' => '扣除200积分',
|
||||
]);
|
||||
$u2 = UserModel::get($this->testUserId);
|
||||
$this->assert($res2['code'] === 1 && $u2->score == 400, '积分扣除成功且准确', "当前积分: {$u2->score}");
|
||||
}
|
||||
|
||||
public function testScoreOverdrawn()
|
||||
{
|
||||
// 尝试超额扣除积分
|
||||
$res = $this->invokeRecharge([
|
||||
'id' => $this->testUserId,
|
||||
'type' => 'score',
|
||||
'amount' => -1000,
|
||||
'memo' => '超额扣除积分',
|
||||
]);
|
||||
$this->assert($res['code'] === 0 && strpos($res['msg'], '不足') !== false, '积分不足拦截超扣', $res['msg'] ?? '');
|
||||
}
|
||||
|
||||
public function testMadowRechargeAndDeduct()
|
||||
{
|
||||
// 充值 5.12345678 颗马豆
|
||||
$res1 = $this->invokeRecharge([
|
||||
'id' => $this->testUserId,
|
||||
'type' => 'madow',
|
||||
'amount' => '5.12345678',
|
||||
'memo' => '充值马豆',
|
||||
]);
|
||||
$agent1 = AgentModel::where('user_id', $this->testUserId)->find();
|
||||
$this->assert($res1['code'] === 1 && $agent1->madow == '15.12345678', '马豆8位高精度充值到账', "当前马豆: {$agent1->madow}");
|
||||
|
||||
// 扣除 3.00000000 颗马豆
|
||||
$res2 = $this->invokeRecharge([
|
||||
'id' => $this->testUserId,
|
||||
'type' => 'madow',
|
||||
'amount' => '-3.00000000',
|
||||
'memo' => '扣除马豆',
|
||||
]);
|
||||
$agent2 = AgentModel::where('user_id', $this->testUserId)->find();
|
||||
$this->assert($res2['code'] === 1 && $agent2->madow == '12.12345678', '马豆高精度扣除成功', "当前马豆: {$agent2->madow}");
|
||||
|
||||
// 验证流水表记录
|
||||
$log = RewardModel::where('user_id', $this->testUserId)->where('reward_type', 'admin_madow')->order('id', 'desc')->find();
|
||||
$this->assert($log && $log->status == 1, '马豆充值写入收益流水明细');
|
||||
}
|
||||
|
||||
public function testMadowOverdrawn()
|
||||
{
|
||||
// 尝试超额扣除马豆
|
||||
$res = $this->invokeRecharge([
|
||||
'id' => $this->testUserId,
|
||||
'type' => 'madow',
|
||||
'amount' => '-99.00000000',
|
||||
'memo' => '超额扣除马豆',
|
||||
]);
|
||||
$this->assert($res['code'] === 0 && strpos($res['msg'], '不足') !== false, '马豆持仓不足拦截超扣', $res['msg'] ?? '');
|
||||
}
|
||||
|
||||
public function testVoucherRechargeAndDeduct()
|
||||
{
|
||||
// 当前 released=200, used=50, available=150
|
||||
// 充值 100 元兑换券
|
||||
$res1 = $this->invokeRecharge([
|
||||
'id' => $this->testUserId,
|
||||
'type' => 'voucher',
|
||||
'amount' => 100,
|
||||
'memo' => '充值兑换券',
|
||||
]);
|
||||
$agent1 = AgentModel::where('user_id', $this->testUserId)->find();
|
||||
$avail1 = $agent1->voucher_released - $agent1->voucher_used;
|
||||
$this->assert($res1['code'] === 1 && $avail1 == 250.00, '兑换券充值成功可用额度增加', "可用券: {$avail1}");
|
||||
|
||||
// 扣除 80 元兑换券
|
||||
$res2 = $this->invokeRecharge([
|
||||
'id' => $this->testUserId,
|
||||
'type' => 'voucher',
|
||||
'amount' => -80,
|
||||
'memo' => '扣除兑换券',
|
||||
]);
|
||||
$agent2 = AgentModel::where('user_id', $this->testUserId)->find();
|
||||
$avail2 = $agent2->voucher_released - $agent2->voucher_used;
|
||||
$this->assert($res2['code'] === 1 && $avail2 == 170.00, '兑换券扣除成功可用额度准确减少', "可用券: {$avail2}");
|
||||
|
||||
// 验证流水表记录
|
||||
$log = RewardModel::where('user_id', $this->testUserId)->where('reward_type', 'admin_voucher')->order('id', 'desc')->find();
|
||||
$this->assert($log && $log->amount == -80.00, '兑换券变动写入收益流水明细');
|
||||
}
|
||||
|
||||
public function testVoucherOverdrawn()
|
||||
{
|
||||
// 尝试超额扣除兑换券 (当前可用 170)
|
||||
$res = $this->invokeRecharge([
|
||||
'id' => $this->testUserId,
|
||||
'type' => 'voucher',
|
||||
'amount' => -300,
|
||||
'memo' => '超额扣除兑换券',
|
||||
]);
|
||||
$this->assert($res['code'] === 0 && strpos($res['msg'], '不足') !== false, '兑换券可用额度不足拦截超扣', $res['msg'] ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
$test = new MultiAssetRechargeTest();
|
||||
exit($test->run());
|
||||
111
tests/partner/User10Verify.php
普通文件
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
namespace tests\partner;
|
||||
|
||||
define('APP_PATH', __DIR__ . '/../../application/');
|
||||
define('ROOT_PATH', dirname(realpath(APP_PATH)) . '/');
|
||||
require_once __DIR__ . '/../../thinkphp/base.php';
|
||||
require_once __DIR__ . '/../../thinkphp/helper.php';
|
||||
|
||||
if (is_file(ROOT_PATH . '.env')) {
|
||||
$env = parse_ini_file(ROOT_PATH . '.env', true);
|
||||
foreach ($env as $key => $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 __DIR__ . '/../../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 __DIR__ . '/../../addons/shopro/helper/helper.php';
|
||||
|
||||
use think\Request;
|
||||
use think\Db;
|
||||
|
||||
class DirectTestController extends \app\admin\controller\shopro\user\User
|
||||
{
|
||||
public function _initialize()
|
||||
{
|
||||
$this->model = new \app\admin\model\shopro\user\User();
|
||||
}
|
||||
}
|
||||
|
||||
function doRecharge($type, $amount, $memo)
|
||||
{
|
||||
$ctrl = new DirectTestController();
|
||||
$req = Request::create('shopro/user/user/recharge', 'POST', [
|
||||
'id' => 10,
|
||||
'type' => $type,
|
||||
'amount' => $amount,
|
||||
'memo' => $memo
|
||||
], [], [], ['HTTP_X_REQUESTED_WITH' => 'xmlhttprequest', 'HTTP_ACCEPT' => 'application/json']);
|
||||
|
||||
$ref = new \ReflectionClass($ctrl);
|
||||
$p = $ref->getProperty('request');
|
||||
$p->setAccessible(true);
|
||||
$p->setValue($ctrl, $req);
|
||||
|
||||
$refInst = new \ReflectionClass(Request::class);
|
||||
$inst = $refInst->getProperty('instance');
|
||||
$inst->setAccessible(true);
|
||||
$inst->setValue(null, $req);
|
||||
|
||||
try {
|
||||
$ctrl->recharge();
|
||||
} catch (\think\exception\HttpResponseException $e) {
|
||||
$d = $e->getResponse()->getData();
|
||||
return is_array($d) ? $d : json_decode($d, true);
|
||||
}
|
||||
return ['code' => 0, 'msg' => '未知'];
|
||||
}
|
||||
|
||||
echo "=== 开始对会员 10 (尚善) 真实链路充值验证 ===\n";
|
||||
|
||||
// 1. 马豆充值 +8 颗
|
||||
$r1 = doRecharge('madow', '8.00000000', '真实环境测试充值马豆');
|
||||
$agent1 = Db::name('shopro_commission_agent')->where('user_id', 10)->find();
|
||||
echo "1. 充值马豆+8结果: " . json_encode($r1, JSON_UNESCAPED_UNICODE) . " 当前持仓: " . ($agent1['madow'] ?? '0') . "\n";
|
||||
|
||||
// 2. 马豆扣除 -8 颗
|
||||
$r2 = doRecharge('madow', '-8.00000000', '真实环境测试扣除马豆');
|
||||
$agent2 = Db::name('shopro_commission_agent')->where('user_id', 10)->find();
|
||||
echo "2. 扣除马豆-8结果: " . json_encode($r2, JSON_UNESCAPED_UNICODE) . " 恢复持仓: " . ($agent2['madow'] ?? '0') . "\n";
|
||||
|
||||
// 3. 兑换券充值 +20 元
|
||||
$r3 = doRecharge('voucher', '20.00', '真实环境测试充值兑换券');
|
||||
$agent3 = Db::name('shopro_commission_agent')->where('user_id', 10)->find();
|
||||
echo "3. 充值兑换券+20结果: " . json_encode($r3, JSON_UNESCAPED_UNICODE) . " released: " . ($agent3['voucher_released'] ?? '0') . " used: " . ($agent3['voucher_used'] ?? '0') . "\n";
|
||||
|
||||
// 4. 兑换券扣除 -20 元
|
||||
$r4 = doRecharge('voucher', '-20.00', '真实环境测试扣除兑换券');
|
||||
$agent4 = Db::name('shopro_commission_agent')->where('user_id', 10)->find();
|
||||
echo "4. 扣除兑换券-20结果: " . json_encode($r4, JSON_UNESCAPED_UNICODE) . " released: " . ($agent4['voucher_released'] ?? '0') . " used: " . ($agent4['voucher_used'] ?? '0') . "\n";
|
||||
|
||||
// 5. 积分充值 +10
|
||||
$r5 = doRecharge('score', 10, '真实环境测试充值积分');
|
||||
$u5 = Db::name('user')->where('id', 10)->find();
|
||||
echo "5. 充值积分+10结果: " . json_encode($r5, JSON_UNESCAPED_UNICODE) . " 当前积分: " . ($u5['score'] ?? 0) . "\n";
|
||||
|
||||
// 6. 积分扣除 -10
|
||||
$r6 = doRecharge('score', -10, '真实环境测试扣除积分');
|
||||
$u6 = Db::name('user')->where('id', 10)->find();
|
||||
echo "6. 扣除积分-10结果: " . json_encode($r6, JSON_UNESCAPED_UNICODE) . " 恢复积分: " . ($u6['score'] ?? 0) . "\n";
|
||||
|
||||
// 7. 查询收益中心流水表中新增的充值流水明细
|
||||
$rewards = Db::name('shopro_partner_reward')->where('user_id', 10)->whereIn('reward_type', ['admin_madow', 'admin_voucher'])->order('id', 'desc')->limit(4)->select();
|
||||
echo "\n=== 会员 10 收益流水落库检查 (共 " . count($rewards) . " 笔充值/扣除流水) ===\n";
|
||||
foreach ($rewards as $idx => $item) {
|
||||
echo " [" . ($idx + 1) . "] 类型: {$item['reward_type']} | 金额: {$item['amount']} | 马豆: {$item['madow']} | 备注: " . $item['ext'] . "\n";
|
||||
}
|
||||
|
||||
echo "\n=== 会员 10 真实测试全流程 100% 成功完毕 ===\n";
|
||||
@@ -0,0 +1,96 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
function assert(condition, testName, detail = '') {
|
||||
if (condition) {
|
||||
console.log(` [PASS] ${testName}`);
|
||||
passed++;
|
||||
} else {
|
||||
console.error(` [FAIL] ${testName} - ${detail}`);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log("========================================================");
|
||||
console.log(" 订单/分润数据清理与马豆汇率回归初始值规范自动化测试");
|
||||
console.log("========================================================\n");
|
||||
|
||||
// 1. 验证控制台清理命令 PartnerClean.php 存在且逻辑完整
|
||||
const cleanCmdPath = path.resolve(__dirname, '../addons/shopro/console/PartnerClean.php');
|
||||
assert(fs.existsSync(cleanCmdPath), 'PartnerClean.php 控制台命令文件存在');
|
||||
const cleanCmdContent = fs.readFileSync(cleanCmdPath, 'utf-8');
|
||||
|
||||
assert(
|
||||
cleanCmdContent.includes("shopro:partner:clean"),
|
||||
'命令名称定义为 shopro:partner:clean'
|
||||
);
|
||||
assert(
|
||||
cleanCmdContent.includes("shopro_order") && cleanCmdContent.includes("shopro_order_item") && cleanCmdContent.includes("shopro_order_aftersale"),
|
||||
'涵盖清理商城订单主表、明细表与售后表'
|
||||
);
|
||||
assert(
|
||||
cleanCmdContent.includes("shopro_partner_reward") && cleanCmdContent.includes("shopro_commission_order") && cleanCmdContent.includes("shopro_commission_reward"),
|
||||
'涵盖清理二开合伙人流水与分销佣金明细'
|
||||
);
|
||||
assert(
|
||||
cleanCmdContent.includes("shopro_partner_rate_log") && cleanCmdContent.includes("shopro_partner_daily_report") && cleanCmdContent.includes("shopro_partner_voucher_plan"),
|
||||
'涵盖清理马豆汇率大盘快照、日结对账报表与兑换券释放计划'
|
||||
);
|
||||
assert(
|
||||
cleanCmdContent.includes("`madow` = 0.00000000") && cleanCmdContent.includes("`direct_order_seq` = 0") && cleanCmdContent.includes("`voucher_total` = 0.00"),
|
||||
'涵盖重置分销商持仓马豆、推三返一序号、兑换券资产归零'
|
||||
);
|
||||
assert(
|
||||
cleanCmdContent.includes("cache('partner_madow_current_rate', null)"),
|
||||
'命令执行后清除马豆汇率缓存 partner_madow_current_rate'
|
||||
);
|
||||
assert(
|
||||
cleanCmdContent.includes("Db::startTrans()") && cleanCmdContent.includes("Db::commit()"),
|
||||
'命令具备严谨的事务控制与异常回滚机制'
|
||||
);
|
||||
|
||||
// 2. 验证 Shopro.php 已注册该 CLI 命令
|
||||
const shoproPath = path.resolve(__dirname, '../addons/shopro/Shopro.php');
|
||||
assert(fs.existsSync(shoproPath), 'Shopro.php 文件存在');
|
||||
const shoproContent = fs.readFileSync(shoproPath, 'utf-8');
|
||||
assert(
|
||||
shoproContent.includes("'addons\\shopro\\console\\PartnerClean'"),
|
||||
'Shopro.php 中已自动注册 PartnerClean CLI 控制台命令'
|
||||
);
|
||||
|
||||
// 3. 验证归档 SQL 文件
|
||||
const sqlPath = path.resolve(__dirname, '../docs/sql/clean_order_and_partner_data.sql');
|
||||
assert(fs.existsSync(sqlPath), 'clean_order_and_partner_data.sql 归档文件存在');
|
||||
const sqlContent = fs.readFileSync(sqlPath, 'utf-8');
|
||||
|
||||
assert(
|
||||
sqlContent.includes("TRUNCATE TABLE `fa_shopro_order`") && sqlContent.includes("TRUNCATE TABLE `fa_shopro_order_item`"),
|
||||
'SQL 脚本包含清空订单表与订单项表'
|
||||
);
|
||||
assert(
|
||||
sqlContent.includes("TRUNCATE TABLE `fa_shopro_partner_reward`"),
|
||||
'SQL 脚本包含清空二开收益中心流水表'
|
||||
);
|
||||
assert(
|
||||
sqlContent.includes("TRUNCATE TABLE `fa_shopro_partner_rate_log`"),
|
||||
'SQL 脚本包含清空马豆汇率历史快照日志'
|
||||
);
|
||||
assert(
|
||||
sqlContent.includes("`madow` = 0.00000000") && sqlContent.includes("`direct_order_seq` = 0"),
|
||||
'SQL 脚本包含重置分销商马豆与推三返一序号'
|
||||
);
|
||||
assert(
|
||||
sqlContent.includes("START TRANSACTION;") && sqlContent.includes("COMMIT;"),
|
||||
'SQL 脚本具备严格的事务包裹'
|
||||
);
|
||||
|
||||
console.log("\n--------------------------------------------------------");
|
||||
console.log(`测试执行结果: 通过 ${passed} 项, 失败 ${failed} 项`);
|
||||
console.log("========================================================\n");
|
||||
|
||||
if (failed > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
function assert(condition, testName, detail = '') {
|
||||
if (condition) {
|
||||
console.log(` [PASS] ${testName}`);
|
||||
passed++;
|
||||
} else {
|
||||
console.error(` [FAIL] ${testName} - ${detail}`);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log("========================================================");
|
||||
console.log(" 马豆实时动态汇率、首单步进对齐与精度自适应自动化测试");
|
||||
console.log("========================================================\n");
|
||||
|
||||
// 1. RateService 实时汇率与缓存解耦验证
|
||||
const rateServicePath = path.resolve(__dirname, '../addons/shopro/service/partner/RateService.php');
|
||||
assert(fs.existsSync(rateServicePath), 'RateService.php 文件存在');
|
||||
const rateServiceContent = fs.readFileSync(rateServicePath, 'utf-8');
|
||||
|
||||
assert(
|
||||
!rateServiceContent.includes("RateLog::order('id', 'desc')->find()"),
|
||||
'currentRate() 已移除日结历史快照死锁逻辑,不再永久返回旧汇率'
|
||||
);
|
||||
assert(
|
||||
rateServiceContent.includes('currentRate($forceRefresh = false)'),
|
||||
'currentRate() 支持 $forceRefresh 参数穿透短缓存实时重算'
|
||||
);
|
||||
assert(
|
||||
rateServiceContent.includes('public static function clearCache()'),
|
||||
'RateService 包含 clearCache() 静态方法支持外部主动刷新'
|
||||
);
|
||||
assert(
|
||||
rateServiceContent.includes('$multiple = max(1, floor($n / $step))'),
|
||||
'calcRate() 算法严谨保留起步下限保底与线性步进'
|
||||
);
|
||||
|
||||
// 2. 订单支付监听器联动验证
|
||||
const listenerPath = path.resolve(__dirname, '../addons/shopro/listener/Partner.php');
|
||||
assert(fs.existsSync(listenerPath), 'Partner.php 监听器存在');
|
||||
const listenerContent = fs.readFileSync(listenerPath, 'utf-8');
|
||||
assert(
|
||||
listenerContent.includes('RateService::clearCache()'),
|
||||
'合格订单支付成功后自动调用 RateService::clearCache() 触发实时汇率联动'
|
||||
);
|
||||
|
||||
// 3. IdentityService 马豆格式化与精度优化验证
|
||||
const identityPath = path.resolve(__dirname, '../addons/shopro/service/partner/IdentityService.php');
|
||||
assert(fs.existsSync(identityPath), 'IdentityService.php 存在');
|
||||
const identityContent = fs.readFileSync(identityPath, 'utf-8');
|
||||
|
||||
assert(
|
||||
identityContent.includes("number_format((float) $value, 0, '.', '')"),
|
||||
'formatMadow() 保持历史纯整数输出契约'
|
||||
);
|
||||
assert(
|
||||
identityContent.includes('$floatVal > 0 && $floatVal < 1'),
|
||||
'formatMadow() 具备小于 1 豆的小数自适应保留能力,杜绝分润到账显示为 0 豆'
|
||||
);
|
||||
|
||||
// 4. 用户端 madow.vue 界面精度与兜底验证
|
||||
const madowVuePath = path.resolve(__dirname, '../uniapp/pages/commission/madow.vue');
|
||||
assert(fs.existsSync(madowVuePath), 'madow.vue 存在');
|
||||
const madowVueContent = fs.readFileSync(madowVuePath, 'utf-8');
|
||||
|
||||
assert(
|
||||
madowVueContent.includes('formatInt') && madowVueContent.includes('num > 0 && num < 1'),
|
||||
'madow.vue 中的 formatInt 支持微量小数自适应显示'
|
||||
);
|
||||
assert(
|
||||
madowVueContent.includes('<0.01'),
|
||||
'madow.vue 对微小市值(<0.01元)具备友好的 <0.01 优雅兜底展示'
|
||||
);
|
||||
|
||||
// 5. 纯算法逻辑与数值模拟测试
|
||||
function calcRateSim(n, base = 0.00000075, step = 1) {
|
||||
const multiple = Math.max(1, Math.floor(n / step));
|
||||
return Number((base * multiple).toFixed(10));
|
||||
}
|
||||
|
||||
assert(calcRateSim(0) === 0.00000075, '模拟测试:N=0 单时保底输出基准汇率 0.00000075');
|
||||
assert(calcRateSim(1) === 0.00000075, '模拟测试:N=1 单时等于 0.00000075');
|
||||
assert(calcRateSim(2) === 0.0000015, '模拟测试:N=2 单时递增至 0.00000150');
|
||||
assert(calcRateSim(10000) === 0.0075, '模拟测试:N=10,000 单时精确达到 0.0075 元/豆');
|
||||
|
||||
function formatMadowSim(val) {
|
||||
const num = parseFloat(val) || 0;
|
||||
if (num > 0 && num < 1) {
|
||||
return parseFloat(num.toFixed(4)).toString();
|
||||
}
|
||||
return String(Math.floor(num));
|
||||
}
|
||||
|
||||
assert(formatMadowSim(0.85) === '0.85', '模拟测试:0.85 豆自适应显示为 0.85');
|
||||
assert(formatMadowSim(0.005) === '0.005', '模拟测试:0.005 豆自适应显示为 0.005');
|
||||
assert(formatMadowSim(100) === '100', '模拟测试:100 豆整洁显示为 100');
|
||||
assert(formatMadowSim(0) === '0', '模拟测试:0 豆整洁显示为 0');
|
||||
|
||||
console.log("\n--------------------------------------------------------");
|
||||
console.log(`测试执行结果: 通过 ${passed} 项, 失败 ${failed} 项`);
|
||||
console.log("========================================================\n");
|
||||
|
||||
if (failed > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "恒链数通",
|
||||
"appid": "__UNI__082C0BA",
|
||||
"appid": "__UNI__5BAD75E",
|
||||
"description": "Shopro是由SheepJS团队开发,使用Uniapp+Vue3技术驱动的在线商城系统,内含诸多功能与丰富的活动,期待您的使用和反馈。",
|
||||
"versionName": "3.2.0",
|
||||
"versionCode": 320,
|
||||
@@ -185,6 +185,7 @@
|
||||
"appid": "wx604780831204335b",
|
||||
"setting": {
|
||||
"urlCheck": false,
|
||||
"es6": true,
|
||||
"minified": true,
|
||||
"postcss": true
|
||||
},
|
||||
|
||||
@@ -36,7 +36,9 @@
|
||||
<text class="metric-label">预估马币总市值</text>
|
||||
<view class="metric-val-wrap ss-flex ss-col-bottom">
|
||||
<text class="currency-symbol">¥</text>
|
||||
<text class="metric-val">{{ state.info.madow_value || '0.00' }}</text>
|
||||
<text class="metric-val" v-if="parseFloat(state.info.madow_value) > 0">{{ state.info.madow_value }}</text>
|
||||
<text class="metric-val" v-else-if="parseFloat(state.info.madow) > 0"><0.01</text>
|
||||
<text class="metric-val" v-else>{{ state.info.madow_value || '0.00' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="footer-tip">
|
||||
@@ -187,8 +189,11 @@
|
||||
|
||||
function formatInt(val) {
|
||||
if (val === '' || val === null || val === undefined || val === '--') return '0';
|
||||
const num = Math.floor(parseFloat(val) || 0);
|
||||
return String(num);
|
||||
const num = parseFloat(val) || 0;
|
||||
if (num > 0 && num < 1) {
|
||||
return parseFloat(num.toFixed(4)).toString();
|
||||
}
|
||||
return String(Math.floor(num));
|
||||
}
|
||||
|
||||
function pickList(payload) {
|
||||
|
||||
@@ -1,65 +1,65 @@
|
||||
import request from '@/sheep/request';
|
||||
|
||||
export default {
|
||||
// 分销商详情
|
||||
agent: () =>
|
||||
request({
|
||||
url: 'commission.agent',
|
||||
method: 'GET',
|
||||
custom: {
|
||||
showLoading: false,
|
||||
showError: false,
|
||||
},
|
||||
}),
|
||||
// 分销表单
|
||||
form: () =>
|
||||
request({
|
||||
url: 'commission.agent/form',
|
||||
method: 'GET',
|
||||
}),
|
||||
// 申请分销商
|
||||
apply: (data) =>
|
||||
request({
|
||||
url: 'commission.agent/apply',
|
||||
method: 'POST',
|
||||
data,
|
||||
custom: {
|
||||
showSuccess: true,
|
||||
},
|
||||
}),
|
||||
// 分销动态
|
||||
log: (params) =>
|
||||
request({
|
||||
url: 'commission.log',
|
||||
method: 'GET',
|
||||
params,
|
||||
}),
|
||||
// 分销订单
|
||||
order: (params) =>
|
||||
request({
|
||||
url: 'commission.order',
|
||||
method: 'GET',
|
||||
params,
|
||||
}),
|
||||
// 分销商品
|
||||
goods: (params) =>
|
||||
request({
|
||||
url: 'commission.goods',
|
||||
method: 'GET',
|
||||
params,
|
||||
}),
|
||||
// 我的团队
|
||||
team: (params) =>
|
||||
request({
|
||||
url: 'commission.agent/team',
|
||||
method: 'GET',
|
||||
params,
|
||||
}),
|
||||
// 佣金转余额
|
||||
transfer: (data) =>
|
||||
request({
|
||||
url: 'commission.agent/transfer',
|
||||
method: 'POST',
|
||||
data,
|
||||
}),
|
||||
};
|
||||
import request from "@/sheep/request";
|
||||
|
||||
export default {
|
||||
// 分销商详情
|
||||
agent: () =>
|
||||
request({
|
||||
url: "commission.agent",
|
||||
method: "GET",
|
||||
custom: {
|
||||
showLoading: false,
|
||||
showError: false,
|
||||
},
|
||||
}),
|
||||
// 分销表单
|
||||
form: () =>
|
||||
request({
|
||||
url: "commission.agent/form",
|
||||
method: "GET",
|
||||
}),
|
||||
// 申请分销商
|
||||
apply: (data) =>
|
||||
request({
|
||||
url: "commission.agent/apply",
|
||||
method: "POST",
|
||||
data,
|
||||
custom: {
|
||||
showSuccess: true,
|
||||
},
|
||||
}),
|
||||
// 分销动态
|
||||
log: (params) =>
|
||||
request({
|
||||
url: "commission.log/index",
|
||||
method: "GET",
|
||||
params,
|
||||
}),
|
||||
// 分销订单
|
||||
order: (params) =>
|
||||
request({
|
||||
url: "commission.order",
|
||||
method: "GET",
|
||||
params,
|
||||
}),
|
||||
// 分销商品
|
||||
goods: (params) =>
|
||||
request({
|
||||
url: "commission.goods",
|
||||
method: "GET",
|
||||
params,
|
||||
}),
|
||||
// 我的团队
|
||||
team: (params) =>
|
||||
request({
|
||||
url: "commission.agent/team",
|
||||
method: "GET",
|
||||
params,
|
||||
}),
|
||||
// 佣金转余额
|
||||
transfer: (data) =>
|
||||
request({
|
||||
url: "commission.agent/transfer",
|
||||
method: "POST",
|
||||
data,
|
||||
}),
|
||||
};
|
||||
|
||||
|
之前 宽度: | 高度: | 大小: 160 KiB 之后 宽度: | 高度: | 大小: 28 KiB |