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:
2025-11-13 13:40:19 +08:00
parent ea0c6a8b16
commit 984ccccc63
63 changed files with 12099 additions and 83 deletions

502
tests/unit/model_test.go Normal file
View File

@@ -0,0 +1,502 @@
package unit
import (
"testing"
"time"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/go-playground/validator/v10"
"github.com/stretchr/testify/assert"
)
// TestUserValidation 测试用户模型验证
func TestUserValidation(t *testing.T) {
validate := validator.New()
t.Run("有效的创建用户请求", func(t *testing.T) {
req := &model.CreateUserRequest{
Username: "validuser",
Email: "valid@example.com",
Password: "password123",
}
err := validate.Struct(req)
assert.NoError(t, err)
})
t.Run("用户名太短", func(t *testing.T) {
req := &model.CreateUserRequest{
Username: "ab", // 少于 3 个字符
Email: "valid@example.com",
Password: "password123",
}
err := validate.Struct(req)
assert.Error(t, err)
})
t.Run("用户名太长", func(t *testing.T) {
req := &model.CreateUserRequest{
Username: "a123456789012345678901234567890123456789012345678901", // 超过 50 个字符
Email: "valid@example.com",
Password: "password123",
}
err := validate.Struct(req)
assert.Error(t, err)
})
t.Run("无效的邮箱格式", func(t *testing.T) {
req := &model.CreateUserRequest{
Username: "validuser",
Email: "invalid-email",
Password: "password123",
}
err := validate.Struct(req)
assert.Error(t, err)
})
t.Run("密码太短", func(t *testing.T) {
req := &model.CreateUserRequest{
Username: "validuser",
Email: "valid@example.com",
Password: "short", // 少于 8 个字符
}
err := validate.Struct(req)
assert.Error(t, err)
})
t.Run("缺少必填字段", func(t *testing.T) {
req := &model.CreateUserRequest{
Username: "validuser",
// 缺少 Email 和 Password
}
err := validate.Struct(req)
assert.Error(t, err)
})
}
// TestUserUpdateValidation 测试用户更新验证
func TestUserUpdateValidation(t *testing.T) {
validate := validator.New()
t.Run("有效的更新请求", func(t *testing.T) {
email := "newemail@example.com"
status := constants.UserStatusActive
req := &model.UpdateUserRequest{
Email: &email,
Status: &status,
}
err := validate.Struct(req)
assert.NoError(t, err)
})
t.Run("无效的邮箱格式", func(t *testing.T) {
email := "invalid-email"
req := &model.UpdateUserRequest{
Email: &email,
}
err := validate.Struct(req)
assert.Error(t, err)
})
t.Run("无效的状态值", func(t *testing.T) {
status := "invalid_status"
req := &model.UpdateUserRequest{
Status: &status,
}
err := validate.Struct(req)
assert.Error(t, err)
})
t.Run("空更新请求", func(t *testing.T) {
req := &model.UpdateUserRequest{}
err := validate.Struct(req)
assert.NoError(t, err) // 空更新请求应该是有效的
})
}
// TestOrderValidation 测试订单模型验证
func TestOrderValidation(t *testing.T) {
validate := validator.New()
t.Run("有效的创建订单请求", func(t *testing.T) {
req := &model.CreateOrderRequest{
OrderID: "ORD-2025-001",
UserID: 1,
Amount: 10000,
Remark: "测试订单",
}
err := validate.Struct(req)
assert.NoError(t, err)
})
t.Run("订单号太短", func(t *testing.T) {
req := &model.CreateOrderRequest{
OrderID: "ORD-123", // 少于 10 个字符
UserID: 1,
Amount: 10000,
}
err := validate.Struct(req)
assert.Error(t, err)
})
t.Run("订单号太长", func(t *testing.T) {
req := &model.CreateOrderRequest{
OrderID: "ORD-12345678901234567890123456789012345678901234567890", // 超过 50 个字符
UserID: 1,
Amount: 10000,
}
err := validate.Struct(req)
assert.Error(t, err)
})
t.Run("用户ID无效", func(t *testing.T) {
req := &model.CreateOrderRequest{
OrderID: "ORD-2025-001",
UserID: 0, // 用户ID必须大于0
Amount: 10000,
}
err := validate.Struct(req)
assert.Error(t, err)
})
t.Run("金额为负数", func(t *testing.T) {
req := &model.CreateOrderRequest{
OrderID: "ORD-2025-001",
UserID: 1,
Amount: -1000, // 金额不能为负数
}
err := validate.Struct(req)
assert.Error(t, err)
})
t.Run("缺少必填字段", func(t *testing.T) {
req := &model.CreateOrderRequest{
OrderID: "ORD-2025-001",
// 缺少 UserID 和 Amount
}
err := validate.Struct(req)
assert.Error(t, err)
})
}
// TestOrderUpdateValidation 测试订单更新验证
func TestOrderUpdateValidation(t *testing.T) {
validate := validator.New()
t.Run("有效的更新请求", func(t *testing.T) {
status := constants.OrderStatusPaid
remark := "已支付"
req := &model.UpdateOrderRequest{
Status: &status,
Remark: &remark,
}
err := validate.Struct(req)
assert.NoError(t, err)
})
t.Run("无效的状态值", func(t *testing.T) {
status := "invalid_status"
req := &model.UpdateOrderRequest{
Status: &status,
}
err := validate.Struct(req)
assert.Error(t, err)
})
}
// TestUserModel 测试用户模型
func TestUserModel(t *testing.T) {
t.Run("创建用户模型", func(t *testing.T) {
user := &model.User{
Username: "testuser",
Email: "test@example.com",
Password: "hashedpassword",
Status: constants.UserStatusActive,
}
assert.Equal(t, "testuser", user.Username)
assert.Equal(t, "test@example.com", user.Email)
assert.Equal(t, constants.UserStatusActive, user.Status)
})
t.Run("用户表名", func(t *testing.T) {
user := &model.User{}
assert.Equal(t, "tb_user", user.TableName())
})
t.Run("软删除字段", func(t *testing.T) {
user := &model.User{
Username: "testuser",
Email: "test@example.com",
Password: "hashedpassword",
Status: constants.UserStatusActive,
}
// DeletedAt 应该是 nil (未删除)
assert.True(t, user.DeletedAt.Time.IsZero())
})
t.Run("LastLoginAt 可选字段", func(t *testing.T) {
user := &model.User{
Username: "testuser",
Email: "test@example.com",
Password: "hashedpassword",
Status: constants.UserStatusActive,
}
assert.Nil(t, user.LastLoginAt)
// 设置登录时间
now := time.Now()
user.LastLoginAt = &now
assert.NotNil(t, user.LastLoginAt)
assert.Equal(t, now, *user.LastLoginAt)
})
}
// TestOrderModel 测试订单模型
func TestOrderModel(t *testing.T) {
t.Run("创建订单模型", func(t *testing.T) {
order := &model.Order{
OrderID: "ORD-2025-001",
UserID: 1,
Amount: 10000,
Status: constants.OrderStatusPending,
Remark: "测试订单",
}
assert.Equal(t, "ORD-2025-001", order.OrderID)
assert.Equal(t, uint(1), order.UserID)
assert.Equal(t, int64(10000), order.Amount)
assert.Equal(t, constants.OrderStatusPending, order.Status)
})
t.Run("订单表名", func(t *testing.T) {
order := &model.Order{}
assert.Equal(t, "tb_order", order.TableName())
})
t.Run("可选时间字段", func(t *testing.T) {
order := &model.Order{
OrderID: "ORD-2025-001",
UserID: 1,
Amount: 10000,
Status: constants.OrderStatusPending,
}
assert.Nil(t, order.PaidAt)
assert.Nil(t, order.CompletedAt)
// 设置支付时间
now := time.Now()
order.PaidAt = &now
assert.NotNil(t, order.PaidAt)
assert.Equal(t, now, *order.PaidAt)
})
}
// TestBaseModel 测试基础模型
func TestBaseModel(t *testing.T) {
t.Run("BaseModel 字段", func(t *testing.T) {
user := &model.User{
Username: "testuser",
Email: "test@example.com",
Password: "hashedpassword",
Status: constants.UserStatusActive,
}
// ID 应该是 0 (未保存)
assert.Zero(t, user.ID)
// 时间戳应该是零值
assert.True(t, user.CreatedAt.IsZero())
assert.True(t, user.UpdatedAt.IsZero())
})
}
// TestUserStatusConstants 测试用户状态常量
func TestUserStatusConstants(t *testing.T) {
t.Run("用户状态常量定义", func(t *testing.T) {
assert.Equal(t, "active", constants.UserStatusActive)
assert.Equal(t, "inactive", constants.UserStatusInactive)
assert.Equal(t, "suspended", constants.UserStatusSuspended)
})
t.Run("用户状态验证", func(t *testing.T) {
validStatuses := []string{
constants.UserStatusActive,
constants.UserStatusInactive,
constants.UserStatusSuspended,
}
for _, status := range validStatuses {
user := &model.User{
Username: "testuser",
Email: "test@example.com",
Password: "hashedpassword",
Status: status,
}
assert.Contains(t, validStatuses, user.Status)
}
})
}
// TestOrderStatusConstants 测试订单状态常量
func TestOrderStatusConstants(t *testing.T) {
t.Run("订单状态常量定义", func(t *testing.T) {
assert.Equal(t, "pending", constants.OrderStatusPending)
assert.Equal(t, "paid", constants.OrderStatusPaid)
assert.Equal(t, "processing", constants.OrderStatusProcessing)
assert.Equal(t, "completed", constants.OrderStatusCompleted)
assert.Equal(t, "cancelled", constants.OrderStatusCancelled)
})
t.Run("订单状态流转", func(t *testing.T) {
order := &model.Order{
OrderID: "ORD-2025-001",
UserID: 1,
Amount: 10000,
Status: constants.OrderStatusPending,
}
// 订单状态流转pending -> paid -> processing -> completed
assert.Equal(t, constants.OrderStatusPending, order.Status)
order.Status = constants.OrderStatusPaid
assert.Equal(t, constants.OrderStatusPaid, order.Status)
order.Status = constants.OrderStatusProcessing
assert.Equal(t, constants.OrderStatusProcessing, order.Status)
order.Status = constants.OrderStatusCompleted
assert.Equal(t, constants.OrderStatusCompleted, order.Status)
})
t.Run("订单取消", func(t *testing.T) {
order := &model.Order{
OrderID: "ORD-2025-002",
UserID: 1,
Amount: 10000,
Status: constants.OrderStatusPending,
}
// 从任何状态都可以取消
order.Status = constants.OrderStatusCancelled
assert.Equal(t, constants.OrderStatusCancelled, order.Status)
})
}
// TestUserResponse 测试用户响应模型
func TestUserResponse(t *testing.T) {
t.Run("创建用户响应", func(t *testing.T) {
now := time.Now()
resp := &model.UserResponse{
ID: 1,
Username: "testuser",
Email: "test@example.com",
Status: constants.UserStatusActive,
CreatedAt: now,
UpdatedAt: now,
}
assert.Equal(t, uint(1), resp.ID)
assert.Equal(t, "testuser", resp.Username)
assert.Equal(t, "test@example.com", resp.Email)
assert.Equal(t, constants.UserStatusActive, resp.Status)
})
t.Run("用户响应不包含密码", func(t *testing.T) {
// UserResponse 结构体不应该包含 Password 字段
resp := &model.UserResponse{
ID: 1,
Username: "testuser",
Email: "test@example.com",
Status: constants.UserStatusActive,
}
// 验证结构体大小合理 (不包含密码字段)
assert.NotNil(t, resp)
})
}
// TestListResponse 测试列表响应模型
func TestListResponse(t *testing.T) {
t.Run("用户列表响应", func(t *testing.T) {
users := []model.UserResponse{
{ID: 1, Username: "user1", Email: "user1@example.com", Status: constants.UserStatusActive},
{ID: 2, Username: "user2", Email: "user2@example.com", Status: constants.UserStatusActive},
}
resp := &model.ListUsersResponse{
Users: users,
Page: 1,
PageSize: 20,
Total: 100,
TotalPages: 5,
}
assert.Equal(t, 2, len(resp.Users))
assert.Equal(t, 1, resp.Page)
assert.Equal(t, 20, resp.PageSize)
assert.Equal(t, int64(100), resp.Total)
assert.Equal(t, 5, resp.TotalPages)
})
t.Run("订单列表响应", func(t *testing.T) {
orders := []model.OrderResponse{
{ID: 1, OrderID: "ORD-001", UserID: 1, Amount: 10000, Status: constants.OrderStatusPending},
{ID: 2, OrderID: "ORD-002", UserID: 1, Amount: 20000, Status: constants.OrderStatusPaid},
}
resp := &model.ListOrdersResponse{
Orders: orders,
Page: 1,
PageSize: 20,
Total: 50,
TotalPages: 3,
}
assert.Equal(t, 2, len(resp.Orders))
assert.Equal(t, 1, resp.Page)
assert.Equal(t, 20, resp.PageSize)
assert.Equal(t, int64(50), resp.Total)
assert.Equal(t, 3, resp.TotalPages)
})
}
// TestFieldTags 测试字段标签
func TestFieldTags(t *testing.T) {
t.Run("User GORM 标签", func(t *testing.T) {
user := &model.User{}
// 验证 TableName 方法存在
tableName := user.TableName()
assert.Equal(t, "tb_user", tableName)
})
t.Run("Order GORM 标签", func(t *testing.T) {
order := &model.Order{}
// 验证 TableName 方法存在
tableName := order.TableName()
assert.Equal(t, "tb_order", tableName)
})
}