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