ShareClaimAuditService.php 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. <?php
  2. namespace App\Services\Sharing;
  3. use App\Models\User;
  4. use App\Models\Wallet;
  5. use App\Services\BalanceLogService;
  6. use App\Services\OperationAuditService;
  7. use Carbon\Carbon;
  8. use Illuminate\Support\Facades\DB;
  9. use Illuminate\Support\Facades\Validator;
  10. use Illuminate\Validation\Rule;
  11. use Illuminate\Validation\ValidationException;
  12. class ShareClaimAuditService
  13. {
  14. private const AUDIT_TYPE = 'agent_commission';
  15. public function options(): array
  16. {
  17. return ['audit_types' => [['value' => self::AUDIT_TYPE, 'label' => '代理佣金']]];
  18. }
  19. public function list(array $filters): array
  20. {
  21. $params = Validator::make($filters, [
  22. 'page' => ['nullable', 'integer', 'min:1'], 'limit' => ['nullable', 'integer', 'min:1', 'max:200'],
  23. 'status' => ['nullable', Rule::in(['pending', 'approved', 'rejected'])],
  24. // This endpoint currently reads only agent-commission claims.
  25. 'audit_type' => ['nullable', 'string', Rule::in([self::AUDIT_TYPE])],
  26. 'account' => ['nullable', 'string', 'max:100'], 'user_id' => ['nullable', 'integer', 'min:1'],
  27. 'start_date' => ['nullable', 'date_format:Y-m-d'],
  28. 'end_date' => ['nullable', 'date_format:Y-m-d', 'after_or_equal:start_date'],
  29. ])->validate();
  30. $query = DB::table('share_claims as c')->leftJoin('users as u', 'u.id', '=', 'c.user_id');
  31. if (!empty($params['status'])) $query->where('c.status', $params['status']);
  32. if (!empty($params['account'])) $query->where('u.account', 'like', '%' . $params['account'] . '%');
  33. if (!empty($params['user_id'])) $query->where('c.user_id', $params['user_id']);
  34. if (!empty($params['start_date'])) $query->where('c.created_at', '>=', $params['start_date'] . ' 00:00:00');
  35. if (!empty($params['end_date'])) $query->where('c.created_at', '<=', $params['end_date'] . ' 23:59:59');
  36. $total = $query->count();
  37. $rows = $query->select(['c.*', 'u.account', 'u.member_id', 'u.first_name as nickname'])->orderByDesc('c.id')
  38. ->forPage((int)($params['page'] ?? 1), (int)($params['limit'] ?? 20))->get();
  39. return ['total' => $total, 'page' => (int)($params['page'] ?? 1), 'limit' => (int)($params['limit'] ?? 20),
  40. 'data' => $rows->map(fn ($row) => $this->claim($row))->all()];
  41. }
  42. public function detail(int $id): array
  43. {
  44. $row = DB::table('share_claims as c')->leftJoin('users as u', 'u.id', '=', 'c.user_id')
  45. ->where('c.id', $id)->select(['c.*', 'u.account', 'u.member_id', 'u.first_name as nickname'])->first();
  46. if (!$row) $this->invalid('id', '领取申请不存在');
  47. $account = DB::table('share_accounts')->where('user_id', $row->user_id)->first();
  48. $earned = (string)($account->earned_amount ?? '0');
  49. $paid = (string)($account->paid_amount ?? '0');
  50. $reserved = (string)($account->reserved_amount ?? '0');
  51. $balance = bcsub($earned, $paid, 10);
  52. $spendable = bcsub($balance, $reserved, 10);
  53. return $this->claim($row) + ['commission_account' => [
  54. 'earned_amount' => $this->money($earned), 'paid_amount' => $this->money($paid), 'reserved_amount' => $this->money($reserved),
  55. 'pending_amount' => $this->money($this->positive($balance)), 'spendable_amount' => $this->money($this->positive($spendable)),
  56. 'debt_amount' => $this->money($this->positive(bcsub($paid, $earned, 10))),
  57. ], 'can_approve' => $row->status === 'pending' && bccomp($balance, (string)$row->amount, 10) >= 0];
  58. }
  59. public function audit(int $id, string $action, int $adminId, string $remark = ''): array
  60. {
  61. Validator::make(compact('id', 'action', 'remark'), ['id' => ['required', 'integer', 'min:1'],
  62. 'action' => ['required', Rule::in(['approve', 'reject'])], 'remark' => ['string', 'max:2000']])->validate();
  63. if ($adminId <= 0) $this->invalid('admin', '请使用后台管理员身份审核');
  64. $userId = DB::table('share_claims')->where('id', $id)->value('user_id');
  65. if (!$userId) $this->invalid('id', '领取申请不存在');
  66. return DB::transaction(function () use ($id, $action, $adminId, $remark, $userId) {
  67. // Every claim mutation uses the same lock order as member requests and settlement corrections.
  68. $account = DB::table('share_accounts')->where('user_id', $userId)->lockForUpdate()->first();
  69. if (!$account) $this->invalid('id', '会员佣金账户不存在');
  70. $claim = DB::table('share_claims')->where('id', $id)->lockForUpdate()->first();
  71. if (!$claim || (int)$claim->user_id !== (int)$userId) $this->invalid('id', '领取申请不存在');
  72. $targetStatus = $action === 'approve' ? 'approved' : 'rejected';
  73. if ($claim->status === $targetStatus) return $this->detail($id);
  74. if ($claim->status !== 'pending') $this->invalid('id', '领取申请已审核,不可更改结果');
  75. $amount = (string)$claim->amount;
  76. if (bccomp($amount, '0.01', 10) < 0 || bccomp((string)$account->reserved_amount, $amount, 10) < 0) {
  77. $this->invalid('id', '申请金额或预留佣金异常,请核对');
  78. }
  79. $now = Carbon::now(ShareDefaults::TIMEZONE)->format('Y-m-d H:i:s');
  80. $changes = ['reserved_amount' => bcsub((string)$account->reserved_amount, $amount, 10), 'updated_at' => $now];
  81. $claimChanges = ['status' => $targetStatus, 'active_user_id' => null, 'audit_admin_id' => $adminId,
  82. 'audit_remark' => $remark, 'audited_at' => $now, 'updated_at' => $now];
  83. if ($action === 'approve') {
  84. if (bccomp(bcsub((string)$account->earned_amount, (string)$account->paid_amount, 10), $amount, 10) < 0) {
  85. $this->invalid('amount', '佣金修正后余额不足,请驳回原申请后由会员重新申请');
  86. }
  87. $user = User::query()->find($userId);
  88. if (!$user) $this->invalid('user_id', '会员不存在');
  89. $wallet = Wallet::query()->where('member_id', $user->member_id)->orderBy('id')->lockForUpdate()->first();
  90. if (!$wallet) $this->invalid('user_id', '会员钱包不存在');
  91. $before = (string)$wallet->available_balance;
  92. $after = bcadd($before, $amount, 10);
  93. $wallet->available_balance = $after;
  94. $wallet->save();
  95. $log = BalanceLogService::addLog($user->member_id, $amount, $before, $after, '分享佣金领取', $id, $remark);
  96. $claimChanges['credited_log_id'] = (int)$log->id;
  97. $changes['paid_amount'] = bcadd((string)$account->paid_amount, $amount, 10);
  98. }
  99. DB::table('share_accounts')->where('user_id', $userId)->update($changes);
  100. DB::table('share_claims')->where('id', $id)->update($claimChanges);
  101. OperationAuditService::record('share_claim', $id, $action, (array)$claim, (array)DB::table('share_claims')->where('id', $id)->first());
  102. return $this->detail($id);
  103. });
  104. }
  105. private function claim(object $row): array
  106. {
  107. return ['id' => (int)$row->id, 'audit_type' => self::AUDIT_TYPE, 'activity_name' => '代理佣金',
  108. 'user_id' => (int)$row->user_id, 'member_id' => (string)($row->member_id ?? ''),
  109. 'account' => (string)($row->account ?? ''), 'nickname' => (string)($row->nickname ?? ''), 'request_id' => (string)$row->request_id,
  110. 'amount' => $this->money($row->amount), 'status' => $row->status,
  111. 'audit_admin_id' => $row->audit_admin_id === null ? null : (int)$row->audit_admin_id,
  112. 'audit_remark' => (string)($row->audit_remark ?? ''), 'audited_at' => $row->audited_at,
  113. 'credited_log_id' => $row->credited_log_id === null ? null : (int)$row->credited_log_id,
  114. 'created_at' => $row->created_at, 'updated_at' => $row->updated_at];
  115. }
  116. private function positive(string $value): string { return bccomp($value, '0', 10) > 0 ? $value : '0'; }
  117. private function money($value): string { return bcadd((string)$value, '0', 2); }
  118. private function invalid(string $key, string $message): void { throw ValidationException::withMessages([$key => $message]); }
  119. }