优化日志系统
This commit is contained in:
@@ -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
@@ -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, */*")
|
||||
|
||||
@@ -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
@@ -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"),
|
||||
|
||||
@@ -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
@@ -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"):
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
@@ -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() {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user