Files
邹进 0f1ea5bc3e
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 10m17s
fix: 第1批 M9 套餐临期提醒按资产、扫码进入时触发
- 每日扫描不再给个人客户写临期通知,店铺通知不变
- H5 资产信息在当前资产剩余 0~15 天且当天首次进入时返回 expiry_reminder,并为该客户、该资产写一条临期通知(Redis 去重,失败不占键)
- 个人通知列表、未读数、全部已读中临期提醒只处理当前资产(按 ref_key)
- OpenSpec Change scope-expiry-reminder-to-current-asset:6/9,临期 fixture 端到端验证与每日扫描验证待测试部署后完成
2026-10-12 00:15:27 +09:00

99 lines
3.8 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package packageexpiry
import (
"context"
"time"
"github.com/redis/go-redis/v9"
"go.uber.org/zap"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
packageexpiryquery "github.com/break/junhong_cmp_fiber/internal/query/packageexpiry"
"github.com/break/junhong_cmp_fiber/pkg/constants"
)
// entryReminderWindowDays 临期提醒窗口:剩余 0~15 个上海自然日。
const entryReminderWindowDays = 15
var entryReminderLocation = time.FixedZone("Asia/Shanghai", 8*60*60)
// PersonalReminderPublisher 为单个个人客户幂等写一条临期通知。
type PersonalReminderPublisher interface {
PublishPersonal(ctx context.Context, candidate dto.ExpiringAssetItem, customerID uint) error
}
// EntryReminder 在 H5 进入资产时判定并发出套餐临期提醒,同一客户、同一资产、同一上海自然日至多一次。
type EntryReminder struct {
redis *redis.Client
publisher PersonalReminderPublisher
logger *zap.Logger
now func() time.Time
}
// NewEntryReminder 创建进入资产时的临期提醒用例。
func NewEntryReminder(redisClient *redis.Client, publisher PersonalReminderPublisher, logger *zap.Logger) *EntryReminder {
return &EntryReminder{redis: redisClient, publisher: publisher, logger: logger, now: time.Now}
}
// Remind 返回本次应展示的临期提醒;不在窗口、当天已提醒或通知写入失败时返回 nil,不让资产信息接口失败。
// 顺序:检查去重键 → 写通知 → SET NX → 抢到键才返回;Redis 不可用按未提醒处理,通知由事件号幂等。
func (r *EntryReminder) Remind(ctx context.Context, customerID uint, candidate dto.ExpiringAssetItem) *dto.AssetExpiryReminder {
if r == nil || r.publisher == nil || customerID == 0 || !inEntryReminderWindow(candidate.PackageExpiryEstimate) {
return nil
}
now := r.now().In(entryReminderLocation)
key := constants.RedisPackageExpiryEntryReminderKey(customerID, candidate.AssetType, candidate.Identifier, now.Format("20060102"))
redisOK := r.redis != nil
if redisOK {
exists, err := r.redis.Exists(ctx, key).Result()
if err != nil {
redisOK = false
r.warn("检查临期提醒去重键失败,按未提醒处理", err)
} else if exists > 0 {
return nil
}
}
if err := r.publisher.PublishPersonal(ctx, candidate, customerID); err != nil {
// 写通知失败不占键、不返回字段,下次进入重试。
r.warn("写入进入资产临期通知失败", err)
return nil
}
if redisOK {
ttl := endOfShanghaiDay(now).Sub(now) + constants.PackageExpiryEntryReminderTTLBuffer
acquired, err := r.redis.SetNX(ctx, key, "1", ttl).Result()
if err != nil {
r.warn("占用临期提醒去重键失败,按未提醒处理", err)
} else if !acquired {
return nil
}
}
days := *candidate.DaysUntilFinalExpiry
level, levelName := packageexpiryquery.ExpiryLevel(days)
return &dto.AssetExpiryReminder{
RemainingDays: days, ExpiresAt: *candidate.EstimatedFinalExpiresAt,
Level: level, LevelName: levelName,
}
}
// inEntryReminderWindow 判断最终到期可精确推算且剩余 0~15 个上海自然日。
func inEntryReminderWindow(estimate dto.PackageExpiryEstimate) bool {
if estimate.ExpiryEstimateStatus != constants.PackageExpiryEstimateStatusExact ||
estimate.DaysUntilFinalExpiry == nil || estimate.EstimatedFinalExpiresAt == nil {
return false
}
days := *estimate.DaysUntilFinalExpiry
return days >= 0 && days <= entryReminderWindowDays
}
// endOfShanghaiDay 返回上海时间当天结束时刻(次日零点)。
func endOfShanghaiDay(now time.Time) time.Time {
year, month, day := now.In(entryReminderLocation).Date()
return time.Date(year, month, day+1, 0, 0, 0, 0, entryReminderLocation)
}
func (r *EntryReminder) warn(message string, err error) {
if r.logger != nil {
r.logger.Warn(message, zap.Error(err))
}
}