feat: en redis and logs

This commit is contained in:
zzw
2026-08-06 09:49:54 +08:00
parent 38be479cef
commit 23f5087a57
14 changed files with 358 additions and 469 deletions
+24 -12
View File
@@ -6,16 +6,19 @@ import (
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"gopkg.in/natefinch/lumberjack.v2"
)
// Logger 保留给 utils 内旧代码使用;新代码请用 pkg.local/log。
var Logger = zap.NewNop().Sugar()
var CronLogger *zap.SugaredLogger
func InitCronLog() {
// 设置一些基本日志格式 具体含义还比较好理解,直接看zap源码也不难懂
encoder := zapcore.NewConsoleEncoder(zapcore.EncoderConfig{
MessageKey: "msg",
LevelKey: "level",
EncodeLevel: zapcore.LowercaseColorLevelEncoder, //设置颜色
EncodeLevel: zapcore.LowercaseColorLevelEncoder,
TimeKey: "ts",
EncodeTime: func(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
enc.AppendString(t.Format("2006-01-02 15:04:05"))
@@ -25,7 +28,7 @@ func InitCronLog() {
EncodeDuration: func(d time.Duration, enc zapcore.PrimitiveArrayEncoder) {
enc.AppendInt64(int64(d) / 1000000)
},
}) // 实现两个判断日志等级的interface
})
fileEncoderConfig := zapcore.EncoderConfig{
TimeKey: "time",
@@ -35,9 +38,9 @@ func InitCronLog() {
EncodeTime: func(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
enc.AppendString(t.Format("2006-01-02 15:04:05"))
},
EncodeLevel: zapcore.LowercaseLevelEncoder, // 无颜色
EncodeLevel: zapcore.LowercaseLevelEncoder,
EncodeCaller: zapcore.ShortCallerEncoder,
ConsoleSeparator: " ", // tab分隔
ConsoleSeparator: " ",
}
fileEncoder := zapcore.NewConsoleEncoder(fileEncoderConfig)
@@ -51,21 +54,30 @@ func InitCronLog() {
return lvl >= zapcore.FatalLevel
})
// 获取 error日志文件的io.Writer 抽象 getWriter() 在下方实现
path := GetConfigString("log.path")
errorFile := GetConfigString("log.errorFile")
errorWriter := getWriter(path + "/cron-" + errorFile) // 最后创建具体的Logger
fatalWriter := getWriter(path + "/cron-" + errorFile)
infoFile := GetConfigString("log.infoFile")
infoWriter := getWriter(path + "/cron-" + infoFile)
errorWriter := cronWriter(path + "/cron-" + errorFile)
fatalWriter := cronWriter(path + "/cron-" + errorFile)
infoWriter := cronWriter(path + "/cron-" + infoFile)
core := zapcore.NewTee(
zapcore.NewCore(encoder, zapcore.AddSync(os.Stdout), infoLevel), //打印到控制台
zapcore.NewCore(encoder, zapcore.AddSync(os.Stdout), infoLevel),
zapcore.NewCore(fileEncoder, fatalWriter, fatalLevel),
zapcore.NewCore(fileEncoder, errorWriter, errorLevel),
zapcore.NewCore(fileEncoder, infoWriter, infoLevel),
)
log := zap.New(core, zap.AddCaller()) // 需要传入 zap.AddCaller() 才会显示打日志点的文件名和行数, 有点小坑
CronLogger = log.Sugar()
CronLogger = zap.New(core, zap.AddCaller()).Sugar()
}
func cronWriter(filename string) zapcore.WriteSyncer {
return zapcore.AddSync(&lumberjack.Logger{
Filename: filename,
MaxSize: GetConfigInt("log.maxSize"),
MaxBackups: GetConfigInt("log.maxBackups"),
MaxAge: GetConfigInt("log.maxAge"),
Compress: false,
LocalTime: true,
})
}
-94
View File
@@ -1,94 +0,0 @@
package utils
import (
"os"
"time"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"gopkg.in/natefinch/lumberjack.v2"
)
var Logger *zap.SugaredLogger
func InitLog() {
// 设置控制台编码器(带颜色)
consoleEncoder := zapcore.NewConsoleEncoder(zapcore.EncoderConfig{
MessageKey: "msg",
LevelKey: "level",
EncodeLevel: zapcore.LowercaseColorLevelEncoder, //设置颜色
TimeKey: "ts",
EncodeTime: func(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
enc.AppendString(t.Format("2006-01-02 15:04:05"))
},
CallerKey: "file",
EncodeCaller: zapcore.ShortCallerEncoder,
EncodeDuration: func(d time.Duration, enc zapcore.PrimitiveArrayEncoder) {
enc.AppendInt64(int64(d) / 1000000)
},
})
// 设置文件编码器(无颜色)
fileEncoder := zapcore.NewConsoleEncoder(zapcore.EncoderConfig{
MessageKey: "msg",
LevelKey: "level",
EncodeLevel: zapcore.LowercaseLevelEncoder, // 无颜色
TimeKey: "ts",
EncodeTime: func(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
enc.AppendString(t.Format("2006-01-02 15:04:05"))
},
CallerKey: "file",
EncodeCaller: zapcore.ShortCallerEncoder,
EncodeDuration: func(d time.Duration, enc zapcore.PrimitiveArrayEncoder) {
enc.AppendInt64(int64(d) / 1000000)
},
})
// 实现日志等级的interface
infoLevel := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
return lvl == zapcore.InfoLevel
})
errorLevel := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
return lvl == zapcore.ErrorLevel
})
fatalLevel := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
return lvl == zapcore.FatalLevel
})
// 获取日志文件的io.Writer 抽象
path := GetConfigString("log.path")
infoFile := GetConfigString("log.infoFile")
errorFile := GetConfigString("log.errorFile")
fatalFile := GetConfigString("log.fatalFile")
// 生成带日期的文件名
today := time.Now().Format("2006-01-02")
infoWriter := getWriter(path + "/" + today + "-" + infoFile)
errorWriter := getWriter(path + "/" + today + "-" + errorFile)
fatalWriter := getWriter(path + "/" + today + "-" + fatalFile)
// 创建核心
core := zapcore.NewTee(
zapcore.NewCore(consoleEncoder, zapcore.AddSync(os.Stdout), zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
return lvl >= zapcore.InfoLevel
})), // 控制台打印所有级别,带颜色
zapcore.NewCore(fileEncoder, infoWriter, infoLevel), // info 只写入 info 文件
zapcore.NewCore(fileEncoder, errorWriter, errorLevel), // error 只写入 error 文件
zapcore.NewCore(fileEncoder, fatalWriter, fatalLevel), // fatal 只写入 fatal 文件
)
log := zap.New(core, zap.AddCaller()) // 需要传入 zap.AddCaller() 才会显示打日志点的文件名和行数
Logger = log.Sugar()
}
func getWriter(filename string) zapcore.WriteSyncer {
lumberJackLogger := &lumberjack.Logger{
Filename: filename,
MaxSize: GetConfigInt("log.maxSize"),
MaxBackups: GetConfigInt("log.maxBackups"),
MaxAge: GetConfigInt("log.maxAge"),
Compress: false,
LocalTime: true,
}
return zapcore.AddSync(lumberJackLogger)
}
-317
View File
@@ -1,317 +0,0 @@
package utils
import (
"fmt"
"strconv"
"strings"
)
// PHPValue 定义 PHP 序列化值的类型(兼容字符串、整数、数组)
type PHPValue interface{}
// PHPDecode 原生解析 PHP 序列化字符串为 map[string]PHPValue
func PHPDecode(serialized string) (map[string]PHPValue, error) {
// 预处理:去除首尾空白
serialized = strings.TrimSpace(serialized)
// 验证是否为 PHP 数组格式(a:N:{...}
if !strings.HasPrefix(serialized, "a:") {
return nil, fmt.Errorf("不是 PHP 数组格式")
}
// 提取数组内容(去掉外层 a:N:{ 和 末尾的 })
colonIdx := strings.Index(serialized, ":")
if colonIdx == -1 {
return nil, fmt.Errorf("格式错误:缺少冒号")
}
braceOpenIdx := strings.Index(serialized, "{")
if braceOpenIdx == -1 {
return nil, fmt.Errorf("格式错误:缺少左大括号")
}
content := serialized[braceOpenIdx+1 : len(serialized)-1]
// 递归解析数组内容
result := make(map[string]PHPValue)
idx := 0
contentLen := len(content)
for idx < contentLen {
// 跳过空白字符
for idx < contentLen && (content[idx] == ' ' || content[idx] == '\t' || content[idx] == '\n') {
idx++
}
if idx >= contentLen {
break
}
// 解析 Key
key, keyEndIdx, err := parsePHPValueAt(content, idx)
if err != nil {
return nil, fmt.Errorf("解析 Key 失败:%v", err)
}
idx = keyEndIdx + 1 // 跳过 Key 后的分号
// 解析 Value
val, valEndIdx, err := parsePHPValueAt(content, idx)
if err != nil {
return nil, fmt.Errorf("解析 Value 失败:%v", err)
}
idx = valEndIdx + 1 // 跳过 Value 后的分号
// 将 Key 转为字符串并存入结果
var keyStr string
switch k := key.(type) {
case string:
keyStr = k
case int:
keyStr = strconv.Itoa(k)
default:
keyStr = fmt.Sprintf("%v", k)
}
result[keyStr] = val
}
return result, nil
}
// parsePHPValueAt 从指定索引解析单个 PHP 序列化值
func parsePHPValueAt(s string, start int) (PHPValue, int, error) {
if start >= len(s) {
return nil, -1, fmt.Errorf("索引越界")
}
// 根据类型前缀解析
switch s[start] {
case 's': // 字符串类型:s:长度:"内容";
return parsePHPString(s, start)
case 'i': // 整数类型:i:数值;
return parsePHPInt(s, start)
case 'a': // 数组类型:a:长度:{...};
return parsePHPArray(s, start)
default:
return nil, -1, fmt.Errorf("不支持的类型:%c", s[start])
}
}
// parsePHPString 解析 PHP 字符串类型(s:len:"value"
func parsePHPString(s string, start int) (string, int, error) {
// 格式:s:3:"abc";
// 1. 跳过 "s:"
if len(s) < start+2 || s[start:start+2] != "s:" {
return "", -1, fmt.Errorf("不是字符串类型")
}
idx := start + 2
// 2. 解析长度
lenStr := ""
for idx < len(s) && s[idx] != ':' {
lenStr += string(s[idx])
idx++
}
if idx >= len(s) || s[idx] != ':' {
return "", -1, fmt.Errorf("字符串长度解析失败")
}
strLen, err := strconv.Atoi(lenStr)
if err != nil {
return "", -1, fmt.Errorf("长度不是数字:%v", err)
}
idx++ // 跳过冒号
// 3. 跳过引号
if idx >= len(s) || s[idx] != '"' {
return "", -1, fmt.Errorf("缺少字符串起始引号")
}
idx++
// 4. 提取字符串内容
endIdx := idx + strLen
if endIdx > len(s) {
return "", -1, fmt.Errorf("字符串长度不足")
}
strVal := s[idx:endIdx]
idx = endIdx
// 5. 跳过结束引号和分号
if idx >= len(s) || s[idx] != '"' {
return "", -1, fmt.Errorf("缺少字符串结束引号")
}
idx++
if idx >= len(s) || s[idx] != ';' {
return "", -1, fmt.Errorf("缺少字符串结束分号")
}
return strVal, idx, nil
}
// parsePHPInt 解析 PHP 整数类型(i:123;
func parsePHPInt(s string, start int) (int, int, error) {
// 格式:i:123;
// 1. 跳过 "i:"
if len(s) < start+2 || s[start:start+2] != "i:" {
return 0, -1, fmt.Errorf("不是整数类型")
}
idx := start + 2
// 2. 解析数字
numStr := ""
for idx < len(s) && s[idx] != ';' {
numStr += string(s[idx])
idx++
}
if numStr == "" {
return 0, -1, fmt.Errorf("整数为空")
}
numVal, err := strconv.Atoi(numStr)
if err != nil {
return 0, -1, fmt.Errorf("整数解析失败:%v", err)
}
return numVal, idx, nil
}
// parsePHPArray 解析 PHP 数组类型(a:len:{...}
func parsePHPArray(s string, start int) (map[string]PHPValue, int, error) {
// 格式:a:2:{s:1:"k";i:123;s:1:"v";s:3:"abc";};
// 1. 跳过 "a:"
if len(s) < start+2 || s[start:start+2] != "a:" {
return nil, -1, fmt.Errorf("不是数组类型")
}
idx := start + 2
// 2. 解析数组长度(仅校验,实际按内容解析)
for idx < len(s) && s[idx] != ':' {
idx++
}
if idx >= len(s) || s[idx] != ':' {
return nil, -1, fmt.Errorf("数组长度解析失败")
}
idx++
// 3. 找到数组起始大括号
if idx >= len(s) || s[idx] != '{' {
return nil, -1, fmt.Errorf("缺少数组起始大括号")
}
idx++
// 4. 解析数组内容(递归调用 PHPDecode 的核心逻辑)
arrayContentStart := idx
depth := 1
arrayContentEnd := -1
// 找到匹配的结束大括号(处理嵌套数组)
for idx < len(s) && depth > 0 {
switch s[idx] {
case '{':
depth++
case '}':
depth--
if depth == 0 {
arrayContentEnd = idx
}
}
idx++
}
if arrayContentEnd == -1 {
return nil, -1, fmt.Errorf("数组缺少结束大括号")
}
// 5. 解析数组内容为 map
arrayContent := s[arrayContentStart:arrayContentEnd]
arrayResult := make(map[string]PHPValue)
innerIdx := 0
contentLen := len(arrayContent)
for innerIdx < contentLen {
// 跳过空白
for innerIdx < contentLen && (arrayContent[innerIdx] == ' ' || arrayContent[innerIdx] == '\t') {
innerIdx++
}
if innerIdx >= contentLen {
break
}
// 解析 Key
key, keyEnd, err := parsePHPValueAt(arrayContent, innerIdx)
if err != nil {
return nil, -1, fmt.Errorf("解析数组 Key 失败:%v", err)
}
innerIdx = keyEnd + 1 // 跳过分号
// 解析 Value
val, valEnd, err := parsePHPValueAt(arrayContent, innerIdx)
if err != nil {
return nil, -1, fmt.Errorf("解析数组 Value 失败:%v", err)
}
innerIdx = valEnd + 1 // 跳过分号
// 转换 Key 为字符串
var keyStr string
switch k := key.(type) {
case string:
keyStr = k
case int:
keyStr = strconv.Itoa(k)
default:
keyStr = fmt.Sprintf("%v", k)
}
arrayResult[keyStr] = val
}
// 6. 跳过数组结束后的分号
if idx < len(s) && s[idx] == ';' {
idx++
}
return arrayResult, idx - 1, nil // 返回数组结束的索引
}
// GetInt 从 map 中安全获取 int 类型值,失败返回默认值
func GetInt(m map[string]PHPValue, key string, defaultValue int) int {
val, ok := m[key]
if !ok {
return defaultValue
}
// 类型断言:将 interface{} 转为 int
num, ok := val.(int)
if !ok {
fmt.Printf("警告:%s 不是 int 类型,使用默认值 %d\n", key, defaultValue)
return defaultValue
}
return num
}
// GetString 从 map 中安全获取 string 类型值,失败返回默认值
func GetString(m map[string]PHPValue, key string, defaultValue string) string {
val, ok := m[key]
if !ok {
return defaultValue
}
// 类型断言:将 interface{} 转为 string
str, ok := val.(string)
if !ok {
fmt.Printf("警告:%s 不是 string 类型,使用默认值 %s\n", key, defaultValue)
return defaultValue
}
return str
}
// GetMap 从 map 中安全获取嵌套 map 类型值,失败返回空 map
func GetMap(m map[string]PHPValue, key string) map[string]interface{} {
val, ok := m[key]
if !ok {
return make(map[string]interface{})
}
// 类型断言:将 interface{} 转为 map[string]interface{}
nestedMap, ok := val.(map[string]PHPValue)
if !ok {
fmt.Printf("警告:%s 不是数组类型\n", key)
return make(map[string]interface{})
}
// 转换为通用 map[string]interface{}
result := make(map[string]interface{})
for k, v := range nestedMap {
result[k] = v
}
return result
}
-30
View File
@@ -1,30 +0,0 @@
package utils
import (
"base-service/pkg/queue"
"fmt"
"github.com/go-redis/redis"
)
var Redis *redis.Client
const RedisNil = "redis: nil"
func InitRedis() {
password := GetConfigString("redis.password")
host := GetConfigString("redis.host")
port := GetConfigInt("redis.port")
defaultDb := GetConfigInt("redis.default_db")
Redis = redis.NewClient(&redis.Options{
Addr: fmt.Sprintf("%s:%d", host, port),
Password: password, // no password set
DB: defaultDb, // use default DB
})
_, err := Redis.Ping().Result()
if err != nil {
Logger.Fatal("clint redis error ", err)
}
queue.RegisterRedis(Redis)
}