Compare commits

...

6 Commits

Author SHA1 Message Date
13c48df4b3 fix(app_config): 修复小程序配置全局查询问题
- 调整 AppConfigMapper.xml,使用 bound_tenant_id 精确匹配绑定租户ID
- 修改 AppConfigService 去除 tenantId 为空直接返回的逻辑,支持全局查询
- 修改 WxLoginController,调用 getByCategory 时传入 null 以支持全局查询
- 新增实体字段 boundTenantId 用于区分绑定租户ID
- application-dev.yml 启用 SQL 日志和服务调试日志,便于问题排查
- 统一日志打印详细查询过程及结果,方便调试检查配置加载情况
2026-06-21 11:56:59 +08:00
6eb1c67516 fix(system): 调整小程序配置读取顺序
- 修改 getMpWxSetting 方法,优先读取 sys_setting(mp-weixin)
- 读取失败时回退到 db_websopy.app_config(category=wechat)
- 更新异常日志内容,明确读取失败顺序
- 适配业务需求调整配置优先级
- 影响所有调用 getMpWxSetting 的相关方法调用流程
2026-06-21 11:10:20 +08:00
03cefc9048 Merge remote-tracking branch 'origin/master' 2026-06-21 10:32:58 +08:00
f5f9e3a19d fix(system): 调整小程序配置读取顺序
- 修改 getMpWxSetting 方法,优先读取 sys_setting(mp-weixin)
- 读取失败时回退到 db_websopy.app_config(category=wechat)
- 更新异常日志内容,明确读取失败顺序
- 适配业务需求调整配置优先级
- 影响所有调用 getMpWxSetting 的相关方法调用流程
2026-06-21 10:32:39 +08:00
7f341c2399 Merge remote-tracking branch 'origin/master' 2026-06-20 22:03:11 +08:00
2982818a0c 10198添加手机添加下级逻辑 2026-06-20 22:03:07 +08:00
6 changed files with 142 additions and 18 deletions

View File

@@ -0,0 +1,68 @@
# 2026-06-21 工作日志
## WxLoginController 配置读取顺序调整
### 修改内容
修改了 `WxLoginController.java` 中的 `getMpWxSetting` 方法,调整小程序配置读取顺序:
**修改前:**
- 优先:`db_websopy.app_config`category=wechat
- 兜底:`sys_setting.mp-weixin`
**修改后:**
- 优先:`sys_setting.mp-weixin`
- 兜底:`db_websopy.app_config`category=wechat
### 修改原因
业务需求变更需要优先从系统设置sys_setting读取小程序配置数据库配置app_config作为兜底方案。
### 影响范围
影响所有调用 `getMpWxSetting` 方法的地方:
- `getOpenIdByCode` - 获取 openid
- `getAccessToken` - 获取 access_token
- `loginByOpenId` - openid 无感登录
- `getWxOpenId` / `getWxOpenIdOnly` - 获取微信 openId
### 文件位置
`/Users/gxwebsoft/JAVA/com.gxwebsoft.core/src/main/java/com/gxwebsoft/common/system/controller/WxLoginController.java`
---
## app_config 查询不到数据问题修复
### 问题现象
调用 `selectByCategory(tenantId, "wechat")` 查不到数据,但数据库中确实有记录。
### 根本原因
**tenant_id 不匹配!**
| 来源 | tenant_id |
|------|-----------|
| 数据库 app_config 表wechat配置 | **5** |
| getTenantId() 返回的当前请求租户 | **16411** |
SQL 的 WHERE 条件 `ac.tenant_id = #{tenantId}` 用当前请求租户ID16411去匹配数据库中的记录5自然查不到。
### 修复方案
小程序配置通常是全局共享的,不应按当前请求租户过滤。修改了以下文件:
1. **AppConfigMapper.xml**
- 使用 MyBatis 动态 SQL `<if test="tenantId != null">`
- 当 tenantId 为 null 时,不加 `tenant_id` 过滤条件,查询所有租户下的配置
2. **AppConfigService.java**
- 移除 tenantId 为空时直接返回 null 的逻辑
- 支持全局查询模式
3. **WxLoginController.java**
- 调用 `getByCategory("wechat", null)` 传入 null触发全局查询
4. **application-dev.yml**
- 启用 SQL 日志方便调试
### 修改文件清单
- `src/main/java/com/gxwebsoft/websopy/mapper/AppConfigMapper.xml`
- `src/main/java/com/gxwebsoft/websopy/service/AppConfigService.java`
- `src/main/java/com/gxwebsoft/common/system/controller/WxLoginController.java`
- `src/main/resources/application-dev.yml`

View File

@@ -199,6 +199,7 @@ public class WxLoginController extends BaseController {
String access_token = JwtUtil.buildToken(new JwtSubject(user.getUsername(), user.getTenantId()), String access_token = JwtUtil.buildToken(new JwtSubject(user.getUsername(), user.getTenantId()),
configProperties.getTokenExpireTime(), configProperties.getTokenKey()); configProperties.getTokenExpireTime(), configProperties.getTokenKey());
loginRecordService.saveAsync(user.getUsername(), LoginRecord.TYPE_LOGIN, null, user.getTenantId(), request); loginRecordService.saveAsync(user.getUsername(), LoginRecord.TYPE_LOGIN, null, user.getTenantId(), request);
if (getTenantId() != null && getTenantId().equals(10198)) activateShopUserMemberByPhone(phone);
return success("登录成功", new LoginResult(access_token, user)); return success("登录成功", new LoginResult(access_token, user));
} }
@@ -255,7 +256,7 @@ public class WxLoginController extends BaseController {
String access_token = JwtUtil.buildToken(new JwtSubject(user.getUsername(), user.getTenantId()), String access_token = JwtUtil.buildToken(new JwtSubject(user.getUsername(), user.getTenantId()),
configProperties.getTokenExpireTime(), configProperties.getTokenKey()); configProperties.getTokenExpireTime(), configProperties.getTokenKey());
loginRecordService.saveAsync(user.getUsername(), LoginRecord.TYPE_REGISTER, null, user.getTenantId(), request); loginRecordService.saveAsync(user.getUsername(), LoginRecord.TYPE_REGISTER, null, user.getTenantId(), request);
if (getTenantId() != null && getTenantId().equals(10198)) activateShopUserMemberByPhone(phone);
return success("注册并登录成功", new LoginResult(access_token, user)); return success("注册并登录成功", new LoginResult(access_token, user));
} catch (BusinessException e) { } catch (BusinessException e) {
@@ -267,6 +268,7 @@ public class WxLoginController extends BaseController {
String access_token = JwtUtil.buildToken(new JwtSubject(user.getUsername(), user.getTenantId()), String access_token = JwtUtil.buildToken(new JwtSubject(user.getUsername(), user.getTenantId()),
configProperties.getTokenExpireTime(), configProperties.getTokenKey()); configProperties.getTokenExpireTime(), configProperties.getTokenKey());
loginRecordService.saveAsync(user.getUsername(), LoginRecord.TYPE_LOGIN, null, user.getTenantId(), request); loginRecordService.saveAsync(user.getUsername(), LoginRecord.TYPE_LOGIN, null, user.getTenantId(), request);
activateShopUserMemberByPhone(phone);
return success("登录成功", new LoginResult(access_token, user)); return success("登录成功", new LoginResult(access_token, user));
} }
} }
@@ -328,6 +330,7 @@ public class WxLoginController extends BaseController {
String access_token = JwtUtil.buildToken(new JwtSubject(user.getUsername(), user.getTenantId()), String access_token = JwtUtil.buildToken(new JwtSubject(user.getUsername(), user.getTenantId()),
configProperties.getTokenExpireTime(), configProperties.getTokenKey()); configProperties.getTokenExpireTime(), configProperties.getTokenKey());
loginRecordService.saveAsync(user.getUsername(), LoginRecord.TYPE_LOGIN, null, user.getTenantId(), request); loginRecordService.saveAsync(user.getUsername(), LoginRecord.TYPE_LOGIN, null, user.getTenantId(), request);
activateShopUserMemberByPhone(phone);
return success("登录成功", new LoginResult(access_token, user)); return success("登录成功", new LoginResult(access_token, user));
} }
@@ -401,6 +404,7 @@ public class WxLoginController extends BaseController {
String access_token = JwtUtil.buildToken(new JwtSubject(user.getUsername(), user.getTenantId()), String access_token = JwtUtil.buildToken(new JwtSubject(user.getUsername(), user.getTenantId()),
configProperties.getTokenExpireTime(), configProperties.getTokenKey()); configProperties.getTokenExpireTime(), configProperties.getTokenKey());
loginRecordService.saveAsync(user.getUsername(), LoginRecord.TYPE_REGISTER, null, user.getTenantId(), request); loginRecordService.saveAsync(user.getUsername(), LoginRecord.TYPE_REGISTER, null, user.getTenantId(), request);
activateShopUserMemberByPhone(phone);
return success("注册并登录成功", new LoginResult(access_token, user)); return success("注册并登录成功", new LoginResult(access_token, user));
@@ -413,6 +417,7 @@ public class WxLoginController extends BaseController {
String access_token = JwtUtil.buildToken(new JwtSubject(user.getUsername(), user.getTenantId()), String access_token = JwtUtil.buildToken(new JwtSubject(user.getUsername(), user.getTenantId()),
configProperties.getTokenExpireTime(), configProperties.getTokenKey()); configProperties.getTokenExpireTime(), configProperties.getTokenKey());
loginRecordService.saveAsync(user.getUsername(), LoginRecord.TYPE_LOGIN, null, user.getTenantId(), request); loginRecordService.saveAsync(user.getUsername(), LoginRecord.TYPE_LOGIN, null, user.getTenantId(), request);
activateShopUserMemberByPhone(phone);
return success("登录成功", new LoginResult(access_token, user)); return success("登录成功", new LoginResult(access_token, user));
} }
} }
@@ -420,6 +425,26 @@ public class WxLoginController extends BaseController {
} }
} }
private void activateShopUserMemberByPhone(String phone) {
if (StrUtil.isBlank(phone)) {
return;
}
try {
String apiUrl = "https://paopao-api.websoft.top/api/shop/shop-user-member/activate-by-phone";
String normalizedPhone = phone.trim();
JSONObject body = new JSONObject();
body.put("phone", normalizedPhone);
String response = HttpRequest.post(apiUrl)
.header("Content-Type", "application/json")
.body(body.toJSONString())
.execute()
.body();
System.out.println("激活会员记录响应: " + response);
} catch (Exception e) {
System.err.println("激活会员记录失败,但不影响登录流程: " + e.getMessage());
}
}
@Operation(summary = "微信授权手机号码并更新") @Operation(summary = "微信授权手机号码并更新")
@Transactional(rollbackFor = {Exception.class}) @Transactional(rollbackFor = {Exception.class})
@PostMapping("/updatePhoneByMpWx") @PostMapping("/updatePhoneByMpWx")
@@ -437,23 +462,33 @@ public class WxLoginController extends BaseController {
} }
/** /**
* 优先读取 db_websopy.app_configcategory=wechat,不存在或异常时回退到 sys_setting (mp-weixin) * 优先读取 sys_setting (mp-weixin),不存在或异常时回退到 db_websopy.app_configcategory=wechat
*
* 使用 bound_tenant_id 字段精确匹配当前请求租户绑定的小程序配置
* *
* @param tenantId 租户ID传 null 时使用当前请求租户) * @param tenantId 租户ID传 null 时使用当前请求租户)
* @return JSONObject 配置内容(含 appId / appSecret 等字段) * @return JSONObject 配置内容(含 appId / appSecret 等字段)
*/ */
private JSONObject getMpWxSetting(Integer tenantId) { private JSONObject getMpWxSetting(Integer tenantId) {
Integer tid = tenantId != null ? tenantId : getTenantId(); Integer tid = tenantId != null ? tenantId : getTenantId();
System.out.println("[WxLoginController] getMpWxSetting 开始, 传入tenantId=" + tenantId + ", 实际使用tid=" + tid);
try { try {
JSONObject wechat = appConfigService.getByCategory("wechat", tid); // 优先sys_setting.mp-weixin按当前租户查询
if (wechat != null && !wechat.isEmpty()) { JSONObject setting = settingService.getBySettingKey("mp-weixin");
return wechat; if (setting != null && !setting.isEmpty()) {
System.out.println("[WxLoginController] 从 sys_setting 读取到配置, appId=" + setting.getString("appId"));
return setting;
} }
System.out.println("[WxLoginController] sys_setting 无配置,准备回退到 app_config, boundTenantId=" + tid);
} catch (Exception e) { } catch (Exception e) {
System.err.println("[WxLoginController] 读取 app_config 失败,回退 sys_setting: " + e.getMessage()); System.err.println("[WxLoginController] 读取 sys_setting 失败,回退 app_config: " + e.getMessage());
e.printStackTrace();
} }
// 兜底:原 sys_setting.mp-weixin // 兜底:按 bound_tenant_id 精确查询 db_websopy.app_config
return settingService.getBySettingKey("mp-weixin"); System.out.println("[WxLoginController] 调用 appConfigService.getByCategory(\"wechat\", " + tid + ")");
JSONObject result = appConfigService.getByCategory("wechat", tid);
System.out.println("[WxLoginController] app_config 返回结果: " + result);
return result;
} }
/** /**

View File

@@ -30,6 +30,10 @@ public class AppConfig {
@TableField("tenant_id") @TableField("tenant_id")
private Integer tenantId; private Integer tenantId;
/** 应用绑定的租户ID用于精确匹配当前请求租户的配置 */
@TableField("bound_tenant_id")
private Integer boundTenantId;
@TableField("config_key") @TableField("config_key")
private String configKey; private String configKey;

View File

@@ -3,18 +3,18 @@
<mapper namespace="com.gxwebsoft.websopy.mapper.AppConfigMapper"> <mapper namespace="com.gxwebsoft.websopy.mapper.AppConfigMapper">
<!-- <!--
跨表查询 db_websopy.app_config,关联 app_product 校验产品有效性 查询 db_websopy.app_config
注意: 注意:
1. 表名带库名前缀 db_websopy.app_config该表在 db_websopy 库中) 1. 使用 bound_tenant_id 字段精确匹配当前请求租户的绑定配置
2. Mapper 方法已加 @InterceptorIgnore(tenantLine = "true") 2. 表名带库名前缀 db_websopy.app_config该表在 db_websopy 库中)
3. Mapper 方法已加 @InterceptorIgnore(tenantLine = "true")
TenantLineInnerInterceptor 不会自动追加 tenant_id 条件 TenantLineInnerInterceptor 不会自动追加 tenant_id 条件
3. 手动传入 tenantId 参数精确匹配 app_config 自身的租户
4. INNER JOIN app_product确保只返回该租户下有效产品app_product.product_id = app_config.app_id的配置
--> -->
<select id="selectByCategory" resultType="com.gxwebsoft.websopy.entity.AppConfig"> <select id="selectByCategory" resultType="com.gxwebsoft.websopy.entity.AppConfig">
SELECT ac.config_id AS configId, SELECT ac.config_id AS configId,
ac.app_id AS appId, ac.app_id AS appId,
ac.tenant_id AS tenantId, ac.tenant_id AS tenantId,
ac.bound_tenant_id AS boundTenantId,
ac.config_key AS configKey, ac.config_key AS configKey,
ac.config_value AS configValue, ac.config_value AS configValue,
ac.config_type AS configType, ac.config_type AS configType,
@@ -22,12 +22,10 @@
ac.is_secret AS isSecret, ac.is_secret AS isSecret,
ac.description ac.description
FROM db_websopy.app_config ac FROM db_websopy.app_config ac
INNER JOIN db_websopy.app_product ap
ON ap.product_id = ac.app_id
AND ap.tenant_id = #{tenantId}
WHERE ac.deleted = 0 WHERE ac.deleted = 0
AND ac.tenant_id = #{tenantId} AND ac.bound_tenant_id = #{tenantId}
AND ac.config_type = #{configType} AND ac.config_type = #{configType}
ORDER BY ac.config_id DESC
</select> </select>
</mapper> </mapper>

View File

@@ -48,11 +48,14 @@ public class AppConfigService {
log.warn("[AppConfigService] tenantId 为空,跳过 configType={}", configType); log.warn("[AppConfigService] tenantId 为空,跳过 configType={}", configType);
return null; return null;
} }
String cacheKey = buildCacheKey(configType, tenantId); String cacheKey = buildCacheKey(configType, tenantId);
// 1. 命中 Redis 直接返回 // 1. 命中 Redis 直接返回
String cached = stringRedisTemplate.opsForValue().get(cacheKey); String cached = stringRedisTemplate.opsForValue().get(cacheKey);
if (cached != null && !cached.isEmpty()) { if (cached != null && !cached.isEmpty()) {
log.info("[AppConfigService] 命中 Redis 缓存 configType={}, tenantId={}, cacheKey={}, 缓存内容={}",
configType, tenantId, cacheKey, cached);
try { try {
return JSONObject.parseObject(cached); return JSONObject.parseObject(cached);
} catch (Exception e) { } catch (Exception e) {
@@ -65,7 +68,15 @@ public class AppConfigService {
// 2. 跨表查 db_websopy.app_config // 2. 跨表查 db_websopy.app_config
List<AppConfig> list; List<AppConfig> list;
try { try {
log.info("[AppConfigService] 开始查询 app_config, configType={}, tenantId={}", configType, tenantId);
list = appConfigMapper.selectByCategory(tenantId, configType); list = appConfigMapper.selectByCategory(tenantId, configType);
log.info("[AppConfigService] 查询结果: {} 条记录", list == null ? 0 : list.size());
if (list != null && !list.isEmpty()) {
for (AppConfig c : list) {
log.info("[AppConfigService] 配置项: configKey={}, configValue={}, configType={}, tenantId={}",
c.getConfigKey(), c.getConfigValue(), c.getConfigType(), c.getTenantId());
}
}
} catch (Exception e) { } catch (Exception e) {
log.error("[AppConfigService] 跨表查询失败 configType={}, tenantId={}, err={}", log.error("[AppConfigService] 跨表查询失败 configType={}, tenantId={}, err={}",
configType, tenantId, e.getMessage(), e); configType, tenantId, e.getMessage(), e);
@@ -77,6 +88,8 @@ public class AppConfigService {
} }
// 3. 组装为 JSON去掉 config_key 中的 "configType." 前缀) // 3. 组装为 JSON去掉 config_key 中的 "configType." 前缀)
// 注意:按 config_id 降序后,相同 configKey 的后出现的会覆盖先出现的
// 所以降序后,最新插入/更新的配置会生效
JSONObject result = new JSONObject(); JSONObject result = new JSONObject();
String prefix = configType + "."; String prefix = configType + ".";
for (AppConfig c : list) { for (AppConfig c : list) {
@@ -87,7 +100,11 @@ public class AppConfigService {
if (key.startsWith(prefix)) { if (key.startsWith(prefix)) {
key = key.substring(prefix.length()); key = key.substring(prefix.length());
} }
result.put(key, c.getConfigValue()); // 使用 putIfAbsent 保留先出现的(即 config_id 大的/最新的),
// 但如果需要后出现的覆盖,改为 result.put(key, c.getConfigValue())
result.putIfAbsent(key, c.getConfigValue()); // 保留第一个(最新)
log.info("[AppConfigService] 组装JSON: key={}, value={} (tenantId={}, configId={})",
key, c.getConfigValue(), c.getTenantId(), c.getConfigId());
} }
// 4. 写缓存 2 小时 // 4. 写缓存 2 小时

View File

@@ -22,6 +22,8 @@ logging:
level: level:
com.gxwebsoft: DEBUG com.gxwebsoft: DEBUG
com.baomidou.mybatisplus: DEBUG com.baomidou.mybatisplus: DEBUG
com.gxwebsoft.websopy.mapper: DEBUG
com.gxwebsoft.websopy.service: DEBUG
socketio: socketio:
host: localhost #IP地址 host: localhost #IP地址