文件
yjyf/server/application/common/library/FinanceConfigService.php

681 行
32 KiB
PHP

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

<?php
namespace app\common\library;
use think\Db;
/**
* 财务结算与提现规则配置中心服务
*
* 职责:
* 1. 统一管理独立配置中心表 fa_finance_config(订单默认抽佣兜底 + 提现风控 + 微信小程序商家转账 1005 规范参数)
* 2. 提供提现手续费灵活计算(按比例 / 按笔固定 / 免费 + 单笔保底与封顶)
* 3. 执行符合微信支付 APIv3 新版「商家转账(用户确认模式)」与微信小程序规范的提现前置校验与配置下发
*/
class FinanceConfigService
{
/** 微信新版商家转账官方单笔最低到账净额(0.10元 = 10分) */
const WX_TRANSFER_MIN_ARRIVE = 0.10;
/** 微信新版商家转账官方单笔硬上限(20000.00元) */
const WX_TRANSFER_HARD_MAX = 20000.00;
/** 微信新版商家转账强制加密校验真实姓名阈值(2000.00元) */
const WX_TRANSFER_REALNAME_THRESHOLD = 2000.00;
/** @var array|null 请求内静态缓存 */
protected static $cache = null;
/**
* 默认配置字典(当数据库尚未初始化某项时的安全兜底)
*/
public static function defaults(): array
{
return [
// 订单抽佣全局兜底
'default_calc_mode' => 'percent',
'default_commission_rate' => '10.00',
'default_fixed_fee' => '0.00',
'default_min_commission' => '0.00',
'default_max_commission' => '0.00',
'include_diff_in_commission' => '1',
// 提现与微信小程序商家转账配置
'withdraw_enabled' => '1',
'withdraw_close_tip' => '系统正在进行财务结算维护,请稍后再发起提现',
'withdraw_channels' => 'wechat',
'withdraw_require_realname' => '1',
'withdraw_min_amount' => '10.00',
'withdraw_max_amount' => '200.00',
'withdraw_daily_amount' => '2000.00',
'withdraw_daily_count' => '3',
'withdraw_allow_weekdays' => '1,2,3,4,5,6,7',
'withdraw_time_start' => '00:00',
'withdraw_time_end' => '23:59',
'withdraw_fee_mode' => 'percent',
'withdraw_fee_rate' => '0.60',
'withdraw_fee_fixed' => '0.00',
'withdraw_fee_min' => '0.00',
'withdraw_fee_max' => '0.00',
'withdraw_transfer_mode' => 'wx_mch_transfer',
'withdraw_audit_mode' => 'manual',
'withdraw_auto_max_amount' => '50.00',
'wx_transfer_scene_id' => '1005',
'wx_recv_perception' => '劳务报酬',
'wx_job_type' => '上门安装维修师傅',
'wx_reward_desc' => '平台工单安装维修劳务报酬',
'wx_transfer_remark' => '工单安装维修服务劳务报酬',
];
}
/**
* 清空请求内静态缓存
*/
public static function clearCache(): void
{
self::$cache = null;
}
/**
* 获取全部原始配置键值数组
*/
public static function getAll(bool $forceRefresh = false): array
{
if (self::$cache === null || $forceRefresh) {
$base = self::defaults();
try {
$dbMap = Db::name('finance_config')->column('value', 'name');
if (is_array($dbMap) && !empty($dbMap)) {
foreach ($dbMap as $k => $v) {
if ($v !== null) {
$base[$k] = (string)$v;
}
}
}
} catch (\Throwable $e) {
// 表未就绪时平滑回退默认字典
}
self::$cache = $base;
}
return self::$cache;
}
/**
* 获取订单默认兜底抽佣配置
*/
public static function getCommissionDefaults(): array
{
$all = self::getAll();
$mode = in_array($all['default_calc_mode'], ['percent', 'fixed', 'mixed'], true)
? $all['default_calc_mode']
: 'percent';
return [
'calc_mode' => $mode,
'commission_rate' => round(min(100.00, max(0.00, (float)$all['default_commission_rate'])), 2),
'fixed_fee' => round(max(0.00, (float)$all['default_fixed_fee']), 2),
'min_commission' => round(max(0.00, (float)$all['default_min_commission']), 2),
'max_commission' => round(max(0.00, (float)$all['default_max_commission']), 2),
'include_diff_in_commission' => (int)$all['include_diff_in_commission'] === 1 ? 1 : 0,
];
}
/**
* 获取结构化的提现与微信小程序转账配置
*/
public static function getWithdrawConfig(): array
{
$all = self::getAll();
$rawChannels = array_filter(array_map('trim', explode(',', (string)$all['withdraw_channels'])));
$channels = [];
foreach ($rawChannels as $ch) {
if (in_array($ch, ['wechat', 'bank'], true) && !in_array($ch, $channels, true)) {
$channels[] = $ch;
}
}
if (empty($channels)) {
$channels = ['wechat'];
}
$rawDays = array_filter(array_map('intval', explode(',', (string)$all['withdraw_allow_weekdays'])));
$weekdays = [];
foreach ($rawDays as $d) {
if ($d >= 1 && $d <= 7 && !in_array($d, $weekdays, true)) {
$weekdays[] = $d;
}
}
sort($weekdays);
if (empty($weekdays)) {
$weekdays = [1, 2, 3, 4, 5, 6, 7];
}
$minAmt = round(max(0.10, (float)$all['withdraw_min_amount']), 2);
$maxAmt = round(min(self::WX_TRANSFER_HARD_MAX, max($minAmt, (float)$all['withdraw_max_amount'])), 2);
$dailyAmt = round(max(0.00, (float)$all['withdraw_daily_amount']), 2);
if ($dailyAmt > 0 && $dailyAmt < $minAmt) {
$dailyAmt = $maxAmt;
}
$feeMode = in_array($all['withdraw_fee_mode'], ['percent', 'fixed', 'free'], true)
? $all['withdraw_fee_mode']
: 'percent';
$transferMode = in_array($all['withdraw_transfer_mode'], ['wx_mch_transfer', 'offline_manual'], true)
? $all['withdraw_transfer_mode']
: 'wx_mch_transfer';
$auditMode = in_array($all['withdraw_audit_mode'], ['manual', 'auto_small'], true)
? $all['withdraw_audit_mode']
: 'manual';
return [
'enabled' => (int)$all['withdraw_enabled'] === 1 ? 1 : 0,
'close_tip' => trim((string)$all['withdraw_close_tip']) ?: '当前暂未开放提现,请稍后再试',
'channels' => $channels,
'require_realname' => (int)$all['withdraw_require_realname'] === 1 ? 1 : 0,
'min_amount' => $minAmt,
'max_amount' => $maxAmt,
'daily_amount' => $dailyAmt,
'daily_count' => max(0, (int)$all['withdraw_daily_count']),
'allow_weekdays' => $weekdays,
'time_start' => preg_match('/^\d{2}:\d{2}$/', (string)$all['withdraw_time_start']) ? $all['withdraw_time_start'] : '00:00',
'time_end' => preg_match('/^\d{2}:\d{2}$/', (string)$all['withdraw_time_end']) ? $all['withdraw_time_end'] : '23:59',
'fee_mode' => $feeMode,
'fee_rate' => round(min(50.00, max(0.00, (float)$all['withdraw_fee_rate'])), 2),
'fee_fixed' => round(max(0.00, (float)$all['withdraw_fee_fixed']), 2),
'fee_min' => round(max(0.00, (float)$all['withdraw_fee_min']), 2),
'fee_max' => round(max(0.00, (float)$all['withdraw_fee_max']), 2),
'transfer_mode' => $transferMode,
'audit_mode' => $auditMode,
'auto_max_amount' => round(max(0.00, (float)$all['withdraw_auto_max_amount']), 2),
'wx_scene_id' => trim((string)$all['wx_transfer_scene_id']) ?: '1005',
'wx_perception' => mb_substr(trim((string)$all['wx_recv_perception']) ?: '劳务报酬', 0, 32),
'wx_job_type' => mb_substr(trim((string)$all['wx_job_type']) ?: '上门安装维修师傅', 0, 32),
'wx_reward_desc' => mb_substr(trim((string)$all['wx_reward_desc']) ?: '平台工单安装维修劳务报酬', 0, 32),
'wx_remark' => mb_substr(trim((string)$all['wx_transfer_remark']) ?: '工单安装维修服务劳务报酬', 0, 32),
];
}
/**
* 计算单笔提现的手续费与实际到账净额
*/
public static function calcWithdrawFee(float $amount, ?array $cfg = null): array
{
$cfg = $cfg ?: self::getWithdrawConfig();
$amt = max(0.00, round($amount, 2));
$fee = 0.00;
if ($amt > 0) {
if ($cfg['fee_mode'] === 'percent') {
$fee = round($amt * ((float)$cfg['fee_rate']) / 100, 2);
if ($cfg['fee_min'] > 0 && $fee < $cfg['fee_min']) {
$fee = $cfg['fee_min'];
}
if ($cfg['fee_max'] > 0 && $fee > $cfg['fee_max']) {
$fee = $cfg['fee_max'];
}
} elseif ($cfg['fee_mode'] === 'fixed') {
$fee = round((float)$cfg['fee_fixed'], 2);
} else {
$fee = 0.00;
}
}
if ($fee >= $amt && $amt > 0) {
$fee = max(0.00, round($amt - self::WX_TRANSFER_MIN_ARRIVE, 2));
}
$arrive = round(max(0.00, $amt - $fee), 2);
return [
'amount' => $amt,
'fee' => $fee,
'arrive_amount' => $arrive,
'fee_mode' => $cfg['fee_mode'],
'fee_rate' => $cfg['fee_rate'],
'fee_fixed' => $cfg['fee_fixed'],
];
}
/**
* 统计师傅今日已申请提现次数与金额(排除已驳回单)
*/
public static function getMasterTodayWithdrawStats(int $masterId): array
{
$todayStart = strtotime(date('Y-m-d 00:00:00'));
$rows = Db::name('withdraw')
->field('amount')
->where('master_id', $masterId)
->where('status', '<>', 'reject')
->where('createtime', '>=', $todayStart)
->select();
$cnt = count($rows);
$sum = 0.00;
foreach ($rows as $r) {
$sum += (float)$r['amount'];
}
return [
'today_count' => $cnt,
'today_amount' => round($sum, 2),
];
}
/**
* 生成下发给小程序「我的钱包」页面的动态提现配置与规则说明
*/
public static function buildMasterWithdrawClientPayload(array $master): array
{
$cfg = self::getWithdrawConfig();
$stats = self::getMasterTodayWithdrawStats((int)$master['id']);
$remainDailyAmount = $cfg['daily_amount'] > 0
? round(max(0.00, $cfg['daily_amount'] - $stats['today_amount']), 2)
: -1;
$remainDailyCount = $cfg['daily_count'] > 0
? max(0, $cfg['daily_count'] - $stats['today_count'])
: -1;
if ($cfg['fee_mode'] === 'free' || ($cfg['fee_mode'] === 'percent' && $cfg['fee_rate'] <= 0) || ($cfg['fee_mode'] === 'fixed' && $cfg['fee_fixed'] <= 0)) {
$feeText = '免通道手续费';
} elseif ($cfg['fee_mode'] === 'fixed') {
$feeText = '手续费 ¥' . number_format($cfg['fee_fixed'], 2, '.', '') . '/笔';
} else {
$feeText = '通道费率 ' . rtrim(rtrim(number_format($cfg['fee_rate'], 2, '.', ''), '0'), '.') . '%';
if ($cfg['fee_min'] > 0 || $cfg['fee_max'] > 0) {
$parts = [];
if ($cfg['fee_min'] > 0) {
$parts[] = '最低¥' . number_format($cfg['fee_min'], 2, '.', '');
}
if ($cfg['fee_max'] > 0) {
$parts[] = '封顶¥' . number_format($cfg['fee_max'], 2, '.', '');
}
$feeText .= '(' . implode(',', $parts) . ')';
}
}
$minTxt = rtrim(rtrim(number_format($cfg['min_amount'], 2, '.', ''), '0'), '.');
$maxTxt = rtrim(rtrim(number_format($cfg['max_amount'], 2, '.', ''), '0'), '.');
$ruleTip = "完工核销后工费即刻入账;单笔 ¥{$minTxt}~¥{$maxTxt} 可提现,{$feeText}。";
if (!$cfg['enabled']) {
$ruleTip = $cfg['close_tip'];
}
$arrivalDesc = $cfg['transfer_mode'] === 'wx_mch_transfer'
? ($cfg['audit_mode'] === 'auto_small' ? '微信零钱(小额免审,确认收款即到账)' : '微信零钱(审核后小程序确认收款即到账)')
: '微信零钱 / 人工财务打款';
return [
'enabled' => (bool)$cfg['enabled'],
'close_tip' => $cfg['close_tip'],
'channels' => $cfg['channels'],
'require_realname' => (bool)$cfg['require_realname'],
'min_amount' => $cfg['min_amount'],
'max_amount' => $cfg['max_amount'],
'daily_amount' => $cfg['daily_amount'],
'daily_count' => $cfg['daily_count'],
'today_amount' => $stats['today_amount'],
'today_count' => $stats['today_count'],
'remain_daily_amount' => $remainDailyAmount,
'remain_daily_count' => $remainDailyCount,
'allow_weekdays' => $cfg['allow_weekdays'],
'time_start' => $cfg['time_start'],
'time_end' => $cfg['time_end'],
'fee_mode' => $cfg['fee_mode'],
'fee_rate' => $cfg['fee_rate'],
'fee_fixed' => $cfg['fee_fixed'],
'fee_min' => $cfg['fee_min'],
'fee_max' => $cfg['fee_max'],
'fee_text' => $feeText,
'transfer_mode' => $cfg['transfer_mode'],
'audit_mode' => $cfg['audit_mode'],
'auto_max_amount' => $cfg['auto_max_amount'],
'arrival_desc' => $arrivalDesc,
'rule_tip' => $ruleTip,
];
}
/**
* 校验师傅发起的提现请求(全量业务规则 + 微信小程序商家转账硬规范)
*
* @param array $master fa_master 行记录
* @param float $amount 申请提现金额
* @param string $channel 申请提现渠道 wechat | bank
* @param string $account 银行卡等收款账号信息
* @return array ['fee' => float, 'arrive_amount' => float, 'cfg' => array, 'rule_snapshot' => string]
* @throws \InvalidArgumentException
*/
public static function validateWithdrawRequest(array $master, float $amount, string $channel, string $account = ''): array
{
$cfg = self::getWithdrawConfig();
if (!$cfg['enabled']) {
throw new \InvalidArgumentException($cfg['close_tip']);
}
if (!in_array($channel, $cfg['channels'], true)) {
$chLabel = $channel === 'bank' ? '银行储蓄卡' : '微信零钱';
throw new \InvalidArgumentException("当前暂未开放【{$chLabel}】提现渠道");
}
if ($channel === 'bank' && trim($account) === '') {
throw new \InvalidArgumentException('请填写收款银行卡号与开户行信息');
}
// 校验允许提现星期与每日时段
$curWeekday = (int)date('N'); // 1(周一) ~ 7(周日)
if (!in_array($curWeekday, $cfg['allow_weekdays'], true)) {
$dayNames = [1 => '周一', 2 => '周二', 3 => '周三', 4 => '周四', 5 => '周五', 6 => '周六', 7 => '周日'];
$allowLabels = array_map(function ($d) use ($dayNames) {
return $dayNames[$d] ?? '';
}, $cfg['allow_weekdays']);
throw new \InvalidArgumentException('当前不在提现开放日(开放时间:' . implode('、', array_filter($allowLabels)) . ')');
}
$nowHm = date('H:i');
if ($nowHm < $cfg['time_start'] || $nowHm > $cfg['time_end']) {
throw new \InvalidArgumentException("当前不在每日提现开放时段({$cfg['time_start']} ~ {$cfg['time_end']})");
}
// 实名认证合规校验(后台开启强制实名,或微信单笔 >= 2000 元微信官方强制实名)
$isRealnameVerified = (($master['verify_status'] ?? '') === 'pass') && trim((string)($master['realname'] ?? '')) !== '';
if ($cfg['require_realname'] && !$isRealnameVerified) {
throw new \InvalidArgumentException('根据资金合规要求,请先完成师傅实名认证审核通过后再申请提现');
}
$amt = round($amount, 2);
if ($amt <= 0) {
throw new \InvalidArgumentException('请输入有效的提现金额');
}
if ($amt < $cfg['min_amount']) {
throw new \InvalidArgumentException('单笔提现金额不能低于 ¥' . number_format($cfg['min_amount'], 2, '.', ''));
}
if ($amt > $cfg['max_amount']) {
throw new \InvalidArgumentException('单笔提现金额不能超过 ¥' . number_format($cfg['max_amount'], 2, '.', ''));
}
if ($channel === 'wechat' && $amt > self::WX_TRANSFER_HARD_MAX) {
throw new \InvalidArgumentException('微信零钱单笔提现不能超过官方上限 ¥20,000.00');
}
$feeCalc = self::calcWithdrawFee($amt, $cfg);
$arrive = $feeCalc['arrive_amount'];
if ($channel === 'wechat') {
if ($arrive < self::WX_TRANSFER_MIN_ARRIVE) {
throw new \InvalidArgumentException('扣除手续费后实际到账金额不能低于 ¥0.10(微信转账最低限额)');
}
if ($arrive >= self::WX_TRANSFER_REALNAME_THRESHOLD && !$isRealnameVerified) {
throw new \InvalidArgumentException('微信官方规定单笔转账满 ¥2,000.00 必须校验真实姓名,请先完成实名认证');
}
}
// 单日次数与累计额度校验
$stats = self::getMasterTodayWithdrawStats((int)$master['id']);
if ($cfg['daily_count'] > 0 && $stats['today_count'] >= $cfg['daily_count']) {
throw new \InvalidArgumentException("今日提现次数已达上限(每日最多 {$cfg['daily_count']} 次),请明日再试");
}
if ($cfg['daily_amount'] > 0 && round($stats['today_amount'] + $amt, 2) > $cfg['daily_amount']) {
$remain = max(0.00, round($cfg['daily_amount'] - $stats['today_amount'], 2));
throw new \InvalidArgumentException('超出单日累计提现上限(今日剩余可提额度 ¥' . number_format($remain, 2, '.', '') . ')');
}
$snapshot = [
'fee_mode' => $cfg['fee_mode'],
'fee_rate' => $cfg['fee_rate'],
'fee_fixed' => $cfg['fee_fixed'],
'fee_min' => $cfg['fee_min'],
'fee_max' => $cfg['fee_max'],
'fee' => $feeCalc['fee'],
'arrive_amount' => $arrive,
'transfer_mode' => $cfg['transfer_mode'],
'audit_mode' => $cfg['audit_mode'],
'snapshot_time' => time(),
];
return [
'fee' => $feeCalc['fee'],
'arrive_amount' => $arrive,
'cfg' => $cfg,
'rule_snapshot' => json_encode($snapshot, JSON_UNESCAPED_UNICODE),
];
}
/**
* 后台配置中心保存并严格校验全量配置项
*
* @param array $input
* @return void
* @throws \InvalidArgumentException
*/
public static function saveConfigs(array $input): void
{
$defaults = self::defaults();
$clean = [];
// 1. 校验订单默认抽佣兜底配置
if (isset($input['default_calc_mode'])) {
if (!in_array($input['default_calc_mode'], ['percent', 'fixed', 'mixed'], true)) {
throw new \InvalidArgumentException('请选择有效的默认抽佣计费方式');
}
$clean['default_calc_mode'] = $input['default_calc_mode'];
}
if (isset($input['default_commission_rate'])) {
if (!is_numeric($input['default_commission_rate']) || (float)$input['default_commission_rate'] < 0 || (float)$input['default_commission_rate'] > 100) {
throw new \InvalidArgumentException('默认抽佣比例必须在 0% ~ 100% 之间');
}
$clean['default_commission_rate'] = number_format(round((float)$input['default_commission_rate'], 2), 2, '.', '');
}
foreach (['default_fixed_fee' => '默认单笔固定抽佣', 'default_min_commission' => '默认单笔保底抽佣', 'default_max_commission' => '默认单笔封顶抽佣'] as $k => $label) {
if (isset($input[$k])) {
if (!is_numeric($input[$k]) || (float)$input[$k] < 0) {
throw new \InvalidArgumentException($label . '不能为负数');
}
$clean[$k] = number_format(round((float)$input[$k], 2), 2, '.', '');
}
}
if (isset($clean['default_min_commission'], $clean['default_max_commission'])) {
$minC = (float)$clean['default_min_commission'];
$maxC = (float)$clean['default_max_commission'];
if ($minC > 0 && $maxC > 0 && $minC > $maxC) {
throw new \InvalidArgumentException('默认单笔保底抽佣不能大于单笔封顶抽佣');
}
}
if (isset($input['include_diff_in_commission'])) {
$clean['include_diff_in_commission'] = (int)$input['include_diff_in_commission'] === 1 ? '1' : '0';
}
// 2. 校验提现与微信小程序商家转账配置
if (isset($input['withdraw_enabled'])) {
$clean['withdraw_enabled'] = (int)$input['withdraw_enabled'] === 1 ? '1' : '0';
}
if (isset($input['withdraw_close_tip'])) {
$clean['withdraw_close_tip'] = mb_substr(trim((string)$input['withdraw_close_tip']), 0, 200);
}
if (isset($input['withdraw_channels'])) {
$chArr = is_array($input['withdraw_channels']) ? $input['withdraw_channels'] : explode(',', (string)$input['withdraw_channels']);
$validCh = [];
foreach ($chArr as $c) {
$c = trim((string)$c);
if (in_array($c, ['wechat', 'bank'], true) && !in_array($c, $validCh, true)) {
$validCh[] = $c;
}
}
if (empty($validCh)) {
throw new \InvalidArgumentException('请至少勾选一种开放提现渠道');
}
$clean['withdraw_channels'] = implode(',', $validCh);
}
if (isset($input['withdraw_require_realname'])) {
$clean['withdraw_require_realname'] = (int)$input['withdraw_require_realname'] === 1 ? '1' : '0';
}
if (isset($input['withdraw_min_amount'])) {
if (!is_numeric($input['withdraw_min_amount']) || (float)$input['withdraw_min_amount'] < 0.10) {
throw new \InvalidArgumentException('符合微信小程序商家转账规范,单笔最低提现金额不得低于 ¥0.10');
}
$clean['withdraw_min_amount'] = number_format(round((float)$input['withdraw_min_amount'], 2), 2, '.', '');
}
if (isset($input['withdraw_max_amount'])) {
if (!is_numeric($input['withdraw_max_amount']) || (float)$input['withdraw_max_amount'] > self::WX_TRANSFER_HARD_MAX) {
throw new \InvalidArgumentException('微信商家转账官方规定单笔最高金额不得超过 ¥20,000.00');
}
$clean['withdraw_max_amount'] = number_format(round((float)$input['withdraw_max_amount'], 2), 2, '.', '');
}
if (isset($clean['withdraw_min_amount'], $clean['withdraw_max_amount']) && (float)$clean['withdraw_min_amount'] > (float)$clean['withdraw_max_amount']) {
throw new \InvalidArgumentException('单笔最低提现金额不能大于单笔最高提现金额');
}
if (isset($input['withdraw_daily_amount'])) {
if (!is_numeric($input['withdraw_daily_amount']) || (float)$input['withdraw_daily_amount'] < 0) {
throw new \InvalidArgumentException('单人单日累计提现上限不能为负数');
}
$clean['withdraw_daily_amount'] = number_format(round((float)$input['withdraw_daily_amount'], 2), 2, '.', '');
if ((float)$clean['withdraw_daily_amount'] > 0 && isset($clean['withdraw_min_amount']) && (float)$clean['withdraw_daily_amount'] < (float)$clean['withdraw_min_amount']) {
throw new \InvalidArgumentException('单人单日累计上限不能低于单笔最低提现金额');
}
}
if (isset($input['withdraw_daily_count'])) {
$clean['withdraw_daily_count'] = (string)max(0, (int)$input['withdraw_daily_count']);
}
if (isset($input['withdraw_allow_weekdays'])) {
$dayArr = is_array($input['withdraw_allow_weekdays']) ? $input['withdraw_allow_weekdays'] : explode(',', (string)$input['withdraw_allow_weekdays']);
$validDays = [];
foreach ($dayArr as $d) {
$di = (int)$d;
if ($di >= 1 && $di <= 7 && !in_array($di, $validDays, true)) {
$validDays[] = $di;
}
}
if (empty($validDays)) {
throw new \InvalidArgumentException('请至少选择一天允许提现的周期');
}
sort($validDays);
$clean['withdraw_allow_weekdays'] = implode(',', $validDays);
}
if (isset($input['withdraw_time_start'])) {
$ts = trim((string)$input['withdraw_time_start']);
if (!preg_match('/^([01]\d|2[0-3]):[0-5]\d$/', $ts)) {
throw new \InvalidArgumentException('每日开始时间格式须为 HH:mm(如 00:00)');
}
$clean['withdraw_time_start'] = $ts;
}
if (isset($input['withdraw_time_end'])) {
$te = trim((string)$input['withdraw_time_end']);
if (!preg_match('/^([01]\d|2[0-3]):[0-5]\d$/', $te)) {
throw new \InvalidArgumentException('每日结束时间格式须为 HH:mm(如 23:59)');
}
$clean['withdraw_time_end'] = $te;
}
if (isset($clean['withdraw_time_start'], $clean['withdraw_time_end']) && $clean['withdraw_time_start'] >= $clean['withdraw_time_end']) {
throw new \InvalidArgumentException('每日允许提现开始时间必须早于结束时间');
}
if (isset($input['withdraw_fee_mode'])) {
if (!in_array($input['withdraw_fee_mode'], ['percent', 'fixed', 'free'], true)) {
throw new \InvalidArgumentException('请选择有效的手续费计收方式');
}
$clean['withdraw_fee_mode'] = $input['withdraw_fee_mode'];
}
if (isset($input['withdraw_fee_rate'])) {
if (!is_numeric($input['withdraw_fee_rate']) || (float)$input['withdraw_fee_rate'] < 0 || (float)$input['withdraw_fee_rate'] > 50) {
throw new \InvalidArgumentException('提现手续费率须在 0% ~ 50% 之间');
}
$clean['withdraw_fee_rate'] = number_format(round((float)$input['withdraw_fee_rate'], 2), 2, '.', '');
}
foreach (['withdraw_fee_fixed' => '单笔固定手续费', 'withdraw_fee_min' => '单笔最低保底手续费', 'withdraw_fee_max' => '单笔最高封顶手续费'] as $k => $label) {
if (isset($input[$k])) {
if (!is_numeric($input[$k]) || (float)$input[$k] < 0) {
throw new \InvalidArgumentException($label . '不能为负数');
}
$clean[$k] = number_format(round((float)$input[$k], 2), 2, '.', '');
}
}
if (isset($clean['withdraw_fee_min'], $clean['withdraw_fee_max'])) {
$fMin = (float)$clean['withdraw_fee_min'];
$fMax = (float)$clean['withdraw_fee_max'];
if ($fMin > 0 && $fMax > 0 && $fMin > $fMax) {
throw new \InvalidArgumentException('单笔最低保底手续费不能大于最高封顶手续费');
}
}
// 校验最低提现额扣除手续费后不低于微信官方 0.10 元底线
$mergedPreview = array_merge(self::getAll(), $clean);
$minTestAmt = (float)$mergedPreview['withdraw_min_amount'];
if (($mergedPreview['withdraw_fee_mode'] ?? 'percent') === 'fixed' && $minTestAmt - (float)$mergedPreview['withdraw_fee_fixed'] < self::WX_TRANSFER_MIN_ARRIVE) {
throw new \InvalidArgumentException('单笔最低提现金额扣除固定手续费后实际到账低于 ¥0.10,请调高最低提现额或调低固定手续费');
}
if (isset($input['withdraw_transfer_mode'])) {
if (!in_array($input['withdraw_transfer_mode'], ['wx_mch_transfer', 'offline_manual'], true)) {
throw new \InvalidArgumentException('无效的微信渠道打款执行方式');
}
$clean['withdraw_transfer_mode'] = $input['withdraw_transfer_mode'];
}
if (isset($input['withdraw_audit_mode'])) {
if (!in_array($input['withdraw_audit_mode'], ['manual', 'auto_small'], true)) {
throw new \InvalidArgumentException('无效的提现审核模式');
}
$clean['withdraw_audit_mode'] = $input['withdraw_audit_mode'];
}
if (isset($input['withdraw_auto_max_amount'])) {
if (!is_numeric($input['withdraw_auto_max_amount']) || (float)$input['withdraw_auto_max_amount'] < 0 || (float)$input['withdraw_auto_max_amount'] > self::WX_TRANSFER_HARD_MAX) {
throw new \InvalidArgumentException('小额免审自动转账上限须在 ¥0 ~ ¥20,000 之间');
}
$clean['withdraw_auto_max_amount'] = number_format(round((float)$input['withdraw_auto_max_amount'], 2), 2, '.', '');
}
foreach ([
'wx_transfer_scene_id' => ['label' => '微信转账场景ID', 'max' => 16],
'wx_recv_perception' => ['label' => '微信收款用户感知', 'max' => 32],
'wx_job_type' => ['label' => '微信场景报备-岗位类型', 'max' => 32],
'wx_reward_desc' => ['label' => '微信场景报备-报酬说明', 'max' => 32],
'wx_transfer_remark' => ['label' => '微信零钱转账备注', 'max' => 32],
] as $k => $meta) {
if (isset($input[$k])) {
$val = trim((string)$input[$k]);
if ($val === '') {
throw new \InvalidArgumentException($meta['label'] . '不能为空(微信支付 APIv3 商家转账必填项)');
}
if (mb_strlen($val) > $meta['max']) {
throw new \InvalidArgumentException($meta['label'] . '不能超过 ' . $meta['max'] . ' 个字符(微信接口长度限制)');
}
$clean[$k] = $val;
}
}
$now = time();
Db::startTrans();
try {
foreach ($clean as $name => $val) {
if (!array_key_exists($name, $defaults)) {
continue;
}
$group = strpos($name, 'default_') === 0 || $name === 'include_diff_in_commission' ? 'commission' : 'withdraw';
$exists = Db::name('finance_config')->where('name', $name)->find();
if ($exists) {
Db::name('finance_config')->where('id', (int)$exists['id'])->update([
'value' => $val,
'updatetime' => $now,
]);
} else {
Db::name('finance_config')->insert([
'name' => $name,
'group' => $group,
'title' => $name,
'value' => $val,
'remark' => '',
'updatetime' => $now,
]);
}
}
Db::commit();
} catch (\Throwable $e) {
Db::rollback();
throw $e;
}
self::clearCache();
}
}