允许流转到的目标状态 * 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); } } }