@@ -1048,6 +1048,10 @@ func (s *AiGatewayService) LogContent(entry model.AiContentLog) {
|
||||
if err != nil || !ok {
|
||||
return err
|
||||
}
|
||||
on, err := contentLogStillOn(tx, entry.KeyID)
|
||||
if err != nil || !on {
|
||||
return err
|
||||
}
|
||||
return tx.Create(&entry).Error
|
||||
})
|
||||
if err != nil {
|
||||
@@ -1055,6 +1059,20 @@ func (s *AiGatewayService) LogContent(entry model.AiContentLog) {
|
||||
}
|
||||
}
|
||||
|
||||
// contentLogStillOn 写入事务内回读密钥:已删除、被停用或日志窗口已过期时放弃写入,
|
||||
// 防止管理员「立即关闭」后,在途长请求仍按鉴权时的旧快照把敏感正文落库。
|
||||
func contentLogStillOn(tx *gorm.DB, keyID uint) (bool, error) {
|
||||
var key model.AiKey
|
||||
err := tx.First(&key, keyID).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return key.Enabled && key.ContentLogUntil != nil && time.Now().Before(*key.ContentLogUntil), nil
|
||||
}
|
||||
|
||||
func truncateBody(s string) string {
|
||||
if len(s) > aiContentBodyLimit {
|
||||
return s[:aiContentBodyLimit]
|
||||
@@ -1110,7 +1128,12 @@ func (s *AiGatewayService) cleanupOnce(ctx context.Context) {
|
||||
s.cleanupTable(ctx, &model.AiContentLog{}, aiContentLogRetention, aiContentLogMaxRows, "ai content log")
|
||||
}
|
||||
|
||||
// cleanupTable 按保留期与行数上限清理日志表(超限删最旧)。
|
||||
// cleanupBatch 是超限清理每轮删除的行数上限;远小于各数据库绑定变量硬上限
|
||||
// (modernc SQLite 32766、MySQL/PG 65535),跨库安全。var 供测试注入小批次。
|
||||
var cleanupBatch = 10000
|
||||
|
||||
// cleanupTable 按保留期与行数上限清理日志表(超限删最旧,固定批次循环,
|
||||
// 任一批失败必须记日志并中断,静默失败会让日志表无界增长)。
|
||||
func (s *AiGatewayService) cleanupTable(ctx context.Context, m any, retention time.Duration, maxRows int, tag string) {
|
||||
cutoff := time.Now().Add(-retention)
|
||||
if err := s.db.WithContext(ctx).Where("created_at < ?", cutoff).Delete(m).Error; err != nil {
|
||||
@@ -1119,16 +1142,36 @@ func (s *AiGatewayService) cleanupTable(ctx context.Context, m any, retention ti
|
||||
}
|
||||
var total int64
|
||||
if err := s.db.WithContext(ctx).Model(m).Count(&total).Error; err != nil {
|
||||
log.Printf("%s cleanup count: %v", tag, err)
|
||||
return
|
||||
}
|
||||
if overflow := int(total) - maxRows; overflow > 0 {
|
||||
var ids []uint
|
||||
s.db.WithContext(ctx).Model(m).Order("id ASC").Limit(overflow).Pluck("id", &ids)
|
||||
if len(ids) > 0 {
|
||||
s.db.WithContext(ctx).Delete(m, ids)
|
||||
for overflow := int(total) - maxRows; overflow > 0; {
|
||||
n, err := s.deleteOldestBatch(ctx, m, min(overflow, cleanupBatch))
|
||||
if err != nil {
|
||||
log.Printf("%s cleanup overflow: %v", tag, err)
|
||||
return
|
||||
}
|
||||
if n == 0 {
|
||||
return
|
||||
}
|
||||
overflow -= n
|
||||
}
|
||||
}
|
||||
|
||||
// deleteOldestBatch 删除表内最旧的至多 limit 行,返回实际删除数。
|
||||
func (s *AiGatewayService) deleteOldestBatch(ctx context.Context, m any, limit int) (int, error) {
|
||||
var ids []uint
|
||||
if err := s.db.WithContext(ctx).Model(m).Order("id ASC").Limit(limit).Pluck("id", &ids).Error; err != nil {
|
||||
return 0, fmt.Errorf("pluck oldest: %w", err)
|
||||
}
|
||||
if len(ids) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
if err := s.db.WithContext(ctx).Delete(m, ids).Error; err != nil {
|
||||
return 0, fmt.Errorf("delete batch: %w", err)
|
||||
}
|
||||
return len(ids), nil
|
||||
}
|
||||
|
||||
// Wait 等待后台清理 goroutine 退出。
|
||||
func (s *AiGatewayService) Wait() { s.wg.Wait() }
|
||||
|
||||
Reference in New Issue
Block a user