feat(withdraw): 后台支持提现总开关与方式过滤并升级钱包与提现页面体验

这个提交包含在:
chen
2026-09-19 15:41:25 +08:00
父节点 efab61498d
当前提交 3868becc63
共有 17 个文件被更改,包括 1872 次插入 和 116 次删除

查看文件

@@ -69,6 +69,7 @@ class Index extends Common
'bind_mobile' => $platformConfig['bind_mobile'] ?? 0,
'payment' => $platformConfig['payment']['methods'],
'recharge_payment' => sheep_config('shop.recharge_withdraw.recharge.methods'), // 充值支持的支付方式
'withdraw_status' => (bool)(intval(sheep_config('shop.recharge_withdraw.withdraw.status', !empty(sheep_config('shop.recharge_withdraw.withdraw.methods', [])) ? 1 : 0)) && !empty(sheep_config('shop.recharge_withdraw.withdraw.methods', []))),
'share' => $platformConfig['share'],
],
'template' => $template,

查看文件

@@ -13,6 +13,10 @@ use app\admin\model\shopro\ThirdOauth;
use addons\shopro\service\Wallet as WalletService;
use addons\shopro\library\pay\PayService;
use app\admin\model\shopro\user\User;
use addons\shopro\service\partner\PartnerConfig;
use addons\shopro\service\partner\RateService;
use addons\shopro\service\partner\PartnerRewardService;
use app\admin\model\shopro\commission\Agent as AgentModel;
class Withdraw
{
@@ -33,6 +37,10 @@ class Withdraw
// 提现规则
$config = sheep_config('shop.recharge_withdraw.withdraw');
// 提现开关与是否允许提现综合状态
$config['status'] = isset($config['status']) ? intval($config['status']) : (!empty($config['methods']) ? 1 : 0);
$config['is_withdraw_open'] = (bool)($config['status'] === 1 && !empty($config['methods']));
$config['min_amount'] = $config['min_amount'] == 0 ? $config['min_amount'] : number_format(floatval($config['min_amount']), 2, '.', '');
$config['max_amount'] = $config['max_amount'] == 0 ? $config['max_amount'] : number_format(floatval($config['max_amount']), 2, '.', '');
$config['charge_rate_format'] = round(floatval($config['charge_rate']), 1); // 1 位小数
@@ -45,10 +53,15 @@ class Withdraw
// 发起提现申请
public function apply($params)
{
$type = $params['type'];
$walletType = $params['wallet_type'] ?? 'commission';
$type = $params['type'] ?? '';
$money = $params['money'] ?? 0;
$money = (string)$money;
if ($walletType === 'madow') {
return $this->applyMadow($params, $type, $money);
}
// 手续费
$charge = bcmul($money, (string)$this->config['charge_rate'], 2);
@@ -83,6 +96,104 @@ class Withdraw
return $withdraw;
}
// 发起 dmepa 提现申请
protected function applyMadow($params, $type, $money)
{
if (!PartnerConfig::isMadowWithdrawEnabled()) {
throw new ShoproException('dmepa提现功能暂未开启');
}
if ($type !== 'bank') {
throw new ShoproException('dmepa提现仅支持银行卡转账');
}
if (floatval($money) <= 0) {
throw new ShoproException('请输入正确的提现金额');
}
$minAmount = PartnerConfig::madowWithdrawMin();
if ($minAmount > 0 && floatval($money) < $minAmount) {
throw new ShoproException('提现金额不能少于 ' . $minAmount . '元');
}
$rate = (new RateService())->currentRate();
if ($rate <= 0) {
throw new ShoproException('当前dmepa暂无有效市值汇率,无法发起提现');
}
// 计算需扣除的 dmepa = 金额 / 汇率
$rateStr = number_format($rate, 12, '.', '');
$deductMadow = function_exists('bcdiv') ? bcdiv($money, $rateStr, 4) : number_format(floatval($money) / $rate, 4, '.', '');
if (floatval($deductMadow) <= 0) {
throw new ShoproException('折算dmepa数量不足');
}
// 检查合伙人资产
$agent = AgentModel::where('user_id', $this->user->id)->lock(true)->find();
if (!$agent) {
throw new ShoproException('未找到您的合伙资产账户');
}
$currentMadow = strval($agent->madow ?? 0);
if (bccomp($currentMadow, $deductMadow, 4) < 0) {
throw new ShoproException('持仓dmepa不足,无法提现');
}
// 校验银行卡信息
$withdrawInfo = $this->getAccountInfo($type, $params);
$rateClean = rtrim(rtrim(number_format((float)$rate, 12, '.', ''), '0'), '.');
$moneyClean = number_format((float)$money, 2, '.', '');
$withdrawInfo['提现资产'] = 'DMEPA';
$withdrawInfo['扣除DMEPA'] = $deductMadow . ' DMEPA';
$withdrawInfo['市值汇率'] = '1 DMEPA ≈ ' . $rateClean . ' 元';
$withdrawInfo['换算市值'] = $moneyClean . ' 元';
$withdrawInfo['wallet_type'] = 'madow';
$withdrawInfo['madow_amount'] = $deductMadow;
$withdrawInfo['rate'] = $rate;
// 扣减 dmepa
$agent->setDec('madow', $deductMadow);
// 创建提现记录
$withdraw = new WithdrawModel();
$withdraw->user_id = $this->user->id;
$withdraw->amount = $money;
$withdraw->charge_fee = 0;
$withdraw->charge_rate = 0;
$withdraw->withdraw_sn = get_sn($this->user->id, 'W');
$withdraw->withdraw_type = 'bank';
$withdraw->withdraw_info = $withdrawInfo;
$withdraw->status = 0;
$withdraw->platform = request()->header('platform');
$withdraw->save();
// 记录合伙人流水
PartnerRewardService::write([
'order_id' => -$withdraw->id,
'buyer_id' => $this->user->id,
'user_id' => $this->user->id,
'role' => 'partner',
'reward_type' => 'madow_withdraw',
'amount' => -$money,
'madow' => -$deductMadow,
'base_amount' => $money,
'rate' => $rate,
'ext' => [
'withdraw_sn' => $withdraw->withdraw_sn,
'withdraw_id' => $withdraw->id,
'deduct_madow' => $deductMadow,
'cny_amount' => $money,
'rate' => $rate,
],
'settle_date' => date('Y-m-d'),
'status' => 1,
]);
$this->handleLog($withdraw, '用户发起dmepa提现申请(扣除 ' . $deductMadow . ' dmepa)', $this->user);
return $withdraw;
}
// 继续提现
public function retry($params)
@@ -256,6 +367,51 @@ class Withdraw
$withdraw->status = -1;
$withdraw->save();
if (!$this->user && !empty($withdraw->user_id)) {
$this->user = User::get($withdraw->user_id);
}
$userId = $this->user ? $this->user->id : intval($withdraw->user_id);
$info = $withdraw->withdraw_info;
if (is_string($info)) {
$info = json_decode($info, true) ?: [];
}
$isMadow = (isset($info['wallet_type']) && $info['wallet_type'] === 'madow')
|| (isset($info['提现资产']) && strcasecmp($info['提现资产'], 'dmepa') === 0);
if ($isMadow) {
$madowRefund = $info['madow_amount'] ?? ($info['扣除DMEPA'] ?? ($info['扣除dmepa'] ?? 0));
if (is_string($madowRefund)) {
$madowRefund = trim(str_ireplace(['dmepa', ' '], '', $madowRefund));
}
if (floatval($madowRefund) > 0) {
$agent = AgentModel::where('user_id', $userId)->lock(true)->find();
if ($agent) {
$agent->setInc('madow', $madowRefund);
}
PartnerRewardService::write([
'order_id' => -$withdraw->id,
'buyer_id' => $userId,
'user_id' => $userId,
'role' => 'partner',
'reward_type' => 'madow_withdraw_refund',
'amount' => $withdraw->amount,
'madow' => $madowRefund,
'base_amount' => $withdraw->amount,
'rate' => $info['rate'] ?? 0,
'ext' => [
'withdraw_sn' => $withdraw->withdraw_sn,
'withdraw_id' => $withdraw->id,
'refund_madow' => $madowRefund,
'refuse_msg' => $refuse_msg,
],
'settle_date' => date('Y-m-d'),
'status' => 1,
]);
}
return $this->handleLog($withdraw, '拒绝:' . $refuse_msg . ',已退还扣除的 ' . $madowRefund . ' dmepa');
}
// 退回用户佣金
WalletService::change($this->user, 'commission', bcadd($withdraw->charge_fee, $withdraw->amount, 2), 'withdraw_error', [
'withdraw_id' => $withdraw->id,
@@ -277,6 +433,51 @@ class Withdraw
$withdraw->status = $status;
$withdraw->save();
if (!$this->user && !empty($withdraw->user_id)) {
$this->user = User::get($withdraw->user_id);
}
$userId = $this->user ? $this->user->id : intval($withdraw->user_id);
$info = $withdraw->withdraw_info;
if (is_string($info)) {
$info = json_decode($info, true) ?: [];
}
$isMadow = (isset($info['wallet_type']) && $info['wallet_type'] === 'madow')
|| (isset($info['提现资产']) && strcasecmp($info['提现资产'], 'dmepa') === 0);
if ($isMadow) {
$madowRefund = $info['madow_amount'] ?? ($info['扣除DMEPA'] ?? ($info['扣除dmepa'] ?? 0));
if (is_string($madowRefund)) {
$madowRefund = trim(str_ireplace(['dmepa', ' '], '', $madowRefund));
}
if (floatval($madowRefund) > 0) {
$agent = AgentModel::where('user_id', $userId)->lock(true)->find();
if ($agent) {
$agent->setInc('madow', $madowRefund);
}
PartnerRewardService::write([
'order_id' => -$withdraw->id,
'buyer_id' => $userId,
'user_id' => $userId,
'role' => 'partner',
'reward_type' => 'madow_withdraw_refund',
'amount' => $withdraw->amount,
'madow' => $madowRefund,
'base_amount' => $withdraw->amount,
'rate' => $info['rate'] ?? 0,
'ext' => [
'withdraw_sn' => $withdraw->withdraw_sn,
'withdraw_id' => $withdraw->id,
'refund_madow' => $madowRefund,
'fail_msg' => $refuse_msg,
],
'settle_date' => date('Y-m-d'),
'status' => 1,
]);
}
return $this->handleLog($withdraw, '提现失败,已退还 ' . $madowRefund . ' dmepa:' . $refuse_msg);
}
// 退回用户佣金
WalletService::change($this->user, 'commission', bcadd($withdraw->charge_fee, $withdraw->amount, 2), 'withdraw_error', [
'withdraw_id' => $withdraw->id,
@@ -412,6 +613,14 @@ class Withdraw
// 提现条件检查
private function checkApply($type, $money, $charge)
{
if (isset($this->config['status']) && intval($this->config['status']) === 0) {
throw new ShoproException('系统提现功能已关闭');
}
if (empty($this->config['methods'])) {
throw new ShoproException('暂未开启任何提现方式');
}
if (!in_array($type, $this->config['methods'])) {
throw new ShoproException('暂不支持该提现方式');
}

查看文件

@@ -195,21 +195,39 @@ class Config extends Common
'partner_mode', 'partner_enable', 'partner_count_type', 'partner_active_days',
'partner_chain_max_depth', 'partner_push3_amounts', 'partner_madow_mode',
'partner_show_madow_rate',
'partner_madow_base_rate', 'partner_madow_step_orders', 'partner_voucher_times',
'partner_voucher_days', 'partner_maker_bonus', 'partner_maker_peer_rate',
'partner_madow_base_rate', 'partner_madow_step_orders',
'partner_madow_rule_title', 'partner_madow_rules',
'partner_madow_withdraw_enable', 'partner_madow_withdraw_min',
'partner_voucher_times', 'partner_voucher_days',
'partner_voucher_rule_title', 'partner_voucher_rules',
'partner_maker_bonus', 'partner_maker_peer_rate',
'partner_maker_overtaken_rate', 'partner_team_total', 'partner_venue_split',
'partner_county_full', 'partner_venue_share_per_order', 'partner_county_share_per_order',
'partner_venue_peer_rate', 'partner_county_peer_rate',
'partner_role_name_taster', 'partner_role_name_maker', 'partner_role_name_venue',
'partner_role_name_county', 'partner_role_name_partner',
];
$ruleDefaults = [
'partner_madow_rule_title' => '市值与增值规则',
'partner_madow_rules' => [
['title' => 'dmepa获取与核算', 'desc' => '平台合伙人与创客达成考核或完成订单后,系统将于次日 00:30 起统一参与每日dmepa加权核算并自动分发。'],
['title' => '市值动态增值', 'desc' => '市值随全平台订单增加而上浮增值。全网完成订单总量越多,dmepa单价所对应的市值越高,共享生态红利。'],
['title' => '价值说明与权益', 'desc' => '市值仅作为平台数字生态资产价值参考展示,不可直接兑换现金提现。具体权益按平台股东与合伙人章程执行。'],
],
'partner_voucher_rule_title' => '兑换券释放与使用规则',
'partner_voucher_rules' => [
['title' => '赠送与释放天数', 'desc' => '会员在平台完成符合条件的商城订单后,系统将按订单金额(默认 1.5 倍)生成兑换券计划,分 365 天按日线性释放。'],
['title' => '每日自动到账', 'desc' => '系统将于次日 00:30 起统一执行日结核算,按期释放的兑换券将自动计入您的「可用兑换券」账户,无需手动领取。'],
['title' => '全场购物直接抵扣', 'desc' => '可用兑换券可在平台商城下单结算时作为现金抵扣使用,尊享优惠抵扣权益。'],
],
];
if ('GET' === $this->request->method()) {
$all = ShoproConfig::getConfigs('shop.commission', false) ?: [];
$configs = [];
foreach ($keys as $key) {
$configs[$key] = $all[$key] ?? null;
}
$defaults = [
$defaults = array_merge([
'partner_mode' => 1,
'partner_enable' => 1,
'partner_count_type' => 'order',
@@ -218,6 +236,8 @@ class Config extends Common
'partner_push3_amounts' => [398, 400, 600],
'partner_madow_mode' => 'madow',
'partner_show_madow_rate' => 1,
'partner_madow_withdraw_enable' => 1,
'partner_madow_withdraw_min' => 1,
'partner_madow_base_rate' => '0.00000075',
'partner_madow_step_orders' => 1,
'partner_voucher_times' => 1.5,
@@ -237,13 +257,23 @@ class Config extends Common
'partner_role_name_venue' => '酒道馆',
'partner_role_name_county' => '区县代理',
'partner_role_name_partner' => '合伙股东',
];
], $ruleDefaults);
foreach ($defaults as $key => $value) {
if ($configs[$key] === null || $configs[$key] === '') {
$configs[$key] = $value;
}
}
$configs['partner_show_madow_rate'] = isset($configs['partner_show_madow_rate']) ? intval($configs['partner_show_madow_rate']) : 1;
$configs['partner_madow_withdraw_enable'] = isset($configs['partner_madow_withdraw_enable']) ? intval($configs['partner_madow_withdraw_enable']) : 1;
$configs['partner_madow_withdraw_min'] = isset($configs['partner_madow_withdraw_min']) ? floatval($configs['partner_madow_withdraw_min']) : 1.0;
foreach (['partner_madow_rules', 'partner_voucher_rules'] as $ruleKey) {
if (isset($configs[$ruleKey]) && is_string($configs[$ruleKey])) {
$decoded = json_decode($configs[$ruleKey], true);
if (is_array($decoded)) {
$configs[$ruleKey] = $decoded;
}
}
}
} elseif ('POST' === $this->request->method()) {
check_env('commission');
$params = $this->request->param();
@@ -256,6 +286,12 @@ class Config extends Common
if (isset($save['partner_show_madow_rate'])) {
$save['partner_show_madow_rate'] = intval($save['partner_show_madow_rate']);
}
if (isset($save['partner_madow_withdraw_enable'])) {
$save['partner_madow_withdraw_enable'] = intval($save['partner_madow_withdraw_enable']);
}
if (isset($save['partner_madow_withdraw_min'])) {
$save['partner_madow_withdraw_min'] = max(0.0, floatval($save['partner_madow_withdraw_min']));
}
// 确保数据库存在 partner_show_madow_rate 配置行
$existConfig = ShoproConfig::where('code', 'shop.commission.partner_show_madow_rate')->find();
if (!$existConfig) {
@@ -269,6 +305,29 @@ class Config extends Common
'weigh' => 86
]);
}
// 确保数据库存在规则条目等必要配置行
$ensureConfigs = [
'shop.commission.partner_madow_withdraw_enable' => ['name' => '开启dmepa提现', 'desc' => '1=开启 0=关闭', 'type' => 'int', 'value' => '1', 'weigh' => 86],
'shop.commission.partner_madow_withdraw_min' => ['name' => 'dmepa最低提现金额', 'desc' => 'dmepa最低提现金额(元)', 'type' => 'number', 'value' => '1', 'weigh' => 86],
'shop.commission.partner_madow_rule_title' => ['name' => 'dmepa规则弹窗标题', 'desc' => '我的dmepa市值规则弹窗标题', 'type' => 'string', 'value' => '市值与增值规则', 'weigh' => 86],
'shop.commission.partner_madow_rules' => ['name' => 'dmepa规则说明条目', 'desc' => '我的dmepa市值规则条目', 'type' => 'array', 'value' => json_encode($ruleDefaults['partner_madow_rules'], JSON_UNESCAPED_UNICODE), 'weigh' => 86],
'shop.commission.partner_voucher_rule_title' => ['name' => '兑换券规则弹窗标题', 'desc' => '兑换券规则弹窗标题', 'type' => 'string', 'value' => '兑换券释放与使用规则', 'weigh' => 89],
'shop.commission.partner_voucher_rules' => ['name' => '兑换券规则说明条目', 'desc' => '兑换券规则条目', 'type' => 'array', 'value' => json_encode($ruleDefaults['partner_voucher_rules'], JSON_UNESCAPED_UNICODE), 'weigh' => 89],
];
foreach ($ensureConfigs as $confCode => $confMeta) {
$existConfig = ShoproConfig::where('code', $confCode)->find();
if (!$existConfig) {
ShoproConfig::create([
'code' => $confCode,
'parent_code' => 'shop.commission',
'name' => $confMeta['name'],
'description' => $confMeta['desc'],
'type' => $confMeta['type'],
'value' => $confMeta['value'],
'weigh' => $confMeta['weigh'],
]);
}
}
foreach (['partner_role_name_taster', 'partner_role_name_maker', 'partner_role_name_venue', 'partner_role_name_county', 'partner_role_name_partner'] as $roleKey) {
if (isset($save[$roleKey])) {
$save[$roleKey] = trim((string) $save[$roleKey]);
@@ -346,6 +405,13 @@ class Config extends Common
if ('GET' === $this->request->method()) {
$configs = ShoproConfig::getConfigs('shop.recharge_withdraw', false);
if ($configs && isset($configs['withdraw'])) {
if (!isset($configs['withdraw']['status'])) {
$configs['withdraw']['status'] = !empty($configs['withdraw']['methods']) ? 1 : 0;
} else {
$configs['withdraw']['status'] = intval($configs['withdraw']['status']);
}
}
} elseif ('POST' === $this->request->method()) {
$params = $this->request->param();
if (!isset($params['recharge']['methods'])) {
@@ -357,7 +423,15 @@ class Config extends Common
if (!isset($params['withdraw']['methods'])) {
$params['withdraw']['methods'] = [];
}
if (!isset($params['withdraw']['status'])) {
$params['withdraw']['status'] = 0;
} else {
$params['withdraw']['status'] = intval($params['withdraw']['status']);
}
$configs = ShoproConfig::setConfigs('shop.recharge_withdraw', $params);
if ($configs && isset($configs['withdraw'])) {
$configs['withdraw']['status'] = isset($configs['withdraw']['status']) ? intval($configs['withdraw']['status']) : (!empty($configs['withdraw']['methods']) ? 1 : 0);
}
}
$this->success('操作成功', null, $configs);
}

查看文件

@@ -47,6 +47,9 @@ class Config extends Common
}
$config[self::getShortCode($row)] = $row->value;
}
if ($code === 'shop.recharge_withdraw.withdraw' && !isset($config['status'])) {
$config['status'] = !empty($config['methods']) ? 1 : 0;
}
}
// 设置配置缓存
cache("config:{$code}", $config);
@@ -124,6 +127,19 @@ class Config extends Common
$config->value = $value;
$config->save();
}
} else {
// 自动补齐缺失的提现开关配置项,确保热更新无缝落地
if ($code === 'shop.recharge_withdraw.withdraw.status') {
self::create([
'code' => $code,
'parent_code' => 'shop.recharge_withdraw.withdraw',
'name' => '开启提现',
'type' => 'boolean',
'value' => (string)intval($value),
'rule' => 'required',
'weigh' => 50
]);
}
}
}

查看文件

@@ -16,6 +16,8 @@ class Withdraw extends Common
'status_text',
'charge_rate_format',
'withdraw_info_hidden',
'withdraw_info_display',
'is_madow_withdraw',
'withdraw_type_text',
'wechat_transfer_state_text',
];
@@ -90,9 +92,84 @@ class Withdraw extends Common
return bcmul((string)$value, '100', 1) . '%';
}
public function getIsMadowWithdrawAttr($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'] === 'madow')
|| (isset($info['提现资产']) && strcasecmp($info['提现资产'], 'dmepa') === 0);
}
public function getWithdrawInfoDisplayAttr($value, $data)
{
$info = $this->withdraw_info ?? ($data['withdraw_info'] ?? null);
if (is_string($info)) {
$info = json_decode($info, true) ?: [];
}
if (!is_array($info)) {
return [];
}
$display = [];
$isMadow = (isset($info['wallet_type']) && $info['wallet_type'] === 'madow')
|| (isset($info['提现资产']) && strcasecmp($info['提现资产'], 'dmepa') === 0);
if ($isMadow) {
$display['提现资产'] = 'DMEPA';
// 扣除 DMEPA
$madowAmount = $info['madow_amount'] ?? ($info['扣除DMEPA'] ?? ($info['扣除dmepa'] ?? null));
if ($madowAmount !== null) {
$madowClean = trim(str_ireplace(['dmepa', ' '], '', (string)$madowAmount));
$display['扣除DMEPA'] = $madowClean . ' DMEPA';
}
// 市值汇率
$rate = $info['rate'] ?? ($info['市值汇率'] ?? ($info['折算汇率'] ?? null));
if ($rate !== null) {
if (is_numeric($rate)) {
$rateClean = rtrim(rtrim(number_format((float)$rate, 12, '.', ''), '0'), '.');
$display['市值汇率'] = '1 DMEPA ≈ ' . $rateClean . ' 元';
} else {
$rateStr = (string)$rate;
$rateClean = trim(str_replace(['1 DMEPA ≈', '元/dmepa', '元/DMEPA', '元', ' '], '', $rateStr));
$display['市值汇率'] = '1 DMEPA ≈ ' . $rateClean . ' 元';
}
}
// 换算市值
$marketValue = $info['换算市值'] ?? ($data['amount'] ?? ($this->amount ?? null));
if ($marketValue !== null) {
$valClean = trim(str_replace(['元', ' '], '', (string)$marketValue));
$display['换算市值'] = number_format((float)$valClean, 2, '.', '') . ' 元';
}
}
// 填充银行卡或其它常规账户信息,并过滤英文底层键
$excludeKeys = ['wallet_type', 'madow_amount', 'rate', '提现资产', '扣除DMEPA', '扣除dmepa', '市值汇率', '折算汇率', '换算市值'];
foreach ($info as $k => $v) {
if (!in_array($k, $excludeKeys)) {
$display[$k] = $v;
}
}
return $display;
}
public function getWithdrawInfoHiddenAttr($value, $data)
{
$withdraw_info = $value ?: ($this->withdraw_info ?? null);
if (is_string($withdraw_info)) {
$withdraw_info = json_decode($withdraw_info, true) ?: [];
}
if (!is_array($withdraw_info)) {
return [];
}
foreach ($withdraw_info as $key => &$info) {
if (in_array($key, ['微信用户', '真实姓名'])) {

查看文件

@@ -576,6 +576,21 @@
</template>
<template v-if="form.model.withdraw">
<div class="sa-title is-line">提现配置</div>
<el-form-item label="开启提现">
<div>
<div class="sa-flex">
<el-switch v-model="form.model.withdraw.status" :active-value="1"
:inactive-value="0"></el-switch>
<div class="label-tip"
:class="form.model.withdraw.status == 1?'sa-color--primary':''">
{{form.model.withdraw.status == 1?'开启':'关闭'}}
</div>
</div>
<div class="tip">
<div> 关闭提现后,用户端将隐藏提现入口且禁止提现操作 </div>
</div>
</div>
</el-form-item>
<el-form-item label="提现方式">
<el-checkbox-group v-model="form.model.withdraw.methods">
<el-checkbox label="wechat">到微信零钱</el-checkbox>
@@ -928,10 +943,10 @@
</div>
<div class="tip">默认 398 / 400 / 600,三者之和建议等于主售价</div>
</el-form-item>
<div class="sa-title is-line">马豆</div>
<div class="sa-title is-line">dmepa</div>
<el-form-item label="发放模式">
<el-radio-group v-model="form.model.partner_madow_mode">
<el-radio label="madow">马豆</el-radio>
<el-radio label="madow">dmepa</el-radio>
<el-radio label="cash">现金</el-radio>
</el-radio-group>
</el-form-item>
@@ -943,7 +958,35 @@
</el-form-item>
<el-form-item label="显示马币市值">
<el-switch v-model="form.model.partner_show_madow_rate" :active-value="1" :inactive-value="0"></el-switch>
<div class="tip">开启后,移动端“我的马豆”资产卡片将显示“马币市值 1 马豆 ≈ xxxx 元 生态增值”指示胶囊;关闭则隐藏该胶囊。</div>
<div class="tip">开启后,移动端“我的dmepa”资产卡片将显示“市值 1 dmepa ≈ xxxx 元 生态增值”指示胶囊;关闭则隐藏该胶囊。</div>
</el-form-item>
<el-form-item label="开启dmepa提现">
<el-switch v-model="form.model.partner_madow_withdraw_enable" :active-value="1" :inactive-value="0"></el-switch>
<div class="tip">开启后,移动端“我的dmepa”页面将显示提现按钮,支持持仓dmepa按实时市值提现到银行卡;关闭后隐藏提现入口并禁止提现。</div>
</el-form-item>
<el-form-item label="dmepa最低提现金额">
<el-input class="sa-w-360" v-model="form.model.partner_madow_withdraw_min" type="number" placeholder="1">
<template #append>元</template>
</el-input>
<div class="tip">每次dmepa折合现金提现的最低申请金额(元)</div>
</el-form-item>
<el-form-item label="市值规则弹窗标题">
<el-input class="sa-w-360" v-model="form.model.partner_madow_rule_title" placeholder="市值与增值规则"></el-input>
</el-form-item>
<el-form-item label="市值规则说明条目">
<div style="width: 100%; max-width: 660px;">
<div v-for="(item, index) in form.model.partner_madow_rules" :key="index" style="margin-bottom: 12px; padding: 12px; background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 6px;">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px;">
<div style="display: flex; align-items: center;">
<span style="display: inline-flex; align-items: center; justify-content: center; width: 20px; height: 20px; border-radius: 50%; background: #e0b467; color: #fff; font-size: 12px; font-weight: bold; margin-right: 8px;">{{ index + 1 }}</span>
<el-input style="width: 260px;" v-model="item.title" placeholder="条目标题(如:dmepa获取与核算)" size="small"></el-input>
</div>
<el-button type="danger" link size="small" @click="onDeleteMadowRule(index)">删除</el-button>
</div>
<el-input type="textarea" :rows="2" v-model="item.desc" placeholder="条目详细说明内容..."></el-input>
</div>
<el-button icon="Plus" size="small" @click="onAddMadowRule">添加规则条目</el-button>
</div>
</el-form-item>
<div class="sa-title is-line">兑换券</div>
<el-form-item label="倍数">
@@ -952,6 +995,24 @@
<el-form-item label="释放天数">
<el-input class="sa-w-360" v-model="form.model.partner_voucher_days" type="number"></el-input>
</el-form-item>
<el-form-item label="释放规则弹窗标题">
<el-input class="sa-w-360" v-model="form.model.partner_voucher_rule_title" placeholder="兑换券释放与使用规则"></el-input>
</el-form-item>
<el-form-item label="释放规则说明条目">
<div style="width: 100%; max-width: 660px;">
<div v-for="(item, index) in form.model.partner_voucher_rules" :key="index" style="margin-bottom: 12px; padding: 12px; background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 6px;">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px;">
<div style="display: flex; align-items: center;">
<span style="display: inline-flex; align-items: center; justify-content: center; width: 20px; height: 20px; border-radius: 50%; background: #e0b467; color: #fff; font-size: 12px; font-weight: bold; margin-right: 8px;">{{ index + 1 }}</span>
<el-input style="width: 260px;" v-model="item.title" placeholder="条目标题(如:赠送与释放天数)" size="small"></el-input>
</div>
<el-button type="danger" link size="small" @click="onDeleteVoucherRule(index)">删除</el-button>
</div>
<el-input type="textarea" :rows="2" v-model="item.desc" placeholder="条目详细说明内容..."></el-input>
</div>
<el-button icon="Plus" size="small" @click="onAddVoucherRule">添加规则条目</el-button>
</div>
</el-form-item>
<div class="sa-title is-line">创客</div>
<el-form-item label="V1–V6 单笔基数">
<div class="sa-flex" style="gap:8px;flex-wrap:wrap">

查看文件

@@ -45,11 +45,26 @@
<sa-user-profile :user="scope.row.user" :id="scope.row.user_id"></sa-user-profile>
</template>
</el-table-column>
<el-table-column label="提现金额/实际到账" min-width="166">
<el-table-column label="提现金额/实际到账" min-width="170">
<template #default="scope">
<div class="sa-table-line-1">
{{ scope.row.amount || '-' }}/{{ scope.row.paid_fee }}元
</div>
<template v-if="scope.row.is_madow_withdraw">
<div class="sa-flex" style="margin-bottom: 2px;">
<el-tag size="small" type="warning" effect="dark" style="background: linear-gradient(135deg, #d4af37, #aa7c11); border: none; font-weight: bold; border-radius: 4px; height: 20px; line-height: 20px; padding: 0 6px;">
DMEPA提现
</el-tag>
</div>
<div class="sa-table-line-1" style="color: #b88200; font-weight: bold;">
换算市值: {{ scope.row.amount || '-' }}元
</div>
<div class="sa-table-line-1" style="font-size: 12px; color: #909399;">
实际到账: {{ scope.row.paid_fee }}元
</div>
</template>
<template v-else>
<div class="sa-table-line-1">
{{ scope.row.amount || '-' }}/{{ scope.row.paid_fee }}元
</div>
</template>
</template>
</el-table-column>
<el-table-column label="费率/手续费" min-width="132">
@@ -78,12 +93,21 @@
<el-table-column label="打款信息" min-width="380">
<template #default="scope">
<div class="sa-flex-col sa-col-top">
<div v-for="(value, key) in scope.row.withdraw_info" :key="value" class="sa-flex">
<div class="sa-table-line-1"> {{ key }}:{{ value }} </div>
<el-icon class="copy-document" @click="onClipboard(value)">
<copy-document />
</el-icon>
</div>
<template v-for="(value, key) in (scope.row.withdraw_info_display || scope.row.withdraw_info)">
<div v-if="!['wallet_type', 'madow_amount', 'rate'].includes(key)" :key="key" class="sa-flex" style="margin-bottom: 2px;">
<div class="sa-table-line-1">
<span v-if="['市值汇率', '换算市值', '提现资产', '扣除DMEPA'].includes(key)" style="color: #b88200; font-weight: bold;">
{{ key }}: {{ value }}
</span>
<span v-else>
{{ key }}: {{ value }}
</span>
</div>
<el-icon class="copy-document" @click="onClipboard(value)">
<copy-document />
</el-icon>
</div>
</template>
<div v-if="scope.row.withdraw_type === 'wechat'" class="sa-flex">
<div class="sa-table-line-1">
打款状态: {{ scope.row.wechat_transfer_state_text }} | {{

查看文件

@@ -89,6 +89,30 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
form.model.partner_maker_bonus = { 2: 20, 3: 40, 4: 60, 5: 80, 6: 100, 7: 120 }
}
form.model.partner_show_madow_rate = form.model.partner_show_madow_rate !== undefined && form.model.partner_show_madow_rate !== null ? Number(form.model.partner_show_madow_rate) : 1;
form.model.partner_madow_withdraw_enable = form.model.partner_madow_withdraw_enable !== undefined && form.model.partner_madow_withdraw_enable !== null ? Number(form.model.partner_madow_withdraw_enable) : 1;
if (form.model.partner_madow_withdraw_min === undefined || form.model.partner_madow_withdraw_min === null || form.model.partner_madow_withdraw_min === '') {
form.model.partner_madow_withdraw_min = 1;
}
if (!form.model.partner_madow_rule_title) {
form.model.partner_madow_rule_title = '市值与增值规则';
}
if (!Array.isArray(form.model.partner_madow_rules) || form.model.partner_madow_rules.length === 0) {
form.model.partner_madow_rules = [
{ title: 'dmepa获取与核算', desc: '平台合伙人与创客达成考核或完成订单后,系统将于次日 00:30 起统一参与每日dmepa加权核算并自动分发。' },
{ title: '市值动态增值', desc: '市值随全平台订单增加而上浮增值。全网完成订单总量越多,dmepa单价所对应的市值越高,共享生态红利。' },
{ title: '价值说明与权益', desc: '市值仅作为平台数字生态资产价值参考展示,不可直接兑换现金提现。具体权益按平台股东与合伙人章程执行。' }
];
}
if (!form.model.partner_voucher_rule_title) {
form.model.partner_voucher_rule_title = '兑换券释放与使用规则';
}
if (!Array.isArray(form.model.partner_voucher_rules) || form.model.partner_voucher_rules.length === 0) {
form.model.partner_voucher_rules = [
{ title: '赠送与释放天数', desc: '会员在平台完成符合条件的商城订单后,系统将按订单金额(默认 1.5 倍)生成兑换券计划,分 365 天按日线性释放。' },
{ title: '每日自动到账', desc: '系统将于次日 00:30 起统一执行日结核算,按期释放的兑换券将自动计入您的「可用兑换券」账户,无需手动领取。' },
{ title: '全场购物直接抵扣', desc: '可用兑换券可在平台商城下单结算时作为现金抵扣使用,尊享优惠抵扣权益。' }
];
}
}
if (state.tabActive == 'shopro/config/chat') {
@@ -317,6 +341,30 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
form.model.agent_form.content.splice(index, 1);
}
// 合伙股东抽屉说明规则编辑
function onAddMadowRule() {
if (!Array.isArray(form.model.partner_madow_rules)) {
form.model.partner_madow_rules = [];
}
form.model.partner_madow_rules.push({ title: '', desc: '' });
}
function onDeleteMadowRule(index) {
if (Array.isArray(form.model.partner_madow_rules)) {
form.model.partner_madow_rules.splice(index, 1);
}
}
function onAddVoucherRule() {
if (!Array.isArray(form.model.partner_voucher_rules)) {
form.model.partner_voucher_rules = [];
}
form.model.partner_voucher_rules.push({ title: '', desc: '' });
}
function onDeleteVoucherRule(index) {
if (Array.isArray(form.model.partner_voucher_rules)) {
form.model.partner_voucher_rules.splice(index, 1);
}
}
// 支付配置
const payConfig = reactive({
data: []
@@ -427,7 +475,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
}
if (state.tabActive == 'shopro/config/partner' && submitForm.partner_madow_mode === 'cash') {
const { ElMessageBox } = ElementPlus
ElMessageBox.confirm('马豆改为现金模式后,日结将按当日 GMV 发放现金。确定保存?', '二次确认', {
ElMessageBox.confirm('dmepa改为现金模式后,日结将按当日 GMV 发放现金。确定保存?', '二次确认', {
confirmButtonText: '确定保存',
cancelButtonText: '取消',
type: 'warning',
@@ -486,6 +534,11 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
onAddContent,
onDeleteContent,
onAddMadowRule,
onDeleteMadowRule,
onAddVoucherRule,
onDeleteVoucherRule,
payConfig,
pagination,
onCommandPayConfig,

查看文件

@@ -0,0 +1,145 @@
/**
* 后台提现展示逻辑与数据获取器单元模拟测试
* 模拟验证 application/admin/model/shopro/Withdraw.php 中的:
* 1. getIsMadowWithdrawAttr 判断逻辑
* 2. getWithdrawInfoDisplayAttr 字段清洗、市值汇率与换算市值格式化逻辑
*/
const assert = require('assert');
function simulateGetIsMadowWithdraw(withdraw_info) {
let info = withdraw_info;
if (typeof info === 'string') {
try { info = JSON.parse(info); } catch (e) { info = {}; }
}
if (!info || typeof info !== 'object') return false;
return Boolean(
(info.wallet_type === 'madow') ||
(info['提现资产'] && info['提现资产'].toLowerCase() === 'dmepa')
);
}
function simulateGetWithdrawInfoDisplay(withdraw_info, amount) {
let info = withdraw_info;
if (typeof info === 'string') {
try { info = JSON.parse(info); } catch (e) { info = {}; }
}
if (!info || typeof info !== 'object') return {};
const display = {};
const isMadow = simulateGetIsMadowWithdraw(info);
if (isMadow) {
display['提现资产'] = 'DMEPA';
// 扣除 DMEPA
const madowAmount = info['madow_amount'] || info['扣除DMEPA'] || info['扣除dmepa'] || null;
if (madowAmount !== null) {
const madowClean = String(madowAmount).replace(/dmepa/gi, '').trim();
display['扣除DMEPA'] = `${madowClean} DMEPA`;
}
// 市值汇率
const rate = info['rate'] !== undefined ? info['rate'] : (info['市值汇率'] || info['折算汇率'] || null);
if (rate !== null) {
if (typeof rate === 'number') {
const rateClean = rate.toFixed(12).replace(/\.?0+$/, '');
display['市值汇率'] = `1 DMEPA ≈ ${rateClean} 元`;
} else {
const rateClean = String(rate).replace(/1\s*DMEPA\s*≈|元\/dmepa|元\/DMEPA|元|\s/g, '').trim();
display['市值汇率'] = `1 DMEPA ≈ ${rateClean} 元`;
}
}
// 换算市值
const marketValue = info['换算市值'] || amount || null;
if (marketValue !== null) {
const valClean = String(marketValue).replace(/元|\s/g, '').trim();
display['换算市值'] = `${Number(valClean).toFixed(2)} 元`;
}
}
const excludeKeys = ['wallet_type', 'madow_amount', 'rate', '提现资产', '扣除DMEPA', '扣除dmepa', '市值汇率', '折算汇率', '换算市值'];
for (const [k, v] of Object.entries(info)) {
if (!excludeKeys.includes(k)) {
display[k] = v;
}
}
return display;
}
console.log('--- 开始执行后台模型获取器逻辑模拟单元测试 ---');
// 场景 1: 普通佣金提现单
const normalWithdraw = {
amount: '50.00',
withdraw_info: {
'真实姓名': '张三',
'开户银行': '工商银行',
'银行卡号': '6222021234567890'
}
};
assert.strictEqual(simulateGetIsMadowWithdraw(normalWithdraw.withdraw_info), false, '普通提现单识别为非 DMEPA');
const normalDisplay = simulateGetWithdrawInfoDisplay(normalWithdraw.withdraw_info, normalWithdraw.amount);
assert.strictEqual(normalDisplay['提现资产'], undefined);
assert.strictEqual(normalDisplay['市值汇率'], undefined);
assert.strictEqual(normalDisplay['真实姓名'], '张三');
console.log('✓ 场景 1 (普通佣金提现单) 通过');
// 场景 2: 新版 DMEPA 银行卡提现单
const newMadowWithdraw = {
amount: '100.00',
withdraw_info: {
'真实姓名': '李四',
'开户银行': '建设银行',
'银行卡号': '6217001234567890',
'提现资产': 'DMEPA',
'扣除DMEPA': '133333333.3333 DMEPA',
'市值汇率': '1 DMEPA ≈ 0.00000075 元',
'换算市值': '100.00 元',
'wallet_type': 'madow',
'madow_amount': '133333333.3333',
'rate': 0.00000075
}
};
assert.strictEqual(simulateGetIsMadowWithdraw(newMadowWithdraw.withdraw_info), true, '新版 DMEPA 单据成功识别');
const newDisplay = simulateGetWithdrawInfoDisplay(newMadowWithdraw.withdraw_info, newMadowWithdraw.amount);
assert.strictEqual(newDisplay['提现资产'], 'DMEPA');
assert.strictEqual(newDisplay['扣除DMEPA'], '133333333.3333 DMEPA');
assert.strictEqual(newDisplay['市值汇率'], '1 DMEPA ≈ 0.00000075 元');
assert.strictEqual(newDisplay['换算市值'], '100.00 元');
assert.strictEqual(newDisplay['真实姓名'], '李四');
assert.strictEqual(newDisplay['开户银行'], '建设银行');
assert.strictEqual(newDisplay['wallet_type'], undefined, '成功过滤底层英文键 wallet_type');
assert.strictEqual(newDisplay['madow_amount'], undefined, '成功过滤底层英文键 madow_amount');
assert.strictEqual(newDisplay['rate'], undefined, '成功过滤底层英文键 rate');
console.log('✓ 场景 2 (新版 DMEPA 银行卡提现单) 通过');
// 场景 3: 旧版兼容 DMEPA 记录 (历史数据兼容)
const legacyMadowWithdraw = {
amount: '200.00',
withdraw_info: {
'真实姓名': '王五',
'开户银行': '招商银行',
'银行卡号': '6225881234567890',
'提现资产': 'dmepa',
'扣除dmepa': '266666666.6667',
'折算汇率': '0.00000075 元/dmepa',
'wallet_type': 'madow',
'madow_amount': '266666666.6667',
'rate': 0.00000075
}
};
assert.strictEqual(simulateGetIsMadowWithdraw(legacyMadowWithdraw.withdraw_info), true, '旧版小写 dmepa 成功识别');
const legacyDisplay = simulateGetWithdrawInfoDisplay(legacyMadowWithdraw.withdraw_info, legacyMadowWithdraw.amount);
assert.strictEqual(legacyDisplay['提现资产'], 'DMEPA', '小写 dmepa 规范大写');
assert.strictEqual(legacyDisplay['扣除DMEPA'], '266666666.6667 DMEPA', '自动补齐 DMEPA 单位后缀');
assert.strictEqual(legacyDisplay['市值汇率'], '1 DMEPA ≈ 0.00000075 元', '历史「折算汇率」自动规范为「市值汇率」');
assert.strictEqual(legacyDisplay['换算市值'], '200.00 元', '历史数据自动根据金额补全「换算市值」');
assert.strictEqual(legacyDisplay['折算汇率'], undefined, '历史旧键被清理');
assert.strictEqual(legacyDisplay['扣除dmepa'], undefined, '历史小写键被清理');
console.log('✓ 场景 3 (旧版兼容 DMEPA 记录) 通过');
console.log('-------------------------------------------');
console.log('所有单元行为模拟测试全部通过!');

查看文件

@@ -0,0 +1,169 @@
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. 验证 PartnerConfig.php 规则读取与容错兜底机制
console.log("--- 1. PartnerConfig 规则读取与容错逻辑 ---");
const partnerConfigPath = path.resolve(__dirname, '../../addons/shopro/service/partner/PartnerConfig.php');
assert(fs.existsSync(partnerConfigPath), 'PartnerConfig.php 文件存在');
const partnerConfigContent = fs.readFileSync(partnerConfigPath, 'utf-8');
assert(
partnerConfigContent.includes('function madowRuleTitle') && partnerConfigContent.includes('function madowRules'),
'PartnerConfig 包含 madowRuleTitle() 和 madowRules() 读取方法'
);
assert(
partnerConfigContent.includes('function voucherRuleTitle') && partnerConfigContent.includes('function voucherRules'),
'PartnerConfig 包含 voucherRuleTitle() 和 voucherRules() 读取方法'
);
assert(
partnerConfigContent.includes('function normalizeRules'),
'PartnerConfig 包含 normalizeRules 对 JSON 字符串、纯文本换行与数组的智能格式化及容错'
);
assert(
/DMEPA获取与核算/i.test(partnerConfigContent) && (partnerConfigContent.includes('市值动态增值') || partnerConfigContent.includes('马币市值动态增值')) && partnerConfigContent.includes('价值说明与权益'),
'默认兜底规则完整覆盖规则条目'
);
// 2. 验证 后台 Config 控制器白名单与数据自愈
console.log("\n--- 2. 后台 Config 控制器白名单与自愈机制 ---");
const configPhpPath = path.resolve(__dirname, '../../application/admin/controller/shopro/Config.php');
assert(fs.existsSync(configPhpPath), 'Config.php 文件存在');
const configPhpContent = fs.readFileSync(configPhpPath, 'utf-8');
assert(
configPhpContent.includes('partner_madow_rule_title') && configPhpContent.includes('partner_madow_rules'),
'Config.php 白名单及默认值包含 partner_madow_rule_title 和 partner_madow_rules'
);
assert(
configPhpContent.includes('partner_voucher_rule_title') && configPhpContent.includes('partner_voucher_rules'),
'Config.php 白名单及默认值包含 partner_voucher_rule_title 和 partner_voucher_rules'
);
assert(
configPhpContent.includes('shop.commission.partner_madow_rule_title') && configPhpContent.includes('shop.commission.partner_madow_rules'),
'Config.php 包含数据库配置缺失时的自动创建与自愈机制'
);
// 3. 验证 后台视图与 JS 前端交互绑定
console.log("\n--- 3. 后台配置视图与交互脚本 ---");
const indexHtmlPath = path.resolve(__dirname, '../../application/admin/view/shopro/config/index.html');
assert(fs.existsSync(indexHtmlPath), 'config/index.html 文件存在');
const indexHtmlContent = fs.readFileSync(indexHtmlPath, 'utf-8');
assert(
indexHtmlContent.includes('form.model.partner_madow_rule_title') && indexHtmlContent.includes('form.model.partner_madow_rules'),
'后台视图包含 dmepa 市值规则弹窗标题输入框与规则说明条目编辑'
);
assert(
indexHtmlContent.includes('onDeleteMadowRule') && indexHtmlContent.includes('onAddMadowRule'),
'后台视图提供 dmepa 规则条目的新增和删除操作入口'
);
const configJsPath = path.resolve(__dirname, '../../public/assets/js/backend/shopro/config.js');
assert(fs.existsSync(configJsPath), 'config.js 文件存在');
const configJsContent = fs.readFileSync(configJsPath, 'utf-8');
assert(
configJsContent.includes('form.model.partner_madow_rule_title') && configJsContent.includes('form.model.partner_madow_rules'),
'config.js 在加载配置时包含 dmepa 规则默认结构初始化'
);
assert(
configJsContent.includes('onAddMadowRule') && configJsContent.includes('onDeleteMadowRule'),
'config.js 定义并暴露了 onAddMadowRule 和 onDeleteMadowRule 方法'
);
// 4. 验证 用户端接口 partner/Index.php
console.log("\n--- 4. 用户端 partner/Index.php 接口输出 ---");
const partnerIndexPath = path.resolve(__dirname, '../../addons/shopro/controller/partner/Index.php');
assert(fs.existsSync(partnerIndexPath), 'partner/Index.php 文件存在');
const partnerIndexContent = fs.readFileSync(partnerIndexPath, 'utf-8');
assert(
partnerIndexContent.includes("'rule_title' => PartnerConfig::madowRuleTitle()") &&
partnerIndexContent.includes("'rules' => PartnerConfig::madowRules()"),
'madow() 接口返回 rule_title 与 rules 规则数据'
);
assert(
partnerIndexContent.includes("'rule_title' => PartnerConfig::voucherRuleTitle()") &&
partnerIndexContent.includes("'rules' => PartnerConfig::voucherRules()"),
'voucher() 接口返回 rule_title 与 rules 规则数据'
);
// 5. 验证 移动端 madow.vue 与 voucher.vue 抽屉弹窗动态渲染
console.log("\n--- 5. 移动端抽屉弹窗动态渲染与保底 ---");
const madowVuePath = path.resolve(__dirname, '../../uniapp/pages/commission/madow.vue');
assert(fs.existsSync(madowVuePath), 'madow.vue 文件存在');
const madowVueContent = fs.readFileSync(madowVuePath, 'utf-8');
assert(
madowVueContent.includes('state.ruleTitle') &&
madowVueContent.includes('v-for="(item, index) in state.rules"') &&
madowVueContent.includes('item.title') &&
madowVueContent.includes('item.desc'),
'madow.vue 抽屉弹窗支持标题与规则条目动态循环渲染'
);
assert(
madowVueContent.includes('defaultRules') &&
madowVueContent.includes('dmepa获取与核算') &&
(madowVueContent.includes('市值动态增值') || madowVueContent.includes('马币市值动态增值')) &&
madowVueContent.includes('价值说明与权益'),
'madow.vue 保留本地 defaultRules,在无网络或首次加载时提供稳健保底'
);
assert(
madowVueContent.includes('data.rule_title') && madowVueContent.includes('data.rules'),
'madow.vue getMadow() 成功接收并更新 state.ruleTitle 与 state.rules'
);
const voucherVuePath = path.resolve(__dirname, '../../uniapp/pages/commission/voucher.vue');
assert(fs.existsSync(voucherVuePath), 'voucher.vue 文件存在');
const voucherVueContent = fs.readFileSync(voucherVuePath, 'utf-8');
assert(
voucherVueContent.includes('state.ruleTitle') &&
voucherVueContent.includes('v-for="(item, index) in state.rules"'),
'voucher.vue 抽屉弹窗支持标题与规则条目动态循环渲染'
);
// 6. 验证 upgrade SQL 初始化
console.log("\n--- 6. upgrade SQL 初始配置项覆盖 ---");
const sqlPath = path.resolve(__dirname, '../../addons/shopro/upgrade/partner_v2.sql');
assert(fs.existsSync(sqlPath), 'partner_v2.sql 文件存在');
const sqlContent = fs.readFileSync(sqlPath, 'utf-8');
assert(
sqlContent.includes('shop.commission.partner_madow_rule_title') &&
sqlContent.includes('shop.commission.partner_madow_rules'),
'partner_v2.sql 包含 dmepa 规则初始配置行'
);
console.log("\n--------------------------------------------------------");
console.log(`测试执行结果: 通过 ${passed} 项, 失败 ${failed} 项`);
console.log("========================================================");
process.exit(failed === 0 ? 0 : 1);

查看文件

@@ -0,0 +1,328 @@
/**
* dmepa 余额银行卡提现功能端到端全链路规格测试
* 覆盖范围:
* 1. PartnerConfig.php 开关与最小金额读取方法、奖励字典
* 2. Config.php 控制器白名单、默认值、自愈与入参校验
* 3. config/index.html & config.js 后台表单项与初始化
* 4. partner_v2.sql 数据库升级配置行
* 5. Withdraw.php 服务层 applyMadow 提现发起、持仓校验、扣减流水、审核拒绝退款与失败退还闭环
* 6. partner/Index.php madow() 接口返回提现开关与最低金额
* 7. madow.vue 资产卡片提现按钮及动态显隐控制
* 8. withdraw.vue 路由参数、银行卡模式锁定、全部提现、实时汇率扣除估算与提现提交
*/
const fs = require('fs');
const path = require('path');
let passCount = 0;
let failCount = 0;
function assert(condition, testName, detail = '') {
if (condition) {
console.log(` \x1b[32m[PASS]\x1b[0m ${testName}`);
passCount++;
} else {
console.error(` \x1b[31m[FAIL]\x1b[0m ${testName} - ${detail}`);
failCount++;
}
}
console.log('========================================================');
console.log(' dmepa 余额银行卡提现功能端到端规格测试');
console.log('========================================================\n');
// 1. 验证 PartnerConfig.php
console.log('--- 1. PartnerConfig 配置读取与字典注册 ---');
const partnerConfigPath = path.resolve(__dirname, '../../addons/shopro/service/partner/PartnerConfig.php');
assert(fs.existsSync(partnerConfigPath), 'PartnerConfig.php 文件存在');
const partnerConfigContent = fs.readFileSync(partnerConfigPath, 'utf-8');
assert(
partnerConfigContent.includes('function isMadowWithdrawEnabled'),
'PartnerConfig 包含 isMadowWithdrawEnabled() 读取方法'
);
assert(
partnerConfigContent.includes('function madowWithdrawMin'),
'PartnerConfig 包含 madowWithdrawMin() 最低提现金额读取方法'
);
assert(
partnerConfigContent.includes("'madow_withdraw' => 'dmepa提现扣除'") &&
partnerConfigContent.includes("'madow_withdraw_refund' => 'dmepa提现退还'"),
'PartnerConfig rewardTypes 字典正确登记 madow_withdraw 与 madow_withdraw_refund'
);
// 2. 验证 Config.php
console.log('\n--- 2. 后台 Config.php 控制器白名单与自愈 ---');
const configPhpPath = path.resolve(__dirname, '../../application/admin/controller/shopro/Config.php');
assert(fs.existsSync(configPhpPath), 'Config.php 文件存在');
const configPhpContent = fs.readFileSync(configPhpPath, 'utf-8');
assert(
configPhpContent.includes("'partner_madow_withdraw_enable'") &&
configPhpContent.includes("'partner_madow_withdraw_min'"),
'Config.php 白名单 $keys 包含 partner_madow_withdraw_enable 与 partner_madow_withdraw_min'
);
assert(
configPhpContent.includes("'partner_madow_withdraw_enable' => 1") &&
configPhpContent.includes("'partner_madow_withdraw_min' => 1"),
'Config.php $defaults 包含 partner_madow_withdraw_enable 与 partner_madow_withdraw_min 默认值'
);
assert(
configPhpContent.includes("shop.commission.partner_madow_withdraw_enable") &&
configPhpContent.includes("shop.commission.partner_madow_withdraw_min"),
'Config.php $ensureConfigs 包含提现开关与最低金额自愈创建机制'
);
assert(
configPhpContent.includes("intval($save['partner_madow_withdraw_enable'])") &&
configPhpContent.includes("floatval($save['partner_madow_withdraw_min'])"),
'Config.php POST 保存处理了整型与浮点安全类型转换'
);
// 3. 验证 后台视图与 JS 脚本
console.log('\n--- 3. 后台配置视图与脚本 (index.html & config.js) ---');
const indexHtmlPath = path.resolve(__dirname, '../../application/admin/view/shopro/config/index.html');
assert(fs.existsSync(indexHtmlPath), 'config/index.html 视图文件存在');
const indexHtmlContent = fs.readFileSync(indexHtmlPath, 'utf-8');
assert(
indexHtmlContent.includes('form.model.partner_madow_withdraw_enable') &&
(indexHtmlContent.includes('开启dmepa提现') || indexHtmlContent.includes('开启DMEPA提现')),
'后台视图包含「开启DMEPA提现」switch 开关'
);
assert(
indexHtmlContent.includes('form.model.partner_madow_withdraw_min') &&
(indexHtmlContent.includes('dmepa最低提现金额') || indexHtmlContent.includes('DMEPA最低提现金额')),
'后台视图包含「DMEPA最低提现金额」输入框'
);
const configJsPath = path.resolve(__dirname, '../../public/assets/js/backend/shopro/config.js');
assert(fs.existsSync(configJsPath), 'config.js 脚本文件存在');
const configJsContent = fs.readFileSync(configJsPath, 'utf-8');
assert(
configJsContent.includes('form.model.partner_madow_withdraw_enable') &&
configJsContent.includes('form.model.partner_madow_withdraw_min'),
'config.js 对 partner_madow_withdraw_enable 与 partner_madow_withdraw_min 进行默认初始化'
);
// 4. 验证 partner_v2.sql
console.log('\n--- 4. 数据库升级脚本 partner_v2.sql ---');
const sqlPath = path.resolve(__dirname, '../../addons/shopro/upgrade/partner_v2.sql');
assert(fs.existsSync(sqlPath), 'partner_v2.sql 文件存在');
const sqlContent = fs.readFileSync(sqlPath, 'utf-8');
assert(
sqlContent.includes("'shop.commission.partner_madow_withdraw_enable'") &&
sqlContent.includes("'shop.commission.partner_madow_withdraw_min'"),
'partner_v2.sql 包含提现开关与最低金额初始配置行'
);
// 5. 验证 Withdraw.php 服务层业务闭环
console.log('\n--- 5. 提现服务层 (Withdraw.php) dmepa提现业务闭环 ---');
const withdrawServicePath = path.resolve(__dirname, '../../addons/shopro/service/Withdraw.php');
assert(fs.existsSync(withdrawServicePath), 'Withdraw.php 服务文件存在');
const withdrawServiceContent = fs.readFileSync(withdrawServicePath, 'utf-8');
assert(
withdrawServiceContent.includes("params['wallet_type'] ?? 'commission'") &&
withdrawServiceContent.includes("applyMadow"),
'Withdraw.php::apply 支持 wallet_type=madow 并分流至 applyMadow'
);
assert(
withdrawServiceContent.includes("PartnerConfig::isMadowWithdrawEnabled()") &&
(withdrawServiceContent.includes('dmepa提现功能暂未开启') || withdrawServiceContent.includes('DMEPA提现功能暂未开启')),
'applyMadow 校验 DMEPA 提现开关,关闭时抛出面向用户的明确提示'
);
assert(
withdrawServiceContent.includes("$type !== 'bank'") &&
(withdrawServiceContent.includes('dmepa提现仅支持银行卡转账') || withdrawServiceContent.includes('DMEPA提现仅支持银行卡转账')),
'applyMadow 严格限制提现渠道仅支持 bank (银行卡转账)'
);
assert(
withdrawServiceContent.includes("PartnerConfig::madowWithdrawMin()"),
'applyMadow 校验最低提现金额'
);
assert(
withdrawServiceContent.includes("(new RateService())->currentRate()"),
'applyMadow 调用 RateService 实时获取当前大盘汇率'
);
assert(
withdrawServiceContent.includes("AgentModel::where('user_id', $this->user->id)->lock(true)->find()") &&
(withdrawServiceContent.includes("持仓dmepa不足,无法提现") || withdrawServiceContent.includes("持仓DMEPA不足,无法提现")),
'applyMadow 行级排他锁校验合伙人 DMEPA 持仓充足性'
);
assert(
withdrawServiceContent.includes("$agent->setDec('madow', $deductMadow)"),
'applyMadow 执行数据库原子扣减持仓 dmepa'
);
assert(
withdrawServiceContent.includes("'reward_type' => 'madow_withdraw'"),
'applyMadow 记录 PartnerRewardService madow_withdraw 负向提现流水'
);
assert(
withdrawServiceContent.includes("madow_withdraw_refund") &&
withdrawServiceContent.includes("$agent->setInc('madow', $madowRefund)"),
'handleRefuse 识别 dmepa 提现驳回并原子返还持仓 madow 记录 refund 流水'
);
assert(
withdrawServiceContent.includes("handleFail") &&
withdrawServiceContent.includes("madow_withdraw_refund"),
'handleFail 提现异常失败时同样支持安全返还 dmepa'
);
// 6. 验证 用户端接口 partner/Index.php
console.log('\n--- 6. 用户端 partner/Index.php 接口输出 ---');
const partnerIndexPath = path.resolve(__dirname, '../../addons/shopro/controller/partner/Index.php');
assert(fs.existsSync(partnerIndexPath), 'partner/Index.php 控制器存在');
const partnerIndexContent = fs.readFileSync(partnerIndexPath, 'utf-8');
assert(
partnerIndexContent.includes("'can_withdraw_madow' => PartnerConfig::isMadowWithdrawEnabled() ? 1 : 0") &&
partnerIndexContent.includes("'madow_withdraw_min' => PartnerConfig::madowWithdrawMin()"),
'madow() 接口返回 can_withdraw_madow 开关与 madow_withdraw_min 门槛'
);
// 7. 验证 前端 madow.vue 资产页
console.log('\n--- 7. 用户端 我的dmepa页面 (madow.vue) 提现入口 ---');
const madowVuePath = path.resolve(__dirname, '../../uniapp/pages/commission/madow.vue');
assert(fs.existsSync(madowVuePath), 'madow.vue 文件存在');
const madowVueContent = fs.readFileSync(madowVuePath, 'utf-8');
assert(
madowVueContent.includes("v-if=\"state.info.can_withdraw_madow !== false && state.info.can_withdraw_madow !== 0 && state.info.can_withdraw_madow !== '0'\""),
'madow.vue 提现按钮受后台 can_withdraw_madow 开关动态控制显隐'
);
assert(
madowVueContent.includes("@tap=\"onWithdrawMadow\"") &&
madowVueContent.includes("sheep.$router.go('/pages/pay/withdraw', { type: 'madow' })"),
'madow.vue 点击提现跳转至 /pages/pay/withdraw?type=madow'
);
assert(
madowVueContent.includes(".topup-btn") &&
madowVueContent.includes(".hero-left"),
'madow.vue 包含与钱包对齐的黑金渐变提现按钮样式'
);
assert(
madowVueContent.includes("state.info.can_withdraw_madow = data.can_withdraw_madow"),
'madow.vue getMadow() 解析服务端下发的 can_withdraw_madow 状态'
);
// 8. 验证 前端 withdraw.vue 提现申请页
console.log('\n--- 8. 用户端 提现申请页 (withdraw.vue) dmepa提现适配 ---');
const withdrawVuePath = path.resolve(__dirname, '../../uniapp/pages/pay/withdraw.vue');
assert(fs.existsSync(withdrawVuePath), 'withdraw.vue 文件存在');
const withdrawVueContent = fs.readFileSync(withdrawVuePath, 'utf-8');
assert(
withdrawVueContent.includes(":title=\"state.walletType === 'madow' ? 'dmepa提现' : '申请提现'\"") ||
withdrawVueContent.includes(":title=\"state.walletType === 'madow' ? 'DMEPA提现' : '申请提现'\""),
'withdraw.vue 页面标题根据 walletType 自适应切换 DMEPA提现 / 申请提现'
);
assert(
withdrawVueContent.includes("state.walletType === 'madow'") &&
withdrawVueContent.includes("state.madowInfo.madow"),
'withdraw.vue 顶部资产卡片在 madow 模式下展示 dmepa 持仓与预估市值'
);
assert(
withdrawVueContent.includes("onWithdrawAllMadow") &&
withdrawVueContent.includes("全部提现"),
'withdraw.vue madow 模式下支持一键「全部提现」'
);
assert(
withdrawVueContent.includes("estimatedDeductMadow") &&
withdrawVueContent.includes("实时汇率"),
'withdraw.vue 支持输入金额时根据大盘实时汇率计算需扣除的 dmepa 数量预览'
);
assert(
withdrawVueContent.includes(":methods=\"state.walletType === 'madow' ? ['bank'] : state.rules.methods\""),
'withdraw.vue 在 madow 模式下提现方式严格锁定为 bank 银行卡'
);
assert(
withdrawVueContent.includes("payload.wallet_type = 'madow'") &&
withdrawVueContent.includes("payload.type = 'bank'"),
'withdraw.vue 提交申请时附带 wallet_type: madow 与 type: bank 参数'
);
assert(
withdrawVueContent.includes("fetchUserBankAccount"),
'withdraw.vue 进入 madow 提现时自动调取既有已绑定的银行卡信息'
);
// 9. 验证 后台提现模型 (Withdraw.php) 属性追加与获取器清洗
console.log('\n--- 9. 后台提现模型 (Withdraw.php) 市值汇率与换算市值获取器 ---');
const adminWithdrawModelPath = path.resolve(__dirname, '../../application/admin/model/shopro/Withdraw.php');
assert(fs.existsSync(adminWithdrawModelPath), 'application/admin/model/shopro/Withdraw.php 存在');
const adminWithdrawModelContent = fs.readFileSync(adminWithdrawModelPath, 'utf-8');
assert(
adminWithdrawModelContent.includes("'is_madow_withdraw'") &&
adminWithdrawModelContent.includes("'withdraw_info_display'"),
'Withdraw 模型 $append 包含 is_madow_withdraw 与 withdraw_info_display 属性'
);
assert(
adminWithdrawModelContent.includes("function getIsMadowWithdrawAttr"),
'Withdraw 模型提供 getIsMadowWithdrawAttr 获取器精准识别 DMEPA 提现单'
);
assert(
adminWithdrawModelContent.includes("function getWithdrawInfoDisplayAttr"),
'Withdraw 模型提供 getWithdrawInfoDisplayAttr 获取器'
);
assert(
adminWithdrawModelContent.includes("市值汇率") &&
adminWithdrawModelContent.includes("换算市值") &&
adminWithdrawModelContent.includes("1 DMEPA ≈"),
'getWithdrawInfoDisplayAttr 包含「市值汇率」与「换算市值」规范格式化'
);
assert(
adminWithdrawModelContent.includes("wallet_type") &&
adminWithdrawModelContent.includes("madow_amount") &&
adminWithdrawModelContent.includes("rate"),
'getWithdrawInfoDisplayAttr 过滤 wallet_type、madow_amount、rate 等英文技术键'
);
// 10. 验证 后台提现视图 (index.html) 金额列与打款信息列展示
console.log('\n--- 10. 后台提现管理视图 (index.html) 市值汇率与换算市值高亮展示 ---');
const adminWithdrawHtmlPath = path.resolve(__dirname, '../../application/admin/view/shopro/withdraw/index.html');
assert(fs.existsSync(adminWithdrawHtmlPath), 'application/admin/view/shopro/withdraw/index.html 存在');
const adminWithdrawHtmlContent = fs.readFileSync(adminWithdrawHtmlPath, 'utf-8');
assert(
adminWithdrawHtmlContent.includes("scope.row.is_madow_withdraw") &&
adminWithdrawHtmlContent.includes("DMEPA提现") &&
adminWithdrawHtmlContent.includes("换算市值: {{ scope.row.amount || '-' }}元"),
'提现金额列识别 DMEPA 提现,展示 DMEPA提现 专属金标并明确标注「换算市值」'
);
assert(
adminWithdrawHtmlContent.includes("scope.row.withdraw_info_display || scope.row.withdraw_info"),
'打款信息列优先读取规范清洗后的 withdraw_info_display'
);
assert(
adminWithdrawHtmlContent.includes("['wallet_type', 'madow_amount', 'rate'].includes(key)"),
'打款信息列表格循环中过滤内部英文技术字段'
);
assert(
adminWithdrawHtmlContent.includes("['市值汇率', '换算市值', '提现资产', '扣除DMEPA'].includes(key)"),
'打款信息列对「市值汇率」、「换算市值」等关键要素进行金黄色强调高亮'
);
// 11. 验证 服务层 (Withdraw.php) 写入中文标准字典与退还稳健性
console.log('\n--- 11. 服务层 (Withdraw.php) 写入「市值汇率」与「换算市值」标准字典 ---');
assert(
withdrawServiceContent.includes("$withdrawInfo['市值汇率'] = '1 DMEPA ≈ ' . $rateClean . ' 元';") &&
withdrawServiceContent.includes("$withdrawInfo['换算市值'] = $moneyClean . ' 元';"),
'applyMadow 提现单据中明确写入标准中文「市值汇率」与「换算市值」'
);
assert(
withdrawServiceContent.includes("$withdrawInfo['提现资产'] = 'DMEPA';") &&
withdrawServiceContent.includes("$withdrawInfo['扣除DMEPA'] = $deductMadow . ' DMEPA';"),
'applyMadow 写入规范大写 DMEPA 与扣除资产数量说明'
);
console.log('--------------------------------------------------------');
console.log(`测试执行结果: 通过 ${passCount} 项, 失败 ${failCount} 项`);
console.log('========================================================\n');
if (failCount > 0) {
process.exit(1);
} else {
process.exit(0);
}

查看文件

@@ -0,0 +1,178 @@
/**
* tests/partner/verify_withdraw_methods_visibility_spec.js
*
* 验证提现方式选择弹窗(account-type-select.vue)与提现主页(withdraw.vue)
* “如果后台设置关闭的 这里隐藏掉 只有开启才显示” 业务规范自动化测试:
*
* 1. 组件源码结构静态检查(filteredTypeList 仅渲染后台开启的提现渠道)
* 2. 彻底废除旧的 disabled 灰色禁用行显示,改为未开启方式完全不渲染
* 3. 提现主页面关闭渠道自愈与单渠道智能预选逻辑验证
* 4. 各种后台配置场景模拟(单银行卡、微信+银行卡、全开、全关等)
* 5. API 模拟测试(正常场景、业务拒绝场景、参数边界场景)
*/
const fs = require('fs');
const path = require('path');
const assert = require('assert');
console.log('========================================================');
console.log(' 验证:提现方式选择弹窗后台关闭项完全隐藏专项测试');
console.log('========================================================\n');
let passCount = 0;
let failCount = 0;
function runTest(title, fn) {
try {
fn();
console.log(`[PASS] ${title}`);
passCount++;
} catch (err) {
console.error(`[FAIL] ${title}`);
console.error(` 错误原因: ${err.message}`);
failCount++;
}
}
const rootDir = path.resolve(__dirname, '../../');
// 1. 验证 uniapp/pages/pay/components/account-type-select.vue
runTest('account-type-select.vue 源码实现 filteredTypeList 动态过滤与彻底隐藏未开启项', () => {
const file = path.join(rootDir, 'uniapp/pages/pay/components/account-type-select.vue');
assert.ok(fs.existsSync(file), 'account-type-select.vue 文件存在');
const content = fs.readFileSync(file, 'utf-8');
// 验证 template 中通过 filteredTypeList 遍历,而非全量 typeList
assert.ok(content.includes('v-for="(item, index) in filteredTypeList"'), '必须遍历 filteredTypeList');
assert.ok(!content.includes(':disabled="!methods.includes(item.value)"'), '严禁以 disabled 形式展示未开启的灰色提现项');
// 验证 computed 定义与过滤逻辑
assert.ok(content.includes('const filteredTypeList = computed('), '必须包含 filteredTypeList 计算属性');
assert.ok(content.includes('props.methods.includes(item.value)'), '必须根据 props.methods 严格过滤项');
// 验证空状态提示
assert.ok(content.includes('暂无可用的提现方式'), '包含全关闭时的友好兜底提示');
// 验证响应式同步
assert.ok(content.includes('function syncCurrentValue('), '包含有效提现方式同步校验函数');
});
// 2. 验证 uniapp/pages/pay/withdraw.vue 协同自愈逻辑
runTest('withdraw.vue 规则加载后自动清理后台已关闭的提现方式并支持单方式预选', () => {
const file = path.join(rootDir, 'uniapp/pages/pay/withdraw.vue');
assert.ok(fs.existsSync(file), 'withdraw.vue 文件存在');
const content = fs.readFileSync(file, 'utf-8');
// 验证对已失效提现方式的清理逻辑
assert.ok(
content.includes('!data.methods.includes(state.accountInfo.type)'),
'当后台关闭某提现方式时,清空当前失效选中'
);
// 验证单提现方式自动预选
assert.ok(
content.includes('data.methods.length === 1'),
'当后台仅开启1种提现方式时,自动预选该方式'
);
});
// 3. 核心算法模拟测试:矩阵过滤验证
const allTypes = [
{ icon: 'wechat.png', title: '微信零钱', value: 'wechat' },
{ icon: 'alipay.png', title: '支付宝账户', value: 'alipay' },
{ icon: 'bank.png', title: '银行卡转账', value: 'bank' },
];
function filterTypeList(methods) {
if (!Array.isArray(methods) || methods.length === 0) return [];
return allTypes.filter((item) => methods.includes(item.value));
}
runTest('场景1(如用户截图):后台仅开启银行卡,微信与支付宝必须完全隐藏', () => {
const result = filterTypeList(['bank']);
assert.strictEqual(result.length, 1);
assert.strictEqual(result[0].value, 'bank');
assert.strictEqual(result[0].title, '银行卡转账');
const hasWechat = result.some((r) => r.value === 'wechat');
const hasAlipay = result.some((r) => r.value === 'alipay');
assert.strictEqual(hasWechat, false, '微信零钱必须完全隐藏');
assert.strictEqual(hasAlipay, false, '支付宝账户必须完全隐藏');
});
runTest('场景2:后台开启微信和银行卡,支付宝关闭,仅显示微信和银行卡', () => {
const result = filterTypeList(['wechat', 'bank']);
assert.strictEqual(result.length, 2);
assert.deepStrictEqual(result.map(r => r.value), ['wechat', 'bank']);
const hasAlipay = result.some((r) => r.value === 'alipay');
assert.strictEqual(hasAlipay, false, '支付宝账户必须完全隐藏');
});
runTest('场景3:后台开启全部三种提现方式,三者均正常显示', () => {
const result = filterTypeList(['wechat', 'alipay', 'bank']);
assert.strictEqual(result.length, 3);
assert.deepStrictEqual(result.map(r => r.value), ['wechat', 'alipay', 'bank']);
});
runTest('场景4:后台关闭所有提现方式,列表为空且触发兜底', () => {
const result1 = filterTypeList([]);
assert.strictEqual(result1.length, 0);
const result2 = filterTypeList(null);
assert.strictEqual(result2.length, 0);
});
// 4. API 模拟测试
runTest('API 模拟测试:pay/withdraw/rules 返回仅包含 bank 的提现规则', () => {
const mockRulesResponse = {
code: 1,
msg: '提现规则',
data: {
status: 1,
is_withdraw_open: true,
min_amount: '1.00',
max_amount: '50000.00',
charge_rate_format: 0.5,
charge_rate: 0.005,
methods: ['bank'],
auto_arrival: 0,
}
};
assert.strictEqual(mockRulesResponse.code, 1);
assert.deepStrictEqual(mockRulesResponse.data.methods, ['bank']);
const visible = filterTypeList(mockRulesResponse.data.methods);
assert.strictEqual(visible.length, 1);
assert.strictEqual(visible[0].value, 'bank');
});
runTest('API 模拟测试:提现申请发起时若渠道已被关闭,服务端拦截', () => {
const mockConfig = { status: 1, methods: ['bank'] };
function simulateApply(type) {
if (!mockConfig.methods.includes(type)) {
return { code: 3001, message: '暂不支持该提现方式' };
}
return { code: 0, message: '操作成功', data: { withdraw_sn: 'W123456789' } };
}
const resWechat = simulateApply('wechat');
assert.strictEqual(resWechat.code, 3001);
assert.strictEqual(resWechat.message, '暂不支持该提现方式');
const resBank = simulateApply('bank');
assert.strictEqual(resBank.code, 0);
assert.strictEqual(resBank.data.withdraw_sn, 'W123456789');
});
runTest('API 模拟测试:参数异常(非法提现方式字符串)前端与接口防穿透拦截', () => {
const allowed = ['bank'];
const testInput = 'malicious_eval_type';
const isAllowed = allowed.includes(testInput);
assert.strictEqual(isAllowed, false);
});
console.log('\n========================================================');
console.log(`测试完成: 成功 ${passCount} 项, 失败 ${failCount} 项`);
console.log('========================================================');
if (failCount > 0) {
process.exit(1);
}

查看文件

@@ -0,0 +1,205 @@
/**
* 后台提现开关与端到端提现显隐控制专项测试
* 覆盖范围:
* 1. application/admin/view/shopro/config/index.html 后台「开启提现」视图控件
* 2. application/admin/controller/shopro/Config.php 提现开关参数持久化
* 3. application/admin/model/shopro/Config.php 提现开关配置项自愈与向下兼容
* 4. addons/shopro/service/Withdraw.php 提现开关读取与 checkApply 拦截
* 5. addons/shopro/controller/Index.php 全局平台配置下发 withdraw_status
* 6. uniapp/pages/user/wallet/money.vue 钱包页充值改提现、开关判断与按钮显隐
* 7. uniapp/pages/pay/withdraw.vue 提现申请页前置校验与安全防御
* 8. 提现开启状态算法判定矩阵与异常拦截模拟
*/
const fs = require('fs');
const path = require('path');
let passCount = 0;
let failCount = 0;
function assert(condition, message) {
if (condition) {
console.log(` \x1b[32m[PASS]\x1b[0m ${message}`);
passCount++;
} else {
console.error(` \x1b[31m[FAIL]\x1b[0m ${message}`);
failCount++;
}
}
console.log('========================================================');
console.log(' 后台提现开关与端到端提现显隐控制专项测试');
console.log('========================================================\n');
// ----------------------------------------------------
// 1. 后台视图配置 index.html 验证
// ----------------------------------------------------
console.log('--- 1. 后台提现配置视图 (index.html) 验证 ---');
const adminConfigHtmlPath = path.resolve(__dirname, '../../application/admin/view/shopro/config/index.html');
assert(fs.existsSync(adminConfigHtmlPath), 'config/index.html 文件存在');
if (fs.existsSync(adminConfigHtmlPath)) {
const content = fs.readFileSync(adminConfigHtmlPath, 'utf-8');
assert(content.includes('form.model.withdraw'), '包含提现配置表单区域');
assert(content.includes('label="开启提现"'), '包含「开启提现」表单项标签');
assert(content.includes('v-model="form.model.withdraw.status"'), '开启提现绑定 form.model.withdraw.status');
assert(content.includes(':active-value="1"') && content.includes(':inactive-value="0"'), 'switch 开关 active 值为 1,inactive 值为 0');
assert(content.includes('关闭提现后,用户端将隐藏提现入口且禁止提现操作'), '包含明确的后台辅助说明文案');
}
// ----------------------------------------------------
// 2. 后台配置控制器 Config.php 验证
// ----------------------------------------------------
console.log('\n--- 2. 后台配置控制器 (Config.php) 验证 ---');
const adminConfigPhpPath = path.resolve(__dirname, '../../application/admin/controller/shopro/Config.php');
assert(fs.existsSync(adminConfigPhpPath), 'Config.php 文件存在');
if (fs.existsSync(adminConfigPhpPath)) {
const content = fs.readFileSync(adminConfigPhpPath, 'utf-8');
assert(content.includes("public function rechargeWithdraw()"), '包含 rechargeWithdraw() 配置方法');
assert(content.includes("isset($params['withdraw']['status'])") || content.includes("$params['withdraw']['status']"), '处理了 withdraw.status 的入参保存');
assert(content.includes("intval($params['withdraw']['status'])"), 'withdraw.status 转为整型保证数据严密');
}
// ----------------------------------------------------
// 3. 模型层 Config.php 验证
// ----------------------------------------------------
console.log('\n--- 3. 配置模型 (app/admin/model/shopro/Config.php) 验证 ---');
const modelConfigPhpPath = path.resolve(__dirname, '../../application/admin/model/shopro/Config.php');
assert(fs.existsSync(modelConfigPhpPath), 'Config.php 模型文件存在');
if (fs.existsSync(modelConfigPhpPath)) {
const content = fs.readFileSync(modelConfigPhpPath, 'utf-8');
assert(content.includes("'shop.recharge_withdraw.withdraw.status'"), 'setConfigField 支持提现开关缺失时自动补齐创建');
assert(content.includes("'shop.recharge_withdraw.withdraw' && !isset($config['status'])"), 'getConfigs 支持提现配置缺省 status 时的智能兼容');
}
// ----------------------------------------------------
// 4. 服务层 Withdraw.php 与控制器验证
// ----------------------------------------------------
console.log('\n--- 4. 提现服务层 (Withdraw.php) 校验规则验证 ---');
const withdrawServicePath = path.resolve(__dirname, '../../addons/shopro/service/Withdraw.php');
assert(fs.existsSync(withdrawServicePath), 'Withdraw.php 服务文件存在');
if (fs.existsSync(withdrawServicePath)) {
const content = fs.readFileSync(withdrawServicePath, 'utf-8');
assert(content.includes("config['is_withdraw_open'] = (bool)($config['status'] === 1 && !empty($config['methods']))"), '构造函数综合计算 is_withdraw_open 标志');
assert(content.includes("intval($this->config['status']) === 0"), 'checkApply 校验提现总开关');
assert(content.includes("empty($this->config['methods'])"), 'checkApply 校验提现方式配置非空');
assert(content.includes('系统提现功能已关闭'), '提现关闭时抛出面向用户的明确提示文案');
}
const indexControllerPath = path.resolve(__dirname, '../../addons/shopro/controller/Index.php');
assert(fs.existsSync(indexControllerPath), 'Index.php 控制器存在');
if (fs.existsSync(indexControllerPath)) {
const content = fs.readFileSync(indexControllerPath, 'utf-8');
assert(content.includes("'withdraw_status' => (bool)"), 'Index.php init 接口下发 withdraw_status');
}
// ----------------------------------------------------
// 5. 用户端钱包资产页面 (money.vue) 对齐验证
// ----------------------------------------------------
console.log('\n--- 5. 用户端钱包资产页面 (money.vue) 验证 ---');
const moneyVuePath = path.resolve(__dirname, '../../uniapp/pages/user/wallet/money.vue');
assert(fs.existsSync(moneyVuePath), 'money.vue 文件存在');
if (fs.existsSync(moneyVuePath)) {
const content = fs.readFileSync(moneyVuePath, 'utf-8');
assert(content.includes('v-if="state.canWithdraw"'), '提现按钮使用 state.canWithdraw 控制显隐');
assert(content.includes('<text>提现</text>'), '按钮文案已由「充值」改为「提现」');
assert(!content.includes("@tap=\"sheep.$router.go('/pages/pay/recharge')\""), '已移除直接跳转充值页面的绑定');
assert(content.includes('@tap="onWithdraw"'), '点击事件绑定 onWithdraw');
assert(content.includes("canWithdraw: false"), 'state.canWithdraw 默认设为 false 防未授权闪现');
assert(content.includes("async function checkWithdrawStatus()"), '实现 checkWithdrawStatus 开关校验方法');
assert(content.includes("sheep.$api.pay.withdraw.rules()"), '调用提现规则接口获取实时开关状态');
assert(content.includes("sheep.$router.go('/pages/pay/withdraw')"), '校验通过后跳转标准提现页面 /pages/pay/withdraw');
assert(content.includes("checkWithdrawStatus()"), 'onLoad 与 onPullDownRefresh 触发检测');
}
// ----------------------------------------------------
// 6. 用户端申请提现页面 (withdraw.vue) 对齐验证
// ----------------------------------------------------
console.log('\n--- 6. 用户端申请提现页面 (withdraw.vue) 验证 ---');
const withdrawVuePath = path.resolve(__dirname, '../../uniapp/pages/pay/withdraw.vue');
assert(fs.existsSync(withdrawVuePath), 'withdraw.vue 文件存在');
if (fs.existsSync(withdrawVuePath)) {
const content = fs.readFileSync(withdrawVuePath, 'utf-8');
assert(content.includes('isWithdrawOpen'), '包含 isWithdrawOpen 响应式开关状态');
assert(content.includes("state.isWithdrawOpen ? '确认提现' : '提现暂未开启'"), '提现关闭时按钮提示「提现暂未开启」');
assert(content.includes("!state.isWithdrawOpen"), '提现关闭时按钮呈现灰度禁用态并在 onConfirm 中拦截');
}
// ----------------------------------------------------
// 7. 提现状态综合判定算法矩阵与异常拦截模拟
// ----------------------------------------------------
console.log('\n--- 7. 提现状态综合判定算法矩阵与异常拦截模拟 ---');
function evaluateWithdrawOpen(config) {
const status = config.status !== undefined
? Number(config.status)
: (Array.isArray(config.methods) && config.methods.length > 0 ? 1 : 0);
const hasMethods = Array.isArray(config.methods) && config.methods.length > 0;
return status === 1 && hasMethods;
}
function simulateCheckApply(config, method, amount, commission) {
if (config.status !== undefined && Number(config.status) === 0) {
throw new Error('系统提现功能已关闭');
}
if (!config.methods || config.methods.length === 0) {
throw new Error('暂未开启任何提现方式');
}
if (!config.methods.includes(method)) {
throw new Error('暂不支持该提现方式');
}
if (amount <= 0) {
throw new Error('请输入正确的提现金额');
}
if (commission < amount) {
throw new Error('可提现佣金不足');
}
return { ok: true, message: '提现申请校验通过' };
}
// 场景 1: 开启且有方式
const case1 = evaluateWithdrawOpen({ status: 1, methods: ['wechat', 'alipay'] });
assert(case1 === true, '场景1: status=1 且 methods 非空 => 允许提现 (显示按钮)');
// 场景 2: 关闭且有方式
const case2 = evaluateWithdrawOpen({ status: 0, methods: ['wechat', 'alipay'] });
assert(case2 === false, '场景2: status=0 且 methods 非空 => 禁止提现 (隐藏按钮)');
// 场景 3: 开启但无方式
const case3 = evaluateWithdrawOpen({ status: 1, methods: [] });
assert(case3 === false, '场景3: status=1 但 methods 为空 => 禁止提现 (隐藏按钮)');
// 场景 4: 未配置 status 但有方式 (历史升级平滑兼容)
const case4 = evaluateWithdrawOpen({ methods: ['bank'] });
assert(case4 === true, '场景4: 未配置 status 但 methods 非空 => 自动兼容开启 (显示按钮)');
// 场景 5: 未配置 status 且 methods 为空
const case5 = evaluateWithdrawOpen({ methods: [] });
assert(case5 === false, '场景5: 未配置 status 且 methods 为空 => 判定关闭 (隐藏按钮)');
// 场景 6: 接口级别直接阻断测试
let errorMsg = '';
try {
simulateCheckApply({ status: 0, methods: ['wechat'] }, 'wechat', 100, 500);
} catch (e) {
errorMsg = e.message;
}
assert(errorMsg === '系统提现功能已关闭', '直接调用接口: status=0 时被正确拦截抛出「系统提现功能已关闭」');
errorMsg = '';
try {
simulateCheckApply({ status: 1, methods: [] }, 'wechat', 100, 500);
} catch (e) {
errorMsg = e.message;
}
assert(errorMsg === '暂未开启任何提现方式', '直接调用接口: methods 为空时被正确拦截抛出「暂未开启任何提现方式」');
const successResult = simulateCheckApply({ status: 1, methods: ['wechat'] }, 'wechat', 100, 500);
assert(successResult.ok === true, '直接调用接口: 条件完全满足时正常放行提现');
console.log('\n========================================================');
console.log(`测试完成!通过: ${passCount}, 失败: ${failCount}`);
console.log('========================================================');
if (failCount > 0) {
process.exit(1);
}

查看文件

@@ -30,13 +30,12 @@ assert(
'未升级为 asset-card 结构'
);
// 3. 验证余额与充值按钮
// 3. 验证余额与资金操作按钮
assert(
content.includes('userInfo.money') &&
content.includes('/pages/pay/recharge') &&
content.includes('充值'),
'包含余额展示与充值跳转入口',
'缺少余额绑定或充值入口'
(content.includes('/pages/pay/recharge') || content.includes('onWithdraw') || content.includes('state.canWithdraw')),
'包含余额展示与资金操作入口',
'缺少余额绑定或资金操作入口'
);
// 4. 验证金额显隐切换

查看文件

@@ -5,10 +5,10 @@
<text class="modal-title ss-m-b-20">选择提现方式</text>
</view>
<view class="modal-content ss-flex-1 ss-p-b-100">
<radio-group @change="onChange">
<radio-group @change="onChange" v-if="filteredTypeList.length > 0">
<label
class="container-list ss-p-l-34 ss-p-r-24 ss-flex ss-col-center ss-row-center"
v-for="(item, index) in typeList"
v-for="(item, index) in filteredTypeList"
:key="index"
>
<view class="container-icon ss-flex ss-m-r-20">
@@ -20,10 +20,12 @@
:value="item.value"
color="var(--ui-BG-Main)"
:checked="item.value === state.currentValue"
:disabled="!methods.includes(item.value)"
/>
</label>
</radio-group>
<view v-else class="empty-tip ss-p-40 ss-text-center" style="padding: 40rpx; text-align: center; color: #999; font-size: 26rpx;">
暂无可用的提现方式
</view>
</view>
<view class="modal-footer ss-flex ss-row-center ss-col-center">
<button class="ss-reset-button save-btn" @tap="onConfirm">确定</button>
@@ -33,7 +35,7 @@
</template>
<script setup>
import { reactive, onBeforeMount, nextTick } from 'vue';
import { reactive, onBeforeMount, computed, watch } from 'vue';
import sheep from '@/sheep';
const props = defineProps({
@@ -47,7 +49,7 @@
},
methods: {
type: Array,
default: [],
default: () => [],
},
});
const emits = defineEmits(['update:modelValue', 'change', 'close']);
@@ -71,23 +73,89 @@
value: 'bank',
},
];
// 仅展示后台已开启的提现方式(后台关闭的在此彻底隐藏)
const filteredTypeList = computed(() => {
if (!Array.isArray(props.methods) || props.methods.length === 0) {
return [];
}
return typeList.filter((item) => props.methods.includes(item.value));
});
function syncCurrentValue() {
const validMethods = Array.isArray(props.methods) ? props.methods : [];
// 1. 若当前已选中的值在后台开启的列表中,优先保留
if (props.modelValue?.type && validMethods.includes(props.modelValue.type)) {
state.currentValue = props.modelValue.type;
return;
}
// 2. 若 state.currentValue 仍属于有效开启列表,保留
if (state.currentValue && validMethods.includes(state.currentValue)) {
return;
}
// 3. 若仅有 1 种开启的提现方式,默认选中
if (validMethods.length === 1) {
state.currentValue = validMethods[0];
return;
}
// 4. 若开启了多种但未选择或选择已失效,且开启列表非空,默认选择首项
if (validMethods.length > 0 && !validMethods.includes(state.currentValue)) {
state.currentValue = validMethods[0];
return;
}
state.currentValue = '';
}
watch(
() => props.methods,
() => {
syncCurrentValue();
},
{ immediate: true, deep: true }
);
watch(
() => props.modelValue,
() => {
syncCurrentValue();
},
{ immediate: true, deep: true }
);
watch(
() => props.show,
(val) => {
if (val) {
syncCurrentValue();
}
}
);
const getWalletAccountInfo = async () => {
if (!state.currentValue) return;
return new Promise(async (resolve, reject) => {
let res = await sheep.$api.user.account.info({
type: state.currentValue,
});
if (res.code === 1) {
if (!props.methods.includes(res.data.type)) {
return;
}
state.currentValue = res.data.type;
emits('update:modelValue', {
type: res.data.type,
account_header: res.data.account_header,
account_name: res.data.account_name,
account_no: res.data.account_no,
try {
let res = await sheep.$api.user.account.info({
type: state.currentValue,
});
} else {
if (res.code === 1) {
if (!props.methods.includes(res.data.type)) {
resolve();
return;
}
state.currentValue = res.data.type;
emits('update:modelValue', {
type: res.data.type,
account_header: res.data.account_header,
account_name: res.data.account_name,
account_no: res.data.account_no,
});
} else {
emits('update:modelValue', {
type: state.currentValue,
});
}
} catch (e) {
emits('update:modelValue', {
type: state.currentValue,
});
@@ -105,6 +173,10 @@
sheep.$helper.toast('请选择提现方式');
return;
}
if (!props.methods.includes(state.currentValue)) {
sheep.$helper.toast('该提现方式暂未开启');
return;
}
await getWalletAccountInfo();
emits('close');
};
@@ -114,7 +186,10 @@
};
onBeforeMount(async () => {
await getWalletAccountInfo();
syncCurrentValue();
if (state.currentValue) {
await getWalletAccountInfo();
}
});
</script>

查看文件

@@ -1,5 +1,5 @@
<template>
<s-layout title="申请提现" class="withdraw-wrap" navbar="inner">
<s-layout :title="state.walletType === 'madow' ? 'dmepa提现' : '申请提现'" class="withdraw-wrap" navbar="inner">
<!-- <view class="page-bg"></view> -->
<view
class="wallet-num-box ss-flex ss-col-center ss-row-between"
@@ -10,7 +10,14 @@
},
]"
>
<view class="">
<view class="" v-if="state.walletType === 'madow'">
<view class="num-title">可提现dmepa(枚)</view>
<view class="wallet-num">{{ state.madowInfo.madow || '0' }}</view>
<view class="ss-m-t-10" style="font-size: 22rpx; color: rgba(255,255,255,0.85);">
预估总市值:¥{{ state.madowInfo.madow_value || '0.00' }} (1 dmepa ≈ ¥{{ state.madowInfo.madow_rate || '0' }})
</view>
</view>
<view class="" v-else>
<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);">
@@ -23,7 +30,16 @@
</view>
<!-- 提现输入卡片-->
<view class="draw-card">
<view class="card-title">提现金额</view>
<view class="card-title ss-flex ss-row-between ss-col-center">
<text>提现金额</text>
<text
v-if="state.walletType === 'madow' && Number(state.madowInfo.madow_value) > 0"
style="font-size: 24rpx; color: #b78328; font-weight: 500;"
@tap="onWithdrawAllMadow"
>
全部提现
</text>
</view>
<view class="input-box ss-flex ss-col-center border-bottom">
<view class="unit">¥</view>
<uni-easyinput
@@ -34,14 +50,20 @@
placeholder="请输入提现金额"
/>
</view>
<view class="bank-box ss-flex ss-col-center ss-row-between ss-m-b-30">
<view
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' }})
</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 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>
<view v-if="state.accountInfo.type === 'wechat'" class="empty-text">微信零钱</view>
<view v-if="state.accountInfo.type === 'alipay'" class="empty-text">支付宝账户</view>
<view v-if="state.accountInfo.type === 'bank'" class="empty-text">银行卡转账</view>
<text class="cicon-forward"></text>
<text class="cicon-forward" v-if="state.walletType !== 'madow'"></text>
</view>
</view>
<view class="bind-box ss-flex ss-col-center ss-row-between" v-if="state.accountInfo.type">
@@ -53,17 +75,24 @@
{{ state.accountInfo.account_name ? '修改' : '添加' }}
</button>
</view>
<button class="ss-reset-button save-btn ui-BG-Main-Gradient ui-Shadow-Main" @tap="onConfirm">
确认提现
<button
class="ss-reset-button save-btn ui-BG-Main-Gradient ui-Shadow-Main"
:style="!state.isWithdrawOpen ? 'opacity: 0.6; filter: grayscale(1);' : ''"
@tap="onConfirm"
>
{{ state.isWithdrawOpen ? '确认提现' : '提现暂未开启' }}
</button>
</view>
<!-- 提现说明 -->
<view class="draw-notice">
<view class="title ss-m-b-20">提现说明</view>
<view class="tip-banner ss-m-b-20" style="background: #fff8f2; color: #ff6000; font-size: 22rpx; padding: 16rpx 20rpx; border-radius: 12rpx; line-height: 1.5;">
<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 将全额原路退回。
</view>
<view class="draw-list" v-for="(rule, index) in state.rulesList" :key="index">
{{ index + 1 }}.{{ rule }}
</view>
@@ -75,7 +104,7 @@
@close="onAccountSelect(false)"
round="10"
v-model="state.accountInfo"
:methods="state.rules.methods"
:methods="state.walletType === 'madow' ? ['bank'] : state.rules.methods"
/>
<!-- 编辑账户信息 -->
<account-info-modal
@@ -90,11 +119,10 @@
<script setup>
import { computed, reactive, onBeforeMount } from 'vue';
import { onShow } from '@dcloudio/uni-app';
import { onShow, onLoad, onPageScroll } from '@dcloudio/uni-app';
import sheep from '@/sheep';
import accountTypeSelect from './components/account-type-select.vue';
import accountInfoModal from './components/account-info-modal.vue';
import { onPageScroll } from '@dcloudio/uni-app';
const headerBg = sheep.$url.css('/assets/addons/shopro/uniapp/user/withdraw_bg.png');
onPageScroll(() => {});
@@ -131,11 +159,19 @@
const userStore = sheep.$store('user');
const userInfo = computed(() => userStore.userInfo);
const state = reactive({
walletType: '',
amount: '',
type: '',
accountInfo: {},
accountSelect: false,
accountEdit: false,
madowInfo: {
madow: '0',
madow_rate: '0',
madow_value: '0.00',
can_withdraw_madow: 1,
madow_withdraw_min: 1,
},
rules: {
min_amount: 0,
max_amount: 0,
@@ -146,8 +182,63 @@
methods: [],
},
rulesList: [],
isWithdrawOpen: true,
});
const estimatedDeductMadow = computed(() => {
if (!state.amount || !state.madowInfo.madow_rate || Number(state.madowInfo.madow_rate) <= 0) return '0.0000';
const deduct = Number(state.amount) / Number(state.madowInfo.madow_rate);
return deduct.toFixed(4);
});
function onWithdrawAllMadow() {
if (state.madowInfo.madow_value) {
state.amount = String(state.madowInfo.madow_value);
}
}
async function fetchUserBankAccount() {
try {
let res = await sheep.$api.user.account.info({
type: 'bank',
});
if (res?.code === 1 && res.data) {
state.accountInfo = {
type: 'bank',
account_header: res.data.account_header,
account_name: res.data.account_name,
account_no: res.data.account_no,
};
}
} catch (e) {}
}
async function getMadowInfo() {
try {
const res = await sheep.$api.partner.madow();
if (res?.code === 1 && res.data) {
state.madowInfo = {
madow: res.data.madow || '0',
madow_rate: res.data.madow_rate || '0',
madow_value: res.data.madow_value || '0.00',
can_withdraw_madow: res.data.can_withdraw_madow ?? 1,
madow_withdraw_min: res.data.madow_withdraw_min ?? 1,
};
const isOpen = state.madowInfo.can_withdraw_madow !== 0 && state.madowInfo.can_withdraw_madow !== false;
state.isWithdrawOpen = Boolean(isOpen);
if (!state.isWithdrawOpen) {
sheep.$helper.toast('dmepa提现暂未开启');
}
state.rulesList = [
`dmepa按大盘实时市值换算扣除(当前 1 dmepa ≈ ¥${state.madowInfo.madow_rate});`,
`单次提现金额最低 ${state.madowInfo.madow_withdraw_min || 1} 元;`,
'提现仅支持打款至银行卡,申请提交后由财务人工审核;',
'审核驳回或打款失败后,对应dmepa将全额原路退还至账户持仓。',
];
}
} catch (e) {}
}
const onAccountEdit = (e) => {
state.accountEdit = e;
};
@@ -157,19 +248,43 @@
};
const onConfirm = async () => {
if (!state.isWithdrawOpen) {
sheep.$helper.toast(state.walletType === 'madow' ? 'dmepa提现暂未开启' : '系统提现功能暂未开启');
return;
}
let payload = {
money: state.amount,
...state.accountInfo,
};
if (payload.money > userInfo.commission || payload.money <= 0) {
sheep.$helper.toast('请输入正确的提现金额');
return;
}
if (state.walletType === 'madow') {
payload.wallet_type = 'madow';
payload.type = 'bank';
if (!payload.type) {
sheep.$helper.toast('请选择提现方式');
return;
if (Number(payload.money) <= 0) {
sheep.$helper.toast('请输入正确的提现金额');
return;
}
const minMadowCny = Number(state.madowInfo.madow_withdraw_min || 1);
if (minMadowCny > 0 && Number(payload.money) < minMadowCny) {
sheep.$helper.toast(`最低提现金额为 ${minMadowCny} 元`);
return;
}
if (Number(payload.money) > Number(state.madowInfo.madow_value || 0)) {
sheep.$helper.toast('提现金额超出持有dmepa可用市值');
return;
}
} else {
if (payload.money > userInfo.commission || payload.money <= 0) {
sheep.$helper.toast('请输入正确的提现金额');
return;
}
if (!payload.type) {
sheep.$helper.toast('请选择提现方式');
return;
}
}
if (!payload.account_name || !payload.account_header || !payload.account_no) {
@@ -189,6 +304,13 @@
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') {
sheep.$platform.useProvider('wechat').transfer(data.transfer_data);
@@ -200,16 +322,57 @@
let { code, data } = await sheep.$api.pay.withdraw.rules();
if (code === 1) {
state.rules = data;
state.rulesList = filterRules(state.rules);
if (state.walletType !== 'madow') {
state.rulesList = filterRules(state.rules);
const isOpen = (data.status === undefined || Number(data.status) === 1)
&& Array.isArray(data.methods)
&& data.methods.length > 0;
state.isWithdrawOpen = Boolean(isOpen);
if (!state.isWithdrawOpen) {
sheep.$helper.toast('系统提现功能暂未开启');
}
// 如果当前选中的方式已被后台关闭,清空该选择
if (state.accountInfo.type && Array.isArray(data.methods) && !data.methods.includes(state.accountInfo.type)) {
state.accountInfo = {};
}
// 若当前未选择提现方式,且后台仅开启了 1 种提现方式,默认选中该方式并自动读取对应账户
if (!state.accountInfo.type && Array.isArray(data.methods) && data.methods.length === 1) {
const onlyType = data.methods[0];
state.accountInfo.type = onlyType;
try {
let res = await sheep.$api.user.account.info({ type: onlyType });
if (res?.code === 1 && res.data) {
state.accountInfo = {
type: onlyType,
account_header: res.data.account_header,
account_name: res.data.account_name,
account_no: res.data.account_no,
};
}
} catch (e) {}
}
}
}
}
onLoad(async (options) => {
if (options?.type === 'madow') {
state.walletType = 'madow';
state.accountInfo.type = 'bank';
await getMadowInfo();
fetchUserBankAccount();
}
});
onBeforeMount(() => {
getWithdrawRules();
});
onShow(() => {
userStore.getInfo();
if (state.walletType === 'madow') {
getMadowInfo();
}
});
</script>

查看文件

@@ -7,7 +7,6 @@
<view class="card-top ss-flex ss-row-between ss-col-center">
<view class="top-tag ss-flex ss-col-center">
<text class="tag-badge">现金资产</text>
<text class="top-title">账户钱包余额</text>
</view>
<view
class="eye-btn ss-flex ss-col-center"
@@ -29,23 +28,15 @@
<text class="hero-unit">元</text>
</view>
<button
v-if="state.canWithdraw"
class="ss-reset-button topup-btn ss-flex ss-row-center ss-col-center"
@tap="sheep.$router.go('/pages/pay/recharge')"
@tap="onWithdraw"
>
<text class="cicon-moneybag ss-m-r-6"></text>
<text>充值</text>
<text>提现</text>
</button>
</view>
<!-- 动态指示胶囊 -->
<view class="rate-capsule ss-flex ss-row-between ss-col-center">
<view class="capsule-left ss-flex ss-col-center">
<text class="capsule-dot"></text>
<text class="capsule-text">线上充值与收益分成全额入账 · 下单直接抵扣</text>
</view>
<text class="capsule-tag">即时可用</text>
</view>
<!-- 底部数据栏:当前周期总收入 / 总支出 -->
<view class="card-footer ss-flex ss-row-between ss-col-center">
<view class="footer-metric">
@@ -173,6 +164,7 @@
const state = reactive({
showMoney: true,
canWithdraw: false,
date: [],
currentTab: 0,
pagination,
@@ -210,6 +202,32 @@
}
});
async function checkWithdrawStatus() {
try {
const appStore = sheep.$store('app');
if (appStore?.platform && typeof appStore.platform.withdraw_status === 'boolean') {
state.canWithdraw = appStore.platform.withdraw_status;
}
const res = await sheep.$api.pay.withdraw.rules();
if (res?.code === 1 && res.data) {
const isOpen = (res.data.status === undefined || Number(res.data.status) === 1)
&& Array.isArray(res.data.methods)
&& res.data.methods.length > 0;
state.canWithdraw = Boolean(isOpen);
}
} catch (e) {
state.canWithdraw = false;
}
}
function onWithdraw() {
if (!state.canWithdraw) {
sheep.$helper.toast('暂未开启提现功能');
return;
}
sheep.$router.go('/pages/pay/withdraw');
}
async function getLogList(page = 1, list_rows = 10) {
state.loadStatus = 'loading';
try {
@@ -239,11 +257,13 @@
state.today = dayjs().format('YYYY-MM-DD');
state.date = [state.today, state.today];
sheep.$store('user').getInfo();
checkWithdrawStatus();
getLogList(1);
});
onPullDownRefresh(async () => {
await sheep.$store('user').getInfo();
await checkWithdrawStatus();
await getLogList(1);
uni.stopPullDownRefresh();
});
@@ -318,14 +338,6 @@
padding: 4rpx 14rpx;
border-radius: 8rpx;
font-weight: 600;
margin-right: 12rpx;
}
.top-title {
font-size: 26rpx;
color: rgba(255, 255, 255, 0.85);
font-weight: 400;
letter-spacing: 1rpx;
}
}
@@ -393,39 +405,6 @@
}
}
/* 胶囊指示栏 */
.rate-capsule {
margin-top: 24rpx;
background: rgba(244, 211, 151, 0.1);
border: 1rpx solid rgba(244, 211, 151, 0.22);
border-radius: 12rpx;
padding: 14rpx 20rpx;
position: relative;
z-index: 2;
.capsule-left {
.capsule-dot {
width: 10rpx;
height: 10rpx;
border-radius: 50%;
background-color: #eabf76;
margin-right: 12rpx;
box-shadow: 0 0 8rpx #eabf76;
}
.capsule-text {
font-size: 24rpx;
color: #fce8bf;
font-weight: 500;
}
}
.capsule-tag {
font-size: 20rpx;
color: #f7d598;
opacity: 0.85;
}
}
/* 底部收支汇总栏 */
.card-footer {