|
|
@@ -0,0 +1,404 @@
|
|
|
+package com.fs.company.controller.wx;
|
|
|
+
|
|
|
+import com.alibaba.fastjson.JSONArray;
|
|
|
+import com.alibaba.fastjson.JSONObject;
|
|
|
+import com.fs.company.domain.CompanyConfig;
|
|
|
+import com.fs.company.domain.CompanyUser;
|
|
|
+import com.fs.company.service.ICompanyConfigService;
|
|
|
+import com.fs.core.config.WxMpProperties;
|
|
|
+import com.fs.framework.datasource.DynamicDataSourceContextHolder;
|
|
|
+import com.fs.framework.datasource.TenantDataSourceManager;
|
|
|
+import com.fs.tenant.domain.TenantInfo;
|
|
|
+import com.fs.tenant.service.TenantInfoService;
|
|
|
+import com.fs.wx.mp.service.WxMpSubscribeService;
|
|
|
+import com.fs.common.core.controller.BaseController;
|
|
|
+import com.fs.common.core.domain.AjaxResult;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import me.chanjar.weixin.mp.api.WxMpService;
|
|
|
+import me.chanjar.weixin.mp.api.impl.WxMpServiceImpl;
|
|
|
+import me.chanjar.weixin.mp.config.impl.WxMpDefaultConfigImpl;
|
|
|
+import org.apache.commons.lang3.StringUtils;
|
|
|
+import org.springframework.beans.factory.annotation.Autowired;
|
|
|
+import org.springframework.web.bind.annotation.*;
|
|
|
+
|
|
|
+import java.text.SimpleDateFormat;
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.Date;
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Map;
|
|
|
+import java.util.concurrent.ConcurrentHashMap;
|
|
|
+/**
|
|
|
+ * 微信公众号订阅通知控制器
|
|
|
+ * 提供给销售端(CompanyUI)调用的接口
|
|
|
+ * 所有接口路径包含 {tenantCode} 以支持多租户数据源切换
|
|
|
+ * 配置从 company_config 表读取(key: wx:mp:subscribe:config),无配置时回退到 sys_config
|
|
|
+ */
|
|
|
+@Slf4j
|
|
|
+@RestController
|
|
|
+@RequestMapping("/wxmp/subscribe/{tenantCode}")
|
|
|
+public class WxMpSubscribeController extends BaseController {
|
|
|
+
|
|
|
+ private static final String SUBSCRIBE_CONFIG_KEY = "wx:mp:subscribe:config";
|
|
|
+
|
|
|
+ /** 按tenantCode缓存WxMpService实例,避免每次请求都创建新实例导致access_token无法复用 */
|
|
|
+ private static final ConcurrentHashMap<String, WxMpService> WX_MP_SERVICE_CACHE = new ConcurrentHashMap<>();
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private WxMpSubscribeService wxMpSubscribeService;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private WxMpProperties wxMpProperties;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private TenantDataSourceManager tenantDataSourceManager;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private TenantInfoService tenantInfoService;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private ICompanyConfigService companyConfigService;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 根据tenantCode查询租户信息并切换数据源(用于匿名接口)
|
|
|
+ */
|
|
|
+ private void switchTenant(String tenantCode) {
|
|
|
+ if (StringUtils.isBlank(tenantCode)) {
|
|
|
+ log.warn("[WxMpSubscribe] tenantCode为空,无法切换数据源");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ TenantInfo tenantInfo = tenantInfoService.selectTenantInfoByCode(tenantCode);
|
|
|
+ if (tenantInfo != null) {
|
|
|
+ tenantDataSourceManager.ensureSwitchByTenantId(tenantInfo.getId());
|
|
|
+ } else {
|
|
|
+ log.warn("[WxMpSubscribe] 未找到tenantCode={}对应的租户信息", tenantCode);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 从company_config读取订阅配置JSON
|
|
|
+ * 数据源切换后,租户数据库中company_config按key查询即可
|
|
|
+ */
|
|
|
+ private JSONObject getSubscribeConfigJson() {
|
|
|
+ try {
|
|
|
+ CompanyConfig config = companyConfigService.selectCompanyConfigByServerKey(SUBSCRIBE_CONFIG_KEY);
|
|
|
+ if (config != null && StringUtils.isNotBlank(config.getConfigValue())) {
|
|
|
+ return JSONObject.parseObject(config.getConfigValue());
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("[WxMpSubscribe] 从company_config读取订阅配置失败", e);
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取当前请求的tenantCode(从数据源上下文推断)
|
|
|
+ * 缓存key使用 tenantCode + appId 组合,配置变更时自动失效
|
|
|
+ */
|
|
|
+ private String getCacheKey(String tenantCode, String appId) {
|
|
|
+ return tenantCode + ":" + appId;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 根据company_config获取WxMpService(带缓存),无配置时回退到sys_config
|
|
|
+ * 缓存WxMpService实例以复用access_token,避免每次请求都调用微信API
|
|
|
+ */
|
|
|
+ private WxMpService getSubscribeWxMpService(String tenantCode) {
|
|
|
+ JSONObject configJson = getSubscribeConfigJson();
|
|
|
+ if (configJson != null) {
|
|
|
+ String appId = configJson.getString("appId");
|
|
|
+ String secret = configJson.getString("secret");
|
|
|
+ if (StringUtils.isNotBlank(appId) && StringUtils.isNotBlank(secret)) {
|
|
|
+ String cacheKey = getCacheKey(tenantCode, appId);
|
|
|
+ WxMpService cached = WX_MP_SERVICE_CACHE.get(cacheKey);
|
|
|
+ if (cached != null) {
|
|
|
+ return cached;
|
|
|
+ }
|
|
|
+ WxMpDefaultConfigImpl wxConfig = new WxMpDefaultConfigImpl();
|
|
|
+ wxConfig.setAppId(appId);
|
|
|
+ wxConfig.setSecret(secret);
|
|
|
+ wxConfig.setToken(configJson.getString("token"));
|
|
|
+ wxConfig.setAesKey(configJson.getString("aesKey"));
|
|
|
+ WxMpService wxMpService = new WxMpServiceImpl();
|
|
|
+ wxMpService.setWxMpConfigStorage(wxConfig);
|
|
|
+ WX_MP_SERVICE_CACHE.put(cacheKey, wxMpService);
|
|
|
+ log.info("[WxMpSubscribe] 创建并缓存WxMpService实例,tenantCode={}, appId={}", tenantCode, appId);
|
|
|
+ return wxMpService;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 回退到sys_config(不缓存,因为sys_config的配置可能变化)
|
|
|
+ log.warn("[WxMpSubscribe] company_config中未找到订阅配置,回退到sys_config");
|
|
|
+ return wxMpProperties.createFirstWxMpService();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 清除指定租户的WxMpService缓存(配置变更时调用)
|
|
|
+ */
|
|
|
+ public static void clearWxMpServiceCache(String tenantCode) {
|
|
|
+ WX_MP_SERVICE_CACHE.keySet().removeIf(key -> key.startsWith(tenantCode + ":"));
|
|
|
+ log.info("[WxMpSubscribe] 已清除tenantCode={}的WxMpService缓存", tenantCode);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 从company_config获取订阅模板ID列表,无配置时回退到sys_config
|
|
|
+ */
|
|
|
+ private List<String> getSubscribeTemplateIdsFromConfig() {
|
|
|
+ JSONObject configJson = getSubscribeConfigJson();
|
|
|
+ if (configJson != null) {
|
|
|
+ JSONArray arr = configJson.getJSONArray("subscribeTemplateIds");
|
|
|
+ if (arr != null && !arr.isEmpty()) {
|
|
|
+ List<String> ids = new ArrayList<>();
|
|
|
+ for (int i = 0; i < arr.size(); i++) {
|
|
|
+ ids.add(arr.getString(i));
|
|
|
+ }
|
|
|
+ return ids;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 回退到sys_config
|
|
|
+ log.warn("[WxMpSubscribe] company_config中未找到订阅模板配置,回退到sys_config");
|
|
|
+ return wxMpProperties.getSubscribeTemplateIds();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 从company_config的pushTemplates中按类型查找推送模板
|
|
|
+ * 未匹配类型时返回第一个模板,未配置pushTemplates时返回null
|
|
|
+ *
|
|
|
+ * @param type 推送模板类型,如"个微AI转人工"
|
|
|
+ * @return 匹配的模板JSONObject(含templateId、type、title),或null
|
|
|
+ */
|
|
|
+ private JSONObject getPushTemplateByType(String type) {
|
|
|
+ JSONObject configJson = getSubscribeConfigJson();
|
|
|
+ if (configJson != null) {
|
|
|
+ JSONArray arr = configJson.getJSONArray("pushTemplates");
|
|
|
+ if (arr != null && !arr.isEmpty()) {
|
|
|
+ for (int i = 0; i < arr.size(); i++) {
|
|
|
+ JSONObject tmpl = arr.getJSONObject(i);
|
|
|
+ if (type.equals(tmpl.getString("type"))) {
|
|
|
+ return tmpl;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 未匹配类型时返回第一个
|
|
|
+ return arr.getJSONObject(0);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成订阅通知二维码(base64图片)
|
|
|
+ *
|
|
|
+ * @param tenantCode 租户编码
|
|
|
+ * @param userId 员工用户ID
|
|
|
+ */
|
|
|
+ @GetMapping("/qrcode/{userId}")
|
|
|
+ public AjaxResult generateQrCode(@PathVariable String tenantCode, @PathVariable Long userId) {
|
|
|
+ try {
|
|
|
+ switchTenant(tenantCode);
|
|
|
+ String base64Image = wxMpSubscribeService.generateSubscribeQrCode(userId, tenantCode);
|
|
|
+ return AjaxResult.success("生成二维码成功", base64Image);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("生成订阅二维码失败,tenantCode={}, userId={}", tenantCode, userId, e);
|
|
|
+ return AjaxResult.error("生成二维码失败:" + e.getMessage());
|
|
|
+ } finally {
|
|
|
+ DynamicDataSourceContextHolder.clearDataSourceType();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 查询员工订阅状态(匿名访问,由HTML页面调用)
|
|
|
+ */
|
|
|
+ @GetMapping("/status/{userId}")
|
|
|
+ public AjaxResult getSubscribeStatus(@PathVariable String tenantCode, @PathVariable Long userId) {
|
|
|
+ try {
|
|
|
+ switchTenant(tenantCode);
|
|
|
+ CompanyUser user = wxMpSubscribeService.getSubscribeStatus(userId);
|
|
|
+ if (user == null) {
|
|
|
+ return AjaxResult.error("未找到用户");
|
|
|
+ }
|
|
|
+ Map<String, Object> data = new HashMap<>();
|
|
|
+ data.put("mpOpenId", user.getMpOpenId());
|
|
|
+ data.put("mpSubscribed", user.getMpSubscribed());
|
|
|
+ data.put("bound", StringUtils.isNotBlank(user.getMpOpenId()));
|
|
|
+ return AjaxResult.success(data);
|
|
|
+ } finally {
|
|
|
+ DynamicDataSourceContextHolder.clearDataSourceType();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 确认订阅通知(匿名访问,由HTML页面调用)
|
|
|
+ */
|
|
|
+ @GetMapping("/confirm")
|
|
|
+ public AjaxResult confirmSubscribe(@PathVariable String tenantCode, @RequestParam Long userId) {
|
|
|
+ try {
|
|
|
+ switchTenant(tenantCode);
|
|
|
+ wxMpSubscribeService.confirmSubscribe(userId);
|
|
|
+ return AjaxResult.success("订阅成功");
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("确认订阅失败,tenantCode={}, userId={}", tenantCode, userId, e);
|
|
|
+ return AjaxResult.error("订阅失败:" + e.getMessage());
|
|
|
+ } finally {
|
|
|
+ DynamicDataSourceContextHolder.clearDataSourceType();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取微信公众号JS-SDK配置(匿名访问)
|
|
|
+ */
|
|
|
+ @GetMapping("/jsapiSignature")
|
|
|
+ public AjaxResult getJsapiSignature(@PathVariable String tenantCode, @RequestParam String url) {
|
|
|
+ try {
|
|
|
+ switchTenant(tenantCode);
|
|
|
+ WxMpService wxMpService = getSubscribeWxMpService(tenantCode);
|
|
|
+ me.chanjar.weixin.common.bean.WxJsapiSignature signature = wxMpService.createJsapiSignature(url);
|
|
|
+ Map<String, Object> data = new HashMap<>();
|
|
|
+ data.put("appId", signature.getAppId());
|
|
|
+ data.put("timestamp", signature.getTimestamp());
|
|
|
+ data.put("nonceStr", signature.getNonceStr());
|
|
|
+ data.put("signature", signature.getSignature());
|
|
|
+ return AjaxResult.success(data);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("获取JS-SDK签名失败,tenantCode={}, url={}", tenantCode, url, e);
|
|
|
+ return AjaxResult.error("获取签名失败:" + e.getMessage());
|
|
|
+ } finally {
|
|
|
+ DynamicDataSourceContextHolder.clearDataSourceType();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 通过微信OAuth code绑定openId(匿名访问,由HTML页面调用)
|
|
|
+ */
|
|
|
+ @GetMapping("/bindOpenId")
|
|
|
+ public AjaxResult bindOpenId(@PathVariable String tenantCode, @RequestParam String code, @RequestParam Long userId) {
|
|
|
+ try {
|
|
|
+ switchTenant(tenantCode);
|
|
|
+ WxMpService wxMpService = getSubscribeWxMpService(tenantCode);
|
|
|
+ String openId = wxMpSubscribeService.bindOpenIdByOAuthCode(code, userId, wxMpService);
|
|
|
+ if (openId != null) {
|
|
|
+ Map<String, Object> data = new HashMap<>();
|
|
|
+ data.put("openId", openId);
|
|
|
+ data.put("bound", true);
|
|
|
+ return AjaxResult.success("绑定成功", data);
|
|
|
+ }
|
|
|
+ return AjaxResult.error("绑定失败,未获取到openId");
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("OAuth绑定openId失败,tenantCode={}, code={}, userId={}", tenantCode, code, userId, e);
|
|
|
+ return AjaxResult.error("绑定失败:" + e.getMessage());
|
|
|
+ } finally {
|
|
|
+ DynamicDataSourceContextHolder.clearDataSourceType();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取微信公众号appId(匿名访问,用于HTML页面构建OAuth授权链接)
|
|
|
+ */
|
|
|
+ @GetMapping("/appId")
|
|
|
+ public AjaxResult getAppId(@PathVariable String tenantCode) {
|
|
|
+ try {
|
|
|
+ switchTenant(tenantCode);
|
|
|
+ WxMpService wxMpService = getSubscribeWxMpService(tenantCode);
|
|
|
+ Map<String, Object> data = new HashMap<>();
|
|
|
+ data.put("appId", wxMpService.getWxMpConfigStorage().getAppId());
|
|
|
+ return AjaxResult.success(data);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("获取appId失败,tenantCode={}", tenantCode, e);
|
|
|
+ return AjaxResult.error("获取appId失败");
|
|
|
+ } finally {
|
|
|
+ DynamicDataSourceContextHolder.clearDataSourceType();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取订阅消息模板ID列表(匿名访问,用于HTML页面调用wx.requestSubscribeMessage)
|
|
|
+ */
|
|
|
+ @GetMapping("/templateIds")
|
|
|
+ public AjaxResult getTemplateIds(@PathVariable String tenantCode) {
|
|
|
+ try {
|
|
|
+ switchTenant(tenantCode);
|
|
|
+ List<String> templateIds = getSubscribeTemplateIdsFromConfig();
|
|
|
+ Map<String, Object> data = new HashMap<>();
|
|
|
+ data.put("templateIds", templateIds);
|
|
|
+ return AjaxResult.success(data);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("获取模板ID失败,tenantCode={}", tenantCode, e);
|
|
|
+ return AjaxResult.error("获取模板ID失败");
|
|
|
+ } finally {
|
|
|
+ DynamicDataSourceContextHolder.clearDataSourceType();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 推送公众号订阅消息给已订阅的员工
|
|
|
+ * 需要登录认证,由销售端(CompanyUI)管理员操作
|
|
|
+ *
|
|
|
+ * @param tenantCode 租户编码
|
|
|
+ * @param params 请求参数:userId(员工ID), customerName(客户姓名), type(推送类型,如"个微AI转人工")
|
|
|
+ */
|
|
|
+ @PostMapping("/push")
|
|
|
+ public AjaxResult pushSubscribeMessage(@PathVariable String tenantCode,
|
|
|
+ @RequestBody Map<String, Object> params) {
|
|
|
+ try {
|
|
|
+ // 参数校验
|
|
|
+ Object userIdObj = params.get("userId");
|
|
|
+ String customerName = (String) params.get("customerName");
|
|
|
+ String type = (String) params.get("type");
|
|
|
+
|
|
|
+ if (userIdObj == null) {
|
|
|
+ return AjaxResult.error("缺少参数:userId");
|
|
|
+ }
|
|
|
+ if (StringUtils.isBlank(customerName)) {
|
|
|
+ return AjaxResult.error("缺少参数:customerName");
|
|
|
+ }
|
|
|
+ if (StringUtils.isBlank(type)) {
|
|
|
+ type = "个微AI转人工";
|
|
|
+ }
|
|
|
+
|
|
|
+ Long userId;
|
|
|
+ if (userIdObj instanceof Integer) {
|
|
|
+ userId = ((Integer) userIdObj).longValue();
|
|
|
+ } else if (userIdObj instanceof Long) {
|
|
|
+ userId = (Long) userIdObj;
|
|
|
+ } else {
|
|
|
+ userId = Long.parseLong(userIdObj.toString());
|
|
|
+ }
|
|
|
+
|
|
|
+ // thing类型字段限制20字符
|
|
|
+ if (customerName.length() > 20) {
|
|
|
+ customerName = customerName.substring(0, 20);
|
|
|
+ }
|
|
|
+
|
|
|
+ switchTenant(tenantCode);
|
|
|
+
|
|
|
+ // 从company_config查找推送模板
|
|
|
+ JSONObject pushTemplate = getPushTemplateByType(type);
|
|
|
+ if (pushTemplate == null || StringUtils.isBlank(pushTemplate.getString("templateId"))) {
|
|
|
+ return AjaxResult.error("未配置推送模板,请在「企业配置→订阅配置」中添加推送消息模板");
|
|
|
+ }
|
|
|
+ String templateId = pushTemplate.getString("templateId");
|
|
|
+
|
|
|
+ // 获取WxMpService(带缓存)
|
|
|
+ WxMpService wxMpService = getSubscribeWxMpService(tenantCode);
|
|
|
+
|
|
|
+ // 构建模板数据:thing2=客户姓名, time6=下单时间(当前时间)
|
|
|
+ String orderTime = new SimpleDateFormat("yyyy年MM月dd日 HH:mm").format(new Date());
|
|
|
+ Map<String, String> data = new HashMap<>();
|
|
|
+ data.put("thing2", customerName);
|
|
|
+ data.put("time6", orderTime);
|
|
|
+
|
|
|
+ boolean success = wxMpSubscribeService.sendSubscribePush(userId, wxMpService, templateId, data);
|
|
|
+ if (success) {
|
|
|
+ return AjaxResult.success("推送成功,员工将在微信中收到订阅消息通知");
|
|
|
+ } else {
|
|
|
+ return AjaxResult.error("推送失败");
|
|
|
+ }
|
|
|
+ } catch (RuntimeException e) {
|
|
|
+ log.error("[WxMpSubscribe] 推送订阅消息失败,tenantCode={}", tenantCode, e);
|
|
|
+ return AjaxResult.error(e.getMessage());
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("[WxMpSubscribe] 推送订阅消息异常,tenantCode={}", tenantCode, e);
|
|
|
+ return AjaxResult.error("推送异常:" + e.getMessage());
|
|
|
+ } finally {
|
|
|
+ DynamicDataSourceContextHolder.clearDataSourceType();
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|