feat: i18n

This commit is contained in:
meilin.huang
2024-11-20 22:43:53 +08:00
parent 74ae031853
commit 99a746085b
308 changed files with 8177 additions and 3880 deletions

View File

@@ -3,7 +3,6 @@ package base
import (
"context"
"fmt"
"mayfly-go/pkg/contextx"
"mayfly-go/pkg/global"
"mayfly-go/pkg/model"
@@ -162,13 +161,13 @@ func (ai *AppImpl[T, R]) CountByCond(cond any) int64 {
// Tx 执行事务操作
func (ai *AppImpl[T, R]) Tx(ctx context.Context, funcs ...func(context.Context) error) (err error) {
tx := contextx.GetTx(ctx)
tx := GetTxFromCtx(ctx)
dbCtx := ctx
var txDb *gorm.DB
if tx == nil {
txDb = global.Db.Begin()
dbCtx, tx = contextx.WithTxDb(ctx, txDb)
dbCtx, tx = NewCtxWithTxDb(ctx, txDb)
} else {
txDb = tx.DB
tx.Count++

45
server/pkg/base/ctx.go Normal file
View File

@@ -0,0 +1,45 @@
package base
import (
"context"
"gorm.io/gorm"
)
type CtxKey string
const (
DbKey CtxKey = "db"
)
// Tx 事务上下文信息
type Tx struct {
Count int
DB *gorm.DB
}
// NewCtxWithTxDb 将事务db放置context中
func NewCtxWithTxDb(ctx context.Context, db *gorm.DB) (context.Context, *Tx) {
if tx := GetTxFromCtx(ctx); tx != nil {
return ctx, tx
}
tx := &Tx{Count: 1, DB: db}
return context.WithValue(ctx, DbKey, tx), tx
}
// GetDbFromCtx 获取ctx中的事务db
func GetDbFromCtx(ctx context.Context) *gorm.DB {
if tx := GetTxFromCtx(ctx); tx != nil {
return tx.DB
}
return nil
}
// GetTxFromCtx 获取当前ctx事务
func GetTxFromCtx(ctx context.Context) *Tx {
if tx, ok := ctx.Value(DbKey).(*Tx); ok {
return tx
}
return nil
}

View File

@@ -100,7 +100,7 @@ type RepoImpl[T model.ModelI] struct {
}
func (br *RepoImpl[T]) Insert(ctx context.Context, e T) error {
if db := contextx.GetDb(ctx); db != nil {
if db := GetDbFromCtx(ctx); db != nil {
return br.InsertWithDb(ctx, db, e)
}
return gormx.Insert(br.fillBaseInfo(ctx, e))
@@ -111,7 +111,7 @@ func (br *RepoImpl[T]) InsertWithDb(ctx context.Context, db *gorm.DB, e T) error
}
func (br *RepoImpl[T]) BatchInsert(ctx context.Context, es []T) error {
if db := contextx.GetDb(ctx); db != nil {
if db := GetDbFromCtx(ctx); db != nil {
return br.BatchInsertWithDb(ctx, db, es)
}
for _, e := range es {
@@ -129,7 +129,7 @@ func (br *RepoImpl[T]) BatchInsertWithDb(ctx context.Context, db *gorm.DB, es []
}
func (br *RepoImpl[T]) UpdateById(ctx context.Context, e T, columns ...string) error {
return br.UpdateByIdWithDb(ctx, contextx.GetDb(ctx), e, columns...)
return br.UpdateByIdWithDb(ctx, GetDbFromCtx(ctx), e, columns...)
}
func (br *RepoImpl[T]) UpdateByIdWithDb(ctx context.Context, db *gorm.DB, e T, columns ...string) error {
@@ -140,7 +140,7 @@ func (br *RepoImpl[T]) UpdateByIdWithDb(ctx context.Context, db *gorm.DB, e T, c
}
func (br *RepoImpl[T]) UpdateByCond(ctx context.Context, values any, cond any) error {
return br.UpdateByCondWithDb(ctx, contextx.GetDb(ctx), values, cond)
return br.UpdateByCondWithDb(ctx, GetDbFromCtx(ctx), values, cond)
}
func (br *RepoImpl[T]) UpdateByCondWithDb(ctx context.Context, db *gorm.DB, values any, cond any) error {
@@ -190,7 +190,7 @@ func (br *RepoImpl[T]) SaveWithDb(ctx context.Context, db *gorm.DB, e T) error {
}
func (br *RepoImpl[T]) DeleteById(ctx context.Context, id ...uint64) error {
if db := contextx.GetDb(ctx); db != nil {
if db := GetDbFromCtx(ctx); db != nil {
return br.DeleteByIdWithDb(ctx, db, id...)
}
return gormx.DeleteById(br.getModel(), id...)
@@ -201,7 +201,7 @@ func (br *RepoImpl[T]) DeleteByIdWithDb(ctx context.Context, db *gorm.DB, id ...
}
func (br *RepoImpl[T]) DeleteByCond(ctx context.Context, cond any) error {
if db := contextx.GetDb(ctx); db != nil {
if db := GetDbFromCtx(ctx); db != nil {
return br.DeleteByCondWithDb(ctx, db, cond)
}
return gormx.DeleteByCond(br.getModel(), toQueryCond(cond))

111
server/pkg/base/sql.go Normal file
View File

@@ -0,0 +1,111 @@
package base
import (
"embed"
"io/fs"
"path"
"path/filepath"
"strings"
)
// SQLStatement 结构体用于存储解析后的 SQL 语句及其注释
type SQLStatement struct {
Comment string
SQL string
}
var sqlMap = make(map[string]string)
func RegisterSql(fs embed.FS) error {
return walkDir(fs, ".", func(fp string, data []byte) error {
if filepath.Ext(fp) != ".sql" {
return nil
}
fileNameWithExt := path.Base(fp)
sqls, err := parseSQL(string(data))
if err != nil {
return err
}
filename := strings.TrimSuffix(fileNameWithExt, path.Ext(fileNameWithExt))
for _, sql := range sqls {
sqlMap[filename+"."+strings.TrimSpace(sql.Comment)] = strings.TrimSpace(sql.SQL)
}
return nil
})
}
func GetSQL(filename, stmt string) string {
return sqlMap[filename+"."+stmt]
}
// walkDir 递归遍历目录
func walkDir(fsys fs.FS, path string, callback func(filePath string, data []byte) error) error {
entries, err := fs.ReadDir(fsys, path)
if err != nil {
return err
}
for _, entry := range entries {
entryPath := filepath.Join(path, entry.Name())
if entry.IsDir() {
// 递归遍历子目录
if err := walkDir(fsys, entryPath, callback); err != nil {
return err
}
} else {
// 读取文件内容
data, err := fs.ReadFile(fsys, entryPath)
if err != nil {
return err
}
if err := callback(entryPath, data); err != nil {
return err
}
}
}
return nil
}
// parseSQL 解析带有注释的 SQL 语句
func parseSQL(sql string) ([]SQLStatement, error) {
var statements []SQLStatement
lines := strings.Split(sql, "\n")
var currentComment string
var currentSQL string
for _, line := range lines {
trimmedLine := strings.TrimSpace(line)
if strings.HasPrefix(trimmedLine, "--") {
// 处理单行注释
if currentSQL != "" {
statements = append(statements, SQLStatement{Comment: currentComment, SQL: strings.TrimRight(currentSQL, " ")})
currentComment = ""
currentSQL = ""
}
currentComment += strings.TrimPrefix(trimmedLine, "--") + "\n"
continue
}
if trimmedLine == "" {
continue
}
currentSQL += line + " "
if strings.HasSuffix(trimmedLine, ";") {
statements = append(statements, SQLStatement{Comment: currentComment, SQL: strings.TrimRight(currentSQL, " ")})
currentComment = ""
currentSQL = ""
}
}
// 处理最后一段未结束的 SQL 语句
if currentSQL != "" {
statements = append(statements, SQLStatement{Comment: currentComment, SQL: strings.TrimRight(currentSQL, " ")})
}
return statements, nil
}

View File

@@ -0,0 +1,28 @@
package base
import (
"fmt"
"testing"
)
func TestParserSql(t *testing.T) {
sql := `-- selectByCond
Select * from tdb where id > 10;
-- another comment
Select * from another_table where name = 'test'
and age = ?;
-- multi-line comment
-- continues here
Select * from yet_another_table
Where id = ?;`
statements, err := parseSQL(sql)
if err != nil {
fmt.Println("Error:", err)
return
}
for _, stmt := range statements {
fmt.Printf("Comment: %s\nSQL: %s\n\n", stmt.Comment, stmt.SQL)
}
}

View File

@@ -1,8 +1,10 @@
package biz
import (
"context"
"fmt"
"mayfly-go/pkg/errorx"
"mayfly-go/pkg/i18n"
"mayfly-go/pkg/utils/anyx"
"reflect"
@@ -11,9 +13,9 @@ import (
// 断言错误为ni
// @param msgAndParams 消息与参数占位符,第一位为错误消息可包含%s等格式化标识。其余为Sprintf格式化值内容
//
// ErrIsNil(err)
// ErrIsNil(err, "xxxx")
// ErrIsNil(err, "xxxx: %s", "yyyy")
// ErrIsNil(err)
// ErrIsNil(err, "xxxx")
// ErrIsNil(err, "xxxx: %s", "yyyy")
func ErrIsNil(err error, msgAndParams ...any) {
if err != nil {
if len(msgAndParams) == 0 {
@@ -24,24 +26,60 @@ func ErrIsNil(err error, msgAndParams ...any) {
}
}
// 断言错误为ni
// @param msgId i18n消息id
//
// ErrIsNil(err)
// ErrIsNil(err, "xxxx")
// ErrIsNil(err, "xxxx: %s", "yyyy")
func ErrIsNilI(ctx context.Context, err error, msgId i18n.MsgId, attrs ...any) {
if err != nil {
if len(attrs) == 0 {
panic(errorx.NewBiz(err.Error()))
}
panic(errorx.NewBiz(i18n.TC(ctx, msgId, attrs...)))
}
}
func ErrNotNil(err error, msg string, params ...any) {
if err == nil {
panic(errorx.NewBiz(fmt.Sprintf(msg, params...)))
}
}
// ErrIsNilAppendErr 断言错误为nil否则抛出业务异常异常消息可包含%s进行错误信息提示
//
// // -> xxxx: err.Error()
// biz.ErrIsNilAppendErr(err, "xxxx: %s")
func ErrIsNilAppendErr(err error, msg string) {
if err != nil {
panic(errorx.NewBiz(fmt.Sprintf(msg, err.Error())))
}
}
// ErrIsNilAppendErr 断言错误为nil否则抛出业务异常异常消息可包含%s进行错误信息提示
//
// // -> xxxx: err.Error()
// biz.ErrIsNilAppendErr(err, "xxxx: %s")
func ErrIsNilAppendErrI(ctx context.Context, err error, msgId i18n.MsgId) {
if err != nil {
panic(errorx.NewBiz(fmt.Sprintf(i18n.TC(ctx, msgId), err.Error())))
}
}
func IsTrue(exp bool, msg string, params ...any) {
if !exp {
panic(errorx.NewBiz(fmt.Sprintf(msg, params...)))
}
}
func IsTrueI(ctx context.Context, exp bool, msgId i18n.MsgId, attrs ...any) {
if !exp {
panic(errorx.NewBiz(i18n.TC(ctx, msgId, attrs...)))
}
}
func IsTrueBy(exp bool, err *errorx.BizError) {
if !exp {
panic(err)

View File

@@ -5,6 +5,7 @@ import (
)
type Server struct {
Lang string `yaml:"lang"`
Port int `yaml:"port"`
Model string `yaml:"model"`
ContextPath string `yaml:"context-path"` // 请求路径上下文
@@ -15,6 +16,9 @@ type Server struct {
}
func (s *Server) Default() {
if s.Lang == "" {
s.Lang = "zh_CN"
}
if s.Model == "" {
s.Model = "release"
}

View File

@@ -4,8 +4,6 @@ import (
"context"
"mayfly-go/pkg/model"
"mayfly-go/pkg/utils/stringx"
"gorm.io/gorm"
)
type CtxKey string
@@ -13,7 +11,6 @@ type CtxKey string
const (
LoginAccountKey CtxKey = "loginAccount"
TraceIdKey CtxKey = "traceId"
DbKey CtxKey = "db"
)
func NewLoginAccount(la *model.LoginAccount) context.Context {
@@ -24,7 +21,7 @@ func WithLoginAccount(ctx context.Context, la *model.LoginAccount) context.Conte
return context.WithValue(ctx, LoginAccountKey, la)
}
// 从context中获取登录账号信息不存在返回nil
// GetLoginAccount 从context中获取登录账号信息不存在返回nil
func GetLoginAccount(ctx context.Context) *model.LoginAccount {
if la, ok := ctx.Value(LoginAccountKey).(*model.LoginAccount); ok {
return la
@@ -36,6 +33,7 @@ func NewTraceId() context.Context {
return WithTraceId(context.Background())
}
// WithTraceId 将traceId放置context中
func WithTraceId(ctx context.Context) context.Context {
return context.WithValue(ctx, TraceIdKey, stringx.RandByChars(16, stringx.Nums+stringx.LowerChars))
}
@@ -47,35 +45,3 @@ func GetTraceId(ctx context.Context) string {
}
return ""
}
// Tx 事务上下文信息
type Tx struct {
Count int
DB *gorm.DB
}
// WithTxDb 将事务db放置context中
func WithTxDb(ctx context.Context, db *gorm.DB) (context.Context, *Tx) {
if tx := GetTx(ctx); tx != nil {
return ctx, tx
}
tx := &Tx{Count: 1, DB: db}
return context.WithValue(ctx, DbKey, tx), tx
}
// GetDb 获取ctx中的事务db
func GetDb(ctx context.Context) *gorm.DB {
if tx := GetTx(ctx); tx != nil {
return tx.DB
}
return nil
}
// GetTx 获取当前ctx事务
func GetTx(ctx context.Context) *Tx {
if tx, ok := ctx.Value(DbKey).(*Tx); ok {
return tx
}
return nil
}

View File

@@ -1,7 +1,9 @@
package errorx
import (
"context"
"fmt"
"mayfly-go/pkg/i18n"
)
// 业务错误
@@ -32,9 +34,17 @@ func (e BizError) String() string {
return fmt.Sprintf("errCode: %d, errMsg: %s", e.Code(), e.Error())
}
// 创建业务逻辑错误结构体,默认为业务逻辑错误
func NewBiz(msg string, formats ...any) *BizError {
return &BizError{code: BizErr.code, err: fmt.Sprintf(msg, formats...)}
// NewBiz 创建业务逻辑错误结构体,默认为业务逻辑错误
func NewBiz(msg string, formatValues ...any) *BizError {
return &BizError{code: BizErr.code, err: fmt.Sprintf(msg, formatValues...)}
}
// NewBizI 使用i18n的msgId创建业务逻辑错误结构体默认为业务逻辑错误 (使用ctx中的国际化语言)
//
// // NameErr = {{.name}} is invalid => xxx is invalid
// NewBizI(ctx, imsg.NameErr, "name", "xxxx")
func NewBizI(ctx context.Context, msgId i18n.MsgId, attrs ...any) *BizError {
return &BizError{code: BizErr.code, err: i18n.TC(ctx, msgId, attrs...)}
}
// 创建业务逻辑错误结构体可设置指定错误code

View File

@@ -113,7 +113,7 @@ func (bus *EventBus) Unsubscribe(topic string, subId string) error {
defer bus.lock.Unlock()
subManager := bus.subscriberManager[topic]
if subManager == nil {
return errors.New("该主题不存在订阅者")
return errors.New("there is no subscriber for this topic")
}
subManager.delSubscriber(subId)
return nil
@@ -122,7 +122,7 @@ func (bus *EventBus) Unsubscribe(topic string, subId string) error {
func (bus *EventBus) Publish(ctx context.Context, topic string, val any) {
bus.lock.Lock() // will unlock if handler is not found or always after setUpPublish
defer bus.lock.Unlock()
logx.Debugf("主题-[%s]-发布了事件", topic)
logx.Debugf("topic - [%s] - published the event", topic)
event := &Event{
Topic: topic,
Val: val,
@@ -138,7 +138,7 @@ func (bus *EventBus) Publish(ctx context.Context, topic string, val any) {
}
for subId, handler := range handlers {
logx.Debugf("订阅者-[%s]-开始执行主题-[%s]-发布的事件", subId, topic)
logx.Debugf("subscriber - [%s] - starts executing events published by topic - [%s]", subId, topic)
if handler.once {
subscriberManager.delSubscriber(subId)
}
@@ -164,7 +164,7 @@ func (bus *EventBus) WaitAsync() {
func (bus *EventBus) doSubscribe(topic string, subId string, handler *eventHandler) error {
bus.lock.Lock()
defer bus.lock.Unlock()
logx.Debugf("订阅者-[%s]-订阅了主题-[%s]", subId, topic)
logx.Debugf("subscribers - [%s] - subscribed to topic - [%s]", subId, topic)
subManager := bus.subscriberManager[topic]
if subManager == nil {
subManager = NewSubscriberManager()
@@ -177,7 +177,7 @@ func (bus *EventBus) doSubscribe(topic string, subId string, handler *eventHandl
func (bus *EventBus) doPublish(ctx context.Context, handler *eventHandler, event *Event) error {
err := handler.handlerFunc(ctx, event)
if err != nil {
logx.Errorf("订阅者执行主题[%s]失败: %s", event.Topic, err.Error())
logx.Errorf("subscriber failed to execute topic [%s]: %s", event.Topic, err.Error())
}
return err
}

22
server/pkg/i18n/ctx.go Normal file
View File

@@ -0,0 +1,22 @@
package i18n
import "context"
type CtxKey string
const (
LangKey CtxKey = "lang"
)
// NewCtxWithLang 将lang放置context中
func NewCtxWithLang(ctx context.Context, lang string) context.Context {
return context.WithValue(ctx, LangKey, lang)
}
// GetLangFromCtx 从context中获取lang
func GetLangFromCtx(ctx context.Context) string {
if val, ok := ctx.Value(LangKey).(string); ok {
return val
}
return ""
}

68
server/pkg/i18n/i18n.go Normal file
View File

@@ -0,0 +1,68 @@
package i18n
import (
"context"
"mayfly-go/pkg/utils/collx"
"mayfly-go/pkg/utils/stringx"
)
type MsgId int
const (
Zh_CN = "zh-cn"
En = "en"
)
// langMsgs key=lang, value=msgs
var langMsgs = make(map[string]map[MsgId]string)
var defaultLang = Zh_CN
// AppendLangMsg append lang msg
func AppendLangMsg(lang string, msgs map[MsgId]string) {
langMsgs[lang] = collx.MapMerge(langMsgs[lang], msgs)
}
// SetLang set default lang
func SetLang(lang string) {
defaultLang = lang
}
// T load msg by key, and use default lang
//
// // NameErr = {{.name}} is invalid => xxx is invalid
// T(imsg.NameErr, "name", "xxxx")
func T(msgId MsgId, attrs ...any) string {
return TL(defaultLang, msgId, attrs...)
}
// TC load msg by key, and use context lang
func TC(ctx context.Context, msgId MsgId, attrs ...any) string {
return TL(GetLangFromCtx(ctx), msgId, attrs...)
}
// TL load msg by lang
//
// // NameErr = {{.name}} is invalid => xxx is invalid
// T(imsg.NameErr, "name", "xxxx")
func TL(lang string, msgId MsgId, attrs ...any) string {
if lang == "" {
lang = defaultLang
}
msgs := langMsgs[lang]
if msgs == nil {
msgs = langMsgs[defaultLang]
}
msg := msgs[msgId]
if len(attrs) == 0 {
return msg
}
if parseMsg, err := stringx.TemplateParse(msg, collx.Kvs(attrs...)); err != nil {
return msg
} else {
return parseMsg
}
}

View File

@@ -67,7 +67,7 @@ func (c *Container) Inject(obj any) error {
// 对所有组件实例执行Inject。即为实例字段注入依赖的组件实例
func (c *Container) InjectComponents() error {
componentsGroups := collx.ArraySplit[*Component](collx.MapValues(c.components), 3)
componentsGroups := collx.ArraySplit[*Component](collx.MapValues(c.components), 5)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

View File

@@ -4,6 +4,7 @@ import (
"fmt"
"mayfly-go/pkg/contextx"
"mayfly-go/pkg/errorx"
"mayfly-go/pkg/i18n"
"mayfly-go/pkg/logx"
"mayfly-go/pkg/utils/anyx"
"mayfly-go/pkg/utils/runtimex"
@@ -25,16 +26,26 @@ type LogInfo struct {
save bool // 是否保存日志
}
// 新建日志信息,默认不保存该日志
// NewLog 新建日志信息,默认不保存该日志
func NewLog(description string) *LogInfo {
return &LogInfo{Description: description, LogResp: false, save: false}
}
// 新建日志信息,并且需要保存该日志信息
// NewLogI 使用msgId新建日志信息,默认不保存该日志
func NewLogI(msgId i18n.MsgId) *LogInfo {
return &LogInfo{Description: i18n.T(msgId), LogResp: false, save: false}
}
// NewLogSave 新建日志信息,并且需要保存该日志信息
func NewLogSave(description string) *LogInfo {
return &LogInfo{Description: description, LogResp: false, save: true}
}
// NewLogSaveI 使用msgId新建日志信息并且需要保存该日志信息
func NewLogSaveI(msgId i18n.MsgId) *LogInfo {
return &LogInfo{Description: i18n.T(msgId), LogResp: false, save: true}
}
// 记录返回结果
func (i *LogInfo) WithLogResp() *LogInfo {
i.LogResp = true

View File

@@ -5,6 +5,7 @@ import (
"mayfly-go/pkg/biz"
"mayfly-go/pkg/contextx"
"mayfly-go/pkg/errorx"
"mayfly-go/pkg/i18n"
"mayfly-go/pkg/logx"
"mayfly-go/pkg/model"
"mayfly-go/pkg/utils/assert"
@@ -28,7 +29,10 @@ type Ctx struct {
}
func NewCtx(f F) *Ctx {
ctx := &Ctx{MetaCtx: contextx.WithTraceId(f.GetRequest().Context())}
metaCtx := contextx.WithTraceId(f.GetRequest().Context())
metaCtx = i18n.NewCtxWithLang(metaCtx, f.GetRequest().Header.Get("Accept-Language"))
ctx := &Ctx{MetaCtx: metaCtx}
ctx.wrapperF = NewWrapperF(f)
return ctx
}

View File

@@ -5,6 +5,7 @@ import (
"errors"
"mayfly-go/initialize"
"mayfly-go/pkg/config"
"mayfly-go/pkg/i18n"
"mayfly-go/pkg/logx"
"mayfly-go/pkg/req"
"net/http"
@@ -28,6 +29,8 @@ func runWebServer(ctx context.Context) {
// 设置日志保存函数
req.SetSaveLogFunc(sysapp.GetSyslogApp().SaveFromReq)
i18n.SetLang(config.Conf.Server.Lang)
srv := http.Server{
Addr: config.Conf.Server.GetPort(),
// 注册路由
@@ -41,7 +44,7 @@ func runWebServer(ctx context.Context) {
defer cancel()
err := srv.Shutdown(timeout)
if err != nil {
logx.Errorf("Failed to Shutdown HTTP Server: %v", err)
logx.Errorf("failed to Shutdown HTTP Server: %v", err)
}
initialize.Terminate()