Replace virtual payments with WeChat Pay API v3 and verify callbacks and order polling. Cache WeChat access tokens in Redis and write structured Zap logs. Support configurable capacity plans across the API, admin and mini program, with purchase snapshots and quota enforcement. Allow personal albums before binding and share them with the current partner. Update deployment configuration and domain routing.
125 lines
3.4 KiB
Go
125 lines
3.4 KiB
Go
// Package logging owns zap initialization, rotation and shutdown.
|
|
package logging
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
"time"
|
|
|
|
"go.uber.org/zap"
|
|
"go.uber.org/zap/zapcore"
|
|
"gopkg.in/natefinch/lumberjack.v2"
|
|
"past/internal/config"
|
|
)
|
|
|
|
// New writes JSON files in log/YYYY-MM-DD/{level}.log, and optionally to stdout.
|
|
func New(dir string, cfg config.LogConfig) (*zap.Logger, func(), error) {
|
|
var minimum zapcore.Level
|
|
if err := minimum.UnmarshalText([]byte(cfg.Level)); err != nil {
|
|
return nil, nil, fmt.Errorf("日志级别无效: %w", err)
|
|
}
|
|
if cfg.RetentionDays <= 0 || cfg.MaxSizeMB <= 0 || cfg.MaxBackups <= 0 {
|
|
return nil, nil, fmt.Errorf("日志保留天数、大小和备份数量必须大于零")
|
|
}
|
|
if err := os.MkdirAll(dir, 0700); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
enc := zap.NewProductionEncoderConfig()
|
|
enc.EncodeTime = zapcore.ISO8601TimeEncoder
|
|
cores := []zapcore.Core{}
|
|
writers := []*dailyWriter{}
|
|
for level := zapcore.DebugLevel; level <= zapcore.FatalLevel; level++ {
|
|
if level < minimum {
|
|
continue
|
|
}
|
|
w := &dailyWriter{dir: dir, level: level.String(), cfg: cfg, now: time.Now}
|
|
// Open eagerly so an unwritable directory fails startup instead of losing logs.
|
|
if err := w.rotate(); err != nil {
|
|
for _, old := range writers {
|
|
old.Close()
|
|
}
|
|
return nil, nil, err
|
|
}
|
|
writers = append(writers, w)
|
|
exact := level
|
|
cores = append(cores, zapcore.NewCore(zapcore.NewJSONEncoder(enc), zapcore.AddSync(w), zap.LevelEnablerFunc(func(l zapcore.Level) bool { return l == exact })))
|
|
}
|
|
if cfg.Console {
|
|
cores = append(cores, zapcore.NewCore(zapcore.NewConsoleEncoder(enc), zapcore.Lock(os.Stdout), minimum))
|
|
}
|
|
log := zap.New(zapcore.NewTee(cores...), zap.AddCaller(), zap.AddStacktrace(zapcore.ErrorLevel))
|
|
cleanup := func() {
|
|
_ = log.Sync()
|
|
for _, w := range writers {
|
|
_ = w.Close()
|
|
}
|
|
}
|
|
return log, cleanup, nil
|
|
}
|
|
|
|
type dailyWriter struct {
|
|
mu sync.Mutex
|
|
dir, level, day string
|
|
cfg config.LogConfig
|
|
now func() time.Time
|
|
writer *lumberjack.Logger
|
|
}
|
|
|
|
// Caller holds mu (initialization happens before the writer is shared).
|
|
func (w *dailyWriter) rotate() error {
|
|
now := w.now()
|
|
day := now.Format(time.DateOnly)
|
|
if day == w.day && w.writer != nil {
|
|
return nil
|
|
}
|
|
path := filepath.Join(w.dir, day)
|
|
if err := os.MkdirAll(path, 0700); err != nil {
|
|
return err
|
|
}
|
|
filename := filepath.Join(path, w.level+".log")
|
|
f, err := os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_ = f.Close()
|
|
if w.writer != nil {
|
|
_ = w.writer.Close()
|
|
}
|
|
w.writer = &lumberjack.Logger{Filename: filename, MaxSize: w.cfg.MaxSizeMB, MaxAge: w.cfg.RetentionDays, MaxBackups: w.cfg.MaxBackups, Compress: true, LocalTime: true}
|
|
w.day = day
|
|
entries, err := os.ReadDir(w.dir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cutoff := now.AddDate(0, 0, -w.cfg.RetentionDays).Format(time.DateOnly)
|
|
for _, entry := range entries {
|
|
if !entry.IsDir() {
|
|
continue
|
|
}
|
|
if _, err := time.Parse(time.DateOnly, entry.Name()); err == nil && entry.Name() < cutoff {
|
|
if err := os.RemoveAll(filepath.Join(w.dir, entry.Name())); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
func (w *dailyWriter) Write(p []byte) (int, error) {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
if err := w.rotate(); err != nil {
|
|
return 0, err
|
|
}
|
|
return w.writer.Write(p)
|
|
}
|
|
func (w *dailyWriter) Close() error {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
if w.writer != nil {
|
|
return w.writer.Close()
|
|
}
|
|
return nil
|
|
}
|