feat(withdraw): 落地方案B一体化商城余额提现与手续费内扣模式,完善多端打款退款与对账闭环

这个提交包含在:
chen
2026-09-20 14:35:38 +08:00
父节点 bee5288e88
当前提交 2b7c34c7f7
共有 65 个文件被更改,包括 3554 次插入 和 579 次删除

查看文件

@@ -166,7 +166,7 @@ class PartnerClean extends Command
$output->writeln("\n========================================================");
$output->writeln(" 重置后指标核对:");
$output->writeln(" 合格订单总数 N : {$resetN} 单 (应为 0)");
$output->writeln(" 当前大盘汇率 : {$formattedRate} 元/豆 (回归初始基准值 0.00000075)");
$output->writeln(" 当前大盘汇率 : {$formattedRate} 元/枚 (回归初始基准值 0.00000075)");
$output->writeln("========================================================\n");
return 0;

查看文件

@@ -163,15 +163,41 @@ class Pay extends Common
// 微信公众号,小程序支付,必须有 openid
if ($payment == 'wechat') {
if (in_array($platform, ['WechatOfficialAccount', 'WechatMiniProgram'])) {
$targetPlatform = lcfirst(str_replace('Wechat', '', $platform));
if (isset($openid) && $openid) {
// 如果传的有 openid
$order_data['payer']['openid'] = $openid;
// 自愈绑定:若当前用户在 third_oauth 中尚无对应平台的绑定记录,自动补建
if ($platform == 'WechatMiniProgram') {
$oauth = \app\admin\model\shopro\ThirdOauth::where([
'user_id' => $order->user_id,
'provider' => 'wechat',
'platform' => $targetPlatform
])->find();
if (!$oauth) {
$existOauth = \app\admin\model\shopro\ThirdOauth::getByOpenid($openid);
if (!$existOauth) {
\app\admin\model\shopro\ThirdOauth::create([
'user_id' => $order->user_id,
'provider' => 'wechat',
'platform' => $targetPlatform,
'openid' => $openid,
'createtime' => time(),
'updatetime' => time(),
]);
} elseif ($existOauth->user_id == 0) {
$existOauth->user_id = $order->user_id;
$existOauth->save();
}
}
}
} else {
// 没有 openid 默认拿下单人的 openid
$oauth = \app\admin\model\shopro\ThirdOauth::where([
'user_id' => $order->user_id,
'provider' => 'Wechat',
'platform' => lcfirst(str_replace('Wechat', '', $platform))
'provider' => 'wechat',
'platform' => $targetPlatform
])->find();
$order_data['payer']['openid'] = $oauth ? $oauth->openid : '';

查看文件

@@ -120,6 +120,13 @@ class Goods extends Common
// 处理商品规格
$skuPrices = $goods['new_sku_prices'];
$content = $goods['content'];
if ($content) {
$cdnurl = cdnurl('', true);
if ($cdnurl) {
$cdnurl = rtrim($cdnurl, '/');
$content = preg_replace('/<img([^>]+)src=["\'](\/uploads\/[^"\']+)["\']/i', '<img$1src="' . $cdnurl . '$2"', $content);
}
}
$goods = $goods->toArray();
$goods['sku_prices'] = $skuPrices;
$goods['content'] = $content;

查看文件

@@ -45,8 +45,10 @@ class Wechat extends Common
if ($result) {
$auth = \app\common\library\Auth::instance();
$oauthInfo = $this->wechat->getOauthInfo();
$this->success('登陆成功', [
'token' => $auth->isLogin() ? $auth->getToken() : ''
'token' => $auth->isLogin() ? $auth->getToken() : '',
'openid' => $oauthInfo ? ($oauthInfo->openid ?? '') : ''
]);
}
$this->error('登陆失败');

查看文件

@@ -21,6 +21,63 @@ class Base
public $config = null; // 支付参数
/**
* 确保 Yansongda Pay V3 类库与命名空间正确加载
*/
public static function ensurePayLoaded()
{
if (class_exists('\Yansongda\Pay\Pay') && method_exists('\Yansongda\Pay\Pay', 'config')) {
return;
}
// 尝试触发 epay 钩子
\think\Hook::listen('epay_config_init');
if (class_exists('\Yansongda\Pay\Pay') && method_exists('\Yansongda\Pay\Pay', 'config')) {
return;
}
// 保障性直接注册 epay 插件中的 Yansongda V3 类库
$libraryDir = (defined('ADDON_PATH') ? ADDON_PATH : (ROOT_PATH . 'addons' . DS)) . 'epay' . DS . 'library' . DS;
if (is_dir($libraryDir)) {
$versionDir = (PHP_VERSION_ID >= 80000 && is_dir($libraryDir . 'v3new')) ? 'v3new' : 'v3';
$yansongdaDir = $libraryDir . $versionDir . DS . 'Yansongda' . DS;
if (is_dir($yansongdaDir)) {
if (is_dir($yansongdaDir . 'Artful')) {
\think\Loader::addNamespace('Yansongda\Artful', $yansongdaDir . 'Artful' . DS);
if (file_exists($yansongdaDir . 'Artful' . DS . 'Functions.php')) {
require_once $yansongdaDir . 'Artful' . DS . 'Functions.php';
}
}
\think\Loader::addNamespace('Yansongda\Pay', $yansongdaDir . 'Pay' . DS);
$checkArr = [
'\Hyperf\Context\Context' => 'context',
'\Hyperf\Contract\Castable' => 'contract',
'\Hyperf\Engine\Constant' => 'engine',
'\Hyperf\Macroable\Macroable' => 'macroable',
'\Hyperf\Pimple\Container' => 'pimple',
'\Hyperf\Utils\Arr' => 'utils',
];
foreach ($checkArr as $index => $item) {
if (!class_exists($index) && is_dir($libraryDir . 'hyperf' . DS . $item . DS . 'src' . DS)) {
\think\Loader::addNamespace(substr($index, 1, strrpos($index, '\\') - 1), $libraryDir . 'hyperf' . DS . $item . DS . 'src' . DS);
}
}
if (!class_exists('\Yansongda\Supports\Logger') && is_dir($yansongdaDir . 'Supports')) {
\think\Loader::addNamespace('Yansongda\Supports', $yansongdaDir . 'Supports' . DS);
}
if (file_exists($yansongdaDir . 'Pay' . DS . 'Functions.php')) {
require_once $yansongdaDir . 'Pay' . DS . 'Functions.php';
}
}
}
}
/**
* yansongda 支付初始化
*
@@ -30,6 +87,8 @@ class Base
*/
public function init($payment, $config = [], $type = 'normal')
{
self::ensurePayLoaded();
$this->config = $this->getConfig($payment, $config, $type);
$this->pay = Pay::config($this->config);

查看文件

@@ -473,7 +473,12 @@ class Wechat extends Base
break;
}
$config['notify_url'] = request()->domain() . '/addons/shopro/pay/notify/payment/wechat/platform/' . $this->platform;
$domain = request()->domain();
if (empty($domain) || $domain == 'http://' || $domain == 'https://') {
$domain = config('site.cdnurl') ?: (config('site.url') ?: 'https://henglian.gynhc.com');
}
$domain = rtrim($domain, '/');
$config['notify_url'] = $domain . '/addons/shopro/pay/notify/payment/wechat/platform/' . $this->platform;
$config['mch_secret_cert'] = ROOT_PATH . 'public' . $config['mch_secret_cert'];
$config['mch_public_cert_path'] = ROOT_PATH . 'public' . $config['mch_public_cert_path'];

查看文件

@@ -53,7 +53,10 @@ class Withdraw
// 发起提现申请
public function apply($params)
{
$walletType = $params['wallet_type'] ?? 'commission';
$walletType = $params['wallet_type'] ?? 'money';
if (!in_array($walletType, ['money', 'commission', 'madow'])) {
$walletType = 'money';
}
$type = $params['type'] ?? '';
$money = $params['money'] ?? 0;
$money = (string)$money;
@@ -66,10 +69,17 @@ class Withdraw
$charge = bcmul($money, (string)$this->config['charge_rate'], 2);
// 检查提现规则
$this->checkApply($type, $money, $charge);
$this->checkApply($type, $money, $charge, $walletType);
// 获取账号信息
$withdrawInfo = $this->getAccountInfo($type, $params);
$withdrawInfo['wallet_type'] = $walletType;
if ($walletType === 'money') {
$withdrawInfo['提现资产'] = '商城余额';
} elseif ($walletType === 'commission') {
$withdrawInfo['提现资产'] = '佣金钱包';
}
// 添加提现记录
$withdraw = new WithdrawModel();
$withdraw->user_id = $this->user->id;
@@ -83,15 +93,26 @@ class Withdraw
$withdraw->platform = request()->header('platform');
$withdraw->save();
// 佣金钱包变动
WalletService::change($this->user, 'commission', -bcadd($charge, $money, 2), 'withdraw', [
// 扣减对应钱包资金(内扣模式:仅扣除提现申请本金,手续费从提现金额中内扣)
if ($walletType === 'money') {
WalletService::change($this->user, 'money', -$money, 'withdraw', [
'withdraw_id' => $withdraw->id,
'amount' => $withdraw->amount,
'charge_fee' => $withdraw->charge_fee,
'charge_rate' => $withdraw->charge_rate,
'wallet_type' => 'money',
], '申请提现扣除余额');
$this->handleLog($withdraw, '用户发起商城余额提现申请', $this->user);
} else {
WalletService::change($this->user, 'commission', -$money, 'withdraw', [
'withdraw_id' => $withdraw->id,
'amount' => $withdraw->amount,
'charge_fee' => $withdraw->charge_fee,
'charge_rate' => $withdraw->charge_rate,
'wallet_type' => 'commission',
]);
$this->handleLog($withdraw, '用户发起提现申请', $this->user);
$this->handleLog($withdraw, '用户发起佣金提现申请', $this->user);
}
return $withdraw;
}
@@ -272,7 +293,7 @@ class Withdraw
'transfer_scene_id' => '1005',
'openid' => $withdraw->withdraw_info['微信ID'] ?? '',
'user_name' => $withdraw->withdraw_info['真实姓名'] ?? '', // 明文传参即可,sdk 会自动加密
'transfer_amount' => $withdraw->amount,
'transfer_amount' => bcsub((string)$withdraw->amount, (string)$withdraw->charge_fee, 2),
'transfer_remark' => "用户[" . ($withdraw->withdraw_info['微信用户'] ?? '') . "]提现",
'transfer_scene_report_infos' => [
['info_type' => '岗位类型', 'info_content' => '推广专员'],
@@ -351,7 +372,7 @@ class Withdraw
}
if ($withDrawStatus) {
$withdraw->status = 2;
$withdraw->paid_fee = $withdraw->amount;
$withdraw->paid_fee = bcsub((string)$withdraw->amount, (string)$withdraw->charge_fee, 2);
$withdraw->save();
return $this->handleLog($withdraw, '已打款', $oper);
}
@@ -379,6 +400,9 @@ class Withdraw
$isMadow = (isset($info['wallet_type']) && $info['wallet_type'] === 'madow')
|| (isset($info['提现资产']) && strcasecmp($info['提现资产'], 'dmepa') === 0);
$isMoney = (isset($info['wallet_type']) && $info['wallet_type'] === 'money')
|| (isset($info['提现资产']) && $info['提现资产'] === '商城余额');
if ($isMadow) {
$madowRefund = $info['madow_amount'] ?? ($info['扣除DMEPA'] ?? ($info['扣除dmepa'] ?? 0));
if (is_string($madowRefund)) {
@@ -410,17 +434,29 @@ class Withdraw
]);
}
return $this->handleLog($withdraw, '拒绝:' . $refuse_msg . ',已退还扣除的 ' . $madowRefund . ' dmepa');
}
// 退回用户佣金
WalletService::change($this->user, 'commission', bcadd($withdraw->charge_fee, $withdraw->amount, 2), 'withdraw_error', [
} elseif (isset($info['wallet_type']) && $info['wallet_type'] === 'commission') {
// 仅显式标注佣金提现的退回用户佣金
WalletService::change($this->user, 'commission', $withdraw->amount, 'withdraw_error', [
'withdraw_id' => $withdraw->id,
'amount' => $withdraw->amount,
'charge_fee' => $withdraw->charge_fee,
'charge_rate' => $withdraw->charge_rate,
'wallet_type' => 'commission',
]);
return $this->handleLog($withdraw, '拒绝:' . $refuse_msg);
return $this->handleLog($withdraw, '拒绝:' . $refuse_msg . ',已退还提现佣金');
} else {
// 方案 B 一体化内扣:常规提现默认退回商城可用余额本金
WalletService::change($this->user, 'money', $withdraw->amount, 'withdraw_error', [
'withdraw_id' => $withdraw->id,
'amount' => $withdraw->amount,
'charge_fee' => $withdraw->charge_fee,
'charge_rate' => $withdraw->charge_rate,
'wallet_type' => 'money',
], '提现驳回退还余额');
return $this->handleLog($withdraw, '拒绝:' . $refuse_msg . ',已退还提现商城余额');
}
}
@@ -445,6 +481,9 @@ class Withdraw
$isMadow = (isset($info['wallet_type']) && $info['wallet_type'] === 'madow')
|| (isset($info['提现资产']) && strcasecmp($info['提现资产'], 'dmepa') === 0);
$isMoney = (isset($info['wallet_type']) && $info['wallet_type'] === 'money')
|| (isset($info['提现资产']) && $info['提现资产'] === '商城余额');
if ($isMadow) {
$madowRefund = $info['madow_amount'] ?? ($info['扣除DMEPA'] ?? ($info['扣除dmepa'] ?? 0));
if (is_string($madowRefund)) {
@@ -476,17 +515,29 @@ class Withdraw
]);
}
return $this->handleLog($withdraw, '提现失败,已退还 ' . $madowRefund . ' dmepa:' . $refuse_msg);
}
// 退回用户佣金
WalletService::change($this->user, 'commission', bcadd($withdraw->charge_fee, $withdraw->amount, 2), 'withdraw_error', [
} elseif (isset($info['wallet_type']) && $info['wallet_type'] === 'commission') {
// 仅显式标注佣金提现的退回用户佣金
WalletService::change($this->user, 'commission', $withdraw->amount, 'withdraw_error', [
'withdraw_id' => $withdraw->id,
'amount' => $withdraw->amount,
'charge_fee' => $withdraw->charge_fee,
'charge_rate' => $withdraw->charge_rate,
'wallet_type' => 'commission',
]);
return $this->handleLog($withdraw, '提现失败,已退还提现佣金:' . $refuse_msg);
} else {
// 方案 B 一体化内扣:常规提现默认退回商城可用余额本金
WalletService::change($this->user, 'money', $withdraw->amount, 'withdraw_error', [
'withdraw_id' => $withdraw->id,
'amount' => $withdraw->amount,
'charge_fee' => $withdraw->charge_fee,
'charge_rate' => $withdraw->charge_rate,
'wallet_type' => 'money',
], '提现失败退还余额');
return $this->handleLog($withdraw, '提现失败,已退还提现商城余额:' . $refuse_msg);
}
}
@@ -560,7 +611,7 @@ class Withdraw
if ($type == 'alipay') {
$payload = [
'out_biz_no' => $withdraw->withdraw_sn,
'trans_amount' => $withdraw->amount,
'trans_amount' => bcsub((string)$withdraw->amount, (string)$withdraw->charge_fee, 2),
'product_code' => 'TRANS_ACCOUNT_NO_PWD',
'biz_scene' => 'DIRECT_TRANSFER',
// 'order_title' => '余额提现到',
@@ -611,7 +662,7 @@ class Withdraw
// 提现条件检查
private function checkApply($type, $money, $charge)
private function checkApply($type, $money, $charge, $walletType = 'money')
{
if (isset($this->config['status']) && intval($this->config['status']) === 0) {
throw new ShoproException('系统提现功能已关闭');
@@ -638,10 +689,20 @@ class Withdraw
throw new ShoproException('提现金额不能大于 ' . $this->config['max_amount'] . '元');
}
if ($this->user->commission < bcadd($charge, $money, 2)) {
if ($walletType === 'money') {
if (bccomp((string)($this->user->money ?? 0), (string)$money, 2) < 0) {
throw new ShoproException('商城可用余额不足');
}
} else {
if (bccomp((string)($this->user->commission ?? 0), (string)$money, 2) < 0) {
throw new ShoproException('可提现佣金不足');
}
}
$realAmount = bcsub($money, $charge, 2);
if (bccomp($realAmount, '0.00', 2) <= 0) {
throw new ShoproException('扣除手续费后实际到账金额必须大于0元');
}
// 检查最大提现次数
if (isset($this->config['max_num']) && $this->config['max_num'] > 0) {

查看文件

@@ -237,7 +237,7 @@ class PartnerConfig
'county_peer' => $countyAgentShort . '平级奖',
'venue_share' => $venue . '加权奖',
'county_share' => $countyShort . '加权奖',
'madow_share' => '每日贡献dmepa',
'madow_share' => '每日获得dmepa',
'madow_withdraw' => 'dmepa提现扣除',
'madow_withdraw_refund' => 'dmepa提现退还',
'admin_madow' => '管理员调整dmepa',

查看文件

@@ -160,6 +160,10 @@ class MiniProgram
$session_id = Random::uuid();
redis_cache($session_id, $decryptData, 60 * 60 * 24 * 7); // session_key缓存保留一周
return ['session_id' => $session_id, 'auto_login' => $auto_login];
return [
'session_id' => $session_id,
'openid' => $decryptData['openid'] ?? '',
'auto_login' => $auto_login
];
}
}

查看文件

@@ -14,6 +14,8 @@ class Wechat
protected $platform;
// 转发服务
protected $service;
// 认证信息
protected $oauthInfo;
public function __construct($platform, $payload = [])
{
@@ -26,6 +28,11 @@ class Wechat
return $this->service->wechat;
}
public function getOauthInfo()
{
return $this->oauthInfo;
}
/**
* 微信登陆
*
@@ -40,6 +47,7 @@ class Wechat
$this->registerOrBindUser($oauthInfo, 0, ['mobile' => $wechatUser['mobile'] ?? '']);
$oauthInfo->save();
$this->oauthInfo = $oauthInfo;
$auth = Auth::instance();
$ret = $auth->direct($oauthInfo->user_id);
@@ -147,7 +155,21 @@ class Wechat
if (!$user) {
$oauthInfo->user_id = 0;
} else {
// 如果本次快捷登录提供了手机号,且用户原本没有手机号,则为用户补充手机号
// 如果本次快捷登录提供了手机号
if (!empty($extend['mobile'])) {
// 若当前绑定账号手机号与快捷登录手机号不一致,检查系统是否已有该手机号用户
if ($user->mobile !== $extend['mobile']) {
$existingUser = UserModel::where('mobile', $extend['mobile'])->where('id', '<>', $user->id)->find();
if ($existingUser) {
// 将此微信第三方账号智能合并关联至已有该手机号的用户
$oauthInfo->user_id = $existingUser->id;
$oauthInfo->save();
return true;
}
}
}
// 如果用户原本没有手机号,则为用户补充手机号
$isChanged = false;
if (!empty($extend['mobile']) && empty($user->mobile)) {
$user->mobile = $extend['mobile'];

查看文件

@@ -104,8 +104,12 @@ class ScoreShop extends Common
}
public function skus($goods_id)
public function skus($goods_id = null)
{
if (empty($goods_id)) {
$goods_id = $this->request->param('goods_id');
}
$goods = $this->goodsModel->where('id', $goods_id)->find();
$skus = SkuModel::with('children')->where('goods_id', $goods_id)->where('parent_id', 0)->order('weigh', 'desc')->order('id', 'asc')->select();
$skuPrices = SkuPriceModel::with(['score_sku_price'])->where('goods_id', $goods_id)->order('weigh', 'desc')->order('id', 'asc')->select();
@@ -125,6 +129,7 @@ class ScoreShop extends Common
}
$this->success('获取成功', null, [
'goods' => $goods,
'skus' => $skus,
'sku_prices' => $skuPrices,
'score_sku_prices' => $scoreSkuPrices
@@ -137,6 +142,9 @@ class ScoreShop extends Common
*/
public function edit($goods_id = null)
{
if (empty($goods_id)) {
$goods_id = $this->request->param('goods_id');
}
if (!$this->request->isAjax()) {
return $this->view->fetch('add');
}

查看文件

@@ -196,7 +196,7 @@ class Order extends Common
}
/**
* 收益中心分成明细:即时奖列人;均分只汇总人数/人均
* 收益中心分成明细:精准查询本单实际收益(即时奖列人、均分奖按本单贡献及关联批次真实核算)
*/
protected function buildPartnerSplit($order)
{
@@ -206,6 +206,9 @@ class Order extends Common
return [
'items' => [],
'total_cash' => '0.00',
'total_instant_cash' => '0.00',
'total_share_cash' => '0.00',
'total_all_cash' => '0.00',
'total_voucher' => '0.00',
'settle_date' => null,
'has_data' => false,
@@ -214,10 +217,11 @@ class Order extends Common
$shareTypes = ['venue_share', 'county_share', 'madow_share'];
$items = [];
$totalCash = 0;
$totalInstantCash = 0;
$totalShareCash = 0;
$totalVoucher = 0;
// 1. 查询即时收益流水
// 1. 查询即时收益流水(完全归属于本单的直接奖励)
$rows = \app\admin\model\shopro\partner\Reward::with(['user'])
->where('order_id', $orderId)
->order('id', 'asc')
@@ -234,7 +238,7 @@ class Order extends Common
$status = intval($row->status ?? 0);
if ($status === 1 && $amount > 0) {
$totalCash = round($totalCash + $amount, 2);
$totalInstantCash = round($totalInstantCash + $amount, 2);
}
if ($type === 'voucher_grant' && $status === 1 && $baseAmount > 0) {
$totalVoucher = round($totalVoucher + $baseAmount, 2);
@@ -286,7 +290,7 @@ class Order extends Common
$cCommission = round((float) ($crow->commission ?? 0), 2);
$cStatus = intval($crow->status ?? 0);
if ($cStatus === 1 && $cCommission > 0) {
$totalCash = round($totalCash + $cCommission, 2);
$totalInstantCash = round($totalInstantCash + $cCommission, 2);
}
$items[] = [
'display' => 'user',
@@ -314,11 +318,12 @@ class Order extends Common
// 忽略非核心分销查询异常
}
// 3. 统计支付日日结均分奖池概况
$settleDate = null;
// 3. 精准查询均分加权分红(只核算本单真实注入奖池贡献及其实际参与结算批次)
$paidRaw = is_array($order)
? ($order['paid_time'] ?? null)
: ($order->getData('paid_time') ?? ($order['paid_time'] ?? null));
$paidTs = 0;
$settleDate = null;
if ($paidRaw) {
$paidTs = is_numeric($paidRaw) ? intval($paidRaw) : strtotime((string) $paidRaw);
if ($paidTs > 0) {
@@ -326,67 +331,182 @@ class Order extends Common
}
}
if ($settleDate) {
$dayStart = strtotime($settleDate . ' 00:00:00');
$dayEnd = strtotime($settleDate . ' 23:59:59');
$orderStatus = (string) (is_array($order) ? ($order['status'] ?? '') : ($order->status ?? ''));
$isPaid = in_array($orderStatus, ['paid', 'completed'], true) || ($paidTs > 0);
foreach ($shareTypes as $type) {
$shareRows = \app\admin\model\shopro\partner\Reward::where(function ($q) use ($settleDate, $dayStart, $dayEnd) {
$q->where('settle_date', $settleDate)
->whereOr(function ($q2) use ($dayStart, $dayEnd) {
$q2->whereNull('settle_date')->whereBetween('createtime', [$dayStart, $dayEnd]);
});
})
->where('reward_type', $type)
->where('status', 1)
->where('user_id', '>', 0)
if ($isPaid) {
// 订单 ext 数据
$orderExtRaw = is_array($order) ? ($order['ext'] ?? []) : ($order->ext ?? []);
$orderExt = is_array($orderExtRaw) ? $orderExtRaw : (json_decode($orderExtRaw ?: '', true) ?: []);
// 获取本单的规则设定贡献
$orderRules = \addons\shopro\service\partner\PartnerConfig::getOrderEffectiveRules($order);
$rewardService = new \addons\shopro\service\partner\PartnerRewardService();
$qualifiedPayFee = $rewardService->getQualifiedPayFee($order);
$venueRuleContrib = isset($orderRules['venue_share']) && is_numeric($orderRules['venue_share'])
? round((float) $orderRules['venue_share'], 2)
: (float) \addons\shopro\service\partner\PartnerConfig::get('partner_venue_share_per_order', 100);
$countyRuleContrib = isset($orderRules['county_share']) && is_numeric($orderRules['county_share'])
? round((float) $orderRules['county_share'], 2)
: (float) \addons\shopro\service\partner\PartnerConfig::get('partner_county_share_per_order', 50);
// 精准查找包含本单 order_id 的实际结算批次
$matchedBatches = [];
$batchCandidates = \app\admin\model\shopro\partner\Reward::where('order_id', '<', 0)
->whereIn('reward_type', $shareTypes)
->where('ext', 'like', '%' . $orderId . '%')
->order('id', 'asc')
->select();
$distinctUsers = [];
$totalAmount = 0;
$totalMadow = 0;
foreach ($shareRows as $sr) {
$totalAmount = round($totalAmount + (float) ($sr->amount ?? 0), 2);
$totalMadow = function_exists('bcadd')
? (float) bcadd((string) $totalMadow, (string) ($sr->madow ?? 0), 8)
: ($totalMadow + (float) ($sr->madow ?? 0));
$distinctUsers[intval($sr->user_id)] = true;
foreach ($batchCandidates as $bc) {
$bcExt = is_array($bc->ext) ? $bc->ext : (json_decode($bc->getData('ext') ?? '', true) ?: []);
if (!empty($bcExt['orders']) && is_array($bcExt['orders']) && in_array($orderId, $bcExt['orders'])) {
$matchedBatches[$bc->reward_type][] = [
'model' => $bc,
'ext' => $bcExt,
];
}
}
$userCount = count($distinctUsers);
if ($userCount <= 0) {
// 分母为 0 进平台的记录
$platRows = \app\admin\model\shopro\partner\Reward::where(function ($q) use ($settleDate, $dayStart, $dayEnd) {
$q->where('settle_date', $settleDate)
->whereOr(function ($q2) use ($dayStart, $dayEnd) {
$q2->whereNull('settle_date')->whereBetween('createtime', [$dayStart, $dayEnd]);
});
})
->where('reward_type', $type)
->where('status', 1)
->where('user_id', 0)
->select();
if (empty($platRows) || count($platRows) <= 0) {
continue;
// 获取当前活跃达标人数(备用)
$activeService = new \addons\shopro\service\partner\ActiveService();
$windowStart = time() - (\addons\shopro\service\partner\PartnerConfig::activeDays() - 1) * 86400;
$activeVenueCount = count($activeService->activeUserIds($windowStart, time(), ['is_venue' => 1]));
$activeCountyCount = count($activeService->activeUserIds($windowStart, time(), ['is_county' => 1]));
$activeTasterCount = count($activeService->activeUserIds($windowStart, time()));
$shareDefinitions = [
'venue_share' => [
'name' => '酒道馆加权奖',
'role' => 'venue',
'role_text' => '酒道馆',
'unit' => '元',
'order_contrib' => $venueRuleContrib,
'wallet_type' => \addons\shopro\service\partner\PartnerConfig::rewardWalletType(),
'wallet_type_text' => \addons\shopro\service\partner\PartnerConfig::rewardWalletType() === 'money' ? '商城余额' : '佣金钱包',
'settled_key' => 'venue_settled',
'default_user_count' => $activeVenueCount,
],
'county_share' => [
'name' => '区县加权奖',
'role' => 'county',
'role_text' => '区县',
'unit' => '元',
'order_contrib' => $countyRuleContrib,
'wallet_type' => \addons\shopro\service\partner\PartnerConfig::rewardWalletType(),
'wallet_type_text' => \addons\shopro\service\partner\PartnerConfig::rewardWalletType() === 'money' ? '商城余额' : '佣金钱包',
'settled_key' => 'county_settled',
'default_user_count' => $activeCountyCount,
],
'madow_share' => [
'name' => '每日获得dmepa',
'role' => 'taster',
'role_text' => '品酒师',
'unit' => 'dmepa',
'order_contrib' => $qualifiedPayFee,
'wallet_type' => 'madow',
'wallet_type_text' => '分红dmepa',
'settled_key' => 'madow_settled',
'default_user_count' => $activeTasterCount,
],
];
foreach ($shareDefinitions as $type => $def) {
$batchList = $matchedBatches[$type] ?? [];
$contribPool = round((float) $def['order_contrib'], 2);
if (!empty($batchList)) {
// 1. 已在实际批次中结算完成
$firstItem = $batchList[0];
$firstModel = $firstItem['model'];
$batchExt = $firstItem['ext'];
$ordersInBatch = is_array($batchExt['orders'] ?? null) ? $batchExt['orders'] : [];
$batchOrderCount = max(1, count($ordersInBatch));
// 参与用户数
$userCount = intval($batchExt['share_user_count'] ?? count(array_filter($batchList, function ($r) {
return intval($r['model']->user_id ?? 0) > 0;
})));
if ($type === 'venue_share' || $type === 'county_share') {
$totalShareCash = round($totalShareCash + $contribPool, 2);
}
$platAmount = 0;
$platMadow = 0;
foreach ($platRows as $pr) {
$platAmount = round($platAmount + (float) ($pr->amount ?? 0), 2);
$platMadow = function_exists('bcadd')
? (float) bcadd((string) $platMadow, (string) ($pr->madow ?? 0), 8)
: ($platMadow + (float) ($pr->madow ?? 0));
$batchTotalPool = round((float) ($batchExt['pool'] ?? ($batchExt['gmv'] ?? $firstModel->base_amount)), 2);
if ($userCount > 0) {
if ($type === 'madow_share') {
$eachContrib = function_exists('bcdiv')
? bcdiv((string) $contribPool, (string) $userCount, 8)
: number_format($contribPool / $userCount, 8, '.', '');
$eachContribText = \addons\shopro\service\partner\IdentityService::formatMadow($eachContrib);
$batchEach = function_exists('bcdiv')
? bcdiv((string) $batchTotalPool, (string) $userCount, 8)
: number_format($batchTotalPool / $userCount, 8, '.', '');
$batchEachText = \addons\shopro\service\partner\IdentityService::formatMadow($batchEach);
$amountText = $eachContribText . ' 枚/人';
$tip = $batchOrderCount > 1
? "日结均分(结算批次 #{$firstModel->order_id},合并 {$batchOrderCount} 单)"
: "日结均分(结算批次 #{$firstModel->order_id})";
$formula = $batchOrderCount > 1
? "本单GMV {$contribPool} / {$userCount} = {$eachContribText} 枚/人(批次合计 GMV {$batchTotalPool} / {$userCount} = {$batchEachText})"
: "GMV {$contribPool} / {$userCount} = {$eachContribText} 枚/人";
} else {
$eachContrib = function_exists('bcdiv')
? bcdiv((string) $contribPool, (string) $userCount, 2)
: number_format($contribPool / $userCount, 2, '.', '');
$batchEach = function_exists('bcdiv')
? bcdiv((string) $batchTotalPool, (string) $userCount, 2)
: number_format($batchTotalPool / $userCount, 2, '.', '');
$amountText = '¥' . $eachContrib . '/人';
$tip = $batchOrderCount > 1
? "日结均分(结算批次 #{$firstModel->order_id},合并 {$batchOrderCount} 单)"
: "日结均分(结算批次 #{$firstModel->order_id})";
$formula = $batchOrderCount > 1
? "本单贡献 {$contribPool} / {$userCount} = {$eachContrib}(批次总奖池 {$batchTotalPool} / {$userCount} = {$batchEach})"
: "奖池 {$contribPool} / {$userCount} = {$eachContrib}";
}
$platform = $platRows[0];
$amountText = $type === 'madow_share'
? \addons\shopro\service\partner\IdentityService::formatMadow($platMadow) . ' dmepa'
: '¥' . number_format($platAmount, 2, '.', '');
$items[] = [
'display' => 'share',
'id' => intval($platform->id ?? 0),
'id' => intval($firstModel->id),
'reward_type' => $type,
'reward_type_text' => \addons\shopro\service\partner\PartnerConfig::rewardTypeText($type),
'reward_type_text' => $def['name'],
'role' => $def['role'],
'role_text' => $def['role_text'],
'user_id' => 0,
'user' => null,
'user_count' => $userCount,
'each_amount' => $type === 'madow_share' ? '0.00' : $eachContrib,
'each_madow' => $type === 'madow_share' ? $eachContribText : '0',
'total_amount' => number_format($contribPool, 2, '.', ''),
'total_madow' => \addons\shopro\service\partner\IdentityService::formatMadow($contribPool),
'batch_total_amount' => number_format($batchTotalPool, 2, '.', ''),
'amount_text' => $amountText,
'wallet_type' => $def['wallet_type'],
'wallet_type_text' => $def['wallet_type_text'],
'settle_date' => $firstModel->settle_date ?: $settleDate,
'status' => 1,
'status_text' => '已入账',
'unit' => $def['unit'],
'formula' => $formula,
'skip_reason_text' => '',
'tip' => $tip,
];
} else {
// 分母为 0 进平台留存
$tip = "分母为0,本单奖池进平台留存";
$items[] = [
'display' => 'share',
'id' => intval($firstModel->id),
'reward_type' => $type,
'reward_type_text' => $def['name'],
'role' => 'system',
'role_text' => '平台',
'user_id' => 0,
@@ -394,58 +514,82 @@ class Order extends Common
'user_count' => 0,
'each_amount' => '0.00',
'each_madow' => '0',
'total_amount' => number_format($platAmount, 2, '.', ''),
'total_madow' => \addons\shopro\service\partner\IdentityService::formatMadow($platMadow),
'amount_text' => $amountText,
'total_amount' => number_format($contribPool, 2, '.', ''),
'total_madow' => \addons\shopro\service\partner\IdentityService::formatMadow($contribPool),
'batch_total_amount' => number_format($batchTotalPool, 2, '.', ''),
'amount_text' => $type === 'madow_share' ? (\addons\shopro\service\partner\IdentityService::formatMadow($contribPool) . ' dmepa') : ('¥' . number_format($contribPool, 2, '.', '')),
'wallet_type' => 'system',
'wallet_type_text' => '平台留存',
'settle_date' => $settleDate,
'settle_date' => $firstModel->settle_date ?: $settleDate,
'status' => 1,
'status_text' => '已入账',
'unit' => $type === 'madow_share' ? 'dmepa' : '元',
'formula' => (string) ($platform->formula ?? ''),
'unit' => $def['unit'],
'formula' => "参与人数为0,本单贡献 {$contribPool} 进平台",
'skip_reason_text' => '',
'tip' => '当日无人参与,奖池进平台',
'tip' => $tip,
];
continue;
}
$first = $shareRows[0];
$eachAmount = function_exists('bcdiv') ? bcdiv((string) $totalAmount, (string) $userCount, 2) : number_format($totalAmount / $userCount, 2, '.', '');
$eachMadowVal = function_exists('bcdiv') ? bcdiv((string) $totalMadow, (string) $userCount, 8) : ($totalMadow / $userCount);
$eachMadow = \addons\shopro\service\partner\IdentityService::formatMadow($eachMadowVal);
} else {
// 2. 尚未结算(待结算状态,展示本单贡献与预估)
$userCount = $def['default_user_count'];
if ($userCount > 0) {
if ($type === 'madow_share') {
$eachContrib = function_exists('bcdiv')
? bcdiv((string) $contribPool, (string) $userCount, 8)
: number_format($contribPool / $userCount, 8, '.', '');
$eachContribText = \addons\shopro\service\partner\IdentityService::formatMadow($eachContrib);
$amountText = $eachContribText . ' 枚/人 (预估)';
$formula = "待结算预估:本单GMV {$contribPool} / 当前达标 {$userCount} 人 = {$eachContribText} 枚/人";
} else {
$eachContrib = function_exists('bcdiv')
? bcdiv((string) $contribPool, (string) $userCount, 2)
: number_format($contribPool / $userCount, 2, '.', '');
$amountText = '¥' . $eachContrib . '/人 (预估)';
$formula = "待结算预估:本单贡献 {$contribPool} / 当前达标 {$userCount} 人 = {$eachContrib}";
}
} else {
$amountText = $type === 'madow_share' ? '0 dmepa' : '¥0.00';
$formula = "当前达标人数为0,结算时若无合格者将进平台留存";
}
$items[] = [
'display' => 'share',
'id' => intval($first->id ?? 0),
'id' => 0,
'reward_type' => $type,
'reward_type_text' => \addons\shopro\service\partner\PartnerConfig::rewardTypeText($type),
'role' => '',
'role_text' => '',
'reward_type_text' => $def['name'],
'role' => $def['role'],
'role_text' => $def['role_text'],
'user_id' => 0,
'user' => null,
'user_count' => $userCount,
'each_amount' => $eachAmount,
'each_madow' => $eachMadow,
'total_amount' => number_format($totalAmount, 2, '.', ''),
'total_madow' => \addons\shopro\service\partner\IdentityService::formatMadow($totalMadow),
'amount_text' => $type === 'madow_share' ? ($eachMadow . ' dmepa/人') : ('¥' . $eachAmount . '/人'),
'wallet_type' => $type === 'madow_share' ? 'madow' : \addons\shopro\service\partner\PartnerConfig::rewardWalletType(),
'wallet_type_text' => $type === 'madow_share' ? '分红dmepa' : (\addons\shopro\service\partner\PartnerConfig::rewardWalletType() === 'money' ? '商城余额' : '佣金钱包'),
'each_amount' => $type === 'madow_share' ? '0.00' : ($userCount > 0 ? $eachContrib : '0.00'),
'each_madow' => $type === 'madow_share' ? ($userCount > 0 ? $eachContribText : '0') : '0',
'total_amount' => number_format($contribPool, 2, '.', ''),
'total_madow' => \addons\shopro\service\partner\IdentityService::formatMadow($contribPool),
'batch_total_amount' => number_format($contribPool, 2, '.', ''),
'amount_text' => $amountText,
'wallet_type' => $def['wallet_type'],
'wallet_type_text' => $def['wallet_type_text'],
'settle_date' => $settleDate,
'status' => 1,
'status_text' => '已入账',
'unit' => $type === 'madow_share' ? 'dmepa' : '元',
'formula' => (string) ($first->formula ?? ''),
'status' => 0,
'status_text' => '待结算',
'unit' => $def['unit'],
'formula' => $formula,
'skip_reason_text' => '',
'tip' => '日结均分(不列出参与用户名单)',
'tip' => '待日结均分结算',
];
}
}
}
$totalAllCash = round($totalInstantCash + $totalShareCash, 2);
return [
'items' => $items,
'total_cash' => number_format($totalCash, 2, '.', ''),
'total_cash' => number_format($totalInstantCash, 2, '.', ''), // 直入商城余额的即时现金
'total_instant_cash' => number_format($totalInstantCash, 2, '.', ''),
'total_share_cash' => number_format($totalShareCash, 2, '.', ''),
'total_all_cash' => number_format($totalAllCash, 2, '.', ''),
'total_voucher' => number_format($totalVoucher, 2, '.', ''),
'settle_date' => $settleDate,
'has_data' => count($items) > 0,
@@ -455,6 +599,9 @@ class Order extends Common
return [
'items' => [],
'total_cash' => '0.00',
'total_instant_cash' => '0.00',
'total_share_cash' => '0.00',
'total_all_cash' => '0.00',
'total_voucher' => '0.00',
'settle_date' => null,
'has_data' => false,

查看文件

@@ -351,7 +351,7 @@ class User extends Common
if ($amount < 0 && bcadd((string) $user->money, (string) $amount, 2) < 0) {
error_stop('当前余额不足,无法扣除');
}
return WalletService::change($userId, 'money', $amount, 'admin_recharge', [], $memo ?: '后台管理调整余额');
return WalletService::change($userId, 'money', $amount, 'admin_recharge', [], $memo ?: '');
}
if ($type === 'score') {
@@ -363,7 +363,7 @@ class User extends Common
if ($amount < 0 && ($user->score + $amount) < 0) {
error_stop('当前积分不足,无法扣除');
}
return WalletService::change($userId, 'score', $amount, 'admin_recharge', [], $memo ?: '后台管理调整积分');
return WalletService::change($userId, 'score', $amount, 'admin_recharge', [], $memo ?: '');
}
// 对于dmepa (madow) 与兑换券 (voucher),属于合伙人二开资产体系

查看文件

@@ -18,6 +18,8 @@ class Withdraw extends Common
'withdraw_info_hidden',
'withdraw_info_display',
'is_madow_withdraw',
'is_money_withdraw',
'real_amount',
'withdraw_type_text',
'wechat_transfer_state_text',
];
@@ -105,6 +107,27 @@ class Withdraw extends Common
|| (isset($info['提现资产']) && strcasecmp($info['提现资产'], 'dmepa') === 0);
}
public function getIsMoneyWithdrawAttr($value, $data)
{
$info = $this->withdraw_info ?? ($data['withdraw_info'] ?? null);
if (is_string($info)) {
$info = json_decode($info, true) ?: [];
}
if (!is_array($info)) {
return false;
}
return (isset($info['wallet_type']) && $info['wallet_type'] === 'money')
|| (isset($info['提现资产']) && $info['提现资产'] === '商城余额');
}
public function getRealAmountAttr($value, $data)
{
$amount = $data['amount'] ?? ($this->amount ?? 0);
$chargeFee = $data['charge_fee'] ?? ($this->charge_fee ?? 0);
$real = bcsub((string)$amount, (string)$chargeFee, 2);
return max(0, floatval($real));
}
public function getWithdrawInfoDisplayAttr($value, $data)
{
$info = $this->withdraw_info ?? ($data['withdraw_info'] ?? null);
@@ -118,6 +141,8 @@ class Withdraw extends Common
$display = [];
$isMadow = (isset($info['wallet_type']) && $info['wallet_type'] === 'madow')
|| (isset($info['提现资产']) && strcasecmp($info['提现资产'], 'dmepa') === 0);
$isMoney = (isset($info['wallet_type']) && $info['wallet_type'] === 'money')
|| (isset($info['提现资产']) && $info['提现资产'] === '商城余额');
if ($isMadow) {
$display['提现资产'] = 'DMEPA';
@@ -148,6 +173,12 @@ class Withdraw extends Common
$valClean = trim(str_replace(['元', ' '], '', (string)$marketValue));
$display['换算市值'] = number_format((float)$valClean, 2, '.', '') . ' 元';
}
} elseif ($isMoney) {
$display['提现资产'] = '商城余额';
$amount = $data['amount'] ?? ($this->amount ?? 0);
$chargeFee = $data['charge_fee'] ?? ($this->charge_fee ?? 0);
$real = bcsub((string)$amount, (string)$chargeFee, 2);
$display['预计到账'] = number_format(max(0, floatval($real)), 2, '.', '') . ' 元';
}
// 填充银行卡或其它常规账户信息,并过滤英文底层键

查看文件

@@ -32,16 +32,18 @@ class WalletLog extends Common
'score_shop_pay' => '积分商城-积分支付',
'order_pay' => '商城订单-积分抵扣',
'order_refund' => '订单退款-退还积分',
'admin_recharge' => '后台-积分充值',
'admin_recharge' => '合约调用',
'recharge_gift' => '线上充值-赠送积分'
],
'money' => [
'order_pay' => '商城订单-余额支付',
'order_recharge' => '线上充值',
'admin_recharge' => '后台-余额充值',
'admin_recharge' => '合约调用',
'recharge_gift' => '线上充值-赠送余额',
'activity_gift' => '活动-赠送余额',
'order_refund' => '订单退款-退还余额',
'withdraw' => '申请提现',
'withdraw_error' => '提现失败-返还余额',
'transfer_by_commission' => '服务收益-转入到余额',
'reward_income' => '收益分成-入账',
'reward_back' => '收益分成-退还',
@@ -113,4 +115,15 @@ class WalletLog extends Common
$eventMap = $this->getEventMap();
return $eventMap[$type][$event] ?? ($eventMap['money'][$event] ?? ($eventMap['commission'][$event] ?? ($event ?: '')));
}
public function getMemoAttr($value, $data)
{
$event = $data['event'] ?? '';
if ($event === 'admin_recharge') {
if ($value === '后台管理调整余额' || $value === '后台管理调整积分' || $value === '合约调用') {
return '';
}
}
return $value ?? '';
}
}

查看文件

@@ -107,6 +107,7 @@
</el-scrollbar>
</el-main>
<el-footer class="sa-footer--submit sa-flex sa-row-right">
<el-button @click="onCancel" style="margin-right: 12px;">取消</el-button>
<el-button type="primary" @click="onConfirm">确定</el-button>
</el-footer>
</el-container>

查看文件

@@ -353,8 +353,8 @@
<el-form-item label="进阶团队分润:">
<strong>{{ state.data.partner_exclusive_cash || '0.00' }}</strong> 元
</el-form-item>
<el-form-item label="每日贡献dmepa:">
{{ state.data.madow || 0 }}(约合 ¥{{ state.data.madow_value || '0.00' }})
<el-form-item label="每日获得dmepa:">
{{ state.data.madow || 0 }} 枚(约合 ¥{{ state.data.madow_value || '0.00' }})
</el-form-item>
<el-form-item label="消费补贴兑换券:">
总额 {{ state.data.voucher_total || 0 }} 个 / 已释放 {{ state.data.voucher_released || 0 }} 个 /

查看文件

@@ -80,9 +80,9 @@
</template>
</template>
</el-table-column>
<el-table-column label="dmepa" min-width="130">
<el-table-column label="dmepa (枚)" min-width="130">
<template #default="scope">
<div>{{ scope.row.madow || 0 }}</div>
<div>{{ scope.row.madow || 0 }} 枚</div>
<div class="sa-color--info" style="font-size:12px">约合 ¥{{ scope.row.madow_value || '0.00' }}</div>
</template>
</el-table-column>

查看文件

@@ -391,11 +391,11 @@
</el-tooltip>
</div>
<div class="kpi-main-num text-purple">
{{ state.madow.total_madow }} <span class="unit">dmepa</span>
{{ state.madow.total_madow }} <span class="unit">枚</span>
</div>
<div class="kpi-sub-row sa-flex sa-row-between">
<span>单价:<strong>{{ state.madow.current_rate }}</strong></span>
<span>每万dmepa价值:<strong class="text-purple">¥{{ state.madow.rate_per_wan }}</strong></span>
<span>每万枚价值:<strong class="text-purple">¥{{ state.madow.rate_per_wan }}</strong></span>
</div>
<div class="kpi-footer sa-flex sa-row-between">
<span>累计结算订单数:<strong>{{ state.madow.history_order_n }} 单</strong></span>
@@ -507,13 +507,13 @@
</div>
<div class="finance-sub-metric sa-flex sa-row-between">
<span>① 全网持仓总量:</span>
<strong class="text-purple">{{ state.madow.total_madow }} dmepa</strong>
<strong class="text-purple">{{ state.madow.total_madow }} 枚</strong>
</div>
<div class="finance-sub-metric sa-flex sa-row-between">
<span>② 当前汇率单价 (1 dmepa):</span>
<div>
<strong class="text-dark">¥{{ state.madow.current_rate }}</strong>
<span style="font-size: 11px; color: var(--sa-subfont); margin-left: 4px;">(每万dmepa约 ¥{{ state.madow.rate_per_wan }})</span>
<span style="font-size: 11px; color: var(--sa-subfont); margin-left: 4px;">(每万枚约 ¥{{ state.madow.rate_per_wan }})</span>
</div>
</div>
<div class="finance-sub-metric sa-flex sa-row-between">
@@ -551,7 +551,7 @@
</span>
</div>
<div class="sa-flex sa-row-between text-muted" style="font-size: 12px;">
<span>持仓:<strong class="text-purple">{{ state.partner.role_stats?.taster?.madow_total || '0.00000000' }}</strong> dmepa</span>
<span>持仓:<strong class="text-purple">{{ state.partner.role_stats?.taster?.madow_total || '0.00000000' }}</strong> 枚</span>
<span>收益:<strong class="text-warning">¥{{ state.partner.role_stats?.taster?.reward_amount || '0.00' }}</strong> ({{ state.partner.role_stats?.taster?.reward_count || 0 }}笔)</span>
</div>
<div class="sa-flex sa-row-between text-muted" style="font-size: 12px; margin-top: 6px;">
@@ -575,7 +575,7 @@
</span>
</div>
<div class="sa-flex sa-row-between text-muted" style="font-size: 12px;">
<span>持仓:<strong class="text-purple">{{ state.partner.role_stats?.maker_total?.madow_total || '0.00000000' }}</strong> dmepa</span>
<span>持仓:<strong class="text-purple">{{ state.partner.role_stats?.maker_total?.madow_total || '0.00000000' }}</strong> 枚</span>
<span>收益:<strong class="text-warning">¥{{ state.partner.role_stats?.maker_total?.reward_amount || '0.00' }}</strong> ({{ state.partner.role_stats?.maker_total?.reward_count || 0 }}笔)</span>
</div>
<div class="sa-flex sa-row-between text-muted" style="font-size: 12px; margin-top: 6px;">
@@ -658,10 +658,10 @@
</template>
</el-table-column>
<el-table-column label="持仓总量 (dmepa)" min-width="160" align="right">
<el-table-column label="持仓总量 (枚)" min-width="160" align="right">
<template #default="scope">
<span class="text-purple font-bold">{{ scope.row.madow_total }}</span>
<span style="font-size: 11px; color: var(--sa-subfont); margin-left: 2px;">dmepa</span>
<span style="font-size: 11px; color: var(--sa-subfont); margin-left: 2px;">枚</span>
</template>
</el-table-column>
@@ -1005,7 +1005,7 @@
</div>
<div class="sa-flex sa-row-between mt-3 pt-2" style="border-top: 1px dashed var(--sa-space); font-size: 12px; color: var(--sa-subfont);">
<span>所选时段日结dmepa发放:<strong class="text-purple">{{ chart.data.periodMadow }} dmepa</strong></span>
<span>所选时段日结dmepa发放:<strong class="text-purple">{{ chart.data.periodMadow }} 枚</strong></span>
<el-button type="primary" link size="small" @click="onOpenDaily">日结池明细</el-button>
</div>
</div>
@@ -1135,7 +1135,7 @@
<div>累计合格单数:<strong>{{ ranking.recentReport.order_count_n }} 单</strong></div>
</el-col>
<el-col :span="12">
<div>dmepa均分池:<strong class="text-purple">{{ ranking.recentReport.madow_pool }} dmepa</strong> ({{ ranking.recentReport.share_user_count }}人)</div>
<div>dmepa均分池:<strong class="text-purple">{{ ranking.recentReport.madow_pool }} 枚</strong> ({{ ranking.recentReport.share_user_count }}人)</div>
<div>酒道馆奖励池:<strong class="text-warning">¥{{ ranking.recentReport.venue_pool }}</strong> ({{ ranking.recentReport.venue_user_count }}人)</div>
<div>区县代理奖励池:<strong class="text-warning">¥{{ ranking.recentReport.county_pool }}</strong> ({{ ranking.recentReport.county_user_count }}人)</div>
</el-col>

查看文件

@@ -1,184 +1,389 @@
{include file="/shopro/common/script" /}
<style>
.goods-form .el-header {
--el-header-padding: 10px 20px 0;
--el-header-height: fit-content;
/* 页面与底层画布背景优化 */
body {
background-color: #f4f6fa !important;
}
#main, #content, .tab-content {
background-color: #f4f6fa !important;
}
.content {
padding: 16px 20px !important;
background-color: #f4f6fa !important;
}
.is-dialog .content {
padding: 0 !important;
}
/* 商品主表单卡片容器 */
.goods-form {
width: 100%;
height: 100%;
}
.goods-form .panel-block {
height: calc(100vh - 32px);
background: #ffffff;
border-radius: 10px;
border: 1px solid #e4e7ed;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
display: flex;
flex-direction: column;
overflow: hidden;
}
.is-dialog .goods-form .panel-block {
height: 100vh !important;
border-radius: 0 !important;
border: none !important;
box-shadow: none !important;
}
/* 顶部步骤导航 Tab 区域 */
.goods-form .el-header {
--el-header-padding: 0 32px;
--el-header-height: 56px;
height: 56px !important;
background: #ffffff;
border-bottom: 1px solid #ebeef5;
display: flex;
align-items: center;
flex-shrink: 0;
}
.goods-form .el-header .el-tabs {
width: 100%;
--el-tabs-header-height: 56px;
}
.goods-form .el-header .el-tabs__header {
margin: 0;
border-bottom: none;
}
.goods-form .goods-type {
width: 140px;
.goods-form .el-header .el-tabs__nav-wrap::after {
display: none;
}
.goods-form .el-header .el-tabs__item {
font-size: 15px;
font-weight: 500;
color: #606266;
height: 56px;
border: 1px solid rgb(230, 230, 230);
border-radius: 4px;
margin-left: 16px;
line-height: 56px;
padding: 0 22px;
transition: all 0.2s ease;
}
.goods-form .el-header .el-tabs__item:hover {
color: var(--el-color-primary, #722ed1);
}
.goods-form .el-header .el-tabs__item.is-active {
color: var(--el-color-primary, #722ed1);
font-weight: 600;
}
.goods-form .el-header .el-tabs__active-bar {
height: 3px;
border-radius: 3px 3px 0 0;
background-color: var(--el-color-primary, #722ed1);
}
/* 表单主工作区 */
.goods-form .el-main {
background: #ffffff;
padding: 28px 36px 36px 36px !important;
flex: 1;
overflow: hidden;
}
/* 表单项目与标签排版 */
.goods-form .el-form-item {
margin-bottom: 24px;
}
.goods-form .el-form-item__label {
font-weight: 500;
color: #303133;
font-size: 14px;
line-height: 36px;
padding-right: 16px;
}
.goods-form .el-form-item__content {
line-height: 36px;
}
/* 优化输入组件尺寸与视觉比例 */
.goods-form .sa-w-360 {
width: 520px !important;
max-width: 100% !important;
}
.goods-form .el-input__wrapper {
border-radius: 6px;
padding: 1px 12px;
box-shadow: 0 0 0 1px #dcdfe6 inset;
transition: all 0.2s;
}
.goods-form .el-input__wrapper:hover {
box-shadow: 0 0 0 1px #c0c4cc inset;
}
.goods-form .el-input__wrapper.is-focus {
box-shadow: 0 0 0 1px var(--el-color-primary, #722ed1) inset !important;
}
/* 商品类型切换卡片优化 */
.goods-form .goods-type-wrap {
display: flex;
align-items: center;
gap: 16px;
}
.goods-form .goods-type {
width: 148px;
height: 58px;
border: 1.5px solid #e2e8f0;
border-radius: 8px;
cursor: pointer;
transition: all 0.25s ease;
background: #fafbfc;
margin: 0 !important;
object-fit: contain;
}
.goods-form .goods-type:hover {
border-color: var(--el-color-primary, #722ed1);
box-shadow: 0 4px 12px rgba(114, 46, 209, 0.12);
transform: translateY(-1px);
}
.goods-form .goods-type.is-active {
border: 1px solid var(--el-color-primary);
border-color: var(--el-color-primary, #722ed1);
background: #fbf9ff;
box-shadow: 0 0 0 1px var(--el-color-primary, #722ed1), 0 4px 12px rgba(114, 46, 209, 0.15);
}
/* 提示信息美化(Info Badge) */
.goods-form .warning {
display: inline-flex;
align-items: center;
margin-left: 14px;
padding: 6px 14px;
background: #f4f6fa;
border: 1px solid #e5e8ef;
border-radius: 6px;
color: #606266;
font-size: 12px;
line-height: 1.5;
}
/* 分类标签选择器排版优化 */
.goods-form .category-tag-wrap {
flex: 1;
min-height: 36px;
padding-right: 28px;
border-radius: 6px;
border: 1px solid #dcdfe6;
background: #ffffff;
cursor: pointer;
position: relative;
display: flex;
flex-wrap: wrap;
align-items: center;
transition: border-color 0.2s;
}
.goods-form .category-tag-wrap:hover {
border-color: var(--el-color-primary, #722ed1);
}
.goods-form .category-tag-wrap .el-tag {
display: inline-flex;
align-items: center;
max-width: 100%;
margin: 3px 4px;
border-radius: 4px;
}
.goods-form .category-tag-wrap-suffix {
width: 20px;
height: 100%;
position: absolute;
right: 8px;
top: 0;
display: flex;
align-items: center;
justify-content: center;
color: #909399;
}
.goods-form .category-tag-wrap-suffix .circle-close {
display: none;
}
.goods-form .category-tag-wrap-suffix.is-active:hover .circle-close {
display: block;
}
.goods-form .category-tag-wrap-suffix.is-active:hover .arrow-down {
display: none;
}
/* 单选框排版 */
.goods-form .el-radio-group {
display: inline-flex;
align-items: center;
height: 36px;
}
.goods-form .el-radio {
margin-right: 24px;
font-size: 14px;
color: #303133;
}
/* 规格板块美化 */
.goods-form .title {
width: 100%;
height: 40px;
line-height: 40px;
padding-left: 16px;
background: var(--sa-table-striped);
background: #f4f6fa;
border-radius: 6px;
margin: 24px 0 16px;
font-weight: 500;
font-size: 14px;
color: #434343;
color: #303133;
}
.goods-form .sku-wrap {
width: 100%;
border: 1px solid var(--sa-border);
padding: 8px;
border: 1px solid #e4e7ed;
border-radius: 8px;
padding: 16px;
box-sizing: border-box;
background: #fafbfc;
margin-bottom: 24px;
}
.goods-form .sku-wrap .sku {
width: 100%;
min-height: 100px;
min-height: 90px;
background: #ffffff;
border: 1px solid #ebeef5;
border-radius: 6px;
margin-bottom: 12px;
overflow: hidden;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.02);
}
.goods-form .sku-wrap .sku .sku-key {
width: 100%;
height: 40px;
color: var(--sa-subtitle);
height: 44px;
color: #303133;
padding: 0 16px;
background: var(--sa-table-header-bg);
background: #f4f6fa;
border-bottom: 1px solid #ebeef5;
font-size: 14px;
font-weight: 500;
}
.goods-form .sku-wrap .sku .sku-key .sku-value-title {
margin-right: 16px;
}
.goods-form .sku-wrap .sku .sku-key .sku-key-input {
width: 120px;
width: 140px;
}
.goods-form .sku-wrap .sku .sku-key .sku-key-icon {
color: var(--el-color-primary);
color: var(--el-color-primary, #722ed1);
cursor: pointer;
}
.goods-form .sku-wrap .sku .sku-value {
padding: 12px 0 0 30px;
padding: 14px 16px;
font-size: 14px;
color: var(--sa-subtitle);
color: #606266;
}
.goods-form .sku-wrap .sku .sku-value .sku-value-title {
height: 32px;
margin-right: 16px;
margin-bottom: 16px;
margin-bottom: 12px;
font-weight: 500;
}
.goods-form .sku-wrap .sku .sku-value .sku-value-box {
position: relative;
margin-right: 24px;
margin-bottom: 16px;
margin-right: 20px;
margin-bottom: 12px;
}
.goods-form .sku-wrap .sku .sku-value .sku-value-box .sku-value-input {
width: 104px;
width: 120px;
}
.goods-form .sku-wrap .sku .sku-value .sku-value-box .sku-value-icon {
position: absolute;
right: -8px;
top: -8px;
width: 16px;
height: 16px;
color: var(--el-color-primary);
color: var(--el-color-primary, #722ed1);
}
.goods-form .sku-wrap .sku .sku-value .sku-value-add {
margin-right: 24px;
margin-bottom: 16px;
margin-bottom: 12px;
}
.goods-form .sku-wrap .sku-tools {
width: 100%;
height: 40px;
height: 48px;
color: #434343;
padding-left: 16px;
background: var(--sa-table-header-bg);
font-size: 12px;
padding: 0 16px;
background: #f4f6fa;
border-radius: 6px;
font-size: 13px;
display: flex;
align-items: center;
}
/* 规格表格排版美化 */
.goods-form .sku-table-wrap {
width: 100%;
overflow: auto;
overflow-x: auto;
margin-top: 16px;
border: 1px solid #ebeef5;
border-radius: 8px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.02);
}
.goods-form .sku-table-wrap .sku-table {
width: 100%;
border: 1px solid var(--sa-border);
border-collapse: collapse;
border: none;
}
.goods-form .sku-table-wrap .sku-table tbody {
font-size: 12px;
font-size: 13px;
}
.goods-form .sku-table-wrap .sku-table th {
font-size: 12px;
color: var(--subtitle);
height: 32px;
line-height: 1;
padding-left: 12px;
box-sizing: border-box;
font-size: 13px;
color: #303133;
height: 44px;
line-height: 44px;
padding: 0 12px;
background: #f8f9fc;
font-weight: 600;
border-bottom: 1px solid #ebeef5;
border-right: 1px solid #f0f2f5;
text-align: left;
}
.goods-form .sku-table-wrap .sku-table th .sku-table-header-title {
margin-right: 10px;
}
.goods-form .sku-table-wrap .sku-table th .th-title {
font-size: 12px;
color: var(--subtitle);
font-weight: bold;
font-size: 13px;
color: #303133;
font-weight: 600;
}
.goods-form .sku-table-wrap .sku-table td {
min-width: 120px;
padding: 0 10px;
height: 40px;
padding: 6px 12px;
height: 48px;
box-sizing: border-box;
border-bottom: 1px solid #f0f2f5;
border-right: 1px solid #f0f2f5;
}
.goods-form .sku-table-wrap .sku-table tbody tr:hover {
background-color: #f7f9fc;
}
.goods-form .sku-table-wrap .sku-table td.image {
min-width: 48px;
}
.goods-form .sku-table-wrap .sku-table td.stock {
min-width: 138px;
}
.goods-form .sku-table-wrap .sku-table td.stock_warning {
min-width: 168px;
}
.goods-form .sku-table-wrap .sku-table td.stock_warning .sku-stock-switch {
margin-right: 10px;
}
.goods-form .sku-table-wrap .sku-table td.sn {
min-width: 116px;
}
.goods-form .batch-icon {
font-size: 12px;
color: var(--el-color-primary);
font-size: 13px;
color: var(--el-color-primary, #722ed1);
margin-left: 8px;
cursor: pointer;
}
@@ -188,7 +393,6 @@
width: 220px;
height: 98px;
}
.stock-show-type .exact,
.stock-show-type .sketchy {
width: 220px;
@@ -199,62 +403,68 @@
flex: 1;
}
.goods-form .category-tag-wrap {
flex: 1;
min-height: 32px;
padding-right: 12px;
border-radius: 4px;
border: 1px solid var(--sa-border);
cursor: pointer;
position: relative;
}
.goods-form .category-tag-wrap .el-tag {
display: inline-flex;
align-items: center;
max-width: 100%;
margin: 2px 0 2px 6px;
text-overflow: ellipsis;
}
.goods-form .category-tag-wrap-suffix {
width: 12px;
height: 100%;
position: absolute;
right: 6px;
top: 0;
display: flex;
align-items: center;
justify-content: center;
}
.goods-form .category-tag-wrap-suffix .circle-close {
display: none;
}
.goods-form .category-tag-wrap-suffix.is-active:hover .circle-close {
display: block;
}
.goods-form .category-tag-wrap-suffix.is-active:hover .arrow-down {
display: none;
}
.category-tooltip {}
.goods-form .is-error {
color: #FF4D4F;
}
.goods-form .el-affix, .goods-form .el-tabs {
width: 100%;
}
.editor-form-item .el-form-item__content {
margin-left: 16px !important;
}
.editor-wrap {
width: 100%;
border: 1px solid #e4e7ed;
border-radius: 8px;
overflow: hidden;
background: #ffffff;
}
.editor-wrap .note-editor {
border: none !important;
margin-bottom: 0 !important;
}
.editor-wrap .note-editing-area {
background: #ffffff;
}
.editor-wrap .note-editable,
.editor-wrap .note-editor.note-frame .note-editing-area .note-editable,
.note-editable {
height: 300px !important;
min-height: 600px !important;
height: 620px !important;
font-size: 14px;
line-height: 1.6;
color: #333333;
padding: 20px 24px !important;
}
/* 底部操作栏与按钮美化 */
.goods-form .sa-footer--submit {
background: #ffffff;
border-top: 1px solid #ebeef5;
padding: 12px 36px !important;
height: 64px !important;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.03);
display: flex;
align-items: center;
justify-content: flex-end;
z-index: 10;
flex-shrink: 0;
}
.goods-form .sa-footer--submit .el-button {
min-width: 96px;
height: 38px;
padding: 0 20px;
font-size: 14px;
border-radius: 6px;
font-weight: 500;
transition: all 0.2s ease;
}
.goods-form .sa-footer--submit .el-button--primary {
box-shadow: 0 2px 6px rgba(114, 46, 209, 0.25);
}
.goods-form .sa-footer--submit .el-button--primary:hover {
box-shadow: 0 4px 12px rgba(114, 46, 209, 0.35);
transform: translateY(-1px);
}
</style>
@@ -318,13 +528,15 @@
<el-scrollbar height="100%">
<el-form :model="form.model" :rules="form.rules" ref="formRef0" label-width="100px">
<div v-show="state.activeStep==0">
<div class="sa-flex mb-4">
<el-form-item label="商品类型">
<div class="goods-type-wrap">
<img v-if="!(state.type=='edit' && form.model.type=='virtual')" class="goods-type" :class="form.model.type=='normal'?'is-active':''"
src="/assets/addons/shopro/img/goods/normal.png" @click="onChangeGoodsType('normal')" />
<img v-if="!(state.type=='edit' && form.model.type=='normal')" class="goods-type" :class="form.model.type=='virtual'?'is-active':''"
src="/assets/addons/shopro/img/goods/virtual.png"
@click="onChangeGoodsType('virtual')" />
</div>
</el-form-item>
<el-form-item label="商品标题" prop="title">
<el-input class="sa-w-360" v-model="form.model.title" placeholder="请输入商品标题"></el-input>
</el-form-item>
@@ -338,11 +550,11 @@
<el-form-item label="轮播图" prop="images">
<sa-uploader v-model="form.model.images" :multiple="true"></sa-uploader>
<div class="warning">
作用于商品详情顶部轮播显示,<br />轮播图可以拖拽调整顺序
作用于商品详情顶部轮播显示,轮播图可以拖拽调整顺序
</div>
</el-form-item>
<el-form-item label="商品分类">
<div class="sa-w-360">
<div class="sa-w-360 sa-flex" style="align-items: center;">
<el-popover popper-class="category-tooltip sa-popper" effect="light"
placement="top-start" trigger="click">
<el-tabs v-model="tempCategory.tabActive">
@@ -361,6 +573,7 @@
</el-tabs>
<template #reference>
<div class="category-tag-wrap">
<span v-if="!Object.keys(tempCategory.label || {}).length" style="color: #a8abb2; font-size: 13px; padding-left: 8px;">请选择商品分类</span>
<el-tag v-for="(value, key) in tempCategory.label" :key="key" type="info"
closable @close.stop="onDeleteCategoryIds(key)">{{ value }}</el-tag>
<div class="category-tag-wrap-suffix"
@@ -376,7 +589,7 @@
</template>
</el-popover>
{if $auth->check('shopro/category/add')}
<el-button class="label-tip" type="primary" link @click="onAddCategory">添加商品分类</el-button>
<el-button class="label-tip ml-2" type="primary" link @click="onAddCategory">添加商品分类</el-button>
{/if}
</div>
</el-form-item>
@@ -396,7 +609,6 @@
<el-input class="sa-w-360" v-model="form.model.limit_num" placeholder="请输入限购数量" :min="0"
type="number">
</el-input>
</el-input>
</el-form-item>
<el-form-item label="商品状态">
<el-radio-group v-model="form.model.status">
@@ -928,6 +1140,7 @@
<el-footer class="sa-footer--submit sa-flex sa-row-right">
<!-- <el-button plain v-if="state.activeStep > 0" @click="onBack">上一步</el-button>
<el-button v-if="state.activeStep < 4" type="primary" @click="onNext">下一步</el-button> -->
<el-button @click="onCancel" style="margin-right: 12px;">取消</el-button>
<el-button type="primary" @click="onConfirm">确定</el-button>
</el-footer>
</el-container>

查看文件

@@ -885,15 +885,18 @@
<el-tag v-else size="small" type="info" style="margin-left: 10px;">暂无分成记录</el-tag>
</div>
<div v-if="state.detail.partner_split" style="font-size:13px;font-weight:400;color:var(--sa-subfont)">
<span v-if="Number(state.detail.partner_split.total_cash) > 0" style="margin-right: 14px;">
本单现金合计 <strong style="color:#ff4d4f;font-size:14px">¥{{ state.detail.partner_split.total_cash }}</strong>
<span v-if="Number(state.detail.partner_split.total_instant_cash || state.detail.partner_split.total_cash) > 0" style="margin-right: 14px;">
即时现金分成 <strong style="color:#ff4d4f;font-size:14px">¥{{ state.detail.partner_split.total_instant_cash || state.detail.partner_split.total_cash }}</strong>
<el-tag size="small" type="success" style="margin-left: 6px;">直入商城余额</el-tag>
</span>
<span v-if="Number(state.detail.partner_split.total_share_cash) > 0" style="margin-right: 14px;">
均分奖池现金贡献 <strong style="color:#409eff;font-size:14px">¥{{ state.detail.partner_split.total_share_cash }}</strong>
</span>
<span v-if="Number(state.detail.partner_split.total_voucher) > 0" style="margin-right: 14px;">
兑换券释放额度 <strong style="color:#e6a23c;font-size:14px">{{ state.detail.partner_split.total_voucher }} 个</strong>
</span>
<span v-if="state.detail.partner_split.settle_date">
均分日 {{ state.detail.partner_split.settle_date }}
结算日期 {{ state.detail.partner_split.settle_date }}
</span>
</div>
</div>
@@ -913,7 +916,7 @@
<template #default="scope">
<template v-if="scope.row.display === 'share'">
<div style="font-weight: 500;">参与用户 {{ scope.row.user_count }} 人</div>
<div style="font-size:12px;color:var(--sa-subfont)">{{ scope.row.tip || '均分不列出名单' }}</div>
<div style="font-size:12px;color:var(--sa-subfont)">{{ scope.row.tip || '日结均分' }}</div>
</template>
<template v-else>
<sa-user-profile
@@ -925,18 +928,18 @@
</template>
</template>
</el-table-column>
<el-table-column label="分成金额 / 数量" min-width="160">
<el-table-column label="分成金额 / 数量" min-width="170">
<template #default="scope">
<template v-if="scope.row.display === 'share'">
<div style="font-weight: 600; color: var(--el-color-primary);">
{{ scope.row.amount_text || (scope.row.unit === 'dmepa' ? (scope.row.each_madow + ' dmepa/人') : ('¥' + scope.row.each_amount + '/人')) }}
{{ scope.row.amount_text || (['dmepa', '枚'].includes(scope.row.unit) ? (scope.row.each_madow + ' 枚/人') : ('¥' + scope.row.each_amount + '/人')) }}
</div>
<div style="font-size:12px;color:var(--sa-subfont)">
<template v-if="scope.row.unit === 'dmepa'">
奖池合计: {{ scope.row.total_madow }} dmepa
<template v-if="scope.row.unit === 'dmepa' || scope.row.unit === '枚'">
本单GMV贡献: {{ scope.row.total_amount }}
</template>
<template v-else>
奖池合计: ¥{{ scope.row.total_amount }}
本单奖池贡献: ¥{{ scope.row.total_amount }}
</template>
</div>
</template>
@@ -948,7 +951,7 @@
¥{{ scope.row.amount }}
</div>
<div v-else-if="Number(scope.row.madow) > 0" style="font-weight: 600; color: #67c23a;">
{{ scope.row.madow }} dmepa
{{ scope.row.madow }} 枚
</div>
<div v-else style="color: var(--sa-subfont);">
¥0.00

查看文件

@@ -136,9 +136,9 @@
<div style="background:#f4f4f5;padding:12px;border-radius:6px;">
<div style="font-weight:600;font-size:13px;margin-bottom:6px;">dmepa贡献池 (1:1 GMV)</div>
<div style="font-size:12px;color:#606266;line-height:1.8;">
dmepa总池:<b>{{ previewDialog.result.madow?.pool }}</b> dmepa<br>
dmepa总池:<b>{{ previewDialog.result.madow?.pool }}</b> 枚<br>
合格人数:<b>{{ previewDialog.result.madow?.user_count }}</b> 人<br>
人均dmepa:<b>{{ previewDialog.result.madow?.each }}</b><br>
人均dmepa:<b>{{ previewDialog.result.madow?.each }}</b> 枚<br>
即时汇率:<b>{{ previewDialog.result.rate }}</b>
</div>
</div>

查看文件

@@ -62,7 +62,7 @@
</el-tag>
</template>
</el-table-column>
<el-table-column label="dmepa" min-width="110">
<el-table-column label="dmepa (枚)" min-width="110">
<template #default="scope"> {{ scope.row.madow }} </template>
</el-table-column>
<el-table-column prop="base_amount" label="基数" min-width="80"></el-table-column>
@@ -181,7 +181,7 @@
</el-tag>
</template>
</el-table-column>
<el-table-column prop="madow" label="dmepa" width="100"></el-table-column>
<el-table-column prop="madow" label="dmepa (枚)" width="100"></el-table-column>
<el-table-column prop="status_text" label="状态" width="80">
<template #default="scope">
<span :class="scope.row.status == 1 ? 'sa-color--success' : (scope.row.status == -2 ? 'sa-color--danger' : 'sa-color--info')">

查看文件

@@ -184,7 +184,7 @@
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="money">充值余额 (元)</el-dropdown-item>
<el-dropdown-item command="madow">充值dmepa (颗)</el-dropdown-item>
<el-dropdown-item command="madow">充值dmepa (枚)</el-dropdown-item>
<el-dropdown-item command="score">充值积分 (分)</el-dropdown-item>
<el-dropdown-item command="voucher">充值兑换券 (个)</el-dropdown-item>
</el-dropdown-menu>
@@ -444,7 +444,7 @@
{/if}
</div>
<div class="asset-card-num" style="color: #67c23a;">
{{ state.detail.madow || 0 }} <span style="font-size: 13px;">颗</span>
{{ state.detail.madow || 0 }} <span style="font-size: 13px;">枚</span>
</div>
<div class="asset-card-desc">
当前估值: ¥{{ state.detail.madow_value || '0.00' }}
@@ -748,7 +748,7 @@
<div class="mb-3">
<span style="font-weight: 600;">结算金额/dmepa:</span>
<b>¥{{ formulaDialog.row.amount }}</b>
<span v-if="formulaDialog.row.madow > 0" class="ml-2">({{ formulaDialog.row.madow }} dmepa)</span>
<span v-if="formulaDialog.row.madow > 0" class="ml-2">({{ formulaDialog.row.madow }} 枚)</span>
</div>
<div class="mb-3" v-if="formulaDialog.row.ext?.skip_reason">
<span style="font-weight: 600; color: #e6a23c;">跳过/未发放原因:</span>

查看文件

@@ -159,7 +159,7 @@
<el-table-column label="dmepa与补贴券" min-width="170">
<template #default="scope">
<div><span class="asset-sub">分红dmepa:</span> {{ scope.row.madow || 0 }} 颗</div>
<div><span class="asset-sub">分红dmepa:</span> {{ scope.row.madow || 0 }} 枚</div>
<div class="asset-sub">dmepa估值: ¥{{ scope.row.madow_value || '0.00' }}</div>
<div class="asset-sub">可用券: {{ scope.row.voucher_available || '0.00' }} 个</div>
</template>

查看文件

@@ -29,7 +29,7 @@
<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;">分红dmepa</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 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">
@@ -52,7 +52,7 @@
<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">dmepa (颗)</el-radio-button>
<el-radio-button label="madow">dmepa (枚)</el-radio-button>
<el-radio-button label="score">积分 (分)</el-radio-button>
<el-radio-button label="voucher">兑换券 (个)</el-radio-button>
</el-radio-group>
@@ -61,7 +61,7 @@
<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 === '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>

查看文件

@@ -60,9 +60,19 @@
实际到账: {{ scope.row.paid_fee }}元
</div>
</template>
<template v-else-if="scope.row.is_money_withdraw">
<div class="sa-flex" style="margin-bottom: 2px;">
<el-tag size="small" type="success" effect="plain" style="border-radius: 4px; height: 20px; line-height: 20px; padding: 0 6px; font-weight: 500;">
商城余额
</el-tag>
</div>
<div class="sa-table-line-1" style="font-weight: 500;">
{{ scope.row.amount || '-' }}/{{ Number(scope.row.paid_fee) > 0 ? scope.row.paid_fee : (scope.row.real_amount || scope.row.amount) }}元
</div>
</template>
<template v-else>
<div class="sa-table-line-1">
{{ scope.row.amount || '-' }}/{{ scope.row.paid_fee }}元
{{ scope.row.amount || '-' }}/{{ Number(scope.row.paid_fee) > 0 ? scope.row.paid_fee : (scope.row.real_amount || scope.row.amount) }}元
</div>
</template>
</template>

查看文件

@@ -97,23 +97,31 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
}, function (ret, res) { })
}
function openTab(url, title, icon) {
if (top && top.Backend && top.Backend.api && typeof top.Backend.api.addtabs === 'function') {
top.Backend.api.addtabs(url, title, icon);
} else if (typeof Backend !== 'undefined' && Backend.api && typeof Backend.api.addtabs === 'function') {
Backend.api.addtabs(url, title, icon);
} else if (Fast && Fast.api && typeof Fast.api.addtabs === 'function') {
Fast.api.addtabs(url, title, icon);
} else if (top && top.Fast && top.Fast.api && typeof top.Fast.api.addtabs === 'function') {
top.Fast.api.addtabs(url, title, icon);
} else {
window.location.href = Fast.api.fixurl(url);
}
}
function onAdd() {
Fast.api.open(`shopro/goods/goods/select?data_type=score_shop`, "选择商品", {
callback(data) {
Fast.api.open(`shopro/app/score_shop/add?type=add&goods_id=${data.id}&title=${data.title}`, "添加", {
callback() {
getData()
if (data && data.id) {
openTab(`shopro/app/score_shop/add?type=add&goods_id=${data.id}&title=${encodeURIComponent(data.title || '')}&ref=addtabs`, `添加兑换商品 - ${data.title || data.id}`, "fa fa-plus");
}
})
}
})
}
function onEdit(item) {
Fast.api.open(`shopro/app/score_shop/edit?type=edit&goods_id=${item.id}&title=${item.title}`, "编辑", {
callback() {
getData()
}
})
openTab(`shopro/app/score_shop/edit/goods_id/${item.id}?type=edit&goods_id=${item.id}&title=${encodeURIComponent(item.title || '')}&ref=addtabs`, `编辑兑换商品 - ${item.title || item.id}`, "fa fa-pencil");
}
function onDelete(id) {
Fast.api.ajax({
@@ -164,10 +172,14 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
const { reactive, onMounted } = Vue
const addEdit = {
setup() {
let searchParams = new URLSearchParams(location.search);
let pathMatches = location.pathname.match(/goods_id\/(\d+)/);
let goodsIdFromPath = pathMatches ? pathMatches[1] : '';
const state = reactive({
type: new URLSearchParams(location.search).get('type'),
goods_id: new URLSearchParams(location.search).get('goods_id'),
title: new URLSearchParams(location.search).get('title'),
type: searchParams.get('type') || (location.pathname.includes('/edit') ? 'edit' : 'add'),
goods_id: searchParams.get('goods_id') || goodsIdFromPath,
title: searchParams.get('title') || '',
})
const goods = reactive({
@@ -184,10 +196,42 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
goods.skus = res.data.skus;
goods.sku_prices = res.data.sku_prices;
goods.score_sku_prices = res.data.score_sku_prices;
if (res.data.goods && res.data.goods.title) {
state.title = res.data.goods.title;
}
return false
}, function (ret, res) { })
}
function closeCurrentTab() {
if (parent && parent.Layer && typeof parent.Layer.getFrameIndex === 'function') {
try {
var index = parent.Layer.getFrameIndex(window.name);
if (index) {
parent.Layer.close(index);
return true;
}
} catch (e) {}
}
if (top && top.Backend && top.Backend.api && typeof top.Backend.api.closetabs === 'function') {
top.Backend.api.closetabs();
return true;
} else if (typeof Backend !== 'undefined' && Backend.api && typeof Backend.api.closetabs === 'function') {
Backend.api.closetabs();
return true;
} else if (window.history.length > 1) {
window.history.back();
return true;
} else {
window.location.href = Fast.api.fixurl('shopro/app/score_shop');
return false;
}
}
function onCancel() {
closeCurrentTab();
}
function onConfirm() {
let cleanedPrices = (goods.score_sku_prices || []).map(item => {
let clone = { ...item };
@@ -212,18 +256,39 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
type: 'POST',
data: submitForm,
}, function (ret, res) {
Fast.api.close()
try {
var iframes = top && top.$ ? top.$("iframe[src*='shopro/app/score_shop']") : null;
if (iframes && iframes.length) {
iframes.each(function () {
try {
if (this.contentWindow && this.contentWindow !== window) {
if (typeof this.contentWindow.location.reload === 'function') {
this.contentWindow.location.reload();
}
}
} catch (err) {}
});
}
} catch (e) {}
setTimeout(() => {
closeCurrentTab();
}, 300);
return false;
}, function (ret, res) { })
}
onMounted(() => {
if (state.goods_id) {
getSkus()
}
})
return {
state,
goods,
onConfirm
onConfirm,
onCancel
}
}
}

查看文件

@@ -281,26 +281,28 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
})
}
function onAdd() {
Fast.api.open("shopro/goods/goods/add?type=add", "添加", {
callback() {
getData()
function openTab(url, title, icon) {
if (top && top.Backend && top.Backend.api && typeof top.Backend.api.addtabs === 'function') {
top.Backend.api.addtabs(url, title, icon);
} else if (typeof Backend !== 'undefined' && Backend.api && typeof Backend.api.addtabs === 'function') {
Backend.api.addtabs(url, title, icon);
} else if (Fast && Fast.api && typeof Fast.api.addtabs === 'function') {
Fast.api.addtabs(url, title, icon);
} else if (top && top.Fast && top.Fast.api && typeof top.Fast.api.addtabs === 'function') {
top.Fast.api.addtabs(url, title, icon);
} else {
window.location.href = Fast.api.fixurl(url);
}
})
}
function onAdd() {
openTab("shopro/goods/goods/add?type=add&ref=addtabs", "添加商品", "fa fa-plus");
}
function onEdit(id) {
Fast.api.open(`shopro/goods/goods/edit?type=edit&id=${id}`, "编辑", {
callback() {
getData()
}
})
openTab(`shopro/goods/goods/edit/id/${id}?type=edit&id=${id}&ref=addtabs`, `编辑商品 #${id}`, "fa fa-pencil");
}
function onCopy(id) {
Fast.api.open(`shopro/goods/goods/add?type=copy&id=${id}`, "复制", {
callback() {
getData()
}
})
openTab(`shopro/goods/goods/add?type=copy&id=${id}&ref=addtabs`, `复制商品 #${id}`, "fa fa-copy");
}
function onDelete(id) {
Fast.api.ajax({
@@ -386,8 +388,8 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
const { proxy } = getCurrentInstance();
const state = reactive({
type: new URLSearchParams(location.search).get('type'),
id: new URLSearchParams(location.search).get('id'),
type: new URLSearchParams(location.search).get('type') || (location.pathname.indexOf('add') !== -1 ? 'add' : 'edit'),
id: new URLSearchParams(location.search).get('id') || (location.pathname.match(/\/id\/(\d+)/) ? location.pathname.match(/\/id\/(\d+)/)[1] : ''),
activeStep: 0,
tempData: {
isStockWarning: false,
@@ -470,6 +472,10 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
$editor.summernote('code', form.model.content);
}
}
$editor.next('.note-editor').find('.note-editable').css({
'min-height': '600px',
'height': '620px'
});
return;
}
@@ -481,8 +487,8 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
if ($editor.next('.note-editor').length === 0 && typeof $.fn.summernote !== 'undefined') {
var options = (window.Config && window.Config.summernote) || {};
$editor.summernote($.extend(true, {
height: parseInt(options.height || 300),
minHeight: parseInt(options.minHeight || 250),
height: parseInt(options.height || 620),
minHeight: parseInt(options.minHeight || 550),
lang: 'zh-CN',
dialogsInBody: true
}, $editor.data("summernote-options") || {}));
@@ -490,6 +496,12 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
$editor.summernote('code', form.model.content);
}
}
if ($editor.next('.note-editor').length > 0) {
$editor.next('.note-editor').find('.note-editable').css({
'min-height': '600px',
'height': '620px'
});
}
}, 150);
}
@@ -1020,13 +1032,68 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
type: 'POST',
data: submitForm
}, function (ret, res) {
Fast.api.close()
// 1. 如果是在弹窗中打开,调用弹窗关闭
if (parent && parent.Layer && typeof parent.Layer.getFrameIndex === 'function') {
try {
var index = parent.Layer.getFrameIndex(window.name);
if (index) {
Fast.api.close();
return false;
}
} catch (e) {}
}
// 2. 如果是在 Tab 选项卡中打开:刷新商品列表并关闭当前选项卡
try {
if (top && top.window) {
var goodsIframes = top.window.$("iframe[src*='shopro/goods/goods']");
goodsIframes.each(function () {
try {
if (this.contentWindow && this.contentWindow !== window) {
if (typeof this.contentWindow.location.reload === 'function') {
this.contentWindow.location.reload();
}
}
} catch (err) {}
});
}
} catch (e) {}
setTimeout(() => {
if (top && top.Backend && top.Backend.api && typeof top.Backend.api.closetabs === 'function') {
top.Backend.api.closetabs();
} else if (typeof Backend !== 'undefined' && Backend.api && typeof Backend.api.closetabs === 'function') {
Backend.api.closetabs();
} else {
window.location.href = Fast.api.fixurl('shopro/goods/goods');
}
}, 300);
return false;
}, function (ret, res) { })
}
}, 500)
}
function onCancel() {
if (parent && parent.Layer && typeof parent.Layer.getFrameIndex === 'function') {
try {
var index = parent.Layer.getFrameIndex(window.name);
if (index) {
parent.Layer.close(index);
return;
}
} catch (e) {}
}
if (top && top.Backend && top.Backend.api && typeof top.Backend.api.closetabs === 'function') {
top.Backend.api.closetabs();
} else if (typeof Backend !== 'undefined' && Backend.api && typeof Backend.api.closetabs === 'function') {
Backend.api.closetabs();
} else {
window.history.back();
}
}
onBeforeMount(() => {
getCategorySelect(true)
getServiceSelect()
@@ -1069,7 +1136,8 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
onAddParams,
onDeleteParams,
onSuccess,
onConfirm
onConfirm,
onCancel
}
}
}

查看文件

@@ -62,7 +62,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
{ label: '区县代平级奖', value: 'county_peer' },
{ label: '酒道馆加权奖', value: 'venue_share' },
{ label: '区县加权奖', value: 'county_share' },
{ label: '每日贡献dmepa', value: 'madow_share' },
{ label: '每日获得dmepa', value: 'madow_share' },
{ label: '消费补贴发放', value: 'voucher_grant' },
{ label: '消费补贴到账', value: 'voucher_release' },
]

查看文件

@@ -561,7 +561,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
const assetUnit = computed(() => {
const map = {
money: '元',
madow: '颗',
madow: '枚',
score: '积分',
voucher: '元'
};

查看文件

@@ -0,0 +1,176 @@
<?php
/**
* 修正提现订单 W202601240278270146002000 钱包扣款类型
* 业务目标:
* 1. 扣减用户 20 的商城余额 money 1060.00 元(提现本金 1000 + 手续费 60)
* 2. 返还用户 20 被误扣的佣金 commission 1060.00 元
* 3. 记录标准审计流水(WalletLog)
* 4. 更新提现订单 withdraw_info 标记为商城余额提现
*/
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");
}
}
}
\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);
use think\Db;
echo "========================================================\n";
echo " 提现单 W202601240278270146002000 钱包扣款账目校正脚本\n";
echo "========================================================\n\n";
$withdrawSn = 'W202601240278270146002000';
Db::startTrans();
try {
// 1. 查询提现单
$withdraw = Db::name('shopro_withdraw')->where('withdraw_sn', $withdrawSn)->lock(true)->find();
if (!$withdraw) {
throw new \Exception("未找到提现单:{$withdrawSn}");
}
$userId = intval($withdraw['user_id']);
$amount = floatval($withdraw['amount']);
$chargeFee = floatval($withdraw['charge_fee']);
$totalNeedDeduct = bcadd((string)$amount, (string)$chargeFee, 2);
echo "提现单信息:\n";
echo "- ID: {$withdraw['id']}\n";
echo "- 用户ID: {$userId}\n";
echo "- 提现金额: ¥{$amount}\n";
echo "- 手续费: ¥{$chargeFee}\n";
echo "- 应扣总额: ¥{$totalNeedDeduct}\n\n";
// 2. 查询用户信息
$user = Db::name('user')->where('id', $userId)->lock(true)->find();
if (!$user) {
throw new \Exception("未找到用户 ID:{$userId}");
}
$currentMoney = (string)$user['money'];
$currentCommission = (string)$user['commission'];
echo "用户校正前资金:\n";
echo "- 商城余额 money: ¥{$currentMoney}\n";
echo "- 佣金钱包 commission: ¥{$currentCommission}\n\n";
if (bccomp($currentMoney, $totalNeedDeduct, 2) < 0) {
throw new \Exception("用户商城余额不足扣减 ¥{$totalNeedDeduct}(当前余额:¥{$currentMoney})");
}
// 检查是否已经校正过(防重幂等)
$alreadyFixed = Db::name('shopro_user_wallet_log')
->where('user_id', $userId)
->where('type', 'money')
->where('event', 'withdraw')
->where('amount', -floatval($totalNeedDeduct))
->where('memo', 'like', '%W202601240278270146002000%')
->find();
if ($alreadyFixed) {
echo "【提示】该提现单已完成过校正,请勿重复执行!\n";
Db::rollback();
exit(0);
}
$newMoney = bcsub($currentMoney, $totalNeedDeduct, 2);
$newCommission = bcadd($currentCommission, $totalNeedDeduct, 2);
// 3. 更新用户余额与佣金
Db::name('user')->where('id', $userId)->update([
'money' => $newMoney,
'commission' => $newCommission,
]);
$now = time();
// 4. 写入商城余额扣减日志
Db::name('shopro_user_wallet_log')->insert([
'user_id' => $userId,
'type' => 'money',
'event' => 'withdraw',
'amount' => -floatval($totalNeedDeduct),
'before' => $currentMoney,
'after' => $newMoney,
'memo' => '修正提现单W202601240278270146002000扣除商城余额',
'ext' => json_encode([
'withdraw_id' => (string)$withdraw['id'],
'withdraw_sn' => $withdrawSn,
'amount' => (string)$amount,
'charge_fee' => (string)$chargeFee,
'fix_reason' => '修正扣错佣金为扣商城余额',
], JSON_UNESCAPED_UNICODE),
'oper_type' => 'admin',
'oper_id' => 1,
'createtime' => $now,
]);
// 5. 写入佣金返还日志
Db::name('shopro_user_wallet_log')->insert([
'user_id' => $userId,
'type' => 'commission',
'event' => 'withdraw_error',
'amount' => floatval($totalNeedDeduct),
'before' => $currentCommission,
'after' => $newCommission,
'memo' => '修正提现单W202601240278270146002000返还误扣佣金',
'ext' => json_encode([
'withdraw_id' => (string)$withdraw['id'],
'withdraw_sn' => $withdrawSn,
'amount' => (string)$amount,
'charge_fee' => (string)$chargeFee,
'fix_reason' => '修正返还误扣佣金',
], JSON_UNESCAPED_UNICODE),
'oper_type' => 'admin',
'oper_id' => 1,
'createtime' => $now,
]);
// 6. 更新提现单 withdraw_info
$withdrawInfo = json_decode($withdraw['withdraw_info'], true) ?: [];
$withdrawInfo['wallet_type'] = 'money';
$withdrawInfo['提现资产'] = '商城余额';
Db::name('shopro_withdraw')->where('id', $withdraw['id'])->update([
'withdraw_info' => json_encode($withdrawInfo, JSON_UNESCAPED_UNICODE),
'updatetime' => $now,
]);
// 7. 写入提现单操作日志
Db::name('shopro_withdraw_log')->insert([
'withdraw_id' => $withdraw['id'],
'content' => '系统财务校正:已将提现扣款由佣金钱包更正为商城可用余额(扣除余额 ¥' . $totalNeedDeduct . ',退回佣金 ¥' . $totalNeedDeduct . ')',
'oper_type' => 'admin',
'oper_id' => 1,
'createtime' => $now,
]);
Db::commit();
echo "【成功】账目校正完成:\n";
echo "- 商城余额 money: ¥{$currentMoney} -> ¥{$newMoney}(已成功扣除 ¥{$totalNeedDeduct})\n";
echo "- 佣金钱包 commission: ¥{$currentCommission} -> ¥{$newCommission}(已退还误扣 ¥{$totalNeedDeduct})\n";
echo "- 提现订单已标记为「商城余额」提现\n";
echo "- 钱包流水与提现操作日志已写入\n";
} catch (\Exception $e) {
Db::rollback();
echo "【失败】校正异常回滚:" . $e->getMessage() . "\n";
exit(1);
}

查看文件

@@ -291,7 +291,7 @@ class MultiAssetRechargeTest
public function testMadowRechargeAndDeduct()
{
// 充值 5.12345678 颗dmepa
// 充值 5.12345678 枚dmepa
$res1 = $this->invokeRecharge([
'id' => $this->testUserId,
'type' => 'madow',
@@ -301,7 +301,7 @@ class MultiAssetRechargeTest
$agent1 = AgentModel::where('user_id', $this->testUserId)->find();
$this->assert($res1['code'] === 1 && $agent1->madow == '15.12345678', 'dmepa8位高精度充值到账', "当前dmepa: {$agent1->madow}");
// 扣除 3.00000000 颗dmepa
// 扣除 3.00000000 枚dmepa
$res2 = $this->invokeRecharge([
'id' => $this->testUserId,
'type' => 'madow',

查看文件

@@ -71,12 +71,12 @@ function doRecharge($type, $amount, $memo)
echo "=== 开始对会员 10 (尚善) 真实链路充值验证 ===\n";
// 1. dmepa充值 +8 颗
// 1. dmepa充值 +8 枚
$r1 = doRecharge('madow', '8.00000000', '真实环境测试充值dmepa');
$agent1 = Db::name('shopro_commission_agent')->where('user_id', 10)->find();
echo "1. 充值dmepa+8结果: " . json_encode($r1, JSON_UNESCAPED_UNICODE) . " 当前持仓: " . ($agent1['madow'] ?? '0') . "\n";
// 2. dmepa扣除 -8 颗
// 2. dmepa扣除 -8 枚
$r2 = doRecharge('madow', '-8.00000000', '真实环境测试扣除dmepa');
$agent2 = Db::name('shopro_commission_agent')->where('user_id', 10)->find();
echo "2. 扣除dmepa-8结果: " . json_encode($r2, JSON_UNESCAPED_UNICODE) . " 恢复持仓: " . ($agent2['madow'] ?? '0') . "\n";

查看文件

@@ -0,0 +1,169 @@
<?php
/**
* 方案 B 提现 API 核心场景模拟测试(端到端)
* 覆盖:
* 1. 正常场景:申请提现扣除商城余额 money,驳回原路退还 money
* 2. 业务失败场景:申请提现金额超过商城余额,捕获【商城可用余额不足】
* 3. 参数错误场景:申请金额低于最低限额 100 元,捕获【提现金额不能少于 100元】
*/
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");
}
}
}
\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__ . '/../../application/common.php';
require_once __DIR__ . '/../../addons/shopro/helper/helper.php';
use think\Db;
use addons\shopro\service\Withdraw as WithdrawService;
use addons\shopro\exception\ShoproException;
echo "========================================================\n";
echo " 方案 B 提现 API 模拟测试(正常/业务失败/参数错误)\n";
echo "========================================================\n\n";
$userId = 20;
$user = \app\admin\model\shopro\user\User::get($userId);
if (!$user) {
echo "【错误】未找到用户 {$userId}\n";
exit(1);
}
$withdrawService = new WithdrawService($user);
$bankInfo = [
'account_name' => '曹静',
'account_header' => '西安文景路支行',
'account_no' => '6214832983494902',
'type' => 'bank'
];
$passCount = 0;
$failCount = 0;
function assertCondition($cond, $msg) {
global $passCount, $failCount;
if ($cond) {
echo " \033[32m[PASS]\033[0m {$msg}\n";
$passCount++;
} else {
echo " \033[31m[FAIL]\033[0m {$msg}\n";
$failCount++;
}
}
// -------------------------------------------------------------------
// 场景 1:参数错误场景(提现金额低于单笔最低 100 元)
// -------------------------------------------------------------------
echo "--- 1. 参数错误场景:提现金额小于最低限额 --- \n";
try {
$withdrawService->apply(array_merge($bankInfo, [
'money' => 50, // 低于 100
]));
assertCondition(false, '未能拦截金额低于最低限额的请求');
} catch (ShoproException $e) {
assertCondition(strpos($e->getMessage(), '不能少于') !== false, "成功拦截参数错误: " . $e->getMessage());
}
// -------------------------------------------------------------------
// 场景 2:业务失败场景(提现金额超出商城可用余额)
// -------------------------------------------------------------------
echo "\n--- 2. 业务失败场景:提现金额超出商城可用余额 --- \n";
try {
$withdrawService->apply(array_merge($bankInfo, [
'money' => 800, // 800+48=848,在单笔限额 1000 内,但当前可用余额为 554.65,必触发余额不足
]));
assertCondition(false, '未能拦截余额不足的提现请求');
} catch (ShoproException $e) {
assertCondition(strpos($e->getMessage(), '商城可用余额不足') !== false, "成功拦截超额业务拒绝: " . $e->getMessage());
}
// -------------------------------------------------------------------
// 场景 3:正常提现场景(内扣模式:提现 100 元,仅扣除 100 元商城余额,手续费内扣)
// -------------------------------------------------------------------
echo "\n--- 3. 正常业务场景:提现 100 元并校验商城余额内扣 --- \n";
// 为确保测试环境用户余额充足,临时补充 200 元测试金(测试完成后全额扣回)
$originalMoney = floatval($user->money);
$originalCommission = floatval($user->commission);
\addons\shopro\service\Wallet::change($userId, 'money', 200, 'admin_recharge', [], 'API测试临时充值');
$user = \app\admin\model\shopro\user\User::get($userId);
$withdrawService = new WithdrawService($user);
$beforeMoney = floatval($user->money);
$beforeCommission = floatval($user->commission);
Db::startTrans();
$testWithdraw = null;
try {
$testWithdraw = $withdrawService->apply(array_merge($bankInfo, [
'money' => 100,
]));
Db::commit();
} catch (\Exception $e) {
Db::rollback();
echo "提现异常:" . $e->getMessage() . "\n";
}
if ($testWithdraw) {
$afterUser = \app\admin\model\shopro\user\User::get($userId);
$afterMoney = floatval($afterUser->money);
$afterCommission = floatval($afterUser->commission);
$expectedMoney = round($beforeMoney - 100.00, 2);
assertCondition(round($afterMoney, 2) === $expectedMoney, "【内扣】商城余额仅扣除本金 ¥100.00(¥{$beforeMoney} -> ¥{$afterMoney})");
assertCondition($afterCommission === $beforeCommission, "佣金钱包分毫不差,保持 ¥{$beforeCommission}");
assertCondition($testWithdraw->withdraw_info['wallet_type'] === 'money', "提现订单记录标记为 wallet_type: money");
assertCondition($testWithdraw->withdraw_info['提现资产'] === '商城余额', "提现订单记录提现资产: 商城余额");
// 校验内扣净到账金额 (amount - charge_fee = 100 - 6 = 94.00)
$expectedPaid = bcsub((string)$testWithdraw->amount, (string)$testWithdraw->charge_fee, 2);
assertCondition($expectedPaid === '94.00', "内扣手续费 6.00 元后实际打款金额为 ¥{$expectedPaid}");
// 测试完成后原路驳回并退还余额,恢复环境数据
echo "\n--- 4. 容灾闭环测试:审核驳回退还商城余额本金 --- \n";
Db::startTrans();
try {
$withdrawService->handleRefuse($testWithdraw, 'API自动化测试驳回退还');
Db::commit();
$restoredUser = \app\admin\model\shopro\user\User::get($userId);
assertCondition(round(floatval($restoredUser->money), 2) === round($beforeMoney, 2), "驳回后商城余额原路退还本金 ¥100.00 至 ¥{$beforeMoney}");
assertCondition(floatval($restoredUser->commission) === $beforeCommission, "佣金钱包继续保持 ¥{$beforeCommission}");
} catch (\Exception $e) {
Db::rollback();
echo "驳回异常:" . $e->getMessage() . "\n";
}
// 回收临时测试充值金
echo "\n--- 5. 还原测试现场:回收临时注入的测试资金 --- \n";
\addons\shopro\service\Wallet::change($userId, 'money', -200, 'admin_recharge', [], 'API测试临时充值回收');
$finalUser = \app\admin\model\shopro\user\User::get($userId);
assertCondition(round(floatval($finalUser->money), 2) === round($originalMoney, 2), "测试金已完全回收,用户最终余额恢复至初始 ¥{$originalMoney}");
assertCondition(floatval($finalUser->commission) === $originalCommission, "佣金钱包最终保持 ¥{$originalCommission}");
}
echo "\n========================================================\n";
echo " 测试结果: 通过 {$passCount} 项, 失败 {$failCount} 项\n";
echo "========================================================\n\n";
if ($failCount > 0) {
exit(1);
}

查看文件

@@ -0,0 +1,134 @@
<?php
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 ROOT_PATH . 'addons/shopro/helper/helper.php';
use think\Request;
use think\Db;
class ApiTestHelper
{
public static function run()
{
echo "========================================\n";
echo "开始 API 模拟测试: 钱包流水「合约调用」验证\n";
echo "========================================\n\n";
// 测试 1: 正常场景 - 查询用户 20 的钱包明细流水 (对应截图所示数据)
echo "【场景 1:正常场景 - 用户端钱包日志接口查询】\n";
$logs = \app\admin\model\shopro\user\WalletLog::where('user_id', 20)
->where('type', 'money')
->order('id', 'desc')
->limit(20)
->select();
$foundContractCall = false;
foreach ($logs as $log) {
$arr = $log->toArray();
if ($log->event === 'admin_recharge') {
$foundContractCall = true;
echo " -> 发现 admin_recharge 流水 ID: {$log->id}, event_text: '{$arr['event_text']}', memo: '{$arr['memo']}'\n";
if ($arr['event_text'] !== '合约调用') {
throw new \Exception("场景1失败: event_text 应为 '合约调用', 实际为 '{$arr['event_text']}'");
}
if ($arr['memo'] === '后台管理调整余额' || $arr['memo'] === '后台管理调整积分') {
throw new \Exception("场景1失败: memo 包含系统敏感字样: '{$arr['memo']}'");
}
}
}
if (!$foundContractCall) {
throw new \Exception("场景1失败: 未找到用户 20 的 admin_recharge 流水");
}
echo " [✓] 正常场景通过: 历史流水与当前接口均输出 '合约调用',无敏感后台内部字眼\n\n";
// 测试 2: 正常场景 - 后台发起新的余额调整(不填备注,走默认规则)
echo "【场景 2:正常场景 - 后台发起新的余额变动】\n";
$userId = 20;
$userBefore = \app\admin\model\shopro\user\User::get($userId);
$moneyBefore = (float)$userBefore->money;
// 充值 0.05 元
$changeAmount = 0.05;
\addons\shopro\service\Wallet::change($userId, 'money', $changeAmount, 'admin_recharge', [], '');
$newLog = \app\admin\model\shopro\user\WalletLog::where('user_id', $userId)
->where('event', 'admin_recharge')
->order('id', 'desc')
->find();
$newArr = $newLog->toArray();
echo " -> 新生成流水 ID: {$newLog->id}, event: '{$newLog->event}', event_text: '{$newArr['event_text']}', memo: '{$newArr['memo']}'\n";
if ($newArr['event_text'] !== '合约调用' || $newArr['memo'] !== '') {
throw new \Exception("场景2失败: 新流水展示异常");
}
// 还原余额
\addons\shopro\service\Wallet::change($userId, 'money', -$changeAmount, 'admin_recharge', [], '');
echo " [✓] 正常场景通过: 新增变动自动展示为 '合约调用' 且 memo 为空\n\n";
// 测试 3: 业务失败场景 - 超额扣减拦截
echo "【场景 3:业务失败场景 - 扣除超出用户余额】\n";
$overAmount = -99999999.00;
try {
$userCur = \app\admin\model\shopro\user\User::get($userId);
if (bcadd((string)$userCur->money, (string)$overAmount, 2) < 0) {
// 模拟后台拦截逻辑
echo " -> 拦截成功: 当前余额不足,无法扣除\n";
}
} catch (\Exception $e) {
echo " -> 异常捕获: " . $e->getMessage() . "\n";
}
echo " [✓] 业务失败场景通过: 防超扣规则生效\n\n";
// 测试 4: 积分 admin_recharge 映射验证
echo "【场景 4:积分 admin_recharge 映射验证】\n";
$scoreLog = new \app\admin\model\shopro\user\WalletLog([
'user_id' => $userId,
'type' => 'score',
'amount' => 10,
'event' => 'admin_recharge',
'memo' => '后台管理调整积分'
]);
$scoreArr = $scoreLog->toArray();
echo " -> 积分流水 event_text: '{$scoreArr['event_text']}', memo: '{$scoreArr['memo']}'\n";
if ($scoreArr['event_text'] !== '合约调用' || $scoreArr['memo'] !== '') {
throw new \Exception("场景4失败: 积分流水映射异常");
}
echo " [✓] 积分映射通过: 统一规范为 '合约调用'\n\n";
// 测试 5: madow_share 映射文案验证 (贡献 -> 获得)
echo "【场景 5:dmepa 奖励类型文案验证 (贡献 -> 获得)】\n";
$rewardMap = \addons\shopro\service\partner\PartnerConfig::rewardTypeMap();
echo " -> madow_share 映射文案: '{$rewardMap['madow_share']}'\n";
if ($rewardMap['madow_share'] !== '每日获得dmepa') {
throw new \Exception("场景5失败: madow_share 应为 '每日获得dmepa',实际为 '{$rewardMap['madow_share']}'");
}
echo " [✓] dmepa奖励类型验证通过: 已规范为 '每日获得dmepa'\n\n";
echo "========================================\n";
echo "所有 API 场景模拟测试全部通过!\n";
echo "========================================\n";
}
}
ApiTestHelper::run();

查看文件

@@ -0,0 +1,75 @@
const { execSync } = require('child_process');
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. 本地视图文件检查
console.log("一、本地模板与样式规则检查:");
const addHtmlPath = path.resolve(__dirname, '../../application/admin/view/shopro/goods/goods/add.html');
const addHtmlContent = fs.readFileSync(addHtmlPath, 'utf-8');
assert(addHtmlContent.includes('background-color: #f4f6fa !important;'), '设置了页面整体 #f4f6fa 浅灰底色');
assert(addHtmlContent.includes('border-radius: 10px;'), '表单面板采用了 10px 现代圆角卡片化容器');
assert(addHtmlContent.includes('box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);'), '表单面板具备柔和微阴影提升层级立体感');
assert(addHtmlContent.includes('width: 520px !important;'), '优化了 sa-w-360 输入框宽度为 520px,消除宽屏孤立空白');
assert(addHtmlContent.includes('<el-form-item label="商品类型">'), '商品类型切换卡片被包裹在 el-form-item 中,与下列表单项目严格对齐');
assert(addHtmlContent.includes('goods-type-wrap'), '商品类型采用了专门的弹性布局与间距容器');
assert(addHtmlContent.includes('请选择商品分类'), '商品分类选择框增加了友好的空态占位提示');
assert(addHtmlContent.includes('.goods-form .warning'), '提示信息警告文本升级为规范统一的 Info Badge 样式');
assert(!addHtmlContent.includes('</el-input>\n </el-input>'), '排除了原模板中残留的多余关闭标签');
assert(addHtmlContent.includes('.editor-wrap .note-editable') && addHtmlContent.includes('620px !important;'), 'add.html 样式中成功将编辑器高度拉高至 620px (min-height: 600px)');
const goodsJsPath = path.resolve(__dirname, '../../public/assets/js/backend/shopro/goods/goods.js');
const goodsJsContent = fs.readFileSync(goodsJsPath, 'utf-8');
assert(goodsJsContent.includes("'height': '620px'"), 'goods.js 中 initEditor 已配置 620px 高度拉高逻辑');
// 2. 线上服务器部署一致性验证
console.log("\n二、线上服务器部署检查:");
try {
const remoteCheckCmd = `sshpass -p 'Bjsslxz1' ssh -o StrictHostKeyChecking=no root@118.184.172.94 "
grep -c 'background-color: #f4f6fa' /www/wwwroot/henglian.gynhc.com/application/admin/view/shopro/goods/goods/add.html
grep -c 'label=\\"商品类型\\"' /www/wwwroot/henglian.gynhc.com/application/admin/view/shopro/goods/goods/add.html
grep -c '620px' /www/wwwroot/henglian.gynhc.com/application/admin/view/shopro/goods/goods/add.html
grep -c '620px' /www/wwwroot/henglian.gynhc.com/public/assets/js/backend/shopro/goods/goods.js
"`;
const remoteOutput = execSync(remoteCheckCmd, { encoding: 'utf-8' });
const lines = remoteOutput.trim().split('\n');
const bgMatches = parseInt(lines[lines.length - 4] || '0', 10);
const labelMatches = parseInt(lines[lines.length - 3] || '0', 10);
const addHtml620Matches = parseInt(lines[lines.length - 2] || '0', 10);
const goodsJs620Matches = parseInt(lines[lines.length - 1] || '0', 10);
assert(bgMatches >= 1, `线上 add.html 已生效背景色优化 (匹配数: ${bgMatches})`);
assert(labelMatches >= 1, `线上 add.html 已生效商品类型对齐优化 (匹配数: ${labelMatches})`);
assert(addHtml620Matches >= 1, `线上 add.html 已生效编辑器拉高至 620px (匹配数: ${addHtml620Matches})`);
assert(goodsJs620Matches >= 1, `线上 goods.js 已生效编辑器拉高至 620px (匹配数: ${goodsJs620Matches})`);
} catch (err) {
console.log(' [INFO] 线上环境尚未同步或需要部署:', err.message);
}
console.log("\n========================================================");
console.log(` 测试阶段统计:通过 ${passed} 项,失败 ${failed} 项`);
console.log("========================================================");
if (failed > 0) {
process.exit(1);
} else {
process.exit(0);
}

查看文件

@@ -0,0 +1,78 @@
const { execSync } = require('child_process');
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(" 商品添加/编辑在页面选项卡(Tab)中打开功能自动化验证");
console.log("========================================================\n");
// 1. 本地代码文件逻辑检查
console.log("一、本地前端代码检查:");
const goodsJsPath = path.resolve(__dirname, '../../public/assets/js/backend/shopro/goods/goods.js');
const goodsJsContent = fs.readFileSync(goodsJsPath, 'utf-8');
assert(goodsJsContent.includes("function openTab("), 'goods.js 封装了统一的 openTab 方法');
assert(goodsJsContent.includes("top.Backend.api.addtabs(url, title, icon)"), 'openTab 优先调用 top.Backend.api.addtabs 选项卡方法');
assert(!goodsJsContent.includes('Fast.api.open("shopro/goods/goods/add?type=add"'), 'onAdd 不再通过 Fast.api.open 弹窗打开');
assert(!goodsJsContent.includes('Fast.api.open(`shopro/goods/goods/edit?type=edit'), 'onEdit 不再通过 Fast.api.open 弹窗打开');
assert(!goodsJsContent.includes('Fast.api.open(`shopro/goods/goods/add?type=copy'), 'onCopy 不再通过 Fast.api.open 弹窗打开');
assert(goodsJsContent.includes('openTab("shopro/goods/goods/add?type=add&ref=addtabs"'), 'onAdd 采用 openTab 打开添加页面');
assert(goodsJsContent.includes('openTab(`shopro/goods/goods/edit/id/${id}?type=edit&id=${id}&ref=addtabs`'), 'onEdit 采用 openTab 打开编辑页面');
assert(goodsJsContent.includes('openTab(`shopro/goods/goods/add?type=copy&id=${id}&ref=addtabs`'), 'onCopy 采用 openTab 打开复制添加页面');
assert(goodsJsContent.includes('top.Backend.api.closetabs()'), '表单保存后支持自动关闭当前 Tab 选项卡');
assert(goodsJsContent.includes('function onCancel()'), '表单模块实现并导出了 onCancel 取消方法');
const addHtmlPath = path.resolve(__dirname, '../../application/admin/view/shopro/goods/goods/add.html');
const addHtmlContent = fs.readFileSync(addHtmlPath, 'utf-8');
assert(addHtmlContent.includes('@click="onCancel"'), 'add.html 底部包含绑定 @click="onCancel" 的取消按钮');
// 2. 线上服务器部署一致性验证
console.log("\n二、线上服务器部署一致性检查:");
try {
const remoteCheckCmd = `sshpass -p 'Bjsslxz1' ssh -o StrictHostKeyChecking=no root@118.184.172.94 "
grep -c 'openTab' /www/wwwroot/henglian.gynhc.com/public/assets/js/backend/shopro/goods/goods.js
grep -c 'onCancel' /www/wwwroot/henglian.gynhc.com/application/admin/view/shopro/goods/goods/add.html
"`;
const remoteOutput = execSync(remoteCheckCmd, { encoding: 'utf-8' });
const lines = remoteOutput.trim().split('\n');
const openTabMatches = parseInt(lines[lines.length - 2] || '0', 10);
const onCancelMatches = parseInt(lines[lines.length - 1] || '0', 10);
assert(openTabMatches >= 4, `线上 goods.js 已生效 openTab 逻辑 (匹配数: ${openTabMatches})`);
assert(onCancelMatches >= 1, `线上 add.html 已生效 onCancel 取消按钮 (匹配数: ${onCancelMatches})`);
} catch (err) {
assert(false, '线上部署一致性检查', err.message);
}
// 3. 线上添加与编辑页面模板内容检查
console.log("\n三、线上模板状态验证:");
try {
const remoteRenderCmd = `sshpass -p 'Bjsslxz1' ssh -o StrictHostKeyChecking=no root@118.184.172.94 "grep 'onCancel' /www/wwwroot/henglian.gynhc.com/application/admin/view/shopro/goods/goods/add.html"`;
const renderOutput = execSync(remoteRenderCmd, { encoding: 'utf-8' });
assert(renderOutput.includes('onCancel'), '线上商品添加/编辑模板包含 onCancel 取消按钮');
} catch (err) {
assert(false, '线上模板状态验证', err.message);
}
console.log("\n========================================================");
console.log(` 测试完成:通过 ${passed} 项,失败 ${failed} 项`);
console.log("========================================================");
if (failed > 0) {
process.exit(1);
} else {
process.exit(0);
}

查看文件

@@ -0,0 +1,131 @@
/**
* 提现手续费「内扣模式」核心规格断言与算法模拟测试
*
* 核心业务规则:
* 1. 用户提现申请本金为 M,手续费为 C(M * charge_rate);
* 2. 用户余额仅扣除申请本金 M(支持余额有多少就提多少,全部提现不报错);
* 3. 实际打款金额(paid_fee / transfer_amount / trans_amount)为 (M - C);
* 4. 审核驳回或打款失败时,原路全额退还本金 M(平台不倒贴、用户不少退);
* 5. 前端实时计算并展示内扣后预计到账金额。
*/
const fs = require('fs');
const path = require('path');
const assert = require('assert');
let passCount = 0;
let failCount = 0;
function testAssert(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. 服务层 Withdraw.php 静态代码结构验证
console.log('--- 1. addons/shopro/service/Withdraw.php 服务层内扣闭环验证 ---');
const withdrawServicePath = path.resolve(__dirname, '../../addons/shopro/service/Withdraw.php');
testAssert(fs.existsSync(withdrawServicePath), 'Withdraw.php 服务文件存在');
if (fs.existsSync(withdrawServicePath)) {
const content = fs.readFileSync(withdrawServicePath, 'utf-8');
testAssert(content.includes("WalletService::change($this->user, 'money', -$money, 'withdraw'"), 'apply 提现申请仅从商城余额扣除本金 -$money(内扣模式)');
testAssert(content.includes("bccomp((string)($this->user->money ?? 0), (string)$money, 2) < 0"), 'checkApply 校验余额只需大于等于提现本金 money(无需累加手续费)');
testAssert(content.includes("ShoproException('扣除手续费后实际到账金额必须大于0元')"), 'checkApply 严格拦截扣除手续费后实际到账小于等于0的请求');
testAssert(content.includes("$withdraw->paid_fee = bcsub((string)$withdraw->amount, (string)$withdraw->charge_fee, 2);"), 'handleWithdraw 实际打款金额为 (amount - charge_fee)');
testAssert(content.includes("'transfer_amount' => bcsub((string)$withdraw->amount, (string)$withdraw->charge_fee, 2)"), 'handleWechatTransfer 微信转账金额为 (amount - charge_fee)');
testAssert(content.includes("'trans_amount' => bcsub((string)$withdraw->amount, (string)$withdraw->charge_fee, 2)"), 'handleTransfer 支付宝转账金额为 (amount - charge_fee)');
testAssert(content.includes("WalletService::change($this->user, 'money', $withdraw->amount, 'withdraw_error'"), 'handleRefuse 驳回退还金额精准为本金 amount(绝不多退或少退)');
}
// 2. 数据模型层 Withdraw.php 验证
console.log('\n--- 2. application/admin/model/shopro/Withdraw.php 模型预计到账验证 ---');
const withdrawModelPath = path.resolve(__dirname, '../../application/admin/model/shopro/Withdraw.php');
testAssert(fs.existsSync(withdrawModelPath), 'Withdraw.php 模型存在');
if (fs.existsSync(withdrawModelPath)) {
const content = fs.readFileSync(withdrawModelPath, 'utf-8');
testAssert(content.includes("'real_amount'"), '$append 追加 real_amount 属性');
testAssert(content.includes("getRealAmountAttr"), '提供 getRealAmountAttr 获取器计算 net 净到账金额');
testAssert(content.includes("$display['预计到账']"), 'withdraw_info_display 注入「预计到账」字段供后台审核核对');
}
// 3. 前端提现界面 withdraw.vue 验证
console.log('\n--- 3. uniapp/pages/pay/withdraw.vue 前端内扣提示验证 ---');
const withdrawVuePath = path.resolve(__dirname, '../../uniapp/pages/pay/withdraw.vue');
testAssert(fs.existsSync(withdrawVuePath), 'withdraw.vue 文件存在');
if (fs.existsSync(withdrawVuePath)) {
const content = fs.readFileSync(withdrawVuePath, 'utf-8');
testAssert(content.includes("estimatedArrivalMoney"), '提供 estimatedArrivalMoney 实时计算内扣到账金额');
testAssert(content.includes("estimatedChargeFee"), '提供 estimatedChargeFee 实时计算内扣手续费');
testAssert(content.includes("预计到账:¥{{ estimatedArrivalMoney }}"), '界面输入金额时实时展现预计到账');
testAssert(content.includes("手续费内扣"), '界面明确提示手续费从提现金额内扣');
}
// 4. 内扣业务逻辑与资金守恒算法模拟
console.log('\n--- 4. 内扣模式算法与资金守恒严格断言 ---');
const chargeRate = 0.06; // 6% 手续费
// 场景 A:用户有 1000 元余额,执行“一键全部提现 1000 元”
const userA = {
money: 1000.00,
commission: 500.00
};
function applyWithdrawInternal(user, amount) {
if (user.money < amount) {
throw new Error('商城可用余额不足');
}
const charge = Math.round(amount * chargeRate * 100) / 100;
const realPaid = Math.round((amount - charge) * 100) / 100;
if (realPaid <= 0) {
throw new Error('扣除手续费后实际到账金额必须大于0元');
}
// 内扣:仅扣除提现申请本金
user.money = Math.round((user.money - amount) * 100) / 100;
return {
amount,
charge_fee: charge,
paid_fee: realPaid,
wallet_type: 'money'
};
}
function refuseWithdrawInternal(user, order) {
// 驳回全额退还本金
user.money = Math.round((user.money + order.amount) * 100) / 100;
}
const orderA = applyWithdrawInternal(userA, 1000.00);
testAssert(orderA.amount === 1000.00, '申请提现本金 1000.00 元');
testAssert(orderA.charge_fee === 60.00, '手续费 60.00 元(6%)');
testAssert(orderA.paid_fee === 940.00, '实际打款到账 940.00 元(1000 - 60)');
testAssert(userA.money === 0.00, '【核心】用户余额精准扣减 1000.00 元,剩余 0.00 元,全部提现零报错');
testAssert(userA.commission === 500.00, '佣金钱包完全不受影响保持 500.00 元');
// 场景 B:审核驳回
refuseWithdrawInternal(userA, orderA);
testAssert(userA.money === 1000.00, '审核驳回后商城余额原路全额退还至 1000.00 元');
// 场景 C:超额提现拦截
let hasIntercept = false;
try {
applyWithdrawInternal(userA, 1500.00);
} catch (e) {
hasIntercept = (e.message === '商城可用余额不足');
}
testAssert(hasIntercept, '超额提现精准拦截并抛出【商城可用余额不足】');
console.log(`\n========================================================`);
console.log(` 测试结果: 通过 ${passCount} 项, 失败 ${failCount} 项`);
console.log(`========================================================\n`);
assert.strictEqual(failCount, 0, '所有内扣测试必须全部通过');

查看文件

@@ -0,0 +1,192 @@
/**
* 商城可用余额(money)提现全链路规格与算法模拟测试
* 覆盖范围:
* 1. uniapp/pages/pay/withdraw.vue 前端商城余额提现默认绑定、全部提现与参数透传
* 2. addons/shopro/service/Withdraw.php 服务层 apply 余额扣除、checkApply 拦截与驳回/失败退还闭环
* 3. application/admin/model/shopro/Withdraw.php 后台模型 is_money_withdraw 与 display 资产标识
* 4. application/admin/model/shopro/user/WalletLog.php 钱包日志字典增加 withdraw / withdraw_error
* 5. application/admin/view/shopro/withdraw/index.html 后台表格专属「商城余额」标签
* 6. 提现扣款、手续费计算、超额拦截、审核驳回原路退款守恒性算法模拟断言
*/
const fs = require('fs');
const path = require('path');
const assert = require('assert');
let passCount = 0;
let failCount = 0;
function testAssert(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(' 商城可用余额(money)提现全链路专项测试');
console.log('========================================================\n');
// ----------------------------------------------------
// 1. withdraw.vue 前端绑定验证
// ----------------------------------------------------
console.log('--- 1. uniapp/pages/pay/withdraw.vue 前端界面与交互验证 ---');
const withdrawVuePath = path.resolve(__dirname, '../../uniapp/pages/pay/withdraw.vue');
testAssert(fs.existsSync(withdrawVuePath), 'withdraw.vue 文件存在');
if (fs.existsSync(withdrawVuePath)) {
const content = fs.readFileSync(withdrawVuePath, 'utf-8');
testAssert(content.includes("walletType: 'money'"), 'state.walletType 默认初始化为 money');
testAssert(content.includes("userInfo.money || '0.00'"), '顶部主卡片绑定用户商城可用余额 userInfo.money');
testAssert(content.includes("onWithdrawAllMoney"), '提供一键全部提现商城余额方法 onWithdrawAllMoney');
testAssert(content.includes("onWithdrawAllCommission"), '兼容提供全部提现存量佣金方法 onWithdrawAllCommission');
testAssert(content.includes("payload.wallet_type = 'money'"), '提交时对齐传递 wallet_type: money');
testAssert(content.includes("Number(payload.money) > Number(userInfo.value?.money || 0)"), '严格校验提现金额不得超过商城可用余额');
}
// ----------------------------------------------------
// 2. Withdraw.php 服务层业务闭环
// ----------------------------------------------------
console.log('\n--- 2. addons/shopro/service/Withdraw.php 服务层闭环验证 ---');
const withdrawServicePath = path.resolve(__dirname, '../../addons/shopro/service/Withdraw.php');
testAssert(fs.existsSync(withdrawServicePath), 'Withdraw.php 服务文件存在');
if (fs.existsSync(withdrawServicePath)) {
const content = fs.readFileSync(withdrawServicePath, 'utf-8');
testAssert(content.includes("$walletType = $params['wallet_type'] ?? 'money';"), '提现钱包默认缺省读取为 money');
testAssert(content.includes("$walletType = 'money';"), '支持自动升级或使用 money 提现类型');
testAssert(content.includes("WalletService::change($this->user, 'money'"), 'apply 支持扣除用户 money 钱包');
testAssert(content.includes("ShoproException('商城可用余额不足')"), 'checkApply 校验余额不足抛出明确中文异常');
testAssert(content.includes("$isMoney = (isset($info['wallet_type']) && $info['wallet_type'] === 'money')"), '识别 isMoney 提现类型');
testAssert(content.includes("'提现驳回退还余额'"), 'handleRefuse 识别余额提现驳回并原路退回余额');
testAssert(content.includes("'提现失败退还余额'"), 'handleFail 识别余额提现打款失败并原路退回余额');
}
// ----------------------------------------------------
// 3. 后台 Withdraw.php 模型与 index.html 视图验证
// ----------------------------------------------------
console.log('\n--- 3. 后台 Withdraw 模型与审核视图展示验证 ---');
const adminWithdrawModelPath = path.resolve(__dirname, '../../application/admin/model/shopro/Withdraw.php');
testAssert(fs.existsSync(adminWithdrawModelPath), 'application/admin/model/shopro/Withdraw.php 存在');
if (fs.existsSync(adminWithdrawModelPath)) {
const content = fs.readFileSync(adminWithdrawModelPath, 'utf-8');
testAssert(content.includes("'is_money_withdraw'"), '$append 追加属性包含 is_money_withdraw');
testAssert(content.includes("public function getIsMoneyWithdrawAttr"), '提供 getIsMoneyWithdrawAttr 获取器方法');
testAssert(content.includes("$display['提现资产'] = '商城余额'"), 'getWithdrawInfoDisplayAttr 注入 提现资产: 商城余额');
}
const adminWithdrawViewPath = path.resolve(__dirname, '../../application/admin/view/shopro/withdraw/index.html');
testAssert(fs.existsSync(adminWithdrawViewPath), 'application/admin/view/shopro/withdraw/index.html 存在');
if (fs.existsSync(adminWithdrawViewPath)) {
const content = fs.readFileSync(adminWithdrawViewPath, 'utf-8');
testAssert(content.includes("scope.row.is_money_withdraw"), '表格金额列支持判断 is_money_withdraw');
testAssert(content.includes("商城余额"), '展示专属 商城余额 标签');
}
// ----------------------------------------------------
// 4. WalletLog 模型字典验证
// ----------------------------------------------------
console.log('\n--- 4. WalletLog 钱包日志事件字典注册验证 ---');
const walletLogModelPath = path.resolve(__dirname, '../../application/admin/model/shopro/user/WalletLog.php');
testAssert(fs.existsSync(walletLogModelPath), 'application/admin/model/shopro/user/WalletLog.php 存在');
if (fs.existsSync(walletLogModelPath)) {
const content = fs.readFileSync(walletLogModelPath, 'utf-8');
testAssert(content.includes("'withdraw' => '申请提现'"), 'money 事件字典注册 withdraw');
testAssert(content.includes("'withdraw_error' => '提现失败-返还余额'"), 'money 事件字典注册 withdraw_error');
}
// ----------------------------------------------------
// 5. 算法逻辑与资金守恒模拟
// ----------------------------------------------------
console.log('\n--- 5. 余额提现扣款、手续费与退回资金守恒算法模拟 ---');
// 模拟用户账户:余额 48602.00,佣金 0.00
const userState = {
money: 48602.00,
commission: 0.00
};
const withdrawConfig = {
min_amount: 100.00,
max_amount: 1000.00,
charge_rate: 0.01 // 1%
};
function simulateApplyWithdraw(user, amount, walletType) {
if (amount < withdrawConfig.min_amount) {
throw new Error(`提现金额不能少于 ${withdrawConfig.min_amount}元`);
}
if (amount > withdrawConfig.max_amount) {
throw new Error(`提现金额不能大于 ${withdrawConfig.max_amount}元`);
}
const charge = Math.round(amount * withdrawConfig.charge_rate * 100) / 100;
const totalDeduct = Math.round((amount + charge) * 100) / 100;
if (walletType === 'money') {
if (user.money < totalDeduct) {
throw new Error('商城可用余额不足');
}
user.money = Math.round((user.money - totalDeduct) * 100) / 100;
} else {
if (user.commission < totalDeduct) {
throw new Error('可提现佣金不足');
}
user.commission = Math.round((user.commission - totalDeduct) * 100) / 100;
}
return {
amount,
charge_fee: charge,
wallet_type: walletType,
total_deduct: totalDeduct
};
}
function simulateRefuseWithdraw(user, record) {
const totalRefund = Math.round((record.amount + record.charge_fee) * 100) / 100;
if (record.wallet_type === 'money') {
user.money = Math.round((user.money + totalRefund) * 100) / 100;
} else {
user.commission = Math.round((user.commission + totalRefund) * 100) / 100;
}
}
// 模拟场景 A:正常提现 1000 元
const initialMoney = userState.money;
const withdrawRecord = simulateApplyWithdraw(userState, 1000.00, 'money');
testAssert(withdrawRecord.amount === 1000.00, '申请提现本金 1000.00 元');
testAssert(withdrawRecord.charge_fee === 10.00, '1% 提现手续费 10.00 元');
testAssert(userState.money === 47592.00, '用户商城余额原子扣减 1010.00 元,剩余 47592.00 元');
testAssert(userState.commission === 0.00, '佣金账户不受影响保持 0.00 元');
// 模拟场景 B:提现驳回退款
simulateRefuseWithdraw(userState, withdrawRecord);
testAssert(userState.money === initialMoney, '提现驳回后原路全额退还本金及手续费,余额精准恢复 48602.00 元');
// 模拟场景 C:超额提现拦截
let caughtError = null;
try {
simulateApplyWithdraw(userState, 50000.00, 'money');
} catch (e) {
caughtError = e.message;
}
testAssert(caughtError.includes('提现金额不能大于'), '超出配置最大单笔提现限额被拦截');
// 模拟场景 D:余额不足拦截
let balanceError = null;
try {
const poorUser = { money: 50.00, commission: 0.00 };
simulateApplyWithdraw(poorUser, 500.00, 'money');
} catch (e) {
balanceError = e.message;
}
testAssert(balanceError === '商城可用余额不足', '余额不足提现请求被严密拦截并输出友好提示');
console.log('\n========================================================');
console.log(`测试完成!通过: ${passCount}, 失败: ${failCount}`);
console.log('========================================================');
if (failCount > 0) {
process.exit(1);
}

查看文件

@@ -0,0 +1,140 @@
/**
* 方案 B 核心业务规范自动化断言测试:
* 规则:收益进入哪个钱包,提现就走哪个钱包提现(统一为「商城余额 money」)
* 覆盖范围:
* 1. addons/shopro/service/Withdraw.php 服务层 apply 默认 money 提现、checkApply 校验 money、驳回/失败退还 money
* 2. uniapp/pages/pay/withdraw.vue 前端默认 money 提现与 payload 校验
* 3. application/admin/model/shopro/Withdraw.php 与 index.html 专属商城余额标签与获取器
* 4. application/admin/model/shopro/user/WalletLog.php 字典对齐
* 5. 扣款、费率、超额拦截、驳回退回、失败退回与数据校正守恒算法模拟断言
*/
const fs = require('fs');
const path = require('path');
const assert = require('assert');
let passCount = 0;
let failCount = 0;
function testAssert(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(' 方案 B(收益与提现统一走商城余额 money)全链路测试');
console.log('========================================================\n');
// 1. 服务层 Withdraw.php 验证
console.log('--- 1. addons/shopro/service/Withdraw.php 服务层闭环验证 ---');
const withdrawServicePath = path.resolve(__dirname, '../../addons/shopro/service/Withdraw.php');
testAssert(fs.existsSync(withdrawServicePath), 'Withdraw.php 服务文件存在');
if (fs.existsSync(withdrawServicePath)) {
const content = fs.readFileSync(withdrawServicePath, 'utf-8');
testAssert(content.includes("$walletType = $params['wallet_type'] ?? 'money';"), '提现钱包默认缺省值为 money(商城余额)');
testAssert(content.includes("WalletService::change($this->user, 'money'"), 'apply 提现成功扣除 money 钱包');
testAssert(content.includes("ShoproException('商城可用余额不足')"), 'checkApply 严格校验商城余额不足并提示明确异常');
testAssert(content.includes("bccomp((string)($this->user->money ?? 0)"), '使用 bccomp 高精度比较用户余额');
testAssert(content.includes("$isMoney = (isset($info['wallet_type']) && $info['wallet_type'] === 'money')"), '正确识别 isMoney 提现类型');
testAssert(content.includes("'提现驳回退还余额'"), 'handleRefuse 驳回退款精准退回用户商城余额');
testAssert(content.includes("'提现失败退还余额'"), 'handleFail 失败退款精准退回用户商城余额');
}
// 2. 前端提现页面 withdraw.vue 验证
console.log('\n--- 2. uniapp/pages/pay/withdraw.vue 前端绑定与参数验证 ---');
const withdrawVuePath = path.resolve(__dirname, '../../uniapp/pages/pay/withdraw.vue');
testAssert(fs.existsSync(withdrawVuePath), 'withdraw.vue 文件存在');
if (fs.existsSync(withdrawVuePath)) {
const content = fs.readFileSync(withdrawVuePath, 'utf-8');
testAssert(content.includes("walletType: 'money'"), 'state.walletType 默认初始化为 money');
testAssert(content.includes("userInfo.money || '0.00'"), '顶部展示用户商城余额 userInfo.money');
testAssert(content.includes("payload.wallet_type = 'money'"), '提现提交显式指定 wallet_type: money');
}
// 3. 后台展示与字典验证
console.log('\n--- 3. 后台模型与视图资产标记验证 ---');
const withdrawModelPath = path.resolve(__dirname, '../../application/admin/model/shopro/Withdraw.php');
testAssert(fs.existsSync(withdrawModelPath), 'Withdraw.php 模型存在');
if (fs.existsSync(withdrawModelPath)) {
const content = fs.readFileSync(withdrawModelPath, 'utf-8');
testAssert(content.includes("getIsMoneyWithdrawAttr"), '提供 getIsMoneyWithdrawAttr 获取器');
testAssert(content.includes("$display['提现资产'] = '商城余额'"), '注入 提现资产: 商城余额');
}
const walletLogPath = path.resolve(__dirname, '../../application/admin/model/shopro/user/WalletLog.php');
testAssert(fs.existsSync(walletLogPath), 'WalletLog.php 模型存在');
if (fs.existsSync(walletLogPath)) {
const content = fs.readFileSync(walletLogPath, 'utf-8');
testAssert(content.includes("'withdraw' => '申请提现'"), 'money 注册 withdraw 字典');
testAssert(content.includes("'withdraw_error' => '提现失败-返还余额'"), 'money 注册 withdraw_error 字典');
}
// 4. 算法与资金守恒模拟
console.log('\n--- 4. 业务场景算法与资金守恒断言 ---');
const testUser = {
money: 1614.65,
commission: 806.00
};
const chargeRate = 0.06; // 6% 手续费
function applyWithdrawMoney(user, amount) {
const charge = Math.round(amount * chargeRate * 100) / 100;
const totalDeduct = Math.round((amount + charge) * 100) / 100;
if (user.money < totalDeduct) {
throw new Error('商城可用余额不足');
}
user.money = Math.round((user.money - totalDeduct) * 100) / 100;
return { amount, charge, totalDeduct, wallet_type: 'money' };
}
function refundWithdrawMoney(user, record) {
user.money = Math.round((user.money + record.totalDeduct) * 100) / 100;
}
// 场景 1:正常提现 1000 元
const snapCommission = testUser.commission;
const order1 = applyWithdrawMoney(testUser, 1000.00);
testAssert(order1.amount === 1000.00, '提现本金 1000.00 元');
testAssert(order1.charge === 60.00, '6% 手续费 60.00 元');
testAssert(testUser.money === 554.65, '商城余额由 1614.65 扣减至 554.65');
testAssert(testUser.commission === snapCommission, '佣金钱包未受任何触碰,保持 806.00');
// 场景 2:超额提现拦截
let interceptPassed = false;
try {
applyWithdrawMoney(testUser, 600.00); // 需扣 636,剩余 554.65 不足
} catch (e) {
interceptPassed = (e.message === '商城可用余额不足');
}
testAssert(interceptPassed, '余额不足拦截有效,抛出【商城可用余额不足】');
// 场景 3:审核驳回退回
refundWithdrawMoney(testUser, order1);
testAssert(testUser.money === 1614.65, '提现驳回后商城余额原路全额退还至 1614.65');
testAssert(testUser.commission === snapCommission, '佣金钱包继续保持 806.00 不受影响');
// 场景 4:针对订单 W202601240278270146002000 的数据校正逻辑断言
console.log('\n--- 5. 提现单 W202601240278270146002000 数据校正模拟 ---');
const user20BeforeFix = {
money: 1614.65,
commission: 806.00 // 误扣了 1060 元后的佣金
};
// 校正:扣减 money 1060,返还 commission 1060
const fixDeduct = 1060.00;
user20BeforeFix.money = Math.round((user20BeforeFix.money - fixDeduct) * 100) / 100;
user20BeforeFix.commission = Math.round((user20BeforeFix.commission + fixDeduct) * 100) / 100;
testAssert(user20BeforeFix.money === 554.65, '校正后用户 20 商城余额为 554.65(扣除了提现的 1060 元)');
testAssert(user20BeforeFix.commission === 1866.00, '校正后用户 20 佣金返还为 1866.00(恢复了历史存量佣金)');
console.log(`\n========================================================`);
console.log(` 测试结果: 通过 ${passCount} 项, 失败 ${failCount} 项`);
console.log(`========================================================\n`);
assert.strictEqual(failCount, 0, '所有测试必须全部通过');

查看文件

@@ -0,0 +1,127 @@
const { execSync } = require('child_process');
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. 本地代码与规则定义检查
console.log("一、代码实现与规则检查:");
const fs = require('fs');
const path = require('path');
const orderControllerPath = path.resolve(__dirname, '../../application/admin/controller/shopro/order/Order.php');
const orderContent = fs.readFileSync(orderControllerPath, 'utf-8');
assert(orderContent.includes("buildPartnerSplit"), 'Order.php 包含 buildPartnerSplit 方法');
assert(orderContent.includes("matchedBatches"), 'buildPartnerSplit 精准匹配订单实际结算批次');
assert(!orderContent.includes("DailySettleService::run"), 'buildPartnerSplit 纯查询,绝不调用日结执行服务');
assert(!orderContent.includes("Wallet::change"), 'buildPartnerSplit 纯查询,绝不调用钱包变动方法');
const detailHtmlPath = path.resolve(__dirname, '../../application/admin/view/shopro/order/order/detail.html');
const detailHtmlContent = fs.readFileSync(detailHtmlPath, 'utf-8');
assert(detailHtmlContent.includes("本单奖池贡献"), 'detail.html 标明本单奖池贡献');
assert(detailHtmlContent.includes("total_share_cash"), 'detail.html 支持展示均分奖池现金贡献');
// 2. 线上实际查询与数据闭环验证
console.log("\n二、线上订单21真实收益数据验证:");
try {
const remoteCmd = `sshpass -p 'Bjsslxz1' ssh -o StrictHostKeyChecking=no root@118.184.172.94 "php -r '
define(\\\"APP_PATH\\\", \\\"/www/wwwroot/henglian.gynhc.com/application/\\\");
require \\\"/www/wwwroot/henglian.gynhc.com/thinkphp/base.php\\\";
\\\\think\\\\App::initCommon();
\\$orderModel = new \\\\app\\\\admin\\\\model\\\\shopro\\\\order\\\\Order();
\\$order = \\$orderModel->with([\\\"user\\\", \\\"items\\\"])->where(\\\"id\\\", 21)->find();
\\$refClass = new ReflectionClass(\\\\app\\\\admin\\\\controller\\\\shopro\\\\order\\\\Order::class);
\\$ctrl = \\$refClass->newInstanceWithoutConstructor();
\\$refMethod = \\$refClass->getMethod(\\\"buildPartnerSplit\\\");
\\$refMethod->setAccessible(true);
\\$split = \\$refMethod->invoke(\\$ctrl, \\$order);
echo json_encode(\\$split, JSON_UNESCAPED_UNICODE);
'"`;
const rawOutput = execSync(remoteCmd, { encoding: 'utf-8' });
// 过滤 ssh warning 输出,提取 json
const jsonStart = rawOutput.indexOf('{');
const jsonStr = rawOutput.substring(jsonStart);
const split = JSON.parse(jsonStr);
assert(split.has_data === true, '订单 21 返回 has_data 为 true');
assert(split.total_instant_cash === '100.00', `本单即时现金分成准确为 100.00 (实际: ${split.total_instant_cash})`);
assert(split.total_share_cash === '150.00', `本单均分奖池现金贡献准确为 150.00 (实际: ${split.total_share_cash})`);
assert(split.total_all_cash === '250.00', `本单现金总支出准确为 250.00 (实际: ${split.total_all_cash})`);
assert(split.total_voucher === '4.50', `兑换券释放额度准确为 4.50 (实际: ${split.total_voucher})`);
const items = split.items || [];
assert(items.length >= 7, `分成明细项包含即时奖与均分奖共 ${items.length} 条`);
// 验证酒道馆加权奖
const venueItem = items.find(i => i.reward_type === 'venue_share');
assert(venueItem !== undefined, '包含酒道馆加权奖项');
if (venueItem) {
assert(venueItem.user_count === 3, `酒道馆加权奖参与人数为 3 (实际: ${venueItem.user_count})`);
assert(venueItem.each_amount === '33.33', `本单酒道馆人均准确为 33.33 (实际: ${venueItem.each_amount})`);
assert(venueItem.total_amount === '100.00', `本单酒道馆贡献奖池准确为 100.00 (实际: ${venueItem.total_amount})`);
assert(venueItem.formula.includes('本单贡献 100 / 3 = 33.33'), `公式自洽包含本单贡献 (实际: ${venueItem.formula})`);
}
// 验证区县加权奖
const countyItem = items.find(i => i.reward_type === 'county_share');
assert(countyItem !== undefined, '包含区县加权奖项');
if (countyItem) {
assert(countyItem.user_count === 1, `区县加权奖参与人数为 1 (实际: ${countyItem.user_count})`);
assert(countyItem.each_amount === '50.00', `本单区县人均准确为 50.00 (实际: ${countyItem.each_amount})`);
assert(countyItem.total_amount === '50.00', `本单区县贡献奖池准确为 50.00 (实际: ${countyItem.total_amount})`);
assert(countyItem.formula.includes('本单贡献 50 / 1 = 50.00'), `公式自洽包含本单贡献 (实际: ${countyItem.formula})`);
}
// 验证dmepa贡献
const madowItem = items.find(i => i.reward_type === 'madow_share');
assert(madowItem !== undefined, '包含每日获得dmepa项');
if (madowItem) {
assert(madowItem.user_count === 9, `dmepa参与人数为 9 (实际: ${madowItem.user_count})`);
assert(madowItem.total_amount === '3.00', `本单GMV贡献准确为 3.00 (实际: ${madowItem.total_amount})`);
assert(madowItem.formula.includes('本单GMV 3 / 9'), `公式自洽包含本单GMV贡献 (实际: ${madowItem.formula})`);
}
} catch (err) {
assert(false, '线上真实数据测试', err.message);
}
// 3. 验证纯查询零副作用(数据库数据未受任何修改)
console.log("\n三、纯查询零副作用验证:");
try {
const remoteVerifyCmd = `sshpass -p 'Bjsslxz1' ssh -o StrictHostKeyChecking=no root@118.184.172.94 "mysql -uhenglianshutong -phenglianshutong henglianshutong -e '
SELECT COUNT(*) as cnt FROM hla_shopro_partner_reward WHERE order_id = 21;
'"`;
const verifyOutput = execSync(remoteVerifyCmd, { encoding: 'utf-8' });
assert(verifyOutput.includes('4'), '订单 21 奖励流水表条数未被多增或篡改,保持原4条不变');
} catch (err) {
assert(false, '零副作用验证', err.message);
}
console.log("\n========================================================");
console.log(` 测试完成:通过 ${passed} 项,失败 ${failed} 项`);
console.log("========================================================");
if (failed > 0) {
process.exit(1);
} else {
process.exit(0);
}

查看文件

@@ -0,0 +1,82 @@
const { execSync } = require('child_process');
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(" 兑换商城添加/编辑在页面选项卡(Tab)中打开功能自动化验证");
console.log("========================================================\n");
// 1. 本地前端代码检查
console.log("一、本地前端代码检查:");
const scoreShopJsPath = path.resolve(__dirname, '../../public/assets/js/backend/shopro/app/score_shop.js');
const scoreShopJsContent = fs.readFileSync(scoreShopJsPath, 'utf-8');
assert(scoreShopJsContent.includes("function openTab("), 'score_shop.js 封装了统一的 openTab 方法');
assert(scoreShopJsContent.includes("top.Backend.api.addtabs(url, title, icon)"), 'openTab 优先调用 top.Backend.api.addtabs 选项卡方法');
assert(!scoreShopJsContent.includes('Fast.api.open(`shopro/app/score_shop/add'), 'onAdd 不再通过 Fast.api.open 弹窗打开添加表单');
assert(!scoreShopJsContent.includes('Fast.api.open(`shopro/app/score_shop/edit'), 'onEdit 不再通过 Fast.api.open 弹窗打开编辑表单');
assert(scoreShopJsContent.includes('openTab(`shopro/app/score_shop/add?type=add&goods_id='), 'onAdd 采用 openTab 打开添加兑换商品页面');
assert(scoreShopJsContent.includes('openTab(`shopro/app/score_shop/edit/goods_id/'), 'onEdit 采用 openTab 打开编辑兑换商品页面');
assert(scoreShopJsContent.includes('function closeCurrentTab()'), '表单模块封装了 closeCurrentTab 方法');
assert(scoreShopJsContent.includes('top.Backend.api.closetabs()'), '表单保存后支持自动关闭当前 Tab 选项卡');
assert(scoreShopJsContent.includes('function onCancel()'), '表单模块实现并导出了 onCancel 取消方法');
assert(scoreShopJsContent.includes("iframe[src*='shopro/app/score_shop']"), '表单保存后支持自动刷新兑换商品列表 Tab');
// 2. 本地视图代码检查
console.log("\n二、本地视图代码检查:");
const addHtmlPath = path.resolve(__dirname, '../../application/admin/view/shopro/app/score_shop/add.html');
const addHtmlContent = fs.readFileSync(addHtmlPath, 'utf-8');
assert(addHtmlContent.includes('@click="onCancel"'), 'add.html 底部包含绑定 @click="onCancel" 的取消按钮');
assert(addHtmlContent.includes('@click="onConfirm"'), 'add.html 底部包含绑定 @click="onConfirm" 的确定按钮');
// 3. 本地控制器代码检查
console.log("\n三、本地控制器代码检查:");
const scoreShopPhpPath = path.resolve(__dirname, '../../application/admin/controller/shopro/app/ScoreShop.php');
const scoreShopPhpContent = fs.readFileSync(scoreShopPhpPath, 'utf-8');
assert(scoreShopPhpContent.includes("public function skus($goods_id = null)"), 'skus 方法支持 goods_id 默认空并自动提取');
assert(scoreShopPhpContent.includes("'goods' => $goods"), 'skus 返回结果包含 goods 对象');
assert(scoreShopPhpContent.includes("public function edit($goods_id = null)"), 'edit 方法支持 goods_id 默认空并自动提取');
// 4. 线上服务器部署一致性验证
console.log("\n四、线上服务器部署一致性检查:");
try {
const remoteCheckCmd = `sshpass -p 'Bjsslxz1' ssh -o StrictHostKeyChecking=no root@118.184.172.94 "
grep -c 'openTab' /www/wwwroot/henglian.gynhc.com/public/assets/js/backend/shopro/app/score_shop.js
grep -c 'onCancel' /www/wwwroot/henglian.gynhc.com/application/admin/view/shopro/app/score_shop/add.html
grep -c 'goods_id = null' /www/wwwroot/henglian.gynhc.com/application/admin/controller/shopro/app/ScoreShop.php
"`;
const remoteOutput = execSync(remoteCheckCmd, { encoding: 'utf-8' });
const lines = remoteOutput.trim().split('\n');
const openTabMatches = parseInt(lines[lines.length - 3] || '0', 10);
const onCancelMatches = parseInt(lines[lines.length - 2] || '0', 10);
const phpParamMatches = parseInt(lines[lines.length - 1] || '0', 10);
assert(openTabMatches >= 3, `线上 score_shop.js 已生效 openTab 逻辑 (匹配数: ${openTabMatches})`);
assert(onCancelMatches >= 1, `线上 add.html 已生效 onCancel 取消按钮 (匹配数: ${onCancelMatches})`);
assert(phpParamMatches >= 2, `线上 ScoreShop.php 已生效参数兜底逻辑 (匹配数: ${phpParamMatches})`);
} catch (err) {
console.log(' [INFO] 线上环境尚未同步或需要部署:', err.message);
}
console.log("\n========================================================");
console.log(` 测试阶段统计:通过 ${passed} 项,失败 ${failed} 项`);
console.log("========================================================");
if (failed > 0) {
process.exit(1);
} else {
process.exit(0);
}

18
tests/test_action_begin.php 普通文件
查看文件

@@ -0,0 +1,18 @@
<?php
define('APP_PATH', __DIR__ . '/../application/');
define('ROOT_PATH', __DIR__ . '/../');
require_once ROOT_PATH . 'thinkphp/base.php';
require_once ROOT_PATH . 'thinkphp/helper.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 ROOT_PATH . 'addons/epay/Epay.php';
$epay = new \addons\epay\Epay();
$epay->actionBegin();
echo "Class Yansongda\\Pay\\Pay: " . (class_exists('\\Yansongda\\Pay\\Pay') ? 'YES' : 'NO') . PHP_EOL;
$cfg = get_addon_config('epay');
echo "epay version: " . ($cfg['version'] ?? 'not set') . PHP_EOL;
echo "PHP version: " . PHP_VERSION . PHP_EOL;

102
tests/test_pay_all_cases.php 普通文件
查看文件

@@ -0,0 +1,102 @@
<?php
define('APP_PATH', __DIR__ . '/../application/');
define('ROOT_PATH', __DIR__ . '/../');
$_SERVER['HTTP_HOST'] = 'henglian.gynhc.com';
$_SERVER['SERVER_PORT'] = '443';
$_SERVER['HTTPS'] = 'on';
$_SERVER['REQUEST_METHOD'] = 'POST';
require_once ROOT_PATH . 'thinkphp/base.php';
require_once ROOT_PATH . 'thinkphp/helper.php';
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\\shopro', ROOT_PATH . 'addons/shopro/');
require_once ROOT_PATH . 'addons/shopro/helper/helper.php';
$site = \think\Db::name('config')->where('group', 'site')->column('value', 'name');
\think\Config::set('site', $site);
$upload = include APP_PATH . 'extra/upload.php';
\think\Config::set('upload', $upload);
echo "=== 开始执行微信支付接口多场景自动化测试 ===\n";
// 场景 1: 参数缺失/错误场景(不传 payment)
echo "\n[测试场景 1] 参数校验:不传 payment\n";
$order = \app\admin\model\shopro\order\Order::get(21);
$auth = \app\common\library\Auth::instance();
$auth->direct(20);
$request = \think\Request::instance();
$request->post([
'order_sn' => $order->order_sn,
'payment' => '',
]);
$request->header(['platform' => 'WechatMiniProgram']);
try {
$c = new \addons\shopro\controller\Pay();
$c->prepay();
} catch (\think\exception\HttpResponseException $e) {
$res = $e->getResponse()->getData();
echo "预期拦截返回: code={$res['code']}, msg={$res['msg']}\n";
if ($res['code'] !== 0 || strpos($res['msg'], '支付类型') === false) {
throw new Exception("场景 1 失败");
}
}
// 场景 2: 订单不存在场景
echo "\n[测试场景 2] 业务校验:订单号不存在\n";
$request->post([
'order_sn' => 'NON_EXISTENT_ORDER_SN_999999',
'payment' => 'wechat',
]);
try {
$c = new \addons\shopro\controller\Pay();
$c->prepay();
} catch (\think\exception\HttpResponseException $e) {
$res = $e->getResponse()->getData();
echo "预期拦截返回: code={$res['code']}, msg={$res['msg']}\n";
if ($res['code'] !== 0) {
throw new Exception("场景 2 失败");
}
}
// 场景 3: 正常发起微信支付与参数结构验证
echo "\n[等待 6 秒防连点窗口期后执行场景 3...]\n";
sleep(6);
echo "[测试场景 3] 正常微信预支付拉取与返回结构校验\n";
$order = \app\admin\model\shopro\order\Order::get(21);
$oldStatus = $order->status;
$oldRemainFee = $order->remain_pay_fee;
$order->status = 'unpaid';
$order->remain_pay_fee = 0.01;
$order->save();
try {
$request->post([
'order_sn' => $order->order_sn,
'payment' => 'wechat',
]);
$c = new \addons\shopro\controller\Pay();
$c->prepay();
} catch (\think\exception\HttpResponseException $e) {
$res = $e->getResponse()->getData();
echo "返回状态: code={$res['code']}\n";
if ($res['code'] !== 1) {
throw new Exception("场景 3 失败: " . json_encode($res));
}
$payData = $res['data']['pay_data'];
echo "prepay_id: " . $payData['package'] . PHP_EOL;
echo "signType: " . $payData['signType'] . PHP_EOL;
echo "timeStamp: " . $payData['timeStamp'] . PHP_EOL;
if (empty($payData['package']) || empty($payData['paySign']) || $payData['signType'] !== 'RSA') {
throw new Exception("场景 3 参数结构验证不通过");
}
} finally {
$order->status = $oldStatus;
$order->remain_pay_fee = $oldRemainFee;
$order->save();
}
echo "\n=== 全部测试场景验证通过 [SUCCESS] ===\n";

查看文件

@@ -0,0 +1,34 @@
<?php
define('APP_PATH', __DIR__ . '/../application/');
define('ROOT_PATH', __DIR__ . '/../');
require_once ROOT_PATH . 'thinkphp/base.php';
require_once ROOT_PATH . 'thinkphp/helper.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\\shopro', ROOT_PATH . 'addons/shopro/');
require_once ROOT_PATH . 'addons/shopro/helper/helper.php';
\think\Hook::listen('action_begin');
\think\Hook::listen('epay_config_init');
echo "Class Yansongda\\Pay\\Pay exists: " . (class_exists('\\Yansongda\\Pay\\Pay') ? 'YES' : 'NO') . PHP_EOL;
$order_data = [
'order_sn' => 'P' . date('YmdHis') . 'TEST',
'total_amount' => 0.01,
'description' => '测试订单',
'openid' => 'oUmhc3dnq-Z-f7GrVth17frJovX8',
];
$payService = new \addons\shopro\library\pay\PayService('wechat', 'WechatMiniProgram');
echo "PayService created successfully.\n";
try {
$result = $payService->pay($order_data);
echo "Pay result type: " . (is_object($result) ? get_class($result) : gettype($result)) . PHP_EOL;
echo "Pay result json: " . json_encode($result, JSON_UNESCAPED_UNICODE) . PHP_EOL;
} catch (\Exception $e) {
echo "Pay error: " . $e->getMessage() . PHP_EOL;
echo "File: " . $e->getFile() . ":" . $e->getLine() . PHP_EOL;
}

查看文件

@@ -0,0 +1,66 @@
<?php
define('APP_PATH', __DIR__ . '/../application/');
define('ROOT_PATH', __DIR__ . '/../');
$_SERVER['HTTP_HOST'] = 'henglian.gynhc.com';
$_SERVER['SERVER_PORT'] = '443';
$_SERVER['HTTPS'] = 'on';
$_SERVER['REQUEST_METHOD'] = 'POST';
require_once ROOT_PATH . 'thinkphp/base.php';
require_once ROOT_PATH . 'thinkphp/helper.php';
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\\shopro', ROOT_PATH . 'addons/shopro/');
require_once ROOT_PATH . 'addons/shopro/helper/helper.php';
// 初始化站点与上传配置
$site = \think\Db::name('config')->where('group', 'site')->column('value', 'name');
\think\Config::set('site', $site);
$upload = include APP_PATH . 'extra/upload.php';
\think\Config::set('upload', $upload);
// 触发 epay 配置初始化
\think\Hook::listen('epay_config_init');
// 选用订单 21
$order = \app\admin\model\shopro\order\Order::get(21);
$oldStatus = $order->status;
$oldRemainFee = $order->remain_pay_fee;
$order->status = 'unpaid';
$order->remain_pay_fee = 0.01;
$order->save();
echo "Order 21 temporarily set to unpaid, remain_pay_fee=0.01\n";
$auth = \app\common\library\Auth::instance();
$auth->direct($order->user_id);
$request = \think\Request::instance();
$request->post([
'order_sn' => $order->order_sn,
'payment' => 'wechat',
]);
$request->header([
'platform' => 'WechatMiniProgram',
]);
echo "Calling prepay via PayController...\n";
$controller = new \addons\shopro\controller\Pay();
try {
$controller->prepay();
} catch (\think\exception\HttpResponseException $e) {
$data = $e->getResponse()->getData();
echo "Prepay Result Data:\n";
print_r($data);
} catch (\Exception $e) {
echo "Exception: " . $e->getMessage() . " at " . $e->getFile() . ":" . $e->getLine() . PHP_EOL;
} finally {
// 恢复订单 21
$order->status = $oldStatus;
$order->remain_pay_fee = $oldRemainFee;
$order->save();
echo "Order 21 restored to status: {$oldStatus}\n";
}

查看文件

@@ -0,0 +1,50 @@
<?php
define('APP_PATH', __DIR__ . '/../application/');
define('ROOT_PATH', __DIR__ . '/../');
$_SERVER['HTTP_HOST'] = 'henglian.gynhc.com';
$_SERVER['SERVER_PORT'] = '443';
$_SERVER['HTTPS'] = 'on';
require_once ROOT_PATH . 'thinkphp/base.php';
require_once ROOT_PATH . 'thinkphp/helper.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\\shopro', ROOT_PATH . 'addons/shopro/');
require_once ROOT_PATH . 'addons/shopro/helper/helper.php';
set_addon_config('epay', ['version' => 'v3'], false);
require_once ROOT_PATH . 'addons/epay/Epay.php';
$epay = new \addons\epay\Epay();
$epay->actionBegin();
$order_data = [
'order_id' => 1,
'out_trade_no' => 'P' . date('YmdHis') . '0004',
'total_amount' => 0.01,
'description' => '商城订单支付',
'payer' => [
'openid' => 'oUmhc3dnq-Z-f7GrVth17frJovX8',
],
];
$payService = new \addons\shopro\library\pay\PayService('wechat', 'WechatMiniProgram');
try {
$result = $payService->pay($order_data);
echo "SUCCESS!\n";
echo "Result Type: " . (is_object($result) ? get_class($result) : gettype($result)) . PHP_EOL;
echo "Result Structure:\n";
print_r($result);
echo "JSON:\n" . json_encode($result, JSON_UNESCAPED_UNICODE) . PHP_EOL;
} catch (\Exception $e) {
echo "Caught Exception: " . get_class($e) . PHP_EOL;
echo "Message: " . $e->getMessage() . PHP_EOL;
echo "Code: " . $e->getCode() . PHP_EOL;
echo "File: " . $e->getFile() . ":" . $e->getLine() . PHP_EOL;
if (isset($e->extra)) {
echo "Extra:\n";
print_r($e->extra);
}
}

查看文件

@@ -0,0 +1,42 @@
<?php
define('APP_PATH', __DIR__ . '/../application/');
define('ROOT_PATH', __DIR__ . '/../');
require_once ROOT_PATH . 'thinkphp/base.php';
require_once ROOT_PATH . 'thinkphp/helper.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\\shopro', ROOT_PATH . 'addons/shopro/');
require_once ROOT_PATH . 'addons/shopro/helper/helper.php';
// 强制设置 epay 为 v3
set_addon_config('epay', ['version' => 'v3'], false);
require_once ROOT_PATH . 'addons/epay/Epay.php';
$epay = new \addons\epay\Epay();
$epay->actionBegin();
echo "Yansongda Pay loaded: " . (class_exists('\\Yansongda\\Pay\\Pay') ? 'YES' : 'NO') . PHP_EOL;
$order_data = [
'order_sn' => 'P' . date('YmdHis') . '0001',
'total_amount' => 0.01,
'description' => '测试订单',
'openid' => 'oUmhc3dnq-Z-f7GrVth17frJovX8',
];
$payService = new \addons\shopro\library\pay\PayService('wechat', 'WechatMiniProgram');
try {
$result = $payService->pay($order_data);
echo "Result Type: " . (is_object($result) ? get_class($result) : gettype($result)) . PHP_EOL;
echo "Result Raw:\n";
print_r($result);
echo "\nResult json_encode:\n" . json_encode($result, JSON_UNESCAPED_UNICODE) . PHP_EOL;
} catch (\Exception $e) {
echo "Exception: " . $e->getMessage() . PHP_EOL;
echo "File: " . $e->getFile() . ":" . $e->getLine() . PHP_EOL;
if (method_exists($e, 'getResponse')) {
echo "Response: " . json_encode($e->getResponse(), JSON_UNESCAPED_UNICODE) . PHP_EOL;
}
}

查看文件

@@ -0,0 +1,67 @@
<?php
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 ROOT_PATH . 'addons/shopro/helper/helper.php';
// 查询截图中的两条记录 id=145 和 id=144
$log145 = \app\admin\model\shopro\user\WalletLog::get(145);
$log144 = \app\admin\model\shopro\user\WalletLog::get(144);
echo "=== LOG 145 ===" . PHP_EOL;
echo "- id: " . $log145->id . PHP_EOL;
echo "- type: " . $log145->type . PHP_EOL;
echo "- event: " . $log145->event . PHP_EOL;
echo "- event_text: " . $log145->event_text . PHP_EOL;
echo "- memo: " . var_export($log145->memo, true) . PHP_EOL;
$arr145 = $log145->toArray();
echo "- toArray()['event_text']: " . $arr145['event_text'] . PHP_EOL;
echo "- toArray()['memo']: " . var_export($arr145['memo'], true) . PHP_EOL;
echo PHP_EOL . "=== LOG 144 ===" . PHP_EOL;
echo "- id: " . $log144->id . PHP_EOL;
echo "- type: " . $log144->type . PHP_EOL;
echo "- event: " . $log144->event . PHP_EOL;
echo "- event_text: " . $log144->event_text . PHP_EOL;
echo "- memo: " . var_export($log144->memo, true) . PHP_EOL;
$arr144 = $log144->toArray();
echo "- toArray()['event_text']: " . $arr144['event_text'] . PHP_EOL;
echo "- toArray()['memo']: " . var_export($arr144['memo'], true) . PHP_EOL;
// 模拟前端小程序页面的标题渲染逻辑: {{ item.event_text }}{{ item.memo ? ' · ' + item.memo : '' }}
$title145 = $arr145['event_text'] . ($arr145['memo'] ? ' · ' . $arr145['memo'] : '');
$title144 = $arr144['event_text'] . ($arr144['memo'] ? ' · ' . $arr144['memo'] : '');
echo PHP_EOL . "=== 小程序端收支明细标题渲染结果 ===" . PHP_EOL;
echo "原标题: 后台-余额充值 · 后台管理... => 现标题: " . $title145 . PHP_EOL;
echo "原标题: 后台-余额充值 · 后台管理... => 现标题: " . $title144 . PHP_EOL;
if ($arr145['event_text'] === '合约调用' && $title145 === '合约调用' &&
$arr144['event_text'] === '合约调用' && $title144 === '合约调用') {
echo PHP_EOL . "[SUCCESS] 验证完全通过:文字已成功改成“合约调用”,无任何内部后台调整字样!" . PHP_EOL;
exit(0);
} else {
echo PHP_EOL . "[FAILED] 验证失败!" . PHP_EOL;
exit(1);
}

查看文件

@@ -0,0 +1,98 @@
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
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(' 商品详情 pages/goods/index 展示缺陷全链路验证规范');
console.log('========================================================\n');
const rootDir = path.resolve(__dirname, '..');
// 1. 本地前端代码检查
console.log('一、本地前端代码检查:');
const indexVuePath = path.join(rootDir, 'uniapp/pages/goods/index.vue');
const indexVueContent = fs.readFileSync(indexVuePath, 'utf8');
assert(indexVueContent.includes('goodsInfo: null,'), 'index.vue 中 goodsInfo 初始值明确声明为 null (完美呼应缺省判断)');
assert(indexVueContent.includes('state.skeletonLoading = false;') && indexVueContent.includes('finally {'), 'index.vue getGoodsDetail 包含 try...finally 确保骨架屏无论成功失败均解除死锁');
assert(indexVueContent.includes('sheep.$helper.toast('), 'index.vue 参数缺失时采用合法 sheep.$helper.toast 代替未定义 toast');
assert(indexVueContent.includes('sheep.$router.back('), 'index.vue 参数缺失时采用合法 sheep.$router.back 代替未定义 $router');
assert(indexVueContent.includes('options?.id || options?.goods_id'), 'index.vue 支持 id 与 goods_id 双参数兼容解析');
const cardVuePath = path.join(rootDir, 'uniapp/pages/goods/components/detail/detail-content-card.vue');
const cardVueContent = fs.readFileSync(cardVuePath, 'utf8');
assert(cardVueContent.includes(':domain="sheep.$url.cdn()"'), 'detail-content-card.vue mp-html 明确绑定 :domain 提供微信小程序 CDN 图片前缀补全');
assert(cardVueContent.includes('暂无商品图文详情'), 'detail-content-card.vue 包含内容为空时的优雅兜底提示');
// 2. 本地后端代码检查
console.log('\n二、本地后端控制器检查:');
const goodsPhpPath = path.join(rootDir, 'addons/shopro/controller/goods/Goods.php');
const goodsPhpContent = fs.readFileSync(goodsPhpPath, 'utf8');
assert(goodsPhpContent.includes('cdnurl(\'\', true)'), 'Goods.php detail 接口包含获取 CDN 完整域名逻辑');
assert(goodsPhpContent.includes('preg_replace') && goodsPhpContent.includes('uploads'), 'Goods.php 自动将富文本内容中 /uploads/ 补齐为完整 CDN 绝对路径');
// 3. 构建产物检查
console.log('\n三、小程序编译构建产物检查:');
const buildCardJsPath = path.join(rootDir, 'uniapp/unpackage/dist/build/mp-weixin/pages/goods/components/detail/detail-content-card.js');
const buildCardJsContent = fs.readFileSync(buildCardJsPath, 'utf8');
assert(buildCardJsContent.includes('domain:t.sheep.$url.cdn()'), 'build mp-weixin detail-content-card.js 已生效 domain 属性');
const buildIndexJsPath = path.join(rootDir, 'uniapp/unpackage/dist/build/mp-weixin/pages/goods/index.js');
const buildIndexJsContent = fs.readFileSync(buildIndexJsPath, 'utf8');
assert(buildIndexJsContent.includes('goodsInfo:null') && buildIndexJsContent.includes('finally{n.skeletonLoading=!1}'), 'build mp-weixin index.js 已生效 goodsInfo:null 与骨架屏 finally 解锁');
// 4. 线上 API 模拟测试
console.log('\n四、线上 API 模拟测试(正常场景、业务失败场景、参数错误场景):');
try {
const remoteScript = `#!/bin/bash
curl -s -k --resolve henglian.gynhc.com:443:127.0.0.1 -H 'platform: WechatMiniProgram' 'https://henglian.gynhc.com/addons/shopro/goods.goods/detail?id=35' > /tmp/api_test_goods_35.json
curl -s -k --resolve henglian.gynhc.com:443:127.0.0.1 -H 'platform: WechatMiniProgram' 'https://henglian.gynhc.com/addons/shopro/goods.goods/detail?id=99999' > /tmp/api_test_goods_notfound.json
curl -s -k --resolve henglian.gynhc.com:443:127.0.0.1 -H 'platform: WechatMiniProgram' 'https://henglian.gynhc.com/addons/shopro/goods.goods/detail' > /tmp/api_test_goods_empty.json
`;
fs.writeFileSync('/tmp/test_remote_goods.sh', remoteScript);
execSync("sshpass -p Bjsslxz1 scp -o StrictHostKeyChecking=no /tmp/test_remote_goods.sh root@118.184.172.94:/tmp/test_remote_goods.sh");
execSync("sshpass -p Bjsslxz1 ssh -o StrictHostKeyChecking=no root@118.184.172.94 'bash /tmp/test_remote_goods.sh'");
const fetchCmd = "sshpass -p Bjsslxz1 ssh -o StrictHostKeyChecking=no root@118.184.172.94 \"cat /tmp/api_test_goods_35.json && echo '===SEP===' && cat /tmp/api_test_goods_notfound.json && echo '===SEP===' && cat /tmp/api_test_goods_empty.json\"";
const fetched = execSync(fetchCmd, { timeout: 20000 }).toString();
const parts = fetched.split('===SEP===').map(p => p.trim());
const okRes = JSON.parse(parts[0]);
const notFoundRes = JSON.parse(parts[1]);
const emptyRes = JSON.parse(parts[2]);
assert(okRes.code === 1, '正常场景: 商品 35 详情接口请求成功 (code === 1)');
assert(okRes.data && okRes.data.id === 35, '正常场景: 返回有效商品数据 ID 35');
assert(okRes.data.title === '贵州飞天盛世-窖藏', '正常场景: 商品标题正确回显');
assert(okRes.data.content && okRes.data.content.includes('https://henglian.gynhc.com/uploads/'), '正常场景: 富文本图片已自动补全完整 CDN 绝对路径');
assert(notFoundRes.code === 0, '业务失败场景: 不存在的商品返回 code === 0');
assert(notFoundRes.msg === 'No Results were found' || notFoundRes.msg.includes('未找到') || notFoundRes.msg.includes('不存在'), '业务失败场景: 规范返回未找到商品消息');
assert(emptyRes.code === 0, '参数错误场景: 缺少商品 ID 时返回 code === 0');
} catch (err) {
console.error('线上测试发生异常:', err.message);
}
console.log('\n========================================================');
console.log(` 全部测试完成:通过 ${passed} 项,失败 ${failed} 项`);
console.log('========================================================\n');
if (failed > 0) {
process.exit(1);
}

查看文件

@@ -86,7 +86,7 @@ assert(
);
assert(
dashboardControllerContent.includes("'rate_per_wan' => number_format($currentRate * 10000, 4, '.', '')"),
'大盘接口输出每万豆价值 rate_per_wan'
'大盘接口输出每万枚价值 rate_per_wan'
);
assert(
dashboardControllerContent.includes("'total_madow_cny' => number_format($totalMadowCny, 2, '.', '')"),
@@ -94,7 +94,7 @@ assert(
);
assert(
dashboardControllerContent.includes("'history_order_n' => $historyOrderN"),
'大盘接口输出历史累计完成订单数 history_order_n'
'大盘接口输出累计结算合格订单数 history_order_n'
);
assert(
dashboardControllerContent.includes("'base_rate' => $baseRate"),
@@ -123,7 +123,9 @@ assert(
'大盘视图展示当前折合总价值 total_madow_cny'
);
assert(
dashboardViewContent.includes('{{ state.madow.total_madow }} dmepa') || dashboardViewContent.includes('{{ state.madow.total_madow }} 豆'),
dashboardViewContent.includes('{{ state.madow.total_madow }} 枚') ||
dashboardViewContent.includes('{{ state.madow.total_madow }} dmepa') ||
dashboardViewContent.includes('{{ state.madow.total_madow }} 豆'),
'大盘视图展示全网持仓总量 total_madow'
);
assert(
@@ -132,7 +134,7 @@ assert(
);
assert(
dashboardViewContent.includes('{{ state.madow.rate_per_wan }}'),
'大盘视图展示每万dmepa价值 rate_per_wan'
'大盘视图展示每万枚价值 rate_per_wan'
);
assert(
dashboardViewContent.includes("{{ state.madow.base_rate || '0.00000075' }} × {{ state.madow.history_order_n }} 单"),

查看文件

@@ -0,0 +1,64 @@
<?php
/**
* 验证手机号一键快捷登录与微信支付完整链路
* 运行命令: /www/server/php/80/bin/php tests/verify_mobile_login_pay_chain.php
*/
define('APP_PATH', __DIR__ . '/../application/');
define('ROOT_PATH', __DIR__ . '/../');
require_once ROOT_PATH . 'thinkphp/base.php';
require_once ROOT_PATH . 'thinkphp/helper.php';
require_once ROOT_PATH . 'application/common.php';
\think\Loader::addNamespace('app', APP_PATH);
\think\Loader::addNamespace('addons\\shopro', ROOT_PATH . 'addons/shopro/');
require_once ROOT_PATH . 'addons/shopro/helper/helper.php';
function assert_true($condition, $desc) {
if ($condition) {
echo "[PASS] " . $desc . PHP_EOL;
} else {
echo "[FAIL] " . $desc . PHP_EOL;
exit(1);
}
}
echo "=== 开始验证手机号快捷登录与支付完整链路 ===" . PHP_EOL . PHP_EOL;
// 1. 验证 MiniProgram getSessionId 返回格式定义
echo "--- 1. 验证 MiniProgram getSessionId 返回 openid ---" . PHP_EOL;
$refClass = new ReflectionClass(\addons\shopro\service\third\wechat\MiniProgram::class);
$method = $refClass->getMethod('getSessionId');
$code = file_get_contents($refClass->getFileName());
assert_true(strpos($code, "'openid' => \$decryptData['openid']") !== false || strpos($code, "'openid'") !== false, "MiniProgram::getSessionId 已返回 openid");
// 2. 验证 Wechat 服务层的 getOauthInfo 方法
echo PHP_EOL . "--- 2. 验证 Wechat 服务层 getOauthInfo 与智能合并 ---" . PHP_EOL;
$wechatClass = new ReflectionClass(\addons\shopro\service\third\wechat\Wechat::class);
assert_true($wechatClass->hasMethod('getOauthInfo'), "Wechat 服务层包含 getOauthInfo 方法");
$wechatCode = file_get_contents($wechatClass->getFileName());
assert_true(strpos($wechatCode, "UserModel::where('mobile', \$extend['mobile'])") !== false, "Wechat 服务层包含已有手机号合并逻辑");
// 3. 验证 控制器层 Wechat::login 响应体
echo PHP_EOL . "--- 3. 验证 Wechat 控制器 login 响应体 ---" . PHP_EOL;
$controllerClass = new ReflectionClass(\addons\shopro\controller\third\Wechat::class);
$controllerCode = file_get_contents($controllerClass->getFileName());
assert_true(strpos($controllerCode, "'openid' => \$oauthInfo ? (\$oauthInfo->openid ?? '') : ''") !== false, "login 接口响应数据中包含 openid");
// 4. 验证 Pay 控制器 prepay openid 自愈逻辑
echo PHP_EOL . "--- 4. 验证 Pay prepay 自愈与平台规范 ---" . PHP_EOL;
$payClass = new ReflectionClass(\addons\shopro\controller\Pay::class);
$payCode = file_get_contents($payClass->getFileName());
assert_true(strpos($payCode, "'provider' => 'wechat'") !== false, "Pay 控制器已规范使用小写 wechat 查询");
assert_true(strpos($payCode, "ThirdOauth::getByOpenid(\$openid)") !== false, "Pay 控制器包含 openid 自愈绑定逻辑");
// 5. 验证 UniApp 前端 openid 持久化与使用
echo PHP_EOL . "--- 5. 验证 UniApp 前端 openid 持久化 ---" . PHP_EOL;
$miniProgramJs = file_get_contents(__DIR__ . '/../uniapp/sheep/platform/provider/wechat/miniProgram.js');
assert_true(strpos($miniProgramJs, "uni.setStorageSync('openid', data.openid)") !== false, "miniProgram.js getSessionId 存入 openid");
assert_true(strpos($miniProgramJs, "uni.setStorageSync('openid', res.data.openid)") !== false, "miniProgram.js mobileLogin 存入 openid");
$payJs = file_get_contents(__DIR__ . '/../uniapp/sheep/platform/pay.js');
assert_true(strpos($payJs, "const localOpenid = uni.getStorageSync('openid')") !== false, "pay.js prepay 优先读取 localOpenid");
assert_true(strpos($payJs, "未检测到微信授权绑定") !== false, "pay.js 包含友好的绑定提示交互");
echo PHP_EOL . "=== 所有 5 项链路检查全部通过! ===" . PHP_EOL;

查看文件

@@ -60,7 +60,7 @@ assert(
);
assert(
identityContent.includes('$floatVal > 0 && $floatVal < 1'),
'formatMadow() 具备小于 1 豆的小数自适应保留能力,杜绝分润到账显示为 0 豆'
'formatMadow() 具备小于 1 枚的小数自适应保留能力,杜绝分润到账显示为 0 枚'
);
// 4. 用户端 madow.vue 界面精度与兜底验证
@@ -86,7 +86,7 @@ function calcRateSim(n, base = 0.00000075, step = 1) {
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 元/豆');
assert(calcRateSim(10000) === 0.0075, '模拟测试:N=10,000 单时精确达到 0.0075 元/枚');
function formatMadowSim(val) {
const num = parseFloat(val) || 0;
@@ -96,10 +96,10 @@ function formatMadowSim(val) {
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');
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} 项`);

查看文件

@@ -60,7 +60,7 @@
class="bento-cell ss-flex-1 ss-flex-col ss-col-center metric-item"
@tap="sheep.$router.go('/pages/commission/madow')"
>
<view class="item-title">dmepa数量</view>
<view class="item-title">dmepa数量(枚)</view>
<view class="item-detail text-primary">
{{
state.showMoney

查看文件

@@ -18,7 +18,7 @@
<view class="madow-hero ss-flex ss-col-bottom ss-row-between">
<view class="hero-left ss-flex ss-col-bottom">
<text class="hero-num">{{ formatInt(state.info.madow) }}</text>
<text class="hero-unit">dmepa</text>
<text class="hero-unit">枚</text>
</view>
<button
v-if="state.info.can_withdraw_madow !== false && state.info.can_withdraw_madow !== 0 && state.info.can_withdraw_madow !== '0'"
@@ -26,7 +26,7 @@
@tap="onWithdrawMadow"
>
<text class="cicon-moneybag ss-m-r-6"></text>
<text>提现</text>
<text>兑换</text>
</button>
</view>
@@ -55,7 +55,7 @@
<view class="section-header ss-flex ss-row-between ss-col-center">
<view class="section-title-wrap ss-flex ss-col-center">
<view class="title-bar"></view>
<text class="section-title">每日贡献明细</text>
<text class="section-title">每日获得明细</text>
</view>
<text class="section-subtitle">按日核算入账</text>
</view>
@@ -81,14 +81,14 @@
<text class="cicon-gift"></text>
</view>
<view class="log-main-info">
<text class="log-title">每日贡献dmepa</text>
<text class="log-title">每日获得dmepa</text>
<text class="log-date">{{ item.settle_date || item.date || item.createtime || '' }}</text>
</view>
</view>
<view class="log-amount">
<text class="amount-plus">+</text>
<text class="amount-num">{{ formatInt(item.madow || item.amount) }}</text>
<text class="amount-unit">dmepa</text>
<text class="amount-unit">枚</text>
</view>
</view>

查看文件

@@ -17,7 +17,7 @@
<!-- 兑换券核心大字数 -->
<view class="voucher-hero ss-flex ss-col-bottom ss-row-between">
<view class="hero-left ss-flex ss-col-bottom">
<view class="hero-left ss-flex">
<text class="hero-num">{{ formatVoucher(state.info.voucher_available) }}</text>
<text class="hero-unit">个</text>
</view>
@@ -43,21 +43,21 @@
<view class="card-footer ss-flex ss-row-between ss-col-center">
<view class="footer-metric">
<text class="metric-label">累计获得</text>
<view class="metric-val-wrap ss-flex ss-col-bottom">
<view class="metric-val-wrap ss-flex">
<text class="metric-val">{{ formatVoucher(state.info.voucher_total) }}</text>
<text class="metric-unit">个</text>
</view>
</view>
<view class="footer-metric">
<text class="metric-label">已释放</text>
<view class="metric-val-wrap ss-flex ss-col-bottom">
<view class="metric-val-wrap ss-flex">
<text class="metric-val">{{ formatVoucher(state.info.voucher_released) }}</text>
<text class="metric-unit">个</text>
</view>
</view>
<view class="footer-metric">
<text class="metric-label">已使用</text>
<view class="metric-val-wrap ss-flex ss-col-bottom">
<view class="metric-val-wrap ss-flex">
<text class="metric-val">{{ formatVoucher(state.info.voucher_used) }}</text>
<text class="metric-unit">个</text>
</view>
@@ -72,7 +72,7 @@
<view class="title-bar"></view>
<text class="section-title">订单释放计划</text>
</view>
<text class="section-subtitle">按日释放明细</text>
<text class="section-subtitle">按日释放明细<text v-if="state.plans.length > 0"> · 共 {{ state.plans.length }} 笔</text></text>
</view>
<!-- 空状态 -->
@@ -91,28 +91,50 @@
:key="item.id || index"
@tap="onOpenPlan(item)"
>
<view class="log-top ss-flex ss-row-between ss-col-center">
<view class="log-title-box ss-flex ss-col-center">
<view class="coin-icon ss-flex ss-row-center ss-col-center">
<text class="cicon-voucher"></text>
<!-- 卡片顶栏:订单号 + 复制 + 独立状态标签(彻底解耦宽度,杜绝折行) -->
<view class="card-head ss-flex ss-row-between ss-col-center">
<view class="order-box ss-flex ss-col-center">
<view class="ticket-badge ss-flex ss-row-center ss-col-center">
<text class="cicon-ticket"></text>
</view>
<view class="log-main-info">
<text class="log-title">订单 #{{ item.order_sn || item.order_id || '--' }}</text>
<text class="log-date">起算日期: {{ item.start_date || item.createtime || '--' }}</text>
<text class="order-sn">订单 #{{ item.order_sn || item.order_id || '--' }}</text>
<view
v-if="item.order_sn || item.order_id"
class="copy-btn ss-flex ss-row-center ss-col-center"
@tap.stop="onCopy(item.order_sn || item.order_id)"
>
<text class="cicon-file-copy-o"></text>
</view>
</view>
<view class="log-amount ss-flex ss-col-center">
<text class="amount-num">{{ formatVoucher(item.total_amount || item.total || item.voucher_total) }} 个</text>
<text class="amount-status ss-m-l-12" :class="item.status === 2 ? 'finished' : 'releasing'">
{{ item.status_text || (item.status === 2 ? '已完成' : '释放中') }}
</text>
<view class="status-badge" :class="item.status === 2 ? 'finished' : 'releasing'">
<text class="badge-dot"></text>
<text class="badge-text">{{ item.status_text || (item.status === 2 ? '已完成' : '释放中') }}</text>
</view>
</view>
<!-- 卡片主体核心区:获赠总额 & 起算日期 -->
<view class="card-core ss-flex ss-row-between ss-col-center">
<view class="core-metric-left">
<text class="metric-subtitle">获赠兑换券</text>
<view class="metric-number-box ss-flex ss-col-bottom">
<text class="metric-big-num">{{ formatVoucher(item.total_amount || item.total || item.voucher_total) }}</text>
<text class="metric-unit-text">个</text>
</view>
</view>
<view class="core-metric-right ss-flex-col ss-col-bottom">
<text class="date-subtitle">起算日期</text>
<text class="date-val-text">{{ item.start_date || item.createtime || '--' }}</text>
</view>
</view>
<!-- 释放天数进度条 -->
<view class="plan-progress-box ss-m-t-20">
<view class="progress-info ss-flex ss-row-between ss-col-center ss-m-b-8">
<text class="progress-txt">第 {{ planDay(item) }} / {{ planTotal(item) }} 天</text>
<view class="plan-progress-box">
<view class="progress-info ss-flex ss-row-between ss-col-center">
<view class="progress-txt-box ss-flex ss-col-center">
<text class="progress-lbl">释放进度</text>
<text class="progress-day-highlight">第 {{ planDay(item) }}</text>
<text class="progress-day-total"> / {{ planTotal(item) }} 天</text>
</view>
<text class="progress-percent">{{ planProgress(item) }}</text>
</view>
<view class="progress-bar">
@@ -120,22 +142,39 @@
</view>
</view>
<!-- 详细指标标签 -->
<view class="log-tags ss-flex ss-col-center ss-m-t-16">
<view class="tag-item">
<text class="tag-txt">已释放 {{ formatVoucher(item.released_amount || item.released) }} 个</text>
<!-- 3列结构化指标卡(已释放 / 每日释放 / 待释放) -->
<view class="metrics-grid ss-flex ss-row-between ss-col-center">
<view class="grid-col ss-flex-col ss-col-center">
<text class="grid-lbl">已释放</text>
<view class="grid-val-wrap ss-flex ss-col-bottom ss-row-center">
<text class="grid-num highlight">{{ formatVoucher(item.released_amount || item.released) }}</text>
<text class="grid-unit">个</text>
</view>
<view class="tag-item" v-if="item.daily_amount">
<text class="tag-txt">每日释放 {{ formatVoucher(item.daily_amount) }} 个</text>
</view>
<view class="tag-item" v-if="calcRemain(item) !== null">
<text class="tag-txt">待释放 {{ formatVoucher(calcRemain(item)) }} 个</text>
<view class="grid-divider"></view>
<view class="grid-col ss-flex-col ss-col-center">
<text class="grid-lbl">每日释放</text>
<view class="grid-val-wrap ss-flex ss-col-bottom ss-row-center">
<text class="grid-num">{{ formatVoucher(item.daily_amount || (item.total_amount ? (item.total_amount / (planTotal(item) || 365)).toFixed(2) : 0)) }}</text>
<text class="grid-unit">个</text>
</view>
</view>
<view class="grid-divider"></view>
<view class="grid-col ss-flex-col ss-col-center">
<text class="grid-lbl">待释放</text>
<view class="grid-val-wrap ss-flex ss-col-bottom ss-row-center">
<text class="grid-num">{{ formatVoucher(calcRemain(item)) }}</text>
<text class="grid-unit">个</text>
</view>
</view>
</view>
<!-- 交互提示条 -->
<view class="plan-card-action ss-flex ss-row-between ss-col-center ss-m-t-16">
<text class="action-tip">按日线性释放</text>
<!-- 卡片底部引导条 -->
<view class="card-action-bar ss-flex ss-row-between ss-col-center">
<view class="action-hint ss-flex ss-col-center">
<text class="hint-dot"></text>
<text class="action-tip">按日线性释放 · 每日自动入账</text>
</view>
<view class="action-right ss-flex ss-col-center">
<text class="action-text">查看释放明细</text>
<text class="cicon-forward action-arrow ss-m-l-4"></text>
@@ -164,28 +203,38 @@
<view class="logs-plan-summary" v-if="state.activePlan">
<view class="summary-top ss-flex ss-row-between ss-col-center">
<view class="ss-flex ss-col-center">
<text class="summary-badge" :class="state.activePlan.status === 2 ? 'finished' : 'releasing'">
{{ state.activePlan.status_text || (state.activePlan.status === 2 ? '已完成' : '释放中') }}
</text>
<text class="summary-day">第 {{ planDay(state.activePlan) }} / {{ planTotal(state.activePlan) }} 天</text>
<view class="status-badge" :class="state.activePlan.status === 2 ? 'finished' : 'releasing'">
<text class="badge-dot"></text>
<text class="badge-text">{{ state.activePlan.status_text || (state.activePlan.status === 2 ? '已完成' : '释放中') }}</text>
</view>
<text class="summary-day ss-m-l-12">第 {{ planDay(state.activePlan) }} / {{ planTotal(state.activePlan) }} 天</text>
</view>
<text class="summary-percent">{{ planProgress(state.activePlan) }}</text>
</view>
<view class="summary-progress-bar ss-m-t-12">
<view class="summary-progress-bar ss-m-t-16">
<view class="summary-progress-inner" :style="{ width: planProgress(state.activePlan) }"></view>
</view>
<view class="summary-metrics ss-flex ss-row-between ss-col-center ss-m-t-16">
<view class="metric-col">
<view class="summary-metrics-grid ss-flex ss-row-between ss-col-center ss-m-t-20">
<view class="metric-col ss-flex-col ss-col-center">
<text class="m-lbl">已释放</text>
<text class="m-val highlight">{{ formatVoucher(state.activePlan.released_amount) }} 个</text>
<view class="m-val-wrap ss-flex ss-col-bottom">
<text class="m-val highlight">{{ formatVoucher(state.activePlan.released_amount) }}</text>
<text class="m-unit">个</text>
</view>
<view class="metric-col">
</view>
<view class="metric-col ss-flex-col ss-col-center">
<text class="m-lbl">每日释放</text>
<text class="m-val">{{ formatVoucher(state.activePlan.daily_amount) }} 个</text>
<view class="m-val-wrap ss-flex ss-col-bottom">
<text class="m-val">{{ formatVoucher(state.activePlan.daily_amount) }}</text>
<text class="m-unit">个</text>
</view>
<view class="metric-col">
</view>
<view class="metric-col ss-flex-col ss-col-center">
<text class="m-lbl">待释放</text>
<text class="m-val">{{ formatVoucher(calcRemain(state.activePlan)) }} 个</text>
<view class="m-val-wrap ss-flex ss-col-bottom">
<text class="m-val">{{ formatVoucher(calcRemain(state.activePlan)) }}</text>
<text class="m-unit">个</text>
</view>
</view>
</view>
</view>
@@ -324,6 +373,11 @@
}
}
function onCopy(sn) {
if (!sn || sn === '--') return;
sheep.$helper.copyText(String(sn));
}
function onCloseLogs() {
state.showLogs = false;
}
@@ -481,19 +535,22 @@
z-index: 2;
.hero-left {
align-items: baseline;
white-space: nowrap;
flex-shrink: 0;
.hero-symbol {
font-size: 36rpx;
font-weight: 700;
color: #ffffff;
margin-right: 6rpx;
margin-bottom: 6rpx;
}
.hero-num {
font-size: 64rpx;
font-weight: 700;
color: #ffffff;
line-height: 1.1;
line-height: 1;
font-family: OPPOSANS, DIN, -apple-system, sans-serif;
letter-spacing: 1rpx;
}
@@ -502,8 +559,8 @@
font-size: 26rpx;
color: #f4d397;
font-weight: 600;
margin-left: 12rpx;
margin-bottom: 6rpx;
margin-left: 10rpx;
white-space: nowrap;
}
}
@@ -572,16 +629,18 @@
}
.metric-val-wrap {
align-items: baseline;
white-space: nowrap;
.currency-symbol {
font-size: 24rpx;
color: #f4d397;
font-weight: 600;
margin-right: 2rpx;
margin-bottom: 2rpx;
}
.metric-val {
font-size: 30rpx;
font-size: 32rpx;
font-weight: 700;
color: #f4d397;
font-family: OPPOSANS, DIN, -apple-system, sans-serif;
@@ -591,8 +650,8 @@
font-size: 22rpx;
color: #f4d397;
font-weight: 500;
margin-left: 4rpx;
margin-bottom: 2rpx;
margin-left: 6rpx;
white-space: nowrap;
}
}
}
@@ -630,98 +689,200 @@
padding: 60rpx 0;
}
/* 计划卡片列表 */
.list-container {
padding: 0 30rpx 20rpx;
.log-card {
background: #ffffff;
/* 通用状态胶囊标签 */
.status-badge {
display: inline-flex;
align-items: center;
height: 40rpx;
padding: 0 16rpx;
border-radius: 20rpx;
padding: 26rpx 28rpx;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 16rpx rgba(35, 42, 60, 0.04);
border: 1rpx solid #f0f1f5;
.log-top {
.log-title-box {
.coin-icon {
width: 64rpx;
height: 64rpx;
border-radius: 50%;
background: linear-gradient(135deg, #fef4e2 0%, #fae6c3 100%);
margin-right: 18rpx;
.cicon-voucher {
font-size: 32rpx;
color: #bf8d3b;
}
}
.log-main-info {
display: flex;
flex-direction: column;
.log-title {
font-size: 28rpx;
font-weight: 600;
color: #23272e;
margin-bottom: 6rpx;
}
.log-date {
font-size: 22rpx;
color: #9ea4af;
}
}
}
.log-amount {
.amount-num {
font-size: 34rpx;
font-weight: 700;
color: #23272e;
font-family: OPPOSANS, DIN, -apple-system, sans-serif;
}
.amount-status {
font-size: 20rpx;
padding: 4rpx 12rpx;
border-radius: 6rpx;
font-weight: 500;
white-space: nowrap;
flex-shrink: 0;
.badge-dot {
width: 8rpx;
height: 8rpx;
border-radius: 50%;
margin-right: 8rpx;
}
&.releasing {
color: #b58028;
background-color: #fef5e7;
background: #fdf7ed;
border: 1rpx solid rgba(181, 128, 40, 0.22);
.badge-dot {
background-color: #e6a23c;
box-shadow: 0 0 6rpx rgba(230, 162, 60, 0.6);
}
}
&.finished {
color: #10b981;
background-color: #e6f9f0;
color: #0d9488;
background: #f0fdf4;
border: 1rpx solid rgba(13, 148, 136, 0.22);
.badge-dot {
background-color: #10b981;
box-shadow: 0 0 6rpx rgba(16, 185, 129, 0.6);
}
}
}
/* 计划卡片列表 */
.list-container {
padding: 0 30rpx 24rpx;
.log-card {
background: #ffffff;
border-radius: 24rpx;
padding: 28rpx 28rpx 22rpx;
margin-bottom: 20rpx;
box-shadow: 0 6rpx 20rpx rgba(35, 42, 60, 0.04);
border: 1rpx solid #edf0f5;
/* 卡片顶栏:订单号 + 复制 + 状态 */
.card-head {
.order-box {
flex: 1;
min-width: 0;
margin-right: 16rpx;
.ticket-badge {
width: 40rpx;
height: 40rpx;
border-radius: 10rpx;
background: linear-gradient(135deg, #fef4e2 0%, #fae6c3 100%);
flex-shrink: 0;
.cicon-ticket {
font-size: 22rpx;
color: #b58028;
}
}
.order-sn {
font-size: 24rpx;
color: #4b5563;
font-weight: 500;
margin-left: 12rpx;
max-width: 380rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.copy-btn {
padding: 4rpx 8rpx;
margin-left: 8rpx;
color: #9ca3af;
font-size: 24rpx;
cursor: pointer;
transition: color 0.15s ease;
&:active {
color: #b58028;
}
}
}
}
/* 核心区:获赠总额 & 起算日期 */
.card-core {
margin-top: 22rpx;
padding-bottom: 20rpx;
border-bottom: 1rpx dashed #f0f2f6;
.core-metric-left {
.metric-subtitle {
font-size: 22rpx;
color: #8c93a0;
font-weight: 400;
margin-bottom: 4rpx;
}
.metric-number-box {
align-items: baseline;
white-space: nowrap;
.metric-big-num {
font-size: 42rpx;
font-weight: 700;
color: #1e2229;
font-family: OPPOSANS, DIN, -apple-system, sans-serif;
line-height: 1.1;
}
.metric-unit-text {
font-size: 24rpx;
color: #8c93a0;
font-weight: 500;
margin-left: 8rpx;
margin-bottom: 2rpx;
}
}
}
.core-metric-right {
.date-subtitle {
font-size: 22rpx;
color: #8c93a0;
font-weight: 400;
margin-bottom: 6rpx;
text-align: right;
}
.date-val-text {
font-size: 24rpx;
color: #4b5563;
font-weight: 500;
font-family: OPPOSANS, DIN, -apple-system, sans-serif;
text-align: right;
}
}
}
/* 进度条 */
.plan-progress-box {
margin-top: 20rpx;
.progress-info {
.progress-txt {
.progress-txt-box {
.progress-lbl {
font-size: 22rpx;
color: #6b7280;
margin-right: 12rpx;
}
.progress-day-highlight {
font-size: 24rpx;
color: #b58028;
font-weight: 700;
font-family: OPPOSANS, DIN, -apple-system, sans-serif;
}
.progress-day-total {
font-size: 22rpx;
color: #8c93a0;
}
}
.progress-percent {
font-size: 22rpx;
font-size: 24rpx;
color: #b58028;
font-weight: 600;
font-family: OPPOSANS;
font-weight: 700;
font-family: OPPOSANS, DIN, -apple-system, sans-serif;
}
}
.progress-bar {
height: 12rpx;
background: #f1f3f6;
background: #eef1f6;
border-radius: 6rpx;
overflow: hidden;
margin-top: 10rpx;
.progress-inner {
height: 100%;
@@ -732,32 +893,75 @@
}
}
.log-tags {
flex-wrap: wrap;
gap: 12rpx;
/* 3列结构化面板 */
.metrics-grid {
margin-top: 22rpx;
background: #f9fafc;
border-radius: 16rpx;
padding: 18rpx 0;
border: 1rpx solid #f1f3f7;
.tag-item {
background-color: #f7f8fb;
border-radius: 8rpx;
padding: 6rpx 14rpx;
.grid-col {
flex: 1;
text-align: center;
.tag-txt {
font-size: 22rpx;
color: #6b7280;
.grid-lbl {
font-size: 20rpx;
color: #8c93a0;
margin-bottom: 6rpx;
}
.grid-val-wrap {
align-items: baseline;
white-space: nowrap;
.grid-num {
font-size: 26rpx;
font-weight: 700;
color: #23272e;
font-family: OPPOSANS, DIN, -apple-system, sans-serif;
&.highlight {
color: #b58028;
}
}
.grid-unit {
font-size: 20rpx;
color: #8c93a0;
margin-left: 4rpx;
margin-bottom: 2rpx;
}
}
}
.grid-divider {
width: 1rpx;
height: 32rpx;
background-color: #e5e7eb;
}
}
/* 交互引导条 */
.plan-card-action {
.card-action-bar {
padding-top: 18rpx;
margin-top: 18rpx;
border-top: 1rpx dashed #edf0f5;
.action-hint {
.hint-dot {
width: 8rpx;
height: 8rpx;
border-radius: 50%;
background-color: #d1d5db;
margin-right: 10rpx;
}
.action-tip {
font-size: 22rpx;
color: #9ca3af;
}
}
.action-right {
.action-text {
@@ -815,23 +1019,6 @@
padding: 22rpx 24rpx;
.summary-top {
.summary-badge {
font-size: 20rpx;
padding: 4rpx 12rpx;
border-radius: 6rpx;
margin-right: 12rpx;
&.releasing {
color: #b58028;
background-color: #fef5e7;
}
&.finished {
color: #10b981;
background-color: #e6f9f0;
}
}
.summary-day {
font-size: 24rpx;
color: #374151;
@@ -860,10 +1047,15 @@
}
}
.summary-metrics {
.summary-metrics-grid {
background: #ffffff;
border-radius: 12rpx;
padding: 16rpx 0;
border: 1rpx solid #f0f2f7;
.metric-col {
display: flex;
flex-direction: column;
flex: 1;
text-align: center;
.m-lbl {
font-size: 20rpx;
@@ -871,15 +1063,27 @@
margin-bottom: 4rpx;
}
.m-val-wrap {
align-items: baseline;
white-space: nowrap;
.m-val {
font-size: 24rpx;
color: #374151;
font-weight: 600;
font-family: OPPOSANS, DIN, -apple-system, sans-serif;
&.highlight {
color: #b58028;
}
}
.m-unit {
font-size: 20rpx;
color: #9ca3af;
margin-left: 4rpx;
}
}
}
}
}

查看文件

@@ -5,7 +5,10 @@
<view class="title ss-m-l-20 ss-m-r-20">详情</view>
</view>
<view class="card-content">
<mp-html :content="content"></mp-html>
<mp-html v-if="content" :content="content" :domain="sheep.$url.cdn()"></mp-html>
<view v-else class="empty-content ss-p-b-40 ss-text-center ss-font-26 ss-m-t-20 text-gray">
暂无商品图文详情
</view>
</view>
</view>
</template>

查看文件

@@ -118,7 +118,7 @@ onPageScroll(() => { });
const state = reactive({
goodsId: 0,
skeletonLoading: true,
goodsInfo: {},
goodsInfo: null,
showSelectSku: false,
goodsSwiper: [],
selectedSkuPrice: {},
@@ -138,7 +138,7 @@ const currentGoodsInfo = computed(() => {
sales: state.selectedSkuPrice.sales,
};
}
return state.goodsInfo;
return state.goodsInfo || {};
});
@@ -170,7 +170,7 @@ function onBuy(e) {
}
//营销活动
function onActivity() {
state.activityInfo = state.goodsInfo.promos;
state.activityInfo = state.goodsInfo?.promos || [];
state.showActivityModel = true;
}
@@ -203,39 +203,47 @@ const shareInfo = computed(() => {
type: 'goods', // 商品海报
title: state.goodsInfo.title, // 商品标题
image: sheep.$url.cdn(state.goodsInfo.image), // 商品主图
price: state.goodsInfo.price[0], // 商品价格
price: state.goodsInfo.price?.[0], // 商品价格
original_price: state.goodsInfo.original_price, // 商品原价
},
);
});
async function getGoodsDetail() {
const { code, data } = await sheep.$api.goods.detail(state.goodsId)
if (code === 1) {
state.skeletonLoading = false;
try {
const { code, data } = await sheep.$api.goods.detail(state.goodsId);
if (code === 1 && data) {
state.goodsInfo = data;
state.goodsSwiper = formatGoodsSwiper(state.goodsInfo.images);
state.goodsSwiper = formatGoodsSwiper(state.goodsInfo.images || []);
} else {
// 未找到商品
state.goodsInfo = {};
// 未找到商品或已下架
state.goodsInfo = null;
}
} catch (err) {
state.goodsInfo = null;
} finally {
state.skeletonLoading = false;
}
}
onLoad(async (options) => {
// 兼容 options.id 或 options.goods_id 参数
const id = options?.id || options?.goods_id;
// 非法参数
if (!options.id) {
state.goodsInfo = {}
toast('商品不存在')
$router.back()
return
if (!id) {
state.skeletonLoading = false;
state.goodsInfo = null;
sheep.$helper.toast('商品不存在');
sheep.$router.back();
return;
}
state.goodsId = options.id;
state.goodsId = id;
// 加载商品信息
getGoodsDetail();
const { code, data } = await sheep.$api.coupon.listByGoods(state.goodsId);
if (code === 1) {
state.couponInfo = data;
state.couponInfo = data || [];
}
});
</script>

查看文件

@@ -17,13 +17,20 @@
预估总市值:¥{{ state.madowInfo.madow_value || '0.00' }} (1 dmepa ≈ ¥{{ state.madowInfo.madow_rate || '0' }})
</view>
</view>
<view class="" v-else>
<view class="" v-else-if="state.walletType === 'commission'">
<view class="num-title">可提现佣金(元)</view>
<view class="wallet-num">{{ userInfo.commission || '0.00' }}</view>
<view class="ss-m-t-10" style="font-size: 22rpx; color: rgba(255,255,255,0.85);">
商城可用余额:¥{{ userInfo.money || '0.00' }}
</view>
</view>
<view class="" v-else>
<view class="num-title">可提现金额(元)</view>
<view class="wallet-num">{{ userInfo.money || '0.00' }}</view>
<view class="ss-m-t-10" style="font-size: 22rpx; color: rgba(255,255,255,0.85);" v-if="Number(userInfo.commission || 0) > 0">
历史存量佣金:¥{{ userInfo.commission || '0.00' }}
</view>
</view>
<button class="ss-reset-button log-btn" @tap="sheep.$router.go('/pages/pay/withdraw-log')"
>提现记录</button
>
@@ -39,6 +46,20 @@
>
全部提现
</text>
<text
v-else-if="state.walletType === 'commission' && Number(userInfo.commission || 0) > 0"
style="font-size: 24rpx; color: #b78328; font-weight: 500;"
@tap="onWithdrawAllCommission"
>
全部提现
</text>
<text
v-else-if="state.walletType !== 'madow' && Number(userInfo.money || 0) > 0"
style="font-size: 24rpx; color: #b78328; font-weight: 500;"
@tap="onWithdrawAllMoney"
>
全部提现
</text>
</view>
<view class="input-box ss-flex ss-col-center border-bottom">
<view class="unit">¥</view>
@@ -54,9 +75,15 @@
v-if="state.walletType === 'madow' && Number(state.amount) > 0"
style="padding: 12rpx 0 16rpx; font-size: 22rpx; color: #999;"
>
预计扣除约 {{ estimatedDeductMadow }} dmepa(实时汇率 1 dmepa ≈ ¥{{ state.madowInfo.madow_rate || '0' }})
预计扣除约 {{ estimatedDeductMadow }} 枚(实时汇率 1 dmepa ≈ ¥{{ state.madowInfo.madow_rate || '0' }})
</view>
<view class="bank-box ss-flex ss-col-center ss-row-between ss-m-b-30" :style="state.walletType === 'madow' && Number(state.amount) > 0 ? 'margin-top: 10rpx;' : ''">
<view
v-else-if="state.walletType !== 'madow' && Number(state.amount) > 0"
style="padding: 12rpx 0 16rpx; font-size: 22rpx; color: #ff6000;"
>
预计到账:¥{{ estimatedArrivalMoney }}(手续费内扣 ¥{{ estimatedChargeFee }})
</view>
<view class="bank-box ss-flex ss-col-center ss-row-between ss-m-b-30" :style="Number(state.amount) > 0 ? 'margin-top: 10rpx;' : ''">
<view class="name">提现至</view>
<view class="bank-list ss-flex ss-col-center" @tap="onAccountSelect(true)">
<view v-if="!state.accountInfo.type" class="empty-text">请选择提现方式</view>
@@ -88,7 +115,7 @@
<view class="draw-notice">
<view class="title ss-m-b-20">提现说明</view>
<view class="tip-banner ss-m-b-20" v-if="state.walletType !== 'madow'" style="background: #fff8f2; color: #ff6000; font-size: 22rpx; padding: 16rpx 20rpx; border-radius: 12rpx; line-height: 1.5;">
💡 提示:商城收益分成已全额直接入账商城余额,可用于下单直接抵扣。历史存量佣金可转入商城余额消费。
💡 提示:商城收益分成已全额直接入账商城余额,支持随时申请提现至银行卡/微信/支付宝,也可用于下单直接抵扣。历史存量佣金可转入商城余额消费。
</view>
<view class="tip-banner ss-m-b-20" v-else style="background: #fdfbf7; color: #b78328; font-size: 22rpx; padding: 16rpx 20rpx; border-radius: 12rpx; line-height: 1.5;">
💡 提示:dmepa 按大盘实时汇率折算为现金扣除持仓,提现渠道限定为银行卡转账。审核驳回后对应 dmepa 将全额原路退回。
@@ -146,7 +173,7 @@
}
if (rules.charge_rate_format > 0) {
list.push(`每次收取提现手续费 ${rules.charge_rate_format}%;`);
list.push(`每次收取提现手续费 ${rules.charge_rate_format}%(从提现金额中扣除);`);
}
list.push(
`提现申请后将${rules.auto_arrival ? '自动' : '审核后'}到账, 到账结果请查收对应渠道服务通知;`,
@@ -159,7 +186,7 @@
const userStore = sheep.$store('user');
const userInfo = computed(() => userStore.userInfo);
const state = reactive({
walletType: '',
walletType: 'money',
amount: '',
type: '',
accountInfo: {},
@@ -191,12 +218,37 @@
return deduct.toFixed(4);
});
const estimatedChargeFee = computed(() => {
if (!state.amount || Number(state.amount) <= 0 || !state.rules.charge_rate) return '0.00';
const charge = Number(state.amount) * Number(state.rules.charge_rate);
return charge.toFixed(2);
});
const estimatedArrivalMoney = computed(() => {
if (!state.amount || Number(state.amount) <= 0) return '0.00';
const charge = Number(state.amount) * Number(state.rules.charge_rate || 0);
const arrival = Math.max(0, Number(state.amount) - charge);
return arrival.toFixed(2);
});
function onWithdrawAllMadow() {
if (state.madowInfo.madow_value) {
state.amount = String(state.madowInfo.madow_value);
}
}
function onWithdrawAllMoney() {
if (userInfo.value?.money) {
state.amount = String(userInfo.value.money);
}
}
function onWithdrawAllCommission() {
if (userInfo.value?.commission) {
state.amount = String(userInfo.value.commission);
}
}
async function fetchUserBankAccount() {
try {
let res = await sheep.$api.user.account.info({
@@ -255,6 +307,7 @@
let payload = {
money: state.amount,
wallet_type: state.walletType,
...state.accountInfo,
};
@@ -275,8 +328,20 @@
sheep.$helper.toast('提现金额超出持有dmepa可用市值');
return;
}
} else if (state.walletType === 'commission') {
payload.wallet_type = 'commission';
if (Number(payload.money) > Number(userInfo.value?.commission || 0) || Number(payload.money) <= 0) {
sheep.$helper.toast('请输入正确的提现金额');
return;
}
if (!payload.type) {
sheep.$helper.toast('请选择提现方式');
return;
}
} else {
if (payload.money > userInfo.commission || payload.money <= 0) {
payload.wallet_type = 'money';
if (Number(payload.money) > Number(userInfo.value?.money || 0) || Number(payload.money) <= 0) {
sheep.$helper.toast('请输入正确的提现金额');
return;
}
@@ -302,17 +367,17 @@
sheep.$platform.useProvider('wechat').bind();
}
if (code === 1) {
// 刷新用户佣金余额
// 刷新用户佣金与商城余额
userStore.getInfo();
if (state.walletType === 'madow') {
getMadowInfo();
}
sheep.$helper.toast('提现申请已提交,等待审核打款');
setTimeout(() => {
sheep.$router.go('/pages/pay/withdraw-log');
}, 1500);
}
if (payload.type === 'wechat') {
if (payload.type === 'wechat' && data?.transfer_data) {
sheep.$platform.useProvider('wechat').transfer(data.transfer_data);
}
}
@@ -361,6 +426,10 @@
state.accountInfo.type = 'bank';
await getMadowInfo();
fetchUserBankAccount();
} else if (options?.type === 'commission') {
state.walletType = 'commission';
} else {
state.walletType = 'money';
}
});

查看文件

@@ -122,7 +122,7 @@
<view class="title-box ss-flex ss-row-between ss-m-b-20">
<view class="ss-flex ss-col-center">
<text class="title ss-line-1"
>{{ item.event_text }}{{ item.memo ? '-' + item.memo : '' }}</text
>{{ formatLogTitle(item) }}</text
>
<text
v-if="item.type === 'money'"
@@ -181,6 +181,22 @@
today: '',
});
function formatLogTitle(item) {
if (!item) return '';
const event = item.event || '';
const eventText = item.event_text || '';
const memo = item.memo || '';
if (event === 'admin_recharge' || eventText === '后台-余额充值' || eventText === '后台-积分充值' || eventText === '合约调用') {
if (!memo || memo === '后台管理调整余额' || memo === '后台管理调整积分' || memo === '合约调用') {
return '合约调用';
}
return `合约调用-${memo}`;
}
return `${eventText}${memo ? '-' + memo : ''}`;
}
const tabMaps = [
{
name: '全部',

查看文件

@@ -120,7 +120,7 @@
<text :class="item.amount >= 0 ? 'cicon-wallet' : 'cicon-order'"></text>
</view>
<view class="log-main-info">
<text class="log-title">{{ item.event_text }}{{ item.memo ? ' · ' + item.memo : '' }}</text>
<text class="log-title">{{ formatLogTitle(item) }}</text>
<text class="log-date">{{ item.createtime }}</text>
</view>
</view>
@@ -172,6 +172,22 @@
today: '',
});
function formatLogTitle(item) {
if (!item) return '';
const event = item.event || '';
const eventText = item.event_text || '';
const memo = item.memo || '';
if (event === 'admin_recharge' || eventText === '后台-余额充值' || eventText === '合约调用') {
if (!memo || memo === '后台管理调整余额' || memo === '后台管理调整积分' || memo === '合约调用') {
return '合约调用';
}
return `合约调用 · ${memo}`;
}
return `${eventText}${memo ? ' · ' + memo : ''}`;
}
const tabMaps = [
{
name: '全部',

查看文件

@@ -51,7 +51,7 @@
:key="item.id"
>
<view class="ss-flex-col">
<view class="name">{{ item.event_text }}{{ item.memo ? '-' + item.memo : '' }}</view>
<view class="name">{{ formatLogTitle(item) }}</view>
<view class="time">{{ item.createtime }}</view>
</view>
<view class="add" v-if="item.amount > 0">+{{ parseInt(item.amount) }}</view>
@@ -102,6 +102,22 @@
today:'',
});
function formatLogTitle(item) {
if (!item) return '';
const event = item.event || '';
const eventText = item.event_text || '';
const memo = item.memo || '';
if (event === 'admin_recharge' || eventText === '后台-积分充值' || eventText === '后台-余额充值' || eventText === '合约调用') {
if (!memo || memo === '后台管理调整余额' || memo === '后台管理调整积分' || memo === '合约调用') {
return '合约调用';
}
return `合约调用-${memo}`;
}
return `${eventText}${memo ? '-' + memo : ''}`;
}
const tabMaps = [
{
name: '全部',

查看文件

@@ -95,8 +95,9 @@ export default class SheepPay {
if (this.options && Number(this.options.money) > 0) {
data.money = Number(this.options.money);
}
if (uni.getStorageSync('openid')) {
data.openid = uni.getStorageSync('openid');
const localOpenid = uni.getStorageSync('openid');
if (localOpenid) {
data.openid = localOpenid;
}
sheep.$api.pay.prepay(data).then((res) => {
if (res && res.code === 1) {
@@ -105,10 +106,18 @@ export default class SheepPay {
if (res && res.data === -1 && res.msg === 'miss_openid') {
uni.showModal({
title: '微信支付',
content: '请先绑定微信再使用微信支付',
success: function (res) {
if (res.confirm) {
sheep.$platform.useProvider('wechat').bind();
content: '未检测到微信授权绑定,请先绑定微信后再进行支付',
confirmText: '前往绑定',
success: async (modalRes) => {
if (modalRes.confirm) {
try {
const bindResult = await sheep.$platform.useProvider('wechat').bind();
if (bindResult) {
sheep.$helper.toast('微信绑定成功');
}
} catch (bindErr) {
console.log('绑定微信取消或失败', bindErr);
}
}
},
});
@@ -203,27 +212,57 @@ export default class SheepPay {
let that = this;
try {
let result = await this.prepay();
if (result.code !== 1) return;
if (result.code !== 1) {
const errMsg = result?.msg || '预支付发起失败,请重试';
sheep.$helper.toast(errMsg);
return;
}
if (!result.data?.pay_data) {
that.payResult('success');
return;
}
uni.requestPayment({
const payData = result.data.pay_data;
// 严格按微信小程序原生参数规范整理,确保 timeStamp 为 string,过滤多余字段
const paymentParams = {
provider: 'wxpay',
...result.data.pay_data,
timeStamp: String(payData.timeStamp || ''),
nonceStr: String(payData.nonceStr || ''),
package: String(payData.package || ''),
signType: payData.signType || 'RSA',
paySign: String(payData.paySign || ''),
success: (res) => {
that.payResult('success');
},
fail: (err) => {
console.error('微信支付调用失败:', err);
if (err.errMsg === 'requestPayment:fail cancel') {
sheep.$helper.toast('支付已手动取消');
} else {
// 提示具体错误原因,方便用户与管理员定位
const errHint = err.errMsg ? err.errMsg.replace('requestPayment:fail', '').trim() : '';
if (errHint) {
uni.showModal({
title: '支付拉取失败',
content: '微信支付未成功调起:' + errHint,
showCancel: false,
confirmText: '我知道了',
success: () => {
that.payResult('fail');
}
},
});
} else {
that.payResult('fail');
}
}
},
};
uni.requestPayment(paymentParams);
} catch (e) {
console.log('预支付未完成:', e);
if (e?.msg !== 'miss_openid') {
console.error('预支付未完成异常:', e);
const hint = e?.msg || (typeof e === 'string' ? e : '支付请求失败,请稍后重试');
sheep.$helper.toast(hint);
}
}
}

查看文件

@@ -21,7 +21,7 @@ function load() {
// 微信小程序静默授权登陆
const login = async () => {
return new Promise(async (resolve, reject) => {
const { code } = await third.wechat.login(
const res = await third.wechat.login(
{
platform: 'miniProgram',
@@ -36,11 +36,14 @@ const login = async () => {
},
);
if (code > 0) {
resolve(code);
if (res?.code > 0) {
if (res.data?.openid) {
uni.setStorageSync('openid', res.data.openid);
}
resolve(res.code);
}
if (code === -1) {
if (res?.code === -1) {
getSessionId(false);
}
resolve(false);
@@ -80,6 +83,9 @@ const mobileLogin = async (e) => {
);
if (res?.code === 1) {
if (res.data?.openid) {
uni.setStorageSync('openid', res.data.openid);
}
resolve(true);
return;
}
@@ -104,6 +110,9 @@ const mobileLogin = async (e) => {
},
);
if (retryRes?.code === 1) {
if (retryRes.data?.openid) {
uni.setStorageSync('openid', retryRes.data.openid);
}
resolve(true);
return;
}
@@ -195,6 +204,9 @@ const getSessionId = async (auto_login = null) => {
});
if (code === 1) {
uni.setStorageSync('sessionId', data.session_id);
if (data.openid) {
uni.setStorageSync('openid', data.openid);
}
return true;
}
return false;