浏览代码

Merge branch 'sit' into uat-20260202

dujian 6 小时之前
父节点
当前提交
c723967ba3
共有 34 个文件被更改,包括 1713 次插入79 次删除
  1. 3 2
      alien-entity/src/main/java/shop/alien/entity/store/LawyerUser.java
  2. 218 0
      alien-entity/src/main/java/shop/alien/entity/store/OceanEngineClickMonitorLog.java
  3. 16 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/StoreUserPasswordDto.java
  4. 20 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/RegisteredLawyerUserCountVo.java
  5. 20 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/RegisteredStoreUserCountVo.java
  6. 20 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/RegisteredUserCountVo.java
  7. 1 1
      alien-entity/src/main/java/shop/alien/mapper/LifeUserMapper.java
  8. 13 0
      alien-entity/src/main/java/shop/alien/mapper/OceanEngineClickMonitorLogMapper.java
  9. 2 1
      alien-entity/src/main/resources/mapper/OrderReviewMapper.xml
  10. 3 1
      alien-gateway/src/main/java/shop/alien/gateway/controller/LifeUserController.java
  11. 19 6
      alien-gateway/src/main/java/shop/alien/gateway/controller/StoreUserController.java
  12. 26 5
      alien-gateway/src/main/java/shop/alien/gateway/service/LifeUserService.java
  13. 10 35
      alien-lawyer/src/main/java/shop/alien/lawyer/controller/LawyerUserLogInController.java
  14. 5 0
      alien-lawyer/src/main/java/shop/alien/lawyer/service/LawyerUserLogInService.java
  15. 148 8
      alien-lawyer/src/main/java/shop/alien/lawyer/service/impl/LawyerUserLogInServiceImpl.java
  16. 81 0
      alien-store/src/main/java/shop/alien/store/controller/OceanEngineAdTrackController.java
  17. 79 0
      alien-store/src/main/java/shop/alien/store/controller/RegisteredUserStatisticsController.java
  18. 17 0
      alien-store/src/main/java/shop/alien/store/controller/StoreUserController.java
  19. 25 0
      alien-store/src/main/java/shop/alien/store/dto/OceanEngineClickBindUserDto.java
  20. 17 1
      alien-store/src/main/java/shop/alien/store/service/LifeUserDynamicsService.java
  21. 1 1
      alien-store/src/main/java/shop/alien/store/service/LifeUserService.java
  22. 40 0
      alien-store/src/main/java/shop/alien/store/service/OceanEngineClickMonitorLogService.java
  23. 23 0
      alien-store/src/main/java/shop/alien/store/service/RegisteredUserStatisticsService.java
  24. 10 0
      alien-store/src/main/java/shop/alien/store/service/StoreUserPasswordService.java
  25. 18 2
      alien-store/src/main/java/shop/alien/store/service/channel/CommonPushVendorHttpClient.java
  26. 10 1
      alien-store/src/main/java/shop/alien/store/service/impl/AccountLogoutCompleteServiceImpl.java
  27. 1 1
      alien-store/src/main/java/shop/alien/store/service/impl/CommonCommentServiceImpl.java
  28. 278 0
      alien-store/src/main/java/shop/alien/store/service/impl/OceanEngineClickMonitorLogServiceImpl.java
  29. 70 0
      alien-store/src/main/java/shop/alien/store/service/impl/RegisteredUserStatisticsServiceImpl.java
  30. 3 1
      alien-store/src/main/java/shop/alien/store/service/impl/StoreLogoutSyncServiceImpl.java
  31. 54 0
      alien-store/src/main/java/shop/alien/store/service/impl/StoreUserPasswordServiceImpl.java
  32. 130 0
      alien-store/src/main/java/shop/alien/store/service/impl/StoreUserServiceImpl.java
  33. 7 13
      docs/devops/dev/Introduction.md
  34. 325 0
      docs/devops/dev/middleware/nginx/dev.conf

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

@@ -7,6 +7,7 @@ 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;
@@ -23,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> {
 
@@ -69,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 = "支付密码")

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

@@ -0,0 +1,218 @@
+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 = "删除标记, 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;
+}

+ 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;
+}

+ 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("账号被禁用");

+ 26 - 5
alien-gateway/src/main/java/shop/alien/gateway/service/LifeUserService.java

@@ -144,16 +144,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<>();
@@ -235,9 +239,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) {

+ 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);
 

+ 148 - 8
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)) {
@@ -204,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());
@@ -224,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 {

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

@@ -0,0 +1,81 @@
+package shop.alien.store.controller;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiImplicitParam;
+import io.swagger.annotations.ApiImplicitParams;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiOperationSupport;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.OceanEngineClickMonitorLog;
+import shop.alien.store.dto.OceanEngineClickBindUserDto;
+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;
+
+    /**
+     * 点击监测链接接收入口(巨量引擎以 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);
+    }
+}

+ 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;
+}

+ 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;
     }

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

@@ -89,7 +89,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 =

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

@@ -0,0 +1,40 @@
+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 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);
+}

+ 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;
     }
 

+ 10 - 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() + " 已注销");

+ 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){

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

@@ -0,0 +1,278 @@
+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.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.service.OceanEngineClickMonitorLogService;
+
+import javax.servlet.http.HttpServletRequest;
+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 {
+
+    @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));
+    }
+
+    /**
+     * 从 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;
+    }
+}