2025-11-20 19:03:46 +08:00

520 lines
19 KiB
PHP
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace addons\shopro\service;
use addons\shopro\exception\ShoproException;
use think\Db;
use think\Log;
use think\exception\HttpResponseException;
use app\admin\model\shopro\Withdraw as WithdrawModel;
use app\admin\model\shopro\WithdrawLog as WithdrawLogModel;
use addons\shopro\library\Operator;
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;
class Withdraw
{
protected $user = null;
/**
* @var array
*/
public $config = [];
public function __construct($user = null)
{
if ($user) {
$this->user = is_numeric($user) ? User::get($user) : $user;
}
// 提现规则
$config = sheep_config('shop.recharge_withdraw.withdraw');
$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 位小数
$config['charge_rate'] = round((floatval($config['charge_rate']) * 0.01), 3);
$this->config = $config;
}
// 发起提现申请
public function apply($params)
{
$type = $params['type'];
$money = $params['money'] ?? 0;
$money = (string)$money;
// 手续费
$charge = bcmul($money, (string)$this->config['charge_rate'], 2);
// 检查提现规则
$this->checkApply($type, $money, $charge);
// 获取账号信息
$withdrawInfo = $this->getAccountInfo($type, $params);
// 添加提现记录
$withdraw = new WithdrawModel();
$withdraw->user_id = $this->user->id;
$withdraw->amount = $money;
$withdraw->charge_fee = $charge;
$withdraw->charge_rate = $this->config['charge_rate'];
$withdraw->withdraw_sn = get_sn($this->user->id, 'W');
$withdraw->withdraw_type = $type;
$withdraw->withdraw_info = $withdrawInfo;
$withdraw->status = 0;
$withdraw->platform = request()->header('platform');
$withdraw->save();
// 佣金钱包变动
WalletService::change($this->user, 'commission', -bcadd($charge, $money, 2), 'withdraw', [
'withdraw_id' => $withdraw->id,
'amount' => $withdraw->amount,
'charge_fee' => $withdraw->charge_fee,
'charge_rate' => $withdraw->charge_rate,
]);
$this->handleLog($withdraw, '用户发起提现申请', $this->user);
return $withdraw;
}
// 继续提现
public function retry($params)
{
$withdraw = WithdrawModel::where('user_id', $this->user->id)
->where('withdraw_sn', $params['withdraw_sn'])
->where('status', 1)
->where('withdraw_type', 'wechat')
->find();
if (!$withdraw) {
throw new ShoproException('提现记录不存在');
}
if (request()->header('platform') !== $withdraw->platform) {
throw new ShoproException('请在发起该次提现的平台操作');
}
$this->handleLog($withdraw, '用户重新发起提现申请');
// 实时检查提现单状态
return $this->checkWechatTransferResult($withdraw);
}
// 取消提现(适用于微信商家转账)
public function cancel($params)
{
$withdraw = WithdrawModel::where('user_id', $this->user->id)
->where('withdraw_sn', $params['withdraw_sn'])
->where('status', 1)
->where('withdraw_type', 'wechat')
->find();
if (!$withdraw) {
throw new ShoproException('提现记录不存在');
}
if (request()->header('platform') !== $withdraw->platform) {
throw new ShoproException('请在发起该次提现的平台操作');
}
// 实时检查提现单状态
$withdraw = $this->checkWechatTransferResult($withdraw);
if ($withdraw->wechat_transfer_state !== 'WAIT_USER_CONFIRM') {
throw new ShoproException('该提现单暂无法发起取消操作,请联系客服');
}
// 提交微信撤销单
$payService = new PayService($withdraw->withdraw_type, $withdraw->platform);
try {
$result = $payService->cancelTransferOrder([
'withdraw_sn' => $withdraw->withdraw_sn,
]);
} catch (\Exception $e) {
\think\Log::error('撤销提现失败: ' . ': 行号: ' . $e->getLine() . ': 文件: ' . $e->getFile() . ': 错误信息: ' . $e->getMessage());
$this->handleLog($withdraw, '撤销微信商家转账失败: ' . $e->getMessage());
throw new ShoproException($e->getMessage());
}
$withdraw->wechat_transfer_state = $result['state'];
$withdraw->save();
$this->handleLog($withdraw, '申请撤销微信商家转账成功,报文信息:' . json_encode($result, JSON_UNESCAPED_UNICODE));
return $withdraw;
}
// 新版本微信商家转账
// https://pay.weixin.qq.com/doc/v3/merchant/4012711988
public function handleWechatTransfer($withdraw)
{
operate_disabled();
$payService = new PayService($withdraw->withdraw_type, $withdraw->platform);
$payload = [
'_action' => 'mch_transfer', // 新版转账
'out_bill_no' => $withdraw->withdraw_sn,
'transfer_scene_id' => '1005',
'openid' => $withdraw->withdraw_info['微信ID'] ?? '',
'user_name' => $withdraw->withdraw_info['真实姓名'] ?? '', // 明文传参即可sdk 会自动加密
'transfer_amount' => $withdraw->amount,
'transfer_remark' => "用户[" . ($withdraw->withdraw_info['微信用户'] ?? '') . "]提现",
'transfer_scene_report_infos' => [
['info_type' => '岗位类型', 'info_content' => '推广专员'],
['info_type' => '报酬说明', 'info_content' => '推广佣金报酬'],
],
];
try {
list($response, $transferData) = $payService->transfer($payload);
$withdraw->status = 1; // 提现处理中
$withdraw->wechat_transfer_state = $response['state'];
$withdraw->payment_json = json_encode($response, JSON_UNESCAPED_UNICODE);
$withdraw->save();
$this->handleLog($withdraw, '用户发起微信商家转账收款,报文信息:' . json_encode($response, JSON_UNESCAPED_UNICODE), $this->user);
return $transferData;
} catch (\Exception $e) {
\think\Log::error('提现失败: ' . ': 行号: ' . $e->getLine() . ': 文件: ' . $e->getFile() . ': 错误信息: ' . $e->getMessage());
// $withdraw->wechat_transfer_state = 'NOT_FOUND';
$withdraw->save();
$this->handleFail($withdraw, $e->getMessage());
throw new ShoproException($e->getMessage());
}
// 发起提现失败,自动处理退还佣金
}
// 支付宝提现
public function handleAlipayWithdraw($withdraw)
{
operate_disabled();
Db::startTrans();
try {
$withdraw = $this->handleAgree($withdraw, $this->user);
$withdraw = $this->handleWithdraw($withdraw, $this->user);
Db::commit();
} catch (ShoproException $e) {
Db::rollback(); // 回滚事务因为7天限制等原因导致打款失败
throw new ShoproException($e->getMessage());
} catch (HttpResponseException $e) {
Db::rollback();
$data = $e->getResponse()->getData();
$message = $data ? ($data['msg'] ?? '') : $e->getMessage();
throw new ShoproException($message);
} catch (\Exception $e) {
Db::rollback();
throw new ShoproException($e->getMessage());
}
}
// 同意操作
public function handleAgree($withdraw, $oper = null)
{
if ($withdraw->status != 0) {
throw new ShoproException('请勿重复操作');
}
$withdraw->status = 1;
$withdraw->save();
return $this->handleLog($withdraw, '同意提现申请', $oper);
}
// 处理打款
public function handleWithdraw($withdraw, $oper = null)
{
$withDrawStatus = false;
if ($withdraw->status != 1) {
throw new ShoproException('请勿重复操作');
}
// 检查提现打款时间限制(小时)
$limitHours = isset($this->config['days_7']) ? intval($this->config['days_7']) : 0;
if ($limitHours > 0) {
$createtime = intval($withdraw->createtime);
if ($createtime > 0) {
$hoursDiff = (time() - $createtime) / 3600; // 3600秒 = 1小时
if ($hoursDiff < $limitHours) {
$remainingHours = ceil($limitHours - $hoursDiff);
$remainingDays = floor($remainingHours / 24);
$remainingHoursInDay = $remainingHours % 24;
$timeText = '';
if ($remainingDays > 0) {
$timeText = $remainingDays . '天';
}
if ($remainingHoursInDay > 0) {
$timeText .= ($timeText ? '零' : '') . $remainingHoursInDay . '小时';
}
if (empty($timeText)) {
$timeText = '1小时';
}
throw new ShoproException('提现申请时间未超过' . $limitHours . '小时,还需等待' . $timeText . '才能放款');
}
}
}
if ($withdraw->withdraw_type !== 'bank') {
$withDrawStatus = $this->handleTransfer($withdraw);
} else {
$withDrawStatus = true;
}
if ($withDrawStatus) {
$withdraw->status = 2;
$withdraw->paid_fee = $withdraw->amount;
$withdraw->save();
return $this->handleLog($withdraw, '已打款', $oper);
}
return $withdraw;
}
// 拒绝操作
public function handleRefuse($withdraw, $refuse_msg)
{
if ($withdraw->status != 0 && $withdraw->status != 1) {
throw new ShoproException('请勿重复操作');
}
$withdraw->status = -1;
$withdraw->save();
// 退回用户佣金
WalletService::change($this->user, 'commission', bcadd($withdraw->charge_fee, $withdraw->amount, 2), 'withdraw_error', [
'withdraw_id' => $withdraw->id,
'amount' => $withdraw->amount,
'charge_fee' => $withdraw->charge_fee,
'charge_rate' => $withdraw->charge_rate,
]);
return $this->handleLog($withdraw, '拒绝:' . $refuse_msg);
}
// 失败
public function handleFail($withdraw, $refuse_msg, $status = -2)
{
if ($withdraw->status != 0 && $withdraw->status != 1) {
throw new ShoproException('请勿重复操作');
}
$withdraw->status = $status;
$withdraw->save();
// 退回用户佣金
WalletService::change($this->user, 'commission', bcadd($withdraw->charge_fee, $withdraw->amount, 2), 'withdraw_error', [
'withdraw_id' => $withdraw->id,
'amount' => $withdraw->amount,
'charge_fee' => $withdraw->charge_fee,
'charge_rate' => $withdraw->charge_rate,
]);
return $this->handleLog($withdraw, '提现失败,已退还提现佣金:' . $refuse_msg);
}
// 商家转账回调
public function handleWechatTransferNotify($action, $withdrawSn, $originData)
{
$withdraw = WithdrawModel::where('withdraw_sn', $withdrawSn)->find();
if (!$withdraw) {
throw new ShoproException('提现记录不存在');
}
if ($withdraw->status !== 1) {
throw new ShoproException('提现记录状态不正确');
}
$this->user = User::get($withdraw->user_id);
if (!$this->user) {
throw new ShoproException('用户不存在');
}
$withdraw->wechat_transfer_state = $action;
switch ($action) {
case 'SUCCESS':
$withdraw->status = 2;
$this->handleLog($withdraw, '微信商家转账成功,报文信息: ' . json_encode($originData, JSON_UNESCAPED_UNICODE), $this->user);
$withdraw->save();
break;
case 'FAILED':
$this->handleFail($withdraw, '微信商家转账失败,报文信息: ' . json_encode($originData, JSON_UNESCAPED_UNICODE), -2);
break;
case 'CANCELLED': // 撤销完成
$this->handleFail($withdraw, '微信商家转账撤销成功,报文信息: ' . json_encode($originData, JSON_UNESCAPED_UNICODE), -3);
break;
default:
$this->handleLog($withdraw, '回调结果不是终态,报文信息: ' . json_encode($originData, JSON_UNESCAPED_UNICODE), $this->user);
}
return true;
}
// 检查微信商家转账单结果
public function checkWechatTransferResult($withdraw)
{
if ($withdraw->createtime < strtotime('-30 day')) {
throw new ShoproException('提现单据已过期,请联系客服解决');
}
$payService = new PayService($withdraw->withdraw_type, $withdraw->platform);
$result = $payService->queryTransferOrder([
'withdraw_sn' => $withdraw->withdraw_sn,
]);
$withdraw->wechat_transfer_state = $result['state'];
$withdraw->save();
$this->handleLog($withdraw, '查询微信商家转账单,报文信息:' . json_encode($result, JSON_UNESCAPED_UNICODE));
return $withdraw;
}
// 企业付款提现
private function handleTransfer($withdraw)
{
operate_disabled();
$type = $withdraw->withdraw_type;
$platform = $withdraw->platform;
$payService = new PayService($type, $platform);
if ($type == 'alipay') {
$payload = [
'out_biz_no' => $withdraw->withdraw_sn,
'trans_amount' => $withdraw->amount,
'product_code' => 'TRANS_ACCOUNT_NO_PWD',
'biz_scene' => 'DIRECT_TRANSFER',
// 'order_title' => '余额提现到',
'remark' => '用户提现',
'payee_info' => [
'identity' => $withdraw->withdraw_info['支付宝账户'] ?? '',
'identity_type' => 'ALIPAY_LOGON_ID',
'name' => $withdraw->withdraw_info['真实姓名'] ?? '',
]
];
}
try {
list($code, $response) = $payService->transfer($payload);
if ($code === 1) {
$withdraw->payment_json = json_encode($response, JSON_UNESCAPED_UNICODE);
$withdraw->save();
return true;
}
throw new ShoproException(json_encode($response, JSON_UNESCAPED_UNICODE));
} catch (HttpResponseException $e) {
$data = $e->getResponse()->getData();
$message = $data ? ($data['msg'] ?? '') : $e->getMessage();
throw new ShoproException($message);
} catch (\Exception $e) {
\think\Log::error('提现失败:' . ' 行号:' . $e->getLine() . '文件:' . $e->getFile() . '错误信息:' . $e->getMessage());
$this->handleLog($withdraw, '提现失败:' . $e->getMessage());
throw new ShoproException($e->getMessage()); // 弹出错误信息
}
return false;
}
// 添加日志
private function handleLog($withdraw, $oper_info, $oper = null)
{
$oper = Operator::get($oper);
WithdrawLogModel::insert([
'withdraw_id' => $withdraw->id,
'content' => $oper_info,
'oper_type' => $oper['type'],
'oper_id' => $oper['id'],
'createtime' => time()
]);
return $withdraw;
}
// 提现条件检查
private function checkApply($type, $money, $charge)
{
if (!in_array($type, $this->config['methods'])) {
throw new ShoproException('暂不支持该提现方式');
}
if ($money <= 0) {
throw new ShoproException('请输入正确的提现金额');
}
// 检查最小提现金额
if ($this->config['min_amount'] > 0 && $money < $this->config['min_amount']) {
throw new ShoproException('提现金额不能少于 ' . $this->config['min_amount'] . '元');
}
// 检查最大提现金额
if ($this->config['max_amount'] > 0 && $money > $this->config['max_amount']) {
throw new ShoproException('提现金额不能大于 ' . $this->config['max_amount'] . '元');
}
if ($this->user->commission < bcadd($charge, $money, 2)) {
throw new ShoproException('可提现佣金不足');
}
// 检查最大提现次数
if (isset($this->config['max_num']) && $this->config['max_num'] > 0) {
$start_time = $this->config['num_unit'] == 'day' ? strtotime(date('Y-m-d', time())) : strtotime(date('Y-m', time()));
$num = WithdrawModel::where('user_id', $this->user->id)->where('createtime', '>=', $start_time)->count();
if ($num >= $this->config['max_num']) {
throw new ShoproException('每' . ($this->config['num_unit'] == 'day' ? '日' : '月') . '提现次数不能大于 ' . $this->config['max_num'] . '次');
}
}
}
// 获取提现账户信息
private function getAccountInfo($type, $params)
{
$platform = request()->header('platform');
switch ($type) {
case 'wechat':
if ($platform == 'App') {
$platform = 'openPlatform';
} elseif (in_array($platform, ['WechatOfficialAccount', 'WechatMiniProgram'])) {
$platform = lcfirst(str_replace('Wechat', '', $platform));
}
$thirdOauth = ThirdOauth::where('provider', 'wechat')->where('platform', $platform)->where('user_id', $this->user->id)->find();
if (!$thirdOauth) {
throw new ShoproException('请先绑定微信账号', -1);
}
$withdrawInfo = [
'真实姓名' => $params['account_name'],
'微信用户' => $thirdOauth['nickname'] ?: $this->user['nickname'],
'微信ID' => $thirdOauth['openid'],
];
break;
case 'alipay':
$withdrawInfo = [
'真实姓名' => $params['account_name'],
'支付宝账户' => $params['account_no']
];
break;
case 'bank':
$withdrawInfo = [
'真实姓名' => $params['account_name'],
'开户行' => $params['account_header'] ?? '',
'银行卡号' => $params['account_no']
];
break;
}
if (!isset($withdrawInfo)) {
throw new ShoproException('您的提现信息有误');
}
return $withdrawInfo;
}
}