From 797a140f111a828da635d813e5923ba2f902aeab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Thu, 18 Jun 2026 16:31:04 +0800 Subject: [PATCH 1/2] =?UTF-8?q?refactor(wxlogin):=20=E6=94=B9=E9=80=A0?= =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E9=85=8D=E7=BD=AE=E8=AF=BB=E5=8F=96=E4=BC=98?= =?UTF-8?q?=E5=85=88=E4=BB=8E=20app=5Fconfig=20=E8=8E=B7=E5=8F=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 AppConfig 实体及跨库 Mapper、XML 实现 db_websopy.app_config 查询 - 增加 AppConfigService 提供带缓存的配置读取接口,缓存两小时 - WxLoginController 注入 AppConfigService,新增私有方法 getMpWxSetting() - getMpWxSetting() 优先读取 app_config(category=wechat),失败回退 sys_setting.mp-weixin - 替换微信小程序配置获取调用点,统一通过 getMpWxSetting() 获取配置 - 避免跨库 TenantLineInterceptor 干扰,Mapper 方法加 @InterceptorIgnore 标注 - 修正 StringRedisTemplate 注入避免 Bean 名冲突 - 新增读取失败或无配置时的降级和异常处理逻辑 - 日志补充便于排查配置读取过程及缓存情况 --- .workbuddy/memory/2026-06-17.md | 58 +++++++++ .../system/controller/WxLoginController.java | 51 +++++--- .../gxwebsoft/websopy/entity/AppConfig.java | 51 ++++++++ .../websopy/mapper/AppConfigMapper.java | 35 ++++++ .../websopy/mapper/AppConfigMapper.xml | 29 +++++ .../websopy/service/AppConfigService.java | 119 ++++++++++++++++++ 6 files changed, 329 insertions(+), 14 deletions(-) create mode 100644 .workbuddy/memory/2026-06-17.md create mode 100644 src/main/java/com/gxwebsoft/websopy/entity/AppConfig.java create mode 100644 src/main/java/com/gxwebsoft/websopy/mapper/AppConfigMapper.java create mode 100644 src/main/java/com/gxwebsoft/websopy/mapper/AppConfigMapper.xml create mode 100644 src/main/java/com/gxwebsoft/websopy/service/AppConfigService.java diff --git a/.workbuddy/memory/2026-06-17.md b/.workbuddy/memory/2026-06-17.md new file mode 100644 index 0000000..1c81467 --- /dev/null +++ b/.workbuddy/memory/2026-06-17.md @@ -0,0 +1,58 @@ +# 2026-06-17 + +## WxLoginController 配置读取改造(app_config 跨表查询) + +### 需求 +- `WxLoginController.loginByMpWxPhone` 等微信小程序相关接口读取配置时,优先从 `gxwebsoft_core.app_config`(db_websopy 库)读取,命中失败时回退到 `gxwebsoft_core.sys_setting.mp-weixin`。 +- app_config 是 KV 行式存储:`id, tenant_id, config_key, config_value, category`(如 `category=wechat`,`config_key=wechat.appId`)。 + +### 方案(无需新建数据源) +- 直接 MyBatis XML 跨表:`FROM gxwebsoft_core.app_config` +- 走主数据源,依赖 gxwebsoft_core 库账号对 app_config 有 SELECT 权限 + +### 新增文件 +- `src/main/java/com/gxwebsoft/websopy/entity/AppConfig.java` —— 实体 +- `src/main/java/com/gxwebsoft/websopy/mapper/AppConfigMapper.java` —— Mapper(声明 selectByCategory) +- `src/main/java/com/gxwebsoft/websopy/mapper/AppConfigMapper.xml` —— XML 跨表查询(**XML 与 Mapper 同包**) +- `src/main/java/com/gxwebsoft/websopy/service/AppConfigService.java` —— 带 Redis 缓存 + +### 改造 WxLoginController +- 注入 `AppConfigService appConfigService` +- 新增私有方法 `getMpWxSetting(Integer tenantId)`:先读 app_config category=wechat,失败回退 `settingService.getBySettingKey("mp-weixin")` +- 替换 5 处调用: + - `getOpenIdByCode`(约 521 行) + - `getAccessToken(Integer)`(约 716 行) + - `getWxOpenId`(约 834 行) + - `getWxOpenIdOnly`(约 861 行) + - `loginByOpenId`(约 959 行,写回 AppId/AppSecret Redis 缓存保持一致) + +### 关键细节 +- 缓存 Key 前缀 `appConfig:{tenantId}:{category}`(与 setting:* 隔离) +- 缓存 TTL 2 小时 +- 配置文件组装:去掉 `config_key` 中的 `wechat.` 前缀,直接得到 `appId`/`appSecret` +- 编译错误原因及修复:Mapper interface 必须显式声明 selectByCategory 方法(编译期不查 XML) + +### 编译验证 +- `./mvnw clean compile -DskipTests` 通过 ✅ + +### 运行期 BeanCreationException 修复(2026-06-18 补记) +- 报错:`Bean named 'redisTemplate' expected StringRedisTemplate but was actually RedisTemplate` +- 原因:`@Resource` 按字段名(bean name)注入,容器中 `redisTemplate` 是 `RedisTemplate`,而 `StringRedisTemplate` bean 叫 `stringRedisTemplate` +- 修复:`AppConfigService` 字段名从 `redisTemplate` 改为 `stringRedisTemplate`,4 处引用同步修改 +- **项目规律:统一用字段名 `stringRedisTemplate`,禁止用 `redisTemplate` 注入 StringRedisTemplate** + +### 运行期 SQLSyntaxErrorException 修复(2026-06-18 补记) +- 报错:`Table 'gxwebsoft_core.app_config' doesn't exist` + TenantLineInnerInterceptor 自动追加 `AND tenant_id = 10611` +- 原因 1:app_config 表实际在 `db_websopy` 库中,不在 gxwebsoft_core +- 原因 2:`TenantLineInnerInterceptor` 对所有 SQL 自动追加 tenant_id 条件,导致参数重复 +- 实际表结构(从用户截图确认): + - 主键 `config_id`(不是 id) + - 分类字段 `config_type`(不是 category) + - 有 `deleted` 逻辑删除字段 + - tenant_id 示例值 105885 +- 修复: + 1. XML 表名改为 `db_websopy.app_config` + 2. Mapper 方法加 `@InterceptorIgnore(tenantLine = "true")`(与项目其他跨库 Mapper 一致) + 3. 实体类字段对齐实际表结构(configId/configType/deleted) + 4. Service 参数名 category → configType 全部对齐 +- **关键教训:跨库查询必须加 @InterceptorIgnore(tenantLine="true"),且表名必须带正确库名前缀** diff --git a/src/main/java/com/gxwebsoft/common/system/controller/WxLoginController.java b/src/main/java/com/gxwebsoft/common/system/controller/WxLoginController.java index 183888b..9f5332a 100644 --- a/src/main/java/com/gxwebsoft/common/system/controller/WxLoginController.java +++ b/src/main/java/com/gxwebsoft/common/system/controller/WxLoginController.java @@ -25,6 +25,7 @@ import com.gxwebsoft.common.system.param.LoginParam; import com.gxwebsoft.common.system.param.UserParam; import com.gxwebsoft.common.system.result.LoginResult; import com.gxwebsoft.common.system.service.*; +import com.gxwebsoft.websopy.service.AppConfigService; import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.Operation; import org.springframework.data.redis.core.StringRedisTemplate; @@ -67,6 +68,8 @@ public class WxLoginController extends BaseController { private UserRefereeService userRefereeService; @Resource private UserSyncService userSyncService; + @Resource + private AppConfigService appConfigService; public WxLoginController(StringRedisTemplate redisTemplate) { this.redisTemplate = redisTemplate; @@ -433,6 +436,26 @@ public class WxLoginController extends BaseController { return fail("更新失败"); } + /** + * 优先读取 db_websopy.app_config(category=wechat),不存在或异常时回退到 sys_setting (mp-weixin) + * + * @param tenantId 租户ID(传 null 时使用当前请求租户) + * @return JSONObject 配置内容(含 appId / appSecret 等字段) + */ + private JSONObject getMpWxSetting(Integer tenantId) { + Integer tid = tenantId != null ? tenantId : getTenantId(); + try { + JSONObject wechat = appConfigService.getByCategory("wechat", tid); + if (wechat != null && !wechat.isEmpty()) { + return wechat; + } + } catch (Exception e) { + System.err.println("[WxLoginController] 读取 app_config 失败,回退 sys_setting: " + e.getMessage()); + } + // 兜底:原 sys_setting.mp-weixin + return settingService.getBySettingKey("mp-weixin"); + } + /** * 新用户注册 */ @@ -494,8 +517,8 @@ public class WxLoginController extends BaseController { // 获取openid private JSONObject getOpenIdByCode(UserParam userParam) { try { - // 获取微信小程序配置信息 - JSONObject setting = settingService.getBySettingKey("mp-weixin"); + // 获取微信小程序配置信息(优先 app_config,回退 sys_setting) + JSONObject setting = getMpWxSetting(null); // 获取openId String apiUrl = "https://api.weixin.qq.com/sns/jscode2session?appid=" + setting.getString("appId") + "&secret=" + setting.getString("appSecret") + "&js_code=" + userParam.getCode() + "&grant_type=authorization_code"; // 执行get请求 @@ -689,8 +712,8 @@ public class WxLoginController extends BaseController { tenantId = getTenantId(); } String key = ACCESS_TOKEN_KEY.concat(":").concat(tenantId.toString()); - // 获取微信小程序配置信息 - JSONObject setting = settingService.getBySettingKey("mp-weixin"); + // 获取微信小程序配置信息(优先 app_config,回退 sys_setting) + JSONObject setting = getMpWxSetting(tenantId); if (setting == null) { throw new BusinessException("请先配置小程序"); } @@ -808,7 +831,7 @@ public class WxLoginController extends BaseController { // 请求微信接口获取openid String apiUrl = "https://api.weixin.qq.com/sns/jscode2session"; final HashMap map = new HashMap<>(); - final JSONObject setting = settingService.getBySettingKey("mp-weixin"); + final JSONObject setting = getMpWxSetting(null); final String appId = setting.getString("appId"); final String appSecret = setting.getString("appSecret"); map.put("appid", appId); @@ -835,7 +858,7 @@ public class WxLoginController extends BaseController { String apiUrl = "https://api.weixin.qq.com/sns/jscode2session"; final HashMap map = new HashMap<>(); - final JSONObject setting = settingService.getBySettingKey("mp-weixin"); + final JSONObject setting = getMpWxSetting(null); final String appId = setting.getString("appId"); final String appSecret = setting.getString("appSecret"); map.put("appid", appId); @@ -926,15 +949,15 @@ public class WxLoginController extends BaseController { @Operation(summary = "openid无感登录") @PostMapping("/loginByOpenId") public ApiResult loginByOpenId(@RequestBody Mp mp, HttpServletRequest request) { - // 获取小程序配置信息 - String key1 = "AppId:".concat(mp.getTenantId().toString()); - String key2 = "AppSecret:".concat(mp.getTenantId().toString()); - String AppId = redisUtil.get(key1); - String AppSecret = redisUtil.get(key2); + // 优先读 app_config(category=wechat),回退 sys_setting.mp-weixin + final JSONObject setting = getMpWxSetting(mp.getTenantId()); + if (setting == null) { + return fail("小程序未配置"); + } + final String AppId = setting.getString("appId"); + final String AppSecret = setting.getString("appSecret"); if (StrUtil.isBlank(AppId) || StrUtil.isBlank(AppSecret)) { - final JSONObject setting = settingService.getBySettingKey("mp-weixin"); - AppId = setting.getString("appId"); - AppSecret = setting.getString("appSecret"); + return fail("小程序配置不完整,请检查 appId 和 appSecret"); } // 请求微信接口获取openid diff --git a/src/main/java/com/gxwebsoft/websopy/entity/AppConfig.java b/src/main/java/com/gxwebsoft/websopy/entity/AppConfig.java new file mode 100644 index 0000000..dce9b6e --- /dev/null +++ b/src/main/java/com/gxwebsoft/websopy/entity/AppConfig.java @@ -0,0 +1,51 @@ +package com.gxwebsoft.websopy.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import lombok.Data; + +/** + * app_config(db_websopy)跨表映射实体 + * 实际查询通过 XML 使用全限定表名 db_websopy.app_config + * + * 实际表结构: + * config_id, app_id, config_key, config_value, config_type, + * is_encrypted, is_secret, updated_time, created_time, + * description, sort_number, tenant_id, deleted + * + * @author WebSoft + * @since 2026-06-17 + */ +@Data +public class AppConfig { + private static final long serialVersionUID = 1L; + + @TableId(value = "config_id", type = IdType.AUTO) + private Integer configId; + + @TableField("app_id") + private Integer appId; + + @TableField("tenant_id") + private Integer tenantId; + + @TableField("config_key") + private String configKey; + + @TableField("config_value") + private String configValue; + + /** 分类字段,如 wechat / payment / sms */ + @TableField("config_type") + private String configType; + + @TableField("is_encrypted") + private Integer isEncrypted; + + @TableField("is_secret") + private Integer isSecret; + + @TableField("description") + private String description; +} diff --git a/src/main/java/com/gxwebsoft/websopy/mapper/AppConfigMapper.java b/src/main/java/com/gxwebsoft/websopy/mapper/AppConfigMapper.java new file mode 100644 index 0000000..46da1c0 --- /dev/null +++ b/src/main/java/com/gxwebsoft/websopy/mapper/AppConfigMapper.java @@ -0,0 +1,35 @@ +package com.gxwebsoft.websopy.mapper; + +import com.baomidou.mybatisplus.annotation.InterceptorIgnore; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.gxwebsoft.websopy.entity.AppConfig; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * app_config(db_websopy)跨表查询 Mapper + * + * 注意:selectByCategory 标注了 @InterceptorIgnore(tenantLine = "true") + * 因为 XML 中使用全限定表名 db_websopy.app_config, + * TenantLineInnerInterceptor 会自动追加 tenant_id 条件导致参数重复。 + * 同时 app_config 的 tenant_id 与 gxwebsoft_core 不是同一套。 + * + * @author WebSoft + * @since 2026-06-17 + */ +@Mapper +public interface AppConfigMapper extends BaseMapper { + + /** + * 跨表查询 db_websopy.app_config(按 config_type 分组) + * + * @param tenantId 租户ID(app_config 表中的 tenant_id) + * @param configType 配置分类(如 wechat) + * @return 配置项列表 + */ + @InterceptorIgnore(tenantLine = "true") + List selectByCategory(@Param("tenantId") Integer tenantId, + @Param("configType") String configType); +} diff --git a/src/main/java/com/gxwebsoft/websopy/mapper/AppConfigMapper.xml b/src/main/java/com/gxwebsoft/websopy/mapper/AppConfigMapper.xml new file mode 100644 index 0000000..7ded385 --- /dev/null +++ b/src/main/java/com/gxwebsoft/websopy/mapper/AppConfigMapper.xml @@ -0,0 +1,29 @@ + + + + + + + + diff --git a/src/main/java/com/gxwebsoft/websopy/service/AppConfigService.java b/src/main/java/com/gxwebsoft/websopy/service/AppConfigService.java new file mode 100644 index 0000000..3612645 --- /dev/null +++ b/src/main/java/com/gxwebsoft/websopy/service/AppConfigService.java @@ -0,0 +1,119 @@ +package com.gxwebsoft.websopy.service; + +import com.alibaba.fastjson.JSONObject; +import com.gxwebsoft.websopy.entity.AppConfig; +import com.gxwebsoft.websopy.mapper.AppConfigMapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * app_config(db_websopy)配置读取服务 + * 优先读 Redis 缓存,未命中跨表查 gxwebsoft_core.app_config 并写入缓存 + * + * @author WebSoft + * @since 2026-06-17 + */ +@Slf4j +@Service +public class AppConfigService { + + @Resource + private AppConfigMapper appConfigMapper; + + // 字段名与 bean 名保持一致:stringRedisTemplate(StringRedisTemplate bean) + // 不能用 redisTemplate,容器中该名字对应的是 RedisTemplate + @Resource + private StringRedisTemplate stringRedisTemplate; + + /** 缓存有效期:2 小时(与微信 access_token 一致,避免长时间 stale) */ + private static final long CACHE_TTL_HOURS = 2L; + + /** 缓存 Key 前缀,避免与 setting:* 冲突 */ + private static final String CACHE_KEY_PREFIX = "appConfig:"; + + /** + * 按 config_type 读取 app_config 全部配置项,并组装为 JSON + * + * @param configType 配置分类(如 wechat,对应 app_config.config_type 字段) + * @param tenantId 租户 ID(必传) + * @return 拼装后的 JSON;若 config_type 下无记录或发生异常,返回 null(由调用方回退到 sys_setting) + */ + public JSONObject getByCategory(String configType, Integer tenantId) { + if (tenantId == null) { + log.warn("[AppConfigService] tenantId 为空,跳过 configType={}", configType); + return null; + } + String cacheKey = buildCacheKey(configType, tenantId); + + // 1. 命中 Redis 直接返回 + String cached = stringRedisTemplate.opsForValue().get(cacheKey); + if (cached != null && !cached.isEmpty()) { + try { + return JSONObject.parseObject(cached); + } catch (Exception e) { + log.warn("[AppConfigService] 缓存解析失败,删除后回源 configType={}, tenantId={}, err={}", + configType, tenantId, e.getMessage()); + stringRedisTemplate.delete(cacheKey); + } + } + + // 2. 跨表查 db_websopy.app_config + List list; + try { + list = appConfigMapper.selectByCategory(tenantId, configType); + } catch (Exception e) { + log.error("[AppConfigService] 跨表查询失败 configType={}, tenantId={}, err={}", + configType, tenantId, e.getMessage(), e); + return null; + } + if (list == null || list.isEmpty()) { + log.info("[AppConfigService] 未找到配置 configType={}, tenantId={}", configType, tenantId); + return null; + } + + // 3. 组装为 JSON(去掉 config_key 中的 "configType." 前缀) + JSONObject result = new JSONObject(); + String prefix = configType + "."; + for (AppConfig c : list) { + String key = c.getConfigKey(); + if (key == null) { + continue; + } + if (key.startsWith(prefix)) { + key = key.substring(prefix.length()); + } + result.put(key, c.getConfigValue()); + } + + // 4. 写缓存 2 小时 + try { + stringRedisTemplate.opsForValue().set(cacheKey, result.toJSONString(), + CACHE_TTL_HOURS, TimeUnit.HOURS); + } catch (Exception e) { + log.warn("[AppConfigService] 写缓存失败(不影响返回) err={}", e.getMessage()); + } + + log.info("[AppConfigService] 从 app_config 读取配置 configType={}, tenantId={}, fields={}", + configType, tenantId, result.size()); + return result; + } + + /** + * 主动失效缓存(供配置变更时调用) + */ + public void evict(String configType, Integer tenantId) { + if (tenantId == null) { + return; + } + stringRedisTemplate.delete(buildCacheKey(configType, tenantId)); + } + + private String buildCacheKey(String configType, Integer tenantId) { + return CACHE_KEY_PREFIX + tenantId + ":" + configType; + } +} From 307c20956581dac9bbb628989a02251974051c50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Thu, 18 Jun 2026 17:17:45 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix(appconfig):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E8=B7=A8=E8=A1=A8=E6=9F=A5=E8=AF=A2=E4=BB=A5=E7=A1=AE=E4=BF=9D?= =?UTF-8?q?=E4=BA=A7=E5=93=81=E6=9C=89=E6=95=88=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - selectByCategory SQL 增加 INNER JOIN app_product,确保只返回该租户下有效产品配置 - 加入关联条件 ap.product_id = ac.app_id 和 ap.tenant_id = #{tenantId} - 保证查询结果中 app_config 的 app_id 必须对应有效且属于当前租户的产品 - 维护原有租户过滤和配置类型过滤逻辑,增强数据准确性 - 该改动对 Service 和 Controller 层无影响,无需修改调用逻辑 --- .workbuddy/memory/2026-06-18.md | 11 +++++++ .../websopy/mapper/AppConfigMapper.xml | 32 +++++++++++-------- 2 files changed, 29 insertions(+), 14 deletions(-) create mode 100644 .workbuddy/memory/2026-06-18.md diff --git a/.workbuddy/memory/2026-06-18.md b/.workbuddy/memory/2026-06-18.md new file mode 100644 index 0000000..28da208 --- /dev/null +++ b/.workbuddy/memory/2026-06-18.md @@ -0,0 +1,11 @@ +# 2026-06-18 工作日志 + +## AppConfigMapper.xml 添加关联查询 + +**改动文件**: `src/main/java/com/gxwebsoft/websopy/mapper/AppConfigMapper.xml` + +- `selectByCategory` SQL 增加了 `INNER JOIN db_websopy.app_product`,关联条件: + - `ap.product_id = ac.app_id` + - `ap.tenant_id = #{tenantId}` +- 目的:确保只返回该租户下有效产品的配置,防止 app_config 中的 app_id 指向非该租户的产品 +- Service 层和 Controller 层无需改动,不影响现有调用 diff --git a/src/main/java/com/gxwebsoft/websopy/mapper/AppConfigMapper.xml b/src/main/java/com/gxwebsoft/websopy/mapper/AppConfigMapper.xml index 7ded385..8d8ef92 100644 --- a/src/main/java/com/gxwebsoft/websopy/mapper/AppConfigMapper.xml +++ b/src/main/java/com/gxwebsoft/websopy/mapper/AppConfigMapper.xml @@ -3,27 +3,31 @@