From 8073a95f74c6870d64ca74b4bce8c8015dc02ec6 Mon Sep 17 00:00:00 2001 From: wangys <3401275564@qq.com> Date: Fri, 9 Jan 2026 12:17:21 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=B0=8F=E7=A8=8B=E5=BA=8F?= =?UTF-8?q?=E7=99=BB=E5=BD=95=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../database/postgres/sql/initDataConfig.sql | 6 +- .../auth/utils/WechatMiniProgramUtil.java | 0 .../xyzh/common/core/domain/LoginParam.java | 26 ++ .../config/WeChatMiniProgramConfig.java | 25 ++ .../common/wechat/pojo/WeChatPhoneResult.java | 52 +++ .../wechat/pojo/WeChatSessionResult.java | 28 ++ .../service/WeChatMiniProgramService.java | 300 ++++++++++++++++ urbanLifelineServ/system/pom.xml | 4 + .../system/controller/GuestController.java | 72 +++- .../packages/workcase_wechat/api/ai/aiChat.ts | 4 +- .../packages/workcase_wechat/api/base.ts | 45 +++ .../api/workcase/workcaseChat.ts | 8 +- .../packages/workcase_wechat/config/index.ts | 11 +- .../packages/workcase_wechat/manifest.json | 43 ++- .../pages/chatRoom/chatRoom/chatRoom.uvue | 2 +- .../chatRoom/chatRoomList/chatRoomList.uvue | 2 +- .../workcase_wechat/pages/index/index.scss | 100 ++++++ .../workcase_wechat/pages/index/index.uvue | 233 ++++++++++-- .../workcase_wechat/project.config.json | 6 +- .../static/imgs/defaultchat.png | Bin 1816548 -> 0 bytes .../workcase_wechat/types/auth/auth.ts | 10 + .../workcase_wechat/手机号授权说明.md | 165 +++++++++ .../packages/workcase_wechat/最终登录方案.md | 333 ++++++++++++++++++ 23 files changed, 1417 insertions(+), 58 deletions(-) create mode 100644 urbanLifelineServ/auth/src/main/java/org/xyzh/auth/utils/WechatMiniProgramUtil.java create mode 100644 urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/config/WeChatMiniProgramConfig.java create mode 100644 urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/pojo/WeChatPhoneResult.java create mode 100644 urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/pojo/WeChatSessionResult.java create mode 100644 urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/service/WeChatMiniProgramService.java delete mode 100644 urbanLifelineWeb/packages/workcase_wechat/static/imgs/defaultchat.png create mode 100644 urbanLifelineWeb/packages/workcase_wechat/手机号授权说明.md create mode 100644 urbanLifelineWeb/packages/workcase_wechat/最终登录方案.md diff --git a/urbanLifelineServ/.bin/database/postgres/sql/initDataConfig.sql b/urbanLifelineServ/.bin/database/postgres/sql/initDataConfig.sql index b011712b..5539bfaa 100644 --- a/urbanLifelineServ/.bin/database/postgres/sql/initDataConfig.sql +++ b/urbanLifelineServ/.bin/database/postgres/sql/initDataConfig.sql @@ -103,6 +103,10 @@ INSERT INTO config.tb_sys_config ( ('CFG-0703', 'cfg_wechat_kefu_token', 'wechat.kefu.token', '回调Token', '', 'String', 'input', '消息回调的Token', NULL, NULL, 'wechat', 'mod_workcase', 30, 1, '用于验证消息回调的Token', 'system', NULL, NULL, now(), NULL, NULL, false), ('CFG-0704', 'cfg_wechat_kefu_aeskey', 'wechat.kefu.encodingAesKey','回调加密密钥', '', 'String', 'password', '消息回调的EncodingAESKey', NULL, NULL, 'wechat', 'mod_workcase', 40, 1, '用于解密消息回调的AES密钥', 'system', NULL, NULL, now(), NULL, NULL, false), ('CFG-0705', 'cfg_wechat_kefu_openkfid', 'wechat.kefu.openKfid', '客服账号ID', '', 'String', 'input', '微信客服账号的open_kfid', NULL, NULL, 'wechat', 'mod_workcase', 50, 1, '用于发送消息的客服账号ID', 'system', NULL, NULL, now(), NULL, NULL, false), -('CFG-0706', 'cfg_wechat_kefu_welcome', 'wechat.kefu.welcomeTemplate','欢迎语模板', '您好,您的工单已创建。\n工单编号:{workcaseId}\n问题类型:{type}\n设备:{device}\n我们将尽快为您处理。', 'String', 'textarea', '客服欢迎语消息模板', NULL, NULL, 'wechat', 'mod_workcase', 60, 1, '支持变量:{workcaseId},{type},{device},{username}', 'system', NULL, NULL, now(), NULL, NULL, false); +('CFG-0706', 'cfg_wechat_kefu_welcome', 'wechat.kefu.welcomeTemplate','欢迎语模板', '您好,您的工单已创建。\n工单编号:{workcaseId}\n问题类型:{type}\n设备:{device}\n我们将尽快为您处理。', 'String', 'textarea', '客服欢迎语消息模板', NULL, NULL, 'wechat', 'mod_workcase', 60, 1, '支持变量:{workcaseId},{type},{device},{username}', 'system', NULL, NULL, now(), NULL, NULL, false), + +-- 微信小程序配置 +('CFG-0710', 'cfg_wechat_mp_appid', 'wechat.miniprogram.appid', '小程序AppID', 'wx3708f41b1dc31f52', 'String', 'input', '微信小程序的AppID', NULL, NULL, 'wechat', 'mod_workcase', 70, 1, '在微信公众平台获取', 'system', NULL, NULL, now(), NULL, NULL, false), +('CFG-0711', 'cfg_wechat_mp_appsecret', 'wechat.miniprogram.appsecret', '小程序AppSecret', '127dcc9c90dd1b66a700b52094922253', 'String', 'password', '微信小程序的AppSecret', NULL, NULL, 'wechat', 'mod_workcase', 80, 1, '在微信公众平台获取,用于获取openid和解密手机号', 'system', NULL, NULL, now(), NULL, NULL, false); diff --git a/urbanLifelineServ/auth/src/main/java/org/xyzh/auth/utils/WechatMiniProgramUtil.java b/urbanLifelineServ/auth/src/main/java/org/xyzh/auth/utils/WechatMiniProgramUtil.java new file mode 100644 index 00000000..e69de29b diff --git a/urbanLifelineServ/common/common-core/src/main/java/org/xyzh/common/core/domain/LoginParam.java b/urbanLifelineServ/common/common-core/src/main/java/org/xyzh/common/core/domain/LoginParam.java index 154f0a73..cbed0cbf 100644 --- a/urbanLifelineServ/common/common-core/src/main/java/org/xyzh/common/core/domain/LoginParam.java +++ b/urbanLifelineServ/common/common-core/src/main/java/org/xyzh/common/core/domain/LoginParam.java @@ -94,4 +94,30 @@ public class LoginParam implements Serializable { */ private String token; + // ========== 微信小程序登录相关字段 ========== + + /** + * 微信登录code(wx.login返回,用于换取openid和session_key) + * @since 2026-01-09 + */ + private String code; + + /** + * 手机号授权code(getPhoneNumber返回,新版API推荐使用) + * @since 2026-01-09 + */ + private String phoneCode; + + /** + * 加密数据(getPhoneNumber返回,旧版API用于解密手机号) + * @since 2026-01-09 + */ + private String encryptedData; + + /** + * 解密向量(getPhoneNumber返回,旧版API用于解密手机号) + * @since 2026-01-09 + */ + private String iv; + } diff --git a/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/config/WeChatMiniProgramConfig.java b/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/config/WeChatMiniProgramConfig.java new file mode 100644 index 00000000..7f78b080 --- /dev/null +++ b/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/config/WeChatMiniProgramConfig.java @@ -0,0 +1,25 @@ +package org.xyzh.common.wechat.config; + +import lombok.Data; + +/** + * 微信小程序配置 + * @author cascade + * @since 2026-01-09 + */ +@Data +public class WeChatMiniProgramConfig { + + /** 小程序AppID */ + private String appId; + + /** 小程序AppSecret */ + private String appSecret; + + /** 当前access_token */ + private String accessToken; + + /** access_token过期时间(毫秒时间戳) */ + private Long accessTokenExpireTime; + +} diff --git a/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/pojo/WeChatPhoneResult.java b/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/pojo/WeChatPhoneResult.java new file mode 100644 index 00000000..be9c4aaf --- /dev/null +++ b/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/pojo/WeChatPhoneResult.java @@ -0,0 +1,52 @@ +package org.xyzh.common.wechat.pojo; + +import lombok.Data; + +/** + * 微信小程序获取手机号接口返回结果 + * @author cascade + * @since 2026-01-09 + */ +@Data +public class WeChatPhoneResult { + + /** 错误码 */ + private Integer errcode; + + /** 错误信息 */ + private String errmsg; + + /** 手机号信息 */ + private PhoneInfo phoneInfo; + + /** + * 手机号信息 + */ + @Data + public static class PhoneInfo { + /** 用户绑定的手机号(国外手机号会有区号) */ + private String phoneNumber; + + /** 没有区号的手机号 */ + private String purePhoneNumber; + + /** 区号 */ + private String countryCode; + + /** 数据水印 */ + private Watermark watermark; + } + + /** + * 数据水印 + */ + @Data + public static class Watermark { + /** 小程序appid */ + private String appid; + + /** 时间戳 */ + private Long timestamp; + } + +} diff --git a/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/pojo/WeChatSessionResult.java b/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/pojo/WeChatSessionResult.java new file mode 100644 index 00000000..ce9e20c0 --- /dev/null +++ b/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/pojo/WeChatSessionResult.java @@ -0,0 +1,28 @@ +package org.xyzh.common.wechat.pojo; + +import lombok.Data; + +/** + * 微信小程序 code2Session 接口返回结果 + * @author cascade + * @since 2026-01-09 + */ +@Data +public class WeChatSessionResult { + + /** 用户唯一标识 */ + private String openid; + + /** 会话密钥 */ + private String sessionKey; + + /** 用户在开放平台的唯一标识符(需要绑定开放平台) */ + private String unionid; + + /** 错误码 */ + private Integer errcode; + + /** 错误信息 */ + private String errmsg; + +} diff --git a/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/service/WeChatMiniProgramService.java b/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/service/WeChatMiniProgramService.java new file mode 100644 index 00000000..3ba3cd71 --- /dev/null +++ b/urbanLifelineServ/common/common-wechat/src/main/java/org/xyzh/common/wechat/service/WeChatMiniProgramService.java @@ -0,0 +1,300 @@ +package org.xyzh.common.wechat.service; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +import javax.crypto.Cipher; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +import org.apache.dubbo.config.annotation.DubboReference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; +import org.xyzh.api.system.service.SysConfigService; +import org.xyzh.common.core.domain.ResultDomain; +import org.xyzh.common.wechat.config.WeChatMiniProgramConfig; +import org.xyzh.common.wechat.pojo.WeChatPhoneResult; +import org.xyzh.common.wechat.pojo.WeChatSessionResult; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; + +/** + * 微信小程序服务 + * @author cascade + * @since 2026-01-09 + */ +@Service +public class WeChatMiniProgramService { + + private static final Logger logger = LoggerFactory.getLogger(WeChatMiniProgramService.class); + + /** 微信小程序配置缓存 */ + private WeChatMiniProgramConfig config; + + /** code2Session接口地址 */ + private static final String CODE2SESSION_URL = "https://api.weixin.qq.com/sns/jscode2session"; + + /** 获取access_token接口地址 */ + private static final String ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token"; + + /** 获取手机号接口地址(新版API) */ + private static final String GET_PHONE_URL = "https://api.weixin.qq.com/wxa/business/getuserphonenumber"; + + @DubboReference(version = "1.0.0", group = "system", timeout = 5000, check = false, retries = 0) + private SysConfigService sysConfigService; + + /** + * 获取微信小程序配置 + */ + public WeChatMiniProgramConfig getConfig() { + if (config == null) { + loadConfig(); + } + return config; + } + + /** + * 从系统配置加载微信小程序配置 + */ + private synchronized void loadConfig() { + if (config != null) { + return; + } + + config = new WeChatMiniProgramConfig(); + + try { + // 从系统配置获取小程序AppID + ResultDomain appIdResult = sysConfigService.getConfigValue("wechat.miniprogram.appid"); + if (appIdResult.getSuccess() && appIdResult.getData() != null) { + config.setAppId(appIdResult.getData()); + } + + // 从系统配置获取小程序AppSecret + ResultDomain appSecretResult = sysConfigService.getConfigValue("wechat.miniprogram.appsecret"); + if (appSecretResult.getSuccess() && appSecretResult.getData() != null) { + config.setAppSecret(appSecretResult.getData()); + } + + logger.info("微信小程序配置加载成功, appId: {}", config.getAppId()); + } catch (Exception e) { + logger.error("加载微信小程序配置失败", e); + } + } + + /** + * 刷新配置 + */ + public void refreshConfig() { + config = null; + loadConfig(); + } + + /** + * 通过code获取session信息(openid和session_key) + * @param code wx.login返回的code + * @return session信息 + */ + public ResultDomain code2Session(String code) { + WeChatMiniProgramConfig cfg = getConfig(); + if (cfg == null || cfg.getAppId() == null || cfg.getAppSecret() == null) { + return ResultDomain.failure("微信小程序配置未初始化"); + } + + try { + String url = String.format("%s?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code", + CODE2SESSION_URL, cfg.getAppId(), cfg.getAppSecret(), code); + + String response = httpGet(url); + logger.debug("code2Session响应: {}", response); + + WeChatSessionResult result = JSON.parseObject(response, WeChatSessionResult.class); + + if (result.getErrcode() != null && result.getErrcode() != 0) { + logger.error("code2Session失败: {} - {}", result.getErrcode(), result.getErrmsg()); + return ResultDomain.failure("获取session失败: " + result.getErrmsg()); + } + + return ResultDomain.success("获取session成功", result); + } catch (Exception e) { + logger.error("code2Session异常", e); + return ResultDomain.failure("获取session异常: " + e.getMessage()); + } + } + + /** + * 获取access_token + * @return access_token + */ + public String getAccessToken() { + WeChatMiniProgramConfig cfg = getConfig(); + if (cfg == null) { + return null; + } + + // 检查缓存的token是否有效 + if (cfg.getAccessToken() != null && cfg.getAccessTokenExpireTime() != null + && System.currentTimeMillis() < cfg.getAccessTokenExpireTime()) { + return cfg.getAccessToken(); + } + + // 重新获取token + try { + String url = String.format("%s?grant_type=client_credential&appid=%s&secret=%s", + ACCESS_TOKEN_URL, cfg.getAppId(), cfg.getAppSecret()); + + String response = httpGet(url); + JSONObject json = JSON.parseObject(response); + + if (json.containsKey("access_token")) { + String accessToken = json.getString("access_token"); + int expiresIn = json.getIntValue("expires_in"); + + cfg.setAccessToken(accessToken); + // 提前5分钟过期 + cfg.setAccessTokenExpireTime(System.currentTimeMillis() + (expiresIn - 300) * 1000L); + + logger.info("获取access_token成功, 有效期: {}秒", expiresIn); + return accessToken; + } else { + logger.error("获取access_token失败: {}", response); + return null; + } + } catch (Exception e) { + logger.error("获取access_token异常", e); + return null; + } + } + + /** + * 通过phoneCode获取手机号(新版API,推荐使用) + * @param phoneCode getPhoneNumber返回的code + * @return 手机号信息 + */ + public ResultDomain getPhoneNumber(String phoneCode) { + String accessToken = getAccessToken(); + if (accessToken == null) { + return ResultDomain.failure("获取access_token失败"); + } + + try { + String url = GET_PHONE_URL + "?access_token=" + accessToken; + + JSONObject requestBody = new JSONObject(); + requestBody.put("code", phoneCode); + + String response = httpPost(url, requestBody.toJSONString()); + logger.debug("getPhoneNumber响应: {}", response); + + WeChatPhoneResult result = JSON.parseObject(response, WeChatPhoneResult.class); + + if (result.getErrcode() != null && result.getErrcode() != 0) { + logger.error("getPhoneNumber失败: {} - {}", result.getErrcode(), result.getErrmsg()); + return ResultDomain.failure("获取手机号失败: " + result.getErrmsg()); + } + + return ResultDomain.success("获取手机号成功", result); + } catch (Exception e) { + logger.error("getPhoneNumber异常", e); + return ResultDomain.failure("获取手机号异常: " + e.getMessage()); + } + } + + /** + * 通过encryptedData和iv解密手机号(旧版API) + * @param sessionKey 会话密钥 + * @param encryptedData 加密数据 + * @param iv 解密向量 + * @return 解密后的手机号 + */ + public ResultDomain decryptPhoneNumber(String sessionKey, String encryptedData, String iv) { + try { + byte[] sessionKeyBytes = Base64.getDecoder().decode(sessionKey); + byte[] encryptedDataBytes = Base64.getDecoder().decode(encryptedData); + byte[] ivBytes = Base64.getDecoder().decode(iv); + + SecretKeySpec keySpec = new SecretKeySpec(sessionKeyBytes, "AES"); + IvParameterSpec ivSpec = new IvParameterSpec(ivBytes); + + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec); + + byte[] decryptedBytes = cipher.doFinal(encryptedDataBytes); + String decryptedData = new String(decryptedBytes, StandardCharsets.UTF_8); + + logger.debug("解密数据: {}", decryptedData); + + JSONObject json = JSON.parseObject(decryptedData); + String phoneNumber = json.getString("phoneNumber"); + + if (phoneNumber == null || phoneNumber.isEmpty()) { + phoneNumber = json.getString("purePhoneNumber"); + } + + return ResultDomain.success("解密成功", phoneNumber); + } catch (Exception e) { + logger.error("解密手机号失败", e); + return ResultDomain.failure("解密手机号失败: " + e.getMessage()); + } + } + + /** + * HTTP GET请求 + */ + private String httpGet(String urlStr) throws Exception { + URL url = new URL(urlStr); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("GET"); + conn.setConnectTimeout(5000); + conn.setReadTimeout(5000); + + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) { + StringBuilder response = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + response.append(line); + } + return response.toString(); + } finally { + conn.disconnect(); + } + } + + /** + * HTTP POST请求 + */ + private String httpPost(String urlStr, String body) throws Exception { + URL url = new URL(urlStr); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("POST"); + conn.setConnectTimeout(5000); + conn.setReadTimeout(5000); + conn.setDoOutput(true); + conn.setRequestProperty("Content-Type", "application/json"); + + try (OutputStream os = conn.getOutputStream()) { + os.write(body.getBytes(StandardCharsets.UTF_8)); + } + + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) { + StringBuilder response = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + response.append(line); + } + return response.toString(); + } finally { + conn.disconnect(); + } + } + +} diff --git a/urbanLifelineServ/system/pom.xml b/urbanLifelineServ/system/pom.xml index aa1f39e9..9e9f9305 100644 --- a/urbanLifelineServ/system/pom.xml +++ b/urbanLifelineServ/system/pom.xml @@ -25,6 +25,10 @@ org.xyzh.common common-all + + org.xyzh.common + common-wechat + org.xyzh.apis api-system diff --git a/urbanLifelineServ/system/src/main/java/org/xyzh/system/controller/GuestController.java b/urbanLifelineServ/system/src/main/java/org/xyzh/system/controller/GuestController.java index 46160110..1744dbaa 100644 --- a/urbanLifelineServ/system/src/main/java/org/xyzh/system/controller/GuestController.java +++ b/urbanLifelineServ/system/src/main/java/org/xyzh/system/controller/GuestController.java @@ -8,6 +8,8 @@ import java.util.concurrent.TimeUnit; import com.alibaba.fastjson2.JSON; import org.apache.dubbo.config.annotation.DubboReference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.DeleteMapping; @@ -34,6 +36,9 @@ import org.xyzh.common.dto.sys.TbSysUserRoleDTO; import org.xyzh.common.dto.sys.TbSysViewDTO; import org.xyzh.common.utils.id.IdUtil; import org.xyzh.common.utils.validation.ValidationUtils; +import org.xyzh.common.wechat.pojo.WeChatPhoneResult; +import org.xyzh.common.wechat.pojo.WeChatSessionResult; +import org.xyzh.common.wechat.service.WeChatMiniProgramService; import org.xyzh.common.auth.utils.JwtTokenUtil; import org.xyzh.common.redis.service.RedisService; @@ -54,6 +59,8 @@ import jakarta.validation.constraints.NotNull; @RequestMapping("/system/guest") public class GuestController { + private static final Logger logger = LoggerFactory.getLogger(GuestController.class); + @Autowired private GuestService guestService; @@ -69,6 +76,9 @@ public class GuestController { @Autowired private RedisService redisService; + @Autowired + private WeChatMiniProgramService weChatMiniProgramService; + @PostMapping public ResultDomain createGuest(TbGuestDTO guest) { @@ -120,6 +130,63 @@ public class GuestController { @Operation(summary = "微信小程序用户识别登录") @PostMapping("/identify") public ResultDomain identifyUser(@RequestBody LoginParam loginParam, HttpServletRequest request) { + logger.info("微信小程序登录请求: wechatId={}, code={}, phoneCode={}", + loginParam.getWechatId(), + loginParam.getCode() != null ? loginParam.getCode().substring(0, Math.min(10, loginParam.getCode().length())) + "..." : null, + loginParam.getPhoneCode() != null ? "有" : "无"); + + // 1. 处理微信登录code,获取openid + String openid = null; + String sessionKey = null; + if (loginParam.getCode() != null && !loginParam.getCode().trim().isEmpty()) { + ResultDomain sessionResult = weChatMiniProgramService.code2Session(loginParam.getCode()); + if (sessionResult.getSuccess() && sessionResult.getData() != null) { + openid = sessionResult.getData().getOpenid(); + sessionKey = sessionResult.getData().getSessionKey(); + logger.info("获取openid成功: {}", openid); + // 使用openid作为wechatId + loginParam.setWechatId(openid); + } else { + logger.warn("获取openid失败: {}", sessionResult.getMessage()); + } + } + + // 2. 处理手机号授权 + String phoneNumber = null; + + // 方式1:使用phoneCode获取手机号(新版API,推荐) + if (loginParam.getPhoneCode() != null && !loginParam.getPhoneCode().trim().isEmpty()) { + ResultDomain phoneResult = weChatMiniProgramService.getPhoneNumber(loginParam.getPhoneCode()); + if (phoneResult.getSuccess() && phoneResult.getData() != null && phoneResult.getData().getPhoneInfo() != null) { + phoneNumber = phoneResult.getData().getPhoneInfo().getPurePhoneNumber(); + if (phoneNumber == null) { + phoneNumber = phoneResult.getData().getPhoneInfo().getPhoneNumber(); + } + logger.info("通过phoneCode获取手机号成功: {}", phoneNumber); + } else { + logger.warn("通过phoneCode获取手机号失败: {}", phoneResult.getMessage()); + } + } + + // 方式2:使用encryptedData和iv解密手机号(旧版API) + if (phoneNumber == null && sessionKey != null + && loginParam.getEncryptedData() != null && !loginParam.getEncryptedData().trim().isEmpty() + && loginParam.getIv() != null && !loginParam.getIv().trim().isEmpty()) { + ResultDomain decryptResult = weChatMiniProgramService.decryptPhoneNumber( + sessionKey, loginParam.getEncryptedData(), loginParam.getIv()); + if (decryptResult.getSuccess() && decryptResult.getData() != null) { + phoneNumber = decryptResult.getData(); + logger.info("通过解密获取手机号成功: {}", phoneNumber); + } else { + logger.warn("解密手机号失败: {}", decryptResult.getMessage()); + } + } + + // 设置手机号 + if (phoneNumber != null) { + loginParam.setPhone(phoneNumber); + } + // 验证参数:必须有wechatId或phone if ((loginParam.getWechatId() == null || loginParam.getWechatId().trim().isEmpty()) && (loginParam.getPhone() == null || loginParam.getPhone().trim().isEmpty())) { @@ -132,14 +199,15 @@ public class GuestController { // 从 request 中提取客户端 IP 并设置到 loginParam loginParam.setClientIp(getClientIP(request)); - // 1. 尝试通过AuthService登录(员工) + // 3. 尝试通过AuthService登录(员工) ResultDomain loginResult = authService.login(loginParam); if (loginResult.getSuccess() && loginResult.getData() != null) { // 登录成功,是系统员工 + logger.info("员工登录成功: userId={}", loginResult.getData().getUser().getUserId()); return loginResult; } - // 2. 登录失败,查询/注册来客 + // 4. 登录失败,查询/注册来客 return handleGuestLogin(loginParam); } diff --git a/urbanLifelineWeb/packages/workcase_wechat/api/ai/aiChat.ts b/urbanLifelineWeb/packages/workcase_wechat/api/ai/aiChat.ts index aaa0dc05..a2eb1bae 100644 --- a/urbanLifelineWeb/packages/workcase_wechat/api/ai/aiChat.ts +++ b/urbanLifelineWeb/packages/workcase_wechat/api/ai/aiChat.ts @@ -1,5 +1,6 @@ import { request, uploadFile } from '../base' import type { ResultDomain } from '../../types' +import { BASE_URL } from '../../config' import type { TbChat, TbChatMessage, @@ -21,9 +22,6 @@ declare const uni: { request: (options: any) => any } -// API 基础配置 -const BASE_URL = 'http://localhost:8180' - /** * @description AI对话相关接口(直接调用ai模块) * @filename aiChat.ts diff --git a/urbanLifelineWeb/packages/workcase_wechat/api/base.ts b/urbanLifelineWeb/packages/workcase_wechat/api/base.ts index 00a68aae..b6f4c55b 100644 --- a/urbanLifelineWeb/packages/workcase_wechat/api/base.ts +++ b/urbanLifelineWeb/packages/workcase_wechat/api/base.ts @@ -6,8 +6,12 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ declare const uni: { getStorageSync: (key: string) => any + removeStorageSync: (key: string) => void + setStorageSync: (key: string, data: any) => void request: (options: any) => void uploadFile: (options: any) => void + showToast: (options: any) => void + reLaunch: (options: any) => void } import type { ResultDomain } from '../types' @@ -36,6 +40,10 @@ export function request(options: { success: (res: any) => { if (res.statusCode === 200) { resolve(res.data as ResultDomain) + } else if (res.statusCode === 401) { + // Token 过期或无效,清除缓存并跳转到授权页面 + handleTokenExpired() + reject(new Error('登录已过期,请重新登录')) } else { reject(new Error(`请求失败: ${res.statusCode}`)) } @@ -47,6 +55,30 @@ export function request(options: { }) } +// 处理 Token 过期 +function handleTokenExpired() { + // 清除所有登录信息 + uni.removeStorageSync('token') + uni.removeStorageSync('userInfo') + uni.removeStorageSync('loginDomain') + uni.removeStorageSync('wechatId') + uni.removeStorageSync('nickname') + + // 提示用户 + uni.showToast({ + title: '登录已过期,正在重新登录', + icon: 'none', + duration: 2000 + }) + + // 刷新页面,触发自动登录 + setTimeout(() => { + uni.reLaunch({ + url: '/pages/index/index' + }) + }, 2000) +} + // 文件上传方法 export function uploadFile(options: { url: string @@ -71,6 +103,10 @@ export function uploadFile(options: { if (res.statusCode === 200) { const result = typeof res.data === 'string' ? JSON.parse(res.data) : res.data resolve(result as ResultDomain) + } else if (res.statusCode === 401) { + // Token 过期或无效 + handleTokenExpired() + reject(new Error('登录已过期,请重新登录')) } else { reject(new Error(`上传失败: ${res.statusCode}`)) } @@ -84,3 +120,12 @@ export function uploadFile(options: { }) }) } + +// 导出清除登录信息的方法,供其他地方使用 +export function clearLoginInfo() { + uni.removeStorageSync('token') + uni.removeStorageSync('userInfo') + uni.removeStorageSync('loginDomain') + uni.removeStorageSync('wechatId') + uni.removeStorageSync('nickname') +} diff --git a/urbanLifelineWeb/packages/workcase_wechat/api/workcase/workcaseChat.ts b/urbanLifelineWeb/packages/workcase_wechat/api/workcase/workcaseChat.ts index ae02c07a..972d8896 100644 --- a/urbanLifelineWeb/packages/workcase_wechat/api/workcase/workcaseChat.ts +++ b/urbanLifelineWeb/packages/workcase_wechat/api/workcase/workcaseChat.ts @@ -1,6 +1,6 @@ import { request } from '../base' import type { ResultDomain, PageRequest } from '../../types' -import type { TbWorkcaseDTO } from '../../types/workcase/workcase' +import { BASE_URL } from '../../config' import type { TbChatRoomDTO, TbChatRoomMemberDTO, @@ -23,7 +23,8 @@ import type { ChatMessageListParam, SSECallbacks, SSETask, - SSEMessageData + SSEMessageData, + TbWorkcaseDTO } from '../../types/ai/aiChat' /* eslint-disable @typescript-eslint/no-explicit-any */ @@ -32,9 +33,6 @@ declare const uni: { request: (options: any) => any } -// API 基础配置 -const BASE_URL = 'http://localhost:8180' - /** * @description 工单对话相关接口 * @filename workcaseChat.ts diff --git a/urbanLifelineWeb/packages/workcase_wechat/config/index.ts b/urbanLifelineWeb/packages/workcase_wechat/config/index.ts index 2a1b5669..b15949c8 100644 --- a/urbanLifelineWeb/packages/workcase_wechat/config/index.ts +++ b/urbanLifelineWeb/packages/workcase_wechat/config/index.ts @@ -1,4 +1,7 @@ -export const AGENT_ID = '17664699513920001' -export const BASE_URL = 'http://localhost:8180' -export const WS_HOST = 'localhost:8180' // WebSocket host(不包含协议) -export const FILE_DOWNLOAD_URL = 'http://localhost:8180/urban-lifeline/sys-file/download?fileId=' \ No newline at end of file +export const AGENT_ID = '17678420499370001' +// export const BASE_URL = 'http://localhost:8180' +// 根据宝塔nginx配置,/urban-lifeline/ 会代理到后端网关 +export const BASE_URL = 'https://demo-urbanlifeline.tensorgrove.com/urban-lifeline' + +// export const WS_HOST = 'localhost:8180' // WebSocket host(不包含协议) +export const WS_HOST = 'demo-urbanlifeline.tensorgrove.com/urban-lifeline' // WebSocket host(不包含协议) \ No newline at end of file diff --git a/urbanLifelineWeb/packages/workcase_wechat/manifest.json b/urbanLifelineWeb/packages/workcase_wechat/manifest.json index e03c5d9d..2840b796 100644 --- a/urbanLifelineWeb/packages/workcase_wechat/manifest.json +++ b/urbanLifelineWeb/packages/workcase_wechat/manifest.json @@ -1,15 +1,17 @@ { "name" : "泰豪小电", - "appid" : "", + "appid" : "__UNI__69FD573", "description" : "泰豪小电智能工单系统", "versionName" : "1.0.0", "versionCode" : "100", "uni-app-x" : {}, "quickapp" : {}, "mp-weixin" : { - "appid" : "", + "appid" : "wx15e67484db6d431f", "setting" : { - "urlCheck" : false + "urlCheck" : false, + "postcss" : true, + "minified" : true }, "usingComponents" : true, "permission" : { @@ -59,5 +61,40 @@ } } } + }, + "app-android" : { + "distribute" : { + "permissions" : [ + "", + "", + "" + ], + "modules" : {}, + "icons" : { + "hdpi" : "", + "xhdpi" : "", + "xxhdpi" : "", + "xxxhdpi" : "" + }, + "splashScreens" : { + "default" : {} + } + } + }, + "app-ios" : { + "distribute" : { + "privacyDescription" : { + "NSCameraUsageDescription" : "用于视频会议时开启摄像头", + "NSMicrophoneUsageDescription" : "用于视频会议时开启麦克风" + }, + "modules" : {}, + "icons" : {}, + "splashScreens" : {} + } + }, + "web" : { + "router" : { + "mode" : "" + } } } diff --git a/urbanLifelineWeb/packages/workcase_wechat/pages/chatRoom/chatRoom/chatRoom.uvue b/urbanLifelineWeb/packages/workcase_wechat/pages/chatRoom/chatRoom/chatRoom.uvue index fa2be217..17ea3ce2 100644 --- a/urbanLifelineWeb/packages/workcase_wechat/pages/chatRoom/chatRoom/chatRoom.uvue +++ b/urbanLifelineWeb/packages/workcase_wechat/pages/chatRoom/chatRoom/chatRoom.uvue @@ -739,7 +739,7 @@ async function initWebSocket() { // 构建WebSocket URL // 开发环境:ws://localhost:8180 或 ws://192.168.x.x:8180 // 生产环境:wss://your-domain.com - const protocol = 'ws:' // 开发环境使用ws,生产环境改为wss + const protocol = 'wss:' // 开发环境使用ws,生产环境改为wss // 小程序使用原生WebSocket端点(不是SockJS端点) const wsUrl = `${protocol}//${WS_HOST}/urban-lifeline/workcase/ws/chat?token=${encodeURIComponent(token)}` diff --git a/urbanLifelineWeb/packages/workcase_wechat/pages/chatRoom/chatRoomList/chatRoomList.uvue b/urbanLifelineWeb/packages/workcase_wechat/pages/chatRoom/chatRoomList/chatRoomList.uvue index cb7f3d19..fdb08999 100644 --- a/urbanLifelineWeb/packages/workcase_wechat/pages/chatRoom/chatRoomList/chatRoomList.uvue +++ b/urbanLifelineWeb/packages/workcase_wechat/pages/chatRoom/chatRoomList/chatRoomList.uvue @@ -227,7 +227,7 @@ async function initWebSocket() { // 构建WebSocket URL // 开发环境:ws://localhost:8180 或 ws://192.168.x.x:8180 // 生产环境:wss://your-domain.com - const protocol = 'ws:' // 开发环境使用ws,生产环境改为wss + const protocol = 'wss:' // 开发环境使用ws,生产环境改为wss // 小程序使用原生WebSocket端点(不是SockJS端点) const wsUrl = `${protocol}//${WS_HOST}/urban-lifeline/workcase/ws/chat?token=${encodeURIComponent(token)}` diff --git a/urbanLifelineWeb/packages/workcase_wechat/pages/index/index.scss b/urbanLifelineWeb/packages/workcase_wechat/pages/index/index.scss index 16853d0b..9172285d 100644 --- a/urbanLifelineWeb/packages/workcase_wechat/pages/index/index.scss +++ b/urbanLifelineWeb/packages/workcase_wechat/pages/index/index.scss @@ -80,6 +80,15 @@ font-weight: 500; } +// 退出按钮特殊样式 +.logout-btn { + background: rgba(255, 59, 48, 0.1); + + .btn-text { + color: #ff3b30; + } +} + // 欢迎区域(机器人+浮动标签) .hero { position: relative; @@ -803,3 +812,94 @@ .modal-btn .btn-text { font-size: 16px; } + +// 手机号授权弹窗样式 +.phone-auth-modal { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 1001; + display: flex; + align-items: center; + justify-content: center; +} + +.phone-auth-content { + width: 85%; + max-width: 340px; + padding: 30px 24px; +} + +.phone-auth-body { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; +} + +.auth-icon-wrap { + width: 80px; + height: 80px; + border-radius: 50%; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 20px; +} + +.auth-icon { + font-size: 40px; +} + +.auth-desc { + font-size: 14px; + color: #666; + line-height: 1.6; + margin-bottom: 10px; +} + +.phone-auth-footer { + flex-direction: column; + gap: 12px; +} + +.phone-auth-btn { + width: 100%; + height: 48px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-radius: 24px; + margin: 0; + padding: 0; +} + +.phone-auth-btn .btn-text { + color: white; + font-size: 16px; + font-weight: 600; +} + +.phone-auth-btn::after { + border: none; +} + +.skip-auth-btn { + width: 100%; + height: 40px; + background: transparent; + border-radius: 20px; + margin: 0; + padding: 0; +} + +.skip-auth-btn .skip-text { + color: #999; + font-size: 14px; + font-weight: 400; +} + +.skip-auth-btn::after { + border: none; +} diff --git a/urbanLifelineWeb/packages/workcase_wechat/pages/index/index.uvue b/urbanLifelineWeb/packages/workcase_wechat/pages/index/index.uvue index 3744829a..f43758c0 100644 --- a/urbanLifelineWeb/packages/workcase_wechat/pages/index/index.uvue +++ b/urbanLifelineWeb/packages/workcase_wechat/pages/index/index.uvue @@ -4,9 +4,6 @@ 泰豪小电 - @@ -175,14 +172,40 @@ + + + + + + + 欢迎使用泰豪小电 + + + + 📱 + + 为了给您提供更好的服务,需要获取您的手机号用于身份识别和工单通知 + + + + + +