feat(withdraw): 后台支持提现总开关与方式过滤并升级钱包与提现页面体验
这个提交包含在:
@@ -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 {
|
||||
|
||||
在新工单中引用
屏蔽一个用户