Files
junhong_cmp_fiber/.planning/phases/02-perm-fin-realname-poll/02-02-PLAN.md

445 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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>