docs(02-perm-fin-realname-poll): 创建 Phase 2 执行计划(2 个 Plan)

This commit is contained in:
2026-03-28 10:29:50 +08:00
parent 84aca2fd63
commit 62eb43a1cf
3 changed files with 884 additions and 2 deletions

View File

@@ -0,0 +1,444 @@
---
phase: 02-perm-fin-realname-poll
plan: 02
type: execute
wave: 1
depends_on: []
files_modified:
- migrations/000089_realname_expiry_base.up.sql
- migrations/000089_realname_expiry_base.down.sql
- internal/model/package.go
- internal/model/dto/package_dto.go
- internal/store/postgres/package_store.go
- internal/service/order/service.go
- internal/service/client_order/service.go
- internal/service/package/activation_service.go
autonomous: true
requirements:
- REALNAME-01
- REALNAME-02
- REALNAME-03
- REALNAME-04
- REALNAME-05
must_haves:
truths:
- "数据库 tb_package 表不再有 enable_realname_activation 字段,已有 expiry_base 字段默认 from_activation"
- "行业卡card_category=industry套餐永远直接激活无视 expiry_base"
- "C 端购买普通卡套餐前检查实名状态card_category=normal删除 packagesNeedRealname 函数"
- "后台囤货 expiry_base=from_purchase 时直接激活from_activation 时等实名"
- "ActivateByRealname 按 ExpiryBase 选择计时基准from_purchase 用 CreatedAtfrom_activation 用当前时刻)"
- "套餐管理 API 的 DTO 不再有 enable_realname_activation改为 expiry_base 枚举字段"
- "全仓库搜索 EnableRealnameActivation 零引用"
artifacts:
- path: "migrations/000089_realname_expiry_base.up.sql"
provides: "REALNAME-01 DB 迁移DROP enable_realname_activation + ADD expiry_base"
- path: "migrations/000089_realname_expiry_base.down.sql"
provides: "REALNAME-01 回滚迁移"
- path: "internal/model/package.go"
provides: "REALNAME-01 Model 同步:删旧字段,加 ExpiryBase"
- path: "internal/model/dto/package_dto.go"
provides: "REALNAME-05 DTO 更新:移除 EnableRealnameActivation新增 ExpiryBase"
- path: "internal/store/postgres/package_store.go"
provides: "REALNAME-01 删除两步写入特殊处理"
- path: "internal/service/order/service.go"
provides: "REALNAME-02 activateMainPackage 三维决策重写"
- path: "internal/service/client_order/service.go"
provides: "REALNAME-03 实名检查改按卡类型,删 packagesNeedRealname"
- path: "internal/service/package/activation_service.go"
provides: "REALNAME-04 ActivateByRealname 按 ExpiryBase 选择激活基准"
key_links:
- from: "internal/service/order/service.go:activateMainPackage"
to: "model.Package.ExpiryBase"
via: "pkg.ExpiryBase == from_activation"
pattern: "ExpiryBase.*from_activation"
- from: "internal/service/client_order/service.go"
to: "assetInfo.CardCategory"
via: "card_category == normal"
pattern: "CardCategory.*normal"
- from: "internal/service/package/activation_service.go:ActivateByRealname"
to: "packageUsage.CreatedAt"
via: "pkg.ExpiryBase == from_purchase"
pattern: "ExpiryBase.*from_purchase.*CreatedAt"
---
<objective>
原子性重构实名激活架构REALNAME-01~05将 enable_realname_activation 布尔字段替换为 expiry_base 枚举,重构激活决策为三维判断(卡类型+购买路径+expiry_base修复 C 端实名检查按卡类型判断。
Purpose: 当前 enable_realname_activation 字段混淆了"是否等实名"和"计时起点"两个独立语义,架构重构后逻辑清晰分离,生产套餐全部默认 from_activation运营后续按需手动调整
Output: DB 迁移 + Model + Store + Service + DTO 全链路变更,原子完成,不可拆分。
</objective>
<execution_context>
@$HOME/.config/opencode/get-shit-done/workflows/execute-plan.md
@$HOME/.config/opencode/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/02-perm-fin-realname-poll/02-CONTEXT.md
# 修复规格(核心参考)
@.sisyphus/plans/修正业务-完整方案.md方案 G 第 1492-1611 行)
<interfaces>
<!-- 本 plan 执行时所需的关键接口,直接使用,无需探索 -->
**当前 Package Model内含 EnableRealnameActivation需替换**
```go
// internal/model/package.go
EnableRealnameActivation bool `gorm:"column:enable_realname_activation;type:boolean;default:true;comment:是否启用实名激活"`
// 改为:
ExpiryBase string `gorm:"column:expiry_base;type:varchar(30);not null;default:'from_activation';comment:到期时间基准 from_activation-实名激活时起算 from_purchase-购买时起算"`
```
**当前 package_store.go 两步写入REALNAME-01 后需删除整个特殊处理块)**
```go
// internal/store/postgres/package_store.go:23-28
// GORM 对零值字段有特殊处理,先创建然后立即更新 enable_realname_activation 字段确保正确设置
if err := s.db.WithContext(ctx).Omit("enable_realname_activation").Create(pkg).Error; err != nil {
return err
}
// 明确更新 enable_realname_activation 字段(包括零值 false
return s.db.WithContext(ctx).Model(pkg).Update("enable_realname_activation", pkg.EnableRealnameActivation).Error
```
删除后改为直接 `s.db.WithContext(ctx).Create(pkg).Error`ExpiryBase 是 string无零值布尔问题
**当前 package_dto.go EnableRealnameActivation 三处REALNAME-05**
```go
// 第 18 行CreatePackageRequest
EnableRealnameActivation *bool `json:"enable_realname_activation" description:"是否启用实名激活"`
// 第 35 行UpdatePackageRequest
EnableRealnameActivation *bool `json:"enable_realname_activation" description:"是否启用实名激活"`
// 第 98 行PackageResponse
EnableRealnameActivation bool `json:"enable_realname_activation" description:"是否启用实名激活"`
// 全部改为 ExpiryBase
ExpiryBase string `json:"expiry_base" validate:"omitempty,oneof=from_activation from_purchase" description:"到期时间基准 (from_activation:实名激活时起算, from_purchase:购买时起算)"`
```
**当前 activateMainPackage 激活决策(第 1873 行REALNAME-02**
```go
// 任务 8.9: 后台囤货场景
if pkg.EnableRealnameActivation {
// 需要实名后才能激活
status = constants.PackageUsageStatusPending
pendingRealnameActivation = true
}
```
改写为三维决策(见 action 中详细说明)。
**当前 client_order/service.go 实名检查(第 115 行REALNAME-03**
```go
if packagesNeedRealname(validationResult.Packages) && assetInfo.RealNameStatus != constants.RealNameStatusVerified {
return nil, errors.New(errors.CodeNeedRealname)
}
```
改为:
```go
if assetInfo.CardCategory == "normal" && assetInfo.RealNameStatus != constants.RealNameStatusVerified {
return nil, errors.New(errors.CodeNeedRealname)
}
```
然后删除 `packagesNeedRealname` 函数(第 619-626 行)。
**当前 ActivateByRealname 激活时间固定用 now第 113 行附近REALNAME-04**
```go
activatedAt := now
expiresAt := CalculateExpiryTime(pkg.CalendarType, activatedAt, pkg.DurationMonths, pkg.DurationDays)
```
改为:
```go
var activatedAt time.Time
if pkg.ExpiryBase == "from_purchase" {
activatedAt = usage.CreatedAt // 用记录创建时间(购买时刻)
} else {
activatedAt = now // 实名被触发的当前时刻
}
```
**迁移命名参考(现有最新为 000088**
新建 `migrations/000089_realname_expiry_base.up.sql``000089_realname_expiry_base.down.sql`
**card_category 访问路径(已确认)**
- `assetInfo.CardCategory` 来自 `dto.AssetResolveResponse.CardCategory`(第 49 行),值由 `asset/service.go:175` 填充
- `activateMainPackage` 中通过 order 可以获取 `carrierType`/`carrierID`card_category 需要从 `IotCard` 查询
- 建议在 `activateMainPackage` 函数中通过 `tx.First(&card, carrierID)` 获取卡信息,再读 `card.CardCategory`
**BuyerTypePersonal 常量**
`model.BuyerTypePersonal` 已存在(参考 service.go 第 450 行 `orderBuyerType = model.BuyerTypePersonal`
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: DB 迁移 + Model + Store 清理REALNAME-01</name>
<files>
migrations/000089_realname_expiry_base.up.sql,
migrations/000089_realname_expiry_base.down.sql,
internal/model/package.go,
internal/store/postgres/package_store.go
</files>
<action>
**步骤 1创建迁移文件per D-10**
`migrations/000089_realname_expiry_base.up.sql`
```sql
-- REALNAME-01: 实名激活架构重构
-- 移除旧的布尔字段 enable_realname_activation
-- 新增 expiry_base 枚举字段控制套餐计时起点
-- 注意:不做旧字段到新字段的数据迁移,现有记录全部默认 from_activation
-- 移除旧字段
ALTER TABLE tb_package DROP COLUMN IF EXISTS enable_realname_activation;
-- 新增到期时间基准字段
ALTER TABLE tb_package
ADD COLUMN IF NOT EXISTS expiry_base VARCHAR(30) NOT NULL DEFAULT 'from_activation';
COMMENT ON COLUMN tb_package.expiry_base IS '到期时间基准 from_activation-实名激活时起算 from_purchase-购买时起算';
```
`migrations/000089_realname_expiry_base.down.sql`
```sql
-- 回滚:恢复旧字段,移除新字段
ALTER TABLE tb_package DROP COLUMN IF EXISTS expiry_base;
ALTER TABLE tb_package
ADD COLUMN IF NOT EXISTS enable_realname_activation BOOLEAN NOT NULL DEFAULT TRUE;
COMMENT ON COLUMN tb_package.enable_realname_activation IS '是否启用实名激活 true-需实名后激活 false-立即激活';
```
执行迁移并验证:
```bash
make migrate-up
make migrate-version
```
**步骤 2更新 Model`internal/model/package.go`**
删除 `EnableRealnameActivation` 字段:
```go
EnableRealnameActivation bool `gorm:"column:enable_realname_activation;type:boolean;..."`
```
新增 `ExpiryBase` 字段(放在原来的位置附近):
```go
// ExpiryBase 到期时间基准from_activation-实名激活时起算from_purchase-购买时起算
ExpiryBase string `gorm:"column:expiry_base;type:varchar(30);not null;default:'from_activation';comment:到期时间基准 from_activation-实名激活时起算 from_purchase-购买时起算" json:"expiry_base"`
```
**步骤 3清理 package_store.go 两步写入**
找到 `internal/store/postgres/package_store.go:23-28` 的特殊处理块:
```go
// GORM 对零值字段有特殊处理,先创建然后立即更新 enable_realname_activation 字段确保正确设置
if err := s.db.WithContext(ctx).Omit("enable_realname_activation").Create(pkg).Error; err != nil {
return err
}
// 明确更新 enable_realname_activation 字段(包括零值 false
return s.db.WithContext(ctx).Model(pkg).Update("enable_realname_activation", pkg.EnableRealnameActivation).Error
```
替换为直接创建ExpiryBase 是 string无零值问题
```go
return s.db.WithContext(ctx).Create(pkg).Error
```
同时删除其上方的注释说明(第 22-23 行注释块)。
完成后 commit`fix(realname): DB 迁移 + Model + Store 清理 REALNAME-01`
</action>
<verify>
<automated>make migrate-version && go build ./... 2>&1 | head -30</automated>
</verify>
<done>
- make migrate-version 显示版本 89dirty=false
- tb_package 不再有 enable_realname_activation已有 expiry_base 字段
- package.go Package 结构体有 ExpiryBase 字段,无 EnableRealnameActivation 字段
- package_store.go Create 函数已是直接一步 Create
- go build ./... 无编译错误(此时 service 层还有旧引用编译会报错先记录Task 2 修复)
</done>
</task>
<task type="auto">
<name>Task 2: Service + DTO 重构REALNAME-02/03/04/05</name>
<files>
internal/model/dto/package_dto.go,
internal/service/order/service.go,
internal/service/client_order/service.go,
internal/service/package/activation_service.go
</files>
<action>
**REALNAME-05per D-10**`internal/model/dto/package_dto.go`
替换三处 `EnableRealnameActivation``ExpiryBase`
1. 第 18 行CreatePackageRequest
```go
// 删除EnableRealnameActivation *bool `json:"enable_realname_activation" description:"是否启用实名激活 (true:需实名后激活, false:立即激活)"`
// 新增:
ExpiryBase string `json:"expiry_base" validate:"omitempty,oneof=from_activation from_purchase" description:"到期时间基准 (from_activation:实名激活时起算, from_purchase:购买时起算)" example:"from_activation"`
```
2. 第 35 行UpdatePackageRequest同上写法
3. 第 98 行PackageResponse
```go
// 删除EnableRealnameActivation bool `json:"enable_realname_activation" description:"是否启用实名激活 (true:需实名后激活, false:立即激活)"`
// 新增:
ExpiryBase string `json:"expiry_base" description:"到期时间基准 (from_activation:实名激活时起算, from_purchase:购买时起算)"`
```
完成后 commit`fix(realname): DTO 更新 expiry_base 替换 enable_realname_activation REALNAME-05`
**REALNAME-02per D-11**`internal/service/order/service.go:activateMainPackage`
找到第 1873 行 `if pkg.EnableRealnameActivation` 块,替换为三维决策逻辑:
```go
// REALNAME-02: 按卡类型 + 购买路径 + expiry_base 三维决策
// 需要在 else 分支(无生效中主套餐,立即激活)之后插入以下逻辑:
// 获取卡类型(行业卡永远直接激活,不等实名)
var cardCategory string
if carrierType == "iot_card" {
var card model.IotCard
if err := tx.Select("card_category").First(&card, carrierID).Error; err == nil {
cardCategory = card.CardCategory
}
}
// 判断是否 C 端购买C 端购买前已做实名检查,直接激活)
isCEndPurchase := order.BuyerType == model.BuyerTypePersonal
// 三维决策:覆盖 activateMainPackage 已有的 status 赋值
if cardCategory == "industry" {
// 行业卡:永远直接激活,不等实名
// status 已在 else 分支设为 Active保持不变
} else if isCEndPurchase {
// C 端购买:前置实名检查已完成,直接激活
// status 已在 else 分支设为 Active保持不变
} else {
// 后台囤货:按 expiry_base 决定
if pkg.ExpiryBase == "from_purchase" {
// 购买即激活立即开始计时status 保持 Active已设置
} else {
// from_activation默认等实名后激活
status = constants.PackageUsageStatusPending
pendingRealnameActivation = true
// 清空激活时间(排队状态不设置)
activatedAt = time.Time{}
expiresAt = time.Time{}
nextResetAt = nil
}
}
```
注意:上述三维决策逻辑用于替换原来第 1873-1876 行的 `if pkg.EnableRealnameActivation` 块。放置位置:在 `hasActiveMain` 分支结束之后、`usage := &model.PackageUsage` 创建之前(即原 8.9 任务注释区域)。
完成后 commit`fix(realname): activateMainPackage 三维决策重构 REALNAME-02`
**REALNAME-03per D-12**`internal/service/client_order/service.go`
第 115 行,替换实名检查逻辑:
```go
// 删除:
if packagesNeedRealname(validationResult.Packages) && assetInfo.RealNameStatus != constants.RealNameStatusVerified {
// 替换为:
// REALNAME-03: 按卡类型判断实名需求,普通卡需要实名,行业卡不需要
if assetInfo.CardCategory == "normal" && assetInfo.RealNameStatus != constants.RealNameStatusVerified {
```
然后删除 `packagesNeedRealname` 函数(第 619-626 行)。
检查 `validationResult.Packages` 是否仍被其他逻辑使用;若不再使用,保留 `validationResult` 结构但不必删除(其他字段如 `Packages` 本身可能仍用于后续订单创建)。
完成后 commit`fix(realname): C 端实名检查改为按卡类型判断 REALNAME-03`
**REALNAME-04per D-13**`internal/service/package/activation_service.go:ActivateByRealname`
找到循环内 `activatedAt := now` 的赋值(约第 113 行),替换为:
```go
// REALNAME-04: 按 ExpiryBase 选择激活计时基准
var activatedAt time.Time
if pkg.ExpiryBase == "from_purchase" {
// 购买时起算:用 PackageUsage 记录的创建时间
activatedAt = usage.CreatedAt
} else {
// 实名激活时起算from_activation 或默认):用当前时刻
activatedAt = now
}
```
注意:`usage` 变量在循环中已有(为 `*model.PackageUsage`),直接访问 `.CreatedAt` 即可。
完成后 commit`fix(realname): ActivateByRealname 按 ExpiryBase 选择计时基准 REALNAME-04`
最终检查所有旧引用清零:
```bash
grep -rn "EnableRealnameActivation\|enable_realname_activation\|packagesNeedRealname" \
internal/ --include="*.go"
# 预期零结果DTO、Model、Service 全部清理完成)
```
</action>
<verify>
<automated>go build ./... 2>&1 | head -30</automated>
</verify>
<done>
- package_dto.go 三处 EnableRealnameActivation 已替换为 ExpiryBasedescription 使用中文枚举说明
- order/service.go:activateMainPackage 已有三维决策逻辑industry/C端/后台囤货+expiry_base
- client_order/service.go 实名检查用 CardCategory==normalpackagesNeedRealname 函数已删除
- activation_service.go:ActivateByRealname 用 ExpiryBase 分支选择激活时间
- grep 搜索 EnableRealnameActivation/enable_realname_activation/packagesNeedRealname 零结果
- go build ./... 无编译错误
- 已产出 4 个独立 commitREALNAME-05、02、03、04
</done>
</task>
</tasks>
<verification>
REALNAME 重构完成后执行最终验收:
```bash
# 编译验证
go build ./...
# DB 迁移验证
make migrate-version
# 预期version=89, dirty=false
# 旧字段引用清零
grep -rn "EnableRealnameActivation\|enable_realname_activation\|packagesNeedRealname" \
internal/ --include="*.go"
# 预期:零结果
# 新字段引用确认
grep -rn "ExpiryBase\|expiry_base" internal/ --include="*.go" | grep -v "_test.go"
# 预期:至少在 package.go, package_dto.go, order/service.go,
# client_order/service.go, activation_service.go 中出现
# Model 验证
grep -n "ExpiryBase" internal/model/package.go
# 预期:有 gorm column:expiry_base 标签的字段定义
```
</verification>
<success_criteria>
- go build ./... 零错误
- make migrate-version 显示 version=89, dirty=false
- 全仓库 EnableRealnameActivation/enable_realname_activation/packagesNeedRealname 零引用
- package.go 有 ExpiryBase 字段gorm tag: expiry_base, varchar(30), default:from_activation
- package_store.go Create 函数为直接一步创建(无两步写入特殊处理)
- activateMainPackage 包含三维决策(行业卡/C端/后台囤货+expiry_base
- client_order 实名检查用 CardCategory=="normal"
- ActivateByRealname 按 ExpiryBase 分支选择 activatedAt 基准
- 已产出 5 个独立 commitREALNAME-01、05、02、03、04
</success_criteria>
<output>
完成后创建:`.planning/phases/02-perm-fin-realname-poll/02-02-SUMMARY.md`
</output>