优化日志系统

This commit is contained in:
yml2213
2026-06-19 12:10:55 +08:00
parent 1805edc63a
commit 59b9dd12ef
11 changed files with 232 additions and 66 deletions
+4 -5
View File
@@ -3,7 +3,6 @@ package main
import (
"encoding/json"
"errors"
"log"
"os"
"path/filepath"
)
@@ -36,21 +35,21 @@ func loadConfig(path string) Config {
f, err := os.Open(path)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
log.Printf("配置加载完成: 内置默认配置")
logEvent("配置", "info", "配置加载完成: 内置默认配置")
} else {
log.Printf("读取 %s 出错,使用内置默认配置: %v", path, err)
logEvent("配置", "warn", "读取 %s 出错,使用内置默认配置: %v", path, err)
}
return cfg
}
defer f.Close()
if err := json.NewDecoder(f).Decode(&cfg); err != nil {
log.Printf("解析 %s 失败,使用内置默认配置: %v", path, err)
logEvent("配置", "warn", "解析 %s 失败,使用内置默认配置: %v", path, err)
return Config{TimeAPIURL: defaultTimeAPI}
}
if cfg.TimeAPIURL == "" {
cfg.TimeAPIURL = defaultTimeAPI
}
log.Printf("配置加载完成: %s", path)
logEvent("配置", "success", "配置加载完成: %s", path)
return cfg
}
+112 -12
View File
@@ -8,6 +8,7 @@ import (
"io"
"mime/multipart"
"net/http"
"reflect"
"strconv"
"strings"
"time"
@@ -54,7 +55,7 @@ func (c *DouyuClient) fetchServerTimeOnce() (time.Time, error) {
} `json:"preExchange"`
} `json:"data"`
}
if err := c.getJSON(url, "", &result); err != nil {
if err := c.getJSON(url, "", "服务时间", &result); err != nil {
return time.Time{}, err
}
ts, err := strconv.ParseInt(result.Data.PreExchange.CurrentTime, 10, 64)
@@ -72,7 +73,7 @@ func (c *DouyuClient) Products() ([]Product, error) {
List []Product `json:"list"`
} `json:"data"`
}
if err := c.getJSON(storeHomeURL, "", &result); err != nil {
if err := c.getJSON(storeHomeURL, "", "商品列表", &result); err != nil {
return nil, err
}
if result.Error != 0 {
@@ -96,7 +97,7 @@ func (c *DouyuClient) Score(cookie string) (int, error) {
Balance int `json:"balance"`
} `json:"data"`
}
if err := c.doJSON(req, &result); err != nil {
if err := c.doJSON(req, &result, "积分余额"); err != nil {
return 0, err
}
if result.Error != 0 {
@@ -113,32 +114,41 @@ func (c *DouyuClient) GenerateCSRF(cookie string) (string, error) {
}
c.setBrowserHeaders(req, cookie)
start := time.Now()
resp, err := c.httpClient.Do(req)
if err != nil {
logNetworkError("生成CSRF", req, time.Since(start), err)
return "", err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
logNetworkError("生成CSRF", req, time.Since(start), err)
return "", err
}
if resp.StatusCode != http.StatusOK {
logNetworkHTTPError("生成CSRF", req, resp.StatusCode, time.Since(start), body)
return "", fmt.Errorf("CSRF HTTP %d: %s", resp.StatusCode, trimForLog(string(body), 120))
}
var api APIResponse
if err := json.Unmarshal(body, &api); err != nil {
logNetworkParseError("生成CSRF", req, time.Since(start), body, err)
return "", fmt.Errorf("CSRF 响应解析失败: %w", err)
}
if api.Error != 0 {
logNetworkAPIResult("生成CSRF", req, time.Since(start), api.Error, api.Msg)
return "", fmt.Errorf("CSRF 接口失败: %s", api.Msg)
}
for _, ck := range resp.Cookies() {
if ck.Name == "cvl_csrf_token" && ck.Value != "" {
logNetworkSuccess("生成CSRF", req, time.Since(start), 0, "")
return ck.Value, nil
}
}
logEvent("网络", "warn", "接口返回异常: 生成CSRF %s %s 耗时=%s msg=响应没有 cvl_csrf_token",
req.Method, requestPath(req), roundDuration(time.Since(start)))
return "", errors.New("CSRF 响应没有 cvl_csrf_token")
}
@@ -150,7 +160,7 @@ func (c *DouyuClient) BatchExchangeLimit(cookie, commodityID, csrf string) (int,
Limit string `json:"limit"`
} `json:"data"`
}
if err := c.postMultipart(batchLimitURL, cookie, csrf, map[string]string{
if err := c.postMultipart(batchLimitURL, cookie, csrf, "批量限制", map[string]string{
"manualID": manualID,
"commodityID": commodityID,
"rid": roomID,
@@ -171,7 +181,7 @@ func (c *DouyuClient) BatchExchangeLimit(cookie, commodityID, csrf string) (int,
// PreExchange 按抓包流程执行预兑换检查。
func (c *DouyuClient) PreExchange(cookie, commodityID, csrf string) error {
var result APIResponse
if err := c.postMultipart(preExchangeURL, cookie, csrf, map[string]string{
if err := c.postMultipart(preExchangeURL, cookie, csrf, "预兑换", map[string]string{
"manualID": manualID,
"rid": roomID,
"commodityID": commodityID,
@@ -196,7 +206,7 @@ func (c *DouyuClient) Exchange(cookie, commodityID, csrf string) (string, error)
CommodityTyp int `json:"commodityType"`
} `json:"data"`
}
if err := c.postMultipart(exchangeURL, cookie, csrf, map[string]string{
if err := c.postMultipart(exchangeURL, cookie, csrf, "正式兑换", map[string]string{
"manualID": manualID,
"commodityID": commodityID,
"num": "1",
@@ -224,7 +234,7 @@ func (c *DouyuClient) RoleName(cookie string) (string, error) {
} `json:"gameRole"`
} `json:"data"`
}
if err := c.getJSON(bindInfoURL, cookie, &result); err != nil {
if err := c.getJSON(bindInfoURL, cookie, "角色绑定", &result); err != nil {
return "", err
}
if result.Error != 0 {
@@ -234,17 +244,17 @@ func (c *DouyuClient) RoleName(cookie string) (string, error) {
}
// getJSON 发送 GET 请求并解析 JSON。
func (c *DouyuClient) getJSON(url, cookie string, out any) error {
func (c *DouyuClient) getJSON(url, cookie, label string, out any) error {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return err
}
c.setBrowserHeaders(req, cookie)
return c.doJSON(req, out)
return c.doJSON(req, out, label)
}
// postMultipart 发送 multipart/form-data 请求并解析 JSON。
func (c *DouyuClient) postMultipart(url, cookie, csrf string, fields map[string]string, out any) error {
func (c *DouyuClient) postMultipart(url, cookie, csrf, label string, fields map[string]string, out any) error {
var body bytes.Buffer
writer := multipart.NewWriter(&body)
for key, value := range fields {
@@ -262,30 +272,120 @@ func (c *DouyuClient) postMultipart(url, cookie, csrf string, fields map[string]
}
c.setBrowserHeaders(req, mergeCookie(cookie, "cvl_csrf_token", csrf))
req.Header.Set("Content-Type", writer.FormDataContentType())
return c.doJSON(req, out)
return c.doJSON(req, out, label)
}
// doJSON 执行请求、检查 HTTP 状态码并解析 JSON。
func (c *DouyuClient) doJSON(req *http.Request, out any) error {
func (c *DouyuClient) doJSON(req *http.Request, out any, label string) error {
start := time.Now()
resp, err := c.httpClient.Do(req)
if err != nil {
logNetworkError(label, req, time.Since(start), err)
return err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
logNetworkError(label, req, time.Since(start), err)
return err
}
if resp.StatusCode != http.StatusOK {
logNetworkHTTPError(label, req, resp.StatusCode, time.Since(start), body)
return fmt.Errorf("HTTP %d: %s", resp.StatusCode, trimForLog(string(body), 160))
}
if err := json.Unmarshal(body, out); err != nil {
logNetworkParseError(label, req, time.Since(start), body, err)
return fmt.Errorf("JSON 解析失败: %w, body=%s", err, trimForLog(string(body), 160))
}
if apiError, apiMsg, ok := extractAPIStatus(out); ok && apiError != 0 {
logNetworkAPIResult(label, req, time.Since(start), apiError, apiMsg)
} else if shouldLogNetworkSuccess(label) {
logNetworkSuccess(label, req, time.Since(start), 0, "")
}
return nil
}
func shouldLogNetworkSuccess(label string) bool {
return label != "服务时间"
}
func logNetworkSuccess(label string, req *http.Request, cost time.Duration, apiError int, apiMsg string) {
if !shouldLogNetworkSuccess(label) {
return
}
extra := ""
if apiMsg != "" || apiError != 0 {
extra = fmt.Sprintf(" apiError=%d msg=%s", apiError, trimForLog(apiMsg, 80))
}
logEvent("网络", "info", "请求完成: %s %s %s 耗时=%s%s", label, req.Method, requestPath(req), roundDuration(cost), extra)
}
func logNetworkAPIResult(label string, req *http.Request, cost time.Duration, apiError int, apiMsg string) {
logEvent("网络", "warn", "接口返回异常: %s %s %s 耗时=%s apiError=%d msg=%s",
label, req.Method, requestPath(req), roundDuration(cost), apiError, trimForLog(apiMsg, 120))
}
func logNetworkHTTPError(label string, req *http.Request, status int, cost time.Duration, body []byte) {
logEvent("网络", "error", "HTTP异常: %s %s %s 状态=%d 耗时=%s 响应=%s",
label, req.Method, requestPath(req), status, roundDuration(cost), trimForLog(string(body), 160))
}
func logNetworkParseError(label string, req *http.Request, cost time.Duration, body []byte, err error) {
logEvent("网络", "error", "响应解析失败: %s %s %s 耗时=%s 错误=%v 响应=%s",
label, req.Method, requestPath(req), roundDuration(cost), err, trimForLog(string(body), 160))
}
func logNetworkError(label string, req *http.Request, cost time.Duration, err error) {
logEvent("网络", "error", "请求失败: %s %s %s 耗时=%s 错误=%v",
label, req.Method, requestPath(req), roundDuration(cost), err)
}
func requestPath(req *http.Request) string {
if req == nil || req.URL == nil {
return "-"
}
return req.URL.Path
}
func roundDuration(d time.Duration) time.Duration {
return d.Round(time.Millisecond)
}
func extractAPIStatus(out any) (int, string, bool) {
if out == nil {
return 0, "", false
}
v := reflect.ValueOf(out)
if v.Kind() == reflect.Pointer {
if v.IsNil() {
return 0, "", false
}
v = v.Elem()
}
if !v.IsValid() {
return 0, "", false
}
if v.Type() == reflect.TypeOf(APIResponse{}) {
api := v.Interface().(APIResponse)
return api.Error, api.Msg, true
}
if v.Kind() != reflect.Struct {
return 0, "", false
}
if field := v.FieldByName("APIResponse"); field.IsValid() && field.CanInterface() {
if api, ok := field.Interface().(APIResponse); ok {
return api.Error, api.Msg, true
}
}
errorField := v.FieldByName("Error")
msgField := v.FieldByName("Msg")
if errorField.IsValid() && msgField.IsValid() && errorField.CanInt() && msgField.Kind() == reflect.String {
return int(errorField.Int()), msgField.String(), true
}
return 0, "", false
}
// setBrowserHeaders 设置接近浏览器抓包的通用请求头。
func (c *DouyuClient) setBrowserHeaders(req *http.Request, cookie string) {
req.Header.Set("Accept", "application/json, text/plain, */*")
+5 -6
View File
@@ -2,7 +2,6 @@ package main
import (
"errors"
"log"
"os"
"runtime"
@@ -59,13 +58,13 @@ func (s *appState) loadFont() {
source = "内置 Go Regular 字体"
font, err = freetype.ParseFont(goregular.TTF)
if err != nil {
log.Printf("内置字体解析失败,成功图片功能将跳过: %v", err)
logEvent("字体", "error", "内置字体解析失败,成功图片功能将跳过: %v", err)
return
}
log.Printf("未找到可用系统字体,使用内置字体兜底,中文可能显示不完整")
logEvent("字体", "warn", "未找到可用系统字体,使用内置字体兜底,中文可能显示不完整")
}
s.font = font
log.Printf("字体加载完成: %s", source)
logEvent("字体", "success", "字体加载完成: %s", source)
}
func parseFirstFont(candidates []string) (string, *truetype.Font, error) {
@@ -76,7 +75,7 @@ func parseFirstFont(candidates []string) (string, *truetype.Font, error) {
fontBytes, err := os.ReadFile(path)
if err != nil {
if !errors.Is(err, os.ErrNotExist) {
log.Printf("字体读取失败,已跳过: %s: %v", path, err)
logEvent("字体", "warn", "字体读取失败,已跳过: %s: %v", path, err)
}
continue
}
@@ -85,7 +84,7 @@ func parseFirstFont(candidates []string) (string, *truetype.Font, error) {
}
font, err := freetype.ParseFont(fontBytes)
if err != nil {
log.Printf("字体解析失败,已跳过: %s: %v", path, err)
logEvent("字体", "warn", "字体解析失败,已跳过: %s: %v", path, err)
continue
}
return path, font, nil
+5 -6
View File
@@ -4,7 +4,6 @@ import (
"errors"
"fmt"
"io"
"log"
"net/http"
"strconv"
"strings"
@@ -34,13 +33,13 @@ func (s *appState) handleProducts(w http.ResponseWriter, r *http.Request) {
if refresh {
products, err := s.client.Products()
if err != nil {
log.Printf("商品刷新失败: %v", err)
logEvent("商品", "error", "商品刷新失败: %v", err)
writeError(w, http.StatusBadGateway, err)
return
}
fetchedAt := time.Now()
s.setProducts(products, fetchedAt)
log.Printf("商品刷新完成: %d 个,刷新时间=%s", len(products), fetchedAt.In(cst).Format("15:04:05.000"))
logEvent("商品", "success", "商品刷新完成: %d 个,刷新时间=%s", len(products), fetchedAt.In(cst).Format("15:04:05.000"))
writeJSON(w, s.productPayload("live"))
return
}
@@ -51,13 +50,13 @@ func (s *appState) handleProducts(w http.ResponseWriter, r *http.Request) {
func (s *appState) handleLoadProducts(w http.ResponseWriter, _ *http.Request) {
products, err := s.client.Products()
if err != nil {
log.Printf("商品刷新失败: %v", err)
logEvent("商品", "error", "商品刷新失败: %v", err)
writeError(w, http.StatusBadGateway, err)
return
}
fetchedAt := time.Now()
s.setProducts(products, fetchedAt)
log.Printf("商品刷新完成: %d 个,刷新时间=%s", len(products), fetchedAt.In(cst).Format("15:04:05.000"))
logEvent("商品", "success", "商品刷新完成: %d 个,刷新时间=%s", len(products), fetchedAt.In(cst).Format("15:04:05.000"))
writeJSON(w, s.productPayload("live"))
}
@@ -159,7 +158,7 @@ func (s *appState) handleTime(w http.ResponseWriter, _ *http.Request) {
source := "currentTime"
if err != nil {
source = "localFallback"
log.Printf("时间接口降级: %v", err)
logEvent("时间", "warn", "时间接口降级: %v", err)
}
writeJSON(w, map[string]any{
"serverTime": serverTime.In(cst).Format("2006-01-02 15:04:05.000"),
+3 -4
View File
@@ -5,7 +5,6 @@ import (
"image"
"image/color"
"image/png"
"log"
"os"
"path/filepath"
"time"
@@ -71,13 +70,13 @@ func (s *appState) saveSuccessImage(account Account, product Product, roleName s
path := filepath.Join(s.paths.SuccessDir, name)
file, err := os.Create(path)
if err != nil {
log.Printf("成功图片创建失败: %v", err)
logEvent("图片", "error", "成功图片创建失败: %v", err)
return
}
defer file.Close()
if err := png.Encode(file, img); err != nil {
log.Printf("成功图片保存失败: %v", err)
logEvent("图片", "error", "成功图片保存失败: %v", err)
return
}
log.Printf("成功图片已保存: %s", path)
logEvent("图片", "success", "成功图片已保存: %s", path)
}
+66 -4
View File
@@ -1,6 +1,7 @@
package main
import (
"fmt"
"io"
"log"
"os"
@@ -61,12 +62,17 @@ func (s *memoryLogStore) Write(p []byte) (int, error) {
if strings.TrimSpace(line) == "" {
continue
}
category, level := parseLogMeta(line)
if level == "" {
level = detectLogLevel(line)
}
s.nextID++
s.entries = append(s.entries, LogEntry{
ID: s.nextID,
Time: time.Now().In(cst).Format("2006-01-02 15:04:05.000"),
Level: detectLogLevel(line),
Message: line,
ID: s.nextID,
Time: time.Now().In(cst).Format("2006-01-02 15:04:05.000"),
Level: level,
Category: category,
Message: line,
})
if len(s.entries) > s.max {
copy(s.entries, s.entries[len(s.entries)-s.max:])
@@ -76,6 +82,54 @@ func (s *memoryLogStore) Write(p []byte) (int, error) {
return len(p), nil
}
// logEvent 输出带分类和级别的结构化文本日志。
func logEvent(category, level, format string, args ...any) {
if category == "" {
category = "系统"
}
if level == "" {
level = "info"
}
log.Printf("[%s][%s] %s", category, level, fmt.Sprintf(format, args...))
}
// parseLogMeta 从标准日志行中提取类似 [网络][warn] 的分类和级别。
func parseLogMeta(line string) (string, string) {
idx := strings.Index(line, "[")
if idx < 0 {
return "", ""
}
category, rest, ok := cutBracket(line[idx:])
if !ok {
return "", ""
}
level, _, ok := cutBracket(rest)
if !ok || !isLogLevel(level) {
return category, ""
}
return category, level
}
func cutBracket(s string) (string, string, bool) {
if !strings.HasPrefix(s, "[") {
return "", s, false
}
end := strings.Index(s, "]")
if end <= 1 {
return "", s, false
}
return s[1:end], s[end+1:], true
}
func isLogLevel(level string) bool {
switch strings.ToLower(level) {
case "success", "error", "warn", "info":
return true
default:
return false
}
}
// setFilePath 保存当前日志文件路径,供前端展示或排查使用。
func (s *memoryLogStore) setFilePath(path string) {
s.mu.Lock()
@@ -108,6 +162,14 @@ func (s *memoryLogStore) FilePath() string {
func detectLogLevel(line string) string {
lower := strings.ToLower(line)
switch {
case strings.Contains(lower, "[success]"):
return "success"
case strings.Contains(lower, "[error]"):
return "error"
case strings.Contains(lower, "[warn]"):
return "warn"
case strings.Contains(lower, "[info]"):
return "info"
case strings.Contains(line, "成功"):
return "success"
case strings.Contains(line, "失败") || strings.Contains(line, "错误") || strings.Contains(lower, "error") || strings.Contains(lower, "panic"):
+4 -4
View File
@@ -19,18 +19,18 @@ func main() {
state.loadAccounts()
state.loadFont()
if err := os.MkdirAll(state.paths.SuccessDir, 0755); err != nil {
log.Printf("成功图片目录创建失败,图片保存功能可能不可用: %v", err)
logEvent("图片", "warn", "成功图片目录创建失败,图片保存功能可能不可用: %v", err)
}
if serverTime, err := state.client.ServerTime(); err == nil {
log.Printf("服务端时间获取成功: %s", serverTime.In(cst).Format("2006-01-02 15:04:05.000"))
logEvent("时间", "success", "服务端时间获取成功: %s", serverTime.In(cst).Format("2006-01-02 15:04:05.000"))
} else {
log.Printf("服务端时间获取失败: %v", err)
logEvent("时间", "error", "服务端时间获取失败: %v", err)
}
mux := http.NewServeMux()
state.registerRoutes(mux)
log.Printf("服务启动: http://%s | 账号: %d", serverAddr, state.accountCount())
logEvent("系统", "success", "服务启动: http://%s | 账号: %d", serverAddr, state.accountCount())
log.Fatal(http.ListenAndServe(serverAddr, mux))
}
+11 -10
View File
@@ -3,7 +3,6 @@ package main
import (
"errors"
"fmt"
"log"
"strings"
"sync"
"time"
@@ -14,11 +13,11 @@ func (s *appState) runNow(accountProducts map[string]string) {
s.clearResults()
attempts := s.buildAttempts(accountProducts)
if len(attempts) == 0 {
log.Printf("没有可执行的抢购任务")
logEvent("抢购", "warn", "没有可执行的抢购任务")
return
}
log.Printf("开始抢购: %d 个请求", len(attempts))
logEvent("抢购", "info", "开始抢购: %d 个请求", len(attempts))
limit := make(chan struct{}, defaultConcurrency)
var wg sync.WaitGroup
for _, attempt := range attempts {
@@ -32,15 +31,15 @@ func (s *appState) runNow(accountProducts map[string]string) {
key := fmt.Sprintf("%s_第%d包", result.Account, result.Attempt)
s.setResult(key, result.Message)
if result.OK {
log.Printf("成功: %s %s 第%d包 %s", result.Account, result.Product, result.Attempt, result.Message)
logEvent("抢购", "success", "成功: %s %s 第%d包 %s", result.Account, result.Product, result.Attempt, result.Message)
go s.saveSuccessImage(attempt.Account, attempt.Product, result.RoleName)
} else {
log.Printf("失败: %s %s 第%d包 %s", result.Account, result.Product, result.Attempt, result.Message)
logEvent("抢购", "error", "失败: %s %s 第%d包 %s", result.Account, result.Product, result.Attempt, result.Message)
}
}(attempt)
}
wg.Wait()
log.Printf("抢购任务结束")
logEvent("抢购", "info", "抢购任务结束")
}
// buildAttempts 根据前端传入的账号到商品映射,生成具体兑换尝试。
@@ -81,6 +80,7 @@ func (s *appState) runAttempt(attempt ExchangeAttempt) ExchangeResult {
Attempt: attempt.Index,
}
logEvent("抢购", "info", "执行兑换链路: 账号=%s 商品=%s 第%d包", attempt.Account.Name, attempt.Product.CommodityName, attempt.Index)
limitCSRF, err := s.client.GenerateCSRF(attempt.Account.Cookie)
if err != nil {
result.Message = "失败|生成CSRF失败: " + err.Error()
@@ -96,6 +96,7 @@ func (s *appState) runAttempt(attempt ExchangeAttempt) ExchangeResult {
result.Message = "失败|当前商品限制兑换"
return result
}
logEvent("抢购", "info", "批量限制通过: 账号=%s 商品=%s limit=%d", attempt.Account.Name, attempt.Product.CommodityName, limit)
preCSRF, err := s.client.GenerateCSRF(attempt.Account.Cookie)
if err != nil {
@@ -120,7 +121,7 @@ func (s *appState) runAttempt(attempt ExchangeAttempt) ExchangeResult {
roleName, err := s.client.RoleName(attempt.Account.Cookie)
if err != nil {
log.Printf("%s 角色名查询失败: %v", attempt.Account.Name, err)
logEvent("抢购", "warn", "%s 角色名查询失败: %v", attempt.Account.Name, err)
}
result.OK = true
@@ -150,7 +151,7 @@ func (s *appState) scheduleRun(req SeckillRequest) (map[string]any, error) {
serverTime, err := s.client.ServerTime()
if err != nil {
log.Printf("定时使用降级时间: %v", err)
logEvent("定时", "warn", "定时使用降级时间: %v", err)
}
advance := time.Duration(req.Advance) * time.Millisecond
@@ -166,12 +167,12 @@ func (s *appState) scheduleRun(req SeckillRequest) (map[string]any, error) {
s.schedule.Stop()
}
s.schedule = time.AfterFunc(remain, func() {
log.Printf("定时触发: 本地=%s", time.Now().In(cst).Format("15:04:05.000"))
logEvent("定时", "success", "定时触发: 本地=%s", time.Now().In(cst).Format("15:04:05.000"))
s.runNow(ap)
})
s.scheduleMu.Unlock()
log.Printf("定时已设置: 目标=%s 触发=%s 提前=%dms",
logEvent("定时", "success", "定时已设置: 目标=%s 触发=%s 提前=%dms",
targetTime.In(cst).Format("2006-01-02 15:04:05.000"),
triggerTime.In(cst).Format("2006-01-02 15:04:05.000"),
req.Advance,
+4 -5
View File
@@ -2,7 +2,6 @@ package main
import (
"bufio"
"log"
"os"
"strings"
"time"
@@ -16,7 +15,7 @@ func (s *appState) loadAccounts() {
}
f, err := os.Open(path)
if err != nil {
log.Printf("读取账号文件失败: %s: %v", path, err)
logEvent("账号", "error", "读取账号文件失败: %s: %v", path, err)
s.accountsMu.Lock()
s.accounts = nil
s.accountsMu.Unlock()
@@ -34,7 +33,7 @@ func (s *appState) loadAccounts() {
}
parts := strings.SplitN(line, "----", 3)
if len(parts) != 3 {
log.Printf("账号行格式错误,已跳过: %s", line)
logEvent("账号", "warn", "账号行格式错误,已跳过: %s", line)
continue
}
list = append(list, Account{
@@ -44,13 +43,13 @@ func (s *appState) loadAccounts() {
})
}
if err := scanner.Err(); err != nil {
log.Printf("读取账号文件中断: %v", err)
logEvent("账号", "error", "读取账号文件中断: %v", err)
}
s.accountsMu.Lock()
s.accounts = list
s.accountsMu.Unlock()
log.Printf("账号加载完成: %d 个,来源=%s", len(list), path)
logEvent("账号", "success", "账号加载完成: %d 个,来源=%s", len(list), path)
}
// accountCount 返回当前账号数。
+13 -6
View File
@@ -76,9 +76,10 @@
.log-body{flex:1;overflow-y:auto;padding:10px 16px;font-family:'Consolas','Courier New',monospace;font-size:13px;line-height:1.8}
.log-s{color:#7ee787}
.log-e{color:#f78166}
.log-i{color:#58a6ff}
.log-w{color:#f0883e}
.log-result{background:#1a1f2b;padding:2px 8px;margin:2px 0;border-radius:3px;word-break:break-all}
.log-i{color:#58a6ff}
.log-w{color:#f0883e}
.log-result{background:#1a1f2b;padding:2px 8px;margin:2px 0;border-radius:3px;word-break:break-all}
.log-cat{display:inline-block;min-width:34px;text-align:center;margin-right:6px;padding:0 5px;border:1px solid#30363d;border-radius:4px;color:#8b949e;font-size:11px;line-height:1.4}
/* ========== 右侧滑入提示 ========== */
.toast-container{position:fixed;top:20px;right:-420px;z-index:9999;transition:right .4s ease}
@@ -632,18 +633,24 @@
if (item.level === 'success') type = 's';
else if (item.level === 'error') type = 'e';
else if (item.level === 'warn') type = 'w';
addServerLog(item.message, type);
addServerLog(item.message, type, item.category);
});
} catch (e) { }
}
// addServerLog 追加一条后端日志,和本地操作日志共用同一个日志区域。
function addServerLog(msg, type = 'i') {
function addServerLog(msg, type = 'i', category = '') {
let el = document.getElementById('logBody');
el.innerHTML += `<div class="log-${type}">[后端] ${escHtml(msg)}</div>`;
let cat = category ? `<span class="log-cat">${escHtml(category)}</span>` : '';
el.innerHTML += `<div class="log-${type}">[后端] ${cat}${escHtml(cleanServerLogMessage(msg))}</div>`;
el.scrollTop = el.scrollHeight;
}
// cleanServerLogMessage 去掉后端结构化日志前缀,分类由单独标签展示。
function cleanServerLogMessage(msg) {
return String(msg ?? '').replace(/\[[^\]]+\]\[(info|success|warn|error)\]\s*/i, '');
}
// buildAP 根据已勾选账号和账号绑定商品,生成后端需要的账号到商品 ID 映射。
function buildAP() {
+5 -4
View File
@@ -70,10 +70,11 @@ type APIResponse struct {
}
type LogEntry struct {
ID int64 `json:"id"`
Time string `json:"time"`
Level string `json:"level"`
Message string `json:"message"`
ID int64 `json:"id"`
Time string `json:"time"`
Level string `json:"level"`
Category string `json:"category"`
Message string `json:"message"`
}
type appState struct {