功能: - 新增CK管理页面,支持复验抢购结果和整理账号文件 - 复验功能:查询斗鱼真实兑换记录,统计已抢到/未抢到的账号 - 整理CK功能:分离已抢到的账号到新文件,原文件只保留未抢到的 - 导出文件名优化:使用中文商品名+日期时间格式(如:user_已抢到_雪国2号_20260626_150405.txt) - 复用左侧商品选择,统一数据源 接口: - VerifyAndExport: 复验并统计 - ExportAccountsWithProduct: 导出账号(自动命名) - OrganizeAccounts: 整理账号文件 版本: 0.1.0
715 lines
19 KiB
Go
715 lines
19 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/golang/freetype/truetype"
|
|
|
|
"douyu_go/internal/client"
|
|
"douyu_go/internal/config"
|
|
"douyu_go/internal/constants"
|
|
imgpkg "douyu_go/internal/image"
|
|
"douyu_go/internal/logger"
|
|
"douyu_go/internal/seckill"
|
|
"douyu_go/internal/trace"
|
|
"douyu_go/internal/types"
|
|
)
|
|
|
|
// App 应用主结构体,所有导出方法可被前端通过 Wails 绑定调用。
|
|
type App struct {
|
|
ctx context.Context
|
|
|
|
client *client.DouyuClient
|
|
scheduler *seckill.Scheduler
|
|
logStore *logger.MemoryLogStore
|
|
trace *trace.Recorder
|
|
paths types.AppPaths
|
|
font *truetype.Font
|
|
cfg types.Config
|
|
|
|
productsMu sync.RWMutex
|
|
products []types.Product
|
|
productsAt time.Time
|
|
|
|
closeLog func()
|
|
closeTrace func()
|
|
}
|
|
|
|
// NewApp 创建 App 实例。
|
|
func NewApp() *App {
|
|
return &App{}
|
|
}
|
|
|
|
// startup Wails 应用启动时调用。
|
|
func (a *App) startup(ctx context.Context) {
|
|
a.ctx = ctx
|
|
|
|
a.paths = config.DefaultAppPaths()
|
|
a.logStore = logger.NewMemoryLogStore(800)
|
|
a.closeLog = logger.InitLogging(a.logStore, a.paths.LogDir)
|
|
|
|
// 加载配置并应用运行时设置
|
|
a.cfg = config.LoadConfig(a.paths.ConfigFile)
|
|
logger.SetMinLevel(a.cfg.LogLevel)
|
|
|
|
traceCfg := types.TraceConfig{
|
|
Enabled: a.cfg.TraceEnabled,
|
|
MaxBodyBytes: a.cfg.TraceMaxBody,
|
|
}
|
|
tr, closeTrace := trace.InitTrace(a.paths.LogDir, traceCfg)
|
|
a.trace = tr
|
|
a.closeTrace = closeTrace
|
|
|
|
a.client = client.New(a.cfg.TimeAPIURL, a.trace)
|
|
a.font = imgpkg.LoadFont(a.paths.FontFiles)
|
|
a.scheduler = seckill.New(a.client, a.paths, a.font, a.cfg.Concurrency, a.cfg.Attempts)
|
|
a.scheduler.LoadAccounts()
|
|
|
|
if err := os.MkdirAll(a.paths.SuccessDir, 0755); err != nil {
|
|
logger.LogEvent("图片", "warn", "成功图片目录创建失败,图片保存功能可能不可用: %v", err)
|
|
}
|
|
|
|
if serverTime, err := a.client.ServerTime(); err == nil {
|
|
logger.LogEvent("时间", "success", "服务端时间获取成功: %s", serverTime.In(constants.CST).Format("2006-01-02 15:04:05.000"))
|
|
} else {
|
|
logger.LogEvent("时间", "error", "服务端时间获取失败: %v", err)
|
|
}
|
|
|
|
logger.LogEvent("系统", "success", "应用启动完成 | 账号: %d", a.scheduler.AccountCount())
|
|
}
|
|
|
|
// shutdown Wails 应用关闭时调用。
|
|
func (a *App) shutdown(ctx context.Context) {
|
|
if a.closeTrace != nil {
|
|
a.closeTrace()
|
|
}
|
|
if a.closeLog != nil {
|
|
a.closeLog()
|
|
}
|
|
}
|
|
|
|
// ==================== 前端可调用方法 ====================
|
|
|
|
// LoadProducts 刷新商品列表。refresh=true 时从斗鱼实时拉取。
|
|
func (a *App) LoadProducts(refresh bool) types.ProductPayload {
|
|
if refresh {
|
|
products, err := a.client.Products()
|
|
if err != nil {
|
|
logger.LogEvent("商品", "error", "商品刷新失败: %v", err)
|
|
return types.ProductPayload{Success: false}
|
|
}
|
|
fetchedAt := time.Now()
|
|
a.setProducts(products, fetchedAt)
|
|
logger.LogEvent("商品", "success", "商品刷新完成: %d 个,刷新时间=%s", len(products), fetchedAt.In(constants.CST).Format("15:04:05.000"))
|
|
return a.productPayload("live")
|
|
}
|
|
return a.productPayload("cache")
|
|
}
|
|
|
|
// GetAccounts 返回账号列表。
|
|
func (a *App) GetAccounts() []types.AccountView {
|
|
accounts := a.scheduler.AccountSnapshot()
|
|
list := make([]types.AccountView, 0, len(accounts))
|
|
for i, account := range accounts {
|
|
list = append(list, types.AccountView{
|
|
Index: i,
|
|
Name: account.Name,
|
|
Cookie: maskCookie(account.Cookie),
|
|
})
|
|
}
|
|
return list
|
|
}
|
|
|
|
// ReloadAccounts 重新加载账号文件。
|
|
func (a *App) ReloadAccounts() map[string]any {
|
|
a.scheduler.LoadAccounts()
|
|
return map[string]any{"success": true, "count": a.scheduler.AccountCount()}
|
|
}
|
|
|
|
// StartSeckill 立即启动抢购。
|
|
func (a *App) StartSeckill(accountProducts map[string]string) map[string]any {
|
|
if len(accountProducts) == 0 {
|
|
return map[string]any{"success": false, "error": "请选择账号和商品"}
|
|
}
|
|
if !a.scheduler.TryStartRun() {
|
|
return map[string]any{"success": false, "error": "抢购任务正在运行,请勿重复提交"}
|
|
}
|
|
go a.scheduler.RunNow(cloneMap(accountProducts))
|
|
return map[string]any{"success": true, "msg": "抢购已启动"}
|
|
}
|
|
|
|
// SetSchedule 设置定时抢购。
|
|
func (a *App) SetSchedule(req types.SeckillRequest) (map[string]any, error) {
|
|
return a.scheduler.ScheduleRun(req)
|
|
}
|
|
|
|
// CancelSchedule 取消未触发的定时抢购。
|
|
func (a *App) CancelSchedule() map[string]any {
|
|
stopped := a.scheduler.CancelSchedule()
|
|
return map[string]any{"success": true, "stopped": stopped}
|
|
}
|
|
|
|
// GetResults 返回当前抢购结果。
|
|
func (a *App) GetResults() map[string]string {
|
|
return a.scheduler.Results()
|
|
}
|
|
|
|
// GetLogs 返回后端内存日志,支持增量获取。
|
|
func (a *App) GetLogs(since int64) map[string]any {
|
|
entries := a.logStore.Since(since)
|
|
nextSince := since
|
|
if len(entries) > 0 {
|
|
nextSince = entries[len(entries)-1].ID
|
|
}
|
|
return map[string]any{
|
|
"success": true,
|
|
"logs": entries,
|
|
"since": nextSince,
|
|
"file": a.logStore.FilePath(),
|
|
}
|
|
}
|
|
|
|
// GetTime 返回服务端时间和本地时间。
|
|
func (a *App) GetTime() map[string]any {
|
|
serverTime, err := a.client.ServerTime()
|
|
source := "currentTime"
|
|
if err != nil {
|
|
source = "localFallback"
|
|
logger.LogEvent("时间", "warn", "时间接口降级: %v", err)
|
|
}
|
|
return map[string]any{
|
|
"serverTime": serverTime.In(constants.CST).Format("2006-01-02 15:04:05.000"),
|
|
"localTime": time.Now().In(constants.CST).Format("2006-01-02 15:04:05.000"),
|
|
"source": source,
|
|
}
|
|
}
|
|
|
|
// ClearResults 清空抢购结果。
|
|
func (a *App) ClearResults() map[string]any {
|
|
a.scheduler.ClearResults()
|
|
return map[string]any{"success": true}
|
|
}
|
|
|
|
// QueryScore 查询单个账号的积分。
|
|
func (a *App) QueryScore(accountName string) map[string]any {
|
|
if accountName == "" {
|
|
return map[string]any{"score": "缺少账号名"}
|
|
}
|
|
cookie, ok := a.scheduler.AccountCookieByName(accountName)
|
|
if !ok || cookie == "" {
|
|
return map[string]any{"score": fmt.Sprintf("账号不存在或 Cookie 为空: %s", accountName)}
|
|
}
|
|
score, err := a.client.Score(cookie)
|
|
if err != nil {
|
|
return map[string]any{"score": "查询失败: " + err.Error()}
|
|
}
|
|
return map[string]any{"score": score}
|
|
}
|
|
|
|
// VerifyExchange 查询兑换记录,用于复验抢购结果。
|
|
func (a *App) VerifyExchange(accountName string) map[string]any {
|
|
if accountName == "" {
|
|
return map[string]any{"success": false, "error": "缺少账号名"}
|
|
}
|
|
cookie, ok := a.scheduler.AccountCookieByName(accountName)
|
|
if !ok || cookie == "" {
|
|
return map[string]any{"success": false, "error": fmt.Sprintf("账号不存在或 Cookie 为空: %s", accountName)}
|
|
}
|
|
|
|
records, err := a.client.ExchangeList(cookie)
|
|
if err != nil {
|
|
return map[string]any{"success": false, "error": "查询失败: " + err.Error()}
|
|
}
|
|
|
|
// 过滤当前活动的记录
|
|
var matched []client.ExchangeRecord
|
|
for _, r := range records {
|
|
if r.CommodityID != "" {
|
|
matched = append(matched, r)
|
|
}
|
|
}
|
|
|
|
return map[string]any{
|
|
"success": true,
|
|
"records": matched,
|
|
"count": len(matched),
|
|
}
|
|
}
|
|
|
|
// VerifyAllExchange 批量查询兑换记录,带间隔避免请求过快。
|
|
func (a *App) VerifyAllExchange() map[string]any {
|
|
accounts := a.scheduler.AccountSnapshot()
|
|
results := make(map[string]any)
|
|
|
|
for i, account := range accounts {
|
|
if i > 0 {
|
|
// 每个请求间隔 200ms,避免太快
|
|
time.Sleep(200 * time.Millisecond)
|
|
}
|
|
|
|
if account.Cookie == "" {
|
|
results[account.Name] = map[string]any{
|
|
"success": false,
|
|
"error": "Cookie 为空",
|
|
}
|
|
continue
|
|
}
|
|
|
|
records, err := a.client.ExchangeList(account.Cookie)
|
|
if err != nil {
|
|
results[account.Name] = map[string]any{
|
|
"success": false,
|
|
"error": "查询失败: " + err.Error(),
|
|
}
|
|
continue
|
|
}
|
|
|
|
// 过滤当前活动的记录
|
|
var matched []client.ExchangeRecord
|
|
for _, r := range records {
|
|
if r.CommodityID != "" {
|
|
matched = append(matched, r)
|
|
}
|
|
}
|
|
|
|
results[account.Name] = map[string]any{
|
|
"success": true,
|
|
"records": matched,
|
|
"count": len(matched),
|
|
}
|
|
}
|
|
|
|
return results
|
|
}
|
|
|
|
// VerifyAndExport 复验并导出结果。
|
|
func (a *App) VerifyAndExport(commodityID string) map[string]any {
|
|
accounts := a.scheduler.AccountSnapshot()
|
|
results := make(map[string]any)
|
|
hitAccounts := make([]string, 0)
|
|
missAccounts := make([]string, 0)
|
|
|
|
for i, account := range accounts {
|
|
if i > 0 {
|
|
time.Sleep(200 * time.Millisecond)
|
|
}
|
|
|
|
if account.Cookie == "" {
|
|
missAccounts = append(missAccounts, account.Name)
|
|
results[account.Name] = map[string]any{
|
|
"success": false,
|
|
"error": "Cookie 为空",
|
|
}
|
|
continue
|
|
}
|
|
|
|
records, err := a.client.ExchangeList(account.Cookie)
|
|
if err != nil {
|
|
missAccounts = append(missAccounts, account.Name)
|
|
results[account.Name] = map[string]any{
|
|
"success": false,
|
|
"error": "查询失败: " + err.Error(),
|
|
}
|
|
continue
|
|
}
|
|
|
|
// 检查是否抢到了指定商品
|
|
hit := false
|
|
for _, r := range records {
|
|
if r.CommodityID == commodityID {
|
|
hit = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if hit {
|
|
hitAccounts = append(hitAccounts, account.Name)
|
|
results[account.Name] = map[string]any{
|
|
"success": true,
|
|
"hit": true,
|
|
}
|
|
} else {
|
|
missAccounts = append(missAccounts, account.Name)
|
|
results[account.Name] = map[string]any{
|
|
"success": true,
|
|
"hit": false,
|
|
}
|
|
}
|
|
}
|
|
|
|
return map[string]any{
|
|
"success": true,
|
|
"hitCount": len(hitAccounts),
|
|
"missCount": len(missAccounts),
|
|
"hitAccounts": hitAccounts,
|
|
"missAccounts": missAccounts,
|
|
"details": results,
|
|
}
|
|
}
|
|
|
|
// ExportAccounts 导出指定账号到文件。
|
|
func (a *App) ExportAccounts(accountNames []string, filename string) map[string]any {
|
|
if len(accountNames) == 0 {
|
|
return map[string]any{"success": false, "error": "没有要导出的账号"}
|
|
}
|
|
|
|
// 读取原账号文件
|
|
accountFile := a.scheduler.AccountFilePath()
|
|
lines, err := os.ReadFile(accountFile)
|
|
if err != nil {
|
|
return map[string]any{"success": false, "error": "读取账号文件失败: " + err.Error()}
|
|
}
|
|
|
|
// 解析账号行
|
|
nameSet := make(map[string]bool)
|
|
for _, name := range accountNames {
|
|
nameSet[name] = true
|
|
}
|
|
|
|
var matchedLines []string
|
|
scanner := bufio.NewScanner(strings.NewReader(string(lines)))
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
trimmed := strings.TrimSpace(line)
|
|
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
|
|
continue
|
|
}
|
|
parts := strings.SplitN(trimmed, "----", 3)
|
|
if len(parts) >= 1 {
|
|
name := strings.TrimSpace(parts[0])
|
|
if nameSet[name] {
|
|
matchedLines = append(matchedLines, line)
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(matchedLines) == 0 {
|
|
return map[string]any{"success": false, "error": "没有找到匹配的账号行"}
|
|
}
|
|
|
|
// 生成导出文件路径
|
|
exportPath := filepath.Join(filepath.Dir(accountFile), filename)
|
|
content := strings.Join(matchedLines, "\n") + "\n"
|
|
|
|
if err := os.WriteFile(exportPath, []byte(content), 0644); err != nil {
|
|
return map[string]any{"success": false, "error": "写入文件失败: " + err.Error()}
|
|
}
|
|
|
|
logger.LogEvent("导出", "success", "已导出 %d 个账号到: %s", len(matchedLines), exportPath)
|
|
|
|
return map[string]any{
|
|
"success": true,
|
|
"count": len(matchedLines),
|
|
"filePath": exportPath,
|
|
}
|
|
}
|
|
|
|
// ExportAccountsWithProduct 导出指定账号到文件,自动使用中文商品名和时间戳命名。
|
|
func (a *App) ExportAccountsWithProduct(accountNames []string, commodityID, commodityName string) map[string]any {
|
|
// 生成文件名: user_已抢到_商品中文名_20260626_150405.txt
|
|
now := time.Now()
|
|
timestamp := now.Format("20060102_150405")
|
|
// 清理商品名中的特殊字符
|
|
safeName := strings.ReplaceAll(commodityName, "/", "_")
|
|
safeName = strings.ReplaceAll(safeName, "\\", "_")
|
|
safeName = strings.ReplaceAll(safeName, ":", "_")
|
|
safeName = strings.ReplaceAll(safeName, "*", "_")
|
|
safeName = strings.ReplaceAll(safeName, "?", "_")
|
|
safeName = strings.ReplaceAll(safeName, "\"", "_")
|
|
safeName = strings.ReplaceAll(safeName, "<", "_")
|
|
safeName = strings.ReplaceAll(safeName, ">", "_")
|
|
safeName = strings.ReplaceAll(safeName, "|", "_")
|
|
filename := fmt.Sprintf("user_已抢到_%s_%s.txt", safeName, timestamp)
|
|
|
|
return a.ExportAccounts(accountNames, filename)
|
|
}
|
|
|
|
// OrganizeAccounts 整理账号,把已抢到指定商品的账号分离出来。
|
|
func (a *App) OrganizeAccounts(commodityID, commodityName string, startTime, endTime int64) map[string]any {
|
|
accounts := a.scheduler.AccountSnapshot()
|
|
hitAccounts := make([]string, 0)
|
|
missAccounts := make([]string, 0)
|
|
|
|
for i, account := range accounts {
|
|
if i > 0 {
|
|
time.Sleep(200 * time.Millisecond)
|
|
}
|
|
|
|
if account.Cookie == "" {
|
|
missAccounts = append(missAccounts, account.Name)
|
|
continue
|
|
}
|
|
|
|
records, err := a.client.ExchangeList(account.Cookie)
|
|
if err != nil {
|
|
missAccounts = append(missAccounts, account.Name)
|
|
continue
|
|
}
|
|
|
|
// 检查是否在指定时间段内抢到了指定商品
|
|
hit := false
|
|
for _, r := range records {
|
|
if r.CommodityID == commodityID {
|
|
// 检查时间范围
|
|
recordTime, _ := strconv.ParseInt(r.Time, 10, 64)
|
|
if startTime == 0 || (recordTime >= startTime && recordTime <= endTime) {
|
|
hit = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if hit {
|
|
hitAccounts = append(hitAccounts, account.Name)
|
|
} else {
|
|
missAccounts = append(missAccounts, account.Name)
|
|
}
|
|
}
|
|
|
|
// 导出已抢到的账号
|
|
var exportResult map[string]any
|
|
if len(hitAccounts) > 0 {
|
|
exportResult = a.ExportAccountsWithProduct(hitAccounts, commodityID, commodityName)
|
|
}
|
|
|
|
// 更新原文件,只保留未抢到的
|
|
accountFile := a.scheduler.AccountFilePath()
|
|
lines, err := os.ReadFile(accountFile)
|
|
if err != nil {
|
|
return map[string]any{"success": false, "error": "读取账号文件失败: " + err.Error()}
|
|
}
|
|
|
|
missSet := make(map[string]bool)
|
|
for _, name := range missAccounts {
|
|
missSet[name] = true
|
|
}
|
|
|
|
var keepLines []string
|
|
scanner := bufio.NewScanner(strings.NewReader(string(lines)))
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
trimmed := strings.TrimSpace(line)
|
|
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
|
|
keepLines = append(keepLines, line)
|
|
continue
|
|
}
|
|
parts := strings.SplitN(trimmed, "----", 3)
|
|
if len(parts) >= 1 {
|
|
name := strings.TrimSpace(parts[0])
|
|
if missSet[name] {
|
|
keepLines = append(keepLines, line)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 写回原文件
|
|
newContent := strings.Join(keepLines, "\n") + "\n"
|
|
if err := os.WriteFile(accountFile, []byte(newContent), 0644); err != nil {
|
|
return map[string]any{"success": false, "error": "更新账号文件失败: " + err.Error()}
|
|
}
|
|
|
|
// 重新加载账号
|
|
a.scheduler.LoadAccounts()
|
|
|
|
logger.LogEvent("整理", "success", "整理完成: 已抢到=%d, 未抢到=%d", len(hitAccounts), len(missAccounts))
|
|
|
|
return map[string]any{
|
|
"success": true,
|
|
"hitCount": len(hitAccounts),
|
|
"missCount": len(missAccounts),
|
|
"hitAccounts": hitAccounts,
|
|
"missAccounts": missAccounts,
|
|
"exportResult": exportResult,
|
|
}
|
|
}
|
|
|
|
// GetVersion 返回应用版本号。
|
|
func (a *App) GetVersion() string {
|
|
return constants.Version
|
|
}
|
|
|
|
// GetSettings 返回当前配置。
|
|
func (a *App) GetSettings() types.Config {
|
|
return a.cfg
|
|
}
|
|
|
|
// SaveSettings 保存配置并运行时生效。
|
|
func (a *App) SaveSettings(cfg types.Config) map[string]any {
|
|
// 校验
|
|
if cfg.Concurrency <= 0 {
|
|
cfg.Concurrency = constants.DefaultConcurrency
|
|
}
|
|
if cfg.Attempts <= 0 {
|
|
cfg.Attempts = constants.DefaultAttempts
|
|
}
|
|
if cfg.LogLevel == "" {
|
|
cfg.LogLevel = "info"
|
|
}
|
|
|
|
// 写入文件
|
|
if err := config.SaveConfig(a.paths.ConfigFile, cfg); err != nil {
|
|
logger.LogEvent("配置", "error", "配置保存失败: %v", err)
|
|
return map[string]any{"success": false, "error": err.Error()}
|
|
}
|
|
|
|
// 运行时生效
|
|
a.cfg = cfg
|
|
logger.SetMinLevel(cfg.LogLevel)
|
|
a.scheduler.UpdateSettings(cfg.Concurrency, cfg.Attempts)
|
|
|
|
logger.LogEvent("配置", "success", "配置已保存并生效: 日志等级=%s 并发=%d 重试=%d trace=%v",
|
|
cfg.LogLevel, cfg.Concurrency, cfg.Attempts, cfg.TraceEnabled)
|
|
return map[string]any{"success": true}
|
|
}
|
|
|
|
// ==================== 内部辅助方法 ====================
|
|
|
|
func (a *App) setProducts(products []types.Product, fetchedAt time.Time) {
|
|
a.productsMu.Lock()
|
|
defer a.productsMu.Unlock()
|
|
a.products = products
|
|
a.productsAt = fetchedAt
|
|
}
|
|
|
|
func (a *App) productPayload(source string) types.ProductPayload {
|
|
a.productsMu.RLock()
|
|
defer a.productsMu.RUnlock()
|
|
|
|
payload := types.ProductPayload{
|
|
Success: true,
|
|
Source: source,
|
|
Products: a.products,
|
|
Count: len(a.products),
|
|
}
|
|
if !a.productsAt.IsZero() {
|
|
age := time.Since(a.productsAt)
|
|
if age < 0 {
|
|
age = 0
|
|
}
|
|
payload.FetchedAt = a.productsAt.In(constants.CST).Format("2006-01-02 15:04:05.000")
|
|
payload.AgeSecond = int(age.Seconds())
|
|
payload.Stale = age > constants.ProductCacheTTL
|
|
} else {
|
|
payload.Stale = true
|
|
}
|
|
return payload
|
|
}
|
|
|
|
func cloneMap(in map[string]string) map[string]string {
|
|
out := make(map[string]string, len(in))
|
|
for k, v := range in {
|
|
out[k] = v
|
|
}
|
|
return out
|
|
}
|
|
|
|
func maskCookie(cookie string) string {
|
|
pairs := splitCookie(cookie)
|
|
masked := make([]string, 0, 3)
|
|
total := 0
|
|
for _, pair := range pairs {
|
|
key, value := splitCookiePair(pair)
|
|
if key == "" {
|
|
continue
|
|
}
|
|
total++
|
|
if len(masked) >= 3 {
|
|
continue
|
|
}
|
|
masked = append(masked, key+"="+maskValue(value))
|
|
}
|
|
if total == 0 {
|
|
return ""
|
|
}
|
|
if total > len(masked) {
|
|
masked = append(masked, fmt.Sprintf("...共%d项", total))
|
|
}
|
|
return joinStrings(masked, "; ")
|
|
}
|
|
|
|
func splitCookie(cookie string) []string {
|
|
var result []string
|
|
for _, part := range splitString(cookie, ";") {
|
|
trimmed := trimSpace(part)
|
|
if trimmed != "" {
|
|
result = append(result, trimmed)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func splitCookiePair(pair string) (string, string) {
|
|
idx := indexOf(pair, "=")
|
|
if idx < 0 {
|
|
return trimSpace(pair), ""
|
|
}
|
|
return trimSpace(pair[:idx]), pair[idx+1:]
|
|
}
|
|
|
|
func maskValue(value string) string {
|
|
value = trimSpace(value)
|
|
if value == "" {
|
|
return "****"
|
|
}
|
|
runes := []rune(value)
|
|
if len(runes) <= 8 {
|
|
return "****"
|
|
}
|
|
return string(runes[:4]) + "****" + string(runes[len(runes)-4:])
|
|
}
|
|
|
|
// 简单的字符串工具函数,避免引入额外包
|
|
func splitString(s, sep string) []string {
|
|
var result []string
|
|
start := 0
|
|
for i := 0; i+len(sep) <= len(s); {
|
|
if s[i:i+len(sep)] == sep {
|
|
result = append(result, s[start:i])
|
|
start = i + len(sep)
|
|
i = start
|
|
} else {
|
|
i++
|
|
}
|
|
}
|
|
result = append(result, s[start:])
|
|
return result
|
|
}
|
|
|
|
func trimSpace(s string) string {
|
|
for len(s) > 0 && (s[0] == ' ' || s[0] == '\t' || s[0] == '\n' || s[0] == '\r') {
|
|
s = s[1:]
|
|
}
|
|
for len(s) > 0 && (s[len(s)-1] == ' ' || s[len(s)-1] == '\t' || s[len(s)-1] == '\n' || s[len(s)-1] == '\r') {
|
|
s = s[:len(s)-1]
|
|
}
|
|
return s
|
|
}
|
|
|
|
func indexOf(s, sub string) int {
|
|
for i := 0; i+len(sub) <= len(s); i++ {
|
|
if s[i:i+len(sub)] == sub {
|
|
return i
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
func joinStrings(parts []string, sep string) string {
|
|
if len(parts) == 0 {
|
|
return ""
|
|
}
|
|
result := parts[0]
|
|
for _, p := range parts[1:] {
|
|
result += sep + p
|
|
}
|
|
return result
|
|
}
|