From f22892968f3c14680f8ac5f20b477bc6a4318113 Mon Sep 17 00:00:00 2001
From: ZF sun <34314687@qq.com>
Date: Tue, 13 Oct 2026 10:22:15 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E5=90=8D=E7=89=87?=
=?UTF-8?q?=E4=BA=91=20API=20=E6=96=87=E6=A1=A3=EF=BC=8C=E7=A7=BB=E9=99=A4?=
=?UTF-8?q?=20Telegram=20=E5=92=8C=20Messenger=20=E7=8B=AC=E7=AB=8B?=
=?UTF-8?q?=E5=AD=97=E6=AE=B5=EF=BC=8C=E7=BB=9F=E4=B8=80=E4=B8=BA=E7=A4=BE?=
=?UTF-8?q?=E4=BA=A4=E5=AA=92=E4=BD=93=E8=B4=A6=E5=8F=B7?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
docs/CARD_API_REFERENCE.md | 216 +++++++++++++++++++++++++++++++++++++
pages_tool/card/detail.vue | 3 +-
pages_tool/card/edit.vue | 19 ++--
3 files changed, 224 insertions(+), 14 deletions(-)
create mode 100644 docs/CARD_API_REFERENCE.md
diff --git a/docs/CARD_API_REFERENCE.md b/docs/CARD_API_REFERENCE.md
new file mode 100644
index 0000000..c293667
--- /dev/null
+++ b/docs/CARD_API_REFERENCE.md
@@ -0,0 +1,216 @@
+# 名片云(card addon)API 对接文档
+
+> 读者:UniApp 前端开发人员(微信小程序 / H5 / 鸿蒙元服务三端)。
+> 事实来源:`backend/src/addon/card/api/controller/`(Card.php / Contact.php / Lead.php / Recognize.php / BaseCardApi.php)。
+> 文档与源码如有出入,以源码为准。最后更新:2026-10-13。
+
+---
+
+## 1. 总览
+
+| 模块 | URL 前缀 | 用途 |
+|---|---|---|
+| 名片夹 | `/card/api/card/*` | 我的名片(列表 / 详情 / 编辑 / 认领 / 识别建卡) |
+| 人脉库 | `/card/api/contact/*` | 我的人脉(增删改查 / 转线索) |
+| 潜在客户 | `/card/api/lead/*` | 名片收到的留资线索(列表 / 跟进状态) |
+| 识别(旧) | `/card/api/recognize/execute` | 旧版识别入口,新代码请用 `/card/api/card/recognize` |
+
+**前端收口要求**:所有 URL 必须集中在 API 层统一管理(项目内已有页面直连这些地址,路径一个字符都不能变,只做收口不做改名)。
+
+---
+
+## 2. 通用约定
+
+### 2.1 URL 规则
+
+addon 标准路由:`/{addon}/api/{controller}/{action}`,例如:
+
+```
+POST https://<域名>/card/api/card/lists
+```
+
+### 2.2 鉴权(全部接口强制)
+
+- 所有接口都要求 member token(后端 `BaseCardApi::requireAuth()` 强制,匿名一律 `code < 0` 拒绝)。
+- 走项目现有请求工具(自动携带 `token` 与 `uniacid` 参数/头),**不要额外传 `site_id`**——后端 site_id 只认 token 绑定值,入参传了也会被忽略。
+
+### 2.3 返回信封
+
+统一 JSON:
+
+```json
+{ "code": 0, "message": "success", "data": { } }
+```
+
+- `code === 0`:成功,业务数据在 `data`。
+- `code !== 0`:失败,`message` 可直接 toast(用户可读文案)。
+
+### 2.4 行级权限语义(重要,影响 UI)
+
+- 所有数据按 `owner_id = token 会员` 行级隔离:列表只返回自己的卡/人脉/线索。
+- 访问**不存在**或**不属于自己**的资源,后端统一报「不存在」(如「卡片不存在」「人脉不存在」「线索不存在」),不区分两种情况(防探测)。前端提示文案按"内容不存在或已删除"处理,不要提示"无权限"。
+- `owner_id`、`source_type` 等系统字段不可通过入参修改(后端显式收口)。
+
+---
+
+## 3. 名片夹 `/card/api/card/*`
+
+### 3.1 我的名片列表 `card/lists`
+
+- 入参:`page`(默认 1)、`page_size`(默认 20,**上限 100**)、`keyword`(姓名/手机/公司模糊,选填)
+- 返回 `data`:
+
+```json
+{ "list": [ /* relation 行,见 3.2 字段说明;不含 access_password */ ],
+ "count": 0, "page_count": 0 }
+```
+
+### 3.2 卡片详情 `card/detail`
+
+- 入参:`card_id`(必填,字符串,形如 `c_1_shop_20261013...`)
+- 返回 `data`:
+
+```json
+{
+ "relation": { /* 卡片主行,字段见下方"卡片字段表" */ },
+ "source": { /* 识别来源行,可为 null */ },
+ "share_list": [ /* 关联分享配置 */ ]
+}
+```
+
+卡片字段表(`relation`,渲染用带 `_list` 后缀的键):
+
+| 键 | 说明 |
+|---|---|
+| `card_id` | 卡片唯一标识(页面路由参数用它,不用自增 id) |
+| `name / company / job_title / mobile / phone / email / website / address / postal_code / country / city / province / wechat / telegram / messenger / avatar / bio` | 标量字段 |
+| `mobiles_list` | `[{value}]` 多手机号(解码后) |
+| `socials_list` | `[{type, label, value}]` 社交账号(`label` 是后端翻译好的中文名,直接展示)。**Telegram/Messenger 已并入此处**(旧独立字段软下线,后端读取时自动合并旧列值) |
+| `custom_list` | `[{label, value}]` 自定义字段 |
+| `locked_fields` | `[{key}]` 站点锁定字段清单,**编辑页据此渲染只读**,提交时后端也会剥离(静默丢弃,不报错) |
+| `source_type` | 来源:manual / manual_batch / scan / import_csv / import_xlsx / clone / api |
+| `owner_id` | 归属会员 id(只读) |
+
+- 原始 JSON 串 `mobiles / socials / custom_fields` 仍在返回中(兼容保留),**新代码一律消费 `*_list`**。
+
+### 3.3 更新卡片 `card/update`
+
+- 入参:`card_id`(必填)+ `fields`(嵌套对象;后端按 `fields/a` 收)
+- ⚠️ **normalize 语义(历史事故定稿,必须遵守)**:
+ - **键存在才写**:`fields` 里带了哪个键就更新哪个键;
+ - 空字符串 `""` = 显式清空该字段;
+ - **键缺席 = 不擦除**(保持原值);
+ - 因此局部更新时**只传用户实际编辑过的键**,不要把整包表单无脑回传。
+- 富化字段入参 wire format(放在 `fields` 内):
+
+```json
+{
+ "mobiles": ["13800000000", { "value": "13911112222" }],
+ "socials": [ { "type": "douyin", "value": "xxx" } ],
+ "custom_fields": [ { "label": "展厅号", "value": "E3-108" } ]
+}
+```
+
+ - `socials.type` 白名单:`alipay / douyin / wxvideo / xiaohongshu / weibo / kuaishou / bilibili / qq / linkedin / facebook / instagram / x / telegram / messenger / other`(type 非法或 value 空的条目被后端静默丢弃);
+ - ⚠️ `telegram / messenger` 已**不再是独立标量字段**(2026-10-13 软下线):`fields.telegram` / `fields.messenger` 传了也会被忽略,统一走 `socials` 数组按类型添加;旧数据由后端在 `socials_list` 中自动合并返回;
+ - `custom_fields`:label 与 value 必须同时非空,否则该条被丢弃;label 截断 20 字,value 截断 255 字。
+- 返回 `data`:更新后的 relation 行。
+- 嵌套对象跨端序列化:请用 JSON body(或项目 `sendJson()` 通道);后端收 `fields` 为对象/数组。
+
+### 3.4 认领名片 `card/claim`
+
+- 入参:`card_id`(必填)
+- 场景:商家后台建的卡未归属(owner_id=0),员工把它认领到自己名下。
+- 返回 `data`:认领结果(失败时 `message` 可直接展示)。
+
+### 3.5 识别建卡 `card/recognize`
+
+- 入参:`media_path`(必填,图片/媒体地址)、`media_type`(默认 `image`)、`source_type`(默认 `scan`)
+- 返回 `data`:
+
+```json
+{ "source_id": 1, "relation_id": 2, "card_id": "c_1_shop_...",
+ "deduped": false, "fields": { /* 归一化后的识别字段 */ } }
+```
+
+- ⚠️ `deduped === true` 表示站点内已有同手机号名片,**未新建**、绑到了既有卡上——前端应提示用户"已存在同名片"。
+- 注意:当前后端识别服务为桩实现(`识别服务未注册` 会作为错误返回),对接时请按失败分支兜底 UI。
+
+### 3.6 识别失败重试 `card/retry`
+
+- 入参:`source_id`(必填)
+- 约束:仅本站 + status=failed 的来源可重试,有次数上限(超限报错,message 可直接展示)。
+- 返回 `data`:`{ source_id, relation_id, card_id, retry_count, deduped, message }`。
+
+### 3.7 批量识别 `card/recognizeBatch`
+
+- 入参:`items`:非空数组,1~10 条,每项 `{ source_type?, media_type?, media_path, media_key? }`。
+- 返回 `data`:`{ items: [ { ok: bool, message, data } ], ok: 成功数, failed: 失败数 }`——逐项独立成败,单条失败不影响其它。
+
+### 3.8 旧入口 `recognize/execute`
+
+- 入参与 `card/recognize` 相同。**已上线页面可能仍在用,新代码一律用 `card/recognize`。**
+
+---
+
+## 4. 人脉库 `/card/api/contact/*`
+
+### 4.1 人脉列表 `contact/lists`
+
+- 入参:`page`、`page_size`(≤100)、`keyword`(姓名/手机/公司模糊)、`source`(manual/scan/exchange/lead_in,选填)、`status`(0/1,选填)
+- 返回 `data`:`{ list, count, page_count }`
+
+### 4.2 新增人脉 `contact/add`
+
+- 入参:`name`(**必填**);选填:`company / job_title / mobile / email / wechat / website / address / remark`
+- 返回 `data`:新增行。
+
+### 4.3 人脉详情 `contact/info`
+
+- 入参:`id`(数字自增主键,注意与名片的 `card_id` 字符串不同)
+- 返回 `data`:人脉行(编辑页回填用)。
+
+### 4.4 更新人脉 `contact/update`
+
+- 入参:`id` + 字段。兼容两种传参:`fields` 对象,或顶层平铺字段(顶层会剔除 `id/token/uniacid/fields`)。
+- 可更新白名单:`name / company / job_title / mobile / email / wechat / website / address / remark / status`
+- 返回 `data`:更新后行。
+
+### 4.5 删除人脉 `contact/delete`
+
+- 入参:`id`。**真删**(不可恢复),前端建议二次确认弹窗。
+
+### 4.6 人脉 → 潜在客户 `contact/toLead`
+
+- 入参:`id`(人脉 id)
+- 行为:写一条潜在客户线索(归属当前员工),**不删人脉行**;同员工同手机号已转过则报错(手机号为空时直接插入)。
+- 返回 `data`:新线索信息。
+
+---
+
+## 5. 潜在客户 `/card/api/lead/*`
+
+### 5.1 线索列表 `lead/lists`
+
+- 入参:`page`、`page_size`(≤100)、`status`(0-3,选填)、`keyword`(姓名/手机模糊)
+- 可见范围:**自己名下名片收到的线索**(按名片归属过滤)+ 自己从人脉转入的线索。没有名片时列表为空属正常现象。
+- 返回 `data`:`{ list, count, page_count }`
+
+### 5.2 更新跟进状态 `lead/setStatus`
+
+- 入参:`id`(线索 id)、`status`(**只能是 0/1/2/3**,其它值报「参数不合法」)
+- 返回 `data`:`{ id, status }`
+
+---
+
+## 6. 对接 checklist(前端收口时逐条核对)
+
+1. [ ] 所有 `/card/api/*` URL 收口到 API 层,与本文档路径逐一比对(现有 7 个页面 12 处直连,路径不可变)。
+2. [ ] 统一走项目请求工具(自动带 token / uniacid),任何接口不传 `site_id`。
+3. [ ] 失败分支统一 `toast(message)`;「不存在」类错误按"内容不存在或已删除"提示。
+4. [ ] `card/update` 只传编辑过的键(键存在才写语义)。
+5. [ ] 详情/列表渲染富化字段用 `*_list` 解码键,不解析原始 JSON 串。
+6. [ ] 编辑页对 `locked_fields` 命中的字段渲染只读。
+7. [ ] `recognize` 成功后检查 `deduped`,为 true 时提示"已存在同手机号名片"。
+8. [ ] 分页参数 `page_size` 不超过 100。
+9. [ ] 名片路由参数用 `card_id`(字符串),人脉/线索用 `id`(数字),不要混用。
diff --git a/pages_tool/card/detail.vue b/pages_tool/card/detail.vue
index cf1b690..b2253f7 100644
--- a/pages_tool/card/detail.vue
+++ b/pages_tool/card/detail.vue
@@ -105,8 +105,7 @@ export default {
push('网站', r.website);
push('地址', [r.country, r.province, r.city, r.address].filter(Boolean).join(' '));
push('邮编', r.postal_code);
- push('Telegram', r.telegram);
- push('Messenger', r.messenger);
+ // Telegram/Messenger 已软下线为社交流, 经 socials_list 统一渲染
// 附加手机号(mobiles_list, 后端 CardRichFields 解码) → 可拨打
(r.mobiles_list || []).forEach((m, i) => {
diff --git a/pages_tool/card/edit.vue b/pages_tool/card/edit.vue
index fa91523..023dc31 100644
--- a/pages_tool/card/edit.vue
+++ b/pages_tool/card/edit.vue
@@ -66,16 +66,7 @@
form.wechat = e.detail.value" />
-
- Telegram
- form.telegram = e.detail.value" />
-
-
- Messenger
- form.messenger = e.detail.value" />
-
+
@@ -164,10 +155,12 @@
// 后端 Card::update 的 richFieldSaves 仅对显式携带的键做 CardRichFields 清洗。
const FIELDS = [
'name', 'company', 'job_title', 'avatar',
- 'mobile', 'phone', 'email', 'website', 'wechat', 'telegram', 'messenger',
+ 'mobile', 'phone', 'email', 'website', 'wechat',
'country', 'province', 'city', 'address', 'postal_code',
'bio'
];
+// telegram/messenger 已软下线(2026-10-13): 不再是独立标量字段,
+// 统一在「社交媒体账号」动态行里以类型方式添加(见下方 SOCIAL_TYPES)。
// 社交媒体类型: 必须与后端 CardRichFields::SOCIAL_TYPES 白名单保持一致(增减需两边同步)
const SOCIAL_TYPES = [
@@ -183,6 +176,8 @@ const SOCIAL_TYPES = [
{ key: 'facebook', label: 'Facebook' },
{ key: 'instagram', label: 'Instagram' },
{ key: 'x', label: 'X' },
+ { key: 'telegram', label: 'Telegram' },
+ { key: 'messenger', label: 'Messenger' },
{ key: 'other', label: '其他' }
];
@@ -197,7 +192,7 @@ export default {
cardId: '',
form: {
name: '', company: '', job_title: '', avatar: '',
- mobile: '', phone: '', email: '', website: '', wechat: '', telegram: '', messenger: '',
+ mobile: '', phone: '', email: '', website: '', wechat: '',
country: '', province: '', city: '', address: '', postal_code: '',
bio: ''
},