[Go]GopherAI项目学习记录:项目框架

一个网页AI聊天,图像识别项目,使用到的技术栈有,Gin框架,GORM,rabbitmq,redis,eino框架,Vue框架等

项目地址:https://github.com/youngyangyang04/GopherAI


总体架构

项目使用分层架构设计,可分为以下几层:

  • 前端(Vue组件,发起http请求)
  • 路由层(Gin框架提供的路由器匹配路径,将请求分发到相应控制器)
  • 中间件层(JWT鉴权,校验token,除了登录界面不需要外其余API访问都需要)
  • 控制器层(绑定请求参数,调用服务层方法)
  • 服务层(执行业务逻辑)
  • 数据访问层(DAO,与MySQL交互)

此外还有通用组件,如Redis缓存验证码,Email发送账号邮件,JWT生成HS256签名的Token等

以及外部第三方服务,如SMTP服务器发送邮件,调用大模型API等

使用分层架构可以使代码逻辑清晰,便于维护测试以及代码复用

路由层

定义了URL路由规则,将请求分发到对应的controller,配置了JWT中间件

router/router.go里,创建了根路由组/api/v1,并在其下创建了/user,/AI,/image三个路由组,每个路由组里又调用了函数来绑定handler方法给各个API接口

为/AI和/image路径使用了jwt中间件

package router

import (
    "GopherAI/middleware/jwt"

    "github.com/gin-gonic/gin"
)

// 初始化URL路由规则
func InitRouter() *gin.Engine {

    r := gin.Default()
    enterRouter := r.Group("/api/v1")   // 创建路由组
    {
        RegisterUserRouter(enterRouter.Group("/user"))
    }

    //后续登录的接口需要jwt鉴权
    {
        AIGroup := enterRouter.Group("/AI")
        AIGroup.Use(jwt.Auth())         // 使用JWT中间件
        AIRouter(AIGroup)
    }

    {
        ImageGroup := enterRouter.Group("/image")
        ImageGroup.Use(jwt.Auth())      // 使用JWT中间件
        ImageRouter(ImageGroup)
    }

    return r
}

以下是router/user.go,为API接口设置了handler函数(即分发到控制器层)

package router

import (
    "GopherAI/controller/user"

    "github.com/gin-gonic/gin"
)

// 为接口路径设置handler函数
func RegisterUserRouter(r *gin.RouterGroup) {
    {
        r.POST("/register", user.Register)          // 注册
        r.POST("/login", user.Login)                // 登录
        r.POST("/captcha", user.HandleCaptcha)      // 验证码
    }
}

控制器层

接收HTTP请求,解析请求参数,进行参数验证,调用Service层处理业务逻辑,构造响应并返回

例如以下是/controller/user/user.go中的/login接口的handler函数

// /login接口的handler函数
func Login(c *gin.Context) {
    // 定义请求与响应结构体
    req := new(LoginRequest)
    res := new(LoginResponse)
    // 绑定和验证参数
    if err := c.ShouldBindJSON(req); err != nil {
        c.JSON(http.StatusOK, res.CodeOf(code.CodeInvalidParams))
        return
    }
    // 调用service层
    token, code_ := user.Login(req.Username, req.Password)
    // 构造响应
    if code_ != code.CodeSuccess {
        c.JSON(http.StatusOK, res.CodeOf(code_))
        return
    }
    res.Success()
    res.Token = token
    c.JSON(http.StatusOK, res)
}

服务层

实现具体的业务逻辑,协调DAO操作,处理数据转换以及封装

例如以下是Login函数,实现了用户登录的具体业务逻辑

func Login(username, password string) (string, code.Code) {
    var userInformation *model.User
    var ok bool
    //1:判断用户是否存在
    if ok, userInformation = user.IsExistUser(username); !ok {

        return "", code.CodeUserNotExist
    }
    //2:判断用户是否密码账号正确
    if userInformation.Password != utils.MD5(password) {
        return "", code.CodeInvalidPassword
    }
    //3:返回一个Token
    token, err := myjwt.GenerateToken(userInformation.ID, userInformation.Username)

    if err != nil {
        return "", code.CodeServerBusy
    }
    return token, code.CodeSuccess
}

数据访问(DAO)层

封装数据库操作,提供数据库访问接口,管理数据库连接

以下是/dao/user/user.go中的部分函数

func IsExistUser(username string) (bool, *model.User) {

    user, err := mysql.GetUserByUsername(username)

    if err == gorm.ErrRecordNotFound || user == nil {
        return false, nil
    }

    return true, user
}

func Register(username, email, password string) (*model.User, bool) {
    if user, err := mysql.InsertUser(&model.User{
        Email:    email,
        Name:     username,
        Username: username,
        Password: utils.MD5(password),
    }); err != nil {
        return nil, false
    } else {
        return user, true
    }
}

以下是/common/mysql/mysql.go的部分函数,底层使用gorm实现

func InsertUser(user *model.User) (*model.User, error) {
    err := DB.Create(&user).Error
    return user, err
}

func GetUserByUsername(username string) (*model.User, error) {
    user := new(model.User)
    err := DB.Where("username = ?", username).First(user).Error
    return user, err
}

第三方组件

邮件服务

使用QQ邮箱的SMTP服务器,使用gomail封装,以下是/common/email/email.go

package email

import (
    "GopherAI/config"
    "fmt"

    "gopkg.in/gomail.v2"
)

const (
    CodeMsg     = "GopherAI验证码如下(验证码仅限于2分钟有效): "
    UserNameMsg = "GopherAI的账号如下,请保留好,后续可以用账号/邮箱登录 "
)

func SendCaptcha(email, code, msg string) error {
    m := gomail.NewMessage()

    // 发件人
    m.SetHeader("From", config.GetConfig().EmailConfig.Email)
    // 收件人
    m.SetHeader("To", email)
    // 主题
    m.SetHeader("Subject", "来自GopherAI的信息")
    // 正文内容(纯文本形式,也可以用 text/html)
    m.SetBody("text/plain", msg+" "+code)

    // 配置 SMTP 服务器和授权码,587:是 SMTP 的明文/STARTTLS 端口号
    d := gomail.NewDialer("smtp.qq.com", 587, config.GetConfig().EmailConfig.Email, config.GetConfig().EmailConfig.Authcode)

    // 发送邮件
    if err := d.DialAndSend(m); err != nil {
        fmt.Printf("DialAndSend err %v:\n", err)
        return err
    }
    fmt.Printf("send mail success\n")
    return nil
}

图像识别

使用了微软的ONNXRuntime框架来进行图形推理(没有用cuda,纯CPU跑的),调用onnxruntime.so动态库文件

func (r *ImageRecognizer) PredictFromImage(img image.Image) (string, error) {

    resizedImg := image.NewRGBA(image.Rect(0, 0, r.inputW, r.inputH))

    draw.CatmullRom.Scale(resizedImg, resizedImg.Bounds(), img, img.Bounds(), draw.Over, nil)

    h, w := r.inputH, r.inputW
    ch := 3 // R, G, B
    data := make([]float32, h*w*ch)

    for y := 0; y < h; y++ {
        for x := 0; x < w; x++ {
            c := resizedImg.At(x, y)

            r, g, b, _ := c.RGBA()

            rf := float32(r>>8) / 255.0
            gf := float32(g>>8) / 255.0
            bf := float32(b>>8) / 255.0

            // NCHW format
            data[y*w+x] = rf
            data[h*w+y*w+x] = gf
            data[2*h*w+y*w+x] = bf
        }
    }

    inData := r.inputTensor.GetData()
    copy(inData, data)

    if err := r.session.Run(); err != nil {
        return "", fmt.Errorf("onnx run error: %w", err)
    }

    outData := r.outputTensor.GetData()
    if len(outData) == 0 {
        return "", errors.New("empty output from model")
    }

    maxIdx := 0
    maxVal := outData[0]
    for i := 1; i < len(outData); i++ {
        if outData[i] > maxVal {
            maxVal = outData[i]
            maxIdx = i
        }
    }

    if maxIdx >= 0 && maxIdx < len(r.labels) {
        return r.labels[maxIdx], nil
    }
    return "Unknown", nil
}

func loadLabels(path string) ([]string, error) {
    f, err := os.Open(filepath.Clean(path))
    if err != nil {
        return nil, fmt.Errorf("open label file failed: %w", err)
    }
    defer f.Close()

    var labels []string
    sc := bufio.NewScanner(f)
    for sc.Scan() {
        line := sc.Text()
        if line != "" {
            labels = append(labels, line)
        }
    }
    if err := sc.Err(); err != nil {
        return nil, fmt.Errorf("read labels failed: %w", err)
    }
    if len(labels) == 0 {
        return nil, fmt.Errorf("no labels found in %s", path)
    }
    return labels, nil
}

AI接入

使用字节的EINO框架接入,支持OpenAI模型API以及本地Ollama模型

type StreamCallback func(msg string)

// AIModel 定义AI模型接口
type AIModel interface {
    GenerateResponse(ctx context.Context, messages []*schema.Message) (*schema.Message, error)
    StreamResponse(ctx context.Context, messages []*schema.Message, cb StreamCallback) (string, error)
    GetModelType() string
}

// =================== OpenAI 实现 ===================
type OpenAIModel struct {
    llm model.ToolCallingChatModel
}

func NewOpenAIModel(ctx context.Context) (*OpenAIModel, error) {
    key := os.Getenv("OPENAI_API_KEY")
    modelName := os.Getenv("OPENAI_MODEL_NAME")
    baseURL := os.Getenv("OPENAI_BASE_URL")

    llm, err := openai.NewChatModel(ctx, &openai.ChatModelConfig{
        BaseURL: baseURL,
        Model:   modelName,
        APIKey:  key,
    })
    if err != nil {
        return nil, fmt.Errorf("create openai model failed: %v", err)
    }
    return &OpenAIModel{llm: llm}, nil
}

func (o *OpenAIModel) GenerateResponse(ctx context.Context, messages []*schema.Message) (*schema.Message, error) {
    resp, err := o.llm.Generate(ctx, messages)
    if err != nil {
        return nil, fmt.Errorf("openai generate failed: %v", err)
    }
    return resp, nil
}

func (o *OpenAIModel) StreamResponse(ctx context.Context, messages []*schema.Message, cb StreamCallback) (string, error) {
    stream, err := o.llm.Stream(ctx, messages)
    if err != nil {
        return "", fmt.Errorf("openai stream failed: %v", err)
    }
    defer stream.Close()

    var fullResp strings.Builder

    for {
        msg, err := stream.Recv()
        if err == io.EOF {
            break
        }
        if err != nil {
            return "", fmt.Errorf("openai stream recv failed: %v", err)
        }
        if len(msg.Content) > 0 {
            fullResp.WriteString(msg.Content) // 聚合

            cb(msg.Content) // 实时调用cb函数,方便主动发送给前端
        }
    }

    return fullResp.String(), nil //返回完整内容,方便后续存储
}

func (o *OpenAIModel) GetModelType() string { return "openai" }

// =================== Ollama 实现 ===================

// OllamaModel Ollama模型实现
type OllamaModel struct {
    llm model.ToolCallingChatModel
}

func NewOllamaModel(ctx context.Context, baseURL, modelName string) (*OllamaModel, error) {
    llm, err := ollama.NewChatModel(ctx, &ollama.ChatModelConfig{
        BaseURL: baseURL,
        Model:   modelName,
    })
    if err != nil {
        return nil, fmt.Errorf("create ollama model failed: %v", err)
    }
    return &OllamaModel{llm: llm}, nil
}

func (o *OllamaModel) GenerateResponse(ctx context.Context, messages []*schema.Message) (*schema.Message, error) {
    resp, err := o.llm.Generate(ctx, messages)
    if err != nil {
        return nil, fmt.Errorf("ollama generate failed: %v", err)
    }
    return resp, nil
}

func (o *OllamaModel) StreamResponse(ctx context.Context, messages []*schema.Message, cb StreamCallback) (string, error) {
    stream, err := o.llm.Stream(ctx, messages)
    if err != nil {
        return "", fmt.Errorf("ollama stream failed: %v", err)
    }
    defer stream.Close()
    var fullResp strings.Builder
    for {
        msg, err := stream.Recv()
        if err == io.EOF {
            break
        }
        if err != nil {
            return "", fmt.Errorf("openai stream recv failed: %v", err)
        }
        if len(msg.Content) > 0 {
            fullResp.WriteString(msg.Content) // 聚合
            cb(msg.Content)                   // 实时调用cb函数,方便主动发送给前端
        }
    }
    return fullResp.String(), nil //返回完整内容,方便后续存储
}

func (o *OllamaModel) GetModelType() string { return "ollama" }

评论

  1. Sankkooos
    Android Firefox 148.0
    10 小时前
    2026-3-24 19:07:18

    咪咪咪咪

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇