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

fix(backend): user delete soft-delete + ActivityProvision entity + DAN execution service

Xiaogang Liao 3 месяцев назад
Родитель
Сommit
98052ee0c7
23 измененных файлов с 786 добавлено и 2 удалено
  1. 8 0
      cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java
  2. 2 2
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminController.java
  3. 37 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/product/ProductOrderController.java
  4. 10 0
      cfc-backend/src/main/java/com/etotem/cfc/dto/ExecutionCompleteRequest.java
  5. 9 0
      cfc-backend/src/main/java/com/etotem/cfc/dto/ExecutionConfirmRequest.java
  6. 34 0
      cfc-backend/src/main/java/com/etotem/cfc/dto/ExecutionDetailResponse.java
  7. 15 0
      cfc-backend/src/main/java/com/etotem/cfc/dto/ServiceRateRequest.java
  8. 12 0
      cfc-backend/src/main/java/com/etotem/cfc/dto/SettlementCreateRequest.java
  9. 36 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/ActivityProvision.java
  10. 39 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/DanAssessmentExecution.java
  11. 6 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/ProductOrder.java
  12. 31 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/ServiceRating.java
  13. 33 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/ServiceSettlement.java
  14. 6 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/User.java
  15. 9 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/ActivityProvisionMapper.java
  16. 9 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/DanAssessmentExecutionMapper.java
  17. 9 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/ServiceRatingMapper.java
  18. 9 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/ServiceSettlementMapper.java
  19. 155 0
      cfc-backend/src/main/java/com/etotem/cfc/service/ActivityProvisionService.java
  20. 167 0
      cfc-backend/src/main/java/com/etotem/cfc/service/DanAssessmentExecutionService.java
  21. 136 0
      cfc-backend/src/main/java/com/etotem/cfc/service/DanSettlementService.java
  22. 8 0
      cfc-backend/src/main/java/com/etotem/cfc/service/UserService.java
  23. 6 0
      cfc-backend/src/main/resources/schema.sql

+ 8 - 0
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -5981,6 +5981,14 @@ try {
         } catch (Exception e) {
             log.warn("DAN佣金配置种子数据可能已存在: " + e.getMessage());
         }
+
+        // 迁移 98: users 表添加软删除标记(@TableLogic)
+        try {
+            jdbcTemplate.execute("ALTER TABLE users ADD COLUMN deleted TINYINT DEFAULT 0 COMMENT '软删除标记: 0=未删除, 1=已删除'");
+            log.info("已添加deleted列到users表");
+        } catch (Exception e) {
+            // 列已存在,忽略错误
+        }
     }
 
     private void runMigration93() {

+ 2 - 2
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminController.java

@@ -175,8 +175,8 @@ public class AdminController {
 
     @PostMapping("/users/{id}/delete")
     public Result<Boolean> deleteUser(@PathVariable Long id) {
-        userMapper.deleteById(id);
-        return Result.success(true);
+        boolean ok = userService.deleteUser(id);
+        return ok ? Result.success(true) : Result.error("用户不存在或删除失败");
     }
 
     // 孩子管理

+ 37 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/product/ProductOrderController.java

@@ -5,6 +5,8 @@ import com.alibaba.fastjson.JSONObject;
 import com.etotem.cfc.common.Result;
 import com.etotem.cfc.dto.CreateProductOrderDTO;
 import com.etotem.cfc.dto.ProductOrderDTO;
+import com.etotem.cfc.entity.ActivityProvision;
+import com.etotem.cfc.service.ActivityProvisionService;
 import com.etotem.cfc.service.PaymentService;
 import com.etotem.cfc.service.ProductOrderService;
 import org.springframework.web.bind.annotation.PostMapping;
@@ -30,6 +32,9 @@ public class ProductOrderController {
     @Resource
     private PaymentService paymentService;
 
+    @Resource
+    private ActivityProvisionService activityProvisionService;
+
     @PostMapping("/create")
     public Result<ProductOrderDTO> create(@RequestBody CreateProductOrderDTO dto,
                                          @RequestAttribute("userId") Long userId) {
@@ -144,4 +149,36 @@ public class ProductOrderController {
             return err;
         }
     }
+
+    @PostMapping("/activity/audit")
+    public Result<String> activityAudit(@RequestBody Map<String, Object> params,
+                                        @RequestAttribute("userId") Long userId) {
+        Long orderId = Long.valueOf(params.get("orderId").toString());
+        String action = (String) params.get("action");
+        if ("approve".equals(action)) {
+            return activityProvisionService.provision(orderId, userId);
+        } else {
+            String reason = (String) params.get("reason");
+            return activityProvisionService.cancel(orderId, userId, reason);
+        }
+    }
+
+    @PostMapping("/activity/sign-in")
+    public Result<String> activitySignIn(@RequestBody Map<String, Object> params) {
+        Long orderId = Long.valueOf(params.get("orderId").toString());
+        Long childId = Long.valueOf(params.get("childId").toString());
+        return activityProvisionService.signIn(orderId, childId);
+    }
+
+    @PostMapping("/activity/complete")
+    public Result<String> activityComplete(@RequestBody Map<String, Object> params) {
+        Long orderId = Long.valueOf(params.get("orderId").toString());
+        return activityProvisionService.complete(orderId);
+    }
+
+    @PostMapping("/activity/provision")
+    public Result<ActivityProvision> activityProvision(@RequestBody Map<String, Object> params) {
+        Long orderId = Long.valueOf(params.get("orderId").toString());
+        return activityProvisionService.getByOrderId(orderId);
+    }
 }

+ 10 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/ExecutionCompleteRequest.java

@@ -0,0 +1,10 @@
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+
+@Data
+public class ExecutionCompleteRequest {
+    private Long executionId;
+    private String role; // assessor / planner
+    private String notes;
+}

+ 9 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/ExecutionConfirmRequest.java

@@ -0,0 +1,9 @@
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+
+@Data
+public class ExecutionConfirmRequest {
+    private Long executionId;
+    private Integer confirmed; // 1=confirm, 0=reject
+}

+ 34 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/ExecutionDetailResponse.java

@@ -0,0 +1,34 @@
+package com.etotem.cfc.dto;
+
+import com.etotem.cfc.entity.ServiceSettlement;
+import lombok.Data;
+
+import java.util.Date;
+
+@Data
+public class ExecutionDetailResponse {
+    private Long executionId;
+    private Long assessmentOrderId;
+    private Long childId;
+    private Long familyId;
+    private Long quotaId;
+
+    private Long assessorId;
+    private String assessorStatus;
+    private Date assessorCompletedAt;
+    private String assessorNotes;
+
+    private Long plannerId;
+    private String plannerStatus;
+    private Date plannerCompletedAt;
+    private String plannerNotes;
+
+    private Integer userConfirmed;
+    private Date userConfirmedAt;
+
+    private Date createdAt;
+    private Date updatedAt;
+
+    private ServiceSettlement assessorSettlement;
+    private ServiceSettlement plannerSettlement;
+}

+ 15 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/ServiceRateRequest.java

@@ -0,0 +1,15 @@
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+
+@Data
+public class ServiceRateRequest {
+    private Long executionId;
+    private Long userId;
+    private Long assessorId;
+    private Integer assessorRating;
+    private String assessorComment;
+    private Long plannerId;
+    private Integer plannerRating;
+    private String plannerComment;
+}

+ 12 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/SettlementCreateRequest.java

@@ -0,0 +1,12 @@
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+
+@Data
+public class SettlementCreateRequest {
+    private Long executionId;
+    private Long assessmentOrderId;
+    private Long providerId;
+    private String providerType; // assessor / planner
+    private Long serviceFee;
+}

+ 36 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/ActivityProvision.java

@@ -0,0 +1,36 @@
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+@Data
+@TableName("activity_provisions")
+public class ActivityProvision implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long orderId;
+    private String orderNo;
+    private Long productId;
+    private Long activityId;
+    private Long buyerId;
+    private Long familyId;
+    private Long childId;
+
+    private String status;
+    private String qrCode;
+    private Date checkedInAt;
+    private Date completedAt;
+
+    private Integer refundAmount;
+    private Date refundAt;
+
+    private Date createdAt;
+    private Date updatedAt;
+}

+ 39 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/DanAssessmentExecution.java

@@ -0,0 +1,39 @@
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+@Data
+@TableName("dan_assessment_executions")
+public class DanAssessmentExecution implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long assessmentExecutionId;
+    private Long assessmentOrderId;
+    private Long childId;
+    private Long familyId;
+    private Long quotaId;
+
+    private Long assessorId;
+    private String assessorStatus;
+    private Date assessorCompletedAt;
+    private String assessorNotes;
+
+    private Long plannerId;
+    private String plannerStatus;
+    private Date plannerCompletedAt;
+    private String plannerNotes;
+
+    private Integer userConfirmed;
+    private Date userConfirmedAt;
+
+    private Date createdAt;
+    private Date updatedAt;
+}

+ 6 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/ProductOrder.java

@@ -58,6 +58,12 @@ public class ProductOrder implements Serializable {
     private Long consigneeId;          // 收货人ID
     private String purchaseInfo;       // 购买信息快照(JSON)
 
+    private Long activityId;
+    private String provisionStatus;
+    private Long auditorId;
+    private Date auditedAt;
+    private String auditReason;
+
     private Date createdAt;
     private Date updatedAt;
 

+ 31 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/ServiceRating.java

@@ -0,0 +1,31 @@
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+@Data
+@TableName("service_ratings")
+public class ServiceRating implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long executionId;
+    private Long assessmentOrderId;
+    private Long userId;
+
+    private Long assessorId;
+    private Integer assessorRating;
+    private String assessorComment;
+
+    private Long plannerId;
+    private Integer plannerRating;
+    private String plannerComment;
+
+    private Date createdAt;
+}

+ 33 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/ServiceSettlement.java

@@ -0,0 +1,33 @@
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+@Data
+@TableName("service_settlements")
+public class ServiceSettlement implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long executionId;
+    private Long assessmentOrderId;
+    private Long providerId;
+    private String providerType;
+
+    private Long serviceFee;
+    private Integer commissionRate;
+    private Long commissionAmount;
+
+    private String status;
+    private Date settledAt;
+    private String transactionId;
+
+    private Date createdAt;
+    private Date updatedAt;
+}

+ 6 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/User.java

@@ -1,7 +1,9 @@
 package com.etotem.cfc.entity;
 
 import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableLogic;
 import com.baomidou.mybatisplus.annotation.TableName;
 import lombok.Data;
 
@@ -147,4 +149,8 @@ public class User implements Serializable {
     private Date createdAt;
 
     private Date updatedAt;
+
+    @TableLogic
+    @TableField("deleted")
+    private Integer deleted;
 }

+ 9 - 0
cfc-backend/src/main/java/com/etotem/cfc/mapper/ActivityProvisionMapper.java

@@ -0,0 +1,9 @@
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.ActivityProvision;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface ActivityProvisionMapper extends BaseMapper<ActivityProvision> {
+}

+ 9 - 0
cfc-backend/src/main/java/com/etotem/cfc/mapper/DanAssessmentExecutionMapper.java

@@ -0,0 +1,9 @@
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.DanAssessmentExecution;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface DanAssessmentExecutionMapper extends BaseMapper<DanAssessmentExecution> {
+}

+ 9 - 0
cfc-backend/src/main/java/com/etotem/cfc/mapper/ServiceRatingMapper.java

@@ -0,0 +1,9 @@
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.ServiceRating;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface ServiceRatingMapper extends BaseMapper<ServiceRating> {
+}

+ 9 - 0
cfc-backend/src/main/java/com/etotem/cfc/mapper/ServiceSettlementMapper.java

@@ -0,0 +1,9 @@
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.ServiceSettlement;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface ServiceSettlementMapper extends BaseMapper<ServiceSettlement> {
+}

+ 155 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/ActivityProvisionService.java

@@ -0,0 +1,155 @@
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.ActivityProvision;
+import com.etotem.cfc.entity.ProductOrder;
+import com.etotem.cfc.mapper.ActivityProvisionMapper;
+import com.etotem.cfc.mapper.ProductOrderMapper;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.UUID;
+
+@Service
+public class ActivityProvisionService {
+
+    @Resource
+    private ActivityProvisionMapper activityProvisionMapper;
+
+    @Resource
+    private ProductOrderMapper productOrderMapper;
+
+    @Resource
+    private PaymentService paymentService;
+
+    public Result<String> provision(Long orderId, Long auditorId) {
+        ProductOrder order = productOrderMapper.selectById(orderId);
+        if (order == null) {
+            return Result.error("订单不存在");
+        }
+        if (!"unprovisioned".equals(order.getProvisionStatus())) {
+            return Result.error("当前状态不允许审核");
+        }
+        ActivityProvision provision = new ActivityProvision();
+        provision.setOrderId(orderId);
+        provision.setOrderNo(order.getOrderNo());
+        provision.setProductId(order.getProductId());
+        provision.setActivityId(order.getActivityId() != null ? order.getActivityId() : order.getProductId());
+        provision.setBuyerId(order.getBuyerId());
+        provision.setFamilyId(order.getFamilyId());
+        provision.setStatus("provisioned");
+        provision.setQrCode(generateQrCode(order.getOrderNo()));
+        provision.setCreatedAt(new Date());
+        provision.setUpdatedAt(new Date());
+        activityProvisionMapper.insert(provision);
+
+        order.setProvisionStatus("provisioned");
+        order.setAuditorId(auditorId);
+        order.setAuditedAt(new Date());
+        productOrderMapper.updateById(order);
+        return Result.success("审核通过,已生成签到码");
+    }
+
+    @Transactional
+    public Result<String> cancel(Long orderId, Long auditorId, String reason) {
+        ProductOrder order = productOrderMapper.selectById(orderId);
+        if (order == null) {
+            return Result.error("订单不存在");
+        }
+        if (!"unprovisioned".equals(order.getProvisionStatus())) {
+            return Result.error("当前状态不允许取消");
+        }
+        ActivityProvision provision = new ActivityProvision();
+        provision.setOrderId(orderId);
+        provision.setOrderNo(order.getOrderNo());
+        provision.setProductId(order.getProductId());
+        provision.setActivityId(order.getActivityId() != null ? order.getActivityId() : order.getProductId());
+        provision.setBuyerId(order.getBuyerId());
+        provision.setFamilyId(order.getFamilyId());
+        provision.setStatus("cancelled");
+        provision.setRefundAmount(order.getTotalAmount());
+        provision.setRefundAt(new Date());
+        provision.setCreatedAt(new Date());
+        provision.setUpdatedAt(new Date());
+        activityProvisionMapper.insert(provision);
+
+        order.setProvisionStatus("cancelled");
+        order.setStatus("cancelled");
+        order.setAuditorId(auditorId);
+        order.setAuditedAt(new Date());
+        order.setAuditReason(reason);
+        productOrderMapper.updateById(order);
+
+        paymentService.refund(order.getOrderNo(), order.getTotalAmount(), "活动审核不通过:" + reason);
+        return Result.success("已拒绝并退款");
+    }
+
+    @Transactional
+    public Result<String> signIn(Long orderId, Long childId) {
+        ActivityProvision provision = activityProvisionMapper.selectOne(
+            new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<ActivityProvision>()
+                .eq(ActivityProvision::getOrderId, orderId)
+        );
+        if (provision == null) {
+            return Result.error("未找到活动 provision 记录");
+        }
+        if (!"provisioned".equals(provision.getStatus())) {
+            return Result.error("当前状态不允许签到");
+        }
+        provision.setChildId(childId);
+        provision.setCheckedInAt(new Date());
+        provision.setStatus("signed_in");
+        provision.setUpdatedAt(new Date());
+        activityProvisionMapper.updateById(provision);
+
+        ProductOrder order = productOrderMapper.selectById(orderId);
+        if (order != null) {
+            order.setProvisionStatus("signed_in");
+            productOrderMapper.updateById(order);
+        }
+        return Result.success("签到成功");
+    }
+
+    @Transactional
+    public Result<String> complete(Long orderId) {
+        ActivityProvision provision = activityProvisionMapper.selectOne(
+            new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<ActivityProvision>()
+                .eq(ActivityProvision::getOrderId, orderId)
+        );
+        if (provision == null) {
+            return Result.error("未找到活动 provision 记录");
+        }
+        if (!"signed_in".equals(provision.getStatus())) {
+            return Result.error("当前状态不允许完成");
+        }
+        provision.setStatus("completed");
+        provision.setCompletedAt(new Date());
+        provision.setUpdatedAt(new Date());
+        activityProvisionMapper.updateById(provision);
+
+        ProductOrder order = productOrderMapper.selectById(orderId);
+        if (order != null) {
+            order.setProvisionStatus("completed");
+            productOrderMapper.updateById(order);
+        }
+        return Result.success("活动完成");
+    }
+
+    public Result<ActivityProvision> getByOrderId(Long orderId) {
+        ActivityProvision provision = activityProvisionMapper.selectOne(
+            new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<ActivityProvision>()
+                .eq(ActivityProvision::getOrderId, orderId)
+        );
+        if (provision == null) {
+            return Result.success(null);
+        }
+        return Result.success(provision);
+    }
+
+    private String generateQrCode(String orderNo) {
+        return "ACT-" + orderNo + "-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase();
+    }
+}

+ 167 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/DanAssessmentExecutionService.java

@@ -0,0 +1,167 @@
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.dto.*;
+import com.etotem.cfc.entity.*;
+import com.etotem.cfc.mapper.DanAssessmentExecutionMapper;
+import com.etotem.cfc.mapper.ServiceRatingMapper;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.List;
+
+@Service
+public class DanAssessmentExecutionService {
+
+    @Resource
+    private DanAssessmentExecutionMapper executionMapper;
+
+    @Resource
+    private ServiceRatingMapper ratingMapper;
+
+    @Resource
+    private DanSettlementService settlementService;
+
+    /**
+     * 创建评估执行记录
+     */
+    @Transactional
+    public DanAssessmentExecution createExecution(Long assessmentOrderId, Long childId, Long familyId,
+            Long quotaId, Long assessorId, Long plannerId) {
+        DanAssessmentExecution exec = new DanAssessmentExecution();
+        exec.setAssessmentOrderId(assessmentOrderId);
+        exec.setChildId(childId);
+        exec.setFamilyId(familyId);
+        exec.setQuotaId(quotaId);
+        exec.setAssessorId(assessorId);
+        exec.setAssessorStatus("pending");
+        exec.setPlannerId(plannerId);
+        exec.setPlannerStatus("pending");
+        exec.setUserConfirmed(0);
+        exec.setCreatedAt(new Date());
+        exec.setUpdatedAt(new Date());
+        executionMapper.insert(exec);
+        return exec;
+    }
+
+    /**
+     * 规划师/评估师完成服务
+     */
+    @Transactional
+    public void completeExecution(Long executionId, String role, String notes) {
+        DanAssessmentExecution exec = executionMapper.selectById(executionId);
+        if (exec == null) {
+            throw new RuntimeException("Execution not found: " + executionId);
+        }
+        Date now = new Date();
+        if ("assessor".equals(role)) {
+            exec.setAssessorStatus("completed");
+            exec.setAssessorCompletedAt(now);
+            exec.setAssessorNotes(notes);
+        } else if ("planner".equals(role)) {
+            exec.setPlannerStatus("completed");
+            exec.setPlannerCompletedAt(now);
+            exec.setPlannerNotes(notes);
+        }
+        exec.setUpdatedAt(now);
+        executionMapper.updateById(exec);
+    }
+
+    /**
+     * 用户确认服务
+     */
+    @Transactional
+    public void confirmExecution(Long executionId, Integer confirmed) {
+        DanAssessmentExecution exec = executionMapper.selectById(executionId);
+        if (exec == null) {
+            throw new RuntimeException("Execution not found: " + executionId);
+        }
+        exec.setUserConfirmed(confirmed);
+        exec.setUserConfirmedAt(new Date());
+        exec.setUpdatedAt(new Date());
+        executionMapper.updateById(exec);
+
+        // 用户确认后自动生成结算记录
+        if (confirmed == 1) {
+            settlementService.autoSettle(executionId);
+        }
+    }
+
+    /**
+     * 用户评价服务
+     */
+    @Transactional
+    public ServiceRating rateService(ServiceRateRequest req) {
+        ServiceRating rating = new ServiceRating();
+        rating.setExecutionId(req.getExecutionId());
+        rating.setUserId(req.getUserId());
+        rating.setAssessorId(req.getAssessorId());
+        rating.setAssessorRating(req.getAssessorRating());
+        rating.setAssessorComment(req.getAssessorComment());
+        rating.setPlannerId(req.getPlannerId());
+        rating.setPlannerRating(req.getPlannerRating());
+        rating.setPlannerComment(req.getPlannerComment());
+        rating.setCreatedAt(new Date());
+        ratingMapper.insert(rating);
+        return rating;
+    }
+
+    /**
+     * 获取执行详情(含结算信息)
+     */
+    public ExecutionDetailResponse getExecutionDetail(Long executionId) {
+        DanAssessmentExecution exec = executionMapper.selectById(executionId);
+        if (exec == null) {
+            return null;
+        }
+        ExecutionDetailResponse resp = new ExecutionDetailResponse();
+        resp.setExecutionId(exec.getId());
+        resp.setAssessmentOrderId(exec.getAssessmentOrderId());
+        resp.setChildId(exec.getChildId());
+        resp.setFamilyId(exec.getFamilyId());
+        resp.setQuotaId(exec.getQuotaId());
+        resp.setAssessorId(exec.getAssessorId());
+        resp.setAssessorStatus(exec.getAssessorStatus());
+        resp.setAssessorCompletedAt(exec.getAssessorCompletedAt());
+        resp.setAssessorNotes(exec.getAssessorNotes());
+        resp.setPlannerId(exec.getPlannerId());
+        resp.setPlannerStatus(exec.getPlannerStatus());
+        resp.setPlannerCompletedAt(exec.getPlannerCompletedAt());
+        resp.setPlannerNotes(exec.getPlannerNotes());
+        resp.setUserConfirmed(exec.getUserConfirmed());
+        resp.setUserConfirmedAt(exec.getUserConfirmedAt());
+        resp.setCreatedAt(exec.getCreatedAt());
+        resp.setUpdatedAt(exec.getUpdatedAt());
+        // 查询结算信息
+        resp.setAssessorSettlement(settlementService.getSettlement(executionId, exec.getAssessorId(), "assessor"));
+        resp.setPlannerSettlement(settlementService.getSettlement(executionId, exec.getPlannerId(), "planner"));
+        return resp;
+    }
+
+    /**
+     * 根据订单ID查询执行记录
+     */
+    public DanAssessmentExecution getByOrderId(Long orderId) {
+        return executionMapper.selectOne(new LambdaQueryWrapper<DanAssessmentExecution>()
+                .eq(DanAssessmentExecution::getAssessmentOrderId, orderId));
+    }
+
+    /**
+     * 根据评估预约ID查询执行记录
+     */
+    public DanAssessmentExecution getByAppointmentId(Long appointmentId) {
+        return executionMapper.selectOne(new LambdaQueryWrapper<DanAssessmentExecution>()
+                .eq(DanAssessmentExecution::getQuotaId, appointmentId));
+    }
+
+    /**
+     * 查询用户的执行记录列表
+     */
+    public List<DanAssessmentExecution> getByFamilyId(Long familyId) {
+        return executionMapper.selectList(new LambdaQueryWrapper<DanAssessmentExecution>()
+                .eq(DanAssessmentExecution::getFamilyId, familyId)
+                .orderByDesc(DanAssessmentExecution::getCreatedAt));
+    }
+}

+ 136 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/DanSettlementService.java

@@ -0,0 +1,136 @@
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.entity.AssessmentOrder;
+import com.etotem.cfc.entity.ServiceSettlement;
+import com.etotem.cfc.entity.SystemConfig;
+import com.etotem.cfc.mapper.AssessmentOrderMapper;
+import com.etotem.cfc.mapper.ServiceSettlementMapper;
+import com.etotem.cfc.mapper.SystemConfigMapper;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.util.Date;
+
+@Service
+public class DanSettlementService {
+
+    private static final int DEFAULT_COMMISSION_RATE = 10; // 默认10%
+
+    @Resource
+    private ServiceSettlementMapper settlementMapper;
+
+    @Resource
+    private AssessmentOrderMapper orderMapper;
+
+    @Resource
+    private SystemConfigMapper systemConfigMapper;
+
+    @Resource
+    private com.etotem.cfc.mapper.DanAssessmentExecutionMapper executionMapper;
+
+    private int getCommissionRate(String providerType) {
+        String key = "dan_commission_rate_" + providerType;
+        SystemConfig cfg = systemConfigMapper.selectOne(
+                new LambdaQueryWrapper<SystemConfig>()
+                        .eq(SystemConfig::getConfigKey, key));
+        if (cfg != null && cfg.getConfigValue() != null) {
+            try {
+                return Integer.parseInt(cfg.getConfigValue());
+            } catch (NumberFormatException ignored) {
+            }
+        }
+        // 回退到通用费率
+        SystemConfig general = systemConfigMapper.selectOne(
+                new LambdaQueryWrapper<SystemConfig>()
+                        .eq(SystemConfig::getConfigKey, "dan_commission_rate"));
+        if (general != null && general.getConfigValue() != null) {
+            try {
+                return Integer.parseInt(general.getConfigValue());
+            } catch (NumberFormatException ignored) {
+            }
+        }
+        return DEFAULT_COMMISSION_RATE;
+    }
+
+    /**
+     * 自动生成结算记录(用户确认后调用)
+     */
+    @Transactional
+    public void autoSettle(Long executionId) {
+        // 根据 executionId 查询执行记录中的 assessorId/plannerId,再查订单价格
+        // 为简化:assessor 和 planner 各分配 50% 服务费,平台扣 commission
+        ServiceSettlement assessorSettlement = createSettlement(executionId, "assessor");
+        ServiceSettlement plannerSettlement = createSettlement(executionId, "planner");
+        if (assessorSettlement != null) {
+            settlementMapper.insert(assessorSettlement);
+        }
+        if (plannerSettlement != null) {
+            settlementMapper.insert(plannerSettlement);
+        }
+    }
+
+    private ServiceSettlement createSettlement(Long executionId, String providerType) {
+        com.etotem.cfc.entity.DanAssessmentExecution exec = executionMapper.selectById(executionId);
+        if (exec == null || exec.getAssessmentOrderId() == null) {
+            return null;
+        }
+        AssessmentOrder order = orderMapper.selectById(exec.getAssessmentOrderId());
+        if (order == null || order.getActualPrice() == null || order.getActualPrice() <= 0) {
+            return null;
+        }
+
+        Long providerId = "assessor".equals(providerType) ? exec.getAssessorId() : exec.getPlannerId();
+        if (providerId == null) {
+            return null;
+        }
+
+        // 50% 服务费给对应角色
+        Long serviceFee = order.getActualPrice() / 2;
+        int commissionRate = getCommissionRate(providerType);
+        Long commissionAmount = serviceFee * commissionRate / 100;
+
+        ServiceSettlement settlement = new ServiceSettlement();
+        settlement.setExecutionId(executionId);
+        settlement.setAssessmentOrderId(exec.getAssessmentOrderId());
+        settlement.setProviderId(providerId);
+        settlement.setProviderType(providerType);
+        settlement.setServiceFee(serviceFee);
+        settlement.setCommissionRate(commissionRate);
+        settlement.setCommissionAmount(commissionAmount);
+        settlement.setStatus("pending");
+        settlement.setCreatedAt(new Date());
+        settlement.setUpdatedAt(new Date());
+        return settlement;
+    }
+
+    /**
+     * 查询某执行记录某角色的结算信息
+     */
+    public ServiceSettlement getSettlement(Long executionId, Long providerId, String providerType) {
+        return settlementMapper.selectOne(new LambdaQueryWrapper<ServiceSettlement>()
+                .eq(ServiceSettlement::getExecutionId, executionId)
+                .eq(ServiceSettlement::getProviderId, providerId)
+                .eq(ServiceSettlement::getProviderType, providerType));
+    }
+
+    /**
+     * 更新结算状态(确认/打款)
+     */
+    @Transactional
+    public void updateSettlementStatus(Long settlementId, String status, String transactionId) {
+        ServiceSettlement s = settlementMapper.selectById(settlementId);
+        if (s != null) {
+            s.setStatus(status);
+            if (transactionId != null) {
+                s.setTransactionId(transactionId);
+            }
+            if ("settled".equals(status)) {
+                s.setSettledAt(new Date());
+            }
+            s.setUpdatedAt(new Date());
+            settlementMapper.updateById(s);
+        }
+    }
+}

+ 8 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/UserService.java

@@ -1481,4 +1481,12 @@ private FamilyInvitationService familyInvitationService;
         return ids;
     }
 
+    public boolean deleteUser(Long userId) {
+        User user = userMapper.selectById(userId);
+        if (user == null) {
+            return false;
+        }
+        return userMapper.deleteById(userId) > 0;
+    }
+
 }

+ 6 - 0
cfc-backend/src/main/resources/schema.sql

@@ -31,6 +31,7 @@ CREATE TABLE IF NOT EXISTS users (
     address VARCHAR(500) COMMENT '地址',
     created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
     updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    deleted TINYINT DEFAULT 0 COMMENT '软删除标记: 0=未删除, 1=已删除',
     butler_status VARCHAR(20) COMMENT '管家状态: null/pending/approved/rejected',
     butler_info TEXT COMMENT '管家资质信息(JSON)',
     butler_approved_at DATETIME COMMENT '管家认证通过时间',
@@ -1090,6 +1091,7 @@ CREATE TABLE IF NOT EXISTS product_orders (
     product_id BIGINT NOT NULL COMMENT '商品ID',
     product_name VARCHAR(200) COMMENT '商品名称',
     product_type VARCHAR(32) COMMENT '商品类型',
+    activity_id BIGINT COMMENT '关联活动ID(product_type=activity时有效)',
     buyer_id BIGINT NOT NULL COMMENT '购买者ID',
     family_id BIGINT COMMENT '家庭ID',
     quantity INT NOT NULL DEFAULT 1 COMMENT '数量',
@@ -1107,6 +1109,10 @@ CREATE TABLE IF NOT EXISTS product_orders (
     money_amount INT DEFAULT 0 COMMENT '现金支付金额(分)',
     consignee_id BIGINT COMMENT '收货人ID',
     purchase_info TEXT COMMENT '购买信息JSON: {id_card, hand_signature, ...}',
+    provision_status VARCHAR(32) COMMENT '活动provision状态: unprovisioned(待审核)/provisioned(已发货)/signed_in(已签到)/completed(已完成)/cancelled(审核拒绝)',
+    auditor_id BIGINT COMMENT '审核人ID',
+    audited_at DATETIME COMMENT '审核时间',
+    audit_reason TEXT COMMENT '审核拒绝原因',
     created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
     updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
     INDEX idx_user_id (user_id),