--- name: DailyRebateLogic重构 overview: 按 `php-code` 规范重构每日返水领取链路:`claim` 编排化、BusinessException、Model 条件更新;同步调整 Controller 异常映射。`settleDate`/`info` 仅做必要小抽取,不改业务语义。 todos: - id: model-mark-claimed content: DailyRebateRecordModel 增加 markClaimedIfClaimable + PHPDoc status: pending - id: logic-claim-refactor content: DailyRebateLogic:claim 拆 assert/perform/format,BusinessException status: pending - id: logic-info-helper content: 可选:isClaimableRecord 供 info 与 assert 复用 status: pending - id: controller-exception content: DailyRebateController:catch BusinessException → FAILED status: pending - id: verify-smoke content: 跑 verify-slot-backend.sh;确认无 RuntimeException 新增 status: pending isProject: false --- # DailyRebateLogic 规范重构 ## 范围(AskQuestion 中断,按推荐默认) - **必做**:[`claim`](slot_console/app/api/logic/DailyRebateLogic.php)、[`DailyRebateController::claim`](slot_console/app/api/controller/DailyRebateController.php)、[`DailyRebateRecordModel`](slot_console/app/model/common/DailyRebateRecordModel.php) - **轻量**:`info()` 抽取「是否可领」判断为 private,与 `assertClaimableRecord` 复用逻辑 - **不改语义**:`settleDate` / `expireDueRecords` / Redis 结算逻辑保持行为一致,仅在有重复代码时抽 1 个 private(可选) ## 现状问题(对照 php-code) [`claim`](slot_console/app/api/logic/DailyRebateLogic.php) L92–157: - 6 段 `RuntimeException` if 墙 - CAS 写在 Logic 内联,未沉淀 Model - Controller `catch RuntimeException` + `PARAMS_ERROR`(verify 会拦新增行) 参照:[`FreeCreditsLogic::claim`](slot_console/app/api/logic/FreeCreditsLogic.php)(`assert*` + `BusinessException` + 状态更新) ## 目标结构 ```mermaid sequenceDiagram participant C as DailyRebateController participant L as DailyRebateLogic participant M as DailyRebateRecordModel participant W as WalletService C->>L: claim(uid, source, statDate) L->>L: assertActivityEnabled L->>L: assertUserDeposited L->>M: findByUidAndDate L->>L: assertClaimableRecord L->>M: markClaimedIfClaimable L->>W: gift(rebate, DAILY_REBATE, remark) L-->>C: formatClaimResult ``` ### 1. Model:条件更新 在 [`DailyRebateRecordModel`](slot_console/app/model/common/DailyRebateRecordModel.php) 新增: ```php /** * 待领取状态下标记为已领取(CAS)。 * * @return int 影响行数,1 表示成功 */ public static function markClaimedIfClaimable(int $id): int ``` 实现:`where id` + `where status = STATUS_CLAIMABLE` → `STATUS_CLAIMED` + `claimed_at`。 ### 2. Logic:`claim` 拆分为编排 + assert + perform | 方法 | 职责 | |------|------| | `claim()` public | 编排 ≤20 行:`resolveSource` → 默认 `statDate` → assert → `performClaim` → 返回 | | `assertActivityEnabled(string $source)` | 活动未开 → `BusinessException` | | `assertUserDeposited(int $uid)` | 未充值 → `BusinessException` | | `assertClaimableRecord(?DailyRebateRecordModel $record)` | 不存在/状态/金额/过期 → 各一条 `BusinessException`(合并原 4 条 if) | | `performClaim(int $uid, DailyRebateRecordModel $record, string $statDate)` | 事务:`markClaimedIfClaimable` → `WalletService::gift` → commit;`affected !== 1` 或 wallet 空 → `BusinessException` | | `formatClaimResult(...)` | 返回 `stat_date` / `rebate_amount` / `display` / `balance` | - 使用 `support\exception\BusinessException`(与 FreeCredits 一致) - `@throws` 改为 `BusinessException`、`\Throwable`(钱包失败) - **钱包 biz_id**:`WalletService::gift()` 内部已 `generateOrderId`([`slot_lib/src/services/WalletService.php`](slot_lib/src/services/WalletService.php) L262–265);幂等依赖 **记录 CAS**,remark 保持 `每日返水 {statDate}` 便于对账 ### 3. Logic:`info` 轻量复用(可选) 抽取 `isClaimableRecord(DailyRebateRecordModel $record): bool`(status + amount + expire),供 `info()` 的 `claimable` 块与 `assertClaimableRecord` 共用,避免两套判断漂移。 ### 4. Controller:异常映射 [`DailyRebateController::claim`](slot_console/app/api/controller/DailyRebateController.php): ```php } catch (BusinessException $e) { return $this->errorCode(ErrorCode::FAILED, $e->getMessage()); } ``` - 对齐 [`SignController`](slot_console/app/api/controller/SignController.php)(业务失败 `FAILED` + 文案) - **API 变更**:`code` 由 `40003`(PARAMS_ERROR)变为 `1`(FAILED);文案仍为中文业务提示。若 PWA 强依赖 `40003`,可在计划中改为保留 `PARAMS_ERROR`(实现时二选一,默认 `FAILED`) 不采用 FreeCredits「无 catch、走全局 Handler」方式,避免未捕获时变成 `SYSTEM_ERROR`(50001)。 ### 5. 不动 / 谨慎 - **事务边界**:维持「先 CAS 再 gift 再 commit」;不在此 PR 改为「先 wallet 后 DB」或 Outbox - **`settleDate`**:逻辑不变;可选抽 `settleOneUserFromRedis(...)` 降低 foreach 嵌套(非必须) ## 文件清单 | 文件 | 变更 | |------|------| | `slot_console/app/model/common/DailyRebateRecordModel.php` | +`markClaimedIfClaimable` | | `slot_console/app/api/logic/DailyRebateLogic.php` | 重构 `claim`;可选 `isClaimableRecord` | | `slot_console/app/api/controller/DailyRebateController.php` | `BusinessException` + `FAILED` | ## 验收 1. `docker exec -w /app/www/slot/slot_console php82 php webman dailyRebateSettle --date=...`(如有环境)结算后,已充值用户可领昨日返水 2. 重复领取 → 业务错误文案,DB 仍为 `CLAIMED`,不重复入账 3. `~/.cursor/hooks/verify-slot-backend.sh` → PASS(无新增 `RuntimeException` 业务态) 4. 最终回复含 `PHPDoc: checked`(触及符号补全 `@throws BusinessException`) ## 风险与回滚 - **PWA 错误码**:若前端按 `code===40003` 分支,需同步前端或 Controller 保留 `PARAMS_ERROR` - 回滚:还原 3 个文件即可