'desc']; protected string|array $orderGuarantee = ['id' => 'desc']; protected array $withJoinTable = ['user', 'channel', 'reviewAdmin']; protected function initController(WebmanRequest $request): ?Response { $this->model = new \app\common\model\WithdrawOrder(); return null; } protected function _index(): Response { if ($this->request && $this->request->get('select')) { return $this->select($this->request); } list($where, $alias, $limit, $order) = $this->queryBuilder(); $table = strtolower($this->model->getTable()); $mainShort = $alias[$table] ?? ''; if ($mainShort !== '' && $this->auth && !$this->auth->isSuperAdmin()) { $where[] = ['user.admin_id', '=', intval(strval($this->auth->id))]; } $res = $this->model ->withJoin($this->withJoinTable, $this->withJoinType) ->with($this->withJoinTable) ->visible([ 'user' => ['username', 'phone'], 'channel' => ['name'], 'reviewAdmin' => ['username'], ]) ->alias($alias) ->where($where) ->order($order) ->paginate($limit); return $this->success('', [ 'list' => $res->items(), 'total' => $res->total(), 'remark' => get_route_remark(), ]); } /** * GET 时返回关联信息,便于编辑弹窗直接渲染 user.username/channel.name */ protected function _edit(): Response { $pk = $this->model->getPk(); $id = $this->request ? ($this->request->post($pk) ?? $this->request->get($pk)) : null; if ($id === null || $id === '') { return $this->error(__('Parameter error')); } if ($this->request && $this->request->method() === 'POST') { // 历史 CRUD 的 POST 编辑已被 approve/reject 替代,这里阻止直接改金额绕过审核流程 return $this->error('请使用通过/拒绝按钮完成审核'); } $row = $this->loadWithRelations(intval(strval($id))); if (!$row) { return $this->error(__('Record not found')); } if (!$this->checkChannelScoped($row)) { return $this->error(__('You have no permission')); } return $this->success('', ['row' => $row]); } /** * 审核通过:允许调整 amount/fee;actual_amount 自动为 amount - fee。 * 对金额差额自动在用户钱包与流水中做增减,保持账务平衡。 */ public function approve(WebmanRequest $request): Response { $response = $this->initializeBackend($request); if ($response !== null) { return $response; } if ($request->method() !== 'POST') { return $this->error(__('Parameter error')); } $id = $this->intParam($request->post('id')); if ($id <= 0) { return $this->error(__('Parameter error')); } $newAmount = $this->decimalParam($request->post('amount'), '0'); $newFee = $this->decimalParam($request->post('fee'), '0'); if (bccomp($newAmount, '0', 2) <= 0) { return $this->error('申请金额必须大于 0'); } if (bccomp($newFee, '0', 2) < 0) { return $this->error('手续费不能为负'); } if (bccomp($newFee, $newAmount, 2) > 0) { return $this->error('手续费不能大于申请金额'); } $newActual = bcsub($newAmount, $newFee, 2); $remarkRaw = $request->post('remark'); $remark = is_string($remarkRaw) ? trim($remarkRaw) : ''; $order = Db::name('withdraw_order')->where('id', $id)->find(); if (!$order) { return $this->error(__('Record not found')); } if (!$this->checkChannelScoped($order)) { return $this->error(__('You have no permission')); } $currentStatus = $this->intParam($order['status'] ?? 0); if ($currentStatus !== 0) { return $this->error('该订单已审核,无需重复操作'); } $userId = $this->intParam($order['user_id'] ?? 0); if ($userId <= 0) { return $this->error('订单缺少用户信息'); } $oldAmount = bcadd(strval($order['amount'] ?? '0'), '0', 2); $diff = bcsub($newAmount, $oldAmount, 2); $now = time(); $adminId = $this->intParam($this->auth->id ?? 0); $adminName = $this->adminDisplayName(); $channelIdRaw = $order['channel_id'] ?? null; $channelId = ($channelIdRaw === null || $channelIdRaw === '') ? null : $this->intParam($channelIdRaw); if ($remark === '') { $remark = '管理员(' . $adminName . ')审核通过:金额 ' . $this->shortAmount($newAmount) . ',手续费 ' . $this->shortAmount($newFee) . ',实际到账 ' . $this->shortAmount($newActual); } Db::startTrans(); try { // 金额调整差额处理 $cmp = bccomp($diff, '0', 2); if ($cmp > 0) { // 新金额更大:再冻结用户 diff $userRow = Db::name('user')->where('id', $userId)->find(); if (!$userRow) { Db::rollback(); return $this->error('关联用户不存在'); } $beforeCoin = bcadd(strval($userRow['coin'] ?? '0'), '0', 2); if (bccomp($beforeCoin, $diff, 2) < 0) { Db::rollback(); return $this->error('用户余额不足以补扣调整差额'); } $afterCoin = bcsub($beforeCoin, $diff, 2); Db::name('user')->where('id', $userId)->update([ 'coin' => $afterCoin, 'total_withdraw_coin' => Db::raw('total_withdraw_coin + ' . $diff), 'update_time' => $now, ]); Db::name('user_wallet_record')->insert([ 'user_id' => $userId, 'channel_id' => $channelId, 'biz_type' => 'withdraw', 'direction' => 2, 'amount' => $diff, 'balance_before' => $beforeCoin, 'balance_after' => $afterCoin, 'ref_type' => 'withdraw_order', 'ref_id' => $id, 'idempotency_key' => 'wd_adjust_add_' . strval($order['order_no'] ?? $id) . '_' . $now, 'operator_admin_id' => $adminId > 0 ? $adminId : null, 'remark' => '管理员(' . $adminName . ')审核调增申请金额差额 ' . $this->shortAmount($diff), 'create_time' => $now, ]); } elseif ($cmp < 0) { // 新金额更小:退回差额 $abs = bcsub('0', $diff, 2); $userRow = Db::name('user')->where('id', $userId)->find(); if (!$userRow) { Db::rollback(); return $this->error('关联用户不存在'); } $beforeCoin = bcadd(strval($userRow['coin'] ?? '0'), '0', 2); $afterCoin = bcadd($beforeCoin, $abs, 2); Db::name('user')->where('id', $userId)->update([ 'coin' => $afterCoin, 'total_withdraw_coin' => Db::raw('total_withdraw_coin - ' . $abs), 'update_time' => $now, ]); Db::name('user_wallet_record')->insert([ 'user_id' => $userId, 'channel_id' => $channelId, 'biz_type' => 'withdraw_refund', 'direction' => 1, 'amount' => $abs, 'balance_before' => $beforeCoin, 'balance_after' => $afterCoin, 'ref_type' => 'withdraw_order', 'ref_id' => $id, 'idempotency_key' => 'wd_adjust_sub_' . strval($order['order_no'] ?? $id) . '_' . $now, 'operator_admin_id' => $adminId > 0 ? $adminId : null, 'remark' => '管理员(' . $adminName . ')审核调减申请金额差额 ' . $this->shortAmount($abs), 'create_time' => $now, ]); } Db::name('withdraw_order')->where('id', $id)->update([ 'amount' => $newAmount, 'fee' => $newFee, 'actual_amount' => $newActual, 'status' => 1, 'review_admin_id' => $adminId > 0 ? $adminId : null, 'review_time' => $now, 'remark' => substr($remark, 0, 255), 'update_time' => $now, ]); Db::commit(); } catch (Throwable $e) { Db::rollback(); return $this->error($e->getMessage()); } return $this->success('审核通过', [ 'id' => $id, 'amount' => $newAmount, 'fee' => $newFee, 'actual_amount' => $newActual, 'status' => 1, ]); } /** * 审核拒绝:必须填写驳回原因(remark)。 * 回冲申请时的冻结:user.coin += amount;total_withdraw_coin -= amount;写一条 withdraw_refund 流水。 */ public function reject(WebmanRequest $request): Response { $response = $this->initializeBackend($request); if ($response !== null) { return $response; } if ($request->method() !== 'POST') { return $this->error(__('Parameter error')); } $id = $this->intParam($request->post('id')); if ($id <= 0) { return $this->error(__('Parameter error')); } $remarkRaw = $request->post('remark'); $remark = is_string($remarkRaw) ? trim($remarkRaw) : ''; if ($remark === '') { return $this->error('请填写拒绝原因'); } $order = Db::name('withdraw_order')->where('id', $id)->find(); if (!$order) { return $this->error(__('Record not found')); } if (!$this->checkChannelScoped($order)) { return $this->error(__('You have no permission')); } $currentStatus = $this->intParam($order['status'] ?? 0); if ($currentStatus !== 0) { return $this->error('该订单已审核,无需重复操作'); } $userId = $this->intParam($order['user_id'] ?? 0); if ($userId <= 0) { return $this->error('订单缺少用户信息'); } $amount = bcadd(strval($order['amount'] ?? '0'), '0', 2); $channelIdRaw = $order['channel_id'] ?? null; $channelId = ($channelIdRaw === null || $channelIdRaw === '') ? null : $this->intParam($channelIdRaw); $now = time(); $adminId = $this->intParam($this->auth->id ?? 0); $adminName = $this->adminDisplayName(); Db::startTrans(); try { $userRow = Db::name('user')->where('id', $userId)->find(); if (!$userRow) { Db::rollback(); return $this->error('关联用户不存在'); } $beforeCoin = bcadd(strval($userRow['coin'] ?? '0'), '0', 2); $afterCoin = bcadd($beforeCoin, $amount, 2); Db::name('user')->where('id', $userId)->update([ 'coin' => $afterCoin, 'total_withdraw_coin' => Db::raw('total_withdraw_coin - ' . $amount), 'update_time' => $now, ]); Db::name('user_wallet_record')->insert([ 'user_id' => $userId, 'channel_id' => $channelId, 'biz_type' => 'withdraw_refund', 'direction' => 1, 'amount' => $amount, 'balance_before' => $beforeCoin, 'balance_after' => $afterCoin, 'ref_type' => 'withdraw_order', 'ref_id' => $id, 'idempotency_key' => 'wd_reject_' . strval($order['order_no'] ?? $id) . '_' . $now, 'operator_admin_id' => $adminId > 0 ? $adminId : null, 'remark' => '管理员(' . $adminName . ')驳回提现,退回冻结金额 ' . $this->shortAmount($amount) . ':' . $remark, 'create_time' => $now, ]); Db::name('withdraw_order')->where('id', $id)->update([ 'status' => 2, 'review_admin_id' => $adminId > 0 ? $adminId : null, 'review_time' => $now, 'remark' => substr($remark, 0, 255), 'update_time' => $now, ]); Db::commit(); } catch (Throwable $e) { Db::rollback(); return $this->error($e->getMessage()); } return $this->success('审核已拒绝', [ 'id' => $id, 'status' => 2, 'remark' => $remark, ]); } private function loadWithRelations(int $id): ?array { $row = $this->model ->withJoin($this->withJoinTable, $this->withJoinType) ->with($this->withJoinTable) ->visible([ 'user' => ['username', 'phone'], 'channel' => ['name'], 'reviewAdmin' => ['username'], ]) ->where($this->model->getTable() . '.id', $id) ->find(); if (!$row) { return null; } return $row->toArray(); } private function checkChannelScoped(array|object $row): bool { if (!$this->auth || $this->auth->isSuperAdmin()) { return true; } $uidRaw = is_array($row) ? ($row['user_id'] ?? null) : ($row->user_id ?? null); $uid = $this->intParam($uidRaw); if ($uid <= 0) { return false; } $user = Db::name('user')->field(['id', 'admin_id'])->where('id', $uid)->find(); if (!is_array($user)) { return false; } $ownerAdminId = $this->intParam($user['admin_id'] ?? 0); return $ownerAdminId > 0 && $ownerAdminId === $this->intParam($this->auth->id ?? 0); } private function intParam($raw): int { if ($raw === null || $raw === '') { return 0; } if (!is_numeric(strval($raw))) { return 0; } return intval(strval($raw)); } private function decimalParam($raw, string $default): string { if ($raw === null || $raw === '' || !is_numeric(strval($raw))) { return bcadd($default, '0', 2); } return bcadd(strval($raw), '0', 2); } private function adminDisplayName(): string { if (!$this->auth) { return 'admin'; } $name = $this->auth->username ?? null; if (is_string($name) && $name !== '') { return $name; } $id = $this->intParam($this->auth->id ?? 0); return '#' . strval($id); } /** * 把 2 位小数金额压缩成最多 2 位小数用于展示(不影响落库精度) */ private function shortAmount(string $amount): string { if (!is_numeric($amount)) { return $amount; } $normalized = bcadd($amount, '0', 2); $negative = false; if (str_starts_with($normalized, '-')) { $negative = true; $normalized = substr($normalized, 1); } $parts = explode('.', $normalized, 2); $intPart = $parts[0] ?? '0'; $fracPart = $parts[1] ?? '0000'; $displayFrac = substr($fracPart, 0, 2); $v = $intPart . '.' . str_pad($displayFrac, 2, '0'); return $negative ? ('-' . $v) : $v; } }