Compare commits
14 Commits
f7e3cad931
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 13c48df4b3 | |||
| 6eb1c67516 | |||
| 03cefc9048 | |||
| f5f9e3a19d | |||
| 7f341c2399 | |||
| 2982818a0c | |||
| 307c209565 | |||
| bae1f75533 | |||
| 797a140f11 | |||
| fb75156a03 | |||
| 779f90e813 | |||
| 2a880df691 | |||
| ec3bededa4 | |||
| e2c84b94b9 |
60
.workbuddy/memory/2026-05-13.md
Normal file
60
.workbuddy/memory/2026-05-13.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# 2026-05-13 工作记录
|
||||
|
||||
## 卡密充值功能开发
|
||||
|
||||
### 后端(paopao-java)
|
||||
|
||||
**已完成文件:**
|
||||
- `ShopRechargeCodeController.java` - 充值兑换码管理
|
||||
- `ShopRechargeCodeService.java` - Service接口
|
||||
- `ShopRechargeCodeServiceImpl.java` - Service实现
|
||||
- `ShopRechargeCode.java` - 实体类
|
||||
- `ShopRechargeRecord.java` - 充值记录实体
|
||||
- `ShopRechargeCodeMapper.java` - Mapper接口
|
||||
- `ShopRechargeCodeParam.java` - 查询参数
|
||||
- `ShopRechargeRecordMapper.java` - 充值记录Mapper
|
||||
- `ShopRechargeRecordService.java` - 充值记录Service
|
||||
- `ShopRechargeRecordServiceImpl.java` - 充值记录Service实现
|
||||
- `ShopRechargeRecordController.java` - 充值记录Controller (新增)
|
||||
- `ShopRechargeRecordParam.java` - 充值记录查询参数 (新增)
|
||||
|
||||
**API接口:**
|
||||
- `GET /api/shop/recharge-code/page` - 分页查询兑换码
|
||||
- `POST /api/shop/recharge-code/generate` - 生成兑换码
|
||||
- `POST /api/shop/recharge-code/batch-generate` - 批量生成兑换码
|
||||
- `POST /api/shop/recharge-code/use` - 使用兑换码
|
||||
- `GET /api/shop/recharge-code/info` - 获取兑换码信息
|
||||
- `GET /api/shop/recharge-code/export/{batchNo}` - 导出批次兑换码
|
||||
- `DELETE /api/shop/recharge-code/{id}` - 删除兑换码
|
||||
- `GET /api/shop/recharge-record/page` - 分页查询充值记录
|
||||
|
||||
### 前端(paopao-vue)
|
||||
|
||||
**已完成文件:**
|
||||
- `/api/shop/shopRechargeCode/index.ts` - 兑换码API
|
||||
- `/api/shop/shopRechargeCode/model/index.ts` - 兑换码Model
|
||||
- `/api/shop/shopRechargeRecord/index.ts` - 充值记录API (新增)
|
||||
- `/api/shop/shopRechargeRecord/model.ts` - 充值记录Model (新增)
|
||||
- `/views/shop/shopRechargeCode/index.vue` - 兑换码管理页面 (整合Tab)
|
||||
- `/views/shop/shopRechargeRecord/index.vue` - 兑换记录页面 (新增)
|
||||
|
||||
**功能特性:**
|
||||
- 生成单个/批量兑换码
|
||||
- 兑换码状态管理(未使用/已使用/已过期)
|
||||
- 兑换明细查询
|
||||
- 统计卡片(总数量、未使用、已使用、已过期)
|
||||
|
||||
### 数据库
|
||||
|
||||
**SQL文件:**
|
||||
- `/sql/shop_recharge_code.sql` - 兑换码表建表SQL
|
||||
- `/sql/shop_recharge_record.sql` - 充值记录表建表SQL (新增)
|
||||
|
||||
**需要执行的SQL:**
|
||||
1. 创建 `shop_recharge_record` 表
|
||||
2. 配置后台菜单权限
|
||||
|
||||
### 待办事项
|
||||
- [ ] 执行数据库建表SQL
|
||||
- [ ] 配置后台菜单权限
|
||||
- [ ] 测试完整流程
|
||||
58
.workbuddy/memory/2026-06-17.md
Normal file
58
.workbuddy/memory/2026-06-17.md
Normal file
@@ -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<Object,Object>`,而 `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"),且表名必须带正确库名前缀**
|
||||
11
.workbuddy/memory/2026-06-18.md
Normal file
11
.workbuddy/memory/2026-06-18.md
Normal file
@@ -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 层无需改动,不影响现有调用
|
||||
68
.workbuddy/memory/2026-06-21.md
Normal file
68
.workbuddy/memory/2026-06-21.md
Normal 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}` 用当前请求租户ID(16411)去匹配数据库中的记录(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`
|
||||
@@ -54,6 +54,8 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
||||
"/hxz/v1/**",
|
||||
"/api/sendSmsCaptcha",
|
||||
"/api/loginBySms",
|
||||
"/api/loginBySuperAdminSms",
|
||||
"/api/loginByDeveloperSms",
|
||||
"/api/system/user/regByPhone",
|
||||
"/api/parseToken/*",
|
||||
"/api/login-alipay/*",
|
||||
|
||||
@@ -463,6 +463,18 @@ public class MainController extends BaseController {
|
||||
return fail("该手机号码未注册!");
|
||||
}
|
||||
}
|
||||
if (param.getScene() != null && param.getScene().equals("superAdminLogin")) {
|
||||
final User superAdmin = userService.getLastLoginSuperAdminByPhone(param.getPhone());
|
||||
if (ObjectUtil.isEmpty(superAdmin)) {
|
||||
return fail("该手机号码未注册超级管理员账号!");
|
||||
}
|
||||
}
|
||||
if (param.getScene() != null && param.getScene().equals("developerLogin")) {
|
||||
final User developer = userService.getLastLoginDeveloperByPhone(param.getPhone());
|
||||
if (ObjectUtil.isEmpty(developer)) {
|
||||
return fail("该手机号码未注册开发者账号!");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Integer tenantId = getTenantId();
|
||||
@@ -693,6 +705,124 @@ public class MainController extends BaseController {
|
||||
return success("登录成功", new LoginResult(access_token, user));
|
||||
}
|
||||
|
||||
@Operation(summary = "超级管理员短信验证码登录")
|
||||
@PostMapping("/loginBySuperAdminSms")
|
||||
public ApiResult<LoginResult> loginBySuperAdminSms(@RequestBody LoginParam param, HttpServletRequest request) {
|
||||
if (param == null) {
|
||||
return fail("参数不能为空", null);
|
||||
}
|
||||
final String phone = param.getPhone();
|
||||
if (!CommonUtil.isValidPhoneNumber(phone)) {
|
||||
return fail("请输入有效的手机号码", null);
|
||||
}
|
||||
String code = param.getCode();
|
||||
if (StrUtil.isBlank(code)) {
|
||||
code = param.getSmsCode();
|
||||
}
|
||||
if (StrUtil.isBlank(code)) {
|
||||
return fail("验证码不能为空", null);
|
||||
}
|
||||
|
||||
String smsCode = redisUtil.get("code:" + phone);
|
||||
String devSmsCode = redisUtil.get(CACHE_KEY_VERIFICATION_CODE_BY_DEV_SMS);
|
||||
if (!StrUtil.equals(code, smsCode) && !StrUtil.equals(code, devSmsCode)) {
|
||||
String message = "验证码不正确";
|
||||
loginRecordService.saveAsync(phone, LoginRecord.TYPE_ERROR, message, null, request);
|
||||
return fail(message, null);
|
||||
}
|
||||
|
||||
User user = userService.getLastLoginSuperAdminByPhone(phone);
|
||||
if (user == null) {
|
||||
String message = "用户不存在";
|
||||
loginRecordService.saveAsync(phone, LoginRecord.TYPE_ERROR, message, null, request);
|
||||
return fail(message, null);
|
||||
}
|
||||
if (!Boolean.TRUE.equals(user.getIsSuperAdmin())) {
|
||||
String message = "非超级管理员账号不允许登录";
|
||||
loginRecordService.saveAsync(phone, LoginRecord.TYPE_ERROR, message, user.getTenantId(), request);
|
||||
return fail(message, null);
|
||||
}
|
||||
if (!user.getStatus().equals(0)) {
|
||||
String message = "账号被冻结";
|
||||
loginRecordService.saveAsync(user.getUsername(), LoginRecord.TYPE_ERROR, message, user.getTenantId(), request);
|
||||
return fail(message, null);
|
||||
}
|
||||
|
||||
Long tokenExpireTime = configProperties.getTokenExpireTime();
|
||||
final JSONObject register = cacheClient.getSettingInfo("register", user.getTenantId());
|
||||
if (register != null) {
|
||||
final String ExpireTime = register.getString("tokenExpireTime");
|
||||
if (ExpireTime != null) {
|
||||
tokenExpireTime = Long.valueOf(ExpireTime);
|
||||
}
|
||||
}
|
||||
|
||||
loginRecordService.saveAsync(user.getUsername(), LoginRecord.TYPE_LOGIN, null, user.getTenantId(), request);
|
||||
String access_token = JwtUtil.buildToken(new JwtSubject(user.getUsername(), user.getTenantId()),
|
||||
tokenExpireTime, configProperties.getTokenKey());
|
||||
redisUtil.set("access_token:" + user.getUserId(), access_token, tokenExpireTime, TimeUnit.SECONDS);
|
||||
return success("登录成功", new LoginResult(access_token, user));
|
||||
}
|
||||
|
||||
@Operation(summary = "开发者短信验证码登录")
|
||||
@PostMapping("/loginByDeveloperSms")
|
||||
public ApiResult<LoginResult> loginByDeveloperSms(@RequestBody LoginParam param, HttpServletRequest request) {
|
||||
if (param == null) {
|
||||
return fail("参数不能为空", null);
|
||||
}
|
||||
final String phone = param.getPhone();
|
||||
if (!CommonUtil.isValidPhoneNumber(phone)) {
|
||||
return fail("请输入有效的手机号码", null);
|
||||
}
|
||||
String code = param.getCode();
|
||||
if (StrUtil.isBlank(code)) {
|
||||
code = param.getSmsCode();
|
||||
}
|
||||
if (StrUtil.isBlank(code)) {
|
||||
return fail("验证码不能为空", null);
|
||||
}
|
||||
|
||||
String smsCode = redisUtil.get("code:" + phone);
|
||||
String devSmsCode = redisUtil.get(CACHE_KEY_VERIFICATION_CODE_BY_DEV_SMS);
|
||||
if (!StrUtil.equals(code, smsCode) && !StrUtil.equals(code, devSmsCode)) {
|
||||
String message = "验证码不正确";
|
||||
loginRecordService.saveAsync(phone, LoginRecord.TYPE_ERROR, message, null, request);
|
||||
return fail(message, null);
|
||||
}
|
||||
|
||||
User user = userService.getLastLoginDeveloperByPhone(phone);
|
||||
if (user == null) {
|
||||
String message = "用户不存在";
|
||||
loginRecordService.saveAsync(phone, LoginRecord.TYPE_ERROR, message, null, request);
|
||||
return fail(message, null);
|
||||
}
|
||||
if (!Boolean.TRUE.equals(user.getIsDeveloper())) {
|
||||
String message = "非开发者账号不允许登录";
|
||||
loginRecordService.saveAsync(phone, LoginRecord.TYPE_ERROR, message, user.getTenantId(), request);
|
||||
return fail(message, null);
|
||||
}
|
||||
if (!user.getStatus().equals(0)) {
|
||||
String message = "账号被冻结";
|
||||
loginRecordService.saveAsync(user.getUsername(), LoginRecord.TYPE_ERROR, message, user.getTenantId(), request);
|
||||
return fail(message, null);
|
||||
}
|
||||
|
||||
Long tokenExpireTime = configProperties.getTokenExpireTime();
|
||||
final JSONObject register = cacheClient.getSettingInfo("register", user.getTenantId());
|
||||
if (register != null) {
|
||||
final String ExpireTime = register.getString("tokenExpireTime");
|
||||
if (ExpireTime != null) {
|
||||
tokenExpireTime = Long.valueOf(ExpireTime);
|
||||
}
|
||||
}
|
||||
|
||||
loginRecordService.saveAsync(user.getUsername(), LoginRecord.TYPE_LOGIN, null, user.getTenantId(), request);
|
||||
String access_token = JwtUtil.buildToken(new JwtSubject(user.getUsername(), user.getTenantId()),
|
||||
tokenExpireTime, configProperties.getTokenKey());
|
||||
redisUtil.set("access_token:" + user.getUserId(), access_token, tokenExpireTime, TimeUnit.SECONDS);
|
||||
return success("登录成功", new LoginResult(access_token, user));
|
||||
}
|
||||
|
||||
@Transactional(rollbackFor = {Exception.class}, isolation = Isolation.SERIALIZABLE)
|
||||
@Operation(summary = "账号注册")
|
||||
@PostMapping("/register")
|
||||
|
||||
@@ -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;
|
||||
@@ -196,6 +199,7 @@ public class WxLoginController extends BaseController {
|
||||
String access_token = JwtUtil.buildToken(new JwtSubject(user.getUsername(), user.getTenantId()),
|
||||
configProperties.getTokenExpireTime(), configProperties.getTokenKey());
|
||||
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));
|
||||
}
|
||||
@@ -252,7 +256,7 @@ public class WxLoginController extends BaseController {
|
||||
String access_token = JwtUtil.buildToken(new JwtSubject(user.getUsername(), user.getTenantId()),
|
||||
configProperties.getTokenExpireTime(), configProperties.getTokenKey());
|
||||
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));
|
||||
|
||||
} catch (BusinessException e) {
|
||||
@@ -264,6 +268,7 @@ public class WxLoginController extends BaseController {
|
||||
String access_token = JwtUtil.buildToken(new JwtSubject(user.getUsername(), user.getTenantId()),
|
||||
configProperties.getTokenExpireTime(), configProperties.getTokenKey());
|
||||
loginRecordService.saveAsync(user.getUsername(), LoginRecord.TYPE_LOGIN, null, user.getTenantId(), request);
|
||||
activateShopUserMemberByPhone(phone);
|
||||
return success("登录成功", new LoginResult(access_token, user));
|
||||
}
|
||||
}
|
||||
@@ -325,6 +330,7 @@ public class WxLoginController extends BaseController {
|
||||
String access_token = JwtUtil.buildToken(new JwtSubject(user.getUsername(), user.getTenantId()),
|
||||
configProperties.getTokenExpireTime(), configProperties.getTokenKey());
|
||||
loginRecordService.saveAsync(user.getUsername(), LoginRecord.TYPE_LOGIN, null, user.getTenantId(), request);
|
||||
activateShopUserMemberByPhone(phone);
|
||||
|
||||
return success("登录成功", new LoginResult(access_token, user));
|
||||
}
|
||||
@@ -398,6 +404,7 @@ public class WxLoginController extends BaseController {
|
||||
String access_token = JwtUtil.buildToken(new JwtSubject(user.getUsername(), user.getTenantId()),
|
||||
configProperties.getTokenExpireTime(), configProperties.getTokenKey());
|
||||
loginRecordService.saveAsync(user.getUsername(), LoginRecord.TYPE_REGISTER, null, user.getTenantId(), request);
|
||||
activateShopUserMemberByPhone(phone);
|
||||
|
||||
return success("注册并登录成功", new LoginResult(access_token, user));
|
||||
|
||||
@@ -410,6 +417,7 @@ public class WxLoginController extends BaseController {
|
||||
String access_token = JwtUtil.buildToken(new JwtSubject(user.getUsername(), user.getTenantId()),
|
||||
configProperties.getTokenExpireTime(), configProperties.getTokenKey());
|
||||
loginRecordService.saveAsync(user.getUsername(), LoginRecord.TYPE_LOGIN, null, user.getTenantId(), request);
|
||||
activateShopUserMemberByPhone(phone);
|
||||
return success("登录成功", new LoginResult(access_token, user));
|
||||
}
|
||||
}
|
||||
@@ -417,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 = "微信授权手机号码并更新")
|
||||
@Transactional(rollbackFor = {Exception.class})
|
||||
@PostMapping("/updatePhoneByMpWx")
|
||||
@@ -433,6 +461,36 @@ public class WxLoginController extends BaseController {
|
||||
return fail("更新失败");
|
||||
}
|
||||
|
||||
/**
|
||||
* 优先读取 sys_setting (mp-weixin),不存在或异常时回退到 db_websopy.app_config(category=wechat)
|
||||
*
|
||||
* 使用 bound_tenant_id 字段精确匹配当前请求租户绑定的小程序配置
|
||||
*
|
||||
* @param tenantId 租户ID(传 null 时使用当前请求租户)
|
||||
* @return JSONObject 配置内容(含 appId / appSecret 等字段)
|
||||
*/
|
||||
private JSONObject getMpWxSetting(Integer tenantId) {
|
||||
Integer tid = tenantId != null ? tenantId : getTenantId();
|
||||
System.out.println("[WxLoginController] getMpWxSetting 开始, 传入tenantId=" + tenantId + ", 实际使用tid=" + tid);
|
||||
try {
|
||||
// 优先:sys_setting.mp-weixin(按当前租户查询)
|
||||
JSONObject setting = settingService.getBySettingKey("mp-weixin");
|
||||
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) {
|
||||
System.err.println("[WxLoginController] 读取 sys_setting 失败,回退 app_config: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
}
|
||||
// 兜底:按 bound_tenant_id 精确查询 db_websopy.app_config
|
||||
System.out.println("[WxLoginController] 调用 appConfigService.getByCategory(\"wechat\", " + tid + ")");
|
||||
JSONObject result = appConfigService.getByCategory("wechat", tid);
|
||||
System.out.println("[WxLoginController] app_config 返回结果: " + result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 新用户注册
|
||||
*/
|
||||
@@ -494,8 +552,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 +747,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 +866,7 @@ public class WxLoginController extends BaseController {
|
||||
// 请求微信接口获取openid
|
||||
String apiUrl = "https://api.weixin.qq.com/sns/jscode2session";
|
||||
final HashMap<String, Object> 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 +893,7 @@ public class WxLoginController extends BaseController {
|
||||
|
||||
String apiUrl = "https://api.weixin.qq.com/sns/jscode2session";
|
||||
final HashMap<String, Object> 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 +984,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
|
||||
|
||||
@@ -76,6 +76,12 @@ public class Payment implements Serializable {
|
||||
@Schema(description = "备注")
|
||||
private String comments;
|
||||
|
||||
@Schema(description = "子应用APPID")
|
||||
private String subAppId;
|
||||
|
||||
@Schema(description = "子商户ID")
|
||||
private String subMchId;
|
||||
|
||||
@Schema(description = "文章排序(数字越小越靠前)")
|
||||
private Integer sortNumber;
|
||||
|
||||
|
||||
@@ -199,6 +199,9 @@ public class User implements UserDetails {
|
||||
@Schema(description = "是否超级管理员")
|
||||
private Boolean isSuperAdmin;
|
||||
|
||||
@Schema(description = "是否开发者")
|
||||
private Boolean isDeveloper;
|
||||
|
||||
@Schema(description = "租户管理员ID")
|
||||
@TableField(exist = false)
|
||||
private Integer adminId;
|
||||
|
||||
@@ -90,4 +90,22 @@ public interface UserMapper extends BaseMapper<User> {
|
||||
@InterceptorIgnore(tenantLine = "true")
|
||||
Integer countByPhone(@Param("phone") String phone);
|
||||
|
||||
/**
|
||||
* 根据手机号查询最近登录的超级管理员账号(忽略租户隔离)
|
||||
*
|
||||
* @param phone 手机号
|
||||
* @return User
|
||||
*/
|
||||
@InterceptorIgnore(tenantLine = "true")
|
||||
User selectLastLoginSuperAdminByPhone(@Param("phone") String phone);
|
||||
|
||||
/**
|
||||
* 根据手机号查询最近登录的开发者账号(忽略租户隔离)
|
||||
*
|
||||
* @param phone 手机号
|
||||
* @return User
|
||||
*/
|
||||
@InterceptorIgnore(tenantLine = "true")
|
||||
User selectLastLoginDeveloperByPhone(@Param("phone") String phone);
|
||||
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<!-- 关联查询sql -->
|
||||
<sql id="selectSql">
|
||||
SELECT a.*,
|
||||
b.nickname,b.avatar,b.phone
|
||||
b.nickname, b.avatar, b.phone
|
||||
FROM sys_user_balance_log a
|
||||
LEFT JOIN sys_user b ON a.user_id = b.user_id
|
||||
<where>
|
||||
@@ -18,7 +18,7 @@
|
||||
<if test="param.scene != null">
|
||||
AND a.scene = #{param.scene}
|
||||
</if>
|
||||
<if test="param.sceneMultiple != null">
|
||||
<if test="param.sceneMultiple != null and param.sceneMultiple != ''">
|
||||
AND a.scene IN
|
||||
<foreach collection="param.sceneMultiple.split(',')" item="item" separator="," open="(" close=")">
|
||||
#{item}
|
||||
@@ -30,16 +30,16 @@
|
||||
<if test="param.balance != null">
|
||||
AND a.balance = #{param.balance}
|
||||
</if>
|
||||
<if test="param.describe != null">
|
||||
<if test="param.describe != null and param.describe != ''">
|
||||
AND a.describe LIKE CONCAT('%', #{param.describe}, '%')
|
||||
</if>
|
||||
<if test="param.remark != null">
|
||||
<if test="param.remark != null and param.remark != ''">
|
||||
AND a.remark LIKE CONCAT('%', #{param.remark}, '%')
|
||||
</if>
|
||||
<if test="param.sortNumber != null">
|
||||
AND a.sort_number = #{param.sortNumber}
|
||||
</if>
|
||||
<if test="param.comments != null">
|
||||
<if test="param.comments != null and param.comments != ''">
|
||||
AND a.comments LIKE CONCAT('%', #{param.comments}, '%')
|
||||
</if>
|
||||
<if test="param.status != null">
|
||||
@@ -51,7 +51,7 @@
|
||||
<if test="param.deleted == null">
|
||||
AND a.deleted = 0
|
||||
</if>
|
||||
<if test="param.merchantCode != null">
|
||||
<if test="param.merchantCode != null and param.merchantCode != ''">
|
||||
AND a.merchant_code LIKE CONCAT('%', #{param.merchantCode}, '%')
|
||||
</if>
|
||||
<if test="param.createTimeStart != null">
|
||||
@@ -60,7 +60,7 @@
|
||||
<if test="param.createTimeEnd != null">
|
||||
AND a.create_time <= #{param.createTimeEnd}
|
||||
</if>
|
||||
<if test="param.keywords != null">
|
||||
<if test="param.keywords != null and param.keywords != ''">
|
||||
AND (
|
||||
a.user_id = #{param.keywords}
|
||||
OR b.nickname LIKE CONCAT('%', #{param.keywords}, '%')
|
||||
|
||||
@@ -125,6 +125,9 @@
|
||||
<if test="param.isAdmin != null">
|
||||
AND a.is_admin = #{param.isAdmin}
|
||||
</if>
|
||||
<if test="param.isDeveloper != null">
|
||||
AND a.is_developer = #{param.isDeveloper}
|
||||
</if>
|
||||
<if test="param.templateId != null">
|
||||
AND a.template_id = #{param.templateId}
|
||||
</if>
|
||||
@@ -379,4 +382,52 @@
|
||||
AND phone = #{phone}
|
||||
</select>
|
||||
|
||||
<!-- 根据手机号查询最近登录的超级管理员账号(忽略租户隔离) -->
|
||||
<select id="selectLastLoginSuperAdminByPhone" resultType="com.gxwebsoft.common.system.entity.User">
|
||||
SELECT u.*
|
||||
FROM sys_user u
|
||||
LEFT JOIN (
|
||||
SELECT tenant_id,
|
||||
username,
|
||||
MAX(create_time) AS last_login_time
|
||||
FROM sys_login_record
|
||||
WHERE login_type = 0
|
||||
GROUP BY tenant_id, username
|
||||
) lr ON lr.tenant_id = u.tenant_id
|
||||
AND (lr.username = u.username OR lr.username = u.phone)
|
||||
WHERE u.deleted = 0
|
||||
AND u.status = 0
|
||||
AND u.phone = #{phone}
|
||||
AND u.is_super_admin = 1
|
||||
ORDER BY lr.last_login_time DESC,
|
||||
u.update_time DESC,
|
||||
u.create_time DESC,
|
||||
u.user_id DESC
|
||||
LIMIT 1
|
||||
</select>
|
||||
|
||||
<!-- 根据手机号查询最近登录的开发者账号(忽略租户隔离) -->
|
||||
<select id="selectLastLoginDeveloperByPhone" resultType="com.gxwebsoft.common.system.entity.User">
|
||||
SELECT u.*
|
||||
FROM sys_user u
|
||||
LEFT JOIN (
|
||||
SELECT tenant_id,
|
||||
username,
|
||||
MAX(create_time) AS last_login_time
|
||||
FROM sys_login_record
|
||||
WHERE login_type = 0
|
||||
GROUP BY tenant_id, username
|
||||
) lr ON lr.tenant_id = u.tenant_id
|
||||
AND (lr.username = u.username OR lr.username = u.phone)
|
||||
WHERE u.deleted = 0
|
||||
AND u.status = 0
|
||||
AND u.phone = #{phone}
|
||||
AND u.is_developer = 1
|
||||
ORDER BY lr.last_login_time DESC,
|
||||
u.update_time DESC,
|
||||
u.create_time DESC,
|
||||
u.user_id DESC
|
||||
LIMIT 1
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
||||
@@ -225,6 +225,10 @@ public class UserParam extends BaseParam {
|
||||
@TableField(exist = false)
|
||||
private Boolean isAdmin;
|
||||
|
||||
@Schema(description = "是否开发者")
|
||||
@TableField(exist = false)
|
||||
private Boolean isDeveloper;
|
||||
|
||||
@Schema(description = "是否企业管理员")
|
||||
private Integer isOrganizationAdmin;
|
||||
|
||||
|
||||
@@ -126,6 +126,22 @@ public interface UserService extends IService<User>, UserDetailsService {
|
||||
|
||||
List<User> getAdminsByPhone(LoginParam param);
|
||||
|
||||
/**
|
||||
* 根据手机号查询最近登录的超级管理员账号(忽略租户隔离)
|
||||
*
|
||||
* @param phone 手机号
|
||||
* @return 用户信息
|
||||
*/
|
||||
User getLastLoginSuperAdminByPhone(String phone);
|
||||
|
||||
/**
|
||||
* 根据手机号查询最近登录的开发者账号(忽略租户隔离)
|
||||
*
|
||||
* @param phone 手机号
|
||||
* @return 用户信息
|
||||
*/
|
||||
User getLastLoginDeveloperByPhone(String phone);
|
||||
|
||||
List<User> pageAll(UserParam param);
|
||||
|
||||
User getByUserId(String userId);
|
||||
|
||||
@@ -370,6 +370,16 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
|
||||
return baseMapper.selectListAllRel(userParam);
|
||||
}
|
||||
|
||||
@Override
|
||||
public User getLastLoginSuperAdminByPhone(String phone) {
|
||||
return baseMapper.selectLastLoginSuperAdminByPhone(phone);
|
||||
}
|
||||
|
||||
@Override
|
||||
public User getLastLoginDeveloperByPhone(String phone) {
|
||||
return baseMapper.selectLastLoginDeveloperByPhone(phone);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<User> pageAll(UserParam param) {
|
||||
return baseMapper.pageRelAll(param);
|
||||
|
||||
55
src/main/java/com/gxwebsoft/websopy/entity/AppConfig.java
Normal file
55
src/main/java/com/gxwebsoft/websopy/entity/AppConfig.java
Normal file
@@ -0,0 +1,55 @@
|
||||
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;
|
||||
|
||||
/** 应用绑定的租户ID(用于精确匹配当前请求租户的配置) */
|
||||
@TableField("bound_tenant_id")
|
||||
private Integer boundTenantId;
|
||||
|
||||
@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;
|
||||
}
|
||||
@@ -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<AppConfig> {
|
||||
|
||||
/**
|
||||
* 跨表查询 db_websopy.app_config(按 config_type 分组)
|
||||
*
|
||||
* @param tenantId 租户ID(app_config 表中的 tenant_id)
|
||||
* @param configType 配置分类(如 wechat)
|
||||
* @return 配置项列表
|
||||
*/
|
||||
@InterceptorIgnore(tenantLine = "true")
|
||||
List<AppConfig> selectByCategory(@Param("tenantId") Integer tenantId,
|
||||
@Param("configType") String configType);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.gxwebsoft.websopy.mapper.AppConfigMapper">
|
||||
|
||||
<!--
|
||||
查询 db_websopy.app_config
|
||||
注意:
|
||||
1. 使用 bound_tenant_id 字段精确匹配当前请求租户的绑定配置
|
||||
2. 表名带库名前缀 db_websopy.app_config(该表在 db_websopy 库中)
|
||||
3. Mapper 方法已加 @InterceptorIgnore(tenantLine = "true"),
|
||||
TenantLineInnerInterceptor 不会自动追加 tenant_id 条件
|
||||
-->
|
||||
<select id="selectByCategory" resultType="com.gxwebsoft.websopy.entity.AppConfig">
|
||||
SELECT ac.config_id AS configId,
|
||||
ac.app_id AS appId,
|
||||
ac.tenant_id AS tenantId,
|
||||
ac.bound_tenant_id AS boundTenantId,
|
||||
ac.config_key AS configKey,
|
||||
ac.config_value AS configValue,
|
||||
ac.config_type AS configType,
|
||||
ac.is_encrypted AS isEncrypted,
|
||||
ac.is_secret AS isSecret,
|
||||
ac.description
|
||||
FROM db_websopy.app_config ac
|
||||
WHERE ac.deleted = 0
|
||||
AND ac.bound_tenant_id = #{tenantId}
|
||||
AND ac.config_type = #{configType}
|
||||
ORDER BY ac.config_id DESC
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,136 @@
|
||||
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<Object, Object>
|
||||
@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()) {
|
||||
log.info("[AppConfigService] 命中 Redis 缓存 configType={}, tenantId={}, cacheKey={}, 缓存内容={}",
|
||||
configType, tenantId, cacheKey, cached);
|
||||
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<AppConfig> list;
|
||||
try {
|
||||
log.info("[AppConfigService] 开始查询 app_config, configType={}, tenantId={}", configType, tenantId);
|
||||
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) {
|
||||
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." 前缀)
|
||||
// 注意:按 config_id 降序后,相同 configKey 的后出现的会覆盖先出现的
|
||||
// 所以降序后,最新插入/更新的配置会生效
|
||||
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());
|
||||
}
|
||||
// 使用 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 小时
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,8 @@ logging:
|
||||
level:
|
||||
com.gxwebsoft: DEBUG
|
||||
com.baomidou.mybatisplus: DEBUG
|
||||
com.gxwebsoft.websopy.mapper: DEBUG
|
||||
com.gxwebsoft.websopy.service: DEBUG
|
||||
|
||||
socketio:
|
||||
host: localhost #IP地址
|
||||
|
||||
Reference in New Issue
Block a user