Jelajahi Sumber

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

刘云鑫 1 Minggu lalu
induk
melakukan
e7ebbcb6df
32 mengubah file dengan 1646 tambahan dan 45 penghapusan
  1. 97 0
      alien-entity/src/main/java/shop/alien/entity/store/LifeUserPersonalizationSetting.java
  2. 60 0
      alien-entity/src/main/java/shop/alien/entity/store/LifeUserPushDevice.java
  3. 60 0
      alien-entity/src/main/java/shop/alien/entity/store/PushDeviceOwnerType.java
  4. 2 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/CommonCommentVo.java
  5. 69 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/MyMergedReviewItemVo.java
  6. 3 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/StoreClockInVo.java
  7. 2 0
      alien-entity/src/main/java/shop/alien/mapper/CommonCommentMapper.java
  8. 20 10
      alien-entity/src/main/java/shop/alien/mapper/LifeFansMapper.java
  9. 7 0
      alien-entity/src/main/java/shop/alien/mapper/LifeUserPersonalizationSettingMapper.java
  10. 7 0
      alien-entity/src/main/java/shop/alien/mapper/LifeUserPushDeviceMapper.java
  11. 1 1
      alien-entity/src/main/java/shop/alien/mapper/StoreClockInMapper.java
  12. 1 1
      alien-lawyer/src/main/java/shop/alien/lawyer/controller/AlipayZftCreateRecordController.java
  13. 3 3
      alien-lawyer/src/main/java/shop/alien/lawyer/service/AlipayZftCreateRecordService.java
  14. 10 7
      alien-lawyer/src/main/java/shop/alien/lawyer/service/impl/AlipayZftCreateRecordServiceImpl.java
  15. 47 0
      alien-store/src/main/java/shop/alien/store/config/UniPushProperties.java
  16. 38 0
      alien-store/src/main/java/shop/alien/store/controller/CommonRatingController.java
  17. 16 8
      alien-store/src/main/java/shop/alien/store/controller/LifeStoreController.java
  18. 84 0
      alien-store/src/main/java/shop/alien/store/controller/LifeUserPersonalizationSettingController.java
  19. 62 0
      alien-store/src/main/java/shop/alien/store/controller/LifeUserPushDeviceController.java
  20. 26 0
      alien-store/src/main/java/shop/alien/store/dto/LifeUserPushBindDto.java
  21. 8 0
      alien-store/src/main/java/shop/alien/store/service/CommonRatingService.java
  22. 129 2
      alien-store/src/main/java/shop/alien/store/service/LifeCommentService.java
  23. 27 12
      alien-store/src/main/java/shop/alien/store/service/LifeStoreService.java
  24. 46 0
      alien-store/src/main/java/shop/alien/store/service/LifeUserPersonalizationSettingService.java
  25. 34 0
      alien-store/src/main/java/shop/alien/store/service/LifeUserPushDeviceService.java
  26. 106 1
      alien-store/src/main/java/shop/alien/store/service/LifeUserService.java
  27. 82 0
      alien-store/src/main/java/shop/alien/store/service/UniPushCloudInvokeService.java
  28. 1 0
      alien-store/src/main/java/shop/alien/store/service/impl/AlipayZftOnboardingServiceImpl.java
  29. 119 0
      alien-store/src/main/java/shop/alien/store/service/impl/CommonRatingServiceImpl.java
  30. 279 0
      alien-store/src/main/java/shop/alien/store/service/impl/LifeUserPersonalizationSettingServiceImpl.java
  31. 194 0
      alien-store/src/main/java/shop/alien/store/service/impl/LifeUserPushDeviceServiceImpl.java
  32. 6 0
      alien-store/src/main/java/shop/alien/store/service/impl/StoreClockInServiceImpl.java

+ 97 - 0
alien-entity/src/main/java/shop/alien/entity/store/LifeUserPersonalizationSetting.java

@@ -0,0 +1,97 @@
+package shop.alien.entity.store;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * life_user 个性化设置
+ */
+@Data
+@JsonInclude
+@TableName("life_user_personalization_setting")
+@ApiModel(value = "LifeUserPersonalizationSetting", description = "用户个性化设置")
+public class LifeUserPersonalizationSetting implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "主键")
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    @ApiModelProperty(value = "用户id,对应 life_user.id")
+    @TableField("user_id")
+    private Integer userId;
+
+    @ApiModelProperty(value = "个性化推荐 0不推荐 1推荐")
+    @TableField("personalized_recommendation")
+    private Integer personalizedRecommendation;
+
+    @ApiModelProperty(value = "允许关注的人评论我 0不允许 1允许")
+    @TableField("only_followers_comment")
+    private Integer onlyFollowersComment;
+
+    @ApiModelProperty(value = "粉丝列表 0不可见 1可见")
+    @TableField("hide_fans_list")
+    private Integer hideFansList;
+
+    @ApiModelProperty(value = "关注列表 0不可见 1可见")
+    @TableField("hide_follow_list")
+    private Integer hideFollowList;
+
+    @ApiModelProperty(value = "接收消息 0不接收 1可接收")
+    @TableField("notify_receive_message")
+    private Integer notifyReceiveMessage;
+
+    @ApiModelProperty(value = "点赞 0不可点赞 1可点赞")
+    @TableField("notify_like")
+    private Integer notifyLike;
+
+    @ApiModelProperty(value = "关注 0不可关注 1可关注")
+    @TableField("notify_follow")
+    private Integer notifyFollow;
+
+    @ApiModelProperty(value = "评论 0不可评论 1可评论")
+    @TableField("notify_comment")
+    private Integer notifyComment;
+
+    @ApiModelProperty(value = "是否跟随系统字体 0不跟随 1跟随;为1时不更新 chatFontLevel")
+    @TableField("follow_system_font")
+    private Integer followSystemFont;
+
+    @ApiModelProperty(value = "字体档位 0~4,仅 followSystemFont=0 时可调整")
+    @TableField("chat_font_level")
+    private Integer chatFontLevel;
+
+    @ApiModelProperty(value = "允许自动刷新 0不允许 1允许")
+    @TableField("auto_refresh")
+    private Integer autoRefresh;
+
+    @ApiModelProperty(value = "删除标记 0未删除 1已删除")
+    @TableField("delete_flag")
+    @TableLogic
+    private Integer deleteFlag;
+
+    @ApiModelProperty(value = "创建时间")
+    @TableField(value = "created_time", fill = FieldFill.INSERT)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date createdTime;
+
+    @ApiModelProperty(value = "创建人ID")
+    @TableField("created_user_id")
+    private Integer createdUserId;
+
+    @ApiModelProperty(value = "修改时间")
+    @TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date updatedTime;
+
+    @ApiModelProperty(value = "修改人ID")
+    @TableField("updated_user_id")
+    private Integer updatedUserId;
+}

+ 60 - 0
alien-entity/src/main/java/shop/alien/entity/store/LifeUserPushDevice.java

@@ -0,0 +1,60 @@
+package shop.alien.entity.store;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 推送设备 cid 绑定;{@link #userId} 含义由 {@link #ownerType} 决定。
+ */
+@Data
+@JsonInclude
+@TableName("life_user_push_device")
+@ApiModel(value = "LifeUserPushDevice", description = "用户推送设备绑定")
+public class LifeUserPushDevice implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "主键")
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    @ApiModelProperty(value = "业务用户主键:owner_type=user 时为 life_user.id;store 为 store_user.id;lawyer 为 lawyer_user.id")
+    @TableField("user_id")
+    private Integer userId;
+
+    @ApiModelProperty(value = "归属类型:user / store / lawyer(与 JWT userType 一致)")
+    @TableField("owner_type")
+    private String ownerType;
+
+    @ApiModelProperty(value = "uni-push client id")
+    @TableField("push_client_id")
+    private String pushClientId;
+
+    @ApiModelProperty(value = "平台:ios、android 等")
+    @TableField("platform")
+    private String platform;
+
+    @ApiModelProperty(value = "DCloud appid,如 __UNI__xxxx")
+    @TableField("dcloud_app_id")
+    private String dcloudAppId;
+
+    @ApiModelProperty(value = "创建时间")
+    @TableField("created_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date createdTime;
+
+    @ApiModelProperty(value = "更新时间")
+    @TableField("updated_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date updatedTime;
+}

+ 60 - 0
alien-entity/src/main/java/shop/alien/entity/store/PushDeviceOwnerType.java

@@ -0,0 +1,60 @@
+package shop.alien.entity.store;
+
+/**
+ * 推送设备绑定归属类型,与 JWT 中 userType 及表 life_user_push_device.owner_type 一致。
+ * <ul>
+ *   <li>{@link #USER} — C 端 life_user.id</li>
+ *   <li>{@link #STORE} — 门店 store_user.id</li>
+ *   <li>{@link #LAWYER} — 律师 lawyer_user.id</li>
+ * </ul>
+ */
+public enum PushDeviceOwnerType {
+
+    USER("user"),
+    STORE("store"),
+    LAWYER("lawyer");
+
+    private final String code;
+
+    PushDeviceOwnerType(String code) {
+        this.code = code;
+    }
+
+    public String getCode() {
+        return code;
+    }
+
+    /**
+     * 根据登录 token 中的 userType 解析;不支持的类型返回 null。
+     */
+    public static PushDeviceOwnerType fromJwtUserType(String jwtUserType) {
+        if (jwtUserType == null || jwtUserType.trim().isEmpty()) {
+            return USER;
+        }
+        String t = jwtUserType.trim().toLowerCase();
+        switch (t) {
+            case "user":
+                return USER;
+            case "store":
+            case "merchant":
+                return STORE;
+            case "lawyer":
+                return LAWYER;
+            default:
+                return null;
+        }
+    }
+
+    public static PushDeviceOwnerType fromCode(String code) {
+        if (code == null || code.trim().isEmpty()) {
+            return null;
+        }
+        String c = code.trim().toLowerCase();
+        for (PushDeviceOwnerType v : values()) {
+            if (v.code.equals(c)) {
+                return v;
+            }
+        }
+        return null;
+    }
+}

+ 2 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/CommonCommentVo.java

@@ -24,4 +24,6 @@ public class CommonCommentVo extends CommonComment {
     private Integer commentCount;
     @ApiModelProperty(value = "评论用户手机号")
     private String headPhone;
+    @ApiModelProperty(value = "商铺名称(评论主体为商户时有效,comment_type=2)")
+    private String storeName;
 }

+ 69 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/MyMergedReviewItemVo.java

@@ -0,0 +1,69 @@
+package shop.alien.entity.store.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 「我的评价」融合列表单项:门店评价 + 律师订单评价
+ */
+@Data
+@ApiModel(value = "MyMergedReviewItemVo", description = "我的评价融合列表项")
+public class MyMergedReviewItemVo {
+
+    @ApiModelProperty(value = "类型:STORE-门店评价,LAWYER-律师订单评价", required = true)
+    private String itemType;
+
+    @ApiModelProperty(value = "门店评价主键(itemType=STORE 时有值)")
+    private Long storeRatingId;
+
+    @ApiModelProperty(value = "律师评价主键(itemType=LAWYER 时有值)")
+    private Integer lawyerReviewId;
+
+    @ApiModelProperty(value = "发布时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date publishTime;
+
+    @ApiModelProperty(value = "总评分/核心评分")
+    private Double score;
+
+    @ApiModelProperty(value = "文字内容")
+    private String content;
+
+    @ApiModelProperty(value = "图片/视频等媒体 URL 列表")
+    private List<String> mediaUrls;
+
+    @ApiModelProperty(value = "门店 id(STORE)")
+    private Integer storeId;
+
+    @ApiModelProperty(value = "门店名称(STORE)")
+    private String storeName;
+
+    @ApiModelProperty(value = "门店图标/头图(STORE,可能为空)")
+    private String storeIcon;
+
+    @ApiModelProperty(value = "律师姓名(LAWYER)")
+    private String lawyerName;
+
+    @ApiModelProperty(value = "律师头像(LAWYER)")
+    private String lawyerAvatar;
+
+    @ApiModelProperty(value = "律所名称(LAWYER)")
+    private String lawyerFirm;
+
+    @ApiModelProperty(value = "评价标签文案,如「我给出超赞」(LAWYER)")
+    private String ratingTag;
+
+    @ApiModelProperty(value = "律师用户 id(LAWYER)")
+    private Integer lawyerUserId;
+
+    @ApiModelProperty(value = "关联订单 id(LAWYER)")
+    private Integer orderId;
+
+    @ApiModelProperty(value = "门店评价业务类型 common_rating.business_type(STORE)")
+    private Integer businessType;
+}

+ 3 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/StoreClockInVo.java

@@ -15,6 +15,9 @@ public class StoreClockInVo extends StoreClockIn {
     @ApiModelProperty(value = "商家姓名")
     private String storeName;
 
+    @ApiModelProperty(value = "门店简介")
+    private String storeBlurb;
+
     @ApiModelProperty(value = "商家位置")
     private String storePosition;
 

+ 2 - 0
alien-entity/src/main/java/shop/alien/mapper/CommonCommentMapper.java

@@ -36,9 +36,11 @@ public interface CommonCommentMapper extends BaseMapper<CommonComment> {
             "IF(cc.comment_type = 1, lu.user_image, su.head_img) AS headImg, " +
             "IF(cc.comment_type = 1, lu.user_name, su.nick_name) AS headName, " +
             "IF(cc.comment_type = 1, lu.user_phone, su.phone) AS headPhone, " +
+            "IF(cc.comment_type = 2, si.store_name, NULL) AS storeName, " +
             "IF(llr.dianzan_id IS NULL, '0', '1') AS isLike " +
             "FROM common_comment cc " +
             "LEFT JOIN life_user lu ON cc.user_id = lu.id AND lu.delete_flag = 0 " +
+            "LEFT JOIN store_info si ON cc.merchant_id = si.id AND si.delete_flag = 0 " +
             "LEFT JOIN store_user su ON cc.merchant_id = su.store_id AND su.delete_flag = 0 " +
             "AND su.id = (SELECT MIN(su2.id) FROM store_user su2 WHERE su2.store_id = cc.merchant_id AND su2.delete_flag = 0) " +
             "LEFT JOIN life_like_record llr ON llr.huifu_id = cc.id " +

+ 20 - 10
alien-entity/src/main/java/shop/alien/mapper/LifeFansMapper.java

@@ -15,8 +15,9 @@ import java.util.List;
 @Mapper
 public interface LifeFansMapper extends BaseMapper<LifeFans> {
 
-    @Select("select MAX(foll.id) id, MAX(foll.name) name, MAX(foll.image) image, foll.phoneId, MAX(foll.blurb) blurb, MAX(foll.blockedType) blockedType, MAX(foll.blockedId) blockedId, MAX(foll.username) username, MAX(foll.accountBlurb) accountBlurb, MAX(foll.isMerchant) isMerchant, " +
-            "  MAX(lb.id) blackListid, MAX(if(isnull(fans.id), 0, 1)) isFollowMe, 1 as isFollowThis,  " +
+    @Select("<script>" +
+            "select MAX(foll.id) id, MAX(foll.name) name, MAX(foll.image) image, foll.phoneId, MAX(foll.blurb) blurb, MAX(foll.blockedType) blockedType, MAX(foll.blockedId) blockedId, MAX(foll.username) username, MAX(foll.accountBlurb) accountBlurb, MAX(foll.isMerchant) isMerchant, " +
+            "  MAX(lb.id) blackListid, MAX(if(isnull(fans.id), 0, 1)) isFollowMe, MAX(if(isnull(fans_this.id), 0, 1)) isFollowThis, MAX(if(isnull(lb.id), '0', '1')) isBlocked,  " +
             "  (select count(1) from life_fans where fans_id= foll.phoneId and delete_flag =0) followNum, " +
             "  (select count(1) from life_fans where followed_id= foll.phoneId and delete_flag =0) fansNum from ( " +
             "    with follow as (   " +
@@ -31,16 +32,20 @@ public interface LifeFansMapper extends BaseMapper<LifeFans> {
             "    join store_info info on info.id = user.store_id " +
             "    left join store_img img on img.store_id = user.store_id and img.img_type = '10' and img.delete_flag = 0 " +
             "    where foll.flag = 'store' and user.delete_flag = 0 and info.delete_flag = 0 " +
+            "<if test=\"onlyStoreFollowed == false\">" +
             "    union " +
             "    select user.id, user.user_name name, user.user_image image, concat('user_', user.user_phone) phoneId, user.jianjie blurb, 2 blockedType,user.id blockedId,'' username, '' accountBlurb, '0' AS isMerchant " +
             "    from follow foll " +
             "    join life_user user on foll.phone = user.user_phone   " +
             "    where foll.flag = 'user' and user.delete_flag = 0   " +
+            "</if>" +
             ") foll   " +
-            "left join life_fans fans on fans.fans_id = foll.phoneId and fans.followed_id = #{fansId} and fans.delete_flag = 0 " +
+            "left join life_fans fans on fans.fans_id = foll.phoneId and fans.followed_id = #{relationFansId} and fans.delete_flag = 0 " +
+            "left join life_fans fans_this on fans_this.fans_id = #{relationFansId} and fans_this.followed_id = foll.phoneId and fans_this.delete_flag = 0 " +
             "left join life_blacklist lb on lb.blocked_type = foll.blockedType and lb.blocked_id = foll.blockedId and lb.blocker_type = #{blockerType} and lb.blocker_id = #{blockerId} and lb.delete_flag = 0 " +
-            "${ew.customSqlSegment} ")
-    IPage<LifeFansVo> getMyFollowed(IPage<LifeFansVo> iPage, @Param("fansId") String fansId, @Param("blockerType") String blockerType, @Param("blockerId") String blockerId, @Param(Constants.WRAPPER) QueryWrapper<LifeFansVo> wrapper);
+            "${ew.customSqlSegment} " +
+            "</script>")
+    IPage<LifeFansVo> getMyFollowed(IPage<LifeFansVo> iPage, @Param("fansId") String fansId, @Param("relationFansId") String relationFansId, @Param("blockerType") String blockerType, @Param("blockerId") String blockerId, @Param("onlyStoreFollowed") boolean onlyStoreFollowed, @Param(Constants.WRAPPER) QueryWrapper<LifeFansVo> wrapper);
 
 
     @Select("select foll.*, if(isnull(fans.id), 0, 1) isFollowMe, 1 as isFollowThis from ( " +
@@ -64,8 +69,9 @@ public interface LifeFansMapper extends BaseMapper<LifeFans> {
             "left join life_fans fans on fans.fans_id = foll.phoneId and fans.followed_id = #{fansId} and fans.delete_flag = 0 ")
     List<LifeFansVo> getMyFollowedAll(@Param("fansId") String fansId);
 
-    @Select("select MAX(foll.id) id, MAX(foll.name) name, MAX(foll.image) image, foll.phoneId, MAX(foll.blurb) blurb, MAX(foll.blockedType) blockedType, MAX(foll.blockedId) blockedId, MAX(foll.isMerchant) isMerchant, " +
-            "  MAX(lb.id) blackListid, MAX(if(isnull(fans.id), 0, 1)) isFollowThis, 1 as isFollowMe, " +
+    @Select("<script>" +
+            "select MAX(foll.id) id, MAX(foll.name) name, MAX(foll.image) image, foll.phoneId, MAX(foll.blurb) blurb, MAX(foll.blockedType) blockedType, MAX(foll.blockedId) blockedId, MAX(foll.isMerchant) isMerchant, " +
+            "  MAX(lb.id) blackListid, MAX(if(isnull(fans.id), 0, 1)) isFollowThis, MAX(if(isnull(fans_me.id), 0, 1)) isFollowMe, MAX(if(isnull(lb.id), '0', '1')) isBlocked, " +
             "    (select count(1) from life_fans fans2 where fans2.followed_id = foll.phoneId and fans2.delete_flag = 0) fansNum, " +
             "    (select count(1) from life_fans fans3 where fans3.fans_id = foll.phoneId and fans3.delete_flag = 0) followNum " +
             "from ( " +
@@ -80,16 +86,20 @@ public interface LifeFansMapper extends BaseMapper<LifeFans> {
             "    join store_info info on info.id = user.store_id " +
             "    left join store_img img on img.store_id = user.store_id and img.img_type = '10' and img.delete_flag = 0 " +
             "    where foll.flag = 'store' and user.delete_flag = 0 and info.delete_flag = 0" +
+            "<if test=\"onlyStoreFans == false\">" +
             "    union " +
             "    select user.id, user.user_name name, user.user_image image, concat('user_', user.user_phone) phoneId, user.jianjie blurb, 2 blockedType,user.id blockedId, '0' AS isMerchant" +
             "    from follow foll " +
             "    join life_user user on foll.phone = user.user_phone " +
             "    where foll.flag = 'user' and user.delete_flag = 0 " +
+            "</if>" +
             ") foll " +
-            "left join life_fans fans on fans.followed_id = foll.phoneId and fans.fans_id = #{fansId} and fans.delete_flag = 0 " +
+            "left join life_fans fans on fans.followed_id = foll.phoneId and fans.fans_id = #{relationFansId} and fans.delete_flag = 0 " +
+            "left join life_fans fans_me on fans_me.fans_id = foll.phoneId and fans_me.followed_id = #{relationFansId} and fans_me.delete_flag = 0 " +
             "left join life_blacklist lb on lb.blocked_type = foll.blockedType and lb.blocked_id = foll.blockedId and lb.blocker_type = #{blockerType} and lb.blocker_id = #{blockerId} and lb.delete_flag = 0 " +
-            "${ew.customSqlSegment} ")
-    IPage<LifeFansVo> getMyFans(IPage<LifeFansVo> iPage, @Param("fansId") String fansId, @Param("blockerType") String blockerType, @Param("blockerId") String blockerId, @Param(Constants.WRAPPER) QueryWrapper<LifeFansVo> wrapper);
+            "${ew.customSqlSegment} " +
+            "</script>")
+    IPage<LifeFansVo> getMyFans(IPage<LifeFansVo> iPage, @Param("fansId") String fansId, @Param("relationFansId") String relationFansId, @Param("blockerType") String blockerType, @Param("blockerId") String blockerId, @Param("onlyStoreFans") boolean onlyStoreFans, @Param(Constants.WRAPPER) QueryWrapper<LifeFansVo> wrapper);
 
 //    @Select("select foll.*, if(isnull(fans.id), 0, 1) isFollowThis, 1 as isFollowMe, count(fans2.id) fansNum, count(fans3.id) followNum from ( " +
 //            "    with follow as ( " +

+ 7 - 0
alien-entity/src/main/java/shop/alien/mapper/LifeUserPersonalizationSettingMapper.java

@@ -0,0 +1,7 @@
+package shop.alien.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import shop.alien.entity.store.LifeUserPersonalizationSetting;
+
+public interface LifeUserPersonalizationSettingMapper extends BaseMapper<LifeUserPersonalizationSetting> {
+}

+ 7 - 0
alien-entity/src/main/java/shop/alien/mapper/LifeUserPushDeviceMapper.java

@@ -0,0 +1,7 @@
+package shop.alien.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import shop.alien.entity.store.LifeUserPushDevice;
+
+public interface LifeUserPushDeviceMapper extends BaseMapper<LifeUserPushDevice> {
+}

+ 1 - 1
alien-entity/src/main/java/shop/alien/mapper/StoreClockInMapper.java

@@ -22,7 +22,7 @@ import java.util.Map;
  */
 public interface StoreClockInMapper extends BaseMapper<StoreClockIn> {
 
-    @Select("select clock.id, clock.user_id, clock.store_id, clock.permission, user.user_name, store.store_name,store.score_avg score, clock.like_count, store.store_position, clock.content, clock.created_time, clock.img_url clockImg, img.img_url storeImg, user.user_image userImg,clock.maybe_ai_content," +
+    @Select("select clock.id, clock.user_id, clock.store_id, clock.permission, user.user_name, store.store_name, store.store_blurb storeBlurb, store.score_avg score, clock.like_count, store.store_position, clock.content, clock.created_time, clock.img_url clockImg, img.img_url storeImg, user.user_image userImg,clock.maybe_ai_content," +
             "store.administrative_region_district_name region, dict.dict_detail storeType, concat('user_', user.user_phone) phoneId, store.business_section,store.business_section_name,business_types_name,img1.img_url entranceImage," +
             "( " +
             " select ifnull(round(sum(uorder.price) / count(1), 0), 0) " +

+ 1 - 1
alien-lawyer/src/main/java/shop/alien/lawyer/controller/AlipayZftCreateRecordController.java

@@ -21,7 +21,7 @@ public class AlipayZftCreateRecordController {
 
     private final AlipayZftCreateRecordService alipayZftCreateRecordService;
 
-    @ApiOperation("根据商户名称查询是否存在进件记录(alipay_zft_create_record)")
+    @ApiOperation("根据商户名称(law_firm)查询是否存在已填写支付宝二级商户账号的律师用户")
     @ApiOperationSupport(order = 1)
     @ApiImplicitParam(name = "merchantName", value = "商户名称", required = true, paramType = "query", dataType = "String")
     @GetMapping("/existsSuccessful")

+ 3 - 3
alien-lawyer/src/main/java/shop/alien/lawyer/service/AlipayZftCreateRecordService.java

@@ -8,10 +8,10 @@ import shop.alien.entity.result.R;
 public interface AlipayZftCreateRecordService {
 
     /**
-     * 按商户名称查询 {@code alipay_zft_create_record} 是否存在记录
+     * 按商户名称(对应 {@code lawyer_user.law_firm})查询是否存在已绑定支付宝二级商户账号的律师
      *
-     * @param merchantName 商户名称
-     * @return true 存在,false 不存在
+     * @param merchantName 商户名称(律所名称)
+     * @return true 存在且 {@code zfb_secondary_merchant_account} 有有效值否则 false
      */
     R<Boolean> existsByMerchantName(String merchantName);
 }

+ 10 - 7
alien-lawyer/src/main/java/shop/alien/lawyer/service/impl/AlipayZftCreateRecordServiceImpl.java

@@ -6,28 +6,31 @@ import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 import org.springframework.util.StringUtils;
 import shop.alien.entity.result.R;
-import shop.alien.entity.store.AlipayZftCreateRecord;
+import shop.alien.entity.store.LawyerUser;
 import shop.alien.lawyer.service.AlipayZftCreateRecordService;
-import shop.alien.mapper.AlipayZftCreateRecordMapper;
+import shop.alien.mapper.LawyerUserMapper;
 
 /**
- * 支付宝直付通进件创建记录
+ * 支付宝直付通进件创建记录(按律所名称关联律师用户二级商户账号)
  */
 @Slf4j
 @Service
 @RequiredArgsConstructor
 public class AlipayZftCreateRecordServiceImpl implements AlipayZftCreateRecordService {
 
-    private final AlipayZftCreateRecordMapper alipayZftCreateRecordMapper;
+    private final LawyerUserMapper lawyerUserMapper;
 
     @Override
     public R<Boolean> existsByMerchantName(String merchantName) {
         if (!StringUtils.hasText(merchantName)) {
             return R.data(false);
         }
-        LambdaQueryWrapper<AlipayZftCreateRecord> wrapper = new LambdaQueryWrapper<>();
-        wrapper.eq(AlipayZftCreateRecord::getMerchantName, merchantName.trim());
-        long n = alipayZftCreateRecordMapper.selectCount(wrapper);
+        LambdaQueryWrapper<LawyerUser> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(LawyerUser::getLawFirm, merchantName.trim())
+                .eq(LawyerUser::getDeleteFlag, 0)
+                .isNotNull(LawyerUser::getZfbSecondaryMerchantAccount)
+                .ne(LawyerUser::getZfbSecondaryMerchantAccount, "");
+        long n = lawyerUserMapper.selectCount(wrapper);
         return R.data(n > 0);
     }
 }

+ 47 - 0
alien-store/src/main/java/shop/alien/store/config/UniPushProperties.java

@@ -0,0 +1,47 @@
+package shop.alien.store.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.cloud.context.config.annotation.RefreshScope;
+import org.springframework.stereotype.Component;
+
+/**
+ * uniCloud 云函数 URL 化后,由 Java 侧发起推送的 HTTP 配置。
+ * 云函数内需使用 uni-cloud-push 的 sendMessage;请求体字段需与云函数约定一致。
+ */
+@Data
+@Component
+@RefreshScope
+@ConfigurationProperties(prefix = "alien.unipush")
+public class UniPushProperties {
+
+    /**
+     * 是否启用 HTTP 调用云函数推送(未配置 url 时 {@link shop.alien.store.service.UniPushCloudInvokeService} 会直接跳过)
+     */
+    private boolean enabled = false;
+
+    /**
+     * 云函数 URL 化完整地址,例如 https://xxx.com/push/send
+     */
+    private String cloudFunctionUrl = "";
+
+    /**
+     * 可选:鉴权请求头名,如 X-Api-Secret
+     */
+    private String authHeaderName = "";
+
+    /**
+     * 可选:鉴权请求头值
+     */
+    private String authHeaderValue = "";
+
+    /**
+     * 连接超时毫秒
+     */
+    private int connectTimeoutMs = 5000;
+
+    /**
+     * 读超时毫秒
+     */
+    private int readTimeoutMs = 10000;
+}

+ 38 - 0
alien-store/src/main/java/shop/alien/store/controller/CommonRatingController.java

@@ -5,7 +5,9 @@ import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.web.bind.annotation.*;
 import shop.alien.entity.result.R;
+import com.baomidou.mybatisplus.core.metadata.IPage;
 import shop.alien.entity.store.CommonRating;
+import shop.alien.entity.store.vo.MyMergedReviewItemVo;
 import shop.alien.store.annotation.TrackEvent;
 import shop.alien.store.service.CommonCommentService;
 import shop.alien.store.service.CommonRatingService;
@@ -190,4 +192,40 @@ public class CommonRatingController {
         return commonRatingService.getMyRatingList(pageNum, pageSize, businessType, userId, auditStatus);
     }
 
+    @ApiOperation(value = "融合查询我的评价列表",
+            notes = "【请求】GET /commonRating/getMyMergedRatingList\n\n"
+                    + "【功能】合并「门店评价 common_rating」与「律师订单评价」,按发布时间倒序(新在前);服务端合并后再分页。\n\n"
+                    + "【查询参数】\n"
+                    + "- pageNum:页码,默认 1\n"
+                    + "- pageSize:每页条数,默认 10\n"
+                    + "- businessType:门店侧 business_type,不传默认 1(门店评价),语义同 getMyRatingList\n"
+                    + "- userId:必填,被查询的用户 ID(Long)\n"
+                    + "- auditStatus:可选,仅过滤门店评价侧;律师订单评价不受此参数影响\n"
+                    + "- currentUserId:可选,传入时用于律师评价侧「是否已点赞」等当前用户态\n\n"
+                    + "【响应】外层统一 R;success=true 时 data 为分页对象(IPage),元素类型 MyMergedReviewItemVo。\n"
+                    + "- data.records:列表项,itemType=STORE 为门店评价,LAWYER 为律师订单评价,字段见模型说明\n"
+                    + "- data.total:门店评价总数 + 律师评价总数(合并前各自 total 之和)\n"
+                    + "- data.current、data.size、data.pages:标准分页字段\n\n"
+                    + "【错误】userId 为空时返回失败(msg 提示)。")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "pageNum", value = "页码,默认 1", dataType = "Integer", paramType = "query", defaultValue = "1"),
+            @ApiImplicitParam(name = "pageSize", value = "每页条数,默认 10", dataType = "Integer", paramType = "query", defaultValue = "10"),
+            @ApiImplicitParam(name = "businessType", value = "门店侧业务类型,不传默认 1-门店评价", dataType = "Integer", paramType = "query"),
+            @ApiImplicitParam(name = "userId", value = "用户 ID(必填)", dataType = "Long", paramType = "query", required = true, example = "10001"),
+            @ApiImplicitParam(name = "auditStatus", value = "门店评价审核状态筛选,可选;律师侧不过滤", dataType = "Integer", paramType = "query"),
+            @ApiImplicitParam(name = "currentUserId", value = "当前登录用户 ID,可选;律师评价用于点赞态等", dataType = "Integer", paramType = "query")
+    })
+    @GetMapping("/getMyMergedRatingList")
+    public R<IPage<MyMergedReviewItemVo>> getMyMergedRatingList(
+            @RequestParam(defaultValue = "1") Integer pageNum,
+            @RequestParam(defaultValue = "10") Integer pageSize,
+            @RequestParam(required = false) Integer businessType,
+            @RequestParam Long userId,
+            @RequestParam(required = false) Integer auditStatus,
+            @RequestParam(required = false) Integer currentUserId) {
+        log.info("CommonRatingController.getMyMergedRatingList?pageNum={}&pageSize={}&businessType={}&userId={}&auditStatus={}&currentUserId={}",
+                pageNum, pageSize, businessType, userId, auditStatus, currentUserId);
+        return commonRatingService.getMyMergedRatingList(pageNum, pageSize, businessType, userId, auditStatus, currentUserId);
+    }
+
 }

+ 16 - 8
alien-store/src/main/java/shop/alien/store/controller/LifeStoreController.java

@@ -59,11 +59,15 @@ public class LifeStoreController {
     @ApiImplicitParams({@ApiImplicitParam(name = "page", value = "页数", dataType = "Integer", paramType = "query", required = true),
             @ApiImplicitParam(name = "size", value = "页容", dataType = "Integer", paramType = "query", required = true),
             @ApiImplicitParam(name = "fansId", value = "粉丝id", dataType = "String", paramType = "query", required = true),
-            @ApiImplicitParam(name = "name", value = "名称", dataType = "String", paramType = "query")})
+            @ApiImplicitParam(name = "name", value = "名称", dataType = "String", paramType = "query"),
+            @ApiImplicitParam(name = "type", value = "筛选:不传或0=全部关注;1且fansId为店铺时仅店铺关注", dataType = "Integer", paramType = "query"),
+            @ApiImplicitParam(name = "myFansId", value = "可选;有值时关注/拉黑四字段相对该 id,列表仍按 fansId", dataType = "String", paramType = "query")})
     @GetMapping("/getMyFollowed")
-    public R<IPage<LifeFansVo>> getMyFollowed(@RequestParam("fansId") String fansId, @RequestParam("name") String name, @RequestParam("page") int page, @RequestParam("size") int size) {
-        log.info("LifeStoreController.getMyFollowed?fansId={},name={},page={},size={}", fansId, name, page, size);
-        return R.data(lifeStoreService.getMyFollowed(fansId, name, page, size));
+    public R<IPage<LifeFansVo>> getMyFollowed(@RequestParam("fansId") String fansId, @RequestParam("name") String name, @RequestParam("page") int page, @RequestParam("size") int size,
+                                              @RequestParam(required = false) Integer type,
+                                              @RequestParam(required = false) String myFansId) {
+        log.info("LifeStoreController.getMyFollowed?fansId={},name={},page={},size={},type={},myFansId={}", fansId, name, page, size, type, myFansId);
+        return R.data(lifeStoreService.getMyFollowed(fansId, name, page, size, type, myFansId));
     }
 
     @ApiOperationSupport(order = 3)
@@ -71,11 +75,15 @@ public class LifeStoreController {
     @ApiImplicitParams({@ApiImplicitParam(name = "page", value = "页数", dataType = "Integer", paramType = "query", required = true),
             @ApiImplicitParam(name = "size", value = "页容", dataType = "Integer", paramType = "query", required = true),
             @ApiImplicitParam(name = "fansId", value = "粉丝id", dataType = "String", paramType = "query", required = true),
-            @ApiImplicitParam(name = "name", value = "名称", dataType = "String", paramType = "query")})
+            @ApiImplicitParam(name = "name", value = "名称", dataType = "String", paramType = "query"),
+            @ApiImplicitParam(name = "type", value = "筛选:不传或0=全部粉丝;1且fansId为店铺时仅店铺粉丝", dataType = "Integer", paramType = "query"),
+            @ApiImplicitParam(name = "myFansId", value = "可选;有值时关注/拉黑四字段相对该 id,列表仍按 fansId", dataType = "String", paramType = "query")})
     @GetMapping("/getMyFans")
-    public R<IPage<LifeFansVo>> getMyFans(String fansId, @RequestParam(defaultValue = "") String name, @RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "10") int size) {
-        log.info("LifeStoreController.getMyFans?fansId={},name={},page={},size={}", fansId, name, page, size);
-        return R.data(lifeStoreService.getMyFans(fansId, name, page, size));
+    public R<IPage<LifeFansVo>> getMyFans(String fansId, @RequestParam(defaultValue = "") String name, @RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "10") int size,
+                                          @RequestParam(required = false) Integer type,
+                                          @RequestParam(required = false) String myFansId) {
+        log.info("LifeStoreController.getMyFans?fansId={},name={},page={},size={},type={},myFansId={}", fansId, name, page, size, type, myFansId);
+        return R.data(lifeStoreService.getMyFans(fansId, name, page, size, type, myFansId));
     }
 
     @ApiOperationSupport(order = 4)

+ 84 - 0
alien-store/src/main/java/shop/alien/store/controller/LifeUserPersonalizationSettingController.java

@@ -0,0 +1,84 @@
+package shop.alien.store.controller;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiImplicitParam;
+import io.swagger.annotations.ApiImplicitParams;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiOperationSupport;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.LifeUserPersonalizationSetting;
+import shop.alien.store.service.LifeUserPersonalizationSettingService;
+
+@Api(tags = {"用户个性化设置"})
+@Slf4j
+@RestController
+@CrossOrigin
+@RequestMapping("/lifeUserPersonalizationSetting")
+@RequiredArgsConstructor
+public class LifeUserPersonalizationSettingController {
+
+    private final LifeUserPersonalizationSettingService lifeUserPersonalizationSettingService;
+
+    @ApiOperation("新增个性化设置")
+    @ApiOperationSupport(order = 1)
+    @PostMapping("/add")
+    public R<String> add(@RequestBody LifeUserPersonalizationSetting setting) {
+        return lifeUserPersonalizationSettingService.add(setting);
+    }
+
+    @ApiOperation("根据主键删除(逻辑删除)")
+    @ApiOperationSupport(order = 2)
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "id", value = "主键ID", dataType = "Integer", paramType = "query", required = true)
+    })
+    @GetMapping("/deleteById")
+    public R<String> deleteById(@RequestParam Integer id) {
+        return lifeUserPersonalizationSettingService.deleteById(id);
+    }
+
+    @ApiOperation("更新个性化设置;传 id 或传 userId 均可定位记录(无 userId 对应记录时会先插入默认再更新)")
+    @ApiOperationSupport(order = 3)
+    @PostMapping("/update")
+    public R<String> update(@RequestBody LifeUserPersonalizationSetting setting) {
+        return lifeUserPersonalizationSettingService.update(setting);
+    }
+
+    @ApiOperation("根据主键查询")
+    @ApiOperationSupport(order = 4)
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "id", value = "主键ID", dataType = "Integer", paramType = "query", required = true)
+    })
+    @GetMapping("/getById")
+    public R<LifeUserPersonalizationSetting> getById(@RequestParam Integer id) {
+        return lifeUserPersonalizationSettingService.getInfoById(id);
+    }
+
+    @ApiOperation("根据用户ID查询;若无记录则自动插入一条默认值后返回")
+    @ApiOperationSupport(order = 5)
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "userId", value = "用户ID", dataType = "Integer", paramType = "query", required = true)
+    })
+    @GetMapping("/getByUserId")
+    public R<LifeUserPersonalizationSetting> getByUserId(@RequestParam Integer userId) {
+        return lifeUserPersonalizationSettingService.getByUserId(userId);
+    }
+
+    @ApiOperation("分页列表,可按 userId 筛选")
+    @ApiOperationSupport(order = 6)
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "pageNum", value = "页码", dataType = "Integer", paramType = "query", required = true),
+            @ApiImplicitParam(name = "pageSize", value = "每页数量", dataType = "Integer", paramType = "query", required = true),
+            @ApiImplicitParam(name = "userId", value = "用户ID", dataType = "Integer", paramType = "query")
+    })
+    @GetMapping("/list")
+    public R<IPage<LifeUserPersonalizationSetting>> list(
+            @RequestParam Integer pageNum,
+            @RequestParam Integer pageSize,
+            @RequestParam(required = false) Integer userId) {
+        return lifeUserPersonalizationSettingService.list(pageNum, pageSize, userId);
+    }
+}

+ 62 - 0
alien-store/src/main/java/shop/alien/store/controller/LifeUserPushDeviceController.java

@@ -0,0 +1,62 @@
+package shop.alien.store.controller;
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiImplicitParam;
+import io.swagger.annotations.ApiImplicitParams;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiOperationSupport;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.LifeUserPushDevice;
+import shop.alien.entity.store.UserLoginInfo;
+import shop.alien.store.dto.LifeUserPushBindDto;
+import shop.alien.store.service.LifeUserPushDeviceService;
+import shop.alien.util.common.TokenInfo;
+import springfox.documentation.annotations.ApiIgnore;
+
+import java.util.List;
+
+/**
+ * uni-push 等设备 cid 绑定;业务主键与类型取自登录态 JWT(userId + userType),不信任客户端传参区分身份。
+ * <p>userType:user → life_user.id;store/merchant → store_user.id;lawyer → lawyer_user.id</p>
+ */
+@Api(tags = {"用户推送设备"})
+@Slf4j
+@RestController
+@CrossOrigin
+@RequestMapping("/lifeUserPushDevice")
+@RequiredArgsConstructor
+public class LifeUserPushDeviceController {
+
+    private final LifeUserPushDeviceService lifeUserPushDeviceService;
+
+    @ApiOperation("绑定当前用户与 push cid(登录后调用)")
+    @ApiOperationSupport(order = 1)
+    @PostMapping("/bind")
+    public R<String> bind(@ApiIgnore @TokenInfo UserLoginInfo userLoginInfo,
+                          @RequestBody LifeUserPushBindDto dto) {
+        log.info("LifeUserPushDeviceController.bind, userId={}",
+                userLoginInfo == null ? null : userLoginInfo.getUserId());
+        return lifeUserPushDeviceService.bind(userLoginInfo, dto);
+    }
+
+    @ApiOperation("解绑指定 cid(仅删除当前用户名下记录)")
+    @ApiOperationSupport(order = 2)
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "pushClientId", value = "cid", required = true, paramType = "query", dataType = "String")
+    })
+    @PostMapping("/unbind")
+    public R<String> unbind(@ApiIgnore @TokenInfo UserLoginInfo userLoginInfo,
+                            @RequestParam String pushClientId) {
+        return lifeUserPushDeviceService.unbindByCid(userLoginInfo, pushClientId);
+    }
+
+    @ApiOperation("当前用户已绑定的推送设备列表")
+    @ApiOperationSupport(order = 3)
+    @GetMapping("/listMine")
+    public R<List<LifeUserPushDevice>> listMine(@ApiIgnore @TokenInfo UserLoginInfo userLoginInfo) {
+        return lifeUserPushDeviceService.listMine(userLoginInfo);
+    }
+}

+ 26 - 0
alien-store/src/main/java/shop/alien/store/dto/LifeUserPushBindDto.java

@@ -0,0 +1,26 @@
+package shop.alien.store.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * App 上报 uni-push cid 与当前登录用户绑定
+ */
+@Data
+@ApiModel(description = "绑定推送 cid 请求体")
+public class LifeUserPushBindDto implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "uni.getPushClientId 返回的 cid", required = true)
+    private String pushClientId;
+
+    @ApiModelProperty(value = "平台:ios、android、harmony 等")
+    private String platform;
+
+    @ApiModelProperty(value = "DCloud 应用 appid,如 __UNI__xxxx")
+    private String dcloudAppId;
+}

+ 8 - 0
alien-store/src/main/java/shop/alien/store/service/CommonRatingService.java

@@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.extension.service.IService;
 import shop.alien.entity.result.R;
 import shop.alien.entity.store.CommonRating;
 import shop.alien.entity.store.vo.CommonRatingVo;
+import shop.alien.entity.store.vo.MyMergedReviewItemVo;
 
 /**
  * 评价表 服务类
@@ -74,6 +75,13 @@ public interface CommonRatingService extends IService<CommonRating> {
      */
     R getMyRatingList(Integer pageNum, Integer pageSize, Integer businessType, Long userId, Integer auditStatus);
 
+    /**
+     * 融合「门店我的评价」与「律师订单我的评价」,按发布时间倒序分页。
+     * 分页说明:从两侧各取至多 pageNum*pageSize 条(上限 1000)合并后再截取当前页。
+     */
+    R<IPage<MyMergedReviewItemVo>> getMyMergedRatingList(Integer pageNum, Integer pageSize, Integer businessType,
+                                                         Long userId, Integer auditStatus, Integer currentUserId);
+
     R<IPage<CommonRatingVo>> doListBusinessWithType(IPage<CommonRating> page2, Integer i, Long userId, Integer replyStatus);
 
   /*  /**

+ 129 - 2
alien-store/src/main/java/shop/alien/store/service/LifeCommentService.java

@@ -8,6 +8,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
 import org.springframework.util.CollectionUtils;
 import org.springframework.util.ObjectUtils;
@@ -36,6 +37,10 @@ import java.util.stream.Collectors;
 @RequiredArgsConstructor
 public class LifeCommentService {
 
+    /** App 点击推送后跳转页面(uni-app 页面路径) */
+    private static final String DYNAMICS_LIKE_PUSH_OPEN_PATH =
+            "pages/secondHandTransactions/pages/message/noticesAndMessage";
+
     private final LifeCommentMapper lifeCommentMapper;
 
     private final StoreCommentMapper storeCommentMapper;
@@ -62,6 +67,20 @@ public class LifeCommentService {
 
     private final CommonCommentMapper commonCommentMapper;
 
+    private final LifeUserPersonalizationSettingService lifeUserPersonalizationSettingService;
+
+    private final LifeUserPushDeviceService lifeUserPushDeviceService;
+
+    private final UniPushCloudInvokeService uniPushCloudInvokeService;
+
+    private final StoreUserMapper storeUserMapper;
+
+    /**
+     * 系统app通知开关
+     */
+    @Value("${alien.unipush.enabled:false}")
+    private boolean uniPushOn;
+
     /**
      * 点赞操作
      * <p>
@@ -121,9 +140,29 @@ public class LifeCommentService {
             // 根据类型更新对应表的点赞数
             int updateResult = updateLikeCountByType(huifuId, type);
             
-            // 如果是动态类型,发送通知
+            // 动态点赞:按「被点赞动态的发布者」的个性化设置决定是否通知(接收方是否愿意收点赞类通知)
             if (updateResult > 0 && CommonConstant.LIKE_TYPE_DYNAMICS.equals(type)) {
-                insertNotice(userId, huifuId, type);
+                Integer receiverLifeUserId = resolveLifeUserIdFromDynamicsAuthorPhoneId(huifuId);
+                boolean suppressNotice = receiverLifeUserId != null
+                        && lifeUserPersonalizationSettingService.shouldSuppressLikeRelatedNotice(receiverLifeUserId);
+                if (!suppressNotice) {
+                    try {
+                        insertNotice(userId, huifuId, type);
+                    } catch (Exception e) {
+                        log.error("动态点赞站内信保存失败, huifuId={}", huifuId, e);
+                    }
+
+                    // 发送系统通知开关
+                    if(uniPushOn){
+                        try {
+                            sendDynamicsLikeAppPushByDynamicsId(Integer.parseInt(huifuId.trim()));
+                        } catch (NumberFormatException e) {
+                            log.warn("动态点赞 App 推送跳过:huifuId 非合法动态 id, huifuId={}", huifuId);
+                        } catch (Exception e) {
+                            log.warn("动态点赞 App 推送失败, huifuId={}, err={}", huifuId, e.getMessage());
+                        }
+                    }
+                }
             }
             
             log.info("点赞操作完成,userId={},huifuId={},type={},更新结果={}", userId, huifuId, type, updateResult);
@@ -212,6 +251,37 @@ public class LifeCommentService {
     }
 
     /**
+     * 根据动态主键查发布者 phoneId,仅当为 C 端 user_ 前缀时解析为 life_user.id;门店动态 store_ 无 C 端个性化表则返回 null(不拦截通知)。
+     */
+    private Integer resolveLifeUserIdFromDynamicsAuthorPhoneId(String dynamicsIdStr) {
+        if (!StringUtils.hasText(dynamicsIdStr)) {
+            return null;
+        }
+        try {
+            int dynamicsId = Integer.parseInt(dynamicsIdStr.trim());
+            LifeUserDynamics dynamics = lifeUserDynamicsMapper.selectById(dynamicsId);
+            if (dynamics == null || !StringUtils.hasText(dynamics.getPhoneId())) {
+                return null;
+            }
+            String phoneId = dynamics.getPhoneId().trim();
+            if (!phoneId.startsWith("user_")) {
+                return null;
+            }
+            String phone = phoneId.substring("user_".length());
+            if (!StringUtils.hasText(phone)) {
+                return null;
+            }
+            LifeUser u = lifeUserMapper.selectOne(new LambdaQueryWrapper<LifeUser>()
+                    .eq(LifeUser::getUserPhone, phone)
+                    .eq(LifeUser::getDeleteFlag, 0)
+                    .last("LIMIT 1"));
+            return u != null ? u.getId() : null;
+        } catch (NumberFormatException e) {
+            return null;
+        }
+    }
+
+    /**
      * 发送通知
      */
     private void insertNotice(String userId, String huifuId, String type) {
@@ -229,6 +299,63 @@ public class LifeCommentService {
     }
 
     /**
+     * 仅发起 App 推送(uniCloud),与 {@link #insertNotice} 独立;未配置、无 cid 时静默返回。
+     */
+    private void sendDynamicsLikeAppPushByDynamicsId(int dynamicsId) {
+        LifeUserDynamics dynamics = lifeUserDynamicsMapper.selectById(dynamicsId);
+        sendDynamicsLikeAppPushOnly(dynamics, "动态通知", "点赞了你的动态", dynamicsId);
+    }
+
+    private void sendDynamicsLikeAppPushOnly(LifeUserDynamics dynamics, String title, String content, int dynamicsId) {
+        if (dynamics == null || !StringUtils.hasText(dynamics.getPhoneId())) {
+            return;
+        }
+        String phoneId = dynamics.getPhoneId().trim();
+        List<String> cids = resolvePushClientIdsForDynamicsAuthor(phoneId);
+        if (CollectionUtils.isEmpty(cids)) {
+            return;
+        }
+        Map<String, Object> payload = new HashMap<>(12);
+        payload.put("scene", "dynamics_like");
+        payload.put("dynamicsId", dynamicsId);
+        payload.put("noticeType", 0);
+        payload.put("path", DYNAMICS_LIKE_PUSH_OPEN_PATH);
+        uniPushCloudInvokeService.sendToClientIds(cids, title, content, payload);
+    }
+
+    private List<String> resolvePushClientIdsForDynamicsAuthor(String phoneId) {
+        if (phoneId.startsWith("user_")) {
+            String phone = phoneId.substring("user_".length());
+            if (!StringUtils.hasText(phone)) {
+                return Collections.emptyList();
+            }
+            LifeUser u = lifeUserMapper.selectOne(new LambdaQueryWrapper<LifeUser>()
+                    .eq(LifeUser::getUserPhone, phone)
+                    .eq(LifeUser::getDeleteFlag, 0)
+                    .last("LIMIT 1"));
+            if (u == null) {
+                return Collections.emptyList();
+            }
+            return lifeUserPushDeviceService.listPushClientIdsByUserId(u.getId(), PushDeviceOwnerType.USER.getCode());
+        }
+        if (phoneId.startsWith("store_")) {
+            String phone = phoneId.substring("store_".length());
+            if (!StringUtils.hasText(phone)) {
+                return Collections.emptyList();
+            }
+            StoreUser su = storeUserMapper.selectOne(new LambdaQueryWrapper<StoreUser>()
+                    .eq(StoreUser::getPhone, phone)
+                    .eq(StoreUser::getDeleteFlag, 0)
+                    .last("LIMIT 1"));
+            if (su == null) {
+                return Collections.emptyList();
+            }
+            return lifeUserPushDeviceService.listPushClientIdsByUserId(su.getId(), PushDeviceOwnerType.STORE.getCode());
+        }
+        return Collections.emptyList();
+    }
+
+    /**
      * 取消点赞操作
      * <p>
      * 检查是否已点赞,如果已点赞则删除点赞记录并更新对应表的点赞数

+ 27 - 12
alien-store/src/main/java/shop/alien/store/service/LifeStoreService.java

@@ -70,26 +70,34 @@ public class LifeStoreService {
 
     /**
      * 我的关注
+     *
+     * @param type 筛选类型:不传或 0 表示店铺+用户关注;1 且 fansId 为店铺时仅返回店铺类型关注
+     * @param myFansId 可选;有值时 isFollowMe、isFollowThis、isBlocked、blackListid 相对该主体计算,列表仍按 fansId
      */
-    public IPage<LifeFansVo> getMyFollowed(String fansId, String name, int page, int size) {QueryWrapper<LifeFansVo> wrapper = new QueryWrapper<>();
+    public IPage<LifeFansVo> getMyFollowed(String fansId, String name, int page, int size, Integer type, String myFansId) {
+        QueryWrapper<LifeFansVo> wrapper = new QueryWrapper<>();
         wrapper.like(StringUtils.isNotEmpty(name), "foll.name", name)
                 .or(StringUtils.isNotEmpty(name))
                 .like(StringUtils.isNotEmpty(name), "foll.phoneId", name);
         wrapper.groupBy("foll.phoneId");
+        String trimmedMyFansId = StringUtils.isNotEmpty(myFansId) ? myFansId.trim() : null;
+        String relationFansId = StringUtils.isNotEmpty(trimmedMyFansId) ? trimmedMyFansId : fansId;
+        String blockerSourceId = StringUtils.isNotEmpty(trimmedMyFansId) ? trimmedMyFansId : fansId;
         String blockerType = "";
         String blockerId = "";
-        if ("user".equals(fansId.split("_")[0])) {
-            String myselfUserPhone = fansId.split("_")[1];
+        if ("user".equals(blockerSourceId.split("_")[0])) {
+            String myselfUserPhone = blockerSourceId.split("_")[1];
             blockerType = "2";
             LifeUser myLifeUser = lifeUserService.getUserByPhone(myselfUserPhone);
             blockerId = String.valueOf(myLifeUser.getId());
         } else {
-            String myselfStorePhone = fansId.split("_")[1];
+            String myselfStorePhone = blockerSourceId.split("_")[1];
             blockerType = "1";
             StoreUser myStoreUser = storeUserService.getUserByPhone(myselfStorePhone);
             blockerId = String.valueOf(myStoreUser.getId());
         }
-        IPage<LifeFansVo> myFollowed = lifeFansMapper.getMyFollowed(new Page<>(page, size), fansId, blockerType, blockerId, wrapper);
+        boolean onlyStoreFollowed = (type != null && type == 1) && fansId != null && fansId.startsWith("store_");
+        IPage<LifeFansVo> myFollowed = lifeFansMapper.getMyFollowed(new Page<>(page, size), fansId, relationFansId, blockerType, blockerId, onlyStoreFollowed, wrapper);
         // 过滤掉我拉黑的
         List<LifeFansVo> collect = myFollowed.getRecords().stream().filter(x -> null == x.getBlackListid()).collect(Collectors.toList());
         myFollowed.setRecords(collect);
@@ -106,25 +114,32 @@ public class LifeStoreService {
 
     /**
      * 我的粉丝
+     *
+     * @param type 筛选类型:不传或 0 表示店铺+用户粉丝;1 且 fansId 为店铺时仅返回店铺粉丝
+     * @param myFansId 可选;有值时 isFollowMe、isFollowThis、isBlocked、blackListid 相对该主体计算,列表仍按 fansId
      */
-    public IPage<LifeFansVo> getMyFans(String fansId, String name, int page, int size) {
+    public IPage<LifeFansVo> getMyFans(String fansId, String name, int page, int size, Integer type, String myFansId) {
         QueryWrapper<LifeFansVo> wrapper = new QueryWrapper<>();
         wrapper.like(StringUtils.isNotEmpty(name), "foll.name", name);
         wrapper.groupBy("foll.phoneId");
+        String trimmedMyFansId = StringUtils.isNotEmpty(myFansId) ? myFansId.trim() : null;
+        String relationFansId = StringUtils.isNotEmpty(trimmedMyFansId) ? trimmedMyFansId : fansId;
+        String blockerSourceId = StringUtils.isNotEmpty(trimmedMyFansId) ? trimmedMyFansId : fansId;
         String blockerType = "";
         String blockerId = "";
-        if ("user".equals(fansId.split("_")[0])) {
-            String myselfUserPhone = fansId.split("_")[1];
+        if ("user".equals(blockerSourceId.split("_")[0])) {
+            String myselfUserPhone = blockerSourceId.split("_")[1];
             blockerType = "2";
             LifeUser myLifeUser = lifeUserService.getUserByPhone(myselfUserPhone);
             blockerId = String.valueOf(myLifeUser.getId());
         } else {
-            String myselfStorePhone = fansId.split("_")[1];
+            String myselfStorePhone = blockerSourceId.split("_")[1];
             blockerType = "1";
             StoreUser myStoreUser = storeUserService.getUserByPhone(myselfStorePhone);
             blockerId = String.valueOf(myStoreUser.getId());
         }
-        IPage<LifeFansVo> myFans = lifeFansMapper.getMyFans(new Page<>(page, size), fansId, blockerType, blockerId, wrapper);
+        boolean onlyStoreFans = (type != null && type == 1) && fansId != null && fansId.startsWith("store_");
+        IPage<LifeFansVo> myFans = lifeFansMapper.getMyFans(new Page<>(page, size), fansId, relationFansId, blockerType, blockerId, onlyStoreFans, wrapper);
         List<LifeFansVo> collect = myFans.getRecords().stream().filter(x -> null == x.getBlackListid()).collect(Collectors.toList());
         myFans.setRecords(collect);
         myFans.setTotal(collect.size());
@@ -260,11 +275,11 @@ public class LifeStoreService {
         
         // 统计关注数量(过滤被拉黑的)
         int followedNum = countFilteredRecords(phoneId, blockerType, blockerId, wrapper, pageSize, 
-                (page, w) -> lifeFansMapper.getMyFollowed(page, phoneId, blockerType, blockerId, w));
+                (page, w) -> lifeFansMapper.getMyFollowed(page, phoneId, phoneId, blockerType, blockerId, false, w));
 
         // 统计粉丝数量(过滤被拉黑的)
         int fansNum = countFilteredRecords(phoneId, blockerType, blockerId, wrapper, pageSize,
-                (page, w) -> lifeFansMapper.getMyFans(page, phoneId, blockerType, blockerId, w));
+                (page, w) -> lifeFansMapper.getMyFans(page, phoneId, phoneId, blockerType, blockerId, false, w));
 
         // 统计好友数量(过滤被拉黑的)
         int friendNum = countFilteredRecords(phoneId, blockerType, blockerId, wrapper, pageSize,

+ 46 - 0
alien-store/src/main/java/shop/alien/store/service/LifeUserPersonalizationSettingService.java

@@ -0,0 +1,46 @@
+package shop.alien.store.service;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.service.IService;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.LifeUserPersonalizationSetting;
+
+public interface LifeUserPersonalizationSettingService extends IService<LifeUserPersonalizationSetting> {
+
+    R<String> add(LifeUserPersonalizationSetting setting);
+
+    R<String> deleteById(Integer id);
+
+    R<String> update(LifeUserPersonalizationSetting setting);
+
+    R<LifeUserPersonalizationSetting> getInfoById(Integer id);
+
+    R<LifeUserPersonalizationSetting> getByUserId(Integer userId);
+
+    /**
+     * 查询用户个性化设置:优先 Redis;未命中则与 {@link #getByUserId} 一致查库(含无记录时初始化)并写入 Redis。
+     *
+     * @param userId life_user.id
+     * @return 实体;userId 为空或查库/初始化失败时返回 null
+     */
+    LifeUserPersonalizationSetting getByUserIdCacheAside(Integer userId);
+
+    /**
+     * 是否不应向该用户发送与「收到点赞」相关的通知:notifyReceiveMessage=0,或 notifyReceiveMessage=1 且 notifyLike=0。
+     * 调用方应传入接收通知的一方对应的 life_user.id(例如动态点赞场景为动态发布者,而非点赞操作人)。
+     *
+     * @param userId life_user.id(接收方)
+     * @return userId 为空时 false(不拦截);有设置且满足上述条件时 true
+     */
+    boolean shouldSuppressLikeRelatedNotice(Integer userId);
+
+    /**
+     * 是否不应发送与关注相关的通知:notifyReceiveMessage=0,或 notifyReceiveMessage=1 且 notifyFollow=0。
+     *
+     * @param userId life_user.id(被关注方)
+     * @return userId 为空时 false(不拦截);有设置且满足上述条件时 true
+     */
+    boolean shouldSuppressFollowRelatedNotice(Integer userId);
+
+    R<IPage<LifeUserPersonalizationSetting>> list(Integer pageNum, Integer pageSize, Integer userId);
+}

+ 34 - 0
alien-store/src/main/java/shop/alien/store/service/LifeUserPushDeviceService.java

@@ -0,0 +1,34 @@
+package shop.alien.store.service;
+
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.LifeUserPushDevice;
+import shop.alien.entity.store.UserLoginInfo;
+import shop.alien.store.dto.LifeUserPushBindDto;
+
+import java.util.List;
+
+public interface LifeUserPushDeviceService {
+
+    /**
+     * 将 cid 绑定到当前登录用户;同一 cid 全局唯一,重复上报会更新为当前用户并刷新平台等信息。
+     */
+    R<String> bind(UserLoginInfo login, LifeUserPushBindDto dto);
+
+    /**
+     * 当前用户解绑指定 cid(仅可删除本人绑定记录)
+     */
+    R<String> unbindByCid(UserLoginInfo login, String pushClientId);
+
+    /**
+     * 查询当前用户已绑定的设备列表
+     */
+    R<List<LifeUserPushDevice>> listMine(UserLoginInfo login);
+
+    /**
+     * 供业务侧按归属类型 + 业务用户主键查询 cid 列表(去重),用于组装云函数推送目标。
+     *
+     * @param userId    ownerType=user 时为 life_user.id;store 为 store_user.id;lawyer 为 lawyer_user.id
+     * @param ownerType {@link shop.alien.entity.store.PushDeviceOwnerType#getCode()}:user / store / lawyer
+     */
+    List<String> listPushClientIdsByUserId(int userId, String ownerType);
+}

+ 106 - 1
alien-store/src/main/java/shop/alien/store/service/LifeUserService.java

@@ -9,6 +9,7 @@ import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.tuple.Triple;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -24,6 +25,8 @@ import shop.alien.entity.second.SecondRiskControlRecord;
 import shop.alien.entity.store.LifeFans;
 import shop.alien.entity.store.LifeNotice;
 import shop.alien.entity.store.LifeUser;
+import shop.alien.entity.store.PushDeviceOwnerType;
+import shop.alien.entity.store.StoreUser;
 import shop.alien.entity.store.vo.LifeMessageVo;
 import shop.alien.entity.store.vo.LifeUserVo;
 import shop.alien.entity.store.vo.WebSocketVo;
@@ -31,6 +34,7 @@ import shop.alien.mapper.LifeFansMapper;
 import shop.alien.mapper.LifeMessageMapper;
 import shop.alien.mapper.LifeNoticeMapper;
 import shop.alien.mapper.LifeUserMapper;
+import shop.alien.mapper.StoreUserMapper;
 import shop.alien.mapper.second.LifeUserLogMapper;
 import shop.alien.mapper.second.SecondRiskControlRecordMapper;
 import shop.alien.mapper.second.SecondUserCreditMapper;
@@ -48,10 +52,15 @@ import java.util.stream.Collectors;
 /**
  * 用户
  */
+@Slf4j
 @Service
 @RequiredArgsConstructor
 public class LifeUserService extends ServiceImpl<LifeUserMapper, LifeUser> {
 
+    /** App 点击推送后跳转页面(与动态点赞通知一致) */
+    private static final String FOLLOW_APP_PUSH_OPEN_PATH =
+            "pages/secondHandTransactions/pages/message/noticesAndMessage";
+
     private final LifeUserMapper lifeUserMapper;
 
     private final LifeFansMapper lifeFansMapper;
@@ -74,12 +83,26 @@ public class LifeUserService extends ServiceImpl<LifeUserMapper, LifeUser> {
 
     private final SecondRiskControlRecordMapper secondRiskControlRecordMapper;
 
+    private final LifeUserPersonalizationSettingService lifeUserPersonalizationSettingService;
+
+    private final LifeUserPushDeviceService lifeUserPushDeviceService;
+
+    private final UniPushCloudInvokeService uniPushCloudInvokeService;
+
+    private final StoreUserMapper storeUserMapper;
+
     @Autowired
     private RiskControlProperties riskControlProperties;
 
     @Value("${jwt.expiration-time}")
     private String effectiveTime;
 
+    /**
+     * 系统app通知开关
+     */
+    @Value("${alien.unipush.enabled:false}")
+    private boolean uniPushOn;
+
     public IPage<LifeUser> getStoresPage(int page, int size, String realName, String userPhone) {
         IPage<LifeUser> storePage = new Page<>(page, size);
         QueryWrapper<LifeUser> queryWrapper = new QueryWrapper<>();
@@ -126,12 +149,94 @@ public class LifeUserService extends ServiceImpl<LifeUserMapper, LifeUser> {
             if (!CollectionUtils.isEmpty(userList)) {
                 notice.setBusinessId(userList.get(0).getId());
             }
-            lifeNoticeMapper.insert(notice);
+            Integer followedLifeUserId = resolveLifeUserIdFromFollowedId(fans.getFollowedId());
+            boolean suppressNotice = followedLifeUserId != null
+                    && lifeUserPersonalizationSettingService.shouldSuppressFollowRelatedNotice(followedLifeUserId);
+            if (!suppressNotice) {
+                lifeNoticeMapper.insert(notice);
+
+                // 发送系统通知开关
+                if(uniPushOn) {
+                    try {
+                        sendFollowRelationAppPush(fans.getFollowedId());
+                    } catch (Exception e) {
+                        log.warn("关注 App 推送失败,followedId={},err={}", fans.getFollowedId(), e.getMessage());
+                    }
+                }
+            }
         }
 
         return num;
     }
 
+    /**
+     * 关注站内通知写入成功后,向被关注方已绑定 cid 的设备发 App 系统消息;与站内信条件一致,失败不影响关注与通知入库。
+     */
+    private void sendFollowRelationAppPush(String followedId) {
+        if (StringUtils.isBlank(followedId)) {
+            return;
+        }
+        List<String> cids = resolvePushClientIdsForFollowReceiver(followedId.trim());
+        if (CollectionUtils.isEmpty(cids)) {
+            return;
+        }
+        Map<String, Object> payload = new HashMap<>(8);
+        payload.put("scene", "follow");
+        payload.put("noticeType", 0);
+        payload.put("path", FOLLOW_APP_PUSH_OPEN_PATH);
+        uniPushCloudInvokeService.sendToClientIds(cids, "关注通知", "关注了你", payload);
+    }
+
+    private List<String> resolvePushClientIdsForFollowReceiver(String phoneId) {
+        if (phoneId.startsWith("user_")) {
+            String phone = phoneId.substring("user_".length());
+            if (StringUtils.isBlank(phone)) {
+                return Collections.emptyList();
+            }
+            LifeUser u = lifeUserMapper.selectOne(new LambdaQueryWrapper<LifeUser>()
+                    .eq(LifeUser::getUserPhone, phone)
+                    .eq(LifeUser::getDeleteFlag, 0)
+                    .last("LIMIT 1"));
+            if (u == null) {
+                return Collections.emptyList();
+            }
+            return lifeUserPushDeviceService.listPushClientIdsByUserId(u.getId(), PushDeviceOwnerType.USER.getCode());
+        }
+        if (phoneId.startsWith("store_")) {
+            String phone = phoneId.substring("store_".length());
+            if (StringUtils.isBlank(phone)) {
+                return Collections.emptyList();
+            }
+            StoreUser su = storeUserMapper.selectOne(new LambdaQueryWrapper<StoreUser>()
+                    .eq(StoreUser::getPhone, phone)
+                    .eq(StoreUser::getDeleteFlag, 0)
+                    .last("LIMIT 1"));
+            if (su == null) {
+                return Collections.emptyList();
+            }
+            return lifeUserPushDeviceService.listPushClientIdsByUserId(su.getId(), PushDeviceOwnerType.STORE.getCode());
+        }
+        return Collections.emptyList();
+    }
+
+    /**
+     * followedId 为 user_手机号 时解析为 life_user.id;店铺等非用户被关注方返回 null(不读个性化表,照常发通知)
+     */
+    private Integer resolveLifeUserIdFromFollowedId(String followedId) {
+        if (StringUtils.isBlank(followedId) || !followedId.startsWith("user_")) {
+            return null;
+        }
+        String phone = followedId.substring("user_".length());
+        if (StringUtils.isBlank(phone)) {
+            return null;
+        }
+        LifeUser u = lifeUserMapper.selectOne(new LambdaQueryWrapper<LifeUser>()
+                .eq(LifeUser::getUserPhone, phone)
+                .eq(LifeUser::getDeleteFlag, 0)
+                .last("LIMIT 1"));
+        return u != null ? u.getId() : null;
+    }
+
     public int cancelFans(LifeFans fans) {
         LambdaUpdateWrapper<LifeFans> wrapper = new LambdaUpdateWrapper<>();
         wrapper.eq(LifeFans::getFansId, fans.getFansId());

+ 82 - 0
alien-store/src/main/java/shop/alien/store/service/UniPushCloudInvokeService.java

@@ -0,0 +1,82 @@
+package shop.alien.store.service;
+
+import com.alibaba.fastjson2.JSONObject;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.http.client.SimpleClientHttpRequestFactory;
+import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestTemplate;
+import shop.alien.store.config.UniPushProperties;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 调用 uniCloud URL 化云函数触发推送。请求体需与云函数内解析字段一致(示例见类注释)。
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class UniPushCloudInvokeService {
+
+    private final UniPushProperties properties;
+
+    /**
+     * 向云函数 POST JSON,默认结构(云函数内自行读取并调用 uniPush.sendMessage):
+     * <pre>
+     * {
+     *   "push_clientid": ["cid1","cid2"],
+     *   "title": "...",
+     *   "content": "...",
+     *   "payload": { }  // 可选,自定义透传
+     * }
+     * </pre>
+     */
+    public String sendToClientIds(List<String> pushClientIds, String title, String content, Map<String, Object> payload) {
+        if (!properties.isEnabled() || StringUtils.isBlank(properties.getCloudFunctionUrl())) {
+            log.debug("uni-push 云函数调用未启用或未配置 cloudFunctionUrl,跳过");
+            return null;
+        }
+        if (pushClientIds == null || pushClientIds.isEmpty()) {
+            log.warn("uni-push 推送跳过:pushClientIds 为空");
+            return null;
+        }
+
+        JSONObject body = new JSONObject();
+        body.put("push_clientid", pushClientIds);
+        body.put("title", title != null ? title : "");
+        body.put("content", content != null ? content : "");
+        if (payload != null && !payload.isEmpty()) {
+            body.put("payload", payload);
+        }
+
+        SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
+        factory.setConnectTimeout(properties.getConnectTimeoutMs());
+        factory.setReadTimeout(properties.getReadTimeoutMs());
+        RestTemplate restTemplate = new RestTemplate(factory);
+
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(MediaType.APPLICATION_JSON);
+        if (StringUtils.isNotBlank(properties.getAuthHeaderName())
+                && StringUtils.isNotBlank(properties.getAuthHeaderValue())) {
+            headers.set(properties.getAuthHeaderName(), properties.getAuthHeaderValue());
+        }
+
+        HttpEntity<String> entity = new HttpEntity<>(body.toJSONString(), headers);
+        try {
+            ResponseEntity<String> resp = restTemplate.postForEntity(
+                    properties.getCloudFunctionUrl(), entity, String.class);
+            String respBody = resp.getBody();
+            log.info("uni-push 云函数调用完成, status={}, body={}", resp.getStatusCode(), respBody);
+            return respBody;
+        } catch (Exception e) {
+            log.error("uni-push 云函数调用失败: {}", e.getMessage(), e);
+            throw e;
+        }
+    }
+}

+ 1 - 0
alien-store/src/main/java/shop/alien/store/service/impl/AlipayZftOnboardingServiceImpl.java

@@ -271,6 +271,7 @@ public class AlipayZftOnboardingServiceImpl implements AlipayZftOnboardingServic
         if (response != null) {
             record.setSubCode(response.getSubCode());
             record.setSubMsg(response.getSubMsg());
+            record.setOrderId(request1 != null ? response.getOrderId() : null);
         }
         alipayZftCreateRecordMapper.insert(record);
 

+ 119 - 0
alien-store/src/main/java/shop/alien/store/service/impl/CommonRatingServiceImpl.java

@@ -22,6 +22,8 @@ import shop.alien.entity.result.R;
 import shop.alien.entity.store.*;
 import shop.alien.entity.store.vo.CommonCommentVo;
 import shop.alien.entity.store.vo.CommonRatingVo;
+import shop.alien.entity.store.vo.MyMergedReviewItemVo;
+import shop.alien.entity.store.vo.OrderReviewVo;
 import shop.alien.entity.store.vo.StoreInfoScoreVo;
 import shop.alien.entity.store.vo.WebSocketVo;
 import shop.alien.entity.storePlatform.StoreOperationalActivity;
@@ -31,6 +33,7 @@ import shop.alien.util.common.Constants;
 import shop.alien.store.config.WebSocketProcess;
 import shop.alien.store.service.CommonCommentService;
 import shop.alien.store.service.CommonRatingService;
+import shop.alien.store.service.OrderReviewService;
 import shop.alien.store.service.LifeDiscountCouponStoreFriendService;
 import shop.alien.store.util.CommonConstant;
 import shop.alien.store.util.ai.AiContentModerationUtil;
@@ -88,6 +91,7 @@ public class CommonRatingServiceImpl extends ServiceImpl<CommonRatingMapper, Com
     private final StoreOperationalActivityMapper storeOperationalActivityMapper;
     private final ReceiptAuditUtil receiptAuditUtil;
     private final StoreClockInMapper storeClockInMapper;
+    private final OrderReviewService orderReviewService;
 
     public static final List<String> SERVICES_LIST = ImmutableList.of(
             TextReviewServiceEnum.COMMENT_DETECTION_PRO.getService(),
@@ -1132,6 +1136,121 @@ public class CommonRatingServiceImpl extends ServiceImpl<CommonRatingMapper, Com
         return iPageR;
     }
 
+    @Override
+    public R<IPage<MyMergedReviewItemVo>> getMyMergedRatingList(Integer pageNum, Integer pageSize, Integer businessType,
+                                                                Long userId, Integer auditStatus, Integer currentUserId) {
+        if (userId == null) {
+            return R.fail("userId不能为空");
+        }
+        int pn = pageNum == null || pageNum < 1 ? 1 : pageNum;
+        int ps = pageSize == null || pageSize < 1 ? 10 : pageSize;
+        int perSource = Math.min(pn * ps, 1000);
+        int biz = businessType != null ? businessType : RatingBusinessTypeEnum.STORE_RATING.getBusinessType();
+
+        List<MyMergedReviewItemVo> merged = new ArrayList<>();
+
+        R storeR = getMyRatingList(1, perSource, biz, userId, auditStatus);
+        if (R.isSuccess(storeR) && storeR.getData() != null) {
+            IPage<?> sp = (IPage<?>) storeR.getData();
+            for (Object o : sp.getRecords()) {
+                if (o instanceof CommonRatingVo) {
+                    merged.add(toMergedStoreItem((CommonRatingVo) o));
+                }
+            }
+        }
+
+        R<IPage<OrderReviewVo>> lawyerR = orderReviewService.getMyReviewList(1, perSource, userId.intValue(), currentUserId);
+        if (R.isSuccess(lawyerR) && lawyerR.getData() != null && lawyerR.getData().getRecords() != null) {
+            for (OrderReviewVo vo : lawyerR.getData().getRecords()) {
+                merged.add(toMergedLawyerItem(vo));
+            }
+        }
+
+        merged.sort(Comparator.comparing(MyMergedReviewItemVo::getPublishTime,
+                Comparator.nullsLast(Comparator.naturalOrder())).reversed());
+
+        long storeTotal = 0L;
+        if (R.isSuccess(storeR) && storeR.getData() != null) {
+            storeTotal = ((IPage<?>) storeR.getData()).getTotal();
+        }
+        long lawyerTotal = 0L;
+        if (R.isSuccess(lawyerR) && lawyerR.getData() != null) {
+            lawyerTotal = lawyerR.getData().getTotal();
+        }
+        long total = storeTotal + lawyerTotal;
+
+        int from = (pn - 1) * ps;
+        List<MyMergedReviewItemVo> pageSlice;
+        if (from >= merged.size()) {
+            pageSlice = Collections.emptyList();
+        } else {
+            int to = Math.min(from + ps, merged.size());
+            pageSlice = merged.subList(from, to);
+        }
+
+        Page<MyMergedReviewItemVo> page = new Page<>(pn, ps, total);
+        page.setRecords(pageSlice);
+        return R.data(page);
+    }
+
+    private static MyMergedReviewItemVo toMergedStoreItem(CommonRatingVo vo) {
+        MyMergedReviewItemVo m = new MyMergedReviewItemVo();
+        m.setItemType("STORE");
+        m.setStoreRatingId(vo.getId());
+        m.setPublishTime(vo.getCreatedTime());
+        m.setScore(vo.getScore());
+        m.setContent(vo.getContent());
+        m.setMediaUrls(splitImageUrlsToMediaList(vo.getImageUrls()));
+        m.setStoreId(vo.getBusinessId());
+        m.setStoreName(vo.getStoreName());
+        m.setStoreIcon(null);
+        m.setBusinessType(vo.getBusinessType());
+        return m;
+    }
+
+    private static MyMergedReviewItemVo toMergedLawyerItem(OrderReviewVo vo) {
+        MyMergedReviewItemVo m = new MyMergedReviewItemVo();
+        m.setItemType("LAWYER");
+        m.setLawyerReviewId(vo.getId());
+        m.setPublishTime(vo.getCreatedTime());
+        m.setScore(vo.getOverallRating());
+        m.setContent(vo.getReviewContent());
+        m.setMediaUrls(vo.getReviewImages() != null ? new ArrayList<>(vo.getReviewImages()) : new ArrayList<>());
+        m.setLawyerName(vo.getLawyerName());
+        m.setLawyerAvatar(vo.getLawyerAvatar());
+        m.setLawyerFirm(vo.getLawFirmName());
+        m.setRatingTag(lawyerRatingTagText(vo.getOverallRating()));
+        m.setLawyerUserId(vo.getLawyerUserId());
+        m.setOrderId(vo.getOrderId());
+        return m;
+    }
+
+    private static List<String> splitImageUrlsToMediaList(String imageUrls) {
+        if (imageUrls == null || imageUrls.trim().isEmpty()) {
+            return new ArrayList<>();
+        }
+        return Arrays.stream(imageUrls.split(","))
+                .map(String::trim)
+                .filter(s -> !s.isEmpty())
+                .collect(Collectors.toList());
+    }
+
+    private static String lawyerRatingTagText(Double overallRating) {
+        if (overallRating == null) {
+            return "我给出评价";
+        }
+        if (overallRating >= 4.5D) {
+            return "我给出超赞";
+        }
+        if (overallRating >= 3.5D) {
+            return "我给出好评";
+        }
+        if (overallRating >= 2.5D) {
+            return "我给出中评";
+        }
+        return "我给出差评";
+    }
+
 /*
     @Override
     public Double getAverageScore(Integer businessType, Long businessId) {

+ 279 - 0
alien-store/src/main/java/shop/alien/store/service/impl/LifeUserPersonalizationSettingServiceImpl.java

@@ -0,0 +1,279 @@
+package shop.alien.store.service.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.core.toolkit.StringUtils;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.dao.DataIntegrityViolationException;
+import org.springframework.stereotype.Service;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.LifeUserPersonalizationSetting;
+import shop.alien.mapper.LifeUserPersonalizationSettingMapper;
+import shop.alien.store.config.BaseRedisService;
+import shop.alien.store.service.LifeUserPersonalizationSettingService;
+
+@Service
+@Slf4j
+public class LifeUserPersonalizationSettingServiceImpl
+        extends ServiceImpl<LifeUserPersonalizationSettingMapper, LifeUserPersonalizationSetting>
+        implements LifeUserPersonalizationSettingService {
+
+    private static final String CACHE_KEY_PREFIX = "alien:store:life:user:personalization:";
+    private static final long CACHE_TTL_SECONDS = 7 * 24 * 3600L;
+
+    @Autowired
+    private BaseRedisService baseRedisService;
+
+    @Override
+    public R<String> add(LifeUserPersonalizationSetting setting) {
+        log.info("LifeUserPersonalizationSettingServiceImpl.add, param={}", setting);
+        R<String> validated = validateAndApplyFontRule(setting, null);
+        if (validated != null) {
+            return validated;
+        }
+        boolean result = this.save(setting);
+        if (result) {
+            invalidatePersonalizationCache(setting.getUserId());
+            return R.success("新增成功");
+        }
+        return R.fail("新增失败");
+    }
+
+    @Override
+    public R<String> deleteById(Integer id) {
+        log.info("LifeUserPersonalizationSettingServiceImpl.deleteById, id={}", id);
+        LifeUserPersonalizationSetting old = id == null ? null : this.getById(id);
+        boolean result = this.removeById(id);
+        if (result) {
+            if (old != null) {
+                invalidatePersonalizationCache(old.getUserId());
+            }
+            return R.success("删除成功");
+        }
+        return R.fail("删除失败");
+    }
+
+    @Override
+    public R<String> update(LifeUserPersonalizationSetting setting) {
+        log.info("LifeUserPersonalizationSettingServiceImpl.update, param={}", setting);
+        LifeUserPersonalizationSetting existing;
+
+        if (setting.getId() != null) {
+            existing = this.getById(setting.getId());
+            if (existing == null) {
+                return R.fail("记录不存在");
+            }
+            if (setting.getUserId() != null && !setting.getUserId().equals(existing.getUserId())) {
+                return R.fail("userId与记录不一致");
+            }
+        } else if (setting.getUserId() != null) {
+            LambdaQueryWrapper<LifeUserPersonalizationSetting> w = new LambdaQueryWrapper<>();
+            w.eq(LifeUserPersonalizationSetting::getUserId, setting.getUserId());
+            existing = this.getOne(w);
+            if (existing == null) {
+                LifeUserPersonalizationSetting created = buildDefaultForUser(setting.getUserId());
+                R<String> validatedInit = validateAndApplyFontRule(created, null);
+                if (validatedInit != null) {
+                    return validatedInit;
+                }
+                try {
+                    this.save(created);
+                    existing = created;
+                } catch (DataIntegrityViolationException e) {
+                    log.warn("LifeUserPersonalizationSetting update 并发初始化 userId={}", setting.getUserId(), e);
+                    existing = this.getOne(w);
+                    if (existing == null) {
+                        return R.fail("记录不存在");
+                    }
+                }
+            }
+            setting.setId(existing.getId());
+        } else {
+            return R.fail("id与userId至少填写一个");
+        }
+
+        R<String> validated = validateAndApplyFontRule(setting, existing);
+        if (validated != null) {
+            return validated;
+        }
+        boolean result = this.updateById(setting);
+        if (result) {
+            Integer uid = setting.getUserId() != null ? setting.getUserId() : existing.getUserId();
+            invalidatePersonalizationCache(uid);
+            return R.success("更新成功");
+        }
+        return R.fail("更新失败");
+    }
+
+    /**
+     * followSystemFont=1 跟随系统:不采纳请求中的 chatFontLevel,沿用库中已有值(新增时为 0)。
+     * followSystemFont=0:chatFontLevel 必须在 0~4(缺省按 0)。
+     */
+    private R<String> validateAndApplyFontRule(LifeUserPersonalizationSetting incoming, LifeUserPersonalizationSetting existing) {
+        int followSys = incoming.getFollowSystemFont() != null
+                ? incoming.getFollowSystemFont()
+                : (existing != null && existing.getFollowSystemFont() != null ? existing.getFollowSystemFont() : 1);
+        if (followSys != 0 && followSys != 1) {
+            return R.fail("followSystemFont 只能为 0 或 1");
+        }
+        if (followSys == 1) {
+            Integer keep = existing != null ? existing.getChatFontLevel() : null;
+            incoming.setChatFontLevel(keep != null ? keep : 0);
+            return null;
+        }
+        int level = incoming.getChatFontLevel() != null
+                ? incoming.getChatFontLevel()
+                : (existing != null && existing.getChatFontLevel() != null ? existing.getChatFontLevel() : 0);
+        if (level < 0 || level > 4) {
+            return R.fail("chatFontLevel 取值 0~4");
+        }
+        incoming.setChatFontLevel(level);
+        return null;
+    }
+
+    @Override
+    public R<LifeUserPersonalizationSetting> getInfoById(Integer id) {
+        log.info("LifeUserPersonalizationSettingServiceImpl.getInfoById, id={}", id);
+        return R.data(this.getById(id));
+    }
+
+    @Override
+    public R<LifeUserPersonalizationSetting> getByUserId(Integer userId) {
+        log.info("LifeUserPersonalizationSettingServiceImpl.getByUserId, userId={}", userId);
+        if (userId == null) {
+            return R.fail("userId不能为空");
+        }
+        LambdaQueryWrapper<LifeUserPersonalizationSetting> w = new LambdaQueryWrapper<>();
+        w.eq(LifeUserPersonalizationSetting::getUserId, userId);
+        LifeUserPersonalizationSetting one = this.getOne(w);
+        if (one != null) {
+            return R.data(one);
+        }
+        LifeUserPersonalizationSetting created = buildDefaultForUser(userId);
+        R<String> validated = validateAndApplyFontRule(created, null);
+        if (validated != null) {
+            return R.fail(validated.getMsg());
+        }
+        try {
+            this.save(created);
+            return R.data(created);
+        } catch (DataIntegrityViolationException e) {
+            log.warn("LifeUserPersonalizationSetting getByUserId 并发插入 userId={}", userId, e);
+            LifeUserPersonalizationSetting again = this.getOne(w);
+            if (again != null) {
+                return R.data(again);
+            }
+            return R.fail("初始化个性化设置失败");
+        }
+    }
+
+    @Override
+    public LifeUserPersonalizationSetting getByUserIdCacheAside(Integer userId) {
+        if (userId == null) {
+            return null;
+        }
+        String key = cacheKey(userId);
+        String cached = baseRedisService.getString(key);
+        if (StringUtils.isNotEmpty(cached)) {
+            try {
+                LifeUserPersonalizationSetting parsed = JSON.parseObject(cached, LifeUserPersonalizationSetting.class);
+                if (parsed != null) {
+                    return parsed;
+                }
+            } catch (Exception e) {
+                log.warn("LifeUserPersonalizationSetting 缓存反序列化失败,删除 key,userId={}", userId, e);
+                baseRedisService.delete(key);
+            }
+        }
+        R<LifeUserPersonalizationSetting> r = getByUserId(userId);
+        if (!R.isSuccess(r) || r.getData() == null) {
+            return null;
+        }
+        LifeUserPersonalizationSetting data = r.getData();
+        try {
+            baseRedisService.setString(key, JSON.toJSONString(data), CACHE_TTL_SECONDS);
+        } catch (Exception e) {
+            log.warn("LifeUserPersonalizationSetting 写入缓存失败,userId={}", userId, e);
+        }
+        return data;
+    }
+
+    @Override
+    public boolean shouldSuppressLikeRelatedNotice(Integer userId) {
+        if (userId == null) {
+            return false;
+        }
+        LifeUserPersonalizationSetting s = getByUserIdCacheAside(userId);
+        if (s == null) {
+            return false;
+        }
+        Integer nrm = s.getNotifyReceiveMessage();
+        Integer nl = s.getNotifyLike();
+        if (nrm != null && nrm == 0) {
+            return true;
+        }
+        return nrm != null && nrm == 1 && nl != null && nl == 0;
+    }
+
+    @Override
+    public boolean shouldSuppressFollowRelatedNotice(Integer userId) {
+        if (userId == null) {
+            return false;
+        }
+        LifeUserPersonalizationSetting s = getByUserIdCacheAside(userId);
+        if (s == null) {
+            return false;
+        }
+        Integer nrm = s.getNotifyReceiveMessage();
+        Integer nf = s.getNotifyFollow();
+        if (nrm != null && nrm == 0) {
+            return true;
+        }
+        return nrm != null && nrm == 1 && nf != null && nf == 0;
+    }
+
+    private static String cacheKey(Integer userId) {
+        return CACHE_KEY_PREFIX + userId;
+    }
+
+    private void invalidatePersonalizationCache(Integer userId) {
+        if (userId == null) {
+            return;
+        }
+        baseRedisService.delete(cacheKey(userId));
+    }
+
+    /** 与表默认值一致,便于无记录时落库 */
+    private static LifeUserPersonalizationSetting buildDefaultForUser(Integer userId) {
+        LifeUserPersonalizationSetting s = new LifeUserPersonalizationSetting();
+        s.setUserId(userId);
+        s.setPersonalizedRecommendation(1);
+        s.setOnlyFollowersComment(1);
+        s.setHideFansList(1);
+        s.setHideFollowList(1);
+        s.setNotifyReceiveMessage(1);
+        s.setNotifyLike(1);
+        s.setNotifyFollow(1);
+        s.setNotifyComment(1);
+        s.setFollowSystemFont(1);
+        s.setChatFontLevel(0);
+        s.setAutoRefresh(1);
+        return s;
+    }
+
+    @Override
+    public R<IPage<LifeUserPersonalizationSetting>> list(Integer pageNum, Integer pageSize, Integer userId) {
+        log.info("LifeUserPersonalizationSettingServiceImpl.list, pageNum={}, pageSize={}, userId={}", pageNum, pageSize, userId);
+        Page<LifeUserPersonalizationSetting> page = new Page<>(pageNum, pageSize);
+        LambdaQueryWrapper<LifeUserPersonalizationSetting> w = new LambdaQueryWrapper<>();
+        if (userId != null) {
+            w.eq(LifeUserPersonalizationSetting::getUserId, userId);
+        }
+        w.orderByDesc(LifeUserPersonalizationSetting::getUpdatedTime);
+        return R.data(this.page(page, w));
+    }
+}

+ 194 - 0
alien-store/src/main/java/shop/alien/store/service/impl/LifeUserPushDeviceServiceImpl.java

@@ -0,0 +1,194 @@
+package shop.alien.store.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.LawyerUser;
+import shop.alien.entity.store.LifeUser;
+import shop.alien.entity.store.LifeUserPushDevice;
+import shop.alien.entity.store.PushDeviceOwnerType;
+import shop.alien.entity.store.StoreUser;
+import shop.alien.entity.store.UserLoginInfo;
+import shop.alien.mapper.LawyerUserMapper;
+import shop.alien.mapper.LifeUserMapper;
+import shop.alien.mapper.LifeUserPushDeviceMapper;
+import shop.alien.mapper.StoreUserMapper;
+import shop.alien.store.dto.LifeUserPushBindDto;
+import shop.alien.store.service.LifeUserPushDeviceService;
+
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class LifeUserPushDeviceServiceImpl implements LifeUserPushDeviceService {
+
+    private final LifeUserPushDeviceMapper lifeUserPushDeviceMapper;
+    private final LifeUserMapper lifeUserMapper;
+    private final StoreUserMapper storeUserMapper;
+    private final LawyerUserMapper lawyerUserMapper;
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public R<String> bind(UserLoginInfo login, LifeUserPushBindDto dto) {
+        if (login == null || login.getUserId() <= 0) {
+            return R.fail("请先登录");
+        }
+        PushDeviceOwnerType ownerType = PushDeviceOwnerType.fromJwtUserType(login.getType());
+        if (ownerType == null) {
+            return R.fail("当前登录类型不支持绑定推送设备");
+        }
+        if (dto == null || StringUtils.isBlank(dto.getPushClientId())) {
+            return R.fail("pushClientId 不能为空");
+        }
+        String cid = StringUtils.trim(dto.getPushClientId());
+        if (cid.length() > 128) {
+            return R.fail("pushClientId 过长");
+        }
+
+        R<String> valid = validateOwnerExists(ownerType, login.getUserId());
+        if (valid != null) {
+            return valid;
+        }
+
+        LifeUserPushDevice existing = lifeUserPushDeviceMapper.selectOne(
+                new LambdaQueryWrapper<LifeUserPushDevice>()
+                        .eq(LifeUserPushDevice::getPushClientId, cid)
+                        .last("LIMIT 1"));
+
+        Date now = new Date();
+        String platform = StringUtils.trimToNull(dto.getPlatform());
+        String appId = StringUtils.trimToNull(dto.getDcloudAppId());
+        if (platform != null && platform.length() > 32) {
+            platform = platform.substring(0, 32);
+        }
+        if (appId != null && appId.length() > 64) {
+            appId = appId.substring(0, 64);
+        }
+
+        String typeCode = ownerType.getCode();
+        if (existing != null) {
+            existing.setUserId(login.getUserId());
+            existing.setOwnerType(typeCode);
+            existing.setPlatform(platform);
+            existing.setDcloudAppId(appId);
+            existing.setUpdatedTime(now);
+            lifeUserPushDeviceMapper.updateById(existing);
+            log.info("更新推送设备绑定, ownerType={}, userId={}, cid={}", typeCode, login.getUserId(), cid);
+        } else {
+            LifeUserPushDevice row = new LifeUserPushDevice();
+            row.setUserId(login.getUserId());
+            row.setOwnerType(typeCode);
+            row.setPushClientId(cid);
+            row.setPlatform(platform);
+            row.setDcloudAppId(appId);
+            row.setCreatedTime(now);
+            row.setUpdatedTime(now);
+            lifeUserPushDeviceMapper.insert(row);
+            log.info("新增推送设备绑定, ownerType={}, userId={}, cid={}", typeCode, login.getUserId(), cid);
+        }
+        return R.success("绑定成功");
+    }
+
+    /**
+     * @return null 表示校验通过
+     */
+    private R<String> validateOwnerExists(PushDeviceOwnerType ownerType, int businessUserId) {
+        switch (ownerType) {
+            case USER:
+                LifeUser user = lifeUserMapper.selectById(businessUserId);
+                if (user == null) {
+                    return R.fail("用户不存在");
+                }
+                break;
+            case STORE:
+                StoreUser su = storeUserMapper.selectById(businessUserId);
+                if (su == null || (su.getDeleteFlag() != null && su.getDeleteFlag() != 0)) {
+                    return R.fail("门店用户不存在或已删除");
+                }
+                break;
+            case LAWYER:
+                LawyerUser lu = lawyerUserMapper.selectById(businessUserId);
+                if (lu == null || (lu.getDeleteFlag() != null && lu.getDeleteFlag() != 0)) {
+                    return R.fail("律师用户不存在或已删除");
+                }
+                break;
+            default:
+                return R.fail("不支持的归属类型");
+        }
+        return null;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public R<String> unbindByCid(UserLoginInfo login, String pushClientId) {
+        if (login == null || login.getUserId() <= 0) {
+            return R.fail("请先登录");
+        }
+        PushDeviceOwnerType ownerType = PushDeviceOwnerType.fromJwtUserType(login.getType());
+        if (ownerType == null) {
+            return R.fail("当前登录类型不支持解绑推送设备");
+        }
+        if (StringUtils.isBlank(pushClientId)) {
+            return R.fail("pushClientId 不能为空");
+        }
+        String cid = StringUtils.trim(pushClientId);
+        int n = lifeUserPushDeviceMapper.delete(
+                new LambdaQueryWrapper<LifeUserPushDevice>()
+                        .eq(LifeUserPushDevice::getOwnerType, ownerType.getCode())
+                        .eq(LifeUserPushDevice::getUserId, login.getUserId())
+                        .eq(LifeUserPushDevice::getPushClientId, cid));
+        if (n <= 0) {
+            return R.fail("未找到绑定记录");
+        }
+        return R.success("解绑成功");
+    }
+
+    @Override
+    public R<List<LifeUserPushDevice>> listMine(UserLoginInfo login) {
+        if (login == null || login.getUserId() <= 0) {
+            return R.fail("请先登录");
+        }
+        PushDeviceOwnerType ownerType = PushDeviceOwnerType.fromJwtUserType(login.getType());
+        if (ownerType == null) {
+            return R.fail("当前登录类型不支持查询推送设备");
+        }
+        List<LifeUserPushDevice> list = lifeUserPushDeviceMapper.selectList(
+                new LambdaQueryWrapper<LifeUserPushDevice>()
+                        .eq(LifeUserPushDevice::getOwnerType, ownerType.getCode())
+                        .eq(LifeUserPushDevice::getUserId, login.getUserId())
+                        .orderByDesc(LifeUserPushDevice::getUpdatedTime));
+        return R.data(list);
+    }
+
+    @Override
+    public List<String> listPushClientIdsByUserId(int userId, String ownerType) {
+        if (userId <= 0) {
+            return Collections.emptyList();
+        }
+        PushDeviceOwnerType ot = PushDeviceOwnerType.fromCode(ownerType);
+        if (ot == null) {
+            return Collections.emptyList();
+        }
+        List<LifeUserPushDevice> list = lifeUserPushDeviceMapper.selectList(
+                new LambdaQueryWrapper<LifeUserPushDevice>()
+                        .eq(LifeUserPushDevice::getOwnerType, ot.getCode())
+                        .eq(LifeUserPushDevice::getUserId, userId)
+                        .select(LifeUserPushDevice::getPushClientId));
+        if (list == null || list.isEmpty()) {
+            return Collections.emptyList();
+        }
+        return list.stream()
+                .map(LifeUserPushDevice::getPushClientId)
+                .filter(StringUtils::isNotBlank)
+                .distinct()
+                .collect(Collectors.toList());
+    }
+}

+ 6 - 0
alien-store/src/main/java/shop/alien/store/service/impl/StoreClockInServiceImpl.java

@@ -458,6 +458,7 @@ public class StoreClockInServiceImpl extends ServiceImpl<StoreClockInMapper, Sto
         StoreInfo storeInfo = storeInfoMapper.selectById(storeClockIn.getStoreId());
         // 店铺名称
         storeClockInVo.setStoreName(storeInfo.getStoreName());
+        storeClockInVo.setStoreBlurb(storeInfo.getStoreBlurb());
         // 店铺头像
         LambdaQueryWrapper<StoreImg> eq = new LambdaQueryWrapper<StoreImg>().eq(StoreImg::getImgType, 10).eq(StoreImg::getStoreId, storeInfo.getId());
         StoreImg storeImg = storeImgMapper.selectOne(eq);
@@ -488,6 +489,11 @@ public class StoreClockInServiceImpl extends ServiceImpl<StoreClockInMapper, Sto
                 storeClockInVo.setIsCollect("1");
             }
         }
+
+        if(storeInfo.getBusinessSection() != null){
+            storeClockInVo.setBusinessSection(storeInfo.getBusinessSection().toString());
+        }
+        storeClockInVo.setBusinessSectionName(storeInfo.getBusinessSectionName());
         return storeClockInVo;
     }
 }