可靠性: 完善 R2 备份恢复与对象生命周期
This commit is contained in:
+66
-23
@@ -386,10 +386,25 @@ func (db *users) Update(ctx context.Context, id uint, opts UpdateUserOptions) er
|
||||
"Language": opts.Language,
|
||||
}
|
||||
|
||||
if err := db.WithContext(ctx).Model(&User{}).Where("id = ?", id).Updates(updates).Error; err != nil {
|
||||
return errors.Wrap(err, "update user")
|
||||
}
|
||||
return nil
|
||||
return db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
var current User
|
||||
if err := tx.WithContext(ctx).Select("id", "avatar", "background", "background_image").First(¤t, id).Error; err != nil {
|
||||
return errors.Wrap(err, "get current profile images")
|
||||
}
|
||||
if err := tx.WithContext(ctx).Model(&User{}).Where("id = ?", id).Updates(updates).Error; err != nil {
|
||||
return errors.Wrap(err, "update user")
|
||||
}
|
||||
oldValues := make([]string, 0, 3)
|
||||
for _, pair := range [][2]string{{current.Avatar, opts.Avatar}, {current.Background, opts.Background}, {current.BackgroundImage, opts.BackgroundImage}} {
|
||||
if pair[0] != pair[1] {
|
||||
oldValues = append(oldValues, pair[0])
|
||||
}
|
||||
}
|
||||
if err := enqueueR2DeleteTasksForValues(tx.WithContext(ctx), conf.Upload.ImageBucket, oldValues, opts.Avatar, opts.Background, opts.BackgroundImage); err != nil {
|
||||
return errors.Wrap(err, "enqueue replaced profile images")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateHarassmentType 仅更新骚扰防护类型(none / register_only)。
|
||||
@@ -1021,6 +1036,19 @@ func (db *users) BanUserAndCleanup(ctx context.Context, userID uint) error {
|
||||
// 直接复用会把上一条语句的 Where 条件串到下一条上(曾把 messages 的条件串进 follow_ups,
|
||||
// 报「字段 conversation_id 不存在」)。
|
||||
func purgeUserContent(ctx context.Context, tx *gorm.DB, userID uint) error {
|
||||
var profile User
|
||||
if err := tx.WithContext(ctx).Select("id", "avatar", "background", "background_image").First(&profile, userID).Error; err != nil {
|
||||
return errors.Wrap(err, "get profile images of user")
|
||||
}
|
||||
if err := enqueueR2DeleteTasksForValues(tx.WithContext(ctx), conf.Upload.ImageBucket, []string{profile.Avatar, profile.Background, profile.BackgroundImage}); err != nil {
|
||||
return errors.Wrap(err, "enqueue profile images of user")
|
||||
}
|
||||
if err := tx.WithContext(ctx).Model(&User{}).Where("id = ?", userID).Updates(map[string]interface{}{
|
||||
"avatar": "", "background": "", "background_image": "",
|
||||
}).Error; err != nil {
|
||||
return errors.Wrap(err, "clear profile images of user")
|
||||
}
|
||||
|
||||
// 追问:属于该用户提问下的追问
|
||||
if err := tx.WithContext(ctx).Unscoped().
|
||||
Where("question_id IN (SELECT id FROM questions WHERE user_id = ?)", userID).
|
||||
@@ -1029,11 +1057,31 @@ func purgeUserContent(ctx context.Context, tx *gorm.DB, userID uint) error {
|
||||
}
|
||||
|
||||
// 消息图片必须先于消息删除,否则消息子查询将无法再匹配待删图片。
|
||||
// 新数据优先使用物化 ObjectKey,旧数据回退到 URL;外部 URL 不会进入 R2 outbox。
|
||||
messageImageScope := "message_id IN (SELECT id FROM messages WHERE conversation_id IN (SELECT id FROM conversations WHERE owner_id = ? OR asker_id = ?))"
|
||||
var messageImages []*MessageImage
|
||||
if err := tx.WithContext(ctx).Unscoped().
|
||||
Where("message_id IN (SELECT id FROM messages WHERE conversation_id IN (SELECT id FROM conversations WHERE owner_id = ? OR asker_id = ?))", userID, userID).
|
||||
Where(messageImageScope, userID, userID).
|
||||
Find(&messageImages).Error; err != nil {
|
||||
return errors.Wrap(err, "get message images of user")
|
||||
}
|
||||
messageImageValues := make([]string, 0, len(messageImages))
|
||||
for _, image := range messageImages {
|
||||
value := image.ObjectKey
|
||||
if value == "" {
|
||||
value = image.URL
|
||||
}
|
||||
messageImageValues = append(messageImageValues, value)
|
||||
}
|
||||
if err := tx.WithContext(ctx).Unscoped().
|
||||
Where(messageImageScope, userID, userID).
|
||||
Delete(&MessageImage{}).Error; err != nil {
|
||||
return errors.Wrap(err, "delete message images of user")
|
||||
}
|
||||
if err := enqueueR2DeleteTasksForValues(tx.WithContext(ctx), conf.Upload.ImageBucket, messageImageValues); err != nil {
|
||||
return errors.Wrap(err, "enqueue deleted message images")
|
||||
}
|
||||
|
||||
// 消息:属于该用户作为 owner 或 asker 的会话
|
||||
if err := tx.WithContext(ctx).Unscoped().
|
||||
Where("conversation_id IN (SELECT id FROM conversations WHERE owner_id = ? OR asker_id = ?)", userID, userID).
|
||||
@@ -1048,30 +1096,25 @@ func purgeUserContent(ctx context.Context, tx *gorm.DB, userID uint) error {
|
||||
return errors.Wrap(err, "delete conversations of user")
|
||||
}
|
||||
|
||||
// 提问图片元数据:删除本用户上传的孤立图片,以及仅链接到本用户提问的图片。
|
||||
// 仍被其他提问引用的图片必须保留;这里只清数据库元数据,不操作对象存储。
|
||||
if err := tx.WithContext(ctx).Unscoped().
|
||||
Where(`(
|
||||
uploader_user_id = ?
|
||||
OR upload_images.id IN (
|
||||
SELECT upload_image_id FROM upload_image_questions
|
||||
WHERE question_id IN (SELECT id FROM questions WHERE user_id = ?)
|
||||
)
|
||||
) AND NOT EXISTS (
|
||||
SELECT 1 FROM upload_image_questions AS shared_links
|
||||
WHERE shared_links.upload_image_id = upload_images.id
|
||||
AND shared_links.question_id NOT IN (SELECT id FROM questions WHERE user_id = ?)
|
||||
)`, userID, userID, userID).
|
||||
Delete(&UploadImage{}).Error; err != nil {
|
||||
return errors.Wrap(err, "delete upload images of user")
|
||||
// 先确定本用户上传或仅与当前作用域有关的候选主体,再解除本用户题目的关联。
|
||||
// 最终是否可删除由与 UploadImages.DeleteByQuestionID 完全相同的共享保护谓词决定。
|
||||
var uploadImageIDs []uint
|
||||
if err := tx.WithContext(ctx).Unscoped().Model(&UploadImage{}).
|
||||
Where(`uploader_user_id = ? OR upload_images.id IN (
|
||||
SELECT upload_image_id FROM upload_image_questions
|
||||
WHERE question_id IN (SELECT id FROM questions WHERE user_id = ?)
|
||||
)`, userID, userID).
|
||||
Pluck("upload_images.id", &uploadImageIDs).Error; err != nil {
|
||||
return errors.Wrap(err, "get upload images of user")
|
||||
}
|
||||
|
||||
// 提问-图片关联
|
||||
if err := tx.WithContext(ctx).Unscoped().
|
||||
Where("question_id IN (SELECT id FROM questions WHERE user_id = ?)", userID).
|
||||
Delete(&UploadImageQuestion{}).Error; err != nil {
|
||||
return errors.Wrap(err, "delete upload image links of user")
|
||||
}
|
||||
if err := deleteUnreferencedUploadImages(ctx, tx, uploadImageIDs); err != nil {
|
||||
return errors.Wrap(err, "delete upload images of user")
|
||||
}
|
||||
|
||||
// 提问
|
||||
if err := tx.WithContext(ctx).Unscoped().
|
||||
|
||||
Reference in New Issue
Block a user