重构代码结构与认证体系,集成异步任务消费者

This commit is contained in:
2025-11-17 18:38:10 +08:00
parent a97c970166
commit a245229bc2
70 changed files with 2000 additions and 2334 deletions

552
pkg/env/env.go vendored
View File

@@ -1,274 +1,54 @@
package env
import (
"fmt"
"log/slog"
"os"
"platform/pkg/u"
"slices"
"strconv"
"github.com/gofiber/fiber/v2/log"
"github.com/joho/godotenv"
)
// region app
const (
RunModeDev = "debug"
RunModeDev = "development"
RunModeProd = "production"
)
var (
RunMode = RunModeDev
TradeExpire = 30 * 60 // 交易过期时间,单位秒
)
RunMode = RunModeProd
LogLevel = slog.LevelDebug
TradeExpire = 30 * 60 // 交易过期时间,单位秒
SessionAccessExpire = 60 * 60 * 2 // 默认 2 小时
SessionRefreshExpire = 60 * 60 * 24 * 7 // 默认 7 天
DebugHttpDump = false // 是否打印请求和响应的原始数据
DebugExternalChange = true // 是否实际执行外部非幂等接口调用,在开发调试时可以关闭,避免对外部数据产生影响
func loadApp() {
_RunMode := os.Getenv("RUN_MODE")
switch _RunMode {
case RunModeDev, RunModeProd:
RunMode = _RunMode
case "":
break
default:
panic("环境变量 RUN_MODE 的值只能是 " + RunModeDev + " 或 " + RunModeProd)
}
_TradeExpire := os.Getenv("TRADE_EXPIRE")
if _TradeExpire != "" {
value, err := strconv.Atoi(_TradeExpire)
if err != nil {
panic("环境变量 TRADE_EXPIRE 的值不是数字")
}
TradeExpire = value
}
}
// endregion
// region auth
var (
SessionAccessExpire = 60 * 60 * 2 // 2小时
SessionRefreshExpire = 60 * 60 * 24 * 7 // 7天
)
func loadAuth() {
_SessionAccessExpire := os.Getenv("SESSION_ACCESS_EXPIRE")
if _SessionAccessExpire != "" {
value, err := strconv.Atoi(_SessionAccessExpire)
if err != nil {
panic("环境变量 SESSION_ACCESS_EXPIRE 的值不是数字")
}
SessionAccessExpire = value
}
_SessionRefreshExpire := os.Getenv("SESSION_REFRESH_EXPIRE")
if _SessionRefreshExpire != "" {
value, err := strconv.Atoi(_SessionRefreshExpire)
if err != nil {
panic("环境变量 SESSION_REFRESH_EXPIRE 的值不是数字")
}
SessionRefreshExpire = value
}
}
// endregion
// region db
var (
DbHost = "localhost"
DbPort = "5432"
DbName string
DbUserName string
DbPassword string
)
func loadDb() {
_DbHost := os.Getenv("DB_HOST")
if _DbHost != "" {
DbHost = _DbHost
}
RedisHost = "localhost"
RedisPort = "6379"
RedisPassword = ""
_DbPort := os.Getenv("DB_PORT")
if _DbPort != "" {
DbPort = _DbPort
}
_DbName := os.Getenv("DB_NAME")
if _DbName != "" {
DbName = _DbName
} else {
panic("环境变量 DB_NAME 的值不能为空")
}
_DbUserName := os.Getenv("DB_USERNAME")
if _DbUserName != "" {
DbUserName = _DbUserName
} else {
panic("环境变量 DB_USERNAME 的值不能为空")
}
_DbPassword := os.Getenv("DB_PASSWORD")
if _DbPassword != "" {
DbPassword = _DbPassword
} else {
panic("环境变量 DB_PASSWORD 的值不能为空")
}
}
// endregion
// region redis
var (
RedisHost = "localhost"
RedisPort = "6379"
RedisDb = 0
RedisPass = ""
)
func loadRedis() {
_RedisHost := os.Getenv("REDIS_HOST")
if _RedisHost != "" {
RedisHost = _RedisHost
}
_RedisPort := os.Getenv("REDIS_PORT")
if _RedisPort != "" {
RedisPort = _RedisPort
}
_RedisDb := os.Getenv("REDIS_DB")
if _RedisDb != "" {
atoi, err := strconv.Atoi(_RedisDb)
if err != nil {
panic("环境变量 REDIS_DB 的值不是数字")
}
RedisDb = atoi
}
_RedisPass := os.Getenv("REDIS_PASS")
if _RedisPass != "" {
RedisPass = _RedisPass
}
}
// endregion
// region log
var (
LogLevel = slog.LevelDebug
)
func loadLog() {
_LogLevel := os.Getenv("LOG_LEVEL")
switch _LogLevel {
case "debug":
LogLevel = slog.LevelDebug
case "info":
LogLevel = slog.LevelInfo
case "warn":
LogLevel = slog.LevelWarn
case "error":
LogLevel = slog.LevelError
}
}
// endregion
// region remote
var (
BaiyinAddr = "http://103.139.212.110:9989"
BaiyinTokenUrl string
)
var (
IdenCallbackUrl string
IdenAccessKey string
IdenSecretKey string
)
func loadRemote() {
_BaiyinAddr := os.Getenv("BAIYIN_ADDR")
if _BaiyinAddr != "" {
BaiyinAddr = _BaiyinAddr
}
_BaiyinTokenUrl := os.Getenv("BAIYIN_TOKEN_URL")
if _BaiyinTokenUrl == "" {
panic("环境变量 BAIYIN_TOKEN_URL 的值不能为空")
}
BaiyinTokenUrl = _BaiyinTokenUrl
_IdenCallbackUrl := os.Getenv("IDEN_CALLBACK_URL")
if _IdenCallbackUrl == "" {
panic("环境变量 IDEN_CALLBACK_URL 的值不能为空")
}
IdenCallbackUrl = _IdenCallbackUrl
_IdenAccessKey := os.Getenv("IDEN_ACCESS_KEY")
if _IdenAccessKey == "" {
panic("环境变量 IDEN_ACCESS_KEY 的值不能为空")
}
IdenAccessKey = _IdenAccessKey
_IdenSecretKey := os.Getenv("IDEN_SECRET_KEY")
if _IdenSecretKey == "" {
panic("环境变量 IDEN_SECRET_KEY 的值不能为空")
}
IdenSecretKey = _IdenSecretKey
}
// endregion
// region alipay
var (
AlipayAppId string
AlipayAppPrivateKey string
AlipayPublicKey string
AlipayApiCert string
AlipayProduction = false
)
func loadAlipay() {
AlipayAppId = os.Getenv("ALIPAY_APP_ID")
if AlipayAppId == "" {
panic("环境变量 ALIPAY_APP_ID 的值不能为空")
}
AlipayAppPrivateKey = os.Getenv("ALIPAY_APP_PRIVATE_KEY")
if AlipayAppPrivateKey == "" {
panic("环境变量 ALIPAY_APP_PRIVATE_KEY 的值不能为空")
}
AlipayPublicKey = os.Getenv("ALIPAY_PUBLIC_KEY")
if AlipayPublicKey == "" {
panic("环境变量 ALIPAY_PUBLIC_KEY 的值不能为空")
}
AlipayApiCert = os.Getenv("ALIPAY_API_CERT")
if AlipayApiCert == "" {
panic("环境变量 ALIPAY_API_CERT 的值不能为空")
}
_AlipayProduction := os.Getenv("ALIPAY_PRODUCTION")
if _AlipayProduction != "" {
value, err := strconv.ParseBool(_AlipayProduction)
if err != nil {
panic("环境变量 ALIPAY_PRODUCTION 的值不是布尔值")
}
AlipayProduction = value
}
}
// endregion
// region wechatpay
var (
WechatPayAppId string
WechatPayMchId string
WechatPayMchPrivateKeySerial string
@@ -277,185 +57,21 @@ var (
WechatPayPublicKey string
WechatPayApiCert string
WechatPayCallbackUrl string
)
func loadWechatPay() {
WechatPayAppId = os.Getenv("WECHATPAY_APP_ID")
if WechatPayAppId == "" {
panic("环境变量 WECHATPAY_APP_ID 的值不能为空")
}
WechatPayMchId = os.Getenv("WECHATPAY_MCH_ID")
if WechatPayMchId == "" {
panic("环境变量 WECHATPAY_MCH_ID 的值不能为空")
}
WechatPayMchPrivateKeySerial = os.Getenv("WECHATPAY_MCH_PRIVATE_KEY_SERIAL")
if WechatPayMchPrivateKeySerial == "" {
panic("环境变量 WECHATPAY_MCH_PRIVATE_KEY_SERIAL 的值不能为空")
}
WechatPayMchPrivateKey = os.Getenv("WECHATPAY_MCH_PRIVATE_KEY")
if WechatPayMchPrivateKey == "" {
panic("环境变量 WECHATPAY_MCH_PRIVATE_KEY 的值不能为空")
}
WechatPayPublicKeyId = os.Getenv("WECHATPAY_PUBLIC_KEY_ID")
if WechatPayPublicKeyId == "" {
panic("环境变量 WECHATPAY_PUBLIC_KEY_ID 的值不能为空")
}
WechatPayPublicKey = os.Getenv("WECHATPAY_PUBLIC_KEY")
if WechatPayPublicKey == "" {
panic("环境变量 WECHATPAY_PUBLIC_KEY 的值不能为空")
}
WechatPayApiCert = os.Getenv("WECHATPAY_API_CERT")
if WechatPayApiCert == "" {
panic("环境变量 WECHATPAY_API_CERT 的值不能为空")
}
WechatPayCallbackUrl = os.Getenv("WECHATPAY_CALLBACK_URL")
if WechatPayCallbackUrl == "" {
panic("环境变量 WECHATPAY_CALLBACK_URL 的值不能为空")
}
}
// endregion
// region aliyun
var (
AliyunAccessKey string
AliyunAccessKeySecret string
AliyunSmsSignature string
AliyunSmsTemplateLogin string
)
func loadAliyun() {
AliyunAccessKey = os.Getenv("ALIYUN_ACCESS_KEY")
if AliyunAccessKey == "" {
panic("环境变量 ALIYUN_ACCESS_KEY 的值不能为空")
}
AliyunAccessKeySecret = os.Getenv("ALIYUN_ACCESS_KEY_SECRET")
if AliyunAccessKeySecret == "" {
panic("环境变量 ALIYUN_ACCESS_KEY_SECRET 的值不能为空")
}
AliyunSmsSignature = os.Getenv("ALIYUN_SMS_SIGNATURE")
if AliyunSmsSignature == "" {
panic("环境变量 ALIYUN_SMS_SIGNATURE 的值不能为空")
}
AliyunSmsTemplateLogin = os.Getenv("ALIYUN_SMS_TEMPLATE_LOGIN")
if AliyunSmsTemplateLogin == "" {
panic("环境变量 ALIYUN_SMS_TEMPLATE_LOGIN 的值不能为空")
}
}
// endregion
// region 商福通
var (
SftPayEnable = false
SftPayAppId string
SftPayRouteId string
SftPayAppPrivateKey string
SftPayPublicKey string
SftReturnUrl *string
SftNotifyUrl *string
SftReturnUrl string
SftNotifyUrl string
)
func loadSftPay() {
var value string
value = os.Getenv("SFTPAY_ENABLE")
if value != "" {
enabled, err := strconv.ParseBool(value)
if err != nil {
panic("环境变量 SFTPAY_ENABLE 的值不是布尔值")
}
SftPayEnable = enabled
}
value = os.Getenv("SFTPAY_APP_ID")
if value == "" {
panic("环境变量 ALIYUN_SMS_TEMPLATE_LOGIN 的值不能为空")
} else {
SftPayAppId = value
}
value = os.Getenv("SFTPAY_ROUTE_ID")
if value != "" {
SftPayRouteId = value
}
value = os.Getenv("SFTPAY_APP_PRIVATE_KEY")
if value == "" {
panic("环境变量 SFTPAY_APP_PRIVATE_KEY 的值不能为空")
} else {
SftPayAppPrivateKey = value
}
value = os.Getenv("SFTPAY_PUBLIC_KEY")
if value == "" {
panic("环境变量 SFTPAY_PUBLIC_KEY 的值不能为空")
} else {
SftPayPublicKey = value
}
value = os.Getenv("SFTPAY_RETURN_URL")
if value != "" {
SftReturnUrl = &value
} else {
SftReturnUrl = nil
}
value = os.Getenv("SFTPAY_NOTIFY_URL")
if value != "" {
SftNotifyUrl = &value
} else {
SftNotifyUrl = nil
}
}
// endregion
// region debug
var (
// DebugHttpDump 是否打印请求和响应的原始数据
DebugHttpDump = false
// DebugExternalChange 是否实际执行非幂等外部接口的调用。
// 例如外部数据修改接口,在内部接口调试时可以关闭,避免对外部数据产生影响
DebugExternalChange = true
)
func loadDebug() {
debugHttpDump := os.Getenv("DEBUG_HTTP_DUMP")
if debugHttpDump != "" {
value, err := strconv.ParseBool(debugHttpDump)
if err != nil {
panic("环境变量 DEBUG_HTTP_DUMP 的值不是布尔值")
}
DebugHttpDump = value
}
debugExternalChange := os.Getenv("DEBUG_EXTERNAL_CHANGE")
if debugExternalChange != "" {
value, err := strconv.ParseBool(debugExternalChange)
if err != nil {
panic("环境变量 DEBUG_EXTERNAL_CHANGE 的值不是布尔值")
}
DebugExternalChange = value
}
}
// endregion
func Init() {
err := godotenv.Load()
if err != nil {
@@ -464,15 +80,129 @@ func Init() {
log.Debug("✔ 加载本地环境变量")
}
loadApp()
loadAuth()
loadDb()
loadRedis()
loadLog()
loadDebug()
loadRemote()
loadAlipay()
loadWechatPay()
loadAliyun()
loadSftPay()
// 收集所有错误
var errs []error
errs = append(errs, parse(&RunMode, "RUN_MODE", true, &[]string{RunModeDev, RunModeProd}))
errs = append(errs, parse(&LogLevel, "LOG_LEVEL", true, nil, func(value string) (slog.Level, error) {
switch value {
case "debug":
return slog.LevelDebug, nil
case "info":
return slog.LevelInfo, nil
case "warn":
return slog.LevelWarn, nil
case "error":
return slog.LevelError, nil
default:
return slog.LevelInfo, fmt.Errorf("无效的日志级别: %s", value)
}
}))
errs = append(errs, parse(&TradeExpire, "TRADE_EXPIRE", true, nil))
errs = append(errs, parse(&SessionAccessExpire, "SESSION_ACCESS_EXPIRE", true, nil))
errs = append(errs, parse(&SessionRefreshExpire, "SESSION_REFRESH_EXPIRE", true, nil))
errs = append(errs, parse(&DebugHttpDump, "DEBUG_HTTP_DUMP", true, nil))
errs = append(errs, parse(&DebugExternalChange, "DEBUG_EXTERNAL_CHANGE", true, nil))
errs = append(errs, parse(&DbHost, "DB_HOST", true, nil))
errs = append(errs, parse(&DbPort, "DB_PORT", true, nil))
errs = append(errs, parse(&DbName, "DB_NAME", false, nil))
errs = append(errs, parse(&DbUserName, "DB_USERNAME", false, nil))
errs = append(errs, parse(&DbPassword, "DB_PASSWORD", false, nil))
errs = append(errs, parse(&RedisHost, "REDIS_HOST", true, nil))
errs = append(errs, parse(&RedisPort, "REDIS_PORT", true, nil))
errs = append(errs, parse(&RedisPassword, "REDIS_PASS", true, nil))
errs = append(errs, parse(&BaiyinAddr, "BAIYIN_ADDR", true, nil))
errs = append(errs, parse(&BaiyinTokenUrl, "BAIYIN_TOKEN_URL", false, nil))
errs = append(errs, parse(&IdenCallbackUrl, "IDEN_CALLBACK_URL", false, nil))
errs = append(errs, parse(&IdenAccessKey, "IDEN_ACCESS_KEY", false, nil))
errs = append(errs, parse(&IdenSecretKey, "IDEN_SECRET_KEY", false, nil))
errs = append(errs, parse(&AlipayAppId, "ALIPAY_APP_ID", false, nil))
errs = append(errs, parse(&AlipayAppPrivateKey, "ALIPAY_APP_PRIVATE_KEY", false, nil))
errs = append(errs, parse(&AlipayPublicKey, "ALIPAY_PUBLIC_KEY", false, nil))
errs = append(errs, parse(&AlipayApiCert, "ALIPAY_API_CERT", false, nil))
errs = append(errs, parse(&AlipayProduction, "ALIPAY_PRODUCTION", true, nil))
errs = append(errs, parse(&WechatPayAppId, "WECHATPAY_APP_ID", false, nil))
errs = append(errs, parse(&WechatPayMchId, "WECHATPAY_MCH_ID", false, nil))
errs = append(errs, parse(&WechatPayMchPrivateKeySerial, "WECHATPAY_MCH_PRIVATE_KEY_SERIAL", false, nil))
errs = append(errs, parse(&WechatPayMchPrivateKey, "WECHATPAY_MCH_PRIVATE_KEY", false, nil))
errs = append(errs, parse(&WechatPayPublicKeyId, "WECHATPAY_PUBLIC_KEY_ID", false, nil))
errs = append(errs, parse(&WechatPayPublicKey, "WECHATPAY_PUBLIC_KEY", false, nil))
errs = append(errs, parse(&WechatPayApiCert, "WECHATPAY_API_CERT", false, nil))
errs = append(errs, parse(&WechatPayCallbackUrl, "WECHATPAY_CALLBACK_URL", false, nil))
errs = append(errs, parse(&AliyunAccessKey, "ALIYUN_ACCESS_KEY", false, nil))
errs = append(errs, parse(&AliyunAccessKeySecret, "ALIYUN_ACCESS_KEY_SECRET", false, nil))
errs = append(errs, parse(&AliyunSmsSignature, "ALIYUN_SMS_SIGNATURE", false, nil))
errs = append(errs, parse(&AliyunSmsTemplateLogin, "ALIYUN_SMS_TEMPLATE_LOGIN", false, nil))
errs = append(errs, parse(&SftPayEnable, "SFTPAY_ENABLE", true, nil))
errs = append(errs, parse(&SftPayAppId, "SFTPAY_APP_ID", false, nil))
errs = append(errs, parse(&SftPayRouteId, "SFTPAY_ROUTE_ID", true, nil))
errs = append(errs, parse(&SftPayAppPrivateKey, "SFTPAY_APP_PRIVATE_KEY", false, nil))
errs = append(errs, parse(&SftPayPublicKey, "SFTPAY_PUBLIC_KEY", false, nil))
errs = append(errs, parse(&SftReturnUrl, "SFTPAY_RETURN_URL", true, nil))
errs = append(errs, parse(&SftNotifyUrl, "SFTPAY_NOTIFY_URL", true, nil))
// 统一处理错误
if err := u.CombineErrors(errs); err != nil {
panic(err)
}
}
func parse[T comparable](ptr *T, key string, inited bool, enum *[]string, convOpt ...func(value string) (T, error)) error {
value := os.Getenv(key)
// 处理空值
if value == "" {
if inited {
return nil
} else {
return fmt.Errorf("环境变量 %s 未设置", key)
}
}
// 处理枚举映射
if enum != nil {
valid := slices.Contains(*enum, value)
if !valid {
return fmt.Errorf("环境变量 %s 的值 '%s' 必须是 %v 之一", key, value, *enum)
}
}
// 根据指针类型进行赋值和类型转换
switch p := any(ptr).(type) {
case *string:
*p = value
case *int:
intValue, err := strconv.Atoi(value)
if err != nil {
return fmt.Errorf("环境变量 %s 的值 '%s' 不是有效的整数: %v", key, value, err)
}
*p = intValue
case *bool:
boolValue, err := strconv.ParseBool(value)
if err != nil {
return fmt.Errorf("环境变量 %s 的值 '%s' 不是有效的布尔值: %v", key, value, err)
}
*p = boolValue
default:
if len(convOpt) == 0 {
return fmt.Errorf("环境变量 %s 的值 '%s' 无法赋值到目标类型", key, value)
}
conv := convOpt[0]
convertedValue, err := conv(value)
if err != nil {
return fmt.Errorf("环境变量 %s 的值 '%s' 转换失败: %v", key, value, err)
}
*ptr = convertedValue
}
return nil
}

View File

@@ -1,10 +1,11 @@
package logs
import (
"github.com/lmittmann/tint"
"log/slog"
"os"
"platform/pkg/env"
"github.com/lmittmann/tint"
)
func Init() {
@@ -14,7 +15,7 @@ func Init() {
var handler slog.Handler
switch env.RunMode {
case "debug":
case env.RunModeDev:
handler = tint.NewHandler(writer, &tint.Options{
Level: env.LogLevel,
TimeFormat: timeFormat,
@@ -26,7 +27,7 @@ func Init() {
return attr
},
})
case "production":
case env.RunModeProd:
handler = slog.NewJSONHandler(writer, &slog.HandlerOptions{
Level: env.LogLevel,
ReplaceAttr: func(_ []string, a slog.Attr) slog.Attr {

View File

@@ -1,12 +1,31 @@
package u
import "time"
import (
"fmt"
"time"
)
// P 是一个工具函数,用于在表达式内原地创建一个指针
func P[T any](v T) *T {
return &v
}
func Z[T any](v *T) T {
if v == nil {
var zero T
return zero
}
return *v
}
func X[T comparable](v T) *T {
var zero T
if v == zero {
return nil
}
return &v
}
func Today() time.Time {
var now = time.Now()
return time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
@@ -21,14 +40,6 @@ func SameDate(date time.Time) bool {
return date.Year() == now.Year() && date.Month() == now.Month() && date.Day() == now.Day()
}
func Z[T any](v *T) T {
if v == nil {
var zero T
return zero
}
return *v
}
func Or[T any](v *T, or T) T {
if v == nil {
return or
@@ -36,3 +47,17 @@ func Or[T any](v *T, or T) T {
return *v
}
}
func CombineErrors(errs []error) error {
var combinedErr error = nil
for _, err := range errs {
if err != nil {
if combinedErr == nil {
combinedErr = err
} else {
combinedErr = fmt.Errorf("%v; %w", combinedErr, err)
}
}
}
return combinedErr
}