fix: 店铺分配覆盖为实名即生效的套餐纳入沉默期配置口径
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m27s

存量中套餐本身购买即生效、店铺分配覆盖为实名即生效的正式套餐,购买后同样等实名,
但原口径只看套餐本身:无法配置默认沉默期、不进未配置清单与打开前检查,
待实名记录无法回填导致总开关打不开,新购买也绕过沉默期。

- 未配置清单与打开前检查纳入存在实名即生效分配覆盖的套餐
- 购买即生效正式套餐允许选填默认沉默期,加油包仍禁止
- 分配覆盖购买即生效 → 实名即生效在套餐已配置默认沉默期时放行
- 同步 OpenSpec design D11、spec、tasks 3.4/3.5 与前端说明

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
邹进
2026-10-27 17:56:29 +09:00
parent 69ec98aa5f
commit af57de0d4d
7 changed files with 59 additions and 21 deletions

View File

@@ -299,8 +299,8 @@ func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdatePackageReq
}
if req.SilencePeriodDays != nil {
pkg.SilencePeriodDays = req.SilencePeriodDays
} else if !RequiresSilencePeriod(pkg) {
// 套餐不再需要沉默期(如改为加油包)时清空默认值,避免普通套餐残留沉默期配置。
} else if pkg.PackageType == constants.PackageTypeAddon {
// 改为加油包时清空默认值,避免加油包残留沉默期配置;购买即生效正式套餐可保留(分配覆盖为实名即生效时使用)。
pkg.SilencePeriodDays = nil
}
if req.SilenceExtensionEnabled != nil {

View File

@@ -50,17 +50,21 @@ func ExpiryBaseOverrideName(value *string) string {
// ValidateAllocationExpiryBaseOverride 校验分配覆盖不得改变套餐生效条件(add-package-silence-period §3.3)。
// 套餐服务与店铺分配(单个、批量、系列授权)写入路径共用:
// 覆盖把实名即生效套餐改成购买即生效会绕过沉默期;把购买即生效套餐改成实名即生效时该套餐没有默认沉默期,两者都阻断。
// 覆盖为空(跟随套餐默认)或与套餐一致时放行;加油包不进入沉默期,不受此约束。
// 覆盖把实名即生效套餐改成购买即生效会绕过沉默期,阻断;把购买即生效套餐改成实名即生效时,
// 套餐已配置默认沉默期才放行(design D11)。覆盖为空(跟随套餐默认)或与套餐一致时放行;加油包不进入沉默期,不受此约束。
func ValidateAllocationExpiryBaseOverride(pkg *model.Package, override *string) error {
if pkg == nil || override == nil || pkg.PackageType == constants.PackageTypeAddon {
if pkg == nil || override == nil || pkg.PackageType == constants.PackageTypeAddon || *override == pkg.ExpiryBase {
return nil
}
if *override != pkg.ExpiryBase {
return errors.New(errors.CodePackageExpiryBaseChangeForbidden,
"套餐「"+pkg.PackageName+"」不允许通过分配覆盖改变生效条件(实名即生效与购买即生效不能互相覆盖)")
if *override == constants.PackageExpiryBaseFromActivation && pkg.SilencePeriodDays != nil {
return nil
}
return nil
if *override == constants.PackageExpiryBaseFromActivation {
return errors.New(errors.CodePackageExpiryBaseChangeForbidden,
"套餐「"+pkg.PackageName+"」未配置默认沉默期,不允许通过分配覆盖改为实名即生效")
}
return errors.New(errors.CodePackageExpiryBaseChangeForbidden,
"套餐「"+pkg.PackageName+"」不允许通过分配覆盖从实名即生效改为购买即生效")
}
// ValidatePackageExpiryBaseChange 校验套餐自身生效条件变更(§3.3):实名即生效改为购买即生效阻断;
@@ -83,16 +87,23 @@ func RequiresSilencePeriod(pkg *model.Package) bool {
return pkg != nil && pkg.PackageType != constants.PackageTypeAddon && pkg.ExpiryBase == constants.PackageExpiryBaseFromActivation
}
// ValidatePackageSilenceConfig 校验套餐最终的沉默期配置:实名即生效正式套餐必填 1~90 天;其他套餐不得填写。
// ValidatePackageSilenceConfig 校验套餐最终的沉默期配置:实名即生效正式套餐必填 1~90 天;
// 购买即生效正式套餐可选填 1~90 天(只在店铺分配覆盖为实名即生效时使用,design D11);加油包不得填写。
func ValidatePackageSilenceConfig(pkg *model.Package) error {
if RequiresSilencePeriod(pkg) {
if pkg.SilencePeriodDays == nil || *pkg.SilencePeriodDays < constants.SilencePackageDaysMin || *pkg.SilencePeriodDays > constants.SilencePackageDaysMax {
if pkg == nil {
return nil
}
if pkg.SilencePeriodDays == nil {
if RequiresSilencePeriod(pkg) {
return errors.New(errors.CodeSilencePeriodOutOfRange, "实名即生效套餐必须配置默认沉默期(1~90 天)")
}
return nil
}
if pkg != nil && pkg.SilencePeriodDays != nil {
return errors.New(errors.CodeInvalidParam, "只有实名即生效的正式套餐可以配置沉默期")
if pkg.PackageType == constants.PackageTypeAddon {
return errors.New(errors.CodeInvalidParam, "加油包不能配置沉默期")
}
if *pkg.SilencePeriodDays < constants.SilencePackageDaysMin || *pkg.SilencePeriodDays > constants.SilencePackageDaysMax {
return errors.New(errors.CodeSilencePeriodOutOfRange, "默认沉默期须为 1~90 天")
}
return nil
}

View File

@@ -154,7 +154,10 @@ func (s *PackageStore) CountBySeriesID(ctx context.Context, seriesID uint) (int6
}
// SilenceUnconfiguredPackageCondition 返回"未配置默认沉默期的实名即生效正式套餐"的条件(总开关打开前检查与补齐清单共用)。
// 实名即生效包含套餐本身 from_activation,以及存在未删除的店铺分配覆盖为 from_activation 的套餐(design D11)。
func SilenceUnconfiguredPackageCondition(alias string) string {
return alias + ".package_type <> '" + constants.PackageTypeAddon + "' AND " + alias + ".expiry_base = '" +
constants.PackageExpiryBaseFromActivation + "' AND " + alias + ".silence_period_days IS NULL"
return alias + ".package_type <> '" + constants.PackageTypeAddon + "' AND " + alias + ".silence_period_days IS NULL AND (" +
alias + ".expiry_base = '" + constants.PackageExpiryBaseFromActivation + "' OR EXISTS (" +
"SELECT 1 FROM tb_shop_package_allocation AS silence_alloc WHERE silence_alloc.package_id = " + alias + ".id" +
" AND silence_alloc.deleted_at IS NULL AND silence_alloc.expiry_base_override = '" + constants.PackageExpiryBaseFromActivation + "'))"
}