From 8e6131c1979621e19d0db3fb06af8a292c4935c5 Mon Sep 17 00:00:00 2001 From: dzq Date: Mon, 14 Apr 2025 16:54:46 +0800 Subject: [PATCH] =?UTF-8?q?feat(qywx):=20=E6=96=B0=E5=A2=9E=E4=BC=81?= =?UTF-8?q?=E4=B8=9A=E5=BE=AE=E4=BF=A1=E7=9B=B8=E5=85=B3=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=8F=8A=E5=AE=9A=E6=97=B6=E4=BB=BB=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在WeixinConstants中添加agentid和corpid常量 - 扩展QywxApiUtil的sendNewsMessage方法,支持toparty和totag参数 - 在ShopController中新增企业微信授权重定向接口 - 在ReturnApprovalApplicationService中添加发送退货审核通知的功能 - 在QywxScheduleJob中新增定时获取企业授权信息的任务 --- .../customize/service/QywxScheduleJob.java | 151 ++++++++++++++++++ .../api/controller/ShopController.java | 32 ++++ .../common/constant/WeixinConstants.java | 2 + .../domain/qywx/api/QywxApiUtil.java | 21 ++- .../ReturnApprovalApplicationService.java | 35 ++++ 5 files changed, 239 insertions(+), 2 deletions(-) diff --git a/agileboot-admin/src/main/java/com/agileboot/admin/customize/service/QywxScheduleJob.java b/agileboot-admin/src/main/java/com/agileboot/admin/customize/service/QywxScheduleJob.java index 84d5020..f5fbca5 100644 --- a/agileboot-admin/src/main/java/com/agileboot/admin/customize/service/QywxScheduleJob.java +++ b/agileboot-admin/src/main/java/com/agileboot/admin/customize/service/QywxScheduleJob.java @@ -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 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 { diff --git a/agileboot-api/src/main/java/com/agileboot/api/controller/ShopController.java b/agileboot-api/src/main/java/com/agileboot/api/controller/ShopController.java index 68f2807..2d2cd4e 100644 --- a/agileboot-api/src/main/java/com/agileboot/api/controller/ShopController.java +++ b/agileboot-api/src/main/java/com/agileboot/api/controller/ShopController.java @@ -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()); + } } \ No newline at end of file diff --git a/agileboot-common/src/main/java/com/agileboot/common/constant/WeixinConstants.java b/agileboot-common/src/main/java/com/agileboot/common/constant/WeixinConstants.java index 5970a0a..e181179 100644 --- a/agileboot-common/src/main/java/com/agileboot/common/constant/WeixinConstants.java +++ b/agileboot-common/src/main/java/com/agileboot/common/constant/WeixinConstants.java @@ -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"; } diff --git a/agileboot-domain/src/main/java/com/agileboot/domain/qywx/api/QywxApiUtil.java b/agileboot-domain/src/main/java/com/agileboot/domain/qywx/api/QywxApiUtil.java index de20b55..339a307 100644 --- a/agileboot-domain/src/main/java/com/agileboot/domain/qywx/api/QywxApiUtil.java +++ b/agileboot-domain/src/main/java/com/agileboot/domain/qywx/api/QywxApiUtil.java @@ -28,11 +28,13 @@ public class QywxApiUtil { * @param articles 图文条目列表 * @return 消息发送结果 */ - public static NewsMessageResponse sendNewsMessage(String accessToken, Integer agentId, String toUser, List 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 articles) { + String url = "https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=" + accessToken + "&debug=" + 1; Map 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; diff --git a/agileboot-domain/src/main/java/com/agileboot/domain/shop/approval/ReturnApprovalApplicationService.java b/agileboot-domain/src/main/java/com/agileboot/domain/shop/approval/ReturnApprovalApplicationService.java index 558b99c..e91400b 100644 --- a/agileboot-domain/src/main/java/com/agileboot/domain/shop/approval/ReturnApprovalApplicationService.java +++ b/agileboot-domain/src/main/java/com/agileboot/domain/shop/approval/ReturnApprovalApplicationService.java @@ -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 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 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(); }