更新测试文档并增强设备管理逻辑
- 新增了一项功能,用于自动生成文章中的提及内容,从而优化了系统中创建计划流程的测试用例文档。 - 更新了测试关联文档,以纳入基于不同场景的测试用例的归档和复用规则。 - 实现了在未指定设备时选择默认设备的逻辑,确保设备管理流程更加顺畅。 - 在计划创建的上下文中明确了设备组的需求,从而提高了系统的整体可靠性。
This commit is contained in:
@@ -25,6 +25,7 @@
|
||||
| 2026-03-16 | scripts/test 测试用例目录约定:miniapp 小程序接口、web 管理端 | 已完成 |
|
||||
| 2026-03-16 | scripts/test/process 流程测试目录:跨端业务流程 | 已完成 |
|
||||
| 2026-03-16 | pytest 架构、配置从项目读取、运行前显示测试环境 | 已完成 |
|
||||
| 2026-03-16 | 文章 @某人 自动创建存客宝:用例编写、执行、报告;归档规则 | 已完成 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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 空值
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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. 产出与协同
|
||||
|
||||
38
scripts/test/process/2026-03-16-文章@某人自动创建-测试报告.md
Normal file
38
scripts/test/process/2026-03-16-文章@某人自动创建-测试报告.md
Normal file
@@ -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`
|
||||
@@ -11,6 +11,7 @@
|
||||
- **VIP 开通→资料填写→排行展示**:会员流程
|
||||
- **提现申请→审核→到账**:提现流程
|
||||
- **内容发布→审核→上架→用户可见**:内容流转
|
||||
- **文章 @某人 自动创建**:编辑文章输入 @新人物(不存在)→ 自动创建 Person + 存客宝获客计划(`test_article_mention_ckb_flow.py`)
|
||||
|
||||
---
|
||||
|
||||
|
||||
218
scripts/test/process/test_article_mention_ckb_flow.py
Normal file
218
scripts/test/process/test_article_mention_ckb_flow.py
Normal file
@@ -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
|
||||
@@ -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}
|
||||
|
||||
@@ -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()})
|
||||
|
||||
@@ -71,5 +71,5 @@
|
||||
|
||||
## 七、风险与待确认
|
||||
|
||||
- 存客宝创建计划时 deviceGroups 为空是否允许?当前实现不传 deviceGroups,若 CKB 拒绝则需配置默认设备
|
||||
- ~~存客宝创建计划时 deviceGroups 为空是否允许?~~ 已确认:deviceGroups 必填;未传时默认选择名为 soul 的设备(ckbOpenGetDefaultDeviceID)
|
||||
- Person 按 name 查找:取第一个匹配;若有多人同名,会复用第一个
|
||||
|
||||
Reference in New Issue
Block a user