在项目宪章中新增第九条原则"数据库设计原则",明确禁止使用数据库外键约束和ORM关联标签。 主要变更: - 新增原则IX:数据库设计原则(Database Design Principles) - 强制要求:数据库表不得使用外键约束 - 强制要求:GORM模型不得使用ORM关联标签(foreignKey、hasMany等) - 强制要求:表关系必须通过ID字段手动维护 - 强制要求:关联数据查询必须显式编写,避免ORM魔法 - 强制要求:时间字段由GORM处理,不使用数据库触发器 设计理念: - 提升业务逻辑灵活性(无数据库约束限制) - 优化高并发性能(无外键检查开销) - 增强代码可读性(显式查询,无隐式预加载) - 简化数据库架构和迁移流程 - 支持分布式和微服务场景 版本升级:2.3.0 → 2.4.0(MINOR)
830 lines
17 KiB
Markdown
830 lines
17 KiB
Markdown
# Quick Start Guide: 数据持久化与异步任务处理集成
|
||
|
||
**Feature**: 002-gorm-postgres-asynq
|
||
**Date**: 2025-11-12
|
||
**Purpose**: 快速开始指南和使用示例
|
||
|
||
## 概述
|
||
|
||
本指南帮助开发者快速搭建和使用 GORM + PostgreSQL + Asynq 集成的数据持久化和异步任务处理功能。
|
||
|
||
---
|
||
|
||
## 前置要求
|
||
|
||
### 系统要求
|
||
|
||
- Go 1.25.4+
|
||
- PostgreSQL 14+
|
||
- Redis 6.0+
|
||
- golang-migrate CLI 工具
|
||
|
||
### 安装依赖
|
||
|
||
```bash
|
||
# 安装 Go 依赖
|
||
go mod tidy
|
||
|
||
# 安装 golang-migrate(macOS)
|
||
brew install golang-migrate
|
||
|
||
# 安装 golang-migrate(Linux)
|
||
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.15.2/migrate.linux-amd64.tar.gz | tar xvz
|
||
sudo mv migrate /usr/local/bin/
|
||
|
||
# 或使用 Go install
|
||
go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest
|
||
```
|
||
|
||
---
|
||
|
||
## 步骤 1: 启动 PostgreSQL
|
||
|
||
### 使用 Docker(推荐)
|
||
|
||
```bash
|
||
# 启动 PostgreSQL 容器
|
||
docker run --name postgres-dev \
|
||
-e POSTGRES_USER=postgres \
|
||
-e POSTGRES_PASSWORD=password \
|
||
-e POSTGRES_DB=junhong_cmp \
|
||
-p 5432:5432 \
|
||
-d postgres:14
|
||
|
||
# 验证运行状态
|
||
docker ps | grep postgres-dev
|
||
```
|
||
|
||
### 使用本地安装
|
||
|
||
```bash
|
||
# macOS
|
||
brew install postgresql@14
|
||
brew services start postgresql@14
|
||
|
||
# 创建数据库
|
||
createdb junhong_cmp
|
||
```
|
||
|
||
### 验证连接
|
||
|
||
```bash
|
||
# 测试连接
|
||
psql -h localhost -p 5432 -U postgres -d junhong_cmp
|
||
|
||
# 如果成功,会进入 PostgreSQL 命令行
|
||
# 输入 \q 退出
|
||
```
|
||
|
||
---
|
||
|
||
## 步骤 2: 启动 Redis
|
||
|
||
```bash
|
||
# 使用 Docker
|
||
docker run --name redis-dev \
|
||
-p 6379:6379 \
|
||
-d redis:7-alpine
|
||
|
||
# 或使用本地安装(macOS)
|
||
brew install redis
|
||
brew services start redis
|
||
|
||
# 验证 Redis
|
||
redis-cli ping
|
||
# 应返回: PONG
|
||
```
|
||
|
||
---
|
||
|
||
## 步骤 3: 配置数据库连接
|
||
|
||
编辑配置文件 `configs/config.yaml`,添加数据库和队列配置:
|
||
|
||
```yaml
|
||
# configs/config.yaml
|
||
|
||
# 数据库配置
|
||
database:
|
||
host: localhost
|
||
port: 5432
|
||
user: postgres
|
||
password: password # 开发环境明文存储,生产环境使用环境变量
|
||
dbname: junhong_cmp
|
||
sslmode: disable # 开发环境禁用 SSL,生产环境使用 require
|
||
max_open_conns: 25
|
||
max_idle_conns: 10
|
||
conn_max_lifetime: 5m
|
||
|
||
# 任务队列配置
|
||
queue:
|
||
concurrency: 10 # Worker 并发数
|
||
queues: # 队列优先级(权重)
|
||
critical: 6 # 关键任务:60%
|
||
default: 3 # 普通任务:30%
|
||
low: 1 # 低优先级:10%
|
||
retry_max: 5 # 最大重试次数
|
||
timeout: 10m # 任务超时时间
|
||
```
|
||
|
||
---
|
||
|
||
## 步骤 4: 运行数据库迁移
|
||
|
||
### 方法 1: 使用迁移脚本(推荐)
|
||
|
||
```bash
|
||
# 赋予执行权限
|
||
chmod +x scripts/migrate.sh
|
||
|
||
# 向上迁移(应用所有迁移)
|
||
./scripts/migrate.sh up
|
||
|
||
# 查看当前版本
|
||
./scripts/migrate.sh version
|
||
|
||
# 回滚最后一次迁移
|
||
./scripts/migrate.sh down 1
|
||
|
||
# 创建新迁移
|
||
./scripts/migrate.sh create add_sim_table
|
||
```
|
||
|
||
### 方法 2: 直接使用 migrate CLI
|
||
|
||
```bash
|
||
# 设置数据库 URL
|
||
export DATABASE_URL="postgresql://postgres:password@localhost:5432/junhong_cmp?sslmode=disable"
|
||
|
||
# 向上迁移
|
||
migrate -path migrations -database "$DATABASE_URL" up
|
||
|
||
# 查看版本
|
||
migrate -path migrations -database "$DATABASE_URL" version
|
||
```
|
||
|
||
### 验证迁移成功
|
||
|
||
```bash
|
||
# 连接数据库
|
||
psql -h localhost -p 5432 -U postgres -d junhong_cmp
|
||
|
||
# 查看表
|
||
\dt
|
||
|
||
# 应该看到:
|
||
# tb_user
|
||
# tb_order
|
||
# schema_migrations(由 golang-migrate 创建)
|
||
|
||
# 退出
|
||
\q
|
||
```
|
||
|
||
---
|
||
|
||
## 步骤 5: 启动 API 服务
|
||
|
||
```bash
|
||
# 从项目根目录运行
|
||
go run cmd/api/main.go
|
||
|
||
# 预期输出:
|
||
# {"level":"info","timestamp":"...","message":"PostgreSQL 连接成功","host":"localhost","port":5432}
|
||
# {"level":"info","timestamp":"...","message":"Redis 连接成功","addr":"localhost:6379"}
|
||
# {"level":"info","timestamp":"...","message":"服务启动成功","host":"0.0.0.0","port":8080}
|
||
```
|
||
|
||
### 验证 API 服务
|
||
|
||
```bash
|
||
# 测试健康检查
|
||
curl http://localhost:8080/health
|
||
|
||
# 预期响应:
|
||
# {
|
||
# "status": "ok",
|
||
# "postgres": "up",
|
||
# "redis": "up"
|
||
# }
|
||
```
|
||
|
||
---
|
||
|
||
## 步骤 6: 启动 Worker 服务
|
||
|
||
打开新的终端窗口:
|
||
|
||
```bash
|
||
# 从项目根目录运行
|
||
go run cmd/worker/main.go
|
||
|
||
# 预期输出:
|
||
# {"level":"info","timestamp":"...","message":"PostgreSQL 连接成功","host":"localhost","port":5432}
|
||
# {"level":"info","timestamp":"...","message":"Redis 连接成功","addr":"localhost:6379"}
|
||
# {"level":"info","timestamp":"...","message":"Worker 启动成功","concurrency":10}
|
||
```
|
||
|
||
---
|
||
|
||
## 使用示例
|
||
|
||
### 示例 1: 数据库 CRUD 操作
|
||
|
||
#### 创建用户
|
||
|
||
```bash
|
||
curl -X POST http://localhost:8080/api/v1/users \
|
||
-H "Content-Type: application/json" \
|
||
-H "token: valid_token_here" \
|
||
-d '{
|
||
"username": "testuser",
|
||
"email": "test@example.com",
|
||
"password": "password123"
|
||
}'
|
||
|
||
# 响应:
|
||
# {
|
||
# "code": 0,
|
||
# "msg": "success",
|
||
# "data": {
|
||
# "id": 1,
|
||
# "username": "testuser",
|
||
# "email": "test@example.com",
|
||
# "status": "active",
|
||
# "created_at": "2025-11-12T16:00:00+08:00",
|
||
# "updated_at": "2025-11-12T16:00:00+08:00"
|
||
# },
|
||
# "timestamp": "2025-11-12T16:00:00+08:00"
|
||
# }
|
||
```
|
||
|
||
#### 查询用户
|
||
|
||
```bash
|
||
curl http://localhost:8080/api/v1/users/1 \
|
||
-H "token: valid_token_here"
|
||
|
||
# 响应:
|
||
# {
|
||
# "code": 0,
|
||
# "msg": "success",
|
||
# "data": {
|
||
# "id": 1,
|
||
# "username": "testuser",
|
||
# "email": "test@example.com",
|
||
# "status": "active",
|
||
# ...
|
||
# }
|
||
# }
|
||
```
|
||
|
||
#### 更新用户
|
||
|
||
```bash
|
||
curl -X PUT http://localhost:8080/api/v1/users/1 \
|
||
-H "Content-Type: application/json" \
|
||
-H "token: valid_token_here" \
|
||
-d '{
|
||
"email": "newemail@example.com",
|
||
"status": "inactive"
|
||
}'
|
||
```
|
||
|
||
#### 列表查询(分页)
|
||
|
||
```bash
|
||
curl "http://localhost:8080/api/v1/users?page=1&page_size=20" \
|
||
-H "token: valid_token_here"
|
||
|
||
# 响应:
|
||
# {
|
||
# "code": 0,
|
||
# "msg": "success",
|
||
# "data": {
|
||
# "users": [...],
|
||
# "page": 1,
|
||
# "page_size": 20,
|
||
# "total": 100,
|
||
# "total_pages": 5
|
||
# }
|
||
# }
|
||
```
|
||
|
||
#### 删除用户(软删除)
|
||
|
||
```bash
|
||
curl -X DELETE http://localhost:8080/api/v1/users/1 \
|
||
-H "token: valid_token_here"
|
||
```
|
||
|
||
### 示例 2: 提交异步任务
|
||
|
||
#### 提交邮件发送任务
|
||
|
||
```bash
|
||
curl -X POST http://localhost:8080/api/v1/tasks/email \
|
||
-H "Content-Type: application/json" \
|
||
-H "token: valid_token_here" \
|
||
-d '{
|
||
"to": "user@example.com",
|
||
"subject": "Welcome",
|
||
"body": "Welcome to our service!"
|
||
}'
|
||
|
||
# 响应:
|
||
# {
|
||
# "code": 0,
|
||
# "msg": "任务已提交",
|
||
# "data": {
|
||
# "task_id": "550e8400-e29b-41d4-a716-446655440000",
|
||
# "queue": "default"
|
||
# }
|
||
# }
|
||
```
|
||
|
||
#### 提交数据同步任务(高优先级)
|
||
|
||
```bash
|
||
curl -X POST http://localhost:8080/api/v1/tasks/sync \
|
||
-H "Content-Type: application/json" \
|
||
-H "token: valid_token_here" \
|
||
-d '{
|
||
"sync_type": "sim_status",
|
||
"start_date": "2025-11-01",
|
||
"end_date": "2025-11-12",
|
||
"priority": "critical"
|
||
}'
|
||
```
|
||
|
||
### 示例 3: 直接在代码中使用数据库
|
||
|
||
```go
|
||
// internal/service/user/service.go
|
||
package user
|
||
|
||
import (
|
||
"context"
|
||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||
)
|
||
|
||
type Service struct {
|
||
store *postgres.Store
|
||
logger *zap.Logger
|
||
}
|
||
|
||
// CreateUser 创建用户
|
||
func (s *Service) CreateUser(ctx context.Context, req *model.CreateUserRequest) (*model.User, error) {
|
||
// 参数验证
|
||
if err := validate.Struct(req); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 密码哈希
|
||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 创建用户
|
||
user := &model.User{
|
||
Username: req.Username,
|
||
Email: req.Email,
|
||
Password: string(hashedPassword),
|
||
Status: constants.UserStatusActive,
|
||
}
|
||
|
||
if err := s.store.User.Create(ctx, user); err != nil {
|
||
s.logger.Error("创建用户失败",
|
||
zap.String("username", req.Username),
|
||
zap.Error(err))
|
||
return nil, err
|
||
}
|
||
|
||
s.logger.Info("用户创建成功",
|
||
zap.Uint("user_id", user.ID),
|
||
zap.String("username", user.Username))
|
||
|
||
return user, nil
|
||
}
|
||
|
||
// GetUserByID 根据 ID 获取用户
|
||
func (s *Service) GetUserByID(ctx context.Context, id uint) (*model.User, error) {
|
||
user, err := s.store.User.GetByID(ctx, id)
|
||
if err != nil {
|
||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||
return nil, errors.New(errors.CodeNotFound, "用户不存在")
|
||
}
|
||
return nil, err
|
||
}
|
||
return user, nil
|
||
}
|
||
```
|
||
|
||
### 示例 4: 在代码中提交异步任务
|
||
|
||
```go
|
||
// internal/service/email/service.go
|
||
package email
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"github.com/break/junhong_cmp_fiber/internal/task"
|
||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||
"github.com/break/junhong_cmp_fiber/pkg/queue"
|
||
"github.com/hibiken/asynq"
|
||
)
|
||
|
||
type Service struct {
|
||
queueClient *queue.Client
|
||
logger *zap.Logger
|
||
}
|
||
|
||
// SendWelcomeEmail 发送欢迎邮件(异步)
|
||
func (s *Service) SendWelcomeEmail(ctx context.Context, userID uint, email string) error {
|
||
// 构造任务载荷
|
||
payload := &task.EmailPayload{
|
||
RequestID: fmt.Sprintf("welcome-%d", userID),
|
||
To: email,
|
||
Subject: "欢迎加入",
|
||
Body: "感谢您注册我们的服务!",
|
||
}
|
||
|
||
payloadBytes, err := json.Marshal(payload)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
// 提交任务到队列
|
||
err = s.queueClient.EnqueueTask(
|
||
ctx,
|
||
constants.TaskTypeEmailSend,
|
||
payloadBytes,
|
||
asynq.Queue(constants.QueueDefault),
|
||
asynq.MaxRetry(constants.DefaultRetryMax),
|
||
)
|
||
|
||
if err != nil {
|
||
s.logger.Error("提交邮件任务失败",
|
||
zap.Uint("user_id", userID),
|
||
zap.String("email", email),
|
||
zap.Error(err))
|
||
return err
|
||
}
|
||
|
||
s.logger.Info("欢迎邮件任务已提交",
|
||
zap.Uint("user_id", userID),
|
||
zap.String("email", email))
|
||
|
||
return nil
|
||
}
|
||
```
|
||
|
||
### 示例 5: 事务处理
|
||
|
||
```go
|
||
// internal/service/order/service.go
|
||
package order
|
||
|
||
// CreateOrderWithUser 创建订单并更新用户统计(事务)
|
||
func (s *Service) CreateOrderWithUser(ctx context.Context, req *CreateOrderRequest) (*model.Order, error) {
|
||
var order *model.Order
|
||
|
||
// 使用事务
|
||
err := s.store.Transaction(ctx, func(tx *postgres.Store) error {
|
||
// 1. 创建订单
|
||
order = &model.Order{
|
||
OrderID: generateOrderID(),
|
||
UserID: req.UserID,
|
||
Amount: req.Amount,
|
||
Status: constants.OrderStatusPending,
|
||
}
|
||
|
||
if err := tx.Order.Create(ctx, order); err != nil {
|
||
return err
|
||
}
|
||
|
||
// 2. 更新用户订单计数
|
||
user, err := tx.User.GetByID(ctx, req.UserID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
user.OrderCount++
|
||
if err := tx.User.Update(ctx, user); err != nil {
|
||
return err
|
||
}
|
||
|
||
return nil // 提交事务
|
||
})
|
||
|
||
if err != nil {
|
||
s.logger.Error("创建订单失败",
|
||
zap.Uint("user_id", req.UserID),
|
||
zap.Error(err))
|
||
return nil, err
|
||
}
|
||
|
||
return order, nil
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 监控和调试
|
||
|
||
### 查看数据库数据
|
||
|
||
```bash
|
||
# 连接数据库
|
||
psql -h localhost -p 5432 -U postgres -d junhong_cmp
|
||
|
||
# 查询用户
|
||
SELECT * FROM tb_user;
|
||
|
||
# 查询订单
|
||
SELECT * FROM tb_order WHERE user_id = 1;
|
||
|
||
# 查看迁移历史
|
||
SELECT * FROM schema_migrations;
|
||
```
|
||
|
||
### 查看任务队列状态
|
||
|
||
#### 使用 asynqmon(Web UI)
|
||
|
||
```bash
|
||
# 安装 asynqmon
|
||
go install github.com/hibiken/asynqmon@latest
|
||
|
||
# 启动监控面板
|
||
asynqmon --redis-addr=localhost:6379
|
||
|
||
# 访问 http://localhost:8080
|
||
# 可以查看:
|
||
# - 队列统计
|
||
# - 任务状态(pending, active, completed, failed)
|
||
# - 重试历史
|
||
# - 失败任务详情
|
||
```
|
||
|
||
#### 使用 Redis CLI
|
||
|
||
```bash
|
||
# 查看所有队列
|
||
redis-cli KEYS "asynq:*"
|
||
|
||
# 查看 default 队列长度
|
||
redis-cli LLEN "asynq:{default}:pending"
|
||
|
||
# 查看任务详情
|
||
redis-cli HGETALL "asynq:task:{task_id}"
|
||
```
|
||
|
||
### 查看日志
|
||
|
||
```bash
|
||
# 实时查看应用日志
|
||
tail -f logs/app.log | jq .
|
||
|
||
# 过滤错误日志
|
||
tail -f logs/app.log | jq 'select(.level == "error")'
|
||
|
||
# 查看访问日志
|
||
tail -f logs/access.log | jq .
|
||
|
||
# 过滤慢查询
|
||
tail -f logs/app.log | jq 'select(.duration_ms > 100)'
|
||
```
|
||
|
||
---
|
||
|
||
## 测试
|
||
|
||
### 单元测试
|
||
|
||
```bash
|
||
# 运行所有测试
|
||
go test ./...
|
||
|
||
# 运行特定包的测试
|
||
go test ./internal/store/postgres/...
|
||
|
||
# 带覆盖率
|
||
go test -cover ./...
|
||
|
||
# 详细输出
|
||
go test -v ./...
|
||
```
|
||
|
||
### 集成测试
|
||
|
||
```bash
|
||
# 运行集成测试(需要 PostgreSQL 和 Redis)
|
||
go test -v ./tests/integration/...
|
||
|
||
# 单独测试数据库功能
|
||
go test -v ./tests/integration/database_test.go
|
||
|
||
# 单独测试任务队列
|
||
go test -v ./tests/integration/task_test.go
|
||
```
|
||
|
||
### 使用 Testcontainers(推荐)
|
||
|
||
集成测试会自动启动 PostgreSQL 和 Redis 容器:
|
||
|
||
```go
|
||
// tests/integration/database_test.go
|
||
func TestUserCRUD(t *testing.T) {
|
||
// 自动启动 PostgreSQL 容器
|
||
// 运行测试
|
||
// 自动清理容器
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 故障排查
|
||
|
||
### 问题 1: 数据库连接失败
|
||
|
||
**错误**: `dial tcp 127.0.0.1:5432: connect: connection refused`
|
||
|
||
**解决方案**:
|
||
```bash
|
||
# 检查 PostgreSQL 是否运行
|
||
docker ps | grep postgres
|
||
|
||
# 检查端口占用
|
||
lsof -i :5432
|
||
|
||
# 重启 PostgreSQL
|
||
docker restart postgres-dev
|
||
```
|
||
|
||
### 问题 2: 迁移失败
|
||
|
||
**错误**: `Dirty database version 1. Fix and force version.`
|
||
|
||
**解决方案**:
|
||
```bash
|
||
# 强制设置版本
|
||
migrate -path migrations -database "$DATABASE_URL" force 1
|
||
|
||
# 然后重新运行迁移
|
||
migrate -path migrations -database "$DATABASE_URL" up
|
||
```
|
||
|
||
### 问题 3: Worker 无法连接 Redis
|
||
|
||
**错误**: `dial tcp 127.0.0.1:6379: connect: connection refused`
|
||
|
||
**解决方案**:
|
||
```bash
|
||
# 检查 Redis 是否运行
|
||
docker ps | grep redis
|
||
|
||
# 测试连接
|
||
redis-cli ping
|
||
|
||
# 重启 Redis
|
||
docker restart redis-dev
|
||
```
|
||
|
||
### 问题 4: 任务一直重试
|
||
|
||
**原因**: 任务处理函数返回错误
|
||
|
||
**解决方案**:
|
||
1. 检查 Worker 日志:`tail -f logs/app.log | jq 'select(.level == "error")'`
|
||
2. 使用 asynqmon 查看失败详情
|
||
3. 检查任务幂等性实现
|
||
4. 验证 Redis 锁键是否正确设置
|
||
|
||
---
|
||
|
||
## 环境配置
|
||
|
||
### 开发环境
|
||
|
||
```bash
|
||
export CONFIG_ENV=dev
|
||
go run cmd/api/main.go
|
||
```
|
||
|
||
### 预发布环境
|
||
|
||
```bash
|
||
export CONFIG_ENV=staging
|
||
go run cmd/api/main.go
|
||
```
|
||
|
||
### 生产环境
|
||
|
||
```bash
|
||
export CONFIG_ENV=prod
|
||
export DB_PASSWORD=secure_password # 使用环境变量
|
||
go run cmd/api/main.go
|
||
```
|
||
|
||
---
|
||
|
||
## 性能调优建议
|
||
|
||
### 数据库连接池
|
||
|
||
根据服务器资源调整:
|
||
|
||
```yaml
|
||
database:
|
||
max_open_conns: 25 # 增大以支持更多并发
|
||
max_idle_conns: 10 # 保持足够的空闲连接
|
||
conn_max_lifetime: 5m # 定期回收连接
|
||
```
|
||
|
||
### Worker 并发数
|
||
|
||
根据任务类型调整:
|
||
|
||
```yaml
|
||
queue:
|
||
concurrency: 20 # I/O 密集型:CPU 核心数 × 2
|
||
# concurrency: 8 # CPU 密集型:CPU 核心数
|
||
```
|
||
|
||
### 队列优先级
|
||
|
||
根据业务需求调整:
|
||
|
||
```yaml
|
||
queue:
|
||
queues:
|
||
critical: 8 # 提高关键任务权重
|
||
default: 2
|
||
low: 1
|
||
```
|
||
|
||
---
|
||
|
||
## 下一步
|
||
|
||
1. **添加业务模型**: 参考 `internal/model/user.go` 创建 SIM 卡、订单等业务实体
|
||
2. **实现业务逻辑**: 在 Service 层实现具体业务逻辑
|
||
3. **添加迁移文件**: 使用 `./scripts/migrate.sh create` 添加新表
|
||
4. **创建异步任务**: 参考 `internal/task/email.go` 创建新的任务处理器
|
||
5. **编写测试**: 为所有 Service 层业务逻辑编写单元测试
|
||
|
||
---
|
||
|
||
## 参考资料
|
||
|
||
- [GORM 官方文档](https://gorm.io/docs/)
|
||
- [Asynq 官方文档](https://github.com/hibiken/asynq)
|
||
- [golang-migrate 文档](https://github.com/golang-migrate/migrate)
|
||
- [PostgreSQL 文档](https://www.postgresql.org/docs/)
|
||
- [项目 Constitution](../../.specify/memory/constitution.md)
|
||
|
||
---
|
||
|
||
## 常见问题(FAQ)
|
||
|
||
**Q: 如何添加新的数据库表?**
|
||
A: 使用 `./scripts/migrate.sh create table_name` 创建迁移文件,编辑 SQL,然后运行 `./scripts/migrate.sh up`。
|
||
|
||
**Q: 任务失败后会怎样?**
|
||
A: 根据配置自动重试(默认 5 次,指数退避)。5 次后仍失败会进入死信队列,可在 asynqmon 中查看。
|
||
|
||
**Q: 如何保证任务幂等性?**
|
||
A: 使用 Redis 锁或数据库唯一约束。参考 `research.md` 中的幂等性设计模式。
|
||
|
||
**Q: 如何扩展 Worker?**
|
||
A: 启动多个 Worker 进程(不同机器或容器),连接同一个 Redis。Asynq 自动负载均衡。
|
||
|
||
**Q: 数据库密码如何安全存储?**
|
||
A: 生产环境使用环境变量:`export DB_PASSWORD=xxx`,配置文件中使用 `${DB_PASSWORD}`。
|
||
|
||
**Q: 如何监控任务执行情况?**
|
||
A: 使用 asynqmon Web UI 或通过 Redis CLI 查看队列状态。
|
||
|
||
---
|
||
|
||
## 总结
|
||
|
||
本指南涵盖了:
|
||
- ✅ 环境搭建(PostgreSQL、Redis)
|
||
- ✅ 数据库迁移
|
||
- ✅ 服务启动(API + Worker)
|
||
- ✅ CRUD 操作示例
|
||
- ✅ 异步任务提交和处理
|
||
- ✅ 事务处理
|
||
- ✅ 监控和调试
|
||
- ✅ 故障排查
|
||
- ✅ 性能调优
|
||
|
||
**推荐开发流程**:
|
||
1. 设计数据模型 → 2. 创建迁移文件 → 3. 实现 Store 层 → 4. 实现 Service 层 → 5. 实现 Handler 层 → 6. 编写测试 → 7. 运行和验证
|