docs(constitution): 新增数据库设计原则(v2.4.0)
在项目宪章中新增第九条原则"数据库设计原则",明确禁止使用数据库外键约束和ORM关联标签。 主要变更: - 新增原则IX:数据库设计原则(Database Design Principles) - 强制要求:数据库表不得使用外键约束 - 强制要求:GORM模型不得使用ORM关联标签(foreignKey、hasMany等) - 强制要求:表关系必须通过ID字段手动维护 - 强制要求:关联数据查询必须显式编写,避免ORM魔法 - 强制要求:时间字段由GORM处理,不使用数据库触发器 设计理念: - 提升业务逻辑灵活性(无数据库约束限制) - 优化高并发性能(无外键检查开销) - 增强代码可读性(显式查询,无隐式预加载) - 简化数据库架构和迁移流程 - 支持分布式和微服务场景 版本升级:2.3.0 → 2.4.0(MINOR)
This commit is contained in:
@@ -15,6 +15,8 @@ var globalConfig atomic.Pointer[Config]
|
||||
type Config struct {
|
||||
Server ServerConfig `mapstructure:"server"`
|
||||
Redis RedisConfig `mapstructure:"redis"`
|
||||
Database DatabaseConfig `mapstructure:"database"`
|
||||
Queue QueueConfig `mapstructure:"queue"`
|
||||
Logging LoggingConfig `mapstructure:"logging"`
|
||||
Middleware MiddlewareConfig `mapstructure:"middleware"`
|
||||
}
|
||||
@@ -41,6 +43,27 @@ type RedisConfig struct {
|
||||
WriteTimeout time.Duration `mapstructure:"write_timeout"` // 例如 "3s"
|
||||
}
|
||||
|
||||
// DatabaseConfig 数据库连接配置
|
||||
type DatabaseConfig struct {
|
||||
Host string `mapstructure:"host"` // 数据库主机地址
|
||||
Port int `mapstructure:"port"` // 数据库端口
|
||||
User string `mapstructure:"user"` // 数据库用户名
|
||||
Password string `mapstructure:"password"` // 数据库密码(明文存储)
|
||||
DBName string `mapstructure:"dbname"` // 数据库名称
|
||||
SSLMode string `mapstructure:"sslmode"` // SSL 模式:disable, require, verify-ca, verify-full
|
||||
MaxOpenConns int `mapstructure:"max_open_conns"` // 最大打开连接数(默认:25)
|
||||
MaxIdleConns int `mapstructure:"max_idle_conns"` // 最大空闲连接数(默认:10)
|
||||
ConnMaxLifetime time.Duration `mapstructure:"conn_max_lifetime"` // 连接最大生命周期(默认:5m)
|
||||
}
|
||||
|
||||
// QueueConfig 任务队列配置
|
||||
type QueueConfig struct {
|
||||
Concurrency int `mapstructure:"concurrency"` // Worker 并发数(默认:10)
|
||||
Queues map[string]int `mapstructure:"queues"` // 队列优先级配置(队列名 -> 权重)
|
||||
RetryMax int `mapstructure:"retry_max"` // 最大重试次数(默认:5)
|
||||
Timeout time.Duration `mapstructure:"timeout"` // 任务超时时间(默认:10m)
|
||||
}
|
||||
|
||||
// LoggingConfig 日志配置
|
||||
type LoggingConfig struct {
|
||||
Level string `mapstructure:"level"` // debug, info, warn, error
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package constants
|
||||
|
||||
import "time"
|
||||
|
||||
// Fiber Locals 的上下文键
|
||||
const (
|
||||
ContextKeyRequestID = "requestid"
|
||||
@@ -19,3 +21,47 @@ const (
|
||||
DefaultServerAddr = ":3000"
|
||||
DefaultRedisAddr = "localhost:6379"
|
||||
)
|
||||
|
||||
// 数据库配置常量
|
||||
const (
|
||||
DefaultMaxOpenConns = 25
|
||||
DefaultMaxIdleConns = 10
|
||||
DefaultConnMaxLifetime = 5 * time.Minute
|
||||
DefaultPageSize = 20
|
||||
MaxPageSize = 100
|
||||
SlowQueryThreshold = 100 * time.Millisecond
|
||||
)
|
||||
|
||||
// 任务类型常量
|
||||
const (
|
||||
TaskTypeEmailSend = "email:send" // 发送邮件
|
||||
TaskTypeDataSync = "data:sync" // 数据同步
|
||||
TaskTypeSIMStatusSync = "sim:status:sync" // SIM 卡状态同步
|
||||
TaskTypeCommission = "commission:calculate" // 分佣计算
|
||||
)
|
||||
|
||||
// 用户状态常量
|
||||
const (
|
||||
UserStatusActive = "active" // 激活
|
||||
UserStatusInactive = "inactive" // 未激活
|
||||
UserStatusSuspended = "suspended" // 暂停
|
||||
)
|
||||
|
||||
// 订单状态常量
|
||||
const (
|
||||
OrderStatusPending = "pending" // 待支付
|
||||
OrderStatusPaid = "paid" // 已支付
|
||||
OrderStatusProcessing = "processing" // 处理中
|
||||
OrderStatusCompleted = "completed" // 已完成
|
||||
OrderStatusCancelled = "cancelled" // 已取消
|
||||
)
|
||||
|
||||
// 队列配置常量
|
||||
const (
|
||||
QueueCritical = "critical" // 关键任务队列
|
||||
QueueDefault = "default" // 默认队列
|
||||
QueueLow = "low" // 低优先级队列
|
||||
DefaultRetryMax = 5
|
||||
DefaultTimeout = 10 * time.Minute
|
||||
DefaultConcurrency = 10
|
||||
)
|
||||
|
||||
@@ -11,3 +11,17 @@ func RedisAuthTokenKey(token string) string {
|
||||
func RedisRateLimitKey(ip string) string {
|
||||
return fmt.Sprintf("ratelimit:%s", ip)
|
||||
}
|
||||
|
||||
// RedisTaskLockKey 生成任务锁的 Redis 键
|
||||
// 用途:幂等性控制,防止重复执行
|
||||
// 过期时间:24 小时
|
||||
func RedisTaskLockKey(requestID string) string {
|
||||
return fmt.Sprintf("task:lock:%s", requestID)
|
||||
}
|
||||
|
||||
// RedisTaskStatusKey 生成任务状态的 Redis 键
|
||||
// 用途:存储任务执行状态
|
||||
// 过期时间:7 天
|
||||
func RedisTaskStatusKey(taskID string) string {
|
||||
return fmt.Sprintf("task:status:%s", taskID)
|
||||
}
|
||||
|
||||
172
pkg/database/postgres.go
Normal file
172
pkg/database/postgres.go
Normal file
@@ -0,0 +1,172 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/pkg/config"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
// InitPostgreSQL 初始化 PostgreSQL 数据库连接
|
||||
func InitPostgreSQL(cfg *config.DatabaseConfig, log *zap.Logger) (*gorm.DB, error) {
|
||||
// 构建 DSN (数据源名称)
|
||||
dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
|
||||
cfg.Host,
|
||||
cfg.Port,
|
||||
cfg.User,
|
||||
cfg.Password,
|
||||
cfg.DBName,
|
||||
cfg.SSLMode,
|
||||
)
|
||||
|
||||
// 配置 GORM
|
||||
gormConfig := &gorm.Config{
|
||||
// 使用自定义日志器(集成 Zap)
|
||||
Logger: newGormLogger(log),
|
||||
// 禁用自动创建表(使用迁移脚本管理)
|
||||
DisableAutomaticPing: false,
|
||||
SkipDefaultTransaction: true, // 提高性能,手动管理事务
|
||||
PrepareStmt: true, // 预编译语句
|
||||
}
|
||||
|
||||
// 连接数据库
|
||||
db, err := gorm.Open(postgres.Open(dsn), gormConfig)
|
||||
if err != nil {
|
||||
log.Error("PostgreSQL 连接失败",
|
||||
zap.String("host", cfg.Host),
|
||||
zap.Int("port", cfg.Port),
|
||||
zap.String("dbname", cfg.DBName),
|
||||
zap.Error(err))
|
||||
return nil, fmt.Errorf("failed to connect to PostgreSQL: %w", err)
|
||||
}
|
||||
|
||||
// 获取底层 SQL DB 对象
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
log.Error("获取 SQL DB 失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("failed to get SQL DB: %w", err)
|
||||
}
|
||||
|
||||
// 配置连接池
|
||||
maxOpenConns := cfg.MaxOpenConns
|
||||
if maxOpenConns <= 0 {
|
||||
maxOpenConns = constants.DefaultMaxOpenConns
|
||||
}
|
||||
|
||||
maxIdleConns := cfg.MaxIdleConns
|
||||
if maxIdleConns <= 0 {
|
||||
maxIdleConns = constants.DefaultMaxIdleConns
|
||||
}
|
||||
|
||||
connMaxLifetime := cfg.ConnMaxLifetime
|
||||
if connMaxLifetime <= 0 {
|
||||
connMaxLifetime = constants.DefaultConnMaxLifetime
|
||||
}
|
||||
|
||||
sqlDB.SetMaxOpenConns(maxOpenConns)
|
||||
sqlDB.SetMaxIdleConns(maxIdleConns)
|
||||
sqlDB.SetConnMaxLifetime(connMaxLifetime)
|
||||
|
||||
// 验证连接
|
||||
if err := sqlDB.Ping(); err != nil {
|
||||
log.Error("PostgreSQL Ping 失败", zap.Error(err))
|
||||
return nil, fmt.Errorf("failed to ping PostgreSQL: %w", err)
|
||||
}
|
||||
|
||||
log.Info("PostgreSQL 连接成功",
|
||||
zap.String("host", cfg.Host),
|
||||
zap.Int("port", cfg.Port),
|
||||
zap.String("dbname", cfg.DBName),
|
||||
zap.Int("max_open_conns", maxOpenConns),
|
||||
zap.Int("max_idle_conns", maxIdleConns),
|
||||
zap.Duration("conn_max_lifetime", connMaxLifetime))
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
// gormLogger 自定义 GORM 日志器,集成 Zap
|
||||
type gormLogger struct {
|
||||
zap *zap.Logger
|
||||
slowQueryThreshold time.Duration
|
||||
ignoreRecordNotFound bool
|
||||
logLevel logger.LogLevel
|
||||
}
|
||||
|
||||
// newGormLogger 创建新的 GORM 日志器
|
||||
func newGormLogger(log *zap.Logger) logger.Interface {
|
||||
return &gormLogger{
|
||||
zap: log,
|
||||
slowQueryThreshold: constants.SlowQueryThreshold,
|
||||
ignoreRecordNotFound: true,
|
||||
logLevel: logger.Info,
|
||||
}
|
||||
}
|
||||
|
||||
// LogMode 设置日志级别
|
||||
func (l *gormLogger) LogMode(level logger.LogLevel) logger.Interface {
|
||||
newLogger := *l
|
||||
newLogger.logLevel = level
|
||||
return &newLogger
|
||||
}
|
||||
|
||||
// Info 记录 Info 级别日志
|
||||
func (l *gormLogger) Info(ctx context.Context, msg string, data ...interface{}) {
|
||||
if l.logLevel >= logger.Info {
|
||||
l.zap.Sugar().Infof(msg, data...)
|
||||
}
|
||||
}
|
||||
|
||||
// Warn 记录 Warn 级别日志
|
||||
func (l *gormLogger) Warn(ctx context.Context, msg string, data ...interface{}) {
|
||||
if l.logLevel >= logger.Warn {
|
||||
l.zap.Sugar().Warnf(msg, data...)
|
||||
}
|
||||
}
|
||||
|
||||
// Error 记录 Error 级别日志
|
||||
func (l *gormLogger) Error(ctx context.Context, msg string, data ...interface{}) {
|
||||
if l.logLevel >= logger.Error {
|
||||
l.zap.Sugar().Errorf(msg, data...)
|
||||
}
|
||||
}
|
||||
|
||||
// Trace 记录 SQL 查询日志
|
||||
func (l *gormLogger) Trace(ctx context.Context, begin time.Time, fc func() (string, int64), err error) {
|
||||
if l.logLevel <= logger.Silent {
|
||||
return
|
||||
}
|
||||
|
||||
elapsed := time.Since(begin)
|
||||
sql, rows := fc()
|
||||
|
||||
switch {
|
||||
case err != nil && l.logLevel >= logger.Error && (!l.ignoreRecordNotFound || err != gorm.ErrRecordNotFound):
|
||||
// 查询错误
|
||||
l.zap.Error("SQL 查询失败",
|
||||
zap.String("sql", sql),
|
||||
zap.Int64("rows", rows),
|
||||
zap.Duration("elapsed", elapsed),
|
||||
zap.Error(err))
|
||||
|
||||
case elapsed > l.slowQueryThreshold && l.logLevel >= logger.Warn:
|
||||
// 慢查询
|
||||
l.zap.Warn("慢查询检测",
|
||||
zap.String("sql", sql),
|
||||
zap.Int64("rows", rows),
|
||||
zap.Duration("elapsed", elapsed),
|
||||
zap.Duration("threshold", l.slowQueryThreshold))
|
||||
|
||||
case l.logLevel >= logger.Info:
|
||||
// 正常查询
|
||||
l.zap.Debug("SQL 查询",
|
||||
zap.String("sql", sql),
|
||||
zap.Int64("rows", rows),
|
||||
zap.Duration("elapsed", elapsed))
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,10 @@ const (
|
||||
CodeInvalidToken = 1002 // 令牌无效或已过期
|
||||
CodeTooManyRequests = 1003 // 请求过于频繁(限流)
|
||||
CodeAuthServiceUnavailable = 1004 // 认证服务不可用(Redis 宕机)
|
||||
CodeNotFound = 1005 // 资源不存在
|
||||
CodeBadRequest = 1006 // 请求参数错误
|
||||
CodeUnauthorized = 1007 // 未授权
|
||||
CodeForbidden = 1008 // 禁止访问
|
||||
)
|
||||
|
||||
// ErrorMessage 表示双语错误消息
|
||||
@@ -24,6 +28,10 @@ var errorMessages = map[int]ErrorMessage{
|
||||
CodeInvalidToken: {"Invalid or expired token", "令牌无效或已过期"},
|
||||
CodeTooManyRequests: {"Too many requests", "请求过于频繁"},
|
||||
CodeAuthServiceUnavailable: {"Authentication service unavailable", "认证服务不可用"},
|
||||
CodeNotFound: {"Resource not found", "资源不存在"},
|
||||
CodeBadRequest: {"Bad request", "请求参数错误"},
|
||||
CodeUnauthorized: {"Unauthorized", "未授权"},
|
||||
CodeForbidden: {"Forbidden", "禁止访问"},
|
||||
}
|
||||
|
||||
// GetMessage 根据错误码和语言返回错误消息
|
||||
|
||||
88
pkg/queue/client.go
Normal file
88
pkg/queue/client.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package queue
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/pkg/config"
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/hibiken/asynq"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// Client Asynq 任务提交客户端
|
||||
type Client struct {
|
||||
client *asynq.Client
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewClient 创建新的 Asynq 客户端
|
||||
func NewClient(redisClient *redis.Client, logger *zap.Logger) *Client {
|
||||
// 从 Redis 客户端获取配置
|
||||
opts := redisClient.Options()
|
||||
|
||||
asynqClient := asynq.NewClient(asynq.RedisClientOpt{
|
||||
Addr: opts.Addr,
|
||||
Password: opts.Password,
|
||||
DB: opts.DB,
|
||||
})
|
||||
|
||||
return &Client{
|
||||
client: asynqClient,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// EnqueueTask 提交任务到队列
|
||||
func (c *Client) EnqueueTask(ctx context.Context, taskType string, payload interface{}, opts ...asynq.Option) error {
|
||||
// 序列化载荷
|
||||
payloadBytes, err := sonic.Marshal(payload)
|
||||
if err != nil {
|
||||
c.logger.Error("任务载荷序列化失败",
|
||||
zap.String("task_type", taskType),
|
||||
zap.Error(err))
|
||||
return fmt.Errorf("failed to marshal task payload: %w", err)
|
||||
}
|
||||
|
||||
// 创建任务
|
||||
task := asynq.NewTask(taskType, payloadBytes, opts...)
|
||||
|
||||
// 提交任务
|
||||
info, err := c.client.EnqueueContext(ctx, task)
|
||||
if err != nil {
|
||||
c.logger.Error("任务提交失败",
|
||||
zap.String("task_type", taskType),
|
||||
zap.Error(err))
|
||||
return fmt.Errorf("failed to enqueue task: %w", err)
|
||||
}
|
||||
|
||||
c.logger.Info("任务已提交",
|
||||
zap.String("task_id", info.ID),
|
||||
zap.String("task_type", taskType),
|
||||
zap.String("queue", info.Queue),
|
||||
zap.Int("max_retry", info.MaxRetry))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close 关闭客户端
|
||||
func (c *Client) Close() error {
|
||||
if c.client != nil {
|
||||
return c.client.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ParseQueueConfig 解析队列配置为 Asynq 格式
|
||||
func ParseQueueConfig(cfg *config.QueueConfig) map[string]int {
|
||||
if cfg.Queues != nil && len(cfg.Queues) > 0 {
|
||||
return cfg.Queues
|
||||
}
|
||||
// 默认队列优先级
|
||||
return map[string]int{
|
||||
"critical": 6,
|
||||
"default": 3,
|
||||
"low": 1,
|
||||
}
|
||||
}
|
||||
57
pkg/queue/handler.go
Normal file
57
pkg/queue/handler.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package queue
|
||||
|
||||
import (
|
||||
"github.com/hibiken/asynq"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/task"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
)
|
||||
|
||||
// Handler 任务处理器注册
|
||||
type Handler struct {
|
||||
mux *asynq.ServeMux
|
||||
logger *zap.Logger
|
||||
db *gorm.DB
|
||||
redis *redis.Client
|
||||
}
|
||||
|
||||
// NewHandler 创建任务处理器
|
||||
func NewHandler(db *gorm.DB, redis *redis.Client, logger *zap.Logger) *Handler {
|
||||
return &Handler{
|
||||
mux: asynq.NewServeMux(),
|
||||
logger: logger,
|
||||
db: db,
|
||||
redis: redis,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterHandlers 注册所有任务处理器
|
||||
func (h *Handler) RegisterHandlers() *asynq.ServeMux {
|
||||
// 创建任务处理器实例
|
||||
emailHandler := task.NewEmailHandler(h.redis, h.logger)
|
||||
syncHandler := task.NewSyncHandler(h.db, h.logger)
|
||||
simHandler := task.NewSIMHandler(h.db, h.redis, h.logger)
|
||||
|
||||
// 注册邮件发送任务
|
||||
h.mux.HandleFunc(constants.TaskTypeEmailSend, emailHandler.HandleEmailSend)
|
||||
h.logger.Info("注册邮件发送任务处理器", zap.String("task_type", constants.TaskTypeEmailSend))
|
||||
|
||||
// 注册数据同步任务
|
||||
h.mux.HandleFunc(constants.TaskTypeDataSync, syncHandler.HandleDataSync)
|
||||
h.logger.Info("注册数据同步任务处理器", zap.String("task_type", constants.TaskTypeDataSync))
|
||||
|
||||
// 注册 SIM 卡状态同步任务
|
||||
h.mux.HandleFunc(constants.TaskTypeSIMStatusSync, simHandler.HandleSIMStatusSync)
|
||||
h.logger.Info("注册 SIM 状态同步任务处理器", zap.String("task_type", constants.TaskTypeSIMStatusSync))
|
||||
|
||||
h.logger.Info("所有任务处理器注册完成")
|
||||
return h.mux
|
||||
}
|
||||
|
||||
// GetMux 获取 ServeMux(用于启动 Worker 服务器)
|
||||
func (h *Handler) GetMux() *asynq.ServeMux {
|
||||
return h.mux
|
||||
}
|
||||
86
pkg/queue/server.go
Normal file
86
pkg/queue/server.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package queue
|
||||
|
||||
import (
|
||||
"github.com/break/junhong_cmp_fiber/pkg/config"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/hibiken/asynq"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// Server Asynq Worker 服务器
|
||||
type Server struct {
|
||||
server *asynq.Server
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewServer 创建新的 Asynq 服务器
|
||||
func NewServer(redisClient *redis.Client, queueCfg *config.QueueConfig, logger *zap.Logger) *Server {
|
||||
// 从 Redis 客户端获取配置
|
||||
opts := redisClient.Options()
|
||||
|
||||
// 解析队列优先级配置
|
||||
queues := ParseQueueConfig(queueCfg)
|
||||
|
||||
// 设置并发数
|
||||
concurrency := queueCfg.Concurrency
|
||||
if concurrency <= 0 {
|
||||
concurrency = constants.DefaultConcurrency
|
||||
}
|
||||
|
||||
// 创建 Asynq 服务器配置
|
||||
asynqServer := asynq.NewServer(
|
||||
asynq.RedisClientOpt{
|
||||
Addr: opts.Addr,
|
||||
Password: opts.Password,
|
||||
DB: opts.DB,
|
||||
},
|
||||
asynq.Config{
|
||||
// 并发数
|
||||
Concurrency: concurrency,
|
||||
// 队列优先级配置
|
||||
Queues: queues,
|
||||
// 重试延迟函数(指数退避)
|
||||
RetryDelayFunc: asynq.DefaultRetryDelayFunc,
|
||||
// 是否记录详细日志
|
||||
LogLevel: asynq.WarnLevel,
|
||||
},
|
||||
)
|
||||
|
||||
return &Server{
|
||||
server: asynqServer,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Start 启动 Worker 服务器
|
||||
func (s *Server) Start(mux *asynq.ServeMux) error {
|
||||
s.logger.Info("Worker 服务器启动中...")
|
||||
|
||||
if err := s.server.Start(mux); err != nil {
|
||||
s.logger.Error("Worker 服务器启动失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
s.logger.Info("Worker 服务器启动成功")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Shutdown 优雅关闭服务器
|
||||
func (s *Server) Shutdown() {
|
||||
s.logger.Info("Worker 服务器关闭中...")
|
||||
s.server.Shutdown()
|
||||
s.logger.Info("Worker 服务器已关闭")
|
||||
}
|
||||
|
||||
// Run 启动并阻塞运行(用于主函数)
|
||||
func (s *Server) Run(mux *asynq.ServeMux) error {
|
||||
s.logger.Info("Worker 服务器启动中...")
|
||||
|
||||
if err := s.server.Run(mux); err != nil {
|
||||
s.logger.Error("Worker 服务器运行失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
package response
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
@@ -83,7 +83,7 @@ func TestSuccess(t *testing.T) {
|
||||
}
|
||||
|
||||
var response Response
|
||||
if err := json.Unmarshal(body, &response); err != nil {
|
||||
if err := sonic.Unmarshal(body, &response); err != nil {
|
||||
t.Fatalf("Failed to unmarshal response: %v", err)
|
||||
}
|
||||
|
||||
@@ -188,7 +188,7 @@ func TestError(t *testing.T) {
|
||||
}
|
||||
|
||||
var response Response
|
||||
if err := json.Unmarshal(body, &response); err != nil {
|
||||
if err := sonic.Unmarshal(body, &response); err != nil {
|
||||
t.Fatalf("Failed to unmarshal response: %v", err)
|
||||
}
|
||||
|
||||
@@ -272,7 +272,7 @@ func TestSuccessWithMessage(t *testing.T) {
|
||||
}
|
||||
|
||||
var response Response
|
||||
if err := json.Unmarshal(body, &response); err != nil {
|
||||
if err := sonic.Unmarshal(body, &response); err != nil {
|
||||
t.Fatalf("Failed to unmarshal response: %v", err)
|
||||
}
|
||||
|
||||
@@ -337,14 +337,14 @@ func TestResponseSerialization(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// 序列化
|
||||
data, err := json.Marshal(tt.response)
|
||||
data, err := sonic.Marshal(tt.response)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal response: %v", err)
|
||||
}
|
||||
|
||||
// 反序列化
|
||||
var deserialized Response
|
||||
if err := json.Unmarshal(data, &deserialized); err != nil {
|
||||
if err := sonic.Unmarshal(data, &deserialized); err != nil {
|
||||
t.Fatalf("Failed to unmarshal response: %v", err)
|
||||
}
|
||||
|
||||
@@ -373,14 +373,14 @@ func TestResponseStructFields(t *testing.T) {
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
}
|
||||
|
||||
data, err := json.Marshal(response)
|
||||
data, err := sonic.Marshal(response)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal response: %v", err)
|
||||
}
|
||||
|
||||
// 解析为 map 以检查 JSON 键
|
||||
var jsonMap map[string]any
|
||||
if err := json.Unmarshal(data, &jsonMap); err != nil {
|
||||
if err := sonic.Unmarshal(data, &jsonMap); err != nil {
|
||||
t.Fatalf("Failed to unmarshal to map: %v", err)
|
||||
}
|
||||
|
||||
@@ -431,7 +431,7 @@ func TestMultipleResponses(t *testing.T) {
|
||||
resp.Body.Close()
|
||||
|
||||
var response Response
|
||||
if err := json.Unmarshal(body, &response); err != nil {
|
||||
if err := sonic.Unmarshal(body, &response); err != nil {
|
||||
t.Fatalf("Request %d: failed to unmarshal response: %v", i, err)
|
||||
}
|
||||
|
||||
@@ -458,7 +458,7 @@ func TestTimestampFormat(t *testing.T) {
|
||||
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
var response Response
|
||||
if err := json.Unmarshal(body, &response); err != nil {
|
||||
if err := sonic.Unmarshal(body, &response); err != nil {
|
||||
t.Fatalf("Failed to unmarshal response: %v", err)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user