├── .gitignore ├── README.md ├── build.sh ├── cmd └── fg-apiserver │ ├── app │ ├── config.go │ ├── options │ │ └── options.go │ └── server.go │ └── main.go ├── configs ├── .keep ├── fastgo.sql └── fg-apiserver.yaml ├── docs ├── .keep └── images │ └── skills.jpg ├── go.mod ├── go.sum ├── internal ├── apiserver │ ├── biz │ │ ├── README.md │ │ ├── biz.go │ │ ├── doc.go │ │ ├── v1 │ │ │ ├── post │ │ │ │ └── post.go │ │ │ └── user │ │ │ │ └── user.go │ │ └── v2 │ │ │ └── .keep │ ├── handler │ │ ├── handler.go │ │ ├── post.go │ │ └── user.go │ ├── model │ │ ├── hook.go │ │ ├── post.gen.go │ │ └── user.gen.go │ ├── pkg │ │ ├── conversion │ │ │ ├── post.go │ │ │ └── user.go │ │ └── validation │ │ │ ├── post.go │ │ │ ├── user.go │ │ │ └── validation.go │ ├── server.go │ └── store │ │ ├── README.md │ │ ├── doc.go │ │ ├── logger.go │ │ ├── post.go │ │ ├── store.go │ │ └── user.go └── pkg │ ├── contextx │ ├── contextx.go │ └── doc.go │ ├── core │ └── core.go │ ├── errorsx │ ├── code.go │ ├── errorsx.go │ ├── post.go │ └── user.go │ ├── known │ └── known.go │ ├── middleware │ ├── authn.go │ ├── header.go │ └── requestid.go │ └── rid │ ├── doc.go │ ├── rid.go │ ├── rid_test.go │ └── salt.go ├── pkg ├── api │ └── apiserver │ │ └── v1 │ │ ├── post.go │ │ └── user.go ├── auth │ └── authn.go ├── options │ └── mysql_options.go ├── token │ ├── doc.go │ └── token.go └── version │ ├── doc.go │ ├── flag.go │ └── version.go └── scripts ├── .keep └── test.sh /.gitignore: -------------------------------------------------------------------------------- 1 | # 备份文件 2 | *.bak 3 | *~ 4 | 5 | # Go 工作区文件。Go 项目开发中,不建议将 Go 工作区文件提交到代码仓库 6 | go.work 7 | go.work.sum 8 | 9 | # 日志文件 10 | *.log 11 | 12 | # 自定义文件 13 | /_output 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## fastgo 项目 2 | 3 | - 云原生 AI 实战营项目之一,更多精彩项目见:[云原生 AI 实战营](https://konglingfei.com/)。 4 | - 该实战项目配套的课程目录见: [Go 项目开发极速入门课](https://konglingfei.com/cloudai/catalog/newbie.html)。 5 | 6 | 7 | ## fastgo 项目适宜人群 8 | 9 | - 掌握一定 Go 基础语法,想通过一个完整的实战,来快速系统学习 Go 项目开发的初学者; 10 | - 有意从事 Go 项目开发,但尚未入门或入门尚浅的同学。 11 | 12 | ## 项目快速部署 13 | 14 | ```bash 15 | $ mkdir -p $HOME/golang/src/github.com/onexstack/ 16 | $ cd $HOME/golang/src/github.com/onexstack/ 17 | $ git clone https://github.com/onexstack/fastgo 18 | $ cd fastgo/ 19 | $ ./build.sh 20 | $ _output/fg-apiserver -c configs/fg-apiserver.yaml 21 | ``` 22 | 23 | **注意:** 24 | 25 | 1. 要登录 MySQL 并且执行 `source configs/fastgo.sql;` 创建 `fastgo` 数据库及表; 26 | 2. 更新 `configs/fg-apiserver.yaml` 中 `mysql` 配置项。 27 | 28 | ## fastgo 包含的技能点 29 | 30 | 技能点见下图所示: 31 | 32 | ![](./docs/images/skills.jpg) 33 | 34 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 获取脚本所在目录作为项目根目录 4 | PROJ_ROOT_DIR=$(dirname "${BASH_SOURCE[0]}") 5 | 6 | # 定义构建产物的输出目录为项目根目录下的_output文件夹 7 | OUTPUT_DIR=${PROJ_ROOT_DIR}/_output 8 | 9 | # 指定版本信息包的路径,后续会通过-ldflags参数将版本信息注入到这个包的变量中 10 | VERSION_PACKAGE=github.com/onexstack/fastgo/pkg/version 11 | 12 | # 确定VERSION值:如果环境变量中没有设置VERSION,则使用git标签作为版本号 13 | # git describe --tags --always --match='v*'命令会获取最近的v开头的标签,如果没有则使用当前commit的短哈希 14 | if [[ -z "${VERSION}" ]];then 15 | VERSION=$(git describe --tags --always --match='v*') 16 | fi 17 | 18 | # 检查代码仓库状态:判断工作目录是否干净 19 | # 默认状态设为"dirty"(有未提交更改) 20 | GIT_TREE_STATE="dirty" 21 | # 使用git status检查是否有未提交的更改 22 | is_clean=$(shell git status --porcelain 2>/dev/null) 23 | # 如果is_clean为空,说明没有未提交的更改,状态设为"clean" 24 | if [[ -z ${is_clean} ]];then 25 | GIT_TREE_STATE="clean" 26 | fi 27 | 28 | # 获取当前git commit的完整哈希值 29 | GIT_COMMIT=$(git rev-parse HEAD) 30 | 31 | # 构造链接器标志(ldflags) 32 | # 通过-X选项向VERSION_PACKAGE包中注入以下变量的值: 33 | # - gitVersion: 版本号 34 | # - gitCommit: 构建时的commit哈希 35 | # - gitTreeState: 代码仓库状态(clean或dirty) 36 | # - buildDate: 构建日期和时间(UTC格式) 37 | GO_LDFLAGS="-X ${VERSION_PACKAGE}.gitVersion=${VERSION} \ 38 | -X ${VERSION_PACKAGE}.gitCommit=${GIT_COMMIT} \ 39 | -X ${VERSION_PACKAGE}.gitTreeState=${GIT_TREE_STATE} \ 40 | -X ${VERSION_PACKAGE}.buildDate=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" 41 | 42 | # 执行Go构建命令 43 | # -v: 显示详细编译信息 44 | # -ldflags: 传入上面定义的链接器标志 45 | # -o: 指定输出文件路径和名称 46 | # 最后参数是入口文件路径 47 | go build -v -ldflags "${GO_LDFLAGS}" -o ${OUTPUT_DIR}/fg-apiserver -v cmd/fg-apiserver/main.go 48 | -------------------------------------------------------------------------------- /cmd/fg-apiserver/app/config.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 孔令飞 . All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. The original repo for 4 | // this file is https://github.com/onexstack/fastgo. The professional 5 | // version of this repository is https://github.com/onexstack/onex. 6 | 7 | package app 8 | 9 | import ( 10 | "os" 11 | "path/filepath" 12 | "strings" 13 | 14 | "github.com/spf13/cobra" 15 | "github.com/spf13/viper" 16 | ) 17 | 18 | const ( 19 | // defaultHomeDir 定义放置 fastgo 服务配置的默认目录. 20 | defaultHomeDir = ".fastgo" 21 | 22 | // defaultConfigName 指定 fastgo 服务的默认配置文件名. 23 | defaultConfigName = "fg-apiserver.yaml" 24 | ) 25 | 26 | // onInitialize 设置需要读取的配置文件名、环境变量,并将其内容读取到 viper 中. 27 | func onInitialize() { 28 | if configFile != "" { 29 | // 从命令行选项指定的配置文件中读取 30 | viper.SetConfigFile(configFile) 31 | } else { 32 | // 使用默认配置文件路径和名称 33 | for _, dir := range searchDirs() { 34 | // 将 dir 目录加入到配置文件的搜索路径 35 | viper.AddConfigPath(dir) 36 | } 37 | 38 | // 设置配置文件格式为 YAML 39 | viper.SetConfigType("yaml") 40 | 41 | // 配置文件名称(没有文件扩展名) 42 | viper.SetConfigName(defaultConfigName) 43 | } 44 | 45 | // 读取环境变量并设置前缀 46 | setupEnvironmentVariables() 47 | 48 | // 读取配置文件.如果指定了配置文件名,则使用指定的配置文件,否则在注册的搜索路径中搜索 49 | _ = viper.ReadInConfig() 50 | } 51 | 52 | // setupEnvironmentVariables 配置环境变量规则. 53 | func setupEnvironmentVariables() { 54 | // 允许 viper 自动匹配环境变量 55 | viper.AutomaticEnv() 56 | // 设置环境变量前缀 57 | viper.SetEnvPrefix("FASTGO") 58 | // 替换环境变量 key 中的分隔符 '.' 和 '-' 为 '_' 59 | replacer := strings.NewReplacer(".", "_", "-", "_") 60 | viper.SetEnvKeyReplacer(replacer) 61 | } 62 | 63 | // searchDirs 返回默认的配置文件搜索目录. 64 | func searchDirs() []string { 65 | // 获取用户主目录 66 | homeDir, err := os.UserHomeDir() 67 | // 如果获取用户主目录失败,则打印错误信息并退出程序 68 | cobra.CheckErr(err) 69 | return []string{filepath.Join(homeDir, defaultHomeDir), "."} 70 | } 71 | 72 | // filePath 获取默认配置文件的完整路径. 73 | func filePath() string { 74 | home, err := os.UserHomeDir() 75 | // 如果不能获取用户主目录,则记录错误并返回空路径 76 | cobra.CheckErr(err) 77 | return filepath.Join(home, defaultHomeDir, defaultConfigName) 78 | } 79 | -------------------------------------------------------------------------------- /cmd/fg-apiserver/app/options/options.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 孔令飞 . All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. The original repo for 4 | // this file is https://github.com/onexstack/fastgo. The professional 5 | // version of this repository is https://github.com/onexstack/onex. 6 | 7 | // nolint: err113 8 | package options 9 | 10 | import ( 11 | "fmt" 12 | "net" 13 | "strconv" 14 | "time" 15 | 16 | "github.com/onexstack/fastgo/internal/apiserver" 17 | genericoptions "github.com/onexstack/fastgo/pkg/options" 18 | ) 19 | 20 | type ServerOptions struct { 21 | MySQLOptions *genericoptions.MySQLOptions `json:"mysql" mapstructure:"mysql"` 22 | Addr string `json:"addr" mapstructure:"addr"` 23 | // JWTKey 定义 JWT 密钥. 24 | JWTKey string `json:"jwt-key" mapstructure:"jwt-key"` 25 | // Expiration 定义 JWT Token 的过期时间. 26 | Expiration time.Duration `json:"expiration" mapstructure:"expiration"` 27 | } 28 | 29 | // NewServerOptions 创建带有默认值的 ServerOptions 实例. 30 | func NewServerOptions() *ServerOptions { 31 | return &ServerOptions{ 32 | MySQLOptions: genericoptions.NewMySQLOptions(), 33 | Addr: "0.0.0.0:6666", 34 | Expiration: 2 * time.Hour, 35 | } 36 | } 37 | 38 | // Validate 校验 ServerOptions 中的选项是否合法. 39 | // 提示:Validate 方法中的具体校验逻辑可以由 Claude、DeepSeek、GPT 等 LLM 自动生成。 40 | func (o *ServerOptions) Validate() error { 41 | if err := o.MySQLOptions.Validate(); err != nil { 42 | return err 43 | } 44 | 45 | // 验证服务器地址 46 | if o.Addr == "" { 47 | return fmt.Errorf("server address cannot be empty") 48 | } 49 | 50 | // 检查地址格式是否为host:port 51 | _, portStr, err := net.SplitHostPort(o.Addr) 52 | if err != nil { 53 | return fmt.Errorf("invalid server address format '%s': %w", o.Addr, err) 54 | } 55 | 56 | // 验证端口是否为数字且在有效范围内 57 | port, err := strconv.Atoi(portStr) 58 | if err != nil || port < 1 || port > 65535 { 59 | return fmt.Errorf("invalid server port: %s", portStr) 60 | } 61 | 62 | // 校验 JWTKey 长度 63 | if len(o.JWTKey) < 6 { 64 | return fmt.Errorf("JWTKey must be at least 6 characters long") 65 | } 66 | 67 | return nil 68 | } 69 | 70 | // Config 基于 ServerOptions 构建 apiserver.Config. 71 | func (o *ServerOptions) Config() (*apiserver.Config, error) { 72 | return &apiserver.Config{ 73 | MySQLOptions: o.MySQLOptions, 74 | Addr: o.Addr, 75 | JWTKey: o.JWTKey, 76 | Expiration: o.Expiration, 77 | }, nil 78 | } 79 | -------------------------------------------------------------------------------- /cmd/fg-apiserver/app/server.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 孔令飞 . All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. The original repo for 4 | // this file is https://github.com/onexstack/fastgo. The professional 5 | // version of this repository is https://github.com/onexstack/onex. 6 | 7 | package app 8 | 9 | import ( 10 | "io" 11 | "log/slog" 12 | "os" 13 | 14 | "github.com/spf13/cobra" 15 | "github.com/spf13/viper" 16 | 17 | "github.com/onexstack/fastgo/cmd/fg-apiserver/app/options" 18 | "github.com/onexstack/fastgo/pkg/version" 19 | ) 20 | 21 | var configFile string // 配置文件路径 22 | 23 | // NewFastGOCommand 创建一个 *cobra.Command 对象,用于启动应用程序. 24 | func NewFastGOCommand() *cobra.Command { 25 | // 创建默认的应用命令行选项 26 | opts := options.NewServerOptions() 27 | 28 | cmd := &cobra.Command{ 29 | // 指定命令的名字,该名字会出现在帮助信息中 30 | Use: "fg-apiserver", 31 | // 命令的简短描述 32 | Short: "A very lightweight full go project", 33 | Long: `A very lightweight full go project, designed to help beginners quickly 34 | learn Go project development.`, 35 | // 命令出错时,不打印帮助信息。设置为 true 可以确保命令出错时一眼就能看到错误信息 36 | SilenceUsage: true, 37 | // 指定调用 cmd.Execute() 时,执行的 Run 函数 38 | RunE: func(cmd *cobra.Command, args []string) error { 39 | return run(opts) 40 | }, 41 | // 设置命令运行时的参数检查,不需要指定命令行参数。例如:./fg-apiserver param1 param2 42 | Args: cobra.NoArgs, 43 | } 44 | 45 | // 初始化配置函数,在每个命令运行时调用 46 | cobra.OnInitialize(onInitialize) 47 | 48 | // cobra 支持持久性标志(PersistentFlag),该标志可用于它所分配的命令以及该命令下的每个子命令 49 | // 推荐使用配置文件来配置应用,便于管理配置项 50 | cmd.PersistentFlags().StringVarP(&configFile, "config", "c", filePath(), "Path to the fg-apiserver configuration file.") 51 | 52 | // 添加 --version 标志 53 | version.AddFlags(cmd.PersistentFlags()) 54 | 55 | return cmd 56 | } 57 | 58 | // run 是主运行逻辑,负责初始化日志、解析配置、校验选项并启动服务器。 59 | func run(opts *options.ServerOptions) error { 60 | // 如果传入 --version,则打印版本信息并退出 61 | version.PrintAndExitIfRequested() 62 | 63 | // 初始化 slog 64 | initLog() 65 | 66 | // 将 viper 中的配置解析到 opts. 67 | if err := viper.Unmarshal(opts); err != nil { 68 | return err 69 | } 70 | 71 | // 校验命令行选项 72 | if err := opts.Validate(); err != nil { 73 | return err 74 | } 75 | 76 | // 获取应用配置. 77 | // 将命令行选项和应用配置分开,可以更加灵活的处理 2 种不同类型的配置. 78 | cfg, err := opts.Config() 79 | if err != nil { 80 | return err 81 | } 82 | 83 | // 创建服务器实例. 84 | server, err := cfg.NewServer() 85 | if err != nil { 86 | return err 87 | } 88 | 89 | // 启动服务器 90 | return server.Run() 91 | } 92 | 93 | // initLog 初始化全局日志实例 94 | func initLog() { 95 | // 获取日志配置 96 | format := viper.GetString("log.format") // 日志格式,支持:json、text 97 | level := viper.GetString("log.level") // 日志级别,支持:debug, info, warn, error 98 | output := viper.GetString("log.output") // 日志输出路径,支持:标准输出stdout和文件 99 | 100 | // 转换日志级别 101 | var slevel slog.Level 102 | switch level { 103 | case "debug": 104 | slevel = slog.LevelDebug 105 | case "info": 106 | slevel = slog.LevelInfo 107 | case "warn": 108 | slevel = slog.LevelWarn 109 | case "error": 110 | slevel = slog.LevelError 111 | default: 112 | slevel = slog.LevelInfo 113 | } 114 | 115 | opts := &slog.HandlerOptions{Level: slevel} 116 | 117 | var w io.Writer 118 | var err error 119 | // 转换日志输出路径 120 | switch output { 121 | case "": 122 | w = os.Stdout 123 | case "stdout": 124 | w = os.Stdout 125 | default: 126 | w, err = os.OpenFile(output, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) 127 | if err != nil { 128 | panic(err) 129 | } 130 | } 131 | 132 | // 转换日志格式 133 | if err != nil { 134 | return 135 | } 136 | var handler slog.Handler 137 | switch format { 138 | case "json": 139 | handler = slog.NewJSONHandler(w, opts) 140 | case "text": 141 | handler = slog.NewTextHandler(w, opts) 142 | default: 143 | handler = slog.NewJSONHandler(w, opts) 144 | 145 | } 146 | 147 | // 设置全局的日志实例为自定义的日志实例 148 | slog.SetDefault(slog.New(handler)) 149 | } 150 | -------------------------------------------------------------------------------- /cmd/fg-apiserver/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 孔令飞 . All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. The original repo for 4 | // this file is https://github.com/onexstack/fastgo. The professional 5 | // version of this repository is https://github.com/onexstack/onex. 6 | 7 | package main 8 | 9 | import ( 10 | "os" 11 | 12 | "github.com/onexstack/fastgo/cmd/fg-apiserver/app" 13 | // 导入 automaxprocs 包,可以在程序启动时自动设置 GOMAXPROCS 配置, 14 | // 使其与 Linux 容器的 CPU 配额相匹配。 15 | // 这避免了在容器中运行时,因默认 GOMAXPROCS 值不合适导致的性能问题, 16 | // 确保 Go 程序能够充分利用可用的 CPU 资源,避免 CPU 浪费。 17 | _ "go.uber.org/automaxprocs" 18 | ) 19 | 20 | // Go 程序的默认入口函数。阅读项目代码的入口函数. 21 | func main() { 22 | // 创建 Go 极速项目 23 | command := app.NewFastGOCommand() 24 | 25 | // 执行命令并处理错误 26 | if err := command.Execute(); err != nil { 27 | // 如果发生错误,则退出程序 28 | // 返回退出码,可以使其他程序(例如 bash 脚本)根据退出码来判断服务运行状态 29 | os.Exit(1) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /configs/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onexstack/fastgo/5ff4ad326cc4a23b59b793587279a061e238f6c3/configs/.keep -------------------------------------------------------------------------------- /configs/fastgo.sql: -------------------------------------------------------------------------------- 1 | -- Copyright 2024 孔令飞 . All rights reserved. 2 | -- Use of this source code is governed by a MIT style 3 | -- license that can be found in the LICENSE file. The original repo for 4 | -- this file is https://github.com/onexstack/fastgo. The professional 5 | -- version of this repository is https://github.com/onexstack/onex. 6 | 7 | -- MariaDB dump 10.19-11.2.2-MariaDB, for debian-linux-gnu (x86_64) 8 | -- 9 | -- Host: 10.37.91.93 Database: fastgo 10 | -- ------------------------------------------------------ 11 | -- Server version 10.11.6-MariaDB-0+deb12u1 12 | 13 | /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; 14 | /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; 15 | /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; 16 | /*!40101 SET NAMES utf8mb4 */; 17 | /*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; 18 | /*!40103 SET TIME_ZONE='+00:00' */; 19 | /*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; 20 | /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; 21 | /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; 22 | /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; 23 | 24 | -- 25 | -- Current Database: `fastgo` 26 | -- 27 | 28 | /*!40000 DROP DATABASE IF EXISTS `fastgo`*/; 29 | 30 | CREATE DATABASE /*!32312 IF NOT EXISTS*/ `fastgo` /*!40100 DEFAULT CHARACTER SET latin1 COLLATE latin1_swedish_ci */; 31 | 32 | USE `fastgo`; 33 | 34 | -- 35 | -- Table structure for table `post` 36 | -- 37 | 38 | DROP TABLE IF EXISTS `post`; 39 | /*!40101 SET @saved_cs_client = @@character_set_client */; 40 | /*!40101 SET character_set_client = utf8 */; 41 | CREATE TABLE `post` ( 42 | `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, 43 | `userID` varchar(36) NOT NULL DEFAULT '' COMMENT '用户唯一 ID', 44 | `postID` varchar(35) NOT NULL DEFAULT '' COMMENT '博文唯一 ID', 45 | `title` varchar(256) NOT NULL DEFAULT '' COMMENT '博文标题', 46 | `content` longtext NOT NULL DEFAULT '' COMMENT '博文内容', 47 | `createdAt` datetime NOT NULL DEFAULT current_timestamp() COMMENT '博文创建时间', 48 | `updatedAt` datetime NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp() COMMENT '博文最后修改时间', 49 | PRIMARY KEY (`id`), 50 | UNIQUE KEY `post.postID` (`postID`), 51 | KEY `idx.post.userID` (`userID`) 52 | ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci COMMENT='博文表'; 53 | /*!40101 SET character_set_client = @saved_cs_client */; 54 | 55 | -- 56 | -- Dumping data for table `post` 57 | -- 58 | 59 | LOCK TABLES `post` WRITE; 60 | /*!40000 ALTER TABLE `post` DISABLE KEYS */; 61 | /*!40000 ALTER TABLE `post` ENABLE KEYS */; 62 | UNLOCK TABLES; 63 | 64 | -- 65 | -- Table structure for table `user` 66 | -- 67 | 68 | DROP TABLE IF EXISTS `user`; 69 | /*!40101 SET @saved_cs_client = @@character_set_client */; 70 | /*!40101 SET character_set_client = utf8 */; 71 | CREATE TABLE `user` ( 72 | `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, 73 | `userID` varchar(36) NOT NULL DEFAULT '' COMMENT '用户唯一 ID', 74 | `username` varchar(255) NOT NULL DEFAULT '' COMMENT '用户名(唯一)', 75 | `password` varchar(255) NOT NULL DEFAULT '' COMMENT '用户密码(加密后)', 76 | `nickname` varchar(30) NOT NULL DEFAULT '' COMMENT '用户昵称', 77 | `email` varchar(256) NOT NULL DEFAULT '' COMMENT '用户电子邮箱地址', 78 | `phone` varchar(16) NOT NULL DEFAULT '' COMMENT '用户手机号', 79 | `createdAt` datetime NOT NULL DEFAULT current_timestamp() COMMENT '用户创建时间', 80 | `updatedAt` datetime NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp() COMMENT '用户最后修改时间', 81 | PRIMARY KEY (`id`), 82 | UNIQUE KEY `user.userID` (`userID`), 83 | UNIQUE KEY `user.username` (`username`), 84 | UNIQUE KEY `user.phone` (`phone`) 85 | ) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci COMMENT='用户表'; 86 | /*!40101 SET character_set_client = @saved_cs_client */; 87 | 88 | -- 89 | -- Dumping data for table `user` 90 | -- 91 | 92 | LOCK TABLES `user` WRITE; 93 | /*!40000 ALTER TABLE `user` DISABLE KEYS */; 94 | INSERT INTO `user` VALUES 95 | (96,'user-000000','root','$2a$10$ctsFXEUAMd7rXXpmccNlO.ZRiYGYz0eOfj8EicPGWqiz64YBBgR1y','colin404','colin404@foxmail.com','18110000000','2024-12-12 03:55:25','2024-12-12 03:55:25'); 96 | /*!40000 ALTER TABLE `user` ENABLE KEYS */; 97 | UNLOCK TABLES; 98 | /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; 99 | 100 | /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; 101 | /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; 102 | /*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; 103 | /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; 104 | /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; 105 | /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; 106 | /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; 107 | 108 | -- Dump completed on 2024-12-12 4:33:51 109 | -------------------------------------------------------------------------------- /configs/fg-apiserver.yaml: -------------------------------------------------------------------------------- 1 | # 通用配置 2 | # 3 | 4 | # JWT 签发密钥 5 | jwt-key: Rtg8BPKNEf2mB4mgvKONGPZZQSaJWNLijxR42qRgq0iBb5 6 | # JWT Token 过期时间 7 | expiration: 1000h 8 | 9 | # MySQL 数据库相关配置 10 | mysql: 11 | # MySQL 机器 IP 和端口,默认 127.0.0.1:3306 12 | addr: 127.0.0.1:3306 13 | # MySQL 用户名(建议授权最小权限集) 14 | username: fastgo 15 | # MySQL 用户密码 16 | password: fastgo1234 17 | # fastgo 系统所用的数据库名 18 | database: fastgo 19 | # MySQL 最大空闲连接数,默认 100 20 | max-idle-connections: 100 21 | # MySQL 最大打开的连接数,默认 100 22 | max-open-connections: 100 23 | # 空闲连接最大存活时间,默认 10s 24 | max-connection-life-time: 10s 25 | 26 | log: 27 | format: text 28 | level: info 29 | output: stdout 30 | -------------------------------------------------------------------------------- /docs/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onexstack/fastgo/5ff4ad326cc4a23b59b793587279a061e238f6c3/docs/.keep -------------------------------------------------------------------------------- /docs/images/skills.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onexstack/fastgo/5ff4ad326cc4a23b59b793587279a061e238f6c3/docs/images/skills.jpg -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/onexstack/fastgo 2 | 3 | go 1.24.0 4 | 5 | require ( 6 | github.com/gin-gonic/gin v1.10.0 7 | github.com/golang-jwt/jwt/v4 v4.5.1 8 | github.com/google/uuid v1.6.0 9 | github.com/gosuri/uitable v0.0.4 10 | github.com/jinzhu/copier v0.4.0 11 | github.com/onexstack/onexstack v0.0.2 12 | github.com/spf13/cobra v1.9.1 13 | github.com/spf13/pflag v1.0.6 14 | github.com/spf13/viper v1.19.0 15 | github.com/stretchr/testify v1.10.0 16 | go.uber.org/automaxprocs v1.6.0 17 | golang.org/x/crypto v0.32.0 18 | golang.org/x/sync v0.10.0 19 | gorm.io/driver/mysql v1.5.7 20 | gorm.io/gorm v1.25.12 21 | ) 22 | 23 | require ( 24 | filippo.io/edwards25519 v1.1.0 // indirect 25 | github.com/bytedance/sonic v1.12.5 // indirect 26 | github.com/bytedance/sonic/loader v0.2.0 // indirect 27 | github.com/cloudwego/base64x v0.1.4 // indirect 28 | github.com/cloudwego/iasm v0.2.0 // indirect 29 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 30 | github.com/fatih/color v1.18.0 // indirect 31 | github.com/fsnotify/fsnotify v1.8.0 // indirect 32 | github.com/gabriel-vasile/mimetype v1.4.3 // indirect 33 | github.com/gin-contrib/sse v0.1.0 // indirect 34 | github.com/go-playground/locales v0.14.1 // indirect 35 | github.com/go-playground/universal-translator v0.18.1 // indirect 36 | github.com/go-playground/validator/v10 v10.20.0 // indirect 37 | github.com/go-sql-driver/mysql v1.8.1 // indirect 38 | github.com/goccy/go-json v0.10.3 // indirect 39 | github.com/hashicorp/hcl v1.0.0 // indirect 40 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 41 | github.com/jinzhu/inflection v1.0.0 // indirect 42 | github.com/jinzhu/now v1.1.5 // indirect 43 | github.com/json-iterator/go v1.1.12 // indirect 44 | github.com/klauspost/cpuid/v2 v2.2.8 // indirect 45 | github.com/leodido/go-urn v1.4.0 // indirect 46 | github.com/magiconair/properties v1.8.7 // indirect 47 | github.com/mattn/go-colorable v0.1.13 // indirect 48 | github.com/mattn/go-isatty v0.0.20 // indirect 49 | github.com/mattn/go-runewidth v0.0.16 // indirect 50 | github.com/mitchellh/mapstructure v1.5.0 // indirect 51 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 52 | github.com/modern-go/reflect2 v1.0.2 // indirect 53 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect 54 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 55 | github.com/rivo/uniseg v0.4.2 // indirect 56 | github.com/sagikazarmark/locafero v0.4.0 // indirect 57 | github.com/sagikazarmark/slog-shim v0.1.0 // indirect 58 | github.com/sony/sonyflake v1.2.0 // indirect 59 | github.com/sourcegraph/conc v0.3.0 // indirect 60 | github.com/spf13/afero v1.11.0 // indirect 61 | github.com/spf13/cast v1.7.1 // indirect 62 | github.com/subosito/gotenv v1.6.0 // indirect 63 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 64 | github.com/ugorji/go/codec v1.2.12 // indirect 65 | go.uber.org/multierr v1.11.0 // indirect 66 | golang.org/x/arch v0.8.0 // indirect 67 | golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect 68 | golang.org/x/net v0.34.0 // indirect 69 | golang.org/x/sys v0.29.0 // indirect 70 | golang.org/x/text v0.21.0 // indirect 71 | google.golang.org/protobuf v1.36.3 // indirect 72 | gopkg.in/ini.v1 v1.67.0 // indirect 73 | gopkg.in/yaml.v3 v3.0.1 // indirect 74 | ) 75 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= 2 | filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= 3 | github.com/bytedance/sonic v1.12.5 h1:hoZxY8uW+mT+OpkcUWw4k0fDINtOcVavEsGfzwzFU/w= 4 | github.com/bytedance/sonic v1.12.5/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk= 5 | github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= 6 | github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP1iU4kWM= 7 | github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= 8 | github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= 9 | github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= 10 | github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= 11 | github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= 12 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 13 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 14 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 15 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 16 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 17 | github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= 18 | github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= 19 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 20 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 21 | github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= 22 | github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= 23 | github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= 24 | github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= 25 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 26 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 27 | github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= 28 | github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= 29 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 30 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 31 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 32 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 33 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 34 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 35 | github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= 36 | github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= 37 | github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= 38 | github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= 39 | github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= 40 | github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= 41 | github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 42 | github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo= 43 | github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= 44 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 45 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 46 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 47 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 48 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 49 | github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY= 50 | github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo= 51 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 52 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 53 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 54 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 55 | github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= 56 | github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= 57 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 58 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 59 | github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= 60 | github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 61 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 62 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 63 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 64 | github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= 65 | github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 66 | github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= 67 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 68 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 69 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 70 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 71 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 72 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 73 | github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= 74 | github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 75 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 76 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 77 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 78 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 79 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 80 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 81 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 82 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 83 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 84 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 85 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 86 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 87 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 88 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 89 | github.com/onexstack/onexstack v0.0.2 h1:Rs/ffFvTo7cd4YTyNs8dX3WQ5dDOdKaA1q8+LTr7pGc= 90 | github.com/onexstack/onexstack v0.0.2/go.mod h1:5Pp2aMiVEJarNi9XKTlutNYTx/ML/DJgbVNfeCLlfNU= 91 | github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= 92 | github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= 93 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 94 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 95 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 96 | github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= 97 | github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= 98 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 99 | github.com/rivo/uniseg v0.4.2 h1:YwD0ulJSJytLpiaWua0sBDusfsCZohxjxzVTYjwxfV8= 100 | github.com/rivo/uniseg v0.4.2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 101 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 102 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 103 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 104 | github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= 105 | github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= 106 | github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= 107 | github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= 108 | github.com/sony/sonyflake v1.2.0 h1:Pfr3A+ejSg+0SPqpoAmQgEtNDAhc2G1SUYk205qVMLQ= 109 | github.com/sony/sonyflake v1.2.0/go.mod h1:LORtCywH/cq10ZbyfhKrHYgAUGH7mOBa76enV9txy/Y= 110 | github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= 111 | github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= 112 | github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= 113 | github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= 114 | github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= 115 | github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= 116 | github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= 117 | github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= 118 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 119 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 120 | github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= 121 | github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= 122 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 123 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 124 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 125 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 126 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 127 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 128 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 129 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 130 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 131 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 132 | github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= 133 | github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= 134 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 135 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 136 | github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= 137 | github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 138 | go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= 139 | go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= 140 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 141 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 142 | golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= 143 | golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= 144 | golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= 145 | golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= 146 | golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY= 147 | golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8= 148 | golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= 149 | golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= 150 | golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= 151 | golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 152 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 153 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 154 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 155 | golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= 156 | golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 157 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 158 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 159 | google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= 160 | google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 161 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 162 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 163 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 164 | gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= 165 | gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 166 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 167 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 168 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 169 | gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo= 170 | gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= 171 | gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= 172 | gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= 173 | gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= 174 | nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= 175 | -------------------------------------------------------------------------------- /internal/apiserver/biz/README.md: -------------------------------------------------------------------------------- 1 | # Biz 层 2 | 3 | Biz 目录下包含了 `v1`、`v2` 这类版本化的目录及每个资源独占一个目录,原因如下: 4 | 1. 随着产品功能的迭代,业务逻辑可能会出现不兼容变更,也即可能会出现 v2 版本,所以 Biz 目录下包含了`v1`、`v2` 这类版本化目录; 5 | 2. Biz 层用来处理业务逻辑,代码量大,为了便于维护代码,在 Biz 层,将不同资源存放在单独的目录中,例如:`v1/post`、`v1/user`; 6 | -------------------------------------------------------------------------------- /internal/apiserver/biz/biz.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 孔令飞 . All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. The original repo for 4 | // this file is https://github.com/onexstack/fastgo. The professional 5 | // version of this repository is https://github.com/onexstack/onex. 6 | 7 | package biz 8 | 9 | //go:generate mockgen -destination mock_biz.go -package biz github.com/onexstack/fastgo/internal/apiserver/biz IBiz 10 | 11 | import ( 12 | postv1 "github.com/onexstack/fastgo/internal/apiserver/biz/v1/post" 13 | userv1 "github.com/onexstack/fastgo/internal/apiserver/biz/v1/user" 14 | 15 | // Post V2 版本(未实现,仅展示用) 16 | // postv2 "github.com/onexstack/fastgo/internal/apiserver/biz/v2/post". 17 | "github.com/onexstack/fastgo/internal/apiserver/store" 18 | ) 19 | 20 | // IBiz 定义了业务层需要实现的方法. 21 | type IBiz interface { 22 | // 获取用户业务接口. 23 | UserV1() userv1.UserBiz 24 | // 获取帖子业务接口. 25 | PostV1() postv1.PostBiz 26 | // 获取帖子业务接口(V2版本). 27 | // PostV2() post.PostBiz 28 | } 29 | 30 | // biz 是 IBiz 的一个具体实现. 31 | type biz struct { 32 | store store.IStore 33 | } 34 | 35 | // 确保 biz 实现了 IBiz 接口. 36 | var _ IBiz = (*biz)(nil) 37 | 38 | // NewBiz 创建一个 IBiz 类型的实例. 39 | func NewBiz(store store.IStore) *biz { 40 | return &biz{store: store} 41 | } 42 | 43 | // UserV1 返回一个实现了 UserBiz 接口的实例. 44 | func (b *biz) UserV1() userv1.UserBiz { 45 | return userv1.New(b.store) 46 | } 47 | 48 | // PostV1 返回一个实现了 PostBiz 接口的实例. 49 | func (b *biz) PostV1() postv1.PostBiz { 50 | return postv1.New(b.store) 51 | } 52 | -------------------------------------------------------------------------------- /internal/apiserver/biz/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 孔令飞 . All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. The original repo for 4 | // this file is https://github.com/onexstack/fastgo. The professional 5 | // version of this repository is https://github.com/onexstack/onex. 6 | 7 | // Package biz is the place where you can implements more complex business logic. 8 | package biz // import "github.com/onexstack/fastgo/internal/apiserver/biz" 9 | -------------------------------------------------------------------------------- /internal/apiserver/biz/v1/post/post.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 孔令飞 . All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. The original repo for 4 | // this file is https://github.com/onexstack/fastgo. The professional 5 | // version of this repository is https://github.com/onexstack/onex. 6 | 7 | package post 8 | 9 | //go:generate mockgen -destination mock_post.go -package post github.com/onexstack/fastgo/internal/apiserver/biz/v1/post PostBiz 10 | 11 | import ( 12 | "context" 13 | 14 | "github.com/jinzhu/copier" 15 | "github.com/onexstack/onexstack/pkg/store/where" 16 | 17 | "github.com/onexstack/fastgo/internal/apiserver/model" 18 | "github.com/onexstack/fastgo/internal/apiserver/pkg/conversion" 19 | "github.com/onexstack/fastgo/internal/apiserver/store" 20 | "github.com/onexstack/fastgo/internal/pkg/contextx" 21 | apiv1 "github.com/onexstack/fastgo/pkg/api/apiserver/v1" 22 | ) 23 | 24 | // PostBiz 定义处理帖子请求所需的方法. 25 | type PostBiz interface { 26 | Create(ctx context.Context, rq *apiv1.CreatePostRequest) (*apiv1.CreatePostResponse, error) 27 | Update(ctx context.Context, rq *apiv1.UpdatePostRequest) (*apiv1.UpdatePostResponse, error) 28 | Delete(ctx context.Context, rq *apiv1.DeletePostRequest) (*apiv1.DeletePostResponse, error) 29 | Get(ctx context.Context, rq *apiv1.GetPostRequest) (*apiv1.GetPostResponse, error) 30 | List(ctx context.Context, rq *apiv1.ListPostRequest) (*apiv1.ListPostResponse, error) 31 | 32 | PostExpansion 33 | } 34 | 35 | // PostExpansion 定义额外的帖子操作方法. 36 | type PostExpansion interface{} 37 | 38 | // postBiz 是 PostBiz 接口的实现. 39 | type postBiz struct { 40 | store store.IStore 41 | } 42 | 43 | // 确保 postBiz 实现了 PostBiz 接口. 44 | var _ PostBiz = (*postBiz)(nil) 45 | 46 | // New 创建 postBiz 的实例. 47 | func New(store store.IStore) *postBiz { 48 | return &postBiz{store: store} 49 | } 50 | 51 | // Create 实现 PostBiz 接口中的 Create 方法. 52 | func (b *postBiz) Create(ctx context.Context, rq *apiv1.CreatePostRequest) (*apiv1.CreatePostResponse, error) { 53 | var postM model.Post 54 | _ = copier.Copy(&postM, rq) 55 | postM.UserID = contextx.UserID(ctx) 56 | 57 | if err := b.store.Post().Create(ctx, &postM); err != nil { 58 | return nil, err 59 | } 60 | 61 | return &apiv1.CreatePostResponse{PostID: postM.PostID}, nil 62 | } 63 | 64 | // Update 实现 PostBiz 接口中的 Update 方法. 65 | func (b *postBiz) Update(ctx context.Context, rq *apiv1.UpdatePostRequest) (*apiv1.UpdatePostResponse, error) { 66 | whr := where.F("userID", contextx.UserID(ctx), "postID", rq.PostID) 67 | postM, err := b.store.Post().Get(ctx, whr) 68 | if err != nil { 69 | return nil, err 70 | } 71 | 72 | if rq.Title != nil { 73 | postM.Title = *rq.Title 74 | } 75 | 76 | if rq.Content != nil { 77 | postM.Content = *rq.Content 78 | } 79 | 80 | if err := b.store.Post().Update(ctx, postM); err != nil { 81 | return nil, err 82 | } 83 | 84 | return &apiv1.UpdatePostResponse{}, nil 85 | } 86 | 87 | // Delete 实现 PostBiz 接口中的 Delete 方法. 88 | func (b *postBiz) Delete(ctx context.Context, rq *apiv1.DeletePostRequest) (*apiv1.DeletePostResponse, error) { 89 | whr := where.F("userID", contextx.UserID(ctx), "postID", rq.PostIDs) 90 | if err := b.store.Post().Delete(ctx, whr); err != nil { 91 | return nil, err 92 | } 93 | 94 | return &apiv1.DeletePostResponse{}, nil 95 | } 96 | 97 | // Get 实现 PostBiz 接口中的 Get 方法. 98 | func (b *postBiz) Get(ctx context.Context, rq *apiv1.GetPostRequest) (*apiv1.GetPostResponse, error) { 99 | whr := where.F("userID", contextx.UserID(ctx), "postID", rq.PostID) 100 | postM, err := b.store.Post().Get(ctx, whr) 101 | if err != nil { 102 | return nil, err 103 | } 104 | 105 | return &apiv1.GetPostResponse{Post: conversion.PostodelToPostV1(postM)}, nil 106 | } 107 | 108 | // List 实现 PostBiz 接口中的 List 方法. 109 | func (b *postBiz) List(ctx context.Context, rq *apiv1.ListPostRequest) (*apiv1.ListPostResponse, error) { 110 | whr := where.F("userID", contextx.UserID(ctx)).P(int(rq.Offset), int(rq.Limit)) 111 | if rq.Title != nil { 112 | whr = whr.Q("title like ?", "%"+*rq.Title+"%") 113 | } 114 | 115 | count, postList, err := b.store.Post().List(ctx, whr) 116 | if err != nil { 117 | return nil, err 118 | } 119 | 120 | posts := make([]*apiv1.Post, 0, len(postList)) 121 | for _, post := range postList { 122 | converted := conversion.PostodelToPostV1(post) 123 | posts = append(posts, converted) 124 | } 125 | 126 | return &apiv1.ListPostResponse{TotalCount: count, Posts: posts}, nil 127 | } 128 | -------------------------------------------------------------------------------- /internal/apiserver/biz/v1/user/user.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 孔令飞 . All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. The original repo for 4 | // this file is https://github.com/onexstack/fastgo. The professional 5 | // version of this repository is https://github.com/onexstack/onex. 6 | 7 | package user 8 | 9 | //go:generate mockgen -destination mock_user.go -package user github.com/onexstack/fastgo/internal/apiserver/biz/v1/user UserBiz 10 | 11 | import ( 12 | "context" 13 | "log/slog" 14 | "sync" 15 | 16 | "github.com/jinzhu/copier" 17 | "github.com/onexstack/onexstack/pkg/store/where" 18 | "golang.org/x/sync/errgroup" 19 | 20 | "github.com/onexstack/fastgo/internal/apiserver/model" 21 | "github.com/onexstack/fastgo/internal/apiserver/pkg/conversion" 22 | "github.com/onexstack/fastgo/internal/apiserver/store" 23 | "github.com/onexstack/fastgo/internal/pkg/contextx" 24 | "github.com/onexstack/fastgo/internal/pkg/errorsx" 25 | "github.com/onexstack/fastgo/internal/pkg/known" 26 | apiv1 "github.com/onexstack/fastgo/pkg/api/apiserver/v1" 27 | "github.com/onexstack/fastgo/pkg/auth" 28 | "github.com/onexstack/fastgo/pkg/token" 29 | ) 30 | 31 | // UserBiz 定义处理用户请求所需的方法. 32 | type UserBiz interface { 33 | Create(ctx context.Context, rq *apiv1.CreateUserRequest) (*apiv1.CreateUserResponse, error) 34 | Update(ctx context.Context, rq *apiv1.UpdateUserRequest) (*apiv1.UpdateUserResponse, error) 35 | Delete(ctx context.Context, rq *apiv1.DeleteUserRequest) (*apiv1.DeleteUserResponse, error) 36 | Get(ctx context.Context, rq *apiv1.GetUserRequest) (*apiv1.GetUserResponse, error) 37 | List(ctx context.Context, rq *apiv1.ListUserRequest) (*apiv1.ListUserResponse, error) 38 | 39 | UserExpansion 40 | } 41 | 42 | // UserExpansion 定义用户操作的扩展方法. 43 | type UserExpansion interface { 44 | Login(ctx context.Context, rq *apiv1.LoginRequest) (*apiv1.LoginResponse, error) 45 | RefreshToken(ctx context.Context, rq *apiv1.RefreshTokenRequest) (*apiv1.RefreshTokenResponse, error) 46 | ChangePassword(ctx context.Context, rq *apiv1.ChangePasswordRequest) (*apiv1.ChangePasswordResponse, error) 47 | } 48 | 49 | // userBiz 是 UserBiz 接口的实现. 50 | type userBiz struct { 51 | store store.IStore 52 | } 53 | 54 | // 确保 userBiz 实现了 UserBiz 接口. 55 | var _ UserBiz = (*userBiz)(nil) 56 | 57 | func New(store store.IStore) *userBiz { 58 | return &userBiz{store: store} 59 | } 60 | 61 | // Login 实现 UserBiz 接口中的 Login 方法. 62 | func (b *userBiz) Login(ctx context.Context, rq *apiv1.LoginRequest) (*apiv1.LoginResponse, error) { 63 | // 获取登录用户的所有信息 64 | whr := where.F("username", rq.Username) 65 | userM, err := b.store.User().Get(ctx, whr) 66 | if err != nil { 67 | return nil, errorsx.ErrUserNotFound 68 | } 69 | 70 | // 对比传入的明文密码和数据库中已加密过的密码是否匹配 71 | if err := auth.Compare(userM.Password, rq.Password); err != nil { 72 | slog.ErrorContext(ctx, "Failed to compare password", "err", err) 73 | return nil, errorsx.ErrPasswordInvalid 74 | } 75 | 76 | // 如果匹配成功,说明登录成功,签发 token 并返回 77 | tokenStr, expireAt, err := token.Sign(userM.UserID) 78 | if err != nil { 79 | slog.ErrorContext(ctx, "Failed to sign token", "err", err) 80 | return nil, errorsx.ErrSignToken 81 | } 82 | 83 | return &apiv1.LoginResponse{Token: tokenStr, ExpireAt: expireAt}, nil 84 | } 85 | 86 | // RefreshToken 用于刷新用户的身份验证令牌. 87 | // 当用户的令牌即将过期时,可以调用此方法生成一个新的令牌. 88 | func (b *userBiz) RefreshToken(ctx context.Context, rq *apiv1.RefreshTokenRequest) (*apiv1.RefreshTokenResponse, error) { 89 | // 如果匹配成功,说明登录成功,签发 token 并返回 90 | tokenStr, expireAt, err := token.Sign(contextx.UserID(ctx)) 91 | if err != nil { 92 | return nil, errorsx.ErrSignToken.WithMessage(err.Error()) 93 | } 94 | return &apiv1.RefreshTokenResponse{Token: tokenStr, ExpireAt: expireAt}, nil 95 | } 96 | 97 | // ChangePassword 实现 UserBiz 接口中的 ChangePassword 方法. 98 | func (b *userBiz) ChangePassword(ctx context.Context, rq *apiv1.ChangePasswordRequest) (*apiv1.ChangePasswordResponse, error) { 99 | userM, err := b.store.User().Get(ctx, where.F("userID", contextx.UserID(ctx))) 100 | if err != nil { 101 | return nil, err 102 | } 103 | 104 | if err := auth.Compare(userM.Password, rq.OldPassword); err != nil { 105 | slog.ErrorContext(ctx, "Failed to compare password", "err", err) 106 | return nil, errorsx.ErrPasswordInvalid 107 | } 108 | 109 | userM.Password, _ = auth.Encrypt(rq.NewPassword) 110 | if err := b.store.User().Update(ctx, userM); err != nil { 111 | return nil, err 112 | } 113 | 114 | return &apiv1.ChangePasswordResponse{}, nil 115 | } 116 | 117 | // Create 实现 UserBiz 接口中的 Create 方法. 118 | func (b *userBiz) Create(ctx context.Context, rq *apiv1.CreateUserRequest) (*apiv1.CreateUserResponse, error) { 119 | var userM model.User 120 | _ = copier.Copy(&userM, rq) 121 | 122 | if err := b.store.User().Create(ctx, &userM); err != nil { 123 | return nil, err 124 | } 125 | 126 | return &apiv1.CreateUserResponse{UserID: userM.UserID}, nil 127 | } 128 | 129 | // Update 实现 UserBiz 接口中的 Update 方法. 130 | func (b *userBiz) Update(ctx context.Context, rq *apiv1.UpdateUserRequest) (*apiv1.UpdateUserResponse, error) { 131 | userM, err := b.store.User().Get(ctx, where.F("userID", contextx.UserID(ctx))) 132 | if err != nil { 133 | return nil, err 134 | } 135 | 136 | if rq.Username != nil { 137 | userM.Username = *rq.Username 138 | } 139 | if rq.Email != nil { 140 | userM.Email = *rq.Email 141 | } 142 | if rq.Nickname != nil { 143 | userM.Nickname = *rq.Nickname 144 | } 145 | if rq.Phone != nil { 146 | userM.Phone = *rq.Phone 147 | } 148 | 149 | if err := b.store.User().Update(ctx, userM); err != nil { 150 | return nil, err 151 | } 152 | 153 | return &apiv1.UpdateUserResponse{}, nil 154 | } 155 | 156 | // Delete 实现 UserBiz 接口中的 Delete 方法. 157 | func (b *userBiz) Delete(ctx context.Context, rq *apiv1.DeleteUserRequest) (*apiv1.DeleteUserResponse, error) { 158 | if err := b.store.User().Delete(ctx, where.F("userID", contextx.UserID(ctx))); err != nil { 159 | return nil, err 160 | } 161 | 162 | return &apiv1.DeleteUserResponse{}, nil 163 | } 164 | 165 | // Get 实现 UserBiz 接口中的 Get 方法. 166 | func (b *userBiz) Get(ctx context.Context, rq *apiv1.GetUserRequest) (*apiv1.GetUserResponse, error) { 167 | userM, err := b.store.User().Get(ctx, where.F("userID", contextx.UserID(ctx))) 168 | if err != nil { 169 | return nil, err 170 | } 171 | 172 | return &apiv1.GetUserResponse{User: conversion.UserodelToUserV1(userM)}, nil 173 | } 174 | 175 | // List 实现 UserBiz 接口中的 List 方法. 176 | func (b *userBiz) List(ctx context.Context, rq *apiv1.ListUserRequest) (*apiv1.ListUserResponse, error) { 177 | whr := where.P(int(rq.Offset), int(rq.Limit)) 178 | count, userList, err := b.store.User().List(ctx, whr) 179 | if err != nil { 180 | return nil, err 181 | } 182 | 183 | var m sync.Map 184 | eg, ctx := errgroup.WithContext(ctx) 185 | 186 | // 设置最大并发数量为常量 MaxConcurrency 187 | eg.SetLimit(known.MaxErrGroupConcurrency) 188 | 189 | // 使用 goroutine 提高接口性能 190 | for _, user := range userList { 191 | eg.Go(func() error { 192 | select { 193 | case <-ctx.Done(): 194 | return nil 195 | default: 196 | count, _, err := b.store.Post().List(ctx, where.F("userID", contextx.UserID(ctx))) 197 | if err != nil { 198 | return err 199 | } 200 | 201 | converted := conversion.UserodelToUserV1(user) 202 | converted.PostCount = count 203 | m.Store(user.ID, converted) 204 | 205 | return nil 206 | } 207 | }) 208 | } 209 | 210 | if err := eg.Wait(); err != nil { 211 | slog.ErrorContext(ctx, "Failed to wait all function calls returned", "err", err) 212 | return nil, err 213 | } 214 | 215 | users := make([]*apiv1.User, 0, len(userList)) 216 | for _, item := range userList { 217 | user, _ := m.Load(item.ID) 218 | users = append(users, user.(*apiv1.User)) 219 | } 220 | 221 | slog.DebugContext(ctx, "Get users from backend storage", "count", len(users)) 222 | 223 | return &apiv1.ListUserResponse{TotalCount: count, Users: users}, nil 224 | } 225 | -------------------------------------------------------------------------------- /internal/apiserver/biz/v2/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onexstack/fastgo/5ff4ad326cc4a23b59b793587279a061e238f6c3/internal/apiserver/biz/v2/.keep -------------------------------------------------------------------------------- /internal/apiserver/handler/handler.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 孔令飞 . All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. The original repo for 4 | // this file is https://github.com/onexstack/fastgo. The professional 5 | // version of this repository is https://github.com/onexstack/onex. 6 | 7 | package handler 8 | 9 | import ( 10 | "github.com/onexstack/fastgo/internal/apiserver/biz" 11 | "github.com/onexstack/fastgo/internal/apiserver/pkg/validation" 12 | ) 13 | 14 | // Handler 处理博客模块的请求. 15 | type Handler struct { 16 | biz biz.IBiz 17 | val *validation.Validator 18 | } 19 | 20 | // NewHandler 创建新的 Handler 实例. 21 | func NewHandler(biz biz.IBiz, val *validation.Validator) *Handler { 22 | return &Handler{ 23 | biz: biz, 24 | val: val, 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /internal/apiserver/handler/post.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 孔令飞 . All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. The original repo for 4 | // this file is https://github.com/onexstack/fastgo. The professional 5 | // version of this repository is https://github.com/onexstack/onex. 6 | 7 | package handler 8 | 9 | import ( 10 | "log/slog" 11 | 12 | "github.com/gin-gonic/gin" 13 | 14 | "github.com/onexstack/fastgo/internal/pkg/core" 15 | "github.com/onexstack/fastgo/internal/pkg/errorsx" 16 | "github.com/onexstack/fastgo/pkg/api/apiserver/v1" 17 | ) 18 | 19 | // CreatePost 创建新博客. 20 | func (h *Handler) CreatePost(c *gin.Context) { 21 | slog.Info("Create post function called") 22 | 23 | var rq v1.CreatePostRequest 24 | if err := c.ShouldBindJSON(&rq); err != nil { 25 | core.WriteResponse(c, nil, errorsx.ErrBind) 26 | return 27 | } 28 | 29 | if err := h.val.ValidateCreatePostRequest(c.Request.Context(), &rq); err != nil { 30 | core.WriteResponse(c, nil, errorsx.ErrInvalidArgument.WithMessage(err.Error())) 31 | return 32 | } 33 | 34 | resp, err := h.biz.PostV1().Create(c.Request.Context(), &rq) 35 | if err != nil { 36 | core.WriteResponse(c, nil, err) 37 | return 38 | } 39 | 40 | core.WriteResponse(c, resp, nil) 41 | } 42 | 43 | // UpdatePost 更新博客信息. 44 | func (h *Handler) UpdatePost(c *gin.Context) { 45 | slog.Info("Update post function called") 46 | 47 | var rq v1.UpdatePostRequest 48 | if err := c.ShouldBindJSON(&rq); err != nil { 49 | core.WriteResponse(c, nil, errorsx.ErrBind) 50 | return 51 | } 52 | rq.PostID = c.Param("postID") 53 | 54 | if err := h.val.ValidateUpdatePostRequest(c.Request.Context(), &rq); err != nil { 55 | core.WriteResponse(c, nil, errorsx.ErrInvalidArgument.WithMessage(err.Error())) 56 | return 57 | } 58 | 59 | resp, err := h.biz.PostV1().Update(c.Request.Context(), &rq) 60 | if err != nil { 61 | core.WriteResponse(c, nil, err) 62 | return 63 | } 64 | 65 | core.WriteResponse(c, resp, nil) 66 | } 67 | 68 | // DeletePost 删除博客. 69 | func (h *Handler) DeletePost(c *gin.Context) { 70 | slog.Info("Delete post function called") 71 | 72 | var rq v1.DeletePostRequest 73 | if err := c.ShouldBindJSON(&rq); err != nil { 74 | core.WriteResponse(c, nil, errorsx.ErrBind) 75 | return 76 | } 77 | 78 | // 小作业:请你自行补全校验代码 79 | 80 | resp, err := h.biz.PostV1().Delete(c.Request.Context(), &rq) 81 | if err != nil { 82 | core.WriteResponse(c, nil, err) 83 | return 84 | } 85 | 86 | core.WriteResponse(c, resp, nil) 87 | } 88 | 89 | // GetPost 获取博客信息. 90 | func (h *Handler) GetPost(c *gin.Context) { 91 | slog.Info("Get post function called") 92 | 93 | var rq v1.GetPostRequest 94 | if err := c.ShouldBindUri(&rq); err != nil { 95 | core.WriteResponse(c, nil, errorsx.ErrBind) 96 | return 97 | } 98 | 99 | // 小作业:请你自行补全校验代码 100 | 101 | resp, err := h.biz.PostV1().Get(c.Request.Context(), &rq) 102 | if err != nil { 103 | core.WriteResponse(c, nil, err) 104 | return 105 | } 106 | 107 | core.WriteResponse(c, resp, nil) 108 | } 109 | 110 | // ListPost 列出博客信息. 111 | func (h *Handler) ListPost(c *gin.Context) { 112 | slog.Info("List post function called") 113 | 114 | var rq v1.ListPostRequest 115 | if err := c.ShouldBindQuery(&rq); err != nil { 116 | core.WriteResponse(c, nil, errorsx.ErrBind) 117 | return 118 | } 119 | 120 | // 小作业:请你自行补全校验代码 121 | 122 | resp, err := h.biz.PostV1().List(c.Request.Context(), &rq) 123 | if err != nil { 124 | core.WriteResponse(c, nil, err) 125 | return 126 | } 127 | 128 | core.WriteResponse(c, resp, nil) 129 | } 130 | -------------------------------------------------------------------------------- /internal/apiserver/handler/user.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 孔令飞 . All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. The original repo for 4 | // this file is https://github.com/onexstack/fastgo. The professional 5 | // version of this repository is https://github.com/onexstack/onex. 6 | 7 | package handler 8 | 9 | import ( 10 | "log/slog" 11 | 12 | "github.com/gin-gonic/gin" 13 | 14 | "github.com/onexstack/fastgo/internal/pkg/core" 15 | "github.com/onexstack/fastgo/internal/pkg/errorsx" 16 | "github.com/onexstack/fastgo/pkg/api/apiserver/v1" 17 | ) 18 | 19 | // Login 用户登录并返回 JWT Token. 20 | func (h *Handler) Login(c *gin.Context) { 21 | slog.Info("Login function called") 22 | 23 | var rq v1.LoginRequest 24 | if err := c.ShouldBindJSON(&rq); err != nil { 25 | core.WriteResponse(c, nil, errorsx.ErrBind) 26 | return 27 | } 28 | 29 | // 小作业:请你自行补全校验代码 30 | 31 | resp, err := h.biz.UserV1().Login(c.Request.Context(), &rq) 32 | if err != nil { 33 | core.WriteResponse(c, nil, err) 34 | return 35 | } 36 | 37 | core.WriteResponse(c, resp, nil) 38 | } 39 | 40 | // RefreshToken 刷新 JWT Token. 41 | func (h *Handler) RefreshToken(c *gin.Context) { 42 | slog.Info("Refresh token function called") 43 | 44 | var rq v1.RefreshTokenRequest 45 | if err := c.ShouldBindJSON(&rq); err != nil { 46 | core.WriteResponse(c, nil, errorsx.ErrBind) 47 | return 48 | } 49 | 50 | // 小作业:请你自行补全校验代码 51 | 52 | resp, err := h.biz.UserV1().RefreshToken(c.Request.Context(), &rq) 53 | if err != nil { 54 | core.WriteResponse(c, nil, err) 55 | return 56 | } 57 | 58 | core.WriteResponse(c, resp, nil) 59 | } 60 | 61 | // ChangeUserPassword 修改用户密码. 62 | func (h *Handler) ChangePassword(c *gin.Context) { 63 | slog.Info("Change password function called") 64 | 65 | var rq v1.ChangePasswordRequest 66 | if err := c.ShouldBindJSON(&rq); err != nil { 67 | core.WriteResponse(c, nil, errorsx.ErrBind) 68 | return 69 | } 70 | 71 | // 小作业:请你自行补全校验代码 72 | 73 | resp, err := h.biz.UserV1().ChangePassword(c.Request.Context(), &rq) 74 | if err != nil { 75 | core.WriteResponse(c, nil, err) 76 | return 77 | } 78 | 79 | core.WriteResponse(c, resp, nil) 80 | } 81 | 82 | // CreateUser 创建新用户. 83 | func (h *Handler) CreateUser(c *gin.Context) { 84 | slog.Info("Create user function called") 85 | 86 | var rq v1.CreateUserRequest 87 | if err := c.ShouldBindJSON(&rq); err != nil { 88 | core.WriteResponse(c, nil, errorsx.ErrBind) 89 | return 90 | } 91 | 92 | if err := h.val.ValidateCreateUserRequest(c.Request.Context(), &rq); err != nil { 93 | core.WriteResponse(c, nil, errorsx.ErrInvalidArgument.WithMessage(err.Error())) 94 | return 95 | } 96 | 97 | resp, err := h.biz.UserV1().Create(c.Request.Context(), &rq) 98 | if err != nil { 99 | core.WriteResponse(c, nil, err) 100 | return 101 | } 102 | 103 | core.WriteResponse(c, resp, nil) 104 | } 105 | 106 | // UpdateUser 更新用户信息. 107 | func (h *Handler) UpdateUser(c *gin.Context) { 108 | slog.Info("Update user function called") 109 | 110 | var rq v1.UpdateUserRequest 111 | if err := c.ShouldBindJSON(&rq); err != nil { 112 | core.WriteResponse(c, nil, errorsx.ErrBind) 113 | return 114 | } 115 | 116 | if err := h.val.ValidateUpdateUserRequest(c.Request.Context(), &rq); err != nil { 117 | core.WriteResponse(c, nil, errorsx.ErrInvalidArgument.WithMessage(err.Error())) 118 | return 119 | } 120 | 121 | resp, err := h.biz.UserV1().Update(c.Request.Context(), &rq) 122 | if err != nil { 123 | core.WriteResponse(c, nil, err) 124 | return 125 | } 126 | 127 | core.WriteResponse(c, resp, nil) 128 | } 129 | 130 | // DeleteUser 删除用户. 131 | func (h *Handler) DeleteUser(c *gin.Context) { 132 | slog.Info("Delete user function called") 133 | 134 | var rq v1.DeleteUserRequest 135 | if err := c.ShouldBindUri(&rq); err != nil { 136 | core.WriteResponse(c, nil, errorsx.ErrBind) 137 | return 138 | } 139 | 140 | // 小作业:请你自行补全校验代码 141 | 142 | resp, err := h.biz.UserV1().Delete(c.Request.Context(), &rq) 143 | if err != nil { 144 | core.WriteResponse(c, nil, err) 145 | return 146 | } 147 | 148 | core.WriteResponse(c, resp, nil) 149 | } 150 | 151 | // GetUser 获取用户信息. 152 | func (h *Handler) GetUser(c *gin.Context) { 153 | slog.Info("Get user function called") 154 | 155 | var rq v1.GetUserRequest 156 | if err := c.ShouldBindUri(&rq); err != nil { 157 | core.WriteResponse(c, nil, errorsx.ErrBind) 158 | return 159 | } 160 | 161 | // 小作业:请你自行补全校验代码 162 | 163 | resp, err := h.biz.UserV1().Get(c.Request.Context(), &rq) 164 | if err != nil { 165 | core.WriteResponse(c, nil, err) 166 | return 167 | } 168 | 169 | core.WriteResponse(c, resp, nil) 170 | } 171 | 172 | // ListUser 列出用户信息. 173 | func (h *Handler) ListUser(c *gin.Context) { 174 | slog.Info("List user function called") 175 | 176 | var rq v1.ListUserRequest 177 | if err := c.ShouldBindQuery(&rq); err != nil { 178 | core.WriteResponse(c, nil, errorsx.ErrBind) 179 | return 180 | } 181 | 182 | // 小作业:请你自行补全校验代码 183 | 184 | resp, err := h.biz.UserV1().List(c.Request.Context(), &rq) 185 | if err != nil { 186 | core.WriteResponse(c, nil, err) 187 | return 188 | } 189 | 190 | core.WriteResponse(c, resp, nil) 191 | } 192 | -------------------------------------------------------------------------------- /internal/apiserver/model/hook.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 孔令飞 . All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. The original repo for 4 | // this file is https://github.com/onexstack/fastgo. The professional 5 | // version of this repository is https://github.com/onexstack/onex. 6 | 7 | package model 8 | 9 | import ( 10 | "gorm.io/gorm" 11 | 12 | "github.com/onexstack/fastgo/internal/pkg/rid" 13 | "github.com/onexstack/fastgo/pkg/auth" 14 | ) 15 | 16 | // AfterCreate 在创建数据库记录之后生成 postID. 17 | func (m *Post) AfterCreate(tx *gorm.DB) error { 18 | m.PostID = rid.PostID.New(uint64(m.ID)) 19 | 20 | return tx.Save(m).Error 21 | } 22 | 23 | // BeforeCreate 在创建数据库记录之前加密明文密码. 24 | func (m *User) BeforeCreate(tx *gorm.DB) error { 25 | // Encrypt the user password. 26 | var err error 27 | m.Password, err = auth.Encrypt(m.Password) 28 | if err != nil { 29 | return err 30 | } 31 | 32 | return nil 33 | } 34 | 35 | // AfterCreate 在创建数据库记录之后生成 userID. 36 | func (m *User) AfterCreate(tx *gorm.DB) error { 37 | m.UserID = rid.UserID.New(uint64(m.ID)) 38 | 39 | return tx.Save(m).Error 40 | } 41 | -------------------------------------------------------------------------------- /internal/apiserver/model/post.gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by gorm.io/gen. DO NOT EDIT. 2 | // Code generated by gorm.io/gen. DO NOT EDIT. 3 | // Code generated by gorm.io/gen. DO NOT EDIT. 4 | 5 | package model 6 | 7 | import ( 8 | "time" 9 | ) 10 | 11 | const TableNamePost = "post" 12 | 13 | // Post 博文表 14 | type Post struct { 15 | ID int64 `gorm:"column:id;primaryKey;autoIncrement:true" json:"id"` 16 | UserID string `gorm:"column:userID;not null;comment:用户唯一 ID" json:"userID"` // 用户唯一 ID 17 | PostID string `gorm:"column:postID;not null;comment:博文唯一 ID" json:"postID"` // 博文唯一 ID 18 | Title string `gorm:"column:title;not null;comment:博文标题" json:"title"` // 博文标题 19 | Content string `gorm:"column:content;not null;comment:博文内容" json:"content"` // 博文内容 20 | CreatedAt time.Time `gorm:"column:createdAt;not null;default:current_timestamp();comment:博文创建时间" json:"createdAt"` // 博文创建时间 21 | UpdatedAt time.Time `gorm:"column:updatedAt;not null;default:current_timestamp();comment:博文最后修改时间" json:"updatedAt"` // 博文最后修改时间 22 | } 23 | 24 | // TableName Post's table name 25 | func (*Post) TableName() string { 26 | return TableNamePost 27 | } 28 | -------------------------------------------------------------------------------- /internal/apiserver/model/user.gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by gorm.io/gen. DO NOT EDIT. 2 | // Code generated by gorm.io/gen. DO NOT EDIT. 3 | // Code generated by gorm.io/gen. DO NOT EDIT. 4 | 5 | package model 6 | 7 | import ( 8 | "time" 9 | ) 10 | 11 | const TableNameUser = "user" 12 | 13 | // User 用户表 14 | type User struct { 15 | ID int64 `gorm:"column:id;primaryKey;autoIncrement:true" json:"id"` 16 | UserID string `gorm:"column:userID;not null;comment:用户唯一 ID" json:"userID"` // 用户唯一 ID 17 | Username string `gorm:"column:username;not null;comment:用户名(唯一)" json:"username"` // 用户名(唯一) 18 | Password string `gorm:"column:password;not null;comment:用户密码(加密后)" json:"password"` // 用户密码(加密后) 19 | Nickname string `gorm:"column:nickname;not null;comment:用户昵称" json:"nickname"` // 用户昵称 20 | Email string `gorm:"column:email;not null;comment:用户电子邮箱地址" json:"email"` // 用户电子邮箱地址 21 | Phone string `gorm:"column:phone;not null;comment:用户手机号" json:"phone"` // 用户手机号 22 | CreatedAt time.Time `gorm:"column:createdAt;not null;default:current_timestamp();comment:用户创建时间" json:"createdAt"` // 用户创建时间 23 | UpdatedAt time.Time `gorm:"column:updatedAt;not null;default:current_timestamp();comment:用户最后修改时间" json:"updatedAt"` // 用户最后修改时间 24 | } 25 | 26 | // TableName User's table name 27 | func (*User) TableName() string { 28 | return TableNameUser 29 | } 30 | -------------------------------------------------------------------------------- /internal/apiserver/pkg/conversion/post.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 孔令飞 . All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. The original repo for 4 | // this file is https://github.com/onexstack/fastgo. The professional 5 | // version of this repository is https://github.com/onexstack/onex. 6 | 7 | package conversion 8 | 9 | import ( 10 | "github.com/jinzhu/copier" 11 | 12 | "github.com/onexstack/fastgo/internal/apiserver/model" 13 | apiv1 "github.com/onexstack/fastgo/pkg/api/apiserver/v1" 14 | ) 15 | 16 | // PostodelToPostV1 将模型层的 Post(博客模型对象)转换为 Protobuf 层的 Post(v1 博客对象). 17 | func PostodelToPostV1(postModel *model.Post) *apiv1.Post { 18 | var protoPost apiv1.Post 19 | _ = copier.Copy(&protoPost, postModel) 20 | return &protoPost 21 | } 22 | 23 | // PostV1ToPostodel 将 Protobuf 层的 Post(v1 博客对象)转换为模型层的 Post(博客模型对象). 24 | func PostV1ToPostodel(protoPost *apiv1.Post) *model.Post { 25 | var postModel model.Post 26 | _ = copier.Copy(&postModel, protoPost) 27 | return &postModel 28 | } 29 | -------------------------------------------------------------------------------- /internal/apiserver/pkg/conversion/user.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 孔令飞 . All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. The original repo for 4 | // this file is https://github.com/onexstack/fastgo. The professional 5 | // version of this repository is https://github.com/onexstack/onex. 6 | 7 | package conversion 8 | 9 | import ( 10 | "github.com/jinzhu/copier" 11 | 12 | "github.com/onexstack/fastgo/internal/apiserver/model" 13 | apiv1 "github.com/onexstack/fastgo/pkg/api/apiserver/v1" 14 | ) 15 | 16 | // UserodelToUserV1 将模型层的 User(用户模型对象)转换为 Protobuf 层的 User(v1 用户对象). 17 | func UserodelToUserV1(userModel *model.User) *apiv1.User { 18 | var protoUser apiv1.User 19 | _ = copier.Copy(&protoUser, userModel) 20 | return &protoUser 21 | } 22 | 23 | // UserV1ToUserodel 将 Protobuf 层的 User(v1 用户对象)转换为模型层的 User(用户模型对象). 24 | func UserV1ToUserodel(protoUser *apiv1.User) *model.User { 25 | var userModel model.User 26 | _ = copier.Copy(&userModel, protoUser) 27 | return &userModel 28 | } 29 | -------------------------------------------------------------------------------- /internal/apiserver/pkg/validation/post.go: -------------------------------------------------------------------------------- 1 | package validation 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/onexstack/fastgo/pkg/api/apiserver/v1" 7 | ) 8 | 9 | func (v *Validator) ValidateCreatePostRequest(ctx context.Context, rq *v1.CreatePostRequest) error { 10 | return nil 11 | } 12 | 13 | func (v *Validator) ValidateUpdatePostRequest(ctx context.Context, rq *v1.UpdatePostRequest) error { 14 | return nil 15 | } 16 | -------------------------------------------------------------------------------- /internal/apiserver/pkg/validation/user.go: -------------------------------------------------------------------------------- 1 | package validation 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/onexstack/fastgo/pkg/api/apiserver/v1" 8 | ) 9 | 10 | func (v *Validator) ValidateCreateUserRequest(ctx context.Context, rq *v1.CreateUserRequest) error { 11 | // Validate username 12 | if rq.Username == "" { 13 | return errors.New("Username cannot be empty") 14 | } 15 | if len(rq.Username) < 4 || len(rq.Username) > 32 { 16 | return errors.New("Username must be between 4 and 32 characters") 17 | } 18 | 19 | // Validate password 20 | if rq.Password == "" { 21 | return errors.New("Password cannot be empty") 22 | } 23 | if len(rq.Password) < 8 || len(rq.Password) > 64 { 24 | return errors.New("Password must be between 8 and 64 characters") 25 | } 26 | 27 | // Validate nickname (if provided) 28 | if rq.Nickname != nil && *rq.Nickname != "" { 29 | if len(*rq.Nickname) > 32 { 30 | return errors.New("Nickname cannot exceed 32 characters") 31 | } 32 | } 33 | 34 | // Validate email 35 | if rq.Email == "" { 36 | return errors.New("Email cannot be empty") 37 | } 38 | 39 | // Validate phone number 40 | if rq.Phone == "" { 41 | return errors.New("Phone number cannot be empty") 42 | } 43 | 44 | return nil 45 | } 46 | 47 | func (v *Validator) ValidateUpdateUserRequest(ctx context.Context, rq *v1.UpdateUserRequest) error { 48 | return nil 49 | } 50 | -------------------------------------------------------------------------------- /internal/apiserver/pkg/validation/validation.go: -------------------------------------------------------------------------------- 1 | package validation 2 | 3 | import ( 4 | "github.com/onexstack/fastgo/internal/apiserver/store" 5 | ) 6 | 7 | // Validator 是验证逻辑的实现结构体. 8 | type Validator struct { 9 | // 有些复杂的验证逻辑,可能需要直接查询数据库 10 | // 这里只是一个举例,如果验证时,有其他依赖的客户端/服务/资源等, 11 | // 都可以一并注入进来 12 | store store.IStore 13 | } 14 | 15 | // NewValidator 创建一个新的 Validator 实例. 16 | func NewValidator(store store.IStore) *Validator { 17 | return &Validator{store: store} 18 | } 19 | -------------------------------------------------------------------------------- /internal/apiserver/server.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 孔令飞 . All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. The original repo for 4 | // this file is https://github.com/onexstack/fastgo. The professional 5 | // version of this repository is https://github.com/onexstack/onex. 6 | 7 | package apiserver 8 | 9 | import ( 10 | "context" 11 | "errors" 12 | "log/slog" 13 | "net/http" 14 | "os" 15 | "os/signal" 16 | "syscall" 17 | "time" 18 | 19 | "github.com/gin-gonic/gin" 20 | 21 | "github.com/onexstack/fastgo/internal/apiserver/biz" 22 | "github.com/onexstack/fastgo/internal/apiserver/handler" 23 | "github.com/onexstack/fastgo/internal/apiserver/pkg/validation" 24 | "github.com/onexstack/fastgo/internal/apiserver/store" 25 | "github.com/onexstack/fastgo/internal/pkg/core" 26 | "github.com/onexstack/fastgo/internal/pkg/errorsx" 27 | "github.com/onexstack/fastgo/internal/pkg/known" 28 | mw "github.com/onexstack/fastgo/internal/pkg/middleware" 29 | genericoptions "github.com/onexstack/fastgo/pkg/options" 30 | "github.com/onexstack/fastgo/pkg/token" 31 | ) 32 | 33 | // Config 配置结构体,用于存储应用相关的配置. 34 | // 不用 viper.Get,是因为这种方式能更加清晰的知道应用提供了哪些配置项. 35 | type Config struct { 36 | MySQLOptions *genericoptions.MySQLOptions 37 | Addr string 38 | JWTKey string 39 | Expiration time.Duration 40 | } 41 | 42 | // Server 定义一个服务器结构体类型. 43 | type Server struct { 44 | cfg *Config 45 | srv *http.Server 46 | } 47 | 48 | // NewServer 根据配置创建服务器. 49 | func (cfg *Config) NewServer() (*Server, error) { 50 | // 初始化 token 包的签名密钥、认证 Key 及 Token 默认过期时间 51 | token.Init(cfg.JWTKey, known.XUserID, cfg.Expiration) 52 | 53 | // 创建 Gin 引擎 54 | engine := gin.New() 55 | 56 | // gin.Recovery() 中间件,用来捕获任何 panic,并恢复 57 | mws := []gin.HandlerFunc{gin.Recovery(), mw.NoCache, mw.Cors, mw.RequestID()} 58 | engine.Use(mws...) 59 | 60 | // 初始化数据库连接 61 | db, err := cfg.MySQLOptions.NewDB() 62 | if err != nil { 63 | return nil, err 64 | } 65 | store := store.NewStore(db) 66 | 67 | cfg.InstallRESTAPI(engine, store) 68 | 69 | // 创建 HTTP Server 实例 70 | httpsrv := &http.Server{Addr: cfg.Addr, Handler: engine} 71 | 72 | return &Server{cfg: cfg, srv: httpsrv}, nil 73 | } 74 | 75 | // 注册 API 路由。路由的路径和 HTTP 方法,严格遵循 REST 规范. 76 | func (cfg *Config) InstallRESTAPI(engine *gin.Engine, store store.IStore) { 77 | // 注册 404 Handler. 78 | engine.NoRoute(func(c *gin.Context) { 79 | core.WriteResponse(c, nil, errorsx.ErrNotFound.WithMessage("Page not found")) 80 | }) 81 | 82 | // 注册 /healthz handler. 83 | engine.GET("/healthz", func(c *gin.Context) { 84 | core.WriteResponse(c, map[string]string{"status": "ok"}, nil) 85 | }) 86 | 87 | // 创建核心业务处理器 88 | handler := handler.NewHandler(biz.NewBiz(store), validation.NewValidator(store)) 89 | 90 | // 注册用户登录和令牌刷新接口。这2个接口比较简单,所以没有 API 版本 91 | engine.POST("/login", handler.Login) 92 | // 注意:认证中间件要在 handler.RefreshToken 之前加载 93 | engine.PUT("/refresh-token", mw.Authn(), handler.RefreshToken) 94 | 95 | authMiddlewares := []gin.HandlerFunc{mw.Authn()} 96 | 97 | // 注册 v1 版本 API 路由分组 98 | v1 := engine.Group("/v1") 99 | { 100 | // 用户相关路由 101 | userv1 := v1.Group("/users") 102 | { 103 | // 创建用户。这里要注意:创建用户是不用进行认证和授权的 104 | userv1.POST("", handler.CreateUser) 105 | userv1.Use(authMiddlewares...) 106 | userv1.PUT(":userID/change-password", handler.ChangePassword) // 修改用户密码 107 | userv1.PUT(":userID", handler.UpdateUser) // 更新用户信息 108 | userv1.DELETE(":userID", handler.DeleteUser) // 删除用户 109 | userv1.GET(":userID", handler.GetUser) // 查询用户详情 110 | userv1.GET("", handler.ListUser) // 查询用户列表. 111 | } 112 | 113 | // 博客相关路由 114 | postv1 := v1.Group("/posts", authMiddlewares...) 115 | { 116 | postv1.POST("", handler.CreatePost) // 创建博客 117 | postv1.PUT(":postID", handler.UpdatePost) // 更新博客 118 | postv1.DELETE("", handler.DeletePost) // 删除博客 119 | postv1.GET(":postID", handler.GetPost) // 查询博客详情 120 | postv1.GET("", handler.ListPost) // 查询博客列表 121 | } 122 | } 123 | } 124 | 125 | // Run 运行应用. 126 | func (s *Server) Run() error { 127 | // 运行 HTTP 服务器 128 | // 打印一条日志,用来提示 HTTP 服务已经起来,方便排障 129 | slog.Info("Start to listening the incoming requests on http address", "addr", s.cfg.Addr) 130 | go func() { 131 | if err := s.srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { 132 | slog.Error(err.Error()) 133 | os.Exit(1) 134 | } 135 | }() 136 | 137 | // 创建一个 os.Signal 类型的 channel,用于接收系统信号 138 | quit := make(chan os.Signal, 1) 139 | // 当执行 kill 命令时(不带参数),默认会发送 syscall.SIGTERM 信号 140 | // 使用 kill -2 命令会发送 syscall.SIGINT 信号(例如按 CTRL+C 触发) 141 | // 使用 kill -9 命令会发送 syscall.SIGKILL 信号,但 SIGKILL 信号无法被捕获,因此无需监听和处理 142 | signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) 143 | // 阻塞程序,等待从 quit channel 中接收到信号 144 | <-quit 145 | 146 | slog.Info("Shutting down server ...") 147 | 148 | // 优雅关闭服务 149 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 150 | defer cancel() 151 | 152 | // 先关闭依赖的服务,再关闭被依赖的服务 153 | // 10 秒内优雅关闭服务(将未处理完的请求处理完再关闭服务),超过 10 秒就超时退出 154 | if err := s.srv.Shutdown(ctx); err != nil { 155 | slog.Error("Insecure Server forced to shutdown", "err", err) 156 | return err 157 | } 158 | 159 | slog.Info("Server exited") 160 | 161 | return nil 162 | } 163 | -------------------------------------------------------------------------------- /internal/apiserver/store/README.md: -------------------------------------------------------------------------------- 1 | # Store 层 2 | 3 | 因为 Store 代码相对不易变,所以,Store 层很少会随着项目迭代,衍生出V2版本,所以Store层只需要一个版本即可。 4 | -------------------------------------------------------------------------------- /internal/apiserver/store/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 孔令飞 . All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. The original repo for 4 | // this file is https://github.com/onexstack/fastgo. The professional 5 | // version of this repository is https://github.com/onexstack/onex. 6 | 7 | // Package store defines the storage interface for fastgo. 8 | package store // import "github.com/onexstack/fastgo/internal/fastgo/store" 9 | -------------------------------------------------------------------------------- /internal/apiserver/store/logger.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 孔令飞 . All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. The original repo for 4 | // this file is https://github.com/onexstack/fastgo. The professional 5 | // version of this repository is https://github.com/onexstack/onex. 6 | 7 | package store 8 | 9 | import ( 10 | "log/slog" 11 | ) 12 | 13 | // Logger is a logger that implements the Logger interface. 14 | // It uses the log package to log error messages with additional context. 15 | type Logger struct{} 16 | 17 | // NewLogger creates and returns a new instance of Logger. 18 | func NewLogger() *Logger { 19 | return &Logger{} 20 | } 21 | 22 | // Error logs an error message with the provided context using the log package. 23 | func (l *Logger) Error(err error, msg string, kvs ...any) { 24 | slog.Error(msg, append(kvs, "err", err)...) 25 | } 26 | -------------------------------------------------------------------------------- /internal/apiserver/store/post.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 孔令飞 . All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. The original repo for 4 | // this file is https://github.com/onexstack/fastgo. The professional 5 | // version of this repository is https://github.com/onexstack/onex. 6 | 7 | // nolint: dupl 8 | package store 9 | 10 | import ( 11 | "context" 12 | "errors" 13 | "log/slog" 14 | 15 | "github.com/onexstack/onexstack/pkg/store/where" 16 | "gorm.io/gorm" 17 | 18 | "github.com/onexstack/fastgo/internal/apiserver/model" 19 | "github.com/onexstack/fastgo/internal/pkg/errorsx" 20 | ) 21 | 22 | // PostStore 定义了 post 模块在 store 层所实现的方法. 23 | type PostStore interface { 24 | Create(ctx context.Context, obj *model.Post) error 25 | Update(ctx context.Context, obj *model.Post) error 26 | Delete(ctx context.Context, opts *where.Options) error 27 | Get(ctx context.Context, opts *where.Options) (*model.Post, error) 28 | List(ctx context.Context, opts *where.Options) (int64, []*model.Post, error) 29 | 30 | PostExpansion 31 | } 32 | 33 | // PostExpansion 定义了帖子操作的附加方法. 34 | type PostExpansion interface{} 35 | 36 | // postStore 是 PostStore 接口的实现. 37 | type postStore struct { 38 | store *datastore 39 | } 40 | 41 | // 确保 postStore 实现了 PostStore 接口. 42 | var _ PostStore = (*postStore)(nil) 43 | 44 | // newPostStore 创建 postStore 的实例. 45 | func newPostStore(store *datastore) *postStore { 46 | return &postStore{store} 47 | } 48 | 49 | // Create 插入一条帖子记录. 50 | func (s *postStore) Create(ctx context.Context, obj *model.Post) error { 51 | if err := s.store.DB(ctx).Create(&obj).Error; err != nil { 52 | slog.Error("Failed to insert post into database", "err", err, "post", obj) 53 | return errorsx.ErrDBWrite.WithMessage(err.Error()) 54 | } 55 | 56 | return nil 57 | } 58 | 59 | // Update 更新帖子数据库记录. 60 | func (s *postStore) Update(ctx context.Context, obj *model.Post) error { 61 | if err := s.store.DB(ctx).Save(obj).Error; err != nil { 62 | slog.Error("Failed to update post in database", "err", err, "post", obj) 63 | return errorsx.ErrDBWrite.WithMessage(err.Error()) 64 | } 65 | 66 | return nil 67 | } 68 | 69 | // Delete 根据条件删除帖子记录. 70 | func (s *postStore) Delete(ctx context.Context, opts *where.Options) error { 71 | err := s.store.DB(ctx, opts).Delete(new(model.Post)).Error 72 | if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { 73 | slog.Error("Failed to delete post from database", "err", err, "conditions", opts) 74 | return errorsx.ErrDBWrite.WithMessage(err.Error()) 75 | } 76 | 77 | return nil 78 | } 79 | 80 | // Get 根据条件查询帖子记录. 81 | func (s *postStore) Get(ctx context.Context, opts *where.Options) (*model.Post, error) { 82 | var obj model.Post 83 | if err := s.store.DB(ctx, opts).First(&obj).Error; err != nil { 84 | slog.Error("Failed to retrieve post from database", "err", err, "conditions", opts) 85 | if errors.Is(err, gorm.ErrRecordNotFound) { 86 | return nil, errorsx.ErrPostNotFound 87 | } 88 | return nil, errorsx.ErrDBRead.WithMessage(err.Error()) 89 | } 90 | 91 | return &obj, nil 92 | } 93 | 94 | // List 返回帖子列表和总数. 95 | // nolint: nonamedreturns 96 | func (s *postStore) List(ctx context.Context, opts *where.Options) (count int64, ret []*model.Post, err error) { 97 | err = s.store.DB(ctx, opts).Order("id desc").Find(&ret).Offset(-1).Limit(-1).Count(&count).Error 98 | if err != nil { 99 | slog.Error("Failed to list posts from database", "err", err, "conditions", opts) 100 | err = errorsx.ErrDBRead.WithMessage(err.Error()) 101 | } 102 | return 103 | } 104 | -------------------------------------------------------------------------------- /internal/apiserver/store/store.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 孔令飞 . All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. The original repo for 4 | // this file is https://github.com/onexstack/fastgo. The professional 5 | // version of this repository is https://github.com/onexstack/onex. 6 | 7 | package store 8 | 9 | //go:generate mockgen -destination mock_store.go -package store github.com/onexstack/fastgo/internal/fastgo/store IStore,UserStore,PostStore,ConcretePostStore 10 | 11 | import ( 12 | "context" 13 | "sync" 14 | 15 | "github.com/onexstack/onexstack/pkg/store/where" 16 | "gorm.io/gorm" 17 | ) 18 | 19 | var ( 20 | once sync.Once 21 | // 全局变量,方便其它包直接调用已初始化好的 datastore 实例. 22 | S *datastore 23 | ) 24 | 25 | // IStore 定义了 Store 层需要实现的方法. 26 | type IStore interface { 27 | // 返回 Store 层的 *gorm.DB 实例,在少数场景下会被用到. 28 | DB(ctx context.Context, wheres ...where.Where) *gorm.DB 29 | TX(ctx context.Context, fn func(ctx context.Context) error) error 30 | 31 | User() UserStore 32 | Post() PostStore 33 | } 34 | 35 | // transactionKey 用于在 context.Context 中存储事务上下文的键. 36 | type transactionKey struct{} 37 | 38 | // datastore 是 IStore 的具体实现. 39 | type datastore struct { 40 | core *gorm.DB 41 | 42 | // 可以根据需要添加其他数据库实例 43 | // fake *gorm.DB 44 | } 45 | 46 | // 确保 datastore 实现了 IStore 接口. 47 | var _ IStore = (*datastore)(nil) 48 | 49 | // NewStore 创建一个 IStore 类型的实例. 50 | func NewStore(db *gorm.DB) *datastore { 51 | // 确保 S 只被初始化一次 52 | once.Do(func() { 53 | S = &datastore{db} 54 | }) 55 | 56 | return S 57 | } 58 | 59 | // DB 根据传入的条件(wheres)对数据库实例进行筛选. 60 | // 如果未传入任何条件,则返回上下文中的数据库实例(事务实例或核心数据库实例). 61 | func (store *datastore) DB(ctx context.Context, wheres ...where.Where) *gorm.DB { 62 | db := store.core 63 | // 从上下文中提取事务实例 64 | if tx, ok := ctx.Value(transactionKey{}).(*gorm.DB); ok { 65 | db = tx 66 | } 67 | 68 | // 遍历所有传入的条件并逐一叠加到数据库查询对象上 69 | for _, whr := range wheres { 70 | db = whr.Where(db) 71 | } 72 | return db 73 | } 74 | 75 | // TX 返回一个新的事务实例. 76 | // nolint: fatcontext 77 | func (store *datastore) TX(ctx context.Context, fn func(ctx context.Context) error) error { 78 | return store.core.WithContext(ctx).Transaction( 79 | func(tx *gorm.DB) error { 80 | ctx = context.WithValue(ctx, transactionKey{}, tx) 81 | return fn(ctx) 82 | }, 83 | ) 84 | } 85 | 86 | // Users 返回一个实现了 UserStore 接口的实例. 87 | func (store *datastore) User() UserStore { 88 | return newUserStore(store) 89 | } 90 | 91 | // Posts 返回一个实现了 PostStore 接口的实例. 92 | func (store *datastore) Post() PostStore { 93 | return newPostStore(store) 94 | } 95 | -------------------------------------------------------------------------------- /internal/apiserver/store/user.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 孔令飞 . All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. The original repo for 4 | // this file is https://github.com/onexstack/fastgo. The professional 5 | // version of this repository is https://github.com/onexstack/onex. 6 | 7 | // nolint: dupl 8 | package store 9 | 10 | import ( 11 | "context" 12 | "errors" 13 | "log/slog" 14 | 15 | "github.com/onexstack/onexstack/pkg/store/where" 16 | "gorm.io/gorm" 17 | 18 | "github.com/onexstack/fastgo/internal/apiserver/model" 19 | "github.com/onexstack/fastgo/internal/pkg/errorsx" 20 | ) 21 | 22 | // UserStore 定义了 user 模块在 store 层所实现的方法. 23 | type UserStore interface { 24 | Create(ctx context.Context, obj *model.User) error 25 | Update(ctx context.Context, obj *model.User) error 26 | Delete(ctx context.Context, opts *where.Options) error 27 | Get(ctx context.Context, opts *where.Options) (*model.User, error) 28 | List(ctx context.Context, opts *where.Options) (int64, []*model.User, error) 29 | 30 | UserExpansion 31 | } 32 | 33 | // UserExpansion 定义了用户操作的附加方法. 34 | type UserExpansion interface{} 35 | 36 | // userStore 是 UserStore 接口的实现. 37 | type userStore struct { 38 | store *datastore 39 | } 40 | 41 | // 确保 userStore 实现了 UserStore 接口. 42 | var _ UserStore = (*userStore)(nil) 43 | 44 | // newUserStore 创建 userStore 的实例. 45 | func newUserStore(store *datastore) *userStore { 46 | return &userStore{store} 47 | } 48 | 49 | // Create 插入一条用户记录. 50 | func (s *userStore) Create(ctx context.Context, obj *model.User) error { 51 | if err := s.store.DB(ctx).Create(&obj).Error; err != nil { 52 | slog.Error("Failed to insert user into database", "err", err, "user", obj) 53 | return errorsx.ErrDBWrite.WithMessage(err.Error()) 54 | } 55 | 56 | return nil 57 | } 58 | 59 | // Update 更新用户数据库记录. 60 | func (s *userStore) Update(ctx context.Context, obj *model.User) error { 61 | if err := s.store.DB(ctx).Save(obj).Error; err != nil { 62 | slog.Error("Failed to update user in database", "err", err, "user", obj) 63 | return errorsx.ErrDBWrite.WithMessage(err.Error()) 64 | } 65 | 66 | return nil 67 | } 68 | 69 | // Delete 根据条件删除用户记录. 70 | func (s *userStore) Delete(ctx context.Context, opts *where.Options) error { 71 | err := s.store.DB(ctx, opts).Delete(new(model.User)).Error 72 | if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { 73 | slog.Error("Failed to delete user from database", "err", err, "conditions", opts) 74 | return errorsx.ErrDBWrite.WithMessage(err.Error()) 75 | } 76 | 77 | return nil 78 | } 79 | 80 | // Get 根据条件查询用户记录. 81 | func (s *userStore) Get(ctx context.Context, opts *where.Options) (*model.User, error) { 82 | var obj model.User 83 | if err := s.store.DB(ctx, opts).First(&obj).Error; err != nil { 84 | slog.Error("Failed to retrieve user from database", "err", err, "conditions", opts) 85 | if errors.Is(err, gorm.ErrRecordNotFound) { 86 | return nil, errorsx.ErrUserNotFound 87 | } 88 | return nil, errorsx.ErrDBRead.WithMessage(err.Error()) 89 | } 90 | 91 | return &obj, nil 92 | } 93 | 94 | // List 返回用户列表和总数. 95 | // nolint: nonamedreturns 96 | func (s *userStore) List(ctx context.Context, opts *where.Options) (count int64, ret []*model.User, err error) { 97 | err = s.store.DB(ctx, opts).Order("id desc").Find(&ret).Offset(-1).Limit(-1).Count(&count).Error 98 | if err != nil { 99 | slog.Error("Failed to list users from database", "err", err, "conditions", opts) 100 | err = errorsx.ErrDBRead.WithMessage(err.Error()) 101 | } 102 | return 103 | } 104 | -------------------------------------------------------------------------------- /internal/pkg/contextx/contextx.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 孔令飞 . All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. The original repo for 4 | // this file is https://github.com/onexstack/fastgo. The professional 5 | // version of this repository is https://github.com/onexstack/onex. 6 | 7 | package contextx 8 | 9 | import ( 10 | "context" 11 | ) 12 | 13 | // 定义用于上下文的键. 14 | type ( 15 | // requestIDKey 定义请求 ID 的上下文键. 16 | requestIDKey struct{} 17 | // userIDKey 定义用户 ID 的上下文键. 18 | userIDKey struct{} 19 | // usernameKey 定义用户名的上下文键. 20 | usernameKey struct{} 21 | ) 22 | 23 | // WithRequestID 将请求 ID 存放到上下文中. 24 | func WithRequestID(ctx context.Context, requestID string) context.Context { 25 | return context.WithValue(ctx, requestIDKey{}, requestID) 26 | } 27 | 28 | // RequestID 从上下文中提取请求 ID. 29 | func RequestID(ctx context.Context) string { 30 | requestID, _ := ctx.Value(requestIDKey{}).(string) 31 | return requestID 32 | } 33 | 34 | // WithUserID 将用户 ID 存放到上下文中. 35 | func WithUserID(ctx context.Context, userID string) context.Context { 36 | return context.WithValue(ctx, userIDKey{}, userID) 37 | } 38 | 39 | // UserID 从上下文中提取用户 ID. 40 | func UserID(ctx context.Context) string { 41 | userID, _ := ctx.Value(userIDKey{}).(string) 42 | return userID 43 | } 44 | 45 | // WithUsername 将用户名存放到上下文中. 46 | func WithUsername(ctx context.Context, username string) context.Context { 47 | return context.WithValue(ctx, usernameKey{}, username) 48 | } 49 | 50 | // Username 从上下文中提取用户名. 51 | func Username(ctx context.Context) string { 52 | username, _ := ctx.Value(usernameKey{}).(string) 53 | return username 54 | } 55 | -------------------------------------------------------------------------------- /internal/pkg/contextx/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 孔令飞 . All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. The original repo for 4 | // this file is https://github.com/onexstack/fastgo. The professional 5 | // version of this repository is https://github.com/onexstack/onex. 6 | 7 | /* 8 | Package contextx 提供了对上下文(context)的扩展功能,允许在 context 中存储和提取用户相关的信息,如用户ID、用户名和访问令牌。 9 | 10 | 使用后缀 x 表示扩展或变体,使得包名简洁且易于记忆。本包中的函数方便了在上下文中传递和管理用户信息,适用于需要上下文传递数据的场景。 11 | 12 | 典型用法: 13 | 在处理 HTTP 请求的中间件或服务函数中,可以使用这些方法将用户信息存储到上下文中,以便在整个请求生命周期内安全地共享,避免使用全局变量和参数传参。 14 | 15 | 示例: 16 | 17 | // 创建新的上下文 18 | ctx := context.Background() 19 | 20 | // 将用户ID和用户名存放到上下文中 21 | ctx = contextx.WithUserID(ctx, "user-xxxx") 22 | ctx = contextx.WithUsername(ctx, "sampleUser") 23 | 24 | // 从上下文中提取用户信息 25 | userID := contextx.UserID(ctx) 26 | username := contextx.Username(ctx) 27 | */ 28 | package contextx // import "github.com/onexstack/fastgo/internal/pkg/contextx" 29 | -------------------------------------------------------------------------------- /internal/pkg/core/core.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | 8 | "github.com/onexstack/fastgo/internal/pkg/errorsx" 9 | ) 10 | 11 | // ErrorResponse 定义了错误响应的结构, 12 | // 用于 API 请求中发生错误时返回统一的格式化错误信息. 13 | type ErrorResponse struct { 14 | // 错误原因,标识错误类型 15 | Reason string `json:"reason,omitempty"` 16 | // 错误详情的描述信息 17 | Message string `json:"message,omitempty"` 18 | } 19 | 20 | // WriteResponse 是通用的响应函数. 21 | // 它会根据是否发生错误,生成成功响应或标准化的错误响应. 22 | func WriteResponse(c *gin.Context, data any, err error) { 23 | if err != nil { 24 | // 如果发生错误,生成错误响应 25 | errx := errorsx.FromError(err) // 提取错误详细信息 26 | c.JSON(errx.Code, ErrorResponse{ 27 | Reason: errx.Reason, 28 | Message: errx.Message, 29 | }) 30 | return 31 | } 32 | 33 | // 如果没有错误,返回成功响应 34 | c.JSON(http.StatusOK, data) 35 | } 36 | -------------------------------------------------------------------------------- /internal/pkg/errorsx/code.go: -------------------------------------------------------------------------------- 1 | package errorsx 2 | 3 | import "net/http" 4 | 5 | // errorsx 预定义标准的错误. 6 | var ( 7 | // OK 代表请求成功. 8 | OK = &ErrorX{Code: http.StatusOK, Message: ""} 9 | 10 | // ErrInternal 表示所有未知的服务器端错误. 11 | ErrInternal = &ErrorX{Code: http.StatusInternalServerError, Reason: "InternalError", Message: "Internal server error."} 12 | 13 | // ErrNotFound 表示资源未找到. 14 | ErrNotFound = &ErrorX{Code: http.StatusNotFound, Reason: "NotFound", Message: "Resource not found."} 15 | 16 | // ErrDBRead 表示数据库读取失败. 17 | ErrDBRead = &ErrorX{Code: http.StatusInternalServerError, Reason: "InternalError.DBRead", Message: "Database read failure."} 18 | 19 | // ErrDBWrite 表示数据库写入失败. 20 | ErrDBWrite = &ErrorX{Code: http.StatusInternalServerError, Reason: "InternalError.DBWrite", Message: "Database write failure."} 21 | 22 | // ErrBind 表示请求体绑定错误. 23 | ErrBind = &ErrorX{Code: http.StatusBadRequest, Reason: "BindError", Message: "Error occurred while binding the request body to the struct."} 24 | 25 | // ErrInvalidArgument 表示参数验证失败. 26 | ErrInvalidArgument = &ErrorX{Code: http.StatusBadRequest, Reason: "InvalidArgument", Message: "Argument verification failed."} 27 | 28 | // ErrSignToken 表示签发 JWT Token 时出错. 29 | ErrSignToken = &ErrorX{Code: http.StatusUnauthorized, Reason: "Unauthenticated.SignToken", Message: "Error occurred while signing the JSON web token."} 30 | 31 | // ErrTokenInvalid 表示 JWT Token 格式无效. 32 | ErrTokenInvalid = &ErrorX{Code: http.StatusUnauthorized, Reason: "Unauthenticated.TokenInvalid", Message: "Token was invalid."} 33 | ) 34 | -------------------------------------------------------------------------------- /internal/pkg/errorsx/errorsx.go: -------------------------------------------------------------------------------- 1 | package errorsx 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | ) 7 | 8 | // ErrorX 定义了 fastgo 项目中使用的错误类型,用于描述错误的详细信息. 9 | type ErrorX struct { 10 | // Code 表示错误的 HTTP 状态码,用于与客户端进行交互时标识错误的类型. 11 | Code int `json:"code,omitempty"` 12 | 13 | // Reason 表示错误发生的原因,通常为业务错误码,用于精准定位问题. 14 | Reason string `json:"reason,omitempty"` 15 | 16 | // Message 表示简短的错误信息,通常可直接暴露给用户查看. 17 | Message string `json:"message,omitempty"` 18 | } 19 | 20 | // New 创建一个新的错误. 21 | func New(code int, reason string, format string, args ...any) *ErrorX { 22 | return &ErrorX{ 23 | Code: code, 24 | Reason: reason, 25 | Message: fmt.Sprintf(format, args...), 26 | } 27 | } 28 | 29 | // Error 实现 error 接口中的 `Error` 方法. 30 | func (err *ErrorX) Error() string { 31 | return fmt.Sprintf("error: code = %d reason = %s message = %s", err.Code, err.Reason, err.Message) 32 | } 33 | 34 | // WithMessage 设置错误的 Message 字段. 35 | func (err *ErrorX) WithMessage(format string, args ...any) *ErrorX { 36 | err.Message = fmt.Sprintf(format, args...) 37 | return err 38 | } 39 | 40 | // FromError 尝试将一个通用的 error 转换为自定义的 *ErrorX 类型. 41 | func FromError(err error) *ErrorX { 42 | // 如果传入的错误是 nil,则直接返回 nil,表示没有错误需要处理. 43 | if err == nil { 44 | return nil 45 | } 46 | 47 | // 检查传入的 error 是否已经是 ErrorX 类型的实例. 48 | // 如果错误可以通过 errors.As 转换为 *ErrorX 类型,则直接返回该实例. 49 | if errx := new(ErrorX); errors.As(err, &errx) { 50 | return errx 51 | } 52 | 53 | // 默认返回未知错误错误. 该错误代表服务端出错 54 | return New(ErrInternal.Code, ErrInternal.Reason, err.Error()) 55 | } 56 | -------------------------------------------------------------------------------- /internal/pkg/errorsx/post.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 孔令飞 . All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. The original repo for 4 | // this file is https://github.com/onexstack/fastgo. The professional 5 | // version of this repository is https://github.com/onexstack/onex. 6 | 7 | package errorsx 8 | 9 | import ( 10 | "net/http" 11 | ) 12 | 13 | // ErrPostNotFound 表示未找到指定的博客. 14 | var ErrPostNotFound = &ErrorX{Code: http.StatusNotFound, Reason: "NotFound.PostNotFound", Message: "Post not found."} 15 | -------------------------------------------------------------------------------- /internal/pkg/errorsx/user.go: -------------------------------------------------------------------------------- 1 | package errorsx 2 | 3 | import "net/http" 4 | 5 | var ( 6 | // ErrUsernameInvalid 表示用户名不合法. 7 | ErrUsernameInvalid = &ErrorX{ 8 | Code: http.StatusBadRequest, 9 | Reason: "InvalidArgument.UsernameInvalid", 10 | Message: "Invalid username: Username must consist of letters, digits, and underscores only, and its length must be between 3 and 20 characters.", 11 | } 12 | 13 | // ErrPasswordInvalid 表示密码不合法. 14 | ErrPasswordInvalid = &ErrorX{ 15 | Code: http.StatusBadRequest, 16 | Reason: "InvalidArgument.PasswordInvalid", 17 | Message: "Password is incorrect.", 18 | } 19 | 20 | // ErrUserAlreadyExists 表示用户已存在. 21 | ErrUserAlreadyExists = &ErrorX{Code: http.StatusBadRequest, Reason: "AlreadyExist.UserAlreadyExists", Message: "User already exists."} 22 | 23 | // ErrUserNotFound 表示未找到指定用户. 24 | ErrUserNotFound = &ErrorX{Code: http.StatusNotFound, Reason: "NotFound.UserNotFound", Message: "User not found."} 25 | ) 26 | -------------------------------------------------------------------------------- /internal/pkg/known/known.go: -------------------------------------------------------------------------------- 1 | package known 2 | 3 | const ( 4 | // XRequestID 用来定义上下文中的键,代表请求 ID. 5 | XRequestID = "x-request-id" 6 | 7 | // XUserID 用来定义上下文的键,代表请求用户 ID. UserID 整个用户生命周期唯一. 8 | XUserID = "x-user-id" 9 | ) 10 | 11 | // 定义其他常量. 12 | const ( 13 | // MaxErrGroupConcurrency 定义了 errgroup 的最大并发任务数量. 14 | // 用于限制 errgroup 中同时执行的 Goroutine 数量,从而防止资源耗尽,提升程序的稳定性. 15 | // 根据场景需求,可以调整该值大小. 16 | MaxErrGroupConcurrency = 1000 17 | ) 18 | -------------------------------------------------------------------------------- /internal/pkg/middleware/authn.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | 6 | "github.com/onexstack/fastgo/internal/pkg/contextx" 7 | "github.com/onexstack/fastgo/internal/pkg/core" 8 | "github.com/onexstack/fastgo/internal/pkg/errorsx" 9 | "github.com/onexstack/fastgo/pkg/token" 10 | ) 11 | 12 | // Authn 是认证中间件,用来从 gin.Context 中提取 token 并验证 token 是否合法, 13 | // 如果合法则将 token 中的 sub 作为<用户名>存放在 gin.Context 的 XUsernameKey 键中. 14 | func Authn() gin.HandlerFunc { 15 | return func(c *gin.Context) { 16 | // 解析 JWT Token 17 | userID, err := token.ParseRequest(c) 18 | if err != nil { 19 | core.WriteResponse(c, errorsx.ErrTokenInvalid, nil) 20 | c.Abort() 21 | return 22 | } 23 | 24 | // 将用户ID和用户名注入到上下文中 25 | ctx := contextx.WithUserID(c.Request.Context(), userID) 26 | c.Request = c.Request.WithContext(ctx) 27 | 28 | // 继续后续的操作 29 | c.Next() 30 | 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /internal/pkg/middleware/header.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 孔令飞 . All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. The original repo for 4 | // this file is https://github.com/onexstack/fastgo. The professional 5 | // version of this repository is https://github.com/onexstack/onex. 6 | 7 | package middleware 8 | 9 | import ( 10 | "net/http" 11 | "time" 12 | 13 | "github.com/gin-gonic/gin" 14 | ) 15 | 16 | // NoCache 是一个 Gin 中间件,用来禁止客户端缓存 HTTP 请求的返回结果. 17 | func NoCache(c *gin.Context) { 18 | c.Header("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate, value") 19 | c.Header("Expires", "Thu, 01 Jan 1970 00:00:00 GMT") 20 | c.Header("Last-Modified", time.Now().UTC().Format(http.TimeFormat)) 21 | c.Next() 22 | } 23 | 24 | // Cors 是一个 Gin 中间件,用来设置 options 请求的返回头,然后退出中间件链,并结束请求(浏览器跨域设置). 25 | func Cors(c *gin.Context) { 26 | if c.Request.Method != "OPTIONS" { 27 | c.Next() 28 | } else { 29 | c.Header("Access-Control-Allow-Origin", "*") 30 | c.Header("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS") 31 | c.Header("Access-Control-Allow-Headers", "authorization, origin, content-type, accept") 32 | c.Header("Allow", "HEAD,GET,POST,PUT,PATCH,DELETE,OPTIONS") 33 | c.Header("Content-Type", "application/json") 34 | c.AbortWithStatus(200) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /internal/pkg/middleware/requestid.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 孔令飞 . All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. The original repo for 4 | // this file is https://github.com/onexstack/fastgo. The professional 5 | // version of this repository is https://github.com/onexstack/onex. 6 | 7 | package middleware 8 | 9 | import ( 10 | "github.com/gin-gonic/gin" 11 | "github.com/google/uuid" 12 | 13 | "github.com/onexstack/fastgo/internal/pkg/contextx" 14 | "github.com/onexstack/fastgo/internal/pkg/known" 15 | ) 16 | 17 | // RequestID 是一个 Gin 中间件,用来在每一个 HTTP 请求的 context, response 中注入 `x-request-id` 键值对. 18 | func RequestID() gin.HandlerFunc { 19 | return func(c *gin.Context) { 20 | // 从请求头中获取 `x-request-id`,如果不存在则生成新的 UUID 21 | requestID := c.Request.Header.Get(known.XRequestID) 22 | 23 | if requestID == "" { 24 | requestID = uuid.New().String() 25 | } 26 | 27 | // 将 RequestID 保存到 context.Context 中,以便后续程序使用 28 | ctx := contextx.WithRequestID(c.Request.Context(), requestID) 29 | c.Request = c.Request.WithContext(ctx) 30 | 31 | // 将 RequestID 保存到 HTTP 返回头中,Header 的键为 `x-request-id` 32 | c.Writer.Header().Set(known.XRequestID, requestID) 33 | 34 | // 继续处理请求 35 | c.Next() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /internal/pkg/rid/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 孔令飞 . All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. The original repo for 4 | // this file is https://github.com/onexstack/fastgo. The professional 5 | // version of this repository is https://github.com/onexstack/onex. 6 | 7 | package rid // import "github.com/onexstack/fastgo/internal/pkg/rid" 8 | -------------------------------------------------------------------------------- /internal/pkg/rid/rid.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 孔令飞 . All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. The original repo for 4 | // this file is https://github.com/onexstack/fastgo. The professional 5 | // version of this repository is https://github.com/onexstack/onex. 6 | 7 | package rid 8 | 9 | import ( 10 | "github.com/onexstack/onexstack/pkg/id" 11 | ) 12 | 13 | const defaultABC = "abcdefghijklmnopqrstuvwxyz1234567890" 14 | 15 | type ResourceID string 16 | 17 | const ( 18 | // UserID 定义用户资源标识符. 19 | UserID ResourceID = "user" 20 | // PostID 定义博文资源标识符. 21 | PostID ResourceID = "post" 22 | ) 23 | 24 | // String 将资源标识符转换为字符串. 25 | func (rid ResourceID) String() string { 26 | return string(rid) 27 | } 28 | 29 | // New 创建带前缀的唯一标识符. 30 | func (rid ResourceID) New(counter uint64) string { 31 | // 使用自定义选项生成唯一标识符 32 | uniqueStr := id.NewCode( 33 | counter, 34 | id.WithCodeChars([]rune(defaultABC)), 35 | id.WithCodeL(6), 36 | id.WithCodeSalt(Salt()), 37 | ) 38 | return rid.String() + "-" + uniqueStr 39 | } 40 | -------------------------------------------------------------------------------- /internal/pkg/rid/rid_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 孔令飞 . All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. The original repo for 4 | // this file is https://github.com/onexstack/fastgo. The professional 5 | // version of this repository is https://github.com/onexstack/onex. 6 | 7 | package rid_test 8 | 9 | import ( 10 | "strings" 11 | "testing" 12 | 13 | "github.com/stretchr/testify/assert" 14 | 15 | "github.com/onexstack/fastgo/internal/pkg/rid" 16 | ) 17 | 18 | // Mock Salt function used for testing 19 | func Salt() string { 20 | return "staticSalt" 21 | } 22 | 23 | func TestResourceID_String(t *testing.T) { 24 | // 测试 UserID 转换为字符串 25 | userID := rid.UserID 26 | assert.Equal(t, "user", userID.String(), "UserID.String() should return 'user'") 27 | 28 | // 测试 PostID 转换为字符串 29 | postID := rid.PostID 30 | assert.Equal(t, "post", postID.String(), "PostID.String() should return 'post'") 31 | } 32 | 33 | func TestResourceID_New(t *testing.T) { 34 | // 测试生成的ID是否带有正确前缀 35 | userID := rid.UserID 36 | uniqueID := userID.New(1) 37 | 38 | assert.True(t, len(uniqueID) > 0, "Generated ID should not be empty") 39 | assert.Contains(t, uniqueID, "user-", "Generated ID should start with 'user-' prefix") 40 | 41 | // 生成另外一个唯一标识符,确保生成的值不同(唯一性) 42 | anotherID := userID.New(2) 43 | assert.NotEqual(t, uniqueID, anotherID, "Generated IDs should be unique") 44 | } 45 | 46 | func BenchmarkResourceID_New(b *testing.B) { 47 | // 性能测试 48 | b.ResetTimer() 49 | for i := 0; i < b.N; i++ { 50 | userID := rid.UserID 51 | _ = userID.New(uint64(i)) 52 | } 53 | } 54 | 55 | func FuzzResourceID_New(f *testing.F) { 56 | // 添加预置测试数据 57 | f.Add(uint64(1)) // 添加一个种子值counter为1 58 | f.Add(uint64(123456)) // 添加一个较大的种子值 59 | 60 | f.Fuzz(func(t *testing.T, counter uint64) { 61 | // 测试UserID的New方法 62 | result := rid.UserID.New(counter) 63 | 64 | // 断言结果不为空 65 | assert.NotEmpty(t, result, "The generated unique identifier should not be empty") 66 | 67 | // 断言结果必须包含资源标识符前缀 68 | assert.Contains(t, result, rid.UserID.String()+"-", "The generated unique identifier should contain the correct prefix") 69 | 70 | // 断言前缀不会与uniqueStr部分重叠 71 | splitParts := strings.SplitN(result, "-", 2) 72 | assert.Equal(t, rid.UserID.String(), splitParts[0], "The prefix part of the result should correctly match the UserID") 73 | 74 | // 断言生成的ID具有固定长度(基于NewCode的配置) 75 | if len(splitParts) == 2 { 76 | assert.Equal(t, 6, len(splitParts[1]), "The unique identifier part should have a length of 6") 77 | } else { 78 | t.Errorf("The format of the generated unique identifier does not meet expectation") 79 | } 80 | }) 81 | } 82 | -------------------------------------------------------------------------------- /internal/pkg/rid/salt.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 孔令飞 . All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. The original repo for 4 | // this file is https://github.com/onexstack/fastgo. The professional 5 | // version of this repository is https://github.com/onexstack/onex. 6 | 7 | package rid 8 | 9 | import ( 10 | "crypto/rand" 11 | "crypto/sha256" 12 | "fmt" 13 | "hash/fnv" 14 | "os" 15 | ) 16 | 17 | // Salt 计算机器 ID 的哈希值并返回一个 uint64 类型的盐值. 18 | func Salt() uint64 { 19 | // 使用 FNV-1a 哈希算法计算字符串的哈希值 20 | hasher := fnv.New64a() 21 | hasher.Write(ReadMachineID()) 22 | 23 | // 将哈希值转换为 uint64 型的盐 24 | hashValue := hasher.Sum64() 25 | return hashValue 26 | } 27 | 28 | // ReadMachineID 获取机器 ID,如果无法获取,则生成随机 ID. 29 | func ReadMachineID() []byte { 30 | id := make([]byte, 3) 31 | machineID, err := readPlatformMachineID() 32 | if err != nil || len(machineID) == 0 { 33 | machineID, err = os.Hostname() 34 | } 35 | 36 | if err == nil && len(machineID) != 0 { 37 | hasher := sha256.New() 38 | hasher.Write([]byte(machineID)) 39 | copy(id, hasher.Sum(nil)) 40 | } else { 41 | // 如果无法收集机器 ID,则回退到生成随机数 42 | if _, randErr := rand.Reader.Read(id); randErr != nil { 43 | panic(fmt.Errorf("id: cannot get hostname nor generate a random number: %w; %w", err, randErr)) 44 | } 45 | } 46 | return id 47 | } 48 | 49 | // readPlatformMachineID 尝试读取平台特定的机器 ID. 50 | func readPlatformMachineID() (string, error) { 51 | data, err := os.ReadFile("/etc/machine-id") 52 | if err != nil || len(data) == 0 { 53 | data, err = os.ReadFile("/sys/class/dmi/id/product_uuid") 54 | } 55 | return string(data), err 56 | } 57 | -------------------------------------------------------------------------------- /pkg/api/apiserver/v1/post.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 孔令飞 . All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. The original repo for 4 | // this file is https://github.com/onexstack/fastgo. The professional 5 | // version of this repository is https://github.com/onexstack/onex. 6 | 7 | // Post API 定义,包含博客文章的请求和响应消息 8 | 9 | package v1 10 | 11 | import ( 12 | "time" 13 | ) 14 | 15 | // Post 表示博客文章 16 | type Post struct { 17 | // postID 表示博文 ID 18 | PostID string `json:"postID"` 19 | // userID 表示用户 ID 20 | UserID string `json:"userID"` 21 | // title 表示博客标题 22 | Title string `json:"title"` 23 | // content 表示博客内容 24 | Content string `json:"content"` 25 | // createdAt 表示博客创建时间 26 | CreatedAt time.Time `json:"createdAt"` 27 | // updatedAt 表示博客最后更新时间 28 | UpdatedAt time.Time `json:"updatedAt"` 29 | } 30 | 31 | // CreatePostRequest 表示创建文章请求 32 | type CreatePostRequest struct { 33 | // title 表示博客标题 34 | Title string `json:"title"` 35 | // content 表示博客内容 36 | Content string `json:"content"` 37 | } 38 | 39 | // CreatePostResponse 表示创建文章响应 40 | type CreatePostResponse struct { 41 | // postID 表示创建的文章 ID 42 | PostID string `json:"postID"` 43 | } 44 | 45 | // UpdatePostRequest 表示更新文章请求 46 | type UpdatePostRequest struct { 47 | // postID 表示要更新的文章 ID,对应 {postID} 48 | PostID string `json:"postID" uri:"postID"` 49 | // title 表示更新后的博客标题 50 | Title *string `json:"title"` 51 | // content 表示更新后的博客内容 52 | Content *string `json:"content"` 53 | } 54 | 55 | // UpdatePostResponse 表示更新文章响应 56 | type UpdatePostResponse struct { 57 | } 58 | 59 | // DeletePostRequest 表示删除文章请求 60 | type DeletePostRequest struct { 61 | // postIDs 表示要删除的文章 ID 列表 62 | PostIDs []string `json:"postIDs"` 63 | } 64 | 65 | // DeletePostResponse 表示删除文章响应 66 | type DeletePostResponse struct { 67 | } 68 | 69 | // GetPostRequest 表示获取文章请求 70 | type GetPostRequest struct { 71 | // postID 表示要获取的文章 ID 72 | PostID string `json:"postID" uri:"postID"` 73 | } 74 | 75 | // GetPostResponse 表示获取文章响应 76 | type GetPostResponse struct { 77 | // post 表示返回的文章信息 78 | Post *Post `json:"post"` 79 | } 80 | 81 | // ListPostRequest 表示获取文章列表请求 82 | type ListPostRequest struct { 83 | // offset 表示偏移量 84 | Offset int64 `json:"offset"` 85 | // limit 表示每页数量 86 | Limit int64 `json:"limit"` 87 | // title 表示可选的标题过滤 88 | Title *string `json:"title"` 89 | } 90 | 91 | // ListPostResponse 表示获取文章列表响应 92 | type ListPostResponse struct { 93 | // total_count 表示总文章数 94 | TotalCount int64 `json:"total_count"` 95 | // posts 表示文章列表 96 | Posts []*Post `json:"posts"` 97 | } 98 | -------------------------------------------------------------------------------- /pkg/api/apiserver/v1/user.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 孔令飞 . All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. The original repo for 4 | // this file is https://github.com/onexstack/fastgo. The professional 5 | // version of this repository is https://github.com/onexstack/onex. 6 | 7 | // User API 定义,包含用户信息、登录请求和响应等相关消息 8 | 9 | package v1 10 | 11 | import ( 12 | "time" 13 | ) 14 | 15 | // User 表示用户信息 16 | type User struct { 17 | // userID 表示用户 ID 18 | UserID string `json:"userID"` 19 | // username 表示用户名称 20 | Username string `json:"username"` 21 | // nickname 表示用户昵称 22 | Nickname string `json:"nickname"` 23 | // email 表示用户电子邮箱 24 | Email string `json:"email"` 25 | // phone 表示用户手机号 26 | Phone string `json:"phone"` 27 | // postCount 表示用户拥有的博客数量 28 | PostCount int64 `json:"postCount"` 29 | // createdAt 表示用户注册时间 30 | CreatedAt time.Time `json:"createdAt"` 31 | // updatedAt 表示用户最后更新时间 32 | UpdatedAt time.Time `json:"updatedAt"` 33 | } 34 | 35 | // LoginRequest 表示登录请求 36 | type LoginRequest struct { 37 | // username 表示用户名称 38 | Username string `json:"username"` 39 | // password 表示用户密码 40 | Password string `json:"password"` 41 | } 42 | 43 | // LoginResponse 表示登录响应 44 | type LoginResponse struct { 45 | // token 表示返回的身份验证令牌 46 | Token string `json:"token"` 47 | // expireAt 表示该 token 的过期时间 48 | ExpireAt time.Time `json:"expireAt"` 49 | } 50 | 51 | // RefreshTokenRequest 表示刷新令牌的请求 52 | type RefreshTokenRequest struct { 53 | } 54 | 55 | // RefreshTokenResponse 表示刷新令牌的响应 56 | type RefreshTokenResponse struct { 57 | // token 表示返回的身份验证令牌 58 | Token string `json:"token"` 59 | // expireAt 表示该 token 的过期时间 60 | ExpireAt time.Time `json:"expireAt"` 61 | } 62 | 63 | // ChangePasswordRequest 表示修改密码请求 64 | type ChangePasswordRequest struct { 65 | // oldPassword 表示当前密码 66 | OldPassword string `json:"oldPassword"` 67 | // newPassword 表示准备修改的新密码 68 | NewPassword string `json:"newPassword"` 69 | } 70 | 71 | // ChangePasswordResponse 表示修改密码响应 72 | type ChangePasswordResponse struct { 73 | } 74 | 75 | // CreateUserRequest 表示创建用户请求 76 | type CreateUserRequest struct { 77 | // username 表示用户名称 78 | Username string `json:"username"` 79 | // password 表示用户密码 80 | Password string `json:"password"` 81 | // nickname 表示用户昵称 82 | Nickname *string `json:"nickname"` 83 | // email 表示用户电子邮箱 84 | Email string `json:"email"` 85 | // phone 表示用户手机号 86 | Phone string `json:"phone"` 87 | } 88 | 89 | // CreateUserResponse 表示创建用户响应 90 | type CreateUserResponse struct { 91 | // userID 表示新创建的用户 ID 92 | UserID string `json:"userID"` 93 | } 94 | 95 | // UpdateUserRequest 表示更新用户请求 96 | type UpdateUserRequest struct { 97 | // username 表示可选的用户名称 98 | Username *string `json:"username"` 99 | // nickname 表示可选的用户昵称 100 | Nickname *string `json:"nickname"` 101 | // email 表示可选的用户电子邮箱 102 | Email *string `json:"email"` 103 | // phone 表示可选的用户手机号 104 | Phone *string `json:"phone"` 105 | } 106 | 107 | // UpdateUserResponse 表示更新用户响应 108 | type UpdateUserResponse struct { 109 | } 110 | 111 | // DeleteUserRequest 表示删除用户请求 112 | type DeleteUserRequest struct { 113 | } 114 | 115 | // DeleteUserResponse 表示删除用户响应 116 | type DeleteUserResponse struct { 117 | } 118 | 119 | // GetUserRequest 表示获取用户请求 120 | type GetUserRequest struct { 121 | } 122 | 123 | // GetUserResponse 表示获取用户响应 124 | type GetUserResponse struct { 125 | // user 表示返回的用户信息 126 | User *User `json:"user"` 127 | } 128 | 129 | // ListUserRequest 表示用户列表请求 130 | type ListUserRequest struct { 131 | // offset 表示偏移量 132 | Offset int64 `json:"offset"` 133 | // limit 表示每页数量 134 | Limit int64 `json:"limit"` 135 | } 136 | 137 | // ListUserResponse 表示用户列表响应 138 | type ListUserResponse struct { 139 | // totalCount 表示总用户数 140 | TotalCount int64 `json:"totalCount"` 141 | // users 表示用户列表 142 | Users []*User `json:"users"` 143 | } 144 | -------------------------------------------------------------------------------- /pkg/auth/authn.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import "golang.org/x/crypto/bcrypt" 4 | 5 | // Encrypt 使用 bcrypt 加密纯文本. 6 | func Encrypt(source string) (string, error) { 7 | hashedBytes, err := bcrypt.GenerateFromPassword([]byte(source), bcrypt.DefaultCost) 8 | 9 | return string(hashedBytes), err 10 | } 11 | 12 | // Compare 比较密文和明文是否相同. 13 | func Compare(hashedPassword, password string) error { 14 | return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)) 15 | } 16 | -------------------------------------------------------------------------------- /pkg/options/mysql_options.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Lingfei Kong . All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. The original repo for 4 | // this file is https://github.com/onexstack/onex. 5 | // 6 | 7 | package options 8 | 9 | import ( 10 | "fmt" 11 | "net" 12 | "strconv" 13 | "time" 14 | 15 | "gorm.io/driver/mysql" 16 | "gorm.io/gorm" 17 | ) 18 | 19 | // MySQLOptions defines options for mysql database. 20 | type MySQLOptions struct { 21 | Addr string `json:"addr,omitempty" mapstructure:"addr"` 22 | Username string `json:"username,omitempty" mapstructure:"username"` 23 | Password string `json:"-" mapstructure:"password"` 24 | Database string `json:"database" mapstructure:"database"` 25 | MaxIdleConnections int `json:"max-idle-connections,omitempty" mapstructure:"max-idle-connections,omitempty"` 26 | MaxOpenConnections int `json:"max-open-connections,omitempty" mapstructure:"max-open-connections"` 27 | MaxConnectionLifeTime time.Duration `json:"max-connection-life-time,omitempty" mapstructure:"max-connection-life-time"` 28 | } 29 | 30 | // NewMySQLOptions create a `zero` value instance. 31 | func NewMySQLOptions() *MySQLOptions { 32 | return &MySQLOptions{ 33 | Addr: "127.0.0.1:3306", 34 | Username: "onex", 35 | Password: "onex(#)666", 36 | Database: "onex", 37 | MaxIdleConnections: 100, 38 | MaxOpenConnections: 100, 39 | MaxConnectionLifeTime: time.Duration(10) * time.Second, 40 | } 41 | } 42 | 43 | // Validate verifies flags passed to MySQLOptions. 44 | func (o *MySQLOptions) Validate() error { 45 | // 验证MySQL地址格式 46 | if o.Addr == "" { 47 | return fmt.Errorf("MySQL server address cannot be empty") 48 | } 49 | // 检查地址格式是否为host:port 50 | host, portStr, err := net.SplitHostPort(o.Addr) 51 | if err != nil { 52 | return fmt.Errorf("Invalid MySQL address format '%s': %w", o.Addr, err) 53 | } 54 | // 验证端口是否为数字 55 | port, err := strconv.Atoi(portStr) 56 | if err != nil || port < 1 || port > 65535 { 57 | return fmt.Errorf("Invalid MySQL port: %s", portStr) 58 | } 59 | // 验证主机名是否为空 60 | if host == "" { 61 | return fmt.Errorf("MySQL hostname cannot be empty") 62 | } 63 | 64 | // 验证凭据和数据库名 65 | if o.Username == "" { 66 | return fmt.Errorf("MySQL username cannot be empty") 67 | } 68 | 69 | if o.Password == "" { 70 | return fmt.Errorf("MySQL password cannot be empty") 71 | } 72 | 73 | if o.Database == "" { 74 | return fmt.Errorf("MySQL database name cannot be empty") 75 | } 76 | 77 | // 验证连接池参数 78 | if o.MaxIdleConnections <= 0 { 79 | return fmt.Errorf("MySQL max idle connections must be greater than 0") 80 | } 81 | 82 | if o.MaxOpenConnections <= 0 { 83 | return fmt.Errorf("MySQL max open connections must be greater than 0") 84 | } 85 | 86 | if o.MaxIdleConnections > o.MaxOpenConnections { 87 | return fmt.Errorf("MySQL max idle connections cannot be greater than max open connections") 88 | } 89 | 90 | if o.MaxConnectionLifeTime <= 0 { 91 | return fmt.Errorf("MySQL max connection lifetime must be greater than 0") 92 | } 93 | 94 | return nil 95 | } 96 | 97 | // DSN return DSN from MySQLOptions. 98 | func (o *MySQLOptions) DSN() string { 99 | return fmt.Sprintf(`%s:%s@tcp(%s)/%s?charset=utf8&parseTime=%t&loc=%s`, 100 | o.Username, 101 | o.Password, 102 | o.Addr, 103 | o.Database, 104 | true, 105 | "Local") 106 | } 107 | 108 | // NewDB create mysql store with the given config. 109 | func (o *MySQLOptions) NewDB() (*gorm.DB, error) { 110 | db, err := gorm.Open(mysql.Open(o.DSN()), &gorm.Config{ 111 | // PrepareStmt executes the given query in cached statement. 112 | // This can improve performance. 113 | PrepareStmt: true, 114 | }) 115 | if err != nil { 116 | return nil, err 117 | } 118 | 119 | sqlDB, err := db.DB() 120 | if err != nil { 121 | return nil, err 122 | } 123 | 124 | // SetMaxOpenConns sets the maximum number of open connections to the database. 125 | sqlDB.SetMaxOpenConns(o.MaxOpenConnections) 126 | 127 | // SetConnMaxLifetime sets the maximum amount of time a connection may be reused. 128 | sqlDB.SetConnMaxLifetime(o.MaxConnectionLifeTime) 129 | 130 | // SetMaxIdleConns sets the maximum number of connections in the idle connection pool. 131 | sqlDB.SetMaxIdleConns(o.MaxIdleConnections) 132 | 133 | return db, nil 134 | } 135 | -------------------------------------------------------------------------------- /pkg/token/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 孔令飞 . All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. The original repo for 4 | // this file is https://github.com/onexstack/fastgo. The professional 5 | // version of this repository is https://github.com/onexstack/onex. 6 | 7 | package token // import "github.com/onexstack/onexstack/pkg/token" 8 | -------------------------------------------------------------------------------- /pkg/token/token.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 孔令飞 . All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. The original repo for 4 | // this file is https://github.com/onexstack/fastgo. The professional 5 | // version of this repository is https://github.com/onexstack/onex. 6 | 7 | package token 8 | 9 | import ( 10 | "errors" 11 | "fmt" 12 | "sync" 13 | "time" 14 | 15 | "github.com/gin-gonic/gin" 16 | jwt "github.com/golang-jwt/jwt/v4" 17 | ) 18 | 19 | // Config 包括 token 包的配置选项. 20 | type Config struct { 21 | // key 用于签发和解析 token 的密钥. 22 | key string 23 | // identityKey 是 token 中用户身份的键. 24 | identityKey string 25 | // expiration 是签发的 token 过期时间 26 | expiration time.Duration 27 | } 28 | 29 | var ( 30 | config = Config{"Rtg8BPKNEf2mB4mgvKONGPZZQSaJWNLijxR42qRgq0iBb5", "identityKey", 2 * time.Hour} 31 | once sync.Once // 确保配置只被初始化一次 32 | ) 33 | 34 | // Init 设置包级别的配置 config, config 会用于本包后面的 token 签发和解析. 35 | func Init(key string, identityKey string, expiration time.Duration) { 36 | once.Do(func() { 37 | if key != "" { 38 | config.key = key // 设置密钥 39 | } 40 | if identityKey != "" { 41 | config.identityKey = identityKey // 设置身份键 42 | } 43 | if expiration != 0 { 44 | config.expiration = expiration 45 | } 46 | }) 47 | } 48 | 49 | // Parse 使用指定的密钥 key 解析 token,解析成功返回 token 上下文,否则报错. 50 | func Parse(tokenString string, key string) (string, error) { 51 | // 解析 token 52 | token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { 53 | // 确保 token 加密算法是预期的加密算法 54 | if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { 55 | return nil, jwt.ErrSignatureInvalid 56 | } 57 | 58 | return []byte(key), nil // 返回密钥 59 | }) 60 | // 解析失败 61 | if err != nil { 62 | return "", err 63 | } 64 | 65 | var identityKey string 66 | // 如果解析成功,从 token 中取出 token 的主题 67 | if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { 68 | if key, exists := claims[config.identityKey]; exists { 69 | if identity, valid := key.(string); valid { 70 | identityKey = identity // 获取身份键 71 | } 72 | } 73 | } 74 | if identityKey == "" { 75 | return "", jwt.ErrSignatureInvalid 76 | } 77 | 78 | return identityKey, nil 79 | } 80 | 81 | // ParseRequest 从请求头中获取令牌,并将其传递给 Parse 函数以解析令牌. 82 | func ParseRequest(c *gin.Context) (string, error) { 83 | header := c.Request.Header.Get("Authorization") 84 | 85 | if len(header) == 0 { 86 | //nolint: err113 87 | return "", errors.New("the length of the `Authorization` header is zero") // 返回错误 88 | } 89 | 90 | var token string 91 | // 从请求头中取出 token 92 | fmt.Sscanf(header, "Bearer %s", &token) 93 | 94 | return Parse(token, config.key) 95 | } 96 | 97 | // Sign 使用 jwtSecret 签发 token,token 的 claims 中会存放传入的 subject. 98 | func Sign(identityKey string) (string, time.Time, error) { 99 | // 计算过期时间 100 | expireAt := time.Now().Add(config.expiration) 101 | 102 | // Token 的内容 103 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ 104 | config.identityKey: identityKey, // 存放用户身份 105 | "nbf": time.Now().Unix(), // token 生效时间 106 | "iat": time.Now().Unix(), // token 签发时间 107 | "exp": expireAt.Unix(), // token 过期时间 108 | }) 109 | if config.key == "" { 110 | return "", time.Time{}, jwt.ErrInvalidKey 111 | } 112 | 113 | // 签发 token 114 | tokenString, err := token.SignedString([]byte(config.key)) 115 | if err != nil { 116 | return "", time.Time{}, err 117 | } 118 | 119 | return tokenString, expireAt, nil // 返回 token 字符串、过期时间和错误 120 | } 121 | -------------------------------------------------------------------------------- /pkg/version/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Lingfei Kong . All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. The original repo for 4 | // this file is https://github.com/superproj/onex. 5 | // 6 | 7 | package version // import "github.com/superproj/onex/pkg/app/version" 8 | -------------------------------------------------------------------------------- /pkg/version/flag.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Lingfei Kong . All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. The original repo for 4 | // this file is https://github.com/superproj/onex. 5 | // 6 | 7 | // Package verflag defines utility functions to handle command line flags 8 | // related to version of Kubernetes. 9 | package version 10 | 11 | import ( 12 | "fmt" 13 | "os" 14 | "strconv" 15 | 16 | flag "github.com/spf13/pflag" 17 | ) 18 | 19 | type versionValue int 20 | 21 | const ( 22 | // 未设置版本. 23 | VersionNotSet versionValue = 0 24 | // 启用版本. 25 | VersionEnabled versionValue = 1 26 | // 原始版本. 27 | VersionRaw versionValue = 2 28 | ) 29 | 30 | const strRawVersion string = "raw" 31 | 32 | func (v *versionValue) IsBoolFlag() bool { 33 | return true 34 | } 35 | 36 | func (v *versionValue) Get() any { 37 | return *v 38 | } 39 | 40 | func (v *versionValue) Set(s string) error { 41 | if s == strRawVersion { 42 | *v = VersionRaw 43 | return nil 44 | } 45 | boolVal, err := strconv.ParseBool(s) 46 | if boolVal { 47 | *v = VersionEnabled 48 | } else { 49 | *v = VersionNotSet 50 | } 51 | return err 52 | } 53 | 54 | func (v *versionValue) String() string { 55 | if *v == VersionRaw { 56 | return strRawVersion 57 | } 58 | return fmt.Sprintf("%v", bool(*v == VersionEnabled)) 59 | } 60 | 61 | // The type of the flag as required by the pflag.Value interface. 62 | func (v *versionValue) Type() string { 63 | return "version" 64 | } 65 | 66 | func VersionVar(p *versionValue, name string, value versionValue, usage string) { 67 | *p = value 68 | flag.Var(p, name, usage) 69 | // "--version" will be treated as "--version=true" 70 | flag.Lookup(name).NoOptDefVal = "true" 71 | } 72 | 73 | func Version(name string, value versionValue, usage string) *versionValue { 74 | p := new(versionValue) 75 | VersionVar(p, name, value, usage) 76 | return p 77 | } 78 | 79 | const versionFlagName = "version" 80 | 81 | var versionFlag = Version(versionFlagName, VersionNotSet, "Print version information and quit") 82 | 83 | // AddFlags registers this package's flags on arbitrary FlagSets, such that they point to the 84 | // same value as the global flags. 85 | func AddFlags(fs *flag.FlagSet) { 86 | fs.AddFlag(flag.Lookup(versionFlagName)) 87 | } 88 | 89 | // PrintAndExitIfRequested will check if the -version flag was passed 90 | // and, if so, print the version and exit. 91 | func PrintAndExitIfRequested() { 92 | // 检查版本标志的值并打印相应的信息 93 | if *versionFlag == VersionRaw { 94 | fmt.Printf("%s\n", Get().Text()) 95 | os.Exit(0) 96 | } else if *versionFlag == VersionEnabled { 97 | fmt.Printf("%s\n", Get().String()) 98 | os.Exit(0) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /pkg/version/version.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Lingfei Kong . All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package version supplies version information collected at build time to 6 | // apimachinery components. 7 | package version 8 | 9 | import ( 10 | "encoding/json" 11 | "fmt" 12 | "runtime" 13 | 14 | "github.com/gosuri/uitable" 15 | ) 16 | 17 | var ( 18 | // semantic version, derived by build scripts (see 19 | // https://github.com/kubernetes/community/blob/master/contributors/design-proposals/release/versioning.md 20 | // for a detailed discussion of this field) 21 | // 22 | // TODO: This field is still called "gitVersion" for legacy 23 | // reasons. For prerelease versions, the build metadata on the 24 | // semantic version is a git hash, but the version itself is no 25 | // longer the direct output of "git describe", but a slight 26 | // translation to be semver compliant. 27 | 28 | // NOTE: The $Format strings are replaced during 'git archive' thanks to the 29 | // companion .gitattributes file containing 'export-subst' in this same 30 | // directory. See also https://git-scm.com/docs/gitattributes 31 | gitVersion = "v0.0.0-master+$Format:%H$" 32 | gitCommit = "$Format:%H$" // sha1 from git, output of $(git rev-parse HEAD) 33 | gitTreeState = "" // state of git tree, either "clean" or "dirty" 34 | 35 | buildDate = "1970-01-01T00:00:00Z" // build date in ISO8601 format, output of $(date -u +'%Y-%m-%dT%H:%M:%SZ') 36 | ) 37 | 38 | // Info contains versioning information. 39 | type Info struct { 40 | GitVersion string `json:"gitVersion"` 41 | GitCommit string `json:"gitCommit"` 42 | GitTreeState string `json:"gitTreeState"` 43 | BuildDate string `json:"buildDate"` 44 | GoVersion string `json:"goVersion"` 45 | Compiler string `json:"compiler"` 46 | Platform string `json:"platform"` 47 | } 48 | 49 | // String returns info as a human-friendly version string. 50 | func (info Info) String() string { 51 | return info.GitVersion 52 | } 53 | 54 | // ToJSON returns the JSON string of version information. 55 | func (info Info) ToJSON() string { 56 | s, _ := json.Marshal(info) 57 | 58 | return string(s) 59 | } 60 | 61 | // Text encodes the version information into UTF-8-encoded text and 62 | // returns the result. 63 | func (info Info) Text() string { 64 | table := uitable.New() 65 | table.RightAlign(0) 66 | table.MaxColWidth = 80 67 | table.Separator = " " 68 | table.AddRow("gitVersion:", info.GitVersion) 69 | table.AddRow("gitCommit:", info.GitCommit) 70 | table.AddRow("gitTreeState:", info.GitTreeState) 71 | table.AddRow("buildDate:", info.BuildDate) 72 | table.AddRow("goVersion:", info.GoVersion) 73 | table.AddRow("compiler:", info.Compiler) 74 | table.AddRow("platform:", info.Platform) 75 | 76 | return table.String() 77 | } 78 | 79 | // Get returns the overall codebase version. It's for detecting 80 | // what code a binary was built from. 81 | func Get() Info { 82 | // These variables typically come from -ldflags settings and in 83 | // their absence fallback to the settings in pkg/version/base.go 84 | return Info{ 85 | GitVersion: gitVersion, 86 | GitCommit: gitCommit, 87 | GitTreeState: gitTreeState, 88 | BuildDate: buildDate, 89 | GoVersion: runtime.Version(), 90 | Compiler: runtime.Compiler, 91 | Platform: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH), 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /scripts/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onexstack/fastgo/5ff4ad326cc4a23b59b793587279a061e238f6c3/scripts/.keep -------------------------------------------------------------------------------- /scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright 2020 Lingfei Kong . All rights reserved. 4 | # Use of this source code is governed by a MIT style 5 | # license that can be found in the LICENSE file. 6 | 7 | # Common utilities, variables and checks for all build scripts. 8 | set -o errexit 9 | set -o nounset 10 | set -o pipefail 11 | 12 | # The root of the build/dist directory 13 | PROJ_ROOT=$(dirname "${BASH_SOURCE[0]}")/.. 14 | 15 | INSECURE_SERVER="127.0.0.1:6666" 16 | 17 | Header="-HContent-Type: application/json" 18 | CCURL="curl -s -XPOST" # Create 19 | UCURL="curl -s -XPUT" # Update 20 | RCURL="curl -s -XGET" # Retrieve 21 | DCURL="curl -s -XDELETE" # Delete 22 | 23 | # 随机生成用户名 24 | fg::test::username() 25 | { 26 | echo fastgo$(date +%s) 27 | 28 | } 29 | 30 | # 注意:使用 root 用户登录系统,否则无法删除指定的用户 31 | fg::test::login() 32 | { 33 | ${CCURL} "${Header}" http://${INSECURE_SERVER}/login \ 34 | -d'{"username":"'$1'","password":"'$2'"}' | grep -Po 'token[" :]+\K[^"]+' 35 | } 36 | 37 | # 用户相关接口测试函数 38 | fg::test::user() 39 | { 40 | username=$(fg::test::username) 41 | # 1. 创建 fastgo 用户 42 | ${CCURL} "${Header}" http://${INSECURE_SERVER}/v1/users \ 43 | -d'{"username":"'${username}'","password":"fastgo1234","nickname":"fastgo","email":"colin404@foxmail.com","phone":"'$(date +%s)'"}'; echo 44 | echo -e "\033[32m1. 成功创建 ${username} 用户\033[0m" 45 | 46 | token="-HAuthorization: Bearer $(fg::test::login ${username} fastgo1234)" 47 | 48 | # 2. 列出所有用户 49 | ${RCURL} "${token}" "http://${INSECURE_SERVER}/v1/users?offset=0&limit=10"; echo 50 | echo -e "\033[32m2. 成功列出所有用户\033[0m" 51 | 52 | # 3. 获取 fastgo 用户的详细信息 53 | ${RCURL} "${token}" http://${INSECURE_SERVER}/v1/users/${username}; echo 54 | echo -e "\033[32m3. 成功获取 ${username} 用户详细信息\033[0m" 55 | 56 | # 4. 修改 fastgo 用户 57 | ${UCURL} "${Header}" "${token}" http://${INSECURE_SERVER}/v1/users/${username} \ 58 | -d'{"nickname":"fastgo(modified)"}'; echo 59 | echo -e "\033[32m4. 成功修改 ${username} 用户信息\033[0m" 60 | 61 | # 5. 删除 fastgo 用户 62 | ${DCURL} "${token}" http://${INSECURE_SERVER}/v1/users/${username}; echo 63 | echo -e "\033[32m5. 成功删除 ${username} 用户\033[0m" 64 | 65 | echo -e '\033[32m==> 所有用户接口测试成功\033[0m' 66 | } 67 | 68 | # 博客相关接口测试函数 69 | fg::test::post() 70 | { 71 | 72 | username=$(fg::test::username) 73 | # 1. 创建测试用户 74 | ${CCURL} "${Header}" "${token}" http://${INSECURE_SERVER}/v1/users \ 75 | -d'{"username":"'${username}'","password":"fastgo1234","nickname":"fastgo","email":"colin404@foxmail.com","phone":"'$(date +%s)'"}'; echo 76 | echo -e "\033[32m1. 成功创建测试用户: ${username}\033[0m" 77 | 78 | token="-HAuthorization: Bearer $(fg::test::login ${username} fastgo1234)" 79 | 80 | # 2. 创建一条博客 81 | postID=`${CCURL} "${Header}" "${token}" http://${INSECURE_SERVER}/v1/posts -d'{"title":"installation","content":"installation."}' | grep -Po 'post-[a-z0-9]+'` 82 | echo -e "\033[32m2. 成功创建博客: ${postID}\033[0m" 83 | 84 | # 3. 列出所有博客 85 | ${RCURL} "${token}" http://${INSECURE_SERVER}/v1/posts; echo 86 | echo -e "\033[32m3. 成功列出所有博客\033[0m" 87 | 88 | # 4. 获取所创建博客的信息 89 | ${RCURL} "${token}" http://${INSECURE_SERVER}/v1/posts/${postID}; echo 90 | echo -e "\033[32m4. 成功获取博客 ${postID} 详细信息\033[0m" 91 | 92 | # 6. 修改所创建博客的信息 93 | ${UCURL} "${Header}" "${token}" http://${INSECURE_SERVER}/v1/posts/${postID} -d'{"title":"modified"}'; echo 94 | echo -e "\033[32m5. 成功更新博客 ${postID} 信息\033[0m" 95 | 96 | # 7. 删除所创建的博客 97 | ${DCURL} "${token}" http://${INSECURE_SERVER}/v1/posts -d'{"postIDs":["'${postID}'"]}'; echo 98 | echo -e "\033[32m6. 成功删除博客 ${postID}\033[0m" 99 | 100 | ${DCURL} "${token}" http://${INSECURE_SERVER}/v1/users/${username}; echo 101 | echo -e "\033[32m7. 成功删除测试用户:${username}\033[0m" 102 | 103 | echo -e '\033[32m==> 所有博客接口测试成功\033[0m' 104 | } 105 | 106 | # 测试 user 资源 CURD 107 | fg::test::user 108 | 109 | # 测试 post 资源 CURD 110 | fg::test::post 111 | 112 | echo -e '\033[32m==> 所有 fastgo 接口测试成功\033[0m' 113 | --------------------------------------------------------------------------------