diff --git a/config.go b/config.go index 8e040ea..088fa0c 100644 --- a/config.go +++ b/config.go @@ -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 } diff --git a/douyu_client.go b/douyu_client.go index 74c0665..7f419c9 100644 --- a/douyu_client.go +++ b/douyu_client.go @@ -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, */*") diff --git a/font.go b/font.go index 0466ab1..4cb973d 100644 --- a/font.go +++ b/font.go @@ -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 diff --git a/handlers.go b/handlers.go index 3578238..9ffda28 100644 --- a/handlers.go +++ b/handlers.go @@ -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"), diff --git a/image.go b/image.go index dc1185e..5e6f3de 100644 --- a/image.go +++ b/image.go @@ -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) } diff --git a/log_store.go b/log_store.go index 3ead305..f6dabd9 100644 --- a/log_store.go +++ b/log_store.go @@ -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"): diff --git a/main.go b/main.go index 1a7a323..3f7f2f1 100644 --- a/main.go +++ b/main.go @@ -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)) } diff --git a/seckill.go b/seckill.go index 1e0d474..a09377b 100644 --- a/seckill.go +++ b/seckill.go @@ -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, diff --git a/state.go b/state.go index 5eff048..bbb8ba8 100644 --- a/state.go +++ b/state.go @@ -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 返回当前账号数。 diff --git a/static/index.html b/static/index.html index 73f793d..825118f 100644 --- a/static/index.html +++ b/static/index.html @@ -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 += `
[后端] ${escHtml(msg)}
`; + let cat = category ? `${escHtml(category)}` : ''; + el.innerHTML += `
[后端] ${cat}${escHtml(cleanServerLogMessage(msg))}
`; el.scrollTop = el.scrollHeight; } + // cleanServerLogMessage 去掉后端结构化日志前缀,分类由单独标签展示。 + function cleanServerLogMessage(msg) { + return String(msg ?? '').replace(/\[[^\]]+\]\[(info|success|warn|error)\]\s*/i, ''); + } + // buildAP 根据已勾选账号和账号绑定商品,生成后端需要的账号到商品 ID 映射。 function buildAP() { diff --git a/types.go b/types.go index 758b18f..7d71dfb 100644 --- a/types.go +++ b/types.go @@ -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 {