shao 2 هفته پیش
کامیت
91fc9d0cbe
28فایلهای تغییر یافته به همراه1699 افزوده شده و 0 حذف شده
  1. 3 0
      .gitignore
  2. 1 0
      README.md
  3. 69 0
      api/v1/config.go
  4. 31 0
      api/v1/public.go
  5. 36 0
      cmd/cobra.go
  6. 111 0
      cmd/httpserver/server.go
  7. 35 0
      cmd/version/version.go
  8. 172 0
      common/config.go
  9. 31 0
      common/enum.go
  10. 18 0
      common/global.go
  11. 100 0
      common/rescode.go
  12. 63 0
      common/response.go
  13. 66 0
      common/utils.go
  14. 31 0
      config/config.toml.template
  15. 25 0
      dto/config.go
  16. 9 0
      dto/user.go
  17. 67 0
      go.mod
  18. 203 0
      go.sum
  19. 103 0
      logger/gorm.go
  20. 73 0
      logger/logger.go
  21. 9 0
      main.go
  22. 51 0
      models/model.go
  23. 34 0
      models/scope.go
  24. 99 0
      models/setup.go
  25. 122 0
      router/middleware.go
  26. 34 0
      router/router.go
  27. 68 0
      service/config.go
  28. 35 0
      service/user.go

+ 3 - 0
.gitignore

@@ -0,0 +1,3 @@
+.idea
+logs/*
+config/config.toml

+ 1 - 0
README.md

@@ -0,0 +1 @@
+DR Resource Server

+ 69 - 0
api/v1/config.go

@@ -0,0 +1,69 @@
+package v1
+
+import (
+	"log"
+)
+
+import (
+	"github.com/gin-gonic/gin"
+	"github.com/spf13/cast"
+)
+
+import (
+	"resource-server/common"
+	"resource-server/models"
+	"resource-server/service"
+)
+
+func GetConfigs(c *gin.Context) {
+	res, err := service.GetConfigList()
+	if err != nil {
+		common.HttpErr(c, err)
+		return
+	}
+	common.HttpSuccess(c, res)
+}
+
+func GetConfigOptions(c *gin.Context) {
+	flag := c.Query("flag")
+	enable := cast.ToBool(c.Query("enable"))
+	res, err := service.GetConfigOptionList(flag, enable)
+	if err != nil {
+		common.HttpErr(c, err)
+		return
+	}
+	common.HttpSuccess(c, res)
+}
+
+func UpdateConfigItems(c *gin.Context) {
+	var items map[string]interface{}
+	if err := c.ShouldBindJSON(&items); err != nil {
+		common.HttpErr(c, err)
+		return
+	}
+	for key, value := range items {
+		log.Printf("Key: %s, Value: %v", key, value)
+		var configItem models.ConfigItem
+		if err := models.DB.Model(&models.ConfigItem{}).First(&configItem, "uri = ?", key).Error; err != nil {
+			//todo 回滚
+			common.HttpErr(c, err)
+			return
+		} else {
+			//todo 校验 回滚
+			switch configItem.ValueType {
+			case "string":
+				configItem.Value = cast.ToString(value)
+			case "int":
+				configItem.Value = cast.ToString(cast.ToInt(value))
+			case "bool":
+				configItem.Value = cast.ToString(cast.ToBool(value))
+			}
+			err := models.DB.Save(&configItem).Error
+			if err != nil {
+				common.HttpErr(c, err)
+				return
+			}
+		}
+	}
+	common.HttpSuccess(c)
+}

+ 31 - 0
api/v1/public.go

@@ -0,0 +1,31 @@
+package v1
+
+import (
+	"github.com/gin-gonic/gin"
+)
+
+import (
+	"resource-server/common"
+	"resource-server/service"
+)
+
+func Ping(c *gin.Context) {
+	common.HttpSuccess(c, "Pong")
+}
+
+func Login(c *gin.Context) {
+	request := &struct {
+		Username string `json:"username" binding:"required"`
+		Password string `json:"password" binding:"required"`
+	}{}
+	if err := c.ShouldBindJSON(request); err != nil {
+		common.HttpErr(c, err)
+		return
+	}
+	res, err := service.Login(request.Username, request.Password)
+	if err != nil {
+		common.HttpErr(c, err)
+		return
+	}
+	common.HttpSuccess(c, res)
+}

+ 36 - 0
cmd/cobra.go

@@ -0,0 +1,36 @@
+package cmd
+
+import (
+	"os"
+)
+
+import (
+	"github.com/spf13/cobra"
+)
+
+import (
+	"resource-server/cmd/httpserver"
+	"resource-server/cmd/version"
+)
+
+var rootCmd = &cobra.Command{
+	Use:          "resource-server",
+	Short:        "rs",
+	SilenceUsage: true,
+	Long:         `rs server`,
+	Run: func(cmd *cobra.Command, args []string) {
+		cmd.Help()
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(httpserver.StartCmd)
+	rootCmd.AddCommand(version.StartCmd)
+}
+
+// Execute : apply commands
+func Execute() {
+	if err := rootCmd.Execute(); err != nil {
+		os.Exit(-1)
+	}
+}

+ 111 - 0
cmd/httpserver/server.go

@@ -0,0 +1,111 @@
+package httpserver
+
+import (
+	"context"
+	"fmt"
+	"log/slog"
+	"net/http"
+	"os"
+	"os/signal"
+	"strings"
+	"time"
+)
+
+import (
+	"github.com/gin-gonic/gin"
+	"github.com/spf13/cobra"
+	"github.com/spf13/viper"
+)
+
+import (
+	"resource-server/common"
+	"resource-server/logger"
+	"resource-server/models"
+	"resource-server/router"
+	_ "resource-server/service"
+)
+
+var (
+	configFolder string
+	port         int
+	mode         string
+	StartCmd     = &cobra.Command{
+		Use:          "server",
+		Short:        "Start API server",
+		Example:      "skynet server -c config/config.toml",
+		SilenceUsage: true,
+		PreRun: func(cmd *cobra.Command, args []string) {
+			setup()
+		},
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return run()
+		},
+	}
+)
+
+func init() {
+	StartCmd.Flags().StringVarP(&configFolder, "config", "c", "config/", "Start server with provided configuration folder")
+	StartCmd.Flags().IntVarP(&port, "port", "p", 8000, "Tcp port server listening on")
+	StartCmd.Flags().StringVarP(&mode, "mode", "m", "debug", "server mode ; eg:debug,test,release")
+	viper.BindPFlag("port", StartCmd.Flags().Lookup("port"))
+	viper.BindPFlag("mode", StartCmd.Flags().Lookup("mode"))
+}
+
+func setup() {
+	if strings.HasSuffix(configFolder, "/") {
+		configFolder = configFolder[:len(configFolder)-1]
+		if strings.HasSuffix(configFolder, "/") {
+			panic("invalid config folder")
+		}
+	}
+	configToml := configFolder + "/config.toml"
+	//1. 读取配置
+	common.SetupConfig(configToml)
+	//2. 设置日志
+	logger.SetupLogger(true)
+	//3. 初始化gorm
+	models.SetupGorm(true)
+
+	slog.Info(`starting api server`, "pid", os.Getpid())
+}
+
+func run() error {
+	gin.SetMode(common.ApplicationConfig.Mode)
+	r := router.InitRouter()
+
+	srv := &http.Server{
+		Addr:    common.ApplicationConfig.Addr(),
+		Handler: r,
+	}
+
+	go func() {
+		// 服务连接
+		if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
+			slog.Error("listen: ", err)
+			panic("http server closed")
+		}
+	}()
+
+	fmt.Printf("module: %s\n", common.Module)
+	fmt.Printf("desc: %s\n", common.Desc)
+	fmt.Printf("version: %s\n", common.Version)
+	fmt.Printf("build: %s\n", common.Build)
+	fmt.Println("Server run at:")
+	fmt.Printf("-  Network: http://%s:%d/ \n", common.ApplicationConfig.Host, common.ApplicationConfig.Port)
+	fmt.Printf("%s Enter Control + C Shutdown Server \n", common.Now())
+
+	// 等待中断信号以优雅地关闭服务器(设置 5 秒的超时时间)
+	quit := make(chan os.Signal)
+	signal.Notify(quit, os.Interrupt)
+	<-quit
+	fmt.Printf("%s Shutdown Server ... \n", common.Now())
+
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+	if err := srv.Shutdown(ctx); err != nil {
+		slog.Error("Server Shutdown:", "err", err)
+	}
+	slog.Info("Server exiting")
+	logger.ShutdownLogger()
+	return nil
+}

+ 35 - 0
cmd/version/version.go

@@ -0,0 +1,35 @@
+package version
+
+import (
+	"fmt"
+)
+
+import (
+	"github.com/spf13/cobra"
+)
+
+import (
+	"resource-server/common"
+)
+
+var (
+	StartCmd = &cobra.Command{
+		Use:     "version",
+		Short:   "Get version info",
+		Example: "rs version",
+		PreRun: func(cmd *cobra.Command, args []string) {
+
+		},
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return run()
+		},
+	}
+)
+
+func run() error {
+	fmt.Printf("module: %s\n", common.Module)
+	fmt.Printf("desc: %s\n", common.Desc)
+	fmt.Printf("version: %s\n", common.Version)
+	fmt.Printf("build: %s\n", common.Build)
+	return nil
+}

+ 172 - 0
common/config.go

@@ -0,0 +1,172 @@
+package common
+
+import (
+	"fmt"
+)
+
+import (
+	"github.com/spf13/viper"
+)
+
+//配置文件
+
+var (
+	ApplicationConfig = new(application)
+	BasicConfig       = new(basic)
+	LoggerConfig      = new(logger)
+	RobotConfig       = new(robot)
+	PostgresConfig    = new(postgres)
+	MetadataConfig    = new(metadata)
+)
+
+// 应用配置
+type application struct {
+	Host string
+	Port int
+	Mode string
+}
+
+func (p *application) Addr() string {
+	return fmt.Sprintf("%s:%d", p.Host, p.Port)
+}
+
+type basic struct {
+	Jwt string
+}
+
+// 日志配置
+type logger struct {
+	LogDir   string // 日志文件夹路径
+	LogLevel string // 日志打印等级
+	MaxSize  int    //在进行切割之前,日志文件的最大大小(以MB为单位)
+	MaxAge   int    //保留旧文件的最大天数
+	Compress bool   //是否压缩/归档旧文件
+	Stdout   bool
+}
+
+type robot struct {
+	Info string
+	Dev  string
+	Prod string
+}
+
+// 数据库配置
+type postgres struct {
+	Ip       string
+	Port     int
+	Username string
+	Password string
+	Name     string
+}
+
+// metadata配置
+type metadata struct {
+	Languages []Lang
+}
+
+func (p *metadata) setup() {
+	if !ValidLanguages(p.Languages) {
+		panic(fmt.Sprintf("invalid languages, optional values are: %v", AllLanguages()))
+	}
+}
+
+func (p *metadata) GetLanguages() []Lang {
+	if len(p.Languages) == 0 {
+		return AllLanguages()
+	}
+	return p.Languages
+}
+
+func InitApplication() *application {
+	return &application{
+		Host: viper.GetString("host"),
+		Port: viper.GetInt("port"),
+		Mode: viper.GetString("mode"),
+	}
+}
+
+func InitBasic(cfg *viper.Viper) *basic {
+	var basic basic
+	err := cfg.Unmarshal(&basic)
+	if err != nil {
+		panic("InitBasic err")
+	}
+	return &basic
+}
+
+func InitLog(cfg *viper.Viper) *logger {
+	var logger logger
+	err := cfg.Unmarshal(&logger)
+	if err != nil {
+		panic("InitLog err")
+	}
+	return &logger
+}
+
+func InitRobot(cfg *viper.Viper) *robot {
+	var robot robot
+	err := cfg.Unmarshal(&robot)
+	if err != nil {
+		panic("InitRobot error")
+	}
+	return &robot
+}
+
+func InitPostgres(cfg *viper.Viper) *postgres {
+	var postgres postgres
+	err := cfg.Unmarshal(&postgres)
+	if err != nil {
+		panic("InitPostgres err")
+	}
+	return &postgres
+}
+
+func InitMetadata(cfg *viper.Viper) *metadata {
+	var metadata metadata
+	err := cfg.Unmarshal(&metadata)
+	if err != nil {
+		panic("InitMetadata err")
+	}
+
+	metadata.setup()
+	return &metadata
+}
+
+// 载入配置文件
+func SetupConfig(path string) {
+	viper.SetConfigFile(path)
+	if err := viper.ReadInConfig(); err != nil {
+		panic(err)
+	}
+	ApplicationConfig = InitApplication()
+
+	cfgBasic := viper.Sub("basic")
+	if cfgBasic == nil {
+		panic("No found basic in the configuration")
+	}
+	BasicConfig = InitBasic(cfgBasic)
+
+	cfgLog := viper.Sub("logger")
+	if cfgLog == nil {
+		panic("No found logger in the configuration")
+	}
+	LoggerConfig = InitLog(cfgLog)
+
+	cfgRobot := viper.Sub("robot")
+	if cfgLog == nil {
+		panic("No found robot in the configuration")
+	}
+	RobotConfig = InitRobot(cfgRobot)
+
+	cfgPostgres := viper.Sub("postgres")
+	if cfgPostgres == nil {
+		panic("No found postgres in the configuration")
+	}
+	PostgresConfig = InitPostgres(cfgPostgres)
+
+	metadataApolloConfig := viper.Sub("metadata")
+	if metadataApolloConfig == nil {
+		panic("No found metadata in the configuration")
+	}
+	MetadataConfig = InitMetadata(metadataApolloConfig)
+}

+ 31 - 0
common/enum.go

@@ -0,0 +1,31 @@
+package common
+
+type Lang string
+
+const (
+	Lang_en Lang = "en"
+	Lang_zh Lang = "zh"
+)
+
+func (p Lang) toString() string {
+	switch p {
+	case Lang_en:
+		return "en"
+	case Lang_zh:
+		return "zh"
+	}
+	return ""
+}
+
+func ValidLanguages(langs []Lang) bool {
+	for _, lang := range langs {
+		if lang.toString() == "" {
+			return false
+		}
+	}
+	return true
+}
+
+func AllLanguages() []Lang {
+	return []Lang{Lang_en, Lang_zh}
+}

+ 18 - 0
common/global.go

@@ -0,0 +1,18 @@
+package common
+
+import "os"
+
+var (
+	Module  = ""
+	Desc    = ""
+	Version = "latest"
+	Build   = "current"
+
+	Hostname string
+
+	SambaPassword = ""
+)
+
+func init() {
+	Hostname, _ = os.Hostname()
+}

+ 100 - 0
common/rescode.go

@@ -0,0 +1,100 @@
+package common
+
+import (
+	"fmt"
+)
+
+import (
+	"github.com/gin-gonic/gin"
+	ut "github.com/go-playground/universal-translator"
+)
+
+const OKCode = "0x000000"
+const InvalidTokenCode = "0x010101"
+const InvalidRoleCode = "0x010102"
+
+type ResDesc string
+
+var (
+	// 全局
+	Success           ResDesc = "Success"
+	InvalidTokenError ResDesc = "InvalidTokenError"
+	InvalidRoleError  ResDesc = "InvalidRoleError"
+	InvalidParamError ResDesc = "InvalidParamError"
+	NotExistsError    ResDesc = "NotExistsError"
+	NotSupportError   ResDesc = "NotSupportError"
+	NoAuthError       ResDesc = "NoAuthError"
+	ExistsError       ResDesc = "ExistsError"
+	NotAvailableError ResDesc = "NotAvailableError"
+	UnknownError      ResDesc = "UnknownError"
+
+	// 业务 错误
+	InvalidIdError ResDesc = "InvalidIdError"
+	FieldTypeError ResDesc = "FieldTypeError"
+)
+
+var (
+	OK           = &ResStatus{Code: OKCode, Description: Success, Solution: ""}
+	InvalidToken = &ResStatus{Code: InvalidTokenCode, Description: InvalidTokenError, Solution: "请先登录"}
+	InvalidRole  = &ResStatus{Code: InvalidRoleCode, Description: InvalidRoleError, Solution: ""}
+
+	InvalidParam = &ResStatus{Code: "0x010103", Description: InvalidParamError, Solution: ""}
+	NotExists    = &ResStatus{Code: "0x010104", Description: NotExistsError, Solution: ""}
+	NotSupport   = &ResStatus{Code: "0x010105", Description: NotSupportError, Solution: ""}
+	NoAuth       = &ResStatus{Code: "0x010106", Description: NoAuthError, Solution: ""}
+	Exists       = &ResStatus{Code: "0x010107", Description: ExistsError, Solution: ""}
+	NotAvailable = &ResStatus{Code: "0x010108", Description: NotAvailableError, Solution: ""}
+
+	InvalidUsernameOrPasswd = &ResStatus{Code: "0x010201", Description: "InvalidUsernameOrPasswdError", Solution: ""}
+
+	Unknown = &ResStatus{Code: "0x999999", Description: UnknownError, Solution: "服务异常,请联系管理员"}
+)
+
+func helper(code string, msg ResDesc) *ResStatus {
+	return &ResStatus{
+		Code:        code,
+		Description: msg,
+		Solution:    "",
+	}
+}
+
+type ResStatus struct {
+	Code        string        `json:"code"`
+	Description ResDesc       `json:"description"`
+	Params      []interface{} `json:"-"`
+	Solution    string        `json:"solution"`
+}
+
+func (p *ResStatus) Error() string {
+	return string(p.Description)
+}
+
+func (p *ResStatus) Desc(msg ResDesc, params ...interface{}) *ResStatus {
+	return &ResStatus{p.Code, msg, params, p.Solution}
+}
+
+func (p *ResStatus) SetParam(params ...interface{}) *ResStatus {
+	return &ResStatus{p.Code, p.Description, params, p.Solution}
+}
+
+func (p *ResStatus) Translator(trans ut.Translator) *ResStatus {
+	params := []string{}
+	if len(p.Params) > 0 {
+		for _, param := range p.Params {
+			params = append(params, fmt.Sprintf("%v", param))
+		}
+	}
+	t, err := trans.T(string(p.Description), params...)
+	if err != nil {
+		fmt.Printf("translator %s err: %v\n", p.Description, err)
+		t = string(p.Description)
+	}
+	return &ResStatus{p.Code, ResDesc(t), p.Params, p.Solution}
+}
+
+func (p *ResStatus) H(data ...interface{}) gin.H {
+	if len(data) > 0 {
+		return gin.H{"status": helper(p.Code, p.Description), "data": data[0]}
+	}
+	return gin.H{"status": helper(p.Code, p.Description), "data": gin.H{}}
+}

+ 63 - 0
common/response.go

@@ -0,0 +1,63 @@
+package common
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"log/slog"
+	"net/http"
+	"reflect"
+	"runtime/debug"
+)
+
+import (
+	"gorm.io/gorm"
+)
+
+import (
+	"github.com/gin-gonic/gin"
+	"github.com/go-playground/validator/v10"
+)
+
+func HttpSuccess(c *gin.Context, data ...interface{}) {
+	if len(data) > 0 {
+		c.JSON(http.StatusOK, OK.H(data[0]))
+	} else {
+		c.JSON(http.StatusOK, OK.H())
+	}
+}
+
+func ErrToH(err interface{}) gin.H {
+	h := gin.H{}
+
+	switch err.(type) {
+	case *ResStatus:
+		h = err.(*ResStatus).H()
+	case *json.UnmarshalTypeError:
+		utErr := err.(*json.UnmarshalTypeError)
+		h = InvalidParam.Desc(FieldTypeError, utErr.Field, utErr.Type.String()).H()
+	case validator.ValidationErrors:
+		for _, fieldError := range err.(validator.ValidationErrors) {
+			return InvalidParam.Desc(ResDesc(fieldError.Error())).H()
+		}
+	default:
+		if fmt.Sprintf("%v", err) == "EOF" {
+			return InvalidParam.Desc("empty body").H()
+		}
+		if fmt.Sprintf("%v", err) == "unexpected EOF" {
+			return InvalidParam.Desc("body must be in json format").H()
+		}
+		if errors.Is(err.(error), gorm.ErrRecordNotFound) {
+			return NotExists.H()
+		}
+		slog.Error("uncaught error occurred", "type", reflect.TypeOf(err), "err", err, "stack", string(debug.Stack()))
+		h = Unknown.Desc(ResDesc(fmt.Sprintf("%v", err))).H()
+	}
+	return h
+}
+
+func HttpErr(c *gin.Context, err interface{}) {
+	h := ErrToH(err)
+	slog.Warn("http err response", "method", c.Request.Method, "uri", c.Request.RequestURI, "msg", h)
+	c.JSON(http.StatusOK, h)
+}

+ 66 - 0
common/utils.go

@@ -0,0 +1,66 @@
+package common
+
+import (
+	"crypto/md5"
+	"encoding/hex"
+	"fmt"
+	"time"
+)
+
+import (
+	"github.com/golang-jwt/jwt/v5"
+	"github.com/spf13/cast"
+)
+
+var CstSh, _ = time.LoadLocation("Asia/Shanghai")
+
+const (
+	LocateDateFormat  = "2006-01-02"
+	LocateTimeFormat  = "2006-01-02 15:04:05"
+	LocateMilliFormat = "2006-01-02 15:04:05.9999"
+)
+
+func Date() string {
+	return time.Now().In(CstSh).Format(LocateDateFormat)
+}
+
+func Now() string {
+	return time.Now().In(CstSh).Format(LocateTimeFormat)
+}
+
+func NowMilli() string {
+	return time.Now().In(CstSh).Format(LocateMilliFormat)
+}
+
+func NewToken(userId uint, name string) (string, int64, error) {
+	expire := time.Now().AddDate(0, 1, 0)
+	hmacSampleSecret := []byte(BasicConfig.Jwt)
+	token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
+		"id":   userId,
+		"name": name,
+		"exp":  expire.Unix(),
+	})
+	str, err := token.SignedString(hmacSampleSecret)
+	return str, expire.Unix(), err
+}
+
+func ParseToken(tokenString string) (uint, string, error) {
+	token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
+		if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
+			return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
+		}
+		return []byte(BasicConfig.Jwt), nil
+	})
+	if err != nil {
+		return 0, "", err
+	}
+	claims := token.Claims.(jwt.MapClaims)
+	return cast.ToUint(claims["id"]), claims["name"].(string), nil
+}
+
+func MD5(v []byte) string {
+	h := md5.New()
+	h.Write(v)
+	re := h.Sum(nil)
+	return hex.EncodeToString(re)
+}

+ 31 - 0
config/config.toml.template

@@ -0,0 +1,31 @@
+host = "0.0.0.0"
+port = 6001
+mode = "debug" # 模式 debug | test | release
+
+[basic]
+jwt = "BOLQ3HoltjaQqAgWXAG6UXnq2OWGefzqYGwyiYJjAVmuDNyJAOZaFqK8cgQsUrhDA5WDVFuk0mqDRHAK"
+
+[logger]
+logDir = "./logs/dr.log" # 日志存储目录
+logLevel = "debug" # 日志等级:debug; info; warn; error; fatal;
+maxSize = 100 # 在进行切割之前,日志文件的最大大小(以MB为单位)
+maxAge = 90 # 保留旧文件的最大天数
+compress = false # 是否压缩/归档旧文件
+stdout = true
+
+[robot]
+info = ""
+dev = ""
+prod = ""
+
+# 数据库配置
+[postgres]
+ip = "127.0.0.1"
+port = 5432
+username = "mytest"
+password = "123456"
+name = "mytestdatabase"
+
+[metadata]
+# 支持的语言,可选项:"zh"
+languages = ["zh"]

+ 25 - 0
dto/config.go

@@ -0,0 +1,25 @@
+package dto
+
+type ConfigResp struct {
+	Key         string `json:"key"`
+	Value       string `json:"value"`
+	OptionKey   string `json:"option_key"`
+	ValueType   string `json:"value_type"`
+	Description string `json:"description"`
+	Order       int    `json:"order"`
+	IsEnabled   bool   `json:"is_enabled"`
+	Uri         string `json:"uri"`
+}
+
+type ConfigOptionResp struct {
+	Flag   string `json:"flag"`
+	Text   string `json:"text"`
+	Value  string `json:"value"`
+	Order  int    `json:"order"`
+	Enable bool   `json:"enable"`
+}
+
+type ConfigItemReq struct {
+	Title   string   `json:"title"`
+	Columns []string `json:"columns"`
+}

+ 9 - 0
dto/user.go

@@ -0,0 +1,9 @@
+package dto
+
+type UserInfoResp struct {
+	Token  string `json:"token"`
+	Expire int64  `json:"expire"`
+	Uid    uint   `json:"uid"`
+	Name   string `json:"name"`
+	Avatar string `json:"avatar"`
+}

+ 67 - 0
go.mod

@@ -0,0 +1,67 @@
+module resource-server
+
+go 1.24
+
+require (
+	github.com/gin-gonic/gin v1.10.0
+	github.com/go-playground/universal-translator v0.18.1
+	github.com/go-playground/validator/v10 v10.26.0
+	github.com/golang-jwt/jwt/v5 v5.2.2
+	github.com/spf13/cast v1.7.1
+	github.com/spf13/viper v1.20.1
+	golang.org/x/crypto v0.38.0
+	gopkg.in/natefinch/lumberjack.v2 v2.2.1
+	gorm.io/driver/postgres v1.5.11
+	gorm.io/gorm v1.26.1
+)
+
+require (
+	filippo.io/edwards25519 v1.1.0 // indirect
+	github.com/bytedance/sonic v1.13.2 // indirect
+	github.com/bytedance/sonic/loader v0.2.4 // indirect
+	github.com/cloudwego/base64x v0.1.5 // indirect
+	github.com/cloudwego/iasm v0.2.0 // indirect
+	github.com/fsnotify/fsnotify v1.9.0 // indirect
+	github.com/gabriel-vasile/mimetype v1.4.9 // indirect
+	github.com/gin-contrib/sse v1.1.0 // indirect
+	github.com/go-playground/locales v0.14.1 // indirect
+	github.com/go-sql-driver/mysql v1.9.2 // indirect
+	github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
+	github.com/goccy/go-json v0.10.5 // indirect
+	github.com/google/uuid v1.6.0 // indirect
+	github.com/inconshreveable/mousetrap v1.1.0 // indirect
+	github.com/jackc/pgpassfile v1.0.0 // indirect
+	github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
+	github.com/jackc/pgx/v5 v5.7.5 // indirect
+	github.com/jackc/puddle/v2 v2.2.2 // indirect
+	github.com/jinzhu/inflection v1.0.0 // indirect
+	github.com/jinzhu/now v1.1.5 // indirect
+	github.com/json-iterator/go v1.1.12 // indirect
+	github.com/klauspost/cpuid/v2 v2.2.10 // indirect
+	github.com/leodido/go-urn v1.4.0 // indirect
+	github.com/mattn/go-isatty v0.0.20 // indirect
+	github.com/mattn/go-sqlite3 v1.14.28 // indirect
+	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+	github.com/modern-go/reflect2 v1.0.2 // indirect
+	github.com/pelletier/go-toml/v2 v2.2.4 // indirect
+	github.com/sagikazarmark/locafero v0.9.0 // indirect
+	github.com/sourcegraph/conc v0.3.0 // indirect
+	github.com/spf13/afero v1.14.0 // indirect
+	github.com/spf13/cobra v1.9.1 // indirect
+	github.com/spf13/pflag v1.0.6 // indirect
+	github.com/subosito/gotenv v1.6.0 // indirect
+	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
+	github.com/ugorji/go/codec v1.2.12 // indirect
+	go.uber.org/atomic v1.11.0 // indirect
+	go.uber.org/multierr v1.11.0 // indirect
+	golang.org/x/arch v0.16.0 // indirect
+	golang.org/x/net v0.39.0 // indirect
+	golang.org/x/sync v0.14.0 // indirect
+	golang.org/x/sys v0.33.0 // indirect
+	golang.org/x/text v0.25.0 // indirect
+	google.golang.org/protobuf v1.36.6 // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect
+	gorm.io/datatypes v1.2.5 // indirect
+	gorm.io/driver/mysql v1.5.7 // indirect
+	gorm.io/driver/sqlite v1.5.7 // indirect
+)

+ 203 - 0
go.sum

@@ -0,0 +1,203 @@
+filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
+filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
+github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
+github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
+github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
+github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
+github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
+github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
+github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
+github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
+github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
+github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
+github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
+github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
+github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
+github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
+github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
+github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
+github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
+github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
+github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
+github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
+github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
+github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
+github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
+github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
+github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU=
+github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
+github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
+github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
+github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
+github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
+github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
+github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
+github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
+github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
+github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
+github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA=
+github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
+github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
+github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
+github.com/jackc/pgx/v5 v5.3.0/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8=
+github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
+github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
+github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs=
+github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
+github.com/jackc/puddle/v2 v2.2.0/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
+github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
+github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
+github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
+github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
+github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
+github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
+github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
+github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
+github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
+github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
+github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
+github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
+github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
+github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k=
+github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk=
+github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
+github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
+github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
+github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
+github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
+github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
+github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
+github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
+github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
+github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
+github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
+github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
+github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
+github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
+github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
+github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
+go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
+go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
+go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
+golang.org/x/arch v0.16.0 h1:foMtLTdyOmIniqWCHjY6+JxuC54XP1fDwx4N0ASyW+U=
+golang.org/x/arch v0.16.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
+golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
+golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
+golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
+golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
+golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
+golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
+golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
+golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
+golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
+golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
+golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
+golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
+golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
+google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
+gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gorm.io/datatypes v1.2.5 h1:9UogU3jkydFVW1bIVVeoYsTpLRgwDVW3rHfJG6/Ek9I=
+gorm.io/datatypes v1.2.5/go.mod h1:I5FUdlKpLb5PMqeMQhm30CQ6jXP8Rj89xkTeCSAaAD4=
+gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
+gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
+gorm.io/driver/postgres v1.5.0 h1:u2FXTy14l45qc3UeCJ7QaAXZmZfDDv0YrthvmRq1l0U=
+gorm.io/driver/postgres v1.5.0/go.mod h1:FUZXzO+5Uqg5zzwzv4KK49R8lvGIyscBOqYrtI1Ce9A=
+gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314=
+gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
+gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I=
+gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
+gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
+gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
+gorm.io/gorm v1.26.0 h1:9lqQVPG5aNNS6AyHdRiwScAVnXHg/L/Srzx55G5fOgs=
+gorm.io/gorm v1.26.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
+gorm.io/gorm v1.26.1 h1:ghB2gUI9FkS46luZtn6DLZ0f6ooBJ5IbVej2ENFDjRw=
+gorm.io/gorm v1.26.1/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
+nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
+rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

+ 103 - 0
logger/gorm.go

@@ -0,0 +1,103 @@
+package logger
+
+import (
+	"context"
+	"errors"
+	"log/slog"
+	"regexp"
+	"strings"
+	"time"
+)
+
+import (
+	"gorm.io/gorm"
+	gormlogger "gorm.io/gorm/logger"
+)
+
+var (
+	pbkdf2Re *regexp.Regexp
+)
+
+func init() {
+	pbkdf2Re, _ = regexp.Compile(`'pbkdf2:\S+?'`)
+}
+
+func desensitize(str string) string {
+	fs := pbkdf2Re.FindAllString(str, -1)
+	if len(fs) > 0 {
+		for _, f := range fs {
+			str = strings.Replace(str, f, "'******'", -1)
+		}
+	}
+	return str
+}
+
+type GormLogger struct {
+	Logger                    *slog.Logger
+	LogLevel                  gormlogger.LogLevel
+	SlowThreshold             time.Duration
+	SkipCallerLookup          bool
+	IgnoreRecordNotFoundError bool
+}
+
+func NewGormLogger(logger2 *slog.Logger) *GormLogger {
+	return &GormLogger{
+		Logger:                    logger2,
+		LogLevel:                  gormlogger.Info,
+		SlowThreshold:             100 * time.Millisecond,
+		SkipCallerLookup:          false,
+		IgnoreRecordNotFoundError: true,
+	}
+}
+
+func (l GormLogger) SetAsDefault() {
+	gormlogger.Default = l
+}
+
+func (l GormLogger) LogMode(level gormlogger.LogLevel) gormlogger.Interface {
+	return GormLogger{
+		SlowThreshold:             l.SlowThreshold,
+		LogLevel:                  level,
+		SkipCallerLookup:          l.SkipCallerLookup,
+		IgnoreRecordNotFoundError: l.IgnoreRecordNotFoundError,
+	}
+}
+
+func (l GormLogger) Info(ctx context.Context, str string, args ...interface{}) {
+	if l.LogLevel < gormlogger.Info {
+		return
+	}
+	logger.Info(str, slog.Any("data", args))
+}
+
+func (l GormLogger) Warn(ctx context.Context, str string, args ...interface{}) {
+	if l.LogLevel < gormlogger.Warn {
+		return
+	}
+	logger.Warn(str, slog.Any("data", args))
+}
+
+func (l GormLogger) Error(ctx context.Context, str string, args ...interface{}) {
+	if l.LogLevel < gormlogger.Error {
+		return
+	}
+	logger.Error(str, slog.Any("data", args))
+}
+
+func (l GormLogger) Trace(ctx context.Context, begin time.Time, fc func() (string, int64), err error) {
+	if l.LogLevel <= 0 {
+		return
+	}
+	elapsed := time.Since(begin)
+	switch {
+	case err != nil && l.LogLevel >= gormlogger.Error && (!l.IgnoreRecordNotFoundError || !errors.Is(err, gorm.ErrRecordNotFound)):
+		sql, rows := fc()
+		logger.Error("gorm trace error", "err", err, "elapsed", elapsed, "rows", rows, "sql", desensitize(sql))
+	case l.SlowThreshold != 0 && elapsed > l.SlowThreshold && l.LogLevel >= gormlogger.Warn:
+		sql, rows := fc()
+		logger.Warn("gorm trace warn", "elapsed", elapsed, "rows", rows, "sql", desensitize(sql))
+	case l.LogLevel >= gormlogger.Info:
+		sql, rows := fc()
+		logger.Info("gorm trace info", "elapsed", elapsed, "rows", rows, "sql", desensitize(sql))
+	}
+}

+ 73 - 0
logger/logger.go

@@ -0,0 +1,73 @@
+package logger
+
+import (
+	"io"
+	"log/slog"
+	"os"
+)
+
+import (
+	"gopkg.in/natefinch/lumberjack.v2"
+)
+
+import (
+	"resource-server/common"
+)
+
+var lumberJackLogger *lumberjack.Logger
+var logger *slog.Logger
+
+func SetupLogger(persisted bool) {
+	//设置日志级别
+	var level = new(slog.LevelVar)
+	switch common.LoggerConfig.LogLevel {
+	case "DEBUG":
+		level.Set(slog.LevelDebug)
+	case "INFO":
+		level.Set(slog.LevelInfo)
+	case "WARN":
+		level.Set(slog.LevelWarn)
+	case "ERROR":
+		level.Set(slog.LevelError)
+	default:
+		level.Set(slog.LevelInfo)
+	}
+	opts := &slog.HandlerOptions{
+		Level:     level,
+		AddSource: true,
+	}
+
+	lumberJackLogger = &lumberjack.Logger{
+		Filename:  common.LoggerConfig.LogDir,   //日志文件的位置
+		MaxSize:   common.LoggerConfig.MaxSize,  //在进行切割之前,日志文件的最大大小(以MB为单位)
+		MaxAge:    common.LoggerConfig.MaxAge,   //保留旧文件的最大天数
+		Compress:  common.LoggerConfig.Compress, //是否压缩/归档旧文件
+		LocalTime: true,
+	}
+
+	if persisted {
+		if common.LoggerConfig.Stdout {
+			logger = slog.New(slog.NewTextHandler(io.MultiWriter(lumberJackLogger, os.Stdout), opts))
+		} else {
+			logger = slog.New(slog.NewTextHandler(lumberJackLogger, opts))
+		}
+	} else {
+		logger = slog.New(slog.NewTextHandler(os.Stdout, opts))
+	}
+	slog.SetDefault(logger)
+	//todo 输出到robot
+
+	slog.Info("setup logger ok", "level", level.String())
+}
+
+func ShutdownLogger() {
+	lumberJackLogger.Rotate()
+}
+
+func WithGroup(name string) *slog.Logger {
+	return logger.WithGroup(name)
+}
+
+func With(args ...any) *slog.Logger {
+	return logger.With(args)
+}

+ 9 - 0
main.go

@@ -0,0 +1,9 @@
+package main
+
+import (
+	"resource-server/cmd"
+)
+
+func main() {
+	cmd.Execute()
+}

+ 51 - 0
models/model.go

@@ -0,0 +1,51 @@
+package models
+
+import (
+	"time"
+)
+
+import (
+	"golang.org/x/crypto/bcrypt"
+	"gorm.io/gorm"
+)
+
+type User struct {
+	gorm.Model
+	OpenID        string `gorm:"uniqueIndex"`
+	OrgID         uint   `gorm:"index"`
+	Name          string
+	Avatar        string
+	Password      string `json:"-"`
+	Mobile        string `json:"mobile" gorm:"type:varchar(15);unique_index"`
+	Email         string `json:"email"  gorm:"type:varchar(255);unique_index"`
+	LastLoginTime time.Time
+}
+
+func (u *User) GenerateFromPassword(password []byte) error {
+	hashPassword, err := bcrypt.GenerateFromPassword(password, bcrypt.DefaultCost)
+	if err != nil {
+		return err
+	}
+	u.Password = string(hashPassword)
+	return nil
+}
+
+func (u *User) CompareHashAndPassword(password []byte) bool {
+	if err := bcrypt.CompareHashAndPassword([]byte(u.Password), password); err != nil {
+		return false
+	}
+	return true
+}
+
+type ConfigItem struct {
+	gorm.Model
+	Key          string
+	Value        string
+	OptionKey    string
+	ValueType    string
+	Description  string
+	Order        int
+	IsEnabled    bool
+	Uri          string
+	DefaultValue string
+}

+ 34 - 0
models/scope.go

@@ -0,0 +1,34 @@
+package models
+
+import (
+	"gorm.io/gorm"
+)
+
+func Paginate(pageNum, pageSize int) func(db *gorm.DB) *gorm.DB {
+	return func(db *gorm.DB) *gorm.DB {
+		offset := (pageNum - 1) * pageSize
+		return db.Offset(offset).Limit(pageSize)
+	}
+}
+
+func Query(query string, args ...interface{}) func(db *gorm.DB) *gorm.DB {
+	implement := false
+	for _, arg := range args {
+		switch arg.(type) {
+		case string:
+			if len(arg.(string)) > 0 {
+				implement = true
+			}
+		default:
+			implement = true
+		}
+	}
+	if implement {
+		return func(db *gorm.DB) *gorm.DB {
+			return db.Where(query, args...)
+		}
+	}
+	return func(db *gorm.DB) *gorm.DB {
+		return db
+	}
+}

+ 99 - 0
models/setup.go

@@ -0,0 +1,99 @@
+package models
+
+import (
+	"errors"
+	"fmt"
+	"log/slog"
+)
+
+import (
+	"gorm.io/driver/postgres"
+	"gorm.io/gorm"
+)
+
+import (
+	"resource-server/common"
+	"resource-server/logger"
+)
+
+var (
+	DB  *gorm.DB
+	err error
+)
+
+func panicHelper(err error) {
+	if err != nil {
+		panic(err)
+	}
+}
+
+func migrate() {
+	panicHelper(DB.AutoMigrate(&User{}))
+	var user User
+	err := DB.First(&user).Error
+	if errors.Is(err, gorm.ErrRecordNotFound) {
+		newAdmin := User{Name: "admin"}
+		newAdmin.GenerateFromPassword([]byte("123456"))
+		DB.Create(&newAdmin)
+	}
+
+	panicHelper(DB.AutoMigrate(&ConfigItem{}))
+	var count int64
+	DB.Model(&ConfigItem{}).Count(&count)
+	if count == 0 {
+		configItem := ConfigItem{Key: "TimeFormat", Value: "HH:mm:ss", OptionKey: "TimeFormat", ValueType: "string", Description: "", Order: 1, IsEnabled: true, Uri: "System/TimeFormat", DefaultValue: ""}
+		DB.Create(&configItem)
+		configItem = ConfigItem{Key: "DateFormat", Value: "MM/dd/yyyy", OptionKey: "DateFormat", ValueType: "string", Description: "", Order: 1, IsEnabled: true, Uri: "System/DateFormat", DefaultValue: ""}
+		DB.Create(&configItem)
+		configItem = ConfigItem{Key: "DoseUnit", Value: "mGy", OptionKey: "DoseUnit", ValueType: "string", Description: "", Order: 1, IsEnabled: true, Uri: "System/DoseUnit", DefaultValue: ""}
+		DB.Create(&configItem)
+		configItem = ConfigItem{Key: "WeightUnit", Value: "kg", OptionKey: "WeightUnit", ValueType: "string", Description: "", Order: 1, IsEnabled: true, Uri: "System/WeightUnit", DefaultValue: ""}
+		DB.Create(&configItem)
+		configItem = ConfigItem{Key: "LengthUnit", Value: "cm", OptionKey: "LengthUnit", ValueType: "string", Description: "", Order: 1, IsEnabled: true, Uri: "System/LengthUnit", DefaultValue: ""}
+		DB.Create(&configItem)
+		configItem = ConfigItem{Key: "DefaultImageFontSize", Value: "100", OptionKey: "DefaultFontSize", ValueType: "int", Description: "", Order: 1, IsEnabled: true, Uri: "ImageProcess/DefaultImageFontSize", DefaultValue: ""}
+		DB.Create(&configItem)
+		configItem = ConfigItem{Key: "DefaultImageFontFamily", Value: "Arial Unicode MS", OptionKey: "DefaultFontFamily", ValueType: "string", Description: "", Order: 1, IsEnabled: true, Uri: "ImageProcess/DefaultImageFontFamily", DefaultValue: ""}
+		DB.Create(&configItem)
+		configItem = ConfigItem{Key: "TagFontSize", Value: "100", OptionKey: "FontSize", ValueType: "int", Description: "", Order: 1, IsEnabled: true, Uri: "ImageProcess/TagFontSize", DefaultValue: ""}
+		DB.Create(&configItem)
+		configItem = ConfigItem{Key: "TagFontFamily", Value: "Arial", OptionKey: "FontFamily", ValueType: "string", Description: "", Order: 1, IsEnabled: true, Uri: "ImageProcess/TagFontFamily", DefaultValue: ""}
+		DB.Create(&configItem)
+		configItem = ConfigItem{Key: "HidePatientNameDelimiters", Value: "1", OptionKey: "", ValueType: "bool", Description: "", Order: 1, IsEnabled: true, Uri: "Patient/HidePatientNameDelimiters", DefaultValue: ""}
+		DB.Create(&configItem)
+		configItem = ConfigItem{Key: "FirstNameFirstDisplay", Value: "1", OptionKey: "", ValueType: "bool", Description: "", Order: 1, IsEnabled: true, Uri: "Patient/FirstNameFirstDisplay", DefaultValue: ""}
+		DB.Create(&configItem)
+		configItem = ConfigItem{Key: "LRDisplayMode", Value: "L/R", OptionKey: "LRDisplayMode", ValueType: "string", Description: "", Order: 1, IsEnabled: true, Uri: "ImageProcess/LRDisplayMode", DefaultValue: ""}
+		DB.Create(&configItem)
+		configItem = ConfigItem{Key: "PrintTagFontLevel", Value: "6", OptionKey: "", ValueType: "int", Description: "", Order: 1, IsEnabled: true, Uri: "ImageProcess/PrintTagFontLevel", DefaultValue: ""}
+		DB.Create(&configItem)
+		configItem = ConfigItem{Key: "PrintTagFontFamily", Value: "Arial", OptionKey: "", ValueType: "string", Description: "", Order: 1, IsEnabled: true, Uri: "ImageProcess/PrintTagFontFamily", DefaultValue: ""}
+		DB.Create(&configItem)
+	}
+}
+
+func SetupGorm(m bool) {
+	slog.Info("setup gorm")
+
+	dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%d sslmode=disable TimeZone=Asia/Shanghai",
+		common.PostgresConfig.Ip,
+		common.PostgresConfig.Username,
+		common.PostgresConfig.Password,
+		common.PostgresConfig.Name,
+		common.PostgresConfig.Port,
+	)
+	if common.LoggerConfig.LogLevel == "debug" {
+		DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{
+			Logger: logger.NewGormLogger(logger.WithGroup("gorm")),
+		})
+	} else {
+		DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
+	}
+	if err != nil {
+		panic(err)
+	}
+	if m {
+		migrate()
+	}
+	slog.Info("setup gorm ok")
+}

+ 122 - 0
router/middleware.go

@@ -0,0 +1,122 @@
+package router
+
+import (
+	"fmt"
+	"log/slog"
+	"net/http"
+	"runtime/debug"
+	"strings"
+	"time"
+)
+
+import (
+	"github.com/gin-gonic/gin"
+)
+
+import (
+	"resource-server/common"
+	"resource-server/logger"
+)
+
+func InitMiddleware(r *gin.Engine) {
+	// NoCache is a middleware function that appends headers
+	r.Use(NoCache)
+	// 跨域处理
+	r.Use(Options)
+	// Secure is a middleware function that appends security
+	r.Use(Secure)
+	// Use Slog Logger
+	r.Use(GinLogger(logger.WithGroup("gin")))
+	// Global Recover
+	r.Use(GinRecovery(logger.WithGroup("ginRecovery")))
+	// check token
+	r.Use(CheckAuth)
+}
+
+// NoCache is a middleware function that appends headers
+// to prevent the client from caching the HTTP response.
+func NoCache(c *gin.Context) {
+	c.Header("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate, value")
+	c.Header("Expires", "Thu, 01 Jan 1970 00:00:00 GMT")
+	c.Header("Last-Modified", time.Now().UTC().Format(http.TimeFormat))
+	c.Next()
+}
+
+// Options is a middleware function that appends headers
+// for options requests and aborts then exits the middleware
+// chain and ends the request.
+func Options(c *gin.Context) {
+	if c.Request.Method != "OPTIONS" {
+		c.Next()
+	} else {
+		c.Header("Access-Control-Allow-Origin", "*")
+		c.Header("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS")
+		c.Header("Access-Control-Allow-Headers", "authorization, origin, content-type, accept")
+		c.Header("Allow", "HEAD,GET,POST,PUT,PATCH,DELETE,OPTIONS")
+		c.Header("Content-Type", "application/json")
+		c.AbortWithStatus(200)
+	}
+}
+
+// Secure is a middleware function that appends security
+// and resource access headers.
+func Secure(c *gin.Context) {
+	c.Header("Access-Control-Allow-Origin", "*")
+	//c.Header("X-Frame-Options", "DENY")
+	c.Header("X-Content-Type-Options", "nosniff")
+	c.Header("X-XSS-Protection", "1; mode=block")
+	if c.Request.TLS != nil {
+		c.Header("Strict-Transport-Security", "max-age=31536000")
+	}
+
+	// Also consider adding Content-Security-Policy headers
+	// c.Header("Content-Security-Policy", "script-src 'self' https://cdnjs.cloudflare.com")
+}
+
+func CheckAuth(c *gin.Context) {
+	//if common.Hostname == "DESKTOP-2VF4H05" {
+	//	c.Set("uid", cast.ToUint(1))
+	//	c.Set("username", "dev")
+	//	c.Next()
+	//	return
+	//}
+	if strings.HasPrefix(c.FullPath(), "/dr/api/v1/auth") {
+		token := c.Request.Header.Get("Authorization")
+		uid, username, err := common.ParseToken(strings.TrimPrefix(token, "Bearer "))
+		if err != nil {
+			c.AbortWithStatusJSON(200, common.ErrToH(common.InvalidToken))
+		}
+		c.Set("uid", uid)
+		c.Set("username", username)
+	}
+	c.Next()
+}
+
+func GinLogger(logger *slog.Logger) gin.HandlerFunc {
+	return func(c *gin.Context) {
+		start := time.Now()
+		path := c.Request.URL.Path
+		if len(c.Request.URL.RawQuery) > 0 {
+			path += "?" + c.Request.URL.RawQuery
+		}
+		c.Next()
+
+		cost := time.Since(start)
+		logger.Info(fmt.Sprintf("[%s]%s, ip[%s], resp[%d] %s errors[%s]",
+			c.Request.Method,
+			path,
+			c.ClientIP(),
+			c.Writer.Status(),
+			cost.String(),
+			c.Errors.ByType(gin.ErrorTypePrivate).String(),
+		))
+	}
+}
+
+func GinRecovery(logger *slog.Logger) gin.HandlerFunc {
+	return gin.CustomRecovery(func(c *gin.Context, recovered interface{}) {
+		logger.Error("Recovery from panic", "recoverd", recovered, "stack", string(debug.Stack()))
+		common.HttpErr(c, common.Unknown)
+		c.AbortWithStatus(http.StatusOK)
+	})
+}

+ 34 - 0
router/router.go

@@ -0,0 +1,34 @@
+package router
+
+import (
+	"github.com/gin-gonic/gin"
+	apiv1 "resource-server/api/v1"
+)
+
+func InitRouter() *gin.Engine {
+
+	r := gin.New()
+
+	InitMiddleware(r)
+
+	r.StaticFile("/dr/", "./dist/index.html")
+	r.Static("/dr/front/", "./dist/")
+
+	// 注册路由
+	v1 := r.Group("/dr/api/v1/")
+	pubV1 := v1.Group("/pub")
+	{
+		pubV1.GET("/ping", apiv1.Ping)
+		pubV1.POST("login", apiv1.Login)
+	}
+	authV1 := v1.Group("/auth")
+	{
+		configV1 := authV1.Group("/configs")
+		{
+			configV1.GET("items", apiv1.GetConfigs)
+			configV1.GET("options", apiv1.GetConfigOptions)
+			configV1.POST("items", apiv1.UpdateConfigItems)
+		}
+	}
+	return r
+}

+ 68 - 0
service/config.go

@@ -0,0 +1,68 @@
+package service
+
+import (
+	"resource-server/dto"
+	"resource-server/models"
+)
+
+func GetConfigList() ([]*dto.ConfigResp, error) {
+	res := []*dto.ConfigResp{}
+	var items []*models.ConfigItem
+	query := models.DB.Model(&models.ConfigItem{})
+	err := query.Order("id").Find(&items).Error
+	if err != nil {
+		return res, err
+	}
+	for _, c := range items {
+		res = append(res, &dto.ConfigResp{
+			Key:         c.Key,
+			Value:       c.Value,
+			OptionKey:   c.OptionKey,
+			ValueType:   c.ValueType,
+			Description: c.Description,
+			Order:       c.Order,
+			IsEnabled:   c.IsEnabled,
+			Uri:         c.Uri,
+		})
+	}
+	return res, nil
+}
+
+func GetConfigOptionList(flag string, enable bool) ([]*dto.ConfigOptionResp, error) {
+	res := []*dto.ConfigOptionResp{}
+	switch flag {
+	case "TimeFormat":
+		res = append(res, &dto.ConfigOptionResp{Flag: "TimeFormat", Text: "HH:mm:ss", Value: "HH:mm:ss", Order: 1, Enable: true})
+		res = append(res, &dto.ConfigOptionResp{Flag: "TimeFormat", Text: "HH:mm", Value: "HH:mm", Order: 2, Enable: true})
+		res = append(res, &dto.ConfigOptionResp{Flag: "TimeFormat", Text: "HH-mm-ss", Value: "HH-mm-ss", Order: 3, Enable: true})
+		res = append(res, &dto.ConfigOptionResp{Flag: "TimeFormat", Text: "HHmmss", Value: "HHmmss", Order: 4, Enable: true})
+		res = append(res, &dto.ConfigOptionResp{Flag: "TimeFormat", Text: "hh:mm:ss tt", Value: "hh:mm:ss tt", Order: 5, Enable: true})
+		res = append(res, &dto.ConfigOptionResp{Flag: "TimeFormat", Text: "h:mm:ss tt", Value: "h:mm:ss tt", Order: 6, Enable: true})
+	case "DateFormat":
+		res = append(res, &dto.ConfigOptionResp{Flag: "DateFormat", Text: "yyyy.MM.dd", Value: "yyyy.MM.dd", Order: 1, Enable: true})
+		res = append(res, &dto.ConfigOptionResp{Flag: "DateFormat", Text: "yyyy/M/d", Value: "yyyy/M/d", Order: 2, Enable: true})
+		res = append(res, &dto.ConfigOptionResp{Flag: "DateFormat", Text: "M-d-yyyy", Value: "M-d-yyyy", Order: 3, Enable: true})
+		res = append(res, &dto.ConfigOptionResp{Flag: "DateFormat", Text: "yyyy/M/dd", Value: "yyyy/M/dd", Order: 4, Enable: true})
+		res = append(res, &dto.ConfigOptionResp{Flag: "DateFormat", Text: "yyyy-MM-dd", Value: "yyyy-MM-dd", Order: 5, Enable: true})
+		res = append(res, &dto.ConfigOptionResp{Flag: "DateFormat", Text: "M-dd-yyyy", Value: "M-dd-yyyy", Order: 6, Enable: true})
+		res = append(res, &dto.ConfigOptionResp{Flag: "DateFormat", Text: "yyyy.M.dd", Value: "yyyy.M.dd", Order: 7, Enable: true})
+		res = append(res, &dto.ConfigOptionResp{Flag: "DateFormat", Text: "yyyy/MM/dd", Value: "yyyy/MM/dd", Order: 8, Enable: true})
+		res = append(res, &dto.ConfigOptionResp{Flag: "DateFormat", Text: "MMM-dd-yyyy", Value: "MMM-dd-yyyy", Order: 9, Enable: true})
+		res = append(res, &dto.ConfigOptionResp{Flag: "DateFormat", Text: "yyyy-MMM-dd", Value: "yyyy-MMM-dd", Order: 10, Enable: true})
+		res = append(res, &dto.ConfigOptionResp{Flag: "DateFormat", Text: "dd-MMM-yyyy", Value: "dd-MMM-yyyy", Order: 11, Enable: true})
+		res = append(res, &dto.ConfigOptionResp{Flag: "DateFormat", Text: "dd.MM.yyyy", Value: "dd.MM.yyyy", Order: 12, Enable: true})
+	case "DoseUnit":
+		res = append(res, &dto.ConfigOptionResp{Flag: "DoseUnit", Text: "rad", Value: "rad", Order: 1, Enable: true})
+		res = append(res, &dto.ConfigOptionResp{Flag: "DoseUnit", Text: "mGy", Value: "mGy", Order: 2, Enable: true})
+	case "WeightUnit":
+		res = append(res, &dto.ConfigOptionResp{Flag: "WeightUnit", Text: "kg", Value: "kg", Order: 1, Enable: true})
+		res = append(res, &dto.ConfigOptionResp{Flag: "WeightUnit", Text: "pound", Value: "pound", Order: 2, Enable: true})
+	case "LengthUnit":
+		res = append(res, &dto.ConfigOptionResp{Flag: "LengthUnit", Text: "cm", Value: "cm", Order: 1, Enable: true})
+		res = append(res, &dto.ConfigOptionResp{Flag: "LengthUnit", Text: "inch", Value: "inch", Order: 2, Enable: true})
+	case "FontFamily":
+		res = append(res, &dto.ConfigOptionResp{Flag: "FontFamily", Text: "Arial Unicode MS", Value: "Arial Unicode MS", Order: 1, Enable: true})
+		res = append(res, &dto.ConfigOptionResp{Flag: "FontFamily", Text: "Arial", Value: "Arial", Order: 2, Enable: true})
+	}
+	return res, nil
+}

+ 35 - 0
service/user.go

@@ -0,0 +1,35 @@
+package service
+
+import (
+	"resource-server/common"
+	"resource-server/dto"
+	"resource-server/models"
+	"time"
+)
+
+func Login(username string, password string) (*dto.UserInfoResp, error) {
+	var user models.User
+	err := models.DB.Model(&models.User{}).First(&user, "name = ?", username).Error
+	if err != nil {
+		return nil, err
+	}
+	if ok := user.CompareHashAndPassword([]byte(password)); !ok {
+		return nil, common.InvalidUsernameOrPasswd
+	}
+	user.LastLoginTime = time.Now()
+	err = models.DB.Save(&user).Error
+	if err != nil {
+		return nil, err
+	}
+	token, expire, err := common.NewToken(user.ID, user.Name)
+	if err != nil {
+		return nil, err
+	}
+	return &dto.UserInfoResp{
+		Token:  token,
+		Expire: expire,
+		Uid:    user.ID,
+		Name:   user.Name,
+		Avatar: user.Avatar,
+	}, nil
+}