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

19 KiB
Raw Blame History

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
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
true
REALNAME-01
REALNAME-02
REALNAME-03
REALNAME-04
REALNAME-05
truths artifacts key_links
数据库 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 零引用
path provides
migrations/000089_realname_expiry_base.up.sql REALNAME-01 DB 迁移DROP enable_realname_activation + ADD expiry_base
path provides
migrations/000089_realname_expiry_base.down.sql REALNAME-01 回滚迁移
path provides
internal/model/package.go REALNAME-01 Model 同步:删旧字段,加 ExpiryBase
path provides
internal/model/dto/package_dto.go REALNAME-05 DTO 更新:移除 EnableRealnameActivation新增 ExpiryBase
path provides
internal/store/postgres/package_store.go REALNAME-01 删除两步写入特殊处理
path provides
internal/service/order/service.go REALNAME-02 activateMainPackage 三维决策重写
path provides
internal/service/client_order/service.go REALNAME-03 实名检查改按卡类型,删 packagesNeedRealname
path provides
internal/service/package/activation_service.go REALNAME-04 ActivateByRealname 按 ExpiryBase 选择激活基准
from to via pattern
internal/service/order/service.go:activateMainPackage model.Package.ExpiryBase pkg.ExpiryBase == from_activation ExpiryBase.*from_activation
from to via pattern
internal/service/client_order/service.go assetInfo.CardCategory card_category == normal CardCategory.*normal
from to via pattern
internal/service/package/activation_service.go:ActivateByRealname packageUsage.CreatedAt pkg.ExpiryBase == from_purchase ExpiryBase.*from_purchase.*CreatedAt
原子性重构实名激活架构REALNAME-01~05将 enable_realname_activation 布尔字段替换为 expiry_base 枚举,重构激活决策为三维判断(卡类型+购买路径+expiry_base修复 C 端实名检查按卡类型判断。

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).ErrorExpiryBase 是 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.sql000089_realname_expiry_base.down.sql

card_category 访问路径(已确认)

  • assetInfo.CardCategory 来自 dto.AssetResolveResponse.CardCategory(第 49 行),值由 asset/service.go:175 填充
  • activateMainPackage 中通过 order 可以获取 carrierType/carrierIDcard_category 需要从 IotCard 查询
  • 建议在 activateMainPackage 函数中通过 tx.First(&card, carrierID) 获取卡信息,再读 card.CardCategory

BuyerTypePersonal 常量 model.BuyerTypePersonal 已存在(参考 service.go 第 450 行 orderBuyerType = model.BuyerTypePersonal

Task 1: DB 迁移 + Model + Store 清理REALNAME-01 migrations/000089_realname_expiry_base.up.sql, migrations/000089_realname_expiry_base.down.sql, internal/model/package.go, internal/store/postgres/package_store.go **步骤 1创建迁移文件per D-10**

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更新 Modelinternal/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 行注释块)。

完成后 commitfix(realname): DB 迁移 + Model + Store 清理 REALNAME-01 make migrate-version && go build ./... 2>&1 | head -30 - 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 修复)

Task 2: Service + DTO 重构REALNAME-02/03/04/05 internal/model/dto/package_dto.go, internal/service/order/service.go, internal/service/client_order/service.go, internal/service/package/activation_service.go **REALNAME-05per D-10**`internal/model/dto/package_dto.go`

替换三处 EnableRealnameActivationExpiryBase

  1. 第 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"`
  1. 第 35 行UpdatePackageRequest同上写法

  2. 第 98 行PackageResponse

// 删除EnableRealnameActivation bool `json:"enable_realname_activation" description:"是否启用实名激活 (true:需实名后激活, false:立即激活)"`
// 新增:
ExpiryBase string `json:"expiry_base" description:"到期时间基准 (from_activation:实名激活时起算, from_purchase:购买时起算)"`

完成后 commitfix(realname): DTO 更新 expiry_base 替换 enable_realname_activation REALNAME-05

REALNAME-02per D-11internal/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 任务注释区域)。

完成后 commitfix(realname): activateMainPackage 三维决策重构 REALNAME-02

REALNAME-03per D-12internal/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 本身可能仍用于后续订单创建)。

完成后 commitfix(realname): C 端实名检查改为按卡类型判断 REALNAME-03

REALNAME-04per D-13internal/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 即可。

完成后 commitfix(realname): ActivateByRealname 按 ExpiryBase 选择计时基准 REALNAME-04

最终检查所有旧引用清零:

grep -rn "EnableRealnameActivation\|enable_realname_activation\|packagesNeedRealname" \
    internal/ --include="*.go"
# 预期零结果DTO、Model、Service 全部清理完成)
go build ./... 2>&1 | head -30 - 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 REALNAME 重构完成后执行最终验收:
# 编译验证
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 个独立 commitREALNAME-01、05、02、03、04 </success_criteria>
完成后创建:`.planning/phases/02-perm-fin-realname-poll/02-02-SUMMARY.md`