Write the Code. Change the World.

分类目录
7月 11

AI 对话项目,会用到流式返回。一般客户端不会直接调用 AI 对话,而是调用自己的服务,自己的服务再调用 AI. 服务端可以通过 websocket 的方式来达到这个效果,也可以通过 SSE 的方式来达到这个效果。而 AI 也可能会提供这两种方式来提供服务。这样一来,项目就可能有四种组合方式来进行 AI 对话。如下:

客户端 服务端 AI
APP、浏览器 websocket websocket
APP、浏览器 websocket SSE
APP、浏览器 SSE websocket
APP、浏览器 SSE SSE

https://developer.mozilla.org/zh-CN/docs/Web/API/Server-sent_events/Using_server-sent_events

找 AI

websocket 阵营

https://www.xfyun.cn/doc/spark/Web.html#_1-%E6%8E%A5%E5%8F%A3%E8%AF%B4%E6%98%8E

SSE 阵营

https://agents.baidu.com/docs/develop/out-deployment/conversation/

尝试实现四种组合

从最简单的开始。服务端通过 SSE 的方式, AI 也是 SSE 的方式。

先封装一个 curl 工具包,创建 internal/pkg/curl/curl.go,内容如下。

package curl

import (
    "bytes"
    "context"
    "fmt"
    "io"
    "net/http"

    "github.com/gogf/gf/v2/frame/g"
    "github.com/gogf/gf/v2/net/gclient"
)

type HttpClient struct {
    client *gclient.Client
}

func NewHttpClient() *HttpClient {
    return &HttpClient{
        client: g.Client(),
    }
}

func (h *HttpClient) Get(ctx context.Context, url string, headers map[string]string) (string, error) {
    res, err := h.client.Get(ctx, url, headers)
    if err != nil {
        return "", err
    }
    defer res.Close()

    return res.ReadAllString(), nil
}

func (h *HttpClient) Post(ctx context.Context, url string, data interface{}, headers map[string]string) (string, error) {
    for k, v := range headers {
        h.client.SetHeader(k, v)
    }

    res, err := h.client.Post(ctx, url, data, headers)
    if err != nil {
        return "", err
    }
    defer res.Close()
    return res.ReadAllString(), nil
}

func (h *HttpClient) PostStream(ctx context.Context, url string, data interface{}, headers map[string]string) (io.ReadCloser, error) {
    // 将 data 转换为 io.Reader
    var body io.Reader
    switch v := data.(type) {
    case string:
        body = bytes.NewReader([]byte(v))
    case []byte:
        body = bytes.NewReader(v)
    default:
        return nil, fmt.Errorf("unsupported data type: %T", v)
    }

    // 创建 HTTP 请求
    req, err := http.NewRequestWithContext(ctx, "POST", url, body)
    if err != nil {
        return nil, err
    }

    // 设置请求头
    for k, v := range headers {
        req.Header.Set(k, v)
    }

    // 发起请求
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return nil, err
    }

    // 检查响应状态码
    if resp.StatusCode != http.StatusOK {
        return nil, fmt.Errorf("request failed with status code: %d", resp.StatusCode)
    }

    return resp.Body, nil
}

现在来完善 chat 接口。

7月 11

AI 聊天,会用到流式返回的接口。用 goframe 框架尝试尝试。

开始

初始化项目

gf init stream.demo -u

cd stream.demo

gf run main.go

默认路由信息是这样的。

build: main.go
go build -o .\main.exe  main.go
.\main.exe
build running pid: 44364
2025-07-11T13:44:19.160+08:00 [INFO] pid[44364]: http server started listening on [:8000]
2025-07-11T13:44:19.160+08:00 [INFO] {ac79854f201c51188f16b244dba07b30} swagger ui is serving at address: http://127.0.0.1:8000/swagger/
2025-07-11T13:44:19.161+08:00 [INFO] {ac79854f201c51188f16b244dba07b30} openapi specification is serving at address: http://127.0.0.1:8000/api.json

  ADDRESS | METHOD |   ROUTE    |                           HANDLER                           |           MIDDLEWARE
----------|--------|------------|-------------------------------------------------------------|----------------------------------
  :8000   | ALL    | /api.json  | github.com/gogf/gf/v2/net/ghttp.(*Server).openapiSpec       |
----------|--------|------------|-------------------------------------------------------------|----------------------------------
  :8000   | GET    | /hello     | stream.demo/internal/controller/hello.(*ControllerV1).Hello | ghttp.MiddlewareHandlerResponse
----------|--------|------------|-------------------------------------------------------------|----------------------------------
  :8000   | ALL    | /swagger/* | github.com/gogf/gf/v2/net/ghttp.(*Server).swaggerUI         | HOOK_BEFORE_SERVE
----------|--------|------------|-------------------------------------------------------------|----------------------------------

提交版本

git init -b main
git add .
git commit -m 'initialize'

定义接口,创建控制器,定义路由

新建 api/chat/v1/chat.go 文件,定义以下请求和返回

package v1

import "github.com/gogf/gf/v2/frame/g"

type ChatReq struct {
    g.Meta  `path:"/chat" method:"post" tags:"聊天"  summary:"聊天请求"`
    Uid     uint64 `v:"required#用户ID不能为空" json:"uid" dc:"用户ID"`
    Message string `v:"required#消息内容不能为空" json:"message" dc:"消息内容"`
}

type ChatRes struct {
    IsEnd bool `json:"is_end" dc:"是否结束"`
}

执行 gf gen ctrl 生成控制器。修改 internal/cmd/cmd.go,增加路由配置。

import (
    ……
    "stream.demo/internal/controller/hello"
)

……
s.Group("/api", func(group *ghttp.RouterGroup) {
    group.Bind(
        chat.NewV1(),
    )
})

再运行 gf run main.go,把项目跑起来看看。发现 chat 路由已经好了。

build: main.go
go build -o .\main.exe  main.go
.\main.exe
build running pid: 44148
2025-07-11T14:13:29.676+08:00 [INFO] pid[44148]: http server started listening on [:8000]
2025-07-11T14:13:29.676+08:00 [INFO] {5065f8ccb71d5118823d646fdde40224} swagger ui is serving at address: http://127.0.0.1:8000/swagger/
2025-07-11T14:13:29.677+08:00 [INFO] {5065f8ccb71d5118823d646fdde40224} openapi specification is serving at address: http://127.0.0.1:8000/api.json

  ADDRESS | METHOD |   ROUTE    |                           HANDLER                           |           MIDDLEWARE
----------|--------|------------|-------------------------------------------------------------|----------------------------------
  :8000   | ALL    | /api.json  | github.com/gogf/gf/v2/net/ghttp.(*Server).openapiSpec       |
----------|--------|------------|-------------------------------------------------------------|----------------------------------
  :8000   | POST   | /api/chat      | stream.demo/internal/controller/chat.(*ControllerV1).Chat   | ghttp.MiddlewareHandlerResponse
----------|--------|------------|-------------------------------------------------------------|----------------------------------
  :8000   | GET    | /hello     | stream.demo/internal/controller/hello.(*ControllerV1).Hello | ghttp.MiddlewareHandlerResponse
----------|--------|------------|-------------------------------------------------------------|----------------------------------
  :8000   | ALL    | /swagger/* | github.com/gogf/gf/v2/net/ghttp.(*Server).swaggerUI         | HOOK_BEFORE_SERVE
----------|--------|------------|-------------------------------------------------------------|----------------------------------

开始流式 demo

编辑 internal/controller/chat/chat_v1_chat.go

package chat

import (
    "context"
    "fmt"
    "time"

    "github.com/gogf/gf/v2/net/ghttp"

    v1 "stream.demo/api/chat/v1"
)

func (c *ControllerV1) Chat(ctx context.Context, req *v1.ChatReq) (res *v1.ChatRes, err error) {
    r := ghttp.RequestFromCtx(ctx)
    r.Response.Header().Set("Content-Type", "text/event-stream")
    r.Response.Header().Set("Cache-Control", "no-cache")
    r.Response.Header().Set("Connection", "keep-alive")
    r.Response.Header().Set("Content-Encoding", "identity")

    for i := range 10 {
        data := fmt.Sprintf(`{"uid":%d, "message":"%s","index":%d}`, req.Uid, req.Message, i)
        r.Response.Writefln("data: %s\n\n", data)
        r.Response.Flush()
        time.Sleep(time.Millisecond * 1000)
    }

    fmt.Println("发送结束")
    res = &v1.ChatRes{}
    res.IsEnd = true
    return
}

重新运行项目。

测试

可以使用 curl 命令,可以使用 apifox 这种工具,也可以使用浏览器。 这里使用浏览器加 js 的方式来搞。

使用 chrome 打开 http://127.0.0.1:8000 ,然后打开控制台。输入以下测试代码,回车。

fetch("http://127.0.0.1:8000/api/chat", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    message: "舞低杨柳楼心月,歌尽桃花扇底风",
    uid: 1,
  }),
})
  .then(readStream)
  .catch(console.error);

async function readStream(res) {
  const reader = res.body.getReader();
  const decoder = new TextDecoder("utf-8");
  let buffer = "";

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    buffer += decoder.decode(value, { stream: true });

    let lines = buffer.split("\n\n");
    buffer = lines.pop();

    for (const chunk of lines) {
      const dataLine = chunk.trim();
      if (dataLine.startsWith("data: ")) {
        const data = dataLine.slice(6);
        console.log(getNowTime(), "消息:", data);
      }
    }
  }
}

function getNowTime() {
  const date = new Date();
  return date.toLocaleTimeString("zh-CN", {
    hour12: false,
    timeZone: "Asia/Shanghai",
  });
}

截图如下:

提交版本控制。

git add .
git commit -m '增加 chat 接口,并测试流式响应'

nginx 处理

如果走 nginx 代理,需要处理关闭相应缓冲

location /api/ {
    proxy_pass http://你的后端服务地址;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_buffering off;
    proxy_cache off;
    proxy_request_buffering off;
    chunked_transfer_encoding on;
}
4月 26

goframe 生成不重复的 7 位数字,并和数据库 user 表中字段不重复

func generateUniqueID() int {
    src := rand.NewSource(time.Now().UnixNano())
    min := 1000000
    max := 10000000
    return rand.New(src).Intn(max-min) + min
}

func generateUniqueViewID(ctx context.Context) (int, error) {
    for {
        viewId := generateUniqueID()
        id, err := dao.Users.Ctx(ctx).Where(do.Users{Viewid: viewId}).Fields("id").Value()
        if err != nil {
            return 0, err
        }

        if id == nil {
            return viewId, nil
        }
    }
}
9月 18

不能只让 go 在本地跑,得让 go 编译后,在服务器上跑。

好,那就这么干

  • 编译。
  • 放到服务端,使用守护进程的方式启动它。比如可以用 nohub、supervisord、systemctl 等。
  • 使用 nginx 做重定向或代理,配置域名访问。

编译

先看看我的服务器信息。

lsb_release -a

LSB Version:    :core-4.1-amd64:core-4.1-noarch
Distributor ID: AlibabaCloud
Description:    Alibaba Cloud Linux release 3 (Soaring Falcon)
Release:    3
Codename:   SoaringFalcon

hack/config.yaml 中,增加编译的配置信息

  build:
    name:     "main"
    arch:     "amd64"
    system:   "linux"
    mod:      "none"
    packSrc:  "resource,manifest"
    version:  "v1.0.0"
    output:   "./out"
    cgo:      0
    extra:    ""

注意,manifest/config 下的 mysql,redis 等配置,一定要和服务端一致。好了,可以打包了。

gf build

继续阅读

9月 11

orm with 处理也轻微使用了下。现在来处理输出。处理大输出。抱怨总是包裹一层太久了,抱怨无论啥都返回 http 200 。现在要改变这个状况。

在处理这个问题之前,我们得先找到整个框架的返回是怎么实现的。其实,在之前也想到了用后置中间件来处理。看官方自己也确实是用后置中间件来统一处理返回的。我们建立一个自己的中间件,替换掉官方自己的中间件不就好了。

官方默认中间件。

import  "github.com/gogf/gf/v2/net/ghttp"
ghttp.MiddlewareHandlerResponse

之前也说过一次。gf 中间件的函数都有且仅有一个参数 r *ghttp.Request。只要实现了该方法即可。当然 r.Middleware.Next() 这个方法是要调用的。

在这个方法之前处理的逻辑,叫前置。在这个方法之后处理的逻辑,叫后置。

go 的中间件的思路和 laravel 的也是一样的。估计其他语言也是这个思路。就是前端的 axios 等的拦截器也是有着这方面的思路。

继续阅读

9月 10

该后台使用 vite + ts + pnpm + vue3 + element-plus + tailwindcss 等技术栈构成。没有添加任意可视化图标等插件。以最小功能,最基础功能展现。用户可以额外添加可使用的插件逻辑。

该后台后端使用 php8.2 + laravel 10 + mysql

该后台后端 go 语言版本开发中。将使用 gframe2.5.2

源码: https://github.com/vini123/simpleAdmin

在线体验: https://www.zeipan.com/admin

权限以及密码一键复位: https://v3test.yuepaibao.com/admin/api/reset

测试账号以及密码: zhoulin@xiangrong.pro、 111111 (如果发现登录不了,可一键复位谢谢)

继续阅读

9月 09

goframe 没有其他 orm 的 hasOne,hasMany, belongsTo 等等这样的模型关联。 goframe 说这个对开发者有一定的心智负担。goframe 有 scanList 和 自己的 with。先了解试试用吧。

https://goframe.org/pages/viewpage.action?pageId=1114326

https://goframe.org/pages/viewpage.action?pageId=7297190

之前完成了用户的登录、注册以及个人信息的获取。这个个人信息的获取,只是单表的信息。现在想要用户的其他信息。这个时候,就需要更多的查询。

如果使用 orm,就可以通过 with 就可以搞定的。但是 with 只有定义好了规则才有用。所以,这个时候就需要组合想要的 dao 了。而 dao 是通过 gf gen dao 生的,只要表不变, dao 就不变。重复生也不变,所以就不能去修改 dao。那么只能新文件中来组合了。

因为之前定义 req 和 res 的时候,已经在 model 里边加了一个文件。那么在其中再加一个组合关系就好。

于是,修改 internal/model/admin/user.go,增加以下组合。

type User struct {
    entity.Users
    UserExtends *entity.UserExtends      `orm:"with:user_id=id" json:"user_extends"`
    Roles       []map[string]interface{} `json:"roles,omitempty"`
    Permissions []Permission             `json:"permissions,omitempty"`
}

// 用户权限
type Permission struct {
    entity.Permissions
    Permission []Permission `json:"permission,omitempty"`
}

从组合中可以看到使用 orm 来获取的仅仅是扩展信息。另外的 roles 和 permissions 是另外赋值进去的。

roles 和 permissions 之所以是另外赋值。是因为它们的获取会关联更多的表。不太适合嵌套去处理。

虽然 User 中定义了 with 相关的 UserExtends。 如果调用的时候,没使用 with 也是不会去查询的。其他的两个,如果不赋值也是不会有的。最基础的 Users 依然还是 entity 里的那个 user。

后来还是改了,表名还是用复数吧。所以命令回来生成的 dao 和 model 也是复数。复数就复数吧。

继续阅读

9月 09

获取图片验证码、登录、获取个人信息都好了。现在做注册功能。

在 ctrl 中调整好姿势。

internal/controller/user/user_admin_sign_up.go

package user

import (
    "context"

    "goSimpleAdmin/api/user/admin"
    model "goSimpleAdmin/internal/model/admin"
    "goSimpleAdmin/internal/service"
)

func (c *ControllerAdmin) SignUp(ctx context.Context, req *admin.SignUpReq) (res *admin.SignUpRes, err error) {
    data, err := service.User().SignUp(ctx, model.SignUpReq{
        Nickname:   req.Nickname,
        Account:    req.Account,
        Password:   req.Password,
        VerifyKey:  req.VerifyKey,
        VerifyCode: req.VerifyCode,
    })

    if err != nil {
        return
    }

    res = new(admin.SignUpRes)
    res.SignUpRes = data
    return res, err
}

继续阅读

9月 08

上一步中,生成图片验证码接口已经好了。现在完善登录和注册的业务逻辑。这里准备使用 token 的方式来维护用户的状态,选用 jwt 。所以登录或注册成功后,只需返回用户一个 token 就可以了。过期时间暂时先不要。所以登录和注册的返回需要修改一番。

登录注册输出重新定义一下

internal/model/admin/user.go 中定义输入和输出的结构。

type SignInRes struct {
    Token string `json:"token"`
}

type SignUpRes struct {
    Token string `json:"token"`
}

api/user/admin/user.go 中组合输入输出

package admin

import (
    model "goSimpleAdmin/internal/model/admin"

    "github.com/gogf/gf/v2/frame/g"
)

type SignInRes struct {
    model.SignInRes
}

type SignUpRes struct {
    model.SignUpRes
}

因为 api 定义的时候已经定义好了输出。所以 ctrl 和 service 这些不需要再重新生一次了。

但是在 logic 和 service 中没有定义 res。所以需要补上,再重新生一次。 service 就是接口的定义,实现还是在 logic 中。

继续阅读

9月 07

随着业务逻辑的充实,发现前边有些地方还是需要修改和完善。哪里呢。就是最最开始的地方,就是定义 api 的那地方。其实在那里,最开始,只需要定义 Req 和 Res。里边不需要先填充任意的东西。为什么这么说呢。因为 路由指向 ctrl,ctrl 通过 service 的绑定去调用 logic。而 logic 里的输入输出却是在 mode 里定义的。所以,model 里边的输入输出和 api 里的输入输出一般都是一样的。这个时候,只需要定义 model 里的输出输出。 api 那边直接继承过来就可以了。所以,步骤是这样的。

  • 定义 api 里的 req 和 res,空的就行(接口path 和 method 还是要加上),名字和方法名字想好。执行 gf gen ctrl 生就好。
  • 然后再生成 dao 和 model。 执行 gf gen dao 生就好。
  • 再在 model 下定义输入和输出。这个定义输入和输出也有一点矛盾。是业务场景带来的。model 和 dao 自身一般是不会有矛盾的,因为他们对应的数据库里的表。而输出和输入却要对应业务场景。比如一个项目有后台登录和 app 登录两个场景。往往,会分出两个路由组来处理这两种业务场景。虽然都是登录,输入和输出一般都不会都是相同的。这个时候 model 下定义输入和输出就得加个分组了。比如当前逻辑是后台逻辑。我会在 internal/model/ 下再多建立一层文件夹,就叫 admin。然后在里边再建立对应的输入输出文件。其实叫的这个 admin 和 api 下的文件夹是一个意思。就是初始化项目时候 api 下的 v1 的意思。这个时候都是 v1 或都是 admin,继承的时候就要用别名哈。下边会给出例子。
  • 再去建立 logic 的初步实现。然后去生 service,再回头完善 logic。 gf gen service

在 api req 的时候,里边一定要有 g.Meta。否则,不会生成对应的 ctrl 文件。

go 语言刚开始学,goframe 也是第一次用。如果理解错了,可以留言哈。

继续阅读