更新
This commit is contained in:
@@ -0,0 +1,396 @@
|
||||
# 企业微信推广链接能力核验
|
||||
|
||||
核验日期:2026-08-31。范围:企业自建应用的获客助手、客户联系、客户欢迎语。本文是只读研究交付,没有修改应用代码,也没有调用任何企业的写接口。
|
||||
|
||||
来源均为企业微信开发者中心官方文档。网页搜索工具无法打开部分官方页面,实际通过 HTTPS 读取同一官方 URL 的公开 HTML,提取正文核验;没有以 SDK、博客或第三方镜像作为结论依据。以下标为“实现建议”的内容是本项目的工程设计,不是官方 API 自带能力。
|
||||
|
||||
## 1. 结论与能力边界
|
||||
|
||||
| 功能 | 官方能力 | 本地需要实现的部分 |
|
||||
| --- | --- | --- |
|
||||
| 获客成员范围 | `create_link` / `update_link` 的 `range.user_list`、`range.department_list` | 成员开关、有效期、星期时段、当日上限计算后写入范围 |
|
||||
| 老客户优先找原员工 | `priority_option`,且仅部分经营类目支持 | 校验经营类目能力;与排班、上限的冲突提示 |
|
||||
| 按星期、时段自动上下线 | 获客链接接口没有排班字段 | 常驻任务/定时调度重算,调用 `update_link` 覆盖范围 |
|
||||
| 备用员工 | 获客链接接口没有“主用/备用”字段 | 主用无人可接待时才把合格备用成员放入范围 |
|
||||
| 客户标签 | 读取企业标签库、对指定员工的客户 `mark_tag` | 配置标签 ID、回调后应用、幂等和失败补偿 |
|
||||
| 客户备注/描述 | `externalcontact/remark` | 模板变量展开、字符数校验、只更新明确配置的字段 |
|
||||
| 欢迎语文本/附件 | `send_welcome_msg` | 默认/渠道/关闭/分时策略选择、变量展开、素材准备、20 秒内发送 |
|
||||
| 通过 `LinkId` 定位添加客户渠道 | **普通 `add_external_contact` 文档不承诺有 `LinkId`** | 使用 `State` 映射本地推广方案;有 `LinkId` 的获客事件作补充 |
|
||||
|
||||
依据:[获客链接管理](https://developer.work.weixin.qq.com/document/path/97297)、[事件格式](https://developer.work.weixin.qq.com/document/path/92130)、[发送新客户欢迎语](https://developer.work.weixin.qq.com/document/path/92137)。
|
||||
|
||||
## 2. 获客链接 create / update
|
||||
|
||||
官方文档:[获客链接管理](https://developer.work.weixin.qq.com/document/path/97297),页面最后更新 2025-11-17。
|
||||
|
||||
### 2.1 请求
|
||||
|
||||
创建:`POST https://qyapi.weixin.qq.com/cgi-bin/externalcontact/customer_acquisition/create_link?access_token=ACCESS_TOKEN`
|
||||
|
||||
```json
|
||||
{
|
||||
"link_name": "门诊咨询推广",
|
||||
"range": {
|
||||
"user_list": ["assistant_a", "assistant_b"],
|
||||
"department_list": [2]
|
||||
},
|
||||
"skip_verify": true,
|
||||
"priority_option": {
|
||||
"priority_type": 2,
|
||||
"priority_userid_list": ["assistant_a", "assistant_b"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
更新:`POST https://qyapi.weixin.qq.com/cgi-bin/externalcontact/customer_acquisition/update_link?access_token=ACCESS_TOKEN`
|
||||
|
||||
```json
|
||||
{
|
||||
"link_id": "LINK_ID",
|
||||
"range": {
|
||||
"user_list": ["assistant_b"],
|
||||
"department_list": []
|
||||
},
|
||||
"skip_verify": true
|
||||
}
|
||||
```
|
||||
|
||||
示例中的 `priority_option` 仅在企业确实具备该能力且配置了好友优先策略时提交;它不是排班、权重或备用配置。`skip_verify` 应始终使用本地已保存的值,不能在范围同步时意外改变免验证设置。
|
||||
|
||||
### 2.2 已核实限制
|
||||
|
||||
- 创建 `link_name` 必填,更新可选,最长 **30 个字符**。
|
||||
- `range.user_list` 最多 **500 人**;部门覆盖人数也有上限,最终 `range` 覆盖总人数不得超过 **500 人**。
|
||||
- 创建时 `user_list` 与 `department_list` **不可同时为空**。
|
||||
- 更新的 `range` 是**覆盖更新**,不是增量加入或删除。若目的是精确排班,应使用明确的 `user_list`,并清掉不受排班控制的部门范围。
|
||||
- `skip_verify` 缺省值为 `true`。
|
||||
- `priority_type=1`:在全企业内优先分配给已有好友关系的成员。
|
||||
- `priority_type=2`:在 `priority_userid_list` 中优先分配给已有好友关系的成员;创建时该列表必填,最多 **1000 人**。
|
||||
- `priority_option` 也是覆盖更新;仅支持“客户与成员关系绑定”的经营类目可用,需在管理端“高级功能 → 获客助手”确认。
|
||||
- `range` / `priority_userid_list` 受应用可见范围或客户可建联成员范围约束。
|
||||
- 还有 `mark_source`,缺省 `true`,但**只对“营销获客”应用生效**;本项目自建应用不要将其误当作通用渠道标记开关。
|
||||
- 查询、更新、删除的 `link_id` 必须属于当前应用创建的链接。
|
||||
|
||||
### 2.3 权限和不确定点
|
||||
|
||||
官方明确要求使用配置到客户联系“可调用应用”列表中的自建应用 secret 获取的 token;获客链接 API **不支持客户联系系统应用调用**。不能因为客户详情接口过去可用,就推断同一 secret 一定支持获客链接。
|
||||
|
||||
文档没有明确以下行为,不能自行编造 payload:
|
||||
|
||||
1. `priority_type=0` 的含义以及取消已存在 `priority_option` 的正确方式。官方只列出 `1`、`2`;不要宣称传 `0`、空对象或省略字段能清除既有设置。
|
||||
2. 更新链接时提交全空 `range` 是否有特殊停用语义。创建明确不允许空范围,本项目应继续把“至少一名可接待成员”作为有效配置约束。
|
||||
3. `update_link` 的传播延迟以及对已经打开的成员页/已经发起的好友请求是否有追溯影响。
|
||||
4. 好友优先列表与排班范围交叉时的完整路由细节。`priority_type=1` 涵盖全企业,不能承诺严格服从本地排班/上限;需提供冲突说明并做真实企业联调。
|
||||
|
||||
## 3. 星期排班、备用员工与上限
|
||||
|
||||
以下为根据官方范围更新能力提出的实现建议,并非独立的企微“上下线 API”。
|
||||
|
||||
1. 将星期、开始/结束时间、时区、是否启用、有效期、成员角色(主用/备用)存到本地;统一用 `Asia/Shanghai`,时间区间采用左闭右开 `[start, end)`,跨午夜时段拆成两天或显式处理前一日。
|
||||
2. 先计算符合开关、有效期、班次、业务上限的主用成员;主用集合非空就只发送主用集合。主用全部不可用时才选择合格备用成员;备用成员不要日常混在同一 `range` 中,否则企微会把他们当普通候选成员。
|
||||
3. 保存后立即同步;分钟任务持续重算;在时段边界可以额外立即同步。数据库事务只认领任务/保存状态,网络请求放在事务之外。
|
||||
4. `range` 有变化才调用 `update_link`,保留版本号、租约、重试和最后成功应用范围;调用成功后用 `customer_acquisition/get` 回读核验。
|
||||
5. 企微自动跳过“暂时无法添加客户”的异常账号;若整条链接所有成员异常,会推送 `customer_acquisition/link_unavailable`。这是账号异常路由能力,**不代表按本地班次自动启用备用员工**。可以据此触发告警或经过本地规则校验的备用范围切换。[获客助手事件通知](https://developer.work.weixin.qq.com/document/path/97299)
|
||||
6. 若主用和备用都为空,不要把“本地已下线”展示成“官方链接已停用”。保留明确阻塞状态、错误提示,并由业务选择停止曝光/受控入口暂停;不能在保存排班时偷偷删除官方链接。
|
||||
7. 回调记账后更新范围是事后控制;有网络延迟和并发好友申请,不能声称“日上限绝不超发”。UI 应说明本地统计上限与官方建联并发之间的边界。
|
||||
|
||||
现有 `QywxPromotionRangeSyncService` 已有任务租约、版本号、回读范围、分钟重算和空范围阻塞,适合作为扩展点;不必另造一套链路。现有调度代码通过 `State=zyt_pool:{id}` 记账,扩展渠道参数时应保持兼容。
|
||||
|
||||
## 4. 企业标签、客户备注、客户详情
|
||||
|
||||
### 4.1 获取企业客户标签
|
||||
|
||||
文档:[管理企业标签](https://developer.work.weixin.qq.com/document/path/92117),最后更新 2023-12-01。
|
||||
|
||||
`POST https://qyapi.weixin.qq.com/cgi-bin/externalcontact/get_corp_tag_list?access_token=ACCESS_TOKEN`
|
||||
|
||||
```json
|
||||
{}
|
||||
```
|
||||
|
||||
`tag_id`、`group_id` 都不传即返回所有标签;如果需要按组获取:
|
||||
|
||||
```json
|
||||
{"group_id":["GROUP_ID"]}
|
||||
```
|
||||
|
||||
同时传两个筛选条件时以 `group_id` 为准,忽略 `tag_id`。返回 `tag_group[]`,组内为 `tag[]`,标签使用 `id`、`name`,有删除标记时应过滤。应用仅可编辑/删除自己创建的标签,但读取标签库和给客户打已有企业标签是另一个权限层次,不能据此把所有其他来源标签都从选择器隐藏。
|
||||
|
||||
自建应用需被列入客户联系可调用应用;企业标签库最多 10000 个标签。页面没有给 `get_corp_tag_list` 列出分页参数,不要自行增加 cursor 分页。
|
||||
|
||||
### 4.1.1 自定义企业客户标签(2026-08-31 补充核验)
|
||||
|
||||
官方接口:`POST https://qyapi.weixin.qq.com/cgi-bin/externalcontact/add_corp_tag?access_token=ACCESS_TOKEN`。来源仍为 [管理企业标签](https://developer.work.weixin.qq.com/document/path/92117) 中“添加企业客户标签”一节;本次通过 HTTPS 读取官方页面公开 HTML 核实,没有调用企业 API。
|
||||
|
||||
已有“推广渠道”分组时:
|
||||
|
||||
```json
|
||||
{"group_id":"EXISTING_GROUP_ID","tag":[{"name":"直播推广"}]}
|
||||
```
|
||||
|
||||
没有该分组时,一次请求创建分组及标签:
|
||||
|
||||
```json
|
||||
{"group_name":"推广渠道","tag":[{"name":"直播推广"}]}
|
||||
```
|
||||
|
||||
- `tag.name` 必填,最长30个字符;`group_name` 同样最长30个字符,均不是字节上限。
|
||||
- 指定已有分组用 `group_id`。填写该字段后,`group_name` 和标签组 `order` 被忽略。
|
||||
- 通过 `group_name` 创建分组时,如果分组名称已存在,会在已有分组下新增标签;不能创建空分组。
|
||||
- 同组标签不能重名;单次传入多个同名标签只创建一个。官方没有承诺“名称已存在”的每种错误码及返回列表形态,因此不能靠猜测错误码返回本地假ID。
|
||||
- 返回值包含 `tag_group.group_id/group_name/tag[]`,标签真实ID为 `tag[].id`;企业标签总数上限10000。
|
||||
- `agentid` 仅旧第三方多应用套件需要,本项目自建应用不提交。
|
||||
|
||||
本项目新增 `POST firstvisit.wecomPromotion/createTag`,复用页面权限,只接受 `{name}` 并固定使用“推广渠道”分组。名称须非空、最多30字符,不得包含控制/不可见格式字符。先查询同组同名并复用,创建失败或响应无法确认时只读回确认;无法确认则提示刷新列表核对,不再次发送创建请求。每个推广方案保存的 `tag_ids` 仍是数组,但最多一项,开启时必须一项;读取旧多选数据不截断,重新保存时要求用户明确选一个。
|
||||
|
||||
### 4.2 给指定员工的客户打标签
|
||||
|
||||
文档:[编辑客户企业标签](https://developer.work.weixin.qq.com/document/path/92118),最后更新 2023-12-01。
|
||||
|
||||
`POST https://qyapi.weixin.qq.com/cgi-bin/externalcontact/mark_tag?access_token=ACCESS_TOKEN`
|
||||
|
||||
```json
|
||||
{
|
||||
"userid": "assistant_a",
|
||||
"external_userid": "EXTERNAL_USER_ID",
|
||||
"add_tag": ["ENTERPRISE_TAG_ID_A", "ENTERPRISE_TAG_ID_B"]
|
||||
}
|
||||
```
|
||||
|
||||
- 可选 `remove_tag` 用于明确移除;`add_tag` 与 `remove_tag` 不能同时为空。
|
||||
- 客户必须已是该 `userid` 的外部联系人;操作面向**员工与客户的关系**,不是企业下无差别更新所有员工视角。
|
||||
- 每个成员对同一客户最多 3000 个标签;同一标签组可以选多个标签。
|
||||
- 应用只能操作可见范围内成员的客户标签;规则组标签要求同一应用创建该规则组,且成员在其管理范围。
|
||||
- 渠道自动标签建议只增添已配置标签,不能为了“同步一致”删除员工手动添加的其他标签。
|
||||
|
||||
### 4.3 客户备注和描述
|
||||
|
||||
文档:[修改客户备注信息](https://developer.work.weixin.qq.com/document/path/92115),最后更新 2025-11-17。
|
||||
|
||||
`POST https://qyapi.weixin.qq.com/cgi-bin/externalcontact/remark?access_token=ACCESS_TOKEN`
|
||||
|
||||
```json
|
||||
{
|
||||
"userid": "assistant_a",
|
||||
"external_userid": "EXTERNAL_USER_ID",
|
||||
"remark": "渠道A-李女士",
|
||||
"description": "来自门诊咨询推广"
|
||||
}
|
||||
```
|
||||
|
||||
- `remark` 最多 **20 个字符**;`description` 最多 **150 个字符**,均是字符数,不是欢迎语的字节数。
|
||||
- 可选 `remark_company`(最多20字符,仅微信客户有效)、`remark_mobiles`、`remark_pic_mediaid`。
|
||||
- 不可全部为空;仅写本次用户明确启用的字段,避免覆盖人工备注/电话。
|
||||
- 电话数组会覆盖旧值;官方清除全部电话的特殊说明为给 `remark_mobiles` 填一个空字符串。当前推广需求无须触及这一功能。
|
||||
- 修改权限限制在应用可见范围内成员添加的客户。
|
||||
- 文档未对清空 `remark` / `description` 的空字符串语义作同样明确说明,不要将“未配置”自动转换成清空远端。
|
||||
|
||||
### 4.4 获取客户详情、名字与渠道
|
||||
|
||||
文档:[获取客户详情](https://developer.work.weixin.qq.com/document/path/92114),最后更新 2025-12-19。
|
||||
|
||||
`GET https://qyapi.weixin.qq.com/cgi-bin/externalcontact/get?access_token=ACCESS_TOKEN&external_userid=EXTERNAL_USER_ID`
|
||||
|
||||
重要字段:
|
||||
|
||||
- `external_contact.name`:微信客户为微信昵称;企微联系人为其对外别名或实名。
|
||||
- `follow_user[]`:每个跟进人的 `userid`、`remark`、`description`、`tags`、`state`、`add_way`。
|
||||
- `follow_user.add_way=16` 表示获客链接添加;`state` 是本地可自定义渠道,两者不能混用。
|
||||
- 读取/应用关系级备注和标签时,要匹配回调实际 `UserID`,不能直接取第一个 `follow_user`。
|
||||
- 跟进人超过500时,使用返回的 `next_cursor` 分页;只保证获取应用有可见权限的成员信息。
|
||||
- 官方注明自 2023-12-01 起不再支持新场景使用系统应用 secret,存量企业暂不受影响。项目应以已列入可调用列表的自建应用作为正式接入方式。
|
||||
|
||||
## 5. 获客渠道与回调字段
|
||||
|
||||
### 5.1 customer_channel 与 State
|
||||
|
||||
将渠道标识放在已创建的官方链接 URL 查询参数中,而不是写进 `create_link` 的自造 `state` 字段:
|
||||
|
||||
```text
|
||||
https://work.weixin.qq.com/ca/LINK_PATH?customer_channel=zyt_pool%3A123
|
||||
```
|
||||
|
||||
如果原链接已有查询串,应以 URL 解析器安全合并;不能重复叠加 `customer_channel`。自定义字符串最长 **64 字节**,超过会截断,因此应在保存/生成时拒绝超长值,避免两个渠道被截断后碰撞。返回的客户列表与客户详情 `state` 对应这个字符串。[获取由获客链接添加的客户信息](https://developer.work.weixin.qq.com/document/path/97298)
|
||||
|
||||
建议继续采用无个人信息的短、不透明标识;若扩展为独立渠道 ID,应新增明确映射并保持 `zyt_pool:{id}` 老链接兼容。
|
||||
|
||||
### 5.2 添加客户事件
|
||||
|
||||
官方文档:[事件格式](https://developer.work.weixin.qq.com/document/path/92130)。以下是接收 XML 解密后用于本地处理的字段示意(**不是 POST API 请求体**):
|
||||
|
||||
```json
|
||||
{
|
||||
"Event": "change_external_contact",
|
||||
"ChangeType": "add_external_contact",
|
||||
"UserID": "assistant_a",
|
||||
"ExternalUserID": "EXTERNAL_USER_ID",
|
||||
"State": "zyt_pool:123",
|
||||
"WelcomeCode": "WELCOME_CODE",
|
||||
"CreateTime": 1788141600
|
||||
}
|
||||
```
|
||||
|
||||
本事件的官方字段表**没有 `LinkId`**。应以 `State` 识别渠道;不能在本事件没有 `LinkId` 时放弃欢迎语/标签,也不能把后来的首次聊天事件当成欢迎语触发条件。
|
||||
|
||||
`WelcomeCode` 不是必然存在:客户与成员已开始聊天、已经在半客户事件中发过欢迎语等情况不会继续给 code;企业微信商务伙伴自动递名片,也不回调 code。
|
||||
|
||||
`add_half_external_contact` 同样可能带 `State` 与 `WelcomeCode`。若需要支持免验证添加全流程,应让欢迎语处理器在有效 code 出现时就处理,不应被“半客户不入客户表”的早返回吞掉;但打标签和修改备注可以等关系确认后做,不能为等客户详情而消耗欢迎语窗口。
|
||||
|
||||
### 5.3 LinkId 出现在哪些事件
|
||||
|
||||
获客助手专用事件为 `Event=customer_acquisition`。例如 `link_unavailable`、`delete_link`、`open_profile`、`friend_request`、`customer_start_chat`、`message_from_customer` 等有 `LinkId`;其中 `open_profile` / `friend_request` 有 `State`,但**没有可用于发送新客户欢迎语的 `WelcomeCode`**。[获客助手事件通知](https://developer.work.weixin.qq.com/document/path/97299),最后更新 2026-07-22。
|
||||
|
||||
`message_from_customer` 的 `UserID`、`ExternalUserID`、`ChatSeq` 自 2024-12-19 起不再保证回调,须使用 30 分钟内有效的 `ChatKey` 查询。当前项目已接入 ChatKey 处理,应保留这条补偿链路,不能用旧示例假定字段永远齐全。
|
||||
|
||||
### 5.4 接收要求
|
||||
|
||||
配置了客户联系可调用应用、API 接收消息,且勾选“外部联系人变更回调”,才能收到可见范围内成员客户事件。[回调通知概述](https://developer.work.weixin.qq.com/document/path/92129)
|
||||
|
||||
企业微信要求回调在 **5 秒内响应**;连接失败或超时时会重试,官方说明总共重试三次,并明确回调并非100%可靠。接收端应验签解密、快速持久化并应答,业务由立即运行的工作进程处理;需要额外对账。[回调配置](https://developer.work.weixin.qq.com/document/path/90930)
|
||||
|
||||
## 6. 发送新客户欢迎语及附件
|
||||
|
||||
官方文档:[发送新客户欢迎语](https://developer.work.weixin.qq.com/document/path/92137),最后更新 2025-11-17。
|
||||
|
||||
`POST https://qyapi.weixin.qq.com/cgi-bin/externalcontact/send_welcome_msg?access_token=ACCESS_TOKEN`
|
||||
|
||||
```json
|
||||
{
|
||||
"welcome_code": "WELCOME_CODE",
|
||||
"text": {"content": "李女士您好,我是小张医助。"},
|
||||
"attachments": [
|
||||
{
|
||||
"msgtype": "image",
|
||||
"image": {"media_id": "IMAGE_MEDIA_ID"}
|
||||
},
|
||||
{
|
||||
"msgtype": "link",
|
||||
"link": {
|
||||
"title": "就诊指南",
|
||||
"picurl": "https://example.com/guide-cover.jpg",
|
||||
"desc": "查看就诊须知",
|
||||
"url": "https://example.com/guide"
|
||||
}
|
||||
},
|
||||
{
|
||||
"msgtype": "miniprogram",
|
||||
"miniprogram": {
|
||||
"title": "预约入口",
|
||||
"pic_media_id": "COVER_MEDIA_ID",
|
||||
"appid": "ASSOCIATED_MINIPROGRAM_APPID",
|
||||
"page": "/pages/appointment/index"
|
||||
}
|
||||
},
|
||||
{"msgtype": "video", "video": {"media_id": "VIDEO_MEDIA_ID"}},
|
||||
{"msgtype": "file", "file": {"media_id": "FILE_MEDIA_ID"}}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
以上五类均有官方示例/对应字段。官方参数表的 `attachments.msgtype` 行漏列了 `file`,但页面说明、完整示例和 `file.media_id` 行都明确支持文件;这是文档内部不一致,应记录而不是误删文件支持。
|
||||
|
||||
### 6.1 时效、互斥、错误处理
|
||||
|
||||
- 收到相关事件后 **20 秒内**调用,`welcome_code` 有效期20秒,只能成功使用一次;不能靠分钟级任务补发过期欢迎语。
|
||||
- 管理端已为成员配置可用欢迎语时,不会返回 `welcome_code`。本地“关闭渠道欢迎语”只能控制**本应用是否发送**,无法压制企业微信管理端或其他应用自己发的欢迎语。
|
||||
- 长期未登录企业微信的成员不能发送欢迎语。
|
||||
- 已成功下发后再发返回 `41051`,无需重试。
|
||||
- 多应用竞争发送时,后来的应用可能返回 `41096`,表示正在由其他应用分发,不等于已发成功;官方允许重试,但仍受20秒窗口限制。收到 `41051` 则停止。
|
||||
- 自建应用须配置到可调用应用列表;成员须在其可见范围。获客链接可用不等于欢迎语权限和回调配置必然正确。
|
||||
|
||||
### 6.2 内容限制
|
||||
|
||||
| 字段 | 限制 |
|
||||
| --- | --- |
|
||||
| `text.content` | 最长4000字节(UTF-8 字节计算) |
|
||||
| `attachments` | 最多9个;可以同时发文本和附件 |
|
||||
| `text` / `attachments` | 不可同时为空 |
|
||||
| `link.title` | 必填,最长128字节 |
|
||||
| `link.desc` | 可选,最长512字节 |
|
||||
| `link.url` | 必填 |
|
||||
| `link.picurl` | 可选封面 URL;注意字段拼写不是 `pic_url` |
|
||||
| `image.media_id` / `image.pic_url` | 至少一个;都传时 `media_id` 优先 |
|
||||
| `image.pic_url` | 仅可用官方“上传图片”接口得到的 URL,不能直接塞任意本地/CDN图片地址 |
|
||||
| `miniprogram.title` | 必填,最长64字节 |
|
||||
| `miniprogram.pic_media_id` | 必填,封面建议520×416 |
|
||||
| `miniprogram.appid` | 必须是关联到企业的小程序 |
|
||||
| `miniprogram.page` | 必填的小程序页面路径 |
|
||||
| `video.media_id` / `file.media_id` | 对应类型必填 |
|
||||
|
||||
`msgtype` 必须与同项内的内容对象一致。不能把多个附件拼成旧版顶层 `image` / `link` / `miniprogram` 字段。
|
||||
|
||||
### 6.3 素材必须提前准备
|
||||
|
||||
临时素材:`POST https://qyapi.weixin.qq.com/cgi-bin/media/upload?access_token=ACCESS_TOKEN&type=TYPE`,multipart 文件字段名为 `media`。`media_id` 有效 **3天**,同一企业内应用可共享。文件需大于5字节;图片 JPG/PNG ≤10MB,视频 MP4 ≤10MB,普通文件 ≤20MB。[上传临时素材](https://developer.work.weixin.qq.com/document/path/90253)
|
||||
|
||||
永久图片 URL:`POST https://qyapi.weixin.qq.com/cgi-bin/media/uploadimg?access_token=ACCESS_TOKEN`,得到可用于欢迎语的 URL。图片大小5B~2MB,每企业每日最多1000张、每月最多3000张;返回 URL 永久有效,但用途受企微环境限制。[上传图片](https://developer.work.weixin.qq.com/document/path/90256)
|
||||
|
||||
实现建议:配置欢迎语时保存源文件和上传状态,提前转成企微素材并按 hash 去重;临时素材在过期前刷新。发送时不应临时下载大文件再上传,否则20秒窗口很容易失效。失效附件应有明确错误/降级记录,不能假装发送完整成功。
|
||||
|
||||
## 7. 模板变量及默认/渠道/关闭/分时策略
|
||||
|
||||
以下是服务端职责,官方 `send_welcome_msg` 不会替换 `{客户昵称}`、`{员工昵称}` 等模板内容,也没有这些模式参数。
|
||||
|
||||
### 7.1 变量
|
||||
|
||||
- `{客户昵称}`:取 `external_contact.name`,而非把某个员工的 `remark` 当客户原始昵称。缓存缺失时可做有严格超时预算的详情查询;无法取得时使用事先定义的“您”等兜底,不要把未展开的占位符发送出去。
|
||||
- `{员工昵称}`:首先明确产品含义。可用本地配置的对外称呼,或企业成员 `name`;若要 `alias`,需明确优先级。`GET /cgi-bin/user/get?userid=USER_ID` 返回 `name`/`alias` 受应用类型和可见权限影响,第三方并不能普遍拿到姓名/别名。[读取成员](https://developer.work.weixin.qq.com/document/path/90196)
|
||||
- 只支持白名单变量,不运行表达式、不执行任意模板代码;API 发出的最终文本必须已完成替换。
|
||||
- 保存时校验模板结构,发送时在变量展开后再校验长度:欢迎语 UTF-8 字节上限,客户备注20字符、描述150字符。对模板变量导致的超限采用明确的截断/拒绝策略并记录,不能依赖企微静默截断。
|
||||
|
||||
### 7.2 建议的确定性策略
|
||||
|
||||
每条本地推广渠道保存 `welcome_mode = inherit | custom | disabled | scheduled`,并定义唯一优先级:
|
||||
|
||||
1. 能定位的渠道设为 `disabled` → 不发送本应用欢迎语,**不可回退默认欢迎语**。
|
||||
2. 渠道为 `custom` → 使用渠道内容。
|
||||
3. 渠道为 `scheduled` → 按事件发生时间、北京时间和星期选择规则;规则重叠要拒绝或使用明确排序。无匹配时使用该配置明确指定的兜底(默认/固定内容/不发),不能凭实现猜测。
|
||||
4. 渠道为 `inherit`,或业务明确允许未知渠道走默认 → 使用默认欢迎语。
|
||||
|
||||
分时表示“添加客户时选哪一段内容”,不是“把欢迎语延迟到某个时段再发”;延迟通常会越过20秒有效期。配置应和事件一起保存版本或内容快照,避免工作进程稍后读到另一版规则。
|
||||
|
||||
### 7.3 处理链建议
|
||||
|
||||
```text
|
||||
验签解密 → 最小事件幂等落库 → 立即工作进程领取
|
||||
├→ 欢迎语:选策略/展开变量 → 20秒内send_welcome_msg
|
||||
├→ 成员记账/范围同步(独立,可补偿)
|
||||
└→ 关系确认后标签/备注/详情同步(独立,可补偿)
|
||||
```
|
||||
|
||||
回调应答不能等全部远端请求完成;欢迎语必须使用立即消费的队列/工作进程,不能复用分钟任务。若部署没有立即消费能力,应明确补齐部署要求,不能只保存一条“待发送”记录就宣称欢迎语已经打通。
|
||||
|
||||
欢迎语状态建议包含 `pending/processing/sent/skipped/expired/failed`,并保存事件时间、首次接收时间、处理耗时、策略版本、结果码和跳过原因。幂等至少考虑企业、实际员工、客户、事件及 welcome_code;code 本身仅短期保留/加密,日志只记录摘要,不能泄露 token/code。`add_half` / `add` 以及回调重试不应造成重复发送。
|
||||
|
||||
标签、备注的结果单独记录,失败不能撤销已成功的欢迎语;人工重试仅重试失败动作,不重放全部新增客户流程。先读取现有关系可避免覆盖人工信息,但不得挡在欢迎语关键路径前。
|
||||
|
||||
## 8. 对当前代码的落地提示
|
||||
|
||||
只读查看了以下文件:
|
||||
|
||||
- `server/app/common/service/qywx/QywxCustomerAcquisitionApiService.php`
|
||||
- `server/app/api/controller/QywxExternalContactCallbackController.php`
|
||||
- `server/app/common/service/qywx/QywxPromotionRangeSyncService.php`
|
||||
- `server/app/common/service/qywx/QywxPromotionMemberSchedulerService.php`
|
||||
|
||||
观察及建议:
|
||||
|
||||
1. API service 已有 create/update/get/list 和 token 无效单次刷新;它本身没有标签、备注、欢迎语或素材封装。扩展时应区分权限、超时预算和返回错误,而非把前端配置原样塞给 `create_link`。
|
||||
2. 回调已提取 `State`、`WelcomeCode`,但目前欢迎语只记为是否存在;没有发送欢迎语。新增服务要取得真实 code,而不是只接收布尔值。
|
||||
3. `add_half_external_contact` 目前早返回;如果要支持其欢迎语,须在这个返回之前处理有效 code,客户落库的原有跳过行为可保留。
|
||||
4. 目前 `add_external_contact` 会同步触发范围 API,再拉客户详情。欢迎语不应附加在这些操作之后;其超时窗口比范围/资料同步更严格。
|
||||
5. `State` 当前正则为 `^zyt_pool:(\d+)$`。如果新前端生成别的 State 格式而不改兼容解析,现有统计和范围调度会失效。
|
||||
6. 当前范围同步只发送 `link_id/link_name/range/skip_verify`;若新增好友优先策略,要确认创建、编辑、后台定时同步、远端回读都不会意外覆盖/遗失该设置。
|
||||
7. 若排班/备用规则改变了本地“可用成员”判断,应只保留一个统一计算器供页面预览、保存校验、分钟重算和回调后同步共用,避免页面与官方实际范围不一致。
|
||||
|
||||
## 9. 联调时必须验证的项目
|
||||
|
||||
- 当前企业的自建应用已具备获客助手、客户详情、标签、备注、欢迎语权限及成员可见范围;新增这些功能不能仅沿用“获客链接列表成功”的权限检测结果。
|
||||
- 管理端欢迎语是否已关闭/让位,本应用是否实际收到含 `WelcomeCode` 的回调。
|
||||
- `priority_option` 取消语义、旧好友优先与排班范围的实际交互,官方未给明文保证的部分应以联调记录为准。
|
||||
- `range` 更新到官方生效的实际时延;全员下线/备用不可用时既有链接仍可能维持旧范围,UI需如实显示同步阻塞。
|
||||
- 20秒期限下“冷 token、冷客户缓存、素材过期、队列堆积”的处理;5秒回调应答要求。
|
||||
- Unicode昵称展开后的UTF-8字节限制、备注字符限制、小程序关联/页面可用性、每类附件真实接收效果。
|
||||
- 半客户转正式客户、重复回调、多应用竞争、人工欢迎语已发送、成员长期未登录等情况下,不把应跳过/过期误报成普通系统故障。
|
||||
|
||||
上述研究未通过真实企业写接口验收;所有不确定行为已显式列出,不能将研究示例当作企业权限或下发成功证明。
|
||||
@@ -0,0 +1,93 @@
|
||||
# 企业微信推广自动化部署与验收
|
||||
|
||||
此实现覆盖推广渠道欢迎语、企业标签、客户备注/描述及原客户同步补偿。它不会覆盖企业微信后台欢迎语设置,也不意味着企业已开通接口权限。官方能力及限制见 [核验报告](research/wecom-promotion-api-capabilities.md)。
|
||||
|
||||
## 部署顺序
|
||||
|
||||
1. 先执行 `server/sql/1.9.20260831/add_wecom_promotion_automation.sql`。脚本仅使用 `CREATE TABLE IF NOT EXISTS`,默认表前缀 `zyt_`;实际前缀不同须由部署人员调整。本开发任务没有执行业务数据库迁移。
|
||||
2. 部署 PHP 代码。为 PHP-FPM、CLI worker 使用同一项目目录 `server/runtime/qywx_promotion_private/`,授予应用运行用户读写权限。目录必须位于 Web 根目录之外,禁止静态文件映射;保留源文件,不能随意清理该目录。
|
||||
3. 配置客户联系“可调用应用”,优先使用创建获客链接的同一自建应用。代码默认使用 `qywx_customer_acquisition.secret`,无该值才使用 `pay.wechat_work.customer_contact_secret`。需要明确覆盖时设置 `WECHAT_WORK_PROMOTION_CONTACT_SECRET`。不会使用 `external_pay_secret`。新接入不要依赖已受官方限制的客户联系系统应用 Secret。检查应用可见范围、可信 IP、外部联系人变更事件订阅;欢迎语回调必须来自可调用应用的相应配置。
|
||||
4. 建议设置至少32字符的随机 `WECHAT_WORK_PROMOTION_ENCRYPTION_KEY`,所有 API/CLI 节点保持一致。未设置时会自动在项目私有目录生成 `welcome.key`(0600)。多节点使用共享源文件目录和一致密钥;更换密钥前先处理/过期并清空待处理欢迎码。
|
||||
5. 在启用渠道欢迎语之前,启动并监控下面的常驻 worker,安装分钟补偿和素材预热。仅配置分钟任务不足以发送欢迎语。
|
||||
|
||||
若旧版脚本在 `welcome_cipher` 字段的 `COMMENT=` 处报 MySQL 1064,请重新打开已修正的 SQL 文件并完整重跑。字段注释必须使用 `COMMENT '内容'`(不带等号);表级的 `COMMENT='内容'` 是合法语法,无须修改。脚本中的四张表都使用 `CREATE TABLE IF NOT EXISTS`,已成功创建的表会保留,不需要删表。四张表全部创建成功后再启动 worker。
|
||||
|
||||
## 必须运行的进程
|
||||
|
||||
欢迎码只有20秒有效。验签回调中只做数据库和加密操作,并及时返回;欢迎语由独立常驻进程秒级消费,不能等待普通客户同步或临时素材上传。
|
||||
|
||||
```sh
|
||||
cd /path/to/server
|
||||
php think qywx:work-promotion-automation
|
||||
```
|
||||
|
||||
使用 Supervisor 或 systemd 保持常驻、自动重启。建议同一数据库启动2个欢迎语worker,以免单个HTTP请求阻塞其他事件;任务租约保证同一任务不重复领取。高并发须按实际20秒延迟指标增加进程。`--once` 仅用于受控诊断/部署测试,不能替代守护进程。
|
||||
|
||||
例如 Supervisor 配置(路径和用户替换为实际值):
|
||||
|
||||
```ini
|
||||
[program:qywx-promotion-welcome]
|
||||
command=/usr/bin/php /path/to/server/think qywx:work-promotion-automation
|
||||
directory=/path/to/server
|
||||
numprocs=2
|
||||
process_name=%(program_name)s_%(process_num)02d
|
||||
user=www-data
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stopasgroup=true
|
||||
killasgroup=true
|
||||
stopwaitsecs=35
|
||||
stdout_logfile=/var/log/qywx-promotion-welcome.log
|
||||
stderr_logfile=/var/log/qywx-promotion-welcome-error.log
|
||||
```
|
||||
|
||||
每分钟运行补偿,每5分钟预热素材。以下为部署样例,不代表本任务已经安装这些定时任务:
|
||||
|
||||
```cron
|
||||
* * * * * cd /path/to/server && /usr/bin/flock -n /tmp/qywx-promotion-retry.lock /usr/bin/php think qywx:retry-promotion-automation
|
||||
*/5 * * * * cd /path/to/server && /usr/bin/flock -n /tmp/qywx-promotion-media.lock /usr/bin/php think qywx:refresh-promotion-media
|
||||
```
|
||||
|
||||
现有 `qywx:sync-promotion-ranges`、`qywx:retry-customer-acquisition-events` 也须保留。新推广配置事件的标签、备注、描述、成员记账、范围更新、客户同步由补偿任务执行;处理延迟通常不超过一分钟,繁忙时可能更长。不要把这些慢速动作塞进欢迎语worker。方案日上限是回调后的统计控制,不是企微并发建联的硬性上限。
|
||||
|
||||
## 配置与素材契约
|
||||
|
||||
- `QywxPromotionContactApiService::tagOptions(): array` → `{tag_groups:[{group_id,group_name,tag:[{id,name}]}]}`。
|
||||
- 方案客户标签为单选;接口仍使用 `tag_ids: []/[id]`,服务端拒绝多个标签。旧多选配置会显示重新选择提示,不自动截断。自定义标签通过 `POST firstvisit.wecomPromotion/createTag`(`{name}`)调用企微 `externalcontact/add_corp_tag`,固定保存到“推广渠道”分组,名称最多30字符;同组同名复用,创建成功后自动选中。创建立即写入企微标签库,取消方案编辑不会删除该标签;本次调整不新增数据库迁移。
|
||||
- `QywxPromotionMediaService::upload($file, string $type, int $adminId): array` → `{asset_id,name,type}`。仅接受 ThinkPHP 已验证的 `UploadedFile`;不接受服务器路径或网络下载地址。上传时立即预热临时素材。
|
||||
- `validateConfig(array $config, int $adminId, array $existingConfig = []): array` → 保留其他配置字段、规范化附件后的完整配置。调用方必须先校验方案编辑权;第三参数只传数据库读取的旧配置,不能传用户提供的“白名单”。其他管理员只能保留已获授权的旧方案资产,不能新引入别人的资产。
|
||||
- `validateAttachments(array $attachments, int $adminId, array $allowedAssetIds = []): array` 为底层契约;不要直接向HTTP客户端暴露第三参数。
|
||||
- 素材 ID 是随机48位十六进制字符串。图片 JPG/PNG≤10MB、视频MP4≤10MB、文件≤20MB,且大于5字节。普通文件限定PDF、Office、文本、CSV、ZIP、JPG/PNG、MP4;拒绝可执行文件、HTML/SVG等格式。后端使用实际MIME和扩展名,不信任浏览器Content-Type。
|
||||
- 附件支持 `image/video/file.{asset_id}`、`miniprogram.{title,appid,page,pic_asset_id}`、`link.{title,url,desc,picurl?}`。不接受前端任意 `media_id`。本实现不开放 `image.pic_url`:官方只支持uploadimg生成的URL,普通CDN地址不能替代。
|
||||
- 欢迎语文本最多4000 UTF-8字节(产品配置还可额外限制1200字符),最多9附件;链接标题128字节、描述512字节,小程序标题64字节。小程序需已关联企业;本地无法代替企微验证关联与页面可达性。
|
||||
- 源文件与媒体记录持久保留;缓存media_id有效3天,在到期前1小时进入刷新候选。发送只使用剩余至少5分钟的缓存。过期素材不会在欢迎语关键路径上传,不会静默丢掉附件只发文字;会记录准备失败,直至20秒期限结束。
|
||||
- 临时素材与凭证指纹绑定。更换企业/应用Secret后,先运行 `qywx:refresh-promotion-media` 并检查失败数,再恢复渠道曝光。
|
||||
|
||||
## 回调、幂等与失败策略
|
||||
|
||||
- 入口仅在 EasyWeChat 已验签/解密的 `change_external_contact` listener 中调用 `enqueueVerifiedEvent()`;不得把此服务直接作为未验签HTTP接口。
|
||||
- 优先用 `State=zyt_pool:{id}`,无State时才使用事件实际存在的 `LinkId/LinkID`。必须存在正常的推广方案、非删除成员关系、官方有效链接、新配置记录。仅有可猜测的State不是授权。普通客户事件、无新配置旧方案、迁移尚未安装时保留旧同步路径;不会让所有客户突然依赖新worker。
|
||||
- 正式客户add可执行全部动作;半客户add_half只处理欢迎语,不提前打标签、改备注或落正式客户表。事件重复通过唯一事件键去重;同一WelcomeCode在half/add之间通过唯一摘要索引只分配一次消费权。
|
||||
- 队列持久化失败会抛专用异常,listener不吞掉,HTTP返回500供企微重投。成功入队后的业务失败由持久任务补偿,不影响回调应答。
|
||||
- 每条任务保存配置快照;分时欢迎语按事件时间和Asia/Shanghai选择,未命中用基础欢迎语。`default`和`none`均不发送本系统欢迎语,不能抑制企业微信管理端或其他应用发送。
|
||||
- 欢迎码以AES-256-GCM短存;动作进入sent/skipped/expired/uncertain/failed后清除密文。既有客户事件raw字段现在移除WelcomeCode;审计中无明文code/token、上游请求或Guzzle异常链。历史已有raw数据需要另外评估清理,本次未改历史记录。
|
||||
- 发送前先持久化running。网络超时、无法解析响应、HTTP失败,或进程在发送后记录成功前崩溃,均记为uncertain,清理密文并停止自动重发,避免重复推送;需通过企微实际聊天结果人工核对。
|
||||
- 只有明确的企微拒绝响应允许在剩余窗口重试;41096可重试,41051记为已使用并停止。token明确失效可刷新一次。过期或缺少code有独立原因,不会当成成功发送。
|
||||
- 标签仅增添配置标签,不删除人工标签;备注与描述只写明确启用的字段。变量替换只支持白名单,名字接口失败使用本地员工名或“客户顾问”、客户用“您”。备注截断到20字符;欢迎语字节截断有审计原因。
|
||||
- 元数据每个动作单独记录状态。失败最多10次、指数间隔后终态failed;成功动作不重放。最终failed不是成功,应安排告警/人工修复。范围动作仅触发现有范围任务,其最终应用状态仍以原范围同步表为准。
|
||||
|
||||
## 监控与验收
|
||||
|
||||
`zyt_qywx_promotion_automation_task.actions_json` 保存分动作状态、次数、错误码、原因和重试时间;`zyt_qywx_promotion_automation_action_log` 保存每次状态转移。监控欢迎语入队延迟、`expired/uncertain/failed` 数量、待处理最早事件时间、素材 `last_error` 和worker存活。一次没有异常输出不等于客户端已收到消息。
|
||||
|
||||
安全离线验证(不会初始化现有数据库、HTTP全部Mock):
|
||||
|
||||
```sh
|
||||
cd server
|
||||
php tests/QywxPromotionContactApiServiceTest.php
|
||||
php tests/QywxPromotionMediaServiceTest.php
|
||||
php tests/QywxPromotionAutomationServiceTest.php
|
||||
php tests/QywxPromotionCodeCipherTest.php
|
||||
```
|
||||
|
||||
生产验收仍需在授权的企业测试客户/员工上验证真实权限、半客户/正式客户回调、每类附件实际接收、昵称模板、默认欢迎语互斥、worker故障和跨节点存储。此开发没有发出任何真实企业微信写请求。
|
||||
@@ -0,0 +1,22 @@
|
||||
# 企业微信获客配置验证记录
|
||||
|
||||
日期:2026-08-31。
|
||||
|
||||
## 已完成的验证
|
||||
|
||||
- 单选与自定义标签:`QywxPromotionCreateTagTest.php`、`WecomPromotionCreateTagControllerTest.php` 及配置/前端 helper 回归通过。覆盖同组复用、新建分组/已有分组参数、并发冲突和网络不确定时只读回确认、非法名称、页面权限、POST 限制及拒绝多选。隔离浏览器验证单选替换、自定义空值提示、创建期间禁止保存、失败保留原标签、创建成功选中真实响应 ID、最终仅提交一个 ID,以及旧多选配置要求重新选择。未创建真实企微标签。
|
||||
- 迁移修复复验:已修正 `welcome_cipher` 列注释中非法的 `COMMENT=`。在独立临时 MySQL 5.7.26 实例先创建前两张表并写入一条标记记录,再完整执行迁移两次;四张表均存在,字段注释正确,标记记录保持原值。未连接或修改业务数据库。
|
||||
- `QywxPromotionAutomationConfigTest.php`:接待时段起止边界、跨午夜和跨周、主接待优先、日上限和跨日重置、备用禁用、无可用成员、非法配置、分时欢迎语重叠、昵称/日期模板、备注长度、欢迎语 UTF-8 字节限制。
|
||||
- `QywxPromotionContactApiServiceTest.php`、`QywxPromotionMediaServiceTest.php`、`QywxPromotionAutomationServiceTest.php`:官方接口参数、Secret 选择、令牌失效重试、HTTP 不确定结果、素材权限/真实 MIME/私有路径/缓存刷新、欢迎码加密、重复事件、半客户、20 秒时效、分动作补偿和数据库异常触发回调重试。测试使用 HTTP mock 与内存存储,不连接业务数据库。
|
||||
- `QywxPromotionCodeCipherTest.php`:6 个隔离 PHP 进程首次启动共用完整密钥、随机密文、篡改/错误密钥拒绝、损坏密钥不自动覆盖;仅使用临时目录。
|
||||
- 原有成员范围、获客链接 URL、获客 API HTTP mock、获客事件重试、推广浮窗、删除契约及操作人权限契约测试通过。
|
||||
- 浏览器使用独立 Vite 测试入口和虚构数据,替换 API 模块,未请求真实业务接口:验证排班缺项阻止保存、备用候选排除主接待、企业标签选择、渠道欢迎语变量与手机预览、网页附件编辑、客户备注预览和描述,最终保存参数与服务端配置契约一致。浏览器控制台无运行错误。
|
||||
- 三个新增/修改 Vue 单文件组件编译通过,前端配置 helper 12 项边界断言通过。
|
||||
- 完整 Vite 生产构建成功(4057 个模块),产物输出到隔离临时目录,没有执行 `release.mjs`,没有覆盖 `server/public/admin`。现有大型 bundle 和第三方播放器 `eval` 警告仍存在。
|
||||
- 全项目 `vue-tsc --noEmit` 仍有 61 条其他文件的既存错误,本次推广页面、两个新组件、helper 和 API 文件未报错。未扩大范围修复其他模块。
|
||||
|
||||
## 验证边界
|
||||
|
||||
未向真实企业微信客户发送欢迎语、打标签、修改备注或上传测试素材;未执行生产数据库迁移。正式部署后仍须验证当前企业可调用应用的权限、成员可见范围、接收回调配置、后台欢迎语互斥、常驻进程和真实素材下发结果。
|
||||
|
||||
分钟调度存在传播时延;官方直链已打开或在途的好友请求无法由本地上限保证即时撤回。所有成员不可用时保留明确阻塞状态,不声称远端链接已停用。
|
||||
Reference in New Issue
Block a user