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:
@@ -0,0 +1,94 @@
|
||||
// Copyright 2026 tamakyi. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// TestQuestionDeleteCascadesHardDelete 验证删除提问时,追问与上传图片也被**真删**,
|
||||
// 而不是软删残留。
|
||||
//
|
||||
// 背景:questions.DeleteByID 对主表用 Unscoped 真删,但历史上一度只有主表真删、
|
||||
// 追问/图片仍软删,导致提问正文删干净了、追问文本与图片元数据却带着 deleted_at
|
||||
// 永久留在库里(这正是「真删以免个人数据删不干净」想消灭的问题,却在子表漏改)。
|
||||
// 本测试用 Unscoped 计数兜底:软删的行在普通查询里不可见,只有 Unscoped 才能看到,
|
||||
// 因此断言「删除后 Unscoped 计数 = 0」才能证明是真删。
|
||||
func TestQuestionDeleteCascadesHardDelete(t *testing.T) {
|
||||
db, cleanup := newTestDB(t)
|
||||
ctx := context.Background()
|
||||
|
||||
questions := NewQuestionsStore(db)
|
||||
followUps := NewFollowUpsStore(db)
|
||||
uploadImages := NewUploadImagesStore(db)
|
||||
|
||||
t.Cleanup(func() {
|
||||
// 三张表都要清:follow_ups / upload_images(含链接表)在删除后应该已经空了,
|
||||
// 但其他子测试的 data 可能残留,用 cleanup 统一兜底。
|
||||
_ = cleanup("questions", "follow_ups", "upload_images", "upload_image_questions")
|
||||
})
|
||||
|
||||
// 建一条提问
|
||||
q, err := questions.Create(ctx, CreateQuestionOptions{
|
||||
FromIP: "114.5.1.4",
|
||||
UserID: 1,
|
||||
Content: "需要被彻底删除的提问",
|
||||
AskerUserID: 1,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// 给它加一条追问
|
||||
created, err := followUps.CreateIfBelowLimit(ctx, CreateFollowUpOptions{
|
||||
QuestionID: q.ID,
|
||||
Content: "追问内容,也应被真删",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.True(t, created)
|
||||
|
||||
// 给它加一张上传图片
|
||||
img, err := uploadImages.Create(ctx, CreateUploadImageOptions{
|
||||
Type: UploadImageQuestionTypeAsk,
|
||||
QuestionID: q.ID,
|
||||
UploaderUserID: 1,
|
||||
Name: "secret.png",
|
||||
FileSize: 1024,
|
||||
Md5: "deadbeef",
|
||||
Key: "2026/10/29/secret.png",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotZero(t, img.ID)
|
||||
|
||||
// 删除提问
|
||||
require.NoError(t, questions.DeleteByID(ctx, q.ID))
|
||||
|
||||
// 主表:真删
|
||||
require.Equal(t, int64(0), hardCount(t, db, &Question{}, "id = ?", q.ID))
|
||||
|
||||
// 追问:真删(软删的话这里会是 1)
|
||||
require.Equal(t, int64(0), hardCount(t, db, &FollowUp{}, "question_id = ?", q.ID))
|
||||
|
||||
// 上传图片主体:真删
|
||||
require.Equal(t, int64(0), hardCount(t, db, &UploadImage{}, "id = ?", img.ID))
|
||||
|
||||
// 关联行:真删
|
||||
require.Equal(t, int64(0), hardCount(t, db, &UploadImageQuestion{}, "question_id = ?", q.ID))
|
||||
}
|
||||
|
||||
// hardCount 用 Unscoped 统计满足条件的行数,能穿透软删除过滤,
|
||||
// 用于断言「物理上也不存在」。
|
||||
func hardCount(t *testing.T, db *gorm.DB, model interface{}, where string, args ...interface{}) int64 {
|
||||
t.Helper()
|
||||
var n int64
|
||||
err := db.WithContext(context.Background()).Unscoped().
|
||||
Model(model).
|
||||
Where(where, args...).
|
||||
Count(&n).Error
|
||||
require.NoError(t, err)
|
||||
return n
|
||||
}
|
||||
Reference in New Issue
Block a user