Merge branch 'notify_v2'

This commit is contained in:
2026-01-06 11:30:52 +08:00
61 changed files with 3655 additions and 9 deletions

View File

@@ -43,6 +43,13 @@ public interface UserNotificationAuthorizationService {
* @return 授权记录
*/
UserNotificationAuthorizationEntity recordAuthorization(Long memberId, String templateId, Long scenicId);
/**
* 记录用户授权(支持幂等)
*
* @param requestId 前端幂等ID(同一次用户授权动作复用);为空则不做幂等控制
*/
UserNotificationAuthorizationEntity recordAuthorization(Long memberId, String templateId, Long scenicId, String requestId);
/**
* 批量记录用户授权
@@ -53,6 +60,13 @@ public interface UserNotificationAuthorizationService {
* @return 批量授权记录结果
*/
List<AuthorizationRecord> batchRecordAuthorization(Long memberId, List<String> templateIds, Long scenicId);
/**
* 批量记录用户授权(支持幂等)
*
* @param requestId 前端幂等ID(同一次用户授权动作复用);为空则不做幂等控制
*/
List<AuthorizationRecord> batchRecordAuthorization(Long memberId, List<String> templateIds, Long scenicId, String requestId);
/**
* 消费一次授权
@@ -182,4 +196,4 @@ public interface UserNotificationAuthorizationService {
public Date getQueryTime() { return queryTime; }
public void setQueryTime(Date queryTime) { this.queryTime = queryTime; }
}
}
}

View File

@@ -1,10 +1,14 @@
package com.ycwl.basic.service.impl;
import com.ycwl.basic.mapper.UserNotificationAuthorizationMapper;
import com.ycwl.basic.mapper.UserNotificationAuthorizationRecordMapper;
import com.ycwl.basic.model.pc.notify.entity.UserNotificationAuthorizationEntity;
import com.ycwl.basic.model.pc.notify.entity.UserNotificationAuthorizationRecordEntity;
import com.ycwl.basic.service.UserNotificationAuthorizationService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -23,6 +27,9 @@ public class UserNotificationAuthorizationServiceImpl implements UserNotificatio
@Autowired
private UserNotificationAuthorizationMapper userNotificationAuthorizationMapper;
@Autowired
private UserNotificationAuthorizationRecordMapper userNotificationAuthorizationRecordMapper;
@Override
public UserNotificationAuthorizationEntity checkAuthorization(Long memberId, String templateId, Long scenicId) {
@@ -40,7 +47,25 @@ public class UserNotificationAuthorizationServiceImpl implements UserNotificatio
@Override
@Transactional(rollbackFor = Exception.class)
public UserNotificationAuthorizationEntity recordAuthorization(Long memberId, String templateId, Long scenicId) {
return recordAuthorization(memberId, templateId, scenicId, null);
}
@Override
@Transactional(rollbackFor = Exception.class)
public UserNotificationAuthorizationEntity recordAuthorization(Long memberId, String templateId, Long scenicId, String requestId) {
log.info("记录用户授权: memberId={}, templateId={}, scenicId={}", memberId, templateId, scenicId);
// 幂等:同一次用户授权动作(requestId)只计数一次,避免前端重试导致授权次数虚增
if (StringUtils.isNotBlank(requestId)) {
boolean inserted = tryInsertAuthorizationRecord(memberId, templateId, scenicId, requestId);
if (!inserted) {
UserNotificationAuthorizationEntity existing =
userNotificationAuthorizationMapper.selectByMemberAndTemplateAndScenic(memberId, templateId, scenicId);
if (existing != null) {
return existing;
}
}
}
// 先查询是否已存在记录
UserNotificationAuthorizationEntity existingRecord = userNotificationAuthorizationMapper
@@ -85,6 +110,12 @@ public class UserNotificationAuthorizationServiceImpl implements UserNotificatio
@Override
@Transactional(rollbackFor = Exception.class)
public List<AuthorizationRecord> batchRecordAuthorization(Long memberId, List<String> templateIds, Long scenicId) {
return batchRecordAuthorization(memberId, templateIds, scenicId, null);
}
@Override
@Transactional(rollbackFor = Exception.class)
public List<AuthorizationRecord> batchRecordAuthorization(Long memberId, List<String> templateIds, Long scenicId, String requestId) {
log.info("批量记录用户授权: memberId={}, templateIds={}, scenicId={}", memberId, templateIds, scenicId);
List<AuthorizationRecord> results = new ArrayList<>();
@@ -98,7 +129,7 @@ public class UserNotificationAuthorizationServiceImpl implements UserNotificatio
record.setTemplateId(templateId);
try {
UserNotificationAuthorizationEntity entity = recordAuthorization(memberId, templateId, scenicId);
UserNotificationAuthorizationEntity entity = recordAuthorization(memberId, templateId, scenicId, requestId);
// 转换为响应对象
record.setSuccess(true);
@@ -123,6 +154,22 @@ public class UserNotificationAuthorizationServiceImpl implements UserNotificatio
return results;
}
private boolean tryInsertAuthorizationRecord(Long memberId, String templateId, Long scenicId, String requestId) {
try {
UserNotificationAuthorizationRecordEntity record = new UserNotificationAuthorizationRecordEntity();
record.setMemberId(memberId);
record.setTemplateId(templateId);
record.setScenicId(scenicId);
record.setRequestId(requestId);
record.setCreateTime(new Date());
return userNotificationAuthorizationRecordMapper.insert(record) > 0;
} catch (DuplicateKeyException e) {
log.debug("授权幂等命中: memberId={}, scenicId={}, templateId={}, requestId={}",
memberId, scenicId, templateId, requestId);
return false;
}
}
@Override
@Transactional(rollbackFor = Exception.class)
@@ -228,4 +275,4 @@ public class UserNotificationAuthorizationServiceImpl implements UserNotificatio
return stats;
}
}
}

View File

@@ -0,0 +1,81 @@
package com.ycwl.basic.service.notify;
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;
import com.ycwl.basic.repository.WechatSubscribeNotifyConfigRepository;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 微信订阅消息配置查询服务(仅负责“配置解析”,不包含授权/发送逻辑)
*
* @Author: System
* @Date: 2025/12/31
*/
@Service
public class WechatSubscribeNotifyConfigService {
private final WechatSubscribeNotifyConfigRepository configRepository;
public WechatSubscribeNotifyConfigService(WechatSubscribeNotifyConfigRepository configRepository) {
this.configRepository = configRepository;
}
public List<WechatSubscribeTemplateConfigEntity> listSceneTemplateConfigs(Long scenicId, String sceneKey) {
List<WechatSubscribeSceneTemplateEntity> mappings =
configRepository.listEffectiveSceneTemplateMappings(scenicId, sceneKey);
if (CollectionUtils.isEmpty(mappings)) {
return new ArrayList<>();
}
Set<String> templateKeys = mappings.stream()
.map(WechatSubscribeSceneTemplateEntity::getTemplateKey)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
Map<String, WechatSubscribeTemplateConfigEntity> configMap =
configRepository.getEffectiveTemplateConfigs(scenicId, templateKeys);
List<WechatSubscribeTemplateConfigEntity> result = new ArrayList<>();
for (WechatSubscribeSceneTemplateEntity mapping : mappings) {
WechatSubscribeTemplateConfigEntity cfg = configMap.get(mapping.getTemplateKey());
if (cfg == null || !Objects.equals(cfg.getEnabled(), 1)) {
continue;
}
result.add(cfg);
}
return result;
}
public List<WechatSubscribeTemplateConfigEntity> listEventTemplateConfigs(Long scenicId, String eventKey) {
List<WechatSubscribeEventTemplateEntity> mappings =
configRepository.listEffectiveEventTemplateMappings(scenicId, eventKey);
if (CollectionUtils.isEmpty(mappings)) {
return new ArrayList<>();
}
Set<String> templateKeys = mappings.stream()
.map(WechatSubscribeEventTemplateEntity::getTemplateKey)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
Map<String, WechatSubscribeTemplateConfigEntity> configMap =
configRepository.getEffectiveTemplateConfigs(scenicId, templateKeys);
List<WechatSubscribeTemplateConfigEntity> result = new ArrayList<>();
for (WechatSubscribeEventTemplateEntity mapping : mappings) {
WechatSubscribeTemplateConfigEntity cfg = configMap.get(mapping.getTemplateKey());
if (cfg == null || !Objects.equals(cfg.getEnabled(), 1)) {
continue;
}
result.add(cfg);
}
return result;
}
}

View File

@@ -0,0 +1,282 @@
package com.ycwl.basic.service.notify;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.fasterxml.jackson.core.type.TypeReference;
import com.ycwl.basic.integration.message.dto.ZtMessage;
import com.ycwl.basic.integration.message.service.ZtMessageProducerService;
import com.ycwl.basic.mapper.WechatSubscribeSendLogMapper;
import com.ycwl.basic.model.pc.notify.entity.WechatSubscribeSendLogEntity;
import com.ycwl.basic.model.pc.notify.entity.WechatSubscribeTemplateConfigEntity;
import com.ycwl.basic.model.pc.notify.req.WechatSubscribeNotifyTriggerRequest;
import com.ycwl.basic.model.pc.notify.resp.WechatSubscribeNotifyTriggerResult;
import com.ycwl.basic.utils.JacksonUtil;
import com.ycwl.basic.utils.NotificationAuthUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 微信订阅消息统一触发器(后端内部调用)
*
* @Author: System
* @Date: 2025/12/31
*/
@Service
@Slf4j
public class WechatSubscribeNotifyTriggerService {
private static final Pattern VAR_PATTERN = Pattern.compile("\\$\\{([^}]+)}");
private static final String STATUS_INIT = "INIT";
private static final String STATUS_SENT = "SENT";
private static final String STATUS_SKIPPED_NO_AUTH = "SKIPPED_NO_AUTH";
private static final String STATUS_FAILED = "FAILED";
private final WechatSubscribeNotifyConfigService configService;
private final WechatSubscribeSendLogMapper sendLogMapper;
private final NotificationAuthUtils notificationAuthUtils;
private final ZtMessageProducerService ztMessageProducerService;
public WechatSubscribeNotifyTriggerService(WechatSubscribeNotifyConfigService configService,
WechatSubscribeSendLogMapper sendLogMapper,
NotificationAuthUtils notificationAuthUtils,
ZtMessageProducerService ztMessageProducerService) {
this.configService = configService;
this.sendLogMapper = sendLogMapper;
this.notificationAuthUtils = notificationAuthUtils;
this.ztMessageProducerService = ztMessageProducerService;
}
/**
* 触发订阅消息发送(支持按 scenicId 覆盖 + 幂等 + 授权消费)
*/
public WechatSubscribeNotifyTriggerResult trigger(String eventKey, WechatSubscribeNotifyTriggerRequest request) {
WechatSubscribeNotifyTriggerResult result = new WechatSubscribeNotifyTriggerResult();
if (StringUtils.isBlank(eventKey) || request == null) {
log.warn("订阅消息触发入参非法: eventKey={}, request={}", eventKey, request);
return result;
}
if (request.getScenicId() == null || request.getMemberId() == null || StringUtils.isBlank(request.getOpenId())) {
log.warn("订阅消息触发缺少必要字段: eventKey={}, scenicId={}, memberId={}, openId={}",
eventKey, request.getScenicId(), request.getMemberId(), request.getOpenId());
return result;
}
List<WechatSubscribeTemplateConfigEntity> templateConfigs =
configService.listEventTemplateConfigs(request.getScenicId(), eventKey);
if (templateConfigs.isEmpty()) {
return result;
}
result.setConfigFound(true);
Map<String, Object> variables = buildVariables(eventKey, request);
int sentCount = 0;
int skippedCount = 0;
for (WechatSubscribeTemplateConfigEntity cfg : templateConfigs) {
if (cfg == null || StringUtils.isBlank(cfg.getWechatTemplateId()) || StringUtils.isBlank(cfg.getTemplateKey())) {
skippedCount++;
continue;
}
String idempotencyKey = buildIdempotencyKey(eventKey, cfg.getTemplateKey(), request);
WechatSubscribeSendLogEntity sendLog = buildInitLog(idempotencyKey, eventKey, cfg, request);
if (!tryInsertSendLog(sendLog)) {
skippedCount++;
continue;
}
try {
// 检查并消费授权
if (!notificationAuthUtils.checkAndConsumeAuthorization(
request.getMemberId(), cfg.getWechatTemplateId(), request.getScenicId())) {
updateSendLog(sendLog.getId(), STATUS_SKIPPED_NO_AUTH, null, null);
skippedCount++;
continue;
}
ZtMessage msg = buildZtMessage(cfg, request.getOpenId(), variables, eventKey);
ztMessageProducerService.send(msg);
updateSendLog(sendLog.getId(), STATUS_SENT, msg.getMessageId(), null);
sentCount++;
} catch (Exception e) {
updateSendLog(sendLog.getId(), STATUS_FAILED, null, e.getMessage());
skippedCount++;
log.error("订阅消息发送失败: eventKey={}, templateKey={}, memberId={}, scenicId={}, error={}",
eventKey, cfg.getTemplateKey(), request.getMemberId(), request.getScenicId(), e.getMessage(), e);
}
}
result.setSentCount(sentCount);
result.setSkippedCount(skippedCount);
return result;
}
private boolean tryInsertSendLog(WechatSubscribeSendLogEntity logEntity) {
try {
return sendLogMapper.insert(logEntity) > 0;
} catch (DuplicateKeyException e) {
// 幂等命中:直接跳过,不再重复消费授权
return false;
}
}
private void updateSendLog(Long id, String status, String messageId, String errorMessage) {
if (id == null) {
return;
}
WechatSubscribeSendLogEntity update = new WechatSubscribeSendLogEntity();
update.setStatus(status);
update.setZtMessageId(messageId);
update.setErrorMessage(errorMessage);
update.setUpdateTime(new Date());
sendLogMapper.update(update, new QueryWrapper<WechatSubscribeSendLogEntity>().eq("id", id));
}
private static String buildIdempotencyKey(String eventKey, String templateKey, WechatSubscribeNotifyTriggerRequest request) {
String bizId = Objects.toString(request.getBizId(), "");
String raw = eventKey + "|" + templateKey + "|" + request.getScenicId() + "|" + request.getMemberId() + "|" + bizId;
return sha256Hex(raw);
}
private static String sha256Hex(String input) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] digest = md.digest(input.getBytes(StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder(digest.length * 2);
for (byte b : digest) {
sb.append(String.format("%02x", b));
}
return sb.toString();
} catch (Exception e) {
throw new IllegalStateException("sha256计算失败", e);
}
}
private WechatSubscribeSendLogEntity buildInitLog(String idempotencyKey,
String eventKey,
WechatSubscribeTemplateConfigEntity cfg,
WechatSubscribeNotifyTriggerRequest request) {
WechatSubscribeSendLogEntity logEntity = new WechatSubscribeSendLogEntity();
logEntity.setIdempotencyKey(idempotencyKey);
logEntity.setEventKey(eventKey);
logEntity.setTemplateKey(cfg.getTemplateKey());
logEntity.setScenicId(request.getScenicId());
logEntity.setMemberId(request.getMemberId());
logEntity.setOpenId(request.getOpenId());
logEntity.setWechatTemplateId(cfg.getWechatTemplateId());
logEntity.setStatus(STATUS_INIT);
logEntity.setPayloadJson(safeJson(request));
logEntity.setCreateTime(new Date());
logEntity.setUpdateTime(new Date());
return logEntity;
}
private static String safeJson(Object obj) {
try {
return JacksonUtil.toJson(obj);
} catch (Exception e) {
return "{}";
}
}
private static Map<String, Object> buildVariables(String eventKey, WechatSubscribeNotifyTriggerRequest request) {
Map<String, Object> vars = new HashMap<>();
if (request.getVariables() != null) {
vars.putAll(request.getVariables());
}
vars.put("eventKey", eventKey);
vars.put("scenicId", request.getScenicId());
vars.put("memberId", request.getMemberId());
vars.put("openId", request.getOpenId());
vars.put("bizId", request.getBizId());
return vars;
}
private static ZtMessage buildZtMessage(WechatSubscribeTemplateConfigEntity cfg,
String openId,
Map<String, Object> variables,
String eventKey) {
String title = renderOrDefault(cfg.getTitleTemplate(), variables, cfg.getTemplateKey());
String content = renderOrDefault(cfg.getContentTemplate(), variables, title);
String page = renderOrDefault(cfg.getPageTemplate(), variables, "pages/index/index");
Map<String, Object> dataParam = buildDataParam(cfg.getDataTemplateJson(), variables);
Map<String, Object> extra = new HashMap<>();
extra.put("data", dataParam);
extra.put("page", page);
ZtMessage msg = new ZtMessage();
msg.setChannelId(cfg.getWechatTemplateId());
msg.setTitle(title);
msg.setContent(content);
msg.setTarget(openId);
msg.setExtra(extra);
msg.setSendReason(eventKey);
msg.setSendBiz("订阅消息");
return msg;
}
private static Map<String, Object> buildDataParam(String dataTemplateJson, Map<String, Object> variables) {
if (StringUtils.isBlank(dataTemplateJson)) {
throw new IllegalArgumentException("dataTemplateJson为空");
}
Map<String, Object> templateMap = JacksonUtil.fromJson(dataTemplateJson, new TypeReference<Map<String, Object>>() {});
if (templateMap == null || templateMap.isEmpty()) {
throw new IllegalArgumentException("dataTemplateJson解析为空");
}
Map<String, Object> dataParam = new HashMap<>();
for (Map.Entry<String, Object> entry : templateMap.entrySet()) {
String key = entry.getKey();
if (StringUtils.isBlank(key)) {
continue;
}
String rawValue = entry.getValue() != null ? entry.getValue().toString() : "";
dataParam.put(key, render(rawValue, variables));
}
return dataParam;
}
private static String renderOrDefault(String template, Map<String, Object> variables, String defaultValue) {
if (StringUtils.isBlank(template)) {
return defaultValue;
}
String rendered = render(template, variables);
return StringUtils.isBlank(rendered) ? defaultValue : rendered;
}
private static String render(String template, Map<String, Object> variables) {
if (template == null) {
return null;
}
if (variables == null || variables.isEmpty()) {
return template;
}
Matcher matcher = VAR_PATTERN.matcher(template);
StringBuffer sb = new StringBuffer();
while (matcher.find()) {
String key = matcher.group(1);
if (key != null) {
key = key.trim();
}
Object value = key != null ? variables.get(key) : null;
String replacement = value != null ? value.toString() : "";
matcher.appendReplacement(sb, Matcher.quoteReplacement(replacement));
}
matcher.appendTail(sb);
return sb.toString();
}
}

View File

@@ -0,0 +1,53 @@
package com.ycwl.basic.service.pc;
import com.github.pagehelper.PageInfo;
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.WechatSubscribeSendLogEntity;
import com.ycwl.basic.model.pc.notify.entity.WechatSubscribeTemplateConfigEntity;
import com.ycwl.basic.model.pc.notify.req.WechatSubscribeEventTemplatePageReq;
import com.ycwl.basic.model.pc.notify.req.WechatSubscribeEventTemplateSaveReq;
import com.ycwl.basic.model.pc.notify.req.WechatSubscribeSceneTemplatePageReq;
import com.ycwl.basic.model.pc.notify.req.WechatSubscribeSceneTemplateSaveReq;
import com.ycwl.basic.model.pc.notify.req.WechatSubscribeSendLogPageReq;
import com.ycwl.basic.model.pc.notify.req.WechatSubscribeTemplateConfigPageReq;
import com.ycwl.basic.model.pc.notify.req.WechatSubscribeTemplateConfigSaveReq;
import com.ycwl.basic.utils.ApiResponse;
/**
* 微信小程序订阅消息:配置管理(管理后台)
*
* @Author: System
* @Date: 2025/12/31
*/
public interface WechatSubscribeNotifyAdminService {
ApiResponse<PageInfo<WechatSubscribeTemplateConfigEntity>> pageTemplateConfig(WechatSubscribeTemplateConfigPageReq req);
ApiResponse<WechatSubscribeTemplateConfigEntity> getTemplateConfig(Long id);
ApiResponse<Boolean> saveTemplateConfig(WechatSubscribeTemplateConfigSaveReq req);
ApiResponse<Boolean> deleteTemplateConfig(Long id);
ApiResponse<PageInfo<WechatSubscribeSceneTemplateEntity>> pageSceneTemplate(WechatSubscribeSceneTemplatePageReq req);
ApiResponse<WechatSubscribeSceneTemplateEntity> getSceneTemplate(Long id);
ApiResponse<Boolean> saveSceneTemplate(WechatSubscribeSceneTemplateSaveReq req);
ApiResponse<Boolean> deleteSceneTemplate(Long id);
ApiResponse<PageInfo<WechatSubscribeEventTemplateEntity>> pageEventTemplate(WechatSubscribeEventTemplatePageReq req);
ApiResponse<WechatSubscribeEventTemplateEntity> getEventTemplate(Long id);
ApiResponse<Boolean> saveEventTemplate(WechatSubscribeEventTemplateSaveReq req);
ApiResponse<Boolean> deleteEventTemplate(Long id);
ApiResponse<PageInfo<WechatSubscribeSendLogEntity>> pageSendLog(WechatSubscribeSendLogPageReq req);
ApiResponse<WechatSubscribeSendLogEntity> getSendLog(Long id);
}

View File

@@ -0,0 +1,533 @@
package com.ycwl.basic.service.pc.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.fasterxml.jackson.databind.JsonNode;
import com.ycwl.basic.mapper.WechatSubscribeEventTemplateMapper;
import com.ycwl.basic.mapper.WechatSubscribeSceneTemplateMapper;
import com.ycwl.basic.mapper.WechatSubscribeSendLogMapper;
import com.ycwl.basic.mapper.WechatSubscribeTemplateConfigMapper;
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.WechatSubscribeSendLogEntity;
import com.ycwl.basic.model.pc.notify.entity.WechatSubscribeTemplateConfigEntity;
import com.ycwl.basic.model.pc.notify.req.WechatSubscribeEventTemplatePageReq;
import com.ycwl.basic.model.pc.notify.req.WechatSubscribeEventTemplateSaveReq;
import com.ycwl.basic.model.pc.notify.req.WechatSubscribeSceneTemplatePageReq;
import com.ycwl.basic.model.pc.notify.req.WechatSubscribeSceneTemplateSaveReq;
import com.ycwl.basic.model.pc.notify.req.WechatSubscribeSendLogPageReq;
import com.ycwl.basic.model.pc.notify.req.WechatSubscribeTemplateConfigPageReq;
import com.ycwl.basic.model.pc.notify.req.WechatSubscribeTemplateConfigSaveReq;
import com.ycwl.basic.service.pc.WechatSubscribeNotifyAdminService;
import com.ycwl.basic.utils.ApiResponse;
import com.ycwl.basic.utils.JacksonUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.List;
import java.util.Objects;
/**
* 微信小程序订阅消息:配置管理(管理后台)
*
* @Author: System
* @Date: 2025/12/31
*/
@Service
@Slf4j
public class WechatSubscribeNotifyAdminServiceImpl implements WechatSubscribeNotifyAdminService {
private static final int MAX_PAGE_SIZE = 200;
private final WechatSubscribeTemplateConfigMapper templateConfigMapper;
private final WechatSubscribeSceneTemplateMapper sceneTemplateMapper;
private final WechatSubscribeEventTemplateMapper eventTemplateMapper;
private final WechatSubscribeSendLogMapper sendLogMapper;
public WechatSubscribeNotifyAdminServiceImpl(WechatSubscribeTemplateConfigMapper templateConfigMapper,
WechatSubscribeSceneTemplateMapper sceneTemplateMapper,
WechatSubscribeEventTemplateMapper eventTemplateMapper,
WechatSubscribeSendLogMapper sendLogMapper) {
this.templateConfigMapper = templateConfigMapper;
this.sceneTemplateMapper = sceneTemplateMapper;
this.eventTemplateMapper = eventTemplateMapper;
this.sendLogMapper = sendLogMapper;
}
@Override
public ApiResponse<PageInfo<WechatSubscribeTemplateConfigEntity>> pageTemplateConfig(WechatSubscribeTemplateConfigPageReq req) {
try {
if (req == null) {
req = new WechatSubscribeTemplateConfigPageReq();
}
sanitizePage(req);
QueryWrapper<WechatSubscribeTemplateConfigEntity> wrapper = new QueryWrapper<>();
if (req.getScenicId() != null) {
wrapper.eq("scenic_id", req.getScenicId());
}
if (StringUtils.isNotBlank(req.getTemplateKey())) {
wrapper.like("template_key", req.getTemplateKey().trim());
}
if (StringUtils.isNotBlank(req.getWechatTemplateId())) {
wrapper.like("wechat_template_id", req.getWechatTemplateId().trim());
}
if (req.getEnabled() != null) {
wrapper.eq("enabled", req.getEnabled());
}
wrapper.orderByDesc("scenic_id").orderByDesc("update_time").orderByDesc("id");
PageHelper.startPage(req.getPageNum(), req.getPageSize());
List<WechatSubscribeTemplateConfigEntity> list = templateConfigMapper.selectList(wrapper);
return ApiResponse.success(new PageInfo<>(list));
} catch (Exception e) {
log.error("订阅消息|模板配置分页查询失败", e);
return ApiResponse.fail("模板配置分页查询失败: " + e.getMessage());
}
}
@Override
public ApiResponse<WechatSubscribeTemplateConfigEntity> getTemplateConfig(Long id) {
try {
if (id == null) {
return ApiResponse.fail("id不能为空");
}
return ApiResponse.success(templateConfigMapper.selectById(id));
} catch (Exception e) {
log.error("订阅消息|模板配置详情查询失败 id={}", id, e);
return ApiResponse.fail("模板配置详情查询失败: " + e.getMessage());
}
}
@Override
public ApiResponse<Boolean> saveTemplateConfig(WechatSubscribeTemplateConfigSaveReq req) {
try {
String err = validateTemplateConfigSaveReq(req);
if (err != null) {
return ApiResponse.fail(err);
}
WechatSubscribeTemplateConfigEntity entity = new WechatSubscribeTemplateConfigEntity();
entity.setTemplateKey(req.getTemplateKey().trim());
entity.setScenicId(req.getScenicId());
entity.setWechatTemplateId(req.getWechatTemplateId().trim());
entity.setEnabled(req.getEnabled());
entity.setTitleTemplate(StringUtils.trimToNull(req.getTitleTemplate()));
entity.setContentTemplate(StringUtils.trimToNull(req.getContentTemplate()));
entity.setPageTemplate(StringUtils.trimToNull(req.getPageTemplate()));
entity.setDataTemplateJson(StringUtils.trimToNull(req.getDataTemplateJson()));
entity.setDescription(StringUtils.trimToNull(req.getDescription()));
entity.setUpdateTime(new Date());
if (req.getId() != null) {
WechatSubscribeTemplateConfigEntity existing = templateConfigMapper.selectById(req.getId());
if (existing == null) {
return ApiResponse.fail("记录不存在");
}
entity.setId(req.getId());
int updated = templateConfigMapper.updateById(entity);
return ApiResponse.success(updated > 0);
}
// upsert by (template_key, scenic_id)
WechatSubscribeTemplateConfigEntity existing = templateConfigMapper.selectOne(new QueryWrapper<WechatSubscribeTemplateConfigEntity>()
.eq("template_key", entity.getTemplateKey())
.eq("scenic_id", entity.getScenicId()));
if (existing != null) {
entity.setId(existing.getId());
int updated = templateConfigMapper.updateById(entity);
return ApiResponse.success(updated > 0);
}
entity.setCreateTime(new Date());
int inserted = templateConfigMapper.insert(entity);
return ApiResponse.success(inserted > 0);
} catch (DuplicateKeyException e) {
return ApiResponse.fail("保存失败:唯一键冲突(templateKey+scenicId已存在)");
} catch (Exception e) {
log.error("订阅消息|模板配置保存失败", e);
return ApiResponse.fail("模板配置保存失败: " + e.getMessage());
}
}
@Override
public ApiResponse<Boolean> deleteTemplateConfig(Long id) {
try {
if (id == null) {
return ApiResponse.fail("id不能为空");
}
return ApiResponse.success(templateConfigMapper.deleteById(id) > 0);
} catch (Exception e) {
log.error("订阅消息|模板配置删除失败 id={}", id, e);
return ApiResponse.fail("模板配置删除失败: " + e.getMessage());
}
}
@Override
public ApiResponse<PageInfo<WechatSubscribeSceneTemplateEntity>> pageSceneTemplate(WechatSubscribeSceneTemplatePageReq req) {
try {
if (req == null) {
req = new WechatSubscribeSceneTemplatePageReq();
}
sanitizePage(req);
QueryWrapper<WechatSubscribeSceneTemplateEntity> wrapper = new QueryWrapper<>();
if (req.getScenicId() != null) {
wrapper.eq("scenic_id", req.getScenicId());
}
if (StringUtils.isNotBlank(req.getSceneKey())) {
wrapper.like("scene_key", req.getSceneKey().trim());
}
if (StringUtils.isNotBlank(req.getTemplateKey())) {
wrapper.like("template_key", req.getTemplateKey().trim());
}
if (req.getEnabled() != null) {
wrapper.eq("enabled", req.getEnabled());
}
wrapper.orderByDesc("scenic_id").orderByAsc("sort_order").orderByDesc("id");
PageHelper.startPage(req.getPageNum(), req.getPageSize());
List<WechatSubscribeSceneTemplateEntity> list = sceneTemplateMapper.selectList(wrapper);
return ApiResponse.success(new PageInfo<>(list));
} catch (Exception e) {
log.error("订阅消息|场景映射分页查询失败", e);
return ApiResponse.fail("场景映射分页查询失败: " + e.getMessage());
}
}
@Override
public ApiResponse<WechatSubscribeSceneTemplateEntity> getSceneTemplate(Long id) {
try {
if (id == null) {
return ApiResponse.fail("id不能为空");
}
return ApiResponse.success(sceneTemplateMapper.selectById(id));
} catch (Exception e) {
log.error("订阅消息|场景映射详情查询失败 id={}", id, e);
return ApiResponse.fail("场景映射详情查询失败: " + e.getMessage());
}
}
@Override
public ApiResponse<Boolean> saveSceneTemplate(WechatSubscribeSceneTemplateSaveReq req) {
try {
String err = validateSceneTemplateSaveReq(req);
if (err != null) {
return ApiResponse.fail(err);
}
WechatSubscribeSceneTemplateEntity entity = new WechatSubscribeSceneTemplateEntity();
entity.setSceneKey(req.getSceneKey().trim());
entity.setTemplateKey(req.getTemplateKey().trim());
entity.setScenicId(req.getScenicId());
entity.setEnabled(req.getEnabled());
entity.setSortOrder(Objects.requireNonNullElse(req.getSortOrder(), 0));
entity.setUpdateTime(new Date());
if (req.getId() != null) {
WechatSubscribeSceneTemplateEntity existing = sceneTemplateMapper.selectById(req.getId());
if (existing == null) {
return ApiResponse.fail("记录不存在");
}
entity.setId(req.getId());
int updated = sceneTemplateMapper.updateById(entity);
return ApiResponse.success(updated > 0);
}
WechatSubscribeSceneTemplateEntity existing = sceneTemplateMapper.selectOne(new QueryWrapper<WechatSubscribeSceneTemplateEntity>()
.eq("scene_key", entity.getSceneKey())
.eq("template_key", entity.getTemplateKey())
.eq("scenic_id", entity.getScenicId()));
if (existing != null) {
entity.setId(existing.getId());
int updated = sceneTemplateMapper.updateById(entity);
return ApiResponse.success(updated > 0);
}
entity.setCreateTime(new Date());
int inserted = sceneTemplateMapper.insert(entity);
return ApiResponse.success(inserted > 0);
} catch (DuplicateKeyException e) {
return ApiResponse.fail("保存失败:唯一键冲突(sceneKey+templateKey+scenicId已存在)");
} catch (Exception e) {
log.error("订阅消息|场景映射保存失败", e);
return ApiResponse.fail("场景映射保存失败: " + e.getMessage());
}
}
@Override
public ApiResponse<Boolean> deleteSceneTemplate(Long id) {
try {
if (id == null) {
return ApiResponse.fail("id不能为空");
}
return ApiResponse.success(sceneTemplateMapper.deleteById(id) > 0);
} catch (Exception e) {
log.error("订阅消息|场景映射删除失败 id={}", id, e);
return ApiResponse.fail("场景映射删除失败: " + e.getMessage());
}
}
@Override
public ApiResponse<PageInfo<WechatSubscribeEventTemplateEntity>> pageEventTemplate(WechatSubscribeEventTemplatePageReq req) {
try {
if (req == null) {
req = new WechatSubscribeEventTemplatePageReq();
}
sanitizePage(req);
QueryWrapper<WechatSubscribeEventTemplateEntity> wrapper = new QueryWrapper<>();
if (req.getScenicId() != null) {
wrapper.eq("scenic_id", req.getScenicId());
}
if (StringUtils.isNotBlank(req.getEventKey())) {
wrapper.like("event_key", req.getEventKey().trim());
}
if (StringUtils.isNotBlank(req.getTemplateKey())) {
wrapper.like("template_key", req.getTemplateKey().trim());
}
if (req.getEnabled() != null) {
wrapper.eq("enabled", req.getEnabled());
}
wrapper.orderByDesc("scenic_id").orderByAsc("sort_order").orderByDesc("id");
PageHelper.startPage(req.getPageNum(), req.getPageSize());
List<WechatSubscribeEventTemplateEntity> list = eventTemplateMapper.selectList(wrapper);
return ApiResponse.success(new PageInfo<>(list));
} catch (Exception e) {
log.error("订阅消息|事件映射分页查询失败", e);
return ApiResponse.fail("事件映射分页查询失败: " + e.getMessage());
}
}
@Override
public ApiResponse<WechatSubscribeEventTemplateEntity> getEventTemplate(Long id) {
try {
if (id == null) {
return ApiResponse.fail("id不能为空");
}
return ApiResponse.success(eventTemplateMapper.selectById(id));
} catch (Exception e) {
log.error("订阅消息|事件映射详情查询失败 id={}", id, e);
return ApiResponse.fail("事件映射详情查询失败: " + e.getMessage());
}
}
@Override
public ApiResponse<Boolean> saveEventTemplate(WechatSubscribeEventTemplateSaveReq req) {
try {
String err = validateEventTemplateSaveReq(req);
if (err != null) {
return ApiResponse.fail(err);
}
WechatSubscribeEventTemplateEntity entity = new WechatSubscribeEventTemplateEntity();
entity.setEventKey(req.getEventKey().trim());
entity.setTemplateKey(req.getTemplateKey().trim());
entity.setScenicId(req.getScenicId());
entity.setEnabled(req.getEnabled());
entity.setSortOrder(Objects.requireNonNullElse(req.getSortOrder(), 0));
entity.setSendDelaySeconds(Objects.requireNonNullElse(req.getSendDelaySeconds(), 0));
entity.setDedupSeconds(Objects.requireNonNullElse(req.getDedupSeconds(), 0));
entity.setUpdateTime(new Date());
if (req.getId() != null) {
WechatSubscribeEventTemplateEntity existing = eventTemplateMapper.selectById(req.getId());
if (existing == null) {
return ApiResponse.fail("记录不存在");
}
entity.setId(req.getId());
int updated = eventTemplateMapper.updateById(entity);
return ApiResponse.success(updated > 0);
}
WechatSubscribeEventTemplateEntity existing = eventTemplateMapper.selectOne(new QueryWrapper<WechatSubscribeEventTemplateEntity>()
.eq("event_key", entity.getEventKey())
.eq("template_key", entity.getTemplateKey())
.eq("scenic_id", entity.getScenicId()));
if (existing != null) {
entity.setId(existing.getId());
int updated = eventTemplateMapper.updateById(entity);
return ApiResponse.success(updated > 0);
}
entity.setCreateTime(new Date());
int inserted = eventTemplateMapper.insert(entity);
return ApiResponse.success(inserted > 0);
} catch (DuplicateKeyException e) {
return ApiResponse.fail("保存失败:唯一键冲突(eventKey+templateKey+scenicId已存在)");
} catch (Exception e) {
log.error("订阅消息|事件映射保存失败", e);
return ApiResponse.fail("事件映射保存失败: " + e.getMessage());
}
}
@Override
public ApiResponse<Boolean> deleteEventTemplate(Long id) {
try {
if (id == null) {
return ApiResponse.fail("id不能为空");
}
return ApiResponse.success(eventTemplateMapper.deleteById(id) > 0);
} catch (Exception e) {
log.error("订阅消息|事件映射删除失败 id={}", id, e);
return ApiResponse.fail("事件映射删除失败: " + e.getMessage());
}
}
@Override
public ApiResponse<PageInfo<WechatSubscribeSendLogEntity>> pageSendLog(WechatSubscribeSendLogPageReq req) {
try {
if (req == null) {
req = new WechatSubscribeSendLogPageReq();
}
sanitizePage(req);
QueryWrapper<WechatSubscribeSendLogEntity> wrapper = new QueryWrapper<>();
if (req.getScenicId() != null) {
wrapper.eq("scenic_id", req.getScenicId());
}
if (req.getMemberId() != null) {
wrapper.eq("member_id", req.getMemberId());
}
if (StringUtils.isNotBlank(req.getEventKey())) {
wrapper.eq("event_key", req.getEventKey().trim());
}
if (StringUtils.isNotBlank(req.getTemplateKey())) {
wrapper.eq("template_key", req.getTemplateKey().trim());
}
if (StringUtils.isNotBlank(req.getStatus())) {
wrapper.eq("status", req.getStatus().trim());
}
wrapper.orderByDesc("create_time").orderByDesc("id");
PageHelper.startPage(req.getPageNum(), req.getPageSize());
List<WechatSubscribeSendLogEntity> list = sendLogMapper.selectList(wrapper);
return ApiResponse.success(new PageInfo<>(list));
} catch (Exception e) {
log.error("订阅消息|发送日志分页查询失败", e);
return ApiResponse.fail("发送日志分页查询失败: " + e.getMessage());
}
}
@Override
public ApiResponse<WechatSubscribeSendLogEntity> getSendLog(Long id) {
try {
if (id == null) {
return ApiResponse.fail("id不能为空");
}
return ApiResponse.success(sendLogMapper.selectById(id));
} catch (Exception e) {
log.error("订阅消息|发送日志详情查询失败 id={}", id, e);
return ApiResponse.fail("发送日志详情查询失败: " + e.getMessage());
}
}
private static void sanitizePage(Object req) {
if (req instanceof com.ycwl.basic.model.common.BaseQueryParameterReq base) {
if (base.getPageNum() == null || base.getPageNum() < 1) {
base.setPageNum(1);
}
if (base.getPageSize() == null || base.getPageSize() < 1) {
base.setPageSize(10);
}
if (base.getPageSize() > MAX_PAGE_SIZE) {
base.setPageSize(MAX_PAGE_SIZE);
}
}
}
private static String validateEnabled(Integer enabled) {
if (enabled == null) {
return "enabled不能为空";
}
if (!Objects.equals(enabled, 0) && !Objects.equals(enabled, 1)) {
return "enabled仅支持0或1";
}
return null;
}
private static String validateTemplateConfigSaveReq(WechatSubscribeTemplateConfigSaveReq req) {
if (req == null) {
return "请求体不能为空";
}
if (StringUtils.isBlank(req.getTemplateKey())) {
return "templateKey不能为空";
}
if (req.getScenicId() == null) {
return "scenicId不能为空";
}
if (StringUtils.isBlank(req.getWechatTemplateId())) {
return "wechatTemplateId不能为空";
}
String enabledErr = validateEnabled(req.getEnabled());
if (enabledErr != null) {
return enabledErr;
}
if (StringUtils.isBlank(req.getDataTemplateJson())) {
return "dataTemplateJson不能为空";
}
// 仅验证 JSON 结构为对象,避免运行时发送失败
try {
JsonNode node = JacksonUtil.getJsonNode(req.getDataTemplateJson());
if (node == null || !node.isObject()) {
return "dataTemplateJson必须为JSON对象";
}
} catch (Exception e) {
return "dataTemplateJson不是合法JSON: " + e.getMessage();
}
return null;
}
private static String validateSceneTemplateSaveReq(WechatSubscribeSceneTemplateSaveReq req) {
if (req == null) {
return "请求体不能为空";
}
if (StringUtils.isBlank(req.getSceneKey())) {
return "sceneKey不能为空";
}
if (StringUtils.isBlank(req.getTemplateKey())) {
return "templateKey不能为空";
}
if (req.getScenicId() == null) {
return "scenicId不能为空";
}
String enabledErr = validateEnabled(req.getEnabled());
if (enabledErr != null) {
return enabledErr;
}
return null;
}
private static String validateEventTemplateSaveReq(WechatSubscribeEventTemplateSaveReq req) {
if (req == null) {
return "请求体不能为空";
}
if (StringUtils.isBlank(req.getEventKey())) {
return "eventKey不能为空";
}
if (StringUtils.isBlank(req.getTemplateKey())) {
return "templateKey不能为空";
}
if (req.getScenicId() == null) {
return "scenicId不能为空";
}
String enabledErr = validateEnabled(req.getEnabled());
if (enabledErr != null) {
return enabledErr;
}
if (req.getSendDelaySeconds() != null && req.getSendDelaySeconds() < 0) {
return "sendDelaySeconds不能小于0";
}
if (req.getDedupSeconds() != null && req.getDedupSeconds() < 0) {
return "dedupSeconds不能小于0";
}
return null;
}
}

View File

@@ -37,6 +37,8 @@ import com.ycwl.basic.model.pc.task.resp.TaskRespVO;
import com.ycwl.basic.model.pc.template.resp.TemplateRespVO;
import com.ycwl.basic.model.pc.video.entity.MemberVideoEntity;
import com.ycwl.basic.model.pc.video.entity.VideoEntity;
import com.ycwl.basic.model.pc.notify.req.WechatSubscribeNotifyTriggerRequest;
import com.ycwl.basic.model.pc.notify.resp.WechatSubscribeNotifyTriggerResult;
import com.ycwl.basic.model.task.req.ClientStatusReqVo;
import com.ycwl.basic.model.task.req.TaskReqVo;
import com.ycwl.basic.model.task.req.TaskSuccessReqVo;
@@ -49,6 +51,7 @@ import com.ycwl.basic.repository.ScenicRepository;
import com.ycwl.basic.repository.VideoRepository;
import com.ycwl.basic.repository.VideoTaskRepository;
import com.ycwl.basic.service.pc.ScenicService;
import com.ycwl.basic.service.notify.WechatSubscribeNotifyTriggerService;
import com.ycwl.basic.service.task.TaskService;
import com.ycwl.basic.storage.StorageFactory;
import com.ycwl.basic.storage.adapters.IStorageAdapter;
@@ -129,6 +132,8 @@ public class TaskTaskServiceImpl implements TaskService {
@Autowired
private NotificationAuthUtils notificationAuthUtils;
@Autowired
private WechatSubscribeNotifyTriggerService wechatSubscribeNotifyTriggerService;
@Autowired
private FaceStatusManager faceStatusManager;
private RenderWorkerEntity getWorker(@NonNull WorkerAuthReqVo req) {
@@ -641,6 +646,36 @@ public class TaskTaskServiceImpl implements TaskService {
String openId = member.getOpenId();
MpConfigEntity scenicMp = scenicRepository.getScenicMpConfig(member.getScenicId());
if (StringUtils.isNotBlank(openId) && scenicMp != null) {
Map<String, Object> variables = new HashMap<>();
variables.put("taskId", taskId);
variables.put("faceId", item.getFaceId());
variables.put("videoId", item.getVideoId());
variables.put("nowTime", DateUtil.format(new Date(), "yyyy-MM-dd HH:mm"));
try {
ScenicV2DTO scenicBasic = scenicRepository.getScenicBasic(item.getScenicId());
if (scenicBasic != null && StringUtils.isNotBlank(scenicBasic.getName())) {
variables.put("scenicName", scenicBasic.getName());
}
} catch (Exception e) {
log.debug("获取景区名称失败: scenicId={}, error={}", item.getScenicId(), e.getMessage());
}
WechatSubscribeNotifyTriggerResult triggerResult = wechatSubscribeNotifyTriggerService.trigger(
"VIDEO_GENERATED",
WechatSubscribeNotifyTriggerRequest.builder()
.scenicId(item.getScenicId())
.memberId(memberId)
.openId(openId)
.bizId(String.valueOf(taskId))
.variables(variables)
.build()
);
if (triggerResult.isConfigFound()) {
log.info("memberId:{} VIDEO_GENERATED订阅消息触发完成 sentCount={}, skippedCount={}",
memberId, triggerResult.getSentCount(), triggerResult.getSkippedCount());
return;
}
String templateId = scenicRepository.getVideoGeneratedTemplateId(item.getScenicId());
if (StringUtils.isBlank(templateId)) {
log.warn("未配置视频生成通知模板");