// Copyright 2023 E99p1ant. 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" "github.com/pkg/errors" "github.com/samber/lo" "gorm.io/gorm" ) var UploadImages UploadImagesStore var _ UploadImagesStore = (*uploadImages)(nil) type UploadImagesStore interface { Create(ctx context.Context, opts CreateUploadImageOptions) (*UploadImage, error) GetByQuestionID(ctx context.Context, questionID uint) ([]*UploadImage, error) // GetByQuestionIDs 批量获取多个提问的图片,按 question_id 分组返回。 // 用于账号导出等场景,将 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(ctx context.Context, questionID uint) error } func NewUploadImagesStore(db *gorm.DB) UploadImagesStore { return &uploadImages{db} } type uploadImages struct { *gorm.DB } type UploadImage struct { gorm.Model UploaderUserID uint Name string FileSize int64 Md5 string Key string } type UploadImageQuestionType string const ( UploadImageQuestionTypeAsk UploadImageQuestionType = "ask" UploadImageQuestionTypeAnswer UploadImageQuestionType = "answer" ) type UploadImageQuestion struct { Type UploadImageQuestionType UploadImageID uint `gorm:"primaryKey"` QuestionID uint `gorm:"primaryKey"` } type CreateUploadImageOptions struct { Type UploadImageQuestionType QuestionID uint UploaderUserID uint Name string FileSize int64 Md5 string Key string IsDeletingPrevious bool } func (db *uploadImages) Create(ctx context.Context, opts CreateUploadImageOptions) (*UploadImage, error) { image := &UploadImage{ UploaderUserID: opts.UploaderUserID, Name: opts.Name, FileSize: opts.FileSize, Md5: opts.Md5, Key: opts.Key, } if err := db.Transaction(func(tx *gorm.DB) error { if err := tx.WithContext(ctx).Create(image).Error; err != nil { return errors.Wrap(err, "create image") } if opts.IsDeletingPrevious { if err := tx.WithContext(ctx).Where("type = ? AND question_id = ?", opts.Type, opts.QuestionID).Delete(&UploadImageQuestion{}).Error; err != nil { return errors.Wrap(err, "delete previous image question link") } } if err := tx.WithContext(ctx).Create(&UploadImageQuestion{ Type: opts.Type, UploadImageID: image.ID, QuestionID: opts.QuestionID, }).Error; err != nil { return errors.Wrap(err, "create image question link") } return nil }); err != nil { return nil, err } return image, nil } func (db *uploadImages) getBy(ctx context.Context, where string, args ...interface{}) ([]*UploadImage, error) { var uploadImageQuestions []*UploadImageQuestion if err := db.WithContext(ctx).Model(&UploadImageQuestion{}).Where(where, args...).Find(&uploadImageQuestions).Error; err != nil { return nil, errors.Wrap(err, "get image questions") } uploadImageIDs := lo.Map(uploadImageQuestions, func(item *UploadImageQuestion, _ int) uint { return item.UploadImageID }) var uploadImages []*UploadImage if err := db.WithContext(ctx).Model(&UploadImage{}).Where("id IN ?", uploadImageIDs).Order("id ASC").Find(&uploadImages).Error; err != nil { return nil, errors.Wrap(err, "get images") } return uploadImages, nil } func (db *uploadImages) GetByQuestionID(ctx context.Context, questionID uint) ([]*UploadImage, error) { return db.getBy(ctx, "question_id = ?", questionID) } // GetByQuestionIDs 批量获取多个提问的图片,按 question_id 分组返回(解决导出 N+1)。 // 实现:先按 IN 批量取关联行,再按 IN 批量取图片主体,最后在内存中按 question_id 归组, // 将每个提问原本的 2 次查询降为整体 2 次查询。 func (db *uploadImages) GetByQuestionIDs(ctx context.Context, questionIDs []uint) (map[uint][]*UploadImage, error) { result := make(map[uint][]*UploadImage) if len(questionIDs) == 0 { return result, nil } var links []*UploadImageQuestion if err := db.WithContext(ctx).Model(&UploadImageQuestion{}).Where("question_id IN ?", questionIDs).Find(&links).Error; err != nil { return nil, errors.Wrap(err, "get image questions batch") } if len(links) == 0 { return result, nil } imageIDs := lo.Map(links, func(item *UploadImageQuestion, _ int) uint { return item.UploadImageID }) var images []*UploadImage if err := db.WithContext(ctx).Model(&UploadImage{}).Where("id IN ?", imageIDs).Order("id ASC").Find(&images).Error; err != nil { return nil, errors.Wrap(err, "get images batch") } imageByID := lo.SliceToMap(images, func(img *UploadImage) (uint, *UploadImage) { return img.ID, img }) for _, link := range links { if img, ok := imageByID[link.UploadImageID]; ok { result[link.QuestionID] = append(result[link.QuestionID], img) } } return result, nil } func (db *uploadImages) GetByTypeQuestionID(ctx context.Context, typ UploadImageQuestionType, questionID uint) ([]*UploadImage, error) { return db.getBy(ctx, "type = ? AND question_id = ?", typ, questionID) } // DeleteByQuestionID 删除某条提问时级联清理其全部上传图片记录(软删除,与提问保持一致)。 // 注意:question_id 列在关联表 upload_image_questions 上,不在 upload_images 表上, // 需先查关联行拿到 imageID 列表,再软删图片主体并删除关联行(同一事务内完成)。 // 注:仅清理数据库记录,对象存储中的实际文件不在此处删除(避免误删复用资源)。 func (db *uploadImages) DeleteByQuestionID(ctx context.Context, questionID uint) error { return db.Transaction(func(tx *gorm.DB) error { var links []*UploadImageQuestion if err := tx.WithContext(ctx).Where("question_id = ?", questionID).Find(&links).Error; err != nil { return errors.Wrap(err, "get image questions for delete") } if len(links) == 0 { return nil } 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 { return errors.Wrap(err, "delete upload images by question id") } if err := tx.WithContext(ctx).Where("question_id = ?", questionID).Delete(&UploadImageQuestion{}).Error; err != nil { return errors.Wrap(err, "delete image question links") } return nil }) }