feat: 更新名片云 API 文档,补充商家端架构说明及接口细节

This commit is contained in:
2026-10-13 10:37:47 +08:00
parent 6e3818dff6
commit 1f44f11248

View File

@@ -1,27 +1,44 @@
# 名片云(card addon)API 对接文档
> 读者:UniApp 前端开发人员(微信小程序 / H5 / 鸿蒙元服务三端)。
> 事实来源:`backend/src/addon/card/api/controller/`(Card.php / Contact.php / Lead.php / Recognize.php / BaseCardApi.php)。
> 文档与源码如有出入,以源码为准。最后更新:2026-10-13。
> 读者:前端开发人员(员工端 UniApp:微信小程序 / H5 / 鸿蒙;商家端 PC 后台 layui)。
> 事实来源:员工端 `backend/src/addon/card/api/controller/`;商家端 `backend/src/addon/card/shop/controller/`。
> 文档与源码如有出入,以源码为准。最后更新:2026-10-13(v2:补商家端架构说明,回应《商家端接口需求清单》)。
---
## 1. 总览
## 0. 双端架构总览(先读这一节,避免提错层)
| | 员工端(UniApp) | 商家端(PC 后台) |
|---|---|---|
| 身份 | C 端 member token(`BaseCardApi::requireAuth` 强制) | shop 模块 admin session(`BaseShop`),登录态在 cookie/session |
| URL | `/card/api/{controller}/{action}` | 后台 hash 路由 `shop.html#url=`(addon 页面/JSON action) |
| 数据范围 | **行级隔离**:只看 `owner_id = token 会员` 的数据 | **全站视图**(site_id + app_module 过滤) |
| 页面 | `pages_tool/card/*`(7 页) | `shop/view/card/*`(lists/create/edit/batch_create/detail/duplicates/design/statistics/templates/template_edit/contacts/leads/create_share) |
**⚠️ 三条架构铁律**(对应《商家端接口需求清单》§0 的三个前提,正式裁决如下):
1. **权限模型**:清单的方案 A/B 都不成立。member token 与商家 admin session 是**两套独立身份体系**,不存在"member 即站点所属者"的数据关系;`owner_id` 行级隔离是防探测安全设计,**永不豁免**。商家端管理页一律走 PC 后台(已上线);若未来要做"商家移动端",须新增独立命名空间(如 `/card/api/merchant/*`)+ 商家身份凭证(需先设计 admin→移动端的鉴权通道,属新立项),**不得复用 member token**。
2. **数据可见范围**:**绝不在现有员工接口上加 `scope=site` 之类参数**——那会让任何会员拉走全站数据,一行代码即越权漏洞。全站视图只存在于 shop session 侧接口(本文第 2 部分)。
3. **设计 JSON**:既不是"后端原样存自由 JSON",也不是清单提议的 `modules[]` 结构。存储 = `lucky_card_design` **结构化列**(`layout / field_order / show_fields / card_title / cta_text / cta_url / theme_color / locked_fields / print_size / template_id / font_*`)+ **canvas JSON blocks**(结构见 `CardDesignRenderer::canvas()`:`{width,height,print_size,blocks:[{type,style:{x,y,width,height,rotate,zIndex},props...}]}`)。前端设计器必须对齐既有 canvas blocks,不能另起 schema。
---
# 第 1 部分:员工端(UniApp member API)
## 1.1 总览
| 模块 | URL 前缀 | 用途 |
|---|---|---|
| 名片夹 | `/card/api/card/*` | 我的名片(列表 / 详情 / 编辑 / 认领 / 识别建卡) |
| 名片夹 | `/card/api/card/*` | 我的名片(列表 / 详情 / 新建 / 编辑 / 认领 / 识别建卡) |
| 人脉库 | `/card/api/contact/*` | 我的人脉(增删改查 / 转线索) |
| 潜在客户 | `/card/api/lead/*` | 名片收到的留资线索(列表 / 跟进状态) |
| 识别(旧) | `/card/api/recognize/execute` | 旧版识别入口,新代码请用 `/card/api/card/recognize` |
**前端收口要求**:所有 URL 必须集中在 API 层统一管理(项目内已有页面直连这些地址,路径一个字符都不能变,只做收口不做改名)。
---
## 1.2 通用约定
## 2. 通用约定
### 2.1 URL 规则
### 1.2.1 URL 规则
addon 标准路由:`/{addon}/api/{controller}/{action}`,例如:
@@ -29,12 +46,12 @@ addon 标准路由:`/{addon}/api/{controller}/{action}`,例如:
POST https://<域名>/card/api/card/lists
```
### 2.2 鉴权(全部接口强制)
### 1.2.2 鉴权(全部接口强制)
- 所有接口都要求 member token(后端 `BaseCardApi::requireAuth()` 强制,匿名一律 `code < 0` 拒绝)。
- 走项目现有请求工具(自动携带 `token` 与 `uniacid` 参数/头),**不要额外传 `site_id`**——后端 site_id 只认 token 绑定值,入参传了也会被忽略。
### 2.3 返回信封
### 1.2.3 返回信封
统一 JSON:
@@ -45,27 +62,25 @@ POST https://<域名>/card/api/card/lists
- `code === 0`:成功,业务数据在 `data`。
- `code !== 0`:失败,`message` 可直接 toast(用户可读文案)。
### 2.4 行级权限语义(重要,影响 UI)
### 1.2.4 行级权限语义(重要,影响 UI)
- 所有数据按 `owner_id = token 会员` 行级隔离:列表只返回自己的卡/人脉/线索。
- 访问**不存在**或**不属于自己**的资源,后端统一报「不存在」(如「卡片不存在」「人脉不存在」「线索不存在」),不区分两种情况(防探测)。前端提示文案按"内容不存在或已删除"处理,不要提示"无权限"。
- `owner_id`、`source_type` 等系统字段不可通过入参修改(后端显式收口)。
---
## 1.3 名片夹 `/card/api/card/*`
## 3. 名片夹 `/card/api/card/*`
### 3.1 我的名片列表 `card/lists`
### 1.3.1 我的名片列表 `card/lists`
- 入参:`page`(默认 1)、`page_size`(默认 20,**上限 100**)、`keyword`(姓名/手机/公司模糊,选填)
- 返回 `data`:
```json
{ "list": [ /* relation 行,见 3.2 字段说明;不含 access_password */ ],
{ "list": [ /* relation 行,见 1.3.2 字段说明;不含 access_password */ ],
"count": 0, "page_count": 0 }
```
### 3.2 卡片详情 `card/detail`
### 1.3.2 卡片详情 `card/detail`
- 入参:`card_id`(必填,字符串,形如 `c_1_shop_20261013...`)
- 返回 `data`:
@@ -83,7 +98,7 @@ POST https://<域名>/card/api/card/lists
| 键 | 说明 |
|---|---|
| `card_id` | 卡片唯一标识(页面路由参数用它,不用自增 id) |
| `name / company / job_title / mobile / phone / email / website / address / postal_code / country / city / province / wechat / telegram / messenger / avatar / bio` | 标量字段 |
| `name / company / job_title / mobile / phone / email / website / address / postal_code / country / city / province / wechat / avatar / bio` | 标量字段(**telegram/messenger 已移除**,见下) |
| `mobiles_list` | `[{value}]` 多手机号(解码后) |
| `socials_list` | `[{type, label, value}]` 社交账号(`label` 是后端翻译好的中文名,直接展示)。**Telegram/Messenger 已并入此处**(旧独立字段软下线,后端读取时自动合并旧列值) |
| `custom_list` | `[{label, value}]` 自定义字段 |
@@ -93,7 +108,7 @@ POST https://<域名>/card/api/card/lists
- 原始 JSON 串 `mobiles / socials / custom_fields` 仍在返回中(兼容保留),**新代码一律消费 `*_list`**。
### 3.3 更新卡片 `card/update`
### 1.3.3 更新卡片 `card/update`
- 入参:`card_id`(必填)+ `fields`(嵌套对象;后端按 `fields/a` 收)
- ⚠️ **normalize 语义(历史事故定稿,必须遵守)**:
@@ -117,13 +132,20 @@ POST https://<域名>/card/api/card/lists
- 返回 `data`:更新后的 relation 行。
- 嵌套对象跨端序列化:请用 JSON body(或项目 `sendJson()` 通道);后端收 `fields` 为对象/数组。
### 3.4 认领名片 `card/claim`
### 1.3.4 认领名片 `card/claim`
- 入参:`card_id`(必填)
- 场景:商家后台建的卡未归属(owner_id=0),员工把它认领到自己名下。
- 返回 `data`:认领结果(失败时 `message` 可直接展示)。
### 3.5 识别建卡 `card/recognize`
### 1.3.5 新建名片 `card/create`
- 入参:`fields`(**name 必填**;其余标量/富化字段选填,与 update 的 fields 通道同构)
- 行为:生成站内 card_id,`source_type=manual`,**建卡即归属当前会员**;锁定字段剥离规则同 update。
- 返回 `data`:`{ card_id, relation_id }`
- 前端路径:`list.vue` 顶栏「新建」→ `edit.vue` 无 card_id 新建模式 → 成功后 redirectTo 详情页。
### 1.3.6 识别建卡 `card/recognize`
- 入参:`media_path`(必填,图片/媒体地址)、`media_type`(默认 `image`)、`source_type`(默认 `scan`)
- 返回 `data`:
@@ -136,76 +158,70 @@ POST https://<域名>/card/api/card/lists
- ⚠️ `deduped === true` 表示站点内已有同手机号名片,**未新建**、绑到了既有卡上——前端应提示用户"已存在同名片"。
- 注意:当前后端识别服务为桩实现(`识别服务未注册` 会作为错误返回),对接时请按失败分支兜底 UI。
### 3.6 识别失败重试 `card/retry`
### 1.3.7 识别失败重试 `card/retry`
- 入参:`source_id`(必填)
- 约束:仅本站 + status=failed 的来源可重试,有次数上限(超限报错,message 可直接展示)。
- 返回 `data`:`{ source_id, relation_id, card_id, retry_count, deduped, message }`。
### 3.7 批量识别 `card/recognizeBatch`
### 1.3.8 批量识别 `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`
### 1.3.9 旧入口 `recognize/execute`
- 入参与 `card/recognize` 相同。**已上线页面可能仍在用,新代码一律用 `card/recognize`。**
---
## 1.4 人脉库 `/card/api/contact/*`
## 4. 人脉库 `/card/api/contact/*`
### 4.1 人脉列表 `contact/lists`
### 1.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`
### 1.4.2 新增人脉 `contact/add`
- 入参:`name`(**必填**);选填:`company / job_title / mobile / email / wechat / website / address / remark`
- 返回 `data`:新增行。
### 4.3 人脉详情 `contact/info`
### 1.4.3 人脉详情 `contact/info`
- 入参:`id`(数字自增主键,注意与名片的 `card_id` 字符串不同)
- 返回 `data`:人脉行(编辑页回填用)。
### 4.4 更新人脉 `contact/update`
### 1.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`
### 1.4.5 删除人脉 `contact/delete`
- 入参:`id`。**真删**(不可恢复),前端建议二次确认弹窗。
### 4.6 人脉 → 潜在客户 `contact/toLead`
### 1.4.6 人脉 → 潜在客户 `contact/toLead`
- 入参:`id`(人脉 id)
- 行为:写一条潜在客户线索(归属当前员工),**不删人脉行**;同员工同手机号已转过则报错(手机号为空时直接插入)。
- 返回 `data`:新线索信息。
---
## 1.5 潜在客户 `/card/api/lead/*`
## 5. 潜在客户 `/card/api/lead/*`
### 5.1 线索列表 `lead/lists`
### 1.5.1 线索列表 `lead/lists`
- 入参:`page`、`page_size`(≤100)、`status`(0-3,选填)、`keyword`(姓名/手机模糊)
- 可见范围:**自己名下名片收到的线索**(按名片归属过滤)+ 自己从人脉转入的线索。没有名片时列表为空属正常现象。
- 返回 `data`:`{ list, count, page_count }`
### 5.2 更新跟进状态 `lead/setStatus`
### 1.5.2 更新跟进状态 `lead/setStatus`
- 入参:`id`(线索 id)、`status`(**只能是 0/1/2/3**,其它值报「参数不合法」)
- 返回 `data`:`{ id, status }`
---
## 1.6 员工端对接 checklist
## 6. 对接 checklist(前端收口时逐条核对)
1. [ ] 所有 `/card/api/*` URL 收口到 API 层,与本文档路径逐一比对(现有 7 个页面 12 处直连,路径不可变)。
1. [ ] 所有 `/card/api/*` URL 收口到 API 层,与本文档路径逐一比对(现有页面直连路径不可变)。
2. [ ] 统一走项目请求工具(自动带 token / uniacid),任何接口不传 `site_id`。
3. [ ] 失败分支统一 `toast(message)`;「不存在」类错误按"内容不存在或已删除"提示。
4. [ ] `card/update` 只传编辑过的键(键存在才写语义)。
@@ -214,3 +230,47 @@ POST https://<域名>/card/api/card/lists
7. [ ] `recognize` 成功后检查 `deduped`,为 true 时提示"已存在同手机号名片"。
8. [ ] 分页参数 `page_size` 不超过 100。
9. [ ] 名片路由参数用 `card_id`(字符串),人脉/线索用 `id`(数字),不要混用。
---
# 第 2 部分:商家端(PC 后台,已上线能力对照)
> 对应《名片云商家端接口需求清单》的逐条答复。商家端接口全部走 **shop 模块 admin session** 鉴权
> (页面请求带 `X-Requested-With: XMLHttpRequest` 返回 fragment,JSON 请求直接 `json()`)。
> **以下能力均已上线,前端不需要也不应该在 member API 层重新实现。**
## 2.1 需求清单 → 已上线能力映射表
| 需求清单条目 | 状态 | 商家端位置(shop 模块) | 说明 |
|---|---|---|---|
| §1 `stat/overview` 统计概览 | ✅ 已上线 | `Statistics::index`(页面 statistics.html,支持 JSON) | 返回 `{overview{pv,uv,today,days}, channels, trend(14天), top_cards, contacts{total,dirty}, days}`;卡片计数(总数/待补/收藏)在 `Relation::getStats` |
| §2.1 `card/lists` 增强 | ✅ 大部分已有 | `Card::lists` | 已支持:`search`(姓名模糊)/ `status=dirty`(待补全)/ `card_type` / `tag`(FIND_IN_SET 精确匹配)/ `favorite=1` / `follow_up=due|set`;行内已有 `tags`(逗号串)/ `view_count` / `is_dirty` / `follow_up_at`。**缺**:`tag_list` 解码、`lead_count` 聚合、`design_thumb`、`sort` 排序参数 |
| §2.2 查重合并 | ✅ 已上线 | `Card::duplicates`(页面 duplicates.html)+ `Card::merge` | 按字段(默认 mobile)分组重复行;合并行为以 shop 端实现为准 |
| §2.3 新建 / 批量 / 删除 | ✅ 已上线 | `Card::create`(JSON 单张)、`Card::batchCreate`(`cards` 数组 + 公共 `company/job_title`)、`Card::delete`(**软删** `is_delete=1`) | **缺**:create 暂不支持指定归属员工 `owner_id`(建卡默认未归属,走员工 claim);如需"建卡直派"可在 shop 侧小改 |
| §2.4 导出 | ✅ 已上线 | `Card::exportCsv`(文件流下载,遵循当前筛选)、`Card::exportVcard`(.vcf 文件流)、`Card::vcard`(单卡) | 前端直接 `window.open` / form 提交下载,无 `{file_url}` 包装 |
| §2.5 标签 | ✅ 基本已有 | `Card::setTags`(tags 逗号串,空=清空) | 无独立标签表:标签是"输入即建标"语义,标签字典=全站 tags 聚合。如需跨卡标签管理(改名/计数)需新增 tag 表,属新需求 |
| §3.1/3.2 设计读取/保存 | ✅ 已上线(schema 见 §0 铁律 3) | `Design::design`(编辑页 + 全量 JSON)、`Design::save` | 入参按 `lucky_card_design` 结构化列 + `canvas` blocks;**不是 `modules[]` 数组**。无设计时返回站点默认/空结构 |
| §3.3 模板相关 | ✅ 部分 | `CardTemplatePresets`(内置模板库)+ `Template::lists/edit/save/saveAs/delete` | "另存为模板"=`Template::saveAs`;无独立 `design/templates` JSON API(模板管理走后台页面) |
| §3.4 预览并公开 | ✅ 等价能力 | 公开页默认可访问;访问控制=`CardAccessGate`(密码/有效期,经 `Card::edit` JSON 的 `access_password` 等字段);分享配置见 §5 | 无独立 `is_public` 开关;"不公开"=设置访问密码或停用分享 |
| §3.5 素材库 | ⚠️ 部分 | 上传复用系统 `/api/upload/headimg` | 无独立素材库列表端点,需素材库属新需求 |
| §4 模板管理 | ✅ 已上线 | `Template::lists/edit/save/saveAs/delete`(页面 templates.html / template_edit.html) | 模板主键为数字 `id`;"站点默认设计"语义=`Design::design` 的站点默认行(relation_id=0),不是 template.setDefault |
| §5 分享管理 | ✅ 已上线 | `Card::createShare`(新增/更新等价)、`Card::shareDelete`、`Card::shareToggle`、`Share::getSharePageList`(分页) | 单卡 `share_list` 也在员工端 `card/detail` 返回(只读) |
| §6 名片统计 | ✅ 已上线 | `Statistics::index`(同 §1,`days` 1~365 默认 30) | `top_cards` 即全站维度;单卡维度=行内 `view_count`;按天/渠道趋势已有,单卡趋势如需属小改 |
| §7 已就绪 | ✅ | 员工 API(第 1 部分)+ PC 后台 `Contact::lists/transfer/exportCsv`、`Lead::lists/setStatus/delete/exportCsv` | 商家端人脉/线索页面已上线 |
## 2.2 需求清单里真正的新需求(如确认要做,按此立项)
1. **商家移动端(UniApp 内给商家用)**:架构前提是新增商家身份凭证通道(admin → 移动端),然后建 `/card/api/merchant/*` 命名空间,逐个映射 §2.1 表中的 shop action。**不允许**以任何形式复用 member token 或豁免行级隔离。
2. **建卡直派**(`Card::create` 支持 `owner_id` 指定归属员工):shop 侧小改,需防越权(owner_id 必须是本站会员)。
3. **跨卡标签管理**(标签字典/改名/计数):需新增标签表 + 迁移既有逗号串数据。
4. **`design_thumb` 名片设计缩略图**:canvas → 图片(后端渲染或前端截图上传),设计器联调时定。
5. **素材库**:列表/独立上传端点。
6. **单卡趋势统计**(按天 pv/uv,指定 card_id):View 模型已有按天/渠道聚合,加 card_id 维度即可。
7. **`sort` 排序参数**(newest/hot/name):`Card::lists` 加 order 白名单,小改。
## 2.3 商家端对接注意事项(前端如果直调 shop JSON action)
- JSON 请求带 `X-Requested-With: XMLHttpRequest`,返回信封与员工端一致 `{code, message, data}`。
- 列表页 fragment 请求必须带该头,否则吐整页壳。
- 页面跳转用 `ns.href()` / 模板端 `{:href_url()}`(hash 路由),直连地址会只有外壳骨架内容区空白。
- 二级菜单:非菜单页须在 `config/menu_shop.php` 以 `is_show=0` 隐藏登记,改后跑 `Menu::refreshMenu`。