Ver código fonte

Merge remote-tracking branch 'origin/dev' into dev

jinshihui 2 semanas atrás
pai
commit
7f21c6f24a

+ 49 - 0
nightFragrance-massage/src/main/java/com/ylx/message/controller/MessageController.java

@@ -0,0 +1,49 @@
+package com.ylx.message.controller;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.ylx.common.core.domain.R;
+import com.ylx.message.domain.dto.MessageDetailDTO;
+import com.ylx.message.domain.vo.MessageCategoryVO;
+import com.ylx.message.domain.vo.MessageDetailVO;
+import com.ylx.message.service.IMessageService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.List;
+
+@RestController
+@RequestMapping("/message")
+@Api(tags = {"客户端消息"})
+@Slf4j
+@PreAuthorize("@customerAuth.isCustomer()")
+public class MessageController {
+
+    @Resource
+    private IMessageService messageService;
+
+    @PutMapping("/read/{id}")
+    @ApiOperation("标记单条消息已读")
+    public R<Void> markRead(@PathVariable("id") Long id) {
+        this.messageService.markAsRead(id);
+        return R.ok();
+    }
+
+    @GetMapping("/category/list")
+    @ApiOperation("获取消息分类卡片列表")
+    public R<List<MessageCategoryVO>> getCategorySummaryList() {
+        List<MessageCategoryVO> list = messageService.getCategoryCards();
+        return R.ok(list);
+    }
+
+    @PostMapping("/category/detail/list")
+    @ApiOperation("获取指定分类下的消息详情列表")
+    public R<Page<MessageDetailVO>> getMessageListByCategory(@Validated @RequestBody MessageDetailDTO dto) {
+        Page<MessageDetailVO> data = this.messageService.getCategoryMessages(dto);
+        return R.ok(data);
+    }
+}

+ 34 - 0
nightFragrance-massage/src/main/java/com/ylx/message/domain/MessageContent.java

@@ -0,0 +1,34 @@
+package com.ylx.message.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.ylx.common.core.domain.BaseEntity;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@EqualsAndHashCode(callSuper = true)
+@Data
+public class MessageContent extends BaseEntity {
+
+    @ApiModelProperty("主键ID")
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    @ApiModelProperty("消息分类编码(对应sys_dict_data.dict_value)")
+    private String categoryCode;
+
+    @ApiModelProperty("触发事件标识")
+    private String triggerEvent;
+
+    @ApiModelProperty("消息模板(支持{变量}替换)")
+    private String contentTemplate;
+
+    @ApiModelProperty("跳转路由/链接")
+    private String jumpPage;
+
+    @TableLogic
+    @ApiModelProperty("是否删除 0=否,1=是")
+    private Integer isDelete;
+}

+ 35 - 0
nightFragrance-massage/src/main/java/com/ylx/message/domain/UserMessage.java

@@ -0,0 +1,35 @@
+package com.ylx.message.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.ylx.common.core.domain.BaseEntity;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@EqualsAndHashCode(callSuper = true)
+@Data
+public class UserMessage extends BaseEntity {
+
+    @ApiModelProperty("主键ID")
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    @ApiModelProperty("接收用户ID")
+    private Long userId;
+
+    @ApiModelProperty("关联message_content.id")
+    private Long messageContentId;
+
+    @ApiModelProperty("0-未读(显示小红点) 1-已读")
+    private Integer isRead;
+
+    @ApiModelProperty("变量替换后的最终消息内容")
+    private String personalizedContent;
+
+    @TableLogic
+    @ApiModelProperty("是否删除 0=否,1=是")
+    private Integer isDelete;
+
+}

+ 24 - 0
nightFragrance-massage/src/main/java/com/ylx/message/domain/dto/MessageDetailDTO.java

@@ -0,0 +1,24 @@
+package com.ylx.message.domain.dto;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.ylx.message.domain.vo.MessageDetailVO;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+import java.io.Serializable;
+
+@Data
+@ApiModel("获取指定分类下的消息详情列表DTO")
+public class MessageDetailDTO extends Page<MessageDetailVO> implements Serializable {
+    private static final long serialVersionUID = 6060809754656871634L;
+
+    @NotNull(message = "分类code不能为空")
+    @ApiModelProperty("分类code")
+    private String categoryCode;
+
+    @ApiModelProperty("用户ID")
+    private Long userId;
+
+}

+ 22 - 0
nightFragrance-massage/src/main/java/com/ylx/message/domain/vo/MessageCategoryVO.java

@@ -0,0 +1,22 @@
+package com.ylx.message.domain.vo;
+
+import lombok.Data;
+
+@Data
+public class MessageCategoryVO {
+
+    private String categoryCode;   // MARKETING, ORDER, SYSTEM
+
+    private String title;          // 营销通知 / 订单通知 / 系统通知
+
+    private String summary;        // 最新消息摘要(截断)
+
+    private String createTime;     // HH:mm 格式
+
+    private Integer isRead;        // 0=未读(整个分类有未读则标红)
+
+    private String jumpPage;       // 点击跳转的路由(如 "/marketing/list")
+
+    private Long latestMsgId;      // 最新消息ID,用于标记已读等操作
+
+}

+ 26 - 0
nightFragrance-massage/src/main/java/com/ylx/message/domain/vo/MessageDetailVO.java

@@ -0,0 +1,26 @@
+package com.ylx.message.domain.vo;
+
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Data
+public class MessageDetailVO {
+    /** 消息ID */
+    private Long id;
+
+    /** 消息标题 (来自 message_content.title) */
+    private String title;
+
+    /** 消息具体内容 (来自 user_message.personalized_content) */
+    private String content;
+
+    /** 创建时间 */
+    private LocalDateTime createTime;
+
+    /** 是否已读 (0:未读, 1:已读) - 用于控制右上角红点 */
+    private Integer isRead;
+
+    /** 跳转链接 (可选) */
+    private String jumpUrl;
+}

+ 63 - 0
nightFragrance-massage/src/main/java/com/ylx/message/enums/TriggerEventEnum.java

@@ -0,0 +1,63 @@
+package com.ylx.message.enums;
+
+import lombok.Getter;
+
+@Getter
+public enum TriggerEventEnum {
+
+    // ================== 1. 订单提醒 (Order) ==================
+
+    /** 1. 支付成功:订单支付成功 */
+    ORDER_PAY_SUCCESS(1,"order_pay_success"),
+
+    /** 2. 接单成功:商户确认接单 */
+    ORDER_ACCEPT_SUCCESS(2,"order_accept_success"),
+
+    /** 3. 商户已到达现场:商户到达现场 */
+    MERCHANT_ARRIVED(3,"merchant_arrived"),
+
+    /** 4. 订单完成:商户点击完成 */
+    ORDER_COMPLETE(4,"order_complete"),
+
+    /** 5. 待付款:订单提交未支付 */
+    ORDER_PENDING_PAYMENT(5,"order_pending_payment"),
+
+    /** 6. 商户未接单 */
+    MERCHANT_NOT_ACCEPT(6,"merchant_not_accept"),
+
+    /** 7. 订单失效 */
+    ORDER_INVALID(7,"order_invalid"),
+
+
+    // ================== 2. 售后提醒 (After-Sales) ==================
+
+    /** 1. 退款成功 */
+    REFUND_SUCCESS(8,"refund_success"),
+
+    /** 2. 拒绝退款 */
+    REFUND_REJECTED(9,"refund_rejected"),
+
+    /** 3. 退款中 */
+    REFUND_PROCESSING(10,"refund_processing"),
+
+
+    // ================== 3. 系统通知 (System) ==================
+
+    /** 优惠券到期提醒 */
+    COUPON_EXPIRE_WARN(11,"coupon_expire_warn"),
+
+
+    // ================== 4. 营销互动 (Marketing) ==================
+
+    /** 获得奖品 */
+    PRIZE_RECEIVED(12,"prize_received");
+
+    private final Integer code;
+    private final String desc;
+
+    TriggerEventEnum(Integer code, String desc) {
+        this.code = code;
+        this.desc = desc;
+    }
+
+}

+ 7 - 0
nightFragrance-massage/src/main/java/com/ylx/message/mapper/MessageContentMapper.java

@@ -0,0 +1,7 @@
+package com.ylx.message.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.ylx.message.domain.MessageContent;
+
+public interface MessageContentMapper extends BaseMapper<MessageContent> {
+}

+ 30 - 0
nightFragrance-massage/src/main/java/com/ylx/message/mapper/UserMessageMapper.java

@@ -0,0 +1,30 @@
+package com.ylx.message.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.ylx.message.domain.UserMessage;
+import com.ylx.message.domain.dto.MessageDetailDTO;
+import com.ylx.message.domain.vo.MessageCategoryVO;
+import com.ylx.message.domain.vo.MessageDetailVO;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+public interface UserMessageMapper extends BaseMapper<UserMessage> {
+
+    /**
+     * 查询用户各分类的最新一条消息(每类1条)
+     *
+     * @param userId 用户ID
+     * @return 消息分类卡片列表
+     */
+    List<MessageCategoryVO> selectLatestByCategory(@Param("userId") Long userId);
+
+    /**
+     * 获取指定分类下的消息列表(分页)
+     *
+     * @param dto 用户ID
+     * @return 消息详情列表
+     */
+    Page<MessageDetailVO> selectMessageListByCategory(Page<MessageDetailVO> page, @Param("dto") MessageDetailDTO dto);
+}

+ 19 - 0
nightFragrance-massage/src/main/java/com/ylx/message/service/IMessageService.java

@@ -0,0 +1,19 @@
+package com.ylx.message.service;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.ylx.message.domain.dto.MessageDetailDTO;
+import com.ylx.message.domain.vo.MessageCategoryVO;
+import com.ylx.message.domain.vo.MessageDetailVO;
+
+import java.util.List;
+import java.util.Map;
+
+public interface IMessageService {
+    void markAsRead(Long id);
+
+    List<MessageCategoryVO> getCategoryCards();
+
+    Page<MessageDetailVO> getCategoryMessages(MessageDetailDTO dto);
+
+    void sendMessage(Long userId, String triggerEvent, Map<String, Object> variables);
+}

+ 152 - 0
nightFragrance-massage/src/main/java/com/ylx/message/service/impl/MessageServiceImpl.java

@@ -0,0 +1,152 @@
+package com.ylx.message.service.impl;
+
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.ylx.common.core.domain.model.WxLoginUser;
+import com.ylx.common.exception.ServiceException;
+import com.ylx.common.utils.SecurityUtils;
+import com.ylx.message.domain.MessageContent;
+import com.ylx.message.domain.UserMessage;
+import com.ylx.message.domain.dto.MessageDetailDTO;
+import com.ylx.message.domain.vo.MessageCategoryVO;
+import com.ylx.message.domain.vo.MessageDetailVO;
+import com.ylx.message.mapper.MessageContentMapper;
+import com.ylx.message.mapper.UserMessageMapper;
+import com.ylx.message.service.IMessageService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.util.List;
+import java.util.Map;
+
+@Slf4j
+@Service
+public class MessageServiceImpl implements IMessageService {
+
+    @Resource
+    private MessageContentMapper messageContentMapper;
+    @Resource
+    private UserMessageMapper userMessageMapper;
+
+    /** 消息未读状态 */
+    private static final int READ_STATUS_UNREAD = 0;
+    /** 消息已读状态 */
+    private static final int READ_STATUS_READ = 1;
+    /** 未删除状态 */
+    private static final int NOT_DELETE = 0;
+
+    /**
+     * 🔴 获取用户未读消息数(小红点核心接口)
+     * 联合索引 idx_user_read(user_id, is_read) 保证高效查询
+     */
+    public long getUnreadCount(Long userId) {
+        return userMessageMapper.selectCount(
+                new LambdaQueryWrapper<UserMessage>()
+                        .eq(UserMessage::getUserId, userId)
+                        .eq(UserMessage::getIsRead, READ_STATUS_UNREAD)
+        );
+    }
+
+    /**
+     * 发送消息(由订单、营销等业务模块调用)
+     */
+    @Transactional(rollbackFor = Exception.class)
+    public void sendMessage(Long userId, String triggerEvent, Map<String, Object> variables) {
+        // 1. 查找消息配置
+        MessageContent config = messageContentMapper.selectOne(
+                new LambdaQueryWrapper<MessageContent>()
+                        .eq(MessageContent::getTriggerEvent, triggerEvent)
+                        .eq(MessageContent::getIsDelete, NOT_DELETE)
+        );
+        if (ObjectUtil.isNull(config)) {
+            log.warn("未找到触发事件[{}]对应的消息配置", triggerEvent);
+            return;
+        }
+
+        // 2. 渲染个性化内容
+        String content = renderTemplate(config.getContentTemplate(), variables);
+
+        // 3. 生成用户消息(默认未读,触发小红点)
+        UserMessage userMsg = new UserMessage();
+        userMsg.setUserId(userId);
+        userMsg.setMessageContentId(config.getId());
+        userMsg.setIsRead(READ_STATUS_UNREAD);
+        userMsg.setPersonalizedContent(content);
+        userMessageMapper.insert(userMsg);
+    }
+
+    /**
+     * 标记单条消息已读(点击消息时调用,消除对应小红点)
+     */
+    public void markAsRead(Long messageId) {
+
+        // 获取当前登录用户
+        WxLoginUser wxLoginUser = getCurrentWxLoginUser();
+
+        Long userId = Long.parseLong(wxLoginUser.getId());
+
+        userMessageMapper.update(null,
+                new LambdaUpdateWrapper<UserMessage>()
+                        .eq(UserMessage::getId, messageId)
+                        .eq(UserMessage::getUserId, userId) // 🔒 安全校验,防止越权操作他人消息
+                        .eq(UserMessage::getIsRead, READ_STATUS_UNREAD)       // 仅更新未读记录,避免重复更新
+                        .eq(UserMessage::getIsDelete, NOT_DELETE)
+                        .set(UserMessage::getIsRead, READ_STATUS_READ)
+        );
+    }
+
+    @Override
+    public List<MessageCategoryVO> getCategoryCards() {
+
+        // 获取当前登录用户
+        WxLoginUser wxLoginUser = getCurrentWxLoginUser();
+
+        Long userId = Long.parseLong(wxLoginUser.getId());
+        return userMessageMapper.selectLatestByCategory(userId);
+    }
+
+    @Override
+    public Page<MessageDetailVO> getCategoryMessages(MessageDetailDTO dto) {
+
+        // 获取当前登录用户
+        WxLoginUser wxLoginUser = getCurrentWxLoginUser();
+
+        Long userId = Long.parseLong(wxLoginUser.getId());
+
+        Page<MessageDetailVO> page = new Page<>(dto.getCurrent(), dto.getSize());
+        dto.setUserId(userId);
+        return this.userMessageMapper.selectMessageListByCategory(page, dto);
+    }
+
+    /**
+     * 模板变量替换
+     */
+    private String renderTemplate(String template, Map<String, Object> vars) {
+        if (StrUtil.isBlank(template) || vars == null || vars.isEmpty()) {
+            return template;
+        }
+
+        String result = template;
+        for (Map.Entry<String, Object> entry : vars.entrySet()) {
+            String key = entry.getKey();
+            Object value = entry.getValue();
+            // 安全转换为字符串:null → "", 非字符串对象调用 toString()
+            String strValue = value == null ? "" : String.valueOf(value);
+            result = result.replace("{" + key + "}", strValue);
+        }
+        return result;
+    }
+
+    private WxLoginUser getCurrentWxLoginUser() {
+        WxLoginUser loginUser = SecurityUtils.getWxLoginUser();
+        if (ObjectUtil.isNull(loginUser)) {
+            throw new ServiceException("用户未登录或登录已过期");
+        }
+        return loginUser;
+    }
+}

+ 6 - 0
nightFragrance-massage/src/main/resources/mapper/message/MessageContentMapper.xml

@@ -0,0 +1,6 @@
+<?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.ylx.message.mapper.MessageContentMapper">
+
+</mapper>
+

+ 63 - 0
nightFragrance-massage/src/main/resources/mapper/message/UserMessageMapper.xml

@@ -0,0 +1,63 @@
+<?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.ylx.message.mapper.UserMessageMapper">
+
+    <!-- 结果映射(可选,字段名与VO属性一致时可省略) -->
+    <resultMap id="CategoryCardResultMap" type="com.ylx.message.domain.vo.MessageCategoryVO">
+        <result column="category_code" property="categoryCode"/>
+        <result column="title"         property="title"/>
+        <result column="summary"       property="summary"/>
+        <result column="create_time"   property="createTime"/>
+        <result column="is_read"       property="isRead"/>
+        <result column="jump_page"     property="jumpPage"/>
+        <result column="msg_id"        property="latestMsgId"/>
+    </resultMap>
+
+    <!-- 核心查询:按分类取最新一条消息 -->
+    <select id="selectLatestByCategory" resultMap="CategoryCardResultMap">
+        SELECT
+            t.category_code,
+            t.title,
+            t.summary,
+            DATE_FORMAT(t.create_time, '%H:%i') AS create_time,
+            t.is_read,
+            t.jump_page,
+            t.msg_id
+        FROM (
+             SELECT
+                 mc.category_code,
+                 d.dict_label AS title,
+                 mc.jump_page,
+                 SUBSTRING(um.personalized_content, 1, 50) AS summary,
+                 um.create_time,
+                 um.is_read,
+                 um.id AS msg_id,
+                 ROW_NUMBER() OVER (PARTITION BY mc.category_code ORDER BY um.create_time DESC) AS rn
+             FROM user_message um
+             INNER JOIN message_content mc ON um.message_content_id = mc.id
+             LEFT JOIN sys_dict_data d ON d.dict_type = 'msg_category' AND d.dict_value = mc.category_code
+             WHERE um.user_id = #{userId}
+             AND um.is_delete = 0
+             AND mc.is_delete = 0
+         ) t
+        WHERE t.rn = 1
+        ORDER BY t.create_time DESC
+    </select>
+
+    <select id="selectMessageListByCategory" resultType="com.ylx.message.domain.vo.MessageDetailVO">
+        SELECT
+            um.id,
+            mc.title AS title,           <!-- 从配置表获取标题 -->
+            um.personalized_content AS content, <!-- 渲染后的内容 -->
+            um.create_time AS createTime,
+            um.is_read AS isRead,
+            mc.jump_page AS jumpUrl      <!-- 点击跳转地址 -->
+        FROM user_message um
+        INNER JOIN message_content mc ON um.message_content_id = mc.id
+        WHERE um.user_id = #{dto.userId}
+        AND um.is_delete = 0
+        AND mc.category_code = #{dto.categoryCode} <!-- 核心过滤条件 -->
+        ORDER BY um.create_time DESC
+    </select>
+</mapper>
+