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.
This commit is contained in:
68
internal/query/filters.go
Normal file
68
internal/query/filters.go
Normal 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"`
|
||||
}
|
||||
82
internal/query/filters_test.go
Normal file
82
internal/query/filters_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user