This commit is contained in:
ray zhou
2026-05-29 18:16:34 +08:00
parent 1bcb6120dd
commit ad623aad91
6 changed files with 551 additions and 22 deletions

View File

@@ -0,0 +1,244 @@
---
name: Provider Callback Log
overview: 在 slot-pwa 按分库分表(对齐 GameResultLogModel落地 game_provider_callback_log_{NN},并实现 POP 回调全链路(收包落库 → 验签/解析/处理状态)。其余厂商后续 PR 扩展。
todos:
- id: shard-ddl-model
content: CreateSharedTable 增加 callback_log 分表 DDLshard/GameProviderCallbackLogModel + Params + IdGenerator
status: completed
- id: callback-service
content: ProviderCallbackLogService按 shardUid 路由)+ config/game_gateway.php
status: completed
- id: pop-middleware
content: ProviderCallbackLogMiddleware + pop 中间件链 + POPAuthMiddleware 验签回写
status: completed
- id: pop-logic
content: CashLogic/CashController parse/process 状态(含重复 tid、BALANCE
status: completed
- id: tests
content: Feature 测试shard 写入/更新)+ docker phpunitverify-slot-backend.sh
status: completed
isProject: false
---
# Provider 原始回调日志02实现计划
## 范围(已确认)
- **本期**:分库分表 DDL + Shard Model/Service + POP 两条路由接入 + PHPUnit
- **后续 PR**gasea / quick / jsgame / bbgt / jdb
**明确不做**(属 [03_provider_tx.md](docs/requirements/game_gateway/03_provider_tx.md)`game_provider_tx_01`、wallet 改造、Round 聚合。
**文档与 DDL**:已与 [02 §4.1](docs/requirements/game_gateway/02_provider_callback_log.md)、[`slot-pwa/db/game_provider_callback_log_shard.sql`](slot-pwa/db/game_provider_callback_log_shard.sql) 对齐(无 `gateway_version`;含分库分表说明)。
---
## 现状与约束
| 项 | 现状 |
|---|---|
| POP 路由 | [`config/route.php`](slot-pwa/config/route.php) `/pop/Cash/Get``/pop/Cash/TransferInOut` |
| POP 验签 | [`POPAuthMiddleware`](slot-pwa/app/middleware/POPAuthMiddleware.php) 验签在 Controller 前 |
| **分表参考(本期主参考)** | [`app/model/shard/GameResultLogModel.php`](slot-pwa/app/model/shard/GameResultLogModel.php) + [`CreateSharedTable`](slot-pwa/app/command/CreateSharedTable.php) |
| 非分表参考 | [`GameLaunchSessionModel`](slot-pwa/app/model/game/GameLaunchSessionModel.php) 仅用于 Launch Session**不**用于 callback_log |
| ID 生成 | [`IdGenerator::nextGameResultId()`](slot-pwa/app/service/IdGenerator.php) 同款 Redis 序号 + 毫秒时间戳 |
需求文档表名 `game_provider_callback_log_01` 表示**逻辑表前缀**;物理表为 `user_transaction_{dbNN}.game_provider_callback_log_{tableNN}`(与 `game_result_log_{NN}` 同库族)。
---
## 分库分表设计
### 路由规则(对齐 GameResultLog
```php
// GameResultLogModel
getPreDatabase() => 'user_transaction'
getPreTable() => 'game_provider_callback_log' // 物理表 game_provider_callback_log_01..NN
getShardKey() => 'uid'
```
- 构造:`new GameProviderCallbackLogModel($shardUid)`,由 [`ShardService`](slot-pwa/app/service/ShardService.php) `crc32($shardUid) % (库数×表数)` 定位库表。
- 写入:[`insertData()`](slot-pwa/app/model/shard/SelfBaseModel.php)(同 `TransactionLog::insertGameResult`)。
- 更新:[`updateByWhere()`](slot-pwa/app/model/shard/SelfBaseModel.php),条件必须含 `id` + `uid`(分片键),例如 `['id' => $id, 'uid' => $shardUid]`
### 分片键 `uid` 的选取(收包时)
| 场景 | shardUid |
|------|----------|
| POP 已带 `userid` | `(int) post.userid` |
| 尚未解析 / 验签失败但 body 无 uid | `0`(需求允许 `uid=0`,仍落同一分片桶,保留审计) |
| 解析成功后 uid 与收包时不一致 | **不迁移分片**;仅 `UPDATE` 修正行内 `uid` 等业务字段(行已在收包分片,避免跨库搬家) |
**Request 上下文**Middleware 写入Logic/Middleware 读取):
- `providerCallbackLogId` — 雪花主键
- `providerCallbackShardUid` — 分片键,后续所有 UPDATE 必带
### 与 `GameResultLog` 的差异
| 项 | GameResultLog | callback_log |
|----|---------------|----------------|
| 汇总库 `s_all.*` | 有 [`app/model/all/GameResultLogModel`](slot-pwa/app/model/all/GameResultLogModel.php) + MQ 同步 | **本期不做**(无跨分片报表需求;后续若要全局检索再加 DBSync |
| 字段 | 精简结果字段 | 需求全文raw_body/headers、验签/解析/处理状态等 |
---
## 处理顺序
```mermaid
sequenceDiagram
participant Provider
participant LogMW as ProviderCallbackLogMiddleware
participant PopAuth as POPAuthMiddleware
participant Logic as CashLogic
participant Shard as user_transaction_XX.game_provider_callback_log_YY
Provider->>LogMW: POST
LogMW->>LogMW: resolveShardUid from post
LogMW->>Shard: INSERT id=nextProviderCallbackLogId
LogMW->>PopAuth: request + logId + shardUid
PopAuth->>Shard: UPDATE verify_status
PopAuth->>Logic: handler
Logic->>Shard: UPDATE parse/process
```
---
## 1. 数据库(分表 DDL
### 修改 [`CreateSharedTable`](slot-pwa/app/command/CreateSharedTable.php)
`createShareTable()` 循环内、`createGameResultTable()` 之后增加 `createGameProviderCallbackLogTable($db, $num)`
- 库:`user_transaction_01``user_transaction_{share_database_num}`
- 表:`game_provider_callback_log_01``game_provider_callback_log_{share_table_num}`
- 字段/索引以 [02 §4.1](docs/requirements/game_gateway/02_provider_callback_log.md) 为准,**除外**:不建 `gateway_version`、不建 `idx_gateway_version`;其余 KEY`idx_provider_tx``idx_round``idx_uid_received``idx_body_hash``idx_process_status`)保留
- 主键 `id` **非自增**,应用写入 `IdGenerator::nextProviderCallbackLogId()`
### 参考 SQL文档用非单库执行
新增 [`slot-pwa/db/game_provider_callback_log_shard.sql`](slot-pwa/db/game_provider_callback_log_shard.sql):单分片模板 + 注释说明需通过 `php webman createSharedTable`(或项目既有命令)批量建表。
部署:`docker exec ... php webman createSharedTable`(与现有 `user_transaction_log` / `game_result_log` 相同流程)。
---
## 2. 配置
[`config/game_gateway.php`](slot-pwa/config/game_gateway.php)
| 配置项 | 用途 |
|--------|------|
| `callback_request_id_prefix` | 如 `creq_` |
| `route_callback_map` | 路径 → 默认 `callback_type` |
| `provider_shard_uid_field` | 厂商 → POST 分片字段POP`userid` |
[`RedisKeyManagerService`](slot-pwa/app/service/RedisKeyManagerService.php)`PROVIDER_CALLBACK_LOG_NEXT_ID`
[`IdGenerator`](slot-pwa/app/service/IdGenerator.php)`nextProviderCallbackLogId()`(对齐 `nextGameResultId`
---
## 3. 领域层
### Shard Model — [`app/model/shard/GameProviderCallbackLogModel.php`](slot-pwa/app/model/shard/GameProviderCallbackLogModel.php)
继承 [`BaseShardModel`](slot-pwa/app/model/shard/BaseShardModel.php)
```php
protected function getPreDatabase(): string { return 'user_transaction'; }
protected function getPreTable(): string { return 'game_provider_callback_log'; }
protected function getShardKey(): string { return 'uid'; }
```
- 状态常量:`VERIFY_*` / `PARSE_*` / `PROCESS_*`
- 数据访问(均需构造时传入 `$shardUid`
- `insertReceived(GameProviderCallbackLogReceiveParams $params): void`(内部 `insertData`
- `updateVerifyStatus(int $id, ...)`
- `updateParseResult(int $id, GameProviderCallbackLogParseParams $params)`
- `updateProcessStatus(int $id, ...)`
- `findById(int $id): ?array``findOne(['id' => $id, 'uid' => $this->_shardVal])`
### Value Objects可放 `app/model/game/` 或 `app/entity/game/`
- `GameProviderCallbackLogReceiveParams` — 含 `shardUid`、落库字段
- `GameProviderCallbackLogParseParams` — 解析后业务字段
### Service — [`app/service/game/ProviderCallbackLogService.php`](slot-pwa/app/service/game/ProviderCallbackLogService.php)
- `recordReceivedFromRequest(Request $request): void` — 解析 `shardUid`、生成 `id`/`request_id`、**同步** `new GameProviderCallbackLogModel($shardUid)->insertReceived(...)`,设置 `$request->providerCallbackLogId` / `providerCallbackShardUid`
- `markVerify*(int $shardUid, int $id, ...)`
- `markParse*(int $shardUid, int $id, ...)`
- `markProcess*(int $shardUid, int $id, ...)`
所有 mark 方法内部统一:`new GameProviderCallbackLogModel($shardUid)->updateByWhere(['id' => $id, 'uid' => $shardUid], ...)`
---
## 4. MiddlewarePOP 本期)
- 新增 [`ProviderCallbackLogMiddleware`](slot-pwa/app/middleware/ProviderCallbackLogMiddleware.php) — **最先**执行
- [`config/middleware.php`](slot-pwa/config/middleware.php) `'pop' => [ProviderCallbackLogMiddleware, POPAuthMiddleware]`
- [`POPAuthMiddleware`](slot-pwa/app/middleware/POPAuthMiddleware.php):验签结果写回 callback_log失败 `VERIFY_FAILED` + `PROCESS_REJECTED`
- `/pop/admin/` 跳过落库
---
## 5. POP Logic
[`CashLogic`](slot-pwa/app/pop/logic/CashLogic.php) / [`CashController`](slot-pwa/app/pop/controller/CashController.php)
-`$request` 读取 `providerCallbackLogId` + `providerCallbackShardUid`
- `balance` / `modifyTransferInOut`parse/process 状态;重复 tid → `PROCESS_DUPLICATE`
- Validate 失败:`PARSE_FAILED` + `PROCESS_REJECTED`(保留已落库的 raw 证据)
---
## 6. 测试
参照分表写入方式(非 LaunchSession 单表):
- [`tests/Support/ProviderCallbackLogTestCase.php`](slot-pwa/tests/Support/ProviderCallbackLogTestCase.php) — 固定测试 `shardUid``insertReceived` + teardown `delete`
- [`tests/Feature/GameProviderCallbackLogShardModelTest.php`](slot-pwa/tests/Feature/GameProviderCallbackLogShardModelTest.php)
- 初始 `verify_status=0``process_status=0`
- `updateByWhere` 后终态、`processed_at` 写入
-`body_hash`、同/不同 `shardUid` 可各插一条
`docker exec -w /app/www/slot/slot-pwa php82 ./vendor/bin/phpunit`
---
## 7. 验收对照POP + 分表)
| 验收项 | 实现 |
|--------|------|
| 原始请求可追溯 | `raw_body` / `raw_headers` 写入对应分片表 |
| 验签/解析失败仍有行 | Middleware 先 INSERTshardUid 可为 0 |
| BALANCE 仅 callback_log | 无 provider_tx |
| 重复回调多条 log | 每次 INSERTtid 重复 → `process_status=3` |
| 分库分表 | 与 GameResultLog 相同 `user_transaction_*` 族 |
---
## 8. 后续 PR
- 其它厂商 Middleware + `provider_shard_uid_field`
- 可选:`app/model/all/GameProviderCallbackLogModel` + DBSync仅当需要 `s_all` 汇总检索时)
---
## 关键文件
| 操作 | 路径 |
|------|------|
| 修改 | `app/command/CreateSharedTable.php` |
| 新增 | `db/game_provider_callback_log_shard.sql`(模板) |
| 新增 | `app/model/shard/GameProviderCallbackLogModel.php` |
| 新增 | `app/model/game/GameProviderCallbackLog*Params.php` |
| 新增 | `app/service/game/ProviderCallbackLogService.php` |
| 新增 | `app/middleware/ProviderCallbackLogMiddleware.php` |
| 修改 | `IdGenerator.php`, `RedisKeyManagerService.php`, `middleware.php`, `POPAuthMiddleware.php` |
| 修改 | `app/pop/logic/CashLogic.php`, `app/pop/controller/CashController.php` |
| 新增 | `tests/Feature/GameProviderCallbackLogShardModelTest.php` |
完成编码后执行 `~/.cursor/hooks/verify-slot-backend.sh` 并粘贴输出。

View File

@@ -0,0 +1,178 @@
---
name: 首充剩余分档规则
overview: 将 Free Credits「剩余金额定格额 第一档 $20」从固定 package_amount 均分,改为按 10/20/30/50/100 元阶梯瀑布拆分;后续档解锁改为「累计充值 ≥ 每档门槛(标准档=面额−$1尾档 unlock 按大单位整数向下取整,如 15340qf→15000qf」。
todos:
- id: split-algorithm
content: 实现 buildReleasePackages 瀑布拆分,替换 splitReleasePackageAmounts + createPackages
status: pending
- id: db-unlock-qf
content: free_credits_package 增加 unlock_recharge_qfinstall.sql + 迁移脚本
status: pending
- id: advance-unlock
content: advanceByRecharge 按累计充值+unlock_recharge_qf 解锁;存量 unlock=0 回退旧逻辑
status: pending
- id: admin-config
content: 废弃 package_amount/subsequent_min 后台必填;更新 ActivityValidate
status: pending
- id: tests-docs
content: 单测/集成测 + 需求文档 §5.7/5.8 更新
status: pending
isProject: false
---
# 首充剩余定格 — 分档与解锁规则变更
## 背景与现状
核心逻辑在 [`slot_console/app/api/logic/FreeCreditsLogic.php`](slot_console/app/api/logic/FreeCreditsLogic.php)
- **定格剩余**`leftAmount = frozenAmount - firstCashAmount`(第一档默认 $20来自 `ext_config.first_cash_amount`
- **当前拆分**`splitReleasePackageAmounts($left, package_amount)` — 每档 `min(package_amount, 剩余)` 循环切分(默认 $10/档)
- **当前解锁**:第一档看 `totalRecharge >= recharge_unlock_amount`(默认 $50后续档看 **本笔** `rechargeAmount >= subsequent_min_recharge`(默认 $10且受 `max_unlock_per_recharge` 限制
需求文档旧版 §5.7 见 [`docs/requirements/首充前免费余额定格与分档释放需求文档.md`](docs/requirements/首充前免费余额定格与分档释放需求文档.md)。
## 新规则(产品口径)
**基数**`剩余 = 定格金额 第一档免打码提现额($20`
按顺序「吃掉」剩余金额(瀑布式,非按总额选一档):
| 阶段 | 单档面额 | 本阶段最多档数 | 累计充值解锁门槛(每档) |
|------|---------|---------------|------------------------|
| 1 | $10 | 3 | ≥ $9 |
| 2 | $20 | 5 | ≥ $19 |
| 3 | $30 | 10 | ≥ $29 |
| 4 | $50 | 10 | ≥ $49 |
| 5 | $100 | 直到剩余 < $100 | ≥ $99 |
| 6 | 尾档 | 剩余 < $100 的余额,**一整档**`amount_qf` 可为小数大单位,如 $15.34 | **累计充值门槛 = 面额按大单位整数向下取整**(见下) |
金额在代码中为 **千分位整数qf**$10 → `10000`
### 示例(定格 $78.50,第一档 $20剩余 $58.50
```
阶段1: 3×$10 = $30 → 剩 $28.50
阶段2: 1×$20 = $20 → 剩 $8.50
阶段35: 不足整档,跳过
阶段6: 1×$8.50 → 剩 $0unlock_recharge_qf = 8000即 $8
```
共 5 个 release 档:`10,10,10,20,8.5`(旧规则为 6 档均 $10 + 尾档)。
### 尾档解锁门槛:大单位整数(不允许小数)
尾档 **`amount_qf` 仍保留实际剩余**(可含「分」级精度,如 `15340` qf = $15.34),但写入 `unlock_recharge_qf` 时必须 **向下取整到大单位整数美元**
```text
unlock_recharge_qf = intdiv(amount_qf, 1000) * 1000 // 去掉 qf 余数,等价 floor 到整元
```
| amount_qf小单位/千分位) | 档位展示面额 | unlock_recharge_qf累计充值门槛 |
|-------------------------|-------------|-----------------------------------|
| `15340` | $15.34 | `15000`$15非 $15.34 |
| `8500` | $8.50 | `8000`$8 |
| `10000` | $10.00 | `10000`$10 |
实现时抽私有方法,例如 `calcTailUnlockRechargeQf(int $amountQf): int`,仅用于阶段 6标准档10/20/30/50/100仍为 `amount - 1000` qf本身已是整元。
```mermaid
flowchart TD
left[剩余金额 leftQf]
b1["阶段1: 最多3档 x 10元"]
b2["阶段2: 最多5档 x 20元"]
b3["阶段3: 最多10档 x 30元"]
b4["阶段4: 最多10档 x 50元"]
b5["阶段5: 整百100元直到剩小于100"]
b6["阶段6: 尾档=剩余金额"]
left --> b1 --> b2 --> b3 --> b4 --> b5 --> b6
```
## 实现方案
### 1. 拆分算法(替换 `splitReleasePackageAmounts`
`FreeCreditsLogic` 中新增结构化方法,例如:
```php
/**
* @return list<array{amount_qf: int, unlock_recharge_qf: int}>
*/
public static function buildReleasePackages(int $leftAmountQf): array
```
- 用常量表描述 4 个固定阶段 `{amount: 10000|20000|30000|50000, maxCount: 3|5|10|10, unlock: amount-1000}`
- 阶段 5`while ($left >= 100000)` 追加 `{100000, 99000}`
- 阶段 6`if ($left > 0)` 追加 `{amount_qf: left, unlock_recharge_qf: calcTailUnlockRechargeQf(left)}`**unlock 为整元,不等于 left**
- 每阶段只取 `min(maxCount, floor(left / amount))` 个**整档**,余数进入下一阶段
`createPackages()` 改为遍历上述结构写入 `free_credits_package`,不再读 `package_amount` 配置。
### 2. 库表:每档解锁门槛
[`free_credits_package`](slot_console/db/install.sql) 当前无解锁门槛字段。建议新增:
```sql
unlock_recharge_qf bigint unsigned NOT NULL DEFAULT 0 COMMENT '解锁本档所需累计真实充值,千分位'
```
- 定格写入时一并落库
- **存量用户**(默认策略):已有行 `unlock_recharge_qf=0` 时,`advanceByRecharge` 回退旧逻辑(`subsequent_min_recharge` + 单笔充值),避免行为突变
- **新定格**:一律写入新门槛
(若产品确认「存量也迁移」,另做一次性 SQL/脚本按 player 重算 package 行 — 需单独评估已解锁/已领取档。)
### 3. 解锁逻辑(`advanceByRecharge`
后续 release 档变更:
| 项 | 旧 | 新 |
|----|----|-----|
| 条件 | `rechargeAmount >= subsequent_min` | `totalRecharge >= package.unlock_recharge_qf` |
| 顺序 | 仍按 `package_no` 逐档 | 不变 |
| 单笔上限 | `max_unlock_per_recharge` | **保留**(大额充值仍最多解锁 N 档) |
第一档逻辑不变(仍用 `recharge_unlock_amount`,默认 $50
### 4. 配置与后台
- [`package_amount`](backend/slot_admin_vue/src/views/game/activity/edit.vue) / [`subsequent_min_recharge`](backend/slot_admin_vue/src/views/game/activity/edit.vue):对新定格**不再参与拆分/解锁**;后台表单项可标为「已废弃」或隐藏,校验 [`ActivityValidate::checkFreeCreditsExt`](backend/slot_admin/app/game/validate/ActivityValidate.php) 改为非必填(避免运营误填)
- `max_unlock_per_recharge``first_cash_amount``recharge_unlock_amount` 继续有效
### 5. C 端 API可选增强
[`FreeCreditsController::status`](slot_console/app/api/controller/FreeCreditsController.php) 当前 `packages``id/amount/status`。若前端要展示「再充 $X 解锁下一档」,可在每项增加 `unlock_recharge_amount`(大单位 float**非必须**help 文案可先说明规则。
### 6. 测试
更新/新增 [`FreeCreditsLogicAmountTest.php`](slot_console/tests/Unit/FreeCreditsLogicAmountTest.php)
- DataProvider`58500` qf 剩余 → `[10000×3, 20000, 8500]`,尾档 unlock=`8000`(非 8500
- 尾档取整:`15340` → amount=`15340`, unlock=`15000`
- 大额:`1000000` qf 剩余 → 验证各阶段档数上限与总和守恒
- 边界:`left=0`、整阶段边界30/100/500…
集成测 [`FreeCreditsFirstRechargeFreezeTest`](slot_console/tests/Integration/FreeCreditsFirstRechargeFreezeTest.php) 断言 package 行数与金额。
解锁:在 harness 中 mock `totalRecharge`,验证 `unlock_recharge_qf` 达标后变 `ready`
### 7. 文档
更新 [`docs/requirements/首充前免费余额定格与分档释放需求文档.md`](docs/requirements/首充前免费余额定格与分档释放需求文档.md) §5.7、§5.8 与示例表。
## 影响范围(不改)
- `slot_wallet` 定格/冻结、Claim 入账
- `slot_pay` 第一档提现
- 第一档 $20 / 累计 $50 解锁第一档 — **本次需求未改**
## 风险与验收
- **金额守恒**`sum(release.amount_qf) === frozen - first_cash`
- **幂等**:定格仍按 `first_recharge_order_id` 幂等,不重复拆档
- **存量**:默认 grandfather上线前确认是否有在途「旧档位」用户
- 验收脚本:`verify-slot-backend.sh` + phpunit `FreeCreditsLogicAmountTest`
```bash
docker exec -w /app/www/slot/slot_console php82 ./vendor/bin/phpunit tests/Unit/FreeCreditsLogicAmountTest.php
```

View File

@@ -70,7 +70,7 @@ alwaysApply: true
- 参数超过 3 个时,优先使用 DTO、Value Object 或专门数据结构。
- 核心业务入参不要直接使用含糊的 `array`。
- 返回 `array` 时必须结构明确,并用 PHPDoc 描述结构。
- 私有方法也必须有清晰业务语义。
- 私有方法也必须有清晰业务语义,并写中文 PHPDoc见 §5纯 getter/setter 除外)
- 复杂条件必须封装成有语义的方法。
- 魔法数字必须变成常量或枚举。
- 业务规则判断优先使用 `ensureXxx()` 方法。
@@ -98,7 +98,7 @@ alwaysApply: true
- Logic 是业务用例入口,一个 public 方法通常对应一个业务用例。
- Logic 主方法要像读业务流程。
- Logic 私有方法必须有业务语义。
- Logic 私有方法必须有业务语义与中文 PHPDoc纯 getter/setter 除外)
- Logic 可以查业务主体、调用 Model、调用公共 Service、做业务判断、控制事务、写业务日志、组装返回结果。
- 复杂查询条件应沉淀到 Model。
- 涉及多表写入、钱包变动、订单状态变更、活动领取、返水领取、游戏下注 / 派奖时,事务边界必须放在 Logic。
@@ -164,9 +164,10 @@ class GameLaunchSessionModel extends Model
### 默认规则(必须)
**Logic / Service / Model 的 `public` 方法必须写 PHPDoc**,首行用**中文**说明业务动作或结果(很多人看不懂英文方法名,英文命名不能替代 PHPDoc
**Logic / Service / Model 的 `public` / `protected` / `private` 方法必须写 PHPDoc**,首行用**中文**说明业务动作或结果(很多人看不懂英文方法名,英文命名不能替代 PHPDoc
`protected` 方法若承载业务步骤(非纯 getter/setter同样必须写中文 PHPDoc
- `public`:一律必须(纯 getter/setter 除外,见下文)
- `protected` / `private`Logic 与 Service 中凡承载业务步骤的方法必须写Model 的 `private` 若仅为薄封装且无语义增量可省略,但 `find*` / `mark*` / 访问 DB/Redis 的 `private` 仍必须写。
最小合格格式:
@@ -185,10 +186,11 @@ public static function findActiveBySessionId(string $sessionId): ?self
### 唯一豁免:纯 getter / setter
以下方法可省略 PHPDoc
以下方法(含 `private`可省略 PHPDoc
- 无业务分支、无事务、无外部调用、无查库写库的 `getXxx()` / `setXxx()`。
- 只读或只写入**单个**属性或 DTO 字段。
- 仅做字符串/ key 拼接、无业务判断的极简 accessor如 `return $uid . '_' . $roundId`)。
```php
// 可豁免
@@ -245,7 +247,7 @@ public static function findActiveBySessionId(string $sessionId): ?self
### 废话与行内注释
- **禁止废话 PHPDoc**:无信息增量、纯英文复述签名 → 不合格。
- **除纯 getter/setter 外一律必须**:不得以「方法名够清晰」为由省略 PHPDoc。
- **除纯 getter/setter 外一律必须**`public` / `protected` / `private` 均不得以「方法名够清晰」或「仅内部调用」为由省略 PHPDoc。
- **行内注释**解释为什么,不解释做什么;方法 PHPDoc 首行仍要写「做什么」(中文)。复杂业务、幂等、分布式锁、状态流转、第三方兼容、历史兼容逻辑须在行内注释说明原因。
---
@@ -295,7 +297,7 @@ public static function findActiveBySessionId(string $sessionId): ?self
- 是否泄露敏感信息?
- 返回数据是否明确?
- 是否直接返回 Model
- Logic / Service / Model 的 `public` 方法是否均有**中文** PHPDoc 首行(纯 getter/setter 除外)?
- Logic / Service / Model 的 `public` / `protected` / `private` 方法是否均有**中文** PHPDoc 首行(纯 getter/setter 除外)?
- 是否存在只有 `@param`/`@return` 类型、无中文业务说明的 PHPDoc
- 返回 array / 复杂数组参数 / 抛异常的方法,是否在通用 PHPDoc 之上补全了 `array{...}` 结构与 `@throws`
- 是否存在「仅用于日志」的多余参数?

View File

@@ -2,46 +2,46 @@
"version": 1,
"skills": {
"babysit": {
"lastSyncedAt": 1780046351693
"lastSyncedAt": 1780048673498
},
"canvas": {
"lastSyncedAt": 1780046351693
"lastSyncedAt": 1780048673498
},
"create-hook": {
"lastSyncedAt": 1780046351693
"lastSyncedAt": 1780048673498
},
"create-rule": {
"lastSyncedAt": 1780046351693
"lastSyncedAt": 1780048673498
},
"create-skill": {
"lastSyncedAt": 1780046351693
"lastSyncedAt": 1780048673498
},
"create-subagent": {
"lastSyncedAt": 1780046351693
"lastSyncedAt": 1780048673498
},
"sdk": {
"lastSyncedAt": 1780046351693
"lastSyncedAt": 1780048673498
},
"migrate-to-skills": {
"lastSyncedAt": 1780046351693
"lastSyncedAt": 1780048673498
},
"shell": {
"lastSyncedAt": 1780046351693
"lastSyncedAt": 1780048673498
},
"split-to-prs": {
"lastSyncedAt": 1780046351693
"lastSyncedAt": 1780048673498
},
"statusline": {
"lastSyncedAt": 1780046351693
"lastSyncedAt": 1780048673498
},
"update-cli-config": {
"lastSyncedAt": 1780046351693
"lastSyncedAt": 1780048673498
},
"update-cursor-settings": {
"lastSyncedAt": 1780046351693
"lastSyncedAt": 1780048673498
},
"loop": {
"lastSyncedAt": 1780046351693
"lastSyncedAt": 1780048673498
}
},
"lastInventoryAt": 1780014510207