feat(qywx): 新增企业微信相关功能及定时任务
- 在WeixinConstants中添加agentid和corpid常量 - 扩展QywxApiUtil的sendNewsMessage方法,支持toparty和totag参数 - 在ShopController中新增企业微信授权重定向接口 - 在ReturnApprovalApplicationService中添加发送退货审核通知的功能 - 在QywxScheduleJob中新增定时获取企业授权信息的任务
This commit is contained in:
parent
2453b7bea7
commit
8e6131c197
|
@ -9,8 +9,12 @@ import com.agileboot.domain.qywx.api.QywxApiUtil;
|
|||
import com.agileboot.domain.qywx.api.response.DepartmentInfoResponse;
|
||||
import com.agileboot.domain.qywx.api.response.DepartmentInfoResponse.Department;
|
||||
import com.agileboot.domain.qywx.api.response.DepartmentListResponse;
|
||||
import com.agileboot.domain.qywx.api.response.GetAuthInfoResult;
|
||||
import com.agileboot.domain.qywx.api.response.GetAuthInfoResult.Agent;
|
||||
import com.agileboot.domain.qywx.api.response.GetAuthInfoResult.AuthCorpInfo;
|
||||
import com.agileboot.domain.qywx.api.response.UserListResponse;
|
||||
import com.agileboot.domain.qywx.authCorpInfo.AuthCorpInfoApplicationService;
|
||||
import com.agileboot.domain.qywx.authCorpInfo.command.UpdateAuthCorpInfoCommand;
|
||||
import com.agileboot.domain.qywx.authCorpInfo.db.QyAuthCorpInfoEntity;
|
||||
import com.agileboot.domain.qywx.department.DepartmentApplicationService;
|
||||
import com.agileboot.domain.qywx.department.command.AddDepartmentCommand;
|
||||
|
@ -46,6 +50,15 @@ import org.springframework.beans.BeanUtils;
|
|||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 企业微信定时任务调度器
|
||||
* 包含以下核心功能:
|
||||
* 1. 定时获取企业微信应用凭证(suite_access_token)
|
||||
* 2. 定时刷新企业微信访问令牌(access_token)
|
||||
* 3. 定时同步企业微信组织架构到本地数据库
|
||||
* 4. 定时同步企业微信用户信息到本地数据库
|
||||
* 定时任务均采用每小时固定分钟数执行策略,避免接口调用过于频繁
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@Component
|
||||
@Slf4j
|
||||
|
@ -59,8 +72,25 @@ public class QywxScheduleJob {
|
|||
private final UserModelFactory userModelFactory;
|
||||
|
||||
// private static final String appid = "QYTONG_YS_WXSHOP";
|
||||
/**
|
||||
* 企业微信应用ID常量
|
||||
* 用于标识当前集成的第三方应用,对应企业微信服务商后台配置的应用凭证
|
||||
*/
|
||||
private static final String appid2 = "QWTONG_YS_WXSHOP";
|
||||
|
||||
/**
|
||||
* 定时获取第三方应用凭证(suite_access_token)
|
||||
* 功能说明:
|
||||
* 1. 每小时第10分钟执行一次
|
||||
* 2. 根据应用ID获取企业微信应用配置
|
||||
* 3. 调用企业微信API获取最新的suite_access_token
|
||||
* 4. 更新本地存储的凭证信息
|
||||
* 执行流程:
|
||||
* - 通过appid查询应用模板配置
|
||||
* - 使用suite_id、secret和suite_ticket获取新凭证
|
||||
* - 将新凭证更新至数据库
|
||||
* 异常处理:捕获并记录异常日志,保证任务继续执行
|
||||
*/
|
||||
@Scheduled(cron = "0 10 * * * *")
|
||||
public void getSuiteAccessTokenTask() {
|
||||
/*try {
|
||||
|
@ -97,6 +127,18 @@ public class QywxScheduleJob {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 定时刷新访问令牌(access_token)
|
||||
* 功能说明:
|
||||
* 1. 每小时第20分钟执行一次
|
||||
* 2. 遍历所有已授权企业
|
||||
* 3. 为每个企业获取新的access_token
|
||||
* 4. 更新本地存储的访问令牌
|
||||
* 关键参数:
|
||||
* - corpid:企业微信ID
|
||||
* - permanent_code:企业永久授权码
|
||||
* 安全机制:不同企业间凭证隔离存储
|
||||
*/
|
||||
@Scheduled(cron = "0 20 * * * *")
|
||||
public void getAccessTokenTask() {
|
||||
/*try {
|
||||
|
@ -133,6 +175,18 @@ public class QywxScheduleJob {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 定时同步组织架构信息
|
||||
* 功能说明:
|
||||
* 1. 每小时第30分钟执行一次
|
||||
* 2. 获取企业微信部门列表
|
||||
* 3. 对比本地数据库进行增量同步
|
||||
* 同步策略:
|
||||
* - 新增:企业微信存在但本地不存在的部门
|
||||
* - 更新:名称或父部门发生变化的部门
|
||||
* - 删除:本地存在但企业微信不存在的部门
|
||||
* 数据一致性:通过corpid保证多企业数据隔离
|
||||
*/
|
||||
@Scheduled(cron = "0 30 * * * *")
|
||||
public void syncDepartmentInfoTask() {
|
||||
/*try {
|
||||
|
@ -295,6 +349,18 @@ public class QywxScheduleJob {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 定时同步用户信息
|
||||
* 功能说明:
|
||||
* 1. 每小时第40分钟执行一次
|
||||
* 2. 按部门遍历企业微信用户
|
||||
* 3. 三向对比(本地/企业微信/部门)进行同步
|
||||
* 同步维度:
|
||||
* - 基础信息:姓名、手机、邮箱等
|
||||
* - 组织关系:所属部门、职位信息
|
||||
* - 状态变更:离职/禁用用户
|
||||
* 数据关联:通过userid保持用户唯一标识
|
||||
*/
|
||||
@Scheduled(cron = "0 40 * * * *")
|
||||
public void syncUserInfoTask() {
|
||||
/*try {
|
||||
|
@ -503,6 +569,91 @@ public class QywxScheduleJob {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 定时获取企业授权信息
|
||||
* 功能说明:
|
||||
* 1. 每小时第45分钟执行一次
|
||||
* 2. 通过永久授权码获取企业基本信息、应用权限及授权管理员列表
|
||||
* 3. 更新本地存储的授权企业信息
|
||||
* 关键参数:
|
||||
* - corpid:企业微信ID
|
||||
* - permanent_code:企业永久授权码
|
||||
* API对应关系:
|
||||
* - 企业微信「获取企业授权信息」接口
|
||||
* 异常处理:捕获并记录异常日志,保证任务继续执行
|
||||
*/
|
||||
@Scheduled(cron = "0 45 * * * *")
|
||||
public void getAuthInfoTask() {
|
||||
try {
|
||||
getAuthInfo(appid2);
|
||||
} catch (Exception e) {
|
||||
log.error("getAuthInfoTask error appid: " + appid2, e);
|
||||
}
|
||||
}
|
||||
|
||||
public void getAuthInfo(String appid) {
|
||||
log.info("getAuthInfo Current Thread : {}, Fixed Rate Task : The time is now {}",
|
||||
Thread.currentThread().getName(), DateUtil.formatTime(new Date()));
|
||||
|
||||
try {
|
||||
List<QyAuthCorpInfoEntity> authCorpInfoList = authCorpInfoApplicationService.getByAppid(appid);
|
||||
QyTemplateEntity template = templateApplicationService.getByAppid(appid);
|
||||
|
||||
if (template == null || authCorpInfoList == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String suiteAccessToken = template.getSuiteAccessToken();
|
||||
if (StringUtils.isBlank(suiteAccessToken)) {
|
||||
log.error("getAuthInfo suiteAccessToken is null");
|
||||
return;
|
||||
}
|
||||
|
||||
for (QyAuthCorpInfoEntity authCorp : authCorpInfoList) {
|
||||
GetAuthInfoResult result = QywxApiUtil.getAuthInfo(suiteAccessToken,
|
||||
authCorp.getCorpid(), authCorp.getPermanentCode());
|
||||
|
||||
if (result.getErrcode() == 0) {
|
||||
Agent agent = result.getAuth_info().getAgent().get(0);
|
||||
AuthCorpInfo authCorpInfo = result.getAuth_corp_info();
|
||||
|
||||
UpdateAuthCorpInfoCommand command = new UpdateAuthCorpInfoCommand();
|
||||
command.setId(authCorp.getId());
|
||||
command.setCorpType(authCorpInfo.getCorp_type());
|
||||
command.setCorpSquareLogoUrl(authCorpInfo.getCorp_square_logo_url());
|
||||
command.setCorpUserMax(String.valueOf(authCorpInfo.getCorp_user_max()));
|
||||
command.setCorpFullName(authCorpInfo.getCorp_full_name());
|
||||
command.setSubjectType(String.valueOf(authCorpInfo.getSubject_type()));
|
||||
command.setVerifiedEndTime(String.valueOf(authCorpInfo.getVerified_end_time()));
|
||||
command.setCorpScale(authCorpInfo.getCorp_scale());
|
||||
command.setCorpIndustry(authCorpInfo.getCorp_industry());
|
||||
command.setCorpSubIndustry(authCorpInfo.getCorp_sub_industry());
|
||||
|
||||
command.setAgentid(String.valueOf(agent.getAgentid()));
|
||||
command.setAgentname(agent.getName());
|
||||
|
||||
authCorpInfoApplicationService.updateAuthCorpInfo(command);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("getAuthInfo error", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 定时同步用户绑定关系
|
||||
* 功能说明:
|
||||
* 1. 每小时第50分钟执行一次
|
||||
* 2. 根据企业微信通讯录与本地用户数据匹配
|
||||
* 3. 维护系统用户与企业微信用户的映射关系
|
||||
* 同步维度:
|
||||
* - 用户状态变更(离职/禁用)
|
||||
* - 部门架构变动
|
||||
* - 系统账户匹配规则(手机号/邮箱)
|
||||
* 数据关联:通过userid与系统用户表建立外键关联
|
||||
* 异常处理:单个企业同步失败不影响其他企业执行
|
||||
*/
|
||||
@Scheduled(cron = "0 50 * * * *")
|
||||
public void syncUserBindingsTask() {
|
||||
try {
|
||||
|
|
|
@ -14,12 +14,15 @@ import org.springframework.web.bind.annotation.GetMapping;
|
|||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.view.RedirectView;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/shop")
|
||||
@CrossOrigin(origins = "*", allowedHeaders = "*")
|
||||
@RequiredArgsConstructor
|
||||
public class ShopController {
|
||||
|
||||
private final GoodsApplicationService goodsApplicationService;
|
||||
private final CategoryApplicationService categoryApplicationService;
|
||||
|
||||
|
@ -50,4 +53,33 @@ public class ShopController {
|
|||
+ "&state=STATE#wechat_redirect";
|
||||
return new RedirectView(authUrl);
|
||||
}
|
||||
|
||||
@GetMapping("/qy/wechatAuth")
|
||||
public RedirectView qyWechatAuthRedirect() {
|
||||
String authUrl = "https://open.weixin.qq.com/connect/oauth2/authorize"
|
||||
+ "?appid=" + WeixinConstants.corpid
|
||||
+ "&redirect_uri=http%3A%2F%2Fwxshop.ab98.cn%2Fshop-api%2Fapi%2Fshop%2FapprovalRedirect"
|
||||
+ "&response_type=code"
|
||||
+ "&scope=snsapi_base"
|
||||
+ "&state=STATE"
|
||||
+ "&agentid=" + WeixinConstants.agentid
|
||||
+ "#wechat_redirect";
|
||||
return new RedirectView(authUrl);
|
||||
}
|
||||
|
||||
@GetMapping("/approvalRedirect")
|
||||
public RedirectView approvalRedirect(HttpServletRequest request) {
|
||||
UriComponentsBuilder builder = UriComponentsBuilder
|
||||
.fromHttpUrl("http://wxshop.ab98.cn/shop#/approval/list")
|
||||
.queryParam("corpid", WeixinConstants.corpid)
|
||||
.queryParam("device", "APP");
|
||||
|
||||
request.getParameterMap().forEach((key, values) -> {
|
||||
if (!"corpid".equals(key) && !"device".equals(key)) {
|
||||
builder.queryParam(key, (Object[]) values);
|
||||
}
|
||||
});
|
||||
|
||||
return new RedirectView(builder.build().encode().toUriString());
|
||||
}
|
||||
}
|
|
@ -5,4 +5,6 @@ public class WeixinConstants {
|
|||
// public static String secret = "2a5a8b6ad3654a05f9fdd36524279a50";
|
||||
public static String appid = "wx9922dfbb0d4cd7bb";
|
||||
public static String secret = "7c7ef0dbb90b6be2abc8c269357f980a";
|
||||
public static String agentid = "1000231";
|
||||
public static String corpid = "wpZ1ZrEgAA2QTxIRcB4cMtY7hQbTcPAw";
|
||||
}
|
||||
|
|
|
@ -28,11 +28,13 @@ public class QywxApiUtil {
|
|||
* @param articles 图文条目列表
|
||||
* @return 消息发送结果
|
||||
*/
|
||||
public static NewsMessageResponse sendNewsMessage(String accessToken, Integer agentId, String toUser, List<NewsArticle> articles) {
|
||||
String url = "https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=" + accessToken;
|
||||
public static NewsMessageResponse sendNewsMessage(String accessToken, Integer agentId, String toUser, String toparty, String totag, List<NewsArticle> articles) {
|
||||
String url = "https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=" + accessToken + "&debug=" + 1;
|
||||
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("touser", toUser);
|
||||
params.put("toparty", toparty);
|
||||
params.put("totag", totag);
|
||||
params.put("msgtype", "news");
|
||||
params.put("agentid", agentId);
|
||||
params.put("news", Collections.singletonMap("articles", JSONUtil.parse(articles)));
|
||||
|
@ -136,6 +138,14 @@ public class QywxApiUtil {
|
|||
return JSONUtil.toBean(response, OpenidResponse.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过临时授权码获取企业微信用户ID
|
||||
*
|
||||
* @param accessToken 企业微信API访问凭证(需从企业微信后台获取)
|
||||
* @param code 用户授权后获取的临时授权码(通过OAuth2.0流程获取)
|
||||
* @return 企业微信用户的唯一标识userid
|
||||
* @throws ApiException 当请求失败或返回错误码时抛出异常
|
||||
*/
|
||||
public static String getQyUserid(String accessToken, String code) {
|
||||
try {
|
||||
String url = String.format(
|
||||
|
@ -158,6 +168,13 @@ public class QywxApiUtil {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取企业微信授权信息
|
||||
* @param suiteAccessToken 第三方应用凭证(从企业微信后台获取)
|
||||
* @param corpid 授权方企业微信ID
|
||||
* @param permanentCode 永久授权码(通过授权流程获取)
|
||||
* @return 包含企业授权信息的响应对象(包含agentid、权限集等信息)
|
||||
*/
|
||||
public static GetAuthInfoResult getAuthInfo(String suiteAccessToken, String corpid, String permanentCode) {
|
||||
String url = "https://qyapi.weixin.qq.com/cgi-bin/service/v2/get_auth_info?suite_access_token="+suiteAccessToken;
|
||||
|
||||
|
|
|
@ -3,6 +3,12 @@ package com.agileboot.domain.shop.approval;
|
|||
import com.agileboot.common.constant.PayApiConstants;
|
||||
import com.agileboot.common.core.page.PageDTO;
|
||||
import com.agileboot.domain.common.command.BulkOperationCommand;
|
||||
import com.agileboot.domain.qywx.accessToken.AccessTokenApplicationService;
|
||||
import com.agileboot.domain.qywx.accessToken.db.QyAccessTokenEntity;
|
||||
import com.agileboot.domain.qywx.api.QywxApiUtil;
|
||||
import com.agileboot.domain.qywx.api.response.NewsArticle;
|
||||
import com.agileboot.domain.qywx.authCorpInfo.AuthCorpInfoApplicationService;
|
||||
import com.agileboot.domain.qywx.authCorpInfo.db.QyAuthCorpInfoEntity;
|
||||
import com.agileboot.domain.shop.approval.command.AddReturnApprovalCommand;
|
||||
import com.agileboot.domain.shop.approval.command.UpdateReturnApprovalCommand;
|
||||
import com.agileboot.domain.shop.approval.db.ReturnApprovalEntity;
|
||||
|
@ -25,6 +31,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
@ -48,6 +55,8 @@ public class ReturnApprovalApplicationService {
|
|||
private final OrderModelFactory orderModelFactory;
|
||||
private final PaymentApplicationService paymentApplicationService;
|
||||
private final GoodsModelFactory goodsModelFactory;
|
||||
private final AuthCorpInfoApplicationService authCorpInfoApplicationService;
|
||||
private final AccessTokenApplicationService accessTokenApplicationService;
|
||||
|
||||
/**
|
||||
* 获取退货审批列表
|
||||
|
@ -186,6 +195,32 @@ public class ReturnApprovalApplicationService {
|
|||
orderGoodsModel.setStatus(5);
|
||||
orderGoodsModel.updateById();
|
||||
|
||||
// 发送审核消息
|
||||
try {
|
||||
String appid = "QWTONG_YS_WXSHOP";
|
||||
List<QyAuthCorpInfoEntity> authCorpInfoList = authCorpInfoApplicationService.getByAppid(appid);
|
||||
QyAuthCorpInfoEntity authCorpInfo = authCorpInfoList.stream()
|
||||
.filter(a -> "wpZ1ZrEgAA2QTxIRcB4cMtY7hQbTcPAw".equals(a.getCorpid()))
|
||||
.findFirst().orElse(null);
|
||||
QyAccessTokenEntity accessToken = accessTokenApplicationService.getByAppid(appid, authCorpInfo.getCorpid());
|
||||
// TODO 获取用户ID
|
||||
String toUser = "woZ1ZrEgAAV9AEdRt1MGQxSg-KDJrDlA|woZ1ZrEgAAoFQl9vWHMj4PkFoSc8FR8w";
|
||||
String toparty = "";
|
||||
String totag = "";
|
||||
List<NewsArticle> articles = new ArrayList<>();
|
||||
NewsArticle article = new NewsArticle();
|
||||
article.setTitle("退货审核通知");
|
||||
article.setDescription("退还商品:" + orderGoods.getGoodsName());
|
||||
article.setPicurl(orderGoods.getCoverImg());
|
||||
article.setUrl("http://wxshop.ab98.cn/shop-api/api/shop/qy/wechatAuth");
|
||||
articles.add(article);
|
||||
|
||||
QywxApiUtil.sendNewsMessage(accessToken.getAccessToken(), Integer.valueOf(authCorpInfo.getAgentid()),
|
||||
toUser, toparty, totag, articles);
|
||||
} catch (Exception e) {
|
||||
log.error("发送退货审核通知失败", e);
|
||||
}
|
||||
|
||||
return returnApprovalModel.selectById();
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue