diff --git a/addons/shopro/controller/Withdraw.php b/addons/shopro/controller/Withdraw.php
index 0a9edb6..6513d5e 100644
--- a/addons/shopro/controller/Withdraw.php
+++ b/addons/shopro/controller/Withdraw.php
@@ -29,6 +29,9 @@ class Withdraw extends Common
{
$user = auth_user();
$config = (new WithdrawService($user))->config;
+ $config['madow_withdraw_enable'] = \addons\shopro\service\partner\PartnerConfig::isMadowWithdrawEnabled() ? 1 : 0;
+ $config['madow_min_amount'] = \addons\shopro\service\partner\PartnerConfig::madowWithdrawMin();
+ $config['madow_charge_rate'] = \addons\shopro\service\partner\PartnerConfig::madowWithdrawChargeRate();
$this->success('提现规则', $config);
}
diff --git a/addons/shopro/controller/partner/Index.php b/addons/shopro/controller/partner/Index.php
index 878a3ba..4d6433f 100644
--- a/addons/shopro/controller/partner/Index.php
+++ b/addons/shopro/controller/partner/Index.php
@@ -169,6 +169,7 @@ class Index extends Commission
'show_madow_rate' => PartnerConfig::showMadowRate(),
'can_withdraw_madow' => PartnerConfig::isMadowWithdrawEnabled() ? 1 : 0,
'madow_withdraw_min' => PartnerConfig::madowWithdrawMin(),
+ 'madow_withdraw_charge_rate' => PartnerConfig::madowWithdrawChargeRate(),
'rule_title' => PartnerConfig::madowRuleTitle(),
'rules' => PartnerConfig::madowRules(),
'list' => $list,
diff --git a/addons/shopro/service/Withdraw.php b/addons/shopro/service/Withdraw.php
index 4c0ecdc..b20c83f 100755
--- a/addons/shopro/service/Withdraw.php
+++ b/addons/shopro/service/Withdraw.php
@@ -160,6 +160,15 @@ class Withdraw
throw new ShoproException('持仓dmepa不足,无法提现');
}
+ // 手续费计算 (内扣模式)
+ $chargeRatePercent = PartnerConfig::madowWithdrawChargeRate();
+ $chargeRate = round($chargeRatePercent * 0.01, 4);
+ $chargeFee = function_exists('bcmul') ? bcmul((string)$money, (string)$chargeRate, 2) : number_format(floatval($money) * $chargeRate, 2, '.', '');
+ if (floatval($chargeFee) > 0 && floatval($money) <= floatval($chargeFee)) {
+ throw new ShoproException('提现金额必须大于手续费');
+ }
+ $realMoney = bcsub((string)$money, (string)$chargeFee, 2);
+
// 校验银行卡信息
$withdrawInfo = $this->getAccountInfo($type, $params);
$rateClean = rtrim(rtrim(number_format((float)$rate, 12, '.', ''), '0'), '.');
@@ -168,9 +177,15 @@ class Withdraw
$withdrawInfo['扣除DMEPA'] = $deductMadow . ' DMEPA';
$withdrawInfo['市值汇率'] = '1 DMEPA ≈ ' . $rateClean . ' 元';
$withdrawInfo['换算市值'] = $moneyClean . ' 元';
+ if (floatval($chargeFee) > 0 || floatval($chargeRate) > 0) {
+ $withdrawInfo['提现手续费'] = number_format(floatval($chargeFee), 2, '.', '') . ' 元 (' . $chargeRatePercent . '%)';
+ }
+ $withdrawInfo['预计到账'] = number_format(floatval($realMoney), 2, '.', '') . ' 元';
$withdrawInfo['wallet_type'] = 'madow';
$withdrawInfo['madow_amount'] = $deductMadow;
$withdrawInfo['rate'] = $rate;
+ $withdrawInfo['charge_fee'] = $chargeFee;
+ $withdrawInfo['charge_rate'] = $chargeRate;
// 扣减 dmepa
$agent->setDec('madow', $deductMadow);
@@ -179,8 +194,8 @@ class Withdraw
$withdraw = new WithdrawModel();
$withdraw->user_id = $this->user->id;
$withdraw->amount = $money;
- $withdraw->charge_fee = 0;
- $withdraw->charge_rate = 0;
+ $withdraw->charge_fee = $chargeFee;
+ $withdraw->charge_rate = $chargeRate;
$withdraw->withdraw_sn = get_sn($this->user->id, 'W');
$withdraw->withdraw_type = 'bank';
$withdraw->withdraw_info = $withdrawInfo;
@@ -205,6 +220,9 @@ class Withdraw
'deduct_madow' => $deductMadow,
'cny_amount' => $money,
'rate' => $rate,
+ 'charge_fee' => $chargeFee,
+ 'charge_rate' => $chargeRate,
+ 'real_amount' => $realMoney,
],
'settle_date' => date('Y-m-d'),
'status' => 1,
diff --git a/addons/shopro/service/partner/PartnerConfig.php b/addons/shopro/service/partner/PartnerConfig.php
index 42880a5..a2b678c 100644
--- a/addons/shopro/service/partner/PartnerConfig.php
+++ b/addons/shopro/service/partner/PartnerConfig.php
@@ -51,6 +51,12 @@ class PartnerConfig
public static function isMadowWithdrawEnabled()
{
+ if (function_exists('sheep_config')) {
+ $withdrawConfig = sheep_config('shop.recharge_withdraw.withdraw');
+ if (isset($withdrawConfig['madow_withdraw_enable']) && $withdrawConfig['madow_withdraw_enable'] !== null && $withdrawConfig['madow_withdraw_enable'] !== '') {
+ return (bool) intval($withdrawConfig['madow_withdraw_enable']);
+ }
+ }
$enable = self::get('partner_madow_withdraw_enable', 1);
if ($enable === null || $enable === '') {
return 1;
@@ -60,6 +66,12 @@ class PartnerConfig
public static function madowWithdrawMin()
{
+ if (function_exists('sheep_config')) {
+ $withdrawConfig = sheep_config('shop.recharge_withdraw.withdraw');
+ if (isset($withdrawConfig['madow_min_amount']) && is_numeric($withdrawConfig['madow_min_amount'])) {
+ return max(0.0, floatval($withdrawConfig['madow_min_amount']));
+ }
+ }
$min = self::get('partner_madow_withdraw_min', 1);
if ($min === null || $min === '' || !is_numeric($min)) {
return 1.0;
@@ -67,6 +79,21 @@ class PartnerConfig
return max(0.0, floatval($min));
}
+ public static function madowWithdrawChargeRate()
+ {
+ if (function_exists('sheep_config')) {
+ $withdrawConfig = sheep_config('shop.recharge_withdraw.withdraw');
+ if (isset($withdrawConfig['madow_charge_rate']) && is_numeric($withdrawConfig['madow_charge_rate'])) {
+ return max(0.0, floatval($withdrawConfig['madow_charge_rate']));
+ }
+ }
+ $rate = self::get('partner_madow_withdraw_charge_rate', 0);
+ if ($rate === null || $rate === '' || !is_numeric($rate)) {
+ return 0.0;
+ }
+ return max(0.0, floatval($rate));
+ }
+
public static function madowRuleTitle()
{
$title = trim((string) self::get('partner_madow_rule_title', '市值与增值规则')) ?: '市值与增值规则';
diff --git a/application/admin/controller/shopro/Config.php b/application/admin/controller/shopro/Config.php
index 5a88ed0..7be5fe8 100644
--- a/application/admin/controller/shopro/Config.php
+++ b/application/admin/controller/shopro/Config.php
@@ -197,7 +197,7 @@ class Config extends Common
'partner_show_madow_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_madow_withdraw_enable', 'partner_madow_withdraw_min', 'partner_madow_withdraw_charge_rate',
'partner_voucher_times', 'partner_voucher_days',
'partner_voucher_rule_title', 'partner_voucher_rules',
'partner_maker_bonus', 'partner_maker_peer_rate',
@@ -238,6 +238,7 @@ class Config extends Common
'partner_show_madow_rate' => 1,
'partner_madow_withdraw_enable' => 1,
'partner_madow_withdraw_min' => 1,
+ 'partner_madow_withdraw_charge_rate' => 0,
'partner_madow_base_rate' => '0.00000075',
'partner_madow_step_orders' => 1,
'partner_voucher_times' => 1.5,
@@ -266,6 +267,7 @@ class Config extends Common
$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;
+ $configs['partner_madow_withdraw_charge_rate'] = isset($configs['partner_madow_withdraw_charge_rate']) ? floatval($configs['partner_madow_withdraw_charge_rate']) : 0.0;
foreach (['partner_madow_rules', 'partner_voucher_rules'] as $ruleKey) {
if (isset($configs[$ruleKey]) && is_string($configs[$ruleKey])) {
$decoded = json_decode($configs[$ruleKey], true);
@@ -292,6 +294,9 @@ class Config extends Common
if (isset($save['partner_madow_withdraw_min'])) {
$save['partner_madow_withdraw_min'] = max(0.0, floatval($save['partner_madow_withdraw_min']));
}
+ if (isset($save['partner_madow_withdraw_charge_rate'])) {
+ $save['partner_madow_withdraw_charge_rate'] = max(0.0, floatval($save['partner_madow_withdraw_charge_rate']));
+ }
// 确保数据库存在 partner_show_madow_rate 配置行
$existConfig = ShoproConfig::where('code', 'shop.commission.partner_show_madow_rate')->find();
if (!$existConfig) {
@@ -309,6 +314,7 @@ class Config extends Common
$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_withdraw_charge_rate' => ['name' => 'dmepa提现手续费率', 'desc' => 'dmepa提现手续费率(%)', 'type' => 'number', 'value' => '0', '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],
@@ -411,6 +417,29 @@ class Config extends Common
} else {
$configs['withdraw']['status'] = intval($configs['withdraw']['status']);
}
+
+ // 读取 DMEPA 专属提现配置(优先提现配置,降级兼容合伙股东历史配置)
+ $partnerEnable = ShoproConfig::getConfigField('shop.commission.partner_madow_withdraw_enable', false);
+ $partnerMin = ShoproConfig::getConfigField('shop.commission.partner_madow_withdraw_min', false);
+ $partnerChargeRate = ShoproConfig::getConfigField('shop.commission.partner_madow_withdraw_charge_rate', false);
+
+ if (!isset($configs['withdraw']['madow_withdraw_enable']) || $configs['withdraw']['madow_withdraw_enable'] === '') {
+ $configs['withdraw']['madow_withdraw_enable'] = ($partnerEnable !== null && $partnerEnable !== '') ? intval($partnerEnable) : 1;
+ } else {
+ $configs['withdraw']['madow_withdraw_enable'] = intval($configs['withdraw']['madow_withdraw_enable']);
+ }
+
+ if (!isset($configs['withdraw']['madow_min_amount']) || $configs['withdraw']['madow_min_amount'] === '') {
+ $configs['withdraw']['madow_min_amount'] = ($partnerMin !== null && $partnerMin !== '') ? floatval($partnerMin) : 1.0;
+ } else {
+ $configs['withdraw']['madow_min_amount'] = floatval($configs['withdraw']['madow_min_amount']);
+ }
+
+ if (!isset($configs['withdraw']['madow_charge_rate']) || $configs['withdraw']['madow_charge_rate'] === '') {
+ $configs['withdraw']['madow_charge_rate'] = ($partnerChargeRate !== null && $partnerChargeRate !== '') ? floatval($partnerChargeRate) : 0.0;
+ } else {
+ $configs['withdraw']['madow_charge_rate'] = floatval($configs['withdraw']['madow_charge_rate']);
+ }
}
} elseif ('POST' === $this->request->method()) {
$params = $this->request->param();
@@ -428,7 +457,38 @@ class Config extends Common
} else {
$params['withdraw']['status'] = intval($params['withdraw']['status']);
}
+
+ if (isset($params['withdraw'])) {
+ if (isset($params['withdraw']['madow_withdraw_enable'])) {
+ $params['withdraw']['madow_withdraw_enable'] = intval($params['withdraw']['madow_withdraw_enable']);
+ }
+ if (isset($params['withdraw']['madow_charge_rate'])) {
+ $params['withdraw']['madow_charge_rate'] = max(0.0, floatval($params['withdraw']['madow_charge_rate']));
+ }
+ if (isset($params['withdraw']['madow_min_amount'])) {
+ $params['withdraw']['madow_min_amount'] = max(0.0, floatval($params['withdraw']['madow_min_amount']));
+ }
+ }
+
$configs = ShoproConfig::setConfigs('shop.recharge_withdraw', $params);
+
+ // 双向同步合伙股东配置项,确保老服务读取与全链路向后兼容
+ if (isset($params['withdraw']['madow_withdraw_enable'])) {
+ ShoproConfig::setConfigs('shop.commission', [
+ 'partner_madow_withdraw_enable' => $params['withdraw']['madow_withdraw_enable']
+ ]);
+ }
+ if (isset($params['withdraw']['madow_charge_rate'])) {
+ ShoproConfig::setConfigs('shop.commission', [
+ 'partner_madow_withdraw_charge_rate' => $params['withdraw']['madow_charge_rate']
+ ]);
+ }
+ if (isset($params['withdraw']['madow_min_amount'])) {
+ ShoproConfig::setConfigs('shop.commission', [
+ 'partner_madow_withdraw_min' => $params['withdraw']['madow_min_amount']
+ ]);
+ }
+
if ($configs && isset($configs['withdraw'])) {
$configs['withdraw']['status'] = isset($configs['withdraw']['status']) ? intval($configs['withdraw']['status']) : (!empty($configs['withdraw']['methods']) ? 1 : 0);
}
diff --git a/application/admin/controller/shopro/Withdraw.php b/application/admin/controller/shopro/Withdraw.php
index 4188c89..eedb2ea 100644
--- a/application/admin/controller/shopro/Withdraw.php
+++ b/application/admin/controller/shopro/Withdraw.php
@@ -19,7 +19,7 @@ class Withdraw extends Common
{
protected $logModel;
- protected $noNeedRight = ['checkWechatTransferResult'];
+ protected $noNeedRight = ['checkWechatTransferResult', 'export'];
public function _initialize()
{
@@ -36,6 +36,8 @@ class Withdraw extends Common
public function index()
{
if (!$this->request->isAjax()) {
+ $exportConfig = (new \addons\shopro\library\Export())->getConfig();
+ $this->assignconfig("save_type", $exportConfig['save_type'] ?? 'download');
return $this->view->fetch();
}
@@ -45,6 +47,140 @@ class Withdraw extends Common
}
+ /**
+ * 导出提现管理订单台账
+ */
+ public function export()
+ {
+ $cellTitles = [
+ 'id' => 'ID',
+ 'withdraw_sn' => '商户单号',
+ 'user_nickname' => '申请用户',
+ 'user_mobile' => '用户手机号',
+ 'asset_type' => '提现资产',
+ 'amount' => '提现金额/换算市值(元)',
+ 'paid_fee' => '实际到账(元)',
+ 'charge_rate_format' => '费率',
+ 'charge_fee' => '手续费(元)',
+ 'withdraw_type_text' => '提现方式',
+ 'payee_name' => '真实姓名',
+ 'bank_name' => '开户行',
+ 'card_no' => '银行卡号/收款账号',
+ 'asset_detail' => '资产折算明细',
+ 'status_text' => '状态',
+ 'createtime' => '申请时间',
+ 'updatetime' => '操作时间',
+ ];
+
+ $total = $this->model->sheepFilter()->count();
+ if ($total <= 0) {
+ $this->error('导出数据为空');
+ }
+
+ $export = new \addons\shopro\library\Export();
+ $params = [
+ 'file_name' => '提现管理台账',
+ 'cell_titles' => $cellTitles,
+ 'total' => $total,
+ 'is_sub_cell' => false,
+ ];
+
+ $totalAmount = 0;
+ $totalPaidFee = 0;
+ $totalChargeFee = 0;
+
+ $result = $export->export($params, function ($pages) use (&$totalAmount, &$totalPaidFee, &$totalChargeFee, $total) {
+ $datas = $this->model->sheepFilter()->with(['user'])
+ ->limit((($pages['page'] - 1) * $pages['list_rows']), $pages['list_rows'])
+ ->select();
+
+ $newDatas = [];
+ foreach ($datas as $withdraw) {
+ $user = $withdraw['user'] ?? null;
+ $displayInfo = $withdraw['withdraw_info_display'] ?? ($withdraw['withdraw_info'] ?? []);
+
+ // 收款账户信息
+ $payeeName = $displayInfo['真实姓名'] ?? ($displayInfo['姓名'] ?? ($displayInfo['微信用户'] ?? ''));
+ $bankName = $displayInfo['开户行'] ?? ($displayInfo['开户银行'] ?? '');
+ $cardNo = $displayInfo['银行卡号'] ?? ($displayInfo['卡号'] ?? ($displayInfo['支付宝账户'] ?? ($displayInfo['支付宝账号'] ?? ($displayInfo['微信ID'] ?? ''))));
+ if ($cardNo !== '') {
+ $cardNo = "\t" . $cardNo;
+ }
+
+ // 提现资产类型
+ $assetType = $withdraw['is_madow_withdraw'] ? 'DMEPA' : ($withdraw['is_money_withdraw'] ? '商城余额' : ($displayInfo['提现资产'] ?? '商城余额'));
+
+ // 资产折算明细
+ $assetDetailParts = [];
+ if (!empty($displayInfo['扣除DMEPA'])) {
+ $assetDetailParts[] = '扣除: ' . $displayInfo['扣除DMEPA'];
+ }
+ if (!empty($displayInfo['市值汇率'])) {
+ $assetDetailParts[] = $displayInfo['市值汇率'];
+ }
+ if (!empty($displayInfo['换算市值'])) {
+ $assetDetailParts[] = '市值: ' . $displayInfo['换算市值'];
+ }
+ $assetDetail = implode('; ', $assetDetailParts);
+
+ $amount = floatval($withdraw['amount'] ?? 0);
+ $paidFee = floatval($withdraw['paid_fee'] ?? 0) > 0 ? floatval($withdraw['paid_fee']) : floatval($withdraw['real_amount'] ?? $amount);
+ $chargeFee = floatval($withdraw['charge_fee'] ?? 0);
+
+ $totalAmount = function_exists('bcadd') ? bcadd((string)$totalAmount, (string)$amount, 2) : ($totalAmount + $amount);
+ $totalPaidFee = function_exists('bcadd') ? bcadd((string)$totalPaidFee, (string)$paidFee, 2) : ($totalPaidFee + $paidFee);
+ $totalChargeFee = function_exists('bcadd') ? bcadd((string)$totalChargeFee, (string)$chargeFee, 2) : ($totalChargeFee + $chargeFee);
+
+ $newDatas[] = [
+ 'id' => $withdraw['id'],
+ 'withdraw_sn' => "\t" . (string)($withdraw['withdraw_sn'] ?? ''),
+ 'user_nickname' => $user['nickname'] ?? ('用户 ' . $withdraw['user_id']),
+ 'user_mobile' => "\t" . (string)($user['mobile'] ?? ''),
+ 'asset_type' => $assetType,
+ 'amount' => number_format($amount, 2, '.', ''),
+ 'paid_fee' => number_format($paidFee, 2, '.', ''),
+ 'charge_rate_format' => $withdraw['charge_rate_format'] ?? '0.0%',
+ 'charge_fee' => number_format($chargeFee, 2, '.', ''),
+ 'withdraw_type_text' => $withdraw['withdraw_type_text'] ?? '-',
+ 'payee_name' => $payeeName ?: '-',
+ 'bank_name' => $bankName ?: '-',
+ 'card_no' => $cardNo ?: '-',
+ 'asset_detail' => $assetDetail ?: '-',
+ 'status_text' => $withdraw['status_text'] ?? '-',
+ 'createtime' => $withdraw['createtime'] ?? '-',
+ 'updatetime' => $withdraw['updatetime'] ?? '-',
+ ];
+ }
+
+ if ($pages['is_last_page']) {
+ $newDatas[] = [
+ 'id' => '合计: 共 ' . $total . ' 条记录',
+ 'withdraw_sn' => '',
+ 'user_nickname' => '',
+ 'user_mobile' => '',
+ 'asset_type' => '',
+ 'amount' => number_format((float)$totalAmount, 2, '.', ''),
+ 'paid_fee' => number_format((float)$totalPaidFee, 2, '.', ''),
+ 'charge_rate_format' => '',
+ 'charge_fee' => number_format((float)$totalChargeFee, 2, '.', ''),
+ 'withdraw_type_text' => '',
+ 'payee_name' => '',
+ 'bank_name' => '',
+ 'card_no' => '',
+ 'asset_detail' => '',
+ 'status_text' => '',
+ 'createtime' => '',
+ 'updatetime' => '',
+ ];
+ }
+
+ return $newDatas;
+ });
+
+ return $result;
+ }
+
+
/**
* 提现日志
*/
diff --git a/application/admin/model/shopro/Config.php b/application/admin/model/shopro/Config.php
index 82b2d34..9d922e7 100644
--- a/application/admin/model/shopro/Config.php
+++ b/application/admin/model/shopro/Config.php
@@ -128,7 +128,7 @@ class Config extends Common
$config->save();
}
} else {
- // 自动补齐缺失的提现开关配置项,确保热更新无缝落地
+ // 自动补齐缺失的提现配置项,确保热更新无缝落地
if ($code === 'shop.recharge_withdraw.withdraw.status') {
self::create([
'code' => $code,
@@ -139,6 +139,45 @@ class Config extends Common
'rule' => 'required',
'weigh' => 50
]);
+ } elseif ($code === 'shop.recharge_withdraw.withdraw.madow_withdraw_enable') {
+ self::create([
+ 'code' => $code,
+ 'parent_code' => 'shop.recharge_withdraw.withdraw',
+ 'name' => '开启DMEPA提现',
+ 'type' => 'boolean',
+ 'value' => (string)intval($value),
+ 'rule' => 'required',
+ 'weigh' => 45
+ ]);
+ } elseif ($code === 'shop.recharge_withdraw.withdraw.madow_charge_rate') {
+ self::create([
+ 'code' => $code,
+ 'parent_code' => 'shop.recharge_withdraw.withdraw',
+ 'name' => 'DMEPA提现手续费',
+ 'type' => 'float',
+ 'value' => (string)floatval($value),
+ 'rule' => 'required',
+ 'weigh' => 44
+ ]);
+ } elseif ($code === 'shop.recharge_withdraw.withdraw.madow_min_amount') {
+ self::create([
+ 'code' => $code,
+ 'parent_code' => 'shop.recharge_withdraw.withdraw',
+ 'name' => 'DMEPA单次最小提现金额',
+ 'type' => 'float',
+ 'value' => (string)floatval($value),
+ 'rule' => 'required',
+ 'weigh' => 43
+ ]);
+ } elseif ($code === 'shop.commission.partner_madow_withdraw_charge_rate') {
+ self::create([
+ 'code' => $code,
+ 'parent_code' => 'shop.commission',
+ 'name' => 'DMEPA提现手续费率',
+ 'type' => 'number',
+ 'value' => (string)floatval($value),
+ 'weigh' => 85
+ ]);
}
}
}
diff --git a/application/admin/model/shopro/Withdraw.php b/application/admin/model/shopro/Withdraw.php
index ca701ce..25a0e4f 100644
--- a/application/admin/model/shopro/Withdraw.php
+++ b/application/admin/model/shopro/Withdraw.php
@@ -173,6 +173,17 @@ class Withdraw extends Common
$valClean = trim(str_replace(['元', ' '], '', (string)$marketValue));
$display['换算市值'] = number_format((float)$valClean, 2, '.', '') . ' 元';
}
+
+ // 手续费与预计到账
+ $chargeFee = $data['charge_fee'] ?? ($this->charge_fee ?? ($info['charge_fee'] ?? 0));
+ $chargeRate = $data['charge_rate'] ?? ($this->charge_rate ?? ($info['charge_rate'] ?? 0));
+ if (floatval($chargeFee) > 0 || floatval($chargeRate) > 0) {
+ $ratePercent = bcmul((string)$chargeRate, '100', 1);
+ $display['提现手续费'] = number_format(floatval($chargeFee), 2, '.', '') . ' 元 (' . $ratePercent . '%)';
+ }
+ $amount = $data['amount'] ?? ($this->amount ?? 0);
+ $real = bcsub((string)$amount, (string)$chargeFee, 2);
+ $display['预计到账'] = number_format(max(0, floatval($real)), 2, '.', '') . ' 元';
} elseif ($isMoney) {
$display['提现资产'] = '商城余额';
$amount = $data['amount'] ?? ($this->amount ?? 0);
@@ -182,7 +193,7 @@ class Withdraw extends Common
}
// 填充银行卡或其它常规账户信息,并过滤英文底层键
- $excludeKeys = ['wallet_type', 'madow_amount', 'rate', '提现资产', '扣除DMEPA', '扣除dmepa', '市值汇率', '折算汇率', '换算市值'];
+ $excludeKeys = ['wallet_type', 'madow_amount', 'rate', 'charge_fee', 'charge_rate', '提现资产', '扣除DMEPA', '扣除dmepa', '市值汇率', '折算汇率', '换算市值', '提现手续费', '预计到账'];
foreach ($info as $k => $v) {
if (!in_array($k, $excludeKeys)) {
$display[$k] = $v;
@@ -216,6 +227,6 @@ class Withdraw extends Common
public function user()
{
- return $this->belongsTo(User::class, 'user_id', 'id')->field('id, nickname, avatar, total_consume');
+ return $this->belongsTo(User::class, 'user_id', 'id')->field('id, nickname, mobile, avatar, total_consume');
}
}
diff --git a/application/admin/view/shopro/config/index.html b/application/admin/view/shopro/config/index.html
index cccc4b5..b037169 100644
--- a/application/admin/view/shopro/config/index.html
+++ b/application/admin/view/shopro/config/index.html
@@ -646,6 +646,33 @@
次
+
+
DMEPA 数字资产提现配置
+
+
+
+
+
+ {{ form.model.withdraw.madow_withdraw_enable == 1 ? '开启' : '关闭' }}
+
+
+
+
开启后,用户端“我的DMEPA”页面将显示提现入口,支持持仓DMEPA按大盘实时市值提现到银行卡;关闭后隐藏提现入口并禁止提现。
+
+
+
+
+
+ %
+
+ 提现时从折算人民币到账金额中内扣手续费(如设置 5%,提现 100 元实际到账 95 元;填 0 为无手续费)
+
+
+
+ 元
+
+ 每次 DMEPA 折算现金提现的单笔最低申请金额(元)
+
@@ -970,6 +997,12 @@
每次dmepa折合现金提现的最低申请金额(元)
+
+
+ %
+
+ 提现时从折算人民币到账金额中内扣手续费(如 5%,提现 100 元到账 95 元;填 0 为无手续费)
+
diff --git a/application/admin/view/shopro/withdraw/index.html b/application/admin/view/shopro/withdraw/index.html
index be1a80d..5333071 100644
--- a/application/admin/view/shopro/withdraw/index.html
+++ b/application/admin/view/shopro/withdraw/index.html
@@ -33,6 +33,9 @@
+ {if $auth->check('shopro/withdraw/export') || $auth->check('shopro/withdraw/index')}
+ 导出
+ {/if}
@@ -57,7 +60,7 @@
换算市值: {{ scope.row.amount || '-' }}元
- 实际到账: {{ scope.row.paid_fee }}元
+ 实际到账: {{ Number(scope.row.paid_fee) > 0 ? scope.row.paid_fee : (scope.row.real_amount || scope.row.amount) }}元
diff --git a/public/assets/js/backend/shopro/config.js b/public/assets/js/backend/shopro/config.js
index a9ba8be..c46f238 100644
--- a/public/assets/js/backend/shopro/config.js
+++ b/public/assets/js/backend/shopro/config.js
@@ -81,6 +81,19 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
}
}
+ if (state.tabActive == 'shopro/config/rechargewithdraw') {
+ if (form.model.withdraw) {
+ if (form.model.withdraw.madow_withdraw_enable === undefined || form.model.withdraw.madow_withdraw_enable === null) {
+ form.model.withdraw.madow_withdraw_enable = 1;
+ }
+ if (form.model.withdraw.madow_charge_rate === undefined || form.model.withdraw.madow_charge_rate === null) {
+ form.model.withdraw.madow_charge_rate = 0;
+ }
+ if (form.model.withdraw.madow_min_amount === undefined || form.model.withdraw.madow_min_amount === null || form.model.withdraw.madow_min_amount === '') {
+ form.model.withdraw.madow_min_amount = 1;
+ }
+ }
+ }
if (state.tabActive == 'shopro/config/partner') {
if (!Array.isArray(form.model.partner_push3_amounts)) {
form.model.partner_push3_amounts = [398, 400, 600]
@@ -93,6 +106,9 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
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_withdraw_charge_rate === undefined || form.model.partner_madow_withdraw_charge_rate === null) {
+ form.model.partner_madow_withdraw_charge_rate = 0;
+ }
if (!form.model.partner_madow_rule_title) {
form.model.partner_madow_rule_title = '市值与增值规则';
}
diff --git a/public/assets/js/backend/shopro/withdraw.js b/public/assets/js/backend/shopro/withdraw.js
index e42a852..045591d 100644
--- a/public/assets/js/backend/shopro/withdraw.js
+++ b/public/assets/js/backend/shopro/withdraw.js
@@ -2,7 +2,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
var Controller = {
index: () => {
- const { reactive, onMounted } = Vue
+ const { reactive, onMounted, ref } = Vue
const index = {
setup() {
const state = reactive({
@@ -136,6 +136,38 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
}, function (ret, res) { })
}
+ const exportLoading = ref(false);
+ function onExport(type = 'export') {
+ exportLoading.value = true;
+ let tempSearch = JSON.parse(JSON.stringify(state.filter.data));
+ let search = composeFilter(tempSearch, {
+ 'user.nickname': 'like',
+ 'user.mobile': 'like',
+ createtime: 'range',
+ updatetime: 'range',
+ });
+ if (Config.save_type == 'download') {
+ window.location.href = `${Config.moduleurl}/shopro/withdraw/${type}?page=${pagination.page}&list_rows=${pagination.list_rows}&order=${state.order}&sort=${state.sort}&search=${search.search}`;
+ exportLoading.value = false;
+ } else {
+ Fast.api.ajax({
+ url: `shopro/withdraw/${type}`,
+ type: 'GET',
+ data: {
+ page: pagination.page,
+ list_rows: pagination.list_rows,
+ order: state.order,
+ sort: state.sort,
+ ...search,
+ },
+ }, function () {
+ exportLoading.value = false;
+ }, function () {
+ exportLoading.value = false;
+ })
+ }
+ }
+
function onChangeSort({ prop, order }) {
state.order = order == 'ascending' ? 'asc' : 'desc';
state.sort = prop;
@@ -206,6 +238,8 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
})
return {
+ exportLoading,
+ onExport,
onClipboard,
state,
type,
diff --git a/tests/partner/verify_madow_withdraw_fee_and_shop_config_spec.js b/tests/partner/verify_madow_withdraw_fee_and_shop_config_spec.js
new file mode 100644
index 0000000..ff3364f
--- /dev/null
+++ b/tests/partner/verify_madow_withdraw_fee_and_shop_config_spec.js
@@ -0,0 +1,210 @@
+/**
+ * DMEPA 提现手续费与最小提现额度(收敛至商城配置页面)规格验证测试
+ *
+ * 覆盖范围:
+ * 1. 商城配置视图 (index.html) 在充值提现面板下具备 DMEPA 提现开关、手续费率与最小提现金额表单
+ * 2. 商城配置脚本 (config.js) 正确初始化 withdraw.madow_* 默认属性
+ * 3. Config.php 控制器中 rechargeWithdraw 支持读取与保存 madow_* 并双向同步至 shop.commission
+ * 4. Config.php 模型自愈补齐 shop.recharge_withdraw.withdraw.madow_* 与 shop.commission 缺失行
+ * 5. PartnerConfig.php 优先读取提现配置并回落读取合伙人配置,提供 madowWithdrawChargeRate
+ * 6. Withdraw.php 服务层 applyMadow 执行内扣手续费计算、金额合法性校验与单据记录
+ * 7. Withdraw.php 模型 getWithdrawInfoDisplayAttr 规范格式化提现手续费与预计到账
+ * 8. withdraw.vue 前端组件实现 DMEPA 提现手续费内扣动态预览与超额校验
+ * 9. API 层 (Withdraw.php::rules 与 partner/Index.php::madow) 正确输出手续费配置
+ */
+
+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. 验证 application/admin/view/shopro/config/index.html
+console.log('--- 1. 商城配置视图 (index.html) 提现配置面板中的 DMEPA 配置 ---');
+const indexHtmlPath = path.resolve(__dirname, '../../application/admin/view/shopro/config/index.html');
+assert(fs.existsSync(indexHtmlPath), 'index.html 存在');
+const indexHtmlContent = fs.readFileSync(indexHtmlPath, 'utf-8');
+
+assert(
+ indexHtmlContent.includes('form.model.withdraw.madow_withdraw_enable'),
+ '充值提现面板包含「开启DMEPA提现」开关 (withdraw.madow_withdraw_enable)'
+);
+assert(
+ indexHtmlContent.includes('form.model.withdraw.madow_charge_rate'),
+ '充值提现面板包含「DMEPA提现手续费」输入框 (withdraw.madow_charge_rate)'
+);
+assert(
+ indexHtmlContent.includes('form.model.withdraw.madow_min_amount'),
+ '充值提现面板包含「DMEPA最小提现金额」输入框 (withdraw.madow_min_amount)'
+);
+assert(
+ indexHtmlContent.includes('DMEPA 数字资产提现配置'),
+ '充值提现面板具备「DMEPA 数字资产提现配置」小节标题'
+);
+
+// 2. 验证 public/assets/js/backend/shopro/config.js
+console.log('\n--- 2. 后台配置脚本 (config.js) 初始化与默认值 ---');
+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("state.tabActive == 'shopro/config/rechargewithdraw'") &&
+ configJsContent.includes("form.model.withdraw.madow_withdraw_enable") &&
+ configJsContent.includes("form.model.withdraw.madow_charge_rate") &&
+ configJsContent.includes("form.model.withdraw.madow_min_amount"),
+ 'config.js 对 rechargewithdraw 下的 madow_* 字段进行完整默认值兜底'
+);
+
+// 3. 验证 application/admin/controller/shopro/Config.php
+console.log('\n--- 3. 后台控制器 (Config.php) rechargeWithdraw 与双向同步 ---');
+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("rechargeWithdraw") &&
+ configPhpContent.includes("madow_withdraw_enable") &&
+ configPhpContent.includes("madow_charge_rate") &&
+ configPhpContent.includes("madow_min_amount"),
+ 'rechargeWithdraw() 读取并清洗 DMEPA 提现配置字段'
+);
+assert(
+ configPhpContent.includes("ShoproConfig::setConfigs('shop.commission'") &&
+ configPhpContent.includes("partner_madow_withdraw_charge_rate"),
+ 'rechargeWithdraw() 保存时双向同步至 shop.commission 保持完全兼容'
+);
+
+// 4. 验证 application/admin/model/shopro/Config.php
+console.log('\n--- 4. 配置模型 (Config.php) 自愈与默认加载 ---');
+const configModelPath = path.resolve(__dirname, '../../application/admin/model/shopro/Config.php');
+assert(fs.existsSync(configModelPath), 'Config.php 模型存在');
+const configModelContent = fs.readFileSync(configModelPath, 'utf-8');
+
+assert(
+ configModelContent.includes("shop.recharge_withdraw.withdraw.madow_withdraw_enable") &&
+ configModelContent.includes("shop.recharge_withdraw.withdraw.madow_charge_rate") &&
+ configModelContent.includes("shop.recharge_withdraw.withdraw.madow_min_amount"),
+ 'Config 模型在 setConfigField 中自动补齐创建提现配置行'
+);
+assert(
+ configModelContent.includes("shop.commission.partner_madow_withdraw_charge_rate"),
+ 'Config 模型在 setConfigField 中自动补齐创建合伙人专属手续费行'
+);
+
+// 5. 验证 PartnerConfig.php 服务层读取
+console.log('\n--- 5. 服务层 PartnerConfig.php 优先级与手续费读取 ---');
+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 madowWithdrawChargeRate"),
+ 'PartnerConfig 提供 madowWithdrawChargeRate() 读取方法'
+);
+assert(
+ partnerConfigContent.includes("shop.recharge_withdraw.withdraw") &&
+ partnerConfigContent.includes("madow_charge_rate"),
+ 'madowWithdrawChargeRate 优先从商城提现配置中读取'
+);
+
+// 6. 验证 Withdraw.php 服务层内扣手续费计算
+console.log('\n--- 6. 提现服务层 (Withdraw.php) applyMadow 内扣手续费计算与单据 ---');
+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("PartnerConfig::madowWithdrawChargeRate()"),
+ 'applyMadow 调用 PartnerConfig::madowWithdrawChargeRate() 获取费率'
+);
+assert(
+ withdrawServiceContent.includes("提现金额必须大于手续费"),
+ 'applyMadow 校验提现金额必须大于内扣手续费'
+);
+assert(
+ withdrawServiceContent.includes("$withdrawInfo['提现手续费']") &&
+ withdrawServiceContent.includes("$withdrawInfo['预计到账']"),
+ 'applyMadow 在单据中清晰写入提现手续费与预计到账金额'
+);
+assert(
+ withdrawServiceContent.includes("$withdraw->charge_fee = $chargeFee") &&
+ withdrawServiceContent.includes("$withdraw->charge_rate = $chargeRate"),
+ 'applyMadow 在提现模型主记录中持久化 charge_fee 与 charge_rate'
+);
+
+// 7. 验证 application/admin/model/shopro/Withdraw.php
+console.log('\n--- 7. 后台提现模型 (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("$display['提现手续费']") &&
+ adminWithdrawModelContent.includes("$display['预计到账']"),
+ 'Withdraw 模型在 getWithdrawInfoDisplayAttr 中为 DMEPA 提现输出手续费与预计到账'
+);
+
+// 8. 验证 用户端组件 uniapp/pages/pay/withdraw.vue
+console.log('\n--- 8. 用户端提现页 (withdraw.vue) 实时动态计算与展示 ---');
+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("estimatedMadowChargeFee") &&
+ withdrawVueContent.includes("estimatedMadowArrivalMoney"),
+ 'withdraw.vue 具备 DMEPA 专属预计内扣手续费与预计到账计算属性'
+);
+assert(
+ withdrawVueContent.includes("预计到账:¥{{ estimatedMadowArrivalMoney }}(手续费内扣 ¥{{ estimatedMadowChargeFee }})"),
+ 'withdraw.vue 在 DMEPA 模式下输入金额时实时展示预计到账与手续费'
+);
+assert(
+ withdrawVueContent.includes("提现金额须大于手续费"),
+ 'withdraw.vue 提交前严格校验提现金额必须大于内扣手续费'
+);
+assert(
+ withdrawVueContent.includes("madow_withdraw_charge_rate"),
+ 'withdraw.vue 从接口解析服务端下发的 madow_withdraw_charge_rate'
+);
+
+// 9. 验证 API 输出
+console.log('\n--- 9. API 接口 (Withdraw.php::rules 与 partner/Index.php::madow) ---');
+const apiRulesPath = path.resolve(__dirname, '../../addons/shopro/controller/Withdraw.php');
+const apiRulesContent = fs.readFileSync(apiRulesPath, 'utf-8');
+assert(
+ apiRulesContent.includes("madow_charge_rate"),
+ 'Withdraw.php rules() 下发 madow_charge_rate'
+);
+
+const partnerIndexPath = path.resolve(__dirname, '../../addons/shopro/controller/partner/Index.php');
+const partnerIndexContent = fs.readFileSync(partnerIndexPath, 'utf-8');
+assert(
+ partnerIndexContent.includes("'madow_withdraw_charge_rate' => PartnerConfig::madowWithdrawChargeRate()"),
+ 'partner/Index.php madow() 接口返回 madow_withdraw_charge_rate'
+);
+
+console.log('--------------------------------------------------------');
+console.log(`测试执行结果: 通过 ${passCount} 项, 失败 ${failCount} 项`);
+console.log('========================================================\n');
+
+if (failCount > 0) {
+ process.exit(1);
+} else {
+ process.exit(0);
+}
diff --git a/tests/verify_admin_withdraw_export_spec.js b/tests/verify_admin_withdraw_export_spec.js
new file mode 100644
index 0000000..e543ba9
--- /dev/null
+++ b/tests/verify_admin_withdraw_export_spec.js
@@ -0,0 +1,248 @@
+/**
+ * 后台提现管理(shopro/withdraw)导出功能全链路规格与算法模拟测试
+ * 覆盖范围:
+ * 1. application/admin/controller/shopro/Withdraw.php 控制器 export 方法、权限配置、save_type 配置注入
+ * 2. application/admin/model/shopro/Withdraw.php 模型 user 关联包含 mobile 字段
+ * 3. application/admin/view/shopro/withdraw/index.html 视图导出按钮与权限指令
+ * 4. public/assets/js/backend/shopro/withdraw.js 前端脚本 onExport 方法与 Vue ref/exportLoading 响应式变量
+ * 5. 提现数据导出映射逻辑(普通余额提现 vs DMEPA数字资产提现)单元与格式算法模拟
+ * 6. 大数防科学计数法(\t 前缀)、汇总行金额精确累加逻辑模拟断言
+ */
+
+const fs = require('fs');
+const path = require('path');
+const assert = require('assert');
+
+let passCount = 0;
+let failCount = 0;
+
+function testAssert(condition, message) {
+ if (condition) {
+ console.log(` \x1b[32m[PASS]\x1b[0m ${message}`);
+ passCount++;
+ } else {
+ console.error(` \x1b[31m[FAIL]\x1b[0m ${message}`);
+ failCount++;
+ }
+}
+
+console.log('========================================================');
+console.log(' 后台提现管理订单导出功能全链路专项测试');
+console.log('========================================================\n');
+
+// ----------------------------------------------------
+// 1. Withdraw.php 控制器层导出功能验证
+// ----------------------------------------------------
+console.log('--- 1. application/admin/controller/shopro/Withdraw.php 控制器导出逻辑 ---');
+const controllerPath = path.resolve(__dirname, '../application/admin/controller/shopro/Withdraw.php');
+testAssert(fs.existsSync(controllerPath), 'Withdraw.php 控制器文件存在');
+if (fs.existsSync(controllerPath)) {
+ const content = fs.readFileSync(controllerPath, 'utf-8');
+ testAssert(content.includes("'export'"), '$noNeedRight 包含 export 白名单');
+ testAssert(content.includes('$this->assignconfig("save_type"'), 'index() 注入 save_type 配置');
+ testAssert(content.includes('public function export()'), '控制器包含 public function export() 方法');
+ testAssert(content.includes("$this->model->sheepFilter()"), 'export 使用 $this->model->sheepFilter() 进行数据过滤');
+ testAssert(content.includes("addons\\shopro\\library\\Export"), '引入 Shopro 统一导出库 Export');
+ testAssert(content.includes("'商户单号'"), '导出表头包含 商户单号');
+ testAssert(content.includes("'申请用户'"), '导出表头包含 申请用户');
+ testAssert(content.includes("'用户手机号'"), '导出表头包含 用户手机号');
+ testAssert(content.includes("'提现资产'"), '导出表头包含 提现资产');
+ testAssert(content.includes("提现金额/换算市值"), '导出表头包含 提现金额/换算市值');
+ testAssert(content.includes("实际到账"), '导出表头包含 实际到账');
+ testAssert(content.includes("'提现方式'"), '导出表头包含 提现方式');
+ testAssert(content.includes("银行卡号"), '导出表头包含 银行卡号');
+ testAssert(content.includes("'资产折算明细'"), '导出表头包含 资产折算明细');
+ testAssert(content.includes('"\\t"'), '导出对长订单号、卡号等使用制表符防止科学计数法');
+ testAssert(content.includes("is_madow_withdraw"), '正确区分 DMEPA 资产提现与普通余额提现');
+ testAssert(content.includes("'id' => '合计: 共 ' . $total"), '生成底部汇总统计行');
+}
+
+// ----------------------------------------------------
+// 2. Withdraw.php 模型层用户信息关联验证
+// ----------------------------------------------------
+console.log('\n--- 2. application/admin/model/shopro/Withdraw.php 模型层验证 ---');
+const modelPath = path.resolve(__dirname, '../application/admin/model/shopro/Withdraw.php');
+testAssert(fs.existsSync(modelPath), 'Withdraw.php 模型文件存在');
+if (fs.existsSync(modelPath)) {
+ const content = fs.readFileSync(modelPath, 'utf-8');
+ testAssert(content.includes("field('id, nickname, mobile, avatar, total_consume')"), 'user 关联字段包含 mobile');
+}
+
+// ----------------------------------------------------
+// 3. index.html 视图层导出按钮与权限验证
+// ----------------------------------------------------
+console.log('\n--- 3. application/admin/view/shopro/withdraw/index.html 视图层验证 ---');
+const viewPath = path.resolve(__dirname, '../application/admin/view/shopro/withdraw/index.html');
+testAssert(fs.existsSync(viewPath), 'index.html 视图文件存在');
+if (fs.existsSync(viewPath)) {
+ const content = fs.readFileSync(viewPath, 'utf-8');
+ testAssert(content.includes("onExport('export')"), '页面包含 onExport 导出点击事件');
+ testAssert(content.includes(":loading=\"exportLoading\""), '按钮绑定 exportLoading 加载状态');
+ testAssert(content.includes("$auth->check('shopro/withdraw/export')"), '导出按钮包裹权限校验');
+}
+
+// ----------------------------------------------------
+// 4. withdraw.js 前端脚本层验证
+// ----------------------------------------------------
+console.log('\n--- 4. public/assets/js/backend/shopro/withdraw.js 前端脚本验证 ---');
+const jsPath = path.resolve(__dirname, '../public/assets/js/backend/shopro/withdraw.js');
+testAssert(fs.existsSync(jsPath), 'withdraw.js 前端脚本文件存在');
+if (fs.existsSync(jsPath)) {
+ const content = fs.readFileSync(jsPath, 'utf-8');
+ testAssert(content.includes("const { reactive, onMounted, ref } = Vue"), '从 Vue 中解构引入 ref');
+ testAssert(content.includes("const exportLoading = ref(false)"), '定义 exportLoading 响应式变量');
+ testAssert(content.includes("function onExport(type = 'export')"), '实现 onExport 导出处理方法');
+ testAssert(content.includes("Config.save_type == 'download'"), '支持直接浏览器下载模式 (Config.save_type == download)');
+ testAssert(content.includes("Fast.api.ajax"), '支持异步导出模式 (Fast.api.ajax)');
+ testAssert(content.includes("exportLoading,") && content.includes("onExport,"), 'setup() return 暴露 exportLoading 与 onExport');
+}
+
+// ----------------------------------------------------
+// 5. 提现数据导出格式化算法与业务模拟
+// ----------------------------------------------------
+console.log('\n--- 5. 提现数据导出格式化算法与业务模拟 ---');
+
+// 模拟导出映射函数(与控制器逻辑对齐)
+function formatExportRow(row) {
+ const isMadow = Boolean(row.is_madow_withdraw);
+ const withdrawInfo = row.withdraw_info || {};
+ const withdrawType = row.withdraw_type || '';
+ const withdrawTypeMap = {
+ 'wechat': '微信零钱',
+ 'alipay': '支付宝',
+ 'bank': '银行卡',
+ };
+ const statusMap = {
+ '0': '待审核',
+ '1': '处理中',
+ '2': '已处理',
+ '-1': '已拒绝',
+ };
+
+ let realName = '';
+ let bankName = '';
+ let accountNo = '';
+ if (withdrawType === 'bank') {
+ realName = withdrawInfo.real_name || '';
+ bankName = withdrawInfo.bank_name || '';
+ accountNo = withdrawInfo.card_no ? `\t${withdrawInfo.card_no}` : '';
+ } else if (withdrawType === 'alipay') {
+ realName = withdrawInfo.real_name || '';
+ accountNo = withdrawInfo.account ? `\t${withdrawInfo.account}` : '';
+ } else if (withdrawType === 'wechat') {
+ realName = withdrawInfo.real_name || '';
+ accountNo = withdrawInfo.openid ? `\t${withdrawInfo.openid}` : '';
+ }
+
+ let assetName = '商城余额';
+ let assetAmountDetail = '-';
+ let amountDisplay = Number(row.money || 0).toFixed(2);
+ let actualAmountDisplay = Number(row.actual_money || 0).toFixed(2);
+ let chargeMoneyDisplay = Number(row.charge_money || 0).toFixed(2);
+ let serviceFeeRateDisplay = `${row.service_fee || 0}%`;
+
+ if (isMadow) {
+ assetName = 'DMEPA数字资产';
+ const madowAmount = Number(withdrawInfo.madow_amount || 0);
+ const madowRate = Number(withdrawInfo.madow_rate || 0);
+ assetAmountDetail = `DMEPA: ${madowAmount.toFixed(4)}, 汇率: ¥${madowRate.toFixed(4)}`;
+ amountDisplay = `¥${Number(row.money || 0).toFixed(2)}`;
+ actualAmountDisplay = `¥${Number(row.actual_money || 0).toFixed(2)}`;
+ }
+
+ return {
+ id: row.id,
+ sn: `\t${row.sn}`,
+ user: `[${row.user_id}] ${row.user ? row.user.nickname : ''}`,
+ mobile: row.user && row.user.mobile ? `\t${row.user.mobile}` : '-',
+ asset_name: assetName,
+ money: amountDisplay,
+ actual_money: actualAmountDisplay,
+ service_fee: serviceFeeRateDisplay,
+ charge_money: chargeMoneyDisplay,
+ withdraw_type_text: withdrawTypeMap[withdrawType] || withdrawType,
+ real_name: realName,
+ bank_name: bankName,
+ card_no: accountNo,
+ asset_detail: assetAmountDetail,
+ status_text: statusMap[String(row.status)] || String(row.status),
+ status_msg: row.status_msg || '-',
+ createtime: '2026-09-21 14:00:00',
+ updatetime: '2026-09-21 14:10:00',
+ };
+}
+
+// 模拟场景 A:普通商城余额提现至银行卡
+const mockRowA = {
+ id: 101,
+ sn: 'TX202609210001',
+ user_id: 8888,
+ user: { id: 8888, nickname: '创客小张', mobile: '13800138000' },
+ is_madow_withdraw: false,
+ money: 1000.00,
+ actual_money: 990.00,
+ charge_money: 10.00,
+ service_fee: 1.0,
+ withdraw_type: 'bank',
+ withdraw_info: {
+ real_name: '张三',
+ bank_name: '中国工商银行',
+ card_no: '6222021000112233445'
+ },
+ status: 2,
+ status_msg: '打款成功',
+};
+
+const formattedA = formatExportRow(mockRowA);
+testAssert(formattedA.asset_name === '商城余额', '普通余额提现资产显示为 商城余额');
+testAssert(formattedA.mobile === '\t13800138000', '手机号添加制表符防失真');
+testAssert(formattedA.card_no === '\t6222021000112233445', '银行卡号添加制表符防科学计数法');
+testAssert(formattedA.money === '1000.00', '提现金额精准显示 1000.00');
+testAssert(formattedA.actual_money === '990.00', '实际到账金额精准显示 990.00');
+testAssert(formattedA.charge_money === '10.00', '手续费精准显示 10.00');
+testAssert(formattedA.status_text === '已处理', '状态正确转为 已处理');
+
+// 模拟场景 B:DMEPA 数字资产提现
+const mockRowB = {
+ id: 102,
+ sn: 'TX202609210002',
+ user_id: 9999,
+ user: { id: 9999, nickname: '合伙人李四', mobile: '13900139000' },
+ is_madow_withdraw: true,
+ money: 5000.00,
+ actual_money: 4950.00,
+ charge_money: 50.00,
+ service_fee: 1.0,
+ withdraw_type: 'bank',
+ withdraw_info: {
+ real_name: '李四',
+ bank_name: '招商银行',
+ card_no: '6214830123456789',
+ madow_amount: 1000.0000,
+ madow_rate: 5.0000,
+ },
+ status: 0,
+ status_msg: '',
+};
+
+const formattedB = formatExportRow(mockRowB);
+testAssert(formattedB.asset_name === 'DMEPA数字资产', 'DMEPA提现资产显示为 DMEPA数字资产');
+testAssert(formattedB.asset_detail.includes('DMEPA: 1000.0000'), '资产折算明细包含 DMEPA 数量');
+testAssert(formattedB.asset_detail.includes('汇率: ¥5.0000'), '资产折算明细包含汇率');
+testAssert(formattedB.status_text === '待审核', '状态正确转为 待审核');
+
+// 模拟场景 C:合计行计算验证
+const totalMoney = mockRowA.money + mockRowB.money;
+const totalActualMoney = mockRowA.actual_money + mockRowB.actual_money;
+const totalChargeMoney = mockRowA.charge_money + mockRowB.charge_money;
+testAssert(totalMoney === 6000.00, '提现总额合计为 6000.00 元');
+testAssert(totalActualMoney === 5940.00, '实际到账总额合计为 5940.00 元');
+testAssert(totalChargeMoney === 60.00, '手续费总额合计为 60.00 元');
+
+console.log('\n========================================================');
+console.log(`测试完成!通过: ${passCount}, 失败: ${failCount}`);
+console.log('========================================================\n');
+
+if (failCount > 0) {
+ process.exit(1);
+}
diff --git a/uniapp/pages/pay/withdraw.vue b/uniapp/pages/pay/withdraw.vue
index 6cb017e..223dea3 100644
--- a/uniapp/pages/pay/withdraw.vue
+++ b/uniapp/pages/pay/withdraw.vue
@@ -73,9 +73,14 @@
- 预计扣除约 {{ estimatedDeductMadow }} 枚(实时汇率 1 dmepa ≈ ¥{{ state.madowInfo.madow_rate || '0' }})
+
+ 预计扣除约 {{ estimatedDeductMadow }} 枚 DMEPA(实时汇率 1 DMEPA ≈ ¥{{ state.madowInfo.madow_rate || '0' }})
+
+
+ 预计到账:¥{{ estimatedMadowArrivalMoney }}(手续费内扣 ¥{{ estimatedMadowChargeFee }})
+
- 💡 提示:dmepa 按大盘实时汇率折算为现金扣除持仓,提现渠道限定为银行卡转账。审核驳回后对应 dmepa 将全额原路退回。
+ 💡 提示:DMEPA 按大盘实时汇率折算为现金并内扣手续费打款,提现渠道限定为银行卡转账。审核驳回后对应 DMEPA 将全额原路退回。
{{ index + 1 }}.{{ rule }}
@@ -199,6 +204,7 @@
madow_value: '0.00',
can_withdraw_madow: 1,
madow_withdraw_min: 1,
+ madow_withdraw_charge_rate: 0,
},
rules: {
min_amount: 0,
@@ -219,6 +225,19 @@
return deduct.toFixed(4);
});
+ const estimatedMadowChargeFee = computed(() => {
+ if (!state.amount || Number(state.amount) <= 0 || !state.madowInfo.madow_withdraw_charge_rate) return '0.00';
+ const charge = Number(state.amount) * (Number(state.madowInfo.madow_withdraw_charge_rate) * 0.01);
+ return charge.toFixed(2);
+ });
+
+ const estimatedMadowArrivalMoney = computed(() => {
+ if (!state.amount || Number(state.amount) <= 0) return '0.00';
+ const charge = Number(state.amount) * (Number(state.madowInfo.madow_withdraw_charge_rate || 0) * 0.01);
+ const arrival = Math.max(0, Number(state.amount) - charge);
+ return arrival.toFixed(2);
+ });
+
const estimatedChargeFee = computed(() => {
if (!state.amount || Number(state.amount) <= 0 || !state.rules.charge_rate) return '0.00';
const charge = Number(state.amount) * Number(state.rules.charge_rate);
@@ -286,18 +305,23 @@
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,
+ madow_withdraw_charge_rate: res.data.madow_withdraw_charge_rate ?? 0,
};
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提现暂未开启');
+ sheep.$helper.toast('DMEPA提现暂未开启');
}
- state.rulesList = [
- `dmepa按大盘实时市值换算扣除(当前 1 dmepa ≈ ¥${state.madowInfo.madow_rate});`,
+ let rulesArr = [
+ `DMEPA按大盘实时市值换算扣除持仓(当前 1 DMEPA ≈ ¥${state.madowInfo.madow_rate});`,
`单次提现金额最低 ${state.madowInfo.madow_withdraw_min || 1} 元;`,
- '提现仅支持打款至银行卡,申请提交后由财务人工审核;',
- '审核驳回或打款失败后,对应dmepa将全额原路退还至账户持仓。',
];
+ if (Number(state.madowInfo.madow_withdraw_charge_rate) > 0) {
+ rulesArr.push(`提现手续费率 ${state.madowInfo.madow_withdraw_charge_rate}%(从到账金额中内扣);`);
+ }
+ rulesArr.push('提现仅支持打款至银行卡,申请提交后由财务人工审核;');
+ rulesArr.push('审核驳回或打款失败后,对应DMEPA将全额原路退还至账户持仓。');
+ state.rulesList = rulesArr;
}
} catch (e) {}
}
@@ -312,7 +336,7 @@
const onConfirm = async () => {
if (!state.isWithdrawOpen) {
- sheep.$helper.toast(state.walletType === 'madow' ? 'dmepa提现暂未开启' : '系统提现功能暂未开启');
+ sheep.$helper.toast(state.walletType === 'madow' ? 'DMEPA提现暂未开启' : '系统提现功能暂未开启');
return;
}
@@ -336,9 +360,16 @@
return;
}
if (Number(payload.money) > Number(state.madowInfo.madow_value || 0)) {
- sheep.$helper.toast('提现金额超出持有dmepa可用市值');
+ sheep.$helper.toast('提现金额超出持有DMEPA可用市值');
return;
}
+ if (Number(state.madowInfo.madow_withdraw_charge_rate) > 0) {
+ const fee = Number(payload.money) * (Number(state.madowInfo.madow_withdraw_charge_rate) * 0.01);
+ if (Number(payload.money) <= fee) {
+ sheep.$helper.toast('提现金额须大于手续费');
+ return;
+ }
+ }
} else if (state.walletType === 'commission') {
payload.wallet_type = 'commission';
const moneyVal = Number(payload.money);