You've already forked FrameTour-BE
feat(notify): 添加批量查询用户授权余额功能
- 新增批量查询用户授权余额接口 /api/mobile/notify/auth/batch-remaining - 实现批量检查用户对多个模板的授权记录功能 - 添加景区所有场景及模板列表查询接口并支持缓存 - 优化授权记录查询性能,使用批量查询替代逐个查询 - 新增批量查询请求对象 BatchRemainingCountReq 和响应对象 WechatSubscribeAllScenesResp - 在数据层添加批量查询用户授权记录的 SQL 映射 - 实现缓存管理机制,支持所有场景模板配置的缓存读写与清理
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
package com.ycwl.basic.controller.mobile.notify;
|
||||
|
||||
import com.ycwl.basic.model.mobile.notify.req.BatchRemainingCountReq;
|
||||
import com.ycwl.basic.model.mobile.notify.req.NotificationAuthRecordReq;
|
||||
import com.ycwl.basic.model.mobile.notify.resp.NotificationAuthRecordResp;
|
||||
import com.ycwl.basic.model.mobile.notify.resp.ScenicTemplateAuthResp;
|
||||
import com.ycwl.basic.model.pc.notify.entity.UserNotificationAuthorizationEntity;
|
||||
import com.ycwl.basic.service.UserNotificationAuthorizationService;
|
||||
import com.ycwl.basic.utils.ApiResponse;
|
||||
import com.ycwl.basic.utils.JwtTokenUtil;
|
||||
@@ -14,7 +16,9 @@ import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 用户通知授权记录Controller (移动端API)
|
||||
@@ -92,4 +96,44 @@ public class UserNotificationAuthController {
|
||||
return ApiResponse.fail("记录授权失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量查询用户授权余额
|
||||
* 返回 Map<wechatTemplateId, remainingCount>
|
||||
*/
|
||||
@PostMapping("/batch-remaining")
|
||||
public ApiResponse<Map<String, Integer>> batchGetRemainingCount(
|
||||
@RequestBody BatchRemainingCountReq req) {
|
||||
log.debug("批量查询用户授权余额: templateIds={}, scenicId={}",
|
||||
req.getTemplateIds(), req.getScenicId());
|
||||
|
||||
try {
|
||||
Long memberId = JwtTokenUtil.getWorker().getUserId();
|
||||
if (memberId == null) {
|
||||
return ApiResponse.fail("用户未登录");
|
||||
}
|
||||
|
||||
if (CollectionUtils.isEmpty(req.getTemplateIds())) {
|
||||
return ApiResponse.success(new HashMap<>());
|
||||
}
|
||||
|
||||
Map<String, UserNotificationAuthorizationEntity> authMap =
|
||||
userNotificationAuthorizationService.batchCheckAuthorization(
|
||||
memberId, req.getTemplateIds(), req.getScenicId());
|
||||
|
||||
// 转换为 templateId -> remainingCount
|
||||
Map<String, Integer> result = new HashMap<>();
|
||||
for (String templateId : req.getTemplateIds()) {
|
||||
UserNotificationAuthorizationEntity entity = authMap.get(templateId);
|
||||
int remaining = (entity != null && entity.getRemainingCount() != null)
|
||||
? entity.getRemainingCount() : 0;
|
||||
result.put(templateId, remaining);
|
||||
}
|
||||
|
||||
return ApiResponse.success(result);
|
||||
} catch (Exception e) {
|
||||
log.error("批量查询用户授权余额失败", e);
|
||||
return ApiResponse.fail("查询失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.ycwl.basic.controller.mobile.notify;
|
||||
|
||||
import com.ycwl.basic.annotation.IgnoreToken;
|
||||
import com.ycwl.basic.model.mobile.notify.resp.WechatSubscribeAllScenesResp;
|
||||
import com.ycwl.basic.model.mobile.notify.resp.WechatSubscribeSceneTemplatesResp;
|
||||
import com.ycwl.basic.model.pc.notify.entity.WechatSubscribeTemplateConfigEntity;
|
||||
import com.ycwl.basic.service.notify.WechatSubscribeNotifyConfigService;
|
||||
@@ -87,5 +88,22 @@ public class WechatSubscribeNotifyController {
|
||||
scenicId, sceneKey, memberId, Objects.requireNonNullElse(templates.size(), 0));
|
||||
return ApiResponse.success(resp);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取景区下所有场景及其模板列表(静态配置,带缓存)
|
||||
* 不含用户授权信息,用户授权信息通过 /api/mobile/notify/auth/batch-remaining 接口获取
|
||||
*/
|
||||
@GetMapping("/scenic/{scenicId}/scenes")
|
||||
@IgnoreToken
|
||||
public ApiResponse<WechatSubscribeAllScenesResp> listAllSceneTemplates(@PathVariable("scenicId") Long scenicId) {
|
||||
if (scenicId == null) {
|
||||
return ApiResponse.fail("scenicId不能为空");
|
||||
}
|
||||
|
||||
WechatSubscribeAllScenesResp resp = configService.getAllScenesWithTemplatesCached(scenicId);
|
||||
log.debug("所有场景模板查询: scenicId={}, sceneCount={}",
|
||||
scenicId, resp.getScenes() != null ? resp.getScenes().size() : 0);
|
||||
return ApiResponse.success(resp);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ public interface UserNotificationAuthorizationMapper extends BaseMapper<UserNoti
|
||||
|
||||
/**
|
||||
* 检查用户是否还有剩余授权次数
|
||||
*
|
||||
*
|
||||
* @param memberId 用户ID
|
||||
* @param templateId 模板ID
|
||||
* @param scenicId 景区ID
|
||||
@@ -89,4 +89,18 @@ public interface UserNotificationAuthorizationMapper extends BaseMapper<UserNoti
|
||||
@Param("templateId") String templateId,
|
||||
@Param("scenicId") Long scenicId
|
||||
);
|
||||
|
||||
/**
|
||||
* 批量查询用户对多个模板的授权记录
|
||||
*
|
||||
* @param memberId 用户ID
|
||||
* @param templateIds 模板ID列表
|
||||
* @param scenicId 景区ID
|
||||
* @return 授权记录列表
|
||||
*/
|
||||
List<UserNotificationAuthorizationEntity> selectBatchByTemplateIds(
|
||||
@Param("memberId") Long memberId,
|
||||
@Param("templateIds") List<String> templateIds,
|
||||
@Param("scenicId") Long scenicId
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.ycwl.basic.model.mobile.notify.req;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 批量查询用户授权余额请求
|
||||
*
|
||||
* @Author: System
|
||||
* @Date: 2026/01/10
|
||||
*/
|
||||
@Data
|
||||
public class BatchRemainingCountReq {
|
||||
|
||||
/**
|
||||
* 通知模板ID列表(微信 wechatTemplateId)
|
||||
*/
|
||||
@NotEmpty(message = "模板ID列表不能为空")
|
||||
private List<String> templateIds;
|
||||
|
||||
/**
|
||||
* 景区ID
|
||||
*/
|
||||
@NotNull(message = "景区ID不能为空")
|
||||
private Long scenicId;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.ycwl.basic.model.mobile.notify.resp;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 景区所有场景及其订阅消息模板列表(静态配置,不含用户授权信息)
|
||||
* 用户授权信息通过 /api/mobile/notify/auth/batch-remaining 接口获取
|
||||
*
|
||||
* @Author: System
|
||||
* @Date: 2026/01/10
|
||||
*/
|
||||
@Data
|
||||
public class WechatSubscribeAllScenesResp {
|
||||
|
||||
private Long scenicId;
|
||||
|
||||
private List<SceneWithTemplates> scenes;
|
||||
|
||||
@Data
|
||||
public static class SceneWithTemplates {
|
||||
/**
|
||||
* 场景标识
|
||||
*/
|
||||
private String sceneKey;
|
||||
|
||||
/**
|
||||
* 该场景下的模板列表
|
||||
*/
|
||||
private List<StaticTemplateInfo> templates;
|
||||
}
|
||||
|
||||
/**
|
||||
* 静态模板信息(不含用户授权信息,可缓存)
|
||||
*/
|
||||
@Data
|
||||
public static class StaticTemplateInfo {
|
||||
/**
|
||||
* 逻辑模板键(业务固定)
|
||||
*/
|
||||
private String templateKey;
|
||||
|
||||
/**
|
||||
* 微信订阅消息模板ID(tmplId)
|
||||
*/
|
||||
private String wechatTemplateId;
|
||||
|
||||
/**
|
||||
* 前端展示标题
|
||||
*/
|
||||
private String title;
|
||||
|
||||
/**
|
||||
* 前端展示描述
|
||||
*/
|
||||
private String description;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.ycwl.basic.mapper.WechatSubscribeEventTemplateMapper;
|
||||
import com.ycwl.basic.mapper.WechatSubscribeSceneTemplateMapper;
|
||||
import com.ycwl.basic.mapper.WechatSubscribeTemplateConfigMapper;
|
||||
import com.ycwl.basic.model.mobile.notify.resp.WechatSubscribeAllScenesResp;
|
||||
import com.ycwl.basic.model.pc.notify.entity.WechatSubscribeEventTemplateEntity;
|
||||
import com.ycwl.basic.model.pc.notify.entity.WechatSubscribeSceneTemplateEntity;
|
||||
import com.ycwl.basic.model.pc.notify.entity.WechatSubscribeTemplateConfigEntity;
|
||||
@@ -58,6 +59,11 @@ public class WechatSubscribeNotifyConfigRepository {
|
||||
*/
|
||||
private static final String EVENT_TEMPLATE_SCENIC_PREFIX = "wechat:subscribe:event:configs:%s:*";
|
||||
|
||||
/**
|
||||
* 景区所有场景及模板缓存KEY
|
||||
*/
|
||||
private static final String ALL_SCENES_TEMPLATES_KEY = "wechat:subscribe:all-scenes:configs:%s";
|
||||
|
||||
/**
|
||||
* 缓存过期时间(小时)
|
||||
*/
|
||||
@@ -208,6 +214,98 @@ public class WechatSubscribeNotifyConfigRepository {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取景区下所有启用的场景Key列表(去重)
|
||||
* 包含默认配置(scenicId=0)和景区特定配置
|
||||
*
|
||||
* @param scenicId 景区ID
|
||||
* @return 去重后的场景Key列表
|
||||
*/
|
||||
public List<String> listAllSceneKeys(Long scenicId) {
|
||||
Objects.requireNonNull(scenicId, "scenicId is null");
|
||||
|
||||
List<Long> scenicIds = List.of(DEFAULT_SCENIC_ID, scenicId);
|
||||
QueryWrapper<WechatSubscribeSceneTemplateEntity> wrapper = new QueryWrapper<>();
|
||||
wrapper.in("scenic_id", scenicIds)
|
||||
.eq("enabled", 1)
|
||||
.select("DISTINCT scene_key");
|
||||
List<WechatSubscribeSceneTemplateEntity> rows = sceneTemplateMapper.selectList(wrapper);
|
||||
return rows.stream()
|
||||
.map(WechatSubscribeSceneTemplateEntity::getSceneKey)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.sorted()
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取景区下所有场景及其模板列表(带缓存,不含用户授权信息)
|
||||
*
|
||||
* @param scenicId 景区ID
|
||||
* @return 所有场景及模板配置
|
||||
*/
|
||||
public WechatSubscribeAllScenesResp getAllScenesWithTemplatesCached(Long scenicId) {
|
||||
Objects.requireNonNull(scenicId, "scenicId is null");
|
||||
|
||||
String cacheKey = String.format(ALL_SCENES_TEMPLATES_KEY, scenicId);
|
||||
|
||||
// 1. 尝试从缓存读取
|
||||
Boolean hasKey = redisTemplate.hasKey(cacheKey);
|
||||
if (Boolean.TRUE.equals(hasKey)) {
|
||||
String cacheValue = redisTemplate.opsForValue().get(cacheKey);
|
||||
if (cacheValue != null) {
|
||||
log.debug("从缓存读取所有场景模板配置: scenicId={}", scenicId);
|
||||
return JacksonUtil.parseObject(cacheValue, WechatSubscribeAllScenesResp.class);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 从数据库加载
|
||||
WechatSubscribeAllScenesResp resp = loadAllScenesWithTemplates(scenicId);
|
||||
|
||||
// 3. 写入缓存
|
||||
String json = JacksonUtil.toJSONString(resp);
|
||||
redisTemplate.opsForValue().set(cacheKey, json, CACHE_EXPIRE_HOURS, TimeUnit.HOURS);
|
||||
log.debug("所有场景模板配置缓存写入: scenicId={}, sceneCount={}",
|
||||
scenicId, resp.getScenes() != null ? resp.getScenes().size() : 0);
|
||||
|
||||
return resp;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载所有场景及其模板配置(内部方法)
|
||||
*/
|
||||
private WechatSubscribeAllScenesResp loadAllScenesWithTemplates(Long scenicId) {
|
||||
List<String> sceneKeys = listAllSceneKeys(scenicId);
|
||||
|
||||
WechatSubscribeAllScenesResp resp = new WechatSubscribeAllScenesResp();
|
||||
resp.setScenicId(scenicId);
|
||||
|
||||
List<WechatSubscribeAllScenesResp.SceneWithTemplates> scenes = new ArrayList<>();
|
||||
for (String sceneKey : sceneKeys) {
|
||||
List<WechatSubscribeTemplateConfigEntity> configs = getSceneTemplateConfigsCached(scenicId, sceneKey);
|
||||
|
||||
WechatSubscribeAllScenesResp.SceneWithTemplates sceneWithTemplates = new WechatSubscribeAllScenesResp.SceneWithTemplates();
|
||||
sceneWithTemplates.setSceneKey(sceneKey);
|
||||
|
||||
List<WechatSubscribeAllScenesResp.StaticTemplateInfo> templates = new ArrayList<>();
|
||||
for (WechatSubscribeTemplateConfigEntity cfg : configs) {
|
||||
if (cfg == null || StringUtils.isBlank(cfg.getWechatTemplateId())) {
|
||||
continue;
|
||||
}
|
||||
WechatSubscribeAllScenesResp.StaticTemplateInfo info = new WechatSubscribeAllScenesResp.StaticTemplateInfo();
|
||||
info.setTemplateKey(cfg.getTemplateKey());
|
||||
info.setWechatTemplateId(cfg.getWechatTemplateId());
|
||||
info.setTitle(StringUtils.isNotBlank(cfg.getTitleTemplate()) ? cfg.getTitleTemplate() : cfg.getTemplateKey());
|
||||
info.setDescription(cfg.getDescription());
|
||||
templates.add(info);
|
||||
}
|
||||
sceneWithTemplates.setTemplates(templates);
|
||||
scenes.add(sceneWithTemplates);
|
||||
}
|
||||
resp.setScenes(scenes);
|
||||
return resp;
|
||||
}
|
||||
|
||||
// ==================== 带缓存的配置查询方法 ====================
|
||||
|
||||
/**
|
||||
@@ -386,6 +484,10 @@ public class WechatSubscribeNotifyConfigRepository {
|
||||
String eventPattern = String.format(EVENT_TEMPLATE_SCENIC_PREFIX, scenicId);
|
||||
deleteByPattern(eventPattern);
|
||||
|
||||
// 清除所有场景模板缓存
|
||||
String allScenesKey = String.format(ALL_SCENES_TEMPLATES_KEY, scenicId);
|
||||
redisTemplate.delete(allScenesKey);
|
||||
|
||||
log.info("清除景区所有订阅消息配置缓存: scenicId={}", scenicId);
|
||||
}
|
||||
|
||||
@@ -396,6 +498,7 @@ public class WechatSubscribeNotifyConfigRepository {
|
||||
public void clearAllConfigsCache() {
|
||||
deleteByPattern("wechat:subscribe:scene:configs:*");
|
||||
deleteByPattern("wechat:subscribe:event:configs:*");
|
||||
deleteByPattern("wechat:subscribe:all-scenes:configs:*");
|
||||
log.warn("清除所有订阅消息配置缓存");
|
||||
}
|
||||
|
||||
|
||||
@@ -228,31 +228,30 @@ public class UserNotificationAuthorizationServiceImpl implements UserNotificatio
|
||||
public Map<String, UserNotificationAuthorizationEntity> batchCheckAuthorization(
|
||||
Long memberId, List<String> templateIds, Long scenicId) {
|
||||
log.debug("批量检查用户授权: memberId={}, templateIds={}, scenicId={}", memberId, templateIds, scenicId);
|
||||
|
||||
|
||||
Map<String, UserNotificationAuthorizationEntity> result = new HashMap<>();
|
||||
|
||||
|
||||
if (templateIds == null || templateIds.isEmpty()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// 查询用户在该景区的所有授权记录
|
||||
List<UserNotificationAuthorizationEntity> userRecords = getUserAuthorizations(memberId);
|
||||
|
||||
// 过滤出指定景区和模板的记录
|
||||
Map<String, UserNotificationAuthorizationEntity> recordMap = userRecords.stream()
|
||||
.filter(record -> scenicId.equals(record.getScenicId()))
|
||||
.filter(record -> templateIds.contains(record.getTemplateId()))
|
||||
|
||||
// 使用批量查询方法
|
||||
List<UserNotificationAuthorizationEntity> records =
|
||||
userNotificationAuthorizationMapper.selectBatchByTemplateIds(memberId, templateIds, scenicId);
|
||||
|
||||
// 转换为Map
|
||||
Map<String, UserNotificationAuthorizationEntity> recordMap = records.stream()
|
||||
.collect(Collectors.toMap(
|
||||
UserNotificationAuthorizationEntity::getTemplateId,
|
||||
record -> record,
|
||||
(existing, replacement) -> existing
|
||||
));
|
||||
|
||||
|
||||
// 为每个模板ID填充结果
|
||||
for (String templateId : templateIds) {
|
||||
result.put(templateId, recordMap.get(templateId));
|
||||
}
|
||||
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.ycwl.basic.service.notify;
|
||||
|
||||
import com.ycwl.basic.model.mobile.notify.resp.WechatSubscribeAllScenesResp;
|
||||
import com.ycwl.basic.model.pc.notify.entity.WechatSubscribeTemplateConfigEntity;
|
||||
import com.ycwl.basic.repository.WechatSubscribeNotifyConfigRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -33,6 +34,26 @@ public class WechatSubscribeNotifyConfigService {
|
||||
return configRepository.getSceneTemplateConfigsCached(scenicId, sceneKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取景区下所有场景Key列表
|
||||
*
|
||||
* @param scenicId 景区ID
|
||||
* @return 场景Key列表
|
||||
*/
|
||||
public List<String> listAllSceneKeys(Long scenicId) {
|
||||
return configRepository.listAllSceneKeys(scenicId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取景区下所有场景及其模板列表(带缓存,不含用户授权信息)
|
||||
*
|
||||
* @param scenicId 景区ID
|
||||
* @return 所有场景及模板配置
|
||||
*/
|
||||
public WechatSubscribeAllScenesResp getAllScenesWithTemplatesCached(Long scenicId) {
|
||||
return configRepository.getAllScenesWithTemplatesCached(scenicId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取事件下的模板配置列表(带缓存)
|
||||
*
|
||||
|
||||
@@ -92,12 +92,26 @@
|
||||
<select id="selectRemainingCount" resultType="java.lang.Integer">
|
||||
SELECT COALESCE(remaining_count, 0)
|
||||
FROM user_notification_authorization
|
||||
WHERE member_id = #{memberId}
|
||||
WHERE member_id = #{memberId}
|
||||
AND template_id = #{templateId}
|
||||
AND scenic_id = #{scenicId}
|
||||
AND status = 1
|
||||
AND remaining_count > 0
|
||||
LIMIT 1
|
||||
</select>
|
||||
|
||||
|
||||
<!-- 批量查询用户对多个模板的授权记录 -->
|
||||
<select id="selectBatchByTemplateIds" resultMap="BaseResultMap">
|
||||
SELECT
|
||||
<include refid="Base_Column_List"/>
|
||||
FROM user_notification_authorization
|
||||
WHERE member_id = #{memberId}
|
||||
AND scenic_id = #{scenicId}
|
||||
AND status = 1
|
||||
AND template_id IN
|
||||
<foreach collection="templateIds" item="templateId" open="(" separator="," close=")">
|
||||
#{templateId}
|
||||
</foreach>
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
Reference in New Issue
Block a user