优化日志系统:网络请求降噪并按接口聚合,补齐双时间说明与实例标识,发布0.3.2

- 逐条网络日志(请求完成 / 接口返回异常 / 响应刷新CSRF已同步)从 info/warn 降为 debug:
  实测一次 6 账号的模拟抢购里 233 条日志有 174 条(74%)属于这类;真实抢购会产生几千条,
  把界面仅 800 条的内存日志冲掉,反而看不到关键信息。HTTP/解析/传输层错误仍为 error 级
- 新增按接口聚合的网络统计,抢购任务结束时输出一行,替代逐条刷屏:
  "接口统计: 锁定兑换商品 ×341 失败341 平均=95ms 最慢=312ms 最后错误=活动火爆,请稍后再试"
- 日志文件开头新增表头:实例名、数据目录、双时间格式说明(本地时间 + D 斗鱼估算时间)、
  并说明"校准完成前偏移为 0,只显示本地时间"。双时间一直存在却没有任何地方解释过,
  多实例日志复制或汇总后也分不清来源
- 界面日志面板:显示日志级别并按级别着色(此前级别只存在于 CSS class,复制出去就丢);
  新增"复制"按钮,按与日志文件一致的单行格式导出(此前直接复制会变成 5 行一条、
  1204 行里其实只有 233 条日志)
- 设置页"日志显示等级"补充说明:Info 起只显示关键日志,需要逐请求细节时切到 Debug
- 新增测试:TestRequestStatsAggregatesByLabel、TestLogFileHeaderDocumentsFormat、
  TestRunNowLogsAggregateInsteadOfPerRequest(断言必须有聚合行、且不得出现逐条请求日志)
This commit is contained in:
yml2213
2026-10-03 19:33:14 +08:00
parent 43270e04a6
commit dce9ce2a5a
12 changed files with 328 additions and 11 deletions
+25 -2
View File
@@ -1,10 +1,24 @@
<script setup lang="ts">
import { nextTick, ref, watch } from 'vue'
import { ClipboardSetText } from '@wails/runtime/runtime'
import { useLogs } from '@/composables/useLogs'
const { logs, clearLogs } = useLogs()
const { logs, clearLogs, exportLogsText } = useLogs()
const expanded = ref(false)
const logBodyRef = ref<HTMLElement | null>(null)
const copyState = ref('复制')
const copyLogs = async () => {
if (logs.value.length === 0) return
try {
// 复制成与后端日志文件一致的单行格式,避免面板的多 span 变成 5 行一条、丢失级别。
const ok = await ClipboardSetText(exportLogsText())
copyState.value = ok ? '已复制' : '复制失败'
} catch {
copyState.value = '复制失败'
}
setTimeout(() => { copyState.value = '复制' }, 1500)
}
watch(logs, async () => {
if (!expanded.value) return
@@ -21,7 +35,10 @@ watch(logs, async () => {
<span>日志</span>
<span v-if="logs.length" class="log-count">{{ logs.length }}</span>
</button>
<button v-if="expanded" class="btn btn-compact" @click="clearLogs">清空</button>
<div v-if="expanded" class="log-actions">
<button class="btn btn-compact" @click="copyLogs">{{ copyState }}</button>
<button class="btn btn-compact" @click="clearLogs">清空</button>
</div>
</div>
<div v-if="expanded" ref="logBodyRef" class="log-body">
<div v-if="logs.length === 0" class="log-i">[系统] 就绪,等待操作...</div>
@@ -31,6 +48,7 @@ watch(logs, async () => {
<span v-if="log.time" class="log-time">[{{ log.time }}]</span>
<span v-if="log.douyuTime" class="log-douyu-time">D:{{ log.douyuTime }}</span>
<span v-if="log.category" class="log-cat">{{ log.category }}</span>
<span class="log-level">[{{ log.level || 'info' }}]</span>
<span v-if="log.account" class="log-account">{{ log.account }}</span>
<span class="log-msg">{{ log.message }}</span>
</template>
@@ -51,6 +69,11 @@ watch(logs, async () => {
.log-line { display: flex; align-items: flex-start; gap: 4px; padding: 2px 0; word-break: break-all; }
.log-prefix, .log-time { color: var(--muted); flex-shrink: 0; font-size: 10px; }
.log-douyu-time { color: var(--accent); flex-shrink: 0; font-size: 10px; font-weight: 600; }
.log-actions { display: inline-flex; gap: 6px; }
.log-level { color: var(--muted-2); flex-shrink: 0; font-size: 10px; }
.log-line.log-w .log-level { color: var(--warning); }
.log-line.log-e .log-level { color: var(--danger); }
.log-line.log-s .log-level { color: var(--success); }
.log-cat, .log-account { display: inline-block; padding: 0 4px; border-radius: 3px; font-size: 10px; line-height: 1.4; flex-shrink: 0; }
.log-cat { border: 1px solid var(--line); color: var(--muted); background: var(--surface); }
.log-account { max-width: 96px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; border: 1px solid var(--accent-border); color: var(--accent); background: var(--accent-soft); }
@@ -130,6 +130,10 @@ defineExpose({ open, close })
<option value="warn">Warn</option>
<option value="error">Error</option>
</select>
<div class="form-hint">
Info 起只显示关键日志;需要看每个请求的耗时与错误码时切到 Debug
(抢购任务结束时仍会输出各类接口的<strong>聚合统计</strong>)
</div>
</div>
</div>
</div>
+23
View File
@@ -13,7 +13,9 @@ export interface LogEntry {
export interface LogMessage {
time: string
rawTime?: string
douyuTime?: string
level?: string
type: 's' | 'e' | 'i' | 'w' | 'd'
message: string
category?: string
@@ -57,6 +59,21 @@ const cleanServerLogMessage = (message: string): string => {
.replace(/^\[账号:[^\]]+\]\s*/i, '')
}
// formatLogLine 把一条日志还原成与后端日志文件一致的单行格式。
// 面板用多个 span 渲染,直接复制会变成 5 行一条、还丢掉级别,
// 所以"复制"走这里:时间(D斗鱼时间) [类别][级别][账号] 正文。
const formatLogLine = (log: LogMessage): string => {
if (log.source !== 'server') {
return `[${log.time}] ${log.message}`
}
const time = log.rawTime || log.time || ''
const douyu = log.douyuTime ? `(D${log.douyuTime})` : ''
const category = log.category ? `[${log.category}]` : ''
const level = `[${log.level || 'info'}]`
const account = log.account ? `[账号:${log.account}]` : ''
return `${time}${douyu} ${category}${level}${account} ${log.message}`.replace(/\s+/g, ' ').trim()
}
// 后端结果按“账号_第N包”返回,账号表按账号名展示,这里做一次归并。
const normalizeResultStatuses = (data: Record<string, string>): Record<string, AccountStatus> => {
const next: Record<string, AccountStatus> = {}
@@ -111,7 +128,9 @@ export function useLogs() {
logs.value.push({
time: formatServerLogTime(item.time || ''),
rawTime: item.time || '',
douyuTime: dyTime || undefined,
level: item.level,
type,
message: cleanMsg,
category: item.category,
@@ -162,9 +181,13 @@ export function useLogs() {
clearLocalResults()
}
// exportLogsText 导出全部日志为单行文本,供"复制"按钮使用。
const exportLogsText = (): string => logs.value.map(formatLogLine).join('\n')
return {
logs,
results,
exportLogsText,
addLocalLog,
fetchServerLogs,
checkResults,
+1 -1
View File
@@ -146,7 +146,7 @@ func (a *App) Startup(ctx context.Context) {
}
configMigrated, configMigrationErr := config.MigrateLegacyFile(a.paths.ConfigFile, a.paths.LegacyConfigFile)
a.logStore = logger.NewMemoryLogStore(800)
a.closeLog = logger.InitLogging(a.logStore, a.paths.LogDir)
a.closeLog = logger.InitLogging(a.logStore, a.paths.LogDir, a.instanceName, a.paths.DataDir)
if instanceErr != nil {
// 数据目录仍然是本实例独享的(临时实例),但隔离能力已降级,必须让人看见。
logger.LogEvent("系统", "warn", "实例隔离已降级(%s,数据目录仍独立): %v", a.instanceName, instanceErr)
+73 -3
View File
@@ -32,6 +32,22 @@ type DouyuClient struct {
xiaodianEmbedMu sync.Mutex
xiaodianEmbedCache map[string]xiaodianEmbedCacheItem
statsMu sync.Mutex
reqStats map[string]*RequestStat
}
// RequestStat 累计某一类接口的调用情况。
//
// 逐条网络日志现在降为 debug 级(一次真实抢购会产生几千条),靠这份聚合结果
// 保留诊断价值:哪类接口调了多少次、失败多少、平均/最慢多久、最后一次错在哪。
type RequestStat struct {
Label string
Count int
Failed int
Total time.Duration
Slowest time.Duration
LastErr string
}
type xiaodianEmbedCacheItem struct {
@@ -1698,7 +1714,7 @@ func (c *DouyuClient) postMultipart(url, cookie string, csrf *string, label stri
if resp != nil {
if nextCSRF := csrfFromResponse(resp); nextCSRF != "" && csrf != nil {
if nextCSRF != *csrf {
logger.LogEvent("网络", "info", "响应刷新CSRF已同步: %s %s %s", label, req.Method, requestPath(req))
logger.LogEvent("网络", "debug", "响应刷新CSRF已同步: %s %s %s", label, req.Method, requestPath(req))
}
if ctnVal != "" {
*csrf = nextCSRF + "\x00" + ctnVal
@@ -1788,7 +1804,57 @@ func shouldLogNetworkSuccess(label string) bool {
return label != "服务时间"
}
// recordRequest 记录一次请求结果(每类接口一条聚合)。
func (c *DouyuClient) recordRequest(label string, cost time.Duration, failed bool, errMsg string) {
c.statsMu.Lock()
defer c.statsMu.Unlock()
if c.reqStats == nil {
c.reqStats = make(map[string]*RequestStat)
}
stat := c.reqStats[label]
if stat == nil {
stat = &RequestStat{Label: label}
c.reqStats[label] = stat
}
stat.Count++
stat.Total += cost
if cost > stat.Slowest {
stat.Slowest = cost
}
if failed {
stat.Failed++
if errMsg != "" {
stat.LastErr = utils.TrimForLog(errMsg, 60)
}
}
}
// ResetRequestStats 清空聚合统计(每次抢购任务开始时调用)。
func (c *DouyuClient) ResetRequestStats() {
c.statsMu.Lock()
c.reqStats = make(map[string]*RequestStat)
c.statsMu.Unlock()
}
// RequestStats 返回按调用次数降序排列的聚合统计。
func (c *DouyuClient) RequestStats() []RequestStat {
c.statsMu.Lock()
out := make([]RequestStat, 0, len(c.reqStats))
for _, stat := range c.reqStats {
out = append(out, *stat)
}
c.statsMu.Unlock()
sort.Slice(out, func(i, j int) bool {
if out[i].Count != out[j].Count {
return out[i].Count > out[j].Count
}
return out[i].Label < out[j].Label
})
return out
}
func (c *DouyuClient) logNetworkSuccess(label string, req *http.Request, cost time.Duration, apiError int, apiMsg string) {
c.recordRequest(label, cost, apiError != 0, apiMsg)
if !shouldLogNetworkSuccess(label) {
return
}
@@ -1796,25 +1862,29 @@ func (c *DouyuClient) logNetworkSuccess(label string, req *http.Request, cost ti
if apiMsg != "" || apiError != 0 {
extra = fmt.Sprintf(" apiError=%d msg=%s", apiError, utils.TrimForLog(apiMsg, 80))
}
logger.LogEvent("网络", "info", "请求完成: %s 耗时=%s%s", c.networkDiagnostic(label, req), utils.RoundDuration(cost), extra)
logger.LogEvent("网络", "debug", "请求完成: %s 耗时=%s%s", c.networkDiagnostic(label, req), utils.RoundDuration(cost), extra)
}
func (c *DouyuClient) logNetworkAPIResult(label string, req *http.Request, cost time.Duration, apiError int, apiMsg string) {
logger.LogEvent("网络", "warn", "接口返回异常: %s 耗时=%s apiError=%d msg=%s",
c.recordRequest(label, cost, true, apiMsg)
logger.LogEvent("网络", "debug", "接口返回异常: %s 耗时=%s apiError=%d msg=%s",
c.networkDiagnostic(label, req), utils.RoundDuration(cost), apiError, utils.TrimForLog(apiMsg, 120))
}
func (c *DouyuClient) logNetworkHTTPError(label string, req *http.Request, status int, cost time.Duration, body []byte) {
c.recordRequest(label, cost, true, fmt.Sprintf("HTTP %d", status))
logger.LogEvent("网络", "error", "HTTP异常: %s 状态=%d 耗时=%s 响应=%s",
c.networkDiagnostic(label, req), status, utils.RoundDuration(cost), utils.TrimForLog(string(body), 160))
}
func (c *DouyuClient) logNetworkParseError(label string, req *http.Request, cost time.Duration, body []byte, err error) {
c.recordRequest(label, cost, true, err.Error())
logger.LogEvent("网络", "error", "响应解析失败: %s 耗时=%s 错误=%v 响应=%s",
c.networkDiagnostic(label, req), utils.RoundDuration(cost), err, utils.TrimForLog(string(body), 160))
}
func (c *DouyuClient) logNetworkError(label string, req *http.Request, cost time.Duration, err error) {
c.recordRequest(label, cost, true, err.Error())
logger.LogEvent("网络", "error", "请求失败: %s 耗时=%s 错误=%v",
c.networkDiagnostic(label, req), utils.RoundDuration(cost), err)
}
+42
View File
@@ -8,6 +8,7 @@ import (
"strings"
"sync/atomic"
"testing"
"time"
"douyu_go/internal/activity"
"douyu_go/internal/trace"
@@ -499,3 +500,44 @@ func testCTNClient(t *testing.T, ctnURL string) *DouyuClient {
cli.HTTPClient = http.DefaultClient
return cli
}
// TestRequestStatsAggregatesByLabel 验证网络日志降噪后仍保留诊断价值:
// 同类接口聚合成一条统计(次数/失败/平均/最慢/最后错误),并可按任务重置。
func TestRequestStatsAggregatesByLabel(t *testing.T) {
cli := &DouyuClient{}
cli.recordRequest("锁定兑换商品", 80*time.Millisecond, true, "活动火爆,请稍后再试")
cli.recordRequest("锁定兑换商品", 120*time.Millisecond, true, "当前网络繁忙,请稍后再试")
cli.recordRequest("锁定兑换商品", 60*time.Millisecond, false, "")
cli.recordRequest("支付锁单", 300*time.Millisecond, false, "")
stats := cli.RequestStats()
if len(stats) != 2 {
t.Fatalf("统计条目 = %d, want 2: %#v", len(stats), stats)
}
// 次数多的排前面
if stats[0].Label != "锁定兑换商品" {
t.Fatalf("首条应为调用最多的接口, got %q", stats[0].Label)
}
lock := stats[0]
if lock.Count != 3 || lock.Failed != 2 {
t.Fatalf("锁定兑换商品 次数=%d 失败=%d, want 3/2", lock.Count, lock.Failed)
}
if lock.Slowest != 120*time.Millisecond {
t.Fatalf("最慢 = %v, want 120ms", lock.Slowest)
}
if lock.Total != 260*time.Millisecond {
t.Fatalf("总耗时 = %v, want 260ms", lock.Total)
}
if lock.LastErr != "当前网络繁忙,请稍后再试" {
t.Fatalf("最后一次错误 = %q", lock.LastErr)
}
if stats[1].Failed != 0 || stats[1].LastErr != "" {
t.Fatalf("支付锁单不应有失败记录: %#v", stats[1])
}
cli.ResetRequestStats()
if got := cli.RequestStats(); len(got) != 0 {
t.Fatalf("重置后应为空, got %#v", got)
}
}
+1 -1
View File
@@ -3,7 +3,7 @@ package constants
import "time"
// Version 应用版本号,与 wails.json version 同步。
const Version = "0.3.1"
const Version = "0.3.2"
// BuildTime 构建时间,由编译时 ldflags 注入。
var BuildTime = "unknown"
+30 -1
View File
@@ -141,7 +141,10 @@ var globalSink *sink
// InitLogging 初始化日志输出:日志文件和内存缓冲同时写入。
// 终端输出通过 sink 同步写入(Windows GUI 应用无控制台时自动跳过)。
func InitLogging(store *MemoryLogStore, logDir string) func() {
//
// instanceName / dataDir 只用于写文件表头,便于把日志复制/汇总后仍能看出
// 是哪台机器的哪个实例。
func InitLogging(store *MemoryLogStore, logDir, instanceName, dataDir string) func() {
log.SetFlags(0)
log.SetPrefix("")
@@ -166,6 +169,10 @@ func InitLogging(store *MemoryLogStore, logDir string) func() {
return func() {}
}
// 表头同步写入,确保排在任何日志之前(异步写入器还没启动)。
// 写失败不影响日志本身。
_, _ = file.WriteString(logFileHeader(instanceName, dataDir))
store.SetFilePath(filePath)
globalSink = &sink{
async: NewAsyncLogWriter(file, asyncLogBufferSize),
@@ -181,6 +188,28 @@ func InitLogging(store *MemoryLogStore, logDir string) func() {
}
}
// logFileHeader 说明日志格式与运行上下文。
//
// 双时间(本地时间 + 斗鱼估算时间)一直存在,但日志里没有任何地方解释过;
// 而且时间校准完成前偏移为 0、只显示本地时间,看起来像格式不一致。
// 把格式、实例名、数据目录写在文件开头,复制或汇总多实例日志时就分得清。
func logFileHeader(instanceName, dataDir string) string {
if instanceName == "" {
instanceName = "未知实例"
}
var b strings.Builder
b.WriteString("==================================================================\n")
fmt.Fprintf(&b, "实例: %s\n", instanceName)
if dataDir != "" {
fmt.Fprintf(&b, "数据目录: %s\n", dataDir)
}
b.WriteString("时间格式: 本地时间(D斗鱼估算时间) [类别][级别][账号] 正文\n")
b.WriteString(" D 为经服务端校准的斗鱼时间估算;校准完成前偏移为 0,只显示本地时间\n")
b.WriteString("网络逐条请求为 debug 级,默认不显示;排查时把配置 logLevel 改成 debug\n")
b.WriteString("==================================================================\n")
return b.String()
}
// createUniqueLogFile 以 O_EXCL 递增后缀创建日志文件。
//
// 早期实现用 O_CREATE|O_APPEND + 秒级时间戳:同一目录下同秒启动的两个进程会
+38 -1
View File
@@ -1,6 +1,7 @@
package logger
import (
"os"
"path/filepath"
"strings"
"testing"
@@ -10,7 +11,7 @@ import (
// LogEntry 的 category/level/account 应直接由调用方填入,而非字符串反解析。
func TestStructuredLogging(t *testing.T) {
store := NewMemoryLogStore(100)
cleanup := InitLogging(store, t.TempDir())
cleanup := InitLogging(store, t.TempDir(), "测试实例", "/tmp/douyu-go-test")
defer func() {
cleanup()
globalSink = nil
@@ -98,3 +99,39 @@ func TestCreateUniqueLogFileAvoidsCollision(t *testing.T) {
t.Fatalf("second = %q, want 同目录 -2 后缀", got)
}
}
// TestLogFileHeaderDocumentsFormat 日志文件开头必须说明双时间含义、实例名与数据目录,
// 否则把日志复制或汇总后看不出两个时间分别是什么、来自哪台机器的哪个实例。
func TestLogFileHeaderDocumentsFormat(t *testing.T) {
dir := t.TempDir()
store := NewMemoryLogStore(20)
cleanup := InitLogging(store, dir, "实例 2", `C:\Users\Administrator\AppData\Roaming\douyu-go\instances\instance-2`)
LogEvent("系统", "success", "已启动实例 2")
cleanup()
globalSink = nil
path := store.FilePath()
if path == "" {
t.Fatal("日志文件路径为空")
}
data, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
content := string(data)
for _, want := range []string{
"实例: 实例 2",
"数据目录: ",
"时间格式: 本地时间(D斗鱼估算时间)",
"校准完成前偏移为 0",
"logLevel 改成 debug",
"已启动实例 2",
} {
if !strings.Contains(content, want) {
t.Errorf("日志文件缺少 %q\n实际内容:\n%s", want, content)
}
}
if !strings.HasPrefix(content, "====") {
t.Errorf("表头应在文件最开始:\n%s", content)
}
}
+27
View File
@@ -437,6 +437,12 @@ func (s *Scheduler) RunNow(accountProducts map[string]string) {
logger.LogEvent("抢购", "info", "抢购目标: %s", target)
}
// 逐条网络日志已降为 debug 级,这里开始统计每类接口的调用情况,
// 任务结束时输出一行聚合结果。
if cli := s.clientSnapshot(); cli != nil {
cli.ResetRequestStats()
}
outcomes := make([]accountOutcome, len(tasks))
var wg sync.WaitGroup
for i, task := range tasks {
@@ -449,6 +455,27 @@ func (s *Scheduler) RunNow(accountProducts map[string]string) {
wg.Wait()
s.logRunSummary(outcomes, ctl, time.Since(start))
s.logNetworkSummary(s.clientSnapshot())
}
// logNetworkSummary 输出本次任务期间各类接口的聚合统计,替代逐条刷屏的网络日志。
func (s *Scheduler) logNetworkSummary(cli *client.DouyuClient) {
if cli == nil {
return
}
for _, stat := range cli.RequestStats() {
avg := time.Duration(0)
if stat.Count > 0 {
avg = stat.Total / time.Duration(stat.Count)
}
if stat.LastErr != "" {
logger.LogEvent("网络", "info", "接口统计: %s ×%d 失败%d 平均=%s 最慢=%s 最后错误=%s",
stat.Label, stat.Count, stat.Failed, utils.RoundDuration(avg), utils.RoundDuration(stat.Slowest), stat.LastErr)
continue
}
logger.LogEvent("网络", "info", "接口统计: %s ×%d 失败%d 平均=%s 最慢=%s",
stat.Label, stat.Count, stat.Failed, utils.RoundDuration(avg), utils.RoundDuration(stat.Slowest))
}
}
// 账号最终结束状态。
+62
View File
@@ -15,6 +15,7 @@ import (
"douyu_go/internal/activity"
"douyu_go/internal/client"
"douyu_go/internal/logger"
"douyu_go/internal/trace"
"douyu_go/internal/types"
)
@@ -928,3 +929,64 @@ func TestFilterCooldownAttempts(t *testing.T) {
t.Errorf("nextReady should be after now")
}
}
// TestRunNowLogsAggregateInsteadOfPerRequest 锁住日志降噪:
// 默认 info 级下,逐条网络请求不应进入日志,但任务结束要有一行按接口聚合的统计。
func TestRunNowLogsAggregateInsteadOfPerRequest(t *testing.T) {
speedUpPeace(t)
seckillMaxDuration = 1200 * time.Millisecond
scheduler := newOrderTestScheduler(t, func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
switch r.URL.Path {
case "/create":
_, _ = w.Write([]byte(`{"error":1644402023,"msg":"商品本次活动库存已耗完"}`))
case "/bind":
_, _ = w.Write([]byte(`{"error":0,"data":{"gameRole":{"roleName":"测试角色"}}}`))
}
})
scheduler.paths = writeAccountFile(t, "a", "b")
scheduler.accounts = NewAccountStore(scheduler.paths.AccountFile)
scheduler.LoadAccounts()
scheduler.UpdateProducts([]types.Product{{CommodityID: "goods-1", CommodityName: "测试商品"}})
store := logger.NewMemoryLogStore(4000)
cleanup := logger.InitLogging(store, t.TempDir(), "测试实例", "/tmp/douyu-go-test")
defer cleanup()
scheduler.RunNow(map[string]string{"a": "goods-1", "b": "goods-1"})
var aggregate, perRequest, summary string
for _, entry := range store.Since(0) {
switch {
case strings.Contains(entry.Message, "接口统计: 锁定兑换商品"):
aggregate = entry.Message
case strings.Contains(entry.Message, "请求完成:"):
perRequest = entry.Message
case strings.Contains(entry.Message, "抢购任务结束:"):
summary = entry.Message
}
}
if aggregate == "" {
t.Fatalf("缺少按接口聚合的统计行;实际日志:\n%s", dumpMessages(store))
}
if !strings.Contains(aggregate, "失败") || !strings.Contains(aggregate, "最慢=") {
t.Fatalf("聚合行信息不足: %q", aggregate)
}
if perRequest != "" {
t.Fatalf("逐条网络请求日志不应在 info 级出现: %q", perRequest)
}
if summary == "" {
t.Fatalf("缺少任务汇总行;实际日志:\n%s", dumpMessages(store))
}
}
func dumpMessages(store *logger.MemoryLogStore) string {
var b strings.Builder
for _, entry := range store.Since(0) {
b.WriteString(entry.Message)
b.WriteString("\n")
}
return b.String()
}
+2 -2
View File
@@ -2,7 +2,7 @@
"$schema": "https://wails.io/schemas/config.v2.json",
"name": "douyu_go",
"outputfilename": "douyu_go",
"version": "0.3.1",
"version": "0.3.2",
"frontend:dir": "frontend",
"frontend:install": "npm install",
"frontend:build": "npm run build",
@@ -15,6 +15,6 @@
"email": "yml@example.com"
},
"info": {
"productVersion": "0.3.1"
"productVersion": "0.3.2"
}
}