diff --git a/cash-api/account-server/src/main/java/com/czg/controller/admin/CallTableController.java b/cash-api/account-server/src/main/java/com/czg/controller/admin/CallTableController.java index d7656985..36f0b75a 100644 --- a/cash-api/account-server/src/main/java/com/czg/controller/admin/CallTableController.java +++ b/cash-api/account-server/src/main/java/com/czg/controller/admin/CallTableController.java @@ -1,5 +1,6 @@ package com.czg.controller.admin; +import com.czg.account.dto.WxMsgSubDTO; import com.czg.account.dto.calltable.*; import com.czg.account.entity.CallConfig; import com.czg.account.entity.CallQueue; @@ -173,4 +174,15 @@ public class CallTableController { public CzgResult updateConfig(@RequestBody UpdateConfigDTO configDTO) { return CzgResult.success(callTableService.updateConfig(StpKit.USER.getShopId(), configDTO)); } + + /** + * 消息订阅 + * @return 是否成功 + */ + @PostMapping("subMsg") + public CzgResult subMsg( + @Validated @RequestBody CallSubMsgDTO subMsgDTO + ) { + return CzgResult.success(callTableService.subMsg(subMsgDTO)); + } } diff --git a/cash-common/cash-common-service/src/main/java/com/czg/account/dto/calltable/CallSubMsgDTO.java b/cash-common/cash-common-service/src/main/java/com/czg/account/dto/calltable/CallSubMsgDTO.java new file mode 100644 index 00000000..976a5599 --- /dev/null +++ b/cash-common/cash-common-service/src/main/java/com/czg/account/dto/calltable/CallSubMsgDTO.java @@ -0,0 +1,27 @@ +package com.czg.account.dto.calltable; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * @author Administrator + */ +@Data +public class CallSubMsgDTO { + /** + * 店铺id + */ + @NotNull + private Long shopId; + /** + * 队列id + */ + @NotNull + private Long queueId; + /** + * openId + */ + @NotEmpty + private String openId; +} diff --git a/cash-common/cash-common-service/src/main/java/com/czg/account/dto/calltable/CallTableNumDTO.java b/cash-common/cash-common-service/src/main/java/com/czg/account/dto/calltable/CallTableNumDTO.java index 3f66d6ef..b539961a 100644 --- a/cash-common/cash-common-service/src/main/java/com/czg/account/dto/calltable/CallTableNumDTO.java +++ b/cash-common/cash-common-service/src/main/java/com/czg/account/dto/calltable/CallTableNumDTO.java @@ -21,4 +21,5 @@ public class CallTableNumDTO { * 号码 */ private String callNum; + private Long queueId; } diff --git a/cash-common/cash-common-service/src/main/java/com/czg/account/service/CallTableService.java b/cash-common/cash-common-service/src/main/java/com/czg/account/service/CallTableService.java index c077d493..02535e14 100644 --- a/cash-common/cash-common-service/src/main/java/com/czg/account/service/CallTableService.java +++ b/cash-common/cash-common-service/src/main/java/com/czg/account/service/CallTableService.java @@ -36,4 +36,6 @@ public interface CallTableService extends IService { CallConfig getConfig(Long shopId); boolean updateConfig(Long shopId, UpdateConfigDTO configDTO); + + boolean subMsg(CallSubMsgDTO subMsgDTO); } diff --git a/cash-service/account-service/src/main/java/com/czg/service/account/service/impl/CallTableServiceImpl.java b/cash-service/account-service/src/main/java/com/czg/service/account/service/impl/CallTableServiceImpl.java index c7029113..d98af972 100644 --- a/cash-service/account-service/src/main/java/com/czg/service/account/service/impl/CallTableServiceImpl.java +++ b/cash-service/account-service/src/main/java/com/czg/service/account/service/impl/CallTableServiceImpl.java @@ -15,6 +15,7 @@ import com.czg.resp.CzgResult; import com.czg.service.account.mapper.CallQueueMapper; import com.czg.service.account.mapper.CallTableMapper; import com.czg.service.account.util.FunUtil; +import com.czg.service.account.util.WechatMiniMsgUtil; import com.czg.system.dto.SysParamsDTO; import com.czg.system.service.SysParamsService; import com.czg.utils.JoinQueryWrapper; @@ -62,6 +63,8 @@ public class CallTableServiceImpl extends ServiceImpl(config.getNearNum(), 1)).getRecords(); if (!nearList.isEmpty()) { CallQueue nearQueue = nearList.getFirst(); -// wxMiniUtils.sendCurrentOrNearCallMsg(shopInfo.getShopName(), getStrByState(Integer.valueOf(nearQueue.getState())), -// nearQueue.getCallNum(), current.isEmpty() ? "" : current.get(0).getCallNum(), "排号信息", nearQueue.getOpenId(), true); + miniMsgUtil.sendCurrentOrNearCallMsg(shopInfo.getShopName(), getStrByState(nearQueue.getState()), + nearQueue.getCallNum(), current.isEmpty() ? "" : current.getFirst().getCallNum(), "排号信息", nearQueue.getOpenId(), true); } return 1; } + private String getStrByState(Integer state) { + return switch (state) { + case -1 -> "已取消"; + case 0 -> "排队中"; + case 1 -> "已到号"; + case 3 -> "已过号"; + default -> ""; + }; + } @Override public boolean updateInfo(Long shopId, UpdateCallQueueDTO updateCallQueueDTO) { @@ -543,4 +555,40 @@ public class CallTableServiceImpl extends ServiceImpl 0) { + throw new ApiNotPrintException("您已订阅其他号码,请勿重复订阅"); + } + } + + queue.setSubState(1); + queue.setOpenId(subMsgDTO.getOpenId()); + return callQueueService.updateById(queue); + } } diff --git a/cash-service/account-service/src/main/java/com/czg/service/account/util/WechatMiniMsgUtil.java b/cash-service/account-service/src/main/java/com/czg/service/account/util/WechatMiniMsgUtil.java new file mode 100644 index 00000000..f68dbf1e --- /dev/null +++ b/cash-service/account-service/src/main/java/com/czg/service/account/util/WechatMiniMsgUtil.java @@ -0,0 +1,256 @@ +package com.czg.service.account.util; + +import cn.hutool.core.codec.Base64; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.symmetric.AES; +import cn.hutool.http.HttpRequest; +import cn.hutool.http.HttpUtil; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.czg.config.RedisCst; +import com.czg.resp.CzgResult; +import com.czg.service.RedisService; +import com.czg.system.dto.SysParamsDTO; +import com.czg.system.enums.SysParamCodeEnum; +import com.czg.system.service.SysParamsService; +import com.google.gson.JsonObject; +import jakarta.annotation.Resource; +import jakarta.validation.constraints.NotBlank; +import lombok.extern.slf4j.Slf4j; +import org.apache.dubbo.config.annotation.DubboReference; +import org.springframework.stereotype.Component; + +import java.io.InputStream; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * @author Administrator + */ +@Slf4j +@Component +public class WechatMiniMsgUtil { + @DubboReference(check = false) + private SysParamsService sysParamsService; + @Resource + private RedisService redisService; + @Resource + private AliOssUtil aliOssUtil; + + private static final String TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token"; + private static final String QR_CODE_URL = "https://api.weixin.qq.com/wxa/getwxacode"; + + + static LinkedHashMap linkedHashMap = new LinkedHashMap<>(); + + static { + + linkedHashMap.put("40001", "获取 access_token 时 AppSecret 错误,或者 access_token 无效。请开发者认真比对 AppSecret 的正确性,或查看是否正在为恰当的公众号调用接口"); + linkedHashMap.put("40003", "不合法的 OpenID ,请开发者确认 OpenID (该用户)是否已关注公众号,或是否是其他公众号的 OpenID"); + linkedHashMap.put("40014", "不合法的 access_token ,请开发者认真比对 access_token 的有效性(如是否过期),或查看是否正在为恰当的公众号调用接口"); + linkedHashMap.put("40037", "不合法的 template_id"); + linkedHashMap.put("43101", "用户未订阅消息"); + linkedHashMap.put("43107", "订阅消息能力封禁"); + linkedHashMap.put("43108", "并发下发消息给同一个粉丝"); + linkedHashMap.put("45168", "命中敏感词"); + linkedHashMap.put("47003", "参数错误"); + + } + + public JSONObject sendTempMsg(String tempId, String toUserOpenId, Map data, String note) { + log.info("开始发送" + note + "模板消息, 接收用户openId: {}, 消息数据: {}", toUserOpenId, data); + String token= getAccessToken(); + + JSONObject object1=new JSONObject(); + + object1.put("template_id", tempId); + object1.put("touser", toUserOpenId); + object1.put("data",data); + + object1.put("miniprogram_state","trial"); + object1.put("lang","zh_CN"); + + String response= HttpRequest.post("https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=".concat(token)).body(object1.toString()).execute().body(); + log.info("微信模板消息发送成功,相应内容:{}",response); + JSONObject resObj= JSONObject.parseObject(response); + if(ObjectUtil.isNotEmpty(resObj)&&ObjectUtil.isNotNull(resObj)&&"0".equals(resObj.get("errcode")+"")){ + return resObj; + } + + throw new RuntimeException(linkedHashMap.getOrDefault(resObj.get("errcode") + "", "未知错误")); + } + + public void sendCurrentOrNearCallMsg(String shopName, String state, String callNum, String currentNum, String note, String openId, boolean isNear) { + CzgResult callNear = sysParamsService.getParamsByCode("wx_mini_msg_call_near"); + CzgResult callCurrent = sysParamsService.getParamsByCode("wx_mini_msg_call_current"); + + Map data = new HashMap() {{ + put("thing1", new HashMap() {{ + put("value", shopName); + }}); + put("phrase2", new HashMap() {{ + put("value", state); + }}); + put("character_string3", new HashMap() {{ + put("value", callNum); + }}); + put("character_string4", new HashMap() {{ + put("value", currentNum); + }}); + put("thing5", new HashMap() {{ + put("value", note); + }}); + }}; + try { + sendTempMsg(isNear ? callNear.getData().getParamValue() : callCurrent.getData().getParamValue(), openId, data, "排队到号"); + } catch (Exception e) { + log.error("发送失败, openId:{}, msg: {}", openId, e.getMessage()); + } + } + + public void sendPassCallMsg(String shopName, String state, String callNum, String currentNum, String note, String openId) { + CzgResult callPass = sysParamsService.getParamsByCode("wx_mini_msg_call_pass"); + + Map data = new HashMap() {{ + put("thing1", new HashMap() {{ + put("value", shopName); + }}); + put("character_string2", new HashMap() {{ + put("value", callNum); + }}); + put("character_string3", new HashMap() {{ + put("value", currentNum); + }}); + put("phrase4", new HashMap() {{ + put("value", state); + }}); + put("thing5", new HashMap() {{ + put("value", note); + }}); + }}; + try { + sendTempMsg(callPass.getData().getParamValue(), openId, data, "过号"); + } catch (Exception e) { + log.error("发送失败, openId:{}, msg: {}", openId, e.getMessage()); + } + } + + public String getAccountOpenId(String code) { + CzgResult wxAccountAppId = sysParamsService.getParamsByCode("wx_account_app_id"); + CzgResult wxAccountSecrete = sysParamsService.getParamsByCode("wx_account_secrete"); + String accountAppId = wxAccountAppId.getData().getParamValue(); + String accountSecrete = wxAccountSecrete.getData().getParamValue(); + String requestUrl = "https://api.weixin.qq.com/sns/oauth2/access_token?"; + Map requestUrlParam = new HashMap<>(); + // https://mp.weixin.qq.com/wxopen/devprofile?action=get_profile&token=164113089&lang=zh_CN + //小程序appId + requestUrlParam.put("appid", accountAppId); + //小程序secret + requestUrlParam.put("secret", accountSecrete); + //小程序端返回的code + requestUrlParam.put("code", code); + //默认参数 + requestUrlParam.put("grant_type", "authorization_code"); + log.info("微信获取openid请求报文:{}", requestUrlParam); + //发送post请求读取调用微信接口获取openid用户唯一标识 + String resp = HttpUtil.post(requestUrl, requestUrlParam); + log.info("响应报文{}", resp); + return JSONObject.parseObject(resp).getString("openid"); + } + + //获取小程序token + private String getAccessToken() { + CzgResult wxMiniAppId = sysParamsService.getParamsByCode("wx_mini_app_id"); + CzgResult wxMiniSecrete = sysParamsService.getParamsByCode("wx_mini_secrete"); + String appId = wxMiniAppId.getData().getParamValue(); + String secrete = wxMiniSecrete.getData().getParamValue(); + String url = String.format("%s?grant_type=client_credential&appid=%s&secret=%s", TOKEN_URL, appId, secrete); + String response = HttpUtil.get(url); + JSONObject jsonResponse = JSONObject.parseObject(response); + if (!jsonResponse.containsKey("access_token")) { + throw new RuntimeException("Failed to retrieve access token: " + response); + } + return jsonResponse.getString("access_token"); + } + + /** + * 生成 小程序码 跳转对应页面 + * + */ + public String getFetchQrCode(Map params) throws Exception { + String url = aliOssUtil.upload(fetchQrCode(params), aliOssUtil.getPath("shopVip", "png")); + redisService.set(RedisCst.SHOP_VIP_CODE + params.get("shopId"), url); + return url; + } + + //生成页面地址 + private InputStream fetchQrCode(Map params) { + JsonObject jsonObject = new JsonObject(); + //路径 + jsonObject.addProperty("path", sysParamsService.getSysParamValue(SysParamCodeEnum.WX_MINI_VIP_URL.getCode()) + "?shopId=" + params.get("shopId")); + //是否需要透明底色,为 true 时,生成透明底色的小程序码 + jsonObject.addProperty("is_hyaline", true); + //正式版为 release,体验版为 trial,开发版为 develop + if (params.containsKey("env_version")) { + jsonObject.addProperty("env_version", "trial"); + } + String accessToken = getAccessToken(); + String url = String.format("%s?access_token=%s", QR_CODE_URL, accessToken); + return HttpUtil.createPost(url) + .body(jsonObject.toString(), "application/json") + .execute() + .bodyStream(); + } + + + public JSONObject getSession(String code) { + CzgResult wxMiniSecrete = sysParamsService.getParamsByCode("wx_mini_secrete"); + CzgResult wxMiniAppId = sysParamsService.getParamsByCode("wx_mini_app_id"); + String appId = wxMiniAppId.getData().getParamValue(); + String secrete = wxMiniSecrete.getData().getParamValue(); + String requestUrl = "https://api.weixin.qq.com/sns/jscode2session"; + Map requestUrlParam = new HashMap<>(); + // https://mp.weixin.qq.com/wxopen/devprofile?action=get_profile&token=164113089&lang=zh_CN + //小程序appId + requestUrlParam.put("appid", appId); + //小程序secret + requestUrlParam.put("secret", secrete); + //小程序端返回的code + requestUrlParam.put("js_code", code); + //默认参数 + requestUrlParam.put("grant_type", "authorization_code"); + //发送post请求读取调用微信接口获取openid用户唯一标识 + String resp = HttpUtil.post(requestUrl, requestUrlParam); + JSONObject jsonObject = JSON.parseObject(resp); + log.info("微信获取openid响应报文:{}", resp); + return jsonObject; + } + + public String getSessionKey(String code, String key) { + JSONObject session = getSession(code); + String info = session.getString(key); + if (StrUtil.isBlank(info)) { + throw new RuntimeException(key + "获取失败"); + } + return info; + } + + public String getSessionKeyOrOpenId(String code, boolean isAccount) { + return getSessionKey(code, "openid"); + } + + public static String decrypt(String sessionKey, @NotBlank(message = "数据不能为空") String encryptedData, String iv) { + // Base64 解码 + byte[] keyBytes = Base64.decode(sessionKey); + byte[] encryptedBytes = Base64.decode(encryptedData); + byte[] ivBytes = Base64.decode(iv); + + // 使用 Hutool 进行 AES-CBC 解密 + AES aes = new AES("CBC", "PKCS5Padding", keyBytes, ivBytes); + byte[] decryptedBytes = aes.decrypt(encryptedBytes); + + return new String(decryptedBytes, java.nio.charset.StandardCharsets.UTF_8); + } +}