1. 前端个人中心新增真实姓名输入与实名状态展示,支持微信提现核验 2. 后端Auth接口新增真实姓名修改与手机号冲突校验逻辑 3. 后台新增用户详情综合看板,展示佣金、提现、分销等全量数据 4. 优化微信转账逻辑,支持paying状态重试时查询微信单号避免重复创建 5. 新增数据库ALTER脚本同步transfer_status字段结构 6. 新增部署与数据库变更工具脚本,支持线上线下环境同步
599 行
23 KiB
PHP
599 行
23 KiB
PHP
<?php
|
||
|
||
namespace app\common\library;
|
||
|
||
use addons\epay\library\Service as EpayService;
|
||
use think\Db;
|
||
use think\Log;
|
||
|
||
/**
|
||
* 微信商家转账(V3「商家转账」,接口 /v3/fund-app/mch-transfer/transfer-bills)
|
||
*
|
||
* 业务链路:
|
||
* 后台审核通过 → submit() 调微信创建转账单 → 用户微信确认收款 → 微信回调 transferNotify()
|
||
* → applyResult() 把提现单置为 paid,并记支出流水/站内信。
|
||
*
|
||
* 约束(微信侧):
|
||
* - 只能 PHP 8.0+ 且 version=v3(MchTransfer 插件只存在于 v3new);
|
||
* - 转账场景固定 1005「佣金报酬」,必须带两条 transfer_scene_report_infos;
|
||
* - notify_url 必须 HTTPS 且公网可访问;
|
||
* - 单笔最低 0.1 元;≥2000 元必须传收款人姓名;
|
||
* - 资金从「运营账户」出,余额不足会失败。
|
||
*/
|
||
class WxTransfer
|
||
{
|
||
/** 默认转账场景:佣金报酬 */
|
||
const DEFAULT_SCENE_ID = '1005';
|
||
|
||
/** 提现单状态流转:auditing → paying → paid / failed */
|
||
/**
|
||
* 判断当前环境是否具备商家转账能力
|
||
*
|
||
* @return bool
|
||
*/
|
||
public static function isReady()
|
||
{
|
||
if (PHP_VERSION_ID < 80000) {
|
||
return false;
|
||
}
|
||
if (!EpayService::isVersionV3()) {
|
||
return false;
|
||
}
|
||
|
||
self::loadSdk();
|
||
|
||
return class_exists('\Yansongda\Pay\Plugin\Wechat\V3\Marketing\MchTransfer\CreatePlugin')
|
||
&& class_exists('\Yansongda\Pay\Shortcut\Wechat\MchTransferShortcut');
|
||
}
|
||
|
||
/**
|
||
* 创建转账单
|
||
*
|
||
* 调用方需先把提现单置为 paying 并写入 out_bill_no(同一单重试必须复用同一单号)。
|
||
*
|
||
* @param array $withdraw fc_withdraw 行
|
||
* @return array{state:string,out_bill_no:string,transfer_bill_no:string,package_info:string}
|
||
* @throws \RuntimeException
|
||
*/
|
||
public static function submit(array $withdraw)
|
||
{
|
||
if (!self::isReady()) {
|
||
throw new \RuntimeException('当前环境不支持商家转账(需要 PHP 8.0+ 且 epay 插件 version=v3)');
|
||
}
|
||
|
||
$outBillNo = trim((string)(isset($withdraw['out_bill_no']) ? $withdraw['out_bill_no'] : ''));
|
||
if ($outBillNo === '') {
|
||
throw new \RuntimeException('提现单缺少商户单号(out_bill_no)');
|
||
}
|
||
|
||
$ext = Db::name('fc_user_ext')->where('user_id', (int)$withdraw['user_id'])->find();
|
||
if (!$ext) {
|
||
throw new \RuntimeException('用户扩展信息不存在,无法转账');
|
||
}
|
||
|
||
$openid = trim((string)$ext['openid']);
|
||
if ($openid === '' || strpos($openid, 'h5_') === 0) {
|
||
throw new \RuntimeException('该用户没有可用的微信 openid,无法转账');
|
||
}
|
||
|
||
$amountCents = (int)round((float)$withdraw['amount'] * 100);
|
||
if ($amountCents < 10) {
|
||
throw new \RuntimeException('单笔转账金额不能低于 0.1 元');
|
||
}
|
||
|
||
$realname = trim((string)(isset($ext['realname']) ? $ext['realname'] : ''));
|
||
if ($amountCents >= 200000 && $realname === '') {
|
||
throw new \RuntimeException('转账金额≥2000 元时必须填写收款人真实姓名');
|
||
}
|
||
|
||
$config = EpayService::getConfig('wechat', []);
|
||
$pay = \Yansongda\Pay\Pay::wechat($config);
|
||
|
||
$params = [
|
||
// mini:用 miniapp_id 作为 appid
|
||
'_type' => 'mini',
|
||
'out_bill_no' => $outBillNo,
|
||
'openid' => $openid,
|
||
'transfer_amount' => $amountCents,
|
||
'transfer_remark' => '分销佣金提现',
|
||
'transfer_scene_id' => self::sceneId(),
|
||
'transfer_scene_report_infos' => self::sceneInfos(),
|
||
'user_recv_perception' => '劳务报酬',
|
||
];
|
||
$notifyUrl = self::notifyUrl();
|
||
if ($notifyUrl !== '') {
|
||
$params['notify_url'] = $notifyUrl;
|
||
}
|
||
if ($realname !== '') {
|
||
$params['user_name'] = $realname;
|
||
}
|
||
|
||
try {
|
||
$result = $pay->mchTransfer($params);
|
||
} catch (\Throwable $e) {
|
||
$detail = self::errorDetail($e);
|
||
Log::record('商家转账创建失败:提现单 ' . $withdraw['withdraw_no'] . ',' . $detail, 'error');
|
||
throw new \RuntimeException('微信转账失败:' . $detail);
|
||
}
|
||
|
||
$res = $result instanceof \Yansongda\Supports\Collection ? $result->all() : (array)$result;
|
||
$state = isset($res['state']) ? (string)$res['state'] : '';
|
||
if ($state === '') {
|
||
Log::record('商家转账应答异常:' . json_encode($res, JSON_UNESCAPED_UNICODE), 'error');
|
||
throw new \RuntimeException('微信转账应答异常');
|
||
}
|
||
|
||
return [
|
||
'state' => $state,
|
||
'out_bill_no' => $outBillNo,
|
||
'transfer_bill_no' => isset($res['transfer_bill_no']) ? (string)$res['transfer_bill_no'] : '',
|
||
'package_info' => isset($res['package_info']) ? (string)$res['package_info'] : '',
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 查询转账单(回调丢失时补偿用)
|
||
*
|
||
* @param string $outBillNo
|
||
* @return array
|
||
* @throws \RuntimeException
|
||
*/
|
||
public static function query($outBillNo)
|
||
{
|
||
if (!self::isReady()) {
|
||
throw new \RuntimeException('当前环境不支持商家转账');
|
||
}
|
||
|
||
$config = EpayService::getConfig('wechat', []);
|
||
$pay = \Yansongda\Pay\Pay::wechat($config);
|
||
|
||
try {
|
||
$result = $pay->mchTransfer(['_action' => 'query', 'out_bill_no' => $outBillNo]);
|
||
} catch (\Throwable $e) {
|
||
throw new \RuntimeException('查询转账单失败:' . $e->getMessage());
|
||
}
|
||
|
||
return $result instanceof \Yansongda\Supports\Collection ? $result->all() : (array)$result;
|
||
}
|
||
|
||
/**
|
||
* 审核通过并自动发起商家转账
|
||
*
|
||
* 流程:auditing → paying(落 out_bill_no)→ 调微信创建转账单 → 立即结果 / 等待回调
|
||
* 失败时回滚为 auditing,允许重新审核。
|
||
* 若处于 paying 状态重试,先向微信查询单号状态,避免重复创建转账单。
|
||
*
|
||
* @param int $withdrawId
|
||
* @return array{state:string,withdrawState:string}
|
||
* @throws \RuntimeException
|
||
*/
|
||
public static function approve($withdrawId)
|
||
{
|
||
$withdraw = Db::name('fc_withdraw')->where('id', (int)$withdrawId)->find();
|
||
if (!$withdraw) {
|
||
throw new \RuntimeException('提现记录不存在');
|
||
}
|
||
if (!in_array((string)$withdraw['status'], ['auditing', 'paying'], true)) {
|
||
throw new \RuntimeException('该提现记录已处理(当前状态:' . $withdraw['status'] . ')');
|
||
}
|
||
|
||
// 若当前单据卡在 paying 状态,先主动向微信查询是否已创建转账单
|
||
if ((string)$withdraw['status'] === 'paying' && !empty($withdraw['out_bill_no'])) {
|
||
try {
|
||
$queryRes = self::query($withdraw['out_bill_no']);
|
||
if (!empty($queryRes['state'])) {
|
||
$state = self::applyResult($queryRes);
|
||
$withdrawState = (string)Db::name('fc_withdraw')->where('id', (int)$withdraw['id'])->value('status');
|
||
return ['state' => $state, 'withdrawState' => $withdrawState];
|
||
}
|
||
} catch (\Throwable $e) {
|
||
Log::record('提现记录 #' . $withdrawId . ' 处于 paying,微信查询补偿跳过:' . $e->getMessage(), 'notice');
|
||
}
|
||
}
|
||
|
||
$outBillNo = !empty($withdraw['out_bill_no']) ? $withdraw['out_bill_no'] : self::makeOutBillNo($withdraw);
|
||
$now = time();
|
||
|
||
// 1) 先落「转账中」,防止重复点击造成重复转账
|
||
Db::transaction(function () use ($withdraw, $outBillNo, $now) {
|
||
$locked = Db::name('fc_withdraw')->where('id', (int)$withdraw['id'])->lock(true)->find();
|
||
if (!$locked || !in_array((string)$locked['status'], ['auditing', 'paying'], true)) {
|
||
throw new \RuntimeException('该提现记录已处理,请刷新后重试');
|
||
}
|
||
Db::name('fc_withdraw')->where('id', (int)$locked['id'])->update([
|
||
'status' => 'paying',
|
||
'out_bill_no' => $outBillNo,
|
||
'transfer_status' => '',
|
||
'transfer_id' => '',
|
||
'package_info' => '',
|
||
'fail_reason' => '',
|
||
'updatetime' => $now,
|
||
]);
|
||
});
|
||
|
||
// 2) 调微信创建转账单;失败回滚为待审核
|
||
try {
|
||
$res = self::submit(array_merge($withdraw, ['out_bill_no' => $outBillNo]));
|
||
} catch (\Throwable $e) {
|
||
Db::name('fc_withdraw')->where('id', (int)$withdraw['id'])->where('status', 'paying')->update([
|
||
'status' => 'auditing',
|
||
'fail_reason' => mb_substr($e->getMessage(), 0, 200),
|
||
'updatetime' => time(),
|
||
]);
|
||
throw $e;
|
||
}
|
||
|
||
$transferStatus = substr((string)$res['state'], 0, 32);
|
||
Db::name('fc_withdraw')->where('id', (int)$withdraw['id'])->update([
|
||
'transfer_id' => isset($res['transfer_bill_no']) ? (string)$res['transfer_bill_no'] : '',
|
||
'package_info' => isset($res['package_info']) ? (string)$res['package_info'] : '',
|
||
'transfer_status' => $transferStatus,
|
||
'transfer_time' => time(),
|
||
]);
|
||
|
||
// 3) 应用立即结果(SUCCESS → paid;FAIL/CANCELLED → failed;其余保持 paying)
|
||
$state = self::applyResult([
|
||
'out_bill_no' => $outBillNo,
|
||
'state' => $res['state'],
|
||
'transfer_bill_no' => isset($res['transfer_bill_no']) ? (string)$res['transfer_bill_no'] : '',
|
||
'package_info' => isset($res['package_info']) ? (string)$res['package_info'] : '',
|
||
'fail_reason' => isset($res['fail_reason']) ? $res['fail_reason'] : '',
|
||
]);
|
||
|
||
$withdrawState = (string)Db::name('fc_withdraw')->where('id', (int)$withdraw['id'])->value('status');
|
||
|
||
return ['state' => $state, 'withdrawState' => $withdrawState];
|
||
}
|
||
|
||
/**
|
||
* 主动同步单笔提现的转账状态(回调缺失时兜底)
|
||
*
|
||
* @param int $withdrawId
|
||
* @return string 同步后的提现单状态
|
||
*/
|
||
public static function sync($withdrawId)
|
||
{
|
||
$withdraw = Db::name('fc_withdraw')->where('id', (int)$withdrawId)->find();
|
||
if (!$withdraw) {
|
||
return 'ignored';
|
||
}
|
||
if ((string)$withdraw['status'] !== 'paying') {
|
||
return (string)$withdraw['status'];
|
||
}
|
||
|
||
$outBillNo = trim((string)$withdraw['out_bill_no']);
|
||
if ($outBillNo === '') {
|
||
return 'ignored';
|
||
}
|
||
|
||
try {
|
||
$res = self::query($outBillNo);
|
||
} catch (\Throwable $e) {
|
||
Log::record('查询转账单失败:' . $outBillNo . ',' . $e->getMessage(), 'error');
|
||
return 'paying';
|
||
}
|
||
|
||
return self::applyResult($res);
|
||
}
|
||
|
||
/**
|
||
* 撤销「待用户确认」的转账单
|
||
*
|
||
* 仅当提现单为 paying 且有 out_bill_no 时可撤销;撤销后置为 failed,可重新申请。
|
||
*
|
||
* @param int $withdrawId
|
||
* @return string 处理后的提现单状态
|
||
* @throws \RuntimeException
|
||
*/
|
||
public static function cancel($withdrawId)
|
||
{
|
||
$withdraw = Db::name('fc_withdraw')->where('id', (int)$withdrawId)->find();
|
||
if (!$withdraw) {
|
||
throw new \RuntimeException('提现记录不存在');
|
||
}
|
||
if ((string)$withdraw['status'] !== 'paying') {
|
||
throw new \RuntimeException('只有「转账中」的提现单可以撤销(当前:' . $withdraw['status'] . ')');
|
||
}
|
||
$outBillNo = trim((string)$withdraw['out_bill_no']);
|
||
if ($outBillNo === '') {
|
||
throw new \RuntimeException('提现单缺少商户单号,无法撤销');
|
||
}
|
||
|
||
if (!self::isReady()) {
|
||
throw new \RuntimeException('当前环境不支持商家转账');
|
||
}
|
||
|
||
$config = EpayService::getConfig('wechat', []);
|
||
$pay = \Yansongda\Pay\Pay::wechat($config);
|
||
|
||
try {
|
||
$result = $pay->mchTransfer([
|
||
'_action' => 'cancel',
|
||
'_type' => 'mini',
|
||
'out_bill_no' => $outBillNo,
|
||
]);
|
||
} catch (\Throwable $e) {
|
||
$detail = self::errorDetail($e);
|
||
Log::record('撤销商家转账失败:' . $outBillNo . ',' . $detail, 'error');
|
||
throw new \RuntimeException('撤销失败:' . $detail);
|
||
}
|
||
|
||
$res = $result instanceof \Yansongda\Supports\Collection ? $result->all() : (array)$result;
|
||
$state = isset($res['state']) ? (string)$res['state'] : 'CANCELLED';
|
||
|
||
return self::applyResult([
|
||
'out_bill_no' => $outBillNo,
|
||
'state' => $state === 'CANCELLED' ? 'CANCELLED' : $state,
|
||
'transfer_bill_no' => isset($res['transfer_bill_no']) ? (string)$res['transfer_bill_no'] : '',
|
||
'fail_reason' => '已撤销',
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* 应用转账结果(创建应答 / 回调 / 主动查询 共用)
|
||
*
|
||
* @param array $data 微信返回或回调解密后的数据
|
||
* @return string 处理后的提现单状态(paid / failed / paying / ignored)
|
||
*/
|
||
public static function applyResult(array $data)
|
||
{
|
||
$outBillNo = trim((string)(isset($data['out_bill_no']) ? $data['out_bill_no'] : ''));
|
||
if ($outBillNo === '') {
|
||
return 'ignored';
|
||
}
|
||
|
||
$state = strtoupper(trim((string)(isset($data['state']) ? $data['state'] : '')));
|
||
$transferBillNo = isset($data['transfer_bill_no']) ? (string)$data['transfer_bill_no'] : '';
|
||
$packageInfo = isset($data['package_info']) ? (string)$data['package_info'] : '';
|
||
$failReason = isset($data['fail_reason']) ? (string)$data['fail_reason'] : '';
|
||
|
||
$withdraw = Db::name('fc_withdraw')->where('out_bill_no', $outBillNo)->find();
|
||
if (!$withdraw) {
|
||
Log::record('商家转账回调:未找到提现单 out_bill_no=' . $outBillNo, 'error');
|
||
return 'ignored';
|
||
}
|
||
|
||
// 已到终态:幂等返回
|
||
if ((string)$withdraw['status'] === 'paid') {
|
||
return 'paid';
|
||
}
|
||
|
||
// 金额校验:成功回调的转账金额必须与提现申请一致(防止串单/异常)
|
||
$transferAmount = isset($data['transfer_amount']) ? (int)$data['transfer_amount'] : null;
|
||
$expectedCents = (int)round((float)$withdraw['amount'] * 100);
|
||
if ($state === 'SUCCESS' && $transferAmount !== null && $transferAmount !== $expectedCents) {
|
||
Log::record(
|
||
'商家转账金额不一致:提现单 ' . $outBillNo . ' 申请 ' . $expectedCents . ' 分,回调 ' . $transferAmount . ' 分',
|
||
'error'
|
||
);
|
||
Db::name('fc_withdraw')->where('id', (int)$withdraw['id'])->update([
|
||
'transfer_status' => 'AMOUNT_MISMATCH',
|
||
'fail_reason' => '回调金额与申请不一致,请人工核对',
|
||
'updatetime' => time(),
|
||
]);
|
||
return 'ignored';
|
||
}
|
||
|
||
$now = time();
|
||
|
||
if ($state === 'SUCCESS') {
|
||
Db::transaction(function () use ($withdraw, $outBillNo, $transferBillNo, $now) {
|
||
$locked = Db::name('fc_withdraw')->where('id', (int)$withdraw['id'])->lock(true)->find();
|
||
if (!$locked || (string)$locked['status'] === 'paid') {
|
||
return;
|
||
}
|
||
|
||
$affected = Db::name('fc_withdraw')
|
||
->where('id', (int)$locked['id'])
|
||
->where('status', '<>', 'paid')
|
||
->update([
|
||
'status' => 'paid',
|
||
'paytime' => $now,
|
||
'transfer_status' => 'SUCCESS',
|
||
'transfer_id' => $transferBillNo,
|
||
'fail_reason' => '',
|
||
'transfer_time' => $now,
|
||
'updatetime' => $now,
|
||
]);
|
||
if (!$affected) {
|
||
return;
|
||
}
|
||
|
||
Db::name('fc_finance_flow')->insert([
|
||
'user_id' => (int)$locked['user_id'],
|
||
'flow_no' => 'FWD' . date('YmdHis') . $locked['id'] . bin2hex(random_bytes(2)),
|
||
'type' => 'expense',
|
||
'scene' => '佣金提现',
|
||
'amount' => $locked['amount'],
|
||
'balance' => 0,
|
||
'related_id' => (int)$locked['id'],
|
||
'remark' => '微信商家转账 ' . $transferBillNo,
|
||
'createtime' => $now,
|
||
]);
|
||
|
||
Db::name('fc_message')->insert([
|
||
'user_id' => (int)$locked['user_id'],
|
||
'type' => 'system',
|
||
'title' => '提现已到账',
|
||
'content' => '您的提现申请「' . $locked['withdraw_no'] . '」已转入微信零钱,金额:' . $locked['amount'] . ' 元。',
|
||
'isread' => 0,
|
||
'createtime' => $now,
|
||
]);
|
||
});
|
||
|
||
// 订阅消息:提现到账(未配置模板/未授权时静默跳过)
|
||
\app\common\library\WxSubscribe::send(
|
||
(int)$withdraw['user_id'],
|
||
'transfer',
|
||
[
|
||
'amount1' => ['value' => number_format((float)$withdraw['amount'], 2) . '元'],
|
||
'thing2' => ['value' => '分销佣金提现'],
|
||
'time3' => ['value' => date('Y-m-d H:i')],
|
||
],
|
||
'pages/distribution/index'
|
||
);
|
||
|
||
return 'paid';
|
||
}
|
||
|
||
if (in_array($state, ['FAIL', 'CANCELLED'], true)) {
|
||
Db::transaction(function () use ($withdraw, $transferBillNo, $failReason, $state, $now) {
|
||
$locked = Db::name('fc_withdraw')->where('id', (int)$withdraw['id'])->lock(true)->find();
|
||
if (!$locked || in_array((string)$locked['status'], ['paid', 'rejected'], true)) {
|
||
return;
|
||
}
|
||
|
||
Db::name('fc_withdraw')->where('id', (int)$locked['id'])->update([
|
||
'status' => 'failed',
|
||
'transfer_status' => $state,
|
||
'transfer_id' => $transferBillNo,
|
||
'fail_reason' => mb_substr($failReason, 0, 200),
|
||
'transfer_time' => $now,
|
||
'updatetime' => $now,
|
||
]);
|
||
|
||
Db::name('fc_message')->insert([
|
||
'user_id' => (int)$locked['user_id'],
|
||
'type' => 'system',
|
||
'title' => '提现转账失败',
|
||
'content' => '您的提现申请「' . $locked['withdraw_no'] . '」转账失败' . ($failReason !== '' ? ':' . $failReason : '') . ',可重新申请。',
|
||
'isread' => 0,
|
||
'createtime' => $now,
|
||
]);
|
||
});
|
||
|
||
return 'failed';
|
||
}
|
||
|
||
// WAIT_USER_CONFIRM / ACCEPTED / PROCESSING:保持 paying,等待用户确认或回调
|
||
$updateData = [
|
||
'transfer_status' => substr($state, 0, 32),
|
||
'transfer_time' => $now,
|
||
'updatetime' => $now,
|
||
];
|
||
if ($transferBillNo !== '') {
|
||
$updateData['transfer_id'] = $transferBillNo;
|
||
}
|
||
if ($packageInfo !== '') {
|
||
$updateData['package_info'] = $packageInfo;
|
||
}
|
||
Db::name('fc_withdraw')->where('id', (int)$withdraw['id'])->update($updateData);
|
||
|
||
return 'paying';
|
||
}
|
||
|
||
/* ==================== 配置与工具 ==================== */
|
||
|
||
/** 转账场景 ID(默认 1005 佣金报酬) */
|
||
public static function sceneId()
|
||
{
|
||
$v = trim((string)self::cfg('transfer.sceneId', ''));
|
||
return $v !== '' ? $v : self::DEFAULT_SCENE_ID;
|
||
}
|
||
|
||
/** 场景报备信息(佣金报酬固定两条) */
|
||
public static function sceneInfos()
|
||
{
|
||
$raw = self::cfg('transfer.sceneInfos', '');
|
||
if (is_string($raw) && $raw !== '') {
|
||
$decoded = json_decode($raw, true);
|
||
if (is_array($decoded) && !empty($decoded)) {
|
||
return $decoded;
|
||
}
|
||
}
|
||
|
||
return [
|
||
['info_type' => '岗位类型', 'info_content' => '推广员'],
|
||
['info_type' => '报酬说明', 'info_content' => '分销佣金'],
|
||
];
|
||
}
|
||
|
||
/** 转账结果回调地址(必须 HTTPS 且公网可访问) */
|
||
public static function notifyUrl()
|
||
{
|
||
$configured = trim((string)self::cfg('transfer.notifyUrl', ''));
|
||
if ($configured !== '' && preg_match('#^https://#i', $configured)) {
|
||
return $configured;
|
||
}
|
||
|
||
$root = '';
|
||
if (function_exists('request')) {
|
||
try {
|
||
$root = (string)request()->root(true);
|
||
} catch (\Throwable $e) {
|
||
$root = '';
|
||
}
|
||
}
|
||
if ($root === '' && function_exists('request')) {
|
||
try {
|
||
$root = (string)request()->domain();
|
||
} catch (\Throwable $e) {
|
||
$root = '';
|
||
}
|
||
}
|
||
|
||
$root = rtrim(preg_replace('#/[a-zA-Z0-9_\-]+\.php$#i', '', $root), '/');
|
||
if ($root === '' || !preg_match('#^https://#i', $root)) {
|
||
// 微信要求 notify_url 必须 HTTPS;本地/HTTP 环境不传,改用主动查询同步结果
|
||
return '';
|
||
}
|
||
|
||
return $root . '/api/v1/pay/transfer-notify';
|
||
}
|
||
|
||
/** 生成商户转账单号:TB + 提现单号(≤32,字母数字) */
|
||
public static function makeOutBillNo(array $withdraw)
|
||
{
|
||
$no = preg_replace('/[^A-Za-z0-9]/', '', (string)$withdraw['withdraw_no']);
|
||
|
||
return substr('TB' . $no, 0, 32);
|
||
}
|
||
|
||
/** 读取业务配置 */
|
||
protected static function cfg($key, $default = null)
|
||
{
|
||
$row = Db::name('fc_config')->where('key', $key)->find();
|
||
if (!$row || !isset($row['value']) || $row['value'] === '') {
|
||
return $default;
|
||
}
|
||
|
||
return $row['value'];
|
||
}
|
||
|
||
/**
|
||
* 提取微信返回的错误原因(yansongda 的异常 message 往往只有“状态码异常”)
|
||
*
|
||
* @param \Throwable $e
|
||
* @return string
|
||
*/
|
||
protected static function errorDetail(\Throwable $e)
|
||
{
|
||
$response = null;
|
||
if (isset($e->response)) {
|
||
$response = $e->response;
|
||
}
|
||
if ($response instanceof \Yansongda\Supports\Collection) {
|
||
$response = $response->all();
|
||
}
|
||
|
||
if (is_array($response)) {
|
||
if (!empty($response['message'])) {
|
||
return (string)$response['message'];
|
||
}
|
||
if (!empty($response['code'])) {
|
||
return (string)$response['code'];
|
||
}
|
||
}
|
||
|
||
return $e->getMessage();
|
||
}
|
||
|
||
/** 注册 epay 内置 SDK 命名空间(后台/CLI 场景下钩子可能未触发) */
|
||
protected static function loadSdk()
|
||
{
|
||
if (!class_exists('\Yansongda\Pay\Pay')) {
|
||
(new \addons\epay\Epay())->actionBegin();
|
||
}
|
||
}
|
||
}
|