update
This commit is contained in:
@@ -1,20 +0,0 @@
|
|||||||
# 单独启动某一个业务(如user,make up-svc svc=user)
|
|
||||||
up-svc:
|
|
||||||
docker compose up -d $(svc)
|
|
||||||
|
|
||||||
# 全局停止所有服务
|
|
||||||
down:
|
|
||||||
docker compose down
|
|
||||||
|
|
||||||
# 清理无用镜像/容器(团队开发环境,定期清理)
|
|
||||||
clean:
|
|
||||||
docker system prune -f
|
|
||||||
|
|
||||||
# 初始化Go工作区(新成员拉取代码后,一键执行)
|
|
||||||
go-init:
|
|
||||||
go work init
|
|
||||||
go work use ./common
|
|
||||||
go work use ./services/user
|
|
||||||
go work use ./services/order
|
|
||||||
go work use ./services/pay
|
|
||||||
go mod tidy
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
// common/db/options.go
|
|
||||||
package db
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
// PostgresOptions PostgreSQL连接配置项
|
|
||||||
// 包含基础连接信息+连接池配置,按需暴露,无冗余
|
|
||||||
type PostgresOptions struct {
|
|
||||||
Host string // 数据库地址
|
|
||||||
Port string // 数据库端口
|
|
||||||
User string // 数据库账号
|
|
||||||
Password string // 数据库密码
|
|
||||||
DBName string // 数据库名
|
|
||||||
SSLMode string // SSL模式(开发环境一般disable,生产可enable)
|
|
||||||
TimeZone string // 时区(如Asia/Shanghai)
|
|
||||||
// 连接池配置
|
|
||||||
MaxOpenConns int // 最大打开连接数
|
|
||||||
MaxIdleConns int // 最大空闲连接数
|
|
||||||
ConnMaxLifetime time.Duration // 连接最大生命周期
|
|
||||||
ConnMaxIdleTime time.Duration // 连接最大空闲时间
|
|
||||||
// GORM配置
|
|
||||||
LogLevel gorm.LogLevel // GORM日志级别(开发:Info,生产:Error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// PostgresOption 选项模式函数类型
|
|
||||||
type PostgresOption func(*PostgresOptions)
|
|
||||||
|
|
||||||
// 初始化默认配置,避免用户传参不全导致连接失败
|
|
||||||
// 开发环境常用默认值,生产环境通过业务层传参覆盖
|
|
||||||
func defaultPostgresOptions() *PostgresOptions {
|
|
||||||
return &PostgresOptions{
|
|
||||||
Host: "postgres", // 默认匹配根目录Compose的服务名,开发环境直接用
|
|
||||||
Port: "5432",
|
|
||||||
SSLMode: "disable",
|
|
||||||
TimeZone: "Asia/Shanghai",
|
|
||||||
MaxOpenConns: 20,
|
|
||||||
MaxIdleConns: 10,
|
|
||||||
ConnMaxLifetime: 30 * time.Minute,
|
|
||||||
ConnMaxIdleTime: 10 * time.Minute,
|
|
||||||
LogLevel: gorm.LogLevelInfo, // 开发环境默认打印Info日志
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 以下为配置项的设置函数,用户可通过链式调用配置
|
|
||||||
// 示例:db.WithHost("192.168.1.100").WithUser("prod_user")
|
|
||||||
|
|
||||||
// WithHost 设置数据库地址
|
|
||||||
func WithHost(host string) PostgresOption {
|
|
||||||
return func(o *PostgresOptions) {
|
|
||||||
o.Host = host
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithPort 设置数据库端口
|
|
||||||
func WithPort(port string) PostgresOption {
|
|
||||||
return func(o *PostgresOptions) {
|
|
||||||
o.Port = port
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithUser 设置数据库账号
|
|
||||||
func WithUser(user string) PostgresOption {
|
|
||||||
return func(o *PostgresOptions) {
|
|
||||||
o.User = user
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithPassword 设置数据库密码
|
|
||||||
func WithPassword(pwd string) PostgresOption {
|
|
||||||
return func(o *PostgresOptions) {
|
|
||||||
o.Password = pwd
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithDBName 设置数据库名
|
|
||||||
func WithDBName(dbName string) PostgresOption {
|
|
||||||
return func(o *PostgresOptions) {
|
|
||||||
o.DBName = dbName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithSSLMode 设置SSL模式
|
|
||||||
func WithSSLMode(mode string) PostgresOption {
|
|
||||||
return func(o *PostgresOptions) {
|
|
||||||
o.SSLMode = mode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithTimeZone 设置时区
|
|
||||||
func WithTimeZone(tz string) PostgresOption {
|
|
||||||
return func(o *PostgresOptions) {
|
|
||||||
o.TimeZone = tz
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithMaxOpenConns 设置最大打开连接数
|
|
||||||
func WithMaxOpenConns(num int) PostgresOption {
|
|
||||||
return func(o *PostgresOptions) {
|
|
||||||
o.MaxOpenConns = num
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithLogLevel 设置GORM日志级别
|
|
||||||
func WithLogLevel(level gorm.LogLevel) PostgresOption {
|
|
||||||
return func(o *PostgresOptions) {
|
|
||||||
o.LogLevel = level
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
// common/db/postgres.go
|
|
||||||
package db
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
"fmt"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"gorm.io/driver/postgres"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"gorm.io/gorm/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 全局单例连接,避免重复创建连接(连接池全局复用)
|
|
||||||
var (
|
|
||||||
pgInstance *gorm.DB
|
|
||||||
pgOnce sync.Once
|
|
||||||
pgErr error
|
|
||||||
)
|
|
||||||
|
|
||||||
// InitPostgres 初始化PostgreSQL连接(单例模式,仅执行一次)
|
|
||||||
// 入参:选项模式的配置函数,用户可灵活配置
|
|
||||||
// 返回:原生*gorm.DB + 错误,业务层可直接使用GORM所有方法
|
|
||||||
func InitPostgres(opts ...PostgresOption) (*gorm.DB, error) {
|
|
||||||
pgOnce.Do(func() {
|
|
||||||
// 加载默认配置 + 覆盖用户自定义配置
|
|
||||||
options := defaultPostgresOptions()
|
|
||||||
for _, opt := range opts {
|
|
||||||
opt(options)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 校验必传配置(账号、密码、数据库名不能为空)
|
|
||||||
if options.User == "" || options.Password == "" || options.DBName == "" {
|
|
||||||
pgErr = fmt.Errorf("postgres config error: user/password/dbname can not be empty")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 拼接PostgreSQL DSN(数据源名称),GORM官方规范
|
|
||||||
dsn := fmt.Sprintf(
|
|
||||||
"host=%s port=%s user=%s password=%s dbname=%s sslmode=%s TimeZone=%s",
|
|
||||||
options.Host,
|
|
||||||
options.Port,
|
|
||||||
options.User,
|
|
||||||
options.Password,
|
|
||||||
options.DBName,
|
|
||||||
options.SSLMode,
|
|
||||||
options.TimeZone,
|
|
||||||
)
|
|
||||||
|
|
||||||
// 配置GORM日志(按传入的日志级别,生产环境可关闭)
|
|
||||||
gormConfig := &gorm.Config{
|
|
||||||
Logger: logger.Default.LogMode(options.LogLevel),
|
|
||||||
}
|
|
||||||
|
|
||||||
// 建立GORM连接
|
|
||||||
pgInstance, pgErr = gorm.Open(postgres.Open(dsn), gormConfig)
|
|
||||||
if pgErr != nil {
|
|
||||||
pgErr = fmt.Errorf("postgres connect failed: %w", pgErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取底层*sql.DB,配置连接池(关键:避免连接泄漏,提升性能)
|
|
||||||
sqlDB, err := pgInstance.DB()
|
|
||||||
if err != nil {
|
|
||||||
pgErr = fmt.Errorf("get postgres sql.DB failed: %w", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置连接池参数
|
|
||||||
sqlDB.SetMaxOpenConns(options.MaxOpenConns)
|
|
||||||
sqlDB.SetMaxIdleConns(options.MaxIdleConns)
|
|
||||||
sqlDB.SetConnMaxLifetime(options.ConnMaxLifetime)
|
|
||||||
sqlDB.SetConnMaxIdleTime(options.ConnMaxIdleTime)
|
|
||||||
|
|
||||||
// 测试连接(ping一下,确保连接有效)
|
|
||||||
if err := sqlDB.PingContext(context.Background()); err != nil {
|
|
||||||
pgErr = fmt.Errorf("postgres ping failed: %w", err)
|
|
||||||
pgInstance = nil // 连接失败,置空实例
|
|
||||||
return
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 单例执行完成后,返回实例和错误
|
|
||||||
return pgInstance, pgErr
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPostgres 获取全局PostgreSQL单例连接
|
|
||||||
// 业务层初始化后,可通过此方法直接获取连接,无需重复调用InitPostgres
|
|
||||||
func GetPostgres() (*gorm.DB, error) {
|
|
||||||
if pgInstance == nil {
|
|
||||||
return nil, fmt.Errorf("postgres not initialized, please call InitPostgres first")
|
|
||||||
}
|
|
||||||
return pgInstance, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ClosePostgres 关闭PostgreSQL连接(应用退出时调用,释放资源)
|
|
||||||
// 一般在main函数的defer中调用,如:defer db.ClosePostgres()
|
|
||||||
func ClosePostgres() error {
|
|
||||||
if pgInstance == nil {
|
|
||||||
return fmt.Errorf("postgres not initialized")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取底层*sql.DB,执行关闭
|
|
||||||
sqlDB, err := pgInstance.DB()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("get postgres sql.DB failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := sqlDB.Close(); err != nil {
|
|
||||||
return fmt.Errorf("close postgres failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 关闭后重置单例,避免重复关闭
|
|
||||||
pgOnce = sync.Once{}
|
|
||||||
pgInstance = nil
|
|
||||||
pgErr = nil
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// PingPostgres 测试PostgreSQL连接是否有效
|
|
||||||
// 业务层可定时调用(如健康检查),确保连接未断开
|
|
||||||
func PingPostgres() error {
|
|
||||||
db, err := GetPostgres()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
sqlDB, err := db.DB()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("get postgres sql.DB failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return sqlDB.PingContext(context.Background())
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
module common
|
|
||||||
|
|
||||||
go 1.25.7
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/redis/go-redis/v9 v9.3.0
|
|
||||||
go.uber.org/zap v1.26.0
|
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
|
||||||
gorm.io/driver/postgres v1.6.0
|
|
||||||
gorm.io/gorm v1.31.1
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // 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.6.0 // 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/stretchr/testify v1.8.3 // indirect
|
|
||||||
go.uber.org/multierr v1.10.0 // indirect
|
|
||||||
golang.org/x/crypto v0.31.0 // indirect
|
|
||||||
golang.org/x/sync v0.10.0 // indirect
|
|
||||||
golang.org/x/text v0.21.0 // indirect
|
|
||||||
)
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
|
||||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
|
||||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
|
||||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
|
||||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
|
||||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
|
||||||
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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
|
||||||
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.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
|
|
||||||
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
|
|
||||||
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
||||||
github.com/redis/go-redis/v9 v9.3.0 h1:RiVDjmig62jIWp7Kk4XVLs0hzV6pI3PyTnnL0cnn0u0=
|
|
||||||
github.com/redis/go-redis/v9 v9.3.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
|
|
||||||
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.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
|
|
||||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
|
||||||
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
|
|
||||||
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
|
|
||||||
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
|
||||||
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
|
||||||
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
|
|
||||||
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
|
|
||||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
|
||||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
|
||||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
|
||||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
|
||||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
|
||||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
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.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
|
|
||||||
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
|
|
||||||
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
|
|
||||||
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
|
||||||
@@ -1,240 +0,0 @@
|
|||||||
// common/logger/logger.go
|
|
||||||
package logger
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"go.uber.org/zap"
|
|
||||||
"go.uber.org/zap/zapcore"
|
|
||||||
"gopkg.in/natefinch/lumberjack.v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 全局单例日志实例
|
|
||||||
var (
|
|
||||||
_logger *zap.Logger
|
|
||||||
once sync.Once
|
|
||||||
)
|
|
||||||
|
|
||||||
// LoggerOptions 日志配置项
|
|
||||||
type LoggerOptions struct {
|
|
||||||
Level string // 日志级别:debug/info/warn/error/panic/fatal
|
|
||||||
Format string // 输出格式:console(控制台)/json(JSON)
|
|
||||||
OutputPath string // 文件输出路径(如./logs/app.log)
|
|
||||||
MaxSize int // 单个日志文件最大大小(MB)
|
|
||||||
MaxBackups int // 最大保留日志文件数
|
|
||||||
MaxAge int // 最大保留天数
|
|
||||||
Compress bool // 是否压缩日志文件
|
|
||||||
ShowLine bool // 是否显示代码行号
|
|
||||||
ConsoleColor bool // 控制台是否显示彩色(开发环境用)
|
|
||||||
ServiceName string // 服务名(多业务时区分日志归属,如user/order)
|
|
||||||
StacktraceKey string // 堆栈信息键名
|
|
||||||
CallerSkip int // 调用栈跳过数(适配封装层,正确显示业务代码行号)
|
|
||||||
FlushInterval time.Duration // 日志刷盘间隔
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoggerOption 选项模式函数类型
|
|
||||||
type LoggerOption func(*LoggerOptions)
|
|
||||||
|
|
||||||
// defaultLoggerOptions 初始化默认配置
|
|
||||||
// 开发环境默认:控制台彩色日志、info级别、显示行号
|
|
||||||
func defaultLoggerOptions() *LoggerOptions {
|
|
||||||
return &LoggerOptions{
|
|
||||||
Level: "info",
|
|
||||||
Format: "console",
|
|
||||||
OutputPath: "./logs/app.log",
|
|
||||||
MaxSize: 100, // 单个文件100MB
|
|
||||||
MaxBackups: 10, // 保留10个备份
|
|
||||||
MaxAge: 7, // 保留7天
|
|
||||||
Compress: true, // 压缩备份文件
|
|
||||||
ShowLine: true, // 显示行号
|
|
||||||
ConsoleColor: true, // 控制台彩色
|
|
||||||
ServiceName: "trading_assistant", // 默认服务名
|
|
||||||
StacktraceKey: "stacktrace",
|
|
||||||
CallerSkip: 1, // 跳过当前封装层,正确显示业务代码行号
|
|
||||||
FlushInterval: 3 * time.Second,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 以下为配置项设置函数,支持链式调用
|
|
||||||
func WithLevel(level string) LoggerOption {
|
|
||||||
return func(o *LoggerOptions) { o.Level = level }
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithFormat(format string) LoggerOption {
|
|
||||||
return func(o *LoggerOptions) { o.Format = format }
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithOutputPath(path string) LoggerOption {
|
|
||||||
return func(o *LoggerOptions) { o.OutputPath = path }
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithServiceName(name string) LoggerOption {
|
|
||||||
return func(o *LoggerOptions) { o.ServiceName = name }
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithShowLine(show bool) LoggerOption {
|
|
||||||
return func(o *LoggerOptions) { o.ShowLine = show }
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithConsoleColor(color bool) LoggerOption {
|
|
||||||
return func(o *LoggerOptions) { o.ConsoleColor = color }
|
|
||||||
}
|
|
||||||
|
|
||||||
// getZapLevel 转换日志级别为zapcore.Level
|
|
||||||
func getZapLevel(level string) zapcore.Level {
|
|
||||||
switch level {
|
|
||||||
case "debug":
|
|
||||||
return zapcore.DebugLevel
|
|
||||||
case "warn":
|
|
||||||
return zapcore.WarnLevel
|
|
||||||
case "error":
|
|
||||||
return zapcore.ErrorLevel
|
|
||||||
case "panic":
|
|
||||||
return zapcore.PanicLevel
|
|
||||||
case "fatal":
|
|
||||||
return zapcore.FatalLevel
|
|
||||||
default:
|
|
||||||
return zapcore.InfoLevel // 默认info级别
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// getEncoder 获取日志编码器(console/json)
|
|
||||||
func getEncoder(opts *LoggerOptions) zapcore.Encoder {
|
|
||||||
// 日志基础配置:时间格式、服务名、调用栈等
|
|
||||||
encoderConfig := zapcore.EncoderConfig{
|
|
||||||
TimeKey: "time",
|
|
||||||
LevelKey: "level",
|
|
||||||
NameKey: "service",
|
|
||||||
CallerKey: "caller",
|
|
||||||
MessageKey: "msg",
|
|
||||||
StacktraceKey: opts.StacktraceKey,
|
|
||||||
LineEnding: zapcore.DefaultLineEnding,
|
|
||||||
EncodeLevel: zapcore.CapitalLevelEncoder, // 级别大写(INFO/ERROR)
|
|
||||||
EncodeTime: zapcore.RFC3339TimeEncoder, // 时间格式RFC3339
|
|
||||||
EncodeDuration: zapcore.SecondsDurationEncoder,
|
|
||||||
EncodeCaller: zapcore.ShortCallerEncoder, // 调用者格式:文件:行号
|
|
||||||
}
|
|
||||||
|
|
||||||
// 开发环境:控制台彩色编码器
|
|
||||||
if opts.Format == "console" && opts.ConsoleColor {
|
|
||||||
return zapcore.NewConsoleEncoder(encoderConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生产环境:JSON编码器(便于日志收集分析,如ELK)
|
|
||||||
return zapcore.NewJSONEncoder(encoderConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
// getWriteSyncer 获取日志写入器(文件+控制台)
|
|
||||||
// 同时输出到文件和控制台,文件自动切割
|
|
||||||
func getWriteSyncer(opts *LoggerOptions) zapcore.WriteSyncer {
|
|
||||||
// 日志文件切割配置(基于lumberjack)
|
|
||||||
lumberjackLogger := &lumberjack.Logger{
|
|
||||||
Filename: opts.OutputPath,
|
|
||||||
MaxSize: opts.MaxSize,
|
|
||||||
MaxBackups: opts.MaxBackups,
|
|
||||||
MaxAge: opts.MaxAge,
|
|
||||||
Compress: opts.Compress,
|
|
||||||
}
|
|
||||||
|
|
||||||
// 同时输出到文件和控制台
|
|
||||||
return zapcore.NewMultiWriteSyncer(
|
|
||||||
zapcore.AddSync(os.Stdout),
|
|
||||||
zapcore.AddSync(lumberjackLogger),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// InitLogger 初始化全局单例日志实例
|
|
||||||
func InitLogger(opts ...LoggerOption) *zap.Logger {
|
|
||||||
once.Do(func() {
|
|
||||||
// 加载默认配置 + 覆盖用户自定义配置
|
|
||||||
options := defaultLoggerOptions()
|
|
||||||
for _, opt := range opts {
|
|
||||||
opt(options)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. 设置日志级别
|
|
||||||
level := getZapLevel(options.Level)
|
|
||||||
core := zapcore.NewCore(
|
|
||||||
getEncoder(options), // 编码器
|
|
||||||
getWriteSyncer(options), // 写入器
|
|
||||||
level, // 日志级别
|
|
||||||
)
|
|
||||||
|
|
||||||
// 2. 构建日志实例配置:是否显示调用者、堆栈信息
|
|
||||||
zapOpts := []zap.Option{zap.AddCallerSkip(options.CallerSkip)}
|
|
||||||
if options.ShowLine {
|
|
||||||
zapOpts = append(zapOpts, zap.AddCaller()) // 显示调用者(文件:行号)
|
|
||||||
}
|
|
||||||
// 错误级别及以上显示堆栈信息
|
|
||||||
zapOpts = append(zapOpts, zap.AddStacktrace(zapcore.ErrorLevel))
|
|
||||||
// 设置服务名
|
|
||||||
zapOpts = append(zapOpts, zap.Fields(zap.String("service", options.ServiceName)))
|
|
||||||
|
|
||||||
// 3. 创建日志实例
|
|
||||||
_logger = zap.New(core, zapOpts...)
|
|
||||||
|
|
||||||
// 4. 定时刷盘(避免日志驻留内存)
|
|
||||||
go func() {
|
|
||||||
ticker := time.NewTicker(options.FlushInterval)
|
|
||||||
defer ticker.Stop()
|
|
||||||
for range ticker.C {
|
|
||||||
_ = _logger.Sync()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 5. 应用退出时刷盘
|
|
||||||
os.Setenv("ZAP_FLUSH_ON_EXIT", "true")
|
|
||||||
})
|
|
||||||
|
|
||||||
return _logger
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetLogger 获取全局单例日志实例
|
|
||||||
func GetLogger() *zap.Logger {
|
|
||||||
if _logger == nil {
|
|
||||||
// 未初始化时,返回默认控制台日志(兜底)
|
|
||||||
return InitLogger()
|
|
||||||
}
|
|
||||||
return _logger
|
|
||||||
}
|
|
||||||
|
|
||||||
// --------------- 轻量封装日志方法 ---------------
|
|
||||||
// 简化业务层调用,无需每次获取logger实例
|
|
||||||
// 复杂日志(如带字段)可直接使用GetLogger()获取原生实例
|
|
||||||
|
|
||||||
// Debug 调试日志
|
|
||||||
func Debug(msg string, fields ...zap.Field) {
|
|
||||||
GetLogger().Debug(msg, fields...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Info 信息日志
|
|
||||||
func Info(msg string, fields ...zap.Field) {
|
|
||||||
GetLogger().Info(msg, fields...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Warn 警告日志
|
|
||||||
func Warn(msg string, fields ...zap.Field) {
|
|
||||||
GetLogger().Warn(msg, fields...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error 错误日志(带堆栈)
|
|
||||||
func Error(msg string, fields ...zap.Field) {
|
|
||||||
GetLogger().Error(msg, fields...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Panic 恐慌日志(打印后触发panic)
|
|
||||||
func Panic(msg string, fields ...zap.Field) {
|
|
||||||
GetLogger().Panic(msg, fields...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fatal 致命日志(打印后退出程序)
|
|
||||||
func Fatal(msg string, fields ...zap.Field) {
|
|
||||||
GetLogger().Fatal(msg, fields...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sync 手动刷盘(如重要操作后)
|
|
||||||
func Sync() error {
|
|
||||||
return GetLogger().Sync()
|
|
||||||
}
|
|
||||||
@@ -1,152 +0,0 @@
|
|||||||
// common/middleware/cors.go
|
|
||||||
package middleware
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CORSOptions 跨域配置项
|
|
||||||
// 支持自定义允许的源、方法、头、凭证、缓存时间,按需扩展
|
|
||||||
type CORSOptions struct {
|
|
||||||
AllowOrigins []string // 允许的跨域源,如["http://localhost:8080", "https://xxx.com"],*表示允许所有
|
|
||||||
AllowMethods []string // 允许的HTTP方法,默认GET/POST/PUT/DELETE/PATCH/OPTIONS
|
|
||||||
AllowHeaders []string // 允许的请求头,*表示允许所有
|
|
||||||
AllowCredentials bool // 是否允许携带凭证(Cookie/Token),前后端联调必备
|
|
||||||
ExposeHeaders []string // 允许前端获取的响应头
|
|
||||||
MaxAge time.Duration // 预检请求(OPTIONS)的缓存时间,避免重复预检
|
|
||||||
}
|
|
||||||
|
|
||||||
// CORSOption 选项模式函数类型,用于灵活配置跨域参数
|
|
||||||
type CORSOption func(*CORSOptions)
|
|
||||||
|
|
||||||
// defaultCORSOptions 初始化默认跨域配置
|
|
||||||
// 开发环境默认允许所有源、常用方法,生产环境可通过配置覆盖
|
|
||||||
func defaultCORSOptions() *CORSOptions {
|
|
||||||
return &CORSOptions{
|
|
||||||
AllowOrigins: []string{"*"}, // 开发环境默认允许所有源
|
|
||||||
AllowMethods: []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete, http.MethodPatch, http.MethodOptions},
|
|
||||||
AllowHeaders: []string{"*"}, // 允许所有请求头
|
|
||||||
AllowCredentials: true, // 允许携带凭证
|
|
||||||
ExposeHeaders: []string{"Content-Length", "Content-Type", "X-Token"},
|
|
||||||
MaxAge: 12 * time.Hour, // 预检请求缓存12小时
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 以下为配置项设置函数,支持链式调用
|
|
||||||
// WithAllowOrigins 设置允许的跨域源,示例:WithAllowOrigins("http://localhost:3000", "https://app.com")
|
|
||||||
func WithAllowOrigins(origins ...string) CORSOption {
|
|
||||||
return func(o *CORSOptions) { o.AllowOrigins = origins }
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithAllowCredentials 设置是否允许携带凭证(Cookie/Token)
|
|
||||||
func WithAllowCredentials(allow bool) CORSOption {
|
|
||||||
return func(o *CORSOptions) { o.AllowCredentials = allow }
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithMaxAge 设置预检请求缓存时间
|
|
||||||
func WithMaxAge(age time.Duration) CORSOption {
|
|
||||||
return func(o *CORSOptions) { o.MaxAge = age }
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithAllowHeaders 设置允许的请求头
|
|
||||||
func WithAllowHeaders(headers ...string) CORSOption {
|
|
||||||
return func(o *CORSOptions) { o.AllowHeaders = headers }
|
|
||||||
}
|
|
||||||
|
|
||||||
// CORS 跨域中间件核心方法
|
|
||||||
// 适配Go原生http.Handler,可直接用于Gin/Echo等框架(兼容框架中间件规范)
|
|
||||||
func CORS(opts ...CORSOption) func(http.Handler) http.Handler {
|
|
||||||
// 加载默认配置 + 覆盖用户自定义配置
|
|
||||||
options := defaultCORSOptions()
|
|
||||||
for _, opt := range opts {
|
|
||||||
opt(options)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理允许的源:拼接为字符串
|
|
||||||
allowOrigins := strings.Join(options.AllowOrigins, ", ")
|
|
||||||
// 处理允许的方法:拼接为字符串
|
|
||||||
allowMethods := strings.Join(options.AllowMethods, ", ")
|
|
||||||
// 处理允许的请求头:拼接为字符串
|
|
||||||
allowHeaders := strings.Join(options.AllowHeaders, ", ")
|
|
||||||
// 处理允许暴露的响应头:拼接为字符串
|
|
||||||
exposeHeaders := strings.Join(options.ExposeHeaders, ", ")
|
|
||||||
|
|
||||||
return func(next http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// 1. 获取前端请求的Origin(跨域核心)
|
|
||||||
origin := r.Header.Get("Origin")
|
|
||||||
// 若配置了*,则直接使用请求的Origin;否则使用配置的源(生产环境建议精准配置)
|
|
||||||
if len(options.AllowOrigins) > 0 && options.AllowOrigins[0] == "*" {
|
|
||||||
w.Header().Set("Access-Control-Allow-Origin", origin)
|
|
||||||
} else {
|
|
||||||
w.Header().Set("Access-Control-Allow-Origin", allowOrigins)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 设置跨域核心响应头
|
|
||||||
w.Header().Set("Access-Control-Allow-Methods", allowMethods)
|
|
||||||
w.Header().Set("Access-Control-Allow-Headers", allowHeaders)
|
|
||||||
w.Header().Set("Access-Control-Expose-Headers", exposeHeaders)
|
|
||||||
w.Header().Set("Access-Control-Max-Age", string(rune(options.MaxAge.Seconds())))
|
|
||||||
// 允许携带凭证时,不能将Allow-Origin设为*,需动态匹配请求Origin(已做处理)
|
|
||||||
if options.AllowCredentials {
|
|
||||||
w.Header().Set("Access-Control-Allow-Credentials", "true")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 处理预检请求(OPTIONS):直接返回204,无需执行后续业务逻辑
|
|
||||||
if r.Method == http.MethodOptions {
|
|
||||||
w.WriteHeader(http.StatusNoContent)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. 非预检请求,执行后续业务逻辑
|
|
||||||
next.ServeHTTP(w, r)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- 兼容Gin框架的快捷中间件(可选)----
|
|
||||||
// 若团队使用Gin框架开发,可直接使用此方法,无需额外转换,提升开发效率
|
|
||||||
// 需提前安装Gin:go get github.com/gin-gonic/gin
|
|
||||||
import (
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CorsGin 适配Gin框架的跨域中间件
|
|
||||||
func CorsGin(opts ...CORSOption) gin.HandlerFunc {
|
|
||||||
// 复用原生CORS配置逻辑
|
|
||||||
options := defaultCORSOptions()
|
|
||||||
for _, opt := range opts {
|
|
||||||
opt(options)
|
|
||||||
}
|
|
||||||
allowOrigins := strings.Join(options.AllowOrigins, ", ")
|
|
||||||
allowMethods := strings.Join(options.Methods, ", ")
|
|
||||||
allowHeaders := strings.Join(options.AllowHeaders, ", ")
|
|
||||||
exposeHeaders := strings.Join(options.ExposeHeaders, ", ")
|
|
||||||
|
|
||||||
return func(c *gin.Context) {
|
|
||||||
origin := c.Request.Header.Get("Origin")
|
|
||||||
if options.AllowOrigins[0] == "*" {
|
|
||||||
c.Writer.Header().Set("Access-Control-Allow-Origin", origin)
|
|
||||||
} else {
|
|
||||||
c.Writer.Header().Set("Access-Control-Allow-Origin", allowOrigins)
|
|
||||||
}
|
|
||||||
c.Writer.Header().Set("Access-Control-Allow-Methods", allowMethods)
|
|
||||||
c.Writer.Header().Set("Access-Control-Allow-Headers", allowHeaders)
|
|
||||||
c.Writer.Header().Set("Access-Control-Expose-Headers", exposeHeaders)
|
|
||||||
c.Writer.Header().Set("Access-Control-Max-Age", string(rune(options.MaxAge.Seconds())))
|
|
||||||
if options.AllowCredentials {
|
|
||||||
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理预检请求
|
|
||||||
if c.Request.Method == http.MethodOptions {
|
|
||||||
c.AbortWithStatus(http.StatusNoContent)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 继续执行后续中间件/业务逻辑
|
|
||||||
c.Next()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,220 +0,0 @@
|
|||||||
// common/redis/redis.go
|
|
||||||
package redis
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/redis/go-redis/v9"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 全局单例Redis客户端(连接池)
|
|
||||||
var (
|
|
||||||
clientInstance *redis.Client
|
|
||||||
once sync.Once
|
|
||||||
initErr error
|
|
||||||
)
|
|
||||||
|
|
||||||
// RedisOptions Redis连接配置项
|
|
||||||
// 包含基础连接信息+连接池配置,按需配置
|
|
||||||
type RedisOptions struct {
|
|
||||||
Host string // Redis地址
|
|
||||||
Port string // Redis端口
|
|
||||||
Password string // Redis密码
|
|
||||||
DB int // 数据库索引(建议每个业务一个DB,如user=0, order=1)
|
|
||||||
PoolSize int // 连接池最大连接数
|
|
||||||
MinIdleConns int // 连接池最小空闲连接数
|
|
||||||
ConnTimeout time.Duration // 连接超时时间
|
|
||||||
ReadTimeout time.Duration // 读超时时间
|
|
||||||
WriteTimeout time.Duration // 写超时时间
|
|
||||||
IdleTimeout time.Duration // 连接空闲超时时间
|
|
||||||
}
|
|
||||||
|
|
||||||
// RedisOption 选项模式函数类型
|
|
||||||
type RedisOption func(*RedisOptions)
|
|
||||||
|
|
||||||
// defaultRedisOptions 初始化默认配置
|
|
||||||
// 开发环境直接对接根目录docker-compose的redis服务,无需额外配置
|
|
||||||
func defaultRedisOptions() *RedisOptions {
|
|
||||||
return &RedisOptions{
|
|
||||||
Host: "redis", // 匹配Compose服务名
|
|
||||||
Port: "6379", // 默认端口
|
|
||||||
Password: "", // 开发环境默认无密码
|
|
||||||
DB: 0, // 默认DB0
|
|
||||||
PoolSize: 50, // 连接池最大连接数
|
|
||||||
MinIdleConns: 10, // 最小空闲连接,避免频繁创建连接
|
|
||||||
ConnTimeout: 5 * time.Second,
|
|
||||||
ReadTimeout: 3 * time.Second,
|
|
||||||
WriteTimeout: 3 * time.Second,
|
|
||||||
IdleTimeout: 30 * time.Minute,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 以下为配置项设置函数,支持链式调用
|
|
||||||
func WithHost(host string) RedisOption {
|
|
||||||
return func(o *RedisOptions) { o.Host = host }
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithPort(port string) RedisOption {
|
|
||||||
return func(o *RedisOptions) { o.Port = port }
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithPassword(pwd string) RedisOption {
|
|
||||||
return func(o *RedisOptions) { o.Password = pwd }
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithDB(db int) RedisOption {
|
|
||||||
return func(o *RedisOptions) { o.DB = db }
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithPoolSize(size int) RedisOption {
|
|
||||||
return func(o *RedisOptions) { o.PoolSize = size }
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithLogLevel(level redis.LogLevel) RedisOption {
|
|
||||||
return func(o *RedisOptions) {
|
|
||||||
// 兼容日志级别配置,需配合客户端初始化
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// InitRedis 初始化Redis单例客户端(连接池)
|
|
||||||
// 选项模式配置,仅执行一次,全局复用连接池
|
|
||||||
func InitRedis(opts ...RedisOption) (*redis.Client, error) {
|
|
||||||
once.Do(func() {
|
|
||||||
// 加载默认配置 + 覆盖用户自定义配置
|
|
||||||
options := defaultRedisOptions()
|
|
||||||
for _, opt := range opts {
|
|
||||||
opt(options)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 构建Redis客户端配置
|
|
||||||
rdbOpts := &redis.Options{
|
|
||||||
Addr: fmt.Sprintf("%s:%s", options.Host, options.Port),
|
|
||||||
Password: options.Password,
|
|
||||||
DB: options.DB,
|
|
||||||
PoolSize: options.PoolSize,
|
|
||||||
MinIdleConns: options.MinIdleConns,
|
|
||||||
DialTimeout: options.ConnTimeout,
|
|
||||||
ReadTimeout: options.ReadTimeout,
|
|
||||||
WriteTimeout: options.WriteTimeout,
|
|
||||||
IdleTimeout: options.IdleTimeout,
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建客户端实例
|
|
||||||
clientInstance = redis.NewClient(rdbOpts)
|
|
||||||
|
|
||||||
// 测试连接,确保连接有效
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
if _, err := clientInstance.Ping(ctx).Result(); err != nil {
|
|
||||||
initErr = fmt.Errorf("redis ping failed: %w", err)
|
|
||||||
clientInstance = nil // 连接失败置空实例
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
initErr = nil
|
|
||||||
})
|
|
||||||
|
|
||||||
return clientInstance, initErr
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRedis 获取全局Redis单例客户端
|
|
||||||
// 业务层初始化后,直接调用获取,无需重复初始化
|
|
||||||
func GetRedis() (*redis.Client, error) {
|
|
||||||
if clientInstance == nil {
|
|
||||||
return nil, errors.New("redis not initialized, please call InitRedis first")
|
|
||||||
}
|
|
||||||
return clientInstance, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CloseRedis 关闭Redis连接池(应用退出时调用,优雅释放资源)
|
|
||||||
func CloseRedis() error {
|
|
||||||
if clientInstance == nil {
|
|
||||||
return errors.New("redis not initialized")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := clientInstance.Close(); err != nil {
|
|
||||||
return fmt.Errorf("close redis failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 重置单例,避免重复关闭
|
|
||||||
once = sync.Once{}
|
|
||||||
clientInstance = nil
|
|
||||||
initErr = errors.New("redis client closed")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// --------------- 基础操作封装 ---------------
|
|
||||||
// 轻量封装高频基础操作,简化业务层调用(无需关注context和错误处理基础逻辑)
|
|
||||||
// 复杂操作(如哈希、列表、事务)直接使用原生*redis.Client即可
|
|
||||||
|
|
||||||
// Ctx 全局默认context,业务层可自定义传入
|
|
||||||
var Ctx = context.Background()
|
|
||||||
|
|
||||||
// Set 封装Set操作,带过期时间
|
|
||||||
// key: 键, val: 值, expire: 过期时间(0表示永不过期)
|
|
||||||
func Set(key string, val interface{}, expire time.Duration) error {
|
|
||||||
rdb, err := GetRedis()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return rdb.Set(Ctx, key, val, expire).Err()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get 封装Get操作,返回字符串值
|
|
||||||
func Get(key string) (string, error) {
|
|
||||||
rdb, err := GetRedis()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return rdb.Get(Ctx, key).Result()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Del 封装Del操作,删除一个或多个键
|
|
||||||
func Del(keys ...string) (int64, error) {
|
|
||||||
rdb, err := GetRedis()
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
return rdb.Del(Ctx, keys...).Result()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exists 检查键是否存在
|
|
||||||
func Exists(key string) (bool, error) {
|
|
||||||
rdb, err := GetRedis()
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
res, err := rdb.Exists(Ctx, key).Result()
|
|
||||||
return res > 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Expire 为键设置过期时间
|
|
||||||
func Expire(key string, expire time.Duration) error {
|
|
||||||
rdb, err := GetRedis()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return rdb.Expire(Ctx, key, expire).Err()
|
|
||||||
}
|
|
||||||
|
|
||||||
// HSet 封装哈希Set操作,设置单个字段
|
|
||||||
func HSet(key, field string, val interface{}) error {
|
|
||||||
rdb, err := GetRedis()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return rdb.HSet(Ctx, key, field, val).Err()
|
|
||||||
}
|
|
||||||
|
|
||||||
// HGet 封装哈希Get操作,获取单个字段值
|
|
||||||
func HGet(key, field string) (string, error) {
|
|
||||||
rdb, err := GetRedis()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return rdb.HGet(Ctx, key, field).Result()
|
|
||||||
}
|
|
||||||
@@ -1,276 +0,0 @@
|
|||||||
// common/common.go
|
|
||||||
package common
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"encoding/base64"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"math/big"
|
|
||||||
"os"
|
|
||||||
"reflect"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ---------------------- 环境变量操作 ----------------------
|
|
||||||
// 项目中最常用的通用能力,适配Docker容器化(环境变量传配置)
|
|
||||||
// GetEnv 读取环境变量,若不存在则返回默认值
|
|
||||||
func GetEnv(key, defVal string) string {
|
|
||||||
val := os.Getenv(key)
|
|
||||||
if val == "" {
|
|
||||||
return defVal
|
|
||||||
}
|
|
||||||
return val
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetEnvInt 读取环境变量并转换为int,转换失败/不存在返回默认值
|
|
||||||
func GetEnvInt(key string, defVal int) int {
|
|
||||||
val := os.Getenv(key)
|
|
||||||
if val == "" {
|
|
||||||
return defVal
|
|
||||||
}
|
|
||||||
num, err := strconv.Atoi(val)
|
|
||||||
if err != nil {
|
|
||||||
return defVal
|
|
||||||
}
|
|
||||||
return num
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetEnvBool 读取环境变量并转换为bool(支持1/0、true/false、on/off),失败/不存在返回默认值
|
|
||||||
func GetEnvBool(key string, defVal bool) bool {
|
|
||||||
val := strings.ToLower(os.Getenv(key))
|
|
||||||
switch val {
|
|
||||||
case "1", "true", "on":
|
|
||||||
return true
|
|
||||||
case "0", "false", "off":
|
|
||||||
return false
|
|
||||||
default:
|
|
||||||
return defVal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------- 类型转换 ----------------------
|
|
||||||
// 日常开发中频繁的类型转换,封装后避免重复写err判断
|
|
||||||
// StrToInt 字符串转int,失败返回0和错误
|
|
||||||
func StrToInt(s string) (int, error) {
|
|
||||||
return strconv.Atoi(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
// StrToInt64 字符串转int64,失败返回0和错误
|
|
||||||
func StrToInt64(s string) (int64, error) {
|
|
||||||
return strconv.ParseInt(s, 10, 64)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IntToStr int转字符串
|
|
||||||
func IntToStr(i int) string {
|
|
||||||
return strconv.Itoa(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Int64ToStr int64转字符串
|
|
||||||
func Int64ToStr(i int64) string {
|
|
||||||
return strconv.FormatInt(i, 10)
|
|
||||||
}
|
|
||||||
|
|
||||||
// StrToFloat64 字符串转float64,失败返回0和错误
|
|
||||||
func StrToFloat64(s string) (float64, error) {
|
|
||||||
return strconv.ParseFloat(s, 64)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Float64ToStr float64转字符串,保留n位小数
|
|
||||||
func Float64ToStr(f float64, n int) string {
|
|
||||||
format := fmt.Sprintf("%%.%df", n)
|
|
||||||
return fmt.Sprintf(format, f)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------- 时间通用处理 ----------------------
|
|
||||||
// 统一项目时间格式,避免各业务自定义格式导致的混乱
|
|
||||||
const (
|
|
||||||
// TimeFormatYmdHms 常规时间格式:2006-01-02 15:04:05(Go诞生时间,固定模板)
|
|
||||||
TimeFormatYmdHms = "2006-01-02 15:04:05"
|
|
||||||
// TimeFormatYmd 日期格式:2006-01-02
|
|
||||||
TimeFormatYmd = "2006-01-02"
|
|
||||||
// TimeFormatHms 时间格式:15:04:05
|
|
||||||
TimeFormatHms = "15:04:05"
|
|
||||||
// TimeFormatYmdHmsS 带毫秒的时间格式:2006-01-02 15:04:05.000
|
|
||||||
TimeFormatYmdHmsS = "2006-01-02 15:04:05.000"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TimeToStr 时间转字符串,指定格式(不传则用默认YmdHms)
|
|
||||||
func TimeToStr(t time.Time, format ...string) string {
|
|
||||||
ft := TimeFormatYmdHms
|
|
||||||
if len(format) > 0 && format[0] != "" {
|
|
||||||
ft = format[0]
|
|
||||||
}
|
|
||||||
// 转换为本地时区(避免UTC时间偏移)
|
|
||||||
return t.In(time.Local).Format(ft)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NowToStr 当前时间转字符串,指定格式(不传则用默认YmdHms)
|
|
||||||
func NowToStr(format ...string) string {
|
|
||||||
return TimeToStr(time.Now(), format...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// StrToTime 字符串转时间,指定格式(不传则用默认YmdHms),失败返回零值和错误
|
|
||||||
func StrToTime(s string, format ...string) (time.Time, error) {
|
|
||||||
ft := TimeFormatYmdHms
|
|
||||||
if len(format) > 0 && format[0] != "" {
|
|
||||||
ft = format[0]
|
|
||||||
}
|
|
||||||
return time.ParseInLocation(ft, s, time.Local)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDayStart 获取某天的开始时间(如2026-02-06 00:00:00)
|
|
||||||
func GetDayStart(t time.Time) time.Time {
|
|
||||||
year, month, day := t.In(time.Local).Date()
|
|
||||||
return time.Date(year, month, day, 0, 0, 0, 0, time.Local)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDayEnd 获取某天的结束时间(如2026-02-06 23:59:59.999999999)
|
|
||||||
func GetDayEnd(t time.Time) time.Time {
|
|
||||||
year, month, day := t.In(time.Local).Date()
|
|
||||||
return time.Date(year, month, day, 23, 59, 59, 999999999, time.Local)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------- 空值判断 ----------------------
|
|
||||||
// 通用的空值检查,支持所有基础类型和引用类型,避免业务层重复写if判断
|
|
||||||
// IsEmpty 判断值是否为空(0/""//nil/空切片/空map等)
|
|
||||||
func IsEmpty(v interface{}) bool {
|
|
||||||
if v == nil {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
val := reflect.ValueOf(v)
|
|
||||||
switch val.Kind() {
|
|
||||||
case reflect.String:
|
|
||||||
return val.String() == ""
|
|
||||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
|
||||||
return val.Int() == 0
|
|
||||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
|
||||||
return val.Uint() == 0
|
|
||||||
case reflect.Float32, reflect.Float64:
|
|
||||||
return val.Float() == 0
|
|
||||||
case reflect.Bool:
|
|
||||||
return !val.Bool()
|
|
||||||
case reflect.Slice, reflect.Array:
|
|
||||||
return val.Len() == 0
|
|
||||||
case reflect.Map, reflect.Chan:
|
|
||||||
return val.Len() == 0 || val.IsNil()
|
|
||||||
case reflect.Ptr, reflect.Interface:
|
|
||||||
return val.IsNil()
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsNotEmpty 判断值是否非空,与IsEmpty相反
|
|
||||||
func IsNotEmpty(v interface{}) bool {
|
|
||||||
return !IsEmpty(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------- 字符串通用处理 ----------------------
|
|
||||||
// 高频的字符串操作,封装后简化调用
|
|
||||||
// StrTrim 去除字符串两端的空格/制表符/换行符
|
|
||||||
func StrTrim(s string) string {
|
|
||||||
return strings.TrimSpace(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
// StrToLower 字符串转小写
|
|
||||||
func StrToLower(s string) string {
|
|
||||||
return strings.ToLower(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
// StrToUpper 字符串转大写
|
|
||||||
func StrToUpper(s string) string {
|
|
||||||
return strings.ToUpper(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
// StrIsIn 判断字符串是否在指定的切片中(忽略大小写)
|
|
||||||
func StrIsIn(s string, list []string) bool {
|
|
||||||
s = StrToLower(s)
|
|
||||||
for _, v := range list {
|
|
||||||
if StrToLower(v) == s {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// StrJoin 拼接切片为字符串,指定分隔符
|
|
||||||
func StrJoin(list []string, sep string) string {
|
|
||||||
return strings.Join(list, sep)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------- 随机数/随机字符串 ----------------------
|
|
||||||
// 生成随机数、随机字符串(用于生成验证码、随机密码、唯一标识等)
|
|
||||||
// RandInt 生成[min, max]范围内的随机整数,失败返回0和错误
|
|
||||||
func RandInt(min, max int) (int, error) {
|
|
||||||
if min > max {
|
|
||||||
return 0, errors.New("min is greater than max")
|
|
||||||
}
|
|
||||||
// 基于crypto/rand生成安全随机数(比math/rand更安全,适合生产环境)
|
|
||||||
num, err := rand.Int(rand.Reader, big.NewInt(int64(max-min+1)))
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
return min + int(num.Int64()), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RandStr 生成指定长度的随机字符串(数字+大小写字母),失败返回""和错误
|
|
||||||
func RandStr(length int) (string, error) {
|
|
||||||
if length <= 0 {
|
|
||||||
return "", errors.New("length must be greater than 0")
|
|
||||||
}
|
|
||||||
// 随机字符串源(可根据需求添加/移除字符,如去掉大写字母)
|
|
||||||
chars := "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
|
||||||
charsLen := big.NewInt(int64(len(chars)))
|
|
||||||
result := make([]byte, length)
|
|
||||||
for i := 0; i < length; i++ {
|
|
||||||
// 逐个生成随机字符索引
|
|
||||||
idx, err := rand.Int(rand.Reader, charsLen)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
result[i] = chars[idx.Int64()]
|
|
||||||
}
|
|
||||||
return string(result), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RandNumStr 生成指定长度的纯数字随机字符串,失败返回""和错误
|
|
||||||
func RandNumStr(length int) (string, error) {
|
|
||||||
if length <= 0 {
|
|
||||||
return "", errors.New("length must be greater than 0")
|
|
||||||
}
|
|
||||||
nums := "0123456789"
|
|
||||||
numsLen := big.NewInt(int64(len(nums)))
|
|
||||||
result := make([]byte, length)
|
|
||||||
for i := 0; i < length; i++ {
|
|
||||||
idx, err := rand.Int(rand.Reader, numsLen)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
result[i] = nums[idx.Int64()]
|
|
||||||
}
|
|
||||||
return string(result), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------- 其他通用工具 ----------------------
|
|
||||||
// GetCurrentDir 获取当前程序运行目录(适配Docker容器化,避免路径问题)
|
|
||||||
func GetCurrentDir() (string, error) {
|
|
||||||
dir, err := os.Getwd()
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("get current dir failed: %w", err)
|
|
||||||
}
|
|
||||||
return dir, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// MkdirIfNotExist 目录不存在则创建(支持多级目录),适配日志/文件存储
|
|
||||||
func MkdirIfNotExist(path string) error {
|
|
||||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
|
||||||
// 0755:所有者读写执行,其他用户读执行,符合Linux/Docker权限规范
|
|
||||||
if err := os.MkdirAll(path, 0755); err != nil {
|
|
||||||
return fmt.Errorf("mkdir %s failed: %w", path, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
docker run --name trading_container -itd \
|
|
||||||
-v $(pwd):/app \
|
|
||||||
golang:1.25.7-alpine3.23 \
|
|
||||||
/bin/sh
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
---------------------------------- 国家主表 ----------------------------------
|
|
||||||
|
|
||||||
-- 创建国家ID记录表(主表,存储基础主键+通用字段)
|
|
||||||
CREATE TABLE IF NOT EXISTS countries (
|
|
||||||
country_id UUID PRIMARY KEY DEFAULT uuidv7(),
|
|
||||||
deleted boolean DEFAULT false,
|
|
||||||
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
-- 表注释
|
|
||||||
COMMENT ON TABLE countries IS '国家主表,仅存储国家基础主键及通用公共字段,关联国家/地区子表';
|
|
||||||
-- 字段注释
|
|
||||||
COMMENT ON COLUMN countries.country_id IS '国家主键ID,PG18原生UUIDv7,作为所有子表的关联外键';
|
|
||||||
COMMENT ON COLUMN countries.deleted IS '逻辑删除标识,false-未删除,true-已删除,默认未删除';
|
|
||||||
COMMENT ON COLUMN countries.create_time IS '记录创建时间,默认当前系统时间,不可手动修改';
|
|
||||||
COMMENT ON COLUMN countries.update_time IS '记录最后更新时间,默认当前系统时间';
|
|
||||||
|
|
||||||
-- 创建update_time自动刷新触发器函数(所有表复用)
|
|
||||||
CREATE OR REPLACE FUNCTION update_table_modify_time()
|
|
||||||
RETURNS TRIGGER AS $$
|
|
||||||
BEGIN
|
|
||||||
NEW.update_time = CURRENT_TIMESTAMP;
|
|
||||||
RETURN NEW;
|
|
||||||
END;
|
|
||||||
$$ LANGUAGE plpgsql;
|
|
||||||
-- 绑定触发器到国家主表
|
|
||||||
CREATE TRIGGER tr_countries_update_time
|
|
||||||
BEFORE UPDATE ON countries
|
|
||||||
FOR EACH ROW
|
|
||||||
EXECUTE FUNCTION update_table_modify_time();
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
---------------------------------- 国家核心信息表 ----------------------------------
|
|
||||||
|
|
||||||
-- 创建国家核心信息记录表(子表,存储国家编码/名称等核心信息)
|
|
||||||
CREATE TABLE IF NOT EXISTS country_core_infos (
|
|
||||||
core_id UUID PRIMARY KEY DEFAULT uuidv7(),
|
|
||||||
country_id UUID NOT NULL,
|
|
||||||
country_code VARCHAR(5) NOT NULL, -- 如CN(中国)/US(美国),遵循ISO 3166-1
|
|
||||||
country_cn_short_name VARCHAR(50) NOT NULL, -- 如中国/美国,中文简称
|
|
||||||
deleted boolean DEFAULT false,
|
|
||||||
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
-- 外键约束:关联国家主表,保证数据一致性(删除主表时级联置空/禁止删除,按需选择)
|
|
||||||
CONSTRAINT fk_country_core_infos_country_id
|
|
||||||
FOREIGN KEY (country_id) REFERENCES countries (country_id)
|
|
||||||
ON DELETE RESTRICT -- 主表国家未删除时,禁止删除子表信息(推荐生产使用)
|
|
||||||
-- ON DELETE SET NULL -- 可选:主表删除时,子表country_id置空(适合软删除场景)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- 表注释
|
|
||||||
COMMENT ON TABLE country_core_infos IS '国家核心信息子表,存储国家编码、中文名称等专属信息,与国家主表一对一关联';
|
|
||||||
-- 字段注释
|
|
||||||
COMMENT ON COLUMN country_core_infos.core_id IS '子表主键ID,PG18原生UUIDv7';
|
|
||||||
COMMENT ON COLUMN country_core_infos.country_id IS '关联国家主表countries.country_id,外键约束';
|
|
||||||
COMMENT ON COLUMN country_core_infos.country_code IS '国家二位编码,遵循ISO 3166-1,如CN(中国)/US(美国),唯一标识国家';
|
|
||||||
COMMENT ON COLUMN country_core_infos.country_cn_short_name IS '国家中文简称,如中国/美国,业务展示专用';
|
|
||||||
COMMENT ON COLUMN country_core_infos.deleted IS '逻辑删除标识,false-未删除,true-已删除,默认未删除';
|
|
||||||
COMMENT ON COLUMN country_core_infos.create_time IS '记录创建时间,默认当前系统时间,不可手动修改';
|
|
||||||
COMMENT ON COLUMN country_core_infos.update_time IS '记录最后更新时间,更新时触发器自动刷新';
|
|
||||||
|
|
||||||
-- 索引:优化高频业务查询(核心)
|
|
||||||
CREATE UNIQUE INDEX uk_country_core_infos_code ON country_core_infos (country_code) WHERE deleted = false; -- 唯一索引:国家编码不重复(未删除数据)
|
|
||||||
CREATE INDEX idx_country_core_infos_country_id ON country_core_infos (country_id, deleted); -- 联合索引:按国家ID关联查询+过滤删除数据
|
|
||||||
CREATE INDEX idx_country_core_infos_name ON country_core_infos (country_cn_short_name, deleted); -- 联合索引:按国家名称模糊查询+过滤删除数据
|
|
||||||
|
|
||||||
-- 绑定触发器:更新时自动刷新update_time
|
|
||||||
CREATE TRIGGER tr_country_core_infos_update_time
|
|
||||||
BEFORE UPDATE ON country_core_infos
|
|
||||||
FOR EACH ROW
|
|
||||||
EXECUTE FUNCTION update_table_modify_time();
|
|
||||||
|
|
||||||
|
|
||||||
---------------------------------- 国家地区核心信息表 ----------------------------------
|
|
||||||
|
|
||||||
-- 创建国家地区信息记录表(子表,存储国家下属地区编码/名称等信息,如中国香港/美国纽约)
|
|
||||||
CREATE TABLE IF NOT EXISTS country_region_core_infos (
|
|
||||||
region_id UUID PRIMARY KEY DEFAULT uuidv7(),
|
|
||||||
country_id UUID NOT NULL,
|
|
||||||
region_code VARCHAR(5) NOT NULL, -- 如HK(香港)/TW(台湾)/NYC(纽约),建议按 国家码-地区码 设计如CN-HK
|
|
||||||
region_cn_short_name VARCHAR(50) NOT NULL, -- 如香港/台湾/纽约,中文简称
|
|
||||||
deleted boolean DEFAULT false,
|
|
||||||
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
-- 外键约束:关联国家主表,保证数据一致性
|
|
||||||
CONSTRAINT fk_country_region_core_infos_country_id
|
|
||||||
FOREIGN KEY (country_id) REFERENCES countries (country_id)
|
|
||||||
ON DELETE RESTRICT -- 主表国家未删除时,禁止删除子表地区信息(推荐生产使用)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- 表注释
|
|
||||||
COMMENT ON TABLE country_region_core_infos IS '国家地区核心信息子表,存储国家下属地区编码、中文名称等信息,与国家主表一对多关联,支持区分中国香港/澳门/台湾等地区';
|
|
||||||
-- 字段注释
|
|
||||||
COMMENT ON COLUMN country_region_core_infos.region_id IS '子表主键ID,PG18原生UUIDv7';
|
|
||||||
COMMENT ON COLUMN country_region_core_infos.country_id IS '关联国家主表countries.country_id,外键约束,标识地区所属国家';
|
|
||||||
COMMENT ON COLUMN country_region_core_infos.region_code IS '地区编码,如HK(香港)/TW(台湾)/NYC(纽约),建议遵循ISO 3166-2设计为CN-HK/CN-TW';
|
|
||||||
COMMENT ON COLUMN country_region_core_infos.region_cn_short_name IS '地区中文简称,如香港/台湾/纽约,业务展示专用';
|
|
||||||
COMMENT ON COLUMN country_region_core_infos.deleted IS '逻辑删除标识,false-未删除,true-已删除,默认未删除';
|
|
||||||
COMMENT ON COLUMN country_region_core_infos.create_time IS '记录创建时间,默认当前系统时间,不可手动修改';
|
|
||||||
COMMENT ON COLUMN country_region_core_infos.update_time IS '记录最后更新时间,更新时触发器自动刷新';
|
|
||||||
|
|
||||||
-- 索引:优化高频业务查询(核心)
|
|
||||||
CREATE UNIQUE INDEX uk_country_region_core_infos_country_region ON country_region_core_infos (country_id, region_code) WHERE deleted = false; -- 唯一索引:同一国家下地区编码不重复(未删除数据)
|
|
||||||
CREATE INDEX idx_country_region_core_infos_country_id ON country_region_core_infos (country_id, deleted); -- 联合索引:按国家ID查询下属所有地区+过滤删除数据
|
|
||||||
CREATE INDEX idx_country_region_core_infos_name ON country_region_core_infos (region_cn_short_name, deleted); -- 联合索引:按地区名称模糊查询+过滤删除数据
|
|
||||||
|
|
||||||
-- 绑定触发器:更新时自动刷新update_time
|
|
||||||
CREATE TRIGGER tr_country_region_core_infos_update_time
|
|
||||||
BEFORE UPDATE ON country_region_core_infos
|
|
||||||
FOR EACH ROW
|
|
||||||
EXECUTE FUNCTION update_table_modify_time();
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
# Redis基础配置
|
|
||||||
bind 0.0.0.0
|
|
||||||
protected-mode no
|
|
||||||
port 6379
|
|
||||||
daemonize no
|
|
||||||
requirepass 123456
|
|
||||||
appendonly yes
|
|
||||||
appendfsync everysec
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
module country
|
|
||||||
|
|
||||||
go 1.25.7
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
-- PostgresSQL全局初始化脚本
|
|
||||||
-- 所有业务公共建库/建表语句写在这里,单独业务表建议在各自业务中处理
|
|
||||||
CREATE DATABASE IF NOT EXISTS monorepo;
|
|
||||||
\c monorepo;
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
# Redis基础配置
|
|
||||||
bind 0.0.0.0
|
|
||||||
protected-mode no
|
|
||||||
port 6379
|
|
||||||
daemonize no
|
|
||||||
requirepass 123456
|
|
||||||
appendonly yes
|
|
||||||
appendfsync everysec
|
|
||||||
Reference in New Issue
Block a user