doge 4 часов назад
Родитель
Сommit
5f07cb1645

+ 20 - 3
app/Console/Commands/SyncUserLevels.php

@@ -2,6 +2,7 @@
 
 namespace App\Console\Commands;
 
+use App\Models\Level;
 use App\Models\User;
 use App\Services\BalanceLogService;
 use Illuminate\Console\Command;
@@ -13,10 +14,9 @@ class SyncUserLevels extends Command
 
     public function handle(): int
     {
-        $query = User::query()->select('member_id')->orderBy('id');
-        if ($member = $this->option('member')) $query->where('member_id', $member);
+        if ($member = $this->option('member')) return $this->syncOne($member);
         $changed = 0;
-        $query->chunk(500, function ($users) use (&$changed) {
+        User::query()->select('member_id')->orderBy('id')->chunk(500, function ($users) use (&$changed) {
             foreach ($users as $user) {
                 [$before, $after] = BalanceLogService::syncLevel($user->member_id);
                 if ($after === $before) continue;
@@ -27,4 +27,21 @@ class SyncUserLevels extends Command
         $this->info("updated {$changed} users");
         return self::SUCCESS;
     }
+
+    // 单个用户输出计算依据,便于排查为什么没升级
+    private function syncOne(string $member): int
+    {
+        if (!User::where('member_id', $member)->exists()) {
+            $this->error("member_id {$member} 不存在(请确认传的是 users.member_id,不是 users.id)");
+            return self::FAILURE;
+        }
+        $total = BalanceLogService::getTotalRecharge($member);
+        $this->line('计入等级的类型:' . implode('、', BalanceLogService::$levelRechargeTypes));
+        $this->line("累计充值:{$total},按累计充值计算等级:" . BalanceLogService::calculateLevel($total));
+        $this->line('等级门槛:' . Level::orderBy('level')->get(['level', 'recharge'])
+            ->map(fn ($l) => "{$l->level}={$l->recharge}")->implode(', '));
+        [$before, $after] = BalanceLogService::syncLevel($member);
+        $this->info("当前等级:{$before} -> {$after}" . ($after === $before ? '(未变化,只升不降)' : ''));
+        return self::SUCCESS;
+    }
 }

+ 6 - 4
app/Http/Controllers/admin/ManualAudit.php

@@ -59,7 +59,8 @@ class ManualAudit extends Controller
         try {
             $params = request()->validate([
                 'request_id' => ['required', 'uuid'],
-                'member_id' => ['required', 'string', 'max:64'],
+                'member_id' => ['nullable', 'required_without:account', 'string', 'max:64'],
+                'account' => ['nullable', 'required_without:member_id', 'string', 'max:64'],
                 'change_type' => ['required', 'string', 'in:' . implode(',', BalanceLogService::$manualRecharge)],
                 'amount' => ['required', 'numeric', 'min:0.01', 'regex:' . self::AMOUNT_PATTERN],
                 'remark' => ['nullable', 'string', 'max:255'],
@@ -68,7 +69,7 @@ class ManualAudit extends Controller
             $result = ManualAuditService::credit(
                 (string)$params['request_id'],
                 (int)request()->user->id,
-                (string)$params['member_id'],
+                ManualAuditService::resolveMemberId($params['member_id'] ?? null, $params['account'] ?? null),
                 (string)$params['amount'],
                 (string)$params['change_type'],
                 $remark
@@ -87,7 +88,8 @@ class ManualAudit extends Controller
         try {
             $params = request()->validate([
                 'request_id' => ['required', 'uuid'],
-                'member_id' => ['required', 'string', 'max:64'],
+                'member_id' => ['nullable', 'required_without:account', 'string', 'max:64'],
+                'account' => ['nullable', 'required_without:member_id', 'string', 'max:64'],
                 'amount' => ['required', 'numeric', 'min:0.01', 'regex:' . self::AMOUNT_PATTERN],
                 'remark' => ['nullable', 'string', 'max:255'],
             ]);
@@ -95,7 +97,7 @@ class ManualAudit extends Controller
             $result = ManualAuditService::debit(
                 (string)$params['request_id'],
                 (int)request()->user->id,
-                (string)$params['member_id'],
+                ManualAuditService::resolveMemberId($params['member_id'] ?? null, $params['account'] ?? null),
                 (string)$params['amount'],
                 $remark
             );

+ 2 - 1
app/Http/Controllers/admin/Wallet.php

@@ -226,7 +226,8 @@ class Wallet extends Controller
             $order_id = request()->input('order_id');
             $amount = request()->input('amount');
             $remark = request()->input('remark');
-            $changeType = "人工充值";
+            // 订单补偿单独记类型:不算充值(等级、首充、返现等),报表仍计入人工加款
+            $changeType = "订单补偿";
             $key = 'api_request_' . md5($order_id . json_encode([
                         'amount' => $amount,
                         'remark' => $remark,

+ 1 - 1
app/Services/Agent/AgentReportService.php

@@ -91,7 +91,7 @@ class AgentReportService
         $funds = DB::table('balance_logs')->whereIn('member_id', clone $memberIds)
             ->whereBetween('created_at', [$start, $end])
             ->selectRaw(<<<'SQL'
-                COALESCE(SUM(CASE WHEN change_type = '人工充值' AND related_id IS NULL AND amount > 0 THEN amount ELSE 0 END), 0) AS manual_credit,
+                COALESCE(SUM(CASE WHEN ((change_type = '人工充值' AND related_id IS NULL) OR change_type = '订单补偿') AND amount > 0 THEN amount ELSE 0 END), 0) AS manual_credit,
                 COALESCE(SUM(CASE WHEN change_type = '人工扣款' AND amount < 0 THEN ABS(amount) ELSE 0 END), 0) AS manual_debit,
                 COALESCE(SUM(CASE WHEN (change_type IN ('注册赠送','优惠活动','即充即送','充值返现','老用户回归') OR (change_type = '人工充值' AND related_id = 0)) AND amount > 0 THEN amount ELSE 0 END), 0) AS bonus_amount,
                 COALESCE(SUM(CASE WHEN change_type IN ('比比返','笔笔返','返水','回水') AND amount > 0 THEN amount ELSE 0 END), 0) AS rebate_amount

+ 8 - 8
app/Services/BalanceLogService.php

@@ -20,13 +20,13 @@ class BalanceLogService extends BaseService
     public static string $MODEL = BalanceLog::class;
     //后台充值可选择的类型
     public static array $manualRecharge = ['人工充值', '注册赠送', '优惠活动'];
-    //计入累计充值(等级经验)的类型:前台提交的人工充值审核后记为“充值”,后台直接加款的“人工充值”不计入
-    public static array $levelRechargeTypes = ['充值', '三方充值'];
+    //计入累计充值(等级经验)的类型:后台直接加款的“人工充值”与前台充值一致计入
+    public static array $levelRechargeTypes = ['充值', '人工充值', '三方充值'];
     //需要计算回水的类型
     public static array $computeBackFlowChangeTypes = ['充值', '人工充值', '三方充值', '提现', '人工扣款', '三方提现'];
     //全部类型
     public static array $RW = [
-        '充值', '人工充值', '三方充值', '注册赠送', '优惠活动',
+        '充值', '人工充值', '订单补偿', '三方充值', '注册赠送', '优惠活动',
         '提现', '人工扣款', '三方提现', 
         //'体彩投注','体彩退款','体彩和局退款','体彩中奖','体彩输半退款',
         '新澳门六合彩投注','新澳门六合彩退款','新澳门六合彩和局退款','新澳门六合彩中奖',
@@ -164,7 +164,7 @@ class BalanceLogService extends BaseService
      */
     public static function addLog($memberId, $amount, $before_balance, $after_balance, $change_type, $related_id, $remark, $room_id = null)
     {
-        if (in_array($change_type, ['充值', '三方充值'], true)
+        if (in_array($change_type, self::$levelRechargeTypes, true)
             && DB::connection()->getSchemaBuilder()->hasTable('promotion_first_deposits')) {
             return DB::transaction(function () use ($memberId, $amount, $before_balance, $after_balance, $change_type, $related_id, $remark, $room_id) {
                 Wallet::where('member_id', $memberId)->lockForUpdate()->first();
@@ -197,8 +197,8 @@ class BalanceLogService extends BaseService
             self::syncLevel($memberId);
         }
 
-        //充值返现活动仅限前台用户充值,后台人工充值不触发活动赠送
-        if ( in_array($change_type, ['充值','三方充值'])) {
+        //充值返现活动(后台人工充值同样触发)
+        if ( in_array($change_type, ['充值','人工充值','三方充值'])) {
             $user = User::where('member_id', $memberId)->first();
             if ($user->from == 1) {
                 $level = self::calculateLevel(self::getTotalRecharge($memberId));
@@ -293,7 +293,7 @@ class BalanceLogService extends BaseService
             }
         }
         
-        if (in_array($change_type, ['充值', '三方充值'], true)) (new PromotionService())->onDeposit($result->refresh());
+        if (in_array($change_type, self::$levelRechargeTypes, true)) (new PromotionService())->onDeposit($result->refresh());
         return $result;
     }
 
@@ -466,7 +466,7 @@ class BalanceLogService extends BaseService
             //今日已充值数
             $dailyOrder = static::model()::where('member_id', $memberId)
                 ->whereBetween('created_at', [date('Y-m-d 00:00:00'), date('Y-m-d 23:59:59')])
-                ->whereIn('change_type', ['充值','三方充值'])
+                ->whereIn('change_type', ['充值','人工充值','三方充值'])
                 ->count();
             if ($dailyOrder > $params['reward_limit_count']) {
                 return false;

+ 3 - 3
app/Services/FundsSummaryService.php

@@ -85,7 +85,7 @@ class FundsSummaryService
     {
         $query = DB::table('balance_logs')
             ->whereIn('change_type', [
-                '充值', '三方充值', '人工充值', '人工扣款',
+                '充值', '三方充值', '人工充值', '订单补偿', '人工扣款',
                 '比比返', '笔笔返', '返水', '回水',
                 '注册赠送', '优惠活动', '即充即送', '充值返现', '老用户回归',
             ]);
@@ -105,10 +105,10 @@ class FundsSummaryService
                     WHEN change_type = '三方充值' AND amount > 0 THEN member_id END
                 ) AS third_party_deposit_people,
                 COALESCE(SUM(CASE
-                    WHEN change_type = '人工充值' AND related_id IS NULL AND amount > 0 THEN amount ELSE 0 END), 0
+                    WHEN ((change_type = '人工充值' AND related_id IS NULL) OR change_type = '订单补偿') AND amount > 0 THEN amount ELSE 0 END), 0
                 ) AS manual_credit_amount,
                 COUNT(DISTINCT CASE
-                    WHEN change_type = '人工充值' AND related_id IS NULL AND amount > 0 THEN member_id END
+                    WHEN ((change_type = '人工充值' AND related_id IS NULL) OR change_type = '订单补偿') AND amount > 0 THEN member_id END
                 ) AS manual_credit_people,
                 COALESCE(SUM(CASE
                     WHEN change_type = '人工扣款' AND amount < 0 THEN ABS(amount) ELSE 0 END), 0

+ 34 - 5
app/Services/ManualAuditService.php

@@ -33,9 +33,11 @@ class ManualAuditService
             ->forPage($page, $limit)
             ->get();
 
+        $accounts = self::accounts($rows->pluck('member_id')->all());
+
         return [
             'total' => $total,
-            'data' => $rows->map(static fn(ManualAuditRecord $row): array => self::format($row))->all(),
+            'data' => $rows->map(static fn(ManualAuditRecord $row): array => self::format($row, $accounts))->all(),
             'options' => [
                 'transaction_types' => [
                     ['label' => '人工上分', 'value' => 'credit'],
@@ -60,7 +62,33 @@ class ManualAuditService
             throw new Exception('人工稽查记录不存在', HttpStatus::CUSTOM_ERROR);
         }
 
-        return self::format($row);
+        return self::format($row, self::accounts([$row->member_id]));
+    }
+
+    /** 会员ID和账号可二选一,也可同时填写,但必须指向同一会员 */
+    public static function resolveMemberId(?string $memberId, ?string $account): string
+    {
+        $memberId = trim((string)$memberId);
+        $account = trim((string)$account);
+        if ($account === '') {
+            if ($memberId === '') throw new Exception('请填写会员ID或账号', HttpStatus::CUSTOM_ERROR);
+            return $memberId;
+        }
+        $matches = User::where('account', $account)->limit(2)->pluck('member_id');
+        if ($matches->isEmpty()) throw new Exception('账号不存在', HttpStatus::CUSTOM_ERROR);
+        if ($matches->count() > 1) throw new Exception('该账号对应多个会员,请填写会员ID', HttpStatus::CUSTOM_ERROR);
+        $resolved = (string)$matches->first();
+        if ($memberId !== '' && $memberId !== $resolved) throw new Exception('会员ID与账号不一致', HttpStatus::CUSTOM_ERROR);
+        return $resolved;
+    }
+
+    /** @return array<string,string> member_id => account */
+    private static function accounts(array $memberIds): array
+    {
+        $memberIds = array_values(array_unique(array_filter(array_map('strval', $memberIds), 'strlen')));
+        if (!$memberIds) return [];
+        return User::whereIn('member_id', $memberIds)->pluck('account', 'member_id')
+            ->map(static fn($account): string => (string)($account ?? ''))->all();
     }
 
     public static function credit(
@@ -118,7 +146,7 @@ class ManualAuditService
         $existing = ManualAuditRecord::where('request_id', $requestId)->first();
         if ($existing) {
             self::assertRequestMatches($existing, $requestHash);
-            return self::format($existing);
+            return self::format($existing, self::accounts([$memberId]));
         }
 
         $created = false;
@@ -215,7 +243,7 @@ class ManualAuditService
             self::notifyUser($memberId, (string)$audit->amount, (string)$audit->after_balance);
         }
 
-        return self::format($audit);
+        return self::format($audit, self::accounts([$memberId]));
     }
 
     private static function fail(ManualAuditRecord $audit, string $reason): ManualAuditRecord
@@ -257,7 +285,7 @@ class ManualAuditService
         return $query;
     }
 
-    private static function format(ManualAuditRecord $row): array
+    private static function format(ManualAuditRecord $row, array $accounts = []): array
     {
         $amount = (string)$row->amount;
         $status = (int)$row->status;
@@ -268,6 +296,7 @@ class ManualAuditService
             'admin_id' => $row->admin_id === null ? null : (int)$row->admin_id,
             'balance_log_id' => $row->balance_log_id === null ? null : (int)$row->balance_log_id,
             'member_id' => (string)$row->member_id,
+            'account' => $accounts[(string)$row->member_id] ?? '',
             'username' => (string)($row->username ?? ''),
             'first_name' => (string)($row->first_name ?? ''),
             'transaction_type' => (string)$row->transaction_type,

+ 1 - 1
app/Services/PromotionFacts.php

@@ -63,7 +63,7 @@ class PromotionFacts
         $totals = [];
         if (!$members) return $totals;
         $q = DB::table('balance_logs')->whereIn('member_id', $members)->where('type', 1)->where('amount', '>', 0)
-            ->whereIn('change_type', ['充值', '三方充值'])->where('created_at', '>=', $from)->where('created_at', '<=', self::now())->where('id', '>', $cursor);
+            ->whereIn('change_type', ['充值', '人工充值', '三方充值'])->where('created_at', '>=', $from)->where('created_at', '<=', self::now())->where('id', '>', $cursor);
         if ($until !== null) $q->where('created_at', '<', $until);
         if ($this->usesDecimalAggregates()) {
             foreach ($q->selectRaw('SUBSTR(created_at, 1, 10) AS day, SUM(amount) AS total')->groupByRaw('SUBSTR(created_at, 1, 10)')->get() as $row) {

+ 3 - 3
app/Services/PromotionService.php

@@ -414,14 +414,14 @@ class PromotionService
 
     public function onDeposit(BalanceLog $deposit): void
     {
-        if (!DB::connection()->getSchemaBuilder()->hasTable('promotion_first_deposits') || (int)$deposit->type !== 1 || !in_array($deposit->change_type, ['充值', '三方充值'], true) || bccomp((string)$deposit->amount, '0', 10) <= 0) return;
+        if (!DB::connection()->getSchemaBuilder()->hasTable('promotion_first_deposits') || (int)$deposit->type !== 1 || !in_array($deposit->change_type, ['充值', '人工充值', '三方充值'], true) || bccomp((string)$deposit->amount, '0', 10) <= 0) return;
         $member = User::where('member_id', $deposit->member_id)->first();
         if (!$member || (int)$member->from === 2) return;
         DB::transaction(function () use ($member, $deposit) {
             $wallet = Wallet::where('member_id', $member->member_id)->lockForUpdate()->first();
             if (!$wallet) $this->invalid('会员钱包不存在');
             $first = DB::table('balance_logs')->where('member_id', $member->member_id)->where('type', 1)->where('amount', '>', 0)
-                ->whereIn('change_type', ['充值', '三方充值'])->orderBy('created_at')->orderBy('id')->value('id');
+                ->whereIn('change_type', ['充值', '人工充值', '三方充值'])->orderBy('created_at')->orderBy('id')->value('id');
             if ((int)$first !== (int)$deposit->id) return;
             $inserted = DB::table('promotion_first_deposits')->insertOrIgnore(['user_id' => $member->id, 'deposit_log_id' => $deposit->id,
                 'created_at' => PromotionFacts::now(), 'updated_at' => PromotionFacts::now()]);
@@ -471,7 +471,7 @@ class PromotionService
             $result += ['loss' => bccomp($profit, '0', 10) < 0 ? bcsub('0', $profit, 2) : '0.00', 'applied_at' => $account->applied_at ?? null,
                 'qualified_invitees' => $account ? count($this->lossInvitees($member, $account)) : 0, 'valid_member_policy' => $this->facts->policy()];
         } else {
-            $result['has_deposited'] = DB::table('balance_logs')->where('member_id', $member->member_id)->where('type', 1)->where('amount', '>', 0)->whereIn('change_type', ['充值', '三方充值'])->exists();
+            $result['has_deposited'] = DB::table('balance_logs')->where('member_id', $member->member_id)->where('type', 1)->where('amount', '>', 0)->whereIn('change_type', ['充值', '人工充值', '三方充值'])->exists();
         }
         $result['rewards'] = $account ? DB::table('promotion_rewards')->where('account_id', $account->id)->orderByDesc('round_no')->limit(10)->get()->map(fn ($r) => (array)$r)->all() : [];
         if ($type === Rules::LOSS) {

+ 1 - 0
app/Services/Reporting/ReportSync.php

@@ -217,6 +217,7 @@ class ReportSync
             if (bccomp($amount, '0', 10) > 0 && in_array($type, ['充值', '三方充值'], true)) $kind = 'deposit';
             elseif (bccomp($amount, '0', 10) > 0 && in_array($type, ['注册赠送', '优惠活动', '即充即送', '充值返现', '老用户回归', '比比返', '笔笔返', '返水', '回水', '余额宝利息', '流水解冻'], true) && (int) ($row['type'] ?? 1) === 1) $kind = 'bonus';
             elseif ($type === '人工充值') $kind = isset($row['related_id']) && (string) $row['related_id'] === '0' ? 'bonus' : 'manual_credit';
+            elseif ($type === '订单补偿') $kind = 'manual_credit';
             elseif ($type === '人工扣款') { $kind = 'manual_debit'; $amount = bcsub('0', $amount, 10); }
             elseif ($type === '分享佣金领取') {
                 $claim = DB::table('share_claims')->where('credited_log_id', $row['id'])->where('status', 'approved')->first();

+ 1 - 1
app/Services/ShareCommissionDiagnostics.php

@@ -31,7 +31,7 @@ class ShareCommissionDiagnostics
         });
 
         $deposits = BalanceLog::where('type', 1)->where('amount', '>', 0)
-            ->whereIn('change_type', ['充值', '三方充值']);
+            ->whereIn('change_type', ['充值', '人工充值', '三方充值']);
         $realDeposits = (clone $deposits)->whereExists(function ($q) {
             $q->selectRaw('1')->from('users as invitee')
                 ->whereColumn('invitee.member_id', 'balance_logs.member_id')->where('invitee.from', 1)

+ 1 - 1
app/Services/Sharing/ShareMemberQualification.php

@@ -36,7 +36,7 @@ class ShareMemberQualification
             ->where(function ($q) use ($end) { $q->where('created_at', '<=', $end)->orWhereNull('created_at'); })
             ->get(['id', 'member_id'])->keyBy('id');
         $result['deposits'] = DB::table('balance_logs')->where('type', 1)->where('amount', '>', 0)
-            ->whereIn('change_type', ['充值', '三方充值'])->where('created_at', '<=', $end)
+            ->whereIn('change_type', ['充值', '人工充值', '三方充值'])->where('created_at', '<=', $end)
             ->select('member_id')->selectRaw('SUM(amount) AS total')->groupBy('member_id')->pluck('total', 'member_id');
         $result['effective'] = DB::table('share_bet_facts')->where('settled', 1)->where('voided', 0)
             ->where('currency', $result['currency'])->where('bet_date', '<=', $date)

+ 8 - 3
docs/admin-finance-api.md

@@ -85,7 +85,8 @@ POST 请求使用 `Content-Type: application/json`。统一响应:`code = 0` 
 | `request_id` | 本次人工操作的幂等请求 ID |
 | `admin_id` | 操作管理员 ID;历史导入记录可能为 `null` |
 | `balance_log_id` | 成功落账后的资金流水 ID;失败记录为 `null` |
-| `member_id` / `username` / `first_name` | 会员 ID、账号、昵称 |
+| `member_id` / `account` | 会员 ID、会员账号(`account` 取会员当前账号,会员不存在时为空字符串) |
+| `username` / `first_name` | 操作当时保存的用户名、昵称 |
 | `transaction_type` / `transaction_type_text` | `credit/debit`、人工上分/人工下分 |
 | `change_type` | 充值类型或人工扣款 |
 | `level_before_name` / `level_after_name` | 操作当时保存的会员等级名称 |
@@ -105,6 +106,7 @@ POST 请求使用 `Content-Type: application/json`。统一响应:`code = 0` 
 {
   "request_id": "7fd7a3cb-95f9-4acd-84e2-1ccbd211af92",
   "member_id": "10001",
+  "account": "test01",
   "change_type": "人工充值",
   "amount": 2000,
   "remark": "人工上分"
@@ -115,7 +117,9 @@ POST 请求使用 `Content-Type: application/json`。统一响应:`code = 0` 
 
 `change_type` 可选值由列表接口的 `options.credit_types` 提供。`remark` 可不传。成功时 `data` 返回完整稽查记录。
 
-人工上分不会触发前台充值活动赠送。
+`member_id` 和 `account` 至少填一个,也可以都填,但必须是同一个会员,否则返回“会员ID与账号不一致”。只填账号时,账号不存在返回“账号不存在”;一个账号对应多个会员时返回“该账号对应多个会员,请填写会员ID”。
+
+“人工充值”类型和前台充值一样计入会员等级、首充、充值返现等活动;“注册赠送”“优惠活动”不计入。
 
 ### POST `/admin/manualAudit/debit`
 
@@ -123,12 +127,13 @@ POST 请求使用 `Content-Type: application/json`。统一响应:`code = 0` 
 {
   "request_id": "10c2c632-92ee-419a-b550-e459454a2802",
   "member_id": "10001",
+  "account": "test01",
   "amount": 1000,
   "remark": "人工下分"
 }
 ```
 
-`request_id` 规则与人工上分相同。余额不足、用户不存在或钱包不存在时返回非零 `code`,同时 `data` 返回已保存的失败稽查记录;成功时 `data` 返回完整稽查记录。
+`request_id`、`member_id` / `account` 的规则与人工上分相同。余额不足、用户不存在或钱包不存在时返回非零 `code`,同时 `data` 返回已保存的失败稽查记录;成功时 `data` 返回完整稽查记录。
 
 ### GET `/admin/manualAudit/detail?id=1`
 

+ 1 - 0
lang/en/messages.php

@@ -215,6 +215,7 @@ return [
     "选择语言" => "Select Language",
     "钱包充值记录" => "Wallet Top-up History",
     "人工充值" => "Manual Top-up",
+    "订单补偿" => "Order Compensation",
     "人工扣款" => "Manual Deduction",
     "三方提现" => "Third-party Withdrawal",
     "资产转移" => "Asset Transfer",

+ 1 - 0
lang/vi/messages.php

@@ -215,6 +215,7 @@ return [
     "选择语言" => "Chọn ngôn ngữ",
     "钱包充值记录" => "Lịch sử nạp tiền vào ví",
     "人工充值" => "Nạp tiền thủ công",
+    "订单补偿" => "Bồi thường đơn hàng",
     "人工扣款" => "Khấu trừ thủ công",
     "三方提现" => "Rút tiền qua bên thứ ba",
     "资产转移" => "Chuyển tài sản",

+ 1 - 0
lang/zh/messages.php

@@ -215,6 +215,7 @@ return [
     "选择语言" => "选择语言",
     "钱包充值记录" => "钱包充值记录",
     "人工充值" => "人工充值",
+    "订单补偿" => "订单补偿",
     "人工扣款" => "人工扣款",
     "三方提现" => "三方提现",
     "资产转移" => "资产转移",

+ 87 - 0
tests/Integration/ManualAuditAccountTest.php

@@ -0,0 +1,87 @@
+<?php
+
+namespace Tests\Integration;
+
+use App\Services\ManualAuditService;
+use Illuminate\Config\Repository;
+use Illuminate\Database\Capsule\Manager;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Foundation\Application;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Facade;
+use PHPUnit\Framework\TestCase;
+
+/** Manual credit accepts member_id and/or account, and list rows show the member's account. */
+class ManualAuditAccountTest extends TestCase
+{
+    protected function setUp(): void
+    {
+        parent::setUp();
+        Facade::clearResolvedInstances();
+        $app = new Application(dirname(__DIR__, 2));
+        $app->instance('config', new Repository(['app' => ['timezone' => 'Asia/Shanghai']]));
+        Facade::setFacadeApplication($app);
+        $db = new Manager($app);
+        $db->addConnection(['driver' => 'sqlite', 'database' => ':memory:', 'prefix' => 'bot_']);
+        $db->setAsGlobal();
+        $db->bootEloquent();
+        $app->instance('db', $db->getDatabaseManager());
+        $app->bind('db.schema', fn () => $db->schema());
+        $db->schema()->create('users', function (Blueprint $t) {
+            $t->id(); $t->string('member_id'); $t->string('account')->nullable(); $t->timestamps();
+        });
+        $db->schema()->create('manual_audits', function (Blueprint $t) {
+            $t->id(); $t->string('request_id'); $t->string('request_hash'); $t->integer('admin_id')->nullable();
+            $t->integer('balance_log_id')->nullable(); $t->string('member_id'); $t->string('username')->default('');
+            $t->string('first_name')->default(''); $t->string('transaction_type'); $t->string('change_type');
+            $t->decimal('amount', 30, 10); $t->decimal('before_balance', 30, 10)->nullable(); $t->decimal('after_balance', 30, 10)->nullable();
+            $t->integer('level_before')->default(0); $t->string('level_before_name')->default(''); $t->integer('level_after')->default(0);
+            $t->string('level_after_name')->default(''); $t->integer('status')->default(0); $t->string('remark')->default('');
+            $t->string('failure_reason')->default(''); $t->timestamps();
+        });
+        DB::table('users')->insert([
+            ['member_id' => 'm1', 'account' => 'alice'],
+            ['member_id' => 'm2', 'account' => 'dup'],
+            ['member_id' => 'm3', 'account' => 'dup'],
+        ]);
+    }
+
+    protected function tearDown(): void
+    {
+        DB::disconnect();
+        Facade::clearResolvedInstances();
+        parent::tearDown();
+    }
+
+    public function test_member_id_and_account_resolve_to_one_member(): void
+    {
+        $this->assertSame('m1', ManualAuditService::resolveMemberId('m1', null));
+        $this->assertSame('m1', ManualAuditService::resolveMemberId(null, 'alice'));
+        $this->assertSame('m1', ManualAuditService::resolveMemberId(' m1 ', ' alice '));
+        $this->assertResolveFails('m2', 'alice', '会员ID与账号不一致');
+        $this->assertResolveFails(null, 'nobody', '账号不存在');
+        $this->assertResolveFails(null, 'dup', '对应多个会员');
+        $this->assertResolveFails('', '', '请填写会员ID或账号');
+    }
+
+    public function test_list_and_detail_return_the_member_account(): void
+    {
+        foreach (['m1', 'm9'] as $i => $memberId) {
+            DB::table('manual_audits')->insert(['request_id' => "r{$i}", 'request_hash' => 'h', 'member_id' => $memberId,
+                'transaction_type' => 'credit', 'change_type' => '人工充值', 'amount' => 10, 'status' => 1, 'created_at' => '2026-11-04 12:00:00']);
+        }
+        $rows = array_column(ManualAuditService::list([])['data'], 'account', 'member_id');
+        $this->assertSame(['m9' => '', 'm1' => 'alice'], $rows);
+        $this->assertSame('alice', ManualAuditService::detail(1)['account']);
+    }
+
+    private function assertResolveFails(?string $memberId, ?string $account, string $message): void
+    {
+        try {
+            ManualAuditService::resolveMemberId($memberId, $account);
+            $this->fail('Expected failure: ' . $message);
+        } catch (\Exception $e) {
+            $this->assertStringContainsString($message, $e->getMessage());
+        }
+    }
+}

+ 2 - 1
tests/Integration/ManualDepositFinanceTest.php

@@ -83,13 +83,14 @@ class ManualDepositFinanceTest extends TestCase
                 'status' => PaymentOrderService::STATUS_SUCCESS, 'member_id' => 'unrelated']));
         }
         DB::table('balance_logs')->insert($base + ['change_type' => '人工充值']);
+        DB::table('balance_logs')->insert($base + ['change_type' => '订单补偿']);
         $result = (new AgentMemberService(new AgentReportService()))->finance(Agent::findOrFail(1), [
             'start_date' => '2026-10-25', 'end_date' => '2026-10-25',
         ]);
         $member = $result['list'][0];
         $this->assertSame(3, $member->recharge_count);
         $this->assertSame('30.0000', $member->recharge_amount);
-        $this->assertSame('10.0000', $member->manual_credit);
+        $this->assertSame('20.0000', $member->manual_credit);
         $this->assertSame(3, $member->withdraw_count);
         $this->assertSame('30.0000', $member->withdraw_amount);
     }

+ 8 - 1
tests/Integration/ShareCommissionRecordingTest.php

@@ -296,7 +296,7 @@ class ShareCommissionRecordingTest extends TestCase
 
     public static function unrelatedDeposits(): array
     {
-        return [['人工充值', 'PROMO', 1], ['优惠活动', 'PROMO', 1], ['充值', '', 1], ['充值', 'MISSING', 1], ['充值', 'PROMO', 2]];
+        return [['订单补偿', 'PROMO', 1], ['优惠活动', 'PROMO', 1], ['充值', '', 1], ['充值', 'MISSING', 1], ['充值', 'PROMO', 2]];
     }
 
     #[DataProvider('unrelatedDeposits')]
@@ -309,6 +309,13 @@ class ShareCommissionRecordingTest extends TestCase
         $this->assertEquals('20.00', DB::table('wallets')->where('member_id', '1001')->value('available_balance'));
     }
 
+    public function test_admin_manual_recharge_pays_the_same_rebate_as_a_frontend_deposit(): void
+    {
+        BalanceLogService::addLog('1002', '100.00', '0.00', '100.00', '人工充值', null, '后台加款');
+        $this->assertSame(1, BalanceLog::where('change_type', '充值返现')->count());
+        $this->assertSame(1, ShareCommissionRecord::count());
+    }
+
     public function test_commission_insert_failure_rolls_back_the_rebate_wallet_and_ledger(): void
     {
         DB::statement("CREATE TRIGGER fail_share_insert BEFORE INSERT ON bot_share_commission_records