feat: en redis and logs
This commit is contained in:
+24
-12
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user