Selaa lähdekoodia

商家申诉相关代码修改

zhangchen 1 viikko sitten
vanhempi
commit
1648f9f662
18 muutettua tiedostoa jossa 1527 lisäystä ja 35 poistoa
  1. 85 0
      alien-entity/src/main/java/shop/alien/entity/store/StoreCommentAppealSupplement.java
  2. 29 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/StoreCommentAppealSupplementAddDto.java
  3. 29 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/StoreCommentAppealSupplementUpdateDto.java
  4. 30 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/StoreCommentAppealSupplementVo.java
  5. 6 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/StoreCommentAppealVo.java
  6. 26 0
      alien-entity/src/main/java/shop/alien/mapper/StoreCommentAppealMapper.java
  7. 47 0
      alien-entity/src/main/java/shop/alien/mapper/StoreCommentAppealSupplementMapper.java
  8. 12 0
      alien-job/src/main/java/shop/alien/job/feign/AlienStoreFeign.java
  9. 45 0
      alien-job/src/main/java/shop/alien/job/store/StoreCommentAppealSupplementJob.java
  10. 12 3
      alien-store/src/main/java/shop/alien/store/controller/StoreCommentAppealController.java
  11. 113 0
      alien-store/src/main/java/shop/alien/store/controller/StoreCommentAppealSupplementController.java
  12. 42 0
      alien-store/src/main/java/shop/alien/store/controller/StoreCommentAppealSupplementJobController.java
  13. 38 0
      alien-store/src/main/java/shop/alien/store/controller/StoreCommentAppealSupplementJobTestController.java
  14. 8 0
      alien-store/src/main/java/shop/alien/store/service/StoreCommentAppealService.java
  15. 499 0
      alien-store/src/main/java/shop/alien/store/service/StoreCommentAppealSupplementJobService.java
  16. 54 0
      alien-store/src/main/java/shop/alien/store/service/StoreCommentAppealSupplementService.java
  17. 172 32
      alien-store/src/main/java/shop/alien/store/service/impl/StoreCommentAppealServiceImpl.java
  18. 280 0
      alien-store/src/main/java/shop/alien/store/service/impl/StoreCommentAppealSupplementServiceImpl.java

+ 85 - 0
alien-entity/src/main/java/shop/alien/entity/store/StoreCommentAppealSupplement.java

@@ -0,0 +1,85 @@
+package shop.alien.entity.store;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.baomidou.mybatisplus.extension.activerecord.Model;
+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 lombok.EqualsAndHashCode;
+
+import java.util.Date;
+
+/**
+ * 评论申诉-用户补充内容表
+ *
+ * @author zhangchen
+ * @since 2026-05-29
+ */
+@Data
+@JsonInclude
+@EqualsAndHashCode(callSuper = false)
+@TableName("store_comment_appeal_supplement")
+@ApiModel(value = "StoreCommentAppealSupplement对象", description = "评论申诉-用户补充内容表")
+public class StoreCommentAppealSupplement extends Model<StoreCommentAppealSupplement> {
+
+    @ApiModelProperty(value = "主键")
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    @ApiModelProperty(value = "申诉ID")
+    @TableField("appeal_id")
+    private Integer appealId;
+
+    @ApiModelProperty(value = "评价ID")
+    @TableField("comment_id")
+    private Integer commentId;
+
+    @ApiModelProperty(value = "补充用户ID")
+    @TableField("user_id")
+    private Long userId;
+
+    @ApiModelProperty(value = "门店ID")
+    @TableField("store_id")
+    private Integer storeId;
+
+    @ApiModelProperty(value = "用户补充说明")
+    @TableField("supplement_content")
+    private String supplementContent;
+
+    @ApiModelProperty(value = "补充图片OSS地址,多个英文逗号分隔")
+    @TableField("image_urls")
+    private String imageUrls;
+
+    @ApiModelProperty(value = "审核状态:0-待审核 1-通过 2-驳回")
+    @TableField("audit_status")
+    private Integer auditStatus;
+
+    @ApiModelProperty(value = "审核驳回原因")
+    @TableField("audit_reason")
+    private String auditReason;
+
+    @ApiModelProperty(value = "删除标记, 0:未删除, 1:已删除")
+    @TableField("delete_flag")
+    @TableLogic
+    private Integer deleteFlag;
+
+    @ApiModelProperty(value = "创建时间")
+    @TableField(value = "created_time", fill = FieldFill.INSERT)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date createdTime;
+
+    @ApiModelProperty(value = "创建人ID")
+    @TableField(value = "created_user_id", fill = FieldFill.INSERT)
+    private Integer createdUserId;
+
+    @ApiModelProperty(value = "修改时间")
+    @TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date updatedTime;
+
+    @ApiModelProperty(value = "修改人ID")
+    @TableField(value = "updated_user_id", fill = FieldFill.INSERT_UPDATE)
+    private Integer updatedUserId;
+}

+ 29 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/StoreCommentAppealSupplementAddDto.java

@@ -0,0 +1,29 @@
+package shop.alien.entity.store.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 评论申诉-用户补充新增DTO
+ */
+@Data
+@ApiModel(value = "StoreCommentAppealSupplementAddDto", description = "评论申诉-用户补充新增")
+public class StoreCommentAppealSupplementAddDto implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "申诉ID", required = true)
+    private Integer appealId;
+
+    @ApiModelProperty(value = "补充用户ID(须为原评价作者)", required = true)
+    private Long userId;
+
+    @ApiModelProperty(value = "用户补充说明", required = true)
+    private String supplementContent;
+
+    @ApiModelProperty(value = "补充图片OSS地址,多个英文逗号分隔")
+    private String imageUrls;
+}

+ 29 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/StoreCommentAppealSupplementUpdateDto.java

@@ -0,0 +1,29 @@
+package shop.alien.entity.store.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 评论申诉-用户补充修改DTO
+ */
+@Data
+@ApiModel(value = "StoreCommentAppealSupplementUpdateDto", description = "评论申诉-用户补充修改")
+public class StoreCommentAppealSupplementUpdateDto implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "主键", required = true)
+    private Integer id;
+
+    @ApiModelProperty(value = "操作用户ID(须为补充记录作者)", required = true)
+    private Long userId;
+
+    @ApiModelProperty(value = "用户补充说明")
+    private String supplementContent;
+
+    @ApiModelProperty(value = "补充图片OSS地址,多个英文逗号分隔")
+    private String imageUrls;
+}

+ 30 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/StoreCommentAppealSupplementVo.java

@@ -0,0 +1,30 @@
+package shop.alien.entity.store.vo;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import shop.alien.entity.store.StoreCommentAppealSupplement;
+
+import java.util.List;
+
+/**
+ * 评论申诉-用户补充返回VO
+ */
+@Data
+@JsonInclude
+@EqualsAndHashCode(callSuper = true)
+public class StoreCommentAppealSupplementVo extends StoreCommentAppealSupplement {
+
+    @ApiModelProperty(value = "补充图片URL列表")
+    private List<String> imgList;
+
+    @ApiModelProperty(value = "用户昵称")
+    private String userName;
+
+    @ApiModelProperty(value = "用户头像")
+    private String userImage;
+
+    @ApiModelProperty(value = "操作结果:0-成功 1-失败 2-申诉已结案 3-文本内容异常 4-无权限")
+    private Integer result;
+}

+ 6 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/StoreCommentAppealVo.java

@@ -55,6 +55,9 @@ public class StoreCommentAppealVo extends StoreCommentAppeal {
     @ApiModelProperty(value = "门店名称")
     @ApiModelProperty(value = "门店名称")
     private String storeName;
     private String storeName;
 
 
+    @ApiModelProperty(value = "商家头像")
+    private String storeImage;
+
     @ApiModelProperty(value = "门店联系人")
     @ApiModelProperty(value = "门店联系人")
     private String storeContact;
     private String storeContact;
 
 
@@ -76,4 +79,7 @@ public class StoreCommentAppealVo extends StoreCommentAppeal {
 
 
     @ApiModelProperty(value = "是否匿名(0:否(默认), 1:是)")
     @ApiModelProperty(value = "是否匿名(0:否(默认), 1:是)")
     private String isAnonymous;
     private String isAnonymous;
+
+    @ApiModelProperty(value = "用户补充列表(存在时返回)")
+    private List<StoreCommentAppealSupplementVo> supplementList;
 }
 }

+ 26 - 0
alien-entity/src/main/java/shop/alien/mapper/StoreCommentAppealMapper.java

@@ -86,12 +86,38 @@ public interface StoreCommentAppealMapper extends BaseMapper<StoreCommentAppeal>
             "WHERE sca.appeal_status = 0 and record_id is null AND sca.delete_flag = 0")
             "WHERE sca.appeal_status = 0 and record_id is null AND sca.delete_flag = 0")
     List<Map<String, Object>> getAppealList();
     List<Map<String, Object>> getAppealList();
 
 
+    /**
+     * 申诉列表:申诉创建时间在当前时间 45~72 小时前(含边界),且待处理、未提交 AI 分析,每次最多 10 条
+     */
+    @Select("SELECT sca.id, sca.appeal_reason, sca.appeal_status, cr.content comment_content, si.img_url, cr.image_urls user_img_url, cr.id comment_id, cr.business_id " +
+            "FROM store_comment_appeal sca " +
+            "LEFT JOIN common_rating cr ON sca.comment_id = cr.id " +
+            "LEFT JOIN store_img si ON sca.img_id = si.id " +
+            "WHERE sca.appeal_status = 0 AND sca.record_id IS NULL AND sca.delete_flag = 0 " +
+            "AND sca.created_time <= DATE_SUB(NOW(), INTERVAL 5 MINUTE) " +
+
+//            "AND sca.created_time >= DATE_SUB(NOW(), INTERVAL 72 HOUR) " +
+//            "AND sca.created_time <= DATE_SUB(NOW(), INTERVAL 45 HOUR) " +
+            "ORDER BY sca.created_time ASC " +
+            "LIMIT 10")
+    List<Map<String, Object>> getAppealListBetween45And72Hours();
+
 
 
     @Select("SELECT sca.id, sca.appeal_status, sca.final_result, sca.comment_id, sca.record_id, sca.store_id " +
     @Select("SELECT sca.id, sca.appeal_status, sca.final_result, sca.comment_id, sca.record_id, sca.store_id " +
             "FROM store_comment_appeal sca " +
             "FROM store_comment_appeal sca " +
             "WHERE sca.appeal_status = 0 AND sca.record_id IS NOT NULL AND sca.delete_flag = 0")
             "WHERE sca.appeal_status = 0 AND sca.record_id IS NOT NULL AND sca.delete_flag = 0")
     List<StoreCommentAppeal> getPendingAppeals();
     List<StoreCommentAppeal> getPendingAppeals();
 
 
+    /**
+     * 用户补充申诉-AI 分析结果轮询列表:处理中、已提交 record_id、存在用户补充,每次最多 10 条
+     */
+    @Select("SELECT sca.id, sca.appeal_status, sca.final_result, sca.comment_id, sca.record_id, sca.store_id " +
+            "FROM store_comment_appeal sca " +
+            "WHERE sca.appeal_status = 3 AND sca.record_id IS NOT NULL AND sca.delete_flag = 0 " +
+            "ORDER BY sca.updated_time ASC " +
+            "LIMIT 10")
+    List<StoreCommentAppeal> getSupplementPendingAppeals();
+
     @Update("UPDATE store_comment_appeal " +
     @Update("UPDATE store_comment_appeal " +
             "SET appeal_status = #{appealStatus}, final_result = #{finalResult} " +
             "SET appeal_status = #{appealStatus}, final_result = #{finalResult} " +
             "WHERE record_id = #{recordId}")
             "WHERE record_id = #{recordId}")

+ 47 - 0
alien-entity/src/main/java/shop/alien/mapper/StoreCommentAppealSupplementMapper.java

@@ -0,0 +1,47 @@
+package shop.alien.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+import shop.alien.entity.store.StoreCommentAppealSupplement;
+import shop.alien.entity.store.vo.StoreCommentAppealSupplementVo;
+
+import java.util.List;
+
+/**
+ * 评论申诉-用户补充内容 Mapper
+ */
+@Mapper
+public interface StoreCommentAppealSupplementMapper extends BaseMapper<StoreCommentAppealSupplement> {
+
+    /**
+     * 按申诉ID查询补充列表(含用户信息)
+     */
+    @Select("SELECT s.*, u.user_name, u.user_image " +
+            "FROM store_comment_appeal_supplement s " +
+            "LEFT JOIN life_user u ON s.user_id = u.id AND u.delete_flag = 0 " +
+            "WHERE s.appeal_id = #{appealId} AND s.delete_flag = 0 " +
+            "ORDER BY s.created_time ASC")
+    List<StoreCommentAppealSupplementVo> listByAppealId(@Param("appealId") Integer appealId);
+
+    /**
+     * 按评论ID查询补充列表(含用户信息)
+     */
+    @Select("SELECT s.*, u.user_name, u.user_image " +
+            "FROM store_comment_appeal_supplement s " +
+            "LEFT JOIN life_user u ON s.user_id = u.id AND u.delete_flag = 0 " +
+            "WHERE s.comment_id = #{commentId} AND s.delete_flag = 0 " +
+            "ORDER BY s.created_time ASC")
+    List<StoreCommentAppealSupplementVo> listByCommentId(@Param("commentId") Integer commentId);
+
+    /**
+     * 查询补充详情(含用户信息)
+     */
+    @Select("SELECT s.*, u.user_name, u.user_image " +
+            "FROM store_comment_appeal_supplement s " +
+            "LEFT JOIN life_user u ON s.user_id = u.id AND u.delete_flag = 0 " +
+            "WHERE s.id = #{id} AND s.delete_flag = 0 " +
+            "LIMIT 1")
+    StoreCommentAppealSupplementVo getDetailById(@Param("id") Integer id);
+}

+ 12 - 0
alien-job/src/main/java/shop/alien/job/feign/AlienStoreFeign.java

@@ -98,4 +98,16 @@ public interface AlienStoreFeign {
      */
      */
     @GetMapping("/payment/wechatPartner/v3/applyment4sub/applyment/applyment_id/{applyment_id}")
     @GetMapping("/payment/wechatPartner/v3/applyment4sub/applyment/applyment_id/{applyment_id}")
     R<Map<String, Object>> queryWechatPartnerApplymentState(@PathVariable("applyment_id") Long applymentId);
     R<Map<String, Object>> queryWechatPartnerApplymentState(@PathVariable("applyment_id") Long applymentId);
+
+    /**
+     * 用户补充申诉:提交待处理申诉至 AI 分析(getStoreCommentAppealSupplementAppealJob)
+     */
+    @PostMapping("/storeCommentAppealSupplement/job/submitAppealAnalyze")
+    R<String> submitStoreCommentAppealSupplementAnalyze();
+
+    /**
+     * 用户补充申诉:轮询 AI 分析完成结果(getStoreCommentAppealSupplementCompletedResult)
+     */
+    @PostMapping("/storeCommentAppealSupplement/job/pollCompletedResult")
+    R<String> pollStoreCommentAppealSupplementCompletedResult();
 }
 }

+ 45 - 0
alien-job/src/main/java/shop/alien/job/store/StoreCommentAppealSupplementJob.java

@@ -0,0 +1,45 @@
+package shop.alien.job.store;
+
+import com.xxl.job.core.handler.annotation.XxlJob;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import shop.alien.entity.result.R;
+import shop.alien.job.feign.AlienStoreFeign;
+
+/**
+ * 评论申诉-用户补充 AI 分析定时任务(通过 Feign 调用 alien-store)
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class StoreCommentAppealSupplementJob {
+
+    private final AlienStoreFeign alienStoreFeign;
+
+    @XxlJob("getStoreCommentAppealSupplementAppealJob")
+    public R<String> getStoreCommentAppealSupplementAppealJob() {
+        log.info("【定时任务】用户补充申诉-提交AI分析:开始执行");
+        try {
+            R<String> result = alienStoreFeign.submitStoreCommentAppealSupplementAnalyze();
+            log.info("【定时任务】用户补充申诉-提交AI分析:执行完成,result={}", result);
+            return result != null ? result : R.fail("调用 alien-store 返回为空");
+        } catch (Exception e) {
+            log.error("【定时任务】用户补充申诉-提交AI分析:执行异常", e);
+            throw e;
+        }
+    }
+
+    @XxlJob("getStoreCommentAppealSupplementCompletedResult")
+    public R<String> getStoreCommentAppealSupplementCompletedResult() {
+        log.info("【定时任务】用户补充申诉-轮询AI结果:开始执行");
+        try {
+            R<String> result = alienStoreFeign.pollStoreCommentAppealSupplementCompletedResult();
+            log.info("【定时任务】用户补充申诉-轮询AI结果:执行完成,result={}", result);
+            return result != null ? result : R.fail("调用 alien-store 返回为空");
+        } catch (Exception e) {
+            log.error("【定时任务】用户补充申诉-轮询AI结果:执行异常", e);
+            throw e;
+        }
+    }
+}

+ 12 - 3
alien-store/src/main/java/shop/alien/store/controller/StoreCommentAppealController.java

@@ -95,6 +95,15 @@ public class StoreCommentAppealController {
     }
     }
 
 
     @ApiOperationSupport(order = 4)
     @ApiOperationSupport(order = 4)
+    @ApiOperation(value = "按评价ID查询申诉详情")
+    @ApiImplicitParams({@ApiImplicitParam(name = "commentId", value = "评价ID(common_rating.id)", dataType = "Integer", paramType = "query", required = true)})
+    @GetMapping("/getAppealDetailByCommentId")
+    public R<StoreCommentAppealVo> getAppealDetailByCommentId(@RequestParam Integer commentId) {
+        log.info("StoreCommentAppealController.getAppealDetailByCommentId?commentId={}", commentId);
+        return R.data(storeCommentAppealService.getStoreCommentAppealDetailByCommentId(commentId));
+    }
+
+    @ApiOperationSupport(order = 5)
     @ApiOperation(value = "web-申诉分页查询")
     @ApiOperation(value = "web-申诉分页查询")
     @ApiImplicitParams({
     @ApiImplicitParams({
             @ApiImplicitParam(name = "pageNum", value = "页数", dataType = "int", paramType = "query", required = true),
             @ApiImplicitParam(name = "pageNum", value = "页数", dataType = "int", paramType = "query", required = true),
@@ -116,7 +125,7 @@ public class StoreCommentAppealController {
         return R.data(storeCommentAppealService.getStoreAppealPage(pageNum, pageSize, storeName, storeContact, storePhone, appealStatus));
         return R.data(storeCommentAppealService.getStoreAppealPage(pageNum, pageSize, storeName, storeContact, storePhone, appealStatus));
     }
     }
 
 
-    @ApiOperationSupport(order = 5)
+    @ApiOperationSupport(order = 6)
     @ApiOperation(value = "web-修改申诉状态")
     @ApiOperation(value = "web-修改申诉状态")
     @ApiImplicitParams({
     @ApiImplicitParams({
             @ApiImplicitParam(name = "id", value = "申诉id", dataType = "String", paramType = "query"),
             @ApiImplicitParam(name = "id", value = "申诉id", dataType = "String", paramType = "query"),
@@ -131,7 +140,7 @@ public class StoreCommentAppealController {
         return R.fail("修改申诉状态失败");
         return R.fail("修改申诉状态失败");
     }
     }
 
 
-    @ApiOperationSupport(order = 6)
+    @ApiOperationSupport(order = 7)
     @ApiOperation(value = "web-导出Excel")
     @ApiOperation(value = "web-导出Excel")
     @GetMapping("/export")
     @GetMapping("/export")
     public ResponseEntity<byte[]> exportToExcel() {
     public ResponseEntity<byte[]> exportToExcel() {
@@ -139,7 +148,7 @@ public class StoreCommentAppealController {
         return storeCommentAppealService.exportToExcel();
         return storeCommentAppealService.exportToExcel();
     }
     }
 
 
-    @ApiOperationSupport(order = 7)
+    @ApiOperationSupport(order = 8)
     @ApiOperation("申诉历史各状态数量")
     @ApiOperation("申诉历史各状态数量")
     @ApiImplicitParams({
     @ApiImplicitParams({
             @ApiImplicitParam(name = "storeId", value = "门店id", dataType = "Integer", paramType = "query", required = true)})
             @ApiImplicitParam(name = "storeId", value = "门店id", dataType = "Integer", paramType = "query", required = true)})

+ 113 - 0
alien-store/src/main/java/shop/alien/store/controller/StoreCommentAppealSupplementController.java

@@ -0,0 +1,113 @@
+package shop.alien.store.controller;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import io.swagger.annotations.*;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.dto.StoreCommentAppealSupplementAddDto;
+import shop.alien.entity.store.dto.StoreCommentAppealSupplementUpdateDto;
+import shop.alien.entity.store.vo.StoreCommentAppealSupplementVo;
+import shop.alien.store.service.StoreCommentAppealSupplementService;
+
+import java.util.List;
+
+/**
+ * 评论申诉-用户补充内容 前端控制器
+ */
+@Slf4j
+@Api(tags = {"二期-评论申诉用户补充"})
+@ApiSort(13)
+@CrossOrigin
+@RestController
+@RequestMapping("/storeCommentAppealSupplement")
+@RequiredArgsConstructor
+public class StoreCommentAppealSupplementController {
+
+    private final StoreCommentAppealSupplementService supplementService;
+
+    @ApiOperationSupport(order = 1)
+    @ApiOperation(value = "用户端-新增申诉补充", notes = "result: 0成功 1失败 2申诉已结案 3文本异常 4无权限")
+    @PostMapping("/add")
+    public R<StoreCommentAppealSupplementVo> add(@RequestBody StoreCommentAppealSupplementAddDto dto) {
+        log.info("StoreCommentAppealSupplementController.add?dto={}", dto);
+        return supplementService.addSupplement(dto);
+    }
+
+    @ApiOperationSupport(order = 2)
+    @ApiOperation("用户端-修改申诉补充")
+    @PostMapping("/update")
+    public R<StoreCommentAppealSupplementVo> update(@RequestBody StoreCommentAppealSupplementUpdateDto dto) {
+        log.info("StoreCommentAppealSupplementController.update?dto={}", dto);
+        return supplementService.updateSupplement(dto);
+    }
+
+    @ApiOperationSupport(order = 3)
+    @ApiOperation("用户端-删除申诉补充")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "id", value = "补充ID", dataType = "Integer", paramType = "query", required = true),
+            @ApiImplicitParam(name = "userId", value = "用户ID", dataType = "Long", paramType = "query", required = true)
+    })
+    @DeleteMapping("/delete")
+    public R<Boolean> delete(@RequestParam Integer id, @RequestParam Long userId) {
+        log.info("StoreCommentAppealSupplementController.delete?id={}&userId={}", id, userId);
+        return supplementService.deleteSupplement(id, userId);
+    }
+
+    @ApiOperationSupport(order = 4)
+    @ApiOperation("查询申诉补充详情")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "id", value = "补充ID", dataType = "Integer", paramType = "query", required = true)
+    })
+    @GetMapping("/detail")
+    public R<StoreCommentAppealSupplementVo> detail(@RequestParam Integer id) {
+        log.info("StoreCommentAppealSupplementController.detail?id={}", id);
+        return R.data(supplementService.getDetail(id));
+    }
+
+    @ApiOperationSupport(order = 5)
+    @ApiOperation("按申诉ID查询补充列表")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "appealId", value = "申诉ID", dataType = "Integer", paramType = "query", required = true)
+    })
+    @GetMapping("/listByAppealId")
+    public R<List<StoreCommentAppealSupplementVo>> listByAppealId(@RequestParam Integer appealId) {
+        log.info("StoreCommentAppealSupplementController.listByAppealId?appealId={}", appealId);
+        return R.data(supplementService.listByAppealId(appealId));
+    }
+
+    @ApiOperationSupport(order = 6)
+    @ApiOperation("按评论ID查询补充列表")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "commentId", value = "评论ID", dataType = "Integer", paramType = "query", required = true)
+    })
+    @GetMapping("/listByCommentId")
+    public R<List<StoreCommentAppealSupplementVo>> listByCommentId(@RequestParam Integer commentId) {
+        log.info("StoreCommentAppealSupplementController.listByCommentId?commentId={}", commentId);
+        return R.data(supplementService.listByCommentId(commentId));
+    }
+
+    @ApiOperationSupport(order = 7)
+    @ApiOperation("分页查询申诉补充")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "pageNum", value = "页数", dataType = "Integer", paramType = "query"),
+            @ApiImplicitParam(name = "pageSize", value = "页容", dataType = "Integer", paramType = "query"),
+            @ApiImplicitParam(name = "appealId", value = "申诉ID", dataType = "Integer", paramType = "query"),
+            @ApiImplicitParam(name = "userId", value = "用户ID", dataType = "Long", paramType = "query"),
+            @ApiImplicitParam(name = "storeId", value = "门店ID", dataType = "Integer", paramType = "query"),
+            @ApiImplicitParam(name = "auditStatus", value = "审核状态:0-待审核 1-通过 2-驳回", dataType = "Integer", paramType = "query")
+    })
+    @GetMapping("/page")
+    public R<IPage<StoreCommentAppealSupplementVo>> page(
+            @RequestParam(defaultValue = "1") Integer pageNum,
+            @RequestParam(defaultValue = "10") Integer pageSize,
+            @RequestParam(required = false) Integer appealId,
+            @RequestParam(required = false) Long userId,
+            @RequestParam(required = false) Integer storeId,
+            @RequestParam(required = false) Integer auditStatus) {
+        log.info("StoreCommentAppealSupplementController.page?pageNum={}&pageSize={}&appealId={}&userId={}&storeId={}&auditStatus={}",
+                pageNum, pageSize, appealId, userId, storeId, auditStatus);
+        return R.data(supplementService.getPage(pageNum, pageSize, appealId, userId, storeId, auditStatus));
+    }
+}

+ 42 - 0
alien-store/src/main/java/shop/alien/store/controller/StoreCommentAppealSupplementJobController.java

@@ -0,0 +1,42 @@
+package shop.alien.store.controller;
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import shop.alien.entity.result.R;
+import shop.alien.store.service.StoreCommentAppealSupplementJobService;
+
+/**
+ * 评论申诉-用户补充定时任务回调(供 alien-job 通过 Feign 调用)
+ */
+@Slf4j
+@Api(tags = {"评论申诉用户补充定时任务"})
+@RestController
+@RequestMapping("/storeCommentAppealSupplement/job")
+@RequiredArgsConstructor
+public class StoreCommentAppealSupplementJobController {
+
+    private final StoreCommentAppealSupplementJobService supplementJobService;
+
+    @ApiOperation("提交待处理申诉至 AI 分析(getStoreCommentAppealSupplementAppealJob)")
+    @PostMapping("/submitAppealAnalyze")
+    public R<String> submitAppealAnalyze() {
+        log.info("storeCommentAppealSupplement job: submitAppealAnalyze 开始");
+        R<String> result = supplementJobService.submitAppealAnalyze();
+        log.info("storeCommentAppealSupplement job: submitAppealAnalyze 结束");
+        return result;
+    }
+
+    @ApiOperation("轮询 AI 分析完成结果(getStoreCommentAppealSupplementCompletedResult)")
+    @PostMapping("/pollCompletedResult")
+    public R<String> pollCompletedResult() {
+        log.info("storeCommentAppealSupplement job: pollCompletedResult 开始");
+        R<String> result = supplementJobService.pollCompletedResult();
+        log.info("storeCommentAppealSupplement job: pollCompletedResult 结束");
+        return result;
+    }
+}

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

@@ -0,0 +1,38 @@
+package shop.alien.store.controller;
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import shop.alien.entity.result.R;
+import shop.alien.store.service.StoreCommentAppealSupplementJobService;
+
+/**
+ * 评论申诉-用户补充定时任务测试接口(仅启动 alien-store 即可联调)
+ */
+@Slf4j
+@Api(tags = "评论申诉用户补充-定时任务测试")
+@RestController
+@RequestMapping("/storeCommentAppealSupplementJob/test")
+@RequiredArgsConstructor
+public class StoreCommentAppealSupplementJobTestController {
+
+    private final StoreCommentAppealSupplementJobService supplementJobService;
+
+    @ApiOperation("手动触发:提交申诉AI分析")
+    @PostMapping("/triggerAppealAnalyze")
+    public R<String> triggerAppealAnalyze() {
+        log.info("StoreCommentAppealSupplementJobTestController.triggerAppealAnalyze");
+        return supplementJobService.submitAppealAnalyze();
+    }
+
+    @ApiOperation("手动触发:轮询申诉AI分析结果")
+    @PostMapping("/triggerCompletedResult")
+    public R<String> triggerCompletedResult() {
+        log.info("StoreCommentAppealSupplementJobTestController.triggerCompletedResult");
+        return supplementJobService.pollCompletedResult();
+    }
+}

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

@@ -68,6 +68,14 @@ public interface StoreCommentAppealService extends IService<StoreCommentAppeal>
     StoreCommentAppealVo getStoreCommentAppealDetail(Integer id);
     StoreCommentAppealVo getStoreCommentAppealDetail(Integer id);
 
 
     /**
     /**
+     * 按评价ID查询申诉详情(comment_id 关联 common_rating.id)
+     *
+     * @param commentId 评价ID
+     * @return StoreCommentAppealVo
+     */
+    StoreCommentAppealVo getStoreCommentAppealDetailByCommentId(Integer commentId);
+
+    /**
      * web-申诉分页查询
      * web-申诉分页查询
      *
      *
      * @param pageNum      页数
      * @param pageNum      页数

+ 499 - 0
alien-store/src/main/java/shop/alien/store/service/StoreCommentAppealSupplementJobService.java

@@ -0,0 +1,499 @@
+package shop.alien.store.service;
+
+import com.alibaba.fastjson2.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.collections4.map.HashedMap;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Service;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+import org.springframework.util.StringUtils;
+import org.springframework.web.client.RestTemplate;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.*;
+import shop.alien.mapper.*;
+import shop.alien.util.common.constant.CommentSourceTypeEnum;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * 评论申诉-用户补充 AI 分析任务服务
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class StoreCommentAppealSupplementJobService {
+
+    private final RestTemplate restTemplate;
+    private final StoreCommentAppealMapper storeCommentAppealMapper;
+    private final StoreCommentAppealSupplementMapper storeCommentAppealSupplementMapper;
+    private final StoreUserMapper storeUserMapper;
+    private final LifeUserMapper lifeUserMapper;
+    private final LifeNoticeMapper lifeNoticeMapper;
+    private final CommonRatingMapper commonRatingMapper;
+    private final CommonCommentMapper commonCommentMapper;
+    private final LifeLikeRecordMapper lifeLikeRecordMapper;
+    private final StoreInfoMapper storeInfoMapper;
+
+    @Value("${third-party-user-name.base-url}")
+    private String userName;
+
+    @Value("${third-party-pass-word.base-url}")
+    private String passWord;
+
+    @Value("${third-party-login.base-url}")
+    private String loginUrl;
+
+    @Value("${third-party-ai-analyze.base-url}")
+    private String analyzeUrl;
+
+    @Value("${third-party-ai-resultUrl.base-url}")
+    private String resultUrl;
+
+    /**
+     * 提交待处理申诉至 AI 分析
+     */
+    public R<String> submitAppealAnalyze() {
+        log.info("【用户补充申诉】开始执行 AI 差评申诉分析提交任务");
+        return invokeAnalyzeTask();
+    }
+
+    /**
+     * 轮询 AI 分析完成结果
+     */
+    public R<String> pollCompletedResult() {
+        log.info("【用户补充申诉】开始查询 AI 分析结果");
+        return pollAppealCompletedResult();
+    }
+
+    private R<String> pollAppealCompletedResult() {
+        List<StoreCommentAppeal> pendingAppeals = storeCommentAppealMapper.getSupplementPendingAppeals();
+        if (pendingAppeals == null || pendingAppeals.isEmpty()) {
+            log.info("【用户补充申诉】没有待轮询的分析结果");
+            return R.success("没有待轮询的分析结果");
+        }
+        log.info("【用户补充申诉】本次轮询申述数量(上限10条): {}", pendingAppeals.size());
+
+        RestTemplate restTemplateWithAuth = new RestTemplate();
+
+        for (StoreCommentAppeal appeal : pendingAppeals) {
+            String completedUrl = buildCompletedUrl(appeal.getRecordId());
+            try {
+                ResponseEntity<String> analyzeResp = restTemplateWithAuth.getForEntity(completedUrl, String.class);
+                if (analyzeResp == null || analyzeResp.getStatusCodeValue() != 200) {
+                    if (analyzeResp != null) {
+                        log.error("【用户补充申诉】查询分析结果失败, http状态: {}", analyzeResp.getStatusCode());
+                    }
+                    continue;
+                }
+
+                String analyzeBody = analyzeResp.getBody();
+                log.info("【用户补充申诉】查询分析结果, 申诉ID: {}, 返回: {}", appeal.getId(), analyzeBody);
+
+                JSONObject analyzeJson = JSONObject.parseObject(analyzeBody);
+                Integer respCode = analyzeJson.getInteger("code");
+                JSONObject dataJsonObj = analyzeJson.getJSONObject("data");
+
+                if (respCode != null && respCode == 202) {
+                    JSONObject detailsObj = analyzeJson.getJSONObject("details");
+                    String status = detailsObj != null ? detailsObj.getString("analysis_status") : "unknown";
+                    log.info("【用户补充申诉】分析尚未完成(code=202),申诉ID: {},recordId: {},状态: {}",
+                            appeal.getId(), appeal.getRecordId(), status);
+                    continue;
+                }
+
+                if (respCode == null || respCode != 200) {
+                    log.warn("【用户补充申诉】分析返回非200,申诉ID: {},code: {},message: {}",
+                            appeal.getId(), respCode, analyzeJson.getString("message"));
+                    continue;
+                }
+
+                if (dataJsonObj == null) {
+                    log.error("【用户补充申诉】分析返回数据为空,申诉ID: {}", appeal.getId());
+                    continue;
+                }
+
+                String analysisStatus = dataJsonObj.getString("analysis_status");
+                if (!"completed".equals(analysisStatus)) {
+                    log.info("【用户补充申诉】分析尚未完成,申诉ID: {},recordId: {},状态: {}",
+                            appeal.getId(), appeal.getRecordId(), analysisStatus);
+                    continue;
+                }
+
+                Double userConfidence = dataJsonObj.getDouble("user_confidence");
+                Double merchantConfidence = dataJsonObj.getDouble("merchant_confidence");
+                if (userConfidence == null || merchantConfidence == null) {
+                    log.error("【用户补充申诉】置信度为空,申诉ID: {},user_confidence: {},merchant_confidence: {}",
+                            appeal.getId(), userConfidence, merchantConfidence);
+                    continue;
+                }
+
+                log.info("【用户补充申诉】分析结果,申诉ID: {},user_confidence: {},merchant_confidence: {}",
+                        appeal.getId(), userConfidence, merchantConfidence);
+
+                StoreCommentAppeal updateAppeal = new StoreCommentAppeal();
+                updateAppeal.setRecordId(appeal.getRecordId());
+                updateAppeal.setAppealAiApproval(dataJsonObj.toJSONString());
+
+                if (merchantConfidence > userConfidence) {
+                    updateAppeal.setAppealStatus(2);
+                    updateAppeal.setFinalResult("已同意");
+                    log.info("【用户补充申诉】商家赢,申诉通过,申诉ID: {}", appeal.getId());
+                    Integer ratingId = appeal.getCommentId();
+                    if (ratingId != null) {
+                        deleteRatingAndComments(ratingId);
+                    }
+                } else {
+                    updateAppeal.setAppealStatus(1);
+                    updateAppeal.setFinalResult("已驳回");
+                    log.info("【用户补充申诉】用户赢,申诉驳回,申诉ID: {}", appeal.getId());
+                }
+
+                storeCommentAppealMapper.updateByRecordId(appeal.getRecordId(),
+                        updateAppeal.getAppealStatus(), updateAppeal.getFinalResult());
+                boolean merchantWins = merchantConfidence > userConfidence;
+                sendAppealResultNotifications(appeal, merchantWins);
+            } catch (Exception e) {
+                log.error("【用户补充申诉】查询分析结果异常,申诉ID: {}", appeal.getId(), e);
+            }
+        }
+        return R.success("【用户补充申诉】查询 AI 分析结果完成");
+    }
+
+    private void sendAppealResultNotifications(StoreCommentAppeal appeal, boolean merchantWins) {
+        sendAppealResultNoticeToUser(appeal, merchantWins);
+        sendAppealResultNoticeToStore(appeal, merchantWins);
+    }
+
+    private void sendAppealResultNoticeToUser(StoreCommentAppeal appeal, boolean merchantWins) {
+        try {
+            CommonRating rating = commonRatingMapper.selectById(appeal.getCommentId());
+            if (rating == null || rating.getUserId() == null) {
+                log.warn("【用户补充申诉】用户通知跳过:评价不存在或无用户,appealId={}", appeal.getId());
+                return;
+            }
+            LifeUser lifeUser = lifeUserMapper.selectById(rating.getUserId().intValue());
+            if (lifeUser == null || !StringUtils.hasText(lifeUser.getUserPhone())) {
+                log.warn("【用户补充申诉】用户通知跳过:用户不存在或手机号为空,appealId={}", appeal.getId());
+                return;
+            }
+
+            String message = merchantWins
+                    ? "商家对你的评价申诉已通过审核,该条评价已删除。"
+                    : "商家对你的评价申诉未通过审核,你的评价将继续展示。";
+
+            String receiverId = "user_" + lifeUser.getUserPhone();
+            JSONObject contextJson = new JSONObject();
+            contextJson.put("message", message);
+            contextJson.put("appealId", appeal.getId());
+            contextJson.put("commentId", appeal.getCommentId());
+            insertSystemNotice(receiverId, appeal.getId(), "商家申诉通知", contextJson.toJSONString());
+            log.info("【用户补充申诉】用户申诉结果通知发送成功,appealId={}, merchantWins={}", appeal.getId(), merchantWins);
+        } catch (Exception e) {
+            log.error("【用户补充申诉】用户申诉结果通知发送失败,appealId={}", appeal.getId(), e);
+        }
+    }
+
+    private void sendAppealResultNoticeToStore(StoreCommentAppeal appeal, boolean merchantWins) {
+        try {
+            StoreUser storeUser = storeUserMapper.selectOne(
+                    new QueryWrapper<StoreUser>().eq("store_id", appeal.getStoreId()).eq("delete_flag", 0));
+            if (storeUser == null || !StringUtils.hasText(storeUser.getPhone())) {
+                log.warn("【用户补充申诉】商家通知跳过:未找到店铺用户,appealId={}", appeal.getId());
+                return;
+            }
+
+            CommonRating rating = commonRatingMapper.selectById(appeal.getCommentId());
+            LifeUser ratingUser = null;
+            if (rating != null && rating.getUserId() != null) {
+                ratingUser = lifeUserMapper.selectById(rating.getUserId().intValue());
+            }
+            String maskedNickName = desensitizeUserNickName(rating, ratingUser);
+
+            String message = merchantWins
+                    ? "你对 " + maskedNickName + " 的评价申诉已通过,该评价已下架。"
+                    : "你对 " + maskedNickName + " 的评价申诉未通过,评价将继续展示。";
+
+            String receiverId = "store_" + storeUser.getPhone();
+            JSONObject contextJson = new JSONObject();
+            contextJson.put("message", message);
+            contextJson.put("appealId", appeal.getId());
+            contextJson.put("commentId", appeal.getCommentId());
+
+
+            String title = merchantWins
+                    ? "申诉通过"
+                    : "申诉驳回";
+
+            insertSystemNotice(receiverId, appeal.getId(), title, contextJson.toJSONString());
+            log.info("【用户补充申诉】商家申诉结果通知发送成功,appealId={}, merchantWins={}", appeal.getId(), merchantWins);
+        } catch (Exception e) {
+            log.error("【用户补充申诉】商家申诉结果通知发送失败,appealId={}", appeal.getId(), e);
+        }
+    }
+
+    private void insertSystemNotice(String receiverId, Integer businessId, String title, String context) {
+        LifeNotice lifeNotice = new LifeNotice();
+        lifeNotice.setSenderId("system");
+        lifeNotice.setReceiverId(receiverId);
+        lifeNotice.setBusinessId(businessId);
+        lifeNotice.setTitle(title);
+        lifeNotice.setContext(context);
+        lifeNotice.setNoticeType(1);
+        lifeNotice.setIsRead(0);
+        lifeNoticeMapper.insert(lifeNotice);
+    }
+
+    private String desensitizeUserNickName(CommonRating rating, LifeUser lifeUser) {
+        if (rating != null && rating.getIsAnonymous() != null && rating.getIsAnonymous() == 1) {
+            return "匿名用户";
+        }
+        String nick = lifeUser != null ? lifeUser.getUserName() : null;
+        if (!StringUtils.hasText(nick)) {
+            return "用户";
+        }
+        nick = nick.trim();
+        if (nick.length() == 1) {
+            return nick.charAt(0) + "*";
+        }
+        if (nick.length() == 2) {
+            return nick.charAt(0) + "*";
+        }
+        return nick.charAt(0) + "**" + nick.charAt(nick.length() - 1);
+    }
+
+    private R<String> invokeAnalyzeTask() {
+        log.info("【用户补充申诉】开始调用差评申述置信度分析接口, url: {}", analyzeUrl);
+
+        HttpHeaders analyzeHeaders = new HttpHeaders();
+        analyzeHeaders.setContentType(MediaType.APPLICATION_JSON);
+
+        List<Map<String, Object>> appealList = storeCommentAppealMapper.getAppealListBetween45And72Hours();
+        if (appealList == null || appealList.isEmpty()) {
+            log.info("【用户补充申诉】没有申诉时间在45~72小时内的待处理申述");
+            return R.success("没有申诉时间在45~72小时内的待处理申述");
+        }
+        log.info("【用户补充申诉】本次处理申述数量(申诉45~72小时,上限10条): {}", appealList.size());
+
+        for (Map<String, Object> storeCommentAppeal : appealList) {
+            Map<String, Object> analyzeRequest = new HashedMap<>();
+
+            analyzeRequest.put("merchant_material",
+                    storeCommentAppeal.get("appeal_reason") == null ? "" : storeCommentAppeal.get("appeal_reason").toString());
+            analyzeRequest.put("user_material",
+                    storeCommentAppeal.get("comment_content") == null ? "" : storeCommentAppeal.get("comment_content").toString());
+            Integer appealId = (Integer) storeCommentAppeal.get("id");
+            analyzeRequest.put("user_review", buildUserReviewFromSupplement(appealId));
+
+            List<String> merchantImages = new ArrayList<>();
+            String imgUrls = storeCommentAppeal.get("img_url") == null ? "" : storeCommentAppeal.get("img_url").toString();
+            if (StringUtils.hasText(imgUrls)) {
+                for (String imageUrl : imgUrls.split(",")) {
+                    merchantImages.add(imageUrl.trim());
+                }
+            }
+            analyzeRequest.put("merchant_images", merchantImages);
+
+            List<String> userImages = new ArrayList<>();
+            String userImgUrls = storeCommentAppeal.get("user_img_url") == null ? "" : storeCommentAppeal.get("user_img_url").toString();
+            if (StringUtils.hasText(userImgUrls)) {
+                for (String imageUrl : userImgUrls.split(",")) {
+                    userImages.add(imageUrl.trim());
+                }
+            }
+            analyzeRequest.put("user_images", userImages);
+
+            analyzeRequest.put("case_id", storeCommentAppeal.get("comment_id") == null ? "" : storeCommentAppeal.get("comment_id").toString());
+            analyzeRequest.put("order_id", storeCommentAppeal.get("business_id") == null ? "" : storeCommentAppeal.get("business_id").toString());
+
+            HttpEntity<Map<String, Object>> analyzeEntity = new HttpEntity<>(analyzeRequest, analyzeHeaders);
+
+            ResponseEntity<String> analyzeResp;
+            try {
+                analyzeResp = restTemplate.postForEntity(analyzeUrl, analyzeEntity, String.class);
+            } catch (org.springframework.web.client.HttpServerErrorException.ServiceUnavailable e) {
+                log.error("【用户补充申诉】调用分析接口返回503: {}", e.getResponseBodyAsString());
+                continue;
+            } catch (Exception e) {
+                log.error("【用户补充申诉】调用分析接口异常", e);
+                continue;
+            }
+
+            if (analyzeResp == null || analyzeResp.getStatusCodeValue() != 200) {
+                if (analyzeResp != null) {
+                    log.error("【用户补充申诉】调用分析接口失败, http状态: {}", analyzeResp.getStatusCode());
+                }
+                continue;
+            }
+
+            String analyzeBody = analyzeResp.getBody();
+            log.info("【用户补充申诉】分析提交成功, 返回: {}", analyzeBody);
+
+            JSONObject analyzeJson = JSONObject.parseObject(analyzeBody);
+            JSONObject dataJsonObj = analyzeJson.getJSONObject("data");
+            if (dataJsonObj == null) {
+                log.error("【用户补充申诉】分析返回数据为空");
+                continue;
+            }
+
+            Integer recordId = dataJsonObj.getInteger("record_id");
+            if (recordId == null) {
+                log.error("【用户补充申诉】分析返回 record_id 为空");
+                continue;
+            }
+
+            StoreCommentAppeal sCommentAppeal = new StoreCommentAppeal();
+            sCommentAppeal.setId((Integer) storeCommentAppeal.get("id"));
+            sCommentAppeal.setAppealStatus(3);
+            sCommentAppeal.setRecordId(recordId);
+            storeCommentAppealMapper.updateById(sCommentAppeal);
+        }
+        return R.success("【用户补充申诉】调用差评申述置信度分析接口完成");
+    }
+
+    private String buildUserReviewFromSupplement(Integer appealId) {
+        if (appealId == null) {
+            return "";
+        }
+        List<StoreCommentAppealSupplement> supplements = storeCommentAppealSupplementMapper.selectList(
+                new LambdaQueryWrapper<StoreCommentAppealSupplement>()
+                        .eq(StoreCommentAppealSupplement::getAppealId, appealId)
+                        .eq(StoreCommentAppealSupplement::getDeleteFlag, 0)
+                        .orderByAsc(StoreCommentAppealSupplement::getCreatedTime));
+        return supplements.stream()
+                .map(StoreCommentAppealSupplement::getSupplementContent)
+                .filter(StringUtils::hasText)
+                .collect(Collectors.joining("\n"));
+    }
+
+    private String buildCompletedUrl(Integer recordId) {
+        String baseUrl = resultUrl;
+        if (!StringUtils.hasText(baseUrl)) {
+            throw new IllegalStateException("【用户补充申诉】分析结果接口地址未配置");
+        }
+
+        return baseUrl + "/" + recordId + "/completed";
+    }
+
+    private void deleteRatingAndComments(Integer ratingId) {
+        try {
+            CommonRating rating = commonRatingMapper.selectById(ratingId);
+            if (rating != null) {
+                int rows = commonRatingMapper.logicDeleteById(ratingId);
+                log.info("【用户补充申诉】删除评价,ratingId={},影响行数={}", ratingId, rows);
+            }
+
+            LambdaQueryWrapper<CommonComment> commentQueryWrapper = new LambdaQueryWrapper<>();
+            commentQueryWrapper.eq(CommonComment::getSourceType, CommentSourceTypeEnum.STORE_COMMENT.getType())
+                    .eq(CommonComment::getSourceId, ratingId)
+                    .eq(CommonComment::getDeleteFlag, 0);
+            List<CommonComment> comments = commonCommentMapper.selectList(commentQueryWrapper);
+            List<Long> commentIds = comments.stream().map(CommonComment::getId).collect(Collectors.toList());
+
+            int commentRows = commonCommentMapper.logicDeleteBySourceId(
+                    CommentSourceTypeEnum.STORE_COMMENT.getType(), ratingId);
+            log.info("【用户补充申诉】删除评价下评论,ratingId={},影响行数={}", ratingId, commentRows);
+
+            LambdaUpdateWrapper<LifeLikeRecord> ratingLikeWrapper = new LambdaUpdateWrapper<>();
+            ratingLikeWrapper.eq(LifeLikeRecord::getType, "7")
+                    .eq(LifeLikeRecord::getHuifuId, String.valueOf(ratingId))
+                    .eq(LifeLikeRecord::getDeleteFlag, 0);
+            ratingLikeWrapper.set(LifeLikeRecord::getDeleteFlag, 1);
+            ratingLikeWrapper.set(LifeLikeRecord::getUpdatedTime, new java.util.Date());
+            lifeLikeRecordMapper.update(null, ratingLikeWrapper);
+
+            if (!commentIds.isEmpty()) {
+                for (Long commentId : commentIds) {
+                    LambdaUpdateWrapper<LifeLikeRecord> commentLikeWrapper = new LambdaUpdateWrapper<>();
+                    commentLikeWrapper.eq(LifeLikeRecord::getType, "1")
+                            .eq(LifeLikeRecord::getHuifuId, String.valueOf(commentId))
+                            .eq(LifeLikeRecord::getDeleteFlag, 0);
+                    commentLikeWrapper.set(LifeLikeRecord::getDeleteFlag, 1);
+                    commentLikeWrapper.set(LifeLikeRecord::getUpdatedTime, new java.util.Date());
+                    lifeLikeRecordMapper.update(null, commentLikeWrapper);
+                }
+            }
+
+            if (rating != null && rating.getBusinessType() == 1) {
+                updateStoreScore(rating.getBusinessId());
+            }
+        } catch (Exception e) {
+            log.error("【用户补充申诉】删除评价和评论异常,ratingId={}", ratingId, e);
+        }
+    }
+
+    private void updateStoreScore(Integer businessId) {
+        try {
+            LambdaQueryWrapper<CommonRating> wrapper = new LambdaQueryWrapper<>();
+            wrapper.eq(CommonRating::getBusinessType, 1)
+                    .eq(CommonRating::getBusinessId, businessId)
+                    .eq(CommonRating::getDeleteFlag, 0)
+                    .eq(CommonRating::getIsShow, 1)
+                    .eq(CommonRating::getAuditStatus, 1);
+            List<CommonRating> ratings = commonRatingMapper.selectList(wrapper);
+
+            int total = ratings.size();
+            if (total == 0) {
+                StoreInfo storeInfo = new StoreInfo();
+                storeInfo.setId(businessId);
+                storeInfo.setScoreAvg(5.0);
+                storeInfo.setScoreOne(0.0);
+                storeInfo.setScoreTwo(0.0);
+                storeInfo.setScoreThree(0.0);
+                storeInfoMapper.updateById(storeInfo);
+                return;
+            }
+
+            double scoreSum = ratings.stream().mapToDouble(r -> r.getScore() != null ? r.getScore() : 0.0).sum();
+            double scoreOneSum = ratings.stream().mapToDouble(r -> r.getScoreOne() != null ? r.getScoreOne() : 0.0).sum();
+            double scoreTwoSum = ratings.stream().mapToDouble(r -> r.getScoreTwo() != null ? r.getScoreTwo() : 0.0).sum();
+            double scoreThreeSum = ratings.stream().mapToDouble(r -> r.getScoreThree() != null ? r.getScoreThree() : 0.0).sum();
+
+            StoreInfo storeInfo = new StoreInfo();
+            storeInfo.setId(businessId);
+            storeInfo.setScoreAvg(Math.round((scoreSum / total) * 100.0) / 100.0);
+            storeInfo.setScoreOne(Math.round((scoreOneSum / total) * 100.0) / 100.0);
+            storeInfo.setScoreTwo(Math.round((scoreTwoSum / total) * 100.0) / 100.0);
+            storeInfo.setScoreThree(Math.round((scoreThreeSum / total) * 100.0) / 100.0);
+            storeInfoMapper.updateById(storeInfo);
+        } catch (Exception e) {
+            log.error("【用户补充申诉】更新店铺评分失败,businessId={}", businessId, e);
+        }
+    }
+
+    @SuppressWarnings("unused")
+    private String fetchAiServiceToken() {
+        log.info("【用户补充申诉】登录 AI 服务获取 token, url: {}", loginUrl);
+        MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
+        formData.add("username", userName);
+        formData.add("password", passWord);
+
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
+        HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(formData, headers);
+
+        try {
+            ResponseEntity<String> response = restTemplate.postForEntity(loginUrl, requestEntity, String.class);
+            if (response != null && response.getStatusCodeValue() == 200 && response.getBody() != null) {
+                JSONObject jsonObject = JSONObject.parseObject(response.getBody());
+                JSONObject dataJson = jsonObject.getJSONObject("data");
+                return dataJson != null ? dataJson.getString("access_token") : null;
+            }
+            log.error("【用户补充申诉】登录接口失败, http状态: {}", response != null ? response.getStatusCode() : null);
+        } catch (Exception e) {
+            log.error("【用户补充申诉】登录接口异常", e);
+        }
+        return null;
+    }
+}

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

@@ -0,0 +1,54 @@
+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.StoreCommentAppealSupplement;
+import shop.alien.entity.store.dto.StoreCommentAppealSupplementAddDto;
+import shop.alien.entity.store.dto.StoreCommentAppealSupplementUpdateDto;
+import shop.alien.entity.store.vo.StoreCommentAppealSupplementVo;
+
+import java.util.List;
+
+/**
+ * 评论申诉-用户补充内容 服务类
+ */
+public interface StoreCommentAppealSupplementService extends IService<StoreCommentAppealSupplement> {
+
+    /**
+     * 新增用户补充
+     */
+    R<StoreCommentAppealSupplementVo> addSupplement(StoreCommentAppealSupplementAddDto dto);
+
+    /**
+     * 修改用户补充
+     */
+    R<StoreCommentAppealSupplementVo> updateSupplement(StoreCommentAppealSupplementUpdateDto dto);
+
+    /**
+     * 删除用户补充(逻辑删除)
+     */
+    R<Boolean> deleteSupplement(Integer id, Long userId);
+
+    /**
+     * 查询补充详情
+     */
+    StoreCommentAppealSupplementVo getDetail(Integer id);
+
+    /**
+     * 按申诉ID查询补充列表
+     */
+    List<StoreCommentAppealSupplementVo> listByAppealId(Integer appealId);
+
+    /**
+     * 按评论ID查询补充列表
+     */
+    List<StoreCommentAppealSupplementVo> listByCommentId(Integer commentId);
+
+    /**
+     * 分页查询
+     */
+    IPage<StoreCommentAppealSupplementVo> getPage(Integer pageNum, Integer pageSize,
+                                                  Integer appealId, Long userId,
+                                                  Integer storeId, Integer auditStatus);
+}

+ 172 - 32
alien-store/src/main/java/shop/alien/store/service/impl/StoreCommentAppealServiceImpl.java

@@ -30,6 +30,7 @@ import shop.alien.entity.store.*;
 import shop.alien.entity.store.excelVo.util.ExcelExporter;
 import shop.alien.entity.store.excelVo.util.ExcelExporter;
 import shop.alien.entity.store.vo.StoreCommentAppealLogVo;
 import shop.alien.entity.store.vo.StoreCommentAppealLogVo;
 import shop.alien.entity.store.vo.StoreCommentAppealInfoVo;
 import shop.alien.entity.store.vo.StoreCommentAppealInfoVo;
+import shop.alien.entity.store.vo.StoreCommentAppealSupplementVo;
 import shop.alien.entity.store.vo.StoreCommentAppealVo;
 import shop.alien.entity.store.vo.StoreCommentAppealVo;
 import shop.alien.entity.store.vo.WebSocketVo;
 import shop.alien.entity.store.vo.WebSocketVo;
 import shop.alien.mapper.*;
 import shop.alien.mapper.*;
@@ -82,6 +83,10 @@ public class StoreCommentAppealServiceImpl extends ServiceImpl<StoreCommentAppea
 
 
     private final LifeNoticeMapper lifeNoticeMapper;
     private final LifeNoticeMapper lifeNoticeMapper;
 
 
+    private final LifeUserMapper lifeUserMapper;
+
+    private final StoreCommentAppealSupplementMapper storeCommentAppealSupplementMapper;
+
     private final StoreUserMapper storeUserMapper;
     private final StoreUserMapper storeUserMapper;
 
 
     private final WebSocketProcess webSocketProcess;
     private final WebSocketProcess webSocketProcess;
@@ -321,40 +326,45 @@ public class StoreCommentAppealServiceImpl extends ServiceImpl<StoreCommentAppea
             boolean resultSystem = storeCommentAppealSave && storeCommentAppealLogMapper.insert(storeCommentAppealLogSystem) > 0;
             boolean resultSystem = storeCommentAppealSave && storeCommentAppealLogMapper.insert(storeCommentAppealLogSystem) > 0;
             
             
             // ========== 新增:调用AI分析接口并查询结果 ==========
             // ========== 新增:调用AI分析接口并查询结果 ==========
+            // 新增修改,商家申诉后,不需要调用ai接口进行分析了,定时任务会再25小时后调用ai接口 ---- 2025-5-29新增需求
+//            if (result && resultSystem) {
+//                try {
+//                    // 1. 调用AI分析接口提交分析任务
+//                    Integer recordId = invokeAiAnalyze(storeCommentAppeal);
+//                    if (recordId != null) {
+//                        // 2. 更新申诉记录,保存record_id
+//                        StoreCommentAppeal updateAppeal = new StoreCommentAppeal();
+//                        updateAppeal.setId(storeCommentAppeal.getId());
+//                        updateAppeal.setRecordId(recordId);
+//                        storeCommentAppealMapper.updateById(updateAppeal);
+//
+//                        // 3. 立即查询AI分析结果
+//                        boolean queryResult = queryAiAnalysisResult(storeCommentAppeal.getId(), recordId);
+//                        if (queryResult) {
+//                            log.info("AI分析任务提交并查询结果成功,申诉ID: {},recordId: {}",
+//                                storeCommentAppeal.getId(), recordId);
+//                        } else {
+//                            log.warn("AI分析任务提交成功,但查询结果失败,申诉ID: {},recordId: {},将由定时任务后续查询",
+//                                storeCommentAppeal.getId(), recordId);
+//                        }
+//
+//                        // 更新返回对象(recordId已通过BeanUtils.copyProperties复制)
+//                    } else {
+//                        log.warn("AI分析任务提交失败,但申诉记录已保存,申诉ID: {},状态保持: 待处理(0)",
+//                            storeCommentAppeal.getId());
+//                        // AI调用失败不影响申诉提交,保持原状态(0:待处理)
+//                    }
+//                } catch (Exception e) {
+//                    log.error("调用AI分析接口异常,申诉ID: {}", storeCommentAppeal.getId(), e);
+//                    // AI调用失败不影响申诉提交,保持原状态(0:待处理)
+//                }
+//            }
+            // ========== 新增代码结束 ==========
+
             if (result && resultSystem) {
             if (result && resultSystem) {
-                try {
-                    // 1. 调用AI分析接口提交分析任务
-                    Integer recordId = invokeAiAnalyze(storeCommentAppeal);
-                    if (recordId != null) {
-                        // 2. 更新申诉记录,保存record_id
-                        StoreCommentAppeal updateAppeal = new StoreCommentAppeal();
-                        updateAppeal.setId(storeCommentAppeal.getId());
-                        updateAppeal.setRecordId(recordId);
-                        storeCommentAppealMapper.updateById(updateAppeal);
-                        
-                        // 3. 立即查询AI分析结果
-                        boolean queryResult = queryAiAnalysisResult(storeCommentAppeal.getId(), recordId);
-                        if (queryResult) {
-                            log.info("AI分析任务提交并查询结果成功,申诉ID: {},recordId: {}", 
-                                storeCommentAppeal.getId(), recordId);
-                        } else {
-                            log.warn("AI分析任务提交成功,但查询结果失败,申诉ID: {},recordId: {},将由定时任务后续查询", 
-                                storeCommentAppeal.getId(), recordId);
-                        }
-                        
-                        // 更新返回对象(recordId已通过BeanUtils.copyProperties复制)
-                    } else {
-                        log.warn("AI分析任务提交失败,但申诉记录已保存,申诉ID: {},状态保持: 待处理(0)", 
-                            storeCommentAppeal.getId());
-                        // AI调用失败不影响申诉提交,保持原状态(0:待处理)
-                    }
-                } catch (Exception e) {
-                    log.error("调用AI分析接口异常,申诉ID: {}", storeCommentAppeal.getId(), e);
-                    // AI调用失败不影响申诉提交,保持原状态(0:待处理)
-                }
+                sendMerchantAppealNotificationToUser(storeCommentAppeal);
             }
             }
-            // ========== 新增代码结束 ==========
-            
+
             storeCommentAppealInfoVo.setResult(result && resultSystem ? 0 : 1);
             storeCommentAppealInfoVo.setResult(result && resultSystem ? 0 : 1);
         } catch (Exception e) {
         } catch (Exception e) {
             log.error("新增申诉异常", e);
             log.error("新增申诉异常", e);
@@ -406,10 +416,89 @@ public class StoreCommentAppealServiceImpl extends ServiceImpl<StoreCommentAppea
         if (ObjectUtils.isNotEmpty(storeCommentAppealLogVo)) {
         if (ObjectUtils.isNotEmpty(storeCommentAppealLogVo)) {
             commentDetail.setLogRemark(storeCommentAppealLogVo.get(storeCommentAppealLogVo.size() - 1).getLogRemark());
             commentDetail.setLogRemark(storeCommentAppealLogVo.get(storeCommentAppealLogVo.size() - 1).getLogRemark());
         }
         }
+        fillMerchantInfo(commentDetail);
         return commentDetail;
         return commentDetail;
     }
     }
 
 
     /**
     /**
+     * 填充商家名称与头像(门店名称来自 store_info,头像优先主账号 store_user.head_img)
+     */
+    private void fillMerchantInfo(StoreCommentAppealVo commentDetail) {
+        if (commentDetail == null || commentDetail.getStoreId() == null) {
+            return;
+        }
+        StoreInfo storeInfo = storeInfoMapper.selectById(commentDetail.getStoreId());
+        if (storeInfo != null) {
+            commentDetail.setStoreName(storeInfo.getStoreName());
+        }
+        List<StoreUser> storeUsers = storeUserMapper.selectList(new LambdaQueryWrapper<StoreUser>()
+                .eq(StoreUser::getStoreId, commentDetail.getStoreId())
+                .eq(StoreUser::getDeleteFlag, 0));
+        String storeImage = null;
+        for (StoreUser storeUser : storeUsers) {
+            if (storeUser.getAccountType() != null && storeUser.getAccountType() == 1
+                    && StringUtils.isNotEmpty(storeUser.getHeadImg())) {
+                storeImage = storeUser.getHeadImg();
+                break;
+            }
+            if (storeImage == null && StringUtils.isNotEmpty(storeUser.getHeadImg())) {
+                storeImage = storeUser.getHeadImg();
+            }
+        }
+        commentDetail.setStoreImage(storeImage);
+    }
+
+    @Override
+    public StoreCommentAppealVo getStoreCommentAppealDetailByCommentId(Integer commentId) {
+        QueryWrapper<StoreCommentAppealVo> wrapper = new QueryWrapper<>();
+        wrapper.eq("a.comment_id", commentId)
+                .eq("a.delete_flag", 0)
+                .orderByDesc("a.created_time")
+                .last("limit 1");
+        StoreCommentAppealVo commentDetail = storeCommentAppealMapper.getCommentDetail(wrapper);
+        if (commentDetail == null) {
+            return null;
+        }
+        StoreCommentAppealVo detail = getStoreCommentAppealDetail(commentDetail.getId());
+        fillSupplementList(detail);
+        return detail;
+    }
+
+    /**
+     * 填充用户补充内容列表(按申诉ID查询,无数据时返回空列表)
+     */
+    private void fillSupplementList(StoreCommentAppealVo appealVo) {
+        if (appealVo == null || appealVo.getId() == null) {
+            return;
+        }
+        List<StoreCommentAppealSupplementVo> supplementList =
+                storeCommentAppealSupplementMapper.listByAppealId(appealVo.getId());
+        if (supplementList == null || supplementList.isEmpty()) {
+            appealVo.setSupplementList(new ArrayList<>());
+            return;
+        }
+        supplementList.forEach(this::fillSupplementImgList);
+        appealVo.setSupplementList(supplementList);
+    }
+
+    private void fillSupplementImgList(StoreCommentAppealSupplementVo vo) {
+        if (vo == null || StringUtils.isEmpty(vo.getImageUrls())) {
+            if (vo != null) {
+                vo.setImgList(new ArrayList<>());
+            }
+            return;
+        }
+        List<String> imgList = new ArrayList<>();
+        for (String item : vo.getImageUrls().split(",")) {
+            String trimmed = item.trim();
+            if (StringUtils.isNotEmpty(trimmed)) {
+                imgList.add(trimmed);
+            }
+        }
+        vo.setImgList(imgList);
+    }
+
+    /**
      * web-申诉分页查询
      * web-申诉分页查询
      *
      *
      * @param pageNum      页数
      * @param pageNum      页数
@@ -1055,6 +1144,57 @@ public class StoreCommentAppealServiceImpl extends ServiceImpl<StoreCommentAppea
     }
     }
 
 
     /**
     /**
+     * 商家发起申诉后,向评价用户发送系统通知
+     */
+    private void sendMerchantAppealNotificationToUser(StoreCommentAppeal appeal) {
+        try {
+            CommonRating rating = commonRatingMapper.selectById(appeal.getCommentId());
+            if (rating == null || rating.getUserId() == null) {
+                log.warn("商家申诉通知跳过:评价不存在或无用户,commentId={}", appeal.getCommentId());
+                return;
+            }
+            LifeUser lifeUser = lifeUserMapper.selectById(rating.getUserId().intValue());
+            if (lifeUser == null || StringUtils.isEmpty(lifeUser.getUserPhone())) {
+                log.warn("商家申诉通知跳过:用户不存在或手机号为空,userId={}", rating.getUserId());
+                return;
+            }
+
+            SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+            Date appealTime = appeal.getCreatedTime() != null ? appeal.getCreatedTime() : new Date();
+            String appealTimeStr = simpleDateFormat.format(appealTime);
+            String message = "商家于" + appealTimeStr + "发起评论申诉";
+
+            String receiverId = "user_" + lifeUser.getUserPhone();
+            JSONObject contextJson = new JSONObject();
+            contextJson.put("message", message);
+            contextJson.put("appealId", appeal.getId());
+            contextJson.put("commentId", appeal.getCommentId());
+
+            LifeNotice lifeNotice = new LifeNotice();
+            lifeNotice.setSenderId("system");
+            lifeNotice.setReceiverId(receiverId);
+            lifeNotice.setBusinessId(appeal.getId());
+            lifeNotice.setTitle("商家申诉通知");
+            lifeNotice.setContext(contextJson.toJSONString());
+            lifeNotice.setNoticeType(1);
+            lifeNotice.setIsRead(0);
+            lifeNoticeMapper.insert(lifeNotice);
+
+            WebSocketVo websocketVo = new WebSocketVo();
+            websocketVo.setSenderId("system");
+            websocketVo.setReceiverId(receiverId);
+            websocketVo.setCategory("notice");
+            websocketVo.setNoticeType("1");
+            websocketVo.setIsRead(0);
+            websocketVo.setText(JSONObject.from(lifeNotice).toJSONString());
+            webSocketProcess.sendMessage(receiverId, JSONObject.from(websocketVo).toJSONString());
+            log.info("商家申诉通知发送成功,appealId={}, receiverId={}", appeal.getId(), receiverId);
+        } catch (Exception e) {
+            log.error("发送商家申诉通知失败,appealId={}", appeal.getId(), e);
+        }
+    }
+
+    /**
      * 发送申诉结果通知给商家
      * 发送申诉结果通知给商家
      *
      *
      * @param appealId 申诉ID
      * @param appealId 申诉ID

+ 280 - 0
alien-store/src/main/java/shop/alien/store/service/impl/StoreCommentAppealSupplementServiceImpl.java

@@ -0,0 +1,280 @@
+package shop.alien.store.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.core.toolkit.StringUtils;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.google.common.collect.Lists;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.CommonRating;
+import shop.alien.entity.store.StoreCommentAppeal;
+import shop.alien.entity.store.StoreCommentAppealSupplement;
+import shop.alien.entity.store.dto.StoreCommentAppealSupplementAddDto;
+import shop.alien.entity.store.dto.StoreCommentAppealSupplementUpdateDto;
+import shop.alien.entity.store.vo.StoreCommentAppealSupplementVo;
+import shop.alien.mapper.CommonRatingMapper;
+import shop.alien.mapper.StoreCommentAppealMapper;
+import shop.alien.mapper.StoreCommentAppealSupplementMapper;
+import shop.alien.store.service.StoreCommentAppealSupplementService;
+import shop.alien.util.common.safe.TextModerationResultVO;
+import shop.alien.util.common.safe.TextModerationUtil;
+import shop.alien.util.common.safe.TextReviewServiceEnum;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * 评论申诉-用户补充内容 服务实现类
+ */
+@Slf4j
+@Service
+@Transactional
+@RequiredArgsConstructor
+public class StoreCommentAppealSupplementServiceImpl
+        extends ServiceImpl<StoreCommentAppealSupplementMapper, StoreCommentAppealSupplement>
+        implements StoreCommentAppealSupplementService {
+
+    private static final List<Integer> OPEN_APPEAL_STATUS = Arrays.asList(0, 3);
+
+    private final StoreCommentAppealSupplementMapper supplementMapper;
+    private final StoreCommentAppealMapper storeCommentAppealMapper;
+    private final CommonRatingMapper commonRatingMapper;
+
+    @Autowired
+    private TextModerationUtil textModerationUtil;
+
+    @Override
+    public R<StoreCommentAppealSupplementVo> addSupplement(StoreCommentAppealSupplementAddDto dto) {
+        StoreCommentAppealSupplementVo resultVo = new StoreCommentAppealSupplementVo();
+        try {
+            if (dto == null || dto.getAppealId() == null || dto.getUserId() == null
+                    || StringUtils.isEmpty(dto.getSupplementContent())) {
+                resultVo.setResult(1);
+                return R.data(resultVo);
+            }
+
+            StoreCommentAppeal appeal = storeCommentAppealMapper.selectById(dto.getAppealId());
+            if (appeal == null) {
+                resultVo.setResult(1);
+                return R.data(resultVo);
+            }
+            if (!OPEN_APPEAL_STATUS.contains(appeal.getAppealStatus())) {
+                resultVo.setResult(2);
+                return R.data(resultVo);
+            }
+
+            CommonRating rating = commonRatingMapper.selectById(appeal.getCommentId());
+            if (rating == null || !dto.getUserId().equals(rating.getUserId())) {
+                resultVo.setResult(4);
+                return R.data(resultVo);
+            }
+
+            if (isTextRisk(dto.getSupplementContent())) {
+                resultVo.setResult(3);
+                return R.data(resultVo);
+            }
+
+            StoreCommentAppealSupplement supplement = new StoreCommentAppealSupplement();
+            supplement.setAppealId(dto.getAppealId());
+            supplement.setCommentId(appeal.getCommentId());
+            supplement.setUserId(dto.getUserId());
+            supplement.setStoreId(appeal.getStoreId());
+            supplement.setSupplementContent(dto.getSupplementContent().trim());
+            supplement.setImageUrls(normalizeImageUrls(dto.getImageUrls()));
+            supplement.setAuditStatus(1);
+            supplement.setCreatedUserId(dto.getUserId().intValue());
+
+            if (!this.save(supplement)) {
+                resultVo.setResult(1);
+                return R.data(resultVo);
+            }
+
+            StoreCommentAppealSupplementVo vo = supplementMapper.getDetailById(supplement.getId());
+            fillImgList(vo);
+            vo.setResult(0);
+            return R.data(vo);
+        } catch (Exception e) {
+            log.error("新增申诉补充异常", e);
+            resultVo.setResult(1);
+            return R.data(resultVo);
+        }
+    }
+
+    @Override
+    public R<StoreCommentAppealSupplementVo> updateSupplement(StoreCommentAppealSupplementUpdateDto dto) {
+        try {
+            if (dto == null || dto.getId() == null || dto.getUserId() == null) {
+                return R.fail("参数不完整");
+            }
+
+            StoreCommentAppealSupplement existing = this.getById(dto.getId());
+            if (existing == null) {
+                return R.fail("补充记录不存在");
+            }
+            if (!dto.getUserId().equals(existing.getUserId())) {
+                return R.fail("无权限修改该补充");
+            }
+
+            StoreCommentAppeal appeal = storeCommentAppealMapper.selectById(existing.getAppealId());
+            if (appeal == null || !OPEN_APPEAL_STATUS.contains(appeal.getAppealStatus())) {
+                return R.fail("申诉已结案,无法修改补充");
+            }
+
+            if (StringUtils.isNotEmpty(dto.getSupplementContent())) {
+                if (isTextRisk(dto.getSupplementContent())) {
+                    return R.fail("补充内容包含敏感信息");
+                }
+                existing.setSupplementContent(dto.getSupplementContent().trim());
+            }
+            if (dto.getImageUrls() != null) {
+                existing.setImageUrls(normalizeImageUrls(dto.getImageUrls()));
+            }
+            existing.setUpdatedUserId(dto.getUserId().intValue());
+            existing.setAuditStatus(1);
+
+            if (!this.updateById(existing)) {
+                return R.fail("修改补充失败");
+            }
+
+            StoreCommentAppealSupplementVo vo = supplementMapper.getDetailById(existing.getId());
+            fillImgList(vo);
+            vo.setResult(0);
+            return R.data(vo);
+        } catch (Exception e) {
+            log.error("修改申诉补充异常, id={}", dto != null ? dto.getId() : null, e);
+            return R.fail("修改补充失败");
+        }
+    }
+
+    @Override
+    public R<Boolean> deleteSupplement(Integer id, Long userId) {
+        try {
+            if (id == null || userId == null) {
+                return R.fail("参数不完整");
+            }
+
+            StoreCommentAppealSupplement existing = this.getById(id);
+            if (existing == null) {
+                return R.fail("补充记录不存在");
+            }
+            if (!userId.equals(existing.getUserId())) {
+                return R.fail("无权限删除该补充");
+            }
+
+            StoreCommentAppeal appeal = storeCommentAppealMapper.selectById(existing.getAppealId());
+            if (appeal == null || !OPEN_APPEAL_STATUS.contains(appeal.getAppealStatus())) {
+                return R.fail("申诉已结案,无法删除补充");
+            }
+
+            if (!this.removeById(id)) {
+                return R.fail("删除补充失败");
+            }
+            return R.success("删除成功");
+        } catch (Exception e) {
+            log.error("删除申诉补充异常, id={}", id, e);
+            return R.fail("删除补充失败");
+        }
+    }
+
+    @Override
+    public StoreCommentAppealSupplementVo getDetail(Integer id) {
+        StoreCommentAppealSupplementVo vo = supplementMapper.getDetailById(id);
+        fillImgList(vo);
+        return vo;
+    }
+
+    @Override
+    public List<StoreCommentAppealSupplementVo> listByAppealId(Integer appealId) {
+        List<StoreCommentAppealSupplementVo> list = supplementMapper.listByAppealId(appealId);
+        list.forEach(this::fillImgList);
+        return list;
+    }
+
+    @Override
+    public List<StoreCommentAppealSupplementVo> listByCommentId(Integer commentId) {
+        List<StoreCommentAppealSupplementVo> list = supplementMapper.listByCommentId(commentId);
+        list.forEach(this::fillImgList);
+        return list;
+    }
+
+    @Override
+    public IPage<StoreCommentAppealSupplementVo> getPage(Integer pageNum, Integer pageSize,
+                                                         Integer appealId, Long userId,
+                                                         Integer storeId, Integer auditStatus) {
+        LambdaQueryWrapper<StoreCommentAppealSupplement> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(appealId != null, StoreCommentAppealSupplement::getAppealId, appealId)
+                .eq(userId != null, StoreCommentAppealSupplement::getUserId, userId)
+                .eq(storeId != null, StoreCommentAppealSupplement::getStoreId, storeId)
+                .eq(auditStatus != null, StoreCommentAppealSupplement::getAuditStatus, auditStatus)
+                .orderByDesc(StoreCommentAppealSupplement::getCreatedTime);
+
+        IPage<StoreCommentAppealSupplement> page = this.page(new Page<>(pageNum, pageSize), wrapper);
+
+        Page<StoreCommentAppealSupplementVo> voPage = new Page<>(page.getCurrent(), page.getSize(), page.getTotal());
+        List<StoreCommentAppealSupplementVo> records = new ArrayList<>();
+        for (StoreCommentAppealSupplement item : page.getRecords()) {
+            StoreCommentAppealSupplementVo vo = supplementMapper.getDetailById(item.getId());
+            if (vo == null) {
+                vo = new StoreCommentAppealSupplementVo();
+                BeanUtils.copyProperties(item, vo);
+            }
+            fillImgList(vo);
+            records.add(vo);
+        }
+        voPage.setRecords(records);
+        return voPage;
+    }
+
+    private boolean isTextRisk(String content) {
+        try {
+            List<String> servicesList = Lists.newArrayList();
+            servicesList.add(TextReviewServiceEnum.COMMENT_DETECTION_PRO.getService());
+            servicesList.add(TextReviewServiceEnum.LLM_QUERY_MODERATION.getService());
+            TextModerationResultVO textCheckResult = textModerationUtil.invokeFunction(content, servicesList);
+            return "high".equals(textCheckResult.getRiskLevel());
+        } catch (Exception e) {
+            log.error("补充内容文本审核异常", e);
+            return false;
+        }
+    }
+
+    private String normalizeImageUrls(String imageUrls) {
+        if (StringUtils.isEmpty(imageUrls)) {
+            return null;
+        }
+        return Arrays.stream(imageUrls.split(","))
+                .map(String::trim)
+                .filter(StringUtils::isNotEmpty)
+                .collect(Collectors.joining(","));
+    }
+
+    private void fillImgList(StoreCommentAppealSupplementVo vo) {
+        if (vo == null || StringUtils.isEmpty(vo.getImageUrls())) {
+            if (vo != null) {
+                vo.setImgList(new ArrayList<>());
+            }
+            return;
+        }
+        vo.setImgList(splitImageUrls(vo.getImageUrls()));
+    }
+
+    private List<String> splitImageUrls(String imageUrls) {
+        List<String> imgList = new ArrayList<>();
+        for (String item : imageUrls.split(",")) {
+            String trimmed = item.trim();
+            if (StringUtils.isNotEmpty(trimmed)) {
+                imgList.add(trimmed);
+            }
+        }
+        return imgList;
+    }
+}