From c9cc90c842eba67a575c836b91353a10612a5373 Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Sat, 10 Jan 2026 17:30:48 +0800 Subject: [PATCH] =?UTF-8?q?feat(notify):=20=E6=B7=BB=E5=8A=A0=E6=89=B9?= =?UTF-8?q?=E9=87=8F=E6=9F=A5=E8=AF=A2=E7=94=A8=E6=88=B7=E6=8E=88=E6=9D=83?= =?UTF-8?q?=E4=BD=99=E9=A2=9D=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增批量查询用户授权余额接口 /api/mobile/notify/auth/batch-remaining - 实现批量检查用户对多个模板的授权记录功能 - 添加景区所有场景及模板列表查询接口并支持缓存 - 优化授权记录查询性能,使用批量查询替代逐个查询 - 新增批量查询请求对象 BatchRemainingCountReq 和响应对象 WechatSubscribeAllScenesResp - 在数据层添加批量查询用户授权记录的 SQL 映射 - 实现缓存管理机制,支持所有场景模板配置的缓存读写与清理 --- .../UserNotificationAuthController.java | 44 ++++++++ .../WechatSubscribeNotifyController.java | 18 +++ .../UserNotificationAuthorizationMapper.java | 16 ++- .../notify/req/BatchRemainingCountReq.java | 29 +++++ .../resp/WechatSubscribeAllScenesResp.java | 59 ++++++++++ ...WechatSubscribeNotifyConfigRepository.java | 103 ++++++++++++++++++ ...rNotificationAuthorizationServiceImpl.java | 23 ++-- .../WechatSubscribeNotifyConfigService.java | 21 ++++ .../UserNotificationAuthorizationMapper.xml | 18 ++- 9 files changed, 316 insertions(+), 15 deletions(-) create mode 100644 src/main/java/com/ycwl/basic/model/mobile/notify/req/BatchRemainingCountReq.java create mode 100644 src/main/java/com/ycwl/basic/model/mobile/notify/resp/WechatSubscribeAllScenesResp.java diff --git a/src/main/java/com/ycwl/basic/controller/mobile/notify/UserNotificationAuthController.java b/src/main/java/com/ycwl/basic/controller/mobile/notify/UserNotificationAuthController.java index 009e6b43..ccda5e11 100644 --- a/src/main/java/com/ycwl/basic/controller/mobile/notify/UserNotificationAuthController.java +++ b/src/main/java/com/ycwl/basic/controller/mobile/notify/UserNotificationAuthController.java @@ -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 + */ + @PostMapping("/batch-remaining") + public ApiResponse> 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 authMap = + userNotificationAuthorizationService.batchCheckAuthorization( + memberId, req.getTemplateIds(), req.getScenicId()); + + // 转换为 templateId -> remainingCount + Map 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()); + } + } } diff --git a/src/main/java/com/ycwl/basic/controller/mobile/notify/WechatSubscribeNotifyController.java b/src/main/java/com/ycwl/basic/controller/mobile/notify/WechatSubscribeNotifyController.java index d509eafb..765497c2 100644 --- a/src/main/java/com/ycwl/basic/controller/mobile/notify/WechatSubscribeNotifyController.java +++ b/src/main/java/com/ycwl/basic/controller/mobile/notify/WechatSubscribeNotifyController.java @@ -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 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); + } } diff --git a/src/main/java/com/ycwl/basic/mapper/UserNotificationAuthorizationMapper.java b/src/main/java/com/ycwl/basic/mapper/UserNotificationAuthorizationMapper.java index 728b0205..4c21ad88 100644 --- a/src/main/java/com/ycwl/basic/mapper/UserNotificationAuthorizationMapper.java +++ b/src/main/java/com/ycwl/basic/mapper/UserNotificationAuthorizationMapper.java @@ -78,7 +78,7 @@ public interface UserNotificationAuthorizationMapper extends BaseMapper selectBatchByTemplateIds( + @Param("memberId") Long memberId, + @Param("templateIds") List templateIds, + @Param("scenicId") Long scenicId + ); } \ No newline at end of file diff --git a/src/main/java/com/ycwl/basic/model/mobile/notify/req/BatchRemainingCountReq.java b/src/main/java/com/ycwl/basic/model/mobile/notify/req/BatchRemainingCountReq.java new file mode 100644 index 00000000..00f6d1c3 --- /dev/null +++ b/src/main/java/com/ycwl/basic/model/mobile/notify/req/BatchRemainingCountReq.java @@ -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 templateIds; + + /** + * 景区ID + */ + @NotNull(message = "景区ID不能为空") + private Long scenicId; +} diff --git a/src/main/java/com/ycwl/basic/model/mobile/notify/resp/WechatSubscribeAllScenesResp.java b/src/main/java/com/ycwl/basic/model/mobile/notify/resp/WechatSubscribeAllScenesResp.java new file mode 100644 index 00000000..a19ad8dd --- /dev/null +++ b/src/main/java/com/ycwl/basic/model/mobile/notify/resp/WechatSubscribeAllScenesResp.java @@ -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 scenes; + + @Data + public static class SceneWithTemplates { + /** + * 场景标识 + */ + private String sceneKey; + + /** + * 该场景下的模板列表 + */ + private List templates; + } + + /** + * 静态模板信息(不含用户授权信息,可缓存) + */ + @Data + public static class StaticTemplateInfo { + /** + * 逻辑模板键(业务固定) + */ + private String templateKey; + + /** + * 微信订阅消息模板ID(tmplId) + */ + private String wechatTemplateId; + + /** + * 前端展示标题 + */ + private String title; + + /** + * 前端展示描述 + */ + private String description; + } +} diff --git a/src/main/java/com/ycwl/basic/repository/WechatSubscribeNotifyConfigRepository.java b/src/main/java/com/ycwl/basic/repository/WechatSubscribeNotifyConfigRepository.java index e51dde10..b768ba4f 100644 --- a/src/main/java/com/ycwl/basic/repository/WechatSubscribeNotifyConfigRepository.java +++ b/src/main/java/com/ycwl/basic/repository/WechatSubscribeNotifyConfigRepository.java @@ -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 listAllSceneKeys(Long scenicId) { + Objects.requireNonNull(scenicId, "scenicId is null"); + + List scenicIds = List.of(DEFAULT_SCENIC_ID, scenicId); + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.in("scenic_id", scenicIds) + .eq("enabled", 1) + .select("DISTINCT scene_key"); + List 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 sceneKeys = listAllSceneKeys(scenicId); + + WechatSubscribeAllScenesResp resp = new WechatSubscribeAllScenesResp(); + resp.setScenicId(scenicId); + + List scenes = new ArrayList<>(); + for (String sceneKey : sceneKeys) { + List configs = getSceneTemplateConfigsCached(scenicId, sceneKey); + + WechatSubscribeAllScenesResp.SceneWithTemplates sceneWithTemplates = new WechatSubscribeAllScenesResp.SceneWithTemplates(); + sceneWithTemplates.setSceneKey(sceneKey); + + List 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("清除所有订阅消息配置缓存"); } diff --git a/src/main/java/com/ycwl/basic/service/impl/UserNotificationAuthorizationServiceImpl.java b/src/main/java/com/ycwl/basic/service/impl/UserNotificationAuthorizationServiceImpl.java index f644ab70..c2235c9b 100644 --- a/src/main/java/com/ycwl/basic/service/impl/UserNotificationAuthorizationServiceImpl.java +++ b/src/main/java/com/ycwl/basic/service/impl/UserNotificationAuthorizationServiceImpl.java @@ -228,31 +228,30 @@ public class UserNotificationAuthorizationServiceImpl implements UserNotificatio public Map batchCheckAuthorization( Long memberId, List templateIds, Long scenicId) { log.debug("批量检查用户授权: memberId={}, templateIds={}, scenicId={}", memberId, templateIds, scenicId); - + Map result = new HashMap<>(); - + if (templateIds == null || templateIds.isEmpty()) { return result; } - - // 查询用户在该景区的所有授权记录 - List userRecords = getUserAuthorizations(memberId); - - // 过滤出指定景区和模板的记录 - Map recordMap = userRecords.stream() - .filter(record -> scenicId.equals(record.getScenicId())) - .filter(record -> templateIds.contains(record.getTemplateId())) + + // 使用批量查询方法 + List records = + userNotificationAuthorizationMapper.selectBatchByTemplateIds(memberId, templateIds, scenicId); + + // 转换为Map + Map 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; } diff --git a/src/main/java/com/ycwl/basic/service/notify/WechatSubscribeNotifyConfigService.java b/src/main/java/com/ycwl/basic/service/notify/WechatSubscribeNotifyConfigService.java index bf590ef4..003ab4e9 100644 --- a/src/main/java/com/ycwl/basic/service/notify/WechatSubscribeNotifyConfigService.java +++ b/src/main/java/com/ycwl/basic/service/notify/WechatSubscribeNotifyConfigService.java @@ -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 listAllSceneKeys(Long scenicId) { + return configRepository.listAllSceneKeys(scenicId); + } + + /** + * 获取景区下所有场景及其模板列表(带缓存,不含用户授权信息) + * + * @param scenicId 景区ID + * @return 所有场景及模板配置 + */ + public WechatSubscribeAllScenesResp getAllScenesWithTemplatesCached(Long scenicId) { + return configRepository.getAllScenesWithTemplatesCached(scenicId); + } + /** * 获取事件下的模板配置列表(带缓存) * diff --git a/src/main/resources/mapper/UserNotificationAuthorizationMapper.xml b/src/main/resources/mapper/UserNotificationAuthorizationMapper.xml index fdb48af3..f74bba08 100644 --- a/src/main/resources/mapper/UserNotificationAuthorizationMapper.xml +++ b/src/main/resources/mapper/UserNotificationAuthorizationMapper.xml @@ -92,12 +92,26 @@ - + + + + \ No newline at end of file