# -*- coding: utf-8 -*- """ 流程测试:文章编辑 @某人 不存在时自动创建 Person + 存客宝获客计划 需求来源:临时需求池/2026-03-16-文章编辑自动创建@和#.md 验收:编辑文章输入 @新人物(链接人与事中无)→ 保存 → 链接人与事列表出现「新人物」,存客宝有对应计划 流程:管理端 ensureMentionsAndTags 对 content 中 @name 调用 POST /api/db/persons {name} → 后端按 name 查找,不存在则创建 Person + 调用存客宝创建获客计划 前置条件:存客宝 API 可连通(CKB_OPEN_API_KEY 等配置正确),且存在名为 soul 的设备;否则创建新 Person 会失败 """ import random import time import pytest import requests from util import admin_headers def _unique_name(): """生成唯一名称,避免与已有 Person 冲突""" return f"测试自动创建_{int(time.time() * 1000)}" def test_person_ensure_creates_ckb_plan_when_not_exists(admin_token, base_url): """ @某人 不存在时:POST /api/db/persons 仅传 name → 应创建 Person 并自动创建存客宝获客计划 """ if not admin_token: pytest.skip("admin 登录失败,跳过") name = _unique_name() r = requests.post( f"{base_url}/api/db/persons", headers=admin_headers(admin_token), json={"name": name}, timeout=15, ) assert r.status_code == 200, f"响应: {r.text}" data = r.json() assert data.get("success") is True, f"success 应为 true: {data}" person = data.get("person") assert person is not None, "应返回 person" assert person.get("name") == name assert person.get("personId"), "应有 personId" assert person.get("token"), "应有 token(小程序 @ 点击时兑换密钥)" # 存客宝获客计划应已创建 ckb_plan_id = person.get("ckbPlanId") or 0 assert ckb_plan_id > 0, f"应自动创建存客宝计划,ckbPlanId 应 > 0,实际: {ckb_plan_id}" assert person.get("ckbApiKey"), "应有 ckbApiKey" def test_person_ensure_returns_existing_when_name_exists(admin_token, base_url): """ @某人 已存在时:POST /api/db/persons 仅传 name → 应返回已有 Person,不重复创建 """ if not admin_token: pytest.skip("admin 登录失败,跳过") name = _unique_name() # 第一次创建 r1 = requests.post( f"{base_url}/api/db/persons", headers=admin_headers(admin_token), json={"name": name}, timeout=15, ) assert r1.status_code == 200 and r1.json().get("success") first_id = r1.json()["person"]["personId"] # 第二次相同 name,应返回已有 r2 = requests.post( f"{base_url}/api/db/persons", headers=admin_headers(admin_token), json={"name": name}, timeout=15, ) assert r2.status_code == 200 and r2.json().get("success") second = r2.json()["person"] assert second["personId"] == first_id, "相同 name 应返回同一 Person" def test_person_ensure_rejects_empty_name(admin_token, base_url): """name 为空时 POST /api/db/persons 应返回错误(不依赖存客宝)""" if not admin_token: pytest.skip("admin 登录失败,跳过") r = requests.post( f"{base_url}/api/db/persons", headers=admin_headers(admin_token), json={"name": ""}, timeout=10, ) assert r.status_code == 200 data = r.json() assert data.get("success") is False assert "name" in (data.get("error") or "").lower() or "必填" in (data.get("error") or "") def test_article_mention_flow_persons_list_contains_new(admin_token, base_url): """ 流程:创建新 Person 后,GET /api/db/persons 列表应包含该人 """ if not admin_token: pytest.skip("admin 登录失败,跳过") name = _unique_name() r_create = requests.post( f"{base_url}/api/db/persons", headers=admin_headers(admin_token), json={"name": name}, timeout=15, ) assert r_create.status_code == 200 and r_create.json().get("success") person_id = r_create.json()["person"]["personId"] # 拉列表 r_list = requests.get( f"{base_url}/api/db/persons", headers=admin_headers(admin_token), timeout=10, ) assert r_list.status_code == 200 and r_list.json().get("success") persons = r_list.json().get("persons") or [] found = [p for p in persons if p.get("personId") == person_id] assert len(found) == 1, f"列表应包含新建的 Person {person_id}" assert found[0].get("ckbPlanId", 0) > 0, "列表中应有 ckbPlanId" def test_new_article_save_auto_creates_person_and_ckb(admin_token, base_url): """ 完整流程:新建文章,content 含 @新人物 → 保存时 ensureMentionsAndTags 自动 POST persons → 创建 Person + 存客宝获客计划 → 再 PUT book 保存文章 """ if not admin_token: pytest.skip("admin 登录失败,跳过") ts = int(time.time() * 1000) rnd = random.randint(100000, 999999) name = f"测试新人物_{ts}_{rnd}" # chapters.id 限制 size:20,用短 id section_id = f"t{rnd}" # 1. 获取 book 结构,取第一个 part/chapter r_list = requests.get( f"{base_url}/api/db/book?action=list", headers=admin_headers(admin_token), timeout=10, ) assert r_list.status_code == 200 and r_list.json().get("success") sections = r_list.json().get("sections") or [] part_id = "part-1" chapter_id = "chapter-1" part_title = "未分类" chapter_title = "未分类" if sections: first = sections[0] part_id = first.get("partId") or part_id chapter_id = first.get("chapterId") or chapter_id part_title = first.get("partTitle") or part_title chapter_title = first.get("chapterTitle") or chapter_title # 2. 模拟 ensureMentionsAndTags:content 含 @name 时先 POST persons content = f"这是一篇测试文章,@{name} 会被自动创建并同步存客宝。" r_person = requests.post( f"{base_url}/api/db/persons", headers=admin_headers(admin_token), json={"name": name}, timeout=15, ) assert r_person.status_code == 200, f"创建 Person 失败: {r_person.text}" person_data = r_person.json() assert person_data.get("success") is True, f"Person 创建失败: {person_data}" person = person_data.get("person") assert person and person.get("ckbPlanId", 0) > 0, "应自动创建存客宝获客计划" # 3. 新建文章(PUT /api/db/book) payload = { "id": section_id, "title": f"测试自动创建_{ts}", "content": content, "price": 1, "isFree": False, "partId": part_id, "partTitle": part_title, "chapterId": chapter_id, "chapterTitle": chapter_title, "editionStandard": True, "editionPremium": False, "isNew": False, "hotScore": 0, } r_put = requests.put( f"{base_url}/api/db/book", headers=admin_headers(admin_token), json=payload, timeout=15, ) assert r_put.status_code == 200, f"保存文章失败: {r_put.text}" put_data = r_put.json() assert put_data.get("success") is True, f"保存文章失败: {put_data}" # 4. 验证 persons 列表包含新人物 r_persons = requests.get( f"{base_url}/api/db/persons", headers=admin_headers(admin_token), timeout=10, ) assert r_persons.status_code == 200 persons = r_persons.json().get("persons") or [] found = [p for p in persons if p.get("name") == name] assert len(found) == 1, f"链接人与事列表应包含「{name}」" assert found[0].get("ckbPlanId", 0) > 0, "应有存客宝获客计划" # 5. 清理:删除测试文章(避免重复运行冲突) try: requests.delete( f"{base_url}/api/db/book?id={section_id}", headers=admin_headers(admin_token), timeout=10, ) except Exception: pass