Browse Source

patient_type

shao 1 week ago
parent
commit
40568c68e2

+ 3 - 0
.gitignore

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

+ 4 - 0
.gitmodules

@@ -0,0 +1,4 @@
+[submodule "rpc_idl"]
+	path = rpc_idl
+	url = http://code.pacsonline.cn/DR/rpc_idl.git
+	branch = v1

+ 17 - 0
Makefile

@@ -0,0 +1,17 @@
+BINARY=protocol-server
+
+MODULE=protocol-server
+DESC=Protocol Server repo
+VERSION=`git describe --tags --long`
+BUILD=`date +%F\ %T`
+
+LDFLAGS=-ldflags "-X 'protocol-server/common.Module=${MODULE}' -X 'protocol-server/common.Desc=${DESC}' -X 'protocol-server/common.Version=${VERSION}' -X 'protocol-server/common.Build=${BUILD}'"
+
+build:
+	go build ${LDFLAGS} -o ${BINARY}
+
+install:
+	go install ${LDFLAGS}
+
+clean:
+	if [ -f ${BINARY} ] ; then rm ${BINARY}; fi

+ 32 - 0
README.md

@@ -0,0 +1,32 @@
+DR Protocol Server
+
+#### env
+- golang 1.24
+- postgres14
+
+#### run
+```shell
+go run main.go run
+```
+
+#### deploy
+```shell
+# vim /etc/systemd/system/protocol-server.service
+[Unit]
+Description=dr protocol service
+After=network.target
+ 
+[Service]
+Type=simple
+WorkingDirectory=xxx/DRProtocolServer
+ExecStart=xxx/DRProtocolServer/protocol-server run
+ 
+[Install]
+WantedBy=multi-user.target
+
+# 重新加载systemd配置
+sudo systemctl daemon-load
+
+# 启动服务
+sudo systemctl restart protocol-server.service
+```

+ 37 - 0
cmd/cobra.go

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

+ 49 - 0
cmd/crypto/crypto.go

@@ -0,0 +1,49 @@
+package crypto
+
+import (
+	"fmt"
+)
+
+import (
+	"github.com/spf13/cobra"
+)
+
+import (
+	"protocol-server/common"
+)
+
+var (
+	orig     string
+	StartCmd = &cobra.Command{
+		Use:     "crypto",
+		Short:   "crypto",
+		Example: "protocol-server crypto -o 123456",
+		PreRun: func(cmd *cobra.Command, args []string) {
+
+		},
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return run()
+		},
+	}
+)
+
+func init() {
+	StartCmd.Flags().StringVarP(&orig, "orig", "o", "", "orig")
+}
+
+func run() error {
+	ciphertext, err := common.DBPwdEncrypt([]byte(orig))
+	if err != nil {
+		fmt.Println("Error encrypting:", err)
+		return err
+	}
+	fmt.Printf("Ciphertext: %x\n", ciphertext)
+
+	decryptedtext, err := common.DBPwdDecrypt(ciphertext)
+	if err != nil {
+		fmt.Println("Error decrypting:", err)
+		return err
+	}
+	fmt.Printf("Decryptedtext: %s\n", string(decryptedtext))
+	return nil
+}

+ 104 - 0
cmd/server/server.go

@@ -0,0 +1,104 @@
+package server
+
+import (
+	"fmt"
+	"log/slog"
+	"net"
+	"os"
+	"os/signal"
+	"strings"
+)
+
+import (
+	"github.com/spf13/cobra"
+	"google.golang.org/grpc"
+	"google.golang.org/grpc/health"
+	"google.golang.org/grpc/health/grpc_health_v1"
+)
+
+import (
+	"protocol-server/common"
+	"protocol-server/logger"
+	"protocol-server/models"
+	pb "protocol-server/rpc_idl/dr_protocol_pb"
+	"protocol-server/service"
+)
+
+var (
+	configFolder string
+	addr         string
+	mode         string
+	StartCmd     = &cobra.Command{
+		Use:          "run",
+		Short:        "Start API server",
+		Example:      "protocol-server run -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().StringVarP(&addr, "addr", "a", "", "Tcp server listening on")
+	StartCmd.Flags().StringVarP(&mode, "mode", "m", "debug", "server mode ; eg:debug,test,release")
+}
+
+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, addr, mode)
+	//2. 设置日志
+	logger.SetupLogger(true)
+	//3. 初始化gorm
+	models.SetupGorm(true)
+	//4. 初始化i18n
+	common.SetupTrans()
+
+	slog.Info(`starting server`, "pid", os.Getpid())
+}
+
+func run() error {
+	slog.Info(fmt.Sprintf("common.ServerConfig.Protocol %v", common.ServerConfig.Protocol))
+	lis, err := net.Listen("tcp", common.ServerConfig.Protocol)
+	if err != nil {
+		slog.Error("failed to listen", "err", err.Error())
+		return err
+	}
+	s := grpc.NewServer()
+	grpc_health_v1.RegisterHealthServer(s, health.NewServer())
+	pb.RegisterBasicServer(s, &service.BasicServer{})
+	pb.RegisterProtocolServer(s, &service.ProtocolServer{})
+
+	slog.Info("module: " + common.Module)
+	slog.Info("desc: " + common.Desc)
+	slog.Info("version: " + common.Version)
+	slog.Info("build: " + common.Build)
+	slog.Info(fmt.Sprintf("server run at: %s", common.Now()))
+	slog.Info(fmt.Sprintf("server listening at %v", lis.Addr()))
+	slog.Info("Enter Control + C Shutdown Server \n")
+	if err := s.Serve(lis); err != nil {
+		slog.Error("failed to serve", "err", err.Error())
+		return err
+	}
+
+	quit := make(chan os.Signal)
+	signal.Notify(quit, os.Interrupt)
+	<-quit
+	fmt.Printf("%s Shutdown Server ... \n", common.Now())
+
+	s.GracefulStop()
+	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 (
+	"protocol-server/common"
+)
+
+var (
+	StartCmd = &cobra.Command{
+		Use:     "version",
+		Short:   "Get version info",
+		Example: "protocol-server 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
+}

+ 217 - 0
common/config.go

@@ -0,0 +1,217 @@
+package common
+
+import (
+	"encoding/hex"
+	"fmt"
+)
+
+import (
+	"github.com/spf13/viper"
+)
+
+//配置文件
+
+var (
+	BasicConfig    = new(basic)
+	ServerConfig   = new(server)
+	LoggerConfig   = new(logger)
+	RobotConfig    = new(robot)
+	PostgresConfig = new(postgres)
+	MetadataConfig = new(metadata)
+)
+
+// 应用配置
+type server struct {
+	Auth     string
+	Resource string
+	Study    string
+	Protocol string
+}
+
+type basic struct {
+	Jwt  string
+	Mode string
+}
+
+// 日志配置
+type logger struct {
+	LogDir     string //日志文件夹路径
+	LogLevel   string //日志打印等级
+	MaxSize    int    //在进行切割之前,日志文件的最大大小(以MB为单位)
+	MaxBackups int    //最多保留文件个数
+	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
+}
+
+func (p *postgres) setup() {
+	raw, err := hex.DecodeString(p.Password)
+	if err != nil {
+		panic("db password is invalid")
+	}
+	pwd, err := DBPwdDecrypt(raw)
+	if err != nil {
+		panic("db password is invalid")
+	}
+	p.Password = string(pwd)
+}
+
+// metadata配置
+type metadata struct {
+	Locales   []string
+	Languages []Lang
+	Product   Product
+	Sources   []Source
+}
+
+func (p *metadata) setup() {
+	if !ValidLanguages(p.Languages) {
+		panic(fmt.Sprintf("invalid languages, optional values are: %v", AllLanguages()))
+	}
+	if !ValidProduct(p.Product) {
+		panic(fmt.Sprintf("invalid product, optional values are: %v", AllProducts()))
+	}
+	if !ValidSources(p.Sources) {
+		panic(fmt.Sprintf("invalid sources, optional values are: %v", AllSources()))
+	}
+}
+
+func (p *metadata) GetLanguages() []Lang {
+	if len(p.Languages) == 0 {
+		return AllLanguages()
+	}
+	return p.Languages
+}
+
+func (p *metadata) GetProduct() Product {
+	return p.Product
+}
+
+func (p *metadata) GetSources() []Source {
+	if len(p.Sources) == 0 {
+		return AllSources()
+	}
+	return p.Sources
+}
+
+func InitBasic(cfg *viper.Viper) *basic {
+	var basic basic
+	err := cfg.Unmarshal(&basic)
+	if err != nil {
+		panic("InitBasic err")
+	}
+	return &basic
+}
+
+func InitServer(cfg *viper.Viper) *server {
+	var server server
+	err := cfg.Unmarshal(&server)
+	if err != nil {
+		panic("InitServer err")
+	}
+	return &server
+}
+
+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")
+	}
+
+	postgres.setup()
+	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, addr string, mode string) {
+	viper.SetConfigFile(path)
+	if err := viper.ReadInConfig(); err != nil {
+		panic(err)
+	}
+
+	cfgBasic := viper.Sub("basic")
+	if cfgBasic == nil {
+		panic("No found basic in the configuration")
+	}
+	BasicConfig = InitBasic(cfgBasic)
+
+	cfgServer := viper.Sub("server")
+	if cfgServer == nil {
+		panic("No found server in the configuration")
+	}
+	ServerConfig = InitServer(cfgServer)
+
+	BasicConfig.Mode = mode
+	if addr != "" {
+		ServerConfig.Protocol = addr
+	}
+
+	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)
+}

+ 95 - 0
common/enum.go

@@ -0,0 +1,95 @@
+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"
+	default:
+		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}
+}
+
+type Source string
+
+const (
+	SOURCE_ELECTRON Source = "ELectron"
+	SOURCE_BROWSER  Source = "Browser"
+	SOURCE_ANDROID  Source = "Android"
+	SOURCE_DEV      Source = "Dev"
+)
+
+func (p Source) ToString() string {
+	switch p {
+	case SOURCE_ELECTRON:
+		return "ELectron"
+	case SOURCE_BROWSER:
+		return "Browser"
+	case SOURCE_ANDROID:
+		return "Android"
+	case SOURCE_DEV:
+		return "Dev"
+	default:
+		return ""
+	}
+}
+
+func ValidSources(sources []Source) bool {
+	for _, source := range sources {
+		if source.ToString() == "" {
+			return false
+		}
+	}
+	return true
+}
+
+func AllSources() []Source {
+	return []Source{SOURCE_ELECTRON, SOURCE_BROWSER, SOURCE_ANDROID, SOURCE_DEV}
+}
+
+type Product string
+
+const (
+	PRODUCT_DROC Product = "DROC"
+	PRODUCT_VET  Product = "VETDROC"
+)
+
+func (p Product) ToString() string {
+	switch p {
+	case PRODUCT_DROC:
+		return "DROC"
+	case PRODUCT_VET:
+		return "VETDROC"
+	default:
+		return ""
+	}
+}
+
+func ValidProduct(product Product) bool {
+	return product.ToString() != ""
+}
+
+func AllProducts() []Product {
+	return []Product{PRODUCT_DROC, PRODUCT_VET}
+}

+ 16 - 0
common/global.go

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

+ 31 - 0
common/translator.go

@@ -0,0 +1,31 @@
+package common
+
+import (
+	"log/slog"
+)
+
+import (
+	"github.com/nicksnyder/go-i18n/v2/i18n"
+	"github.com/pelletier/go-toml/v2"
+	"golang.org/x/text/language"
+)
+
+var bundle *i18n.Bundle
+
+func SetupTrans() {
+	slog.Info("setup trans")
+	bundle = i18n.NewBundle(language.English)
+	bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
+	for _, filename := range MetadataConfig.Locales {
+		slog.Info("loading locale", "filename", filename)
+		_, err := bundle.LoadMessageFile(filename)
+		if err != nil {
+			panic(err)
+		}
+	}
+	slog.Info("setup trans ok")
+}
+
+func GetTrans(lang string) *i18n.Localizer {
+	return i18n.NewLocalizer(bundle, lang)
+}

+ 98 - 0
common/utils.go

@@ -0,0 +1,98 @@
+package common
+
+import (
+	"context"
+	"crypto/aes"
+	"crypto/cipher"
+	"crypto/md5"
+	"encoding/hex"
+	"log/slog"
+	"time"
+)
+
+import (
+	md "google.golang.org/grpc/metadata"
+)
+
+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 MD5(v []byte) string {
+	h := md5.New()
+	h.Write(v)
+	re := h.Sum(nil)
+	return hex.EncodeToString(re)
+}
+
+func GetHeader(ctx context.Context) (Product, Source, Lang) {
+	m, ok := md.FromIncomingContext(ctx)
+	if ok {
+		slog.Debug("metadata.FromIncomingContext", "md", m)
+	}
+	return Product(m.Get("product")[0]), Source(m.Get("source")[0]), Lang(m.Get("language")[0])
+}
+
+var dbPwKey = []byte("X3O6wVF&6*&lSVk0*504V~q7>\"k]6S'*") // 32 bytes for AES-256
+var dbPwNonceHex = "1962a6f6f9999447632c8a34"
+
+func EncryptGCM(key []byte, nonce []byte, plaintext []byte) ([]byte, error) {
+	block, err := aes.NewCipher(key)
+	if err != nil {
+		return nil, err
+	}
+
+	gcm, err := cipher.NewGCM(block)
+	if err != nil {
+		return nil, err
+	}
+
+	ciphertext := gcm.Seal(nil, nonce, plaintext, nonce)
+
+	return ciphertext, nil
+}
+
+func DecryptGCM(key []byte, nonce []byte, ciphertext []byte) ([]byte, error) {
+	block, err := aes.NewCipher(key)
+	if err != nil {
+		return nil, err
+	}
+
+	gcm, err := cipher.NewGCM(block)
+	if err != nil {
+		return nil, err
+	}
+
+	plaintext, err := gcm.Open(nil, nonce, ciphertext, nonce)
+	if err != nil {
+		return nil, err
+	}
+
+	return plaintext, nil
+}
+
+func DBPwdEncrypt(ciphertext []byte) ([]byte, error) {
+	nonce, _ := hex.DecodeString(dbPwNonceHex)
+	return EncryptGCM(dbPwKey, nonce, ciphertext)
+}
+
+func DBPwdDecrypt(ciphertext []byte) ([]byte, error) {
+	nonce, _ := hex.DecodeString(dbPwNonceHex)
+	return DecryptGCM(dbPwKey, nonce, ciphertext)
+}

+ 44 - 0
config/config.toml.template

@@ -0,0 +1,44 @@
+[basic]
+jwt = "BOLQ3HoltjaQqAgWXAG6UXnq2OWGefzqYGwyiYJjAVmuDNyJAOZaFqK8cgQsUrhDA5WDVFuk0mqDRHAK"
+mode = "debug" # 模式 debug | test | release
+
+[server]
+auth = "0.0.0.0:6001"
+resource = "localhost:6102"
+study = "localhost:6103"
+protocol = "localhost:6104"
+dcmtk = "localhost:6199"
+
+[logger]
+logDir = "./logs/" # 日志存储目录
+logLevel = "debug" # 日志等级:debug; info; warn; error; fatal;
+maxSize = 10 # 在进行切割之前,日志文件的最大大小(以MB为单位)
+maxBackups = 100 # 最多保留文件个数
+maxAge = 90 # 保留旧文件的最大天数
+compress = false # 是否压缩/归档旧文件
+stdout = true
+
+[robot]
+info = ""
+dev = ""
+prod = ""
+
+# 数据库配置
+[postgres]
+ip = "127.0.0.1"
+port = 5432
+username = "mytest"
+password = "24a4e49b95adb006fbadd7c5a74dbb2ee4d67d61abab"
+name = "mytestdatabase"
+
+[metadata]
+locales = [
+    "../locale/common.en.toml",
+    "../locale/common.zh.toml"
+]
+# 支持的语言,可选项:"en","zh"
+languages = ["en", "zh"]
+# 支持的产品,可选项:"DROC","VETDROC"
+product = "DROC"
+# 支持的访问源,可选项:"Electron","Browser","Android"
+sources = ["ELectron", "Browser", "Android"]

+ 41 - 0
go.mod

@@ -0,0 +1,41 @@
+module protocol-server
+
+go 1.24.3
+
+require (
+	github.com/nicksnyder/go-i18n/v2 v2.6.0
+	github.com/pelletier/go-toml/v2 v2.2.4
+	github.com/spf13/cast v1.8.0
+	github.com/spf13/cobra v1.9.1
+	github.com/spf13/viper v1.20.1
+	golang.org/x/text v0.25.0
+	google.golang.org/grpc v1.72.2
+	google.golang.org/protobuf v1.36.6
+	gopkg.in/natefinch/lumberjack.v2 v2.2.1
+	gorm.io/driver/postgres v1.5.11
+	gorm.io/gorm v1.30.0
+)
+
+require (
+	github.com/fsnotify/fsnotify v1.9.0 // indirect
+	github.com/go-viper/mapstructure/v2 v2.2.1 // 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/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/pflag v1.0.6 // indirect
+	github.com/subosito/gotenv v1.6.0 // indirect
+	go.uber.org/multierr v1.11.0 // indirect
+	golang.org/x/crypto v0.38.0 // indirect
+	golang.org/x/net v0.40.0 // indirect
+	golang.org/x/sync v0.14.0 // indirect
+	golang.org/x/sys v0.33.0 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect
+)

+ 112 - 0
go.sum

@@ -0,0 +1,112 @@
+github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
+github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
+github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
+github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
+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/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
+github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+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/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+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-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
+github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
+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.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/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/nicksnyder/go-i18n/v2 v2.6.0 h1:C/m2NNWNiTB6SK4Ao8df5EWm3JETSTIGNXBpMJTxzxQ=
+github.com/nicksnyder/go-i18n/v2 v2.6.0/go.mod h1:88sRqr0C6OPyJn0/KRNaEz1uWorjxIKP7rUUcvycecE=
+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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
+github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
+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.8.0 h1:gEN9K4b8Xws4EX0+a0reLmhq8moKn7ntRlQYgjPeCDk=
+github.com/spf13/cast v1.8.0/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/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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
+github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
+go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
+go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
+go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
+go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
+go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
+go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
+go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
+go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
+go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
+go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
+go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
+go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
+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/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
+golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
+golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
+golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
+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.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
+golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
+golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 h1:cJfm9zPbe1e873mHJzmQ1nwVEeRDU/T1wXDK2kUSU34=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
+google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8=
+google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
+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-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+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/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314=
+gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
+gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs=
+gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=

+ 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))
+	}
+}

+ 86 - 0
logger/logger.go

@@ -0,0 +1,86 @@
+package logger
+
+import (
+	"io"
+	"log/slog"
+	"os"
+	"strings"
+)
+
+import (
+	"gopkg.in/natefinch/lumberjack.v2"
+)
+
+import (
+	"protocol-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,
+	}
+
+	filename := common.LoggerConfig.LogDir + "/protocol.log"
+	if strings.HasSuffix(common.LoggerConfig.LogDir, "/") {
+		filename = common.LoggerConfig.LogDir + "protocol.log"
+	}
+	lumberJackLogger = &lumberjack.Logger{
+		Filename:   filename,                       //日志文件的位置
+		MaxSize:    common.LoggerConfig.MaxSize,    //在进行切割之前,日志文件的最大大小(以MB为单位)
+		MaxBackups: common.LoggerConfig.MaxBackups, //最多保留文件个数
+		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(),
+		"filename", filename,
+		"max size", common.LoggerConfig.MaxSize,
+		"max backups", common.LoggerConfig.MaxBackups,
+		"max age", common.LoggerConfig.MaxAge,
+		"compress", common.LoggerConfig.Compress,
+	)
+}
+
+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 (
+	"protocol-server/cmd"
+)
+
+func main() {
+	cmd.Execute()
+}

+ 53 - 0
models/initdata.go

@@ -0,0 +1,53 @@
+package models
+
+import (
+	"bufio"
+	"fmt"
+	"io/fs"
+	"log/slog"
+	"strings"
+)
+
+import (
+	"gorm.io/gorm"
+)
+
+import (
+	"protocol-server/static"
+)
+
+func initTable(sqlFilePath string) error {
+	slog.Info("initTable ...", "file", sqlFilePath)
+	file, err := static.GetSqlFile(sqlFilePath)
+	if err != nil {
+		return fmt.Errorf("error opening SQL file: %w", err)
+	}
+	defer func(file fs.File) {
+		err := file.Close()
+		if err != nil {
+			slog.Error("error closing SQL file:", err)
+		}
+	}(file)
+
+	scanner := bufio.NewScanner(file)
+	err = DB.Transaction(func(tx *gorm.DB) error {
+		for scanner.Scan() {
+			line := strings.TrimSpace(scanner.Text())
+			result := tx.Exec(line)
+			if result.Error != nil {
+				return fmt.Errorf("error executing INSERT statement: %s\tError: %w", line, result.Error)
+			}
+		}
+
+		if err := scanner.Err(); err != nil {
+			return fmt.Errorf("error reading SQL file: %w", err)
+		}
+		return nil
+	})
+	if err != nil {
+		return err
+	}
+
+	slog.Info("Processed SQL file: %s.\n", sqlFilePath)
+	return nil
+}

+ 69 - 0
models/model.go

@@ -0,0 +1,69 @@
+package models
+
+import (
+	"gorm.io/gorm"
+)
+
+type PatientType struct {
+	gorm.Model
+	PatientTypeID          string
+	PatientTypeName        string
+	PatientTypeLocal       string
+	PatientTypeDescription string
+	Sort                   int32
+	IsEnabled              bool
+	Product                string
+	IsPreInstall           bool
+}
+
+func (PatientType) TableName() string {
+	return "p_patient_type"
+}
+
+type BodyPart struct {
+	gorm.Model
+	BodyPartID          string
+	BodyPartName        string
+	BodyPartLocal       string
+	BodyPartDescription string
+	PatientType         string
+	Category            string
+	Sort                int32
+	IsEnabled           bool
+	Product             string
+	IsPreInstall        bool
+}
+
+func (BodyPart) TableName() string {
+	return "p_body_part"
+}
+
+type Procedure struct {
+	gorm.Model
+	ProcedureID               string  `gorm:"uniqueIndex"`
+	ProcedureCode             string  //程序代码
+	ProcedureName             string  //程序名称
+	ProcedureOtherName        string  //程序别名
+	ProcedureDescription      string  //程序描述
+	ProtocolCode              string  //协议代码
+	PatientType               string  //病人类型
+	ProcedureGroupID          string  //程序组ID
+	ProcedureGroupDescription string  //程序组描述
+	ProcedureType             string  //程序类型
+	FastSearch                bool    //是否快速搜索
+	ProtocolLaterality        string  //协议体位
+	PrintStyleID              string  //打印样式ID
+	ProcedureCategory         string  //程序类别
+	Modality                  string  //设备类型
+	ConfigObjectValue         string  //配置对象值
+	MagFactor                 float32 //放大倍数
+	ClinicProtocol            bool    //是否临床协议
+	Sort                      int32
+	IsEnabled                 bool
+	Product                   string
+	IsPreInstall              bool
+}
+
+func (Procedure) TableName() string {
+	return "p_procedure"
+}

+ 45 - 0
models/scope.go

@@ -0,0 +1,45 @@
+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
+	}
+}
+
+func String(query string, arg string) func(db *gorm.DB) *gorm.DB {
+	if len(arg) > 0 {
+		return func(db *gorm.DB) *gorm.DB {
+			return db.Where(query, arg)
+		}
+	}
+	return func(db *gorm.DB) *gorm.DB {
+		return db
+	}
+}

+ 68 - 0
models/setup.go

@@ -0,0 +1,68 @@
+package models
+
+import (
+	"fmt"
+	"log/slog"
+)
+
+import (
+	"gorm.io/driver/postgres"
+	"gorm.io/gorm"
+)
+
+import (
+	"protocol-server/common"
+	"protocol-server/logger"
+)
+
+var (
+	DB  *gorm.DB
+	err error
+)
+
+func panicHelper(err error) {
+	if err != nil {
+		panic(err)
+	}
+}
+
+func migrate() {
+	panicHelper(DB.AutoMigrate(&PatientType{}))
+	var count int64
+	DB.Model(&BodyPart{}).Count(&count)
+	if count == 0 {
+		panicHelper(initTable("p_patient_type_202505291630.sql"))
+	}
+	panicHelper(DB.AutoMigrate(&BodyPart{}))
+	DB.Model(&BodyPart{}).Count(&count)
+	if count == 0 {
+		panicHelper(initTable("p_body_part_202505291606.sql"))
+	}
+	panicHelper(DB.AutoMigrate(&Procedure{}))
+}
+
+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")
+}

+ 1 - 0
rpc_idl

@@ -0,0 +1 @@
+Subproject commit fd1299aaac1e201fdd1a946f14143d93f04e4c14

+ 25 - 0
service/basic.go

@@ -0,0 +1,25 @@
+package service
+
+import (
+	"context"
+	"log/slog"
+)
+
+import (
+	"protocol-server/common"
+	pb "protocol-server/rpc_idl/dr_protocol_pb"
+)
+
+type BasicServer struct {
+	pb.UnimplementedBasicServer
+}
+
+func (s *BasicServer) SoftwareInfo(_ context.Context, in *pb.EmptyRequest) (*pb.SoftwareInfoReply, error) {
+	slog.Info("Received SoftwareInfo")
+	return &pb.SoftwareInfoReply{
+		Module:  common.Module,
+		Desc:    common.Desc,
+		Build:   common.Build,
+		Version: common.Version,
+	}, nil
+}

+ 32 - 0
service/protocol.go

@@ -0,0 +1,32 @@
+package service
+
+import (
+	"context"
+	"google.golang.org/grpc/codes"
+	"google.golang.org/grpc/status"
+	"protocol-server/common"
+	"protocol-server/models"
+)
+
+import (
+	pb "protocol-server/rpc_idl/dr_protocol_pb"
+)
+
+type ProtocolServer struct {
+	pb.UnimplementedProtocolServer
+}
+
+func (s *ProtocolServer) GetPatientType(ctx context.Context, in *pb.PatientTypeRequest) (*pb.PatientTypeReply, error) {
+	product, _, _ := common.GetHeader(ctx)
+	res := pb.PatientTypeReply{}
+	err := models.DB.Model(&models.PatientType{}).Scopes(
+		models.String("product = ?", product.ToString()),
+	).Order("sort").Find(&res.PatientTypeList).Error
+	if err != nil {
+		return nil, status.Errorf(codes.Internal, "patient_type find err %v", err)
+	}
+	return &res, nil
+}
+func (s *ProtocolServer) GetBodyPart(ctx context.Context, in *pb.BodyPartRequest) (*pb.BodyPartReply, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method GetBodyPart not implemented")
+}

+ 30 - 0
static/basic.go

@@ -0,0 +1,30 @@
+package static
+
+import (
+	"embed"
+	"errors"
+	"io/fs"
+)
+
+//go:embed sqls
+var sqlFiles embed.FS
+
+func GetSqlFile(filename string) (fs.File, error) {
+	localeEntries, err := sqlFiles.ReadDir("sqls")
+	if err != nil {
+		return nil, err
+	}
+	for _, le := range localeEntries {
+		if le.IsDir() {
+		} else {
+			if le.Name() == filename {
+				f, err := sqlFiles.Open("sqls/" + le.Name())
+				if err != nil {
+					return nil, err
+				}
+				return f, nil
+			}
+		}
+	}
+	return nil, errors.New("sql file not exist, " + filename)
+}

+ 12 - 0
static/locale/common.en.toml

@@ -0,0 +1,12 @@
+ErrCode_Success = "Success"
+ErrCode_InvalidTokenError = "Invalid Token"
+ErrCode_InvalidRoleError = "Invalid Role"
+ErrCode_InvalidParamError = "Invalid Param"
+ErrCode_NotExistsError = "Not Exists"
+ErrCode_NotSupportError = "Not Support"
+ErrCode_NoAuthError = "No Auth"
+ErrCode_ExistsError = "Already Exists"
+ErrCode_NotAvailableError = "Not Available"
+ErrCode_UnknownError = "Service exception, please contact the administrator"
+
+ErrCode_InvalidUsernameOrPasswdError = "Invalid username or password"

+ 12 - 0
static/locale/common.zh.toml

@@ -0,0 +1,12 @@
+ErrCode_Success = "成功"
+ErrCode_InvalidTokenError = "无效的凭证"
+ErrCode_InvalidRoleError = "无效的角色"
+ErrCode_InvalidParamError = "无效的参数"
+ErrCode_NotExistsError = "不存在"
+ErrCode_NotSupportError = "不支持"
+ErrCode_NoAuthError = "无权限"
+ErrCode_ExistsError = "已存在"
+ErrCode_NotAvailableError = "不可用"
+ErrCode_UnknownError = "服务异常,请联系管理员"
+
+ErrCode_InvalidUsernameOrPasswdError = "无效的用户名或密码"

+ 48 - 0
static/sqls/p_body_part_202505291606.sql

@@ -0,0 +1,48 @@
+INSERT INTO public.p_body_part (body_part_id,body_part_name,body_part_local,body_part_description,patient_type,category,sort,is_enabled,product,is_pre_install) VALUES ('Human_SKULL','颅骨',NULL,'Skull','Human','DX',1,true,'DROC',true);
+INSERT INTO public.p_body_part (body_part_id,body_part_name,body_part_local,body_part_description,patient_type,category,sort,is_enabled,product,is_pre_install) VALUES ('Human_NECK','颈部',NULL,'Neck','Human','DX',2,true,'DROC',true);
+INSERT INTO public.p_body_part (body_part_id,body_part_name,body_part_local,body_part_description,patient_type,category,sort,is_enabled,product,is_pre_install) VALUES ('Human_SHOULDER','肩关节',NULL,'Shoulder','Human','DX',3,true,'DROC',true);
+INSERT INTO public.p_body_part (body_part_id,body_part_name,body_part_local,body_part_description,patient_type,category,sort,is_enabled,product,is_pre_install) VALUES ('Human_SPINE','脊柱',NULL,'Spine','Human','DX',4,true,'DROC',true);
+INSERT INTO public.p_body_part (body_part_id,body_part_name,body_part_local,body_part_description,patient_type,category,sort,is_enabled,product,is_pre_install) VALUES ('Human_THORAX','胸部',NULL,'Thorax','Human','DX',5,true,'DROC',true);
+INSERT INTO public.p_body_part (body_part_id,body_part_name,body_part_local,body_part_description,patient_type,category,sort,is_enabled,product,is_pre_install) VALUES ('Human_ABDOMEN','腹部',NULL,'Abdomen','Human','DX',6,true,'DROC',true);
+INSERT INTO public.p_body_part (body_part_id,body_part_name,body_part_local,body_part_description,patient_type,category,sort,is_enabled,product,is_pre_install) VALUES ('Human_PELVIS','骨盆',NULL,'Pelvis','Human','DX',7,true,'DROC',true);
+INSERT INTO public.p_body_part (body_part_id,body_part_name,body_part_local,body_part_description,patient_type,category,sort,is_enabled,product,is_pre_install) VALUES ('Human_UPPER LIMB','上肢',NULL,'UpperLimb','Human','DX',8,true,'DROC',true);
+INSERT INTO public.p_body_part (body_part_id,body_part_name,body_part_local,body_part_description,patient_type,category,sort,is_enabled,product,is_pre_install) VALUES ('Human_HAND','手部',NULL,'Hand','Human','DX',9,true,'DROC',true);
+INSERT INTO public.p_body_part (body_part_id,body_part_name,body_part_local,body_part_description,patient_type,category,sort,is_enabled,product,is_pre_install) VALUES ('Human_LOWER LIMB','下肢',NULL,'LowerLimb','Human','DX',10,true,'DROC',true);
+INSERT INTO public.p_body_part (body_part_id,body_part_name,body_part_local,body_part_description,patient_type,category,sort,is_enabled,product,is_pre_install) VALUES ('Human_FOOT','足部',NULL,'Foot','Human','DX',11,true,'DROC',true);
+INSERT INTO public.p_body_part (body_part_id,body_part_name,body_part_local,body_part_description,patient_type,category,sort,is_enabled,product,is_pre_install) VALUES ('Human_TOMO','TOMO',NULL,'TOMO','Human','DX',12,true,'DROC',true);
+INSERT INTO public.p_body_part (body_part_id,body_part_name,body_part_local,body_part_description,patient_type,category,sort,is_enabled,product,is_pre_install) VALUES ('Human_DUAL ENERGY','双能',NULL,'Human_DUAL ENERGY','Human','DX',13,true,'DROC',true);
+INSERT INTO public.p_body_part (body_part_id,body_part_name,body_part_local,body_part_description,patient_type,category,sort,is_enabled,product,is_pre_install) VALUES ('Human_QC','QC',NULL,'QC','Human','DX',14,true,'DROC',true);
+INSERT INTO public.p_body_part (body_part_id,body_part_name,body_part_local,body_part_description,patient_type,category,sort,is_enabled,product,is_pre_install) VALUES ('Calibration','校准',NULL,'Calibration','SpecialType','DX',15,true,'DROC',true);
+INSERT INTO public.p_body_part (body_part_id,body_part_name,body_part_local,body_part_description,patient_type,category,sort,is_enabled,product,is_pre_install) VALUES ('IQAP','IQAP',NULL,'IQAP','SpecialType','DX',16,true,'DROC',true);
+INSERT INTO public.p_body_part (body_part_id,body_part_name,body_part_local,body_part_description,patient_type,category,sort,is_enabled,product,is_pre_install) VALUES ('ThicknessRange','厚度范围',NULL,'ThicknessRange','SpecialType','DX',17,true,'DROC',true);
+INSERT INTO public.p_body_part (body_part_id,body_part_name,body_part_local,body_part_description,patient_type,category,sort,is_enabled,product,is_pre_install) VALUES ('Cat_Skull','猫 头骨',NULL,'Skull','Cat','DX',1,true,'VETDROC',true);
+INSERT INTO public.p_body_part (body_part_id,body_part_name,body_part_local,body_part_description,patient_type,category,sort,is_enabled,product,is_pre_install) VALUES ('Cat_Spine','猫 脊椎',NULL,'Spine','Cat','DX',2,true,'VETDROC',true);
+INSERT INTO public.p_body_part (body_part_id,body_part_name,body_part_local,body_part_description,patient_type,category,sort,is_enabled,product,is_pre_install) VALUES ('Cat_Thorax','猫 胸部',NULL,'Thorax','Cat','DX',3,true,'VETDROC',true);
+INSERT INTO public.p_body_part (body_part_id,body_part_name,body_part_local,body_part_description,patient_type,category,sort,is_enabled,product,is_pre_install) VALUES ('Cat_FrontExtremities','猫 前肢',NULL,'Forelimb','Cat','DX',4,true,'VETDROC',true);
+INSERT INTO public.p_body_part (body_part_id,body_part_name,body_part_local,body_part_description,patient_type,category,sort,is_enabled,product,is_pre_install) VALUES ('Cat_Abdomen','猫 腹部',NULL,'Abdomen','Cat','DX',5,true,'VETDROC',true);
+INSERT INTO public.p_body_part (body_part_id,body_part_name,body_part_local,body_part_description,patient_type,category,sort,is_enabled,product,is_pre_install) VALUES ('Cat_Hip','猫 臀部',NULL,'Hip','Cat','DX',6,true,'VETDROC',true);
+INSERT INTO public.p_body_part (body_part_id,body_part_name,body_part_local,body_part_description,patient_type,category,sort,is_enabled,product,is_pre_install) VALUES ('Cat_RearExtremities','猫 后肢',NULL,'Hindlimb','Cat','DX',7,true,'VETDROC',true);
+INSERT INTO public.p_body_part (body_part_id,body_part_name,body_part_local,body_part_description,patient_type,category,sort,is_enabled,product,is_pre_install) VALUES ('Dog_Skull','狗 头骨',NULL,'Skull','Dog','DX',8,true,'VETDROC',true);
+INSERT INTO public.p_body_part (body_part_id,body_part_name,body_part_local,body_part_description,patient_type,category,sort,is_enabled,product,is_pre_install) VALUES ('Dog_Spine','狗 脊椎',NULL,'Spine','Dog','DX',9,true,'VETDROC',true);
+INSERT INTO public.p_body_part (body_part_id,body_part_name,body_part_local,body_part_description,patient_type,category,sort,is_enabled,product,is_pre_install) VALUES ('Dog_Thorax','狗 胸部',NULL,'Thorax','Dog','DX',10,true,'VETDROC',true);
+INSERT INTO public.p_body_part (body_part_id,body_part_name,body_part_local,body_part_description,patient_type,category,sort,is_enabled,product,is_pre_install) VALUES ('Dog_FrontExtremities','狗 前肢',NULL,'Forelimb','Dog','DX',11,true,'VETDROC',true);
+INSERT INTO public.p_body_part (body_part_id,body_part_name,body_part_local,body_part_description,patient_type,category,sort,is_enabled,product,is_pre_install) VALUES ('Dog_Abdomen','狗 腹部',NULL,'Abdomen','Dog','DX',12,true,'VETDROC',true);
+INSERT INTO public.p_body_part (body_part_id,body_part_name,body_part_local,body_part_description,patient_type,category,sort,is_enabled,product,is_pre_install) VALUES ('Dog_Hip','狗 臀部',NULL,'Hip','Dog','DX',13,true,'VETDROC',true);
+INSERT INTO public.p_body_part (body_part_id,body_part_name,body_part_local,body_part_description,patient_type,category,sort,is_enabled,product,is_pre_install) VALUES ('Dog_RearExtremities','狗 后肢',NULL,'Hindlimb','Dog','DX',14,true,'VETDROC',true);
+INSERT INTO public.p_body_part (body_part_id,body_part_name,body_part_local,body_part_description,patient_type,category,sort,is_enabled,product,is_pre_install) VALUES ('Equine_Head','马 头',NULL,'Head','Equine','DX',15,true,'VETDROC',true);
+INSERT INTO public.p_body_part (body_part_id,body_part_name,body_part_local,body_part_description,patient_type,category,sort,is_enabled,product,is_pre_install) VALUES ('Equine_Spine','马 脊椎',NULL,'Spine','Equine','DX',16,true,'VETDROC',true);
+INSERT INTO public.p_body_part (body_part_id,body_part_name,body_part_local,body_part_description,patient_type,category,sort,is_enabled,product,is_pre_install) VALUES ('Equine_THORAX','马 胸腔',NULL,'Thorax & Abdomen','Equine','DX',17,true,'VETDROC',true);
+INSERT INTO public.p_body_part (body_part_id,body_part_name,body_part_local,body_part_description,patient_type,category,sort,is_enabled,product,is_pre_install) VALUES ('Equine_Left_Forelimb','马 左前肢',NULL,'Left Forelimb','Equine','DX',18,true,'VETDROC',true);
+INSERT INTO public.p_body_part (body_part_id,body_part_name,body_part_local,body_part_description,patient_type,category,sort,is_enabled,product,is_pre_install) VALUES ('Equine_Right_Forelimb','马 右前肢',NULL,'Right Forelimb','Equine','DX',19,true,'VETDROC',true);
+INSERT INTO public.p_body_part (body_part_id,body_part_name,body_part_local,body_part_description,patient_type,category,sort,is_enabled,product,is_pre_install) VALUES ('Equine_Left_Hindlimb','马 左后肢',NULL,'Left Hindlimb','Equine','DX',20,true,'VETDROC',true);
+INSERT INTO public.p_body_part (body_part_id,body_part_name,body_part_local,body_part_description,patient_type,category,sort,is_enabled,product,is_pre_install) VALUES ('Equine_Right_Hindlimb','马 右后肢',NULL,'Right Hindlimb','Equine','DX',21,true,'VETDROC',true);
+INSERT INTO public.p_body_part (body_part_id,body_part_name,body_part_local,body_part_description,patient_type,category,sort,is_enabled,product,is_pre_install) VALUES ('Exotic_Birds','啮齿类 鸟',NULL,'Avian Species','Birds','DX',22,true,'VETDROC',true);
+INSERT INTO public.p_body_part (body_part_id,body_part_name,body_part_local,body_part_description,patient_type,category,sort,is_enabled,product,is_pre_install) VALUES ('Exotic_Fish','啮齿类 鱼',NULL,'Fish Species','Fish','DX',23,true,'VETDROC',true);
+INSERT INTO public.p_body_part (body_part_id,body_part_name,body_part_local,body_part_description,patient_type,category,sort,is_enabled,product,is_pre_install) VALUES ('Exotic_Gnawer','啮齿类 鼠类',NULL,'Rat & Hamster','Gnawer','DX',24,true,'VETDROC',true);
+INSERT INTO public.p_body_part (body_part_id,body_part_name,body_part_local,body_part_description,patient_type,category,sort,is_enabled,product,is_pre_install) VALUES ('Exotic_Lizard','啮齿类 蜥蜴',NULL,'Lizard Species','Lizard','DX',25,true,'VETDROC',true);
+INSERT INTO public.p_body_part (body_part_id,body_part_name,body_part_local,body_part_description,patient_type,category,sort,is_enabled,product,is_pre_install) VALUES ('Exotic_Rabbit','啮齿类 兔子',NULL,'Rabbit Species','Rabbit','DX',26,true,'VETDROC',true);
+INSERT INTO public.p_body_part (body_part_id,body_part_name,body_part_local,body_part_description,patient_type,category,sort,is_enabled,product,is_pre_install) VALUES ('Exotic_Snake','啮齿类 蛇',NULL,'Snake Species','Snake','DX',27,true,'VETDROC',true);
+INSERT INTO public.p_body_part (body_part_id,body_part_name,body_part_local,body_part_description,patient_type,category,sort,is_enabled,product,is_pre_install) VALUES ('Exotic_Turtle','啮齿类 龟',NULL,'Chelonian Species','Turtle','DX',28,true,'VETDROC',true);
+INSERT INTO public.p_body_part (body_part_id,body_part_name,body_part_local,body_part_description,patient_type,category,sort,is_enabled,product,is_pre_install) VALUES ('Calibration','校准',NULL,'Calibration','Other','DX',29,true,'VETDROC',true);
+INSERT INTO public.p_body_part (body_part_id,body_part_name,body_part_local,body_part_description,patient_type,category,sort,is_enabled,product,is_pre_install) VALUES ('ThicknessRange','厚度范围',NULL,'ThicknessRange','Other','DX',30,true,'VETDROC',true);
+INSERT INTO public.p_body_part (body_part_id,body_part_name,body_part_local,body_part_description,patient_type,category,sort,is_enabled,product,is_pre_install) VALUES ('QC','QC',NULL,'QC','Other','DX',31,true,'VETDROC',true);

+ 13 - 0
static/sqls/p_patient_type_202505291630.sql

@@ -0,0 +1,13 @@
+INSERT INTO public.p_patient_type (patient_type_id,patient_type_name,patient_type_local,patient_type_description,sort,is_enabled,product,is_pre_install) VALUES ('Human','Human','','Human',1,true,'DROC',true);
+INSERT INTO public.p_patient_type (patient_type_id,patient_type_name,patient_type_local,patient_type_description,sort,is_enabled,product,is_pre_install) VALUES ('SpecialType','SpecialType','','SpecialType',2,true,'DROC',true);
+INSERT INTO public.p_patient_type (patient_type_id,patient_type_name,patient_type_local,patient_type_description,sort,is_enabled,product,is_pre_install) VALUES ('Cat','Cat','','Cat',1,true,'VETDROC',true);
+INSERT INTO public.p_patient_type (patient_type_id,patient_type_name,patient_type_local,patient_type_description,sort,is_enabled,product,is_pre_install) VALUES ('Dog','Dog','','Dog',2,true,'VETDROC',true);
+INSERT INTO public.p_patient_type (patient_type_id,patient_type_name,patient_type_local,patient_type_description,sort,is_enabled,product,is_pre_install) VALUES ('Equine','Equine','','Equine',3,true,'VETDROC',true);
+INSERT INTO public.p_patient_type (patient_type_id,patient_type_name,patient_type_local,patient_type_description,sort,is_enabled,product,is_pre_install) VALUES ('Birds','Birds','','Birds',4,true,'VETDROC',true);
+INSERT INTO public.p_patient_type (patient_type_id,patient_type_name,patient_type_local,patient_type_description,sort,is_enabled,product,is_pre_install) VALUES ('Fish','Fish','','Fish',5,false,'VETDROC',true);
+INSERT INTO public.p_patient_type (patient_type_id,patient_type_name,patient_type_local,patient_type_description,sort,is_enabled,product,is_pre_install) VALUES ('Lizard','Lizard','','Lizard',6,true,'VETDROC',true);
+INSERT INTO public.p_patient_type (patient_type_id,patient_type_name,patient_type_local,patient_type_description,sort,is_enabled,product,is_pre_install) VALUES ('Rabbit','Rabbit','','Rabbit',7,true,'VETDROC',true);
+INSERT INTO public.p_patient_type (patient_type_id,patient_type_name,patient_type_local,patient_type_description,sort,is_enabled,product,is_pre_install) VALUES ('Snake','Snake','','Snake',8,true,'VETDROC',true);
+INSERT INTO public.p_patient_type (patient_type_id,patient_type_name,patient_type_local,patient_type_description,sort,is_enabled,product,is_pre_install) VALUES ('Turtle','Turtle','','Turtle',9,true,'VETDROC',true);
+INSERT INTO public.p_patient_type (patient_type_id,patient_type_name,patient_type_local,patient_type_description,sort,is_enabled,product,is_pre_install) VALUES ('Gnawer','Gnawer','','Gnawer',10,true,'VETDROC',true);
+INSERT INTO public.p_patient_type (patient_type_id,patient_type_name,patient_type_local,patient_type_description,sort,is_enabled,product,is_pre_install) VALUES ('Other','Other','','Other',11,false,'VETDROC',true);