依据《代码分析结果.md》P3 章节,逐条修复 20 项细节问题:
正确性修复:
- P3-1 锁定剩余分钟改用 math.Ceil,避免「0.9 分钟」显示为「0 分钟」
- P3-12 移除 CheckAndUpgradeLevel 中恒真的 hasUpdate 标志
- P3-14 移除 CheckTrialExpiry 中恒真表达式(count > 0 || true)调试残留,改用 RowsAffected
- P3-2 成员不存在统一返回 NewNotFoundError(404),与其它方法语义一致
安全加固:
- P3-6 X-Request-Id 加格式校验 ^[A-Za-z0-9-]{1,64}$,防日志注入
- P3-7 新增公共 EscapeLikePattern,转义 admin LIKE 搜索的 %/_ 通配符
- P3-8 Encrypt 输出加 v1: 版本前缀,Decrypt 兼容无前缀历史密文
- P3-9 turnstile 响应体读取加 64KB 上限,防内存耗尽
- P3-11 backup codes 熵由 48-bit 提升到 64-bit
- P3-15 recordAudit 对 multipart/二进制体跳过 Unmarshal 与原文落库
- P3-17 错误日志按 Content-Type 跳过 multipart/二进制体读取
- P3-16 HandleRealtime 加 panic recover 与连接生命周期日志
可维护性/一致性:
- P3-3 oauth.go 时间格式统一为 time.RFC3339
- P3-5 MsgSuccess 统一为 ok,response.go 改用常量引用
- P3-4 错误码分段策略加注释,说明空洞与跳跃的成因
- P3-13 ModelMultiplier 占位语义加注释(计费公式设计位 + 快照列)
- P3-19 抽取 registerCronJobs,主启动流程清爽
重构:
- P3-18 删除死代码 BuildBillingSummary/SummaryParams(仅测试引用),保留共用 helper
- P3-20 抽取 relay.InvalidateMemberModelScopeCache 单一失效入口,
消除三处 member_model 前缀字面量重复(原为有意跨包失效,原建议改名会破坏)
经核实保留 + 文档化:
- P3-10 TOTP 暂保留 SHA-1:totp.Validate 硬编码 SHA-1 且存储不保存算法元信息,
直接切 SHA-256 会导致校验恒失败,需配合 schema 改动,超出 P3 范围
116 lines
3.3 KiB
Go
116 lines
3.3 KiB
Go
package task
|
|
|
|
import (
|
|
"context"
|
|
"github.com/qianfree/team-api/internal/dao"
|
|
do "github.com/qianfree/team-api/internal/model/do"
|
|
"time"
|
|
|
|
"github.com/gogf/gf/v2/frame/g"
|
|
"github.com/gogf/gf/v2/os/gtime"
|
|
|
|
"github.com/qianfree/team-api/internal/logic/tenant"
|
|
)
|
|
|
|
// LifecycleCron 生命周期定时扫描总入口
|
|
func LifecycleCron(ctx context.Context) {
|
|
g.Log().Info(ctx, "[Lifecycle] starting cron scan...")
|
|
CheckTrialExpiry(ctx)
|
|
CheckGracePeriodExpiry(ctx)
|
|
CheckFrozenExpiry(ctx)
|
|
CheckClosingCooldown(ctx)
|
|
g.Log().Info(ctx, "[Lifecycle] cron scan completed")
|
|
}
|
|
|
|
// CheckTrialExpiry 扫描试用到期
|
|
func CheckTrialExpiry(ctx context.Context) {
|
|
now := time.Now()
|
|
result, err := dao.TntTenants.Ctx(ctx).
|
|
Where("status", "trial").
|
|
Where("trial_ends_at IS NOT NULL").
|
|
Where("trial_ends_at < ?", now).
|
|
Data(do.TntTenants{
|
|
Status: "frozen",
|
|
FrozenAt: gtime.NewFromTime(now),
|
|
DataRemovalAt: gtime.NewFromTime(now.Add(30 * 24 * time.Hour)),
|
|
}).Update()
|
|
if err != nil {
|
|
g.Log().Errorf(ctx, "[Lifecycle] CheckTrialExpiry: %v", err)
|
|
return
|
|
}
|
|
if rows, _ := result.RowsAffected(); rows > 0 {
|
|
g.Log().Infof(ctx, "[Lifecycle] CheckTrialExpiry: processed %d trial expiries", rows)
|
|
}
|
|
}
|
|
|
|
// CheckGracePeriodExpiry 扫描宽限期到期
|
|
func CheckGracePeriodExpiry(ctx context.Context) {
|
|
now := time.Now()
|
|
_, err := dao.TntTenants.Ctx(ctx).
|
|
Where("status", "past_due").
|
|
Where("grace_period_ends_at IS NOT NULL").
|
|
Where("grace_period_ends_at < ?", now).
|
|
Data(do.TntTenants{
|
|
Status: "frozen",
|
|
FrozenAt: gtime.NewFromTime(now),
|
|
DataRemovalAt: gtime.NewFromTime(now.Add(30 * 24 * time.Hour)),
|
|
}).Update()
|
|
if err != nil {
|
|
g.Log().Errorf(ctx, "[Lifecycle] CheckGracePeriodExpiry: %v", err)
|
|
return
|
|
}
|
|
g.Log().Info(ctx, "[Lifecycle] CheckGracePeriodExpiry: processed")
|
|
}
|
|
|
|
// CheckFrozenExpiry 扫描冻结到期(30 天后终止)
|
|
func CheckFrozenExpiry(ctx context.Context) {
|
|
now := time.Now()
|
|
_, err := dao.TntTenants.Ctx(ctx).
|
|
Where("status", "frozen").
|
|
Where("data_removal_at IS NOT NULL").
|
|
Where("data_removal_at < ?", now).
|
|
Data(do.TntTenants{
|
|
Status: "terminated",
|
|
}).Update()
|
|
if err != nil {
|
|
g.Log().Errorf(ctx, "[Lifecycle] CheckFrozenExpiry: %v", err)
|
|
return
|
|
}
|
|
g.Log().Info(ctx, "[Lifecycle] CheckFrozenExpiry: processed")
|
|
}
|
|
|
|
// CheckClosingCooldown 扫描冷静期到期(7 天后关闭)
|
|
func CheckClosingCooldown(ctx context.Context) {
|
|
now := time.Now()
|
|
_, err := dao.TntTenants.Ctx(ctx).
|
|
Where("status", "closing").
|
|
Where("closing_requested_at IS NOT NULL").
|
|
Where("closing_requested_at < ?", now.Add(-7*24*time.Hour)).
|
|
Data(do.TntTenants{
|
|
Status: "closed",
|
|
}).Update()
|
|
if err != nil {
|
|
g.Log().Errorf(ctx, "[Lifecycle] CheckClosingCooldown: %v", err)
|
|
return
|
|
}
|
|
g.Log().Info(ctx, "[Lifecycle] CheckClosingCooldown: processed")
|
|
}
|
|
|
|
// EnsureTenantHasWallet 确保租户有钱包(用于解冻时检查)
|
|
func EnsureTenantActive(ctx context.Context, tenantID int64) {
|
|
// Check if tenant has an active subscription
|
|
var activePlan *struct {
|
|
ID int64 `json:"id"`
|
|
}
|
|
err := dao.PlnTenantPlans.Ctx(ctx).
|
|
Where("tenant_id", tenantID).
|
|
Where("status", "active").
|
|
Where("end_at > ?", time.Now()).
|
|
Limit(1).
|
|
Scan(&activePlan)
|
|
if err == nil && activePlan != nil {
|
|
// Has active plan, can unfreeze
|
|
tenant.TransitionTenantStatus(ctx, tenantID, "active")
|
|
}
|
|
}
|