Просмотр исходного кода

Merge branch 'sit' into sit-lutong-dev

# Conflicts:
#	alien-gateway/src/main/java/shop/alien/gateway/service/LifeUserService.java
lutong 10 часов назад
Родитель
Сommit
c196f9f7e1
42 измененных файлов с 2356 добавлено и 114 удалено
  1. 21 2
      alien-entity/src/main/java/shop/alien/entity/store/LawyerUser.java
  2. 15 0
      alien-entity/src/main/java/shop/alien/entity/store/LifeUser.java
  3. 230 0
      alien-entity/src/main/java/shop/alien/entity/store/OceanEngineClickMonitorLog.java
  4. 16 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/StoreUserPasswordDto.java
  5. 9 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/LifeUserExpertVo.java
  6. 20 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/RegisteredLawyerUserCountVo.java
  7. 20 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/RegisteredStoreUserCountVo.java
  8. 20 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/RegisteredUserCountVo.java
  9. 12 10
      alien-entity/src/main/java/shop/alien/mapper/LifeFansMapper.java
  10. 1 1
      alien-entity/src/main/java/shop/alien/mapper/LifeUserExpertMapper.java
  11. 1 1
      alien-entity/src/main/java/shop/alien/mapper/LifeUserMapper.java
  12. 13 0
      alien-entity/src/main/java/shop/alien/mapper/OceanEngineClickMonitorLogMapper.java
  13. 2 1
      alien-entity/src/main/resources/mapper/OrderReviewMapper.xml
  14. 3 1
      alien-gateway/src/main/java/shop/alien/gateway/controller/LifeUserController.java
  15. 19 6
      alien-gateway/src/main/java/shop/alien/gateway/controller/StoreUserController.java
  16. 29 11
      alien-gateway/src/main/java/shop/alien/gateway/service/LifeUserService.java
  17. 10 35
      alien-lawyer/src/main/java/shop/alien/lawyer/controller/LawyerUserLogInController.java
  18. 5 0
      alien-lawyer/src/main/java/shop/alien/lawyer/service/LawyerUserLogInService.java
  19. 150 9
      alien-lawyer/src/main/java/shop/alien/lawyer/service/impl/LawyerUserLogInServiceImpl.java
  20. 17 4
      alien-store/src/main/java/shop/alien/store/controller/AliController.java
  21. 5 5
      alien-store/src/main/java/shop/alien/store/controller/LifeUserDynamicsController.java
  22. 114 0
      alien-store/src/main/java/shop/alien/store/controller/OceanEngineAdTrackController.java
  23. 79 0
      alien-store/src/main/java/shop/alien/store/controller/RegisteredUserStatisticsController.java
  24. 17 0
      alien-store/src/main/java/shop/alien/store/controller/StoreUserController.java
  25. 25 0
      alien-store/src/main/java/shop/alien/store/dto/OceanEngineClickBindUserDto.java
  26. 31 0
      alien-store/src/main/java/shop/alien/store/dto/OceanEngineConversionResultVo.java
  27. 87 0
      alien-store/src/main/java/shop/alien/store/service/LifeStoreService.java
  28. 17 1
      alien-store/src/main/java/shop/alien/store/service/LifeUserDynamicsService.java
  29. 51 9
      alien-store/src/main/java/shop/alien/store/service/LifeUserService.java
  30. 48 0
      alien-store/src/main/java/shop/alien/store/service/OceanEngineClickMonitorLogService.java
  31. 23 0
      alien-store/src/main/java/shop/alien/store/service/RegisteredUserStatisticsService.java
  32. 10 0
      alien-store/src/main/java/shop/alien/store/service/StoreUserPasswordService.java
  33. 18 2
      alien-store/src/main/java/shop/alien/store/service/channel/CommonPushVendorHttpClient.java
  34. 11 1
      alien-store/src/main/java/shop/alien/store/service/impl/AccountLogoutCompleteServiceImpl.java
  35. 1 1
      alien-store/src/main/java/shop/alien/store/service/impl/CommonCommentServiceImpl.java
  36. 617 0
      alien-store/src/main/java/shop/alien/store/service/impl/OceanEngineClickMonitorLogServiceImpl.java
  37. 70 0
      alien-store/src/main/java/shop/alien/store/service/impl/RegisteredUserStatisticsServiceImpl.java
  38. 3 1
      alien-store/src/main/java/shop/alien/store/service/impl/StoreLogoutSyncServiceImpl.java
  39. 54 0
      alien-store/src/main/java/shop/alien/store/service/impl/StoreUserPasswordServiceImpl.java
  40. 130 0
      alien-store/src/main/java/shop/alien/store/service/impl/StoreUserServiceImpl.java
  41. 7 13
      docs/devops/dev/Introduction.md
  42. 325 0
      docs/devops/dev/middleware/nginx/dev.conf

+ 21 - 2
alien-entity/src/main/java/shop/alien/entity/store/LawyerUser.java

@@ -7,8 +7,10 @@ import com.fasterxml.jackson.annotation.JsonInclude;
 import io.swagger.annotations.ApiModel;
 import io.swagger.annotations.ApiModelProperty;
 import lombok.Data;
+import shop.alien.util.encryption.EncryptTypeHandler;
 
 import java.time.LocalDate;
+import java.time.LocalDateTime;
 import java.time.ZoneId;
 import java.time.temporal.ChronoUnit;
 import java.util.Date;
@@ -22,7 +24,7 @@ import java.util.List;
  */
 @Data
 @JsonInclude
-@TableName("lawyer_user")
+@TableName(value = "lawyer_user", autoResultMap = true)
 @ApiModel(value = "LawyerUser对象", description = "律师用户")
 public class LawyerUser extends Model<LawyerUser> {
 
@@ -32,6 +34,8 @@ public class LawyerUser extends Model<LawyerUser> {
     public static final int LOGOUT_FLAG_DONE = 1;
     /** logout_flag:注销冷静期 */
     public static final int LOGOUT_FLAG_COOLING = 2;
+    /** 注销完成后禁止重新注册的天数(自 logoutTime 日起,T+14 00:00 起可注册) */
+    public static final int RE_REGISTER_BLOCK_DAYS = 14;
 
     public LawyerUser() {
     }
@@ -66,7 +70,7 @@ public class LawyerUser extends Model<LawyerUser> {
     private String idCard;
 
     @ApiModelProperty(value = "密码")
-    @TableField("password")
+    @TableField(value = "password", typeHandler = EncryptTypeHandler.class)
     private String password;
 
     @ApiModelProperty(value = "支付密码")
@@ -396,5 +400,20 @@ public class LawyerUser extends Model<LawyerUser> {
     @TableField("zfb_secondary_merchant_account")
     private String zfbSecondaryMerchantAccount;
 
+    /**
+     * 注销完成日 T(logoutTime)起,至 T+{@link #RE_REGISTER_BLOCK_DAYS} 日 00:00 前禁止重新注册/登录。
+     */
+    public static boolean isWithinReRegisterBlockPeriod(Date logoutTime) {
+        if (logoutTime == null) {
+            return false;
+        }
+        LocalDateTime allowFrom = logoutTime.toInstant()
+                .atZone(ZoneId.systemDefault())
+                .toLocalDate()
+                .plusDays(RE_REGISTER_BLOCK_DAYS)
+                .atStartOfDay();
+        return LocalDateTime.now().isBefore(allowFrom);
+    }
+
 }
 

+ 15 - 0
alien-entity/src/main/java/shop/alien/entity/store/LifeUser.java

@@ -131,6 +131,18 @@ public class LifeUser implements Serializable {
     @TableField("id_card")
     private String idCard;
 
+    @ApiModelProperty(value = "国籍")
+    @TableField("nationality")
+    private String nationality;
+
+    @ApiModelProperty(value = "英文名字")
+    @TableField("english_name")
+    private String englishName;
+
+    @ApiModelProperty(value = "证件有效期")
+    @TableField("expiration_date")
+    private String expirationDate;
+
     @ApiModelProperty(value = "邀请码")
     @TableField("invited_num")
     private String invitedNum;
@@ -209,4 +221,7 @@ public class LifeUser implements Serializable {
     @TableField("device_id")
     private String deviceId;
 
+    @ApiModelProperty(value = "ocean_idfa_or_oaid")
+    @TableField("ocean_idfa_or_oaid")
+    private String oceanIdfaOrOaid;
 }

+ 230 - 0
alien-entity/src/main/java/shop/alien/entity/store/OceanEngineClickMonitorLog.java

@@ -0,0 +1,230 @@
+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;
+
+/**
+ * 巨量引擎点击监测日志表
+ *
+ * @author system
+ * @since 2025-06-25
+ */
+@Data
+@JsonInclude
+@TableName("ocean_engine_click_monitor_log")
+@ApiModel(value = "OceanEngineClickMonitorLog对象", description = "巨量引擎点击监测日志表")
+public class OceanEngineClickMonitorLog implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /** bind_status:未绑定 */
+    public static final int BIND_STATUS_UNBOUND = 0;
+    /** bind_status:已绑定 */
+    public static final int BIND_STATUS_BOUND = 1;
+    /** bind_status:绑定失败 */
+    public static final int BIND_STATUS_FAILED = 2;
+
+    /** ad_version:巨量广告 1.0 */
+    public static final int AD_VERSION_V1 = 1;
+    /** ad_version:巨量广告 2.0 */
+    public static final int AD_VERSION_V2 = 2;
+
+    @ApiModelProperty(value = "主键")
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    @ApiModelProperty(value = "点击唯一标识")
+    @TableField("click_id")
+    private String clickId;
+
+    @ApiModelProperty(value = "归因回调参数")
+    @TableField("callback_param")
+    private String callbackParam;
+
+    @ApiModelProperty(value = "请求下发ID")
+    @TableField("request_id")
+    private String requestId;
+
+    @ApiModelProperty(value = "广告主账户ID")
+    @TableField("advertiser_id")
+    private String advertiserId;
+
+    @ApiModelProperty(value = "广告版本:1-1.0, 2-2.0")
+    @TableField("ad_version")
+    private Integer adVersion;
+
+    @ApiModelProperty(value = "广告组ID(1.0)")
+    @TableField("campaign_id")
+    private String campaignId;
+
+    @ApiModelProperty(value = "广告计划ID(1.0)")
+    @TableField("aid")
+    private String aid;
+
+    @ApiModelProperty(value = "广告创意ID(1.0)")
+    @TableField("cid")
+    private String cid;
+
+    @ApiModelProperty(value = "项目ID(2.0)")
+    @TableField("project_id")
+    private String projectId;
+
+    @ApiModelProperty(value = "广告ID(2.0)")
+    @TableField("promotion_id")
+    private String promotionId;
+
+    @ApiModelProperty(value = "转化目标ID")
+    @TableField("convert_id")
+    private String convertId;
+
+    @ApiModelProperty(value = "广告投放位置")
+    @TableField("csite")
+    private String csite;
+
+    @ApiModelProperty(value = "联盟站点")
+    @TableField("union_site")
+    private String unionSite;
+
+    @ApiModelProperty(value = "图片素材ID")
+    @TableField("mid1")
+    private String mid1;
+
+    @ApiModelProperty(value = "标题素材ID")
+    @TableField("mid2")
+    private String mid2;
+
+    @ApiModelProperty(value = "视频素材ID")
+    @TableField("mid3")
+    private String mid3;
+
+    @ApiModelProperty(value = "试玩素材ID")
+    @TableField("mid4")
+    private String mid4;
+
+    @ApiModelProperty(value = "落地页素材ID")
+    @TableField("mid5")
+    private String mid5;
+
+    @ApiModelProperty(value = "安卓下载详情页ID")
+    @TableField("mid6")
+    private String mid6;
+
+    @ApiModelProperty(value = "点击时间戳(秒)")
+    @TableField("click_ts")
+    private Long clickTs;
+
+    @ApiModelProperty(value = "点击时间戳(毫秒)")
+    @TableField("click_ts_ms")
+    private Long clickTsMs;
+
+    @ApiModelProperty(value = "客户端IP")
+    @TableField("client_ip")
+    private String clientIp;
+
+    @ApiModelProperty(value = "IPv4")
+    @TableField("client_ipv4")
+    private String clientIpv4;
+
+    @ApiModelProperty(value = "IPv6")
+    @TableField("client_ipv6")
+    private String clientIpv6;
+
+    @ApiModelProperty(value = "User-Agent")
+    @TableField("user_agent")
+    private String userAgent;
+
+    @ApiModelProperty(value = "操作系统")
+    @TableField("os_type")
+    private String osType;
+
+    @ApiModelProperty(value = "设备型号")
+    @TableField("device_model")
+    private String deviceModel;
+
+    @ApiModelProperty(value = "iOS设备标识")
+    @TableField("idfa")
+    private String idfa;
+
+    @ApiModelProperty(value = "Android IMEI")
+    @TableField("imei")
+    private String imei;
+
+    @ApiModelProperty(value = "Android OAID")
+    @TableField("oaid")
+    private String oaid;
+
+    @ApiModelProperty(value = "OAID MD5")
+    @TableField("oaid_md5")
+    private String oaidMd5;
+
+    @ApiModelProperty(value = "Android ID")
+    @TableField("android_id")
+    private String androidId;
+
+    @ApiModelProperty(value = "关联用户ID")
+    @TableField("user_id")
+    private Integer userId;
+
+    @ApiModelProperty(value = "关联门店ID")
+    @TableField("store_id")
+    private Integer storeId;
+
+    @ApiModelProperty(value = "绑定状态:0-未绑定, 1-已绑定, 2-绑定失败")
+    @TableField("bind_status")
+    private Integer bindStatus;
+
+    @ApiModelProperty(value = "绑定时间")
+    @TableField("bind_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date bindTime;
+
+    @ApiModelProperty(value = "实际收到的完整监测URL")
+    @TableField("monitor_url")
+    private String monitorUrl;
+
+    @ApiModelProperty(value = "原始Query参数JSON")
+    @TableField("raw_params")
+    private String rawParams;
+
+    @ApiModelProperty(value = "服务端接收请求来源IP")
+    @TableField("server_ip")
+    private String serverIp;
+
+    @ApiModelProperty(value = "备注")
+    @TableField("remark")
+    private String remark;
+
+    @ApiModelProperty(value = "转化事件类型(回传专用,如 active_register)")
+    @TableField(exist = false)
+    private String eventType;
+
+    @ApiModelProperty(value = "匹配类型(回传专用,默认 0)")
+    @TableField(exist = false)
+    private Integer matchType;
+
+    @ApiModelProperty(value = "转化回传时间戳毫秒(回传专用,为空则取 clickTsMs 或当前时间)")
+    @TableField(exist = false)
+    private Long conversionTimestamp;
+
+    @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 = "修改时间")
+    @TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date updatedTime;
+}

+ 16 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/StoreUserPasswordDto.java

@@ -0,0 +1,16 @@
+package shop.alien.entity.store.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+@Data
+@ApiModel("商户端密码相关请求")
+public class StoreUserPasswordDto {
+
+    @ApiModelProperty("登录密码")
+    private String password;
+
+    @ApiModelProperty("确认密码")
+    private String confirmPassword;
+}

+ 9 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/LifeUserExpertVo.java

@@ -36,6 +36,15 @@ public class LifeUserExpertVo extends LifeUserExpert {
     @ApiModelProperty(value = "身份证号码")
     private String idCard;
 
+    @ApiModelProperty(value = "国籍")
+    private String nationality;
+
+    @ApiModelProperty(value = "英文名字")
+    private String englishName;
+
+    @ApiModelProperty(value = "证件有效期")
+    private String expirationDate;
+
     @ApiModelProperty(value = "手机号")
     private String userPhone;
 

+ 20 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/RegisteredLawyerUserCountVo.java

@@ -0,0 +1,20 @@
+package shop.alien.entity.store.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 律师端注册用户数量统计
+ */
+@Data
+@ApiModel(value = "RegisteredLawyerUserCountVo", description = "律师端注册用户数量统计")
+public class RegisteredLawyerUserCountVo implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty("律师端注册用户总数(lawyer_user 表,未逻辑删除)")
+    private Long registeredLawyerUserCount;
+}

+ 20 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/RegisteredStoreUserCountVo.java

@@ -0,0 +1,20 @@
+package shop.alien.entity.store.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 店铺端注册用户数量统计
+ */
+@Data
+@ApiModel(value = "RegisteredStoreUserCountVo", description = "店铺端注册用户数量统计")
+public class RegisteredStoreUserCountVo implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty("店铺端注册用户总数(store_user 表,未逻辑删除)")
+    private Long registeredStoreUserCount;
+}

+ 20 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/RegisteredUserCountVo.java

@@ -0,0 +1,20 @@
+package shop.alien.entity.store.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * C 端注册用户数量统计
+ */
+@Data
+@ApiModel(value = "RegisteredUserCountVo", description = "注册用户数量统计")
+public class RegisteredUserCountVo implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty("注册用户总数(life_user 表,未逻辑删除)")
+    private Long registeredUserCount;
+}

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

@@ -28,8 +28,9 @@ public interface LifeFansMapper extends BaseMapper<LifeFans> {
             "        and fans_user_type = #{identityUserType} and fans_ref_id = #{identityRefId} " +
             "</if>" +
             "    )   " +
-            "    select info.id, IF(info.store_application_status = 0, user.nick_name, info.store_name) AS NAME," +
-            "    user.head_img image, concat('store_', user.phone) phoneId, IF(info.store_application_status = 0, user.account_blurb, info.store_blurb) AS blurb, 1 blockedType,user.id blockedId ,IFNULL(user.nick_name, user.name) username, user.account_blurb accountBlurb, IF(info.store_application_status = 1, '1', '0') AS isMerchant, foll.created_time, 2 as followedUserType, user.id as followedRefId " +
+            "    select info.id, IF(info.store_application_status = 0, user.nick_name, info.store_name) AS name, " +
+            "    user.head_img image, concat('store_', user.phone) phoneId, IF(info.store_application_status = 0, user.account_blurb, info.store_blurb) AS blurb, 1 blockedType, user.id blockedId, " +
+            "    IFNULL(user.nick_name, user.name) username, user.account_blurb accountBlurb, IF(info.store_application_status = 1, '1', '0') AS isMerchant, foll.created_time, 2 as followedUserType, user.id as followedRefId " +
             "    from follow foll " +
             "    join store_user user on foll.followedUserType = 2 and user.id = foll.followedRefId " +
             "    join store_info info on info.id = user.store_id " +
@@ -37,7 +38,8 @@ public interface LifeFansMapper extends BaseMapper<LifeFans> {
             "    where 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, foll.created_time, 1 as followedUserType, user.id as followedRefId " +
+            "    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, " +
+            "    user.user_name username, '' accountBlurb, '0' AS isMerchant, foll.created_time, 1 as followedUserType, user.id as followedRefId " +
             "    from follow foll " +
             "    join life_user user on foll.followedUserType = 1 and user.id = foll.followedRefId   " +
             "    where user.delete_flag = 0   " +
@@ -90,7 +92,7 @@ public interface LifeFansMapper extends BaseMapper<LifeFans> {
             "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(foll.created_time) created_time, " +
             "  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_user_type = foll.fansUserType and fans2.followed_ref_id = foll.fansRefId and fans2.delete_flag = 0) fansNum, " +
-            "    (select count(1) from life_fans fans3 where fans3.fans_user_type = foll.fansUserType and fans3.fans_ref_id = foll.fansRefId and fans3.delete_flag = 0) followNum " +
+            "    (select count(1) from life_fans fans3 where fans3.fans_user_type = foll.fansUserType and fans3.fans_ref_id = foll.fansRefId and fans3.delete_flag = 0) followNum, foll.userName username " +
             "from ( " +
             "    with follow as ( " +
             "        select fans_user_type as fansUserType, fans_ref_id as fansRefId, created_time " +
@@ -100,7 +102,7 @@ public interface LifeFansMapper extends BaseMapper<LifeFans> {
             "        and followed_user_type = #{identityUserType} and followed_ref_id = #{identityRefId} " +
             "</if>" +
             "    ) " +
-            "    select user.id, IF(info.store_application_status = 0, user.nick_name, info.store_name) AS name, user.head_img image, concat('store_', user.phone) phoneId, IF(info.store_application_status = 0, user.account_blurb, info.store_blurb) AS blurb, 1 blockedType,user.id blockedId, IF(info.store_application_status = 1, '1', '0') AS isMerchant, foll.created_time, 2 as fansUserType, user.id as fansRefId " +
+            "    select user.id, IF(info.store_application_status = 0, user.nick_name, info.store_name) AS name,user.name userName, user.head_img image, concat('store_', user.phone) phoneId, IF(info.store_application_status = 0, user.account_blurb, info.store_blurb) AS blurb, 1 blockedType,user.id blockedId, IF(info.store_application_status = 1, '1', '0') AS isMerchant, foll.created_time, 2 as fansUserType, user.id as fansRefId " +
             "    from follow foll " +
             "    join store_user user on foll.fansUserType = 2 and user.id = foll.fansRefId " +
             "    join store_info info on info.id = user.store_id " +
@@ -108,7 +110,7 @@ public interface LifeFansMapper extends BaseMapper<LifeFans> {
             "    where 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, foll.created_time, 1 as fansUserType, user.id as fansRefId " +
+            "    select user.id, user.user_name name,user.user_name userName, user.user_image image, concat('user_', user.user_phone) phoneId, user.jianjie blurb, 2 blockedType,user.id blockedId, '0' AS isMerchant, foll.created_time, 1 as fansUserType, user.id as fansRefId " +
             "    from follow foll " +
             "    join life_user user on foll.fansUserType = 1 and user.id = foll.fansRefId " +
             "    where user.delete_flag = 0 " +
@@ -193,7 +195,7 @@ public interface LifeFansMapper extends BaseMapper<LifeFans> {
             "select MAX(foll.id) id, MAX(foll.name) name, MAX(foll.image) image, foll.phoneId, MAX(foll.blurb) blurb, " +
             "  MAX(if(isnull(fans.id), 0, 1)) isFollowThis, 1 as isFollowMe, " +
             "       (select count(1) from life_fans fans2 where fans2.followed_user_type = foll.fansUserType and fans2.followed_ref_id = foll.fansRefId and fans2.delete_flag = 0) fansNum, " +
-            "       (select count(1) from life_fans fans3 where fans3.fans_user_type = foll.fansUserType and fans3.fans_ref_id = foll.fansRefId and fans3.delete_flag = 0) followNum " +
+            "       (select count(1) from life_fans fans3 where fans3.fans_user_type = foll.fansUserType and fans3.fans_ref_id = foll.fansRefId and fans3.delete_flag = 0) followNum, foll.username " +
             " from ( " +
             "    with follow as ( " +
             "    select fans_user_type as fansUserType, fans_ref_id as fansRefId " +
@@ -203,7 +205,7 @@ public interface LifeFansMapper extends BaseMapper<LifeFans> {
             "    and followed_user_type = #{identityUserType} and followed_ref_id = #{identityRefId} " +
             "</if>" +
             "    ) " +
-            "    select user.id, user.user_name name, user.user_image image, concat('user_', user.user_phone) phoneId, user.jianjie blurb, 1 as fansUserType, user.id as fansRefId " +
+            "    select user.id, user.user_name name, user.user_name username, user.user_image image, concat('user_', user.user_phone) phoneId, user.jianjie blurb, 1 as fansUserType, user.id as fansRefId " +
             "    from follow foll " +
             "    join life_user user on foll.fansUserType = 1 and user.id = foll.fansRefId " +
             "    where user.delete_flag = 0 " +
@@ -233,14 +235,14 @@ public interface LifeFansMapper extends BaseMapper<LifeFans> {
             "        and fans1.fans_user_type = #{identityUserType} and fans1.fans_ref_id = #{identityRefId} " +
             "</if>" +
             "    ) " +
-            "    select info.id, IF(info.store_application_status = 0, user.nick_name, info.store_name) AS name, user.head_img image, concat('store_', user.phone) phoneId, IF(info.store_application_status = 0, user.account_blurb, info.store_blurb) AS blurb, 1 blockedType,user.id blockedId ,IFNULL(user.nick_name, user.name) username, user.account_blurb accountBlurb, IF(info.store_application_status = 1, '1', '0') AS isMerchant, foll.created_time, 2 as followedUserType, user.id as followedRefId " +
+            "    select info.id, IF(info.store_application_status = 0, user.nick_name, info.store_name) AS name, user.head_img image, concat('store_', user.phone) phoneId, IF(info.store_application_status = 0, user.account_blurb, info.store_blurb) AS blurb, 1 blockedType,user.id blockedId , user.name username, user.account_blurb accountBlurb, IF(info.store_application_status = 1, '1', '0') AS isMerchant, foll.created_time, 2 as followedUserType, user.id as followedRefId " +
             "    from follow foll " +
             "    join store_user user on foll.followedUserType = 2 and user.id = foll.followedRefId " +
             "    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 user.delete_flag = 0 and info.delete_flag = 0 " +
             "    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, foll.created_time, 1 as followedUserType, user.id as followedRefId " +
+            "    select user.id, user.user_name name,user.user_name username, user.user_image image, concat('user_', user.user_phone) phoneId, user.jianjie blurb, 2 blockedType,user.id blockedId, '' accountBlurb, '0' AS isMerchant, foll.created_time, 1 as followedUserType, user.id as followedRefId " +
             "    from follow foll " +
             "    join life_user user on foll.followedUserType = 1 and user.id = foll.followedRefId " +
             "    where user.delete_flag = 0 " +

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

@@ -177,7 +177,7 @@ public interface LifeUserExpertMapper extends BaseMapper<LifeUserExpert> {
             "    where delete_flag = 0 " +
             "    group by expert_id " +
             ") " +  // 去掉多余的逗号
-            " select expert.id, expert.expert_code, user.user_image, user.real_name, user.id_card, user.user_name, user.jianjie, user.user_phone, " +
+            " select expert.id, expert.expert_code, user.user_image, user.real_name, user.id_card, user.nationality, user.english_name, user.expiration_date, user.user_name, user.jianjie, user.user_phone, " +
             "ifnull(works.worksNum, 0) worksNum, ifnull(works.likeNum, 0) likeNum, " +
             "ifnull(fans.num, 0) fansNum " +
             " from life_user_expert expert " +

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

@@ -41,7 +41,7 @@ public interface LifeUserMapper extends BaseMapper<LifeUser> {
             " USER.nick_name accountName " +
             " FROM store_user USER " +
             " INNER JOIN store_info store ON USER.store_id = store.id " +
-            " WHERE USER.delete_flag = 0 AND store.delete_flag = 0 " +
+            " WHERE USER.delete_flag = 0 AND store.delete_flag = 0 AND USER.logout_flag  in (0,2) " +
             " UNION ALL SELECT id, concat( 'user_', user_phone ) phoneId, user_image imgUrl, jianjie blurb, user_name NAME, user_name accountName " +
             " FROM life_user WHERE delete_flag = 0 ) a " +
             " ${ew.customSqlSegment}")

+ 13 - 0
alien-entity/src/main/java/shop/alien/mapper/OceanEngineClickMonitorLogMapper.java

@@ -0,0 +1,13 @@
+package shop.alien.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import shop.alien.entity.store.OceanEngineClickMonitorLog;
+
+/**
+ * 巨量引擎点击监测日志 Mapper
+ *
+ * @author system
+ * @since 2025-06-25
+ */
+public interface OceanEngineClickMonitorLogMapper extends BaseMapper<OceanEngineClickMonitorLog> {
+}

+ 2 - 1
alien-entity/src/main/resources/mapper/OrderReviewMapper.xml

@@ -215,7 +215,8 @@
             lco.lawyer_user_id,
             lu.name AS lawyer_name,
             lu.head_img AS lawyer_avatar,
-            lf.firm_name AS law_firm_name,
+--             lf.firm_name AS law_firm_name,
+            lu.law_firm AS law_firm_name,
             NULL AS overall_rating,
             NULL AS review_content,
             lco.end_time AS created_time,

+ 3 - 1
alien-gateway/src/main/java/shop/alien/gateway/controller/LifeUserController.java

@@ -11,6 +11,8 @@ import shop.alien.gateway.config.BaseRedisService;
 import shop.alien.gateway.service.ActivityInviteConfigService;
 import shop.alien.gateway.service.LifeUserService;
 
+import java.util.Objects;
+
 /**
  * 用户
  */
@@ -50,7 +52,7 @@ public class LifeUserController {
             return R.fail("登录失败");
         }
 
-        if (1 == userVo.getIsBanned()) {
+        if (Objects.equals(userVo.getIsBanned(), 1)) {
             return R.fail("您的账户因严重违规导致被封禁");
         }
 

+ 19 - 6
alien-gateway/src/main/java/shop/alien/gateway/controller/StoreUserController.java

@@ -12,6 +12,7 @@ import shop.alien.gateway.config.BaseRedisService;
 import shop.alien.gateway.mapper.StoreUserGatewayMapper;
 import shop.alien.gateway.service.StoreUserService;
 
+import java.util.Date;
 import java.util.Objects;
 import java.util.Optional;
 
@@ -40,16 +41,16 @@ public class StoreUserController {
     @ApiOperationSupport(order = 1)
     @ApiImplicitParams({
             @ApiImplicitParam(name = "phone", value = "手机号", dataType = "String", paramType = "query", required = true),
-            @ApiImplicitParam(name = "password", value = "密码", dataType = "String", paramType = "query", required = true),
+            @ApiImplicitParam(name = "password", value = "密码", dataType = "String", paramType = "query", required = false),
             @ApiImplicitParam(name = "isPassword", value = "是否密码登录", dataType = "Boolean", paramType = "query", required = true),
-            @ApiImplicitParam(name = "code", value = "验证码", dataType = "String", paramType = "query", required = true)
+            @ApiImplicitParam(name = "code", value = "验证码", dataType = "String", paramType = "query", required = false)
     })
     @GetMapping("/login")
     public R<StoreUserVo> login(
             @RequestParam("phone") String phone,
-            @RequestParam("password") String password,
+            @RequestParam(value ="password",required = false) String password,
             @RequestParam("isPassword") Boolean isPassword,
-            @RequestParam("code") String code) {
+            @RequestParam(value ="code",required = false) String code) {
         log.info("StoreUserController.login?phone={}&password={}&isPassword={}&code={}", phone, password, isPassword, code);
         if (!isPassword) {
             // 2025-11-04 验证码-商户端登录
@@ -61,10 +62,22 @@ public class StoreUserController {
                 return R.fail("验证码错误,请重新输入");
             }
         }
-        StoreUser storeUser = storeUserMapper.selectOne(new LambdaQueryWrapper<StoreUser>()
+        StoreUser storeUser = new StoreUser();
+        storeUser = storeUserMapper.selectOne(new LambdaQueryWrapper<StoreUser>()
                 .eq(StoreUser::getPhone, phone));
         if (null == storeUser) {
-            return R.fail("当前账号不存在,请先去注册账号!");
+            StoreUser user = new StoreUser();
+            user.setPhone(phone);
+            user.setPassType(2);
+            user.setNickName(phone);
+            user.setCreatedTime(new Date());
+            user.setPassword("");
+            int num = storeUserMapper.insert(user);
+            if(num > 0){
+                storeUser = storeUserMapper.selectOne(new LambdaQueryWrapper<StoreUser>()
+                        .eq(StoreUser::getPhone, phone));
+                storeUserService.createToKen(storeUser);
+            }
         }
         if (storeUser.getStatus() == 1) {
             return R.fail("账号被禁用");

+ 29 - 11
alien-gateway/src/main/java/shop/alien/gateway/service/LifeUserService.java

@@ -2,7 +2,6 @@ package shop.alien.gateway.service;
 
 import com.alibaba.fastjson2.JSONObject;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
@@ -11,7 +10,6 @@ import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
-import org.springframework.transaction.annotation.Propagation;
 import org.springframework.transaction.annotation.Transactional;
 import shop.alien.entity.analytics.dto.AnalyticsUserRegisterDTO;
 import shop.alien.entity.result.R;
@@ -21,8 +19,6 @@ import shop.alien.entity.second.SecondUserCredit;
 import shop.alien.entity.second.SecondUserCreditRecord;
 import shop.alien.entity.second.enums.SecondUserCreditScoreEnum;
 import shop.alien.entity.store.LifeUser;
-import shop.alien.entity.store.StoreImg;
-import shop.alien.entity.store.StoreUser;
 import shop.alien.entity.store.vo.LifeUserVo;
 import shop.alien.gateway.config.BaseRedisService;
 import shop.alien.gateway.config.RiskControlProperties;
@@ -30,6 +26,7 @@ import shop.alien.gateway.feign.SecondServiceFeign;
 import shop.alien.gateway.feign.StoreServiceFeign;
 import shop.alien.gateway.mapper.LifeUserLogGatewayMapper;
 import shop.alien.gateway.mapper.LifeUserGatewayMapper;
+import shop.alien.gateway.mapper.LifeUserLogGatewayMapper;
 import shop.alien.mapper.second.SecondRiskControlRecordMapper;
 import shop.alien.mapper.second.SecondUserCreditMapper;
 import shop.alien.mapper.second.SecondUserCreditRecordMapper;
@@ -152,16 +149,20 @@ public class LifeUserService extends ServiceImpl<LifeUserGatewayMapper, LifeUser
     }
 
     public LifeUserVo userLogin(String phoneNum, String inviteCode, String macIp) {
-        LifeUser user = getUserByPhone(phoneNum);
+        LifeUser user = getUserByPhoneNew(phoneNum);
         if (user == null) {
             LifeUser lifeUser = new LifeUser();
             lifeUser.setUserPhone(phoneNum);
             lifeUser.setUserName(phoneNum);
             lifeUser.setRealName(phoneNum);
+            lifeUser.setLogoutFlag(LifeUser.LOGOUT_FLAG_NORMAL);
             lifeUser.setCreatedTime(new Date());
             int ret = lifeUserMapper.insert(lifeUser);
             if (ret == 1) {
-                LifeUser user2 = getUserByPhone(phoneNum);
+                LifeUser user2 = lifeUser.getId() != null ? lifeUser : getUserByPhoneNew(phoneNum);
+                if (user2 == null) {
+                    return null;
+                }
                 LifeUserVo userVo = new LifeUserVo();
                 BeanUtils.copyProperties(user2, userVo);
                 Map<String, String> tokenMap = new HashMap<>();
@@ -264,9 +265,26 @@ public class LifeUserService extends ServiceImpl<LifeUserGatewayMapper, LifeUser
     }
 
     public LifeUser getUserByPhone(String phoneNum) {
-        LambdaQueryWrapper<LifeUser> lambdaQueryWrapper = new LambdaQueryWrapper<>();
-        lambdaQueryWrapper.eq(LifeUser::getUserPhone, phoneNum);
-        return this.getOne(lambdaQueryWrapper);
+        return findLatestActiveUserByPhone(phoneNum);
+    }
+
+    public LifeUser getUserByPhoneNew(String phoneNum) {
+        return findLatestActiveUserByPhone(phoneNum);
+    }
+
+    /**
+     * 按手机号取最新可用用户:未注销或冷静期;logout_flag 为空视为未注销(历史数据)
+     */
+    private LifeUser findLatestActiveUserByPhone(String phoneNum) {
+        LambdaQueryWrapper<LifeUser> w = new LambdaQueryWrapper<>();
+        w.eq(LifeUser::getUserPhone, phoneNum);
+        w.and(x -> x.isNull(LifeUser::getLogoutFlag)
+                .or().eq(LifeUser::getLogoutFlag, LifeUser.LOGOUT_FLAG_NORMAL)
+                .or().eq(LifeUser::getLogoutFlag, LifeUser.LOGOUT_FLAG_COOLING));
+        w.orderByDesc(LifeUser::getCreatedTime).orderByDesc(LifeUser::getId);
+        w.last("LIMIT 1");
+        List<LifeUser> list = this.list(w);
+        return list.isEmpty() ? null : list.get(0);
     }
 
     public LifeUser getUserByPhoneDelete(String phoneNum) {
@@ -290,8 +308,8 @@ public class LifeUserService extends ServiceImpl<LifeUserGatewayMapper, LifeUser
         LifeUser update = new LifeUser();
         update.setId(id);
         //当原来的字段有值,在后面用逗号拼接
-        update.setDeviceId(user.getDeviceId() == null ? normalized : user.getDeviceId() + "," + normalized);
-//        update.setDeviceId(normalized);
+//        update.setDeviceId(user.getDeviceId() == null ? normalized : user.getDeviceId() + "," + normalized);
+        update.setDeviceId(normalized);
         return lifeUserMapper.updateById(update) > 0 ? R.success("保存成功") : R.fail("保存失败");
     }
 

+ 10 - 35
alien-lawyer/src/main/java/shop/alien/lawyer/controller/LawyerUserLogInController.java

@@ -1,27 +1,19 @@
 package shop.alien.lawyer.controller;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
-import com.baomidou.mybatisplus.core.toolkit.StringUtils;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import io.swagger.annotations.ApiOperationSupport;
 import io.swagger.annotations.ApiSort;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.BeanUtils;
 import org.springframework.web.bind.annotation.*;
 import shop.alien.entity.result.R;
 import shop.alien.entity.store.LawyerUser;
 import shop.alien.entity.store.dto.LawyerUserDto;
 import shop.alien.entity.store.vo.LawyerUserVo;
-import shop.alien.lawyer.config.BaseRedisService;
 import shop.alien.lawyer.service.LawyerUserLogInService;
 import shop.alien.lawyer.service.LawyerUserService;
-import shop.alien.mapper.LawyerUserMapper;
-
-import java.util.Objects;
-import java.util.Optional;
 
 /**
  * 门店用户 前端控制器
@@ -40,8 +32,6 @@ public class LawyerUserLogInController {
 
     private final LawyerUserLogInService lawyerUserService;
     private final LawyerUserService lawyerUserBizService;
-    private final LawyerUserMapper lawyerUserMapper;
-    private final BaseRedisService baseRedisService;
 
     @ApiOperation("律师用户注册")
     @ApiOperationSupport(order = 1)
@@ -51,6 +41,14 @@ public class LawyerUserLogInController {
         return lawyerUserService.register(lawyerUserDto);
     }
 
+    @ApiOperation(value = "完善律师个人信息", notes = "登录后补全资料,返回更新后的用户信息,不重新签发 token")
+    @ApiOperationSupport(order = 8)
+    @PostMapping("/completeProfile")
+    public R<LawyerUserVo> completeProfile(@RequestBody LawyerUserDto lawyerUserDto) {
+        log.info("LawyerUserLogInController.completeProfile?lawyerUserDto={}", lawyerUserDto);
+        return lawyerUserService.completeProfile(lawyerUserDto);
+    }
+
     @ApiOperation("律师用户验证码校验")
     @ApiOperationSupport(order = 2)
     @PostMapping("/checkMsgCode")
@@ -64,25 +62,7 @@ public class LawyerUserLogInController {
     @PostMapping("/login")
     public R<LawyerUserVo> login(@RequestBody LawyerUserDto lawyerUserDto) {
         log.info("LawyerUserLogInController.login?lawyerUserDto={}", lawyerUserDto);
-        LawyerUser lawyerUser = lawyerUserMapper.selectOne(new LambdaQueryWrapper<LawyerUser>()
-                .eq(LawyerUser::getPhone, lawyerUserDto.getPhone()).eq(LawyerUser::getDeleteFlag, 0));
-        if (null == lawyerUser) {
-            return R.fail("当前账号不存在,请先去注册账号!");
-        }
-        R<LawyerUser> loginCheck = lawyerUserBizService.checkLogin(lawyerUserDto.getPhone());
-        if ("账号已注销".equals(loginCheck.getMsg())) {
-            return R.fail("账号已注销");
-        }
-        if ("账号已被禁用".equals(loginCheck.getMsg())) {
-            return R.fail("账号被禁用");
-        }
-        LawyerUser lawyerUser1 = new LawyerUser();
-        lawyerUser1.setId(lawyerUser.getId());
-        lawyerUser1.setIsOnline(1);
-        lawyerUserMapper.updateById(lawyerUser1);
-        return Optional.ofNullable(lawyerUser).
-                map(user -> lawyerUserDto.getIsPassword() ? checkPassword(user, lawyerUserDto.getPassword()) : lawyerUserService.createToKen(user)).
-                orElseGet(() -> R.fail("手机号不存在"));
+        return lawyerUserService.login(lawyerUserDto);
     }
 
     @ApiOperation("律师登录前校验")
@@ -108,7 +88,7 @@ public class LawyerUserLogInController {
     public R updatePassWord(@RequestBody LawyerUserDto lawyerUserDto) {
         log.info("LawyerUserLogInController.updatePassWord?lawyerUserDto={}", lawyerUserDto);
         try {
-            LawyerUser lawyerUser = lawyerUserMapper.selectOne(new LambdaQueryWrapper<LawyerUser>()
+            LawyerUser lawyerUser = lawyerUserBizService.getOne(new LambdaQueryWrapper<LawyerUser>()
                     .eq(LawyerUser::getPhone, lawyerUserDto.getPhone()));
             if (null == lawyerUser) {
                 return R.fail("当前账号不存在,请先去注册账号!");
@@ -135,9 +115,4 @@ public class LawyerUserLogInController {
         }
     }
 
-    private R<LawyerUserVo> checkPassword(LawyerUser lawyerUser, String password) {
-        return Objects.equals(password, lawyerUser.getPassword())
-                ? lawyerUserService.createToKen(lawyerUser)
-                : R.fail("密码错误");
-    }
 }

+ 5 - 0
alien-lawyer/src/main/java/shop/alien/lawyer/service/LawyerUserLogInService.java

@@ -20,8 +20,13 @@ public interface LawyerUserLogInService extends IService<LawyerUser> {
      */
     R<LawyerUserVo> createToKen(LawyerUser lawyerUser);
     R<LawyerUserVo> register(LawyerUserDto lawyerUserDto);
+
+    R<LawyerUserVo> completeProfile(LawyerUserDto lawyerUserDto);
+
     R checkMsgCode(LawyerUserDto lawyerUserDto);
 
+    R<LawyerUserVo> login(LawyerUserDto lawyerUserDto);
+
     R logout(LawyerUserDto lawyerUserDto);
     R updatePassWord(LawyerUserDto lawyerUserDto);
 

+ 150 - 9
alien-lawyer/src/main/java/shop/alien/lawyer/service/impl/LawyerUserLogInServiceImpl.java

@@ -2,6 +2,7 @@ package shop.alien.lawyer.service.impl;
 
 import com.alibaba.fastjson.JSONObject;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
 import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
 import com.baomidou.mybatisplus.core.toolkit.StringUtils;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
@@ -12,21 +13,21 @@ import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.util.CollectionUtils;
 import shop.alien.entity.result.R;
-import shop.alien.entity.store.LawFirm;
+
 import shop.alien.entity.store.LawyerServiceArea;
 import shop.alien.entity.store.LawyerUser;
-import shop.alien.entity.store.StoreUser;
+
 import shop.alien.entity.store.dto.LawyerUserDto;
 import shop.alien.entity.store.vo.LawyerUserVo;
 import shop.alien.lawyer.config.BaseRedisService;
 import shop.alien.lawyer.service.LawyerUserLogInService;
-import shop.alien.mapper.LawFirmMapper;
+import shop.alien.lawyer.service.LawyerUserService;
 import shop.alien.mapper.LawyerServiceAreaMapper;
 import shop.alien.mapper.LawyerUserMapper;
 import shop.alien.util.common.JwtUtil;
 
 import java.util.*;
-import java.util.stream.Collectors;
+
 import java.time.ZoneId;
 import java.time.format.DateTimeFormatter;
 
@@ -53,7 +54,7 @@ public class LawyerUserLogInServiceImpl extends ServiceImpl<LawyerUserMapper, La
     private final BaseRedisService baseRedisService;
     private final LawyerUserMapper lawyerUserMapper;
     private final LawyerServiceAreaMapper lawyerServiceAreaMapper;
-    private final LawFirmMapper lawFirmMapper;
+    private final LawyerUserService lawyerUserService;
 
     /**
      * token
@@ -96,6 +97,8 @@ public class LawyerUserLogInServiceImpl extends ServiceImpl<LawyerUserMapper, La
         tokenMap.put("passType", lawyerUser.getPassType().toString());
         lawyerUserVo.setToken(JwtUtil.createJWT("lawyer_" + lawyerUser.getPhone(), lawyerUser.getName(), JSONObject.toJSONString(tokenMap), effectiveTimeIntLong));
         baseRedisService.setString("lawyer_" + lawyerUser.getPhone(), lawyerUserVo.getToken());
+        log.debug("token:-------------------------------------------");
+        log.debug(lawyerUserVo.getToken());
         return R.data(lawyerUserVo);
     }
 
@@ -175,6 +178,85 @@ public class LawyerUserLogInServiceImpl extends ServiceImpl<LawyerUserMapper, La
     }
 
     @Override
+    public R<LawyerUserVo> completeProfile(LawyerUserDto lawyerUserDto) {
+        if (lawyerUserDto == null || StringUtils.isEmpty(lawyerUserDto.getPhone())) {
+            return R.fail("手机号不能为空");
+        }
+        LawyerUser lawyerUser = lawyerUserMapper.selectOne(new LambdaQueryWrapper<LawyerUser>()
+                .eq(LawyerUser::getPhone, lawyerUserDto.getPhone())
+                .eq(LawyerUser::getDeleteFlag, 0)
+                .last("limit 1"));
+        if (ObjectUtils.isEmpty(lawyerUser)) {
+            return R.fail("当前账号不存在,请先登录");
+        }
+        if (lawyerUser.getStatus() != null && lawyerUser.getStatus() == 0) {
+            return R.fail("账号被禁用");
+        }
+        if (lawyerUser.getLogoutFlag() != null && LawyerUser.LOGOUT_FLAG_DONE == lawyerUser.getLogoutFlag()) {
+            return R.fail("账号已注销");
+        }
+
+        String certNo = lawyerUserDto.getLawyerCertificateNo();
+        if (certNo != null && !certNo.trim().isEmpty()) {
+            certNo = certNo.trim();
+            LawyerUser certDup = lawyerUserMapper.selectOne(new LambdaQueryWrapper<LawyerUser>()
+                    .eq(LawyerUser::getLawyerCertificateNo, certNo)
+                    .eq(LawyerUser::getDeleteFlag, 0)
+                    .ne(LawyerUser::getId, lawyerUser.getId())
+                    .last("limit 1"));
+            if (certDup != null) {
+                return R.fail("该律师执业证号已被使用,无法重复注册");
+            }
+        }
+
+        LawyerUser update = new LawyerUser();
+        BeanUtils.copyProperties(lawyerUserDto, update);
+        update.setId(lawyerUser.getId());
+        update.setPhone(lawyerUser.getPhone());
+        update.setLogoutFlag(lawyerUser.getLogoutFlag());
+        update.setStatus(lawyerUser.getStatus());
+        update.setDeleteFlag(lawyerUser.getDeleteFlag());
+        if (certNo != null && !certNo.isEmpty()) {
+            update.setLawyerCertificateNo(certNo);
+        }
+        if (update.getName() != null && !update.getName().isEmpty()) {
+            update.setNickName(update.getName().charAt(0) + "律师");
+        }
+        if (update.getPassType() == null) {
+            update.setPassType(lawyerUser.getPassType() != null ? lawyerUser.getPassType() : 2);
+        }
+        if (update.getIsOnline() == null) {
+            update.setIsOnline(lawyerUser.getIsOnline() != null ? lawyerUser.getIsOnline() : 1);
+        }
+        if (update.getIsRecommended() == null) {
+            update.setIsRecommended(0);
+        }
+        if (update.getOrderReceivingStatus() == null) {
+            update.setOrderReceivingStatus(1);
+        }
+        lawyerUserMapper.updateById(update);
+
+        List<Integer> problemScenarioIds = lawyerUserDto.getProblemScenarioIds();
+        if (!CollectionUtils.isEmpty(problemScenarioIds)) {
+            UpdateWrapper<LawyerServiceArea> deleteWrapper = new UpdateWrapper<>();
+            deleteWrapper.eq("lawyer_user_id", lawyerUser.getId())
+                    .set("delete_flag", 1)
+                    .set("updated_time", new Date());
+            lawyerServiceAreaMapper.update(null, deleteWrapper);
+            for (int i = 0; i < problemScenarioIds.size(); i++) {
+                LawyerServiceArea lawyerServiceArea = new LawyerServiceArea();
+                lawyerServiceArea.setLawyerUserId(lawyerUser.getId());
+                lawyerServiceArea.setProblemScenarioId(problemScenarioIds.get(i));
+                lawyerServiceArea.setSortOrder(i);
+                lawyerServiceArea.setCreatedTime(new Date());
+                lawyerServiceAreaMapper.insert(lawyerServiceArea);
+            }
+        }
+
+        return lawyerUserService.getLawyerInfo(lawyerUser.getId());
+    }
+
+    @Override
     public R checkMsgCode(LawyerUserDto lawyerUserDto) {
         String string = baseRedisService.getString("verification_lawyer_" + lawyerUserDto.getPhone());
         if (StringUtils.isNotEmpty(string)) {
@@ -187,7 +269,8 @@ public class LawyerUserLogInServiceImpl extends ServiceImpl<LawyerUserMapper, La
                         return R.fail("账号被禁用");
                     }
                     if (lawyerUser.getLogoutFlag() != null && LawyerUser.LOGOUT_FLAG_DONE == lawyerUser.getLogoutFlag()
-                            && (lawyerUser.getDeleteFlag() == null || lawyerUser.getDeleteFlag() == 0)) {
+                            && (lawyerUser.getDeleteFlag() == null || lawyerUser.getDeleteFlag() == 0)
+                            && LawyerUser.isWithinReRegisterBlockPeriod(lawyerUser.getLogoutTime())) {
                         return R.fail("账号已注销");
                     }
                     BeanUtils.copyProperties(lawyerUser, vo);
@@ -203,6 +286,66 @@ public class LawyerUserLogInServiceImpl extends ServiceImpl<LawyerUserMapper, La
     }
 
     @Override
+    public R<LawyerUserVo> login(LawyerUserDto lawyerUserDto) {
+        if (lawyerUserDto == null || StringUtils.isEmpty(lawyerUserDto.getPhone())) {
+            return R.fail("手机号不能为空");
+        }
+        boolean passwordLogin = Boolean.TRUE.equals(lawyerUserDto.getIsPassword());
+        LawyerUser lawyerUser = findActiveLawyerByPhone(lawyerUserDto.getPhone());
+        if (lawyerUser == null) {
+            if (passwordLogin) {
+                return R.fail("当前账号不存在,请先去注册账号");
+            }
+            lawyerUser = createLawyerOnLogin(lawyerUserDto.getPhone());
+        }
+        R<LawyerUser> loginCheck = lawyerUserService.checkLogin(lawyerUserDto.getPhone());
+        if ("账号已注销".equals(loginCheck.getMsg())) {
+            return R.fail("账号已注销");
+        }
+        if ("账号已被禁用".equals(loginCheck.getMsg())) {
+            return R.fail("账号被禁用");
+        }
+        LawyerUser onlineUpdate = new LawyerUser();
+        onlineUpdate.setId(lawyerUser.getId());
+        onlineUpdate.setIsOnline(1);
+        lawyerUserMapper.updateById(onlineUpdate);
+        if (passwordLogin) {
+            return checkPassword(lawyerUser, lawyerUserDto.getPassword());
+        }
+        return createToKen(lawyerUser);
+    }
+
+    private LawyerUser findActiveLawyerByPhone(String phone) {
+        return lawyerUserMapper.selectOne(new LambdaQueryWrapper<LawyerUser>()
+                .eq(LawyerUser::getPhone, phone)
+                .eq(LawyerUser::getDeleteFlag, 0)
+                .orderByDesc(LawyerUser::getCreatedTime)
+                .last("LIMIT 1"));
+    }
+
+    private LawyerUser createLawyerOnLogin(String phone) {
+        LawyerUser user = new LawyerUser();
+        user.setIsOnline(1);
+        user.setPhone(phone);
+        user.setName(phone);
+        user.setPassType(2);
+        lawyerUserMapper.insert(user);
+        return user;
+    }
+
+    private R<LawyerUserVo> checkPassword(LawyerUser lawyerUser, String password) {
+        if (StringUtils.isEmpty(password)) {
+            return R.fail("密码不能为空");
+        }
+        if (StringUtils.isEmpty(lawyerUser.getPassword())) {
+            return R.fail("尚未设置登录密码,请使用验证码登录或设置密码");
+        }
+        return Objects.equals(password, lawyerUser.getPassword())
+                ? createToKen(lawyerUser)
+                : R.fail("密码错误");
+    }
+
+    @Override
     public R logout(LawyerUserDto lawyerUserDto) {
         LawyerUser lawyerUser = new LawyerUser();
         lawyerUser.setId(lawyerUserDto.getId());
@@ -223,11 +366,9 @@ public class LawyerUserLogInServiceImpl extends ServiceImpl<LawyerUserMapper, La
                 LawyerUserVo vo = new LawyerUserVo();
                 if (ObjectUtils.isNotEmpty(lawyerUser)) {
                     LawyerUser update = new LawyerUser();
-                    update.setPassword(lawyerUserDto.getPassword());
                     update.setId(lawyerUser.getId());
+                    update.setPassword(lawyerUserDto.getPassword());
                     lawyerUserMapper.updateById(update);
-                    BeanUtils.copyProperties(lawyerUser, vo);
-                    return createToKen(lawyerUser);
                 }
                 return R.data(ObjectUtils.isNotEmpty(lawyerUser) ? vo : "null");
             }else {

+ 17 - 4
alien-store/src/main/java/shop/alien/store/controller/AliController.java

@@ -61,11 +61,11 @@ public class AliController {
         aliService.notify(request);
     }
 
-    @ApiOperation("身份证二要素核验")
+    @ApiOperation("证二要素核验(身份证走阿里核验,护照等非身份证证件跳过核验)")
     @ApiOperationSupport(order = 2)
     @ApiImplicitParams({
             @ApiImplicitParam(name = "name", value = "姓名", dataType = "String", paramType = "query", required = true),
-            @ApiImplicitParam(name = "idCard", value = "身份证号", dataType = "String", paramType = "query", required = true),
+            @ApiImplicitParam(name = "idCard", value = "证件号(身份证号或护照号等)", dataType = "String", paramType = "query", required = true),
             @ApiImplicitParam(name = "appType", value = "端区分(0:用户, 1:商家)", dataType = "Integer", paramType = "query", required = true, defaultValue = "0")
     })
     @GetMapping("/getIdInfo")
@@ -74,14 +74,18 @@ public class AliController {
         String normalizedIdCard = idCard == null ? null : idCard.trim().toUpperCase();
         log.info("AliController.getIdInfo?name={}&idCard={}", normalizedName, normalizedIdCard);
         if (appType == 0) {
-            // 根据身份证查询未注销用户:同一身份证只能实名一个账号
+            // 根据证件号查询未注销用户:同一证只能实名一个账号
             int size = lifeUserService.count(new LambdaQueryWrapper<LifeUser>()
                     .eq(LifeUser::getIdCard, normalizedIdCard)
                     .eq(LifeUser::getLogoutFlag, 0));
             if (size > 0) {
-                return R.fail("该身份证已实名认证过");
+                return R.fail("该证已实名认证过");
             }
         }
+        if (!isChineseIdCard(normalizedIdCard)) {
+            log.info("AliController.getIdInfo 非中国大陆身份证,跳过二要素核验 idCard={}", normalizedIdCard);
+            return R.success("身份验证成功");
+        }
         if (aliPayConfig.getIdInfo(normalizedName, normalizedIdCard)) {
             return R.success("身份验证成功");
         }
@@ -91,6 +95,15 @@ public class AliController {
         return R.fail("身份证号码与姓名不一致,请检查后重新填写");
     }
 
+    /** 是否为中国大陆居民身份证号(18位或15位) */
+    private static boolean isChineseIdCard(String idCard) {
+        if (idCard == null || idCard.isEmpty()) {
+            return false;
+        }
+        return idCard.matches("^[1-9]\\d{5}(18|19|20)\\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\\d|3[01])\\d{3}[\\dX]$")
+                || idCard.matches("^\\d{15}$");
+    }
+
     @ApiOperation("单笔转账接口")
     @ApiOperationSupport(order = 3)
     @ApiImplicitParams({@ApiImplicitParam(name = "name", value = "收款人姓名", dataType = "String", paramType = "query", required = true),

+ 5 - 5
alien-store/src/main/java/shop/alien/store/controller/LifeUserDynamicsController.java

@@ -75,7 +75,7 @@ public class LifeUserDynamicsController {
             targetType = "POST"
     )
 
-    @ApiOperation(value = "发布动态社区", notes = "0:成功, 1:失败, 2:文本内容异常, 3:图片内容异常")
+    @ApiOperation(value = "发布动态社区", notes = "0:发布失败, -1:文本内容异常, 其他:动态id")
     @ApiOperationSupport(order = 2)
     @PostMapping("/addOrUpdate")
     public R<Integer> addOrUpdate(@RequestBody LifeUserDynamics lifeUserDynamics) {
@@ -94,7 +94,7 @@ public class LifeUserDynamicsController {
                     aiContentModerationUtil.auditContent(lifeUserDynamics.getContext(), null);
             if (!contentAuditResult.isPassed()) {
                 log.warn("发布动态文本审核未通过: {}", contentAuditResult.getFailureReason());
-                return R.data(2);
+                return R.data(-1);
             }
             // 审核通过(含无正文:moderate 侧跳过请求,视为通过)→ 落库审核状态为「审核完成」
             lifeUserDynamics.setCheckFlag(2);
@@ -102,13 +102,13 @@ public class LifeUserDynamicsController {
             lifeUserDynamics.setUpdatedTime(new java.util.Date());
             int cnt = lifeUserDynamicsService.addOrUpdateStore(lifeUserDynamics);
             if (cnt == 0) {
-                return R.data(1);
+                return R.data(0);
             }
+            return R.data(lifeUserDynamics.getId());
         } catch (Exception ex) {
             log.error("LifeUserDynamicsController.addOrUpdate ERROR Msg={}", ex.getMessage());
-            return R.data(1);
+            return R.data(0);
         }
-        return R.data(0);
     }
 
     @ApiOperation("删除动态社区")

+ 114 - 0
alien-store/src/main/java/shop/alien/store/controller/OceanEngineAdTrackController.java

@@ -0,0 +1,114 @@
+package shop.alien.store.controller;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import io.swagger.annotations.*;
+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.OceanEngineClickMonitorLog;
+import shop.alien.store.dto.OceanEngineClickBindUserDto;
+import shop.alien.store.dto.OceanEngineConversionResultVo;
+import shop.alien.store.service.LifeUserService;
+import shop.alien.store.service.OceanEngineClickMonitorLogService;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * 巨量引擎广告点击监测
+ * 文档:https://open.oceanengine.com/labels/7/docs/1696710655781900
+ */
+@Api(tags = {"巨量引擎广告监测"})
+@Slf4j
+@RestController
+@CrossOrigin
+@RequestMapping("/oceanEngine")
+@RequiredArgsConstructor
+public class OceanEngineAdTrackController {
+
+    private final OceanEngineClickMonitorLogService clickMonitorLogService;
+
+    private final LifeUserService lifeUserService;
+
+    /**
+     * 点击监测链接接收入口(巨量引擎以 GET 方式调用)
+     * 配置示例:https://your-domain.com/oceanEngine/click/monitor?clickid=__CLICKID__&callback=__CALLBACK_PARAM__&...
+     */
+    @ApiOperation("点击监测链接接收(巨量引擎 GET 回调)")
+    @ApiOperationSupport(order = 1)
+    @GetMapping("/click/monitor")
+    public R<Long> receiveClickMonitor(HttpServletRequest request) {
+        return clickMonitorLogService.receiveClickMonitor(request);
+    }
+
+    @ApiOperation("点击记录绑定用户")
+    @ApiOperationSupport(order = 2)
+    @PostMapping("/click/bindUser")
+    public R<String> bindUser(@RequestBody OceanEngineClickBindUserDto bindDto) {
+        return clickMonitorLogService.bindUser(bindDto);
+    }
+
+    @ApiOperation("根据主键查询点击监测记录")
+    @ApiOperationSupport(order = 3)
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "id", value = "主键ID", dataType = "Long", paramType = "query", required = true)
+    })
+    @GetMapping("/click/getById")
+    public R<OceanEngineClickMonitorLog> getClickById(@RequestParam Long id) {
+        return clickMonitorLogService.getInfoById(id);
+    }
+
+    @ApiOperation("点击监测记录分页列表")
+    @ApiOperationSupport(order = 4)
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "pageNum", value = "页码", dataType = "Integer", paramType = "query", required = true),
+            @ApiImplicitParam(name = "pageSize", value = "每页数量", dataType = "Integer", paramType = "query", required = true),
+            @ApiImplicitParam(name = "clickId", value = "点击ID", dataType = "String", paramType = "query"),
+            @ApiImplicitParam(name = "advertiserId", value = "广告主ID", dataType = "String", paramType = "query"),
+            @ApiImplicitParam(name = "userId", value = "用户ID", dataType = "Integer", paramType = "query"),
+            @ApiImplicitParam(name = "bindStatus", value = "绑定状态", dataType = "Integer", paramType = "query")
+    })
+    @GetMapping("/click/list")
+    public R<IPage<OceanEngineClickMonitorLog>> clickList(
+            @RequestParam Integer pageNum,
+            @RequestParam Integer pageSize,
+            @RequestParam(required = false) String clickId,
+            @RequestParam(required = false) String advertiserId,
+            @RequestParam(required = false) Integer userId,
+            @RequestParam(required = false) Integer bindStatus) {
+        return clickMonitorLogService.list(pageNum, pageSize, clickId, advertiserId, userId, bindStatus);
+    }
+
+    @ApiOperation("转化事件回传巨量引擎(注册/付费等)")
+    @ApiOperationSupport(order = 5)
+    @PostMapping("/conversion/report")
+    public R<OceanEngineConversionResultVo> reportConversion(@RequestBody OceanEngineClickMonitorLog clickLog) {
+        return clickMonitorLogService.reportConversion(clickLog);
+    }
+
+
+    @ApiOperation("保存用户设备ID(idfa/oaid),巨量引擎使用")
+    @ApiOperationSupport
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "id", value = "ID", dataType = "Integer", paramType = "query", required = true),
+            @ApiImplicitParam(name= "userType", value= "用户类型1,用户;2,商户;3,律师", dataType = "Integer", paramType = "query", required = true),
+            @ApiImplicitParam(name = "oceanIdfaOrOaid", value = "巨量引擎点击监测标签idfa/oaid", dataType = "String", paramType = "query", required = true)
+    })
+    @PostMapping("/saveOceanIdfaOrOaid")
+    public R<String> saveOceanIdfaOrOaid(@RequestParam("id") Integer id,
+                                         @RequestParam("userType") Integer userType,
+                                         @RequestParam("oceanIdfaOrOaid") String oceanIdfaOrOaid) {
+        log.info("LifeUserController.saveOceanIdfaOrOaid?id={}&userType={}&oceanIdfaOrOaid={}", id, userType, oceanIdfaOrOaid);
+        if( userType == 1) {
+            return lifeUserService.saveOceanIdfaOrOaid(id, oceanIdfaOrOaid);
+        } else if (userType == 2) {
+            // TODO 目前不做处理 留口
+//            return storeUserService.saveOceanIdfaOrOaid(id, oceanIdfaOrOaid);
+        } else if (userType == 3) {
+            // TODO 目前不做处理
+        }
+        return R.fail("用户类型错误");
+    }
+
+
+}

+ 79 - 0
alien-store/src/main/java/shop/alien/store/controller/RegisteredUserStatisticsController.java

@@ -0,0 +1,79 @@
+package shop.alien.store.controller;
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiOperationSupport;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.CrossOrigin;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.vo.RegisteredLawyerUserCountVo;
+import shop.alien.entity.store.vo.RegisteredStoreUserCountVo;
+import shop.alien.entity.store.vo.RegisteredUserCountVo;
+import shop.alien.store.service.RegisteredUserStatisticsService;
+
+/**
+ * 各端注册用户数量统计
+ */
+@Api(tags = {"注册用户统计"})
+@Slf4j
+@CrossOrigin
+@RestController
+@RequestMapping("/registeredUserStatistics")
+@RequiredArgsConstructor
+public class RegisteredUserStatisticsController {
+
+    private final RegisteredUserStatisticsService registeredUserStatisticsService;
+
+    @ApiOperation("查询 C 端注册用户数量")
+    @ApiOperationSupport(order = 1)
+    @GetMapping("/count")
+    public R<RegisteredUserCountVo> countRegisteredUsers() {
+        log.info("RegisteredUserStatisticsController.countRegisteredUsers");
+        return R.data(registeredUserStatisticsService.countRegisteredUsers());
+    }
+
+    @ApiOperation(value = "查询 C 端注册用户数量", notes = "响应体仅为数字字符串,Content-Type: text/plain")
+    @ApiOperationSupport(order = 2)
+    @GetMapping(value = "/countPlain", produces = MediaType.TEXT_PLAIN_VALUE)
+    public String countRegisteredUsersPlain() {
+        log.info("RegisteredUserStatisticsController.countRegisteredUsersPlain");
+        return String.valueOf(registeredUserStatisticsService.countRegisteredUserTotal());
+    }
+
+    @ApiOperation("查询店铺端注册用户数量")
+    @ApiOperationSupport(order = 3)
+    @GetMapping("/store/count")
+    public R<RegisteredStoreUserCountVo> countRegisteredStoreUsers() {
+        log.info("RegisteredUserStatisticsController.countRegisteredStoreUsers");
+        return R.data(registeredUserStatisticsService.countRegisteredStoreUsers());
+    }
+
+    @ApiOperation(value = "查询店铺端注册用户数量", notes = "响应体仅为数字字符串,Content-Type: text/plain")
+    @ApiOperationSupport(order = 4)
+    @GetMapping(value = "/store/countPlain", produces = MediaType.TEXT_PLAIN_VALUE)
+    public String countRegisteredStoreUsersPlain() {
+        log.info("RegisteredUserStatisticsController.countRegisteredStoreUsersPlain");
+        return String.valueOf(registeredUserStatisticsService.countRegisteredStoreUserTotal());
+    }
+
+    @ApiOperation("查询律师端注册用户数量")
+    @ApiOperationSupport(order = 5)
+    @GetMapping("/lawyer/count")
+    public R<RegisteredLawyerUserCountVo> countRegisteredLawyerUsers() {
+        log.info("RegisteredUserStatisticsController.countRegisteredLawyerUsers");
+        return R.data(registeredUserStatisticsService.countRegisteredLawyerUsers());
+    }
+
+    @ApiOperation(value = "查询律师端注册用户数量", notes = "响应体仅为数字字符串,Content-Type: text/plain")
+    @ApiOperationSupport(order = 6)
+    @GetMapping(value = "/lawyer/countPlain", produces = MediaType.TEXT_PLAIN_VALUE)
+    public String countRegisteredLawyerUsersPlain() {
+        log.info("RegisteredUserStatisticsController.countRegisteredLawyerUsersPlain");
+        return String.valueOf(registeredUserStatisticsService.countRegisteredLawyerUserTotal());
+    }
+}

+ 17 - 0
alien-store/src/main/java/shop/alien/store/controller/StoreUserController.java

@@ -12,6 +12,8 @@ import shop.alien.entity.result.R;
 import shop.alien.entity.store.StoreImg;
 import shop.alien.entity.store.StorePlatformMenu;
 import shop.alien.entity.store.StoreUser;
+import shop.alien.entity.store.UserLoginInfo;
+import shop.alien.entity.store.dto.StoreUserPasswordDto;
 import shop.alien.entity.store.vo.StoreInfoVo;
 import shop.alien.entity.store.vo.StoreUserVo;
 import shop.alien.entity.store.vo.StoreUserWithStoreVo;
@@ -21,8 +23,11 @@ import shop.alien.mapper.StoreUserMapper;
 import shop.alien.store.config.BaseRedisService;
 import shop.alien.store.service.RoleMenuService;
 import shop.alien.store.service.StoreInfoService;
+import shop.alien.store.service.StoreUserPasswordService;
 import shop.alien.store.service.StoreUserService;
 import shop.alien.store.service.SubAccountStoreService;
+import shop.alien.util.common.TokenInfo;
+import springfox.documentation.annotations.ApiIgnore;
 
 import java.io.IOException;
 import java.util.List;
@@ -45,6 +50,8 @@ public class StoreUserController {
 
     private final StoreUserService storeUserService;
 
+    private final StoreUserPasswordService storeUserPasswordService;
+
     private final StoreUserMapper storeUserMapper;
 
     private final StoreInfoService storeInfoService;
@@ -106,6 +113,16 @@ public class StoreUserController {
         return storeUserService.forgetOrModifyPassword(phone, newPhone, oldPassword, newPassword, confirmNewPassword, verificationCode, type);
     }
 
+    @ApiOperation("设置当前商户账号登录密码(需登录)")
+    @ApiOperationSupport(order = 3)
+    @PostMapping("/password/set")
+    public R<String> setPassword(@ApiIgnore @TokenInfo UserLoginInfo userLoginInfo,
+                                 @RequestBody StoreUserPasswordDto dto) {
+        log.info("StoreUserController.setPassword, userId={}",
+                userLoginInfo == null ? null : userLoginInfo.getUserId());
+        return storeUserPasswordService.setPassword(userLoginInfo, dto);
+    }
+
     @ApiOperation("更换绑定手机号效验原密码或验证码")
     @ApiOperationSupport(order = 2)
     @ApiImplicitParams({@ApiImplicitParam(name = "phone", value = "手机号", dataType = "String", paramType = "query", required = true), @ApiImplicitParam(name = "oldPassword", value = "旧密码", dataType = "String", paramType = "query"), @ApiImplicitParam(name = "verificationCode", value = "验证码", dataType = "String", paramType = "query")})

+ 25 - 0
alien-store/src/main/java/shop/alien/store/dto/OceanEngineClickBindUserDto.java

@@ -0,0 +1,25 @@
+package shop.alien.store.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+/**
+ * 巨量引擎点击监测绑定用户请求
+ */
+@Data
+@ApiModel(value = "OceanEngineClickBindUserDto", description = "点击监测绑定用户")
+public class OceanEngineClickBindUserDto {
+
+    @ApiModelProperty(value = "点击ID(click_id 与 callback_param 至少填一个)")
+    private String clickId;
+
+    @ApiModelProperty(value = "回调参数")
+    private String callbackParam;
+
+    @ApiModelProperty(value = "用户ID", required = true)
+    private Integer userId;
+
+    @ApiModelProperty(value = "门店ID")
+    private Integer storeId;
+}

+ 31 - 0
alien-store/src/main/java/shop/alien/store/dto/OceanEngineConversionResultVo.java

@@ -0,0 +1,31 @@
+package shop.alien.store.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+/**
+ * 巨量引擎转化回传结果
+ */
+@Data
+@ApiModel(value = "OceanEngineConversionResultVo", description = "巨量引擎转化回传结果")
+public class OceanEngineConversionResultVo {
+
+    @ApiModelProperty(value = "是否回传成功")
+    private Boolean success;
+
+    @ApiModelProperty(value = "HTTP 状态码")
+    private Integer httpCode;
+
+    @ApiModelProperty(value = "实际使用的 callback")
+    private String callback;
+
+    @ApiModelProperty(value = "请求体 JSON")
+    private String requestBody;
+
+    @ApiModelProperty(value = "响应体")
+    private String responseBody;
+
+    @ApiModelProperty(value = "提示信息")
+    private String message;
+}

+ 87 - 0
alien-store/src/main/java/shop/alien/store/service/LifeStoreService.java

@@ -40,6 +40,8 @@ import java.util.stream.Collectors;
 @RequiredArgsConstructor
 public class LifeStoreService {
 
+    private static final String LOGOUT_DISPLAY_NAME = "已注销";
+
     private final LifeCollectMapper lifeCollectMapper;
 
     private final LifeUserMapper lifeUserMapper;
@@ -105,6 +107,7 @@ public class LifeStoreService {
         }
         boolean onlyStoreFollowed = (type != null && type == 1) && fansId != null && fansId.startsWith("store_");
         IPage<LifeFansVo> myFollowed = queryMyFollowed(new Page<>(page, size), fansId, relationFansId, blockerType, blockerId, onlyStoreFollowed, wrapper);
+        applyLoggedOutUsername(myFollowed.getRecords());
         // 过滤掉我拉黑的
         List<LifeFansVo> collect = myFollowed.getRecords().stream().filter(x -> null == x.getBlackListid()).collect(Collectors.toList());
         myFollowed.setRecords(collect);
@@ -112,6 +115,88 @@ public class LifeStoreService {
         return myFollowed;
     }
 
+    /** 关注/互关等列表回显:被关注方 logout_flag=1 时 username 展示为「已注销」 */
+    private void applyLoggedOutUsername(List<LifeFansVo> records) {
+        if (CollectionUtils.isEmpty(records)) {
+            return;
+        }
+        for (LifeFansVo vo : records) {
+            if (isFollowedAccountLoggedOut(vo)) {
+                vo.setUsername(LOGOUT_DISPLAY_NAME);
+            }
+        }
+    }
+
+    private boolean isFollowedAccountLoggedOut(LifeFansVo vo) {
+        if (vo == null) {
+            return false;
+        }
+        // 互关/关注列表:商户 id 为 store_info.id,按 store_id 查 store_user.logout_flag
+        if (isStoreFansVo(vo) && StringUtils.isNotEmpty(vo.getId())) {
+            try {
+                if (storeUserService.count(new LambdaQueryWrapper<StoreUser>()
+                        .eq(StoreUser::getStoreId, Integer.valueOf(vo.getId()))
+                        .eq(StoreUser::getDeleteFlag, 0)
+                        .eq(StoreUser::getLogoutFlag, StoreUser.LOGOUT_FLAG_DONE)) > 0) {
+                    return true;
+                }
+            } catch (NumberFormatException ignored) {
+                // ignore
+            }
+        }
+        // C 端用户:列表 id 为 life_user.id
+        if (isLifeUserFansVo(vo) && StringUtils.isNotEmpty(vo.getId())) {
+            try {
+                LifeUser user = lifeUserMapper.selectById(Integer.valueOf(vo.getId()));
+                if (user != null && (user.getDeleteFlag() == null || user.getDeleteFlag() == 0)
+                        && user.getLogoutFlag() != null
+                        && LifeUser.LOGOUT_FLAG_DONE == user.getLogoutFlag()) {
+                    return true;
+                }
+            } catch (NumberFormatException ignored) {
+                // ignore
+            }
+        }
+        if (StringUtils.isEmpty(vo.getPhoneId())) {
+            return false;
+        }
+        String[] parts = vo.getPhoneId().split("_", 2);
+        if (parts.length < 2 || StringUtils.isEmpty(parts[1])) {
+            return false;
+        }
+        if ("user".equals(parts[0])) {
+            LifeUser user = lifeUserMapper.selectOne(new LambdaQueryWrapper<LifeUser>()
+                    .eq(LifeUser::getUserPhone, parts[1])
+                    .eq(LifeUser::getDeleteFlag, 0)
+                    .orderByDesc(LifeUser::getCreatedTime)
+                    .orderByDesc(LifeUser::getId)
+                    .last("LIMIT 1"));
+            return user != null && user.getLogoutFlag() != null
+                    && LifeUser.LOGOUT_FLAG_DONE == user.getLogoutFlag();
+        }
+        if ("store".equals(parts[0])) {
+            StoreUser user = storeUserService.getOne(new LambdaQueryWrapper<StoreUser>()
+                    .eq(StoreUser::getPhone, parts[1])
+                    .eq(StoreUser::getDeleteFlag, 0)
+                    .orderByDesc(StoreUser::getCreatedTime)
+                    .orderByDesc(StoreUser::getId)
+                    .last("LIMIT 1"));
+            return user != null && user.getLogoutFlag() != null
+                    && StoreUser.LOGOUT_FLAG_DONE == user.getLogoutFlag();
+        }
+        return false;
+    }
+
+    private static boolean isStoreFansVo(LifeFansVo vo) {
+        return "1".equals(vo.getIsMerchant())
+                || (vo.getPhoneId() != null && vo.getPhoneId().startsWith("store_"));
+    }
+
+    private static boolean isLifeUserFansVo(LifeFansVo vo) {
+        return !"1".equals(vo.getIsMerchant())
+                && (vo.getPhoneId() == null || vo.getPhoneId().startsWith("user_"));
+    }
+
     /**
      * 我的关注所有
      */
@@ -213,6 +298,7 @@ public class LifeStoreService {
         LifeFansIdentityQuery.Scope scope = LifeFansIdentityQuery.resolve(fansId, typeUtil);
         IPage<LifeFansVo> mutualAttention = lifeFansMapper.getMutualAttention(new Page<>(page, size), fansId,
                 scope.getUserType(), scope.getRefId(), blockerType, blockerId, wrapper);
+        applyLoggedOutUsername(mutualAttention.getRecords());
         List<LifeFansVo> collect = mutualAttention.getRecords().stream().filter(x -> null == x.getBlackListid()).collect(Collectors.toList());
         mutualAttention.setRecords(collect);
         mutualAttention.setTotal(collect.size());
@@ -229,6 +315,7 @@ public class LifeStoreService {
         LifeFansIdentityQuery.Scope scope = LifeFansIdentityQuery.resolve(fansId, typeUtil);
         IPage<LifeFansVo> mutualAttention = lifeFansMapper.getMutualAttentionUser(new Page<>(page, size), fansId,
                 scope.getUserType(), scope.getRefId(), wrapper);
+        applyLoggedOutUsername(mutualAttention.getRecords());
         filterBlocked(fansId, mutualAttention);
         List<LifeFansVo> collect = mutualAttention.getRecords().stream().filter(x -> x.getIsBlocked().equals("0")).collect(Collectors.toList());
         mutualAttention.setRecords(collect);

+ 17 - 1
alien-store/src/main/java/shop/alien/store/service/LifeUserDynamicsService.java

@@ -792,6 +792,7 @@ public class LifeUserDynamicsService extends ServiceImpl<LifeUserDynamicsMapper,
                         blacklist.getBlockerPhoneId()
                 ).filter(Objects::nonNull)) // 过滤null
                 .collect(Collectors.toSet());
+        List<String> storeUserIdList = new ArrayList<>();
         if (!CollectionUtils.isEmpty(lifeUserDynamicsVos)){
             for (LifeUserDynamicsVo lifeUserDynamicsVo : lifeUserDynamicsVos) {
                 dynamicsIdentityHelper.ensureVoLegacyPhoneId(lifeUserDynamicsVo);
@@ -808,9 +809,13 @@ public class LifeUserDynamicsService extends ServiceImpl<LifeUserDynamicsMapper,
                             StoreInfo storeInfo = storeInfoMapper.getStoreNameByPhone(su.getPhone());
                             if (storeInfo != null) {
                                 lifeUserDynamicsVo.setStoreName(storeInfo.getStoreName());
-                                lifeUserDynamicsVo.setBusinessSection(storeInfo.getBusinessSection().toString());
+                                if (storeInfo.getBusinessSection() != null) {
+                                    lifeUserDynamicsVo.setBusinessSection(storeInfo.getBusinessSection().toString());
+                                }
                                 lifeUserDynamicsVo.setBusinessTypeName(storeInfo.getBusinessTypeName());
                                 lifeUserDynamicsVo.setStoreUserId(storeInfo.getId().toString());
+                                lifeUserDynamicsVo.setScoreAvg(storeInfo.getScoreAvg() != null ? storeInfo.getScoreAvg() : 0.0);
+                                storeUserIdList.add(storeInfo.getId().toString());
                             }
                         }
                     }
@@ -825,6 +830,17 @@ public class LifeUserDynamicsService extends ServiceImpl<LifeUserDynamicsMapper,
                     }
                 }
             }
+            Map<String, Integer> ratingCountMap = batchGetRatingCount(storeUserIdList);
+            for (LifeUserDynamicsVo lifeUserDynamicsVo : lifeUserDynamicsVos) {
+                if ("2".equals(lifeUserDynamicsVo.getType())) {
+                    String storeUserId = lifeUserDynamicsVo.getStoreUserId();
+                    int ratingCnt = storeUserId != null ? ratingCountMap.getOrDefault(storeUserId, 0) : 0;
+                    lifeUserDynamicsVo.setRatingCount(String.valueOf(ratingCnt));
+                    if (lifeUserDynamicsVo.getScoreAvg() == null) {
+                        lifeUserDynamicsVo.setScoreAvg(0.0);
+                    }
+                }
+            }
         }
         return  lifeUserDynamicsVos;
     }

+ 51 - 9
alien-store/src/main/java/shop/alien/store/service/LifeUserService.java

@@ -22,6 +22,7 @@ import org.springframework.transaction.annotation.Propagation;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.util.CollectionUtils;
 import shop.alien.config.properties.RiskControlProperties;
+import shop.alien.entity.result.R;
 import shop.alien.entity.second.LifeUserLog;
 import shop.alien.entity.second.SecondGoods;
 import shop.alien.entity.second.SecondRiskControlRecord;
@@ -33,20 +34,16 @@ import shop.alien.entity.store.vo.LifeMessageVo;
 import shop.alien.entity.store.vo.LifeUserVo;
 import shop.alien.entity.store.vo.WebSocketVo;
 import shop.alien.mapper.*;
-import shop.alien.mapper.second.LifeUserLogMapper;
-import shop.alien.mapper.second.SecondGoodsMapper;
-import shop.alien.mapper.second.SecondRiskControlRecordMapper;
-import shop.alien.mapper.second.SecondTradeRecordMapper;
-import shop.alien.mapper.second.SecondUserCreditMapper;
-import shop.alien.util.common.constant.LawyerStatusEnum;
+import shop.alien.mapper.second.*;
 import shop.alien.store.config.BaseRedisService;
 import shop.alien.store.config.WebSocketProcess;
 import shop.alien.store.feign.SecondServiceFeign;
 import shop.alien.store.service.clockin.ClockInRecommendCacheService;
 import shop.alien.store.service.dynamics.DynamicsRecommendCacheService;
 import shop.alien.store.util.FunctionMagic;
-import shop.alien.util.type.LifeFansIdentityQuery;
 import shop.alien.store.util.LifeDynamicsIdentityHelper;
+import shop.alien.util.common.constant.LawyerStatusEnum;
+import shop.alien.util.type.LifeFansIdentityQuery;
 import shop.alien.util.type.LifeNoticeUtil;
 import shop.alien.util.type.TypeUtil;
 
@@ -89,7 +86,7 @@ public class LifeUserService extends ServiceImpl<LifeUserMapper, LifeUser> {
     private static final String CANCEL_MSG_LISTED_SECOND_GOODS =
             "您有上架的二手商品,请确认下架后再申请注销。";
     private static final String CANCEL_MSG_PENDING_SECOND_TRADE =
-            "您有待确认或待交易的二手订单,请完成交易或取消后再申请注销。";
+            "您有进行中的二手订单,请确认收货或与对方协商取消后再申请注销。";
     private static final String CANCEL_MSG_CONSULT_IN_PROGRESS =
             "您有进行中的法律咨询,请与律师确认服务完结后再申请注销。";
     private static final String CANCEL_MSG_UNPAID_CONSULT_FEE =
@@ -156,6 +153,11 @@ public class LifeUserService extends ServiceImpl<LifeUserMapper, LifeUser> {
 
     private final LawyerConsultationOrderMapper lawyerConsultationOrderMapper;
 
+
+    private final OceanEngineClickMonitorLogService oceanEngineClickMonitorLogService;
+
+
+
     @Autowired
     private RiskControlProperties riskControlProperties;
 
@@ -520,7 +522,11 @@ public class LifeUserService extends ServiceImpl<LifeUserMapper, LifeUser> {
     public LifeUser getUserByPhone(String phoneNum) {
         LambdaQueryWrapper<LifeUser> lambdaQueryWrapper = new LambdaQueryWrapper<>();
         lambdaQueryWrapper.eq(LifeUser::getUserPhone, phoneNum);
-        LifeUser user = this.getOne(lambdaQueryWrapper);
+        lambdaQueryWrapper.in(LifeUser::getLogoutFlag,0, 2);
+        lambdaQueryWrapper.orderByDesc(LifeUser::getCreatedTime).orderByDesc(LifeUser::getId);
+        lambdaQueryWrapper.last("LIMIT 1");
+        List<LifeUser> users = this.list(lambdaQueryWrapper);
+        LifeUser user = users.isEmpty() ? null : users.get(0);
         if (user != null) {
             String userId = "user_" + phoneNum;
             LifeFansIdentityQuery.Scope userScope = new LifeFansIdentityQuery.Scope(userId, 1, user.getId());
@@ -780,4 +786,40 @@ public class LifeUserService extends ServiceImpl<LifeUserMapper, LifeUser> {
         Long count = lifeUserMapper.countTotalDeviceNum();
         return count == null ? 0L : count;
     }
+
+    public R<String> saveOceanIdfaOrOaid(Integer id, String oceanIdfaOrOaid) {
+        LifeUser user = lifeUserMapper.selectById(id);
+        if(user == null) {
+            return R.fail("用户不存在");
+        }
+        user.setOceanIdfaOrOaid(oceanIdfaOrOaid);
+        lifeUserMapper.updateById(user);
+        // 1.查看巨量引擎相关记录
+        LambdaQueryWrapper<OceanEngineClickMonitorLog> queryWrapper = new LambdaQueryWrapper<>();
+// 1. 先拼接多字段or匹配(OAID/OAID_MD5/AndroidId/IDFA任一相等)
+        queryWrapper.and(
+                wrapper -> wrapper.eq(OceanEngineClickMonitorLog::getOaid, oceanIdfaOrOaid)
+                        .or()
+                        .eq(OceanEngineClickMonitorLog::getOaidMd5, oceanIdfaOrOaid)
+                        .or()
+                        .eq(OceanEngineClickMonitorLog::getAndroidId, oceanIdfaOrOaid)
+                        .or()
+                        .eq(OceanEngineClickMonitorLog::getIdfa, oceanIdfaOrOaid)
+        );
+// 2. 增加创建时间 >= 当前时间 - 1小时(核心:近一小时)
+        LocalDateTime oneHourAgo = LocalDateTime.now().minusHours(1);
+        queryWrapper.ge(OceanEngineClickMonitorLog::getCreatedTime, oneHourAgo);
+        queryWrapper.ge(OceanEngineClickMonitorLog::getBindStatus, OceanEngineClickMonitorLog.BIND_STATUS_UNBOUND);
+        List<OceanEngineClickMonitorLog> list = oceanEngineClickMonitorLogService.list(queryWrapper);
+        if(list.size() > 0) {
+            // 2.有相关记录,调用转化回传
+            list.forEach(oceanEngineClickMonitorLog -> {
+                oceanEngineClickMonitorLogService.reportConversion(oceanEngineClickMonitorLog);
+                oceanEngineClickMonitorLog.setUserId(id);
+                oceanEngineClickMonitorLog.setBindStatus(OceanEngineClickMonitorLog.BIND_STATUS_BOUND);
+                oceanEngineClickMonitorLogService.updateById(oceanEngineClickMonitorLog);
+            });
+        }
+        return R.success("保存成功");
+    }
 }

+ 48 - 0
alien-store/src/main/java/shop/alien/store/service/OceanEngineClickMonitorLogService.java

@@ -0,0 +1,48 @@
+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.OceanEngineClickMonitorLog;
+import shop.alien.store.dto.OceanEngineClickBindUserDto;
+import shop.alien.store.dto.OceanEngineConversionResultVo;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * 巨量引擎点击监测服务
+ */
+public interface OceanEngineClickMonitorLogService extends IService<OceanEngineClickMonitorLog> {
+
+    /**
+     * 接收巨量引擎点击监测 GET 请求并落库
+     *
+     * @param request 原始 HTTP 请求(含全部 Query 参数)
+     * @return 处理结果
+     */
+    R<Long> receiveClickMonitor(HttpServletRequest request);
+
+    /**
+     * 将点击记录绑定到业务用户
+     */
+    R<String> bindUser(OceanEngineClickBindUserDto bindDto);
+
+    /**
+     * 根据主键查询
+     */
+    R<OceanEngineClickMonitorLog> getInfoById(Long id);
+
+    /**
+     * 分页查询点击监测日志
+     */
+    R<IPage<OceanEngineClickMonitorLog>> list(Integer pageNum, Integer pageSize,
+                                              String clickId, String advertiserId,
+                                              Integer userId, Integer bindStatus);
+
+    /**
+     * 转化事件回传巨量引擎(POST https://analytics.oceanengine.com/api/v2/conversion)
+     *
+     * @param clickLog 点击监测记录(可只传 id,或传 callbackParam/androidId 等完整字段)
+     */
+    R<OceanEngineConversionResultVo> reportConversion(OceanEngineClickMonitorLog clickLog);
+}

+ 23 - 0
alien-store/src/main/java/shop/alien/store/service/RegisteredUserStatisticsService.java

@@ -0,0 +1,23 @@
+package shop.alien.store.service;
+
+import shop.alien.entity.store.vo.RegisteredLawyerUserCountVo;
+import shop.alien.entity.store.vo.RegisteredStoreUserCountVo;
+import shop.alien.entity.store.vo.RegisteredUserCountVo;
+
+/**
+ * 各端注册用户数量统计
+ */
+public interface RegisteredUserStatisticsService {
+
+    RegisteredUserCountVo countRegisteredUsers();
+
+    long countRegisteredUserTotal();
+
+    RegisteredStoreUserCountVo countRegisteredStoreUsers();
+
+    long countRegisteredStoreUserTotal();
+
+    RegisteredLawyerUserCountVo countRegisteredLawyerUsers();
+
+    long countRegisteredLawyerUserTotal();
+}

+ 10 - 0
alien-store/src/main/java/shop/alien/store/service/StoreUserPasswordService.java

@@ -0,0 +1,10 @@
+package shop.alien.store.service;
+
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.UserLoginInfo;
+import shop.alien.entity.store.dto.StoreUserPasswordDto;
+
+public interface StoreUserPasswordService {
+
+    R<String> setPassword(UserLoginInfo login, StoreUserPasswordDto dto);
+}

+ 18 - 2
alien-store/src/main/java/shop/alien/store/service/channel/CommonPushVendorHttpClient.java

@@ -46,7 +46,7 @@ public class CommonPushVendorHttpClient {
     /** 荣耀 Push API */
     private static final String HONOR_PUSH_API_BASE = "https://push-api.cloud.honor.com";
     private static final String HONOR_AUTH_URL =
-            "https://auth.honor.com/auth/realms/developer/protocol/openid-connect/token";
+            "https://iam.developer.honor.com/auth/token";
     /** 华为推送成功响应码 */
     private static final String HUAWEI_SUCCESS_CODE = "80000000";
 
@@ -764,6 +764,7 @@ public class CommonPushVendorHttpClient {
             return CommonPushVendorSendResult.fail();
         }
         String pushClientId = resolveHuaweiOAuthClientId(credential);
+        String projectId = resolveHuaweiProjectId(credential);
         if (StringUtils.isBlank(pushClientId)) {
             log.warn("华为推送缺少 clientId/appId, taskNo={}", task.getTaskNo());
             return CommonPushVendorSendResult.fail();
@@ -796,6 +797,14 @@ public class CommonPushVendorHttpClient {
         return parseHuaweiSendResult(sendResp, task.getTaskNo(), pushTokens.size());
     }
 
+    private String resolveHuaweiProjectId(JSONObject credential) {
+        String projectId = StringUtils.trimToNull(credential.getString("projectId"));
+        if (projectId != null) {
+            return projectId;
+        }
+        return null;
+    }
+
     private CommonPushVendorSendResult sendHonorLike(JSONObject credential, CommonPushTask task, List<String> deviceIds) {
         if (deviceIds == null || deviceIds.isEmpty()) {
             return CommonPushVendorSendResult.fail();
@@ -923,7 +932,9 @@ public class CommonPushVendorHttpClient {
         tokenForm.add("grant_type", "client_credentials");
         tokenForm.add("client_id", appId);
         tokenForm.add("client_secret", appSecret);
-        String tokenResp = postForm(HONOR_AUTH_URL, tokenForm, null);
+        Map<String, String> headers = new HashMap<>();
+        headers.put("Content-Type", "application/x-www-form-urlencoded");
+        String tokenResp = postForm(HONOR_AUTH_URL, tokenForm, headers);
         JSONObject tokenJson = JSONObject.parseObject(tokenResp);
         if (tokenJson == null || StringUtils.isBlank(tokenJson.getString("access_token"))) {
             log.warn("荣耀鉴权失败: {}", tokenResp);
@@ -958,6 +969,8 @@ public class CommonPushVendorHttpClient {
         clickAction.put("type", 3);
         notification.put("click_action", clickAction);
         android.put("notification", notification);
+        // TODO 设置 android 通知参数
+        android.put("category","IM");
         message.put("android", android);
         List<String> huaweiTokens = new ArrayList<>();
         for (String pushToken : pushTokens) {
@@ -996,6 +1009,9 @@ public class CommonPushVendorHttpClient {
         if (markerIndex >= 0) {
             return trimmed.substring(markerIndex);
         }
+        if(trimmed.contains("HUAWEI_CN_")){
+            return trimmed.substring(trimmed.indexOf("HUAWEI_CN_") + 10);
+        }
         return trimmed;
     }
 

+ 11 - 1
alien-store/src/main/java/shop/alien/store/service/impl/AccountLogoutCompleteServiceImpl.java

@@ -106,7 +106,16 @@ public class AccountLogoutCompleteServiceImpl implements AccountLogoutCompleteSe
                 if (Integer.valueOf(-1).equals(storeUser.getStatus())) {
                     storeUser.setStatus(0);
                 }
-                storeUserMapper.updateById(storeUser);
+                LambdaUpdateWrapper<StoreUser> storeUserUpdate = new LambdaUpdateWrapper<StoreUser>()
+                        .eq(StoreUser::getId, storeUser.getId())
+                        .set(StoreUser::getLogoutFlag, StoreUser.LOGOUT_FLAG_DONE)
+                        .set(StoreUser::getName, LOGOUT_DISPLAY_NAME)
+                        .set(StoreUser::getNickName, LOGOUT_DISPLAY_NAME)
+                        .set(StoreUser::getHeadImg, null);
+                if (Integer.valueOf(-1).equals(storeUser.getStatus())) {
+                    storeUserUpdate.set(StoreUser::getStatus, 0);
+                }
+                storeUserMapper.update(null, storeUserUpdate);
                 storeLogoutSyncService.completeLogoutForStoreUser(storeUser, result);
                 result.setSuccessCount(result.getSuccessCount() + 1);
                 result.addDetail("STORE_USER#" + storeUser.getId() + " 已注销");
@@ -233,6 +242,7 @@ public class AccountLogoutCompleteServiceImpl implements AccountLogoutCompleteSe
                         .set(LawyerUser::getHeadImg, null)
                         .set(LawyerUser::getOrderReceivingStatus, 0)
                         .set(LawyerUser::getIsOnline, 0)
+                        .set(LawyerUser::getDeleteFlag, 1)
                         .set(LawyerUser::getIsRecommended, 0)
                         .set(LawyerUser::getLastOnlineTime, new Date()));
                 result.setSuccessCount(result.getSuccessCount() + 1);

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

@@ -340,7 +340,7 @@ public class CommonCommentServiceImpl extends ServiceImpl<CommonCommentMapper, C
             LifeFansIdentityQuery.Scope fanScope = LifeFansIdentityQuery.resolve(fansId, typeUtil);
             LifeFansIdentityQuery.Scope followedScope = LifeFansIdentityQuery.resolve(publisherPhoneId, typeUtil);
             LambdaQueryWrapper<LifeFans> fansQuery = new LambdaQueryWrapper<>();
-            fansQuery.eq(LifeFans::getDeleteFlag, 0).eq(LifeFans::getFansType, fansType);
+            fansQuery.eq(LifeFans::getDeleteFlag, 0);
             LifeFansIdentityQuery.applyPair(fansQuery, fanScope, followedScope);
             LifeFans lifeFans = lifeFansMapper.selectOne(fansQuery.last("limit 1"));
             if(null != lifeFans){

+ 617 - 0
alien-store/src/main/java/shop/alien/store/service/impl/OceanEngineClickMonitorLogServiceImpl.java

@@ -0,0 +1,617 @@
+package shop.alien.store.service.impl;
+
+import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+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.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.OceanEngineClickMonitorLog;
+import shop.alien.mapper.OceanEngineClickMonitorLogMapper;
+import shop.alien.store.dto.OceanEngineClickBindUserDto;
+import shop.alien.store.dto.OceanEngineConversionResultVo;
+import shop.alien.store.service.OceanEngineClickMonitorLogService;
+
+import javax.servlet.http.HttpServletRequest;
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+import java.util.Enumeration;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * 巨量引擎点击监测服务实现
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class OceanEngineClickMonitorLogServiceImpl
+        extends ServiceImpl<OceanEngineClickMonitorLogMapper, OceanEngineClickMonitorLog>
+        implements OceanEngineClickMonitorLogService {
+
+    private static final int HTTP_TIMEOUT_MS = 10000;
+    private static final String DEFAULT_EVENT_TYPE = "active_register";
+    private static final int DEFAULT_MATCH_TYPE = 0;
+
+    /** 巨量引擎转化回传 API 地址 */
+    @Value("${oceanengine.conversion.report-url:https://analytics.oceanengine.com/api/v2/conversion}")
+    private String conversionReportUrl;
+
+    @Override
+    public R<Long> receiveClickMonitor(HttpServletRequest request) {
+        Map<String, String> paramMap = extractQueryParams(request);
+        log.info("OceanEngineClickMonitorLogServiceImpl.receiveClickMonitor, paramCount={}, queryString={}",
+                paramMap.size(), request.getQueryString());
+
+        String clickId = firstNonBlank(paramMap,
+                "clickid", "click_id", "CLICKID");
+        String callbackParam = firstNonBlank(paramMap,
+                "callback", "callback_param", "CALLBACK_PARAM");
+        String requestId = firstNonBlank(paramMap,
+                "request_id", "req_id", "REQUEST_ID");
+
+        // 幂等:相同 click_id 或 request_id 已存在则直接返回
+        OceanEngineClickMonitorLog existing = findExistingLog(clickId, requestId);
+        if (existing != null) {
+            log.info("OceanEngineClickMonitorLogServiceImpl.receiveClickMonitor, duplicate click, id={}, clickId={}",
+                    existing.getId(), clickId);
+            return R.data(existing.getId(), "重复点击,已忽略");
+        }
+
+        OceanEngineClickMonitorLog clickLog = buildClickLog(request, paramMap, clickId, callbackParam, requestId);
+        boolean saved = this.save(clickLog);
+        if (!saved) {
+            log.error("OceanEngineClickMonitorLogServiceImpl.receiveClickMonitor, save failed, clickId={}", clickId);
+            return R.fail("点击监测日志保存失败");
+        }
+        log.info("OceanEngineClickMonitorLogServiceImpl.receiveClickMonitor, saved id={}, clickId={}, advertiserId={}",
+                clickLog.getId(), clickLog.getClickId(), clickLog.getAdvertiserId());
+        return R.data(clickLog.getId(), "接收成功");
+    }
+
+    @Override
+    public R<String> bindUser(OceanEngineClickBindUserDto bindDto) {
+        log.info("OceanEngineClickMonitorLogServiceImpl.bindUser, param={}", bindDto);
+        if (bindDto == null || bindDto.getUserId() == null) {
+            return R.fail("userId不能为空");
+        }
+        if (StringUtils.isAllBlank(bindDto.getClickId(), bindDto.getCallbackParam())) {
+            return R.fail("clickId与callbackParam至少填一个");
+        }
+
+        OceanEngineClickMonitorLog clickLog = findClickLog(bindDto.getClickId(), bindDto.getCallbackParam());
+        if (clickLog == null) {
+            log.warn("OceanEngineClickMonitorLogServiceImpl.bindUser, click log not found, clickId={}, callbackParam={}",
+                    bindDto.getClickId(), bindDto.getCallbackParam());
+            return R.fail("未找到对应的点击监测记录");
+        }
+
+        clickLog.setUserId(bindDto.getUserId());
+        clickLog.setStoreId(bindDto.getStoreId());
+        clickLog.setBindStatus(OceanEngineClickMonitorLog.BIND_STATUS_BOUND);
+        clickLog.setBindTime(new Date());
+        boolean updated = this.updateById(clickLog);
+        return updated ? R.success("绑定成功") : R.fail("绑定失败");
+    }
+
+    @Override
+    public R<OceanEngineClickMonitorLog> getInfoById(Long id) {
+        log.info("OceanEngineClickMonitorLogServiceImpl.getInfoById, id={}", id);
+        return R.data(this.getById(id));
+    }
+
+    @Override
+    public R<IPage<OceanEngineClickMonitorLog>> list(Integer pageNum, Integer pageSize,
+                                                     String clickId, String advertiserId,
+                                                     Integer userId, Integer bindStatus) {
+        log.info("OceanEngineClickMonitorLogServiceImpl.list, pageNum={}, pageSize={}, clickId={}, advertiserId={}, userId={}, bindStatus={}",
+                pageNum, pageSize, clickId, advertiserId, userId, bindStatus);
+        Page<OceanEngineClickMonitorLog> page = new Page<>(pageNum, pageSize);
+        LambdaQueryWrapper<OceanEngineClickMonitorLog> wrapper = new LambdaQueryWrapper<>();
+        if (StringUtils.isNotBlank(clickId)) {
+            wrapper.eq(OceanEngineClickMonitorLog::getClickId, clickId);
+        }
+        if (StringUtils.isNotBlank(advertiserId)) {
+            wrapper.eq(OceanEngineClickMonitorLog::getAdvertiserId, advertiserId);
+        }
+        if (userId != null) {
+            wrapper.eq(OceanEngineClickMonitorLog::getUserId, userId);
+        }
+        if (bindStatus != null) {
+            wrapper.eq(OceanEngineClickMonitorLog::getBindStatus, bindStatus);
+        }
+        wrapper.orderByDesc(OceanEngineClickMonitorLog::getCreatedTime);
+        return R.data(this.page(page, wrapper));
+    }
+
+    /**
+     * 转化事件回传巨量引擎
+     * <p>
+     * 官方接口:POST https://analytics.oceanengine.com/api/v2/conversion
+     * </p>
+     * <p>排查日志关键字:{@code [OceanEngineConversion]}</p>
+     * <p>入参支持:id / clickId / userId / callbackParam 查库,或直接传完整点击记录</p>
+     */
+    @Override
+    public R<OceanEngineConversionResultVo> reportConversion(OceanEngineClickMonitorLog clickLogParam) {
+        log.info("[OceanEngineConversion] 开始转化回传, 入参 id={}, clickId={}, userId={}, eventType={}, matchType={}",
+                clickLogParam != null ? clickLogParam.getId() : null,
+                clickLogParam != null ? clickLogParam.getClickId() : null,
+                clickLogParam != null ? clickLogParam.getUserId() : null,
+                clickLogParam != null ? clickLogParam.getEventType() : null,
+                clickLogParam != null ? clickLogParam.getMatchType() : null);
+
+        if (clickLogParam == null) {
+            log.warn("[OceanEngineConversion] 失败-入参为空");
+            return R.fail("点击监测记录不能为空");
+        }
+
+        // 步骤1:解析/合并点击监测记录(库查 + 入参覆盖)
+        OceanEngineClickMonitorLog clickLog = resolveClickLogForReport(clickLogParam);
+        if (clickLog == null) {
+            log.warn("[OceanEngineConversion] 失败-未找到点击记录, id={}, clickId={}, userId={}, callbackParam={}",
+                    clickLogParam.getId(), clickLogParam.getClickId(), clickLogParam.getUserId(),
+                    maskSensitive(clickLogParam.getCallbackParam()));
+            return R.fail("未找到点击监测记录,请传入 id / clickId / userId 或完整 callbackParam");
+        }
+        log.info("[OceanEngineConversion] 步骤1-点击记录已解析, clickLogId={}, advertiserId={}, promotionId={}, projectId={}",
+                clickLog.getId(), clickLog.getAdvertiserId(), clickLog.getPromotionId(), clickLog.getProjectId());
+
+        // 步骤2:提取归因 callback(巨量匹配核心字段)
+        String eventType = StringUtils.defaultIfBlank(clickLogParam.getEventType(), DEFAULT_EVENT_TYPE);
+        String callback = resolveCallbackFromClickLog(clickLog);
+        if (StringUtils.isBlank(callback)) {
+            log.warn("[OceanEngineConversion] 失败-callback为空, clickLogId={}, callbackParam={}, clickId={}",
+                    clickLog.getId(), maskSensitive(clickLog.getCallbackParam()), clickLog.getClickId());
+            return R.fail("callback 不能为空,点击记录需包含 callbackParam 或 clickId");
+        }
+        log.info("[OceanEngineConversion] 步骤2-callback已解析, source={}, callback={}",
+                StringUtils.isNotBlank(clickLog.getCallbackParam()) ? "callbackParam" : "clickId",
+                maskSensitive(callback));
+
+        // 步骤3:组装回传参数
+        int matchType = clickLogParam.getMatchType() != null ? clickLogParam.getMatchType() : DEFAULT_MATCH_TYPE;
+        long timestamp = resolveConversionTimestamp(clickLogParam, clickLog);
+        log.info("[OceanEngineConversion] 步骤3-回传参数, eventType={}, matchType={}, timestamp={}, androidId={}, idfa={}, oaid={}, imei={}",
+                eventType, matchType, timestamp,
+                maskSensitive(clickLog.getAndroidId()), maskSensitive(clickLog.getIdfa()),
+                maskSensitive(clickLog.getOaid()), maskSensitive(clickLog.getImei()));
+
+        JSONObject requestBody = buildConversionRequestBody(eventType, callback, matchType,
+                clickLog.getAndroidId(), clickLog.getIdfa(), clickLog.getOaid(), clickLog.getImei(), timestamp);
+        String requestBodyJson = requestBody.toJSONString();
+
+        OceanEngineConversionResultVo resultVo = new OceanEngineConversionResultVo();
+        resultVo.setCallback(callback);
+        resultVo.setRequestBody(requestBodyJson);
+
+        // 步骤4:HTTP POST 调用巨量转化回传接口
+        HttpURLConnection connection = null;
+        try {
+            log.info("[OceanEngineConversion] 步骤4-发起HTTP请求, url={}, bodyLength={}",
+                    conversionReportUrl, requestBodyJson.length());
+            log.debug("[OceanEngineConversion] 请求体详情, body={}", requestBodyJson);
+
+            URL url = new URL(conversionReportUrl);
+            connection = (HttpURLConnection) url.openConnection();
+            connection.setRequestMethod("POST");
+            connection.setConnectTimeout(HTTP_TIMEOUT_MS);
+            connection.setReadTimeout(HTTP_TIMEOUT_MS);
+            connection.setDoOutput(true);
+            connection.setRequestProperty("Content-Type", "application/json; charset=utf-8");
+
+            byte[] bodyBytes = requestBodyJson.getBytes(StandardCharsets.UTF_8);
+            connection.setRequestProperty("Content-Length", String.valueOf(bodyBytes.length));
+            try (OutputStream outputStream = connection.getOutputStream()) {
+                outputStream.write(bodyBytes);
+                outputStream.flush();
+            }
+
+            int httpCode = connection.getResponseCode();
+            String responseBody = readHttpResponse(connection, httpCode);
+            boolean success = httpCode >= 200 && httpCode < 300 && isOceanEngineSuccess(responseBody);
+
+            resultVo.setHttpCode(httpCode);
+            resultVo.setResponseBody(responseBody);
+            resultVo.setSuccess(success);
+            resultVo.setMessage(success ? "转化回传成功" : "转化回传失败");
+
+            if (success) {
+                log.info("[OceanEngineConversion] 步骤5-回传成功, clickLogId={}, userId={}, eventType={}, httpCode={}, response={}",
+                        clickLog.getId(), clickLog.getUserId(), eventType, httpCode, responseBody);
+            } else {
+                log.error("[OceanEngineConversion] 步骤5-回传失败, clickLogId={}, userId={}, eventType={}, httpCode={}, response={}, requestBody={}",
+                        clickLog.getId(), clickLog.getUserId(), eventType, httpCode, responseBody, requestBodyJson);
+            }
+            return R.data(resultVo, success ? "转化回传成功" : "转化回传失败");
+        } catch (Exception ex) {
+            log.error("[OceanEngineConversion] 步骤5-回传异常, clickLogId={}, eventType={}, callback={}, url={}, requestBody={}",
+                    clickLog.getId(), eventType, maskSensitive(callback), conversionReportUrl, requestBodyJson, ex);
+            resultVo.setSuccess(false);
+            resultVo.setMessage(ex.getMessage());
+            return R.data(resultVo, "转化回传异常:" + ex.getMessage());
+        } finally {
+            if (connection != null) {
+                connection.disconnect();
+            }
+        }
+    }
+
+    /**
+     * 构建巨量转化回传请求体
+     */
+    private JSONObject buildConversionRequestBody(String eventType, String callback, int matchType,
+                                                  String androidId, String idfa, String oaid, String imei,
+                                                  long timestamp) {
+        JSONObject adContext = new JSONObject();
+        adContext.put("callback", callback);
+        adContext.put("match_type", matchType);
+
+        JSONObject deviceContext = new JSONObject();
+        if (StringUtils.isNotBlank(androidId)) {
+            deviceContext.put("android_id", androidId);
+        }
+        if (StringUtils.isNotBlank(idfa)) {
+            deviceContext.put("idfa", idfa);
+        }
+        if (StringUtils.isNotBlank(oaid)) {
+            deviceContext.put("oaid", oaid);
+        }
+        if (StringUtils.isNotBlank(imei)) {
+            deviceContext.put("imei", imei);
+        }
+
+        JSONObject context = new JSONObject();
+        context.put("ad", adContext);
+        if (!deviceContext.isEmpty()) {
+            context.put("device", deviceContext);
+        }
+
+        JSONObject body = new JSONObject();
+        body.put("event_type", eventType);
+        body.put("context", context);
+        body.put("timestamp", timestamp);
+        return body;
+    }
+
+    /**
+     * 解析用于回传的点击记录
+     * <p>查找优先级:id → clickId → userId(已绑定) → callbackParam → 入参直传</p>
+     */
+    private OceanEngineClickMonitorLog resolveClickLogForReport(OceanEngineClickMonitorLog clickLogParam) {
+        OceanEngineClickMonitorLog dbLog = null;
+        String resolveBy = null;
+
+        if (clickLogParam.getId() != null) {
+            resolveBy = "id";
+            dbLog = this.getById(clickLogParam.getId());
+            log.info("[OceanEngineConversion] 按id查库, id={}, found={}", clickLogParam.getId(), dbLog != null);
+        } else if (StringUtils.isNotBlank(clickLogParam.getClickId())) {
+            resolveBy = "clickId";
+            dbLog = findClickLog(clickLogParam.getClickId(), clickLogParam.getCallbackParam());
+            log.info("[OceanEngineConversion] 按clickId查库, clickId={}, found={}",
+                    clickLogParam.getClickId(), dbLog != null);
+        } else if (clickLogParam.getUserId() != null) {
+            resolveBy = "userId";
+            dbLog = this.getOne(new LambdaQueryWrapper<OceanEngineClickMonitorLog>()
+                    .eq(OceanEngineClickMonitorLog::getUserId, clickLogParam.getUserId())
+                    .eq(OceanEngineClickMonitorLog::getBindStatus, OceanEngineClickMonitorLog.BIND_STATUS_BOUND)
+                    .orderByDesc(OceanEngineClickMonitorLog::getBindTime)
+                    .last("LIMIT 1"));
+            log.info("[OceanEngineConversion] 按userId查已绑定记录, userId={}, found={}",
+                    clickLogParam.getUserId(), dbLog != null);
+        } else if (StringUtils.isNotBlank(clickLogParam.getCallbackParam())) {
+            resolveBy = "callbackParam";
+            dbLog = findClickLog(null, clickLogParam.getCallbackParam());
+            log.info("[OceanEngineConversion] 按callbackParam查库, callback={}, found={}",
+                    maskSensitive(clickLogParam.getCallbackParam()), dbLog != null);
+        }
+
+        if (dbLog == null) {
+            // 库中无记录时,入参自带 callback/clickId 则直接使用(联调/补报场景)
+            if (StringUtils.isNotBlank(clickLogParam.getCallbackParam())
+                    || StringUtils.isNotBlank(clickLogParam.getClickId())) {
+                log.info("[OceanEngineConversion] 库无记录,使用入参直传, clickId={}, callback={}",
+                        clickLogParam.getClickId(), maskSensitive(clickLogParam.getCallbackParam()));
+                return clickLogParam;
+            }
+            log.warn("[OceanEngineConversion] 查库失败且无直传字段, resolveBy={}", resolveBy);
+            return null;
+        }
+
+        OceanEngineClickMonitorLog merged = mergeClickLogForReport(dbLog, clickLogParam);
+        log.info("[OceanEngineConversion] 记录合并完成, resolveBy={}, clickLogId={}", resolveBy, merged.getId());
+        return merged;
+    }
+
+    /**
+     * 库记录与入参合并:入参非空字段覆盖库记录(设备信息、callback 等)
+     */
+    private OceanEngineClickMonitorLog mergeClickLogForReport(OceanEngineClickMonitorLog dbLog,
+                                                              OceanEngineClickMonitorLog param) {
+        if (StringUtils.isNotBlank(param.getCallbackParam())) {
+            log.debug("[OceanEngineConversion] 合并覆盖 callbackParam");
+            dbLog.setCallbackParam(param.getCallbackParam());
+        }
+        if (StringUtils.isNotBlank(param.getClickId())) {
+            log.debug("[OceanEngineConversion] 合并覆盖 clickId");
+            dbLog.setClickId(param.getClickId());
+        }
+        if (StringUtils.isNotBlank(param.getAndroidId())) {
+            log.debug("[OceanEngineConversion] 合并覆盖 androidId");
+            dbLog.setAndroidId(param.getAndroidId());
+        }
+        if (StringUtils.isNotBlank(param.getIdfa())) {
+            log.debug("[OceanEngineConversion] 合并覆盖 idfa");
+            dbLog.setIdfa(param.getIdfa());
+        }
+        if (StringUtils.isNotBlank(param.getOaid())) {
+            log.debug("[OceanEngineConversion] 合并覆盖 oaid");
+            dbLog.setOaid(param.getOaid());
+        }
+        if (StringUtils.isNotBlank(param.getImei())) {
+            log.debug("[OceanEngineConversion] 合并覆盖 imei");
+            dbLog.setImei(param.getImei());
+        }
+        if (param.getUserId() != null) {
+            dbLog.setUserId(param.getUserId());
+        }
+        return dbLog;
+    }
+
+    /**
+     * 从点击记录解析 callback:优先 callbackParam,其次 clickId
+     */
+    private String resolveCallbackFromClickLog(OceanEngineClickMonitorLog clickLog) {
+        if (StringUtils.isNotBlank(clickLog.getCallbackParam())) {
+            return clickLog.getCallbackParam();
+        }
+        return clickLog.getClickId();
+    }
+
+    /**
+     * 解析转化回传时间戳(毫秒)
+     * <p>优先级:conversionTimestamp → clickTsMs → clickTs*1000 → 当前时间</p>
+     */
+    private long resolveConversionTimestamp(OceanEngineClickMonitorLog clickLogParam,
+                                            OceanEngineClickMonitorLog clickLog) {
+        if (clickLogParam.getConversionTimestamp() != null) {
+            log.debug("[OceanEngineConversion] 时间戳来源=conversionTimestamp, value={}",
+                    clickLogParam.getConversionTimestamp());
+            return clickLogParam.getConversionTimestamp();
+        }
+        if (clickLog.getClickTsMs() != null) {
+            log.debug("[OceanEngineConversion] 时间戳来源=clickTsMs, value={}", clickLog.getClickTsMs());
+            return clickLog.getClickTsMs();
+        }
+        if (clickLog.getClickTs() != null) {
+            long tsMs = clickLog.getClickTs() * 1000L;
+            log.debug("[OceanEngineConversion] 时间戳来源=clickTs(秒转毫秒), clickTs={}, tsMs={}",
+                    clickLog.getClickTs(), tsMs);
+            return tsMs;
+        }
+        long now = System.currentTimeMillis();
+        log.debug("[OceanEngineConversion] 时间戳来源=当前时间, value={}", now);
+        return now;
+    }
+
+    /**
+     * 判断巨量引擎业务响应是否成功(HTTP 200 且 body.code 为 0 或空)
+     */
+    private boolean isOceanEngineSuccess(String responseBody) {
+        if (StringUtils.isBlank(responseBody)) {
+            log.debug("[OceanEngineConversion] 响应体为空,视为成功");
+            return true;
+        }
+        try {
+            JSONObject json = JSONObject.parseObject(responseBody);
+            Integer code = json.getInteger("code");
+            String message = json.getString("message");
+            boolean success = code == null || code == 0;
+            if (!success) {
+                log.warn("[OceanEngineConversion] 巨量业务码非0, code={}, message={}, response={}",
+                        code, message, responseBody);
+            }
+            return success;
+        } catch (Exception ex) {
+            log.warn("[OceanEngineConversion] 响应体解析失败,视为成功, response={}", responseBody, ex);
+            return true;
+        }
+    }
+
+    /**
+     * 敏感字段脱敏输出(日志排查用,保留前6后4位)
+     */
+    private String maskSensitive(String value) {
+        if (StringUtils.isBlank(value)) {
+            return "";
+        }
+        if (value.length() <= 12) {
+            return value.substring(0, 1) + "***" + "(len=" + value.length() + ")";
+        }
+        return value.substring(0, 6) + "***" + value.substring(value.length() - 4) + "(len=" + value.length() + ")";
+    }
+
+    private String readHttpResponse(HttpURLConnection connection, int httpCode) throws Exception {
+        java.io.InputStream inputStream = httpCode >= 400 ? connection.getErrorStream() : connection.getInputStream();
+        if (inputStream == null) {
+            return "";
+        }
+        StringBuilder builder = new StringBuilder();
+        try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
+            String line;
+            while ((line = reader.readLine()) != null) {
+                builder.append(line);
+            }
+        }
+        return builder.toString();
+    }
+
+    /**
+     * 从 HttpServletRequest 提取全部 Query 参数
+     */
+    private Map<String, String> extractQueryParams(HttpServletRequest request) {
+        Map<String, String> paramMap = new LinkedHashMap<>();
+        Enumeration<String> names = request.getParameterNames();
+        while (names.hasMoreElements()) {
+            String name = names.nextElement();
+            paramMap.put(name, request.getParameter(name));
+        }
+        return paramMap;
+    }
+
+    /**
+     * 构建点击监测日志实体
+     */
+    private OceanEngineClickMonitorLog buildClickLog(HttpServletRequest request,
+                                                     Map<String, String> paramMap,
+                                                     String clickId,
+                                                     String callbackParam,
+                                                     String requestId) {
+        OceanEngineClickMonitorLog clickLog = new OceanEngineClickMonitorLog();
+        clickLog.setClickId(clickId);
+        clickLog.setCallbackParam(callbackParam);
+        clickLog.setRequestId(requestId);
+        clickLog.setAdvertiserId(firstNonBlank(paramMap, "advertiser_id", "ADVERTISER_ID"));
+        clickLog.setCampaignId(firstNonBlank(paramMap, "campaign_id", "CAMPAIGN_ID"));
+        clickLog.setAid(firstNonBlank(paramMap, "aid", "adid", "AID"));
+        clickLog.setCid(firstNonBlank(paramMap, "cid", "creativeid", "CID"));
+        clickLog.setProjectId(firstNonBlank(paramMap, "project_id", "PROJECT_ID"));
+        clickLog.setPromotionId(firstNonBlank(paramMap, "promotion_id", "PROMOTION_ID"));
+        clickLog.setConvertId(firstNonBlank(paramMap, "convert_id", "CONVERT_ID"));
+        clickLog.setCsite(firstNonBlank(paramMap, "csite", "CSITE"));
+        clickLog.setUnionSite(firstNonBlank(paramMap, "union_site", "UNION_SITE"));
+        clickLog.setMid1(firstNonBlank(paramMap, "mid1", "MID1"));
+        clickLog.setMid2(firstNonBlank(paramMap, "mid2", "MID2"));
+        clickLog.setMid3(firstNonBlank(paramMap, "mid3", "MID3"));
+        clickLog.setMid4(firstNonBlank(paramMap, "mid4", "MID4"));
+        clickLog.setMid5(firstNonBlank(paramMap, "mid5", "MID5"));
+        clickLog.setMid6(firstNonBlank(paramMap, "mid6", "MID6"));
+        clickLog.setClickTs(parseLong(firstNonBlank(paramMap, "ts", "TIMESTAMP", "TS")));
+        clickLog.setClickTsMs(parseLong(firstNonBlank(paramMap, "ts_ms", "TS_MS")));
+        clickLog.setClientIp(firstNonBlank(paramMap, "ip", "IP"));
+        clickLog.setClientIpv4(firstNonBlank(paramMap, "ipv4", "IPV4"));
+        clickLog.setClientIpv6(firstNonBlank(paramMap, "ipv6", "IPV6"));
+        clickLog.setUserAgent(firstNonBlank(paramMap, "ua", "UA"));
+        clickLog.setOsType(firstNonBlank(paramMap, "os", "OS"));
+        clickLog.setDeviceModel(firstNonBlank(paramMap, "model", "MODEL"));
+        clickLog.setIdfa(firstNonBlank(paramMap, "idfa", "IDFA"));
+        clickLog.setImei(firstNonBlank(paramMap, "imei", "IMEI"));
+        clickLog.setOaid(firstNonBlank(paramMap, "oaid", "OAID"));
+        clickLog.setOaidMd5(firstNonBlank(paramMap, "oaid_md5", "OAID_MD5"));
+        clickLog.setAndroidId(firstNonBlank(paramMap, "android_id", "ANDROIDID", "ANDROIDID1"));
+        clickLog.setAdVersion(resolveAdVersion(clickLog));
+        clickLog.setBindStatus(OceanEngineClickMonitorLog.BIND_STATUS_UNBOUND);
+        clickLog.setMonitorUrl(buildMonitorUrl(request));
+        clickLog.setRawParams(JSONObject.toJSONString(paramMap));
+        clickLog.setServerIp(resolveClientIp(request));
+        return clickLog;
+    }
+
+    /**
+     * 推断广告版本:2.0 有 project_id/promotion_id,1.0 有 aid/cid/campaign_id
+     */
+    private Integer resolveAdVersion(OceanEngineClickMonitorLog clickLog) {
+        if (StringUtils.isNotBlank(clickLog.getProjectId())
+                || StringUtils.isNotBlank(clickLog.getPromotionId())) {
+            return OceanEngineClickMonitorLog.AD_VERSION_V2;
+        }
+        if (StringUtils.isNotBlank(clickLog.getAid())
+                || StringUtils.isNotBlank(clickLog.getCid())
+                || StringUtils.isNotBlank(clickLog.getCampaignId())) {
+            return OceanEngineClickMonitorLog.AD_VERSION_V1;
+        }
+        return null;
+    }
+
+    private OceanEngineClickMonitorLog findExistingLog(String clickId, String requestId) {
+        if (StringUtils.isNotBlank(clickId)) {
+            OceanEngineClickMonitorLog byClickId = this.getOne(new LambdaQueryWrapper<OceanEngineClickMonitorLog>()
+                    .eq(OceanEngineClickMonitorLog::getClickId, clickId)
+                    .last("LIMIT 1"));
+            if (byClickId != null) {
+                return byClickId;
+            }
+        }
+        if (StringUtils.isNotBlank(requestId)) {
+            return this.getOne(new LambdaQueryWrapper<OceanEngineClickMonitorLog>()
+                    .eq(OceanEngineClickMonitorLog::getRequestId, requestId)
+                    .last("LIMIT 1"));
+        }
+        return null;
+    }
+
+    private OceanEngineClickMonitorLog findClickLog(String clickId, String callbackParam) {
+        if (StringUtils.isNotBlank(clickId)) {
+            OceanEngineClickMonitorLog log = this.getOne(new LambdaQueryWrapper<OceanEngineClickMonitorLog>()
+                    .eq(OceanEngineClickMonitorLog::getClickId, clickId)
+                    .orderByDesc(OceanEngineClickMonitorLog::getCreatedTime)
+                    .last("LIMIT 1"));
+            if (log != null) {
+                return log;
+            }
+        }
+        if (StringUtils.isNotBlank(callbackParam)) {
+            return this.getOne(new LambdaQueryWrapper<OceanEngineClickMonitorLog>()
+                    .eq(OceanEngineClickMonitorLog::getCallbackParam, callbackParam)
+                    .orderByDesc(OceanEngineClickMonitorLog::getCreatedTime)
+                    .last("LIMIT 1"));
+        }
+        return null;
+    }
+
+    private String buildMonitorUrl(HttpServletRequest request) {
+        String queryString = request.getQueryString();
+        if (StringUtils.isBlank(queryString)) {
+            return request.getRequestURL().toString();
+        }
+        return request.getRequestURL() + "?" + queryString;
+    }
+
+    /**
+     * 解析客户端真实 IP(兼容反向代理)
+     */
+    private String resolveClientIp(HttpServletRequest request) {
+        String ip = request.getHeader("X-Forwarded-For");
+        if (StringUtils.isNotBlank(ip) && !"unknown".equalsIgnoreCase(ip)) {
+            return ip.split(",")[0].trim();
+        }
+        ip = request.getHeader("X-Real-IP");
+        if (StringUtils.isNotBlank(ip) && !"unknown".equalsIgnoreCase(ip)) {
+            return ip;
+        }
+        return request.getRemoteAddr();
+    }
+
+    private String firstNonBlank(Map<String, String> paramMap, String... keys) {
+        for (String key : keys) {
+            String value = paramMap.get(key);
+            if (StringUtils.isNotBlank(value)) {
+                return value;
+            }
+        }
+        return null;
+    }
+
+    private Long parseLong(String value) {
+        if (StringUtils.isBlank(value)) {
+            return null;
+        }
+        try {
+            return Long.parseLong(value);
+        } catch (NumberFormatException ex) {
+            log.warn("OceanEngineClickMonitorLogServiceImpl.parseLong, invalid number={}", value);
+            return null;
+        }
+    }
+}

+ 70 - 0
alien-store/src/main/java/shop/alien/store/service/impl/RegisteredUserStatisticsServiceImpl.java

@@ -0,0 +1,70 @@
+package shop.alien.store.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import shop.alien.entity.store.LawyerUser;
+import shop.alien.entity.store.LifeUser;
+import shop.alien.entity.store.StoreUser;
+import shop.alien.entity.store.vo.RegisteredLawyerUserCountVo;
+import shop.alien.entity.store.vo.RegisteredStoreUserCountVo;
+import shop.alien.entity.store.vo.RegisteredUserCountVo;
+import shop.alien.mapper.LawyerUserMapper;
+import shop.alien.mapper.LifeUserMapper;
+import shop.alien.mapper.StoreUserMapper;
+import shop.alien.store.service.RegisteredUserStatisticsService;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class RegisteredUserStatisticsServiceImpl implements RegisteredUserStatisticsService {
+
+    private final LifeUserMapper lifeUserMapper;
+    private final StoreUserMapper storeUserMapper;
+    private final LawyerUserMapper lawyerUserMapper;
+
+    @Override
+    public RegisteredUserCountVo countRegisteredUsers() {
+        RegisteredUserCountVo vo = new RegisteredUserCountVo();
+        vo.setRegisteredUserCount(countRegisteredUserTotal());
+        return vo;
+    }
+
+    @Override
+    public long countRegisteredUserTotal() {
+        return toLong(lifeUserMapper.selectCount(new LambdaQueryWrapper<LifeUser>()));
+    }
+
+    @Override
+    public RegisteredStoreUserCountVo countRegisteredStoreUsers() {
+        RegisteredStoreUserCountVo vo = new RegisteredStoreUserCountVo();
+        vo.setRegisteredStoreUserCount(countRegisteredStoreUserTotal());
+        return vo;
+    }
+
+    @Override
+    public long countRegisteredStoreUserTotal() {
+        long total = toLong(storeUserMapper.selectCount(new LambdaQueryWrapper<StoreUser>()));
+        log.info("RegisteredUserStatisticsServiceImpl.countRegisteredStoreUserTotal total={}", total);
+        return total;
+    }
+
+    @Override
+    public RegisteredLawyerUserCountVo countRegisteredLawyerUsers() {
+        RegisteredLawyerUserCountVo vo = new RegisteredLawyerUserCountVo();
+        vo.setRegisteredLawyerUserCount(countRegisteredLawyerUserTotal());
+        return vo;
+    }
+
+    @Override
+    public long countRegisteredLawyerUserTotal() {
+        long total = toLong(lawyerUserMapper.selectCount(new LambdaQueryWrapper<LawyerUser>()));
+        log.info("RegisteredUserStatisticsServiceImpl.countRegisteredLawyerUserTotal total={}", total);
+        return total;
+    }
+
+    private static long toLong(Number count) {
+        return count != null ? count.longValue() : 0L;
+    }
+}

+ 3 - 1
alien-store/src/main/java/shop/alien/store/service/impl/StoreLogoutSyncServiceImpl.java

@@ -63,7 +63,9 @@ public class StoreLogoutSyncServiceImpl implements StoreLogoutSyncService {
         storeInfoMapper.update(null, new LambdaUpdateWrapper<StoreInfo>()
                 .eq(StoreInfo::getId, store.getId())
                 .set(StoreInfo::getLogoutFlag, StoreInfo.LOGOUT_FLAG_NORMAL)
-                .set(StoreInfo::getBusinessStatus, StoreInfo.BUSINESS_STATUS_NORMAL)
+                // 暂停营业,当撤销注销时,营业状态变成暂停营业,不管注销之前或者是注销之后的状态是什么,都改为暂停营业
+//                .set(StoreInfo::getBusinessStatus, StoreInfo.BUSINESS_STATUS_NORMAL)
+                .set(StoreInfo::getBusinessStatus, 1)
                 .set(StoreInfo::getStoreStatus, 1)
                 .set(StoreInfo::getLogoutReason, "")
                 .set(StoreInfo::getLogoutCode, "")

+ 54 - 0
alien-store/src/main/java/shop/alien/store/service/impl/StoreUserPasswordServiceImpl.java

@@ -0,0 +1,54 @@
+package shop.alien.store.service.impl;
+
+import lombok.RequiredArgsConstructor;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.stereotype.Service;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.StoreUser;
+import shop.alien.entity.store.UserLoginInfo;
+import shop.alien.entity.store.dto.StoreUserPasswordDto;
+import shop.alien.mapper.StoreUserMapper;
+import shop.alien.store.service.StoreUserPasswordService;
+
+import java.util.Objects;
+
+@Service
+@RequiredArgsConstructor
+public class StoreUserPasswordServiceImpl implements StoreUserPasswordService {
+
+    private final StoreUserMapper storeUserMapper;
+
+    @Override
+    public R<String> setPassword(UserLoginInfo login, StoreUserPasswordDto dto) {
+        if (login == null || login.getUserId() <= 0) {
+            return R.fail("请先登录");
+        }
+        if (!"store".equals(login.getType())) {
+            return R.fail("仅支持商户端操作");
+        }
+        if (dto == null || StringUtils.isBlank(dto.getPassword())) {
+            return R.fail("密码不能为空");
+        }
+        if (!Objects.equals(dto.getPassword(), dto.getConfirmPassword())) {
+            return R.fail("两次密码输入不一致");
+        }
+        StoreUser storeUser = storeUserMapper.selectById(login.getUserId());
+        if (storeUser == null || (storeUser.getDeleteFlag() != null && storeUser.getDeleteFlag() == 1)) {
+            return R.fail("商户账号不存在");
+        }
+        if (storeUser.getStatus() != null && storeUser.getStatus() == 1) {
+            return R.fail("账号被禁用");
+        }
+        if (storeUser.getLogoutFlag() != null && storeUser.getLogoutFlag() == StoreUser.LOGOUT_FLAG_DONE) {
+            return R.fail("账号已注销");
+        }
+        StoreUser update = new StoreUser();
+        update.setId(login.getUserId());
+        update.setPassword(dto.getPassword());
+        update.setPassType(1);
+        if (storeUserMapper.updateById(update) <= 0) {
+            return R.fail("设置密码失败");
+        }
+        return R.success("设置密码成功");
+    }
+}

+ 130 - 0
alien-store/src/main/java/shop/alien/store/service/impl/StoreUserServiceImpl.java

@@ -741,6 +741,132 @@ public class StoreUserServiceImpl extends ServiceImpl<StoreUserMapper, StoreUser
         return list == null ? Collections.emptyList() : list.stream().map(StorePlatformUserRole::getUserId).distinct().collect(Collectors.toList());
     }
 
+
+    /**
+     * 主账号注销时:解除该店铺下所有子账号绑定;若子账号仅绑定此店铺且非主账号,则逻辑删除 store_user。
+     * 逻辑参照 {@code StorePlatformUserRoleServiceImpl#batchDeleteSubAccounts}。
+     */
+    private void unbindSubAccountsOnMainAccountLogout(Integer storeId, Integer mainAccountUserId) {
+        if (storeId == null) {
+            return;
+        }
+        List<Integer> subAccountUserIds = getSubAccountUserIdsByStoreId(storeId, mainAccountUserId);
+        if (CollectionUtils.isEmpty(subAccountUserIds)) {
+            return;
+        }
+        log.info("主账号注销,解除店铺子账号绑定: storeId={}, mainAccountUserId={}, subAccountCount={}",
+                storeId, mainAccountUserId, subAccountUserIds.size());
+        for (Integer userId : subAccountUserIds) {
+            unbindSubAccountFromStore(userId, storeId);
+        }
+    }
+
+    /**
+     * 注销时解除当前账号作为其他店铺子账号的绑定(仅清除 store_platform_user_role,不删除 store_user)。
+     */
+    private void unbindSelfAsSubAccountOnOtherStores(StoreUser storeUser) {
+        if (storeUser == null || storeUser.getId() == null) {
+            return;
+        }
+        LambdaQueryWrapper<StorePlatformUserRole> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(StorePlatformUserRole::getUserId, storeUser.getId())
+                .eq(StorePlatformUserRole::getDeleteFlag, 0);
+        if (storeUser.getStoreId() != null) {
+            queryWrapper.ne(StorePlatformUserRole::getStoreId, storeUser.getStoreId());
+        }
+        List<StorePlatformUserRole> roles = storePlatformUserRoleMapper.selectList(queryWrapper);
+        if (CollectionUtils.isEmpty(roles)) {
+            return;
+        }
+        Set<Integer> otherStoreIds = roles.stream()
+                .map(StorePlatformUserRole::getStoreId)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toCollection(LinkedHashSet::new));
+        log.info("注销账号,解除自身在其他店铺的子账号绑定: userId={}, storeIds={}", storeUser.getId(), otherStoreIds);
+        for (Integer storeId : otherStoreIds) {
+            LambdaUpdateWrapper<StorePlatformUserRole> updateWrapper = new LambdaUpdateWrapper<>();
+            updateWrapper.eq(StorePlatformUserRole::getUserId, storeUser.getId())
+                    .eq(StorePlatformUserRole::getStoreId, storeId)
+                    .eq(StorePlatformUserRole::getDeleteFlag, 0)
+                    .set(StorePlatformUserRole::getDeleteFlag, 1);
+            int updated = storePlatformUserRoleMapper.update(null, updateWrapper);
+            if (updated > 0) {
+                log.info("已解除自身子账号绑定: userId={}, storeId={}, count={}", storeUser.getId(), storeId, updated);
+            }
+        }
+    }
+
+    /**
+     * 解除单个子账号与指定店铺的绑定;仅绑定一个店铺且非主账号时逻辑删除 store_user。
+     */
+    private void unbindSubAccountFromStore(Integer userId, Integer storeId) {
+        if (userId == null || storeId == null) {
+            return;
+        }
+        long subAccountCount = countActiveSubAccountRelations(userId);
+
+        LambdaUpdateWrapper<StorePlatformUserRole> roleUpdateWrapper = new LambdaUpdateWrapper<>();
+        roleUpdateWrapper.eq(StorePlatformUserRole::getUserId, userId)
+                .eq(StorePlatformUserRole::getStoreId, storeId)
+                .eq(StorePlatformUserRole::getDeleteFlag, 0)
+                .set(StorePlatformUserRole::getDeleteFlag, 1);
+        int roleUpdateResult = storePlatformUserRoleMapper.update(null, roleUpdateWrapper);
+        if (roleUpdateResult <= 0) {
+            log.warn("主账号注销解除子账号绑定失败: userId={}, storeId={}", userId, storeId);
+            return;
+        }
+
+        StoreUser subUser = storeUserMapper.selectById(userId);
+        clearStoreUserToken(subUser);
+
+        if (subAccountCount == 1) {
+            if (isStoreMainAccount(subUser)) {
+
+
+                log.info("子账号同时是主账号,仅解除绑定不删除 store_user: userId={}, storeId={}", userId, storeId);
+            } else {
+                LambdaUpdateWrapper<StoreUser> userUpdateWrapper = new LambdaUpdateWrapper<>();
+                userUpdateWrapper.eq(StoreUser::getId, userId)
+                        .eq(StoreUser::getDeleteFlag, 0)
+                        .set(StoreUser::getDeleteFlag, 1);
+                int userUpdateResult = storeUserMapper.update(null, userUpdateWrapper);
+                if (userUpdateResult > 0) {
+                    log.info("子账号仅绑定一个店铺,已逻辑删除 store_user: userId={}, storeId={}", userId, storeId);
+                } else {
+                    log.warn("子账号逻辑删除 store_user 失败: userId={}", userId);
+                }
+            }
+        } else {
+            log.info("子账号绑定多个店铺,仅解除当前店铺绑定: userId={}, storeId={}, totalBindings={}",
+                    userId, storeId, subAccountCount);
+        }
+    }
+
+    private boolean isStoreMainAccount(StoreUser user) {
+        if (user == null) {
+            return false;
+        }
+        if (user.getStoreId() != null && user.getStoreId() > 0) {
+            return true;
+        }
+        return user.getAccountType() != null && user.getAccountType() == 1;
+    }
+
+    private void clearStoreUserToken(StoreUser storeUser) {
+        if (storeUser == null || storeUser.getPhone() == null) {
+            return;
+        }
+        try {
+            String tokenKey = "store_" + storeUser.getPhone();
+            if (baseRedisService.getString(tokenKey) != null) {
+                baseRedisService.delete(tokenKey);
+                log.info("清除子账号 token: userId={}, phone={}", storeUser.getId(), storeUser.getPhone());
+            }
+        } catch (Exception e) {
+            log.warn("清除子账号 token 异常: userId={}, msg={}", storeUser.getId(), e.getMessage());
+        }
+    }
+
     private List<Integer> getSubAccountUserIds(Integer excludeUserId) {
         if (excludeUserId == null) return Collections.emptyList();
         LambdaQueryWrapper<StorePlatformUserRole> w = new LambdaQueryWrapper<>();
@@ -1160,6 +1286,10 @@ public class StoreUserServiceImpl extends ServiceImpl<StoreUserMapper, StoreUser
             if (num > 0) {
                 storeLogoutSyncService.applyCoolingForStoreUser(
                         storeUser, storeUserVo.getLogoutReason(), storeUserVo.getLogoutCode());
+                if (storeUser.getStoreId() != null) {
+                    unbindSubAccountsOnMainAccountLogout(storeUser.getStoreId(), storeUser.getId());
+                }
+                unbindSelfAsSubAccountOnOtherStores(storeUser);
                 // 发送通知
                 LifeNotice lifeMessage = new LifeNotice();
                 lifeMessage.setReceiverId("store_" + storeUser.getPhone());

+ 7 - 13
docs/devops/dev/Introduction.md

@@ -1,6 +1,6 @@
 # 开发环境(deve)运维说明
 
-本文档描述与 **测试环境(sit/test)** 并行运行的 **开发环境** 的设计、部署与日常运维。配置脚本位于仓库 `docs/devops/dev/`。
+本文档描述与 **测试环境(sit)** 并行运行的 **开发环境** 的设计、部署与日常运维。配置脚本位于仓库 `docs/devops/dev/`。
 
 ---
 
@@ -344,23 +344,17 @@ tar -czf /tmp/alien-store-logs-$(date +%Y%m%d-%H%M%S).tar.gz \
 
 ---
 
-## 13. Nginx(待配置)
+## 13. Nginx
 
-在现有 `nginx-1.28` 的 `conf.d` 中增加 dev 入口(示例):
+配置文件:`docs/devops/dev/middleware/nginx/dev.conf` → 宿主机 `/docker/middleware/nginx/conf.d/dev.conf`。
 
-```nginx
-upstream dev_gateway { server 127.0.0.1:28000; }
+域名 **`deve.ailien.shop`**(与阿里云已有 `dev` 记录区分,避免 DNS 冲突)。
 
-server {
-    listen 443 ssl;
-    server_name dev.ailien.shop;
-    location / {
-        proxy_pass http://dev_gateway;
-    }
-}
+```bash
+docker exec nginx-1.28 nginx -t && docker exec nginx-1.28 nginx -s reload
 ```
 
-静态资源建议独立目录,例如 `/docker/middleware/nginx/html-dev`
+静态资源目录:`/deve/html`(如 `https://deve.ailien.shop/group_web/`)。
 
 ---
 

+ 325 - 0
docs/devops/dev/middleware/nginx/dev.conf

@@ -0,0 +1,325 @@
+# ============================================================
+# 开发环境(deve):deve.ailien.shop
+#
+# 仓库: docs/devops/dev/middleware/nginx/dev.conf
+# 宿主机: /docker/middleware/nginx/conf.d/dev.conf
+# 下载备份同级: E:\temp\nginx\conf.d\dev.conf(与 test.conf 同级)
+#
+# 加载关系(见上一级 nginx.conf):
+#   include /docker/middleware/nginx/*.conf;
+# 常见 compose 将宿主机 conf.d 挂载为容器内 /docker/middleware/nginx/,
+# 则本文件在容器内路径为 /docker/middleware/nginx/dev.conf,与 test.conf 一并被 include。
+#
+# 本文件勿重复定义(已在 test.conf 的 http 上下文):
+#   limit_req_zone payment_prepay
+#   map $http_upgrade $connection_upgrade
+#   map $http_origin $cors_origin
+#   upstream gateway / store / dining / upl_ai_upload / test_ai_service
+#
+# 静态资源: Jenkins 部署到宿主机 /deve/html → 容器内 /deve/html(须 nginx 挂载)
+# 上传目录: 宿主机 /deve/data/uploads → 容器内 /deve/data/uploads(可选挂载)
+#
+# Java dev 宿主机端口: gateway 28000, store 28004, second 28005, dining 28014
+# gateway 与 nginx 同在 app-network 时用容器名;store/dining 与 test.conf 一致走宿主机端口。
+# ============================================================
+
+upstream dev_gateway {
+    server gateway-dev:8000;
+    keepalive 32;
+}
+
+upstream dev_store {
+    server 120.26.186.130:28004;
+    keepalive 8;
+}
+
+upstream dev_dining {
+    server 120.26.186.130:28014;
+    keepalive 8;
+}
+
+# --------------- deve.ailien.shop HTTP → HTTPS ---------------
+server {
+    listen 80;
+    server_name deve.ailien.shop;
+    access_log /var/log/nginx/deve.ailien.shop.80.access.log main;
+    error_log  /var/log/nginx/deve.ailien.shop.80.error.log warn;
+
+    location / {
+        return 308 https://$host$request_uri;
+    }
+}
+
+# --------------- deve.ailien.shop (443) ---------------
+server {
+    listen 443 ssl;
+    http2 on;
+    server_name deve.ailien.shop;
+
+    # 与 test.conf 一致,使用 ailien.shop 证书(需证书覆盖 deve.ailien.shop 或 *.ailien.shop)
+    ssl_certificate     /etc/nginx/ssl/ailien.shop.pem;
+    ssl_certificate_key /etc/nginx/ssl/ailien.shop.key;
+
+    ssl_session_timeout 1d;
+    ssl_protocols TLSv1.2 TLSv1.3;
+    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
+
+    client_max_body_size 100m;
+
+    access_log /var/log/nginx/deve.ailien.shop.access.log main;
+    error_log  /var/log/nginx/deve.ailien.shop.error.log warn;
+
+    # Docker 内置 DNS(gateway-dev 容器 IP 变更时重新解析)
+    resolver 127.0.0.11 valid=10s ipv6=off;
+    resolver_timeout 5s;
+
+    # 商户端 PC:https://deve.ailien.shop/group_web_merchant/
+    location /group_web_merchant/ {
+        root /deve/html;
+        index index.html;
+        try_files $uri $uri/ @dev_merchant_spa;
+    }
+    location @dev_merchant_spa {
+        root /deve/html;
+        try_files /group_web_merchant/index.html =404;
+    }
+    location = /group_web_merchant {
+        return 301 $scheme://$host/group_web_merchant/;
+    }
+
+    # 运营中台:https://deve.ailien.shop/group_web/
+    location /group_web/ {
+        root /deve/html;
+        index index.html;
+        try_files $uri $uri/ @dev_group_spa;
+    }
+    location @dev_group_spa {
+        root /deve/html;
+        try_files /group_web/index.html =404;
+    }
+    location = /group_web {
+        return 301 $scheme://$host/group_web/;
+    }
+
+    # 律师 Web:https://deve.ailien.shop/group_lawyer_web/
+    location /group_lawyer_web/ {
+        root /deve/html;
+        index index.html;
+        try_files $uri $uri/ @dev_lawyer_spa;
+    }
+    location @dev_lawyer_spa {
+        root /deve/html;
+        try_files /group_lawyer_web/index.html =404;
+    }
+    location = /group_lawyer_web {
+        return 301 $scheme://$host/group_lawyer_web/;
+    }
+
+    # AI WebSocket(复用 test.conf 中的 test_ai_service)
+    location /ai/ws {
+        proxy_pass http://test_ai_service;
+        proxy_http_version 1.1;
+        proxy_set_header Upgrade $http_upgrade;
+        proxy_set_header Connection $connection_upgrade;
+        proxy_set_header Host $host;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        proxy_set_header X-Forwarded-Proto $scheme;
+        proxy_connect_timeout 60s;
+        proxy_send_timeout 3600s;
+        proxy_read_timeout 3600s;
+    }
+
+    # AI 接口(复用 test.conf 中的 test_ai_service)
+    location /ai/ {
+        if ($request_method = 'OPTIONS') {
+            add_header 'Access-Control-Allow-Origin' $cors_origin;
+            add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, PATCH, OPTIONS';
+            add_header 'Access-Control-Allow-Headers' '*';
+            add_header 'Access-Control-Allow-Credentials' 'true';
+            add_header 'Access-Control-Max-Age' 3600;
+            add_header 'Content-Length' 0;
+            return 204;
+        }
+        add_header 'Access-Control-Allow-Origin' $cors_origin always;
+        add_header 'Access-Control-Allow-Credentials' 'true' always;
+        proxy_pass http://test_ai_service;
+        proxy_http_version 1.1;
+        proxy_set_header Host $host;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        proxy_set_header X-Forwarded-Proto $scheme;
+        proxy_connect_timeout 60s;
+        proxy_send_timeout 60s;
+        proxy_read_timeout 60s;
+    }
+
+    # WebSocket 直连 store-dev:/alienStore/socket/ → /socket/
+    location /alienStore/socket/ {
+        rewrite ^/alienStore/socket/(.*)$ /socket/$1 break;
+        proxy_pass http://dev_store;
+        proxy_http_version 1.1;
+        proxy_set_header Upgrade $http_upgrade;
+        proxy_set_header Connection $connection_upgrade;
+        proxy_set_header Host $host;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        proxy_set_header X-Forwarded-Proto $scheme;
+        proxy_connect_timeout 60s;
+        proxy_send_timeout 3600s;
+        proxy_read_timeout 3600s;
+    }
+
+    # 点餐 SSE:直连 dining-dev
+    location /alienDining/store/order/sse/ {
+        rewrite ^/alienDining/(.*)$ /$1 break;
+        proxy_pass http://dev_dining;
+        proxy_http_version 1.1;
+        proxy_set_header Host $host;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        proxy_set_header X-Forwarded-Proto $scheme;
+        proxy_connect_timeout 60s;
+        proxy_send_timeout 86400s;
+        proxy_read_timeout 86400s;
+        proxy_buffering off;
+    }
+
+    # 支付预下单限流(zone 在 test.conf 定义)
+    location ~* payment/prePay {
+        limit_req zone=payment_prepay burst=1 nodelay;
+        limit_req_status 429;
+        add_header X-Payment-Limit "applied" always;
+        rewrite ^/api/(.*)$ /$1 break;
+        proxy_pass http://dev_gateway;
+        proxy_http_version 1.1;
+        proxy_set_header Host $host;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        proxy_set_header X-Forwarded-Proto $scheme;
+        proxy_set_header Upgrade $http_upgrade;
+        proxy_set_header Connection $connection_upgrade;
+        proxy_connect_timeout 60s;
+        proxy_send_timeout 3600s;
+        proxy_read_timeout 3600s;
+        if ($request_method = 'OPTIONS') {
+            add_header 'Access-Control-Allow-Origin' $cors_origin;
+            add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, PATCH, OPTIONS';
+            add_header 'Access-Control-Allow-Headers' '*';
+            add_header 'Access-Control-Allow-Credentials' 'true';
+            add_header 'Access-Control-Max-Age' 3600;
+            add_header 'Content-Length' 0;
+            return 204;
+        }
+        proxy_hide_header Access-Control-Allow-Origin;
+        proxy_hide_header Access-Control-Allow-Credentials;
+        proxy_hide_header Access-Control-Allow-Methods;
+        proxy_hide_header Access-Control-Allow-Headers;
+        proxy_hide_header Access-Control-Expose-Headers;
+        proxy_hide_header Access-Control-Max-Age;
+        add_header 'Access-Control-Allow-Origin' $cors_origin always;
+        add_header 'Access-Control-Allow-Credentials' 'true' always;
+    }
+
+    # /api/ → gateway-dev(去掉 /api 前缀)
+    location /api/ {
+        rewrite ^/api/(.*)$ /$1 break;
+        proxy_pass http://dev_gateway;
+        proxy_http_version 1.1;
+        proxy_set_header Host $host;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        proxy_set_header X-Forwarded-Proto $scheme;
+        proxy_set_header Upgrade $http_upgrade;
+        proxy_set_header Connection $connection_upgrade;
+        proxy_connect_timeout 60s;
+        proxy_send_timeout 3600s;
+        proxy_read_timeout 3600s;
+        if ($request_method = 'OPTIONS') {
+            add_header 'Access-Control-Allow-Origin' $cors_origin;
+            add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, PATCH, OPTIONS';
+            add_header 'Access-Control-Allow-Headers' '*';
+            add_header 'Access-Control-Allow-Credentials' 'true';
+            add_header 'Access-Control-Max-Age' 3600;
+            add_header 'Content-Length' 0;
+            return 204;
+        }
+        proxy_hide_header Access-Control-Allow-Origin;
+        proxy_hide_header Access-Control-Allow-Credentials;
+        proxy_hide_header Access-Control-Allow-Methods;
+        proxy_hide_header Access-Control-Allow-Headers;
+        proxy_hide_header Access-Control-Expose-Headers;
+        proxy_hide_header Access-Control-Max-Age;
+        add_header 'Access-Control-Allow-Origin' $cors_origin always;
+        add_header 'Access-Control-Allow-Credentials' 'true' always;
+    }
+
+    # H5 静态:/deve/html/h5/HBuilderProjects/...
+    # 对照 test.conf:root 到 html 根,勿写 root .../h5/(会拼成 h5/h5 404)
+    location ^~ /h5/HBuilderProjects/ {
+        root /deve/html;
+        try_files $uri =404;
+        add_header Cache-Control "public, max-age=300";
+    }
+
+    # 业务上传文件(对照 test 的 /alien_test/data/uploads/)
+    location ^~ /uploads/ {
+        alias /deve/data/uploads/;
+        try_files $uri =404;
+        add_header Cache-Control "public, max-age=86400";
+    }
+
+    # Tus/上传:复用 test.conf 的 upl_ai_upload → uat.ailien.shop
+    location = /ai-upload {
+        return 301 $scheme://$host/ai-upload/;
+    }
+    location ^~ /ai-upload/ {
+        rewrite ^/ai-upload(.*)$ $1 break;
+        proxy_pass https://upl_ai_upload;
+        proxy_http_version 1.1;
+        proxy_set_header Connection "";
+        proxy_ssl_server_name on;
+        proxy_ssl_name uat.ailien.shop;
+        proxy_set_header Host uat.ailien.shop;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        proxy_set_header X-Forwarded-Proto $scheme;
+        proxy_connect_timeout 60s;
+        proxy_send_timeout 3600s;
+        proxy_read_timeout 3600s;
+        client_max_body_size 0;
+        proxy_request_buffering off;
+    }
+
+    # 其余请求 → gateway-dev
+    location / {
+        if ($request_method = 'OPTIONS') {
+            add_header 'Access-Control-Allow-Origin' $cors_origin;
+            add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, PATCH, OPTIONS';
+            add_header 'Access-Control-Allow-Headers' '*';
+            add_header 'Access-Control-Allow-Credentials' 'true';
+            add_header 'Access-Control-Max-Age' 3600;
+            add_header 'Content-Length' 0;
+            return 204;
+        }
+        proxy_hide_header Access-Control-Allow-Origin;
+        proxy_hide_header Access-Control-Allow-Credentials;
+        proxy_hide_header Access-Control-Allow-Methods;
+        proxy_hide_header Access-Control-Allow-Headers;
+        proxy_hide_header Access-Control-Expose-Headers;
+        proxy_hide_header Access-Control-Max-Age;
+        add_header 'Access-Control-Allow-Origin' $cors_origin always;
+        add_header 'Access-Control-Allow-Credentials' 'true' always;
+        proxy_pass http://dev_gateway;
+        proxy_http_version 1.1;
+        proxy_set_header Upgrade $http_upgrade;
+        proxy_set_header Connection $connection_upgrade;
+        proxy_set_header Host $host;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        proxy_set_header X-Forwarded-Proto $scheme;
+        proxy_connect_timeout 60s;
+        proxy_send_timeout 3600s;
+        proxy_read_timeout 3600s;
+    }
+}