# 企业微信模块(活动 / 通知 / 通讯录)- 前端联调方案
## 涉及页面
- 活动管理(列表 + 新建/编辑弹窗)
- 活动详情(含报名记录列表、报名统计、二维码展示)
- 通知管理(列表 + 新建/编辑弹窗)
- 成员选择器(推送时挑选部门/成员,数据来自企业微信通讯录)
- 活动报名落地页(**不需要前端开发**,由后端直接渲染 HTML,见文末说明)
## 本次新增能力
| 能力 | 说明 |
|------|------|
| 发起活动 | 创建草稿 → 编辑 → 发布 |
| 推送给成员 | 通过企业微信应用消息推送,成员点消息里的按钮直达报名页 |
| 报名二维码 | 后端生成 PNG,可后台展示/下载打印 |
| 扫码报名 | 成员扫码打开报名页,填姓名+手机号提交,**免登录** |
## API
所有接口前缀 `/admin-api`。
### 活动管理
| 方法 | 路径 | 权限标识 | 说明 |
|------|------|----------|------|
| POST | `/wxcp/activity/create` | `wxcp:activity:create` | 创建活动(存为草稿) |
| PUT | `/wxcp/activity/update` | `wxcp:activity:update` | 修改活动 |
| DELETE | `/wxcp/activity/delete` | `wxcp:activity:delete` | 删除活动 |
| GET | `/wxcp/activity/get` | `wxcp:activity:query` | 活动详情 |
| GET | `/wxcp/activity/page` | `wxcp:activity:query` | 活动分页 |
| PUT | `/wxcp/activity/publish` | `wxcp:activity:update` | 发布活动 |
| PUT | `/wxcp/activity/finish` | `wxcp:activity:update` | 结束活动 |
| PUT | `/wxcp/activity/cancel` | `wxcp:activity:update` | 取消活动 |
**活动分页请求参数:**
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| pageNo | Integer | 是 | 页码 |
| pageSize | Integer | 是 | 每页条数 |
| title | String | 否 | 活动标题,模糊匹配 |
| status | Integer | 否 | 状态:0草稿 1已发布 2已结束 3已取消 |
| pushStatus | Integer | 否 | 推送状态:0未推送 1已推送 |
| startTime | DateTime[] | 否 | 活动开始时间范围,格式 `yyyy-MM-dd HH:mm:ss` |
**创建/修改请求参数:**
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| id | Long | 修改时必填 | 编号 |
| title | String | 是 | 活动标题,≤128 字符 |
| content | String | 否 | 活动详情 |
| blobIds | Long[] | 否 | **封面图文件编号**,最多 1 个;不传 = 不改动封面,传 `[]` = 清空封面 |
| location | String | 否 | 活动地点,≤255 字符 |
| startTime | DateTime | 否 | 活动开始时间 |
| endTime | DateTime | 否 | 活动结束时间 |
| signupStartTime | DateTime | 否 | 报名开始时间 |
| signupEndTime | DateTime | 否 | 报名结束时间 |
| signupLimit | Integer | 是 | 报名上限,**不限传 0** |
| pushScope | Integer | 是 | 推送范围:0全员 1指定部门 2指定成员 |
| pushTarget | String | 否 | 推送目标,逗号分隔;范围选 1 或 2 时必填 |
| remark | String | 否 | 备注 |
**活动响应新增字段:**
| 字段 | 类型 | 说明 |
|------|------|------|
| statusName | String | 状态名,如「已发布」 |
| pushScopeName | String | 推送范围名,如「全员」 |
| signupCount | Integer | 已报名人数 |
| pushStatus | Integer | 0未推送 1已推送 |
| pushTime | DateTime | 最近推送时间 |
| attachmentList | Object[] | **封面图附件列表**(最多 1 条),字段同报名附件:`id`、`name`、`url`、`previewURL`、`downloadURL`。列表缩略图、表单回显都从这里取 |
### 推送与二维码
| 方法 | 路径 | 权限标识 | 说明 |
|------|------|----------|------|
| POST | `/wxcp/activity/push` | `wxcp:activity:push` | 推送给企业微信成员 |
| GET | `/wxcp/activity/qrcode` | `wxcp:activity:query` | 获取报名二维码(返回 PNG) |
| GET | `/wxcp/activity/signup-url` | `wxcp:activity:query` | 获取报名页地址 |
**推送请求参数:**
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| id | Long | 是 | 活动编号 |
| pushScope | Integer | 否 | 覆盖活动上的推送范围,不传则沿用 |
| pushTarget | String | 否 | 覆盖活动上的推送目标,不传则沿用 |
**推送响应:**
```json
{ "code": 0, "data": { "success": true, "errCode": 0, "message": "推送成功" } }
```
**二维码接口:** 直接返回 `image/png` 字节流,不是 JSON。
页面里把这个 URL 当图片地址用即可(如 `
`),需要带令牌时自行拼查询参数。
### 报名记录
| 方法 | 路径 | 权限标识 | 说明 |
|------|------|----------|------|
| GET | `/wxcp/activity/signup/page` | `wxcp:signup:query` | 报名记录分页 |
| PUT | `/wxcp/activity/signup/cancel` | `wxcp:signup:update` | 取消报名 |
| PUT | `/wxcp/activity/signup/check-in` | `wxcp:signup:update` | 签到 |
**分页请求参数:** `activityId`、`name`(模糊)、`mobile`(模糊)、`status`
**响应字段:** `id`、`activityId`、`activityTitle`、`wxUserId`、`name`、`mobile`、`deptName`、`status`、`statusName`、`signupTime`、`checkInTime`、`remark`
### 公开接口(免登录,前端**不需要**调用)
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/wxcp/activity/signup-page?id={活动编号}` | 报名落地页 HTML,由后端渲染 |
| POST | `/wxcp/activity/signup` | 提交报名(支持 `content` 文字 + `blobIds` 附件) |
**提交报名请求参数:**
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| activityId | Long | 是 | 活动编号 |
| name | String | 否 | 姓名,≤64 字符 |
| mobile | String | 否 | 手机号;**填了才校验** 11 位格式,留空通过 |
| wxUserId | String | 否 | 企业微信成员账号 |
| deptName | String | 否 | 部门名称 |
| content | String | 否 | 参与说明 |
| blobIds | Long[] | 否 | 附件文件编号 |
### 报名统计与附件
| 方法 | 路径 | 权限标识 | 说明 |
|------|------|----------|------|
| GET | `/wxcp/activity/stats?id=` | `wxcp:activity:query` | 报名统计 |
| GET | `/wxcp/activity/signup/attachments?signupId=` | `wxcp:signup:query` | 某条报名上传的图片/附件 |
**统计响应:**
```json
{
"activityId": 1, "activityTitle": "2026春季团建活动",
"signupLimit": 50, "totalSignup": 1,
"signedUpCount": 1, "checkedInCount": 0, "canceledCount": 0,
"remaining": 49, "signupPercent": 2, "checkInPercent": 0,
"deptStats": [{ "name": "技术部", "count": 3 }],
"dateStats": [{ "name": "2026-09-29", "count": 1 }],
"sourceStats": [{ "name": "企业微信身份", "count": 0 }, { "name": "表单填写", "count": 1 }]
}
```
> `remaining` / `signupPercent` 在活动**不限名额**(`signupLimit=0`)时为 `null`,前端要显示"不限"。
> 统计**不含已取消**的报名,所以 `totalSignup ≠ 报名记录总条数`。
### 通知管理
| 方法 | 路径 | 权限标识 | 说明 |
|------|------|----------|------|
| POST | `/wxcp/notice/create` | `wxcp:notice:create` | 创建通知(存为草稿) |
| PUT | `/wxcp/notice/update` | `wxcp:notice:update` | 修改通知 |
| DELETE | `/wxcp/notice/delete` | `wxcp:notice:delete` | 删除通知 |
| GET | `/wxcp/notice/get` | `wxcp:notice:query` | 通知详情 |
| GET | `/wxcp/notice/page` | `wxcp:notice:query` | 通知分页 |
| PUT | `/wxcp/notice/publish` | `wxcp:notice:update` | 发布通知 |
| PUT | `/wxcp/notice/revoke` | `wxcp:notice:update` | 撤回通知 |
| POST | `/wxcp/notice/push` | `wxcp:notice:push` | 推送通知 |
**通知与活动的差异:**
| 项 | 活动 | 通知 |
|----|------|------|
| 推送消息类型 | 文本卡片(带「立即报名」按钮跳报名页) | 纯文本 |
| 正文长度 | 由卡片模板限制 | 超过 600 字**自动截断**并提示到系统内查看 |
| 状态 | 草稿/已发布/已结束/已取消 | 草稿/已发布/已撤回 |
| 推送前提 | 必须「已发布」 | 必须「已发布」 |
### 企业微信通讯录
| 方法 | 路径 | 权限标识 | 说明 |
|------|------|----------|------|
| GET | `/wxcp/member/department/tree` | `wxcp:member:query` | 部门树(含 children) |
| GET | `/wxcp/member/department/list` | `wxcp:member:query` | 部门平铺列表(做下拉更方便) |
| GET | `/wxcp/member/list?departmentId=&fetchChild=` | `wxcp:member:query` | 成员列表 |
**成员响应字段:** `userId`(推送时填这个)、`name`、`mobile`、`email`、`avatar`、`position`、`gender`、`status`、`departmentIds`、`departmentNames`
> 这几个接口是**实时调用企业微信**的,企业微信对通讯录接口有频率限制。前端请做缓存(如打开选择器时拉一次),**不要**在搜索框里每输入一个字符就请求一次。
## 业务流程与数据带入
1. 新建活动 → 保存为**草稿**,此时扫码报名会提示「报名已结束」
2. 点「发布」→ 状态变「已发布」,此时二维码和报名页才可用
3. 点「推送」→ 成员企业微信收到卡片消息,标题为活动标题,正文含**活动时间 / 地点 / 报名截止 / 名额**,按钮「立即报名」直达报名页
4. 成员点按钮或扫码 → 打开报名页 →(可选)填姓名和手机号 + 传附件 → 提交
5. 报名成功 → 活动 `signupCount` 自动 +1
6. 后台在活动详情的报名记录列表里可看到该成员,可执行「取消报名」「签到」
**数据带入关系:**
| 来源 | 目标 | 带入内容 |
|------|------|----------|
| 活动编辑保存 | 推送消息 | 标题、时间、地点、报名截止、名额 |
| 活动编号 | 二维码内容 | `{signupUrlPrefix}/admin-api/wxcp/activity/signup-page?id={id}` |
| 扫码报名 | 报名记录 | 活动编号 |
## 字段展示规则
| 字段 | 展示位置 | 说明 |
|------|----------|------|
| title | 列表、详情 | 活动标题,点击进详情 |
| statusName | 列表、详情 | 用后端返回的中文名,前端不要自己映射 |
| startTime ~ endTime | 列表、详情 | 活动时间范围 |
| signupCount / signupLimit | 列表、详情 | `0/50` 这种形式;上限为 0 时显示「不限」 |
| pushStatus / pushTime | 列表、详情 | 未推送 / 已推送 + 时间 |
| pushScopeName | 详情 | 推送范围中文名 |
| qrcode(图片) | 详情 | 报名二维码,需支持下载 |
| statusName(报名) | 报名列表 | 已报名 / 已取消 / 已签到 |
## 业务规则说明
| 场景 | 规则 |
|------|------|
| 创建活动 | 一律存为草稿,不可直接创建为已发布 |
| 修改活动 | 已结束、已取消的活动不可修改;草稿和已发布可改 |
| 推送前提 | **活动必须是「已发布」状态**,草稿状态推送会报「活动尚未发布,请先发布后再推送」 |
| 推送范围=全员 | 使用企业微信 `@all`,即应用可见范围内所有人 |
| 推送范围=指定成员 | `pushTarget` 填成员账号(UserId),多个用**英文逗号**分隔 |
| 推送范围=指定部门 | `pushTarget` 填部门 ID,多个用英文逗号分隔 |
| 推送失败 | 接口返回 `success=false` + 企业微信错误码,**不会抛异常**;前端应把 `message` 直接提示给用户 |
| 推送成功后 | 活动 `pushStatus` 置 1、`pushTime` 记录时间;重复推送会覆盖这两个字段 |
| 报名时间窗口 | 未到 `signupStartTime` 或已过 `signupEndTime` 均不能报名 |
| 报名名额 | `signupLimit=0` 表示不限;已满时提示「报名人数已满」 |
| 报名姓名/手机号 | **均为选填**。手机号填了才校验格式,留空允许提交 |
| 重复报名 | 有企业微信身份按成员账号判重;没有账号但有手机号则按手机号判重;**两者都没有时不做判重**(无法判定身份,每次提交都新建一条) |
| 取消后重新报名 | 复用原记录,不会产生第二条历史 |
| 取消报名 | 活动 `signupCount` 自动 -1 |
| 封面图 | 不存业务表字段,存 `system_storage_attachment`(`record_type = wxcp_activity`)。删除活动时一并清掉附件关系 |
| 封面图展示 | 报名页顶部以 `object-fit: cover` 展示;管理后台列表在标题左侧显示缩略图 |
## 注意事项
- **二维码接口返回的是图片二进制,不是 JSON**,不要用普通 request 封装去取,直接用图片 URL。
- **`signupLimit` 必填**,不限要传 `0` 而不是留空,否则会校验失败。
- **推送范围选「指定部门/成员」时 `pushTarget` 不能为空**,否则报「推送目标不能为空」。
- 推送是**同步**调用企业微信接口,成员多时可能耗时 1~3 秒,前端按钮要有 loading。
- 活动 `content` 字段在报名页是**按纯文本渲染**的(做了 HTML 转义防 XSS)。如果活动详情需要富文本排版,需要另行接入富文本白名单过滤,当前版本不支持。
- 报名页地址依赖后端配置 `wx.cp.signup-url-prefix`(当前为 `http://wcf.viphk.nnhk.cc`)。该地址必须是**成员手机能访问到的外网地址**,否则二维码扫出来打不开。
- 报名记录里的 `wxUserId` 只有在**企业微信内打开报名页且带上了参数**时才有值;普通浏览器扫码报名时为空,此时按手机号去重。
- 权限标识需要在「系统管理 → 菜单管理」里配置好,否则接口会返回 403。
- **推送目标用的是 `userId`(成员账号,如 `LiuYi`),不是姓名、也不是手机号。** 从通讯录接口取 `userId` 填进 `pushTarget`。
- **附件上传走 system 模块的公共上传接口**:报名页先调 `POST /admin-api/system/storage-blob/public-upload`(表单字段名 `files`)拿到 `blobId`,再把 `blobIds` 随报名提交,由**服务端**完成与报名记录的绑定。管理后台查附件用 `/wxcp/activity/signup/attachments`。
- **报名的 `recordType` 固定为 `wxcp_activity_signup`**,已注册进 system 模块的 `StorageRecordTypeEnum` 白名单。以后新增别的附件关联类型,必须同步注册,否则绑定会报「无效的记录类型」。
- **活动封面的 `recordType` 为 `wxcp_activity`**,同样已注册进白名单。封面**不再有 `coverUrl` 字段**,`POST/PUT /wxcp/activity/create|update` 提交 `blobIds`,`GET` 读 `attachmentList`。
- **封面图在报名页显示时会去掉协议和域名**。附件地址里的域名取自 `yudao.storage.domain`(一般是内网地址),而手机扫码是从公网域名打开页面的,直连内网地址加载不出图片,所以后端只保留 `/admin-api/...` 路径交给同源代理。**如果部署时管理后台与报名页不同源,这个处理需要重新评估。**
- 表单里的封面用 `ImageUpload` 组件(`maxNumber: 1`)时,组件返回的是**单值而不是数组**,提交前必须包成数组(`blobIds` 后端签名是 `List`)。
- 通讯录接口**实时调企业微信**,有频率限制,前端务必缓存,不要随输入实时请求。
- 通知正文超过 600 字会**自动截断**(企业微信文本消息有长度上限),截断提示会写在消息末尾。
## 关于报名落地页
报名页 `GET /admin-api/wxcp/activity/signup-page?id=xxx` 由**后端直接渲染 HTML**(含内联样式和少量 JS),前端**无需开发**。
这样做的原因:二维码必须指向一个成员手机能直接打开的页面,而后端渲染可以立即跑通「推送 → 扫码 → 报名」闭环,不必等前端排期。若后续希望改为前端 SPA 页面,把二维码内容改成前端路由地址即可,后端接口(`/wxcp/activity/signup`)不用动。
> 该页面的路径刻意放在 `/admin-api` 下(而非 `/app-api`),因为前端 Vite 开发代理只转发 `/admin-api`,放 `/app-api` 会导致扫码后请求打到前端静态服务返回 404。