feat(notification): 添加微信订阅消息配置管理及幂等授权功能

- 新增微信订阅消息配置管理控制器,支持模板、场景、事件映射配置
- 实现用户通知授权服务的幂等控制,避免前端重试导致授权次数虚增
- 添加微信订阅消息发送日志记录,用于幂等与排障
- 新增视频生成完成时的订阅消息触发功能
- 实现场景模板查询接口,返回用户授权余额信息
- 添加模板V2相关数据表映射器和实体类
- 集成微信订阅消息触发服务到任务完成流程中
This commit is contained in:
2026-01-01 17:53:59 +08:00
parent 81dc2f1b86
commit f1a2958251
61 changed files with 3655 additions and 9 deletions

View File

@@ -14,16 +14,25 @@ import java.util.List;
*/
@Data
public class NotificationAuthRecordReq {
/**
* 通知模板ID列表 - 支持批量授权
*/
@NotEmpty(message = "模板ID列表不能为空")
private List<String> templateIds;
/**
* 景区ID
*/
@NotNull(message = "景区ID不能为空")
private Long scenicId;
}
/**
* 前端幂等ID(可选)
* <p>
* 目的:避免前端重试导致授权次数虚增。
* 同一次用户授权动作(一次 requestSubscribeMessage)建议复用同一个 requestId。
* </p>
*/
private String requestId;
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}