迭代计划准备

This commit is contained in:
2026-07-16 15:07:59 +08:00
parent 1a9db9328e
commit c4f430ccb3
22 changed files with 2969 additions and 1569 deletions

View File

@@ -1,5 +1,7 @@
# 需求05套餐分配生效条件ExpiryBase 覆盖)
> 状态:待评审
---
## 背景
@@ -12,12 +14,17 @@
## 快照链设计
```
ShopPackageAllocation.expiry_base_override分配时设置覆盖值
↓ 订单创建时读取取有效值override 优先,否则用套餐默认)
PackageUsage.expiry_base_snapshot快照进去
↓ 激活时读取
activation_service 读 usage.ExpiryBaseSnapshot不再直接读 pkg.ExpiryBase
```mermaid
flowchart TD
Package[套餐默认 ExpiryBase] --> Effective{分配记录是否覆盖?}
Allocation[ShopPackageAllocation.expiry_base_override] --> Effective
Effective -->|有覆盖| Override[使用分配覆盖值]
Effective -->|无覆盖| Default[使用套餐默认值]
Override --> Snapshot[订单创建时写入 PackageUsage.expiry_base_snapshot]
Default --> Snapshot
Snapshot --> Activation[套餐激活只读快照]
Legacy[旧数据快照为空] --> Fallback[兜底读取套餐默认值]
Fallback --> Activation
```
遗留数据兜底:`ExpiryBaseSnapshot` 为空(旧数据)时,回退读 `pkg.ExpiryBase`,行为不变。
@@ -30,16 +37,29 @@ activation_service 读 usage.ExpiryBaseSnapshot不再直接读 pkg.ExpiryBase
```sql
ALTER TABLE tb_shop_package_allocation
ADD COLUMN expiry_base_override VARCHAR(30) DEFAULT NULL
COMMENT '生效条件覆盖NULL=使用套餐默认值, from_activation=实名即生效, from_purchase=购买即生效)';
ADD COLUMN expiry_base_override VARCHAR(30);
COMMENT ON COLUMN tb_shop_package_allocation.expiry_base_override
IS '生效条件覆盖NULL=使用套餐默认值, from_activation=实名即生效, from_purchase=购买即生效)';
```
### 2. PackageUsage 新增快照字段
```sql
ALTER TABLE tb_package_usage
ADD COLUMN expiry_base_snapshot VARCHAR(30) NOT NULL DEFAULT ''
COMMENT '生效条件快照(创建时从分配记录取有效值写入,空字符串=旧数据兜底读套餐原值)';
ADD COLUMN expiry_base_snapshot VARCHAR(30) NOT NULL DEFAULT '',
ADD COLUMN calendar_type_snapshot VARCHAR(20) NOT NULL DEFAULT '',
ADD COLUMN duration_months_snapshot INT NOT NULL DEFAULT 0,
ADD COLUMN duration_days_snapshot INT NOT NULL DEFAULT 0;
COMMENT ON COLUMN tb_package_usage.expiry_base_snapshot
IS '生效条件快照(创建时从分配记录取有效值写入,空字符串=旧数据兜底读套餐原值)';
COMMENT ON COLUMN tb_package_usage.calendar_type_snapshot
IS '周期类型快照(空字符串=旧数据兜底读套餐原值)';
COMMENT ON COLUMN tb_package_usage.duration_months_snapshot
IS '月数快照0=旧数据兜底读套餐原值)';
COMMENT ON COLUMN tb_package_usage.duration_days_snapshot
IS '天数快照0=旧数据兜底读套餐原值)';
```
旧数据不回填,默认空字符串,激活时自动兜底。
@@ -61,6 +81,11 @@ ExpiryBaseOverride *string `gorm:"column:expiry_base_override;type:varchar(30);c
```go
// ExpiryBaseSnapshot 生效条件快照(创建订单时写入,空字符串=旧数据兜底读套餐原值)
ExpiryBaseSnapshot string `gorm:"column:expiry_base_snapshot;type:varchar(30);not null;default:'';comment:生效条件快照 创建时从分配记录取有效值" json:"expiry_base_snapshot"`
// 以下三个字段和 ExpiryBaseSnapshot 一起固化,供激活和排队最终到期时间计算使用。
CalendarTypeSnapshot string `gorm:"column:calendar_type_snapshot;type:varchar(20);not null;default:'';comment:套餐周期类型快照" json:"calendar_type_snapshot"`
DurationMonthsSnapshot int `gorm:"column:duration_months_snapshot;not null;default:0;comment:套餐月数快照" json:"duration_months_snapshot"`
DurationDaysSnapshot int `gorm:"column:duration_days_snapshot;not null;default:0;comment:套餐天数快照" json:"duration_days_snapshot"`
```
---
@@ -77,26 +102,38 @@ expiryBase := pkg.ExpiryBase
if allocation.ExpiryBaseOverride != nil && *allocation.ExpiryBaseOverride != "" {
expiryBase = *allocation.ExpiryBaseOverride
}
// 创建 PackageUsage 时写入快照
// 创建 PackageUsage 时一次性写入计时快照
usage.ExpiryBaseSnapshot = expiryBase
usage.CalendarTypeSnapshot = pkg.CalendarType
usage.DurationMonthsSnapshot = pkg.DurationMonths
usage.DurationDaysSnapshot = pkg.DurationDays
```
### 2. 激活时读快照(`internal/service/package/activation_service.go`
```go
// 修改前:直接读套餐原值
expiryBase := pkg.ExpiryBase
// 修改后:优先读快照,旧数据兜底
expiryBase := pkg.ExpiryBase // 兜底(旧数据 ExpiryBaseSnapshot 为空)
if usage.ExpiryBaseSnapshot != "" {
expiryBase = usage.ExpiryBaseSnapshot
// 新订单只读购买快照;旧记录兼容回退套餐当前值。
expiryBase := usage.ExpiryBaseSnapshot
if expiryBase == "" {
expiryBase = pkg.ExpiryBase
}
calendarType := usage.CalendarTypeSnapshot
if calendarType == "" {
calendarType = pkg.CalendarType
}
durationMonths := usage.DurationMonthsSnapshot
if durationMonths == 0 {
durationMonths = pkg.DurationMonths
}
durationDays := usage.DurationDaysSnapshot
if durationDays == 0 {
durationDays = pkg.DurationDays
}
```
同文件另一处(`activation_service.go:556`)同理修改
同文件所有激活和排队接续位置都使用同一快照解析函数,禁止某一处重新读取可修改的 `Package` 字段
`internal/service/order/service.go:2290` 中后台囤货路径的 `ExpiryBase` 判断也需同样改为读快照(兜底逻辑一致)
`internal/service/order/service.go` 中后台囤货路径的 `ExpiryBase` 判断也使用已创建的使用记录快照需求06的“预计最后到期时间”同样只读这组快照保证购买后套餐配置变更不会改写历史预测
---
@@ -105,7 +142,7 @@ if usage.ExpiryBaseSnapshot != "" {
### 1. 分配套餐接口(新增参数)
```
POST /admin/shop-package-allocations
POST /api/admin/shop-package-allocations
```
请求 DTO 新增字段:
@@ -117,7 +154,7 @@ ExpiryBaseOverride *string `json:"expiry_base_override" validate:"omitempty,oneo
### 2. 修改已分配套餐的生效条件(新接口)
```
PATCH /admin/shop-package-allocations/{id}/expiry-base
PATCH /api/admin/shop-package-allocations/{id}/expiry-base
```
请求 DTO
@@ -155,4 +192,4 @@ type UpdateAllocationExpiryBaseRequest struct {
| `from_activation` | 实名即生效(已覆盖) |
| `from_purchase` | 购买即生效(已覆盖) |
操作列增加"修改生效条件"按钮,调用 `PATCH /admin/shop-package-allocations/{id}/expiry-base`
操作列增加"修改生效条件"按钮,调用 `PATCH /api/admin/shop-package-allocations/{id}/expiry-base`