fix anwser token and database issus
This commit is contained in:
@@ -66,29 +66,23 @@ type featureControls struct {
|
||||
*gorm.DB
|
||||
}
|
||||
|
||||
// FeatureControl 提问箱单个功能(提问 / 私聊 / 投票)的开关、定时与定量收集配置。
|
||||
// FeatureControl 单个功能(提问 / 私聊 / 投票)的开关、定时与定量收集配置。
|
||||
// Enabled 为手动总开关;OpenAt / CloseAt 为一次性定时(nil 表示不限该侧);
|
||||
// QuotaLimit / QuotaCount 为定量收集(0 表示不限制)。最终状态由 Evaluate 综合计算。
|
||||
//
|
||||
// 三类控制互不覆盖,最终可用状态由 Evaluate 综合计算:
|
||||
// - Enabled:手动总开关;
|
||||
// - OpenAt / CloseAt:定时开放与定时关闭(nil 表示不限制该侧);
|
||||
// - QuotaLimit / QuotaCount:定量收集,累计收集数达到上限后自动关闭(0 表示不限制)。
|
||||
//
|
||||
// 计数不做定时重置:一轮收集结束后由箱主在功能管理页手动「重置计数」开启下一轮,
|
||||
// 避免与定时开关叠加产生难以预期的自动恢复行为。
|
||||
// 计数不做定时重置:一轮结束后由箱主手动「重置计数」,避免与定时开关叠加产生
|
||||
// 难以预期的自动恢复行为。
|
||||
type FeatureControl struct {
|
||||
dbutil.Model
|
||||
UserID uint `gorm:"uniqueIndex:idx_feature_control_user_feature;not null" json:"-"`
|
||||
Feature string `gorm:"size:20;uniqueIndex:idx_feature_control_user_feature;not null" json:"feature"`
|
||||
// ⚠️ 不要给 Enabled 加 gorm:"default:true" 标签:
|
||||
// GORM 在 Create 时会把零值 false 替换为字段默认值(true),导致「取消勾选保存」后
|
||||
// 数据库里仍是 true、复选框自动恢复为勾选。去掉标签后 GORM 会原样写入显式设置的值。
|
||||
Enabled bool `json:"enabled"`
|
||||
// OpenAt / CloseAt:一次性定时开放 / 关闭(nil 表示不限制该侧)。
|
||||
// ⚠️ 不要给 Enabled 加 gorm:"default:true":GORM 在 Create 时会把零值 false 替换为
|
||||
// 默认值 true,导致「取消勾选保存」后库里仍是 true、复选框自动恢复勾选。
|
||||
Enabled bool `json:"enabled"`
|
||||
OpenAt *time.Time `json:"open_at"`
|
||||
CloseAt *time.Time `json:"close_at"`
|
||||
// 循环定时(每周循环):ScheduleDays 为逗号分隔的周几(0=周日…6=周六,与 time.Weekday 一致),
|
||||
// 空表示每天都开放;ScheduleStart/ScheduleEnd 为每日时间段 "HH:MM"(留空表示该日全天)。
|
||||
// 两者都为空时表示未启用循环定时。
|
||||
// 循环定时(每周):ScheduleDays 为逗号分隔的周几(0=周日…6=周六),空表示每天都开放;
|
||||
// ScheduleStart/ScheduleEnd 为每日时间段 "HH:MM"(留空表示全天)。两者皆空即未启用。
|
||||
ScheduleDays string `gorm:"size:31" json:"schedule_days"`
|
||||
ScheduleStart string `gorm:"size:5" json:"schedule_start"`
|
||||
ScheduleEnd string `gorm:"size:5" json:"schedule_end"`
|
||||
@@ -178,8 +172,8 @@ func (fc *FeatureControl) scheduleDayOpen(day time.Time) bool {
|
||||
return fc.ScheduleDays == "" || containsWeekday(fc.ScheduleDays, int(day.Weekday()))
|
||||
}
|
||||
|
||||
// scheduleWindow 返回单日开放窗口的分钟数 [startM, endM)。
|
||||
// 未设置时间段时视为全天 [0, 1440)。配置非法时返回错误(调用方按"不限制"处理)。
|
||||
// scheduleWindow 返回单日开放窗口的分钟数 [startM, endM)。未设置时间段时视为全天;
|
||||
// 配置非法时返回错误(调用方按「不限制」处理)。
|
||||
func (fc *FeatureControl) scheduleWindow() (startM, endM int, err error) {
|
||||
if fc.ScheduleStart == "" && fc.ScheduleEnd == "" {
|
||||
return 0, 24 * 60, nil
|
||||
@@ -342,9 +336,8 @@ func startOfDay(t time.Time) time.Time {
|
||||
return time.Date(y, m, d, 0, 0, 0, 0, t.Location())
|
||||
}
|
||||
|
||||
// DefaultFeatureControl 返回某功能的默认配置(尚未落库时的行为)。
|
||||
// 提问与投票默认开启(与历史行为一致);私聊默认关闭,沿用 users.allow_chat 的默认值,
|
||||
// 避免历史账号在功能上线后被动开启私聊。
|
||||
// DefaultFeatureControl 返回某功能的默认配置(尚未落库时的行为)。提问与投票默认开启
|
||||
// (与历史行为一致);私聊默认关闭,沿用 users.allow_chat 默认值,避免历史账号被动开启。
|
||||
func DefaultFeatureControl(userID uint, feature string) *FeatureControl {
|
||||
return &FeatureControl{
|
||||
UserID: userID,
|
||||
@@ -353,8 +346,8 @@ func DefaultFeatureControl(userID uint, feature string) *FeatureControl {
|
||||
}
|
||||
}
|
||||
|
||||
// FeatureStateOf 便捷入口:读取某用户某功能在当前的综合状态。
|
||||
// 查询失败或无存储时回退到默认配置,保证单个依赖表异常不会让整站功能不可用。
|
||||
// FeatureStateOf 读取某用户某功能当前的综合状态。查询失败或无存储时回退默认配置,
|
||||
// 保证单个依赖表异常不会让整站功能不可用。
|
||||
func FeatureStateOf(ctx context.Context, userID uint, feature string) FeatureState {
|
||||
if FeatureControls == nil {
|
||||
return DefaultFeatureControl(userID, feature).Evaluate(time.Now())
|
||||
@@ -433,7 +426,7 @@ type UpdateFeatureControlOptions struct {
|
||||
QuotaLimit int64
|
||||
}
|
||||
|
||||
// Save 写入功能配置。使用 ON CONFLICT 幂等写入,并显式列出更新列,
|
||||
// Save 写入功能配置。用 ON CONFLICT 幂等写入并显式列出更新列,
|
||||
// 避免把 quota_count(已收集数量)误重置为 0。
|
||||
func (db *featureControls) Save(ctx context.Context, userID uint, feature string, opts UpdateFeatureControlOptions) error {
|
||||
now := time.Now()
|
||||
@@ -544,13 +537,12 @@ func (db *featureControls) IncrementQuota(ctx context.Context, userID uint, feat
|
||||
|
||||
// backfillFeatureControls 为尚未初始化的历史账号补齐功能配置行。
|
||||
//
|
||||
// 背景:私聊开关此前落在 users.allow_chat,提问/投票则一直默认开启。功能上线后若没有
|
||||
// 对应的配置行,Get 会回退到默认值(私聊默认关闭),导致已开启私聊的历史账号被静默关闭。
|
||||
// 因此在 AutoMigrate 之后按 users.allow_chat 回填私聊开关,其余功能按"开启"回填。
|
||||
// 私聊开关此前落在 users.allow_chat,提问/投票一直默认开启;功能上线后没有配置行时
|
||||
// Get 会回退默认值(私聊默认关闭),导致已开启私聊的历史账号被静默关闭,
|
||||
// 故在 AutoMigrate 之后按 users.allow_chat 回填私聊,其余按「开启」回填。
|
||||
//
|
||||
// 幂等:每轮只挑出仍缺配置行的账号,写完后进入下一轮,直到没有遗漏。
|
||||
// 分批 + ON CONFLICT DO NOTHING,避免"边插入边查同一张表"在不同数据库上的差异行为,
|
||||
// 并保证并发启动(多实例同时启动)时不会重复插入。
|
||||
// 幂等:每轮只挑仍缺行的账号,分批 + ON CONFLICT DO NOTHING,避免不同数据库上
|
||||
// 「边插入边查同一张表」的差异行为,也保证多实例并发启动时不重复插入。
|
||||
func backfillFeatureControls(db *gorm.DB) error {
|
||||
// backfillBatchSize 每轮处理的账号数,控制单次事务体积与内存占用。
|
||||
const backfillBatchSize = 500
|
||||
@@ -616,28 +608,20 @@ func backfillFeatureControls(db *gorm.DB) error {
|
||||
|
||||
// reconcileLegacyChatEnabled 修复旧版 backfill 写入的私聊错误开关。
|
||||
//
|
||||
// 背景:早期版本 FeatureControl.Enabled 带 gorm:"default:true" 标签,backfill 为
|
||||
// allow_chat=false 的用户补私聊行时,false 被 GORM 替换为默认值 true,导致私聊被被动开启;
|
||||
// 且 UpdateChatSettings 会把 users.allow_chat 反向同步为 true,形成持久化。
|
||||
// 背景:早期 Enabled 带 gorm:"default:true",backfill 为 allow_chat=false 的用户补行时
|
||||
// false 被替换为 true,私聊被被动开启;UpdateChatSettings 还会把 allow_chat 同步为 true。
|
||||
//
|
||||
// 纠偏条件(必须同时满足,只命中"纯 backfill 默认行",绝不碰箱主配置过的行):
|
||||
// - feature = 'chat' 且 enabled = true;
|
||||
// - 对应 users.allow_chat = false(箱主从未在面板开启私聊——面板开启会同步 allow_chat=true);
|
||||
// - 该行无任何自定义配置(open_at / close_at 为空、quota_limit = 0、循环定时为空);
|
||||
// - updated_at <= created_at(从未被面板保存过,updated_at 未变化)。
|
||||
//
|
||||
// 满足条件即复位 feature_controls.enabled = false,并同步 users.allow_chat = false。
|
||||
// 幂等:每轮只处理仍满足条件的行,直到没有遗漏;已复位/已配置的行不会重复命中。
|
||||
// 纠偏条件(必须同时满足,只命中「纯 backfill 默认行」,绝不碰箱主配置过的行):
|
||||
// feature='chat' 且 enabled=true;对应 users.allow_chat=false;该行无任何自定义配置
|
||||
// (open_at/close_at 为空、quota_limit=0、循环定时为空);updated_at <= created_at。
|
||||
// 满足即复位 enabled=false 并同步 users.allow_chat=false。幂等,已配置行不会重复命中。
|
||||
func reconcileLegacyChatEnabled(db *gorm.DB) error {
|
||||
// reconcileBatchSize 每轮处理的账号数,控制单次事务体积。
|
||||
const reconcileBatchSize = 500
|
||||
|
||||
// pureDefaultFilterFC / pureDefaultFilterPlain 只匹配纯默认配置的私聊行
|
||||
// (无一次性窗口/循环定时/定量上限)。两者仅列前缀不同:
|
||||
// - FC 版带 "fc." 前缀,用于 SELECT(feature_controls fc 别名场景);
|
||||
// - 纯版不带前缀,用于 UPDATE(GORM 不会为 UPDATE 自动加别名,带前缀会报
|
||||
// missing FROM-clause entry for table "fc")。
|
||||
// 同时兼容旧行 schedule 列为 NULL 与新行空串两种情况。
|
||||
// 两者只匹配纯默认配置的私聊行(无一次性窗口/循环定时/定量上限),仅列前缀不同:
|
||||
// FC 版带 "fc." 用于 SELECT;纯版不带前缀用于 UPDATE(GORM 不会为 UPDATE 自动加别名,
|
||||
// 带前缀会报 missing FROM-clause entry for table "fc")。同时兼容旧行 NULL 与新行空串。
|
||||
pureDefaultFilterFC := "fc.open_at IS NULL AND fc.close_at IS NULL AND fc.quota_limit = 0" +
|
||||
" AND (fc.schedule_days IS NULL OR fc.schedule_days = '')" +
|
||||
" AND (fc.schedule_start IS NULL OR fc.schedule_start = '')" +
|
||||
@@ -703,11 +687,9 @@ func reconcileLegacyChatEnabled(db *gorm.DB) error {
|
||||
}
|
||||
|
||||
// normalizeChatQuota 清零私聊行上历史残留的收集上限与计数。
|
||||
//
|
||||
// 背景:私聊不提供收集上限能力(会话是双向对话,限制「新建会话数」意义不大),
|
||||
// UI 已隐藏该字段、保存时强制清零;此处在启动期做一次性兜底,把旧版本
|
||||
// 设置过的私聊上限/计数统一清零,避免达到残留上限后私聊「莫名关闭」。
|
||||
// 幂等:重复执行不产生任何变更,失败由调用方告警(不阻断启动)。
|
||||
// 私聊不提供收集上限(会话是双向对话,限制「新建会话数」意义不大),UI 已隐藏该字段、
|
||||
// 保存时强制清零;此处在启动期兜底清零,避免达到残留上限后私聊「莫名关闭」。
|
||||
// 幂等,失败由调用方告警(不阻断启动)。
|
||||
func normalizeChatQuota(db *gorm.DB) error {
|
||||
res := db.Model(&FeatureControl{}).
|
||||
Where("feature = ? AND (quota_limit > 0 OR quota_count > 0)", FeatureChat).
|
||||
|
||||
Reference in New Issue
Block a user