综合实战 - 企业级项目


第十六章:综合实战 - 企业级项目

16.1 项目概述

构建一个企业级用户权限+订单管理系统,综合运用前面所有知识点:

  • 用户权限模块:RBAC 权限控制
  • 订单模块:复杂业务逻辑、事务处理
  • 技术栈:GORM + GORM Gen 混用

16.2 系统架构

enterprise-example/
├── cmd/
│   └── server/
│       └── main.go
├── internal/
│   ├── domain/          # 领域模型
│   │   ├── user.go
│   │   ├── role.go
│   │   ├── permission.go
│   │   └── order.go
│   ├── repository/      # 数据访问层
│   │   ├── user_repo.go
│   │   └── order_repo.go
│   ├── service/         # 业务逻辑层
│   │   ├── user_service.go
│   │   └── order_service.go
│   ├── api/             # 接口层
│   │   ├── handler/
│   │   └── middleware/
│   └── infrastructure/  # 基础设施
│       ├── database/
│       ├── cache/
│       └── mq/
├── pkg/                 # 公共库
│   ├── errors/
│   ├── utils/
│   └── logger/
├── generate/            # 代码生成
│   └── gen.go
├── dao/                 # GORM Gen 生成
│   ├── model/
│   └── query/
└── configs/             # 配置文件
    └── config.yaml

16.3 领域模型设计

用户领域

package domain

// User 用户领域模型
type User struct {
    ID       uint64
    Username string
    Password string
    Email    string
    Phone    string
    Status   int8   // 1-正常 2-禁用
    Roles    []Role // 角色
}

func (u *User) HasPermission(permCode string) bool {
    for _, role := range u.Roles {
        for _, perm := range role.Permissions {
            if perm.Code == permCode {
                return true
            }
        }
    }
    return false
}

// Role 角色
type Role struct {
    ID          uint64
    Name        string
    Code        string
    Permissions []Permission
}

// Permission 权限
type Permission struct {
    ID     uint64
    Name   string
    Code   string
    Type   int8 // 1-菜单 2-按钮 3-接口
    ParentID uint64
}

订单领域

package domain

// Order 订单领域模型
type Order struct {
    ID          uint64
    OrderNo     string    // 订单号
    UserID      uint64
    Status      int8      // 1-待支付 2-已支付 3-已发货 4-已完成 5-已取消
    TotalAmount float64   // 总金额
    Items       []OrderItem
    Address     Address
    CreatedAt   time.Time
    PaidAt      *time.Time
}

func (o *Order) CanCancel() bool {
    return o.Status == 1 // 只有待支付可以取消
}

func (o *Order) Pay() error {
    if o.Status != 1 {
        return errors.New("订单状态不允许支付")
    }
    now := time.Now()
    o.Status = 2
    o.PaidAt = &now
    return nil
}

// OrderItem 订单项
type OrderItem struct {
    ID          uint64
    OrderID     uint64
    ProductID   uint64
    ProductName string
    Quantity    int
    UnitPrice   float64
    TotalPrice  float64
}

// Address 地址
type Address struct {
    Province   string
    City       string
    District   string
    Detail     string
    Contact    string
    Phone      string
}

16.4 GORM Gen 生成配置

// generate/gen.go
package main

import (
    "gorm.io/driver/mysql"
    "gorm.io/gen"
    "gorm.io/gorm"
)

func main() {
    g := gen.NewGenerator(gen.Config{
        OutPath:      "./dao/query",
        ModelPkgPath: "./dao/model",
        Mode:         gen.WithDefaultQuery | gen.WithQueryInterface,
        FieldNullable:     true,
        FieldCoverable:    true,
        FieldWithTypeTag:  true,
    })

    dsn := "user:password@tcp(127.0.0.1:3306)/enterprise?charset=utf8mb4&parseTime=True&loc=Local"
    db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
    g.UseDB(db)

    // 生成所有表
    g.ApplyBasic(g.GenerateAllTable()...)

    // 为用户表添加自定义方法
    user := g.GenerateModel("sys_user",
        gen.FieldIgnore("password"),
    )
    g.ApplyInterface(func(UserMethod) {}, user)

    g.Execute()
}

type UserMethod interface {
    FindByUsername(username string) (*gen.T, error)
    FindByPhone(phone string) (*gen.T, error)
    UpdatePassword(userID uint64, password string) error
}

16.5 Repository 层实现

基础 Repository

package repository

import (
    "context"
    "gorm.io/gorm"
)

type BaseRepository struct {
    db *gorm.DB
}

func NewBaseRepository(db *gorm.DB) *BaseRepository {
    return &BaseRepository{db: db}
}

func (r *BaseRepository) DB(ctx context.Context) *gorm.DB {
    return r.db.WithContext(ctx)
}

func (r *BaseRepository) Transaction(ctx context.Context, fn func(*gorm.DB) error) error {
    return r.db.WithContext(ctx).Transaction(fn)
}

User Repository(使用 Gen)

package repository

import (
    "context"
    "myapp/dao/model"
    "myapp/dao/query"
    "myapp/internal/domain"
    "gorm.io/gen"
)

type UserRepository struct {
    *BaseRepository
    q *query.Query
}

func NewUserRepository(db *gorm.DB) *UserRepository {
    return &UserRepository{
        BaseRepository: NewBaseRepository(db),
        q:              query.Use(db),
    }
}

// Create 创建用户
func (r *UserRepository) Create(ctx context.Context, user *domain.User) error {
    u := &model.SysUser{
        Username: user.Username,
        Password: user.Password,
        Email:    user.Email,
        Phone:    user.Phone,
        Status:   user.Status,
    }
    return r.q.SysUser.WithContext(ctx).Create(u)
}

// FindByID 根据ID查询
func (r *UserRepository) FindByID(ctx context.Context, id uint64) (*domain.User, error) {
    u := r.q.SysUser
    user, err := u.WithContext(ctx).Where(u.ID.Eq(id)).First()
    if err != nil {
        return nil, err
    }
    return r.toDomain(user), nil
}

// FindByUsername 根据用户名查询
func (r *UserRepository) FindByUsername(ctx context.Context, username string) (*domain.User, error) {
    u := r.q.SysUser
    user, err := u.WithContext(ctx).Where(u.Username.Eq(username)).First()
    if err != nil {
        return nil, err
    }
    return r.toDomain(user), nil
}

// GetUserWithRoles 获取用户及其角色
func (r *UserRepository) GetUserWithRoles(ctx context.Context, id uint64) (*domain.User, error) {
    // 使用原生 GORM 进行复杂关联查询
    var user domain.User
    err := r.DB(ctx).Table("sys_user").
        Select("sys_user.*").
        Where("sys_user.id = ?", id).
        First(&user).Error
    if err != nil {
        return nil, err
    }

    // 查询角色
    var roles []domain.Role
    err = r.DB(ctx).Table("sys_role r").
        Select("r.*").
        Joins("JOIN sys_user_role ur ON r.id = ur.role_id").
        Where("ur.user_id = ?", id).
        Find(&roles).Error
    if err != nil {
        return nil, err
    }
    user.Roles = roles

    return &user, nil
}

// UpdateStatus 更新状态(Gen)
func (r *UserRepository) UpdateStatus(ctx context.Context, id uint64, status int8) error {
    u := r.q.SysUser
    _, err := u.WithContext(ctx).Where(u.ID.Eq(id)).Update(u.Status, status)
    return err
}

// List 用户列表(Gen)
func (r *UserRepository) List(ctx context.Context, page, pageSize int) ([]*domain.User, int64, error) {
    u := r.q.SysUser
    
    // 条件构建
    q := u.WithContext(ctx)
    
    // 统计
    total, err := q.Count()
    if err != nil {
        return nil, 0, err
    }
    
    // 查询
    users, err := q.Order(u.ID.Desc()).FindByPage((page-1)*pageSize, pageSize)
    if err != nil {
        return nil, 0, err
    }
    
    // 转换
    var result []*domain.User
    for _, user := range users {
        result = append(result, r.toDomain(user))
    }
    
    return result, total, nil
}

func (r *UserRepository) toDomain(u *model.SysUser) *domain.User {
    return &domain.User{
        ID:       u.ID,
        Username: u.Username,
        Email:    u.Email,
        Phone:    u.Phone,
        Status:   u.Status,
    }
}

Order Repository(GORM + 原生 SQL 混用)

package repository

import (
    "context"
    "fmt"
    "myapp/internal/domain"
    "gorm.io/gorm"
)

type OrderRepository struct {
    *BaseRepository
}

func NewOrderRepository(db *gorm.DB) *OrderRepository {
    return &OrderRepository{BaseRepository: NewBaseRepository(db)}
}

// Create 创建订单(事务)
func (r *OrderRepository) Create(ctx context.Context, order *domain.Order) error {
    return r.Transaction(ctx, func(tx *gorm.DB) error {
        // 1. 插入订单主表
        if err := tx.Table("order").Create(map[string]interface{}{
            "order_no":     order.OrderNo,
            "user_id":      order.UserID,
            "status":       order.Status,
            "total_amount": order.TotalAmount,
            "created_at":   order.CreatedAt,
        }).Error; err != nil {
            return err
        }
        
        // 2. 插入订单项
        for _, item := range order.Items {
            if err := tx.Table("order_item").Create(map[string]interface{}{
                "order_id":     order.ID,
                "product_id":   item.ProductID,
                "product_name": item.ProductName,
                "quantity":     item.Quantity,
                "unit_price":   item.UnitPrice,
                "total_price":  item.TotalPrice,
            }).Error; err != nil {
                return err
            }
        }
        
        // 3. 插入地址
        if err := tx.Table("order_address").Create(map[string]interface{}{
            "order_id": order.ID,
            "province": order.Address.Province,
            "city":     order.Address.City,
            "district": order.Address.District,
            "detail":   order.Address.Detail,
            "contact":  order.Address.Contact,
            "phone":    order.Address.Phone,
        }).Error; err != nil {
            return err
        }
        
        return nil
    })
}

// FindByOrderNo 根据订单号查询
func (r *OrderRepository) FindByOrderNo(ctx context.Context, orderNo string) (*domain.Order, error) {
    var order domain.Order
    err := r.DB(ctx).Table("order").
        Where("order_no = ?", orderNo).
        First(&order).Error
    if err != nil {
        return nil, err
    }
    
    // 查询订单项
    err = r.DB(ctx).Table("order_item").
        Where("order_id = ?", order.ID).
        Find(&order.Items).Error
    if err != nil {
        return nil, err
    }
    
    return &order, nil
}

// GetOrderStatistics 订单统计(原生 SQL)
func (r *OrderRepository) GetOrderStatistics(ctx context.Context, startDate, endDate string) (*OrderStats, error) {
    var stats OrderStats
    
    err := r.DB(ctx).Raw(`
        SELECT 
            COUNT(*) as total_orders,
            COUNT(CASE WHEN status = 2 THEN 1 END) as paid_orders,
            SUM(total_amount) as total_amount,
            AVG(total_amount) as avg_amount
        FROM order
        WHERE created_at BETWEEN ? AND ?
    `, startDate, endDate).Scan(&stats).Error
    
    return &stats, err
}

type OrderStats struct {
    TotalOrders int64   `json:"total_orders"`
    PaidOrders  int64   `json:"paid_orders"`
    TotalAmount float64 `json:"total_amount"`
    AvgAmount   float64 `json:"avg_amount"`
}

// UpdateStatus 更新订单状态(使用乐观锁)
func (r *OrderRepository) UpdateStatus(ctx context.Context, orderID uint64, fromStatus, toStatus int8) error {
    result := r.DB(ctx).Table("order").
        Where("id = ? AND status = ?", orderID, fromStatus).
        Update("status", toStatus)
    
    if result.RowsAffected == 0 {
        return fmt.Errorf("订单状态已变更或订单不存在")
    }
    
    return result.Error
}

16.6 Service 层

Order Service

package service

import (
    "context"
    "fmt"
    "myapp/internal/domain"
    "myapp/internal/repository"
    "myapp/pkg/snowflake"
    "time"
)

type OrderService struct {
    orderRepo *repository.OrderRepository
    userRepo  *repository.UserRepository
    cache     Cache
}

func NewOrderService(orderRepo *repository.OrderRepository, userRepo *repository.UserRepository) *OrderService {
    return &OrderService{
        orderRepo: orderRepo,
        userRepo:  userRepo,
    }
}

// CreateOrder 创建订单
func (s *OrderService) CreateOrder(ctx context.Context, req CreateOrderRequest) (*domain.Order, error) {
    // 1. 验证用户
    user, err := s.userRepo.FindByID(ctx, req.UserID)
    if err != nil {
        return nil, fmt.Errorf("用户不存在: %w", err)
    }
    if user.Status != 1 {
        return nil, fmt.Errorf("用户状态异常")
    }
    
    // 2. 构建订单
    order := &domain.Order{
        OrderNo:     snowflake.GenerateID().String(),
        UserID:      req.UserID,
        Status:      1, // 待支付
        TotalAmount: 0,
        CreatedAt:   time.Now(),
        Address:     req.Address,
    }
    
    // 3. 构建订单项并计算总价
    for _, itemReq := range req.Items {
        item := domain.OrderItem{
            ProductID:   itemReq.ProductID,
            ProductName: itemReq.ProductName,
            Quantity:    itemReq.Quantity,
            UnitPrice:   itemReq.UnitPrice,
            TotalPrice:  float64(itemReq.Quantity) * itemReq.UnitPrice,
        }
        order.Items = append(order.Items, item)
        order.TotalAmount += item.TotalPrice
    }
    
    // 4. 应用优惠
    if req.CouponID > 0 {
        discount, err := s.applyCoupon(ctx, req.CouponID, order.TotalAmount)
        if err != nil {
            return nil, err
        }
        order.TotalAmount -= discount
    }
    
    // 5. 保存订单
    if err := s.orderRepo.Create(ctx, order); err != nil {
        return nil, fmt.Errorf("创建订单失败: %w", err)
    }
    
    // 6. 发送延迟消息(15分钟后检查支付状态)
    s.sendDelayCheck(order.OrderNo, 15*time.Minute)
    
    return order, nil
}

// PayOrder 支付订单
func (s *OrderService) PayOrder(ctx context.Context, orderNo string, userID uint64) error {
    // 1. 查询订单
    order, err := s.orderRepo.FindByOrderNo(ctx, orderNo)
    if err != nil {
        return fmt.Errorf("订单不存在: %w", err)
    }
    
    // 2. 验证所有权
    if order.UserID != userID {
        return fmt.Errorf("无权操作此订单")
    }
    
    // 3. 验证状态
    if !order.CanCancel() { // 实际上应该是 CanPay()
        return fmt.Errorf("订单状态不允许支付")
    }
    
    // 4. 调用支付接口(略)
    // ...
    
    // 5. 更新订单状态(使用乐观锁)
    if err := s.orderRepo.UpdateStatus(ctx, order.ID, 1, 2); err != nil {
        return fmt.Errorf("更新订单状态失败: %w", err)
    }
    
    // 6. 发送支付成功通知
    s.sendPaySuccessNotify(order)
    
    return nil
}

// CancelOrder 取消订单
func (s *OrderService) CancelOrder(ctx context.Context, orderNo string, userID uint64) error {
    order, err := s.orderRepo.FindByOrderNo(ctx, orderNo)
    if err != nil {
        return err
    }
    
    if order.UserID != userID {
        return fmt.Errorf("无权操作")
    }
    
    if !order.CanCancel() {
        return fmt.Errorf("订单状态不允许取消")
    }
    
    // 回滚库存(略)
    
    return s.orderRepo.UpdateStatus(ctx, order.ID, 1, 5) // 5-已取消
}

type CreateOrderRequest struct {
    UserID   uint64
    Items    []OrderItemRequest
    Address  domain.Address
    CouponID uint64
}

type OrderItemRequest struct {
    ProductID   uint64
    ProductName string
    Quantity    int
    UnitPrice   float64
}

16.7 权限控制中间件

package middleware

import (
    "net/http"
    "strings"
    "github.com/gin-gonic/gin"
    "myapp/internal/domain"
    "myapp/pkg/jwt"
)

// Auth 认证中间件
func Auth() gin.HandlerFunc {
    return func(c *gin.Context) {
        authHeader := c.GetHeader("Authorization")
        if authHeader == "" {
            c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "未登录"})
            return
        }
        
        parts := strings.SplitN(authHeader, " ", 2)
        if len(parts) != 2 || parts[0] != "Bearer" {
            c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "认证格式错误"})
            return
        }
        
        claims, err := jwt.ParseToken(parts[1])
        if err != nil {
            c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "无效的token"})
            return
        }
        
        c.Set("userID", claims.UserID)
        c.Set("username", claims.Username)
        c.Next()
    }
}

// Permission 权限检查中间件
func Permission(permCode string) gin.HandlerFunc {
    return func(c *gin.Context) {
        userID, _ := c.Get("userID")
        
        // 从缓存或数据库获取用户权限
        userService := c.MustGet("userService").(*service.UserService)
        user, err := userService.GetUserWithRoles(c.Request.Context(), userID.(uint64))
        if err != nil {
            c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "获取用户信息失败"})
            return
        }
        
        if !user.HasPermission(permCode) {
            c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "无权限操作"})
            return
        }
        
        c.Next()
    }
}

16.8 性能优化

// 使用缓存优化热点数据
type CachedUserRepository struct {
    *repository.UserRepository
    cache cache.Cache
}

func (r *CachedUserRepository) FindByID(ctx context.Context, id uint64) (*domain.User, error) {
    key := fmt.Sprintf("user:%d", id)
    
    // 先查缓存
    if cached, err := r.cache.Get(ctx, key); err == nil {
        return cached.(*domain.User), nil
    }
    
    // 查数据库
    user, err := r.UserRepository.FindByID(ctx, id)
    if err != nil {
        return nil, err
    }
    
    // 写入缓存
    r.cache.Set(ctx, key, user, 5*time.Minute)
    
    return user, nil
}

// 使用本地缓存 + Redis 二级缓存
type MultiLevelCache struct {
    local  *ristretto.Cache
    redis  *redis.Client
}

16.9 完整启动流程

// cmd/server/main.go
package main

import (
    "context"
    "log"
    "myapp/configs"
    "myapp/dao/query"
    "myapp/internal/api"
    "myapp/internal/repository"
    "myapp/internal/service"
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
    "gorm.io/gorm/logger"
)

func main() {
    cfg := configs.Load()
    
    // 1. 连接数据库
    db, err := gorm.Open(mysql.Open(cfg.Database.DSN), &gorm.Config{
        Logger: logger.Default.LogMode(logger.Info),
    })
    if err != nil {
        log.Fatal("数据库连接失败:", err)
    }
    
    // 2. 设置 GORM Gen
    query.SetDefault(db)
    
    // 3. 初始化 Repository
    userRepo := repository.NewUserRepository(db)
    orderRepo := repository.NewOrderRepository(db)
    
    // 4. 初始化 Service
    userService := service.NewUserService(userRepo)
    orderService := service.NewOrderService(orderRepo, userRepo)
    
    // 5. 初始化 Handler
    handler := api.NewHandler(userService, orderService)
    
    // 6. 启动服务
    router := api.NewRouter(handler)
    log.Println("Server starting on :8080")
    if err := router.Run(":8080"); err != nil {
        log.Fatal("Server failed:", err)
    }
}

16.10 小结

本章展示了一个企业级项目的完整架构,综合运用了:

  • GORM Gen:类型安全的日常 CRUD
  • 原生 GORM:复杂查询和事务
  • 领域驱动设计:清晰的业务边界
  • 性能优化:缓存、连接池
  • 权限控制:RBAC 模型

本文代码地址:https://github.com/LittleMoreInteresting/gorm_study

欢迎关注公众号,一起学习进步!

如有疑问关注公众号给我留言
wx

关注公众号

©2017-2023 鲁ICP备17023316号-1 Powered by Hugo