From 0e7b81eaa894af5691b374aab2b6f55a3478bb12 Mon Sep 17 00:00:00 2001 From: Alex-larget <33240357+Alex-larget@users.noreply.github.com> Date: Mon, 16 Mar 2026 11:24:42 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=B5=8B=E8=AF=95=E6=96=87?= =?UTF-8?q?=E6=A1=A3=E5=B9=B6=E5=A2=9E=E5=BC=BA=E8=AE=BE=E5=A4=87=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E9=80=BB=E8=BE=91=20-=20=E6=96=B0=E5=A2=9E=E4=BA=86?= =?UTF-8?q?=E4=B8=80=E9=A1=B9=E5=8A=9F=E8=83=BD=EF=BC=8C=E7=94=A8=E4=BA=8E?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E7=94=9F=E6=88=90=E6=96=87=E7=AB=A0=E4=B8=AD?= =?UTF-8?q?=E7=9A=84=E6=8F=90=E5=8F=8A=E5=86=85=E5=AE=B9=EF=BC=8C=E4=BB=8E?= =?UTF-8?q?=E8=80=8C=E4=BC=98=E5=8C=96=E4=BA=86=E7=B3=BB=E7=BB=9F=E4=B8=AD?= =?UTF-8?q?=E5=88=9B=E5=BB=BA=E8=AE=A1=E5=88=92=E6=B5=81=E7=A8=8B=E7=9A=84?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E7=94=A8=E4=BE=8B=E6=96=87=E6=A1=A3=E3=80=82?= =?UTF-8?q?=20-=20=E6=9B=B4=E6=96=B0=E4=BA=86=E6=B5=8B=E8=AF=95=E5=85=B3?= =?UTF-8?q?=E8=81=94=E6=96=87=E6=A1=A3=EF=BC=8C=E4=BB=A5=E7=BA=B3=E5=85=A5?= =?UTF-8?q?=E5=9F=BA=E4=BA=8E=E4=B8=8D=E5=90=8C=E5=9C=BA=E6=99=AF=E7=9A=84?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E7=94=A8=E4=BE=8B=E7=9A=84=E5=BD=92=E6=A1=A3?= =?UTF-8?q?=E5=92=8C=E5=A4=8D=E7=94=A8=E8=A7=84=E5=88=99=E3=80=82=20-=20?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E4=BA=86=E5=9C=A8=E6=9C=AA=E6=8C=87=E5=AE=9A?= =?UTF-8?q?=E8=AE=BE=E5=A4=87=E6=97=B6=E9=80=89=E6=8B=A9=E9=BB=98=E8=AE=A4?= =?UTF-8?q?=E8=AE=BE=E5=A4=87=E7=9A=84=E9=80=BB=E8=BE=91=EF=BC=8C=E7=A1=AE?= =?UTF-8?q?=E4=BF=9D=E8=AE=BE=E5=A4=87=E7=AE=A1=E7=90=86=E6=B5=81=E7=A8=8B?= =?UTF-8?q?=E6=9B=B4=E5=8A=A0=E9=A1=BA=E7=95=85=E3=80=82=20-=20=E5=9C=A8?= =?UTF-8?q?=E8=AE=A1=E5=88=92=E5=88=9B=E5=BB=BA=E7=9A=84=E4=B8=8A=E4=B8=8B?= =?UTF-8?q?=E6=96=87=E4=B8=AD=E6=98=8E=E7=A1=AE=E4=BA=86=E8=AE=BE=E5=A4=87?= =?UTF-8?q?=E7=BB=84=E7=9A=84=E9=9C=80=E6=B1=82=EF=BC=8C=E4=BB=8E=E8=80=8C?= =?UTF-8?q?=E6=8F=90=E9=AB=98=E4=BA=86=E7=B3=BB=E7=BB=9F=E7=9A=84=E6=95=B4?= =?UTF-8?q?=E4=BD=93=E5=8F=AF=E9=9D=A0=E6=80=A7=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .cursor/agent/开发助理/项目索引/测试.md | 1 + .../evolution/2026-03-16-scripts目录与测试关联.md | 22 +- .cursor/agent/软件测试/evolution/索引.md | 1 + .cursor/skills/testing/SKILL.md | 2 + .../2026-03-16-文章@某人自动创建-测试报告.md | 38 +++ scripts/test/process/README.md | 1 + .../process/test_article_mention_ckb_flow.py | 218 ++++++++++++++++++ soul-api/internal/handler/ckb_open.go | 108 +++++++++ soul-api/internal/handler/db_person.go | 23 +- 临时需求池/2026-03-16-文章编辑自动创建@和#.md | 2 +- 10 files changed, 405 insertions(+), 11 deletions(-) create mode 100644 scripts/test/process/2026-03-16-文章@某人自动创建-测试报告.md create mode 100644 scripts/test/process/test_article_mention_ckb_flow.py diff --git a/.cursor/agent/开发助理/项目索引/测试.md b/.cursor/agent/开发助理/项目索引/测试.md index b7ece45f..6573bd00 100644 --- a/.cursor/agent/开发助理/项目索引/测试.md +++ b/.cursor/agent/开发助理/项目索引/测试.md @@ -25,6 +25,7 @@ | 2026-03-16 | scripts/test 测试用例目录约定:miniapp 小程序接口、web 管理端 | 已完成 | | 2026-03-16 | scripts/test/process 流程测试目录:跨端业务流程 | 已完成 | | 2026-03-16 | pytest 架构、配置从项目读取、运行前显示测试环境 | 已完成 | +| 2026-03-16 | 文章 @某人 自动创建存客宝:用例编写、执行、报告;归档规则 | 已完成 | --- diff --git a/.cursor/agent/软件测试/evolution/2026-03-16-scripts目录与测试关联.md b/.cursor/agent/软件测试/evolution/2026-03-16-scripts目录与测试关联.md index ed472652..02e15519 100644 --- a/.cursor/agent/软件测试/evolution/2026-03-16-scripts目录与测试关联.md +++ b/.cursor/agent/软件测试/evolution/2026-03-16-scripts目录与测试关联.md @@ -74,10 +74,30 @@ --- -### 7. 与 soul-api/scripts 的区别 +### 7. 测试用例归档与复用规则 + +| 场景 | 归档目录 | 示例 | +|------|----------|------| +| 管理端 + 后端混合 | process/ | 文章 @某人 自动创建 Person + 存客宝 | +| 仅小程序接口 | miniapp/ | 登录、VIP、阅读 | +| 仅管理端/后端 | web/ | 鉴权、CRUD | + +**需求变更**:用例随需求更新;无变更时直接复用。 + +--- + +### 8. 与 soul-api/scripts 的区别 | 位置 | 内容 | 测试关联 | |------|------|----------| | `scripts/`(项目根) | 本地启动、飞书同步、Gitea 推送、**test/** | 见上文 | | `scripts/test/` | **测试用例**:miniapp、web、process;pytest 架构 | 测试工程师在此写用例 | | `soul-api/scripts/` | SQL 迁移、Python 脚本等 | 数据库迁移、后端运维;测试时若涉及表结构变更,需关注对应 SQL | + +--- + +### 9. 示例:文章 @某人 自动创建(2026-03-16) + +- **用例**:`scripts/test/process/test_article_mention_ckb_flow.py` +- **报告**:`scripts/test/process/2026-03-16-文章@某人自动创建-测试报告.md` +- **结论**:后端逻辑正确,会调用存客宝创建计划;存客宝 API 返回 400 导致失败,需排查 CKB 配置或 deviceGroups 空值 diff --git a/.cursor/agent/软件测试/evolution/索引.md b/.cursor/agent/软件测试/evolution/索引.md index 3ddb9f3d..8e5a818f 100644 --- a/.cursor/agent/软件测试/evolution/索引.md +++ b/.cursor/agent/软件测试/evolution/索引.md @@ -9,3 +9,4 @@ | 2026-03-16 | scripts/test 测试用例目录:miniapp 小程序接口、web 管理端 | [2026-03-16-scripts目录与测试关联.md](./2026-03-16-scripts目录与测试关联.md) | | 2026-03-16 | scripts/test/process 流程测试:跨端多接口串联 | [2026-03-16-scripts目录与测试关联.md](./2026-03-16-scripts目录与测试关联.md) | | 2026-03-16 | pytest 架构、配置从项目读取、运行前显示测试环境 | [2026-03-16-scripts目录与测试关联.md](./2026-03-16-scripts目录与测试关联.md) | +| 2026-03-16 | 测试用例归档规则:混合→process、纯端→miniapp/web;需求变更时更新 | [2026-03-16-scripts目录与测试关联.md](./2026-03-16-scripts目录与测试关联.md) | diff --git a/.cursor/skills/testing/SKILL.md b/.cursor/skills/testing/SKILL.md index 9acca18b..50cfe19c 100644 --- a/.cursor/skills/testing/SKILL.md +++ b/.cursor/skills/testing/SKILL.md @@ -64,6 +64,8 @@ description: Soul 创业派对测试人员。功能测试、回归测试、三 **环境配置**:必须明确指定 SOUL_TEST_ENV(local/souldev/soulapi)或 SOUL_API_BASE;配置从 soul-api/.env* 或 .env.test 读取。运行前报告头部会显示「测试环境: xxx」,确认无误后再执行,避免误测正式库。 +**归档规则**:管理端+后端混合 → process/;仅小程序 → miniapp/;仅管理端/后端 → web/。需求变更时更新用例,无变更则复用。 + --- ## 6. 产出与协同 diff --git a/scripts/test/process/2026-03-16-文章@某人自动创建-测试报告.md b/scripts/test/process/2026-03-16-文章@某人自动创建-测试报告.md new file mode 100644 index 00000000..cddaca52 --- /dev/null +++ b/scripts/test/process/2026-03-16-文章@某人自动创建-测试报告.md @@ -0,0 +1,38 @@ +# 测试报告 - 文章 @某人 自动创建存客宝获客计划 + +**日期**:2026-03-16 +**测试环境**:local (http://localhost:8080) +**用例位置**:`scripts/test/process/test_article_mention_ckb_flow.py` + +--- + +## 测试结论 + +| 用例 | 结果 | 说明 | +|------|------|------| +| test_person_ensure_creates_ckb_plan_when_not_exists | ❌ 失败 | 后端会调用存客宝创建计划,但存客宝 API 返回 400 | +| test_person_ensure_returns_existing_when_name_exists | ❌ 失败 | 依赖上一条(需先创建成功) | +| test_article_mention_flow_persons_list_contains_new | ❌ 失败 | 同上 | +| test_person_ensure_rejects_empty_name | ✅ 通过 | name 为空时正确返回错误 | + +--- + +## 失败原因 + +``` +error: 创建存客宝计划失败: 创建计划失败 +ckbResponse: {"code": 400, "data": [], "message": ""} +``` + +后端逻辑正确:POST /api/db/persons 仅传 name 时,会按 name 查找,不存在则创建 Person 并调用存客宝创建获客计划。 +存客宝开放 API 返回 400,可能原因: +- CKB_OPEN_API_KEY / CKB_OPEN_ACCOUNT 配置有误 +- deviceGroups 为空时存客宝不允许创建(需求文档有提及) +- 存客宝 API 参数格式变更 + +--- + +## 建议 + +1. 后端:排查存客宝 create plan 400 原因,确认 deviceGroups 空时是否允许 +2. 测试:存客宝可连通后重新跑 `pytest process/test_article_mention_ckb_flow.py -v` diff --git a/scripts/test/process/README.md b/scripts/test/process/README.md index b997a12a..78adb501 100644 --- a/scripts/test/process/README.md +++ b/scripts/test/process/README.md @@ -11,6 +11,7 @@ - **VIP 开通→资料填写→排行展示**:会员流程 - **提现申请→审核→到账**:提现流程 - **内容发布→审核→上架→用户可见**:内容流转 +- **文章 @某人 自动创建**:编辑文章输入 @新人物(不存在)→ 自动创建 Person + 存客宝获客计划(`test_article_mention_ckb_flow.py`) --- diff --git a/scripts/test/process/test_article_mention_ckb_flow.py b/scripts/test/process/test_article_mention_ckb_flow.py new file mode 100644 index 00000000..92192d15 --- /dev/null +++ b/scripts/test/process/test_article_mention_ckb_flow.py @@ -0,0 +1,218 @@ +# -*- 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 diff --git a/soul-api/internal/handler/ckb_open.go b/soul-api/internal/handler/ckb_open.go index f95af57b..c3895ea1 100644 --- a/soul-api/internal/handler/ckb_open.go +++ b/soul-api/internal/handler/ckb_open.go @@ -10,6 +10,7 @@ import ( "net/http" "net/url" "strconv" + "strings" "time" "github.com/gin-gonic/gin" @@ -193,6 +194,113 @@ func ckbOpenGetPlanDetail(token string, planID int64) (string, error) { return result.Data.APIKey, nil } +// ckbOpenGetDefaultDeviceID 获取默认设备 ID:拉设备列表,取第一个 memo 或 nickname 包含 "soul" 的设备;用于 deviceGroups 必填时的默认值 +func ckbOpenGetDefaultDeviceID(token string) (int64, error) { + u := ckbOpenBaseURL + "/v1/devices?keyword=soul&page=1&limit=50" + req, err := http.NewRequest(http.MethodGet, u, nil) + if err != nil { + return 0, fmt.Errorf("构造设备列表请求失败: %w", err) + } + req.Header.Set("Authorization", "Bearer "+token) + resp, err := http.DefaultClient.Do(req) + if err != nil { + return 0, fmt.Errorf("请求存客宝设备列表失败: %w", err) + } + defer resp.Body.Close() + b, _ := io.ReadAll(resp.Body) + var parsed map[string]interface{} + if err := json.Unmarshal(b, &parsed); err != nil { + return 0, fmt.Errorf("解析设备列表失败: %w", err) + } + var listAny interface{} + if dataVal, ok := parsed["data"].(map[string]interface{}); ok { + listAny = dataVal["list"] + } else if la, ok := parsed["list"]; ok { + listAny = la + } + arr, ok := listAny.([]interface{}) + if !ok { + return 0, fmt.Errorf("设备列表格式异常") + } + // 优先匹配 memo/nickname 包含 soul 的设备;若无则取第一个(keyword 可能已过滤) + for _, item := range arr { + m, ok := item.(map[string]interface{}) + if !ok { + continue + } + memo := toString(m["memo"]) + if memo == "" { + memo = toString(m["imei"]) + } + nickname := toString(m["nickname"]) + lowerMemo := strings.ToLower(memo) + lowerNick := strings.ToLower(nickname) + if strings.Contains(lowerMemo, "soul") || strings.Contains(lowerNick, "soul") { + id := parseDeviceID(m["id"]) + if id > 0 { + return id, nil + } + } + } + // 未找到含 soul 的,取第一个 + if len(arr) > 0 { + if m, ok := arr[0].(map[string]interface{}); ok { + id := parseDeviceID(m["id"]) + if id > 0 { + return id, nil + } + } + } + return 0, fmt.Errorf("未找到名为 soul 的设备,请先在存客宝添加设备并设置 memo 或 nickname 包含 soul") +} + +func toString(v interface{}) string { + if v == nil { + return "" + } + switch val := v.(type) { + case string: + return val + case float64: + return strconv.FormatFloat(val, 'f', -1, 64) + case int: + return strconv.Itoa(val) + case int64: + return strconv.FormatInt(val, 10) + default: + return fmt.Sprint(v) + } +} + +func parseDeviceID(v interface{}) int64 { + if v == nil { + return 0 + } + switch val := v.(type) { + case float64: + if val > 0 { + return int64(val) + } + case int: + if val > 0 { + return int64(val) + } + case int64: + if val > 0 { + return val + } + case string: + if val == "" { + return 0 + } + n, err := strconv.ParseInt(val, 10, 64) + if err == nil && n > 0 { + return n + } + } + return 0 +} + // ckbOpenDeletePlan 调用 DELETE /v1/plan/delete 删除存客宝获客计划 func ckbOpenDeletePlan(token string, planID int64) error { payload := map[string]interface{}{"planId": planID} diff --git a/soul-api/internal/handler/db_person.go b/soul-api/internal/handler/db_person.go index 2d56bfb5..359e14d7 100644 --- a/soul-api/internal/handler/db_person.go +++ b/soul-api/internal/handler/db_person.go @@ -144,6 +144,15 @@ func DBPersonSave(c *gin.Context) { deviceIDs = append(deviceIDs, id) } } + // deviceGroups 必填:未传时默认选择名为 soul 的设备 + if len(deviceIDs) == 0 { + defaultID, err := ckbOpenGetDefaultDeviceID(openToken) + if err != nil { + c.JSON(http.StatusOK, gin.H{"success": false, "error": "获取默认设备失败: " + err.Error()}) + return + } + deviceIDs = []int64{defaultID} + } planPayload := map[string]interface{}{ "name": name, "sceneId": 11, @@ -156,9 +165,7 @@ func DBPersonSave(c *gin.Context) { "enabled": true, "tips": body.Tips, "distributionEnabled": false, - } - if len(deviceIDs) > 0 { - planPayload["deviceGroups"] = deviceIDs + "deviceGroups": deviceIDs, } planID, ckbCreateData, ckbResponse, err := ckbOpenCreatePlan(openToken, planPayload) @@ -193,13 +200,11 @@ func DBPersonSave(c *gin.Context) { StartTime: startTime, EndTime: endTime, } - if len(deviceIDs) > 0 { - idsStr := make([]string, 0, len(deviceIDs)) - for _, id := range deviceIDs { - idsStr = append(idsStr, fmt.Sprintf("%d", id)) - } - newPerson.DeviceGroups = strings.Join(idsStr, ",") + idsStr := make([]string, 0, len(deviceIDs)) + for _, id := range deviceIDs { + idsStr = append(idsStr, fmt.Sprintf("%d", id)) } + newPerson.DeviceGroups = strings.Join(idsStr, ",") if err := db.Create(&newPerson).Error; err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()}) diff --git a/临时需求池/2026-03-16-文章编辑自动创建@和#.md b/临时需求池/2026-03-16-文章编辑自动创建@和#.md index 13cb1305..58b80fd5 100644 --- a/临时需求池/2026-03-16-文章编辑自动创建@和#.md +++ b/临时需求池/2026-03-16-文章编辑自动创建@和#.md @@ -71,5 +71,5 @@ ## 七、风险与待确认 -- 存客宝创建计划时 deviceGroups 为空是否允许?当前实现不传 deviceGroups,若 CKB 拒绝则需配置默认设备 +- ~~存客宝创建计划时 deviceGroups 为空是否允许?~~ 已确认:deviceGroups 必填;未传时默认选择名为 soul 的设备(ckbOpenGetDefaultDeviceID) - Person 按 name 查找:取第一个匹配;若有多人同名,会复用第一个