19 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 02-perm-fin-realname-poll | 02 | execute | 1 |
|
true |
|
|
Purpose: 当前 enable_realname_activation 字段混淆了"是否等实名"和"计时起点"两个独立语义,架构重构后逻辑清晰分离,生产套餐全部默认 from_activation(运营后续按需手动调整)。 Output: DB 迁移 + Model + Store + Service + DTO 全链路变更,原子完成,不可拆分。
<execution_context> @$HOME/.config/opencode/get-shit-done/workflows/execute-plan.md @$HOME/.config/opencode/get-shit-done/templates/summary.md </execution_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 行)
当前 Package Model(内含 EnableRealnameActivation,需替换):
// 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 后需删除整个特殊处理块):
// 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):
// 第 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):
// 任务 8.9: 后台囤货场景
if pkg.EnableRealnameActivation {
// 需要实名后才能激活
status = constants.PackageUsageStatusPending
pendingRealnameActivation = true
}
改写为三维决策(见 action 中详细说明)。
当前 client_order/service.go 实名检查(第 115 行,REALNAME-03):
if packagesNeedRealname(validationResult.Packages) && assetInfo.RealNameStatus != constants.RealNameStatusVerified {
return nil, errors.New(errors.CodeNeedRealname)
}
改为:
if assetInfo.CardCategory == "normal" && assetInfo.RealNameStatus != constants.RealNameStatusVerified {
return nil, errors.New(errors.CodeNeedRealname)
}
然后删除 packagesNeedRealname 函数(第 619-626 行)。
当前 ActivateByRealname 激活时间固定用 now(第 113 行附近,REALNAME-04):
activatedAt := now
expiresAt := CalculateExpiryTime(pkg.CalendarType, activatedAt, pkg.DurationMonths, pkg.DurationDays)
改为:
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)
migrations/000089_realname_expiry_base.up.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:
-- 回滚:恢复旧字段,移除新字段
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-立即激活';
执行迁移并验证:
make migrate-up
make migrate-version
步骤 2:更新 Model(internal/model/package.go)
删除 EnableRealnameActivation 字段:
EnableRealnameActivation bool `gorm:"column:enable_realname_activation;type:boolean;..."`
新增 ExpiryBase 字段(放在原来的位置附近):
// 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 的特殊处理块:
// 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,无零值问题):
return s.db.WithContext(ctx).Create(pkg).Error
同时删除其上方的注释说明(第 22-23 行注释块)。
完成后 commit:fix(realname): DB 迁移 + Model + Store 清理 REALNAME-01
make migrate-version && go build ./... 2>&1 | head -30
- make migrate-version 显示版本 89,dirty=false
- tb_package 不再有 enable_realname_activation,已有 expiry_base 字段
- package.go Package 结构体有 ExpiryBase 字段,无 EnableRealnameActivation 字段
- package_store.go Create 函数已是直接一步 Create
- go build ./... 无编译错误(此时 service 层还有旧引用,编译会报错,先记录,Task 2 修复)
替换三处 EnableRealnameActivation 为 ExpiryBase:
- 第 18 行(CreatePackageRequest):
// 删除: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"`
-
第 35 行(UpdatePackageRequest):同上写法
-
第 98 行(PackageResponse):
// 删除: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-02(per D-11):internal/service/order/service.go:activateMainPackage
找到第 1873 行 if pkg.EnableRealnameActivation 块,替换为三维决策逻辑:
// 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-03(per D-12):internal/service/client_order/service.go
第 115 行,替换实名检查逻辑:
// 删除:
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-04(per D-13):internal/service/package/activation_service.go:ActivateByRealname
找到循环内 activatedAt := now 的赋值(约第 113 行),替换为:
// 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
最终检查所有旧引用清零:
grep -rn "EnableRealnameActivation\|enable_realname_activation\|packagesNeedRealname" \
internal/ --include="*.go"
# 预期:零结果(DTO、Model、Service 全部清理完成)
# 编译验证
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 标签的字段定义
<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 个独立 commit(REALNAME-01、05、02、03、04) </success_criteria>