You've already forked FrameTour-BE
Merge branch 'notify_v2'
This commit is contained in:
@@ -41,7 +41,8 @@ public class UserNotificationAuthController {
|
|||||||
@PostMapping("/record")
|
@PostMapping("/record")
|
||||||
public ApiResponse<NotificationAuthRecordResp> recordAuthorization(
|
public ApiResponse<NotificationAuthRecordResp> recordAuthorization(
|
||||||
@RequestBody NotificationAuthRecordReq req) {
|
@RequestBody NotificationAuthRecordReq req) {
|
||||||
log.debug("记录用户通知授权: templateIds={}, scenicId={}", req.getTemplateIds(), req.getScenicId());
|
log.debug("记录用户通知授权: templateIds={}, scenicId={}, requestId={}",
|
||||||
|
req.getTemplateIds(), req.getScenicId(), req.getRequestId());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 获取当前用户ID
|
// 获取当前用户ID
|
||||||
@@ -50,7 +51,7 @@ public class UserNotificationAuthController {
|
|||||||
// 调用批量授权记录方法
|
// 调用批量授权记录方法
|
||||||
List<UserNotificationAuthorizationService.AuthorizationRecord> records =
|
List<UserNotificationAuthorizationService.AuthorizationRecord> records =
|
||||||
userNotificationAuthorizationService.batchRecordAuthorization(
|
userNotificationAuthorizationService.batchRecordAuthorization(
|
||||||
memberId, req.getTemplateIds(), req.getScenicId());
|
memberId, req.getTemplateIds(), req.getScenicId(), req.getRequestId());
|
||||||
|
|
||||||
NotificationAuthRecordResp resp = new NotificationAuthRecordResp();
|
NotificationAuthRecordResp resp = new NotificationAuthRecordResp();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package com.ycwl.basic.controller.mobile.notify;
|
||||||
|
|
||||||
|
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;
|
||||||
|
import com.ycwl.basic.utils.ApiResponse;
|
||||||
|
import com.ycwl.basic.utils.JwtTokenUtil;
|
||||||
|
import com.ycwl.basic.utils.NotificationAuthUtils;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信小程序订阅消息:场景模板查询(移动端API)
|
||||||
|
*
|
||||||
|
* @Author: System
|
||||||
|
* @Date: 2025/12/31
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/mobile/notify/subscribe")
|
||||||
|
@Slf4j
|
||||||
|
public class WechatSubscribeNotifyController {
|
||||||
|
|
||||||
|
private final WechatSubscribeNotifyConfigService configService;
|
||||||
|
private final NotificationAuthUtils notificationAuthUtils;
|
||||||
|
|
||||||
|
public WechatSubscribeNotifyController(WechatSubscribeNotifyConfigService configService,
|
||||||
|
NotificationAuthUtils notificationAuthUtils) {
|
||||||
|
this.configService = configService;
|
||||||
|
this.notificationAuthUtils = notificationAuthUtils;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取“场景”下可申请授权的模板列表(支持按 scenicId 覆盖模板ID/开关/文案)
|
||||||
|
*/
|
||||||
|
@GetMapping("/scenic/{scenicId}/scenes/{sceneKey}/templates")
|
||||||
|
public ApiResponse<WechatSubscribeSceneTemplatesResp> listSceneTemplates(@PathVariable("scenicId") Long scenicId,
|
||||||
|
@PathVariable("sceneKey") String sceneKey) {
|
||||||
|
if (scenicId == null) {
|
||||||
|
return ApiResponse.fail("scenicId不能为空");
|
||||||
|
}
|
||||||
|
if (StringUtils.isBlank(sceneKey)) {
|
||||||
|
return ApiResponse.fail("sceneKey不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
Long memberId = JwtTokenUtil.getWorker().getUserId();
|
||||||
|
List<WechatSubscribeTemplateConfigEntity> configs = configService.listSceneTemplateConfigs(scenicId, sceneKey);
|
||||||
|
|
||||||
|
WechatSubscribeSceneTemplatesResp resp = new WechatSubscribeSceneTemplatesResp();
|
||||||
|
resp.setScenicId(scenicId);
|
||||||
|
resp.setSceneKey(sceneKey);
|
||||||
|
|
||||||
|
List<WechatSubscribeSceneTemplatesResp.TemplateInfo> templates = new ArrayList<>();
|
||||||
|
for (WechatSubscribeTemplateConfigEntity cfg : configs) {
|
||||||
|
if (cfg == null || StringUtils.isBlank(cfg.getWechatTemplateId())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String title = StringUtils.isNotBlank(cfg.getTitleTemplate())
|
||||||
|
? cfg.getTitleTemplate()
|
||||||
|
: cfg.getTemplateKey();
|
||||||
|
int remaining = notificationAuthUtils.getRemainingCount(memberId, cfg.getWechatTemplateId(), scenicId);
|
||||||
|
|
||||||
|
WechatSubscribeSceneTemplatesResp.TemplateInfo info = new WechatSubscribeSceneTemplatesResp.TemplateInfo();
|
||||||
|
info.setTemplateKey(cfg.getTemplateKey());
|
||||||
|
info.setWechatTemplateId(cfg.getWechatTemplateId());
|
||||||
|
info.setTitle(title);
|
||||||
|
info.setDescription(cfg.getDescription());
|
||||||
|
info.setRemainingCount(remaining);
|
||||||
|
info.setHasAuthorization(remaining > 0);
|
||||||
|
templates.add(info);
|
||||||
|
}
|
||||||
|
resp.setTemplates(templates);
|
||||||
|
|
||||||
|
log.debug("场景模板查询: scenicId={}, sceneKey={}, memberId={}, templateCount={}",
|
||||||
|
scenicId, sceneKey, memberId, Objects.requireNonNullElse(templates.size(), 0));
|
||||||
|
return ApiResponse.success(resp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
package com.ycwl.basic.controller.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.service.pc.WechatSubscribeNotifyAdminService;
|
||||||
|
import com.ycwl.basic.utils.ApiResponse;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信小程序订阅消息:配置管理(管理后台)
|
||||||
|
*
|
||||||
|
* @Author: System
|
||||||
|
* @Date: 2025/12/31
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/wechatSubscribeNotify/v1")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class WechatSubscribeNotifyAdminController {
|
||||||
|
|
||||||
|
private final WechatSubscribeNotifyAdminService adminService;
|
||||||
|
|
||||||
|
// ========================= 模板配置 =========================
|
||||||
|
|
||||||
|
@PostMapping("/templateConfig/page")
|
||||||
|
public ApiResponse<PageInfo<WechatSubscribeTemplateConfigEntity>> pageTemplateConfig(
|
||||||
|
@RequestBody WechatSubscribeTemplateConfigPageReq req) {
|
||||||
|
return adminService.pageTemplateConfig(req);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/templateConfig/detail/{id}")
|
||||||
|
public ApiResponse<WechatSubscribeTemplateConfigEntity> getTemplateConfig(@PathVariable("id") Long id) {
|
||||||
|
return adminService.getTemplateConfig(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/templateConfig/save")
|
||||||
|
public ApiResponse<Boolean> saveTemplateConfig(@RequestBody WechatSubscribeTemplateConfigSaveReq req) {
|
||||||
|
return adminService.saveTemplateConfig(req);
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/templateConfig/delete/{id}")
|
||||||
|
public ApiResponse<Boolean> deleteTemplateConfig(@PathVariable("id") Long id) {
|
||||||
|
return adminService.deleteTemplateConfig(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================= 场景映射 =========================
|
||||||
|
|
||||||
|
@PostMapping("/sceneTemplate/page")
|
||||||
|
public ApiResponse<PageInfo<WechatSubscribeSceneTemplateEntity>> pageSceneTemplate(
|
||||||
|
@RequestBody WechatSubscribeSceneTemplatePageReq req) {
|
||||||
|
return adminService.pageSceneTemplate(req);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/sceneTemplate/detail/{id}")
|
||||||
|
public ApiResponse<WechatSubscribeSceneTemplateEntity> getSceneTemplate(@PathVariable("id") Long id) {
|
||||||
|
return adminService.getSceneTemplate(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/sceneTemplate/save")
|
||||||
|
public ApiResponse<Boolean> saveSceneTemplate(@RequestBody WechatSubscribeSceneTemplateSaveReq req) {
|
||||||
|
return adminService.saveSceneTemplate(req);
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/sceneTemplate/delete/{id}")
|
||||||
|
public ApiResponse<Boolean> deleteSceneTemplate(@PathVariable("id") Long id) {
|
||||||
|
return adminService.deleteSceneTemplate(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================= 事件映射 =========================
|
||||||
|
|
||||||
|
@PostMapping("/eventTemplate/page")
|
||||||
|
public ApiResponse<PageInfo<WechatSubscribeEventTemplateEntity>> pageEventTemplate(
|
||||||
|
@RequestBody WechatSubscribeEventTemplatePageReq req) {
|
||||||
|
return adminService.pageEventTemplate(req);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/eventTemplate/detail/{id}")
|
||||||
|
public ApiResponse<WechatSubscribeEventTemplateEntity> getEventTemplate(@PathVariable("id") Long id) {
|
||||||
|
return adminService.getEventTemplate(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/eventTemplate/save")
|
||||||
|
public ApiResponse<Boolean> saveEventTemplate(@RequestBody WechatSubscribeEventTemplateSaveReq req) {
|
||||||
|
return adminService.saveEventTemplate(req);
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/eventTemplate/delete/{id}")
|
||||||
|
public ApiResponse<Boolean> deleteEventTemplate(@PathVariable("id") Long id) {
|
||||||
|
return adminService.deleteEventTemplate(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================= 发送日志 =========================
|
||||||
|
|
||||||
|
@PostMapping("/sendLog/page")
|
||||||
|
public ApiResponse<PageInfo<WechatSubscribeSendLogEntity>> pageSendLog(@RequestBody WechatSubscribeSendLogPageReq req) {
|
||||||
|
return adminService.pageSendLog(req);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/sendLog/detail/{id}")
|
||||||
|
public ApiResponse<WechatSubscribeSendLogEntity> getSendLog(@PathVariable("id") Long id) {
|
||||||
|
return adminService.getSendLog(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
27
src/main/java/com/ycwl/basic/mapper/TemplateV2Mapper.java
Normal file
27
src/main/java/com/ycwl/basic/mapper/TemplateV2Mapper.java
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package com.ycwl.basic.mapper;
|
||||||
|
|
||||||
|
import com.ycwl.basic.template.model.entity.TemplateV2Entity;
|
||||||
|
import com.ycwl.basic.template.model.req.TemplateV2ReqQuery;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Template v2 Mapper
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface TemplateV2Mapper {
|
||||||
|
List<TemplateV2Entity> list(TemplateV2ReqQuery query);
|
||||||
|
|
||||||
|
TemplateV2Entity getById(@Param("id") Long id);
|
||||||
|
|
||||||
|
int insert(TemplateV2Entity entity);
|
||||||
|
|
||||||
|
int updateById(TemplateV2Entity entity);
|
||||||
|
|
||||||
|
int updateStatus(@Param("id") Long id, @Param("status") Integer status);
|
||||||
|
|
||||||
|
int softDelete(@Param("id") Long id);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.ycwl.basic.mapper;
|
||||||
|
|
||||||
|
import com.ycwl.basic.template.model.entity.TemplateV2SegmentEntity;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Template v2 Segment Mapper
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface TemplateV2SegmentMapper {
|
||||||
|
List<TemplateV2SegmentEntity> listByTemplateId(@Param("templateId") Long templateId);
|
||||||
|
|
||||||
|
int deleteByTemplateId(@Param("templateId") Long templateId);
|
||||||
|
|
||||||
|
int batchInsert(@Param("segments") List<TemplateV2SegmentEntity> segments);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.ycwl.basic.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.ycwl.basic.model.pc.notify.entity.UserNotificationAuthorizationRecordEntity;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户订阅消息授权明细Mapper(幂等)
|
||||||
|
*
|
||||||
|
* @Author: System
|
||||||
|
* @Date: 2025/12/31
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface UserNotificationAuthorizationRecordMapper extends BaseMapper<UserNotificationAuthorizationRecordEntity> {
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.ycwl.basic.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.ycwl.basic.model.pc.notify.entity.WechatSubscribeEventTemplateEntity;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信订阅消息事件模板映射Mapper
|
||||||
|
*
|
||||||
|
* @Author: System
|
||||||
|
* @Date: 2025/12/31
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface WechatSubscribeEventTemplateMapper extends BaseMapper<WechatSubscribeEventTemplateEntity> {
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.ycwl.basic.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.ycwl.basic.model.pc.notify.entity.WechatSubscribeSceneTemplateEntity;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信订阅消息场景模板映射Mapper
|
||||||
|
*
|
||||||
|
* @Author: System
|
||||||
|
* @Date: 2025/12/31
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface WechatSubscribeSceneTemplateMapper extends BaseMapper<WechatSubscribeSceneTemplateEntity> {
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.ycwl.basic.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.ycwl.basic.model.pc.notify.entity.WechatSubscribeSendLogEntity;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信订阅消息发送日志Mapper
|
||||||
|
*
|
||||||
|
* @Author: System
|
||||||
|
* @Date: 2025/12/31
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface WechatSubscribeSendLogMapper extends BaseMapper<WechatSubscribeSendLogEntity> {
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.ycwl.basic.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.ycwl.basic.model.pc.notify.entity.WechatSubscribeTemplateConfigEntity;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信订阅消息模板配置Mapper
|
||||||
|
*
|
||||||
|
* @Author: System
|
||||||
|
* @Date: 2025/12/31
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface WechatSubscribeTemplateConfigMapper extends BaseMapper<WechatSubscribeTemplateConfigEntity> {
|
||||||
|
}
|
||||||
|
|
||||||
@@ -26,4 +26,13 @@ public class NotificationAuthRecordReq {
|
|||||||
*/
|
*/
|
||||||
@NotNull(message = "景区ID不能为空")
|
@NotNull(message = "景区ID不能为空")
|
||||||
private Long scenicId;
|
private Long scenicId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 前端幂等ID(可选)
|
||||||
|
* <p>
|
||||||
|
* 目的:避免前端重试导致授权次数虚增。
|
||||||
|
* 同一次用户授权动作(一次 requestSubscribeMessage)建议复用同一个 requestId。
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
private String requestId;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package com.ycwl.basic.model.mobile.notify.resp;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 场景可申请的订阅消息模板列表(含用户授权余额)
|
||||||
|
*
|
||||||
|
* @Author: System
|
||||||
|
* @Date: 2025/12/31
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class WechatSubscribeSceneTemplatesResp {
|
||||||
|
|
||||||
|
private Long scenicId;
|
||||||
|
|
||||||
|
private String sceneKey;
|
||||||
|
|
||||||
|
private List<TemplateInfo> templates;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class TemplateInfo {
|
||||||
|
/**
|
||||||
|
* 逻辑模板键(业务固定)
|
||||||
|
*/
|
||||||
|
private String templateKey;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信订阅消息模板ID(tmplId)
|
||||||
|
*/
|
||||||
|
private String wechatTemplateId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 前端展示标题
|
||||||
|
*/
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 前端展示描述
|
||||||
|
*/
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户剩余授权次数
|
||||||
|
*/
|
||||||
|
private Integer remainingCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否有授权(remainingCount > 0)
|
||||||
|
*/
|
||||||
|
private Boolean hasAuthorization;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package com.ycwl.basic.model.pc.notify.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户订阅消息授权明细(幂等)
|
||||||
|
*
|
||||||
|
* @Author: System
|
||||||
|
* @Date: 2025/12/31
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@TableName("user_notification_authorization_record")
|
||||||
|
public class UserNotificationAuthorizationRecordEntity {
|
||||||
|
|
||||||
|
@TableId
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
private Long memberId;
|
||||||
|
|
||||||
|
private Long scenicId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信订阅消息模板ID(tmplId)
|
||||||
|
*/
|
||||||
|
private String templateId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 前端幂等ID(同一次用户授权动作复用)
|
||||||
|
*/
|
||||||
|
private String requestId;
|
||||||
|
|
||||||
|
private Date createTime;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package com.ycwl.basic.model.pc.notify.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 事件到模板映射(后端触发发送用,支持按景区覆盖)
|
||||||
|
*
|
||||||
|
* @Author: System
|
||||||
|
* @Date: 2025/12/31
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@TableName("wechat_subscribe_event_template")
|
||||||
|
public class WechatSubscribeEventTemplateEntity {
|
||||||
|
|
||||||
|
@TableId
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
private String eventKey;
|
||||||
|
|
||||||
|
private String templateKey;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 景区ID;0=默认配置
|
||||||
|
*/
|
||||||
|
private Long scenicId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否启用:1启用 0禁用
|
||||||
|
*/
|
||||||
|
private Integer enabled;
|
||||||
|
|
||||||
|
private Integer sortOrder;
|
||||||
|
|
||||||
|
private Integer sendDelaySeconds;
|
||||||
|
|
||||||
|
private Integer dedupSeconds;
|
||||||
|
|
||||||
|
private Date createTime;
|
||||||
|
|
||||||
|
private Date updateTime;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package com.ycwl.basic.model.pc.notify.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 场景到模板映射(前端申请授权用,支持按景区覆盖)
|
||||||
|
*
|
||||||
|
* @Author: System
|
||||||
|
* @Date: 2025/12/31
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@TableName("wechat_subscribe_scene_template")
|
||||||
|
public class WechatSubscribeSceneTemplateEntity {
|
||||||
|
|
||||||
|
@TableId
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
private String sceneKey;
|
||||||
|
|
||||||
|
private String templateKey;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 景区ID;0=默认配置
|
||||||
|
*/
|
||||||
|
private Long scenicId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否启用:1启用 0禁用
|
||||||
|
*/
|
||||||
|
private Integer enabled;
|
||||||
|
|
||||||
|
private Integer sortOrder;
|
||||||
|
|
||||||
|
private Date createTime;
|
||||||
|
|
||||||
|
private Date updateTime;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package com.ycwl.basic.model.pc.notify.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信订阅消息发送日志(用于幂等与排障)
|
||||||
|
*
|
||||||
|
* @Author: System
|
||||||
|
* @Date: 2025/12/31
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@TableName("wechat_subscribe_send_log")
|
||||||
|
public class WechatSubscribeSendLogEntity {
|
||||||
|
|
||||||
|
@TableId
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
private String idempotencyKey;
|
||||||
|
|
||||||
|
private String eventKey;
|
||||||
|
|
||||||
|
private String templateKey;
|
||||||
|
|
||||||
|
private Long scenicId;
|
||||||
|
|
||||||
|
private Long memberId;
|
||||||
|
|
||||||
|
private String openId;
|
||||||
|
|
||||||
|
private String wechatTemplateId;
|
||||||
|
|
||||||
|
private String ztMessageId;
|
||||||
|
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
private String errorMessage;
|
||||||
|
|
||||||
|
private String payloadJson;
|
||||||
|
|
||||||
|
private Date createTime;
|
||||||
|
|
||||||
|
private Date updateTime;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package com.ycwl.basic.model.pc.notify.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信小程序订阅消息模板配置(支持按景区覆盖)
|
||||||
|
*
|
||||||
|
* @Author: System
|
||||||
|
* @Date: 2025/12/31
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@TableName("wechat_subscribe_template_config")
|
||||||
|
public class WechatSubscribeTemplateConfigEntity {
|
||||||
|
|
||||||
|
@TableId
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 逻辑模板键(业务固定)
|
||||||
|
*/
|
||||||
|
private String templateKey;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 景区ID;0=默认配置
|
||||||
|
*/
|
||||||
|
private Long scenicId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信订阅消息模板ID(tmplId)
|
||||||
|
*/
|
||||||
|
private String wechatTemplateId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否启用:1启用 0禁用
|
||||||
|
*/
|
||||||
|
private Integer enabled;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标题模板(用于日志/后台展示)
|
||||||
|
*/
|
||||||
|
private String titleTemplate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 内容模板(用于日志/后台展示)
|
||||||
|
*/
|
||||||
|
private String contentTemplate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跳转页面模板(小程序 page)
|
||||||
|
*/
|
||||||
|
private String pageTemplate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* data模板JSON:{ "thing1":"${scenicName}", "thing3":"${remark}" }
|
||||||
|
*/
|
||||||
|
private String dataTemplateJson;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 前端展示用描述
|
||||||
|
*/
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
private Date createTime;
|
||||||
|
|
||||||
|
private Date updateTime;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.ycwl.basic.model.pc.notify.req;
|
||||||
|
|
||||||
|
import com.ycwl.basic.model.common.BaseQueryParameterReq;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 事件-模板映射分页查询请求
|
||||||
|
*
|
||||||
|
* @Author: System
|
||||||
|
* @Date: 2025/12/31
|
||||||
|
*/
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@Data
|
||||||
|
public class WechatSubscribeEventTemplatePageReq extends BaseQueryParameterReq {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 景区ID;0=默认配置;为空表示不筛选
|
||||||
|
*/
|
||||||
|
private Long scenicId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 事件键(模糊匹配)
|
||||||
|
*/
|
||||||
|
private String eventKey;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 逻辑模板键(模糊匹配)
|
||||||
|
*/
|
||||||
|
private String templateKey;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否启用:1启用 0禁用
|
||||||
|
*/
|
||||||
|
private Integer enabled;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package com.ycwl.basic.model.pc.notify.req;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 事件-模板映射保存请求(新增/修改)
|
||||||
|
*
|
||||||
|
* @Author: System
|
||||||
|
* @Date: 2025/12/31
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class WechatSubscribeEventTemplateSaveReq {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主键ID(为空表示新增;不为空表示更新)
|
||||||
|
*/
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
private String eventKey;
|
||||||
|
|
||||||
|
private String templateKey;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 景区ID;0=默认配置
|
||||||
|
*/
|
||||||
|
private Long scenicId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否启用:1启用 0禁用
|
||||||
|
*/
|
||||||
|
private Integer enabled;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 排序(越小越靠前)
|
||||||
|
*/
|
||||||
|
private Integer sortOrder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送延迟(秒),0表示立即发送(预留)
|
||||||
|
*/
|
||||||
|
private Integer sendDelaySeconds;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 去重窗口(秒),0表示仅依赖幂等键(预留)
|
||||||
|
*/
|
||||||
|
private Integer dedupSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.ycwl.basic.model.pc.notify.req;
|
||||||
|
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信订阅消息触发入参(后端内部调用)
|
||||||
|
*
|
||||||
|
* @Author: System
|
||||||
|
* @Date: 2025/12/31
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
public class WechatSubscribeNotifyTriggerRequest {
|
||||||
|
|
||||||
|
private Long scenicId;
|
||||||
|
|
||||||
|
private Long memberId;
|
||||||
|
|
||||||
|
private String openId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 业务幂等ID(强烈建议必填)
|
||||||
|
* <p>
|
||||||
|
* 示例:taskId、couponId、faceId+日期 等。
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
private String bizId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模板渲染变量(${key})
|
||||||
|
*/
|
||||||
|
private Map<String, Object> variables;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.ycwl.basic.model.pc.notify.req;
|
||||||
|
|
||||||
|
import com.ycwl.basic.model.common.BaseQueryParameterReq;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 场景-模板映射分页查询请求
|
||||||
|
*
|
||||||
|
* @Author: System
|
||||||
|
* @Date: 2025/12/31
|
||||||
|
*/
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@Data
|
||||||
|
public class WechatSubscribeSceneTemplatePageReq extends BaseQueryParameterReq {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 景区ID;0=默认配置;为空表示不筛选
|
||||||
|
*/
|
||||||
|
private Long scenicId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 场景键(模糊匹配)
|
||||||
|
*/
|
||||||
|
private String sceneKey;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 逻辑模板键(模糊匹配)
|
||||||
|
*/
|
||||||
|
private String templateKey;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否启用:1启用 0禁用
|
||||||
|
*/
|
||||||
|
private Integer enabled;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package com.ycwl.basic.model.pc.notify.req;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 场景-模板映射保存请求(新增/修改)
|
||||||
|
*
|
||||||
|
* @Author: System
|
||||||
|
* @Date: 2025/12/31
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class WechatSubscribeSceneTemplateSaveReq {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主键ID(为空表示新增;不为空表示更新)
|
||||||
|
*/
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
private String sceneKey;
|
||||||
|
|
||||||
|
private String templateKey;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 景区ID;0=默认配置
|
||||||
|
*/
|
||||||
|
private Long scenicId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否启用:1启用 0禁用
|
||||||
|
*/
|
||||||
|
private Integer enabled;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 排序(越小越靠前)
|
||||||
|
*/
|
||||||
|
private Integer sortOrder;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.ycwl.basic.model.pc.notify.req;
|
||||||
|
|
||||||
|
import com.ycwl.basic.model.common.BaseQueryParameterReq;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信订阅消息发送日志分页查询请求
|
||||||
|
*
|
||||||
|
* @Author: System
|
||||||
|
* @Date: 2025/12/31
|
||||||
|
*/
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@Data
|
||||||
|
public class WechatSubscribeSendLogPageReq extends BaseQueryParameterReq {
|
||||||
|
|
||||||
|
private Long scenicId;
|
||||||
|
|
||||||
|
private Long memberId;
|
||||||
|
|
||||||
|
private String eventKey;
|
||||||
|
|
||||||
|
private String templateKey;
|
||||||
|
|
||||||
|
private String status;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.ycwl.basic.model.pc.notify.req;
|
||||||
|
|
||||||
|
import com.ycwl.basic.model.common.BaseQueryParameterReq;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信订阅消息模板配置分页查询请求
|
||||||
|
*
|
||||||
|
* @Author: System
|
||||||
|
* @Date: 2025/12/31
|
||||||
|
*/
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@Data
|
||||||
|
public class WechatSubscribeTemplateConfigPageReq extends BaseQueryParameterReq {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 景区ID;0=默认配置;为空表示不筛选
|
||||||
|
*/
|
||||||
|
private Long scenicId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 逻辑模板键(模糊匹配)
|
||||||
|
*/
|
||||||
|
private String templateKey;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信订阅消息模板ID(模糊匹配)
|
||||||
|
*/
|
||||||
|
private String wechatTemplateId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否启用:1启用 0禁用
|
||||||
|
*/
|
||||||
|
private Integer enabled;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package com.ycwl.basic.model.pc.notify.req;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信订阅消息模板配置保存请求(新增/修改)
|
||||||
|
*
|
||||||
|
* @Author: System
|
||||||
|
* @Date: 2025/12/31
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class WechatSubscribeTemplateConfigSaveReq {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主键ID(为空表示新增;不为空表示更新)
|
||||||
|
*/
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 逻辑模板键(业务固定)
|
||||||
|
*/
|
||||||
|
private String templateKey;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 景区ID;0=默认配置
|
||||||
|
*/
|
||||||
|
private Long scenicId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信订阅消息模板ID(tmplId)
|
||||||
|
*/
|
||||||
|
private String wechatTemplateId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否启用:1启用 0禁用
|
||||||
|
*/
|
||||||
|
private Integer enabled;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标题模板(用于日志/后台展示)
|
||||||
|
*/
|
||||||
|
private String titleTemplate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 内容模板(用于日志/后台展示)
|
||||||
|
*/
|
||||||
|
private String contentTemplate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跳转页面模板(小程序 page)
|
||||||
|
*/
|
||||||
|
private String pageTemplate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* data模板JSON:{ "thing1":"${scenicName}", "thing3":"${remark}" }
|
||||||
|
*/
|
||||||
|
private String dataTemplateJson;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 前端展示用描述
|
||||||
|
*/
|
||||||
|
private String description;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package com.ycwl.basic.model.pc.notify.resp;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信订阅消息触发结果(后端内部调用)
|
||||||
|
*
|
||||||
|
* @Author: System
|
||||||
|
* @Date: 2025/12/31
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class WechatSubscribeNotifyTriggerResult {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否找到了可用配置(eventKey -> templateKey -> templateConfig)
|
||||||
|
*/
|
||||||
|
private boolean configFound;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 成功投递到消息系统的数量(以 producer send 成功返回为准)
|
||||||
|
*/
|
||||||
|
private int sentCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跳过数量(无授权/幂等命中/配置不完整等)
|
||||||
|
*/
|
||||||
|
private int skippedCount;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
package com.ycwl.basic.repository;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
|
import com.ycwl.basic.mapper.WechatSubscribeEventTemplateMapper;
|
||||||
|
import com.ycwl.basic.mapper.WechatSubscribeSceneTemplateMapper;
|
||||||
|
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.WechatSubscribeTemplateConfigEntity;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.util.CollectionUtils;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信订阅消息配置仓库(scenic 覆盖:scenic_id=具体值 > scenic_id=0)
|
||||||
|
*
|
||||||
|
* @Author: System
|
||||||
|
* @Date: 2025/12/31
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class WechatSubscribeNotifyConfigRepository {
|
||||||
|
|
||||||
|
private static final long DEFAULT_SCENIC_ID = 0L;
|
||||||
|
|
||||||
|
private final WechatSubscribeTemplateConfigMapper templateConfigMapper;
|
||||||
|
private final WechatSubscribeSceneTemplateMapper sceneTemplateMapper;
|
||||||
|
private final WechatSubscribeEventTemplateMapper eventTemplateMapper;
|
||||||
|
|
||||||
|
public WechatSubscribeNotifyConfigRepository(WechatSubscribeTemplateConfigMapper templateConfigMapper,
|
||||||
|
WechatSubscribeSceneTemplateMapper sceneTemplateMapper,
|
||||||
|
WechatSubscribeEventTemplateMapper eventTemplateMapper) {
|
||||||
|
this.templateConfigMapper = templateConfigMapper;
|
||||||
|
this.sceneTemplateMapper = sceneTemplateMapper;
|
||||||
|
this.eventTemplateMapper = eventTemplateMapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<WechatSubscribeSceneTemplateEntity> listEffectiveSceneTemplateMappings(Long scenicId, String sceneKey) {
|
||||||
|
Objects.requireNonNull(scenicId, "scenicId is null");
|
||||||
|
if (StringUtils.isBlank(sceneKey)) {
|
||||||
|
throw new IllegalArgumentException("sceneKey is blank");
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Long> scenicIds = List.of(DEFAULT_SCENIC_ID, scenicId);
|
||||||
|
QueryWrapper<WechatSubscribeSceneTemplateEntity> wrapper = new QueryWrapper<>();
|
||||||
|
wrapper.eq("scene_key", sceneKey)
|
||||||
|
.in("scenic_id", scenicIds);
|
||||||
|
List<WechatSubscribeSceneTemplateEntity> rows = sceneTemplateMapper.selectList(wrapper);
|
||||||
|
return pickEffectiveByTemplateKey(rows, scenicId).values().stream()
|
||||||
|
.filter(this::isEnabled)
|
||||||
|
.sorted((a, b) -> {
|
||||||
|
int cmp = Integer.compare(safeInt(a.getSortOrder()), safeInt(b.getSortOrder()));
|
||||||
|
if (cmp != 0) {
|
||||||
|
return cmp;
|
||||||
|
}
|
||||||
|
return Objects.toString(a.getTemplateKey(), "").compareTo(Objects.toString(b.getTemplateKey(), ""));
|
||||||
|
})
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<WechatSubscribeEventTemplateEntity> listEffectiveEventTemplateMappings(Long scenicId, String eventKey) {
|
||||||
|
Objects.requireNonNull(scenicId, "scenicId is null");
|
||||||
|
if (StringUtils.isBlank(eventKey)) {
|
||||||
|
throw new IllegalArgumentException("eventKey is blank");
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Long> scenicIds = List.of(DEFAULT_SCENIC_ID, scenicId);
|
||||||
|
QueryWrapper<WechatSubscribeEventTemplateEntity> wrapper = new QueryWrapper<>();
|
||||||
|
wrapper.eq("event_key", eventKey)
|
||||||
|
.in("scenic_id", scenicIds);
|
||||||
|
List<WechatSubscribeEventTemplateEntity> rows = eventTemplateMapper.selectList(wrapper);
|
||||||
|
return pickEffectiveByTemplateKey(rows, scenicId).values().stream()
|
||||||
|
.filter(this::isEnabled)
|
||||||
|
.sorted((a, b) -> {
|
||||||
|
int cmp = Integer.compare(safeInt(a.getSortOrder()), safeInt(b.getSortOrder()));
|
||||||
|
if (cmp != 0) {
|
||||||
|
return cmp;
|
||||||
|
}
|
||||||
|
return Objects.toString(a.getTemplateKey(), "").compareTo(Objects.toString(b.getTemplateKey(), ""));
|
||||||
|
})
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, WechatSubscribeTemplateConfigEntity> getEffectiveTemplateConfigs(Long scenicId,
|
||||||
|
Collection<String> templateKeys) {
|
||||||
|
Objects.requireNonNull(scenicId, "scenicId is null");
|
||||||
|
if (CollectionUtils.isEmpty(templateKeys)) {
|
||||||
|
return new HashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Long> scenicIds = List.of(DEFAULT_SCENIC_ID, scenicId);
|
||||||
|
QueryWrapper<WechatSubscribeTemplateConfigEntity> wrapper = new QueryWrapper<>();
|
||||||
|
wrapper.in("template_key", templateKeys)
|
||||||
|
.in("scenic_id", scenicIds);
|
||||||
|
List<WechatSubscribeTemplateConfigEntity> rows = templateConfigMapper.selectList(wrapper);
|
||||||
|
Map<String, WechatSubscribeTemplateConfigEntity> effective = new HashMap<>();
|
||||||
|
for (WechatSubscribeTemplateConfigEntity row : rows) {
|
||||||
|
if (row == null || row.getTemplateKey() == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
WechatSubscribeTemplateConfigEntity existing = effective.get(row.getTemplateKey());
|
||||||
|
if (existing == null) {
|
||||||
|
effective.put(row.getTemplateKey(), row);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (Objects.equals(row.getScenicId(), scenicId)) {
|
||||||
|
effective.put(row.getTemplateKey(), row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return effective;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isEnabled(WechatSubscribeSceneTemplateEntity entity) {
|
||||||
|
return entity != null && Objects.equals(entity.getEnabled(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isEnabled(WechatSubscribeEventTemplateEntity entity) {
|
||||||
|
return entity != null && Objects.equals(entity.getEnabled(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int safeInt(Integer value) {
|
||||||
|
return value != null ? value : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T> Map<String, T> pickEffectiveByTemplateKey(List<T> rows, Long scenicId) {
|
||||||
|
Map<String, T> effective = new HashMap<>();
|
||||||
|
if (rows == null || rows.isEmpty()) {
|
||||||
|
return effective;
|
||||||
|
}
|
||||||
|
for (T row : rows) {
|
||||||
|
String templateKey = getTemplateKey(row);
|
||||||
|
if (templateKey == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
T existing = effective.get(templateKey);
|
||||||
|
if (existing == null) {
|
||||||
|
effective.put(templateKey, row);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Long rowScenicId = getScenicId(row);
|
||||||
|
if (Objects.equals(rowScenicId, scenicId)) {
|
||||||
|
effective.put(templateKey, row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return effective;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getTemplateKey(Object row) {
|
||||||
|
if (row instanceof WechatSubscribeSceneTemplateEntity entity) {
|
||||||
|
return entity.getTemplateKey();
|
||||||
|
}
|
||||||
|
if (row instanceof WechatSubscribeEventTemplateEntity entity) {
|
||||||
|
return entity.getTemplateKey();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Long getScenicId(Object row) {
|
||||||
|
if (row instanceof WechatSubscribeSceneTemplateEntity entity) {
|
||||||
|
return entity.getScenicId();
|
||||||
|
}
|
||||||
|
if (row instanceof WechatSubscribeEventTemplateEntity entity) {
|
||||||
|
return entity.getScenicId();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -44,6 +44,13 @@ public interface UserNotificationAuthorizationService {
|
|||||||
*/
|
*/
|
||||||
UserNotificationAuthorizationEntity recordAuthorization(Long memberId, String templateId, Long scenicId);
|
UserNotificationAuthorizationEntity recordAuthorization(Long memberId, String templateId, Long scenicId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录用户授权(支持幂等)
|
||||||
|
*
|
||||||
|
* @param requestId 前端幂等ID(同一次用户授权动作复用);为空则不做幂等控制
|
||||||
|
*/
|
||||||
|
UserNotificationAuthorizationEntity recordAuthorization(Long memberId, String templateId, Long scenicId, String requestId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 批量记录用户授权
|
* 批量记录用户授权
|
||||||
*
|
*
|
||||||
@@ -54,6 +61,13 @@ public interface UserNotificationAuthorizationService {
|
|||||||
*/
|
*/
|
||||||
List<AuthorizationRecord> batchRecordAuthorization(Long memberId, List<String> templateIds, Long scenicId);
|
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);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 消费一次授权
|
* 消费一次授权
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
package com.ycwl.basic.service.impl;
|
package com.ycwl.basic.service.impl;
|
||||||
|
|
||||||
import com.ycwl.basic.mapper.UserNotificationAuthorizationMapper;
|
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.UserNotificationAuthorizationEntity;
|
||||||
|
import com.ycwl.basic.model.pc.notify.entity.UserNotificationAuthorizationRecordEntity;
|
||||||
import com.ycwl.basic.service.UserNotificationAuthorizationService;
|
import com.ycwl.basic.service.UserNotificationAuthorizationService;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.dao.DuplicateKeyException;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@@ -24,6 +28,9 @@ public class UserNotificationAuthorizationServiceImpl implements UserNotificatio
|
|||||||
@Autowired
|
@Autowired
|
||||||
private UserNotificationAuthorizationMapper userNotificationAuthorizationMapper;
|
private UserNotificationAuthorizationMapper userNotificationAuthorizationMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private UserNotificationAuthorizationRecordMapper userNotificationAuthorizationRecordMapper;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public UserNotificationAuthorizationEntity checkAuthorization(Long memberId, String templateId, Long scenicId) {
|
public UserNotificationAuthorizationEntity checkAuthorization(Long memberId, String templateId, Long scenicId) {
|
||||||
log.debug("检查用户授权: memberId={}, templateId={}, scenicId={}", memberId, templateId, scenicId);
|
log.debug("检查用户授权: memberId={}, templateId={}, scenicId={}", memberId, templateId, scenicId);
|
||||||
@@ -40,8 +47,26 @@ public class UserNotificationAuthorizationServiceImpl implements UserNotificatio
|
|||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public UserNotificationAuthorizationEntity recordAuthorization(Long memberId, String templateId, Long scenicId) {
|
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);
|
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
|
UserNotificationAuthorizationEntity existingRecord = userNotificationAuthorizationMapper
|
||||||
.selectByMemberAndTemplateAndScenic(memberId, templateId, scenicId);
|
.selectByMemberAndTemplateAndScenic(memberId, templateId, scenicId);
|
||||||
@@ -85,6 +110,12 @@ public class UserNotificationAuthorizationServiceImpl implements UserNotificatio
|
|||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public List<AuthorizationRecord> batchRecordAuthorization(Long memberId, List<String> templateIds, Long scenicId) {
|
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);
|
log.info("批量记录用户授权: memberId={}, templateIds={}, scenicId={}", memberId, templateIds, scenicId);
|
||||||
|
|
||||||
List<AuthorizationRecord> results = new ArrayList<>();
|
List<AuthorizationRecord> results = new ArrayList<>();
|
||||||
@@ -98,7 +129,7 @@ public class UserNotificationAuthorizationServiceImpl implements UserNotificatio
|
|||||||
record.setTemplateId(templateId);
|
record.setTemplateId(templateId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
UserNotificationAuthorizationEntity entity = recordAuthorization(memberId, templateId, scenicId);
|
UserNotificationAuthorizationEntity entity = recordAuthorization(memberId, templateId, scenicId, requestId);
|
||||||
|
|
||||||
// 转换为响应对象
|
// 转换为响应对象
|
||||||
record.setSuccess(true);
|
record.setSuccess(true);
|
||||||
@@ -124,6 +155,22 @@ public class UserNotificationAuthorizationServiceImpl implements UserNotificatio
|
|||||||
return results;
|
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
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public boolean consumeAuthorization(Long memberId, String templateId, Long scenicId) {
|
public boolean consumeAuthorization(Long memberId, String templateId, Long scenicId) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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.template.resp.TemplateRespVO;
|
||||||
import com.ycwl.basic.model.pc.video.entity.MemberVideoEntity;
|
import com.ycwl.basic.model.pc.video.entity.MemberVideoEntity;
|
||||||
import com.ycwl.basic.model.pc.video.entity.VideoEntity;
|
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.ClientStatusReqVo;
|
||||||
import com.ycwl.basic.model.task.req.TaskReqVo;
|
import com.ycwl.basic.model.task.req.TaskReqVo;
|
||||||
import com.ycwl.basic.model.task.req.TaskSuccessReqVo;
|
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.VideoRepository;
|
||||||
import com.ycwl.basic.repository.VideoTaskRepository;
|
import com.ycwl.basic.repository.VideoTaskRepository;
|
||||||
import com.ycwl.basic.service.pc.ScenicService;
|
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.service.task.TaskService;
|
||||||
import com.ycwl.basic.storage.StorageFactory;
|
import com.ycwl.basic.storage.StorageFactory;
|
||||||
import com.ycwl.basic.storage.adapters.IStorageAdapter;
|
import com.ycwl.basic.storage.adapters.IStorageAdapter;
|
||||||
@@ -129,6 +132,8 @@ public class TaskTaskServiceImpl implements TaskService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private NotificationAuthUtils notificationAuthUtils;
|
private NotificationAuthUtils notificationAuthUtils;
|
||||||
@Autowired
|
@Autowired
|
||||||
|
private WechatSubscribeNotifyTriggerService wechatSubscribeNotifyTriggerService;
|
||||||
|
@Autowired
|
||||||
private FaceStatusManager faceStatusManager;
|
private FaceStatusManager faceStatusManager;
|
||||||
|
|
||||||
private RenderWorkerEntity getWorker(@NonNull WorkerAuthReqVo req) {
|
private RenderWorkerEntity getWorker(@NonNull WorkerAuthReqVo req) {
|
||||||
@@ -641,6 +646,36 @@ public class TaskTaskServiceImpl implements TaskService {
|
|||||||
String openId = member.getOpenId();
|
String openId = member.getOpenId();
|
||||||
MpConfigEntity scenicMp = scenicRepository.getScenicMpConfig(member.getScenicId());
|
MpConfigEntity scenicMp = scenicRepository.getScenicMpConfig(member.getScenicId());
|
||||||
if (StringUtils.isNotBlank(openId) && scenicMp != null) {
|
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());
|
String templateId = scenicRepository.getVideoGeneratedTemplateId(item.getScenicId());
|
||||||
if (StringUtils.isBlank(templateId)) {
|
if (StringUtils.isBlank(templateId)) {
|
||||||
log.warn("未配置视频生成通知模板");
|
log.warn("未配置视频生成通知模板");
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.ycwl.basic.template.api;
|
||||||
|
|
||||||
|
import com.ycwl.basic.template.api.dto.RenderPlan;
|
||||||
|
import com.ycwl.basic.template.api.dto.TemplatePlanBuildCommand;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Template v2 对外能力:生成冻结 RenderPlan
|
||||||
|
*/
|
||||||
|
public interface ITemplateRenderPlanApi {
|
||||||
|
RenderPlan buildRenderPlan(TemplatePlanBuildCommand command);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.ycwl.basic.template.api.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 冻结后的最小可执行 RenderPlan
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class RenderPlan {
|
||||||
|
private Long templateId;
|
||||||
|
private Integer templateVersion;
|
||||||
|
|
||||||
|
private Integer outputWidth;
|
||||||
|
private Integer outputHeight;
|
||||||
|
private Integer outputFps;
|
||||||
|
private String bgmUrl;
|
||||||
|
|
||||||
|
private Integer totalDurationMs;
|
||||||
|
private List<RenderPlanSegment> segments;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.ycwl.basic.template.api.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RenderPlan 片段(已裁剪/已绑定/已计算时间轴)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class RenderPlanSegment {
|
||||||
|
private Integer templateSegmentIndex;
|
||||||
|
private Integer planSegmentIndex;
|
||||||
|
|
||||||
|
private Integer startTimeMs;
|
||||||
|
private Integer durationMs;
|
||||||
|
|
||||||
|
private String segmentType;
|
||||||
|
private String sourceType;
|
||||||
|
private String sourceRef;
|
||||||
|
|
||||||
|
private String boundMaterialUrl;
|
||||||
|
|
||||||
|
private JsonNode renderSpec;
|
||||||
|
private JsonNode audioSpec;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.ycwl.basic.template.api.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RenderPlan 生成命令
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class TemplatePlanBuildCommand {
|
||||||
|
private Long scenicId;
|
||||||
|
private Long templateId;
|
||||||
|
/**
|
||||||
|
* 可选:指定模板版本(用于校验模板是否被更新)
|
||||||
|
*/
|
||||||
|
private Integer templateVersion;
|
||||||
|
private Long faceId;
|
||||||
|
private Long memberId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* slotKey -> 素材列表
|
||||||
|
*/
|
||||||
|
private Map<String, List<TemplateSlotMaterial>> materialsBySlot;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.ycwl.basic.template.api.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Slot 素材引用(由调用方提供,Template v2 不直接跨模块查素材库)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class TemplateSlotMaterial {
|
||||||
|
/**
|
||||||
|
* 素材URL(http/https)
|
||||||
|
*/
|
||||||
|
private String url;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建时间(毫秒时间戳,可选,用于确定性选材)
|
||||||
|
*/
|
||||||
|
private Long createTimeMs;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package com.ycwl.basic.template.controller.pc;
|
||||||
|
|
||||||
|
import com.github.pagehelper.PageInfo;
|
||||||
|
import com.ycwl.basic.template.api.ITemplateRenderPlanApi;
|
||||||
|
import com.ycwl.basic.template.api.dto.RenderPlan;
|
||||||
|
import com.ycwl.basic.template.api.dto.TemplatePlanBuildCommand;
|
||||||
|
import com.ycwl.basic.template.model.req.TemplateV2ReqQuery;
|
||||||
|
import com.ycwl.basic.template.model.req.TemplateV2SaveReq;
|
||||||
|
import com.ycwl.basic.template.model.req.TemplateV2StatusUpdateReq;
|
||||||
|
import com.ycwl.basic.template.model.resp.TemplateV2DetailResp;
|
||||||
|
import com.ycwl.basic.template.model.resp.TemplateV2ListResp;
|
||||||
|
import com.ycwl.basic.template.service.TemplateV2ManagementService;
|
||||||
|
import com.ycwl.basic.utils.ApiResponse;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Template v2 管理端接口
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/template/v2")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class TemplateV2Controller {
|
||||||
|
|
||||||
|
private final TemplateV2ManagementService templateV2ManagementService;
|
||||||
|
private final ITemplateRenderPlanApi templateRenderPlanApi;
|
||||||
|
|
||||||
|
@PostMapping("/page")
|
||||||
|
public ApiResponse<PageInfo<TemplateV2ListResp>> page(@RequestBody TemplateV2ReqQuery query) {
|
||||||
|
return templateV2ManagementService.pageQuery(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/list")
|
||||||
|
public ApiResponse<List<TemplateV2ListResp>> list(@RequestBody TemplateV2ReqQuery query) {
|
||||||
|
return templateV2ManagementService.list(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public ApiResponse<TemplateV2DetailResp> detail(@PathVariable("id") Long id) {
|
||||||
|
return templateV2ManagementService.getDetail(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/add")
|
||||||
|
public ApiResponse<Long> add(@RequestBody TemplateV2SaveReq req) {
|
||||||
|
return templateV2ManagementService.create(req);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
public ApiResponse<Boolean> update(@PathVariable("id") Long id, @RequestBody TemplateV2SaveReq req) {
|
||||||
|
return templateV2ManagementService.update(id, req);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{id}/status")
|
||||||
|
public ApiResponse<Boolean> updateStatus(@PathVariable("id") Long id, @RequestBody TemplateV2StatusUpdateReq req) {
|
||||||
|
return templateV2ManagementService.updateStatus(id, req.getStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public ApiResponse<Boolean> delete(@PathVariable("id") Long id) {
|
||||||
|
return templateV2ManagementService.delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理端预览:根据模板 + materialsBySlot 生成冻结 RenderPlan
|
||||||
|
*/
|
||||||
|
@PostMapping("/render-plan/preview")
|
||||||
|
public ApiResponse<RenderPlan> previewRenderPlan(@RequestBody TemplatePlanBuildCommand command) {
|
||||||
|
return ApiResponse.success(templateRenderPlanApi.buildRenderPlan(command));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.ycwl.basic.template.enums;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Template v2 片段类型
|
||||||
|
*/
|
||||||
|
public enum TemplateV2SegmentType {
|
||||||
|
FIXED,
|
||||||
|
RENDER;
|
||||||
|
|
||||||
|
public static TemplateV2SegmentType parse(String value) {
|
||||||
|
if (value == null || value.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (TemplateV2SegmentType type : values()) {
|
||||||
|
if (type.name().equalsIgnoreCase(value)) {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.ycwl.basic.template.enums;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Template v2 素材来源类型
|
||||||
|
*/
|
||||||
|
public enum TemplateV2SourceType {
|
||||||
|
SLOT,
|
||||||
|
FIXED_URL;
|
||||||
|
|
||||||
|
public static TemplateV2SourceType parse(String value) {
|
||||||
|
if (value == null || value.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (TemplateV2SourceType type : values()) {
|
||||||
|
if (type.name().equalsIgnoreCase(value)) {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package com.ycwl.basic.template.model.entity;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Template v2 主表实体(template_v2)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class TemplateV2Entity {
|
||||||
|
private Long id;
|
||||||
|
private Long scenicId;
|
||||||
|
private String name;
|
||||||
|
private Integer version;
|
||||||
|
private Integer status;
|
||||||
|
|
||||||
|
private Integer outputWidth;
|
||||||
|
private Integer outputHeight;
|
||||||
|
private Integer outputFps;
|
||||||
|
|
||||||
|
private String bgmUrl;
|
||||||
|
private Integer totalDurationMs;
|
||||||
|
|
||||||
|
private Date createTime;
|
||||||
|
private Date updateTime;
|
||||||
|
private Integer deleted;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package com.ycwl.basic.template.model.entity;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Template v2 片段表实体(template_v2_segment)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class TemplateV2SegmentEntity {
|
||||||
|
private Long id;
|
||||||
|
private Long templateId;
|
||||||
|
private Integer segmentIndex;
|
||||||
|
|
||||||
|
private Integer durationMs;
|
||||||
|
private String segmentType;
|
||||||
|
|
||||||
|
private String sourceType;
|
||||||
|
private String sourceRef;
|
||||||
|
|
||||||
|
private String onlyIfExpr;
|
||||||
|
private String renderSpecJson;
|
||||||
|
private String audioSpecJson;
|
||||||
|
|
||||||
|
private Date createTime;
|
||||||
|
private Date updateTime;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.ycwl.basic.template.model.req;
|
||||||
|
|
||||||
|
import com.ycwl.basic.model.common.BaseQueryParameterReq;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Template v2 管理端查询参数
|
||||||
|
*/
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@Data
|
||||||
|
public class TemplateV2ReqQuery extends BaseQueryParameterReq {
|
||||||
|
private Long scenicId;
|
||||||
|
private Integer status;
|
||||||
|
private String name;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package com.ycwl.basic.template.model.req;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import jakarta.validation.constraints.Min;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Template v2 新建/更新请求
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class TemplateV2SaveReq {
|
||||||
|
@NotNull(message = "scenicId不能为空")
|
||||||
|
private Long scenicId;
|
||||||
|
|
||||||
|
@NotBlank(message = "name不能为空")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@NotNull(message = "outputWidth不能为空")
|
||||||
|
@Min(value = 1, message = "outputWidth必须大于0")
|
||||||
|
private Integer outputWidth;
|
||||||
|
|
||||||
|
@NotNull(message = "outputHeight不能为空")
|
||||||
|
@Min(value = 1, message = "outputHeight必须大于0")
|
||||||
|
private Integer outputHeight;
|
||||||
|
|
||||||
|
@NotNull(message = "outputFps不能为空")
|
||||||
|
@Min(value = 1, message = "outputFps必须大于0")
|
||||||
|
private Integer outputFps;
|
||||||
|
|
||||||
|
private String bgmUrl;
|
||||||
|
private Integer status;
|
||||||
|
|
||||||
|
@Valid
|
||||||
|
@NotNull(message = "segments不能为空")
|
||||||
|
private List<TemplateV2SegmentSaveReq> segments;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class TemplateV2SegmentSaveReq {
|
||||||
|
@NotNull(message = "durationMs不能为空")
|
||||||
|
@Min(value = 1, message = "durationMs必须大于0")
|
||||||
|
private Integer durationMs;
|
||||||
|
|
||||||
|
@NotBlank(message = "segmentType不能为空")
|
||||||
|
private String segmentType;
|
||||||
|
|
||||||
|
@NotBlank(message = "sourceType不能为空")
|
||||||
|
private String sourceType;
|
||||||
|
|
||||||
|
@NotBlank(message = "sourceRef不能为空")
|
||||||
|
private String sourceRef;
|
||||||
|
|
||||||
|
private JsonNode onlyIfExpr;
|
||||||
|
private JsonNode renderSpec;
|
||||||
|
private JsonNode audioSpec;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.ycwl.basic.template.model.req;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Template v2 启停用更新
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class TemplateV2StatusUpdateReq {
|
||||||
|
@NotNull(message = "status不能为空")
|
||||||
|
private Integer status;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package com.ycwl.basic.template.model.resp;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Template v2 详情返回(含片段)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class TemplateV2DetailResp {
|
||||||
|
private Long id;
|
||||||
|
private Long scenicId;
|
||||||
|
private String scenicName;
|
||||||
|
private String name;
|
||||||
|
private Integer version;
|
||||||
|
private Integer status;
|
||||||
|
private Integer outputWidth;
|
||||||
|
private Integer outputHeight;
|
||||||
|
private Integer outputFps;
|
||||||
|
private String bgmUrl;
|
||||||
|
private Integer totalDurationMs;
|
||||||
|
private Date createTime;
|
||||||
|
private Date updateTime;
|
||||||
|
|
||||||
|
private List<Segment> segments;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class Segment {
|
||||||
|
private Long id;
|
||||||
|
private Integer segmentIndex;
|
||||||
|
private Integer durationMs;
|
||||||
|
private String segmentType;
|
||||||
|
private String sourceType;
|
||||||
|
private String sourceRef;
|
||||||
|
private JsonNode onlyIfExpr;
|
||||||
|
private JsonNode renderSpec;
|
||||||
|
private JsonNode audioSpec;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.ycwl.basic.template.model.resp;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Template v2 列表项返回
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class TemplateV2ListResp {
|
||||||
|
private Long id;
|
||||||
|
private Long scenicId;
|
||||||
|
private String scenicName;
|
||||||
|
private String name;
|
||||||
|
private Integer version;
|
||||||
|
private Integer status;
|
||||||
|
private Integer totalDurationMs;
|
||||||
|
private Date updateTime;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,291 @@
|
|||||||
|
package com.ycwl.basic.template.service;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.ycwl.basic.exception.BizException;
|
||||||
|
import com.ycwl.basic.mapper.TemplateV2Mapper;
|
||||||
|
import com.ycwl.basic.mapper.TemplateV2SegmentMapper;
|
||||||
|
import com.ycwl.basic.template.api.ITemplateRenderPlanApi;
|
||||||
|
import com.ycwl.basic.template.api.dto.RenderPlan;
|
||||||
|
import com.ycwl.basic.template.api.dto.RenderPlanSegment;
|
||||||
|
import com.ycwl.basic.template.api.dto.TemplatePlanBuildCommand;
|
||||||
|
import com.ycwl.basic.template.api.dto.TemplateSlotMaterial;
|
||||||
|
import com.ycwl.basic.template.enums.TemplateV2SegmentType;
|
||||||
|
import com.ycwl.basic.template.enums.TemplateV2SourceType;
|
||||||
|
import com.ycwl.basic.template.model.entity.TemplateV2Entity;
|
||||||
|
import com.ycwl.basic.template.model.entity.TemplateV2SegmentEntity;
|
||||||
|
import com.ycwl.basic.template.service.onlyif.OnlyIfExpression;
|
||||||
|
import com.ycwl.basic.template.service.onlyif.OnlyIfExpressionParser;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Template v2:RenderPlan 冻结服务
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class TemplateRenderPlanService implements ITemplateRenderPlanApi {
|
||||||
|
|
||||||
|
private static final int STATUS_ENABLED = 1;
|
||||||
|
|
||||||
|
private final TemplateV2Mapper templateV2Mapper;
|
||||||
|
private final TemplateV2SegmentMapper templateV2SegmentMapper;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
private final OnlyIfExpressionParser onlyIfExpressionParser;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RenderPlan buildRenderPlan(TemplatePlanBuildCommand command) {
|
||||||
|
if (command == null || command.getTemplateId() == null) {
|
||||||
|
throw new BizException(400, "templateId不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
TemplateV2Entity template = templateV2Mapper.getById(command.getTemplateId());
|
||||||
|
if (template == null) {
|
||||||
|
throw new BizException(404, "模板不存在");
|
||||||
|
}
|
||||||
|
if (command.getTemplateVersion() != null && !command.getTemplateVersion().equals(template.getVersion())) {
|
||||||
|
throw new BizException(400, "模板版本不匹配");
|
||||||
|
}
|
||||||
|
if (template.getStatus() != null && !Objects.equals(template.getStatus(), STATUS_ENABLED)) {
|
||||||
|
throw new BizException(400, "模板未启用");
|
||||||
|
}
|
||||||
|
validateOutputSpec(template);
|
||||||
|
|
||||||
|
List<TemplateV2SegmentEntity> templateSegments = templateV2SegmentMapper.listByTemplateId(template.getId());
|
||||||
|
if (templateSegments == null || templateSegments.isEmpty()) {
|
||||||
|
throw new BizException(400, "模板未配置片段");
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, List<TemplateSlotMaterial>> materialsBySlot = Objects.requireNonNullElse(command.getMaterialsBySlot(), Map.of());
|
||||||
|
Map<String, Integer> slotCounts = buildSlotCounts(materialsBySlot);
|
||||||
|
|
||||||
|
List<RenderPlanSegment> planSegments = new ArrayList<>();
|
||||||
|
int startTimeMs = 0;
|
||||||
|
int planSegmentIndex = 0;
|
||||||
|
for (TemplateV2SegmentEntity templateSegment : templateSegments) {
|
||||||
|
validateSegment(templateSegment);
|
||||||
|
if (!shouldInclude(templateSegment, slotCounts)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
RenderPlanSegment planSegment = new RenderPlanSegment();
|
||||||
|
planSegment.setTemplateSegmentIndex(templateSegment.getSegmentIndex());
|
||||||
|
planSegment.setPlanSegmentIndex(planSegmentIndex++);
|
||||||
|
planSegment.setStartTimeMs(startTimeMs);
|
||||||
|
planSegment.setDurationMs(templateSegment.getDurationMs());
|
||||||
|
planSegment.setSegmentType(templateSegment.getSegmentType());
|
||||||
|
planSegment.setSourceType(templateSegment.getSourceType());
|
||||||
|
planSegment.setSourceRef(templateSegment.getSourceRef());
|
||||||
|
|
||||||
|
String boundMaterialUrl = bindMaterial(templateSegment, materialsBySlot);
|
||||||
|
planSegment.setBoundMaterialUrl(boundMaterialUrl);
|
||||||
|
|
||||||
|
planSegment.setRenderSpec(parseJsonRequiredOrNull(templateSegment.getRenderSpecJson(), "renderSpecJson"));
|
||||||
|
planSegment.setAudioSpec(parseJsonRequiredOrNull(templateSegment.getAudioSpecJson(), "audioSpecJson"));
|
||||||
|
|
||||||
|
planSegments.add(planSegment);
|
||||||
|
startTimeMs = Math.addExact(startTimeMs, templateSegment.getDurationMs());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (planSegments.isEmpty()) {
|
||||||
|
throw new BizException(400, "RenderPlan为空:所有片段均被only_if裁剪");
|
||||||
|
}
|
||||||
|
|
||||||
|
RenderPlan plan = new RenderPlan();
|
||||||
|
plan.setTemplateId(template.getId());
|
||||||
|
plan.setTemplateVersion(template.getVersion());
|
||||||
|
plan.setOutputWidth(template.getOutputWidth());
|
||||||
|
plan.setOutputHeight(template.getOutputHeight());
|
||||||
|
plan.setOutputFps(template.getOutputFps());
|
||||||
|
plan.setBgmUrl(template.getBgmUrl());
|
||||||
|
plan.setTotalDurationMs(startTimeMs);
|
||||||
|
plan.setSegments(planSegments);
|
||||||
|
return plan;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateOutputSpec(TemplateV2Entity template) {
|
||||||
|
if (template.getOutputWidth() == null || template.getOutputWidth() <= 0) {
|
||||||
|
throw new BizException(400, "outputWidth非法");
|
||||||
|
}
|
||||||
|
if (template.getOutputHeight() == null || template.getOutputHeight() <= 0) {
|
||||||
|
throw new BizException(400, "outputHeight非法");
|
||||||
|
}
|
||||||
|
if (template.getOutputFps() == null || template.getOutputFps() <= 0) {
|
||||||
|
throw new BizException(400, "outputFps非法");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Integer> buildSlotCounts(Map<String, List<TemplateSlotMaterial>> materialsBySlot) {
|
||||||
|
Map<String, Integer> counts = new HashMap<>();
|
||||||
|
for (Map.Entry<String, List<TemplateSlotMaterial>> entry : materialsBySlot.entrySet()) {
|
||||||
|
String slotKey = entry.getKey();
|
||||||
|
List<TemplateSlotMaterial> materials = entry.getValue();
|
||||||
|
int count = 0;
|
||||||
|
if (materials != null) {
|
||||||
|
for (TemplateSlotMaterial material : materials) {
|
||||||
|
if (material != null && material.getUrl() != null && !material.getUrl().isBlank()) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
counts.put(slotKey, count);
|
||||||
|
}
|
||||||
|
return counts;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateSegment(TemplateV2SegmentEntity segment) {
|
||||||
|
if (segment.getDurationMs() == null || segment.getDurationMs() <= 0) {
|
||||||
|
throw new BizException(400, "片段durationMs非法:segmentIndex=" + segment.getSegmentIndex());
|
||||||
|
}
|
||||||
|
TemplateV2SegmentType segmentType = TemplateV2SegmentType.parse(segment.getSegmentType());
|
||||||
|
if (segmentType == null) {
|
||||||
|
throw new BizException(400, "片段segmentType非法:segmentIndex=" + segment.getSegmentIndex());
|
||||||
|
}
|
||||||
|
TemplateV2SourceType sourceType = TemplateV2SourceType.parse(segment.getSourceType());
|
||||||
|
if (sourceType == null) {
|
||||||
|
throw new BizException(400, "片段sourceType非法:segmentIndex=" + segment.getSegmentIndex());
|
||||||
|
}
|
||||||
|
if (segmentType == TemplateV2SegmentType.FIXED && sourceType != TemplateV2SourceType.FIXED_URL) {
|
||||||
|
throw new BizException(400, "FIXED片段仅支持sourceType=FIXED_URL:segmentIndex=" + segment.getSegmentIndex());
|
||||||
|
}
|
||||||
|
if (segmentType == TemplateV2SegmentType.RENDER && sourceType != TemplateV2SourceType.SLOT) {
|
||||||
|
throw new BizException(400, "RENDER片段仅支持sourceType=SLOT:segmentIndex=" + segment.getSegmentIndex());
|
||||||
|
}
|
||||||
|
if (segment.getSourceRef() == null || segment.getSourceRef().isBlank()) {
|
||||||
|
throw new BizException(400, "片段sourceRef不能为空:segmentIndex=" + segment.getSegmentIndex());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean shouldInclude(TemplateV2SegmentEntity segment, Map<String, Integer> slotCounts) {
|
||||||
|
String onlyIfExpr = segment.getOnlyIfExpr();
|
||||||
|
if (onlyIfExpr == null || onlyIfExpr.isBlank()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
JsonNode node;
|
||||||
|
try {
|
||||||
|
node = objectMapper.readTree(onlyIfExpr);
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
throw new BizException(400, "onlyIfExpr解析失败:segmentIndex=" + segment.getSegmentIndex());
|
||||||
|
}
|
||||||
|
OnlyIfExpression expr = onlyIfExpressionParser.parse(node);
|
||||||
|
return expr.evaluate(slotCounts);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String bindMaterial(TemplateV2SegmentEntity segment, Map<String, List<TemplateSlotMaterial>> materialsBySlot) {
|
||||||
|
TemplateV2SegmentType segmentType = TemplateV2SegmentType.parse(segment.getSegmentType());
|
||||||
|
if (segmentType == TemplateV2SegmentType.FIXED) {
|
||||||
|
if (!isHttpUrl(segment.getSourceRef())) {
|
||||||
|
throw new BizException(400, "FIXED片段sourceRef必须是http/https URL:segmentIndex=" + segment.getSegmentIndex());
|
||||||
|
}
|
||||||
|
return segment.getSourceRef();
|
||||||
|
}
|
||||||
|
String slotKey = segment.getSourceRef();
|
||||||
|
TemplateSlotMaterial chosen = chooseMaterial(materialsBySlot.get(slotKey));
|
||||||
|
if (chosen == null) {
|
||||||
|
throw new BizException(400, "缺少slot素材:" + slotKey);
|
||||||
|
}
|
||||||
|
if (!isHttpUrl(chosen.getUrl())) {
|
||||||
|
throw new BizException(400, "slot素材URL必须是http/https:" + slotKey);
|
||||||
|
}
|
||||||
|
return chosen.getUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
private TemplateSlotMaterial chooseMaterial(List<TemplateSlotMaterial> materials) {
|
||||||
|
if (materials == null || materials.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
boolean hasCreateTime = false;
|
||||||
|
for (TemplateSlotMaterial material : materials) {
|
||||||
|
if (material != null && material.getCreateTimeMs() != null) {
|
||||||
|
hasCreateTime = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!hasCreateTime) {
|
||||||
|
for (TemplateSlotMaterial material : materials) {
|
||||||
|
if (material != null && material.getUrl() != null && !material.getUrl().isBlank()) {
|
||||||
|
return material;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
TemplateSlotMaterial best = null;
|
||||||
|
for (TemplateSlotMaterial material : materials) {
|
||||||
|
if (material == null || material.getUrl() == null || material.getUrl().isBlank()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (best == null) {
|
||||||
|
best = material;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
best = compareMaterial(material, best) < 0 ? material : best;
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int compareMaterial(TemplateSlotMaterial a, TemplateSlotMaterial b) {
|
||||||
|
Long aTime = a.getCreateTimeMs();
|
||||||
|
Long bTime = b.getCreateTimeMs();
|
||||||
|
if (aTime == null && bTime == null) {
|
||||||
|
return compareUrl(a.getUrl(), b.getUrl());
|
||||||
|
}
|
||||||
|
if (aTime == null) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (bTime == null) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
int timeCompare = Long.compare(bTime, aTime);
|
||||||
|
if (timeCompare != 0) {
|
||||||
|
return timeCompare;
|
||||||
|
}
|
||||||
|
return compareUrl(a.getUrl(), b.getUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
private int compareUrl(String a, String b) {
|
||||||
|
if (a == null && b == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (a == null) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (b == null) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return a.compareTo(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
private JsonNode parseJsonRequiredOrNull(String json, String fieldName) {
|
||||||
|
if (json == null || json.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return objectMapper.readTree(json);
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
throw new BizException(400, fieldName + "解析失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isHttpUrl(String url) {
|
||||||
|
if (url == null || url.isBlank()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
URI uri = URI.create(url);
|
||||||
|
String scheme = uri.getScheme();
|
||||||
|
return "http".equalsIgnoreCase(scheme) || "https".equalsIgnoreCase(scheme);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.ycwl.basic.template.service;
|
||||||
|
|
||||||
|
import com.github.pagehelper.PageInfo;
|
||||||
|
import com.ycwl.basic.template.model.req.TemplateV2ReqQuery;
|
||||||
|
import com.ycwl.basic.template.model.req.TemplateV2SaveReq;
|
||||||
|
import com.ycwl.basic.template.model.resp.TemplateV2DetailResp;
|
||||||
|
import com.ycwl.basic.template.model.resp.TemplateV2ListResp;
|
||||||
|
import com.ycwl.basic.utils.ApiResponse;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Template v2 管理端服务
|
||||||
|
*/
|
||||||
|
public interface TemplateV2ManagementService {
|
||||||
|
ApiResponse<PageInfo<TemplateV2ListResp>> pageQuery(TemplateV2ReqQuery query);
|
||||||
|
|
||||||
|
ApiResponse<List<TemplateV2ListResp>> list(TemplateV2ReqQuery query);
|
||||||
|
|
||||||
|
ApiResponse<TemplateV2DetailResp> getDetail(Long templateId);
|
||||||
|
|
||||||
|
ApiResponse<Long> create(TemplateV2SaveReq req);
|
||||||
|
|
||||||
|
ApiResponse<Boolean> update(Long templateId, TemplateV2SaveReq req);
|
||||||
|
|
||||||
|
ApiResponse<Boolean> updateStatus(Long templateId, Integer status);
|
||||||
|
|
||||||
|
ApiResponse<Boolean> delete(Long templateId);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,319 @@
|
|||||||
|
package com.ycwl.basic.template.service.impl;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.github.pagehelper.PageHelper;
|
||||||
|
import com.github.pagehelper.PageInfo;
|
||||||
|
import com.ycwl.basic.mapper.TemplateV2Mapper;
|
||||||
|
import com.ycwl.basic.mapper.TemplateV2SegmentMapper;
|
||||||
|
import com.ycwl.basic.repository.ScenicRepository;
|
||||||
|
import com.ycwl.basic.template.model.entity.TemplateV2Entity;
|
||||||
|
import com.ycwl.basic.template.model.entity.TemplateV2SegmentEntity;
|
||||||
|
import com.ycwl.basic.template.model.req.TemplateV2ReqQuery;
|
||||||
|
import com.ycwl.basic.template.model.req.TemplateV2SaveReq;
|
||||||
|
import com.ycwl.basic.template.model.resp.TemplateV2DetailResp;
|
||||||
|
import com.ycwl.basic.template.model.resp.TemplateV2ListResp;
|
||||||
|
import com.ycwl.basic.template.service.TemplateV2ManagementService;
|
||||||
|
import com.ycwl.basic.template.service.onlyif.OnlyIfExpressionParser;
|
||||||
|
import com.ycwl.basic.utils.ApiResponse;
|
||||||
|
import com.ycwl.basic.utils.SnowFlakeUtil;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import com.ycwl.basic.template.enums.TemplateV2SegmentType;
|
||||||
|
import com.ycwl.basic.template.enums.TemplateV2SourceType;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import com.ycwl.basic.exception.BizException;
|
||||||
|
import java.net.URI;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class TemplateV2ManagementServiceImpl implements TemplateV2ManagementService {
|
||||||
|
|
||||||
|
private static final int STATUS_ENABLED = 1;
|
||||||
|
private static final int STATUS_DISABLED = 0;
|
||||||
|
|
||||||
|
private final TemplateV2Mapper templateV2Mapper;
|
||||||
|
private final TemplateV2SegmentMapper templateV2SegmentMapper;
|
||||||
|
private final ScenicRepository scenicRepository;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
private final OnlyIfExpressionParser onlyIfExpressionParser;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ApiResponse<PageInfo<TemplateV2ListResp>> pageQuery(TemplateV2ReqQuery query) {
|
||||||
|
PageHelper.startPage(query.getPageNum(), query.getPageSize());
|
||||||
|
List<TemplateV2Entity> entities = templateV2Mapper.list(query);
|
||||||
|
List<TemplateV2ListResp> items = toListResp(entities);
|
||||||
|
return ApiResponse.success(new PageInfo<>(items));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ApiResponse<List<TemplateV2ListResp>> list(TemplateV2ReqQuery query) {
|
||||||
|
List<TemplateV2Entity> entities = templateV2Mapper.list(query);
|
||||||
|
return ApiResponse.success(toListResp(entities));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ApiResponse<TemplateV2DetailResp> getDetail(Long templateId) {
|
||||||
|
TemplateV2Entity template = templateV2Mapper.getById(templateId);
|
||||||
|
if (template == null) {
|
||||||
|
return ApiResponse.fail("模板不存在");
|
||||||
|
}
|
||||||
|
List<TemplateV2SegmentEntity> segments = templateV2SegmentMapper.listByTemplateId(templateId);
|
||||||
|
TemplateV2DetailResp resp = toDetailResp(template, segments);
|
||||||
|
return ApiResponse.success(resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public ApiResponse<Long> create(TemplateV2SaveReq req) {
|
||||||
|
validateSaveReq(req);
|
||||||
|
|
||||||
|
long templateId = SnowFlakeUtil.getLongId();
|
||||||
|
int totalDurationMs = sumDurationMs(req.getSegments());
|
||||||
|
|
||||||
|
TemplateV2Entity entity = new TemplateV2Entity();
|
||||||
|
entity.setId(templateId);
|
||||||
|
entity.setScenicId(req.getScenicId());
|
||||||
|
entity.setName(req.getName());
|
||||||
|
entity.setVersion(1);
|
||||||
|
entity.setStatus(normalizeStatus(req.getStatus()));
|
||||||
|
entity.setOutputWidth(req.getOutputWidth());
|
||||||
|
entity.setOutputHeight(req.getOutputHeight());
|
||||||
|
entity.setOutputFps(req.getOutputFps());
|
||||||
|
entity.setBgmUrl(req.getBgmUrl());
|
||||||
|
entity.setTotalDurationMs(totalDurationMs);
|
||||||
|
templateV2Mapper.insert(entity);
|
||||||
|
|
||||||
|
saveSegments(templateId, req.getSegments());
|
||||||
|
return ApiResponse.success(templateId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public ApiResponse<Boolean> update(Long templateId, TemplateV2SaveReq req) {
|
||||||
|
validateSaveReq(req);
|
||||||
|
|
||||||
|
TemplateV2Entity old = templateV2Mapper.getById(templateId);
|
||||||
|
if (old == null) {
|
||||||
|
return ApiResponse.fail("模板不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
int totalDurationMs = sumDurationMs(req.getSegments());
|
||||||
|
|
||||||
|
TemplateV2Entity entity = new TemplateV2Entity();
|
||||||
|
entity.setId(templateId);
|
||||||
|
entity.setScenicId(req.getScenicId());
|
||||||
|
entity.setName(req.getName());
|
||||||
|
entity.setVersion(old.getVersion() == null ? 1 : old.getVersion() + 1);
|
||||||
|
entity.setStatus(req.getStatus() == null ? normalizeStatus(old.getStatus()) : normalizeStatus(req.getStatus()));
|
||||||
|
entity.setOutputWidth(req.getOutputWidth());
|
||||||
|
entity.setOutputHeight(req.getOutputHeight());
|
||||||
|
entity.setOutputFps(req.getOutputFps());
|
||||||
|
entity.setBgmUrl(req.getBgmUrl());
|
||||||
|
entity.setTotalDurationMs(totalDurationMs);
|
||||||
|
templateV2Mapper.updateById(entity);
|
||||||
|
|
||||||
|
templateV2SegmentMapper.deleteByTemplateId(templateId);
|
||||||
|
saveSegments(templateId, req.getSegments());
|
||||||
|
return ApiResponse.success(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ApiResponse<Boolean> updateStatus(Long templateId, Integer status) {
|
||||||
|
Integer normalized = normalizeStatus(status);
|
||||||
|
int updated = templateV2Mapper.updateStatus(templateId, normalized);
|
||||||
|
if (updated <= 0) {
|
||||||
|
return ApiResponse.fail("更新失败:模板不存在或已删除");
|
||||||
|
}
|
||||||
|
return ApiResponse.success(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ApiResponse<Boolean> delete(Long templateId) {
|
||||||
|
int updated = templateV2Mapper.softDelete(templateId);
|
||||||
|
if (updated <= 0) {
|
||||||
|
return ApiResponse.fail("删除失败:模板不存在或已删除");
|
||||||
|
}
|
||||||
|
return ApiResponse.success(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<TemplateV2ListResp> toListResp(List<TemplateV2Entity> entities) {
|
||||||
|
if (entities == null || entities.isEmpty()) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
List<Long> scenicIds = entities.stream()
|
||||||
|
.map(TemplateV2Entity::getScenicId)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.distinct()
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
Map<Long, String> scenicNames = scenicRepository.batchGetScenicNames(scenicIds);
|
||||||
|
|
||||||
|
return entities.stream().map(item -> {
|
||||||
|
TemplateV2ListResp resp = new TemplateV2ListResp();
|
||||||
|
resp.setId(item.getId());
|
||||||
|
resp.setScenicId(item.getScenicId());
|
||||||
|
resp.setScenicName(item.getScenicId() == null ? null : scenicNames.get(item.getScenicId()));
|
||||||
|
resp.setName(item.getName());
|
||||||
|
resp.setVersion(item.getVersion());
|
||||||
|
resp.setStatus(item.getStatus());
|
||||||
|
resp.setTotalDurationMs(item.getTotalDurationMs());
|
||||||
|
resp.setUpdateTime(item.getUpdateTime());
|
||||||
|
return resp;
|
||||||
|
}).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
private TemplateV2DetailResp toDetailResp(TemplateV2Entity template, List<TemplateV2SegmentEntity> segments) {
|
||||||
|
TemplateV2DetailResp resp = new TemplateV2DetailResp();
|
||||||
|
resp.setId(template.getId());
|
||||||
|
resp.setScenicId(template.getScenicId());
|
||||||
|
if (template.getScenicId() != null) {
|
||||||
|
Map<Long, String> scenicNames = scenicRepository.batchGetScenicNames(List.of(template.getScenicId()));
|
||||||
|
resp.setScenicName(scenicNames.get(template.getScenicId()));
|
||||||
|
}
|
||||||
|
resp.setName(template.getName());
|
||||||
|
resp.setVersion(template.getVersion());
|
||||||
|
resp.setStatus(template.getStatus());
|
||||||
|
resp.setOutputWidth(template.getOutputWidth());
|
||||||
|
resp.setOutputHeight(template.getOutputHeight());
|
||||||
|
resp.setOutputFps(template.getOutputFps());
|
||||||
|
resp.setBgmUrl(template.getBgmUrl());
|
||||||
|
resp.setTotalDurationMs(template.getTotalDurationMs());
|
||||||
|
resp.setCreateTime(template.getCreateTime());
|
||||||
|
resp.setUpdateTime(template.getUpdateTime());
|
||||||
|
|
||||||
|
List<TemplateV2DetailResp.Segment> segmentList = new ArrayList<>();
|
||||||
|
for (TemplateV2SegmentEntity segment : Objects.requireNonNullElse(segments, List.<TemplateV2SegmentEntity>of())) {
|
||||||
|
TemplateV2DetailResp.Segment seg = new TemplateV2DetailResp.Segment();
|
||||||
|
seg.setId(segment.getId());
|
||||||
|
seg.setSegmentIndex(segment.getSegmentIndex());
|
||||||
|
seg.setDurationMs(segment.getDurationMs());
|
||||||
|
seg.setSegmentType(segment.getSegmentType());
|
||||||
|
seg.setSourceType(segment.getSourceType());
|
||||||
|
seg.setSourceRef(segment.getSourceRef());
|
||||||
|
seg.setOnlyIfExpr(parseJsonOrNull(segment.getOnlyIfExpr()));
|
||||||
|
seg.setRenderSpec(parseJsonOrNull(segment.getRenderSpecJson()));
|
||||||
|
seg.setAudioSpec(parseJsonOrNull(segment.getAudioSpecJson()));
|
||||||
|
segmentList.add(seg);
|
||||||
|
}
|
||||||
|
resp.setSegments(segmentList);
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateSaveReq(TemplateV2SaveReq req) {
|
||||||
|
if (req.getSegments() == null || req.getSegments().isEmpty()) {
|
||||||
|
throw new BizException(400, "segments不能为空");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int normalizeStatus(Integer status) {
|
||||||
|
if (status == null) {
|
||||||
|
return STATUS_ENABLED;
|
||||||
|
}
|
||||||
|
if (status == STATUS_ENABLED || status == STATUS_DISABLED) {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
throw new BizException(400, "status仅支持0/1");
|
||||||
|
}
|
||||||
|
|
||||||
|
private int sumDurationMs(List<TemplateV2SaveReq.TemplateV2SegmentSaveReq> segments) {
|
||||||
|
int total = 0;
|
||||||
|
for (TemplateV2SaveReq.TemplateV2SegmentSaveReq item : segments) {
|
||||||
|
total = Math.addExact(total, item.getDurationMs());
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveSegments(Long templateId, List<TemplateV2SaveReq.TemplateV2SegmentSaveReq> segments) {
|
||||||
|
List<TemplateV2SegmentEntity> entities = new ArrayList<>();
|
||||||
|
for (int index = 0; index < segments.size(); index++) {
|
||||||
|
TemplateV2SaveReq.TemplateV2SegmentSaveReq req = segments.get(index);
|
||||||
|
validateOnlyIfExpr(req.getOnlyIfExpr());
|
||||||
|
validateSegmentDefinition(req);
|
||||||
|
|
||||||
|
TemplateV2SegmentEntity entity = new TemplateV2SegmentEntity();
|
||||||
|
entity.setId(SnowFlakeUtil.getLongId());
|
||||||
|
entity.setTemplateId(templateId);
|
||||||
|
entity.setSegmentIndex(index);
|
||||||
|
entity.setDurationMs(req.getDurationMs());
|
||||||
|
entity.setSegmentType(req.getSegmentType());
|
||||||
|
entity.setSourceType(req.getSourceType());
|
||||||
|
entity.setSourceRef(req.getSourceRef());
|
||||||
|
entity.setOnlyIfExpr(writeJsonOrNull(req.getOnlyIfExpr()));
|
||||||
|
entity.setRenderSpecJson(writeJsonOrNull(req.getRenderSpec()));
|
||||||
|
entity.setAudioSpecJson(writeJsonOrNull(req.getAudioSpec()));
|
||||||
|
entities.add(entity);
|
||||||
|
}
|
||||||
|
if (!entities.isEmpty()) {
|
||||||
|
templateV2SegmentMapper.batchInsert(entities);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateSegmentDefinition(TemplateV2SaveReq.TemplateV2SegmentSaveReq req) {
|
||||||
|
TemplateV2SegmentType segmentType = TemplateV2SegmentType.parse(req.getSegmentType());
|
||||||
|
if (segmentType == null) {
|
||||||
|
throw new BizException(400, "segmentType仅支持FIXED/RENDER");
|
||||||
|
}
|
||||||
|
TemplateV2SourceType sourceType = TemplateV2SourceType.parse(req.getSourceType());
|
||||||
|
if (sourceType == null) {
|
||||||
|
throw new BizException(400, "sourceType仅支持SLOT/FIXED_URL");
|
||||||
|
}
|
||||||
|
if (segmentType == TemplateV2SegmentType.FIXED && sourceType != TemplateV2SourceType.FIXED_URL) {
|
||||||
|
throw new BizException(400, "FIXED片段仅支持sourceType=FIXED_URL");
|
||||||
|
}
|
||||||
|
if (segmentType == TemplateV2SegmentType.RENDER && sourceType != TemplateV2SourceType.SLOT) {
|
||||||
|
throw new BizException(400, "RENDER片段仅支持sourceType=SLOT");
|
||||||
|
}
|
||||||
|
if (sourceType == TemplateV2SourceType.FIXED_URL && !isHttpUrl(req.getSourceRef())) {
|
||||||
|
throw new BizException(400, "sourceRef必须是http/https URL");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateOnlyIfExpr(JsonNode onlyIfExpr) {
|
||||||
|
if (onlyIfExpr == null || onlyIfExpr.isNull()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onlyIfExpressionParser.parse(onlyIfExpr);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String writeJsonOrNull(JsonNode node) {
|
||||||
|
if (node == null || node.isNull()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return objectMapper.writeValueAsString(node);
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
throw new BizException(400, "JSON序列化失败:" + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private JsonNode parseJsonOrNull(String json) {
|
||||||
|
if (json == null || json.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return objectMapper.readTree(json);
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isHttpUrl(String url) {
|
||||||
|
if (url == null || url.isBlank()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
URI uri = URI.create(url);
|
||||||
|
String scheme = uri.getScheme();
|
||||||
|
return "http".equalsIgnoreCase(scheme) || "https".equalsIgnoreCase(scheme);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.ycwl.basic.template.service.onlyif;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class AndExpression implements OnlyIfExpression {
|
||||||
|
private final List<OnlyIfExpression> items;
|
||||||
|
|
||||||
|
public AndExpression(List<OnlyIfExpression> items) {
|
||||||
|
this.items = items;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean evaluate(Map<String, Integer> slotCounts) {
|
||||||
|
for (OnlyIfExpression item : items) {
|
||||||
|
if (!item.evaluate(slotCounts)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<OnlyIfExpression> getItems() {
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.ycwl.basic.template.service.onlyif;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class CountGteExpression implements OnlyIfExpression {
|
||||||
|
private final String slotKey;
|
||||||
|
private final int value;
|
||||||
|
|
||||||
|
public CountGteExpression(String slotKey, int value) {
|
||||||
|
this.slotKey = slotKey;
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean evaluate(Map<String, Integer> slotCounts) {
|
||||||
|
if (slotCounts == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return slotCounts.getOrDefault(slotKey, 0) >= value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSlotKey() {
|
||||||
|
return slotKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getValue() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.ycwl.basic.template.service.onlyif;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class ExistsExpression implements OnlyIfExpression {
|
||||||
|
private final String slotKey;
|
||||||
|
|
||||||
|
public ExistsExpression(String slotKey) {
|
||||||
|
this.slotKey = slotKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean evaluate(Map<String, Integer> slotCounts) {
|
||||||
|
if (slotCounts == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return slotCounts.getOrDefault(slotKey, 0) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSlotKey() {
|
||||||
|
return slotKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.ycwl.basic.template.service.onlyif;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class NotExpression implements OnlyIfExpression {
|
||||||
|
private final OnlyIfExpression expr;
|
||||||
|
|
||||||
|
public NotExpression(OnlyIfExpression expr) {
|
||||||
|
this.expr = expr;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean evaluate(Map<String, Integer> slotCounts) {
|
||||||
|
return !expr.evaluate(slotCounts);
|
||||||
|
}
|
||||||
|
|
||||||
|
public OnlyIfExpression getExpr() {
|
||||||
|
return expr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.ycwl.basic.template.service.onlyif;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* only_if 受限表达式(基于 slotKey 的数量判断)
|
||||||
|
*/
|
||||||
|
public interface OnlyIfExpression {
|
||||||
|
boolean evaluate(Map<String, Integer> slotCounts);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
package com.ycwl.basic.template.service.onlyif;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.ycwl.basic.exception.BizException;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* only_if 表达式解析器(JSON AST):
|
||||||
|
* <pre>
|
||||||
|
* {"op":"exists","slotKey":"device:123"}
|
||||||
|
* {"op":"count_gte","slotKey":"P:123","value":2}
|
||||||
|
* {"op":"and","items":[...]}
|
||||||
|
* {"op":"or","items":[...]}
|
||||||
|
* {"op":"not","expr":{...}}
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class OnlyIfExpressionParser {
|
||||||
|
|
||||||
|
public OnlyIfExpression parse(JsonNode node) {
|
||||||
|
if (node == null || node.isNull()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!node.isObject()) {
|
||||||
|
throw new BizException(400, "onlyIfExpr必须是JSON对象");
|
||||||
|
}
|
||||||
|
String op = text(node, "op");
|
||||||
|
if (op == null) {
|
||||||
|
op = text(node, "type");
|
||||||
|
}
|
||||||
|
if (op == null) {
|
||||||
|
throw new BizException(400, "onlyIfExpr缺少op字段");
|
||||||
|
}
|
||||||
|
|
||||||
|
return switch (op.toLowerCase()) {
|
||||||
|
case "exists" -> new ExistsExpression(requiredText(node, "slotKey"));
|
||||||
|
case "count_gte" -> new CountGteExpression(requiredText(node, "slotKey"), requiredInt(node, "value", "n"));
|
||||||
|
case "and" -> new AndExpression(parseItems(node));
|
||||||
|
case "or" -> new OrExpression(parseItems(node));
|
||||||
|
case "not" -> new NotExpression(parse(requiredNode(node, "expr", "item")));
|
||||||
|
default -> throw new BizException(400, "onlyIfExpr不支持的op: " + op);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<OnlyIfExpression> parseItems(JsonNode node) {
|
||||||
|
JsonNode itemsNode = node.get("items");
|
||||||
|
if (itemsNode == null) {
|
||||||
|
itemsNode = node.get("children");
|
||||||
|
}
|
||||||
|
if (itemsNode == null || !itemsNode.isArray() || itemsNode.isEmpty()) {
|
||||||
|
throw new BizException(400, "onlyIfExpr的items必须是非空数组");
|
||||||
|
}
|
||||||
|
List<OnlyIfExpression> items = new ArrayList<>();
|
||||||
|
for (JsonNode item : itemsNode) {
|
||||||
|
items.add(parse(item));
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String requiredText(JsonNode node, String fieldName) {
|
||||||
|
String value = text(node, fieldName);
|
||||||
|
if (value == null || value.isBlank()) {
|
||||||
|
throw new BizException(400, "onlyIfExpr缺少字段: " + fieldName);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int requiredInt(JsonNode node, String... fieldNames) {
|
||||||
|
for (String fieldName : fieldNames) {
|
||||||
|
JsonNode valueNode = node.get(fieldName);
|
||||||
|
if (valueNode != null && valueNode.isNumber()) {
|
||||||
|
int value = valueNode.asInt();
|
||||||
|
if (value < 0) {
|
||||||
|
throw new BizException(400, "onlyIfExpr字段" + fieldName + "不能为负数");
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new BizException(400, "onlyIfExpr缺少字段: value");
|
||||||
|
}
|
||||||
|
|
||||||
|
private JsonNode requiredNode(JsonNode node, String... fieldNames) {
|
||||||
|
for (String fieldName : fieldNames) {
|
||||||
|
JsonNode valueNode = node.get(fieldName);
|
||||||
|
if (valueNode != null && !valueNode.isNull()) {
|
||||||
|
return valueNode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new BizException(400, "onlyIfExpr缺少字段: expr");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String text(JsonNode node, String fieldName) {
|
||||||
|
JsonNode valueNode = node.get(fieldName);
|
||||||
|
if (valueNode == null || valueNode.isNull() || !valueNode.isTextual()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return valueNode.asText();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.ycwl.basic.template.service.onlyif;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class OrExpression implements OnlyIfExpression {
|
||||||
|
private final List<OnlyIfExpression> items;
|
||||||
|
|
||||||
|
public OrExpression(List<OnlyIfExpression> items) {
|
||||||
|
this.items = items;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean evaluate(Map<String, Integer> slotCounts) {
|
||||||
|
for (OnlyIfExpression item : items) {
|
||||||
|
if (item.evaluate(slotCounts)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<OnlyIfExpression> getItems() {
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
72
src/main/resources/mapper/TemplateV2Mapper.xml
Normal file
72
src/main/resources/mapper/TemplateV2Mapper.xml
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
<?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.ycwl.basic.mapper.TemplateV2Mapper">
|
||||||
|
<select id="list" resultType="com.ycwl.basic.template.model.entity.TemplateV2Entity">
|
||||||
|
select *
|
||||||
|
from template_v2
|
||||||
|
<where>
|
||||||
|
deleted = 0
|
||||||
|
<if test="scenicId != null">
|
||||||
|
and scenic_id = #{scenicId}
|
||||||
|
</if>
|
||||||
|
<if test="status != null">
|
||||||
|
and status = #{status}
|
||||||
|
</if>
|
||||||
|
<if test="name != null and name != ''">
|
||||||
|
and locate(#{name}, `name`) > 0
|
||||||
|
</if>
|
||||||
|
</where>
|
||||||
|
order by update_time desc, id desc
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="getById" resultType="com.ycwl.basic.template.model.entity.TemplateV2Entity">
|
||||||
|
select *
|
||||||
|
from template_v2
|
||||||
|
where id = #{id} and deleted = 0
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<insert id="insert">
|
||||||
|
insert into template_v2(
|
||||||
|
id, scenic_id, `name`, version, status,
|
||||||
|
output_width, output_height, output_fps,
|
||||||
|
bgm_url, total_duration_ms,
|
||||||
|
deleted, create_time, update_time
|
||||||
|
)
|
||||||
|
values (
|
||||||
|
#{id}, #{scenicId}, #{name}, #{version}, #{status},
|
||||||
|
#{outputWidth}, #{outputHeight}, #{outputFps},
|
||||||
|
#{bgmUrl}, #{totalDurationMs},
|
||||||
|
0, now(), now()
|
||||||
|
)
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
<update id="updateById">
|
||||||
|
update template_v2
|
||||||
|
<set>
|
||||||
|
update_time = now(),
|
||||||
|
<if test="scenicId != null">scenic_id = #{scenicId},</if>
|
||||||
|
<if test="name != null">`name` = #{name},</if>
|
||||||
|
<if test="version != null">version = #{version},</if>
|
||||||
|
<if test="status != null">status = #{status},</if>
|
||||||
|
<if test="outputWidth != null">output_width = #{outputWidth},</if>
|
||||||
|
<if test="outputHeight != null">output_height = #{outputHeight},</if>
|
||||||
|
<if test="outputFps != null">output_fps = #{outputFps},</if>
|
||||||
|
<if test="bgmUrl != null">bgm_url = #{bgmUrl},</if>
|
||||||
|
<if test="totalDurationMs != null">total_duration_ms = #{totalDurationMs},</if>
|
||||||
|
</set>
|
||||||
|
where id = #{id} and deleted = 0
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<update id="updateStatus">
|
||||||
|
update template_v2
|
||||||
|
set status = #{status}, update_time = now()
|
||||||
|
where id = #{id} and deleted = 0
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<update id="softDelete">
|
||||||
|
update template_v2
|
||||||
|
set deleted = 1, update_time = now()
|
||||||
|
where id = #{id} and deleted = 0
|
||||||
|
</update>
|
||||||
|
</mapper>
|
||||||
|
|
||||||
35
src/main/resources/mapper/TemplateV2SegmentMapper.xml
Normal file
35
src/main/resources/mapper/TemplateV2SegmentMapper.xml
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?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.ycwl.basic.mapper.TemplateV2SegmentMapper">
|
||||||
|
<select id="listByTemplateId" resultType="com.ycwl.basic.template.model.entity.TemplateV2SegmentEntity">
|
||||||
|
select *
|
||||||
|
from template_v2_segment
|
||||||
|
where template_id = #{templateId}
|
||||||
|
order by segment_index
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<delete id="deleteByTemplateId">
|
||||||
|
delete from template_v2_segment where template_id = #{templateId}
|
||||||
|
</delete>
|
||||||
|
|
||||||
|
<insert id="batchInsert">
|
||||||
|
insert into template_v2_segment(
|
||||||
|
id, template_id, segment_index,
|
||||||
|
duration_ms, segment_type,
|
||||||
|
source_type, source_ref,
|
||||||
|
only_if_expr, render_spec_json, audio_spec_json,
|
||||||
|
create_time, update_time
|
||||||
|
)
|
||||||
|
values
|
||||||
|
<foreach collection="segments" item="item" separator=",">
|
||||||
|
(
|
||||||
|
#{item.id}, #{item.templateId}, #{item.segmentIndex},
|
||||||
|
#{item.durationMs}, #{item.segmentType},
|
||||||
|
#{item.sourceType}, #{item.sourceRef},
|
||||||
|
#{item.onlyIfExpr}, #{item.renderSpecJson}, #{item.audioSpecJson},
|
||||||
|
now(), now()
|
||||||
|
)
|
||||||
|
</foreach>
|
||||||
|
</insert>
|
||||||
|
</mapper>
|
||||||
|
|
||||||
Reference in New Issue
Block a user