diff --git a/constants/code.go b/constants/code.go index b372e12..5433ea3 100644 --- a/constants/code.go +++ b/constants/code.go @@ -4,7 +4,7 @@ import "sync" // ICodeType 错误码类型接口 type ICodeType interface { - String() string + String(language ...string) string Int() int } @@ -13,17 +13,19 @@ type CodeType int type errorCodeTypeMsgMap struct { mux *sync.RWMutex - maps map[CodeType]string + maps map[LanguageCode]map[ICodeType]string } var ctm *errorCodeTypeMsgMap // RegisterCode 注册常量代码 // +// @param i18nMsg key为错误码值,value为错误信息 +// // 当code码重复时,后者覆盖前者 -func RegisterCode(code CodeType, msg string) { +func RegisterCode(language LanguageCode, i18nMsg map[ICodeType]string) { ctm.mux.Lock() - ctm.maps[code] = msg + ctm.maps[language] = i18nMsg ctm.mux.Unlock() } @@ -31,11 +33,11 @@ func init() { (&sync.Once{}).Do(func() { ctm = &errorCodeTypeMsgMap{ mux: &sync.RWMutex{}, - maps: make(map[CodeType]string), + maps: make(map[LanguageCode]map[ICodeType]string), } - for code, msg := range initErrorCodeMsgMap { - RegisterCode(code, msg) + for language, msg := range initErrorCodeMsgMap { + RegisterCode(language, msg) } }) } diff --git a/constants/errors.go b/constants/errors.go index 1ca4360..c9e57a6 100644 --- a/constants/errors.go +++ b/constants/errors.go @@ -13,22 +13,44 @@ const ( // 错误码信息表 // // READONLY for concurrency safety -var initErrorCodeMsgMap = map[CodeType]string{ - ErrNone: "SUCCESS", - ErrRequestParamsInvalid: "Bad request parameters", - ErrAuthorizationTokenInvalid: "Authorization token invalid", - ErrInternalServerError: "Internal server error", +var initErrorCodeMsgMap = map[LanguageCode]map[ICodeType]string{ + LanguageEnglish: { + ErrNone: "SUCCESS", + ErrRequestParamsInvalid: "Bad request parameters", + ErrAuthorizationTokenInvalid: "Authorization token invalid", + ErrInternalServerError: "Internal server error", + }, + LanguageEnglishUnitedStates: { + ErrNone: "SUCCESS", + ErrRequestParamsInvalid: "Bad request parameters", + ErrAuthorizationTokenInvalid: "Authorization token invalid", + ErrInternalServerError: "Internal server error", + }, + LanguageChinesePRC: { + ErrNone: "成功", + ErrRequestParamsInvalid: "无效的请求参数", + ErrAuthorizationTokenInvalid: "无效的授权令牌", + ErrInternalServerError: "服务器内部错误", + }, } // String 获取错误信息字符 -func (ct CodeType) String() string { +func (ct CodeType) String(language ...string) string { + var lang = LanguageEnglish + if len(language) > 0 { + //TODO 这里需要考虑当语言不存在时,是否强制使用某种存在的语言代码 + lang = LanguageCode(language[0]) + } ctm.mux.RLock() defer ctm.mux.RUnlock() - if msg, ok := ctm.maps[ct]; ok { - return msg + if i18nMsg, ok := ctm.maps[lang]; ok { + if msg, iOk := i18nMsg[ct]; iOk { + return msg + } + return fmt.Sprintf("[Warn] ErrorCode {%d} Language {%s} not defined!", ct, lang) } - return fmt.Sprintf("[Warn] ErrorCode {%d} not defined!", ct) + return fmt.Sprintf("[Warn] ErrorCode {%d} Language {%s} not defined!", ct, lang) } // Int 获取错误码 diff --git a/constants/i18n.go b/constants/i18n.go new file mode 100644 index 0000000..8a83b80 --- /dev/null +++ b/constants/i18n.go @@ -0,0 +1,293 @@ +package constants + +import "strings" + +// LanguageCode 语言码 +// +// 采用ISO 3166-1标准 +// 例如: +// +// 美式英文: en-US +// +// 简体中文: zh-CN +// +// 繁体中文: zh-TW +// +// @see https://en.wikipedia.org/wiki/ISO_3166-1 +type LanguageCode string + +const ( + LanguageAfrikaans LanguageCode = "af" + LanguageAlbanian LanguageCode = "sq" + LanguageArabicAlgeria LanguageCode = "ar-DZ" + LanguageArabicBahrain LanguageCode = "ar-BH" + LanguageArabicEgypt LanguageCode = "ar-EG" + LanguageArabicIraq LanguageCode = "ar-IQ" + LanguageArabicJordan LanguageCode = "ar-JO" + LanguageArabicKuwait LanguageCode = "ar-KW" + LanguageArabicLebanon LanguageCode = "ar-LB" + LanguageArabicLibya LanguageCode = "ar-LY" + LanguageArabicMorocco LanguageCode = "ar-MA" + LanguageArabicOman LanguageCode = "ar-OM" + LanguageArabicQatar LanguageCode = "ar-QA" + LanguageArabicSaudiArabia LanguageCode = "ar-SA" + LanguageArabicSyria LanguageCode = "ar-SY" + LanguageArabicTunisia LanguageCode = "ar-TN" + LanguageArabicUAE LanguageCode = "ar-AE" + LanguageArabicYemen LanguageCode = "ar-YE" + LanguageBasque LanguageCode = "eu" + LanguageBelarusian LanguageCode = "be" + LanguageBulgarian LanguageCode = "bg" + LanguageCatalan LanguageCode = "ca" + LanguageChineseHongKong LanguageCode = "zh-HK" + LanguageChinesePRC LanguageCode = "zh-CN" + LanguageChineseSingapore LanguageCode = "zh-SG" + LanguageChineseTaiwan LanguageCode = "zh-TW" + LanguageCroatian LanguageCode = "hr" + LanguageCzech LanguageCode = "cs" + LanguageDanish LanguageCode = "da" + LanguageDutchBelgium LanguageCode = "nl-BE" + LanguageDutchStandard LanguageCode = "nl" + LanguageEnglish LanguageCode = "en" + LanguageEnglishAustralia LanguageCode = "en-AU" + LanguageEnglishBelize LanguageCode = "en-BZ" + LanguageEnglishCanada LanguageCode = "en-CA" + LanguageEnglishIreland LanguageCode = "en-IE" + LanguageEnglishJamaica LanguageCode = "en-JM" + LanguageEnglishNewZealand LanguageCode = "en-NZ" + LanguageEnglishSouthAfrica LanguageCode = "en-ZA" + LanguageEnglishTrinidad LanguageCode = "en-TT" + LanguageEnglishUnitedKingdom LanguageCode = "en-GB" + LanguageEnglishUnitedStates LanguageCode = "en-US" + LanguageEstonian LanguageCode = "et" + LanguageFaeroese LanguageCode = "fo" + LanguageFarsi LanguageCode = "fa" + LanguageFinnish LanguageCode = "fi" + LanguageFrenchBelgium LanguageCode = "fr-BE" + LanguageFrenchCanada LanguageCode = "fr-CA" + LanguageFrenchLuxembourg LanguageCode = "fr-LU" + LanguageFrenchStandard LanguageCode = "fr" + LanguageFrenchSwitzerland LanguageCode = "fr-CH" + LanguageGaelicScotland LanguageCode = "gd" + LanguageGermanAustria LanguageCode = "de-AT" + LanguageGermanLiechtenstein LanguageCode = "de-LI" + LanguageGermanLuxembourg LanguageCode = "de-LU" + LanguageGermanStandard LanguageCode = "de" + LanguageGermanSwitzerland LanguageCode = "de-CH" + LanguageGreek LanguageCode = "el" + LanguageHebrew LanguageCode = "he" + LanguageHindi LanguageCode = "hi" + LanguageHungarian LanguageCode = "hu" + LanguageIcelandic LanguageCode = "is" + LanguageIndonesian LanguageCode = "id" + LanguageIrish LanguageCode = "ga" + LanguageItalianStandard LanguageCode = "it" + LanguageItalianSwitzerland LanguageCode = "it-CH" + LanguageJapanese LanguageCode = "ja" + LanguageKorean LanguageCode = "ko" + LanguageKoreanJohab LanguageCode = "ko" + LanguageKurdish LanguageCode = "ku" + LanguageLatvian LanguageCode = "lv" + LanguageLithuanian LanguageCode = "lt" + LanguageMacedonianFYROM LanguageCode = "mk" + LanguageMalayalam LanguageCode = "ml" + LanguageMalaysian LanguageCode = "ms" + LanguageMaltese LanguageCode = "mt" + LanguageNorwegian LanguageCode = "no" + LanguageNorwegianBokmal LanguageCode = "nb" + LanguageNorwegianNynorsk LanguageCode = "nn" + LanguagePolish LanguageCode = "pl" + LanguagePortugueseBrazil LanguageCode = "pt-BR" + LanguagePortuguesePortugal LanguageCode = "pt" + LanguagePunjabi LanguageCode = "pa" + LanguageRhaetoRomanic LanguageCode = "rm" + LanguageRomanian LanguageCode = "ro" + LanguageRomanianRepublicOfMoldova LanguageCode = "ro-MD" + LanguageRussian LanguageCode = "ru" + LanguageRussianRepublicOfMoldova LanguageCode = "ru-MD" + LanguageSerbian LanguageCode = "sr" + LanguageSlovak LanguageCode = "sk" + LanguageSlovenian LanguageCode = "sl" + LanguageSorbian LanguageCode = "sb" + LanguageSpanishArgentina LanguageCode = "es-AR" + LanguageSpanishBolivia LanguageCode = "es-BO" + LanguageSpanishChile LanguageCode = "es-CL" + LanguageSpanishColombia LanguageCode = "es-CO" + LanguageSpanishCostaRica LanguageCode = "es-CR" + LanguageSpanishDominicanRepublic LanguageCode = "es-DO" + LanguageSpanishEcuador LanguageCode = "es-EC" + LanguageSpanishElSalvador LanguageCode = "es-SV" + LanguageSpanishGuatemala LanguageCode = "es-GT" + LanguageSpanishHonduras LanguageCode = "es-HN" + LanguageSpanishMexico LanguageCode = "es-MX" + LanguageSpanishNicaragua LanguageCode = "es-NI" + LanguageSpanishPanama LanguageCode = "es-PA" + LanguageSpanishParaguay LanguageCode = "es-PY" + LanguageSpanishPeru LanguageCode = "es-PE" + LanguageSpanishPuertoRico LanguageCode = "es-PR" + LanguageSpanishSpain LanguageCode = "es" + LanguageSpanishUruguay LanguageCode = "es-UY" + LanguageSpanishVenezuela LanguageCode = "es-VE" + LanguageSwedish LanguageCode = "sv" + LanguageSwedishFinland LanguageCode = "sv-FI" + LanguageThai LanguageCode = "th" + LanguageTsonga LanguageCode = "ts" + LanguageTswana LanguageCode = "tn" + LanguageTurkish LanguageCode = "tr" + LanguageUkrainian LanguageCode = "ua" + LanguageUrdu LanguageCode = "ur" + LanguageVenda LanguageCode = "ve" + LanguageVietnamese LanguageCode = "vi" + LanguageWelsh LanguageCode = "cy" + LanguageXhosa LanguageCode = "xh" + LanguageYiddish LanguageCode = "ji" + LanguageZulu LanguageCode = "zu" +) + +var i18n = []LanguageCode{ + LanguageAfrikaans, + LanguageAlbanian, + LanguageArabicAlgeria, + LanguageArabicBahrain, + LanguageArabicEgypt, + LanguageArabicIraq, + LanguageArabicJordan, + LanguageArabicKuwait, + LanguageArabicLebanon, + LanguageArabicLibya, + LanguageArabicMorocco, + LanguageArabicOman, + LanguageArabicQatar, + LanguageArabicSaudiArabia, + LanguageArabicSyria, + LanguageArabicTunisia, + LanguageArabicUAE, + LanguageArabicYemen, + LanguageBasque, + LanguageBelarusian, + LanguageBulgarian, + LanguageCatalan, + LanguageChineseHongKong, + LanguageChinesePRC, + LanguageChineseSingapore, + LanguageChineseTaiwan, + LanguageCroatian, + LanguageCzech, + LanguageDanish, + LanguageDutchBelgium, + LanguageDutchStandard, + LanguageEnglish, + LanguageEnglishAustralia, + LanguageEnglishBelize, + LanguageEnglishCanada, + LanguageEnglishIreland, + LanguageEnglishJamaica, + LanguageEnglishNewZealand, + LanguageEnglishSouthAfrica, + LanguageEnglishTrinidad, + LanguageEnglishUnitedKingdom, + LanguageEnglishUnitedStates, + LanguageEstonian, + LanguageFaeroese, + LanguageFarsi, + LanguageFinnish, + LanguageFrenchBelgium, + LanguageFrenchCanada, + LanguageFrenchLuxembourg, + LanguageFrenchStandard, + LanguageFrenchSwitzerland, + LanguageGaelicScotland, + LanguageGermanAustria, + LanguageGermanLiechtenstein, + LanguageGermanLuxembourg, + LanguageGermanStandard, + LanguageGermanSwitzerland, + LanguageGreek, + LanguageHebrew, + LanguageHindi, + LanguageHungarian, + LanguageIcelandic, + LanguageIndonesian, + LanguageIrish, + LanguageItalianStandard, + LanguageItalianSwitzerland, + LanguageJapanese, + LanguageKorean, + LanguageKoreanJohab, + LanguageKurdish, + LanguageLatvian, + LanguageLithuanian, + LanguageMacedonianFYROM, + LanguageMalayalam, + LanguageMalaysian, + LanguageMaltese, + LanguageNorwegian, + LanguageNorwegianBokmal, + LanguageNorwegianNynorsk, + LanguagePolish, + LanguagePortugueseBrazil, + LanguagePortuguesePortugal, + LanguagePunjabi, + LanguageRhaetoRomanic, + LanguageRomanian, + LanguageRomanianRepublicOfMoldova, + LanguageRussian, + LanguageRussianRepublicOfMoldova, + LanguageSerbian, + LanguageSlovak, + LanguageSlovenian, + LanguageSorbian, + LanguageSpanishArgentina, + LanguageSpanishBolivia, + LanguageSpanishChile, + LanguageSpanishColombia, + LanguageSpanishCostaRica, + LanguageSpanishDominicanRepublic, + LanguageSpanishEcuador, + LanguageSpanishElSalvador, + LanguageSpanishGuatemala, + LanguageSpanishHonduras, + LanguageSpanishMexico, + LanguageSpanishNicaragua, + LanguageSpanishPanama, + LanguageSpanishParaguay, + LanguageSpanishPeru, + LanguageSpanishPuertoRico, + LanguageSpanishSpain, + LanguageSpanishUruguay, + LanguageSpanishVenezuela, + LanguageSwedish, + LanguageSwedishFinland, + LanguageThai, + LanguageTsonga, + LanguageTswana, + LanguageTurkish, + LanguageUkrainian, + LanguageUrdu, + LanguageVenda, + LanguageVietnamese, + LanguageWelsh, + LanguageXhosa, + LanguageYiddish, + LanguageZulu, +} + +func (lc LanguageCode) ToLowerCase() string { + return strings.ToLower(string(lc)) +} + +func (lc LanguageCode) ToUpperCase() string { + return strings.ToUpper(string(lc)) +} + +func (lc LanguageCode) Exist() bool { + var exist bool + for _, v := range i18n { + if v == lc { + exist = true + break + } + } + + return exist +} diff --git a/http/api/option.go b/http/api/option.go index a6b2ac6..5eab5c3 100644 --- a/http/api/option.go +++ b/http/api/option.go @@ -8,10 +8,11 @@ import ( ) var ( - anotherErrNoneCode constants.ICodeType = constants.ErrNone //被改写后的成功code码 - emptyDataField interface{} = nil //空data字段 - forceHttpCode200 = false //强制使用200作为http的状态码 - timezone = constants.DefaultTimeZone //时区 + anotherErrNoneCode constants.ICodeType = constants.ErrNone //被改写后的成功code码 + emptyDataField interface{} = nil //空data字段 + forceHttpCode200 = false //强制使用200作为http的状态码 + timezone = constants.DefaultTimeZone //时区 + detectAcceptLanguage = false //是否检测客户端语言 ) var ( @@ -32,6 +33,8 @@ type Option struct { ForceHttpCode200 bool //时区 Timezone string + //是否检测客户端语言,用于错误码消息返回 + DetectAcceptLanguage bool } const ( @@ -50,7 +53,7 @@ const ( // 2.空数据序列化结构 func SetupOption(opt Option) { if opt.ErrNoneCode != nil { - constants.RegisterCode(constants.CodeType(opt.ErrNoneCode.Int()), opt.ErrNoneCodeMsg) + constants.RegisterCode(constants.LanguageEnglish, map[constants.ICodeType]string{opt.ErrNoneCode: opt.ErrNoneCodeMsg}) anotherErrNoneCode = opt.ErrNoneCode } switch opt.EmptyDataStruct { @@ -72,6 +75,8 @@ func SetupOption(opt Option) { timezone = opt.Timezone } + detectAcceptLanguage = opt.DetectAcceptLanguage + lc, err := time.LoadLocation(timezone) if err != nil { panic(fmt.Errorf("[GO-SAIL] can not load location: %s", timezone)) diff --git a/http/api/response.go b/http/api/response.go index 447c416..ca62b40 100644 --- a/http/api/response.go +++ b/http/api/response.go @@ -10,24 +10,36 @@ import ( "github.com/keepchen/go-sail/v3/http/pojo/dto" ) -type Emitter interface { - Builder(code constants.ICodeType, resp dto.IResponse, message ...string) Emitter - Assemble(code constants.ICodeType, resp dto.IResponse, message ...string) Emitter - Status(httpCode int) Emitter +// Responder 响应器 +type Responder interface { + // Builder 组装返回数据 + // + // Assemble 方法的语法糖 + Builder(code constants.ICodeType, resp dto.IResponse, message ...string) Responder + // Assemble 组装返回数据 + // + // 该方法会根据传递的code码自动设置http状态、描述信息、当前系统毫秒时间戳以及请求id(需要在路由配置中调用middleware.RequestEntry中间件) + Assemble(code constants.ICodeType, resp dto.IResponse, message ...string) Responder + // Status 指定http状态码 + // + // 该方法会覆盖 Assemble 解析的http状态码值 + Status(httpCode int) Responder + // SendWithCode 以指定http状态码响应请求 SendWithCode(httpCode int) + // Send 响应请求 Send() } -type API struct { +type responseEngine struct { engine *gin.Context httpCode int data interface{} } -var _ Emitter = &API{} +var _ Responder = &responseEngine{} -func New(c *gin.Context) Emitter { - return &API{ +func New(c *gin.Context) Responder { + return &responseEngine{ engine: c, } } @@ -41,26 +53,35 @@ func New(c *gin.Context) Emitter { // Response(c).Builder(...).Send() // // New 方法的语法糖 -func Response(c *gin.Context) Emitter { +func Response(c *gin.Context) Responder { return New(c) } // Builder 组装返回数据 // // Assemble 方法的语法糖 -func (a *API) Builder(code constants.ICodeType, resp dto.IResponse, message ...string) Emitter { +func (a *responseEngine) Builder(code constants.ICodeType, resp dto.IResponse, message ...string) Responder { return a.Assemble(code, resp, message...) } // Assemble 组装返回数据 // -// 该方法会根据传递的code码自动设置http状态、描述信息、当前系统毫秒时间戳以及请求id(需要在路由配置中调用middleware.Before中间件) -func (a *API) Assemble(code constants.ICodeType, resp dto.IResponse, message ...string) Emitter { +// 该方法会根据传递的code码自动设置http状态、描述信息、当前系统毫秒时间戳以及请求id(需要在路由配置中调用middleware.RequestEntry中间件) +func (a *responseEngine) Assemble(code constants.ICodeType, resp dto.IResponse, message ...string) Responder { var ( body dto.Base requestId string httpCode int + language = []string{"en"} ) + //从上下文中获取语言代码 + if detectAcceptLanguage { + if acceptLanguage, ok := a.engine.Get("language"); ok { + if lang, assertOk := acceptLanguage.(string); assertOk { + language[0] = lang + } + } + } //从header中读取X-Request-Id if r1Id := a.engine.GetHeader("X-Request-Id"); len(r1Id) > 0 { requestId = r1Id @@ -79,7 +100,7 @@ func (a *API) Assemble(code constants.ICodeType, resp dto.IResponse, message ... //改写了默认成功code码,且当前code码为None时,需要使用改写后的值 body.Code = anotherErrNoneCode } - body.Message = body.Code.String() + body.Message = body.Code.String(language...) body.Timestamp = time.Now().In(loc).UnixMilli() switch code { case constants.ErrNone, anotherErrNoneCode: @@ -134,18 +155,18 @@ func (a *API) Assemble(code constants.ICodeType, resp dto.IResponse, message ... // Status 指定http状态码 // // 该方法会覆盖 Assemble 解析的http状态码值 -func (a *API) Status(httpCode int) Emitter { +func (a *responseEngine) Status(httpCode int) Responder { a.httpCode = httpCode return a } // SendWithCode 以指定http状态码响应请求 -func (a *API) SendWithCode(httpCode int) { +func (a *responseEngine) SendWithCode(httpCode int) { a.engine.AbortWithStatusJSON(httpCode, a.data) } // Send 响应请求 -func (a *API) Send() { +func (a *responseEngine) Send() { a.SendWithCode(a.httpCode) } diff --git a/http/middleware/detectuseragentlanguage.go b/http/middleware/detectuseragentlanguage.go index 7ea22bb..18fdedd 100644 --- a/http/middleware/detectuseragentlanguage.go +++ b/http/middleware/detectuseragentlanguage.go @@ -10,12 +10,13 @@ import ( // // 通过解析请求头中的`Accept-Language`字段得到 // -// 默认为`en-US` +// 默认为`en` func DetectUserAgentLanguage() gin.HandlerFunc { return func(c *gin.Context) { - var language = "en-US" + var language = "en" al := c.Request.Header.Get("accept-language") if len(al) > 0 { + //example: zh-CN,zh;q=0.9,en;q=0.8,ja;q=0.7 als := strings.Split(al, ",") if len(als) > 0 { language = als[0] diff --git a/http/middleware/requestentry.go b/http/middleware/requestentry.go index 62bc344..2b7fa56 100644 --- a/http/middleware/requestentry.go +++ b/http/middleware/requestentry.go @@ -1,6 +1,7 @@ package middleware import ( + "strings" "time" "github.com/gin-gonic/gin" @@ -19,7 +20,9 @@ import ( // // 3.包装了请求id的日志组件实例 // -// 作用是在请求入口注入必要的内容到上下文,供后续的请求调用链使用 +// 4.客户端语言代码 +// +// 作用是在请求入口注入必要的内容到上下文,供后续的请求调用链使用,一般用于日志追踪、链路追踪 func RequestEntry() gin.HandlerFunc { return func(c *gin.Context) { var ( @@ -43,6 +46,18 @@ func RequestEntry() gin.HandlerFunc { c.Set("logger", logger.GetLogger().With(zap.String("requestId", requestId), zap.String("spanId", spanId))) + //解析客户端语言并注入上下文 + var language = "en" + al := c.Request.Header.Get("accept-language") + if len(al) > 0 { + //example: zh-CN,zh;q=0.9,en;q=0.8,ja;q=0.7 + als := strings.Split(al, ",") + if len(als) > 0 { + language = als[0] + } + } + c.Set("language", language) + c.Next() } } diff --git a/lib/email/pool.go b/lib/email/pool.go index cf2791a..0f4406e 100644 --- a/lib/email/pool.go +++ b/lib/email/pool.go @@ -17,8 +17,22 @@ type Envelope struct { Callback func(e *Envelope, err error) //回调函数 } -// Pool 发送池 -type Pool struct { +// Sender 发送者 +type Sender interface { + // Mount 加入发送队列 + Mount(index int, envelope *Envelope) + // Emit 启动队列 + Emit() + // Done 发送完成 + Done() + // 初始化发送协程 + makeWorkers() + // 执行发送操作 + send(ep *Envelope) error +} + +// sendPool 发送池 +type sendPool struct { workers []chan *Envelope throttle time.Duration workersCount int @@ -27,9 +41,11 @@ type Pool struct { exit chan struct{} } +var _ Sender = &sendPool{} + // NewPool 实例化 -func NewPool(conf Conf) *Pool { - pool := &Pool{ +func NewPool(conf Conf) Sender { + pool := &sendPool{ conf: conf, workersCount: conf.Workers, throttle: time.Duration(conf.WorkerThrottleSeconds) * time.Second, @@ -42,7 +58,7 @@ func NewPool(conf Conf) *Pool { } // Mount 加入发送队列 -func (p *Pool) Mount(index int, envelope *Envelope) { +func (p *sendPool) Mount(index int, envelope *Envelope) { p.wg.Add(1) go func(ep *Envelope) { p.workers[index%p.workersCount] <- ep @@ -50,7 +66,7 @@ func (p *Pool) Mount(index int, envelope *Envelope) { } // Emit 启动队列 -func (p *Pool) Emit() { +func (p *sendPool) Emit() { handler := func(index int, wk chan *Envelope) { LOOP: for { @@ -79,7 +95,7 @@ func (p *Pool) Emit() { } // Done 发送完成 -func (p *Pool) Done() { +func (p *sendPool) Done() { p.wg.Wait() for index := range p.workers { close(p.workers[index]) @@ -89,7 +105,7 @@ func (p *Pool) Done() { } // 初始化发送协程 -func (p *Pool) makeWorkers() { +func (p *sendPool) makeWorkers() { if p.workersCount < 1 { p.workersCount = 1 } @@ -105,7 +121,7 @@ func (p *Pool) makeWorkers() { } // 执行发送操作 -func (p *Pool) send(ep *Envelope) error { +func (p *sendPool) send(ep *Envelope) error { headers := map[string]string{ "From": ep.From, "Subject": ep.Subject, diff --git a/sail/components.go b/sail/components.go index ec04a20..ba45890 100644 --- a/sail/components.go +++ b/sail/components.go @@ -61,7 +61,7 @@ func GetLogger(module ...string) *zap.Logger { } // Response http响应组件 -func Response(c *gin.Context) api.Emitter { +func Response(c *gin.Context) api.Responder { return api.New(c) } diff --git a/sail/httpserver/gin.go b/sail/httpserver/gin.go index a80db05..d479750 100644 --- a/sail/httpserver/gin.go +++ b/sail/httpserver/gin.go @@ -10,6 +10,8 @@ import ( "sync" "syscall" + "github.com/keepchen/go-sail/v3/http/middleware" + "github.com/keepchen/go-sail/v3/sail/config" "github.com/gin-gonic/gin" @@ -19,6 +21,10 @@ import ( ) // InitGinEngine 初始化gin引擎 +// +// # Note: +// +// 该方法会默认使用 RequestEntry 中间件 func InitGinEngine(conf config.HttpServerConf) *gin.Engine { var r *gin.Engine if conf.Debug { @@ -30,6 +36,8 @@ func InitGinEngine(conf config.HttpServerConf) *gin.Engine { r = gin.New() } + r.Use(middleware.RequestEntry()) + return r } diff --git a/schedule/interval.go b/schedule/interval.go index 4ca5f6a..c2e224c 100644 --- a/schedule/interval.go +++ b/schedule/interval.go @@ -5,7 +5,7 @@ import "time" // Every 每隔多久执行一次 // // Note: interval至少需要大于等于1毫秒,否则将被设置为1毫秒 -func (j *TaskJob) Every(interval time.Duration) (cancel CancelFunc) { +func (j *taskJob) Every(interval time.Duration) (cancel CancelFunc) { if interval.Milliseconds() < 1 { interval = time.Millisecond } @@ -18,7 +18,7 @@ func (j *TaskJob) Every(interval time.Duration) (cancel CancelFunc) { } // EverySecond 每秒执行一次 -func (j *TaskJob) EverySecond() (cancel CancelFunc) { +func (j *taskJob) EverySecond() (cancel CancelFunc) { j.interval = time.Second j.run() @@ -28,7 +28,7 @@ func (j *TaskJob) EverySecond() (cancel CancelFunc) { } // EveryFiveSeconds 每5秒执行一次 -func (j *TaskJob) EveryFiveSeconds() (cancel CancelFunc) { +func (j *taskJob) EveryFiveSeconds() (cancel CancelFunc) { j.interval = time.Second * 5 j.run() @@ -38,7 +38,7 @@ func (j *TaskJob) EveryFiveSeconds() (cancel CancelFunc) { } // EveryTenSeconds 每10秒执行一次 -func (j *TaskJob) EveryTenSeconds() (cancel CancelFunc) { +func (j *taskJob) EveryTenSeconds() (cancel CancelFunc) { j.interval = time.Second * 10 j.run() @@ -48,7 +48,7 @@ func (j *TaskJob) EveryTenSeconds() (cancel CancelFunc) { } // EveryTwentySeconds 每20秒执行一次 -func (j *TaskJob) EveryTwentySeconds() (cancel CancelFunc) { +func (j *taskJob) EveryTwentySeconds() (cancel CancelFunc) { j.interval = time.Second * 20 j.run() @@ -58,7 +58,7 @@ func (j *TaskJob) EveryTwentySeconds() (cancel CancelFunc) { } // EveryThirtySeconds 每30秒执行一次 -func (j *TaskJob) EveryThirtySeconds() (cancel CancelFunc) { +func (j *taskJob) EveryThirtySeconds() (cancel CancelFunc) { j.interval = time.Second * 30 j.run() @@ -68,7 +68,7 @@ func (j *TaskJob) EveryThirtySeconds() (cancel CancelFunc) { } // EveryMinute 每分钟执行一次 -func (j *TaskJob) EveryMinute() (cancel CancelFunc) { +func (j *taskJob) EveryMinute() (cancel CancelFunc) { j.interval = time.Minute j.run() @@ -78,7 +78,7 @@ func (j *TaskJob) EveryMinute() (cancel CancelFunc) { } // EveryFiveMinutes 每5分钟执行一次 -func (j *TaskJob) EveryFiveMinutes() (cancel CancelFunc) { +func (j *taskJob) EveryFiveMinutes() (cancel CancelFunc) { j.interval = time.Minute * 5 j.run() @@ -88,7 +88,7 @@ func (j *TaskJob) EveryFiveMinutes() (cancel CancelFunc) { } // EveryTenMinutes 每10分钟执行一次 -func (j *TaskJob) EveryTenMinutes() (cancel CancelFunc) { +func (j *taskJob) EveryTenMinutes() (cancel CancelFunc) { j.interval = time.Minute * 10 j.run() @@ -98,7 +98,7 @@ func (j *TaskJob) EveryTenMinutes() (cancel CancelFunc) { } // EveryTwentyMinutes 每20分钟执行一次 -func (j *TaskJob) EveryTwentyMinutes() (cancel CancelFunc) { +func (j *taskJob) EveryTwentyMinutes() (cancel CancelFunc) { j.interval = time.Minute * 20 j.run() @@ -108,7 +108,7 @@ func (j *TaskJob) EveryTwentyMinutes() (cancel CancelFunc) { } // EveryThirtyMinutes 每30分钟执行一次 -func (j *TaskJob) EveryThirtyMinutes() (cancel CancelFunc) { +func (j *taskJob) EveryThirtyMinutes() (cancel CancelFunc) { j.interval = time.Minute * 30 j.run() @@ -118,7 +118,7 @@ func (j *TaskJob) EveryThirtyMinutes() (cancel CancelFunc) { } // Hourly 每1小时执行一次 -func (j *TaskJob) Hourly() (cancel CancelFunc) { +func (j *taskJob) Hourly() (cancel CancelFunc) { j.interval = time.Hour j.run() @@ -128,7 +128,7 @@ func (j *TaskJob) Hourly() (cancel CancelFunc) { } // EveryFiveHours 每5小时执行一次 -func (j *TaskJob) EveryFiveHours() (cancel CancelFunc) { +func (j *taskJob) EveryFiveHours() (cancel CancelFunc) { j.interval = time.Hour * 5 j.run() @@ -138,7 +138,7 @@ func (j *TaskJob) EveryFiveHours() (cancel CancelFunc) { } // EveryTenHours 每10小时执行一次 -func (j *TaskJob) EveryTenHours() (cancel CancelFunc) { +func (j *taskJob) EveryTenHours() (cancel CancelFunc) { j.interval = time.Hour * 10 j.run() @@ -148,7 +148,7 @@ func (j *TaskJob) EveryTenHours() (cancel CancelFunc) { } // EveryTwentyHours 每20小时执行一次 -func (j *TaskJob) EveryTwentyHours() (cancel CancelFunc) { +func (j *taskJob) EveryTwentyHours() (cancel CancelFunc) { j.interval = time.Hour * 20 j.run() @@ -158,7 +158,7 @@ func (j *TaskJob) EveryTwentyHours() (cancel CancelFunc) { } // Daily 每天执行一次 -func (j *TaskJob) Daily() (cancel CancelFunc) { +func (j *taskJob) Daily() (cancel CancelFunc) { j.interval = time.Hour * 24 j.run() @@ -168,7 +168,7 @@ func (j *TaskJob) Daily() (cancel CancelFunc) { } // Weekly 每周执行一次(每7天) -func (j *TaskJob) Weekly() (cancel CancelFunc) { +func (j *taskJob) Weekly() (cancel CancelFunc) { j.interval = time.Hour * 24 * 7 j.run() @@ -178,7 +178,7 @@ func (j *TaskJob) Weekly() (cancel CancelFunc) { } // Monthly 每月执行一次(每30天) -func (j *TaskJob) Monthly() (cancel CancelFunc) { +func (j *taskJob) Monthly() (cancel CancelFunc) { j.interval = time.Hour * 24 * 30 j.run() @@ -188,7 +188,7 @@ func (j *TaskJob) Monthly() (cancel CancelFunc) { } // Yearly 每年执行一次(每365天) -func (j *TaskJob) Yearly() (cancel CancelFunc) { +func (j *taskJob) Yearly() (cancel CancelFunc) { j.interval = time.Hour * 24 * 365 j.run() diff --git a/schedule/schedule.go b/schedule/schedule.go index 07cc8b3..89d10e3 100644 --- a/schedule/schedule.go +++ b/schedule/schedule.go @@ -11,8 +11,92 @@ import ( type CancelFunc func() -// TaskJob 任务 -type TaskJob struct { +// Scheduler 调度器 +type Scheduler interface { + // WithoutOverlapping 禁止并发执行 + // + // 一个任务仅允许存在一个运行态 + // + // Note: 该方法使用redis锁来保证唯一性, + // + // 因此请确保先使用 redis.InitRedis 或 + // + // redis.InitRedisCluster 实例化redis连接 + WithoutOverlapping() Scheduler + //任务执行函数 + run() + // Every 每隔多久执行一次 + // + // Note: interval至少需要大于等于1毫秒,否则将被设置为1毫秒 + Every(interval time.Duration) (cancel CancelFunc) + // EverySecond 每秒执行一次 + EverySecond() (cancel CancelFunc) + // EveryFiveSeconds 每5秒执行一次 + EveryFiveSeconds() (cancel CancelFunc) + // EveryTenSeconds 每10秒执行一次 + EveryTenSeconds() (cancel CancelFunc) + // EveryTwentySeconds 每20秒执行一次 + EveryTwentySeconds() (cancel CancelFunc) + // EveryThirtySeconds 每30秒执行一次 + EveryThirtySeconds() (cancel CancelFunc) + // EveryMinute 每分钟执行一次 + EveryMinute() (cancel CancelFunc) + // EveryFiveMinutes 每5分钟执行一次 + EveryFiveMinutes() (cancel CancelFunc) + // EveryTenMinutes 每10分钟执行一次 + EveryTenMinutes() (cancel CancelFunc) + // EveryTwentyMinutes 每20分钟执行一次 + EveryTwentyMinutes() (cancel CancelFunc) + // EveryThirtyMinutes 每30分钟执行一次 + EveryThirtyMinutes() (cancel CancelFunc) + // Hourly 每1小时执行一次 + Hourly() (cancel CancelFunc) + // EveryFiveHours 每5小时执行一次 + EveryFiveHours() (cancel CancelFunc) + // EveryTenHours 每10小时执行一次 + EveryTenHours() (cancel CancelFunc) + // EveryTwentyHours 每20小时执行一次 + EveryTwentyHours() (cancel CancelFunc) + // Daily 每天执行一次 + Daily() (cancel CancelFunc) + // Weekly 每周执行一次(每7天) + Weekly() (cancel CancelFunc) + // Monthly 每月执行一次(每30天) + Monthly() (cancel CancelFunc) + // Yearly 每年执行一次(每365天) + Yearly() (cancel CancelFunc) + // RunAt 在某一时刻执行 + // + // @param crontabExpr Linux crontab风格的表达式 + // + // * * * * * + // + // - - - - - + // + // | | | | | + // + // | | | | +----- day of week (0 - 7) (Sunday=0 or 7) OR sun...sat + // + // | | | +---------- month (1 - 12) OR jan,feb,mar,apr ... + // + // | | +--------------- day of month (1 - 31) + // + // | +-------------------- hour (0 - 23) + // + // +------------------------- minute (0 - 59) + RunAt(crontabExpr string) (cancel CancelFunc) + // TenClockAtWeekday 每个工作日(周一~周五)上午10点 + TenClockAtWeekday() (cancel CancelFunc) + // TenClockAtWeekend 每个周末(周六和周日)上午10点 + TenClockAtWeekend() (cancel CancelFunc) + // FirstDayOfMonthly 每月1号 + FirstDayOfMonthly() (cancel CancelFunc) + // LastDayOfMonthly 每月最后一天 + LastDayOfMonthly() (cancel CancelFunc) +} + +// taskJob 任务 +type taskJob struct { name string task func() interval time.Duration @@ -24,20 +108,22 @@ type TaskJob struct { cancelTaskChan chan struct{} } -type TaskJobPool struct { +var _ Scheduler = &taskJob{} + +type taskJobPool struct { mux *sync.RWMutex - pool map[string]*TaskJob + pool map[string]*taskJob } -var taskSchedules *TaskJobPool +var taskSchedules *taskJobPool var cronJob *cron.Cron func init() { (&sync.Once{}).Do(func() { - taskSchedules = &TaskJobPool{ + taskSchedules = &taskJobPool{ mux: &sync.RWMutex{}, - pool: make(map[string]*TaskJob), + pool: make(map[string]*taskJob), } }) } @@ -51,8 +137,8 @@ func generateJobNameKey(name string) string { // @param name 任务名称唯一标识 // // @param task 任务处理函数 -func Job(name string, task func()) *TaskJob { - job := &TaskJob{ +func Job(name string, task func()) Scheduler { + job := &taskJob{ name: name, lockerKey: generateJobNameKey(name), task: task, @@ -83,14 +169,14 @@ func Job(name string, task func()) *TaskJob { // 因此请确保先使用 redis.InitRedis 或 // // redis.InitRedisCluster 实例化redis连接 -func (j *TaskJob) WithoutOverlapping() *TaskJob { +func (j *taskJob) WithoutOverlapping() Scheduler { j.withoutOverlapping = true return j } // 任务执行函数 -func (j *TaskJob) run() { +func (j *taskJob) run() { go func() { ticker := time.NewTicker(j.interval) defer ticker.Stop() @@ -154,7 +240,7 @@ func (j *TaskJob) run() { // | +-------------------- hour (0 - 23) // // +------------------------- minute (0 - 59) -func (j *TaskJob) RunAt(crontabExpr string) (cancel CancelFunc) { +func (j *taskJob) RunAt(crontabExpr string) (cancel CancelFunc) { (&sync.Once{}).Do(func() { cronJob = cron.New() cronJob.Start() diff --git a/schedule/specs.go b/schedule/specs.go index 6a1b38c..051d235 100644 --- a/schedule/specs.go +++ b/schedule/specs.go @@ -1,21 +1,21 @@ package schedule // TenClockAtWeekday 每个工作日(周一~周五)上午10点 -func (j *TaskJob) TenClockAtWeekday() (cancel CancelFunc) { +func (j *taskJob) TenClockAtWeekday() (cancel CancelFunc) { return j.RunAt(TenClockAtWeekday) } // TenClockAtWeekend 每个周末(周六和周日)上午10点 -func (j *TaskJob) TenClockAtWeekend() (cancel CancelFunc) { +func (j *taskJob) TenClockAtWeekend() (cancel CancelFunc) { return j.RunAt(TenClockAtWeekend) } // FirstDayOfMonthly 每月1号 -func (j *TaskJob) FirstDayOfMonthly() (cancel CancelFunc) { +func (j *taskJob) FirstDayOfMonthly() (cancel CancelFunc) { return j.RunAt(FirstDayOfMonth) } // LastDayOfMonthly 每月最后一天 -func (j *TaskJob) LastDayOfMonthly() (cancel CancelFunc) { +func (j *taskJob) LastDayOfMonthly() (cancel CancelFunc) { return j.RunAt(LastDayOfMonth) } diff --git a/utils/crc.go b/utils/crc.go new file mode 100644 index 0000000..cf00722 --- /dev/null +++ b/utils/crc.go @@ -0,0 +1,32 @@ +package utils + +import ( + "hash/crc32" + "hash/crc64" +) + +// Crc64Checksum 求crc64校验码 +func Crc64Checksum(data []byte, table *crc64.Table) uint64 { + return crc64.Checksum(data, table) +} + +// Crc64ChecksumECMA 求crc64校验码 +// +// 使用ECMA多项式 +func Crc64ChecksumECMA(data []byte) uint64 { + crc64Table := crc64.MakeTable(crc64.ECMA) + + return crc64.Checksum(data, crc64Table) +} + +// Crc32Checksum 求crc32校验码 +func Crc32Checksum(data []byte, table *crc32.Table) uint32 { + return crc32.Checksum(data, table) +} + +// Crc32ChecksumIEEE 求crc32校验码 +// +// 使用IEEE多项式 +func Crc32ChecksumIEEE(data []byte) uint32 { + return crc32.ChecksumIEEE(data) +} diff --git a/utils/crc_test.go b/utils/crc_test.go new file mode 100644 index 0000000..e142daa --- /dev/null +++ b/utils/crc_test.go @@ -0,0 +1,41 @@ +package utils + +import ( + "hash/crc32" + "hash/crc64" + "testing" + + "github.com/stretchr/testify/assert" +) + +var ( + crcRawData = []byte(`hello world!你好!こんにちは!안녕하세요!`) + crc32checksum = uint32(411352257) + crc64checksum = uint64(12645825114846618640) +) + +func TestCrc32ChecksumIEEE(t *testing.T) { + checksum := Crc32ChecksumIEEE(crcRawData) + t.Log(checksum) + assert.Equal(t, crc32checksum, checksum) +} + +func TestCrc32Checksum(t *testing.T) { + table := crc32.MakeTable(crc32.IEEE) + checksum := Crc32Checksum(crcRawData, table) + t.Log(checksum) + assert.Equal(t, crc32checksum, checksum) +} + +func TestCrc64ChecksumECMA(t *testing.T) { + checksum := Crc64ChecksumECMA(crcRawData) + t.Log(checksum) + assert.Equal(t, crc64checksum, checksum) +} + +func TestCrc64Checksum(t *testing.T) { + table := crc64.MakeTable(crc64.ECMA) + checksum := Crc64Checksum(crcRawData, table) + t.Log(checksum) + assert.Equal(t, crc64checksum, checksum) +}