refactor(server): decouple services and improve architecture
- Introduce interfaces for all major services (JWT, PostAI, Comment, Message, Notification, QRCodeLogin, Upload, Vote, etc.) to support dependency inversion.
- Move query parameters from `internal/dto` to a new `internal/query` package to separate request payloads from data transfer objects.
- Refactor `internal/router` to use embedded `RouterDeps` for cleaner dependency management.
- Decouple handlers from repositories by injecting services instead of direct repository access, ensuring proper layering.
- Improve database initialization by moving it from `internal/model` to `internal/database`.
- Optimize message decryption by implementing a more efficient `BatchDecrypt` method in `MessageEncryptor` using a worker pool.
- Enhance error handling and security by implementing fail-fast checks for encryption key length during startup.
- Clean up unused code, including the `avatar` package and several unused DTOs.
2026-06-15 03:41:59 +08:00
|
|
|
|
package repository
|
2026-03-25 20:44:12 +08:00
|
|
|
|
|
|
|
|
|
|
import (
|
2026-04-22 16:01:59 +08:00
|
|
|
|
"with_you/internal/model"
|
2026-04-30 12:26:25 +08:00
|
|
|
|
"with_you/internal/pkg/utils"
|
refactor(server): decouple services and improve architecture
- Introduce interfaces for all major services (JWT, PostAI, Comment, Message, Notification, QRCodeLogin, Upload, Vote, etc.) to support dependency inversion.
- Move query parameters from `internal/dto` to a new `internal/query` package to separate request payloads from data transfer objects.
- Refactor `internal/router` to use embedded `RouterDeps` for cleaner dependency management.
- Decouple handlers from repositories by injecting services instead of direct repository access, ensuring proper layering.
- Improve database initialization by moving it from `internal/model` to `internal/database`.
- Optimize message decryption by implementing a more efficient `BatchDecrypt` method in `MessageEncryptor` using a worker pool.
- Enhance error handling and security by implementing fail-fast checks for encryption key length during startup.
- Clean up unused code, including the `avatar` package and several unused DTOs.
2026-06-15 03:41:59 +08:00
|
|
|
|
"with_you/internal/query"
|
2026-03-25 20:44:12 +08:00
|
|
|
|
|
|
|
|
|
|
"gorm.io/gorm"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-03-26 18:14:16 +08:00
|
|
|
|
// MaterialSubjectRepository 学科分类仓储接口
|
|
|
|
|
|
type MaterialSubjectRepository interface {
|
|
|
|
|
|
ListActive() ([]*model.MaterialSubject, error)
|
|
|
|
|
|
ListAll() ([]*model.MaterialSubject, error)
|
|
|
|
|
|
GetByID(id string) (*model.MaterialSubject, error)
|
|
|
|
|
|
Create(subject *model.MaterialSubject) error
|
|
|
|
|
|
Update(subject *model.MaterialSubject) error
|
|
|
|
|
|
Delete(id string) error
|
|
|
|
|
|
CountMaterials(subjectID string) (int64, error)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// materialSubjectRepository 学科分类仓储实现
|
|
|
|
|
|
type materialSubjectRepository struct {
|
2026-03-25 20:44:12 +08:00
|
|
|
|
db *gorm.DB
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-26 18:14:16 +08:00
|
|
|
|
func NewMaterialSubjectRepository(db *gorm.DB) MaterialSubjectRepository {
|
|
|
|
|
|
return &materialSubjectRepository{db: db}
|
2026-03-25 20:44:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ListActive 获取所有启用的学科分类
|
2026-03-26 18:14:16 +08:00
|
|
|
|
func (r *materialSubjectRepository) ListActive() ([]*model.MaterialSubject, error) {
|
2026-03-25 20:44:12 +08:00
|
|
|
|
var subjects []*model.MaterialSubject
|
|
|
|
|
|
err := r.db.
|
|
|
|
|
|
Where("is_active = ?", true).
|
|
|
|
|
|
Order("sort_order ASC, created_at ASC").
|
|
|
|
|
|
Find(&subjects).Error
|
|
|
|
|
|
return subjects, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ListAll 获取所有学科分类(管理端用)
|
2026-03-26 18:14:16 +08:00
|
|
|
|
func (r *materialSubjectRepository) ListAll() ([]*model.MaterialSubject, error) {
|
2026-03-25 20:44:12 +08:00
|
|
|
|
var subjects []*model.MaterialSubject
|
|
|
|
|
|
err := r.db.
|
|
|
|
|
|
Order("sort_order ASC, created_at ASC").
|
|
|
|
|
|
Find(&subjects).Error
|
|
|
|
|
|
return subjects, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetByID 根据ID获取学科分类
|
2026-03-26 18:14:16 +08:00
|
|
|
|
func (r *materialSubjectRepository) GetByID(id string) (*model.MaterialSubject, error) {
|
2026-03-25 20:44:12 +08:00
|
|
|
|
var subject model.MaterialSubject
|
|
|
|
|
|
if err := r.db.First(&subject, "id = ?", id).Error; err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
return &subject, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Create 创建学科分类
|
2026-03-26 18:14:16 +08:00
|
|
|
|
func (r *materialSubjectRepository) Create(subject *model.MaterialSubject) error {
|
2026-03-25 20:44:12 +08:00
|
|
|
|
return r.db.Create(subject).Error
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Update 更新学科分类
|
2026-03-26 18:14:16 +08:00
|
|
|
|
func (r *materialSubjectRepository) Update(subject *model.MaterialSubject) error {
|
2026-03-25 20:44:12 +08:00
|
|
|
|
return r.db.Save(subject).Error
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Delete 删除学科分类
|
2026-03-26 18:14:16 +08:00
|
|
|
|
func (r *materialSubjectRepository) Delete(id string) error {
|
2026-03-25 20:44:12 +08:00
|
|
|
|
return r.db.Delete(&model.MaterialSubject{}, "id = ?", id).Error
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// CountMaterials 统计学科下的资料数量
|
2026-03-26 18:14:16 +08:00
|
|
|
|
func (r *materialSubjectRepository) CountMaterials(subjectID string) (int64, error) {
|
2026-03-25 20:44:12 +08:00
|
|
|
|
var count int64
|
|
|
|
|
|
err := r.db.Model(&model.MaterialFile{}).Where("subject_id = ?", subjectID).Count(&count).Error
|
|
|
|
|
|
return count, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-26 18:14:16 +08:00
|
|
|
|
// MaterialFileRepository 文件资料仓储接口
|
|
|
|
|
|
type MaterialFileRepository interface {
|
refactor(server): decouple services and improve architecture
- Introduce interfaces for all major services (JWT, PostAI, Comment, Message, Notification, QRCodeLogin, Upload, Vote, etc.) to support dependency inversion.
- Move query parameters from `internal/dto` to a new `internal/query` package to separate request payloads from data transfer objects.
- Refactor `internal/router` to use embedded `RouterDeps` for cleaner dependency management.
- Decouple handlers from repositories by injecting services instead of direct repository access, ensuring proper layering.
- Improve database initialization by moving it from `internal/model` to `internal/database`.
- Optimize message decryption by implementing a more efficient `BatchDecrypt` method in `MessageEncryptor` using a worker pool.
- Enhance error handling and security by implementing fail-fast checks for encryption key length during startup.
- Clean up unused code, including the `avatar` package and several unused DTOs.
2026-06-15 03:41:59 +08:00
|
|
|
|
List(params query.MaterialFileQueryParams) ([]*model.MaterialFile, int64, error)
|
2026-03-26 18:14:16 +08:00
|
|
|
|
GetByID(id string) (*model.MaterialFile, error)
|
|
|
|
|
|
GetByIDWithSubject(id string) (*model.MaterialFile, error)
|
|
|
|
|
|
Create(file *model.MaterialFile) error
|
|
|
|
|
|
Update(file *model.MaterialFile) error
|
|
|
|
|
|
Delete(id string) error
|
|
|
|
|
|
IncrementDownloadCount(id string) error
|
|
|
|
|
|
GetBySubjectID(subjectID string, page, pageSize int) ([]*model.MaterialFile, int64, error)
|
|
|
|
|
|
Search(keyword string, page, pageSize int) ([]*model.MaterialFile, int64, error)
|
|
|
|
|
|
GetHotMaterials(limit int) ([]*model.MaterialFile, error)
|
|
|
|
|
|
GetLatestMaterials(limit int) ([]*model.MaterialFile, error)
|
2026-03-25 20:44:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-26 18:14:16 +08:00
|
|
|
|
// materialFileRepository 文件资料仓储实现
|
|
|
|
|
|
type materialFileRepository struct {
|
|
|
|
|
|
db *gorm.DB
|
2026-03-25 20:44:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-26 18:14:16 +08:00
|
|
|
|
func NewMaterialFileRepository(db *gorm.DB) MaterialFileRepository {
|
|
|
|
|
|
return &materialFileRepository{db: db}
|
2026-03-25 20:44:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// List 获取资料列表(支持筛选和分页)
|
refactor(server): decouple services and improve architecture
- Introduce interfaces for all major services (JWT, PostAI, Comment, Message, Notification, QRCodeLogin, Upload, Vote, etc.) to support dependency inversion.
- Move query parameters from `internal/dto` to a new `internal/query` package to separate request payloads from data transfer objects.
- Refactor `internal/router` to use embedded `RouterDeps` for cleaner dependency management.
- Decouple handlers from repositories by injecting services instead of direct repository access, ensuring proper layering.
- Improve database initialization by moving it from `internal/model` to `internal/database`.
- Optimize message decryption by implementing a more efficient `BatchDecrypt` method in `MessageEncryptor` using a worker pool.
- Enhance error handling and security by implementing fail-fast checks for encryption key length during startup.
- Clean up unused code, including the `avatar` package and several unused DTOs.
2026-06-15 03:41:59 +08:00
|
|
|
|
func (r *materialFileRepository) List(params query.MaterialFileQueryParams) ([]*model.MaterialFile, int64, error) {
|
2026-03-25 20:44:12 +08:00
|
|
|
|
var files []*model.MaterialFile
|
|
|
|
|
|
var total int64
|
|
|
|
|
|
|
|
|
|
|
|
query := r.db.Model(&model.MaterialFile{})
|
|
|
|
|
|
|
|
|
|
|
|
// 筛选条件
|
|
|
|
|
|
if params.SubjectID != "" {
|
|
|
|
|
|
query = query.Where("subject_id = ?", params.SubjectID)
|
|
|
|
|
|
}
|
|
|
|
|
|
if params.FileType != "" {
|
|
|
|
|
|
query = query.Where("file_type = ?", params.FileType)
|
|
|
|
|
|
}
|
|
|
|
|
|
if params.Status != "" {
|
|
|
|
|
|
query = query.Where("status = ?", params.Status)
|
|
|
|
|
|
}
|
|
|
|
|
|
if params.Keyword != "" {
|
2026-04-30 12:26:25 +08:00
|
|
|
|
keyword := "%" + utils.EscapeLikeWildcard(params.Keyword) + "%"
|
2026-03-25 20:44:12 +08:00
|
|
|
|
query = query.Where("title LIKE ? OR description LIKE ?", keyword, keyword)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 统计总数
|
|
|
|
|
|
if err := query.Count(&total).Error; err != nil {
|
|
|
|
|
|
return nil, 0, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 排序
|
|
|
|
|
|
sortBy := params.SortBy
|
|
|
|
|
|
if sortBy == "" {
|
|
|
|
|
|
sortBy = "created_at"
|
|
|
|
|
|
}
|
|
|
|
|
|
sortOrder := params.SortOrder
|
|
|
|
|
|
if sortOrder == "" {
|
|
|
|
|
|
sortOrder = "DESC"
|
|
|
|
|
|
}
|
|
|
|
|
|
orderClause := sortBy + " " + sortOrder
|
|
|
|
|
|
|
|
|
|
|
|
// 分页
|
|
|
|
|
|
page := params.Page
|
|
|
|
|
|
if page < 1 {
|
|
|
|
|
|
page = 1
|
|
|
|
|
|
}
|
|
|
|
|
|
pageSize := params.PageSize
|
|
|
|
|
|
if pageSize < 1 {
|
|
|
|
|
|
pageSize = 20
|
|
|
|
|
|
}
|
|
|
|
|
|
offset := (page - 1) * pageSize
|
|
|
|
|
|
|
|
|
|
|
|
// 查询
|
|
|
|
|
|
if err := query.Order(orderClause).Offset(offset).Limit(pageSize).Find(&files).Error; err != nil {
|
|
|
|
|
|
return nil, 0, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return files, total, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetByID 根据ID获取资料详情
|
2026-03-26 18:14:16 +08:00
|
|
|
|
func (r *materialFileRepository) GetByID(id string) (*model.MaterialFile, error) {
|
2026-03-25 20:44:12 +08:00
|
|
|
|
var file model.MaterialFile
|
|
|
|
|
|
if err := r.db.First(&file, "id = ?", id).Error; err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
return &file, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetByIDWithSubject 根据ID获取资料详情(包含学科信息)
|
2026-03-26 18:14:16 +08:00
|
|
|
|
func (r *materialFileRepository) GetByIDWithSubject(id string) (*model.MaterialFile, error) {
|
2026-03-25 20:44:12 +08:00
|
|
|
|
var file model.MaterialFile
|
|
|
|
|
|
if err := r.db.Preload("Subject").First(&file, "id = ?", id).Error; err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
return &file, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Create 创建资料
|
2026-03-26 18:14:16 +08:00
|
|
|
|
func (r *materialFileRepository) Create(file *model.MaterialFile) error {
|
2026-03-25 20:44:12 +08:00
|
|
|
|
return r.db.Create(file).Error
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Update 更新资料
|
2026-03-26 18:14:16 +08:00
|
|
|
|
func (r *materialFileRepository) Update(file *model.MaterialFile) error {
|
2026-03-25 20:44:12 +08:00
|
|
|
|
return r.db.Save(file).Error
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Delete 删除资料
|
2026-03-26 18:14:16 +08:00
|
|
|
|
func (r *materialFileRepository) Delete(id string) error {
|
2026-03-25 20:44:12 +08:00
|
|
|
|
return r.db.Delete(&model.MaterialFile{}, "id = ?", id).Error
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// IncrementDownloadCount 增加下载次数
|
2026-03-26 18:14:16 +08:00
|
|
|
|
func (r *materialFileRepository) IncrementDownloadCount(id string) error {
|
2026-03-25 20:44:12 +08:00
|
|
|
|
return r.db.Model(&model.MaterialFile{}).Where("id = ?", id).
|
|
|
|
|
|
UpdateColumn("download_count", gorm.Expr("download_count + ?", 1)).Error
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetBySubjectID 根据学科ID获取资料列表
|
2026-03-26 18:14:16 +08:00
|
|
|
|
func (r *materialFileRepository) GetBySubjectID(subjectID string, page, pageSize int) ([]*model.MaterialFile, int64, error) {
|
2026-03-25 20:44:12 +08:00
|
|
|
|
var files []*model.MaterialFile
|
|
|
|
|
|
var total int64
|
|
|
|
|
|
|
|
|
|
|
|
query := r.db.Model(&model.MaterialFile{}).Where("subject_id = ? AND status = ?", subjectID, model.MaterialStatusActive)
|
|
|
|
|
|
|
|
|
|
|
|
if err := query.Count(&total).Error; err != nil {
|
|
|
|
|
|
return nil, 0, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
offset := (page - 1) * pageSize
|
|
|
|
|
|
if err := query.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&files).Error; err != nil {
|
|
|
|
|
|
return nil, 0, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return files, total, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Search 搜索资料
|
2026-03-26 18:14:16 +08:00
|
|
|
|
func (r *materialFileRepository) Search(keyword string, page, pageSize int) ([]*model.MaterialFile, int64, error) {
|
2026-03-25 20:44:12 +08:00
|
|
|
|
var files []*model.MaterialFile
|
|
|
|
|
|
var total int64
|
|
|
|
|
|
|
|
|
|
|
|
query := r.db.Model(&model.MaterialFile{}).Where("status = ?", model.MaterialStatusActive)
|
|
|
|
|
|
if keyword != "" {
|
2026-04-30 12:26:25 +08:00
|
|
|
|
kw := "%" + utils.EscapeLikeWildcard(keyword) + "%"
|
2026-03-25 20:44:12 +08:00
|
|
|
|
query = query.Where("title LIKE ? OR description LIKE ?", kw, kw)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if err := query.Count(&total).Error; err != nil {
|
|
|
|
|
|
return nil, 0, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
offset := (page - 1) * pageSize
|
|
|
|
|
|
if err := query.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&files).Error; err != nil {
|
|
|
|
|
|
return nil, 0, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return files, total, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetHotMaterials 获取热门资料(按下载次数排序)
|
2026-03-26 18:14:16 +08:00
|
|
|
|
func (r *materialFileRepository) GetHotMaterials(limit int) ([]*model.MaterialFile, error) {
|
2026-03-25 20:44:12 +08:00
|
|
|
|
var files []*model.MaterialFile
|
|
|
|
|
|
err := r.db.
|
|
|
|
|
|
Where("status = ?", model.MaterialStatusActive).
|
|
|
|
|
|
Order("download_count DESC").
|
|
|
|
|
|
Limit(limit).
|
|
|
|
|
|
Find(&files).Error
|
|
|
|
|
|
return files, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetLatestMaterials 获取最新资料
|
2026-03-26 18:14:16 +08:00
|
|
|
|
func (r *materialFileRepository) GetLatestMaterials(limit int) ([]*model.MaterialFile, error) {
|
2026-03-25 20:44:12 +08:00
|
|
|
|
var files []*model.MaterialFile
|
|
|
|
|
|
err := r.db.
|
|
|
|
|
|
Where("status = ?", model.MaterialStatusActive).
|
|
|
|
|
|
Order("created_at DESC").
|
|
|
|
|
|
Limit(limit).
|
|
|
|
|
|
Find(&files).Error
|
|
|
|
|
|
return files, err
|
refactor(server): decouple services and improve architecture
- Introduce interfaces for all major services (JWT, PostAI, Comment, Message, Notification, QRCodeLogin, Upload, Vote, etc.) to support dependency inversion.
- Move query parameters from `internal/dto` to a new `internal/query` package to separate request payloads from data transfer objects.
- Refactor `internal/router` to use embedded `RouterDeps` for cleaner dependency management.
- Decouple handlers from repositories by injecting services instead of direct repository access, ensuring proper layering.
- Improve database initialization by moving it from `internal/model` to `internal/database`.
- Optimize message decryption by implementing a more efficient `BatchDecrypt` method in `MessageEncryptor` using a worker pool.
- Enhance error handling and security by implementing fail-fast checks for encryption key length during startup.
- Clean up unused code, including the `avatar` package and several unused DTOs.
2026-06-15 03:41:59 +08:00
|
|
|
|
}
|