Files
webman-buildadmin/app/admin/controller/order/WithdrawOrder.php
zhenhui e65c3474bd 1.修改电话号码格式为60前缀,马来西亚格式
2.优化渠道可以查看分红方式,可以查看游玩详情
2026-05-30 11:09:54 +08:00

753 lines
36 KiB
PHP
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 app\admin\controller\order;
use app\common\controller\Backend;
use app\common\library\finance\DDPayGateway;
use app\common\library\finance\MockPay;
use support\think\Db;
use support\Response;
use Throwable;
use Webman\Http\Request as WebmanRequest;
/**
* 提现订单
*
* 当前审核流转:
* - 用户端提交提现时立即冻结余额user.coin - apply_amount并生成 withdraw_orderstatus=0与 withdraw 流水direction=2
* - 管理员在后台审核通过approve→ status=1拒绝reject→ status=2 并回冲用户余额与流水。
* - 通过流程不再额外扣钱包,因为申请时已冻结;仅在管理员调整 amount/fee 时写一条差额流水。
*/
class WithdrawOrder extends Backend
{
protected ?object $model = null;
protected bool $modelValidate = true;
protected bool $modelSceneValidate = true;
protected string|array $quickSearchField = ['id', 'order_no', 'idempotency_key', 'pay_channel', 'receive_type', 'receive_account', 'ddpay_receiver_name', 'receiver_email', 'receiver_mobile', 'remark'];
protected string|array $defaultSortField = ['id' => '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->shouldApplyUserAdminScope()) {
$where[] = ['user.admin_id', 'in', $this->scopedAdminIds()];
}
$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(__('Please use approve/reject buttons to complete the review'));
}
$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/feeactual_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(__('Apply amount must be greater than 0'));
}
if (bccomp($newFee, '0', 2) < 0) {
return $this->error(__('Fee cannot be negative'));
}
if (bccomp($newFee, $newAmount, 2) > 0) {
return $this->error(__('Fee cannot be greater than apply amount'));
}
$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(__('This order has already been reviewed'));
}
$userId = $this->intParam($order['user_id'] ?? 0);
if ($userId <= 0) {
return $this->error(__('Order is missing user info'));
}
$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(__('Related user does not exist'));
}
$beforeCoin = bcadd(strval($userRow['coin'] ?? '0'), '0', 2);
if (bccomp($beforeCoin, $diff, 2) < 0) {
Db::rollback();
return $this->error(__('User balance is insufficient to cover the adjustment difference'));
}
$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(__('Related user does not exist'));
}
$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());
}
// 审核通过后自动发起 DDPAY 出金(并在失败时回冲余额)
try {
$fresh = Db::name('withdraw_order')->where('id', $id)->find();
if (!is_array($fresh)) {
// 理论上不会发生:只写失败备注
Db::name('withdraw_order')->where('id', $id)->update([
'remark' => '[ddpay] payout order missing',
'update_time' => time(),
]);
} else {
$orderNo = is_string($fresh['order_no'] ?? null) ? trim($fresh['order_no'] ?? '') : strval($fresh['order_no'] ?? '');
$receiveType = is_string($fresh['receive_type'] ?? null) ? strtolower(trim($fresh['receive_type'] ?? '')) : '';
$payChannel = is_string($fresh['pay_channel'] ?? null) ? strtolower(trim($fresh['pay_channel'] ?? '')) : '';
// 模拟出金:审核通过即标记已打款,不回冲、不调用 DDPay
if ($orderNo !== '' && MockPay::shouldSimulateWithdrawPayout($payChannel)) {
$prevRemark = is_string($fresh['remark'] ?? null) ? trim($fresh['remark']) : '';
$mockNote = '[mock] 管理员(' . $adminName . ')审核通过,模拟打款成功';
$finalRemark = $prevRemark === '' ? $mockNote : mb_substr($prevRemark . ' | ' . $mockNote, 0, 255);
Db::name('withdraw_order')
->where('id', $id)
->where('status', 1)
->update([
'status' => 3,
'remark' => $finalRemark,
'update_time' => time(),
]);
} elseif ($orderNo !== '' && $receiveType === 'bank' && $payChannel === 'ddpay') {
$base = \app\common\library\finance\DDPayGateway::publicBaseUrlForCallbacks($request);
if ($base === '') {
$base = 'https://' . strval($request->host());
}
$callbackUrl = rtrim($base, '/') . '/api/finance/ddpayPayoutNotify';
$payoutAmount = is_string($fresh['actual_amount'] ?? null) && $fresh['actual_amount'] !== '' ? trim($fresh['actual_amount']) : $newActual;
$receiverName = is_string($fresh['ddpay_receiver_name'] ?? null) ? trim($fresh['ddpay_receiver_name'] ?? '') : '';
$receiverAccount = is_string($fresh['receive_account'] ?? null) ? trim($fresh['receive_account'] ?? '') : '';
$bankName = is_string($fresh['ddpay_bank_name'] ?? null) ? trim($fresh['ddpay_bank_name'] ?? '') : '';
$bankBranch = is_string($fresh['ddpay_bank_branch'] ?? null) ? trim($fresh['ddpay_bank_branch'] ?? '') : 'N/A';
if ($receiverName === '' || $receiverAccount === '' || $bankName === '') {
// 缺少 DDPAY 出金字段:回冲并置为失败
Db::startTrans();
try {
$amountRefund = bcadd(strval($fresh['amount'] ?? '0'), '0', 2);
$updated = Db::name('withdraw_order')
->where('id', $id)
->where('status', 1)
->update([
'status' => 2,
'remark' => '[ddpay] missing payout fields',
'update_time' => time(),
]);
if ($updated > 0 && bccomp($amountRefund, '0', 2) > 0) {
$userIdRefund = intval(strval($fresh['user_id'] ?? 0));
$userRow = Db::name('user')->where('id', $userIdRefund)->find();
$beforeCoin = bcadd(strval($userRow['coin'] ?? '0'), '0', 2);
$afterCoin = bcadd($beforeCoin, $amountRefund, 2);
Db::name('user')->where('id', $userIdRefund)->update([
'coin' => $afterCoin,
'total_withdraw_coin' => Db::raw('total_withdraw_coin - ' . $amountRefund),
'update_time' => time(),
]);
$idempotencyKey = 'wd_ddpay_failed_' . strval($orderNo);
$exists = Db::name('user_wallet_record')->where('idempotency_key', $idempotencyKey)->find();
if (!$exists) {
$channelId = null;
if (isset($fresh['channel_id']) && is_numeric(strval($fresh['channel_id']))) {
$channelId = intval(strval($fresh['channel_id']));
}
Db::name('user_wallet_record')->insert([
'user_id' => $userIdRefund,
'channel_id' => $channelId,
'biz_type' => 'withdraw_refund',
'direction' => 1,
'amount' => $amountRefund,
'balance_before' => $beforeCoin,
'balance_after' => $afterCoin,
'ref_type' => 'withdraw_order',
'ref_id' => intval(strval($id)),
'idempotency_key' => $idempotencyKey,
'operator_admin_id' => null,
'remark' => '[ddpay] missing payout fields refund',
'create_time' => time(),
]);
}
}
Db::commit();
} catch (Throwable $e2) {
Db::rollback();
}
} else {
$clientId = config('app.ddpay_client_id', '');
$identifier = config('app.ddpay_identifier', '');
$ddReq = [
'client_id' => $clientId,
'identifier' => $identifier,
'bill_number' => $orderNo,
'amount' => $payoutAmount,
'receiver_name' => $receiverName,
'receiver_account' => $receiverAccount,
'bank[name]' => $bankName,
'bank_branch' => $bankBranch,
'callback_url' => $callbackUrl,
];
$ddResp = [];
$ts = '';
try {
$ddResp = DDPayGateway::payoutInitiation($ddReq);
$ts = is_string($ddResp['transaction_status'] ?? null) ? strtolower(trim($ddResp['transaction_status'] ?? '')) : '';
} catch (Throwable $e) {
// initiation 异常同“failed”处理回冲并置失败
$ts = 'failed';
$ddResp = ['error' => (string) $e->getMessage()];
}
if (is_array($ddResp)) {
Db::name('withdraw_order')
->where('id', $id)
->update([
'ddpay_payout_snapshot' => json_encode([
'init_request' => $ddReq,
'init_response' => $ddResp,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
]);
}
if ($ts === 'completed') {
Db::name('withdraw_order')
->where('id', $id)
->where('status', 1)
->update([
'status' => 3,
'remark' => '[ddpay] payout completed',
'update_time' => time(),
]);
} elseif ($ts === 'failed') {
$amountRefund = bcadd(strval($fresh['amount'] ?? '0'), '0', 2);
Db::startTrans();
try {
$updated = Db::name('withdraw_order')
->where('id', $id)
->where('status', 1)
->update([
'status' => 2,
'remark' => '[ddpay] payout failed',
'update_time' => time(),
]);
if ($updated > 0 && bccomp($amountRefund, '0', 2) > 0) {
$userIdRefund = intval(strval($fresh['user_id'] ?? 0));
$userRow = Db::name('user')->where('id', $userIdRefund)->find();
$beforeCoin = bcadd(strval($userRow['coin'] ?? '0'), '0', 2);
$afterCoin = bcadd($beforeCoin, $amountRefund, 2);
Db::name('user')->where('id', $userIdRefund)->update([
'coin' => $afterCoin,
'total_withdraw_coin' => Db::raw('total_withdraw_coin - ' . $amountRefund),
'update_time' => time(),
]);
$idempotencyKey = 'wd_ddpay_failed_' . strval($orderNo);
$exists = Db::name('user_wallet_record')->where('idempotency_key', $idempotencyKey)->find();
if (!$exists) {
$channelId = null;
if (isset($fresh['channel_id']) && is_numeric(strval($fresh['channel_id']))) {
$channelId = intval(strval($fresh['channel_id']));
}
Db::name('user_wallet_record')->insert([
'user_id' => $userIdRefund,
'channel_id' => $channelId,
'biz_type' => 'withdraw_refund',
'direction' => 1,
'amount' => $amountRefund,
'balance_before' => $beforeCoin,
'balance_after' => $afterCoin,
'ref_type' => 'withdraw_order',
'ref_id' => intval(strval($id)),
'idempotency_key' => $idempotencyKey,
'operator_admin_id' => null,
'remark' => '[ddpay] payout failed refund',
'create_time' => time(),
]);
}
}
Db::commit();
} catch (Throwable $e3) {
Db::rollback();
}
} else {
// pending按文档做一次 status inquiry 兜底Webhook 仍会最终落账)
try {
$inq = DDPayGateway::payoutStatusInquiry([
'client_id' => $clientId,
'bill_number' => $orderNo,
]);
$ts2 = is_string($inq['transaction_status'] ?? null) ? strtolower(trim($inq['transaction_status'] ?? '')) : '';
if ($ts2 === 'completed') {
Db::name('withdraw_order')
->where('id', $id)
->where('status', 1)
->update([
'status' => 3,
'remark' => '[ddpay] payout completed (after status inquiry)',
'update_time' => time(),
]);
} elseif ($ts2 === 'failed') {
$amountRefund = bcadd(strval($fresh['amount'] ?? '0'), '0', 2);
Db::startTrans();
try {
$updated = Db::name('withdraw_order')
->where('id', $id)
->where('status', 1)
->update([
'status' => 2,
'remark' => '[ddpay] payout failed (after status inquiry)',
'update_time' => time(),
]);
if ($updated > 0 && bccomp($amountRefund, '0', 2) > 0) {
$userIdRefund = intval(strval($fresh['user_id'] ?? 0));
$userRow = Db::name('user')->where('id', $userIdRefund)->find();
$beforeCoin = bcadd(strval($userRow['coin'] ?? '0'), '0', 2);
$afterCoin = bcadd($beforeCoin, $amountRefund, 2);
Db::name('user')->where('id', $userIdRefund)->update([
'coin' => $afterCoin,
'total_withdraw_coin' => Db::raw('total_withdraw_coin - ' . $amountRefund),
'update_time' => time(),
]);
$idempotencyKey = 'wd_ddpay_failed_' . strval($orderNo);
$exists = Db::name('user_wallet_record')->where('idempotency_key', $idempotencyKey)->find();
if (!$exists) {
$channelId = null;
if (isset($fresh['channel_id']) && is_numeric(strval($fresh['channel_id']))) {
$channelId = intval(strval($fresh['channel_id']));
}
Db::name('user_wallet_record')->insert([
'user_id' => $userIdRefund,
'channel_id' => $channelId,
'biz_type' => 'withdraw_refund',
'direction' => 1,
'amount' => $amountRefund,
'balance_before' => $beforeCoin,
'balance_after' => $afterCoin,
'ref_type' => 'withdraw_order',
'ref_id' => intval(strval($id)),
'idempotency_key' => $idempotencyKey,
'operator_admin_id' => null,
'remark' => '[ddpay] payout failed refund (after status inquiry)',
'create_time' => time(),
]);
}
}
Db::commit();
} catch (Throwable $e4) {
Db::rollback();
}
}
} catch (Throwable $e5) {
// ignoreWebhook 会兜底
}
}
}
}
}
} catch (Throwable $e) {
// 外部出金调用失败不阻断审核流:只记录 remark避免阻塞用户提现
Db::name('withdraw_order')->where('id', $id)->update([
'remark' => '[ddpay] payout flow exception: ' . substr((string) $e->getMessage(), 0, 200),
'update_time' => time(),
]);
}
$finalStatus = Db::name('withdraw_order')->where('id', $id)->value('status');
$finalStatusInt = is_numeric($finalStatus) ? intval($finalStatus) : 1;
return $this->success(__('Approved'), [
'id' => $id,
'amount' => $newAmount,
'fee' => $newFee,
'actual_amount' => $newActual,
'status' => $finalStatusInt,
]);
}
/**
* 审核拒绝必须填写驳回原因remark
* 回冲申请时的冻结user.coin += amounttotal_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(__('Please provide reject reason'));
}
$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(__('This order has already been reviewed'));
}
$userId = $this->intParam($order['user_id'] ?? 0);
if ($userId <= 0) {
return $this->error(__('Order is missing user info'));
}
$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(__('Related user does not exist'));
}
$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(__('Rejected'), [
'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() || $this->hasGlobalReadScope()) {
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 && in_array($ownerAdminId, $this->scopedAdminIds(), true);
}
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);
}
/**
* 当前管理员可见的管理员ID集合本人 + 下级角色组内管理员)
*
* @return int[]
*/
private function scopedAdminIds(): array
{
if (!$this->auth) {
return [0];
}
if ($this->auth->isSuperAdmin()) {
return [];
}
$groupIds = $this->auth->getAdminChildGroups();
$adminIds = $groupIds ? $this->auth->getGroupAdmins($groupIds) : [];
$adminIds[] = $this->auth->id;
$adminIds = array_map(fn($id) => $this->intParam($id), $adminIds);
$adminIds = array_values(array_unique(array_filter($adminIds, fn($id) => $id > 0)));
return $adminIds === [] ? [0] : $adminIds;
}
/**
* 把 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;
}
}