package mysql import ( "context" "strings" "time" ) const ( // StatTZUTC 是历史默认口径,保留它可以让旧接口、旧报表和跨服务 UTC 统计规则继续稳定工作。 StatTZUTC = "UTC" // StatTZAsiaShanghai 是 Databi 需要的中国自然日口径;中国当前无夏令时,固定 +08:00 能避免容器缺 tzdata 时切日失败。 StatTZAsiaShanghai = "Asia/Shanghai" ) var asiaShanghaiLocation = time.FixedZone(StatTZAsiaShanghai, 8*60*60) type statDayScope struct { tz string day string } type statTZScopeContextKey struct{} // WithStatTZScope 限制当前聚合只写一个 stat_tz;实时消费不使用它,回灌任务用它避免重放历史事件时污染 UTC 口径。 func WithStatTZScope(ctx context.Context, statTZ string) context.Context { return context.WithValue(ctx, statTZScopeContextKey{}, normalizeStatTZ(statTZ)) } func normalizeStatTZ(value string) string { switch strings.TrimSpace(value) { case StatTZAsiaShanghai: return StatTZAsiaShanghai case StatTZUTC, "": return StatTZUTC default: return StatTZUTC } } func statLocation(statTZ string) *time.Location { if normalizeStatTZ(statTZ) == StatTZAsiaShanghai { return asiaShanghaiLocation } return time.UTC } func statDayIn(ms int64, statTZ string) string { return time.UnixMilli(ms).In(statLocation(statTZ)).Format("2006-01-02") } func statDayScopes(ms int64) []statDayScope { // 同一条业务事实必须一次事务内写出两份读模型:UTC 行维持历史聚合,Asia/Shanghai 行给中国自然日大屏使用。 return []statDayScope{ {tz: StatTZUTC, day: statDayIn(ms, StatTZUTC)}, {tz: StatTZAsiaShanghai, day: statDayIn(ms, StatTZAsiaShanghai)}, } } func statDayScopesFromContext(ctx context.Context, ms int64) []statDayScope { if ctx != nil { if statTZ, ok := ctx.Value(statTZScopeContextKey{}).(string); ok { statTZ = normalizeStatTZ(statTZ) return []statDayScope{{tz: statTZ, day: statDayIn(ms, statTZ)}} } } return statDayScopes(ms) }