341 行
13 KiB
PHP
341 行
13 KiB
PHP
<?php
|
||
|
||
namespace app\admin\controller;
|
||
|
||
use app\common\controller\Backend;
|
||
use app\common\library\FinanceConfigService;
|
||
use app\common\library\PaymentService;
|
||
use think\Db;
|
||
use think\exception\PDOException;
|
||
|
||
/**
|
||
* 提现申请
|
||
*
|
||
* 提现单仅由师傅端发起,后台只负责财务审核与打款标记:
|
||
* 待审核(wait) → 审核通过(pass)/驳回(reject);审核通过(pass) → 打款完成(done)/驳回(reject)
|
||
* 驳回自动解冻并退回师傅余额,打款完成自动划出冻结资金
|
||
*
|
||
* @icon fa fa-rmb
|
||
*/
|
||
class Withdraw extends Backend
|
||
{
|
||
|
||
/**
|
||
* Withdraw模型对象
|
||
* @var \app\admin\model\Withdraw
|
||
*/
|
||
protected $model = null;
|
||
protected $relationSearch = true;
|
||
protected $searchFields = 'id,account,audit_msg,fail_code,fail_reason,out_bill_no,transfer_bill_no,master.realname,master.mobile';
|
||
|
||
/**
|
||
* 审核状态机:当前状态 => 允许流转到的目标状态
|
||
* reject/done 为终态,不允许再变更
|
||
* @var array
|
||
*/
|
||
protected $statusFlow = [
|
||
'wait' => ['pass', 'reject'],
|
||
'pass' => ['done', 'reject'],
|
||
];
|
||
|
||
public function _initialize()
|
||
{
|
||
parent::_initialize();
|
||
$this->model = new \app\admin\model\Withdraw;
|
||
$this->view->assign("channelList", $this->model->getChannelList());
|
||
$this->view->assign("statusList", $this->model->getStatusList());
|
||
}
|
||
|
||
/**
|
||
* 提现列表:关联师傅信息,展示联系方式与处理时间
|
||
*/
|
||
public function index()
|
||
{
|
||
$this->request->filter(['strip_tags', 'trim']);
|
||
if (false === $this->request->isAjax()) {
|
||
return $this->view->fetch();
|
||
}
|
||
if ($this->request->request('keyField')) {
|
||
return $this->selectpage();
|
||
}
|
||
[$where, $sort, $order, $offset, $limit] = $this->buildparams();
|
||
$list = $this->model
|
||
->with(['master'])
|
||
->where($where)
|
||
->order($sort, $order)
|
||
->paginate($limit);
|
||
$result = ['total' => $list->total(), 'rows' => $list->items()];
|
||
return json($result);
|
||
}
|
||
|
||
/**
|
||
* 提现审核与资金来源对账详情
|
||
*/
|
||
public function detail($ids = null)
|
||
{
|
||
$row = $this->model->get($ids);
|
||
if (!$row) {
|
||
$this->error(__('No Results were found'));
|
||
}
|
||
$masterId = (int)$row['master_id'];
|
||
$master = $masterId > 0 ? Db::name('master')->where('id', $masterId)->find() : null;
|
||
$recentSettles = $masterId > 0
|
||
? Db::name('settle')->alias('s')
|
||
->join('__ORDER__ o', 'o.id = s.order_id', 'LEFT')
|
||
->where('s.master_id', $masterId)
|
||
->field('s.*, o.order_no, o.service_name')
|
||
->order('s.id desc')
|
||
->limit(8)
|
||
->select()
|
||
: [];
|
||
$walletLogs = $masterId > 0
|
||
? Db::name('wallet_log')
|
||
->where('user_type', 'master')
|
||
->where('user_id', $masterId)
|
||
->order('id desc')
|
||
->limit(10)
|
||
->select()
|
||
: [];
|
||
|
||
$this->view->assign([
|
||
'row' => $row,
|
||
'master' => $master ?: [],
|
||
'recentSettles' => $recentSettles,
|
||
'walletLogs' => $walletLogs,
|
||
]);
|
||
return $this->view->fetch();
|
||
}
|
||
|
||
/**
|
||
* 财务审核与微信商家转账打款处理:
|
||
* - 微信渠道(channel=wechat):
|
||
* 1) 审核通过(pass):调用微信支付 V3 新版「商家转账(场景 1005 佣金报酬)」接口生成转账单,等待师傅在小程序确认收款
|
||
* 2) 同步状态(sync):主动查询微信商家转账单状态,若 SUCCESS 则划出冻结资金标记 done;若 FAIL/CANCELLED 则解冻退回师傅余额
|
||
* 3) 驳回/撤销(reject):若尚未发微信转账则直接退回余额;若已发微信转账则先调微信查单并撤销确认后再退回余额(防双重资损)
|
||
* - 银行卡渠道(channel=bank):保留审核通过 → 线下打款完成(done) / 驳回(reject)
|
||
*/
|
||
public function edit($ids = null)
|
||
{
|
||
$row = $this->model->get($ids);
|
||
if (!$row) {
|
||
$this->error(__('No Results were found'));
|
||
}
|
||
|
||
$wdCfg = FinanceConfigService::getWithdrawConfig();
|
||
$isWechat = ($row['channel'] === 'wechat');
|
||
$useWxApi = $isWechat && ($wdCfg['transfer_mode'] === 'wx_mch_transfer');
|
||
$hasWxBill = !empty($row['out_bill_no']);
|
||
$txState = strtoupper(trim((string)($row['transfer_state'] ?? '')));
|
||
$flowOptions = [];
|
||
if ($row['status'] === 'wait') {
|
||
if ($hasWxBill) {
|
||
if (in_array($txState, ['FAIL', 'NOT_FOUND'], true) && $useWxApi) {
|
||
$flowOptions = ['pass', 'sync', 'reject'];
|
||
} else {
|
||
$flowOptions = ['sync', 'reject'];
|
||
}
|
||
} else {
|
||
$flowOptions = ['pass', 'reject'];
|
||
}
|
||
} elseif ($row['status'] === 'pass') {
|
||
if ($isWechat && $hasWxBill) {
|
||
if (in_array($txState, ['FAIL', 'NOT_FOUND'], true) && $useWxApi) {
|
||
$flowOptions = ['pass', 'sync', 'reject'];
|
||
} else {
|
||
$flowOptions = ['sync', 'reject'];
|
||
}
|
||
} elseif ($useWxApi && !$hasWxBill) {
|
||
$flowOptions = ['pass', 'done', 'reject'];
|
||
} else {
|
||
$flowOptions = ['done', 'reject'];
|
||
}
|
||
}
|
||
|
||
if (false === $this->request->isPost()) {
|
||
$master = Db::name('master')->where('id', (int)$row['master_id'])->field('id,realname,mobile,user_id,money,frozen_money')->find();
|
||
$this->view->assign('row', $row);
|
||
$this->view->assign('master', $master ?: []);
|
||
$this->view->assign('flowOptions', $flowOptions);
|
||
$this->view->assign('useWxApi', $useWxApi);
|
||
$this->view->assign('transferStateLabel', PaymentService::transferStateLabel($row['transfer_state'] ?? ''));
|
||
return $this->view->fetch();
|
||
}
|
||
|
||
$status = (string)$this->request->post('row.status');
|
||
$auditMsg = trim((string)$this->request->post('row.audit_msg'));
|
||
|
||
if (empty($flowOptions)) {
|
||
$this->error('该提现单已处于终态(已驳回/已打款),不允许再次操作');
|
||
}
|
||
if (!in_array($status, $flowOptions, true)) {
|
||
$this->error('状态流转不合法,请刷新后重试');
|
||
}
|
||
if ($status === 'reject' && $auditMsg === '') {
|
||
$this->error('驳回或撤销转账时必须填写原因');
|
||
}
|
||
|
||
// 1. 驳回或撤销转账(统一走防双重资损方法)
|
||
if ($status === 'reject') {
|
||
try {
|
||
$res = PaymentService::cancelOrRejectWithdraw((int)$row['id'], $auditMsg);
|
||
} catch (\Throwable $e) {
|
||
$this->error($e->getMessage());
|
||
}
|
||
$this->success($res['message'] ?? '已驳回处理');
|
||
}
|
||
|
||
// 2. 微信钱包渠道(启用微信商家转账模式时):发起微信商家转账(1005) 或 同步微信转账状态
|
||
if ($useWxApi && $status === 'pass') {
|
||
try {
|
||
PaymentService::initiateWithdrawTransfer((int)$row['id'], $auditMsg);
|
||
} catch (\Throwable $e) {
|
||
$this->error($e->getMessage());
|
||
}
|
||
$this->success('已审核通过并发起微信商家转账,等待师傅在小程序「我的钱包」确认收款');
|
||
}
|
||
|
||
if ($isWechat && $status === 'sync') {
|
||
try {
|
||
$updated = PaymentService::syncWithdrawTransfer((int)$row['id']);
|
||
} catch (\Throwable $e) {
|
||
$this->error('同步微信状态失败:' . $e->getMessage());
|
||
}
|
||
$stateText = PaymentService::transferStateLabel($updated['transfer_state'] ?? '');
|
||
$this->success('已同步最新微信转账状态:' . $stateText);
|
||
}
|
||
|
||
// 3. 线下银行卡打款(或历史未发微信单的线下打款完成)
|
||
$masterId = (int)$row['master_id'];
|
||
$amount = round((float)$row['amount'], 2);
|
||
Db::startTrans();
|
||
try {
|
||
$affected = Db::name('withdraw')
|
||
->where('id', $row['id'])
|
||
->where('status', $row['status'])
|
||
->update([
|
||
'status' => $status,
|
||
'audit_msg' => $auditMsg !== '' ? $auditMsg : $row['audit_msg'],
|
||
'updatetime' => time(),
|
||
]);
|
||
if (!$affected) {
|
||
throw new \Exception('提现单状态已被其他人变更,请刷新后重试');
|
||
}
|
||
|
||
$master = Db::name('master')->where('id', $masterId)->lock(true)->find();
|
||
if (!$master) {
|
||
throw new \Exception('提现师傅不存在');
|
||
}
|
||
|
||
if ($status === 'pass') {
|
||
$this->sendMasterMessage($masterId, (int)$master['user_id'], '提现审核通过', '您的提现申请 ¥' . $amount . ' 已通过审核,财务将尽快安排打款到银行卡');
|
||
} elseif ($status === 'done') {
|
||
Db::name('master')->where('id', $masterId)->update([
|
||
'frozen_money' => max(0, round((float)$master['frozen_money'] - $amount, 2)),
|
||
'updatetime' => time(),
|
||
]);
|
||
Db::name('wallet_log')->insert([
|
||
'user_type' => 'master',
|
||
'user_id' => $masterId,
|
||
'type' => 'withdraw',
|
||
'amount' => 0,
|
||
'balance' => (float)$master['money'],
|
||
'order_id' => 0,
|
||
'remark' => '提现打款完成,冻结资金 ¥' . $amount . ' 划出(' . ($row['channel'] === 'bank' ? '银行储蓄卡' : '微信钱包') . ' ' . $row['account'] . ')',
|
||
'createtime' => time(),
|
||
]);
|
||
$this->sendMasterMessage($masterId, (int)$master['user_id'], '提现打款完成', '您的提现 ¥' . $amount . ' 已打款成功,请留意到账通知');
|
||
}
|
||
Db::commit();
|
||
} catch (PDOException|\Exception $e) {
|
||
Db::rollback();
|
||
$this->error($e->getMessage());
|
||
}
|
||
$this->success();
|
||
}
|
||
|
||
/**
|
||
* 后台列表一键同步微信商家转账单最新状态
|
||
*/
|
||
public function sync($ids = null)
|
||
{
|
||
$row = $this->model->get($ids);
|
||
if (!$row) {
|
||
$this->error(__('No Results were found'));
|
||
}
|
||
if (empty($row['out_bill_no'])) {
|
||
$this->error('该提现单尚未发起微信商家转账');
|
||
}
|
||
try {
|
||
$updated = PaymentService::syncWithdrawTransfer((int)$row['id']);
|
||
} catch (\Throwable $e) {
|
||
$this->error('同步失败:' . $e->getMessage());
|
||
}
|
||
$stateText = PaymentService::transferStateLabel($updated['transfer_state'] ?? '');
|
||
$this->success('同步成功,当前微信状态:' . $stateText);
|
||
}
|
||
|
||
/**
|
||
* 提现单仅由师傅端发起,禁止后台手工添加
|
||
*/
|
||
public function add()
|
||
{
|
||
$this->error('提现申请仅支持师傅端发起,后台不可手动添加');
|
||
}
|
||
|
||
/**
|
||
* 提现流水记录禁止删除,保障资金对账完整
|
||
*/
|
||
public function del($ids = null)
|
||
{
|
||
$this->error('提现申请记录禁止删除');
|
||
}
|
||
|
||
/**
|
||
* 禁止批量修改状态,状态变更必须走审核流程以触发资金联动
|
||
*/
|
||
public function multi($ids = null)
|
||
{
|
||
$this->error('提现审核不支持批量操作,请逐单审核处理');
|
||
}
|
||
|
||
/**
|
||
* 向师傅端工作台及所属用户发送站内消息
|
||
* @param int $masterId 师傅ID
|
||
* @param int $userId 用户ID
|
||
* @param string $title 消息标题
|
||
* @param string $content 消息内容
|
||
*/
|
||
protected function sendMasterMessage($masterId, $userId, $title, $content)
|
||
{
|
||
$now = time();
|
||
$rows = [];
|
||
if ($masterId > 0) {
|
||
$rows[] = [
|
||
'receive_type' => 'master',
|
||
'receive_id' => (int)$masterId,
|
||
'type' => 'audit',
|
||
'title' => mb_substr((string)$title, 0, 100),
|
||
'content' => mb_substr((string)$content, 0, 500),
|
||
'order_id' => 0,
|
||
'is_read' => 0,
|
||
'createtime' => $now,
|
||
];
|
||
}
|
||
if ($userId > 0) {
|
||
$rows[] = [
|
||
'receive_type' => 'user',
|
||
'receive_id' => (int)$userId,
|
||
'type' => 'system',
|
||
'title' => mb_substr((string)$title, 0, 100),
|
||
'content' => mb_substr((string)$content, 0, 500),
|
||
'order_id' => 0,
|
||
'is_read' => 0,
|
||
'createtime' => $now,
|
||
];
|
||
}
|
||
if ($rows) {
|
||
Db::name('message')->insertAll($rows);
|
||
}
|
||
}
|
||
}
|