发布计划不再绑设备、不再依赖账号台账:上传文件名 `名字_日期_编号`(标题行 `标题 || 正文_名字_日期_编号`),运行时读本机当前抖音昵称取计划;靠循环块 「一轮把本机待发的号跑完」自动切号,一直发到发完。 · 解析/配对换键:右锚定认名字(不许含下划线、不许纯数字 —— 不合法直接拒收并说清原因), 删掉三处"手机号必须命中台账"的校验,加索引 ix_video_plan_name_date(启动时补建) · 新步骤三个:读本机账号名字 / 切换账号(**按名字点控件**、切完回读校验、遇登录页停本设备)/ 循环本机待发的号(试过不回头、跑一轮没进展就换号、停止·超时·上限三个干净出口) · 发布骨架改 18 步:读名字 → 校验账号(cmp_source=account_target)→ 推送(**推送前重读昵称核对**) → 抖音流程 → 标记结果;push_release 的 account 默认 name · 账号管理整页下线:删 core/ledger.py(serial_of 迁到 core/device_pool.py)、6 个 /api/ledger*、 「账号」页与 ledger.js、设备池「账号」列、设备端账号块;发布计划升为一级页签; device_account 表与 42 行历史数据保留、仍在备份清单内(**停用不等于删数据**) · 人工入口「按名字找设备」:新增 core/account_probe.py(只读探测、跳过在跑任务的设备) + POST /api/video_plan/push_by_account,没设备登着的号逐条报出来; 推送时把"实际是谁发的"写进 posted_serial / posted_device_name · 通知:新增 task.account.switch_failed / task.account.need_login · schema v12(posted_*)/ v13(device.cur_account_name / cur_name_at) · 文档同步:TASK_DEV / API / DATA_MODEL / ARCHITECTURE / NOTIFY / DEVICE_AGENT / DEPLOY / DEVELOPMENT / README / .env.example(两个抖音控件选择器抓一次填 .env) · 新增 scripts/tests/:6 套用例 + run_all.py(临时库 + 假设备,不碰真实数据、不连真机)
120 lines
7.2 KiB
Python
120 lines
7.2 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""P1 纯函数测试:`名字_日期_编号` 解析(不碰 DB、不碰用户数据)。"""
|
||
import io
|
||
import os
|
||
import sys
|
||
|
||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
||
# 仓库根 = scripts/tests/ 往上三级(不写死绝对路径,换机器也能跑)
|
||
_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||
sys.path.insert(0, _ROOT)
|
||
os.chdir(_ROOT)
|
||
from core import video_plan as vp # noqa: E402
|
||
|
||
fails = []
|
||
|
||
|
||
def check(label, got, want):
|
||
ok = got == want
|
||
if not ok:
|
||
fails.append(label)
|
||
print((' ✅ ' if ok else ' ❌ ') + label + ('' if ok else '\n 得到 %r\n 期望 %r' % (got, want)))
|
||
|
||
|
||
print('=== parse_video_name:合法 ===')
|
||
check('名字_日期_编号', vp.parse_video_name('小王_20260912_1.mp4')[0],
|
||
{"name": "小王", "date": "2026-09-12", "seq": 1, "seq_given": True})
|
||
check('名字_日期(无编号)', vp.parse_video_name('建材王总_20260913.mp4')[0],
|
||
{"name": "建材王总", "date": "2026-09-13", "seq": None, "seq_given": False})
|
||
check('名字里带数字', vp.parse_video_name('翻斗花园我最强2号_20260913.mp4')[0]['name'], '翻斗花园我最强2号')
|
||
check('名字里带空格/表情', vp.parse_video_name('有牛奶面包 你吃吗?_20260912_2.mp4')[0]['name'],
|
||
'有牛奶面包 你吃吗?')
|
||
check('名字里带表情', vp.parse_video_name('🐟小鱼_20260912.mp4')[0]['name'], '🐟小鱼')
|
||
check('大写扩展名', vp.parse_video_name('小王_20260912.MP4')[0]['date'], '2026-09-12')
|
||
check('编号 3 位', vp.parse_video_name('小王_20260912_999.mp4')[0]['seq'], 999)
|
||
|
||
print('=== parse_video_name:拒收 ===')
|
||
|
||
|
||
def err_of(fn):
|
||
return fn[1]
|
||
|
||
|
||
check('名字带下划线', err_of(vp.parse_video_name('小_王_20260912.mp4')),
|
||
'名字里不能有下划线:`小_王_20260912` 里日期前面有 2 段 ——应为 名字_日期_编号')
|
||
check('名字是纯数字', err_of(vp.parse_video_name('12345678_20260912.mp4')),
|
||
'名字不能是纯数字(12345678 会被当成日期/编号)—— 请填抖音上的昵称')
|
||
check('结尾不是日期', err_of(vp.parse_video_name('小王_20260912_1234.mp4')),
|
||
'文件名格式不对:`小王_20260912_1234` 的结尾既不是日期(8 位数字,如 20260912),'
|
||
'也不是「日期_编号」—— 应为 名字_日期_编号')
|
||
check('老格式(手机号在前)', err_of(vp.parse_video_name('13382508423_20261011_01.mp4')),
|
||
'这一段(13382508423)是手机号 —— 文件名第一段要填**抖音上的名字**,'
|
||
'格式是 名字_日期_编号(现在不认手机号了)')
|
||
check('非法日期', err_of(vp.parse_video_name('小王_20260230.mp4')),
|
||
'日期 20260230 不是合法日期(应为 20260912 这种 8 位)')
|
||
check('只有一段', err_of(vp.parse_video_name('小王.mp4')),
|
||
'文件名格式不对,应为 名字_日期_编号(编号可省略,如 小王_20260912.mp4)')
|
||
check('不是视频', err_of(vp.parse_video_name('小王_20260912.txt')),
|
||
'不是视频文件(支持 mp4/mov/mkv/avi/3gp/flv/wmv/m4v/ts/webm)')
|
||
check('空文件名', err_of(vp.parse_video_name('')), '文件名为空')
|
||
check('多段(名字含下划线还带日期)', err_of(vp.parse_video_name('小_王_总_20260912.mp4')),
|
||
'名字里不能有下划线:`小_王_总_20260912` 里日期前面有 3 段 ——应为 名字_日期_编号')
|
||
|
||
print('=== parse_title_line:合法 ===')
|
||
check('标题 || 正文_名字_日期_编号', vp.parse_title_line('中秋祝福 || 祝大家中秋快乐_小王_20260912_1')[0],
|
||
{"title": "中秋祝福", "body": "祝大家中秋快乐", "name": "小王",
|
||
"date": "2026-09-12", "seq": 1, "seq_given": True})
|
||
check('只有标题', vp.parse_title_line('中秋祝福_小王_20260912')[0],
|
||
{"title": "中秋祝福", "body": "", "name": "小王",
|
||
"date": "2026-09-12", "seq": None, "seq_given": False})
|
||
check('只有正文(|| 在最前)', vp.parse_title_line(' || 只有正文_小王_20260912')[0]['body'], '只有正文')
|
||
check('标题里带下划线', vp.parse_title_line('名字带_下划线也行_小王_20260912')[0]['title'], '名字带_下划线也行')
|
||
check('#话题开头的标题', vp.parse_title_line('#中秋快乐_小王_20260929')[0]['title'], '#中秋快乐')
|
||
check('正文里再有 || 原样保留', vp.parse_title_line('标题 || 正文 || 又一处_小王_20260912')[0]['body'],
|
||
'正文 || 又一处')
|
||
check('标题是纯数字', vp.parse_title_line('2026_小王_20260912_1')[0]['title'], '2026')
|
||
|
||
print('=== parse_title_line:拒收 ===')
|
||
check('只有名字/日期(没文字)', err_of(vp.parse_title_line('小王_20260912')),
|
||
'缺标题/正文内容(只有名字/日期/编号,没有文字)')
|
||
check('名字/日期/编号(没文字)', err_of(vp.parse_title_line('小王_20260912_1')),
|
||
'缺标题/正文内容(只有名字/日期/编号,没有文字)')
|
||
check('老写法(名字在前)', err_of(vp.parse_title_line('19286456013_20260912_1_标题')),
|
||
"格式不对,应为「标题 || 正文_名字_日期_编号」(编号可省略;日期是 8 位数字,如 20260912)")
|
||
check('名字是手机号', err_of(vp.parse_title_line('标题_13382508423_20260912')),
|
||
'这一段(13382508423)是手机号 —— 文件名第一段要填**抖音上的名字**,'
|
||
'格式是 名字_日期_编号(现在不认手机号了)')
|
||
check('标题里带下划线(名字取紧挨日期那段)',
|
||
vp.parse_title_line('标题_小_王_20260912')[0],
|
||
{"title": "标题_小", "body": "", "name": "王", "date": "2026-09-12",
|
||
"seq": None, "seq_given": False})
|
||
check('空行', err_of(vp.parse_title_line(' ')), '空行')
|
||
|
||
print('=== parse_titles_text:注释与多行 ===')
|
||
rows, errors = vp.parse_titles_text(
|
||
'标题一_小王_20260912\n# 这是注释\n\n#中秋快乐_小王_20260913\n'
|
||
'#真注释_没有日期\n标题没有日期')
|
||
check('收下 2 条标题', [r['title'] for r in rows], ['标题一', '#中秋快乐'])
|
||
check('# 认不出名字/日期 → 当注释忽略', [(e['line'], e['raw']) for e in errors], [(6, '标题没有日期')])
|
||
|
||
print('=== pair_slots:按名字配对 ===')
|
||
videos = [dict(vp.parse_video_name('小王_20260912_1.mp4')[0], ref='v1'),
|
||
dict(vp.parse_video_name('小王_20260912_2.mp4')[0], ref='v2'),
|
||
dict(vp.parse_video_name('小李_20260912.mp4')[0], ref='v3')]
|
||
titles = [dict(vp.parse_title_line('标题A_小王_20260912')[0], ref='t1'),
|
||
dict(vp.parse_title_line('标题B_小王_20260912')[0], ref='t2'),
|
||
dict(vp.parse_title_line('标题C_小李_20260912')[0], ref='t3')]
|
||
pairs, rejected = vp.pair_slots(videos, titles)
|
||
check('配成 3 对', len(pairs), 3)
|
||
check('没有拒收', rejected, [])
|
||
check('按 (日期,名字,编号) 排序', [(p['date'], p['name'], p['seq']) for p in pairs],
|
||
[('2026-09-12', '小李', 1), ('2026-09-12', '小王', 1), ('2026-09-12', '小王', 2)])
|
||
check('每对都配上标题', sorted(p['title']['title'] for p in pairs), ['标题A', '标题B', '标题C'])
|
||
check('配对结果里的键是 name', all('name' in p and 'phone' not in p for p in pairs), True)
|
||
|
||
print()
|
||
if fails:
|
||
print('❌ 失败 %d 项:%s' % (len(fails), ' / '.join(fails)))
|
||
sys.exit(1)
|
||
print('✅ 全部通过')
|