fix(data): 删除提问时追问与图片一并真删,不再软删残留

questions.DeleteByID 对主表用 Unscoped 真删,但级联清理的两个子 store 仍用软删:
FollowUp(内嵌 dbutil.Model)与 UploadImage(内嵌 gorm.Model)都带 DeletedAt,
裸 Delete 是软删。于是提问正文(含 FromIP、邮箱)删干净了,追问文本与图片元数据
(name/md5/key)却带着 deleted_at 永久残留,孤儿行的 question_id 指向已不存在的提问
—— 正是「真删以免个人数据删不干净」想消灭的问题,却在子表漏改。

两个 DeleteByQuestionID 改为 Unscoped 真删。关联表 UploadImageQuestion 无 DeletedAt,
本来就真删,无需 Unscoped(已注释)。

新增 question_delete_hard_test.go:用 Unscoped 计数断言删除后追问/图片/关联行物理上=0,
并做变异验证(改回软删 → 测试失败)证明测试真的能抓住这个回归。
This commit is contained in:
tamakyi
2026-10-29 18:52:19 +08:00
parent 7f86db8937
commit 87176516f3
3 changed files with 109 additions and 5 deletions
+8 -2
View File
@@ -23,7 +23,12 @@ type UploadImagesStore interface {
// 用于账号导出等场景,将 N 次单题查询合并为少量 IN 查询,避免 N+1。
GetByQuestionIDs(ctx context.Context, questionIDs []uint) (map[uint][]*UploadImage, error)
GetByTypeQuestionID(ctx context.Context, typ UploadImageQuestionType, questionID uint) ([]*UploadImage, error)
// DeleteByQuestionID 删除某条提问时级联清理其全部上传图片记录(软删除,与提问保持一致)。
// DeleteByQuestionID 删除某条提问时级联清理其全部上传图片记录与关联行。
//
// 用 Unscoped 真删:图片元数据(name/md5/key)随提问一并真删,与 questions.DeleteByID
// 对主表的真删口径一致。软删会留下带 deleted_at 的图片行,且关联行的 question_id
// 指向已不存在的提问。注意这里只清数据库记录,对象存储里的实际文件不在本函数删除
// (图片可能被多条记录复用,避免误删共享资源)。
DeleteByQuestionID(ctx context.Context, questionID uint) error
}
@@ -179,9 +184,10 @@ func (db *uploadImages) DeleteByQuestionID(ctx context.Context, questionID uint)
imageIDs := lo.Map(links, func(item *UploadImageQuestion, _ int) uint {
return item.UploadImageID
})
if err := tx.WithContext(ctx).Where("id IN ?", imageIDs).Delete(&UploadImage{}).Error; err != nil {
if err := tx.WithContext(ctx).Where("id IN ?", imageIDs).Unscoped().Delete(&UploadImage{}).Error; err != nil {
return errors.Wrap(err, "delete upload images by question id")
}
// 关联表 UploadImageQuestion 无 DeletedAt,Delete 本就是真删,无需 Unscoped。
if err := tx.WithContext(ctx).Where("question_id = ?", questionID).Delete(&UploadImageQuestion{}).Error; err != nil {
return errors.Wrap(err, "delete image question links")
}