BUFF系统技术文档.md 52 KB

BUFF选品扫货系统 - 技术文档

1. 系统概述

1.1 项目简介

BUFF选品扫货系统是一个用于网易BUFF平台的CSGO饰品自动化交易工具,主要功能包括:

  • 选品功能: 根据设定的筛选条件,自动从BUFF平台筛选符合价格策略的商品
  • 扫货功能: 多线程自动购买选中的商品
  • 配置管理: 灵活的筛选条件和价格策略配置
  • 日志管理: 完整的操作日志记录和查询

1.2 技术栈

  • UI框架: PyQt5 (桌面应用程序界面)
  • 数据库: SQLite (本地数据存储)
  • HTTP请求: requests库
  • 定时任务: APScheduler (后台定时调度)
  • 日志: logging + TimedRotatingFileHandler (按天轮转)
  • 多线程: threading模块

1.3 项目结构

BUFF/
├── BuffMain.py              # 主程序入口和核心逻辑
├── BUFFAPI.py              # BUFF平台API封装
├── BUFFXP.py               # 选品参数检查和构建
├── InitDB.py               # 数据库初始化
├── CategoryDialog.py       # 品类管理对话框
├── QualityDialog.py        # 品质管理对话框
├── TypeDialog.py           # 类别管理对话框
├── AppearanceDialog.py     # 外观管理对话框
├── CollectibleDialog.py    # 收藏品管理对话框
├── SelectLogDialog.py      # 选品日志管理
├── EditSelectLogDialog.py  # 编辑选品日志对话框
├── tail_xprz.py           # 选品日志监控脚本
├── tail_shrz.py           # 扫货日志监控脚本
├── tail_gmrz.py           # 购买日志监控脚本
├── buff.db                # SQLite数据库
├── xppcids.txt            # 需剔除的商品ID列表
├── 选品日志log/           # 选品日志目录
├── 扫货日志log/           # 扫货日志目录
├── 购买日志log/           # 购买日志目录
├── 详细日志log/           # 详细日志目录
└── 选品日志文件/          # 选品结果导出目录

2. 数据库设计

2.1 数据库表结构

2.1.1 accounts (选品账号信息表)

CREATE TABLE accounts (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    phone_number TEXT NOT NULL,      -- 手机号码
    email TEXT NOT NULL,              -- 邮箱
    email_alias TEXT NOT NULL,        -- 邮箱别名/特殊代码
    cookie TEXT NOT NULL              -- 登录Cookie (支持多个,用换行分隔)
)

2.1.2 sh_account_info (扫货账号信息表)

CREATE TABLE sh_account_info (
    phone TEXT,           -- 手机号码
    email TEXT,           -- 邮箱
    code TEXT,            -- 特殊代码
    cookie TEXT,          -- 登录Cookie
    xcsrftoken TEXT       -- CSRF Token
)

2.1.3 categories (品类表)

CREATE TABLE categories (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,               -- 品类名称
    code TEXT NOT NULL,               -- 品类编码 (BUFF平台定义)
    selected BOOLEAN NOT NULL DEFAULT 0  -- 是否选中
)

示例数据: 手枪(weapon_pistol)、步枪(weapon_rifle)、刀具(weapon_knife)等

2.1.4 qualities (品质表)

CREATE TABLE qualities (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    code TEXT NOT NULL,               -- 品质编码
    name TEXT NOT NULL,               -- 品质名称
    selected BOOLEAN NOT NULL DEFAULT 0
)

示例数据: 隐秘(unusual)、保密(classified)、受限(restricted)等

2.1.5 types (类别表)

CREATE TABLE types (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    code TEXT NOT NULL,               -- 类别编码
    name TEXT NOT NULL,               -- 类别名称
    selected BOOLEAN NOT NULL DEFAULT 0
)

示例数据: 纪念品(tournament)、StatTrak™(strange)等

2.1.6 appearances (外观表)

CREATE TABLE appearances (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    code TEXT NOT NULL,               -- 外观编码
    name TEXT NOT NULL,               -- 外观名称
    selected BOOLEAN NOT NULL DEFAULT 0
)

示例数据: 崭新出厂(factory_new)、略有磨损(minimal_wear)、久经沙场(field_tested)等

2.1.7 collectibles (收藏品表)

CREATE TABLE collectibles (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    code TEXT NOT NULL,               -- 收藏品编码
    name TEXT NOT NULL,               -- 收藏品名称
    selected BOOLEAN NOT NULL DEFAULT 0
)

2.1.8 config (选品配置表)

CREATE TABLE config (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    min_price REAL,                   -- 最低价格
    max_price REAL,                   -- 最高价格
    sort_mode TEXT,                   -- 排序模式: 默认|价格升序|价格降序|在售数量升序
    stop_condition TEXT,              -- 停止条件: 按照成功数量|按照扫描数量
    success_count INTEGER,            -- 成功数量配置
    scan_count INTEGER,               -- 扫描商品数量
    page_size INTEGER,                -- 每页请求数量
    pause_time INTEGER,               -- 接口暂停时间(秒)
    max_pages INTEGER,                -- 最大请求页数
    ref_price_adjust INTEGER,         -- 参考价调整系数(百分比)
    sweep_price_adjust INTEGER,       -- 扫货价调整系数(百分比)
    needed_msg_check_pause_time INTEGER,  -- 需短信验证时暂停时长(秒)
    needed_msg_check_try_times INTEGER,   -- 需短信验证时重试次数
    delete_ids TEXT,                  -- 需剔除的商品ID列表(#号分割)
    cron_time TEXT,                   -- 定时执行时间配置(如: 9:00,15:30)
    sell_num INTEGER                  -- 最低销售数量筛选
)

2.1.9 select_logs (选品日志表)

CREATE TABLE select_logs (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    item_id TEXT NOT NULL,            -- 商品ID
    item_name TEXT NOT NULL,          -- 商品名称
    min_wear TEXT NOT NULL,           -- 最低磨损值
    max_wear TEXT NOT NULL,           -- 最高磨损值
    min_full_wear_price REAL NOT NULL,  -- 全磨损区间最低价
    sweep_price REAL NOT NULL,        -- 扫货价(购买底价)
    judge_price REAL NOT NULL,        -- 判断价(参考价)
    timestamp TEXT NOT NULL           -- 时间戳
)

2.1.10 shpz_settings (扫货配置表)

CREATE TABLE shpz_settings (
    pause_time REAL,                  -- API请求暂停时间(秒)
    thread_count INTEGER,             -- 扫货线程数
    needed_msg_check_pause_time INTEGER,  -- 需短信验证时暂停时长
    needed_msg_check_try_times INTEGER    -- 需短信验证时重试次数
)

3. BUFF平台API接口

3.1 接口基础信息

3.1.1 请求头配置

headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.131 Safari/537.36",
    "Host": "buff.163.com",
    "Referer": "https://buff.163.com/market/?game=csgo",
    "X-Requested-With": "XMLHttpRequest",
    "Cookie": "<登录Cookie>"
}

3.1.2 响应状态码

  • OK: 请求成功
  • Captcha Validate Required: 需要短信验证
  • Login Required: 需要登录
  • Action Forbidden: 操作被禁止
  • System Error: 系统错误(通常是访问频繁)

3.2 核心API接口

3.2.1 获取商品列表

接口: GET https://buff.163.com/api/market/goods

请求参数:

{
    "game": "csgo",
    "category": "weapon_pistol,weapon_rifle",  // 品类(多个用逗号分隔)
    "category_group": "weapon_pistol,weapon_rifle",  // 品类组
    "rarity": "unusual,classified",  // 品质
    "quality": "tournament,strange",  // 类别
    "exterior": "factory_new,minimal_wear",  // 外观
    "itemset": "",  // 收藏品
    "min_price": "10",  // 最低价
    "max_price": "500",  // 最高价
    "sort_by": "price.asc",  // 排序: price.asc|price.desc|sell_num.desc
    "page_size": "80",  // 每页数量(默认80,最大可能是100)
    "page_num": "1"  // 页码
}

响应示例:

{
    "code": "OK",
    "data": {
        "items": [
            {
                "id": "968993",  // 商品ID
                "name": "AK-47 | 红线 (略有磨损)",
                "price": "168.50",
                "sell_num": 150,  // 在售数量
                // ... 其他字段
            }
        ],
        "total_page": 50,  // 总页数
        "total_count": 1000  // 总数量
    }
}

3.2.2 获取商品磨损信息

接口: GET https://buff.163.com/api/market/goods/info

请求参数:

?goods_id=968993

响应示例:

{
    "code": "OK",
    "data": {
        "paintwear_choices": [
            ["0.07", "0.15"],  // 崭新出厂
            ["0.15", "0.38"],  // 略有磨损
            ["0.38", "0.45"],  // 久经沙场
            // ... 其他磨损区间
        ]
    }
}

3.2.3 获取商品在售订单(价格查询)

接口: GET https://buff.163.com/api/market/goods/sell_order

请求参数:

?game=csgo
&page_num=1
&sort_by=price.asc
&goods_id=968993
&min_paintwear=0.07  // 可选: 最低磨损
&max_paintwear=0.15  // 可选: 最高磨损

响应示例:

{
    "code": "OK",
    "data": {
        "items": [
            {
                "id": "sell_order_id_123456",  // 订单ID
                "goods_id": "968993",
                "price": "170.50",  // 价格
                "paintwear": "0.0823",  // 磨损值
                // ... 其他字段
            }
        ]
    }
}

扫货专用请求参数:

?game=csgo
&allow_epay=1           // 允许电子支付
&allow_tradable_cooldown=1  // 允许交易冷却期
&page_num=1
&page_size=95           // 扫货时固定为95(硬编码)
&sort_by=price.asc      // 价格升序
&goods_id=968993
&min_price=0
&max_price=173.40       // 扫货价上限
&min_paintwear=0.07     // 磨损下限
&max_paintwear=0.15     // 磨损上限

注意:

  • 扫货URL在代码中是硬编码的,包含了page_size=95
  • 基础URL: https://buff.163.com/api/market/goods/sell_order?game=csgo&allow_epay=1&allow_tradable_cooldown=1&page_num=1&page_size=95&sort_by=price.asc&goods_id=
  • 然后动态拼接价格和磨损参数

3.2.4 购买商品

接口: POST https://buff.163.com/api/market/goods/buy

请求头额外字段:

headers = {
    # ... 其他字段
    "Referer": f"https://buff.163.com/goods/{goods_id}?from=market",
    "Cookie": "<登录Cookie>",
    "x-csrftoken": "<从响应头set-cookie中获取的csrf_token>"
}

请求体:

{
    "game": "csgo",
    "goods_id": "968993",
    "sell_order_id": "sell_order_id_123456",
    "price": "170.50",
    "pay_method": 1,
    "allow_tradable_cooldown": 0,
    "token": "",
    "cdkey_id": "",
    "hide_non_epay": false
}

响应示例:

{
    "code": "OK",
    "data": {
        "id": "bill_order_id_789",  // 订单ID
        // ... 其他字段
    }
}

4. 核心业务逻辑

4.1 选品流程

4.1.1 选品主流程

1. 参数校验
   ├─ 检查Cookie是否为空
   ├─ 获取筛选条件(品类/品质/类别/外观/收藏品)
   ├─ 获取价格范围、排序方式、停止条件等配置
   └─ 读取剔除ID列表(xppcids.txt)

2. 暂停扫货进程
   └─ 设置扫货停止标志,等待扫货线程退出

3. 保存并清空上次选品结果
   ├─ 导出选品日志到文件(选品日志文件/YYYYMMDDHHMMSS_选品日志文件.json)
   ├─ 清空数据库select_logs表
   └─ 清空界面选品日志列表

4. 初始化选品状态
   ├─ 重置已选品数量(searchedGoodsNum = 0)
   ├─ 重置已选品成功数量(search_success_count = 0)
   ├─ 初始化请求时间戳(last_xp_request_time = 0)
   └─ 构建商品查询参数

5. 主循环(循环2次,xiesi_tag < 2)
   每次循环执行以下步骤:
   
   5.1 初始化本轮数据
       ├─ 清空待检查商品列表(need_check_goods_list)
       ├─ 初始化Cookie状态池(cookie_status = {})
       └─ 分割Cookie文本为列表(用换行分隔)
   
   5.2 检查停止条件
       ├─ 检查停止选品标志
       ├─ 检查成功数量是否达标
       └─ 检查扫描数量是否达标

   5.3 分页查询商品列表
       ├─ 循环请求最多max_pages页
       ├─ Cookie轮询机制(支持多个Cookie轮流使用)
       ├─ 异常重试机制
       │  ├─ System Error: 重试,超过次数跳过
       │  └─ Captcha Validate Required: 切换Cookie,超过次数标记Cookie失效
       └─ 将商品添加到待检查列表

   5.4 商品去重和筛选
       ├─ 根据商品ID去重(使用字典去重)
       ├─ 剔除黑名单商品(xppcids.txt,用#号分割)
       └─ 根据最低销售数量筛选(sell_num >= check_sell_num)

   5.5 遍历商品进行价格分析
       对每个商品执行以下步骤:
       
       5.5.1 获取商品磨损区间信息
           ├─ 调用getPaintwearChoices接口
           ├─ Cookie轮询和异常处理
           └─ 获取paintwear_choices数组
       
       5.5.2 获取全磨损区间最低价(min_price)
           ├─ 调用sell_order接口(不带磨损参数)
           ├─ Cookie轮询和异常处理
           └─ 提取最低价格
       
       5.5.3 获取各磨损区间最低价
           ├─ 遍历磨损区间,调用sell_order接口
           ├─ 每个区间都进行Cookie轮询和异常处理
           └─ 构建min_price_list数组 [{min_wear, max_wear, min_price}]
       
       5.5.4 价格计算与筛选
           ├─ 计算判断价: pd_price = round(round(min_price * (100 + ref_price_adjust) / 100, 2) + 0.01, 2)
           ├─ 筛选磨损区间: 只保留 区间最低价 >= pd_price 的区间
           ├─ 阶梯价格筛选: 保留递减的价格阶梯(价格必须递减,否则剔除后续区间)
           └─ 计算扫货价: sweep_price = round(round(min_price * (100 + sweep_price_adjust) / 100, 2) + 0.01, 2)
       
       5.5.5 保存选品结果
           ├─ 插入数据库select_logs表
           ├─ 通过信号槽添加到界面选品日志列表
           ├─ 记录选品日志(xp_logger)
           └─ 选品成功数量+1

   5.6 更新选品数量
       └─ 每处理一个商品,searchedGoodsNum += 1

6. 恢复扫货进程
   └─ 选品完成后自动调用beginSH()启动扫货

重要说明:

  • 双重循环: 外层循环2次(xiesi_tag)是为了增加选品机会,确保获取足够的商品
  • Cookie轮询: 每个API请求都使用(page/goods_index + retry_count) % len(cookie_list)的方式轮询Cookie
  • 异常累计: Cookie异常次数累计在cookie_status字典中,达到阈值后从可用列表移除

4.1.2 价格计算逻辑详解

示例: 假设某商品全磨损最低价为170元

参数配置:

  • ref_price_adjust = 10 (参考价调整系数10%)
  • sweep_price_adjust = 2 (扫货价调整系数2%)

计算过程:

1. 全磨损最低价: min_price = 170.00

2. 判断价(pd_price) - 使用双重round保证精度:
   第一步: 170.00 * (100 + 10) / 100 = 187.00
   第二步: round(187.00, 2) = 187.00
   第三步: 187.00 + 0.01 = 187.01
   第四步: round(187.01, 2) = 187.01
   
   代码: pd_price = round(round(min_price * (100 + ref_price_adjust) / 100, 2) + 0.01, 2)
   结果: 187.01元

3. 扫货价(sweep_price) - 使用双重round保证精度:
   第一步: 170.00 * (100 + 2) / 100 = 173.40
   第二步: round(173.40, 2) = 173.40
   第三步: 173.40 + 0.01 = 173.41
   第四步: round(173.41, 2) = 173.41
   
   代码: sweep_price = round(round(min_price * (100 + sweep_price_adjust) / 100, 2) + 0.01, 2)
   结果: 173.41元

注意事项:
- 使用双重round是为了避免浮点数精度问题
- 第一次round保证百分比计算后的精度
- 加0.01是为了确保价格略高于临界值
- 第二次round保证最终结果为两位小数

4. 各磨损区间最低价:
   假设API返回:
   - 区间1 [0.00-0.07]: 270元
   - 区间2 [0.07-0.15]: 260元
   - 区间3 [0.15-0.38]: 230元
   - 区间4 [0.38-0.45]: 240元
   - 区间5 [0.45-1.00]: 170元

5. 筛选逻辑:
   步骤1: 过滤低于判断价(187.01)的区间
   - 区间1: 270 >= 187.01 ✓
   - 区间2: 260 >= 187.01 ✓
   - 区间3: 230 >= 187.01 ✓
   - 区间4: 240 >= 187.01 ✓ (但后续会被阶梯筛选剔除)
   - 区间5: 170 < 187.01 ✗ (剔除)

   步骤2: 阶梯价格筛选(保持递减)
   - 区间1: 270
   - 区间2: 260 < 270 ✓ (递减)
   - 区间3: 230 < 260 ✓ (递减)
   - 区间4: 240 > 230 ✗ (不递减,剔除区间4及之后)

   最终保留区间: [0.00-0.38]
   即: 区间1-3

6. 选品结果:
   - 商品ID: 968993
   - 磨损范围: 0.00 - 0.38
   - 全磨损最低价: 170.00
   - 扫货价: 173.41 (扫货时,低于此价格才购买)
   - 判断价: 187.01

业务逻辑解释:

  • 判断价: 用于筛选磨损区间,只保留价格高于判断价的区间
  • 扫货价: 实际购买时的价格上限,低于此价才购买
  • 阶梯筛选: 确保磨损越好价格越高,符合市场规律

4.1.3 Cookie管理机制

多Cookie轮询:

# Cookie池
cookie_text_list = [
    "cookie1",
    "cookie2",
    "cookie3"
]

# Cookie状态记录(记录异常次数)
cookie_status = {
    "cookie1": 0,  # 正常
    "cookie2": 1,  # 异常1次
    "cookie3": 3   # 异常3次,即将被移除
}

# 轮询逻辑
current_cookie = cookie_text_list[(page + temp_cookie_i) % len(cookie_text_list)]

# Cookie失效处理
if cookie_status[cookie] >= needed_msg_check_try_times:
    cookie_text_list.remove(cookie)  # 从池中移除

# 全部失效时退出
if len(cookie_text_list) == 0:
    # 退出选品

异常处理策略:

  • System Error: Cookie正常,但访问频繁,暂停后重试
  • Captcha Validate Required: Cookie需验证,切换下一个Cookie
  • 单个Cookie连续异常达到needed_msg_check_try_times次时,标记为失效
  • 所有Cookie都失效时,退出选品

4.2 扫货流程

4.2.1 扫货主流程

1. 参数校验
   ├─ 检查Cookie是否为空
   └─ 获取扫货配置(暂停时间、线程数等)

2. 暂停选品进程
   └─ 设置选品停止标志,等待选品线程退出

3. 启动多线程扫货
   ├─ 根据配置启动N个扫货线程
   └─ 每个线程独立执行扫货逻辑

4. 线程扫货逻辑(每个线程循环执行)
   ├─ 遍历选品日志列表
   ├─ 线程间任务分配(线程1处理1,4,7... 线程2处理2,5,8...)
   ├─ 对每个任务:
   │  ├─ 调用getGoodsInfo获取可购买订单列表
   │  ├─ 筛选符合价格和磨损条件的订单
   │  ├─ 遍历订单,调用购买接口
   │  └─ 记录购买日志
   └─ 异常处理(重试/切换Cookie等)

5. 停止扫货
   └─ 设置停止标志,各线程检测后退出

4.2.2 扫货线程任务分配

# 假设有10条选品记录,3个扫货线程
select_logs = [record0, record1, ..., record9]
thread_count = 3

# 线程1 (thread_num=1): 处理索引 0, 3, 6, 9
# 线程2 (thread_num=2): 处理索引 1, 4, 7
# 线程3 (thread_num=3): 处理索引 2, 5, 8

for i in range(len(select_logs)):
    if i % thread_count == thread_num - 1:
        # 处理这条记录

4.2.3 获取可购买商品

1. 构建查询URL
   base_url + goods_id + min_price=0 + max_price=sweep_price
   + min_paintwear + max_paintwear

2. 调用sell_order接口
   └─ page_size=95 (扫货时使用较大值)

3. 异常处理
   ├─ Captcha Validate Required: 重试,超过次数退出线程
   ├─ Login Required: 重试,超过次数退出线程
   └─ System Error: 跳过,继续下一个

4. 解析响应
   ├─ 提取csrf_token (从set-cookie响应头)
   └─ 返回items列表(符合条件的订单)

4.2.4 购买商品

1. 构建购买请求
   ├─ 请求体: goods_id, sell_order_id, price等
   └─ 请求头: Cookie, x-csrftoken

2. 调用购买接口
   └─ POST /api/market/goods/buy

3. 异常处理
   ├─ Captcha Validate Required: 重试,超过次数退出
   └─ 其他错误: 记录日志,继续下一个

4. 成功处理
   ├─ 记录购买日志(gm_logger)
   └─ 暂停配置的时间后继续

5. 日志系统

5.1 日志分类

5.1.1 控制台日志 (logger_cmd)

  • 用途: 在控制台实时显示关键操作信息
  • 级别: DEBUG
  • 格式: [时间戳] 消息内容
  • 内容: 选品/扫货进度、Cookie使用情况、异常提示等

5.1.2 详细日志 (detail_logger)

  • 路径: 详细日志log/详细日志.txt
  • 轮转: 每天午夜轮转,保留30天
  • 格式: 时间戳 - 消息内容
  • 内容:
    • 所有API请求和响应摘要
    • Cookie状态变更
    • 价格计算详细过程
    • 异常堆栈信息

5.1.3 选品日志 (xp_logger)

  • 路径: 选品日志log/选品日志.txt
  • 轮转: 每天午夜轮转,保留30天
  • 格式: 时间戳 - 消息内容
  • 典型记录:

    [选品] 正在使用:cookie1  名称:AK-47 | 红线,ID:968993
    [已选] 正在使用:cookie1. 名称:AK-47 | 红线,ID:968993,购买底价:173.41,区间:0.07-0.38
    

5.1.4 扫货日志 (sh_logger)

  • 路径: 扫货日志log/扫货日志.txt
  • 轮转: 每天午夜轮转,保留30天
  • 典型记录:

    扫货线程-1  名称:AK-47 | 红线,ID:968993,购买底价:173.41,区间:0.07-0.38
    扫货线程-1  共获取到5个商品可购买
    

5.1.5 购买日志 (gm_logger)

  • 路径: 购买日志log/购买日志.txt
  • 轮转: 每天午夜轮转,保留30天
  • 典型记录:

    名称:AK-47 | 红线,ID:968993,购买底价:173.41,区间:0.07-0.38,购买价格:170.50
    

5.2 日志文件命名规则

详细日志.txt           # 当天日志
详细日志.2025-10-08.txt  # 历史日志(按天归档)

5.3 日志监控脚本

  • tail_xprz.py: 实时监控选品日志
  • tail_shrz.py: 实时监控扫货日志
  • tail_gmrz.py: 实时监控购买日志

6. 界面功能

6.1 Tab页结构

6.1.1 选品账号 (Tab 1)

  • Cookie输入框: 支持多行,多个Cookie用换行分隔
  • 保存按钮: 保存到数据库accounts表

6.1.2 品类 (Tab 2)

  • TreeWidget: 显示品类列表(复选框+名称+编码)
  • 操作按钮: 添加、删除、编辑、保存
  • 数据表: categories

6.1.3 品质 (Tab 3)

  • TreeWidget: 显示品质列表
  • 数据表: qualities

6.1.4 TYPE (Tab 4)

  • TreeWidget: 显示类别列表
  • 数据表: types

6.1.5 外观 (Tab 5)

  • TreeWidget: 显示外观列表
  • 数据表: appearances

6.1.6 收藏品 (Tab 6)

  • TreeWidget: 显示收藏品列表
  • 数据表: collectibles

6.1.7 选品配置 (Tab 7)

配置项:

  • 最低价、最高价
  • 排序模式: 默认、价格升序、价格降序、在售数量升序
  • 停止条件: 按照成功数量、按照扫描数量
  • 成功数量配置、扫描商品数量
  • 接口每页请求数量(建议80-100)
  • 接口暂停时间(秒,建议5-10)
  • 接口最大请求多少页
  • 商品参考价调整系数% (计算判断价)
  • 商品扫货价调整系数% (计算扫货价)
  • API接口需短信验证时暂停时长(秒)
  • API接口需短信验证时重试次数
  • 需剔除的id列表(从xppcids.txt读取)
  • 每天定时执行时间(如: 9:00,15:30)
  • 销售数量配置(最低销售数量筛选)

6.1.8 选品日志 (Tab 8)

TreeWidget列:

  • 序号、商品ID、商品名称
  • 磨损最低值、磨损最高值
  • 全磨损区间最低价、扫货价、判断价
  • 时间

操作按钮:

  • 添加、删除、编辑
  • 导出(选中项导出为JSON)
  • 导入(从JSON文件导入)
  • 保存(保存到数据库)

导出格式:

{
    "968993": {
        "price": "173.41",
        "min_paintwear": "0.07",
        "max_paintwear": "0.38",
        "name": "AK-47 | 红线 (略有磨损)",
        "商品ID": "968993",
        "商品名称": "AK-47 | 红线 (略有磨损)",
        "磨损最低值": "0.07",
        "磨损最高值": "0.38",
        "全磨损区间最低价": "170.00",
        "扫货价": "173.41",
        "判断价": "187.01",
        "时间": "2025-10-08 10:30:00"
    }
}

6.1.9 扫货账号 (Tab 9)

  • 手机号码、邮箱、邮箱特殊代码
  • Cookie、xcsrftoken
  • 保存按钮

6.1.10 扫货配置 (Tab 10)

  • API接口请求暂停时间(秒)
  • 扫货线程数
  • API接口需短信验证时暂停时长(秒)
  • API接口需短信验证时重试次数

6.2 主控按钮

  • 选品按钮: 启动选品流程
  • 停止选品按钮: 停止选品
  • 扫货按钮: 启动扫货流程
  • 停止扫货按钮: 停止扫货

7. 关键技术细节

7.1 Cookie轮询机制

# 支持多个Cookie,用换行分隔
cookie_text = """
cookie1_value
cookie2_value
cookie3_value
"""
cookie_text_list = cookie_text.split('\n')

# 轮流使用
current_index = (request_count + retry_count) % len(cookie_text_list)
current_cookie = cookie_text_list[current_index]

7.2 请求频率控制

7.2.1 主流程请求控制

# 主流程中记录上次请求时间
self.last_xp_request_time = 0

# 请求前检查间隔
current_time = time.time()
time_since_last = current_time - self.last_xp_request_time
if time_since_last < float(self.pause_time):
    wait_time = float(self.pause_time) - time_since_last
    self.detail_logger.debug(f"暂停 {wait_time:.2f} seconds before making the next request.")
    time.sleep(wait_time)

# 发起请求
response = requests.get(url, headers=headers)

# 更新时间
self.last_xp_request_time = time.time()

7.2.2 BUFFAPI模块独立控制

在BUFFAPI.py中,不同类型的API有独立的请求时间控制:

class BUFFAPI:
    # 磨损信息API的请求时间控制
    @staticmethod
    def getPaintwearChoices(self_buff, goods_id):
        current_time = time.time()
        time_since_last_request = current_time - self_buff.last_paintwear_request_time
        if time_since_last_request < float(self_buff.pause_time):
            wait_time = float(self_buff.pause_time) - time_since_last_request
            time.sleep(float(wait_time))
        
        # ... API调用
        
        self_buff.last_paintwear_request_time = time.time()
    
    # 价格查询API的请求时间控制
    @staticmethod
    def getPaintwearChoicesMinPrice(self, goods_id, min_paint_wear, max_paint_wear):
        current_time = time.time()
        time_since_last_request = current_time - self.last_minprice_request_time
        if time_since_last_request < float(self.pause_time):
            wait_time = float(self.pause_time) - time_since_last_request
            time.sleep(float(wait_time))
        
        # ... API调用
        
        self.last_minprice_request_time = time.time()

关键点:

  • 不同API维护独立的时间戳,避免相互干扰
  • 每次API调用前都检查时间间隔
  • finally块中更新时间戳,确保即使异常也能更新

7.3 异常重试策略

retry_count = 0
max_retries = 3

while retry_count < max_retries:
    response = requests.get(url)
    
    if response.json()['code'] == 'OK':
        break  # 成功,退出
    elif response.json()['code'] == 'System Error':
        time.sleep(30)  # 等待更长时间
        continue
    elif response.json()['code'] == 'Captcha Validate Required':
        retry_count += 1
        time.sleep(pause_time)
        # 切换Cookie
    else:
        break  # 其他错误,退出

if retry_count >= max_retries:
    # 达到最大重试次数,标记Cookie失效或退出

7.4 CSRF Token获取

response = requests.get(url, headers=headers)

# 从响应头获取
for header, value in response.headers.items():
    if header == 'set-cookie':
        cookies_list = value.split(', ')
        for cookie in cookies_list:
            if 'csrf_token' in cookie:
                csrf_token = cookie.split('=')[1].split(';')[0]
                # 用于后续POST请求

7.5 定时任务配置

from apscheduler.schedulers.background import BackgroundScheduler

scheduler = BackgroundScheduler()

# 支持多个时间点
cron_time = "9:00,15:30,21:00"
cron_times = cron_time.split(',')

for cron_time_item in cron_times:
    if ':' in cron_time_item:
        hour, minute = cron_time_item.split(':')
        scheduler.add_job(
            select_goods_function,
            'cron',
            hour=int(hour),
            minute=int(minute),
            second=0
        )

scheduler.start()

7.6 多线程扫货

# 启动多个线程
for i in range(1, thread_count + 1):
    thread = threading.Thread(target=scan_goods, args=(i,))
    thread.start()

# 每个线程处理不同的商品
def scan_goods(thread_num):
    for i in range(len(select_logs)):
        if i % thread_count == thread_num - 1:
            # 处理这条记录
            goods_id = select_logs[i]['item_id']
            # ... 购买逻辑

7.7 信号槽机制(跨线程UI更新)

from PyQt5.QtCore import pyqtSignal, pyqtSlot

class MainWindow(QMainWindow):
    # 定义信号
    log_added = pyqtSignal(str, str, str, str, str, str, str, str)
    
    def __init__(self):
        # 连接信号到槽函数
        self.log_added.connect(self._add_select_log)
    
    def select_goods_thread(self):
        # 在工作线程中发射信号
        self.log_added.emit(
            item_id, item_name, min_wear, max_wear,
            min_price, sweep_price, judge_price, timestamp
        )
    
    @pyqtSlot(str, str, str, str, str, str, str, str)
    def _add_select_log(self, item_id, item_name, ...):
        # 在主线程中更新UI
        item = QTreeWidgetItem(self.tree_widget)
        item.setText(1, item_id)
        # ...

8. Go语言重构建议

8.1 技术选型建议

8.1.1 Web框架

  • Gin: 高性能HTTP框架,适合API服务
  • Echo: 轻量级,中间件丰富

8.1.2 数据库

  • GORM: ORM框架,支持SQLite/MySQL/PostgreSQL
  • 保留SQLite便于部署,可选支持MySQL

8.1.3 定时任务

  • cron: 标准定时任务库
  • gocron: 更友好的API

8.1.4 HTTP客户端

  • 标准库 net/http
  • resty: 更友好的API

8.1.5 配置管理

  • Viper: 支持多种配置格式
  • godotenv: .env文件支持

8.1.6 日志

  • zap: 高性能结构化日志
  • logrus: 功能丰富,Hook机制

8.1.7 并发

  • Goroutine + Channel (Go原生)
  • sync.WaitGroup: 等待多个goroutine完成

8.1.8 前端

  • Fyne: Go原生跨平台GUI
  • Wails: 使用Web技术构建桌面应用
  • 纯Web: 前后端分离(推荐)

8.2 架构设计建议

8.2.1 分层架构

cmd/
  └─ main.go                 # 程序入口
internal/
  ├─ api/                    # API层
  │  ├─ handler/            # HTTP处理器
  │  └─ middleware/         # 中间件
  ├─ service/               # 业务逻辑层
  │  ├─ select_service.go   # 选品服务
  │  └─ scan_service.go     # 扫货服务
  ├─ repository/            # 数据访问层
  │  ├─ config_repo.go
  │  └─ select_log_repo.go
  ├─ model/                 # 数据模型
  │  ├─ config.go
  │  └─ select_log.go
  ├─ client/                # 外部API客户端
  │  └─ buff_client.go
  └─ utils/                 # 工具函数
pkg/                        # 可复用的包
configs/                    # 配置文件
logs/                       # 日志目录
web/                        # 前端资源(如果使用Web)

8.2.2 核心Service设计

type SelectService struct {
    buffClient   *BuffClient
    configRepo   *ConfigRepository
    selectLogRepo *SelectLogRepository
    logger       *zap.Logger
}

func (s *SelectService) StartSelect(ctx context.Context) error {
    // 1. 加载配置
    config, err := s.configRepo.GetConfig()
    
    // 2. 构建查询参数
    params := s.buildQueryParams(config)
    
    // 3. 查询商品列表
    goods, err := s.buffClient.GetGoodsList(ctx, params)
    
    // 4. 分析商品价格
    for _, item := range goods {
        select {
        case <-ctx.Done():
            return ctx.Err()  // 支持取消
        default:
            result, err := s.analyzeGoodsPrice(ctx, item)
            if err != nil {
                continue
            }
            // 5. 保存选品结果
            s.selectLogRepo.Save(result)
        }
    }
    
    return nil
}

8.2.3 并发扫货设计

type ScanService struct {
    buffClient    *BuffClient
    selectLogRepo *SelectLogRepository
    logger        *zap.Logger
}

func (s *ScanService) StartScan(ctx context.Context, threadCount int) error {
    // 1. 获取选品日志
    logs, err := s.selectLogRepo.GetAll()
    
    // 2. 创建任务通道
    taskChan := make(chan *SelectLog, len(logs))
    for _, log := range logs {
        taskChan <- log
    }
    close(taskChan)
    
    // 3. 启动多个worker
    var wg sync.WaitGroup
    for i := 0; i < threadCount; i++ {
        wg.Add(1)
        go func(workerID int) {
            defer wg.Done()
            s.scanWorker(ctx, workerID, taskChan)
        }(i)
    }
    
    // 4. 等待所有worker完成
    wg.Wait()
    return nil
}

func (s *ScanService) scanWorker(ctx context.Context, id int, tasks <-chan *SelectLog) {
    for task := range tasks {
        select {
        case <-ctx.Done():
            return
        default:
            // 获取可购买订单
            orders, err := s.buffClient.GetSellOrders(ctx, task)
            
            // 购买
            for _, order := range orders {
                s.buffClient.BuyGoods(ctx, order)
            }
        }
    }
}

8.2.4 Cookie管理

type CookieManager struct {
    cookies       []string
    currentIndex  int
    failureCounts map[string]int
    maxRetries    int
    mu            sync.RWMutex
}

func (cm *CookieManager) GetCookie() (string, error) {
    cm.mu.RLock()
    defer cm.mu.RUnlock()
    
    if len(cm.cookies) == 0 {
        return "", errors.New("no available cookies")
    }
    
    cookie := cm.cookies[cm.currentIndex]
    cm.currentIndex = (cm.currentIndex + 1) % len(cm.cookies)
    
    return cookie, nil
}

func (cm *CookieManager) MarkFailure(cookie string) {
    cm.mu.Lock()
    defer cm.mu.Unlock()
    
    cm.failureCounts[cookie]++
    
    if cm.failureCounts[cookie] >= cm.maxRetries {
        // 移除失效的Cookie
        cm.removeCookie(cookie)
    }
}

func (cm *CookieManager) MarkSuccess(cookie string) {
    cm.mu.Lock()
    defer cm.mu.Unlock()
    
    cm.failureCounts[cookie] = 0
}

8.3 关键改进点

8.3.1 配置管理

# config.yaml
app:
  name: "BUFF选品扫货系统"
  version: "2.0.0"

database:
  type: "sqlite"
  path: "./buff.db"

buff:
  base_url: "https://buff.163.com"
  user_agent: "Mozilla/5.0..."
  
select:
  pause_time: 5
  max_pages: 10
  ref_price_adjust: 10
  sweep_price_adjust: 2
  
scan:
  pause_time: 3
  thread_count: 3

logging:
  level: "info"
  output: ["stdout", "file"]
  file_path: "./logs/app.log"
  max_size: 100  # MB
  max_age: 30    # days

8.3.2 错误处理

// 定义业务错误
var (
    ErrCookieRequired     = errors.New("cookie is required")
    ErrCookieInvalid      = errors.New("cookie is invalid")
    ErrCaptchaRequired    = errors.New("captcha validation required")
    ErrGoodsNotFound      = errors.New("goods not found")
    ErrInsufficientBalance = errors.New("insufficient balance")
)

// 统一错误处理
func HandleBuffAPIError(code string) error {
    switch code {
    case "OK":
        return nil
    case "Captcha Validate Required":
        return ErrCaptchaRequired
    case "Login Required":
        return ErrCookieInvalid
    case "System Error":
        return errors.New("buff system error")
    default:
        return fmt.Errorf("unknown error: %s", code)
    }
}

8.3.3 上下文管理

// 支持取消和超时
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute)
defer cancel()

// 在select和scan中传递context
err := selectService.StartSelect(ctx)

8.3.4 请求重试

func (c *BuffClient) GetWithRetry(ctx context.Context, url string, maxRetries int) (*Response, error) {
    var lastErr error
    
    for i := 0; i < maxRetries; i++ {
        select {
        case <-ctx.Done():
            return nil, ctx.Err()
        default:
        }
        
        resp, err := c.Get(ctx, url)
        if err == nil {
            return resp, nil
        }
        
        if errors.Is(err, ErrCaptchaRequired) {
            // 切换Cookie
            c.cookieManager.MarkFailure(c.currentCookie)
            c.currentCookie, _ = c.cookieManager.GetCookie()
            
            time.Sleep(time.Duration(c.config.PauseTime) * time.Second)
            continue
        }
        
        lastErr = err
    }
    
    return nil, fmt.Errorf("max retries exceeded: %w", lastErr)
}

8.3.5 结构化日志

logger.Info("开始选品",
    zap.Int("max_pages", config.MaxPages),
    zap.Int("page_size", config.PageSize),
    zap.String("sort_mode", config.SortMode),
)

logger.Info("商品分析完成",
    zap.String("goods_id", goods.ID),
    zap.String("goods_name", goods.Name),
    zap.Float64("min_price", result.MinPrice),
    zap.Float64("sweep_price", result.SweepPrice),
    zap.String("wear_range", fmt.Sprintf("%s-%s", result.MinWear, result.MaxWear)),
)

8.4 部署建议

8.4.1 Docker化

FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY . .
RUN go mod download
RUN go build -o buff-tool cmd/main.go

FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/buff-tool .
COPY --from=builder /app/configs ./configs
CMD ["./buff-tool"]

8.4.2 Docker Compose

version: '3.8'
services:
  buff-tool:
    build: .
    ports:
      - "8080:8080"
    volumes:
      - ./data:/root/data
      - ./logs:/root/logs
    environment:
      - APP_ENV=production

9. 数据迁移指南

9.1 SQLite数据库迁移

// 使用GORM自动迁移
db.AutoMigrate(
    &model.Account{},
    &model.Config{},
    &model.SelectLog{},
    // ...
)

// 或手动执行SQL
sqlFile, _ := ioutil.ReadFile("schema.sql")
db.Exec(string(sqlFile))

9.2 配置文件迁移

# Python: 从数据库读取
cursor.execute("SELECT * FROM config LIMIT 1")
config = cursor.fetchone()

# 导出为YAML
import yaml
config_dict = {
    'select': {
        'min_price': config[1],
        'max_price': config[2],
        # ...
    }
}
with open('config.yaml', 'w') as f:
    yaml.dump(config_dict, f)

9.3 日志文件迁移

保留原有日志目录结构,Go版本直接写入相同目录


10. 测试建议

10.1 单元测试

func TestPriceCalculation(t *testing.T) {
    service := &SelectService{}
    
    minPrice := 170.0
    refAdjust := 10
    sweepAdjust := 2
    
    judgePrice := service.calculateJudgePrice(minPrice, refAdjust)
    sweepPrice := service.calculateSweepPrice(minPrice, sweepAdjust)
    
    assert.Equal(t, 187.01, judgePrice)
    assert.Equal(t, 173.41, sweepPrice)
}

10.2 集成测试

func TestSelectFlow(t *testing.T) {
    // 使用测试数据库
    db := setupTestDB()
    defer db.Close()
    
    // Mock BUFF API
    server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(200)
        json.NewEncoder(w).Encode(map[string]interface{}{
            "code": "OK",
            "data": testGoodsData,
        })
    }))
    defer server.Close()
    
    // 执行选品
    service := NewSelectService(db, server.URL)
    err := service.StartSelect(context.Background())
    
    assert.NoError(t, err)
    
    // 验证结果
    logs := service.selectLogRepo.GetAll()
    assert.Greater(t, len(logs), 0)
}

11. 安全建议

11.1 敏感信息保护

// Cookie加密存储
import "golang.org/x/crypto/bcrypt"

func EncryptCookie(cookie string) (string, error) {
    hash, err := bcrypt.GenerateFromPassword([]byte(cookie), bcrypt.DefaultCost)
    return string(hash), err
}

// 环境变量
os.Setenv("BUFF_COOKIE", "...")
cookie := os.Getenv("BUFF_COOKIE")

11.2 请求限流

import "golang.org/x/time/rate"

limiter := rate.NewLimiter(rate.Every(5*time.Second), 1)

func (c *BuffClient) Get(url string) {
    limiter.Wait(context.Background())
    // 发起请求
}

11.3 HTTPS验证

client := &http.Client{
    Transport: &http.Transport{
        TLSClientConfig: &tls.Config{
            InsecureSkipVerify: false,  // 验证证书
        },
    },
}

12. 性能优化建议

12.1 数据库优化

// 批量插入
tx := db.Begin()
for _, log := range logs {
    tx.Create(&log)
}
tx.Commit()

// 索引
db.Exec("CREATE INDEX idx_goods_id ON select_logs(item_id)")

12.2 并发优化

// 使用worker pool
type WorkerPool struct {
    tasks    chan Task
    workers  int
    wg       sync.WaitGroup
}

func (p *WorkerPool) Start() {
    for i := 0; i < p.workers; i++ {
        p.wg.Add(1)
        go p.worker()
    }
}

func (p *WorkerPool) worker() {
    defer p.wg.Done()
    for task := range p.tasks {
        task.Execute()
    }
}

12.3 缓存优化

import "github.com/patrickmn/go-cache"

// 缓存商品磨损信息(5分钟过期)
c := cache.New(5*time.Minute, 10*time.Minute)
c.Set(goodsID, paintwearData, cache.DefaultExpiration)

if data, found := c.Get(goodsID); found {
    return data.(PaintwearData)
}

13. 附录

13.1 常见问题

Q: 为什么选品价格计算要使用双重round和加0.01? A:

  • 第一次round: 保证百分比计算后的结果为两位小数
  • 加0.01: 确保价格略高于临界值,避免浮点数比较时的精度问题
  • 第二次round: 保证最终结果为标准的两位小数金额
  • 例如: round(round(170.00 * 1.10, 2) + 0.01, 2) = 187.01

Q: 为什么选品要循环2次(xiesi_tag < 2)? A: 增加选品机会,因为第一次查询可能由于各种原因(Cookie异常、API限制等)没有获取到足够的商品,第二次循环可以补充更多商品

Q: 为什么扫货线程要分配任务而不是抢占式? A: 保证每个商品都被处理,避免某些商品被遗漏。使用i % thread_count == thread_num - 1的方式可以均匀分配任务

Q: Cookie为什么要轮询使用? A: 单个账号请求频繁容易触发验证码,多账号轮询降低风险。异常的Cookie会被标记并最终从可用列表中移除

Q: 为什么需要判断价和扫货价两个价格? A:

  • 判断价(pd_price): 用于筛选磨损区间,只保留价格高于判断价的区间
  • 扫货价(sweep_price): 实际购买时的价格上限,低于此价才购买
  • 分离两个价格使得选品和扫货逻辑更清晰,策略更灵活

Q: 为什么需要维护多个独立的请求时间戳? A:

  • last_xp_request_time: 主流程请求时间
  • last_paintwear_request_time: 磨损信息API请求时间
  • last_minprice_request_time: 价格查询API请求时间
  • 独立维护可以更精确地控制每个API的请求频率,避免相互干扰

Q: 为什么价格列表要进行阶梯筛选? A: 确保磨损越好价格越高,符合市场规律。如果出现价格不递减的情况(如区间3价格230,区间4价格240),说明数据异常,需要剔除后续区间

Q: xppcids.txt文件是如何使用的? A:

  • 文件中每行一个商品ID
  • 读取时用换行符分割,然后用#号连接成字符串
  • 在选品时,会过滤掉这些黑名单商品
  • 可以手动添加不想选品的商品ID到此文件

Q: 扫货时为什么page_size固定为95? A:

  • 这是硬编码在URL中的值
  • 95是一个经验值,既能获取足够多的订单,又不会因为请求数据量过大导致超时
  • 如果需要修改,需要直接修改代码中的URL字符串

13.2 BUFF平台限制与最佳实践

  • 每页数量: 最多返回100条商品(选品建议80,扫货使用95)
  • 请求间隔: 建议5秒以上,过快会触发System Error或验证码
  • 验证码触发:
    • 频繁请求会返回Captcha Validate Required
    • 需要切换Cookie或暂停更长时间(30秒以上)
    • 连续异常3次(可配置)会标记Cookie失效
  • 购买限制:
    • 单次购买后建议暂停3-5秒
    • 需要从响应头中提取csrf_token用于POST请求
  • 响应码说明:
    • OK: 正常
    • System Error: 访问频繁,建议暂停30秒
    • Captcha Validate Required: 需要验证,切换Cookie
    • Login Required: Cookie失效,需要重新登录
    • Action Forbidden: 操作被禁止(可能是风控)

13.3 性能调优参数建议

选品配置:

pause_time: 5-10          # API请求间隔(秒),建议5-10秒
page_size: 80-100         # 每页数量,建议80
max_pages: 10-20          # 最大页数,根据需求调整
ref_price_adjust: 10-15   # 参考价调整系数(%),建议10-15
sweep_price_adjust: 2-5   # 扫货价调整系数(%),建议2-5
needed_msg_check_pause_time: 30-60  # 验证码暂停时长(秒)
needed_msg_check_try_times: 3-5     # 验证码重试次数

扫货配置:

pause_time: 3-5           # 扫货请求间隔(秒)
thread_count: 1-3         # 扫货线程数,建议1-3个
page_size: 95            # 固定值,硬编码

Cookie管理:

  • 建议准备3-5个账号的Cookie
  • 每个Cookie用换行符分隔
  • 系统会自动轮询和管理失效Cookie

13.4 关键常量

13.4.1 磨损区间

# 磨损外观对应的磨损值区间
FACTORY_NEW = "factory_new"      # 崭新出厂: 0.00-0.07
MINIMAL_WEAR = "minimal_wear"    # 略有磨损: 0.07-0.15
FIELD_TESTED = "field_tested"    # 久经沙场: 0.15-0.38
WELL_WORN = "well_worn"          # 破损不堪: 0.38-0.45
BATTLE_SCARRED = "battle_scarred" # 战痕累累: 0.45-1.00

13.4.2 排序模式

SORT_DEFAULT = "default"              # 默认排序
SORT_PRICE_ASC = "price.asc"         # 价格升序
SORT_PRICE_DESC = "price.desc"       # 价格降序
SORT_SELL_NUM_DESC = "sell_num.desc" # 在售数量降序

13.4.3 请求URL

# 选品相关URL
GOODS_LIST_URL = "https://buff.163.com/api/market/goods"
GOODS_INFO_URL = "https://buff.163.com/api/market/goods/info"
SELL_ORDER_URL = "https://buff.163.com/api/market/goods/sell_order"

# 扫货专用URL(硬编码参数)
SCAN_URL = "https://buff.163.com/api/market/goods/sell_order?game=csgo&allow_epay=1&allow_tradable_cooldown=1&page_num=1&page_size=95&sort_by=price.asc&goods_id="

# 购买URL
BUY_URL = "https://buff.163.com/api/market/goods/buy"

13.4.4 默认配置值

# 请求控制
DEFAULT_PAUSE_TIME = 5          # 默认暂停时间(秒)
DEFAULT_PAGE_SIZE = 80          # 默认每页数量
DEFAULT_MAX_PAGES = 10          # 默认最大页数
SCAN_PAGE_SIZE = 95            # 扫货固定每页数量

# 价格调整
DEFAULT_REF_ADJUST = 10        # 默认参考价调整(%)
DEFAULT_SWEEP_ADJUST = 2       # 默认扫货价调整(%)

# 重试控制
DEFAULT_RETRY_TIMES = 3        # 默认重试次数
DEFAULT_RETRY_PAUSE = 30       # 默认重试暂停时间(秒)

# 日志保留
LOG_BACKUP_COUNT = 30          # 日志保留天数

13.5 重要代码片段索引

13.5.1 选品主循环

  • 文件: BuffMain.py
  • 行数: 368-964
  • 关键逻辑: xiesi_tag循环、Cookie轮询、价格计算

13.5.2 价格计算

  • 文件: BuffMain.py
  • 行数: 899, 915
  • 公式: round(round(min_price * (100 + adjust) / 100, 2) + 0.01, 2)

13.5.3 扫货线程

  • 文件: BuffMain.py
  • 行数: 1037-1171
  • 关键逻辑: 线程任务分配、购买逻辑

13.5.4 Cookie管理

  • 文件: BuffMain.py
  • 行数: 422-433, 561-573, 665-674
  • 关键逻辑: Cookie状态池、失效处理

13.5.5 API频率控制

  • 文件: BUFFAPI.py
  • 行数: 14-58, 62-141
  • 关键逻辑: 独立时间戳控制

13.6 参考资料


14. Go重构特别注意事项

14.1 价格计算精度

// Go语言实现双重round
func calculatePrice(basePrice float64, adjustPercent int, extraCent float64) float64 {
    // 第一次计算并round
    step1 := basePrice * float64(100 + adjustPercent) / 100.0
    step2 := math.Round(step1*100) / 100  // round到2位小数
    
    // 加0.01
    step3 := step2 + extraCent
    
    // 第二次round
    result := math.Round(step3*100) / 100
    
    return result
}

// 使用示例
judgePrice := calculatePrice(170.0, 10, 0.01)  // 187.01
sweepPrice := calculatePrice(170.0, 2, 0.01)   // 173.41

14.2 阶梯价格筛选逻辑

// 筛选价格递减的区间
func filterPriceList(priceList []PriceRange, judgePrice float64) []PriceRange {
    var filtered []PriceRange
    
    // 步骤1: 过滤低于判断价的区间
    for _, item := range priceList {
        if item.MinPrice >= judgePrice {
            filtered = append(filtered, item)
        } else {
            break  // 后续区间都不符合,直接退出
        }
    }
    
    // 步骤2: 保持价格递减
    var result []PriceRange
    for i, item := range filtered {
        if i == 0 {
            result = append(result, item)
            continue
        }
        
        // 检查是否递减
        if item.MinPrice < result[i-1].MinPrice {
            result = append(result, item)
        } else {
            break  // 不递减,剔除后续所有区间
        }
    }
    
    return result
}

14.3 Cookie池线程安全

type CookiePool struct {
    cookies       []string
    status        map[string]int
    currentIndex  int
    maxRetries    int
    mu            sync.RWMutex  // 读写锁
}

func (cp *CookiePool) GetNext() (string, error) {
    cp.mu.Lock()
    defer cp.mu.Unlock()
    
    if len(cp.cookies) == 0 {
        return "", errors.New("no available cookies")
    }
    
    cookie := cp.cookies[cp.currentIndex]
    cp.currentIndex = (cp.currentIndex + 1) % len(cp.cookies)
    
    return cookie, nil
}

func (cp *CookiePool) MarkFailure(cookie string) {
    cp.mu.Lock()
    defer cp.mu.Unlock()
    
    cp.status[cookie]++
    
    if cp.status[cookie] >= cp.maxRetries {
        cp.removeCookie(cookie)
    }
}

14.4 循环2次的实现

func (s *SelectService) StartSelect(ctx context.Context) error {
    // 外层循环2次
    for xiesiTag := 0; xiesiTag < 2; xiesiTag++ {
        // 初始化本轮数据
        needCheckList := []Goods{}
        cookieStatus := make(map[string]int)
        
        // 检查停止条件
        if s.shouldStop() {
            break
        }
        
        // 查询商品列表
        for page := 1; page <= s.config.MaxPages; page++ {
            goods, err := s.getGoodsPage(ctx, page)
            if err != nil {
                continue
            }
            needCheckList = append(needCheckList, goods...)
        }
        
        // 去重和筛选
        uniqueGoods := s.deduplicateGoods(needCheckList)
        filteredGoods := s.filterGoods(uniqueGoods)
        
        // 分析价格
        for _, goods := range filteredGoods {
            s.analyzeGoods(ctx, goods)
        }
    }
    
    return nil
}

14.5 xppcids.txt的读取

func readExcludeIDs(filepath string) ([]string, error) {
    data, err := ioutil.ReadFile(filepath)
    if err != nil {
        return nil, err
    }
    
    // 按行分割
    lines := strings.Split(string(data), "\n")
    
    var ids []string
    for _, line := range lines {
        line = strings.TrimSpace(line)
        if line != "" {
            ids = append(ids, line)
        }
    }
    
    return ids, nil
}

// 使用集合过滤
func filterExcludeGoods(goods []Goods, excludeIDs []string) []Goods {
    // 转换为map提高查询效率
    excludeSet := make(map[string]bool)
    for _, id := range excludeIDs {
        excludeSet[id] = true
    }
    
    var filtered []Goods
    for _, g := range goods {
        if !excludeSet[g.ID] {
            filtered = append(filtered, g)
        }
    }
    
    return filtered
}

文档版本: v1.1 最后更新: 2025-10-08 作者: AI Assistant
适用版本: Python原版 -> Go重构版 更新内容:

  • 补充选品双重循环逻辑
  • 完善价格计算双重round说明
  • 增加请求频率控制细节
  • 补充常见问题和最佳实践
  • 新增Go重构特别注意事项章节