Commit 7b318c8d by 周海峰

no message

parent a7e1914e
# 企业微信内网应用集成方案
# 企业微信内网应用集成方案
## 项目概述
本文档描述了企业微信内网应用与其他企业微信应用的集成方案,实现统一消息推送和应用跳转功能。
## 需求总结
### 当前状况
- 已有部署正常的企业微信内网应用(基于网页)
- 需要集成多个其他企业微信应用(同样基于网页)
- 这些应用已有自己的消息推送机制,但希望统一通过内网应用推送
- 内网应用目前无API接口,使用用户ID+服务器自认证方式
### 核心需求
- **统一消息推送**:所有应用消息通过内网应用推送
- **统一消息收件箱**:在内网应用中展示所有应用消息
- **应用跳转**:点击消息直接跳转到目标应用的相关内容页面
- **转发页面**:仅作为跳转中介,无需消息预览
## 系统架构
### 整体架构图
```
企业微信客户端
内网应用前端
内网应用后端 ← 直接接收各应用调用
应用A后端 应用B后端(直接调用内网应用后端API)
```
### 核心模块
- **消息接收API**:各应用直接调用内网应用后端API提交消息
- **消息存储**:内网应用后端负责存储所有消息
- **消息推送**:内网应用后端调用现有的消息发送服务
- **统一收件箱**:内网应用前端展示消息列表
- **跳转转发**:处理应用间跳转逻辑
## 流程时序图
### 1. 消息推送流程
```
应用A后端 -> 内网应用后端: POST /api/messages (提交消息)
内网应用后端 -> 内网应用后端: 验证应用身份
内网应用后端 -> 内网应用后端: 存储消息到数据库
内网应用后端 -> 现有消息发送服务: 调用推送接口
现有消息发送服务 -> 企业微信: 推送消息给用户
企业微信 -> 用户: 显示通知
```
### 2. 用户查看消息流程
```
用户 -> 企业微信: 点击内网应用
企业微信 -> 内网应用前端: 加载应用
内网应用前端 -> 内网应用后端: GET /api/messages (获取消息列表)
内网应用后端 -> 内网应用前端: 返回消息数据
内网应用前端 -> 用户: 展示统一收件箱
```
### 3. 消息跳转流程
```
用户 -> 内网应用前端: 点击某条消息
内网应用前端 -> 内网应用后端: GET /api/messages/{id}/redirect
内网应用后端 -> 内网应用后端: 记录点击日志
内网应用后端 -> 内网应用前端: 返回目标应用URL
内网应用前端 -> 企业微信: 跳转到目标应用
企业微信 -> 目标应用前端: 加载具体内容
```
## 接口文档
### 1. 消息提交接口
**POST /api/messages**
**功能描述**:各应用向后端提交消息
**请求头**
```
Content-Type: application/json
Authorization: Bearer {app_token}
```
**请求参数**
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| appId | string | 是 | 应用标识 |
| userId | string | 是 | 目标用户ID |
| title | string | 是 | 消息标题 |
| content | string | 是 | 消息内容 |
| targetUrl | string | 是 | 跳转链接 |
| messageType | string | 是 | 消息类型:text、image、voice、video、file、textcard、mpnews、markdown、miniprogram_notice、template_card |
| timestamp | string | 是 | 时间戳,格式:YYYY-MM-DD HH:MM:SS |
| msgData | object | 否 | 特定消息类型的扩展数据 |
**请求示例**
**文本消息**
```json
{
"appId": "hr_system",
"userId": "zhangsan",
"title": "新的审批任务",
"content": "您有一个请假申请需要审批",
"targetUrl": "https://hr.company.com/approval/detail/123",
"messageType": "text",
"timestamp": "2025-11-03 10:30:00"
}
```
**文本卡片消息**
```json
{
"appId": "hr_system",
"userId": "zhangsan",
"title": "审批提醒",
"content": "您有新的请假申请待审批",
"targetUrl": "https://hr.company.com/approval/detail/123",
"messageType": "textcard",
"timestamp": "2025-11-03 10:30:00",
"msgData": {
"description": "张三提交了请假申请,请假时间:2025-11-05至2025-11-07",
"url": "https://hr.company.com/approval/detail/123",
"btnTxt": "立即审批"
}
}
```
**模板卡片消息**
```json
{
"appId": "approval_system",
"userId": "zhangsan",
"title": "请假审批任务",
"content": "新的请假申请待处理",
"targetUrl": "https://approval.company.com/task/123",
"messageType": "template_card",
"timestamp": "2025-11-03 10:30:00",
"msgData": {
"cardType": "button_interaction",
"source": {
"desc": "人事系统",
"descColor": 1
},
"mainTitle": {
"title": "请假审批提醒",
"desc": "张三申请请假3天"
},
"emphasisContent": {
"title": "3天",
"desc": "请假时长"
},
"subTitleText": "请假时间:2025-11-05至2025-11-07",
"horizontalContentList": [
{
"keyName": "请假类型",
"value": "事假"
},
{
"keyName": "申请时间",
"value": "2025-11-03 10:30"
}
],
"jumpList": [
{
"type": 1,
"title": "查看详情",
"url": "https://approval.company.com/task/123"
}
],
"cardAction": {
"type": 1,
"url": "https://approval.company.com/task/123"
},
"taskId": "task_123456",
"buttonList": [
{
"text": "同意",
"style": 1,
"key": "approve"
},
{
"text": "拒绝",
"style": 2,
"key": "reject"
}
]
}
}
```
**图片消息**
```json
{
"appId": "project_system",
"userId": "zhangsan",
"title": "项目截图",
"content": "项目进度截图",
"targetUrl": "https://project.company.com/detail/456",
"messageType": "image",
"timestamp": "2025-11-03 10:30:00",
"msgData": {
"imageUrl": "https://file.company.com/images/progress_123.jpg",
"imageMd5": "abcd1234567890abcdef1234567890ab"
}
}
```
**文件消息**
```json
{
"appId": "doc_system",
"userId": "zhangsan",
"title": "会议纪要",
"content": "本周会议纪要文档",
"targetUrl": "https://doc.company.com/file/789",
"messageType": "file",
"timestamp": "2025-11-03 10:30:00",
"msgData": {
"fileName": "会议纪要_20251103.docx",
"fileUrl": "https://file.company.com/docs/meeting_20251103.docx",
"fileSize": 1024000,
"fileType": "docx"
}
}
```
**Markdown消息**
```json
{
"appId": "tech_system",
"userId": "zhangsan",
"title": "技术公告",
"content": "系统升级通知",
"targetUrl": "https://tech.company.com/notice/101",
"messageType": "markdown",
"timestamp": "2025-11-03 10:30:00",
"msgData": {
"markdownContent": "# 系统维护通知\n\n**维护时间**:2025-11-05 20:00-24:00\n\n**影响范围**:所有业务系统\n\n**维护内容**:\n- 数据库升级\n- 安全补丁更新\n- 性能优化"
}
}
```
**响应参数**
| 参数名 | 类型 | 说明 |
|--------|------|------|
| code | integer | 响应码,200表示成功 |
| message | string | 响应消息 |
| data | object | 响应数据 |
**响应示例**
```json
{
"code": 200,
"message": "success",
"data": {
"message_id": "msg_20251103103000123",
"response_code": "response_code_abc123", // 仅模板卡片消息返回
"response_code_expire_time": "2025-11-06 10:30:00" // 仅模板卡片消息返回
}
}
```
**错误码**
| 错误码 | 说明 |
|--------|------|
| 400 | 请求参数错误 |
| 401 | 应用认证失败 |
| 500 | 服务器内部错误 |
### 2. 消息列表接口
**GET /api/messages**
**功能描述**:获取用户消息列表
**请求参数**
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| userId | string | 是 | 用户ID |
| page | integer | 否 | 页码,默认1 |
| pageSize | integer | 否 | 每页条数,默认20 |
**响应参数**
| 参数名 | 类型 | 说明 |
|--------|------|------|
| code | integer | 响应码 |
| data | object | 响应数据 |
| total | integer | 总消息数 |
| messages | array | 消息列表 |
**响应示例**
```json
{
"code": 200,
"data": {
"total": 100,
"messages": [
{
"id": "msg_20251103103000123",
"app_id": "hr_system",
"app_name": "人事系统",
"title": "新的审批任务",
"content": "您有一个请假申请需要审批",
"create_time": "2025-11-03 10:30:00",
"is_read": false,
"message_type": "textcard",
"target_url": "https://hr.company.com/approval/detail/123",
"msg_data": {
"description": "张三提交了请假申请,请假时间:2025-11-05至2025-11-07",
"url": "https://hr.company.com/approval/detail/123",
"btntxt": "立即审批"
}
},
{
"id": "msg_20251103102500567",
"app_id": "project_system",
"app_name": "项目管理系统",
"title": "项目进度更新",
"content": "项目A已完成阶段评审",
"create_time": "2025-11-03 10:25:00",
"is_read": true,
"message_type": "image",
"target_url": "https://project.company.com/detail/456",
"msg_data": {
"image_url": "https://file.company.com/images/progress_456.jpg",
"image_md5": "abcd1234567890abcdef1234567890ab"
}
},
{
"id": "msg_20251103102000987",
"app_id": "doc_system",
"app_name": "文档系统",
"title": "会议纪要",
"content": "本周会议纪要文档",
"create_time": "2025-11-03 10:20:00",
"is_read": false,
"message_type": "file",
"target_url": "https://doc.company.com/file/789",
"msg_data": {
"file_name": "会议纪要_20251103.docx",
"file_url": "https://file.company.com/docs/meeting_20251103.docx",
"file_size": 1024000,
"file_type": "docx"
}
}
]
}
}
```
### 3. 消息跳转接口
**GET /api/messages/{id}/redirect**
**功能描述**:获取消息跳转链接
**请求参数**
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| id | string | 是 | 消息ID |
**响应参数**
| 参数名 | 类型 | 说明 |
|--------|------|------|
| code | integer | 响应码 |
| data | object | 响应数据 |
| redirect_url | string | 跳转链接 |
**响应示例**
```json
{
"code": 200,
"data": {
"redirect_url": "https://hr.company.com/approval/detail/123"
}
}
```
### 4. 消息标记已读接口
**PUT /api/messages/{id}/read**
**功能描述**:标记消息为已读
**请求参数**
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| id | string | 是 | 消息ID |
**响应示例**
```json
{
"code": 200,
"message": "success"
}
```
### 5. 模板卡片消息更新接口
**POST /api/messages/{id}/update_template_card**
**功能描述**:更新模板卡片消息的替换文案信息(仅支持按钮交互型、投票选择型、多项选择型的卡片以及填写了action_menu字段的文本通知型、图文展示型)
**请求头**
```
Content-Type: application/json
Authorization: Bearer {app_token}
```
**请求参数**
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| responseCode | string | 是 | 模板卡片消息返回的响应码 |
| templateCardData | object | 是 | 更新的模板卡片数据 |
**请求示例**
```json
{
"responseCode": "response_code_123456",
"templateCardData": {
"taskId": "task_123",
"title": "审批任务状态更新",
"description": "当前状态:已审批通过",
"button": {
"text": "查看详情",
"url": "https://hr.company.com/approval/detail/123?status=approved"
}
}
}
```
**响应示例**
```json
{
"code": 200,
"message": "success",
"data": {
"updated": true,
"new_response_code": "new_response_code_789012"
}
}
```
**注意事项**
- 仅当原卡片为按钮交互型、投票选择型、多项选择型的卡片以及填写了action_menu字段的文本通知型、图文展示型时可以调用本接口
- response_code有效期为72小时,超过72小时后将无法使用
- 每次更新后会返回新的response_code,可用于后续更新
- 当用户点击任务卡片时,回调接口也会带上response_code
## 数据库设计
### 消息表结构
```sql
CREATE TABLE messages (
id VARCHAR(50) PRIMARY KEY COMMENT '消息ID',
app_id VARCHAR(50) NOT NULL COMMENT '应用ID',
user_id VARCHAR(50) NOT NULL COMMENT '用户ID',
title VARCHAR(200) NOT NULL COMMENT '消息标题',
content TEXT COMMENT '消息内容',
target_url VARCHAR(500) COMMENT '跳转链接',
message_type VARCHAR(30) COMMENT '消息类型:text、image、voice、video、file、textcard、mpnews、markdown、miniprogram_notice、template_card',
msg_data JSON COMMENT '消息扩展数据(JSON格式)',
response_code VARCHAR(100) COMMENT '模板卡片消息的响应码,用于后续更新',
response_code_expire_time DATETIME COMMENT '响应码过期时间',
is_read BOOLEAN DEFAULT FALSE COMMENT '是否已读',
create_time DATETIME COMMENT '创建时间',
read_time DATETIME COMMENT '读取时间',
INDEX idx_user_id (user_id),
INDEX idx_create_time (create_time),
INDEX idx_app_id (app_id),
INDEX idx_message_type (message_type),
INDEX idx_response_code (response_code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='消息表';
```
### 应用配置表
```sql
CREATE TABLE apps (
app_id VARCHAR(50) PRIMARY KEY COMMENT '应用ID',
app_name VARCHAR(100) NOT NULL COMMENT '应用名称',
app_token VARCHAR(100) NOT NULL COMMENT '应用访问令牌',
status BOOLEAN DEFAULT TRUE COMMENT '状态',
create_time DATETIME COMMENT '创建时间',
update_time DATETIME COMMENT '更新时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='应用配置表';
```
## 前端页面设计
### 1. 统一消息收件箱页面
- **页面路径**`/messages`
- **功能说明**:展示用户所有消息列表
- **主要功能**
- 消息列表展示(按时间倒序)
- 消息已读/未读状态标识
- 点击消息跳转到对应应用
- 分页加载更多消息
### 2. 消息转发页面
- **页面路径**`/redirect?message_id={id}`
- **功能说明**:处理消息跳转逻辑
- **处理流程**
1. 接收消息ID参数
2. 调用后端API获取跳转链接
3. 记录消息已读状态
4. 跳转到目标应用页面
## 安全设计
### 1. 应用认证
- 每个集成应用分配唯一的`app_id``app_token`
- 应用调用API时需要携带`Authorization`
- 后端验证应用身份和权限
### 2. 用户认证
- 复用现有内网应用的用户认证机制
- 通过企业微信获取用户ID进行身份验证
### 3. 数据安全
- 敏感数据传输使用HTTPS协议
- 数据库连接使用加密连接
- 定期备份重要数据
## 部署建议
### 1. 后端部署
- 建议使用Nginx作为反向代理
- 配置HTTPS证书
- 设置合理的超时时间
### 2. 数据库优化
- 为常用查询字段建立索引
- 定期清理过期消息数据
- 监控数据库性能
### 3. 监控告警
- 监控API调用成功率
- 监控消息推送延迟
- 设置异常告警机制
## 后续扩展
### 1. 功能扩展
- 支持消息分类和标签
- 添加消息搜索功能
- 支持批量操作
### 2. 性能优化
- 引入消息队列处理高并发
- 使用缓存提高查询性能
- 数据库读写分离
### 3. 统计分析
- 消息发送统计
- 用户行为分析
- 应用使用统计
## 不同类型消息处理说明
### 1. 文本消息(text)
- 最简单的消息类型
- 直接显示标题和内容
- 无需额外的msg_data数据
### 2. 文本卡片消息(textcard)
- 支持更丰富的展示效果
- 包含描述、跳转URL、按钮文字
- 适用于重要通知和待办事项
### 3. 图片消息(image)
- 需要包含图片URL和MD5值
- 前端可展示图片预览
- 支持点击查看大图
### 4. 文件消息(file)
- 包含文件名、文件URL、文件大小、文件类型
- 前端可显示文件图标和下载链接
- 支持直接下载或预览
### 5. Markdown消息(markdown)
...@@ -104,7 +104,8 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { ...@@ -104,7 +104,8 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
"/api/ps/account_sel/**", "/znzl/file/**", "/auth/v1/**", "/auth/user/**", "/api/ps/account_sel/**", "/znzl/file/**", "/auth/v1/**", "/auth/user/**",
"/error/**", "/weixin/wxuserinfo/**", "/weixin/information/**", "/auth/dt/**", "/error/**", "/weixin/wxuserinfo/**", "/weixin/information/**", "/auth/dt/**",
"/avatar/**", "/user/updateWxPlatformPersonnelByAccount", "/wechatApi/**", "/avatar/**", "/user/updateWxPlatformPersonnelByAccount", "/wechatApi/**",
"/PlatformUserFavoriteAppsController/**", "/roleuser/addUserNoticeRole" "/PlatformUserFavoriteAppsController/**", "/roleuser/addUserNoticeRole",
"/messagepush/**"
).permitAll() ).permitAll()
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll() .antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
// 除上面外的所有请求全部需要鉴权认证 // 除上面外的所有请求全部需要鉴权认证
......
package com.metro.auth.platform.messagepush.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 企业微信内网应用配置属性
*/
@Data
@Component
@ConfigurationProperties(prefix = "weixin-params")
public class WeChatConfig {
private String wxCorpId;
private String wxAgentId;
private String wxSecret;
private String wxTxSecret;
private String wxTokenUrl;
private String wxSendMessageUrl;
private String wxGetUserInfo;
private String wxGetUserByCode;
private String wxTxGetList;
private String wxUserDeptDetail;
}
package com.metro.auth.platform.messagepush.controller;
import com.alibaba.fastjson.JSONObject;
import com.metro.auth.platform.domain.ResultCode;
import com.metro.auth.platform.domain.ResultJson;
import com.metro.auth.platform.messagepush.model.request.MessageSubmitRequest;
import com.metro.auth.platform.messagepush.service.MessagePushService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
/**
* 企业微信内网应用消息推送控制器
*/
@Slf4j
@RestController
@RequestMapping("/messagepush")
public class MessagePushController {
@Resource
private MessagePushService messagePushService;
/**
* 获取 AccessToken
* GET /messagepush/accesstoken
*/
@GetMapping("/accesstoken")
public ResultJson getAccessToken() {
String token = messagePushService.getAccessToken();
if (StringUtils.hasLength(token)) {
return ResultJson.ok(token);
}
return ResultJson.failure(ResultCode.SERVER_ERROR, "获取access_token失败");
}
/**
* 强制刷新 AccessToken
* GET /messagepush/accesstoken/refresh
*/
@GetMapping("/accesstoken/refresh")
public ResultJson refreshAccessToken() {
String token = messagePushService.refreshAccessToken();
if (StringUtils.hasLength(token)) {
return ResultJson.ok(token);
}
return ResultJson.failure(ResultCode.SERVER_ERROR, "刷新access_token失败");
}
/**
* 获取部门列表
* GET /messagepush/department/list?id=xxx
*/
@GetMapping("/department/list")
public ResultJson getDepartmentList(@RequestParam(required = false) Integer id) {
JSONObject result = messagePushService.getDepartmentList(id);
if (result == null) {
return ResultJson.failure(ResultCode.SERVER_ERROR, "调用企业微信API失败");
}
if (result.getInteger("errcode") != null && result.getInteger("errcode") != 0) {
log.error("获取部门列表失败: {}", result.toJSONString());
return ResultJson.failure(ResultCode.SERVER_ERROR, result.getString("errmsg"));
}
return ResultJson.ok(result.getJSONArray("department"));
}
/**
* 获取部门用户详情列表
* GET /messagepush/user/list?departmentId=1&fetchChild=1
*/
@GetMapping("/user/list")
public ResultJson getUserList(
@RequestParam(required = true) Integer departmentId,
@RequestParam(required = false, defaultValue = "1") Integer fetchChild) {
if (departmentId == null) {
return ResultJson.failure(ResultCode.BAD_REQUEST, "departmentId不能为空");
}
JSONObject result = messagePushService.getUserListByDepartment(departmentId, fetchChild);
if (result == null) {
return ResultJson.failure(ResultCode.SERVER_ERROR, "调用企业微信API失败");
}
if (result.getInteger("errcode") != null && result.getInteger("errcode") != 0) {
log.error("获取用户列表失败: {}", result.toJSONString());
return ResultJson.failure(ResultCode.SERVER_ERROR, result.getString("errmsg"));
}
return ResultJson.ok(result.getJSONArray("userlist"));
}
/**
* 获取应用后台免登用户信息
* GET /messagepush/auth/getuserinfo?code=xxx
*/
@GetMapping("/auth/getuserinfo")
public ResultJson getUserInfo(@RequestParam(required = true) String code) {
if (!StringUtils.hasLength(code)) {
return ResultJson.failure(ResultCode.BAD_REQUEST, "code不能为空");
}
JSONObject result = messagePushService.getUserInfoByCode(code);
if (result == null) {
return ResultJson.failure(ResultCode.SERVER_ERROR, "调用企业微信API失败");
}
if (result.getInteger("errcode") != null && result.getInteger("errcode") != 0) {
log.error("获取用户信息失败: {}", result.toJSONString());
return ResultJson.failure(ResultCode.SERVER_ERROR, result.getString("errmsg"));
}
return ResultJson.ok(result);
}
/**
* 根据 code 获取完整用户信息
* GET /messagepush/user/getuserinfo?code=xxx
*/
@GetMapping("/user/getuserinfo")
public ResultJson getUserDetailByCode(@RequestParam(required = true) String code) {
if (!StringUtils.hasLength(code)) {
return ResultJson.failure(ResultCode.BAD_REQUEST, "code不能为空");
}
JSONObject result = messagePushService.getUserDetailByCode(code);
if (result == null) {
return ResultJson.failure(ResultCode.SERVER_ERROR, "调用企业微信API失败");
}
if (result.getInteger("errcode") != null && result.getInteger("errcode") != 0) {
log.error("获取用户详情失败: {}", result.toJSONString());
return ResultJson.failure(ResultCode.SERVER_ERROR, result.getString("errmsg"));
}
return ResultJson.ok(result);
}
/**
* 发送应用消息(JSON格式,透传)
* POST /messagepush/message/send
*/
@PostMapping("/message/send")
public ResultJson sendMessage(@RequestBody JSONObject messageBody) {
if (messageBody == null || messageBody.isEmpty()) {
return ResultJson.failure(ResultCode.BAD_REQUEST, "消息内容不能为空");
}
log.info("发送应用消息请求: {}", messageBody.toJSONString());
JSONObject result = messagePushService.sendMessage(messageBody.toJSONString());
if (result == null) {
return ResultJson.failure(ResultCode.SERVER_ERROR, "调用企业微信API失败");
}
if (result.getInteger("errcode") != null && result.getInteger("errcode") != 0) {
log.error("发送消息失败: {}", result.toJSONString());
return ResultJson.failure(ResultCode.SERVER_ERROR, result.getString("errmsg"));
}
return ResultJson.ok(result);
}
/**
* 提交应用消息(实体类参数)
* POST /messagepush/message/submit
*/
@PostMapping("/message/submit")
public ResultJson submitMessage(@RequestBody MessageSubmitRequest request) {
if (request == null) {
return ResultJson.failure(ResultCode.BAD_REQUEST, "消息内容不能为空");
}
if (!StringUtils.hasLength(request.getUserId())) {
return ResultJson.failure(ResultCode.BAD_REQUEST, "userId不能为空");
}
if (!StringUtils.hasLength(request.getMessageType())) {
return ResultJson.failure(ResultCode.BAD_REQUEST, "messageType不能为空");
}
log.info("提交应用消息请求: {}", JSONObject.toJSONString(request));
JSONObject result = messagePushService.submitMessage(request);
if (result == null) {
return ResultJson.failure(ResultCode.SERVER_ERROR, "调用企业微信API失败");
}
if (result.getInteger("errcode") != null && result.getInteger("errcode") != 0) {
log.error("提交消息失败: {}", result.toJSONString());
return ResultJson.failure(ResultCode.SERVER_ERROR, result.getString("errmsg"));
}
return ResultJson.ok(result);
}
}
package com.metro.auth.platform.messagepush.model.request;
import lombok.Data;
/**
* 消息提交请求参数
*/
@Data
public class MessageSubmitRequest {
/**
* 应用标识
*/
private String appId;
/**
* 目标用户ID
*/
private String userId;
/**
* 消息标题
*/
private String title;
/**
* 消息内容
*/
private String content;
/**
* 跳转链接
*/
private String targetUrl;
/**
* 消息类型:text、textcard、markdown、image、file、news、mpnews、miniprogram_notice
*/
private String messageType;
/**
* 时间戳,格式:YYYY-MM-DD HH:MM:SS
*/
private String timestamp;
/**
* 特定消息类型的扩展数据
*/
private Object msgData;
}
\ No newline at end of file
package com.metro.auth.platform.messagepush.model.request;
import lombok.Data;
import java.util.List;
/**
* 发送应用消息请求参数
*/
@Data
public class SendMessageRequest {
/**
* 接收人类型:user|party|tag
*/
private String touser;
private String toparty;
private String totag;
/**
* 消息类型:text/textcard/markdown/image/file/news/mpnews/miniprogram_notice
*/
private String msgtype;
/**
* 应用agentid
*/
private Integer agentid;
/**
* 文本消息内容
*/
private TextContent text;
/**
* 文本卡片消息内容
*/
private TextCardContent textcard;
@Data
public static class TextContent {
private String content;
}
@Data
public static class TextCardContent {
private String title;
private String description;
private String url;
private String btntxt;
}
}
package com.metro.auth.platform.messagepush.model.request;
import lombok.Data;
/**
* 获取部门用户列表请求参数
*/
@Data
public class UserListRequest {
/**
* 获取的部门id
*/
private Integer departmentId;
/**
* 1/0:是否递归获取子部门下的用户
*/
private Integer fetchChild = 1;
}
package com.metro.auth.platform.messagepush.model.response;
import lombok.Data;
/**
* 企业微信部门信息
*/
@Data
public class WxDepartment {
/**
* 部门id
*/
private Integer id;
/**
* 部门名称
*/
private String name;
/**
* 父部门id
*/
private Integer parentid;
/**
* 排序字段,值越小排序越靠前
*/
private Integer order;
/**
* 部门层级
*/
private Integer level;
/**
* 子部门数量
*/
private Integer childCount;
}
package com.metro.auth.platform.messagepush.model.response;
import lombok.Data;
/**
* 企业微信消息发送结果
*/
@Data
public class WxMessageResult {
/**
* 返回码,0表示成功
*/
private Integer errcode;
/**
* 对返回码的文本描述
*/
private String errmsg;
/**
* 消息id
*/
private Long msgid;
/**
* 响应结果,0=成功
*/
private Integer responseType;
/**
* 消息是否全部发送成功
*/
private Boolean invalidUser;
/**
* 无效的partyid
*/
private Boolean invalidParty;
/**
* 无效的tagid
*/
private Boolean invalidTag;
}
package com.metro.auth.platform.messagepush.model.response;
import lombok.Data;
/**
* 企业微信用户信息
*/
@Data
public class WxUser {
/**
* 成员UserID
*/
private String userid;
/**
* 成员名称
*/
private String name;
/**
* 部门id列表
*/
private Integer[] department;
/**
* 职位信息
*/
private String position;
/**
* 手机号
*/
private String mobile;
/**
* 邮箱
*/
private String email;
/**
* 头像url
*/
private String avatar;
/**
* 性别。0表示未定义,1表示男性,2表示女性
*/
private Integer gender;
/**
* 员工个人二维码,样式是一个小程序的二维码
*/
private String qrCode;
/**
* 关注状态 1=已关注 2=禁用 4=未关注
*/
private Integer status;
/**
* 是否是系统管理员
*/
private Boolean isleader;
/**
* 成员备注
*/
private String remark;
/**
* 激活状态 1=已激活 5=已退出
*/
private Integer enable;
}
package com.metro.auth.platform.messagepush.model.response;
import lombok.Data;
/**
* 企业微信用户免登信息
*/
@Data
public class WxUserInfo {
/**
* 成员UserID
*/
private String UserId;
/**
* 成员DeviceId
*/
private String DeviceId;
/**
* 只有手机和邮箱没有匹配时返回,扫码登录时会有此字段
*/
private String userTicket;
/**
* userTicket的有效期(userTicket的有效期通过user_ticket获取用户信息时才会返回)
*/
private Integer expiresIn;
}
package com.metro.auth.platform.messagepush.service;
import com.alibaba.fastjson.JSONObject;
import com.metro.auth.platform.messagepush.model.request.MessageSubmitRequest;
/**
* 企业微信内网应用消息推送服务接口
*/
public interface MessagePushService {
/**
* 获取 AccessToken
*/
String getAccessToken();
/**
* 强制刷新 AccessToken
*/
String refreshAccessToken();
/**
* 获取部门列表
*
* @param id 部门id,不传则获取全部
*/
JSONObject getDepartmentList(Integer id);
/**
* 获取部门用户详情列表
*
* @param departmentId 部门id
* @param fetchChild 是否递归获取子部门(1=递归,0=不递归)
*/
JSONObject getUserListByDepartment(Integer departmentId, Integer fetchChild);
/**
* 获取应用后台免登用户信息(OAuth2)
*
* @param code 授权码
*/
JSONObject getUserInfoByCode(String code);
/**
* 根据授权码获取完整用户信息
*
* @param code 授权码
*/
JSONObject getUserDetailByCode(String code);
/**
* 发送应用消息
*
* @param messageJson 消息 JSON
*/
JSONObject sendMessage(String messageJson);
/**
* 发送应用消息
*
* @param request 消息提交请求
*/
JSONObject submitMessage(MessageSubmitRequest request);
}
package com.metro.auth.platform.messagepush.service.impl;
import com.alibaba.fastjson.JSONObject;
import com.metro.auth.platform.messagepush.model.request.MessageSubmitRequest;
import com.metro.auth.platform.messagepush.service.MessagePushService;
import com.metro.auth.platform.messagepush.util.WeChatApiUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
/**
* 企业微信内网应用消息推送服务实现
*/
@Slf4j
@Service
public class MessagePushServiceImpl implements MessagePushService {
@Resource
private WeChatApiUtil weChatApiUtil;
@Override
public String getAccessToken() {
return weChatApiUtil.getAccessToken();
}
@Override
public String refreshAccessToken() {
return weChatApiUtil.refreshAccessToken();
}
@Override
public JSONObject getDepartmentList(Integer id) {
return weChatApiUtil.getDepartmentList(id);
}
@Override
public JSONObject getUserListByDepartment(Integer departmentId, Integer fetchChild) {
return weChatApiUtil.getUserListByDepartment(departmentId, fetchChild);
}
@Override
public JSONObject getUserInfoByCode(String code) {
return weChatApiUtil.getUserInfoByCode(code);
}
@Override
public JSONObject getUserDetailByCode(String code) {
return weChatApiUtil.getUserDetailByCode(code);
}
@Override
public JSONObject sendMessage(String messageJson) {
return weChatApiUtil.sendMessage(messageJson);
}
@Override
public JSONObject submitMessage(MessageSubmitRequest request) {
if (request == null) {
return null;
}
// 构建企业微信消息格式
JSONObject message = new JSONObject();
message.put("touser", request.getUserId());
message.put("msgtype", request.getMessageType());
message.put("agentid", Integer.parseInt(weChatApiUtil.getWeChatConfig().getWxAgentId()));
// 根据消息类型构建对应的消息体
switch (request.getMessageType()) {
case "text":
JSONObject text = new JSONObject();
text.put("content", request.getContent());
message.put("text", text);
break;
case "textcard":
JSONObject textcard = new JSONObject();
textcard.put("title", request.getTitle());
textcard.put("description", request.getContent());
textcard.put("url", request.getTargetUrl());
// msg_data 可能包含 btntxt
if (request.getMsgData() != null) {
if (request.getMsgData() instanceof JSONObject) {
JSONObject msgData = (JSONObject) request.getMsgData();
if (msgData.containsKey("btntxt")) {
textcard.put("btntxt", msgData.getString("btntxt"));
}
}
} else {
textcard.put("btntxt", "查看详情");
}
message.put("textcard", textcard);
break;
case "markdown":
JSONObject markdown = new JSONObject();
if (request.getMsgData() != null && request.getMsgData() instanceof JSONObject) {
markdown.putAll((JSONObject) request.getMsgData());
}
message.put("markdown", markdown);
break;
case "image":
JSONObject image = new JSONObject();
if (request.getMsgData() != null && request.getMsgData() instanceof JSONObject) {
image.putAll((JSONObject) request.getMsgData());
}
message.put("image", image);
break;
case "file":
JSONObject file = new JSONObject();
if (request.getMsgData() != null && request.getMsgData() instanceof JSONObject) {
file.putAll((JSONObject) request.getMsgData());
}
message.put("file", file);
break;
case "news":
case "mpnews":
JSONObject news = new JSONObject();
if (request.getMsgData() != null && request.getMsgData() instanceof JSONObject) {
news.putAll((JSONObject) request.getMsgData());
}
message.put(request.getMessageType(), news);
break;
default:
// 其他消息类型按通用格式处理
if (request.getMsgData() != null && request.getMsgData() instanceof JSONObject) {
message.put(request.getMessageType(), request.getMsgData());
}
break;
}
return weChatApiUtil.sendMessage(message.toJSONString());
}
}
package com.metro.auth.platform.messagepush.util;
import com.alibaba.fastjson.JSONObject;
import com.metro.auth.platform.http.HttpAPIService;
import com.metro.auth.platform.messagepush.config.WeChatConfig;
import com.metro.auth.platform.redis.RedisUtils;
import com.metro.auth.platform.wxmessage.ApiConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
/**
* 企业微信内网应用 API 调用工具类
*/
@Slf4j
@Component
public class WeChatApiUtil {
private static final String ACCESS_TOKEN_KEY = "wechat:access_token";
private static final long ACCESS_TOKEN_EXPIRE = 7100L;
@Resource
private WeChatConfig weChatConfig;
@Resource
private HttpAPIService httpAPIService;
@Resource
private RedisUtils redisUtils;
/**
* 获取 AccessToken(优先从 Redis 缓存获取)
*/
public String getAccessToken() {
Object cached = redisUtils.get(ACCESS_TOKEN_KEY);
if (cached != null && StringUtils.hasLength(cached.toString())) {
log.debug("从Redis缓存获取access_token");
return cached.toString();
}
log.debug("Redis缓存未命中,从API获取access_token");
return refreshAccessToken();
}
/**
* 强制刷新 AccessToken
*/
public String refreshAccessToken() {
String url = weChatConfig.getWxTokenUrl()
+ "?corpid=" + weChatConfig.getWxCorpId()
+ "&corpsecret=" + weChatConfig.getWxSecret();
try {
String result = httpAPIService.doGet(url);
if (result != null) {
JSONObject json = JSONObject.parseObject(result);
String token = json.getString("access_token");
Integer expiresIn = json.getInteger("expires_in");
if (StringUtils.hasLength(token)) {
redisUtils.set(ACCESS_TOKEN_KEY, token, expiresIn != null ? expiresIn : ACCESS_TOKEN_EXPIRE, TimeUnit.SECONDS);
log.info("access_token刷新成功: {}", token);
return token;
}
}
} catch (Exception e) {
log.error("刷新access_token失败", e);
}
return null;
}
/**
* 获取部门列表
*
* @param id 部门id,不传则获取全部
*/
public JSONObject getDepartmentList(Integer id) {
String token = getAccessToken();
String url = weChatConfig.getWxTxGetList() + token;
if (id != null) {
url += "&id=" + id;
}
String result = httpAPIService.doGet(url);
return result != null ? JSONObject.parseObject(result) : null;
}
/**
* 获取部门用户详情列表
*
* @param departmentId 部门id
* @param fetchChild 是否递归获取子部门
*/
public JSONObject getUserListByDepartment(Integer departmentId, Integer fetchChild) {
if (departmentId == null) {
return null;
}
String token = getAccessToken();
String url = weChatConfig.getWxUserDeptDetail() + token
+ "&department_id=" + departmentId
+ "&fetch_child=" + (fetchChild != null ? fetchChild : 1);
String result = httpAPIService.doGet(url);
return result != null ? JSONObject.parseObject(result) : null;
}
/**
* 获取应用后台免登用户信息(OAuth2 授权码)
*
* @param code OAuth2 授权码
*/
public JSONObject getUserInfoByCode(String code) {
if (!StringUtils.hasLength(code)) {
return null;
}
String token = getAccessToken();
String url = weChatConfig.getWxGetUserInfo() + token + "&code=" + code;
String result = httpAPIService.doGet(url);
return result != null ? JSONObject.parseObject(result) : null;
}
/**
* 根据 code 获取完整用户信息
*
* @param code OAuth2 授权码
*/
public JSONObject getUserDetailByCode(String code) {
if (!StringUtils.hasLength(code)) {
return null;
}
String token = getAccessToken();
String url = weChatConfig.getWxGetUserByCode() + token + "&code=" + code;
String result = httpAPIService.doGet(url);
return result != null ? JSONObject.parseObject(result) : null;
}
/**
* 发送应用消息
*
* @param messageJson 消息 JSON
*/
public JSONObject sendMessage(String messageJson) {
String token = getAccessToken();
String url = weChatConfig.getWxSendMessageUrl() + token;
return httpAPIService.doPost(url, messageJson);
}
public WeChatConfig getWeChatConfig() {
return weChatConfig;
}
}
...@@ -182,6 +182,7 @@ weixin-params: ...@@ -182,6 +182,7 @@ weixin-params:
wx_token_url : https://qyapi.weixin.qq.com/cgi-bin/gettoken wx_token_url : https://qyapi.weixin.qq.com/cgi-bin/gettoken
wx_jstiket : https://qyapi.weixin.qq.com/cgi-bin/get_jsapi_ticket?access_token= wx_jstiket : https://qyapi.weixin.qq.com/cgi-bin/get_jsapi_ticket?access_token=
wx_getuserinfo : https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo?access_token= wx_getuserinfo : https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo?access_token=
wx_getuser_bycode : https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo_bycode?access_token=
wx_all_userinfo : https://qyapi.weixin.qq.com/cgi-bin/user/get?access_token= wx_all_userinfo : https://qyapi.weixin.qq.com/cgi-bin/user/get?access_token=
#微信通讯录管理 #微信通讯录管理
#获取通讯录管理secret #获取通讯录管理secret
......
...@@ -185,6 +185,7 @@ weixin-params: ...@@ -185,6 +185,7 @@ weixin-params:
wx_token_url : https://qyapi.weixin.qq.com/cgi-bin/gettoken wx_token_url : https://qyapi.weixin.qq.com/cgi-bin/gettoken
wx_jstiket : https://qyapi.weixin.qq.com/cgi-bin/get_jsapi_ticket?access_token= wx_jstiket : https://qyapi.weixin.qq.com/cgi-bin/get_jsapi_ticket?access_token=
wx_getuserinfo : https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo?access_token= wx_getuserinfo : https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo?access_token=
wx_getuser_bycode : https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo_bycode?access_token=
wx_all_userinfo : https://qyapi.weixin.qq.com/cgi-bin/user/get?access_token= wx_all_userinfo : https://qyapi.weixin.qq.com/cgi-bin/user/get?access_token=
#微信通讯录管理 #微信通讯录管理
#获取通讯录管理secret #获取通讯录管理secret
......
...@@ -159,6 +159,7 @@ weixin-params: ...@@ -159,6 +159,7 @@ weixin-params:
wx_token_url : https://qyapi.weixin.qq.com/cgi-bin/gettoken wx_token_url : https://qyapi.weixin.qq.com/cgi-bin/gettoken
wx_jstiket : https://qyapi.weixin.qq.com/cgi-bin/get_jsapi_ticket?access_token= wx_jstiket : https://qyapi.weixin.qq.com/cgi-bin/get_jsapi_ticket?access_token=
wx_getuserinfo : https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo?access_token= wx_getuserinfo : https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo?access_token=
wx_getuser_bycode : https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo_bycode?access_token=
wx_all_userinfo : https://qyapi.weixin.qq.com/cgi-bin/user/get?access_token= wx_all_userinfo : https://qyapi.weixin.qq.com/cgi-bin/user/get?access_token=
#微信通讯录管理 #微信通讯录管理
#获取通讯录管理secret #获取通讯录管理secret
......
...@@ -183,6 +183,7 @@ weixin-params: ...@@ -183,6 +183,7 @@ weixin-params:
wx_token_url : https://qyapi.weixin.qq.com/cgi-bin/gettoken wx_token_url : https://qyapi.weixin.qq.com/cgi-bin/gettoken
wx_jstiket : https://qyapi.weixin.qq.com/cgi-bin/get_jsapi_ticket?access_token= wx_jstiket : https://qyapi.weixin.qq.com/cgi-bin/get_jsapi_ticket?access_token=
wx_getuserinfo : https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo?access_token= wx_getuserinfo : https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo?access_token=
wx_getuser_bycode : https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo_bycode?access_token=
wx_all_userinfo : https://qyapi.weixin.qq.com/cgi-bin/user/get?access_token= wx_all_userinfo : https://qyapi.weixin.qq.com/cgi-bin/user/get?access_token=
#微信通讯录管理 #微信通讯录管理
#获取通讯录管理secret #获取通讯录管理secret
......
...@@ -185,6 +185,7 @@ weixin-params: ...@@ -185,6 +185,7 @@ weixin-params:
wx_token_url : https://qyapi.weixin.qq.com/cgi-bin/gettoken wx_token_url : https://qyapi.weixin.qq.com/cgi-bin/gettoken
wx_jstiket : https://qyapi.weixin.qq.com/cgi-bin/get_jsapi_ticket?access_token= wx_jstiket : https://qyapi.weixin.qq.com/cgi-bin/get_jsapi_ticket?access_token=
wx_getuserinfo : https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo?access_token= wx_getuserinfo : https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo?access_token=
wx_getuser_bycode : https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo_bycode?access_token=
wx_all_userinfo : https://qyapi.weixin.qq.com/cgi-bin/user/get?access_token= wx_all_userinfo : https://qyapi.weixin.qq.com/cgi-bin/user/get?access_token=
#微信通讯录管理 #微信通讯录管理
#获取通讯录管理secret #获取通讯录管理secret
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论