Ver Fonte

Merge branch 'sit-new-demands' into release_20260123

lutong há 3 meses atrás
pai
commit
0bf16f0fec
21 ficheiros alterados com 2031 adições e 7 exclusões
  1. 81 0
      alien-entity/src/main/java/shop/alien/entity/store/StoreRenovationBrowseRecord.java
  2. 148 0
      alien-entity/src/main/java/shop/alien/entity/store/StoreRenovationRequirement.java
  3. 84 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/StoreRenovationBrowseRequirementDto.java
  4. 110 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/StoreRenovationRequirementDto.java
  5. 3 2
      alien-entity/src/main/java/shop/alien/entity/store/vo/StoreMainInfoVo.java
  6. 17 0
      alien-entity/src/main/java/shop/alien/mapper/StoreRenovationBrowseRecordMapper.java
  7. 17 0
      alien-entity/src/main/java/shop/alien/mapper/StoreRenovationRequirementMapper.java
  8. 171 0
      alien-entity/src/main/resources/db/migration/renovation_chat_analysis.md
  9. 38 0
      alien-entity/src/main/resources/db/migration/store_renovation_requirement.sql
  10. 1 1
      alien-gateway/src/main/resources/bootstrap.yml
  11. 1 1
      alien-lawyer/src/main/resources/bootstrap.yml
  12. 1 1
      alien-second/src/main/resources/bootstrap.yml
  13. 1 1
      alien-store-platform/src/main/resources/bootstrap.yml
  14. 2 0
      alien-store/src/main/java/shop/alien/store/config/WebSocketProcess.java
  15. 252 0
      alien-store/src/main/java/shop/alien/store/controller/StoreRenovationRequirementController.java
  16. 52 0
      alien-store/src/main/java/shop/alien/store/service/StoreRenovationBrowseRecordService.java
  17. 105 0
      alien-store/src/main/java/shop/alien/store/service/StoreRenovationRequirementService.java
  18. 11 0
      alien-store/src/main/java/shop/alien/store/service/impl/StoreInfoServiceImpl.java
  19. 313 0
      alien-store/src/main/java/shop/alien/store/service/impl/StoreRenovationBrowseRecordServiceImpl.java
  20. 622 0
      alien-store/src/main/java/shop/alien/store/service/impl/StoreRenovationRequirementServiceImpl.java
  21. 1 1
      alien-store/src/main/resources/bootstrap.yml

+ 81 - 0
alien-entity/src/main/java/shop/alien/entity/store/StoreRenovationBrowseRecord.java

@@ -0,0 +1,81 @@
+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 org.springframework.format.annotation.DateTimeFormat;
+
+import java.util.Date;
+
+/**
+ * 装修商铺访问装修需求浏览记录表
+ * <p>
+ * 记录装修商铺(装修公司)查看/访问装修需求动态的历史记录
+ * </p>
+ *
+ * @author auto-generated
+ * @since 2025-01-15
+ */
+@Data
+@JsonInclude
+@TableName("store_renovation_browse_record")
+@ApiModel(value = "StoreRenovationBrowseRecord对象", description = "装修商铺访问装修需求浏览记录表")
+public class StoreRenovationBrowseRecord {
+
+    @ApiModelProperty(value = "主键")
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    @ApiModelProperty(value = "装修商铺ID(访问者,装修公司的门店ID)")
+    @TableField("renovation_store_id")
+    private Integer renovationStoreId;
+
+    @ApiModelProperty(value = "装修需求ID(被访问的装修需求)")
+    @TableField("requirement_id")
+    private Integer requirementId;
+
+    @ApiModelProperty(value = "访问时间")
+    @TableField("browse_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date browseTime;
+
+    @ApiModelProperty(value = "是否已联系(0:未联系, 1:已联系)")
+    @TableField("is_contacted")
+    private Integer isContacted;
+
+    @ApiModelProperty(value = "联系时间(首次联系时间)")
+    @TableField("contact_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date contactTime;
+
+    @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")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createdTime;
+
+    @ApiModelProperty(value = "创建人ID")
+    @TableField("created_user_id")
+    private Integer createdUserId;
+
+    @ApiModelProperty(value = "修改时间")
+    @TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date updatedTime;
+
+    @ApiModelProperty(value = "修改人ID")
+    @TableField("updated_user_id")
+    private Integer updatedUserId;
+}
+

+ 148 - 0
alien-entity/src/main/java/shop/alien/entity/store/StoreRenovationRequirement.java

@@ -0,0 +1,148 @@
+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 org.springframework.format.annotation.DateTimeFormat;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 装修需求动态表
+ * <p>
+ * 注意:装修需求的图片和视频附件直接存储在本表的 attachment_urls 字段中
+ * 存储格式:多个URL用逗号拼接,例如:"url1,url2,url3"
+ * 最多支持9个附件(图片或视频)
+ * </p>
+ *
+ * @author auto-generated
+ * @since 2025-01-15
+ */
+@Data
+@JsonInclude
+@TableName("store_renovation_requirement")
+@ApiModel(value = "StoreRenovationRequirement对象", description = "装修需求动态表")
+public class StoreRenovationRequirement {
+
+    @ApiModelProperty(value = "主键")
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    @ApiModelProperty(value = "门店ID(发布装修需求的普通公司门店)")
+    @TableField("store_id")
+    private Integer storeId;
+
+    @ApiModelProperty(value = "需求标题")
+    @TableField("requirement_title")
+    private String requirementTitle;
+
+    @ApiModelProperty(value = "装修类型(1:新房装修, 2:旧房改造, 3:局部装修)")
+    @TableField("renovation_type")
+    private Integer renovationType;
+
+    @ApiModelProperty(value = "房屋面积(平方米)")
+    @TableField("house_area")
+    private BigDecimal houseArea;
+
+    @ApiModelProperty(value = "装修预算(万元)")
+    @TableField("renovation_budget")
+    private BigDecimal renovationBudget;
+
+    @ApiModelProperty(value = "详细需求描述")
+    @TableField("detailed_requirement")
+    private String detailedRequirement;
+
+    @ApiModelProperty(value = "房屋图纸附件(图片和视频URL,多个URL用逗号拼接,最多9个)")
+    @TableField("attachment_urls")
+    private String attachmentUrls;
+
+    @ApiModelProperty(value = "期望装修时间")
+    @TableField("expected_renovation_time")
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
+    @DateTimeFormat(pattern = "yyyy-MM-dd")
+    private Date expectedRenovationTime;
+
+    @ApiModelProperty(value = "联系人姓名")
+    @TableField("contact_name")
+    private String contactName;
+
+    @ApiModelProperty(value = "联系电话")
+    @TableField("contact_phone")
+    private String contactPhone;
+
+    @ApiModelProperty(value = "所在城市")
+    @TableField("city")
+    private String city;
+
+    @ApiModelProperty(value = "城市编码(adcode)")
+    @TableField("city_adcode")
+    private String cityAdcode;
+
+    @ApiModelProperty(value = "详细地址")
+    @TableField("detailed_address")
+    private String detailedAddress;
+
+    @ApiModelProperty(value = "服务协议确认(0:未确认, 1:已确认)")
+    @TableField("agreement_confirmed")
+    private Integer agreementConfirmed;
+
+    @ApiModelProperty(value = "动态状态(0:草稿, 1:已发布, 2:已下架)")
+    @TableField("status")
+    private Integer status;
+
+    @ApiModelProperty(value = "浏览数")
+    @TableField("view_count")
+    private Integer viewCount;
+
+    @ApiModelProperty(value = "咨询数(装修商家查看并联系的数量)")
+    @TableField("inquiry_count")
+    private Integer inquiryCount;
+
+    @ApiModelProperty(value = "审核状态(0:待审核, 1:审核通过, 2:审核失败)")
+    @TableField("audit_status")
+    private Integer auditStatus;
+
+    @ApiModelProperty(value = "审核失败原因")
+    @TableField("audit_reason")
+    private String auditReason;
+
+    @ApiModelProperty(value = "审核时间")
+    @TableField("audit_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date auditTime;
+
+    @ApiModelProperty(value = "审核人ID")
+    @TableField("audit_user_id")
+    private Integer auditUserId;
+
+    @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")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createdTime;
+
+    @ApiModelProperty(value = "创建人ID")
+    @TableField("created_user_id")
+    private Integer createdUserId;
+
+    @ApiModelProperty(value = "修改时间")
+    @TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date updatedTime;
+
+    @ApiModelProperty(value = "修改人ID")
+    @TableField("updated_user_id")
+    private Integer updatedUserId;
+}
+

+ 84 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/StoreRenovationBrowseRequirementDto.java

@@ -0,0 +1,84 @@
+package shop.alien.entity.store.dto;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
+import shop.alien.entity.store.dto.deserializer.StringToListDeserializer;
+
+import java.math.BigDecimal;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 装修商铺浏览过的装修需求DTO(包含浏览信息)
+ *
+ * @author auto-generated
+ * @since 2025-01-15
+ */
+@Data
+@JsonInclude
+@ApiModel(value = "StoreRenovationBrowseRequirementDto对象", description = "装修商铺浏览过的装修需求DTO")
+public class StoreRenovationBrowseRequirementDto {
+
+    @ApiModelProperty(value = "装修需求ID")
+    private Integer requirementId;
+
+    @ApiModelProperty(value = "需求标题")
+    private String requirementTitle;
+
+    @ApiModelProperty(value = "装修类型(1:新房装修, 2:旧房改造, 3:局部装修)")
+    private Integer renovationType;
+
+    @ApiModelProperty(value = "房屋面积(平方米)")
+    private BigDecimal houseArea;
+
+    @ApiModelProperty(value = "装修预算(万元)")
+    private BigDecimal renovationBudget;
+
+    @ApiModelProperty(value = "详细需求描述")
+    private String detailedRequirement;
+
+    @ApiModelProperty(value = "期望装修时间")
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
+    @DateTimeFormat(pattern = "yyyy-MM-dd")
+    private Date expectedRenovationTime;
+
+    @ApiModelProperty(value = "联系人姓名")
+    private String contactName;
+
+    @ApiModelProperty(value = "联系电话")
+    private String contactPhone;
+
+    @ApiModelProperty(value = "所在城市")
+    private String city;
+
+    @ApiModelProperty(value = "详细地址")
+    private String detailedAddress;
+
+    @ApiModelProperty(value = "附件列表(图片/视频URL列表)")
+    @JsonDeserialize(using = StringToListDeserializer.class)
+    private List<String> attachmentUrls;
+
+    @ApiModelProperty(value = "浏览时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date browseTime;
+
+    @ApiModelProperty(value = "是否已联系(0:未联系, 1:已联系)")
+    private Integer isContacted;
+
+    @ApiModelProperty(value = "联系时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date contactTime;
+
+    @ApiModelProperty(value = "创建时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createdTime;
+}
+

+ 110 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/StoreRenovationRequirementDto.java

@@ -0,0 +1,110 @@
+package shop.alien.entity.store.dto;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
+import shop.alien.entity.store.dto.deserializer.StringToListDeserializer;
+
+import java.math.BigDecimal;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 装修需求动态DTO
+ *
+ * @author auto-generated
+ * @since 2025-01-15
+ */
+@Data
+@JsonInclude
+@ApiModel(value = "StoreRenovationRequirementDto对象", description = "装修需求动态DTO")
+public class StoreRenovationRequirementDto {
+
+    @ApiModelProperty(value = "主键")
+    private Integer id;
+
+    @ApiModelProperty(value = "门店ID")
+    private Integer storeId;
+
+    @ApiModelProperty(value = "需求标题")
+    private String requirementTitle;
+
+    @ApiModelProperty(value = "装修类型(1:新房装修, 2:旧房改造, 3:局部装修)")
+    private Integer renovationType;
+
+    @ApiModelProperty(value = "房屋面积(平方米)")
+    private BigDecimal houseArea;
+
+    @ApiModelProperty(value = "装修预算(万元)")
+    private BigDecimal renovationBudget;
+
+    @ApiModelProperty(value = "详细需求描述")
+    private String detailedRequirement;
+
+    @ApiModelProperty(value = "期望装修时间")
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
+    @DateTimeFormat(pattern = "yyyy-MM-dd")
+    private Date expectedRenovationTime;
+
+    @ApiModelProperty(value = "联系人姓名")
+    private String contactName;
+
+    @ApiModelProperty(value = "联系电话")
+    private String contactPhone;
+
+    @ApiModelProperty(value = "所在城市")
+    private String city;
+
+    @ApiModelProperty(value = "城市编码(adcode)")
+    private String cityAdcode;
+
+    @ApiModelProperty(value = "详细地址")
+    private String detailedAddress;
+
+    @ApiModelProperty(value = "服务协议确认(0:未确认, 1:已确认)")
+    private Integer agreementConfirmed;
+
+    @ApiModelProperty(value = "动态状态(0:草稿, 1:已发布, 2:已下架)")
+    private Integer status;
+
+    @ApiModelProperty(value = "浏览数")
+    private Integer viewCount;
+
+    @ApiModelProperty(value = "咨询数")
+    private Integer inquiryCount;
+
+    @ApiModelProperty(value = "附件列表(图片/视频URL列表)")
+    @JsonDeserialize(using = StringToListDeserializer.class)
+    private List<String> attachmentUrls;
+
+    @ApiModelProperty(value = "创建时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createdTime;
+
+    @ApiModelProperty(value = "修改时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date updatedTime;
+
+    // --- 商铺信息字段 ---
+    @ApiModelProperty(value = "商铺名称")
+    private String storeName;
+
+    @ApiModelProperty(value = "商铺电话")
+    private String storeTel;
+
+    @ApiModelProperty(value = "商铺地址")
+    private String storeAddress;
+
+    @ApiModelProperty(value = "商铺简介")
+    private String storeBlurb;
+
+    @ApiModelProperty(value = "是否已与发布商铺发生沟通(true:已沟通, false:未沟通)")
+    private Boolean hasCommunicated;
+}
+

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

@@ -1,7 +1,5 @@
 package shop.alien.entity.store.vo;
 
-import com.baomidou.mybatisplus.annotation.TableField;
-import com.fasterxml.jackson.annotation.JsonFormat;
 import com.fasterxml.jackson.annotation.JsonInclude;
 import io.swagger.annotations.ApiModelProperty;
 import lombok.Data;
@@ -120,4 +118,7 @@ public class StoreMainInfoVo extends StoreInfo {
     @ApiModelProperty(value = "其他资质证明图片列表")
     private List<StoreImg> otherQualificationImages;
 
+    @ApiModelProperty(value = "评价数量")
+    private Integer ratingCount;
+
 }

+ 17 - 0
alien-entity/src/main/java/shop/alien/mapper/StoreRenovationBrowseRecordMapper.java

@@ -0,0 +1,17 @@
+package shop.alien.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+import shop.alien.entity.store.StoreRenovationBrowseRecord;
+
+/**
+ * 装修商铺访问装修需求浏览记录表 Mapper 接口
+ *
+ * @author auto-generated
+ * @since 2025-01-15
+ */
+@Mapper
+public interface StoreRenovationBrowseRecordMapper extends BaseMapper<StoreRenovationBrowseRecord> {
+
+}
+

+ 17 - 0
alien-entity/src/main/java/shop/alien/mapper/StoreRenovationRequirementMapper.java

@@ -0,0 +1,17 @@
+package shop.alien.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+import shop.alien.entity.store.StoreRenovationRequirement;
+
+/**
+ * 装修需求动态表 Mapper 接口
+ *
+ * @author auto-generated
+ * @since 2025-01-15
+ */
+@Mapper
+public interface StoreRenovationRequirementMapper extends BaseMapper<StoreRenovationRequirement> {
+
+}
+

+ 171 - 0
alien-entity/src/main/resources/db/migration/renovation_chat_analysis.md

@@ -0,0 +1,171 @@
+# 装修需求聊天功能分析报告
+
+## 一、现有系统支持情况
+
+### ✅ 已支持的功能
+
+1. **聊天表结构(LifeMessage)**
+   - 支持 `user_` + 手机号 格式(用户端)
+   - 支持 `store_` + 手机号 格式(商户端)
+   - 有 `businessId` 字段可关联装修需求ID
+   - 支持多种消息类型(文本、图片、链接等)
+   - 支持已读/未读状态管理
+
+2. **WebSocket实时通信**
+   - 已实现 WebSocket 实时消息推送
+   - 消息保存时可通过 `businessId` 关联装修需求
+
+3. **现有消息接口**
+   - `/message/getMessageList` - 获取消息列表
+   - `/message/getMessageListByReceiverId` - 获取聊天详情
+   - `/message/getAllNoReadCount` - 获取未读消息数
+
+## 二、需要补充的功能
+
+### 1. 从装修需求发起聊天接口
+
+**需求场景:**
+- 装修商铺查看装修需求后,点击"联系商家"按钮发起聊天
+- 商户查看装修需求后,点击"联系装修商"按钮发起聊天
+
+**需要实现的接口:**
+```
+POST /renovation/startChat
+参数:
+  - requirementId: 装修需求ID
+  - currentUserId: 当前登录用户ID(商户或装修商铺)
+  
+返回:
+  - senderId: 发送方ID(格式:store_手机号 或 user_手机号)
+  - receiverId: 接收方ID(格式:store_手机号 或 user_手机号)
+  - chatInitiated: 是否首次发起聊天(用于统计咨询数)
+```
+
+**实现逻辑:**
+1. 根据 requirementId 查询装修需求信息
+2. 获取装修需求的联系信息(contactPhone)
+3. 根据 store_id 查询装修商铺的 StoreUser,获取手机号
+4. 根据 contactPhone 查询对应的用户(LifeUser 或 StoreUser)
+5. 构造发送方和接收方的ID(user_ 或 store_ + 手机号)
+6. 如果是首次发起聊天,更新 inquiry_count + 1
+7. 返回对方ID,前端可直接跳转到聊天页面
+
+### 2. ID格式转换逻辑
+
+**问题:**
+- `StoreRenovationRequirement.contactPhone` 存储的是普通手机号
+- `StoreRenovationRequirement.store_id` 关联装修商铺
+- 需要转换为聊天系统需要的格式(`user_`/`store_` + 手机号)
+
+**解决方案:**
+```java
+// 1. 获取装修商铺的ID
+StoreInfo storeInfo = storeInfoMapper.selectById(storeId);
+StoreUser storeUser = storeUserMapper.selectOne(
+    new QueryWrapper<StoreUser>().eq("store_id", storeId)
+);
+String renovationStoreId = "store_" + storeUser.getPhone();
+
+// 2. 获取发布需求的商户ID
+// 方式1:通过 contactPhone 查找
+LifeUser lifeUser = lifeUserMapper.selectOne(
+    new QueryWrapper<LifeUser>().eq("user_phone", contactPhone)
+);
+String merchantId = "user_" + contactPhone;
+
+// 方式2:如果有 created_user_id,可通过 StoreUser 查找
+StoreUser merchantStoreUser = storeUserMapper.selectById(createdUserId);
+String merchantId = "store_" + merchantStoreUser.getPhone();
+```
+
+### 3. 消息类型扩展
+
+**当前消息类型:**
+- 1-文本
+- 2-图片
+- 3-链接
+- 4-12: 二手交易相关
+- 8: 律师消息
+
+**建议新增:**
+- 13: 装修需求咨询消息(可在消息列表显示特殊标识)
+- 或直接使用 type=1(文本),通过 businessId 区分业务场景
+
+### 4. 咨询数统计
+
+**实现位置:**
+在发起聊天时(首次聊天),更新 `inquiry_count` 字段:
+```java
+// 检查是否已有聊天记录
+LambdaQueryWrapper<LifeMessage> wrapper = new LambdaQueryWrapper<>();
+wrapper.eq(LifeMessage::getBusinessId, requirementId);
+wrapper.eq(LifeMessage::getSenderId, senderId);
+wrapper.eq(LifeMessage::getReceiverId, receiverId);
+long chatCount = lifeMessageMapper.selectCount(wrapper);
+
+if (chatCount == 0) {
+    // 首次聊天,增加咨询数
+    StoreRenovationRequirement requirement = requirementMapper.selectById(requirementId);
+    requirement.setInquiryCount(requirement.getInquiryCount() + 1);
+    requirementMapper.updateById(requirement);
+}
+```
+
+### 5. 消息列表展示装修需求信息
+
+**需求:**
+在消息列表中,如果是装修需求相关的聊天,显示装修需求标题等信息
+
+**实现方式:**
+- 查询消息列表时,通过 `businessId` 关联查询装修需求信息
+- 在 LifeMessageVo 中添加装修需求相关字段(可选)
+
+## 三、建议实现的接口
+
+### 1. 发起装修需求聊天接口
+```
+POST /renovation/startChat
+```
+
+### 2. 获取装修需求聊天信息接口
+```
+GET /renovation/getChatInfo/{requirementId}
+返回:发送方ID、接收方ID、是否已发起过聊天等
+```
+
+### 3. WebSocket消息扩展
+在发送消息时,确保 `businessId` 字段设置为装修需求ID:
+```java
+lifeMessage.setBusinessId(requirementId);
+```
+
+## 四、数据库字段说明
+
+### StoreRenovationRequirement 表
+- `id`: 装修需求ID(用作 businessId)
+- `store_id`: 装修商铺ID(可通过此ID查询装修商铺的StoreUser获取手机号)
+- `contact_phone`: 发布需求的商户手机号
+- `created_user_id`: 创建用户ID(可辅助查找商户)
+
+### LifeMessage 表
+- `sender_id`: 发送方ID(user_手机号 或 store_手机号)
+- `receiver_id`: 接收方ID(user_手机号 或 store_手机号)
+- `business_id`: 业务ID(设置为装修需求ID)
+- `type`: 消息类型(建议使用 1-文本,或新增 13-装修需求咨询)
+
+## 五、总结
+
+**现状:** 基础的聊天功能已完整支持,可直接使用 LifeMessage 表进行聊天。
+
+**待补充:** 
+1. 从装修需求页面发起聊天的便捷接口
+2. ID格式转换逻辑(手机号 -> user_/store_ + 手机号)
+3. 咨询数统计功能
+4. 可选:装修需求消息类型标识
+
+**优先级:**
+- 🔴 高:发起聊天接口、ID格式转换
+- 🟡 中:咨询数统计
+- 🟢 低:消息类型扩展、消息列表展示装修需求信息
+
+

+ 38 - 0
alien-entity/src/main/resources/db/migration/store_renovation_requirement.sql

@@ -0,0 +1,38 @@
+-- 装修需求动态表
+CREATE TABLE `store_renovation_requirement` (
+    `id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
+    `store_id` INT NOT NULL COMMENT '门店ID(发布装修需求的普通公司门店)',
+    `requirement_title` VARCHAR(200) NOT NULL COMMENT '需求标题',
+    `renovation_type` TINYINT NOT NULL COMMENT '装修类型(1:新房装修, 2:旧房改造, 3:局部装修)',
+    `house_area` DECIMAL(10, 2) DEFAULT NULL COMMENT '房屋面积(平方米)',
+    `renovation_budget` DECIMAL(10, 2) DEFAULT NULL COMMENT '装修预算(万元)',
+    `detailed_requirement` TEXT COMMENT '详细需求描述',
+    `attachment_urls` VARCHAR(2000) DEFAULT NULL COMMENT '房屋图纸附件(图片和视频URL,多个URL用逗号拼接,最多9个)',
+    `expected_renovation_time` DATE DEFAULT NULL COMMENT '期望装修时间',
+    `contact_name` VARCHAR(50) NOT NULL COMMENT '联系人姓名',
+    `contact_phone` VARCHAR(20) NOT NULL COMMENT '联系电话',
+    `city` VARCHAR(50) NOT NULL COMMENT '所在城市',
+    `city_adcode` VARCHAR(20) DEFAULT NULL COMMENT '城市编码(adcode)',
+    `detailed_address` VARCHAR(200) NOT NULL COMMENT '详细地址',
+    `agreement_confirmed` TINYINT(1) DEFAULT '0' COMMENT '服务协议确认(0:未确认, 1:已确认)',
+    `status` TINYINT DEFAULT '0' COMMENT '动态状态(0:草稿, 1:已发布, 2:已下架)',
+    `view_count` INT DEFAULT '0' COMMENT '浏览数',
+    `inquiry_count` INT DEFAULT '0' COMMENT '咨询数(装修商家查看并联系的数量)',
+    `audit_status` TINYINT DEFAULT '0' COMMENT '审核状态(0:待审核, 1:审核通过, 2:审核失败)',
+    `audit_reason` VARCHAR(500) DEFAULT NULL COMMENT '审核失败原因',
+    `audit_time` DATETIME DEFAULT NULL COMMENT '审核时间',
+    `audit_user_id` INT DEFAULT NULL COMMENT '审核人ID',
+    `delete_flag` TINYINT(1) DEFAULT '0' COMMENT '删除标记, 0:未删除, 1:已删除',
+    `created_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    `created_user_id` INT DEFAULT NULL COMMENT '创建人ID',
+    `updated_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
+    `updated_user_id` INT DEFAULT NULL COMMENT '修改人ID',
+    PRIMARY KEY (`id`),
+    KEY `idx_store_id` (`store_id`),
+    KEY `idx_status` (`status`),
+    KEY `idx_audit_status` (`audit_status`),
+    KEY `idx_city` (`city`),
+    KEY `idx_renovation_type` (`renovation_type`),
+    KEY `idx_updated_time` (`updated_time`) USING BTREE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='装修需求动态表';
+

+ 1 - 1
alien-gateway/src/main/resources/bootstrap.yml

@@ -1,3 +1,3 @@
 spring:
   profiles:
-    active: test
+    active: dev

+ 1 - 1
alien-lawyer/src/main/resources/bootstrap.yml

@@ -1,3 +1,3 @@
 spring:
   profiles:
-    active: test
+    active: dev

+ 1 - 1
alien-second/src/main/resources/bootstrap.yml

@@ -1,3 +1,3 @@
 spring:
   profiles:
-    active: test
+    active: dev

+ 1 - 1
alien-store-platform/src/main/resources/bootstrap.yml

@@ -1,3 +1,3 @@
 spring:
   profiles:
-    active: test
+    active: dev

+ 2 - 0
alien-store/src/main/java/shop/alien/store/config/WebSocketProcess.java

@@ -245,4 +245,6 @@ public class WebSocketProcess implements ApplicationContextAware {
         }
     }
 
+
+
 }

+ 252 - 0
alien-store/src/main/java/shop/alien/store/controller/StoreRenovationRequirementController.java

@@ -0,0 +1,252 @@
+package shop.alien.store.controller;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import io.swagger.annotations.*;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.dto.StoreRenovationBrowseRequirementDto;
+import shop.alien.entity.store.dto.StoreRenovationRequirementDto;
+import shop.alien.store.service.StoreRenovationBrowseRecordService;
+import shop.alien.store.service.StoreRenovationRequirementService;
+
+import java.util.List;
+
+/**
+ * 装修需求动态表 Controller
+ *
+ * @author auto-generated
+ * @since 2025-01-15
+ */
+@Slf4j
+@Api(tags = {"装修需求动态"})
+@ApiSort(1)
+@CrossOrigin
+@RestController
+@RequestMapping("/renovation/requirement")
+@RequiredArgsConstructor
+public class StoreRenovationRequirementController {
+
+    private final StoreRenovationRequirementService requirementService;
+    private final StoreRenovationBrowseRecordService browseRecordService;
+
+    @ApiOperation("发布或更新装修需求")
+    @ApiOperationSupport(order = 1)
+    @PostMapping("/saveOrUpdate")
+    public R<Boolean> saveOrUpdate(@RequestBody StoreRenovationRequirementDto dto) {
+        log.info("StoreRenovationRequirementController.saveOrUpdate?dto={}", dto);
+        try {
+            boolean result = requirementService.saveOrUpdateRequirement(dto);
+            if (result) {
+                return R.success("操作成功");
+            }
+            return R.fail("操作失败");
+        } catch (Exception e) {
+            log.error("保存装修需求失败: {}", e.getMessage(), e);
+            return R.fail(e.getMessage());
+        }
+    }
+
+    @ApiOperation("获取装修需求详情")
+    @ApiOperationSupport(order = 2)
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "id", value = "需求ID", dataType = "Integer", paramType = "query", required = true)
+    })
+    @GetMapping("/getDetail")
+    public R<StoreRenovationRequirementDto> getDetail(@RequestParam Integer id) {
+        log.info("StoreRenovationRequirementController.getDetail?id={}", id);
+        try {
+            // 增加浏览数
+            requirementService.incrementViewCount(id);
+            
+            StoreRenovationRequirementDto dto = requirementService.getRequirementDetail(id);
+            if (dto == null) {
+                return R.fail("装修需求不存在");
+            }
+            return R.data(dto);
+        } catch (Exception e) {
+            log.error("获取装修需求详情失败: {}", e.getMessage(), e);
+            return R.fail(e.getMessage());
+        }
+    }
+
+    @ApiOperation("分页查询装修需求列表")
+    @ApiOperationSupport(order = 3)
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "page", value = "页码", dataType = "Integer", paramType = "query", defaultValue = "1"),
+            @ApiImplicitParam(name = "size", value = "每页数量", dataType = "Integer", paramType = "query", defaultValue = "10"),
+            @ApiImplicitParam(name = "storeId", value = "门店ID", dataType = "Integer", paramType = "query"),
+            @ApiImplicitParam(name = "city", value = "城市", dataType = "String", paramType = "query"),
+            @ApiImplicitParam(name = "renovationType", value = "装修类型(1:新房装修, 2:旧房改造, 3:局部装修)", dataType = "Integer", paramType = "query"),
+            @ApiImplicitParam(name = "status", value = "状态(0:草稿, 1:已发布, 2:已下架)", dataType = "Integer", paramType = "query"),
+            @ApiImplicitParam(name = "auditStatus", value = "审核状态(0:待审核, 1:审核通过, 2:审核失败)", dataType = "Integer", paramType = "query"),
+            @ApiImplicitParam(name = "sortType", value = "排序类型(1:最新发布, 2:价格最高, 3:面积最大)", dataType = "Integer", paramType = "query")
+    })
+    @GetMapping("/getPage")
+    public R<IPage<StoreRenovationRequirementDto>> getPage(
+            @RequestParam(defaultValue = "1") Integer page,
+            @RequestParam(defaultValue = "10") Integer size,
+            @RequestParam(required = false) Integer storeId,
+            @RequestParam(required = false) String city,
+            @RequestParam(required = false) Integer renovationType,
+            @RequestParam(required = false) Integer status,
+            @RequestParam(required = false) Integer auditStatus,
+            @RequestParam(required = false, defaultValue = "1") Integer sortType) {
+        log.info("StoreRenovationRequirementController.getPage?page={}, size={}, storeId={}, city={}, renovationType={}, status={}, auditStatus={}, sortType={}",
+                page, size, storeId, city, renovationType, status, auditStatus, sortType);
+        try {
+            Page<StoreRenovationRequirementDto> pageParam = new Page<>(page, size);
+            IPage<StoreRenovationRequirementDto> result = requirementService.getRequirementPage(
+                    pageParam, storeId, city, renovationType, status, auditStatus, sortType);
+            return R.data(result);
+        } catch (Exception e) {
+            log.error("分页查询装修需求失败: {}", e.getMessage(), e);
+            return R.fail(e.getMessage());
+        }
+    }
+
+    @ApiOperation("更新状态(草稿/发布/下架)")
+    @ApiOperationSupport(order = 4)
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "id", value = "需求ID", dataType = "Integer", paramType = "query", required = true),
+            @ApiImplicitParam(name = "status", value = "状态(0:草稿, 1:已发布, 2:已下架)", dataType = "Integer", paramType = "query", required = true)
+    })
+    @PostMapping("/updateStatus")
+    public R<Boolean> updateStatus(@RequestParam Integer id, @RequestParam Integer status) {
+        log.info("StoreRenovationRequirementController.updateStatus?id={}, status={}", id, status);
+        try {
+            boolean result = requirementService.updateStatus(id, status);
+            if (result) {
+                return R.success("更新成功");
+            }
+            return R.fail("更新失败");
+        } catch (Exception e) {
+            log.error("更新状态失败: {}", e.getMessage(), e);
+            return R.fail(e.getMessage());
+        }
+    }
+
+    @ApiOperation("审核装修需求")
+    @ApiOperationSupport(order = 5)
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "id", value = "需求ID", dataType = "Integer", paramType = "query", required = true),
+            @ApiImplicitParam(name = "auditStatus", value = "审核状态(1:审核通过, 2:审核失败)", dataType = "Integer", paramType = "query", required = true),
+            @ApiImplicitParam(name = "auditReason", value = "审核原因(审核失败时必填)", dataType = "String", paramType = "query"),
+            @ApiImplicitParam(name = "auditUserId", value = "审核人ID", dataType = "Integer", paramType = "query", required = true)
+    })
+    @PostMapping("/audit")
+    public R<Boolean> audit(@RequestParam Integer id,
+                            @RequestParam Integer auditStatus,
+                            @RequestParam(required = false) String auditReason,
+                            @RequestParam Integer auditUserId) {
+        log.info("StoreRenovationRequirementController.audit?id={}, auditStatus={}, auditReason={}, auditUserId={}",
+                id, auditStatus, auditReason, auditUserId);
+        try {
+            boolean result = requirementService.auditRequirement(id, auditStatus, auditReason, auditUserId);
+            if (result) {
+                return R.success("审核成功");
+            }
+            return R.fail("审核失败");
+        } catch (Exception e) {
+            log.error("审核装修需求失败: {}", e.getMessage(), e);
+            return R.fail(e.getMessage());
+        }
+    }
+
+    @ApiOperation("删除装修需求")
+    @ApiOperationSupport(order = 6)
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "id", value = "需求ID", dataType = "Integer", paramType = "query", required = true)
+    })
+    @PostMapping("/delete")
+    public R<Boolean> delete(@RequestParam Integer id) {
+        log.info("StoreRenovationRequirementController.delete?id={}", id);
+        try {
+            boolean result = requirementService.deleteRequirement(id);
+            if (result) {
+                return R.success("删除成功");
+            }
+            return R.fail("删除失败");
+        } catch (Exception e) {
+            log.error("删除装修需求失败: {}", e.getMessage(), e);
+            return R.fail(e.getMessage());
+        }
+    }
+
+    @ApiOperation("获取门店发布的装修需求列表")
+    @ApiOperationSupport(order = 7)
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "storeId", value = "门店ID", dataType = "Integer", paramType = "query", required = true)
+    })
+    @GetMapping("/getListByStoreId")
+    public R<List<StoreRenovationRequirementDto>> getListByStoreId(@RequestParam Integer storeId) {
+        log.info("StoreRenovationRequirementController.getListByStoreId?storeId={}", storeId);
+        try {
+            List<StoreRenovationRequirementDto> list = requirementService.getRequirementListByStoreId(storeId);
+            return R.data(list);
+        } catch (Exception e) {
+            log.error("获取门店装修需求列表失败: {}", e.getMessage(), e);
+            return R.fail(e.getMessage());
+        }
+    }
+
+    @ApiOperation("增加咨询数(装修商家查看需求时调用)")
+    @ApiOperationSupport(order = 8)
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "id", value = "需求ID", dataType = "Integer", paramType = "query", required = true)
+    })
+    @PostMapping("/incrementInquiryCount")
+    public R<Boolean> incrementInquiryCount(@RequestParam Integer id) {
+        log.info("StoreRenovationRequirementController.incrementInquiryCount?id={}", id);
+        try {
+            requirementService.incrementInquiryCount(id);
+            return R.success("操作成功");
+        } catch (Exception e) {
+            log.error("增加咨询数失败: {}", e.getMessage(), e);
+            return R.fail(e.getMessage());
+        }
+    }
+
+    @ApiOperation("装修商铺咨询装修需求(记录浏览和咨询历史)")
+    @ApiOperationSupport(order = 9)
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "requirementId", value = "装修需求ID", dataType = "Integer", paramType = "query", required = true)
+    })
+    @PostMapping("/consultRequirement")
+    public R<Boolean> consultRequirement(@RequestParam Integer requirementId) {
+        log.info("StoreRenovationRequirementController.consultRequirement?requirementId={}", requirementId);
+        try {
+            boolean result = browseRecordService.recordInquiryByCurrentUser(requirementId);
+            if (result) {
+                return R.success("咨询记录成功");
+            }
+            return R.fail("咨询记录失败");
+        } catch (Exception e) {
+            log.error("记录咨询失败: {}", e.getMessage(), e);
+            return R.fail(e.getMessage());
+        }
+    }
+
+    @ApiOperation("查询装修商铺浏览过的普通商铺发布的装修需求列表")
+    @ApiOperationSupport(order = 10)
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "renovationStoreTel", value = "装修商铺电话(store_info.store_tel)", dataType = "String", paramType = "query", required = true),
+            @ApiImplicitParam(name = "normalStoreTel", value = "普通商铺电话(store_info.store_tel,发布装修需求的店铺)", dataType = "String", paramType = "query", required = true)
+    })
+    @GetMapping("/getBrowsedRequirements")
+    public R<List<StoreRenovationBrowseRequirementDto>> getBrowsedRequirements(
+            @RequestParam String renovationStoreTel,
+            @RequestParam String normalStoreTel) {
+        log.info("StoreRenovationRequirementController.getBrowsedRequirements?renovationStoreTel={}, normalStoreTel={}", renovationStoreTel, normalStoreTel);
+        try {
+            List<StoreRenovationBrowseRequirementDto> list = browseRecordService.getBrowsedRequirementsByStoreTel(renovationStoreTel, normalStoreTel);
+            return R.data(list);
+        } catch (Exception e) {
+            log.error("查询浏览过的装修需求失败: {}", e.getMessage(), e);
+            return R.fail(e.getMessage());
+        }
+    }
+}
+

+ 52 - 0
alien-store/src/main/java/shop/alien/store/service/StoreRenovationBrowseRecordService.java

@@ -0,0 +1,52 @@
+package shop.alien.store.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import shop.alien.entity.store.StoreRenovationBrowseRecord;
+import shop.alien.entity.store.dto.StoreRenovationBrowseRequirementDto;
+
+import java.util.List;
+
+/**
+ * 装修商铺访问装修需求浏览记录表 服务类
+ *
+ * @author auto-generated
+ * @since 2025-01-15
+ */
+public interface StoreRenovationBrowseRecordService extends IService<StoreRenovationBrowseRecord> {
+
+    /**
+     * 记录装修商铺浏览装修需求(仅浏览)
+     *
+     * @param renovationStoreId 装修商铺ID
+     * @param requirementId     装修需求ID
+     * @return 是否成功
+     */
+    boolean recordBrowse(Integer renovationStoreId, Integer requirementId);
+
+    /**
+     * 记录装修商铺咨询装修需求(浏览并咨询)
+     *
+     * @param renovationStoreId 装修商铺ID
+     * @param requirementId     装修需求ID
+     * @return 是否成功
+     */
+    boolean recordInquiry(Integer renovationStoreId, Integer requirementId);
+
+    /**
+     * 记录当前登录装修商铺咨询装修需求(自动获取当前用户的门店ID)
+     *
+     * @param requirementId 装修需求ID
+     * @return 是否成功
+     */
+    boolean recordInquiryByCurrentUser(Integer requirementId);
+
+    /**
+     * 查询装修商铺浏览过的普通商铺发布的装修需求列表
+     *
+     * @param renovationStoreTel 装修商铺电话(store_info.store_tel)
+     * @param normalStoreTel     普通商铺电话(store_info.store_tel,发布装修需求的店铺)
+     * @return 浏览过的装修需求列表(包含浏览信息)
+     */
+    List<StoreRenovationBrowseRequirementDto> getBrowsedRequirementsByStoreTel(String renovationStoreTel, String normalStoreTel);
+}
+

+ 105 - 0
alien-store/src/main/java/shop/alien/store/service/StoreRenovationRequirementService.java

@@ -0,0 +1,105 @@
+package shop.alien.store.service;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.IService;
+import shop.alien.entity.store.StoreRenovationRequirement;
+import shop.alien.entity.store.dto.StoreRenovationRequirementDto;
+
+import java.util.List;
+
+/**
+ * 装修需求动态表 服务类
+ *
+ * @author auto-generated
+ * @since 2025-01-15
+ */
+public interface StoreRenovationRequirementService extends IService<StoreRenovationRequirement> {
+
+    /**
+     * 发布装修需求(保存或更新)
+     *
+     * @param dto 装修需求DTO
+     * @return 是否成功
+     */
+    boolean saveOrUpdateRequirement(StoreRenovationRequirementDto dto);
+
+    /**
+     * 根据ID获取装修需求详情
+     *
+     * @param id 需求ID
+     * @return 装修需求DTO
+     */
+    StoreRenovationRequirementDto getRequirementDetail(Integer id);
+
+    /**
+     * 分页查询装修需求列表
+     *
+     * @param page 分页对象
+     * @param storeId 门店ID(可选)
+     * @param city 城市(可选)
+     * @param renovationType 装修类型(可选,1:新房装修, 2:旧房改造, 3:局部装修)
+     * @param status 状态(可选,0:草稿, 1:已发布, 2:已下架)
+     * @param auditStatus 审核状态(可选,0:待审核, 1:审核通过, 2:审核失败)
+     * @param sortType 排序类型(1:最新发布, 2:价格最高, 3:面积最大),默认为1
+     * @return 分页结果
+     */
+    IPage<StoreRenovationRequirementDto> getRequirementPage(Page<StoreRenovationRequirementDto> page,
+                                                             Integer storeId,
+                                                             String city,
+                                                             Integer renovationType,
+                                                             Integer status,
+                                                             Integer auditStatus,
+                                                             Integer sortType);
+
+    /**
+     * 更新浏览数
+     *
+     * @param id 需求ID
+     */
+    void incrementViewCount(Integer id);
+
+    /**
+     * 更新咨询数
+     *
+     * @param id 需求ID
+     */
+    void incrementInquiryCount(Integer id);
+
+    /**
+     * 更新状态
+     *
+     * @param id 需求ID
+     * @param status 状态(0:草稿, 1:已发布, 2:已下架)
+     * @return 是否成功
+     */
+    boolean updateStatus(Integer id, Integer status);
+
+    /**
+     * 审核装修需求
+     *
+     * @param id 需求ID
+     * @param auditStatus 审核状态(1:审核通过, 2:审核失败)
+     * @param auditReason 审核原因(审核失败时必填)
+     * @param auditUserId 审核人ID
+     * @return 是否成功
+     */
+    boolean auditRequirement(Integer id, Integer auditStatus, String auditReason, Integer auditUserId);
+
+    /**
+     * 删除装修需求(逻辑删除)
+     *
+     * @param id 需求ID
+     * @return 是否成功
+     */
+    boolean deleteRequirement(Integer id);
+
+    /**
+     * 获取门店发布的装修需求列表
+     *
+     * @param storeId 门店ID
+     * @return 装修需求列表
+     */
+    List<StoreRenovationRequirementDto> getRequirementListByStoreId(Integer storeId);
+}
+

+ 11 - 0
alien-store/src/main/java/shop/alien/store/service/impl/StoreInfoServiceImpl.java

@@ -128,6 +128,8 @@ public class StoreInfoServiceImpl extends ServiceImpl<StoreInfoMapper, StoreInfo
 
     private final StoreCommentMapper storeCommentMapper;
 
+    private final CommonRatingMapper commonRatingMapper;
+
     private final StoreInfoDraftMapper storeInfoDraftMapper;
 
     private final LifeNoticeMapper lifeNoticeMapper;
@@ -335,6 +337,15 @@ public class StoreInfoServiceImpl extends ServiceImpl<StoreInfoMapper, StoreInfo
         } else {
             storeMainInfoVo.setDistance2(0);
         }
+
+        // 统计评价数量(common_rating 表,业务类型1=商铺评价)
+        LambdaQueryWrapper<CommonRating> ratingWrapper = new LambdaQueryWrapper<>();
+        ratingWrapper.eq(CommonRating::getBusinessType, 1); // 业务类型:1-商铺评价
+        ratingWrapper.eq(CommonRating::getBusinessId, id); // 门店ID
+        ratingWrapper.eq(CommonRating::getDeleteFlag, 0); // 未删除
+        Integer ratingCountLong = commonRatingMapper.selectCount(ratingWrapper);
+        storeMainInfoVo.setRatingCount(ratingCountLong != null ? ratingCountLong.intValue() : 0);
+
         return storeMainInfoVo;
     }
 

+ 313 - 0
alien-store/src/main/java/shop/alien/store/service/impl/StoreRenovationBrowseRecordServiceImpl.java

@@ -0,0 +1,313 @@
+package shop.alien.store.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
+import shop.alien.entity.store.StoreInfo;
+import shop.alien.entity.store.StoreRenovationBrowseRecord;
+import shop.alien.entity.store.StoreRenovationRequirement;
+import shop.alien.entity.store.StoreUser;
+import shop.alien.entity.store.dto.StoreRenovationBrowseRequirementDto;
+import shop.alien.mapper.StoreInfoMapper;
+import shop.alien.mapper.StoreRenovationBrowseRecordMapper;
+import shop.alien.mapper.StoreUserMapper;
+import shop.alien.store.service.StoreRenovationBrowseRecordService;
+import shop.alien.store.service.StoreRenovationRequirementService;
+import shop.alien.util.common.JwtUtil;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * 装修商铺访问装修需求浏览记录表 服务实现类
+ *
+ * @author auto-generated
+ * @since 2025-01-15
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+@Transactional(rollbackFor = Exception.class)
+public class StoreRenovationBrowseRecordServiceImpl extends ServiceImpl<StoreRenovationBrowseRecordMapper, StoreRenovationBrowseRecord> implements StoreRenovationBrowseRecordService {
+
+    private final StoreRenovationRequirementService requirementService;
+    private final StoreUserMapper storeUserMapper;
+    private final StoreInfoMapper storeInfoMapper;
+
+    @Override
+    public boolean recordBrowse(Integer renovationStoreId, Integer requirementId) {
+        log.info("StoreRenovationBrowseRecordServiceImpl.recordBrowse?renovationStoreId={}, requirementId={}", renovationStoreId, requirementId);
+        try {
+            StoreRenovationBrowseRecord record = new StoreRenovationBrowseRecord();
+            record.setRenovationStoreId(renovationStoreId);
+            record.setRequirementId(requirementId);
+            record.setBrowseTime(new Date());
+            record.setIsContacted(0); // 仅浏览,未咨询
+
+            // 获取当前登录用户ID
+            Integer currentUserId = null;
+            try {
+                com.alibaba.fastjson.JSONObject userInfo = JwtUtil.getCurrentUserInfo();
+                if (userInfo != null) {
+                    currentUserId = userInfo.getInteger("userId");
+                }
+            } catch (Exception e) {
+                log.warn("获取当前用户ID失败: {}", e.getMessage());
+            }
+            record.setCreatedUserId(currentUserId);
+
+            return this.save(record);
+        } catch (Exception e) {
+            log.error("记录浏览失败: {}", e.getMessage(), e);
+            throw new RuntimeException("记录浏览失败: " + e.getMessage());
+        }
+    }
+
+    @Override
+    public boolean recordInquiry(Integer renovationStoreId, Integer requirementId) {
+        log.info("StoreRenovationBrowseRecordServiceImpl.recordInquiry?renovationStoreId={}, requirementId={}", renovationStoreId, requirementId);
+        try {
+            Date now = new Date();
+
+            // 检查是否已有浏览记录
+            LambdaQueryWrapper<StoreRenovationBrowseRecord> queryWrapper = new LambdaQueryWrapper<>();
+            queryWrapper.eq(StoreRenovationBrowseRecord::getRenovationStoreId, renovationStoreId);
+            queryWrapper.eq(StoreRenovationBrowseRecord::getRequirementId, requirementId);
+            queryWrapper.eq(StoreRenovationBrowseRecord::getDeleteFlag, 0);
+            StoreRenovationBrowseRecord existingRecord = this.getOne(queryWrapper, false);
+
+            boolean isFirstContact = false;
+
+            if (existingRecord != null) {
+                // 如果已有记录但未咨询过,更新为已咨询
+                if (existingRecord.getIsContacted() == null || existingRecord.getIsContacted() == 0) {
+                    LambdaUpdateWrapper<StoreRenovationBrowseRecord> updateWrapper = new LambdaUpdateWrapper<>();
+                    updateWrapper.eq(StoreRenovationBrowseRecord::getId, existingRecord.getId());
+                    updateWrapper.set(StoreRenovationBrowseRecord::getIsContacted, 1);
+                    updateWrapper.set(StoreRenovationBrowseRecord::getContactTime, now);
+
+                    // 获取当前登录用户ID
+                    Integer currentUserId = null;
+                    try {
+                        com.alibaba.fastjson.JSONObject userInfo = JwtUtil.getCurrentUserInfo();
+                        if (userInfo != null) {
+                            currentUserId = userInfo.getInteger("userId");
+                        }
+                    } catch (Exception e) {
+                        log.warn("获取当前用户ID失败: {}", e.getMessage());
+                    }
+                    updateWrapper.set(StoreRenovationBrowseRecord::getUpdatedUserId, currentUserId);
+
+                    this.update(updateWrapper);
+                    isFirstContact = true;
+                }
+                // 如果已咨询过,不再重复记录,但可以记录新的浏览记录(可选)
+                // 这里选择不重复记录咨询,只更新浏览时间
+                existingRecord.setBrowseTime(now);
+                this.updateById(existingRecord);
+            } else {
+                // 如果没有记录,创建新记录(浏览+咨询)
+                StoreRenovationBrowseRecord record = new StoreRenovationBrowseRecord();
+                record.setRenovationStoreId(renovationStoreId);
+                record.setRequirementId(requirementId);
+                record.setBrowseTime(now);
+                record.setIsContacted(1); // 已咨询
+                record.setContactTime(now);
+
+                // 获取当前登录用户ID
+                Integer currentUserId = null;
+                try {
+                    com.alibaba.fastjson.JSONObject userInfo = JwtUtil.getCurrentUserInfo();
+                    if (userInfo != null) {
+                        currentUserId = userInfo.getInteger("userId");
+                    }
+                } catch (Exception e) {
+                    log.warn("获取当前用户ID失败: {}", e.getMessage());
+                }
+                record.setCreatedUserId(currentUserId);
+
+                this.save(record);
+                isFirstContact = true;
+            }
+
+            // 如果是首次咨询,增加需求的咨询数
+            if (isFirstContact) {
+                requirementService.incrementInquiryCount(requirementId);
+            }
+
+            return true;
+        } catch (Exception e) {
+            log.error("记录咨询失败: {}", e.getMessage(), e);
+            throw new RuntimeException("记录咨询失败: " + e.getMessage());
+        }
+    }
+
+    @Override
+    public boolean recordInquiryByCurrentUser(Integer requirementId) {
+        log.info("StoreRenovationBrowseRecordServiceImpl.recordInquiryByCurrentUser?requirementId={}", requirementId);
+        try {
+            // 获取当前登录用户信息
+            Integer currentUserId = null;
+            String phone = null;
+            try {
+                com.alibaba.fastjson.JSONObject userInfo = JwtUtil.getCurrentUserInfo();
+                if (userInfo != null) {
+                    currentUserId = userInfo.getInteger("userId");
+                    phone = userInfo.getString("phone");
+                }
+            } catch (Exception e) {
+                log.warn("获取当前用户信息失败: {}", e.getMessage());
+                throw new RuntimeException("获取当前用户信息失败,请先登录");
+            }
+
+            if (currentUserId == null) {
+                throw new RuntimeException("用户未登录");
+            }
+
+            // 根据用户ID或手机号查询 StoreUser,获取门店ID
+            StoreUser storeUser = null;
+            if (currentUserId != null) {
+                storeUser = storeUserMapper.selectById(currentUserId);
+            }
+            if (storeUser == null && phone != null) {
+                LambdaQueryWrapper<StoreUser> queryWrapper = new LambdaQueryWrapper<>();
+                queryWrapper.eq(StoreUser::getPhone, phone);
+                storeUser = storeUserMapper.selectOne(queryWrapper);
+            }
+
+            if (storeUser == null || storeUser.getStoreId() == null) {
+                throw new RuntimeException("未找到用户门店信息");
+            }
+
+            Integer renovationStoreId = storeUser.getStoreId();
+            return recordInquiry(renovationStoreId, requirementId);
+        } catch (Exception e) {
+            log.error("记录咨询失败: {}", e.getMessage(), e);
+            throw new RuntimeException(e.getMessage());
+        }
+    }
+
+    @Override
+    public List<StoreRenovationBrowseRequirementDto> getBrowsedRequirementsByStoreTel(String renovationStoreTel, String normalStoreTel) {
+        log.info("StoreRenovationBrowseRecordServiceImpl.getBrowsedRequirementsByStoreTel?renovationStoreTel={}, normalStoreTel={}", renovationStoreTel, normalStoreTel);
+        try {
+            // 0. 通过门店电话查询门店ID
+            LambdaQueryWrapper<StoreInfo> renovationStoreWrapper = new LambdaQueryWrapper<>();
+            renovationStoreWrapper.eq(StoreInfo::getStoreTel, renovationStoreTel);
+            renovationStoreWrapper.eq(StoreInfo::getDeleteFlag, 0);
+            StoreInfo renovationStoreInfo = storeInfoMapper.selectOne(renovationStoreWrapper);
+            if (renovationStoreInfo == null) {
+                throw new RuntimeException("装修商铺不存在,门店电话: " + renovationStoreTel);
+            }
+            Integer renovationStoreId = renovationStoreInfo.getId();
+
+            LambdaQueryWrapper<StoreInfo> normalStoreWrapper = new LambdaQueryWrapper<>();
+            normalStoreWrapper.eq(StoreInfo::getStoreTel, normalStoreTel);
+            normalStoreWrapper.eq(StoreInfo::getDeleteFlag, 0);
+            StoreInfo normalStoreInfo = storeInfoMapper.selectOne(normalStoreWrapper);
+            if (normalStoreInfo == null) {
+                throw new RuntimeException("普通商铺不存在,门店电话: " + normalStoreTel);
+            }
+            Integer normalStoreId = normalStoreInfo.getId();
+
+            // 1. 查询普通商铺发布的所有装修需求ID
+            LambdaQueryWrapper<StoreRenovationRequirement> requirementWrapper = new LambdaQueryWrapper<>();
+            requirementWrapper.eq(StoreRenovationRequirement::getStoreId, normalStoreId);
+            requirementWrapper.eq(StoreRenovationRequirement::getDeleteFlag, 0);
+            List<StoreRenovationRequirement> requirements = requirementService.list(requirementWrapper);
+            
+            if (requirements.isEmpty()) {
+                return new ArrayList<>();
+            }
+            
+            List<Integer> requirementIds = requirements.stream()
+                    .map(StoreRenovationRequirement::getId)
+                    .collect(Collectors.toList());
+
+            // 2. 查询装修商铺浏览过哪些需求
+            LambdaQueryWrapper<StoreRenovationBrowseRecord> browseWrapper = new LambdaQueryWrapper<>();
+            browseWrapper.eq(StoreRenovationBrowseRecord::getRenovationStoreId, renovationStoreId);
+            browseWrapper.in(StoreRenovationBrowseRecord::getRequirementId, requirementIds);
+            browseWrapper.eq(StoreRenovationBrowseRecord::getDeleteFlag, 0);
+            browseWrapper.orderByDesc(StoreRenovationBrowseRecord::getBrowseTime);
+            List<StoreRenovationBrowseRecord> browseRecords = this.list(browseWrapper);
+
+            if (browseRecords.isEmpty()) {
+                return new ArrayList<>();
+            }
+
+            // 3. 创建需求ID到浏览记录的映射(取最新的浏览记录)
+            Map<Integer, StoreRenovationBrowseRecord> browseRecordMap = browseRecords.stream()
+                    .collect(Collectors.toMap(
+                            StoreRenovationBrowseRecord::getRequirementId,
+                            record -> record,
+                            (existing, replacement) -> existing.getBrowseTime().after(replacement.getBrowseTime()) ? existing : replacement
+                    ));
+
+            // 4. 创建需求ID到需求详情的映射
+            Map<Integer, StoreRenovationRequirement> requirementMap = requirements.stream()
+                    .collect(Collectors.toMap(StoreRenovationRequirement::getId, req -> req));
+
+            // 5. 组装返回数据(只返回浏览过的需求)
+            List<StoreRenovationBrowseRequirementDto> result = new ArrayList<>();
+            for (StoreRenovationBrowseRecord browseRecord : browseRecordMap.values()) {
+                StoreRenovationRequirement requirement = requirementMap.get(browseRecord.getRequirementId());
+                if (requirement != null) {
+                    StoreRenovationBrowseRequirementDto dto = new StoreRenovationBrowseRequirementDto();
+                    
+                    // 复制需求信息
+                    dto.setRequirementId(requirement.getId());
+                    dto.setRequirementTitle(requirement.getRequirementTitle());
+                    dto.setRenovationType(requirement.getRenovationType());
+                    dto.setHouseArea(requirement.getHouseArea());
+                    dto.setRenovationBudget(requirement.getRenovationBudget());
+                    dto.setDetailedRequirement(requirement.getDetailedRequirement());
+                    dto.setExpectedRenovationTime(requirement.getExpectedRenovationTime());
+                    dto.setContactName(requirement.getContactName());
+                    dto.setContactPhone(requirement.getContactPhone());
+                    dto.setCity(requirement.getCity());
+                    dto.setDetailedAddress(requirement.getDetailedAddress());
+                    dto.setCreatedTime(requirement.getCreatedTime());
+
+                    // 处理附件URL字符串:逗号拼接转List
+                    if (StringUtils.hasText(requirement.getAttachmentUrls())) {
+                        List<String> attachmentUrls = Arrays.asList(requirement.getAttachmentUrls().split(","));
+                        dto.setAttachmentUrls(attachmentUrls);
+                    } else {
+                        dto.setAttachmentUrls(new ArrayList<>());
+                    }
+
+                    // 复制浏览信息
+                    dto.setBrowseTime(browseRecord.getBrowseTime());
+                    dto.setIsContacted(browseRecord.getIsContacted());
+                    dto.setContactTime(browseRecord.getContactTime());
+
+                    result.add(dto);
+                }
+            }
+
+            // 按浏览时间倒序排序
+            result.sort((a, b) -> {
+                if (a.getBrowseTime() == null && b.getBrowseTime() == null) return 0;
+                if (a.getBrowseTime() == null) return 1;
+                if (b.getBrowseTime() == null) return -1;
+                return b.getBrowseTime().compareTo(a.getBrowseTime());
+            });
+
+            return result;
+        } catch (Exception e) {
+            log.error("查询浏览过的装修需求失败: {}", e.getMessage(), e);
+            throw new RuntimeException("查询浏览过的装修需求失败: " + e.getMessage());
+        }
+    }
+}
+

+ 622 - 0
alien-store/src/main/java/shop/alien/store/service/impl/StoreRenovationRequirementServiceImpl.java

@@ -0,0 +1,622 @@
+package shop.alien.store.service.impl;
+
+import com.alibaba.fastjson2.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.baomidou.mybatisplus.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.springframework.beans.BeanUtils;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
+import shop.alien.entity.store.LifeNotice;
+import shop.alien.entity.store.StoreInfo;
+import shop.alien.entity.store.StoreRenovationRequirement;
+import shop.alien.entity.store.StoreUser;
+import shop.alien.entity.store.dto.StoreRenovationRequirementDto;
+import shop.alien.entity.store.vo.WebSocketVo;
+import shop.alien.mapper.LifeNoticeMapper;
+import shop.alien.mapper.StoreInfoMapper;
+import shop.alien.mapper.StoreRenovationRequirementMapper;
+import shop.alien.mapper.StoreUserMapper;
+import shop.alien.store.config.WebSocketProcess;
+import shop.alien.store.service.StoreRenovationRequirementService;
+import shop.alien.store.util.ai.AiContentModerationUtil;
+import shop.alien.store.util.ai.AiVideoModerationUtil;
+import shop.alien.util.common.JwtUtil;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import java.text.SimpleDateFormat;
+import java.util.*;
+import java.util.concurrent.*;
+import java.util.stream.Collectors;
+
+/**
+ * 装修需求动态表 服务实现类
+ *
+ * @author auto-generated
+ * @since 2025-01-15
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+@Transactional(rollbackFor = Exception.class)
+public class StoreRenovationRequirementServiceImpl extends ServiceImpl<StoreRenovationRequirementMapper, StoreRenovationRequirement> implements StoreRenovationRequirementService {
+
+    // 1. 自定义视频审核线程池(全局复用,避免频繁创建线程)
+    private ExecutorService videoAuditExecutor;
+
+    private final StoreInfoMapper storeInfoMapper;
+
+
+    private final LifeNoticeMapper lifeNoticeMapper;
+
+    private final WebSocketProcess webSocketProcess;
+
+    private final StoreUserMapper storeUserMapper;
+
+    private final AiContentModerationUtil aiContentModerationUtil;
+    private final AiVideoModerationUtil aiVideoModerationUtil;
+    // 定义图片后缀常量(不可修改,保证线程安全)
+    private static final Set<String> IMAGE_SUFFIXES = Collections.unmodifiableSet(
+            new HashSet<>(Arrays.asList(".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp"))
+    );
+
+    // 定义视频后缀常量
+    private static final Set<String> VIDEO_SUFFIXES = Collections.unmodifiableSet(
+            new HashSet<>(Arrays.asList(".mp4", ".avi", ".mov", ".flv", ".mkv", ".wmv", ".mpeg"))
+    );
+
+    // 初始化线程池
+    @PostConstruct
+    public void initExecutor() {
+        // 核心参数根据业务调整,IO密集型任务线程数可设为CPU核心数*2~4
+        int corePoolSize = Runtime.getRuntime().availableProcessors() * 2;
+        int maxPoolSize = Runtime.getRuntime().availableProcessors() * 4;
+        videoAuditExecutor = new ThreadPoolExecutor(
+                corePoolSize,
+                maxPoolSize,
+                60L,
+                TimeUnit.SECONDS,
+                new LinkedBlockingQueue<>(500), // 任务队列,避免无界队列溢出
+                new ThreadFactory() {
+                    private int count = 0;
+                    @Override
+                    public Thread newThread(Runnable r) {
+                        Thread t = new Thread(r);
+                        t.setName("video-audit-" + count++); // 自定义线程名,便于排查
+                        return t;
+                    }
+                },
+                // 任务拒绝策略:记录日志+调用者线程兜底(避免任务丢失)
+                new ThreadPoolExecutor.CallerRunsPolicy() {
+                    @Override
+                    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
+                        log.error("视频审核任务被拒绝,队列已满!当前队列大小:{},活跃线程数:{}",
+                                e.getQueue().size(), e.getActiveCount());
+                        super.rejectedExecution(r, e);
+                    }
+                }
+        );
+    }
+
+    // 优雅关闭线程池
+    @PreDestroy
+    public void destroyExecutor() {
+        if (Objects.nonNull(videoAuditExecutor)) {
+            videoAuditExecutor.shutdown();
+            try {
+                if (!videoAuditExecutor.awaitTermination(30, TimeUnit.SECONDS)) {
+                    videoAuditExecutor.shutdownNow();
+                }
+            } catch (InterruptedException e) {
+                videoAuditExecutor.shutdownNow();
+                Thread.currentThread().interrupt();
+            }
+        }
+    }
+
+    @Override
+    public boolean saveOrUpdateRequirement(StoreRenovationRequirementDto dto) {
+        log.info("StoreRenovationRequirementServiceImpl.saveOrUpdateRequirement?dto={}", dto);
+        try {
+            StoreRenovationRequirement requirement = new StoreRenovationRequirement();
+            BeanUtils.copyProperties(dto, requirement);
+
+            // 处理附件URL列表:List转逗号拼接字符串
+            if (dto.getAttachmentUrls() != null && !dto.getAttachmentUrls().isEmpty()) {
+                String attachmentUrlsStr = String.join(",", dto.getAttachmentUrls());
+                requirement.setAttachmentUrls(attachmentUrlsStr);
+            }
+
+            // 获取当前登录用户ID
+            Integer currentUserId = null;
+            try {
+                com.alibaba.fastjson.JSONObject userInfo = JwtUtil.getCurrentUserInfo();
+                if (userInfo != null) {
+                    currentUserId = userInfo.getInteger("userId");
+                }
+            } catch (Exception e) {
+                log.warn("获取当前用户ID失败: {}", e.getMessage());
+            }
+
+            if (requirement.getId() == null) {
+                // 新增
+                requirement.setCreatedUserId(currentUserId);
+                requirement.setCreatedTime(new Date());
+                if (requirement.getStatus() == null) {
+                    requirement.setStatus(0); // 默认草稿
+                }
+                if (requirement.getViewCount() == null) {
+                    requirement.setViewCount(0);
+                }
+                if (requirement.getInquiryCount() == null) {
+                    requirement.setInquiryCount(0);
+                }
+                if (requirement.getAuditStatus() == null) {
+                    requirement.setAuditStatus(0); // 默认待审核
+                }
+            } else {
+                // 更新
+                requirement.setUpdatedUserId(currentUserId);
+                requirement.setUpdatedTime(new Date());
+            }
+            StoreUser storeUser = storeUserMapper.selectOne(new LambdaQueryWrapper<StoreUser>().eq(StoreUser::getStoreId, dto.getStoreId()).eq(StoreUser::getDeleteFlag, 0));
+
+            LifeNotice lifeMessage = new LifeNotice();
+            lifeMessage.setReceiverId("store_" + storeUser.getPhone());
+            String text = "您发布的装修动态已提交审核,请于1-3个工作日进行查看";
+            JSONObject jsonObject = new JSONObject();
+            jsonObject.put("message", text);
+            lifeMessage.setContext(jsonObject.toJSONString());
+            lifeMessage.setTitle("审核通知");
+            lifeMessage.setSenderId("system");
+            lifeMessage.setIsRead(0);
+            lifeMessage.setNoticeType(1);
+            lifeNoticeMapper.insert(lifeMessage);
+
+            WebSocketVo websocketVo = new WebSocketVo();
+            websocketVo.setSenderId("system");
+            websocketVo.setReceiverId("store_" + storeUser.getPhone());
+            websocketVo.setCategory("notice");
+            websocketVo.setNoticeType("1");
+            websocketVo.setIsRead(0);
+            websocketVo.setText(JSONObject.from(lifeMessage).toJSONString());
+            webSocketProcess.sendMessage("store_" + storeUser.getPhone(), JSONObject.from(websocketVo).toJSONString());
+
+
+            // TODO 审核文本和图片内容是否违规
+            // 一次遍历完成分类,避免多次流式处理
+            Map<String, List<String>> urlCategoryMap = classifyUrls(dto.getAttachmentUrls());
+            List<String> videoUrls = urlCategoryMap.get("video");
+            // 1.调用文本+图片审核接口 ai为同步接口
+            AiContentModerationUtil.AuditResult auditResult = aiContentModerationUtil.auditContent(requirement.getDetailedRequirement(), urlCategoryMap.get("image"));
+            if (!auditResult.isPassed()) {
+                requirement.setAuditStatus(2);
+                requirement.setAuditReason(auditResult.getFailureReason());
+                // 发送审核失败通知(文本图片)
+                if (storeUser != null && storeUser.getPhone() != null) {
+                    sendAuditNotification(storeUser.getPhone(), 2, auditResult.getFailureReason(), "text_image");
+                }
+
+            } else {
+                // 如果文本/图片审核通过,且没有视频需要审核,直接设置为审核通过
+                if (videoUrls == null || videoUrls.isEmpty()) {
+                    requirement.setAuditStatus(1);
+                    requirement.setAuditReason(null);
+                    // 发送审核通过通知(文本图片,无视频)
+                    if (storeUser != null && storeUser.getPhone() != null) {
+                        sendAuditNotification(storeUser.getPhone(), 1, null, "text_image");
+                    }
+                } else {
+                    // 异步调用视频审核接口,图片审核通过后调用(核心优化)
+                    // 保存storeUser信息供异步任务使用
+                    final StoreUser finalStoreUser = storeUser;
+                    CompletableFuture.runAsync(() -> {
+                    AiVideoModerationUtil.VideoAuditResult videoAuditResult = null;
+                    try {
+                        // 调用审核接口,增加超时控制(避免接口挂死)
+                        videoAuditResult = CompletableFuture.supplyAsync(
+                                () -> aiVideoModerationUtil.auditVideos(urlCategoryMap.get("video")),
+                                videoAuditExecutor
+                        ).get();
+
+                        // 审核不通过则更新状态和原因
+                        if (Objects.nonNull(videoAuditResult) && !videoAuditResult.isPassed()) {
+                            // 重新查询最新的requirement,避免并发覆盖
+                            StoreRenovationRequirement latestRequirement = this.getById(requirement.getId());
+                            if (Objects.isNull(latestRequirement)) {
+                                log.error("视频审核后更新失败,requirement不存在,ID:{}", requirement.getId());
+                                return;
+                            }
+                            latestRequirement.setAuditStatus(2);
+                            latestRequirement.setAuditReason(videoAuditResult.getFailureReason());
+                            this.saveOrUpdate(latestRequirement);
+                            log.info("视频审核不通过,已更新状态,requirementID:{},原因:{}",
+                                    requirement.getId(), videoAuditResult.getFailureReason());
+                            // 发送审核不通过通知(视频)
+                            if (finalStoreUser != null && finalStoreUser.getPhone() != null) {
+                                sendAuditNotification(finalStoreUser.getPhone(), 2, videoAuditResult.getFailureReason(), "video");
+                            }
+
+                        } else if (Objects.nonNull(videoAuditResult) && videoAuditResult.isPassed()) {
+                            // 审核通过也更新状态(可选,根据业务需求)
+                            StoreRenovationRequirement latestRequirement = this.getById(requirement.getId());
+                            if (Objects.nonNull(latestRequirement)) {
+                                latestRequirement.setAuditStatus(1);
+                                latestRequirement.setAuditReason(null);
+                                this.saveOrUpdate(latestRequirement);
+                                // 发送审核通过通知(视频)
+                                if (finalStoreUser != null && finalStoreUser.getPhone() != null) {
+                                    sendAuditNotification(finalStoreUser.getPhone(), 1, null, "video");
+                                }
+
+                                log.info("视频审核通过,已更新状态,requirementID:{}", requirement.getId());
+                            }
+                        }
+                    } catch (Exception e) {
+                        log.error("视频审核接口调用异常,requirementID:{}", requirement.getId(), e);
+                        StoreRenovationRequirement latestRequirement = this.getById(requirement.getId());
+                        if (Objects.nonNull(latestRequirement)) {
+                            String exceptionMessage = "视频审核接口调用异常:" + e.getMessage();
+                            latestRequirement.setAuditStatus(2);
+                            latestRequirement.setAuditReason(exceptionMessage);
+                            this.saveOrUpdate(latestRequirement);
+                            // 发送审核异常通知(视频)
+                            if (finalStoreUser != null && finalStoreUser.getPhone() != null) {
+                                sendAuditNotification(finalStoreUser.getPhone(), 2, exceptionMessage, "video");
+                            }
+                        }
+                    }
+                    }, videoAuditExecutor);
+                }
+            }
+            requirement.setAuditTime(new Date());
+            boolean isSuccess  = this.saveOrUpdate(requirement);
+
+            return isSuccess;
+        } catch (Exception e) {
+            log.error("保存装修需求失败: {}", e.getMessage(), e);
+            throw new RuntimeException("保存装修需求失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 对URL列表进行分类(图片/视频/其他)
+     * @param attachmentUrls 原始URL列表
+     * @return 分类后的Map
+     */
+    private static Map<String, List<String>> classifyUrls(List<String> attachmentUrls) {
+        // 初始化分类容器
+        List<String> imageUrls = new ArrayList<>();
+        List<String> videoUrls = new ArrayList<>();
+        List<String> otherUrls = new ArrayList<>();
+
+        if (Objects.isNull(attachmentUrls) || attachmentUrls.isEmpty()) {
+            return buildResultMap(imageUrls, videoUrls, otherUrls);
+        }
+
+        for (String url : attachmentUrls) {
+            // 空URL直接归为其他
+            if (url == null || url.trim().isEmpty()) {
+                otherUrls.add(url);
+                continue;
+            }
+
+            String lowerUrl = url.toLowerCase().trim();
+            // 优先判断图片(避免URL中同时包含图片+视频后缀的极端情况)
+            boolean isImage = IMAGE_SUFFIXES.stream().anyMatch(lowerUrl::contains);
+            if (isImage) {
+                imageUrls.add(url); // 保留原始URL,而非小写后的
+                continue;
+            }
+
+            // 判断是否为视频
+            boolean isVideo = VIDEO_SUFFIXES.stream().anyMatch(lowerUrl::contains);
+            if (isVideo) {
+                videoUrls.add(url);
+            } else {
+                otherUrls.add(url);
+            }
+        }
+
+        return buildResultMap(imageUrls, videoUrls, otherUrls);
+    }
+
+    /**
+     * 构建分类结果Map
+     */
+    private static Map<String, List<String>> buildResultMap(List<String> imageUrls, List<String> videoUrls, List<String> otherUrls) {
+        Map<String, List<String>> resultMap = new HashMap<>(3);
+        resultMap.put("image", Collections.unmodifiableList(imageUrls)); // 返回不可修改列表,避免外部篡改
+        resultMap.put("video", Collections.unmodifiableList(videoUrls));
+        resultMap.put("other", Collections.unmodifiableList(otherUrls));
+        return resultMap;
+    }
+
+    /**
+     * 发送审核结果通知
+     * @param storePhone 商铺电话
+     * @param auditStatus 审核状态:1-通过,2-不通过
+     * @param auditReason 审核原因(不通过时提供)
+     * @param auditType 审核类型:text_image-文本图片审核,video-视频审核
+     */
+    private void sendAuditNotification(String storePhone, Integer auditStatus, String auditReason, String auditType) {
+        try {
+            if (storePhone == null || storePhone.trim().isEmpty()) {
+                log.warn("发送审核通知失败,商铺电话为空");
+                return;
+            }
+
+            SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日");
+            String commonDate = sdf.format(new Date());
+
+            String title;
+            String message;
+            if (auditStatus == 1) {
+                // 审核通过
+                title = "审核通知";
+                if ("video".equals(auditType)) {
+                    message = "在" + commonDate + ",您发布的装修动态视频审核已通过。";
+                } else {
+                    message = "在" + commonDate + ",您发布的装修动态审核已通过。";
+                }
+            } else {
+                // 审核不通过
+                title = "审核通知";
+                String reasonText = auditReason != null && !auditReason.trim().isEmpty() 
+                    ? ",原因:" + auditReason 
+                    : "";
+                if ("video".equals(auditType)) {
+                    message = "在" + commonDate + ",您发布的装修动态视频审核未通过" + reasonText + ",请修改后重新提交。";
+                } else {
+                    message = "在" + commonDate + ",您发布的装修动态审核未通过" + reasonText + ",请修改后重新提交。";
+                }
+            }
+
+            LifeNotice lifeNotice = new LifeNotice();
+            lifeNotice.setReceiverId("store_" + storePhone);
+            JSONObject jsonObject = new JSONObject();
+            jsonObject.put("message", message);
+            lifeNotice.setContext(jsonObject.toJSONString());
+            lifeNotice.setTitle(title);
+            lifeNotice.setSenderId("system");
+            lifeNotice.setIsRead(0);
+            lifeNotice.setNoticeType(1);
+            lifeNoticeMapper.insert(lifeNotice);
+
+            WebSocketVo websocketVo = new WebSocketVo();
+            websocketVo.setSenderId("system");
+            websocketVo.setReceiverId("store_" + storePhone);
+            websocketVo.setCategory("notice");
+            websocketVo.setNoticeType("1");
+            websocketVo.setIsRead(0);
+            websocketVo.setText(JSONObject.from(lifeNotice).toJSONString());
+            webSocketProcess.sendMessage("store_" + storePhone, JSONObject.from(websocketVo).toJSONString());
+
+            log.info("审核通知发送成功,商铺电话:{},审核状态:{},审核类型:{}", storePhone, auditStatus, auditType);
+        } catch (Exception e) {
+            log.error("发送审核通知异常,商铺电话:{},审核状态:{},审核类型:{}", storePhone, auditStatus, auditType, e);
+        }
+    }
+
+
+    @Override
+    public StoreRenovationRequirementDto getRequirementDetail(Integer id) {
+        log.info("StoreRenovationRequirementServiceImpl.getRequirementDetail?id={}", id);
+        StoreRenovationRequirement requirement = this.getById(id);
+        if (requirement == null) {
+            return null;
+        }
+
+        StoreRenovationRequirementDto dto = new StoreRenovationRequirementDto();
+        BeanUtils.copyProperties(requirement, dto);
+
+        // 处理附件URL字符串:逗号拼接转List
+        if (StringUtils.hasText(requirement.getAttachmentUrls())) {
+            List<String> attachmentUrls = Arrays.asList(requirement.getAttachmentUrls().split(","));
+            dto.setAttachmentUrls(attachmentUrls);
+        } else {
+            dto.setAttachmentUrls(new ArrayList<>());
+        }
+
+        return dto;
+    }
+
+    @Override
+    public IPage<StoreRenovationRequirementDto> getRequirementPage(Page<StoreRenovationRequirementDto> page,
+                                                                     Integer storeId,
+                                                                     String city,
+                                                                     Integer renovationType,
+                                                                     Integer status,
+                                                                     Integer auditStatus,
+                                                                     Integer sortType) {
+        log.info("StoreRenovationRequirementServiceImpl.getRequirementPage?storeId={}, city={}, renovationType={}, status={}, auditStatus={}, sortType={}",
+                storeId, city, renovationType, status, auditStatus, sortType);
+
+        LambdaQueryWrapper<StoreRenovationRequirement> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(StoreRenovationRequirement::getDeleteFlag, 0);
+
+        if (storeId != null) {
+            wrapper.eq(StoreRenovationRequirement::getStoreId, storeId);
+        }
+        if (StringUtils.hasText(city)) {
+            wrapper.eq(StoreRenovationRequirement::getCity, city);
+        }
+        if (renovationType != null) {
+            wrapper.eq(StoreRenovationRequirement::getRenovationType, renovationType);
+        }
+        if (status != null) {
+            wrapper.eq(StoreRenovationRequirement::getStatus, status);
+        }
+        if (auditStatus != null) {
+            wrapper.eq(StoreRenovationRequirement::getAuditStatus, auditStatus);
+        }
+
+        // 排序处理:1-最新发布, 2-价格最高, 3-面积最大
+        if (sortType == null || sortType == 1) {
+            // 默认:最新发布(按创建时间降序)
+            wrapper.orderByDesc(StoreRenovationRequirement::getCreatedTime);
+        } else if (sortType == 2) {
+            // 价格最高(按装修预算降序)
+            wrapper.orderByDesc(StoreRenovationRequirement::getRenovationBudget);
+            wrapper.orderByDesc(StoreRenovationRequirement::getCreatedTime); // 价格相同则按时间排序
+        } else if (sortType == 3) {
+            // 面积最大(按房屋面积降序)
+            wrapper.orderByDesc(StoreRenovationRequirement::getHouseArea);
+            wrapper.orderByDesc(StoreRenovationRequirement::getCreatedTime); // 面积相同则按时间排序
+        } else {
+            // 无效的排序类型,使用默认排序
+            wrapper.orderByDesc(StoreRenovationRequirement::getCreatedTime);
+        }
+
+        IPage<StoreRenovationRequirement> requirementPage = this.page(new Page<>(page.getCurrent(), page.getSize()), wrapper);
+
+        // 转换为DTO
+        IPage<StoreRenovationRequirementDto> dtoPage = requirementPage.convert(requirement -> {
+            StoreRenovationRequirementDto dto = new StoreRenovationRequirementDto();
+            BeanUtils.copyProperties(requirement, dto);
+
+            // 处理附件URL字符串:逗号拼接转List
+            if (StringUtils.hasText(requirement.getAttachmentUrls())) {
+                List<String> attachmentUrls = Arrays.asList(requirement.getAttachmentUrls().split(","));
+                dto.setAttachmentUrls(attachmentUrls);
+            } else {
+                dto.setAttachmentUrls(new ArrayList<>());
+            }
+
+            return dto;
+        });
+
+        // 批量查询商铺信息并填充到DTO
+        if (dtoPage.getRecords() != null && !dtoPage.getRecords().isEmpty()) {
+            // 收集所有的门店ID
+            List<Integer> storeIds = dtoPage.getRecords().stream()
+                    .map(StoreRenovationRequirementDto::getStoreId)
+                    .filter(id -> id != null)
+                    .distinct()
+                    .collect(Collectors.toList());
+
+            // 批量查询商铺信息
+            if (!storeIds.isEmpty()) {
+                List<StoreInfo> storeInfoList = storeInfoMapper.selectBatchIds(storeIds);
+                // 转换为Map,key为storeId,value为StoreInfo
+                Map<Integer, StoreInfo> storeInfoMap = storeInfoList.stream()
+                        .collect(Collectors.toMap(StoreInfo::getId, storeInfo -> storeInfo, (v1, v2) -> v1));
+
+                // 填充商铺信息到DTO
+                dtoPage.getRecords().forEach(dto -> {
+                    StoreInfo storeInfo = storeInfoMap.get(dto.getStoreId());
+                    if (storeInfo != null) {
+                        dto.setStoreName(storeInfo.getStoreName());
+                        dto.setStoreTel(storeInfo.getStoreTel());
+                        dto.setStoreAddress(storeInfo.getStoreAddress());
+                        dto.setStoreBlurb(storeInfo.getStoreBlurb());
+                    }
+                });
+            }
+        }
+
+        return dtoPage;
+    }
+
+    @Override
+    public void incrementViewCount(Integer id) {
+        log.info("StoreRenovationRequirementServiceImpl.incrementViewCount?id={}", id);
+        LambdaUpdateWrapper<StoreRenovationRequirement> wrapper = new LambdaUpdateWrapper<>();
+        wrapper.eq(StoreRenovationRequirement::getId, id);
+        wrapper.setSql("view_count = view_count + 1");
+        this.update(wrapper);
+    }
+
+    @Override
+    public void incrementInquiryCount(Integer id) {
+        log.info("StoreRenovationRequirementServiceImpl.incrementInquiryCount?id={}", id);
+        LambdaUpdateWrapper<StoreRenovationRequirement> wrapper = new LambdaUpdateWrapper<>();
+        wrapper.eq(StoreRenovationRequirement::getId, id);
+        wrapper.setSql("inquiry_count = inquiry_count + 1");
+        this.update(wrapper);
+    }
+
+    @Override
+    public boolean updateStatus(Integer id, Integer status) {
+        log.info("StoreRenovationRequirementServiceImpl.updateStatus?id={}, status={}", id, status);
+        LambdaUpdateWrapper<StoreRenovationRequirement> wrapper = new LambdaUpdateWrapper<>();
+        wrapper.eq(StoreRenovationRequirement::getId, id);
+        wrapper.set(StoreRenovationRequirement::getStatus, status);
+        wrapper.set(StoreRenovationRequirement::getUpdatedTime, new Date());
+
+        try {
+            com.alibaba.fastjson.JSONObject userInfo = JwtUtil.getCurrentUserInfo();
+            if (userInfo != null) {
+                wrapper.set(StoreRenovationRequirement::getUpdatedUserId, userInfo.getInteger("userId"));
+            }
+        } catch (Exception e) {
+            log.warn("获取当前用户ID失败: {}", e.getMessage());
+        }
+
+        return this.update(wrapper);
+    }
+
+    @Override
+    public boolean auditRequirement(Integer id, Integer auditStatus, String auditReason, Integer auditUserId) {
+        log.info("StoreRenovationRequirementServiceImpl.auditRequirement?id={}, auditStatus={}, auditReason={}, auditUserId={}",
+                id, auditStatus, auditReason, auditUserId);
+
+        if (auditStatus == 2 && !StringUtils.hasText(auditReason)) {
+            throw new RuntimeException("审核失败时必须填写审核原因");
+        }
+
+        LambdaUpdateWrapper<StoreRenovationRequirement> wrapper = new LambdaUpdateWrapper<>();
+        wrapper.eq(StoreRenovationRequirement::getId, id);
+        wrapper.set(StoreRenovationRequirement::getAuditStatus, auditStatus);
+        wrapper.set(StoreRenovationRequirement::getAuditTime, new Date());
+        wrapper.set(StoreRenovationRequirement::getAuditUserId, auditUserId);
+        wrapper.set(StoreRenovationRequirement::getUpdatedTime, new Date());
+
+        if (auditStatus == 2) {
+            wrapper.set(StoreRenovationRequirement::getAuditReason, auditReason);
+        } else {
+            wrapper.set(StoreRenovationRequirement::getAuditReason, null);
+        }
+
+        return this.update(wrapper);
+    }
+
+    @Override
+    public boolean deleteRequirement(Integer id) {
+        log.info("StoreRenovationRequirementServiceImpl.deleteRequirement?id={}", id);
+        return this.removeById(id);
+    }
+
+    @Override
+    public List<StoreRenovationRequirementDto> getRequirementListByStoreId(Integer storeId) {
+        log.info("StoreRenovationRequirementServiceImpl.getRequirementListByStoreId?storeId={}", storeId);
+        LambdaQueryWrapper<StoreRenovationRequirement> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(StoreRenovationRequirement::getStoreId, storeId);
+        wrapper.eq(StoreRenovationRequirement::getDeleteFlag, 0);
+        wrapper.orderByDesc(StoreRenovationRequirement::getCreatedTime);
+
+        List<StoreRenovationRequirement> requirements = this.list(wrapper);
+
+        return requirements.stream().map(requirement -> {
+            StoreRenovationRequirementDto dto = new StoreRenovationRequirementDto();
+            BeanUtils.copyProperties(requirement, dto);
+
+            // 处理附件URL字符串:逗号拼接转List
+            if (StringUtils.hasText(requirement.getAttachmentUrls())) {
+                List<String> attachmentUrls = Arrays.asList(requirement.getAttachmentUrls().split(","));
+                dto.setAttachmentUrls(attachmentUrls);
+            } else {
+                dto.setAttachmentUrls(new ArrayList<>());
+            }
+
+            return dto;
+        }).collect(Collectors.toList());
+    }
+}
+

+ 1 - 1
alien-store/src/main/resources/bootstrap.yml

@@ -1,3 +1,3 @@
 spring:
   profiles:
-    active: test
+    active: dev