16 changed files with 652 additions and 71 deletions
@ -0,0 +1,17 @@ |
|||
package com.youlai.boot.mini.mapper; |
|||
|
|||
import com.youlai.boot.mini.model.entity.MiniUserSubscribe; |
|||
import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
|||
import org.apache.ibatis.annotations.Mapper; |
|||
import org.apache.ibatis.annotations.Select; |
|||
|
|||
/** |
|||
* 用户订阅状态表 Mapper 接口 |
|||
* |
|||
* @author jwy |
|||
* @since |
|||
*/ |
|||
public interface MiniUserSubscribeMapper extends BaseMapper<MiniUserSubscribe> { |
|||
|
|||
String getOpenidByUserId(Long aLong); |
|||
} |
|||
@ -0,0 +1,74 @@ |
|||
package com.youlai.boot.mini.model.entity; |
|||
|
|||
import com.baomidou.mybatisplus.annotation.*; |
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import lombok.Getter; |
|||
import lombok.Setter; |
|||
import lombok.ToString; |
|||
import lombok.experimental.Accessors; |
|||
|
|||
import java.io.Serializable; |
|||
import java.util.Date; |
|||
import com.fasterxml.jackson.annotation.JsonFormat; |
|||
|
|||
@Getter |
|||
@Setter |
|||
@ToString |
|||
@Accessors(chain = true) |
|||
@TableName("mini_user_subscribe") |
|||
@Schema(description = "用户订阅状态表") |
|||
public class MiniUserSubscribe implements Serializable { |
|||
|
|||
@TableId(value = "id", type = IdType.AUTO) |
|||
@Schema(description = "") |
|||
private Long id; |
|||
|
|||
|
|||
@TableField("uuid") |
|||
@Schema(description = "uuid唯一标识,前后端用这个进行数据交互") |
|||
private String uuid; |
|||
|
|||
@TableField("user_id") |
|||
@Schema(description = "用户id") |
|||
private Long userId; |
|||
|
|||
@TableField("template_id") |
|||
@Schema(description = "订阅消息模板ID") |
|||
private String templateId; |
|||
|
|||
@TableField("status") |
|||
@Schema(description = "授权状态:0=拒绝 1=同意一次 2=总是同意") |
|||
private Integer status; |
|||
|
|||
@TableField("create_time") |
|||
@Schema(description = "创建时间") |
|||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") |
|||
private Date createTime; |
|||
|
|||
@TableField("create_timestamp") |
|||
@Schema(description = "创建时间毫秒级时间戳") |
|||
private Long createTimestamp; |
|||
|
|||
@TableField("create_by") |
|||
@Schema(description = "创建人ID") |
|||
private Long createBy; |
|||
|
|||
@TableField("update_time") |
|||
@Schema(description = "更新时间") |
|||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") |
|||
private Date updateTime; |
|||
|
|||
@TableField("update_timestamp") |
|||
@Schema(description = "更新时间毫秒级时间戳") |
|||
private Long updateTimestamp; |
|||
|
|||
@TableField("update_by") |
|||
@Schema(description = "修改人ID") |
|||
private Long updateBy; |
|||
|
|||
@TableField("is_deleted") |
|||
@Schema(description = "逻辑删除标识(0-未删除 1-已删除)") |
|||
private Boolean deleted; |
|||
|
|||
|
|||
} |
|||
@ -0,0 +1,46 @@ |
|||
package com.youlai.boot.mini.model.enums; |
|||
|
|||
import com.fasterxml.jackson.annotation.JsonCreator; |
|||
import com.fasterxml.jackson.annotation.JsonValue; |
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
|
|||
|
|||
@Schema(description = "微信订阅消息授权状态") |
|||
public enum SubscribeStatusEnum { |
|||
|
|||
REJECT(0, "拒绝授权"), |
|||
ONCE(1, "同意一次"), |
|||
ALWAYS(2, "总是同意"); |
|||
|
|||
private final Integer value; |
|||
private final String desc; |
|||
|
|||
SubscribeStatusEnum(Integer value, String desc) { |
|||
this.value = value; |
|||
this.desc = desc; |
|||
} |
|||
|
|||
@JsonValue |
|||
public Integer getValue() { |
|||
return value; |
|||
} |
|||
|
|||
public String getDesc() { |
|||
return desc; |
|||
} |
|||
|
|||
@JsonCreator |
|||
public static SubscribeStatusEnum from(Integer value) { |
|||
if (value == null) return null; |
|||
for (SubscribeStatusEnum e : values()) { |
|||
if (e.value.equals(value)) { |
|||
return e; |
|||
} |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
public static boolean contains(Integer value) { |
|||
return from(value) != null; |
|||
} |
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
package com.youlai.boot.mini.model.form; |
|||
|
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import jakarta.validation.constraints.NotBlank; |
|||
import jakarta.validation.constraints.NotNull; |
|||
import lombok.Data; |
|||
|
|||
/** |
|||
* 上报用户订阅授权请求 |
|||
*/ |
|||
@Data |
|||
@Schema(description = "上报用户订阅授权请求") |
|||
public class WxSubscribeAuthForm { |
|||
|
|||
@NotBlank(message = "用户ID不能为空") |
|||
@Schema(description = "用户ID", requiredMode = Schema.RequiredMode.REQUIRED) |
|||
private Long userId; |
|||
|
|||
@NotBlank(message = "模板ID不能为空") |
|||
@Schema(description = "订阅消息模板ID", requiredMode = Schema.RequiredMode.REQUIRED) |
|||
private String templateId; |
|||
|
|||
@NotNull(message = "授权状态不能为空") |
|||
@Schema(description = "授权状态:0=拒绝 1=同意一次 2=总是同意", requiredMode = Schema.RequiredMode.REQUIRED) |
|||
private Integer status; |
|||
} |
|||
@ -0,0 +1,33 @@ |
|||
package com.youlai.boot.mini.model.form; |
|||
|
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import jakarta.validation.constraints.NotBlank; |
|||
import jakarta.validation.constraints.NotNull; |
|||
import lombok.Data; |
|||
import java.util.Map; |
|||
|
|||
/** |
|||
* 发送订阅消息请求 |
|||
*/ |
|||
@Data |
|||
@Schema(description = "发送订阅消息请求") |
|||
public class WxSubscribeSendForm { |
|||
|
|||
@NotBlank(message = "用户ID不能为空") |
|||
@Schema(description = "用户ID", requiredMode = Schema.RequiredMode.REQUIRED) |
|||
private Long userId; |
|||
|
|||
@NotBlank(message = "模板ID不能为空") |
|||
@Schema(description = "订阅消息模板ID", requiredMode = Schema.RequiredMode.REQUIRED) |
|||
private String templateId; |
|||
|
|||
@Schema(description = "跳转页面,例:/pages/result/index?id=123") |
|||
private String page; |
|||
|
|||
@Schema(description = "跳转版本:developer=开发版 trial=体验版 formal=正式版,默认formal") |
|||
private String miniProgramState = "developer"; |
|||
|
|||
@NotNull(message = "模板参数不能为空") |
|||
@Schema(description = "模板参数,key是模板字段名,value是对应的值", requiredMode = Schema.RequiredMode.REQUIRED) |
|||
private Map<String, String> templateParams; |
|||
} |
|||
@ -0,0 +1,43 @@ |
|||
package com.youlai.boot.mini.model.vo; |
|||
|
|||
import com.fasterxml.jackson.annotation.JsonFormat; |
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import lombok.Data; |
|||
|
|||
import java.time.LocalDateTime; |
|||
import java.util.Date; |
|||
|
|||
/** |
|||
* 用户上传媒体返回VO |
|||
* |
|||
* @author youlai |
|||
*/ |
|||
@Data |
|||
@Schema(description = "用户上传媒体返回VO") |
|||
public class UserUploadMediaVO { |
|||
|
|||
@Schema(description = "媒体ID") |
|||
private Long id; |
|||
|
|||
@Schema(description = "媒体类型:image-图片,video-视频") |
|||
private String mediaType; |
|||
|
|||
@Schema(description = "媒体访问地址") |
|||
private String sourceUrl; |
|||
|
|||
@Schema(description = "缩略图地址(视频才有)") |
|||
private String thumbnailUrl; |
|||
|
|||
@Schema(description = "视频时长(秒,视频才有)") |
|||
private Integer duration; |
|||
|
|||
@Schema(description = "图片宽度") |
|||
private Integer width; |
|||
|
|||
@Schema(description = "图片高度") |
|||
private Integer height; |
|||
|
|||
@Schema(description = "上传时间") |
|||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") |
|||
private Date createTime; |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
package com.youlai.boot.mini.model.vo; |
|||
|
|||
import com.fasterxml.jackson.annotation.JsonProperty; |
|||
import lombok.Data; |
|||
|
|||
/** |
|||
* 微信接口通用返回 |
|||
*/ |
|||
@Data |
|||
public class WxApiResponse { |
|||
@JsonProperty("errcode") |
|||
private Integer errcode; |
|||
|
|||
@JsonProperty("errmsg") |
|||
private String errmsg; |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
package com.youlai.boot.mini.service; |
|||
|
|||
import com.youlai.boot.mini.model.form.WxSubscribeAuthForm; |
|||
import com.youlai.boot.mini.model.form.WxSubscribeSendForm; |
|||
|
|||
/** |
|||
* 微信订阅消息服务 |
|||
*/ |
|||
public interface WxSubscribeService { |
|||
|
|||
void reportAuth(WxSubscribeAuthForm form); |
|||
|
|||
Boolean sendMessage(WxSubscribeSendForm form); |
|||
} |
|||
@ -0,0 +1,147 @@ |
|||
package com.youlai.boot.mini.service.impl; |
|||
|
|||
import cn.binarywang.wx.miniapp.api.WxMaService; |
|||
import cn.hutool.core.util.StrUtil; |
|||
import cn.hutool.json.JSONUtil; |
|||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
|||
import com.youlai.boot.common.exception.BusinessException; |
|||
import com.youlai.boot.common.exception.MsgException; |
|||
import com.youlai.boot.mini.model.enums.SubscribeStatusEnum; |
|||
import com.youlai.boot.mini.mapper.MiniUserSubscribeMapper; |
|||
import com.youlai.boot.mini.model.entity.MiniUserSubscribe; |
|||
import com.youlai.boot.mini.model.form.WxSubscribeAuthForm; |
|||
import com.youlai.boot.mini.model.form.WxSubscribeSendForm; |
|||
import com.youlai.boot.mini.model.vo.WxApiResponse; |
|||
import com.youlai.boot.mini.service.WxSubscribeService; |
|||
|
|||
import lombok.RequiredArgsConstructor; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.stereotype.Service; |
|||
import org.springframework.transaction.annotation.Transactional; |
|||
|
|||
import java.time.LocalDateTime; |
|||
import java.util.*; |
|||
|
|||
/** |
|||
* 微信订阅消息服务实现 |
|||
*/ |
|||
@Slf4j |
|||
@Service |
|||
@RequiredArgsConstructor |
|||
public class WxSubscribeServiceImpl implements WxSubscribeService { |
|||
|
|||
private final WxMaService wxMaService; |
|||
private final MiniUserSubscribeMapper userSubscribeMapper; |
|||
|
|||
@Override |
|||
@Transactional(rollbackFor = Exception.class) |
|||
public void reportAuth(WxSubscribeAuthForm form) { |
|||
try { |
|||
// 查询用户openid
|
|||
String openid = userSubscribeMapper.getOpenidByUserId(form.getUserId()); |
|||
if (StrUtil.isBlank(openid)) { |
|||
throw new MsgException("用户未绑定微信"); |
|||
} |
|||
long currentTime = System.currentTimeMillis(); |
|||
|
|||
// 查询是否已有授权记录
|
|||
MiniUserSubscribe exist = userSubscribeMapper.selectOne(new LambdaQueryWrapper<MiniUserSubscribe>() |
|||
.eq(MiniUserSubscribe::getUserId, form.getUserId()) |
|||
.eq(MiniUserSubscribe::getTemplateId, form.getTemplateId()) |
|||
.eq(MiniUserSubscribe::getDeleted, 0) |
|||
.last("limit 1")); |
|||
|
|||
if (exist != null) { |
|||
exist.setStatus(form.getStatus()); |
|||
exist.setUpdateTime(new Date()); |
|||
exist.setUpdateTimestamp(currentTime); |
|||
userSubscribeMapper.updateById(exist); |
|||
} else { |
|||
MiniUserSubscribe subscribe = new MiniUserSubscribe(); |
|||
subscribe.setUserId(form.getUserId()); |
|||
subscribe.setTemplateId(form.getTemplateId()); |
|||
subscribe.setStatus(form.getStatus()); |
|||
subscribe.setCreateTime(new Date()); |
|||
subscribe.setCreateTimestamp(currentTime); |
|||
subscribe.setUpdateTime(new Date()); |
|||
subscribe.setUpdateTimestamp(currentTime); |
|||
subscribe.setDeleted(false); |
|||
userSubscribeMapper.insert(subscribe); |
|||
} |
|||
log.info("用户{}订阅授权上报成功,模板ID:{},状态:{}", form.getUserId(), form.getTemplateId(), form.getStatus()); |
|||
} catch (Exception e) { |
|||
log.error("上报用户订阅授权失败,请求参数:{}", JSONUtil.toJsonStr(form), e); |
|||
throw new MsgException("上报授权状态失败"); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
@Transactional(rollbackFor = Exception.class) |
|||
public Boolean sendMessage(WxSubscribeSendForm form) { |
|||
log.info("开始发送订阅消息,请求参数:{}", JSONUtil.toJsonStr(form)); |
|||
try { |
|||
// 1. 查询用户openid
|
|||
String openid = userSubscribeMapper.getOpenidByUserId(form.getUserId()); |
|||
if (StrUtil.isBlank(openid)) { |
|||
log.error("用户{}未绑定微信openid,无法发送订阅消息", form.getUserId()); |
|||
return false; |
|||
} |
|||
|
|||
// 2. 构建微信订阅消息参数
|
|||
List<Map<String, String>> dataList = new ArrayList<>(); |
|||
for (Map.Entry<String, String> entry : form.getTemplateParams().entrySet()) { |
|||
Map<String, String> dataMap = new HashMap<>(); |
|||
dataMap.put("name", entry.getKey()); |
|||
dataMap.put("value", entry.getValue()); |
|||
dataList.add(dataMap); |
|||
} |
|||
|
|||
Map<String, Object> messageMap = new HashMap<>(); |
|||
messageMap.put("touser", openid); |
|||
messageMap.put("template_id", form.getTemplateId()); |
|||
messageMap.put("page", form.getPage()); |
|||
messageMap.put("miniprogram_state", form.getMiniProgramState()); |
|||
messageMap.put("lang", "zh_CN"); |
|||
messageMap.put("data", dataList); |
|||
|
|||
// 3. 调用微信接口发送
|
|||
String responseStr = wxMaService.post("https://api.weixin.qq.com/cgi-bin/message/subscribe/send", JSONUtil.toJsonStr(messageMap)); |
|||
WxApiResponse response = JSONUtil.toBean(responseStr, WxApiResponse.class); |
|||
log.info("微信订阅消息发送结果,用户:{},返回:{}", form.getUserId(), JSONUtil.toJsonStr(response)); |
|||
|
|||
// 4. 根据返回结果更新用户订阅状态
|
|||
MiniUserSubscribe subscribe = userSubscribeMapper.selectOne(new LambdaQueryWrapper<MiniUserSubscribe>() |
|||
.eq(MiniUserSubscribe::getUserId, form.getUserId()) |
|||
.eq(MiniUserSubscribe::getTemplateId, form.getTemplateId()) |
|||
.eq(MiniUserSubscribe::getDeleted, 0) |
|||
.last("limit 1") |
|||
); |
|||
|
|||
if (response.getErrcode() == 0) { |
|||
log.info("订阅消息发送成功,用户:{},模板ID:{}", form.getUserId(), form.getTemplateId()); |
|||
// 如果是同意一次的状态,发送后更新为未授权
|
|||
if (subscribe != null && SubscribeStatusEnum.ONCE.getValue().equals(subscribe.getStatus())) { |
|||
subscribe.setStatus(SubscribeStatusEnum.REJECT.getValue()); |
|||
subscribe.setUpdateTime(new Date()); |
|||
userSubscribeMapper.updateById(subscribe); |
|||
} |
|||
return true; |
|||
} else if (response.getErrcode() == 43101) { |
|||
// 用户未订阅,更新本地状态为拒绝
|
|||
log.warn("用户{}未订阅该模板消息,更新本地状态为拒绝", form.getUserId()); |
|||
if (subscribe != null) { |
|||
subscribe.setStatus(SubscribeStatusEnum.REJECT.getValue()); |
|||
subscribe.setUpdateTime(new Date()); |
|||
userSubscribeMapper.updateById(subscribe); |
|||
} |
|||
return false; |
|||
} else { |
|||
log.error("订阅消息发送失败,错误码:{},错误信息:{}", response.getErrcode(), response.getErrmsg()); |
|||
return false; |
|||
} |
|||
} catch (Exception e) { |
|||
log.error("发送订阅消息异常,请求参数:{}", JSONUtil.toJsonStr(form), e); |
|||
return false; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
<?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.youlai.boot.mini.mapper.MiniUserSubscribeMapper"> |
|||
|
|||
<select id="getOpenidByUserId" resultType="java.lang.String"> |
|||
SELECT openid |
|||
FROM sys_user_social |
|||
WHERE user_id = #{userId} |
|||
AND is_deleted = 0 |
|||
LIMIT 1 |
|||
</select> |
|||
|
|||
</mapper> |
|||
Loading…
Reference in new issue