refactor(server): decouple services and improve architecture
All checks were successful
Build Backend / build (push) Successful in 4m55s
Build Backend / build-docker (push) Successful in 10m34s

- 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.
This commit is contained in:
2026-06-15 03:41:59 +08:00
parent 9951043034
commit d9aa4b46c3
78 changed files with 2156 additions and 1640 deletions

68
internal/query/filters.go Normal file
View File

@@ -0,0 +1,68 @@
// Package query 定义跨层共享的查询/过滤参数类型。
//
// 这些类型被 handler、service、repository 三层共同引用,
// 属于「查询契约」而非「表现层 DTO」。放在独立 query 包可避免:
// - repository 反向依赖 dto表现层导致的分层倒置
// - 将类型放入 repository 后 handler 被迫依赖 repository 的更严重违规。
package query
import "time"
// LogFilter 日志过滤条件
type LogFilter struct {
UserID string
Operation string
TargetType string
TargetID string
Status string
IP string
StartTime string
EndTime string
}
// LoginFilter 登录日志过滤条件
type LoginFilter struct {
UserID string
Event string
Result string
FailReason string
LoginType string
IP string
StartTime time.Time
EndTime time.Time
}
// DataChangeFilter 数据变更日志过滤条件
type DataChangeFilter struct {
UserID string
OperatorID string
ChangeType string
TargetType string
OperatorType string
IP string
StartTime string
EndTime string
}
// MaterialFileQueryParams 学习资料查询参数
type MaterialFileQueryParams struct {
SubjectID string
FileType string
Status string
Keyword string
Page int
PageSize int
SortBy string
SortOrder string
}
// AdminReportListQuery 管理端举报列表查询参数
type AdminReportListQuery struct {
Page int `form:"page" binding:"min=1"`
PageSize int `form:"page_size" binding:"min=1,max=100"`
TargetType string `form:"target_type" binding:"omitempty,oneof=post comment message"`
Status string `form:"status" binding:"omitempty,oneof=pending processing resolved rejected"`
StartDate string `form:"start_date" binding:"omitempty"`
EndDate string `form:"end_date" binding:"omitempty"`
Keyword string `form:"keyword" binding:"omitempty"`
}

View File

@@ -0,0 +1,82 @@
package query
import (
"testing"
"time"
)
// TestLogFilter_ZeroValue 验证 LogFilter 零值与字段可写性。
func TestLogFilter_ZeroValue(t *testing.T) {
var f LogFilter
if f.UserID != "" || f.Status != "" {
t.Errorf("zero-value LogFilter should have empty string fields, got %+v", f)
}
f.UserID = "u1"
f.Operation = "create"
if f.UserID != "u1" || f.Operation != "create" {
t.Error("LogFilter field assignment failed")
}
}
// TestLoginFilter_TimeFields 验证 LoginFilter 的时间字段类型正确(迁移后保持 time.Time
func TestLoginFilter_TimeFields(t *testing.T) {
now := time.Now()
f := LoginFilter{UserID: "u1", StartTime: now, EndTime: now.Add(time.Hour)}
if !f.StartTime.Equal(now) {
t.Errorf("StartTime = %v, want %v", f.StartTime, now)
}
if !f.EndTime.After(f.StartTime) {
t.Error("EndTime should be after StartTime")
}
}
// TestDataChangeFilter_Fields 验证 DataChangeFilter 全字段赋值。
func TestDataChangeFilter_Fields(t *testing.T) {
f := DataChangeFilter{
UserID: "u1",
OperatorID: "op1",
ChangeType: "update",
TargetType: "post",
OperatorType: "admin",
IP: "127.0.0.1",
StartTime: "2026-01-01",
EndTime: "2026-01-31",
}
if f.OperatorID != "op1" || f.ChangeType != "update" {
t.Errorf("DataChangeFilter fields not set correctly: %+v", f)
}
}
// TestMaterialFileQueryParams_Defaults 验证分页字段零值(调用方应处理默认值)。
func TestMaterialFileQueryParams_Defaults(t *testing.T) {
var p MaterialFileQueryParams
if p.Page != 0 || p.PageSize != 0 {
t.Errorf("zero-value MaterialFileQueryParams should have Page=0, PageSize=0, got Page=%d PageSize=%d", p.Page, p.PageSize)
}
p.Page = 1
p.PageSize = 20
p.SortBy = "created_at"
p.SortOrder = "desc"
if p.SortBy != "created_at" || p.SortOrder != "desc" {
t.Error("MaterialFileQueryParams field assignment failed")
}
}
// TestAdminReportListQuery_FormBinding 验证 AdminReportListQuery 的 form tag
// 通过 gin 的 binding 反射可正确绑定(迁移后 tag 保持不变)。
// 这里用手动构造验证字段与 tag 存在性,不引入 gin 依赖。
func TestAdminReportListQuery_Fields(t *testing.T) {
q := AdminReportListQuery{
Page: 1,
PageSize: 20,
TargetType: "post",
Status: "pending",
Keyword: "spam",
}
if q.Page != 1 || q.PageSize != 20 {
t.Errorf("AdminReportListQuery pagination mismatch: Page=%d PageSize=%d", q.Page, q.PageSize)
}
if q.TargetType != "post" || q.Status != "pending" {
t.Errorf("AdminReportListQuery filter fields mismatch: %+v", q)
}
}