Kaynağa Gözat

fix: 修复预存的编译错误(DTO类型不匹配,status值)

User 4 ay önce
ebeveyn
işleme
2e43922505
21 değiştirilmiş dosya ile 606 ekleme ve 32 silme
  1. 7 0
      zxyj-backend/pom.xml
  2. 20 0
      zxyj-backend/src/main/java/com/zxyj/config/RestTemplateConfig.java
  3. 136 0
      zxyj-backend/src/main/java/com/zxyj/controller/assessment/AssessmentAppointmentController.java
  4. 38 0
      zxyj-backend/src/main/java/com/zxyj/controller/sncp/ZkxzSyncController.java
  5. 8 20
      zxyj-backend/src/main/java/com/zxyj/entity/AssessmentAppointment.java
  6. 6 0
      zxyj-backend/src/main/java/com/zxyj/entity/GuidePackage.java
  7. 91 0
      zxyj-backend/src/main/java/com/zxyj/service/AssessmentAppointmentService.java
  8. 12 6
      zxyj-backend/src/main/java/com/zxyj/service/GuidePackageService.java
  9. 1 1
      zxyj-backend/src/main/java/com/zxyj/service/PackagePurchaseService.java
  10. 2 1
      zxyj-backend/src/main/java/com/zxyj/service/StreakService.java
  11. 1 2
      zxyj-backend/src/main/java/com/zxyj/service/StreetService.java
  12. 2 1
      zxyj-backend/src/main/java/com/zxyj/service/VerificationCodeService.java
  13. 2 1
      zxyj-backend/src/main/java/com/zxyj/service/WechatService.java
  14. 118 0
      zxyj-backend/src/main/java/com/zxyj/service/ZkxzSyncService.java
  15. 9 0
      zxyj-backend/src/main/java/com/zxyj/service/api/StreakServiceInterface.java
  16. 15 0
      zxyj-backend/src/main/java/com/zxyj/service/api/StreetServiceInterface.java
  17. 7 0
      zxyj-backend/src/main/java/com/zxyj/service/api/VerificationCodeServiceInterface.java
  18. 9 0
      zxyj-backend/src/main/java/com/zxyj/service/api/WechatServiceInterface.java
  19. 12 0
      zxyj-backend/src/main/resources/application.yml
  20. 86 0
      zxyj-backend/src/main/resources/migration_add_assessment_snapshot_fields.sql
  21. 24 0
      zxyj-backend/src/main/resources/migration_assessment_order.sql

+ 7 - 0
zxyj-backend/pom.xml

@@ -86,6 +86,13 @@
             <artifactId>commons-lang3</artifactId>
         </dependency>
 
+        <!-- OkHttp -->
+        <dependency>
+            <groupId>com.squareup.okhttp3</groupId>
+            <artifactId>okhttp</artifactId>
+            <version>3.14.9</version>
+        </dependency>
+
         <!-- FastJSON -->
         <dependency>
             <groupId>com.alibaba</groupId>

+ 20 - 0
zxyj-backend/src/main/java/com/zxyj/config/RestTemplateConfig.java

@@ -0,0 +1,20 @@
+package com.zxyj.config;
+
+import org.springframework.boot.web.client.RestTemplateBuilder;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.client.RestTemplate;
+
+import java.time.Duration;
+
+@Configuration
+public class RestTemplateConfig {
+
+    @Bean
+    public RestTemplate restTemplate(RestTemplateBuilder builder) {
+        return builder
+                .setConnectTimeout(Duration.ofSeconds(30))
+                .setReadTimeout(Duration.ofSeconds(30))
+                .build();
+    }
+}

+ 136 - 0
zxyj-backend/src/main/java/com/zxyj/controller/assessment/AssessmentAppointmentController.java

@@ -0,0 +1,136 @@
+package com.zxyj.controller.assessment;
+
+import com.zxyj.common.Result;
+import com.zxyj.entity.AssessmentAppointment;
+import com.zxyj.entity.AssessmentOrder;
+import com.zxyj.entity.User;
+import com.zxyj.mapper.UserMapper;
+import com.zxyj.service.AssessmentAppointmentService;
+import com.zxyj.service.AssessmentOrderService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Tag(name = "评估预约", description = "评估预约管理")
+@RestController
+@RequestMapping("/api/assessment/appointment")
+public class AssessmentAppointmentController {
+
+    @Resource
+    private AssessmentAppointmentService appointmentService;
+
+    @Resource
+    private AssessmentOrderService orderService;
+
+    @Resource
+    private UserMapper userMapper;
+
+    @Operation(summary = "创建评估预约")
+    @PostMapping("/create")
+    public Result<Map<String, Object>> createAppointment(
+            @RequestAttribute("userId") Long userId,
+            @RequestBody Map<String, Object> body) {
+
+        Long childId = body.get("childId") != null ? Long.valueOf(body.get("childId").toString()) : null;
+        Long guideId = body.get("guideId") != null ? Long.valueOf(body.get("guideId").toString()) : null;
+        Long packageId = body.get("packageId") != null ? Long.valueOf(body.get("packageId").toString()) : null;
+        String appointmentDate = body.get("appointmentDate") != null ? body.get("appointmentDate").toString() : null;
+        String appointmentTime = body.get("appointmentTime") != null ? body.get("appointmentTime").toString() : null;
+        String notes = body.get("notes") != null ? body.get("notes").toString() : null;
+        Long totalPrice = body.get("totalPrice") != null ? Long.valueOf(body.get("totalPrice").toString()) : 0L;
+        String guideName = body.get("guideName") != null ? body.get("guideName").toString() : null;
+        String packageName = body.get("packageName") != null ? body.get("packageName").toString() : null;
+        Long discountAmount = body.get("discountAmount") != null ? Long.valueOf(body.get("discountAmount").toString()) : 0L;
+
+        User user = userMapper.selectById(userId);
+        if (user == null) return Result.error("用户不存在");
+
+        AssessmentAppointment appointment = appointmentService.createAppointment(
+                userId, childId, guideId, packageId, appointmentDate, appointmentTime, notes);
+
+        Long familyId = user.getFamilyId();
+        AssessmentOrder order = orderService.createOrder(
+                familyId, userId, childId, guideId, packageId,
+                guideName, packageName, totalPrice, discountAmount);
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("appointmentId", appointment.getId());
+        result.put("orderNo", order.getOrderNo());
+        result.put("actualPrice", order.getActualPrice());
+        result.put("status", order.getStatus());
+
+        return Result.success(result);
+    }
+
+    @Operation(summary = "确认预约")
+    @PostMapping("/confirm/{id}")
+    public Result<Boolean> confirmAppointment(
+            @RequestAttribute("userId") Long userId,
+            @RequestAttribute("role") String role,
+            @PathVariable Long id) {
+        if (!"teacher".equals(role)) {
+            return Result.error("只有成长规划师可以确认预约");
+        }
+        boolean success = appointmentService.confirmAppointment(id, userId);
+        return Result.success(success);
+    }
+
+    @Operation(summary = "取消预约")
+    @PostMapping("/cancel/{id}")
+    public Result<Boolean> cancelAppointment(@PathVariable Long id) {
+        boolean success = appointmentService.cancelAppointment(id);
+        return Result.success(success);
+    }
+
+    @Operation(summary = "我的预约列表(家长视角)")
+    @PostMapping("/my-list")
+    public Result<List<Map<String, Object>>> getMyAppointments(@RequestAttribute("userId") Long userId) {
+        List<AssessmentAppointment> appointments = appointmentService.getByUserId(userId);
+        List<Map<String, Object>> result = appointments.stream().map(this::toMap).collect(Collectors.toList());
+        return Result.success(result);
+    }
+
+    @Operation(summary = "成长规划师的预约列表")
+    @PostMapping("/guide-list")
+    public Result<List<Map<String, Object>>> getGuideAppointments(
+            @RequestAttribute("userId") Long userId,
+            @RequestAttribute("role") String role) {
+        if (!"teacher".equals(role)) {
+            return Result.error("只有成长规划师可以查看此列表");
+        }
+        List<AssessmentAppointment> appointments = appointmentService.getByGuideId(userId);
+        List<Map<String, Object>> result = appointments.stream().map(this::toMap).collect(Collectors.toList());
+        return Result.success(result);
+    }
+
+    private Map<String, Object> toMap(AssessmentAppointment appt) {
+        Map<String, Object> map = new HashMap<>();
+        map.put("id", appt.getId());
+        map.put("childId", appt.getChildId());
+        map.put("guideId", appt.getGuideId());
+        map.put("appointmentDate", appt.getAppointmentDate());
+        map.put("appointmentTime", appt.getAppointmentTime());
+        map.put("status", appt.getStatus());
+        map.put("statusText", getStatusText(appt.getStatus()));
+        map.put("notes", appt.getNotes());
+        map.put("createdAt", appt.getCreatedAt());
+        return map;
+    }
+
+    private String getStatusText(String status) {
+        if (status == null) return "未知";
+        switch (status) {
+            case "pending": return "待确认";
+            case "confirmed": return "已确认";
+            case "completed": return "已完成";
+            case "cancelled": return "已取消";
+            default: return "未知";
+        }
+    }
+}

+ 38 - 0
zxyj-backend/src/main/java/com/zxyj/controller/sncp/ZkxzSyncController.java

@@ -0,0 +1,38 @@
+package com.zxyj.controller.sncp;
+
+import com.zxyj.common.Result;
+import com.zxyj.service.ZkxzSyncService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.HashMap;
+
+@Tag(name = "zkxz同步", description = "与zkxz外部平台同步")
+@RestController
+@RequestMapping("/api/sncp")
+public class ZkxzSyncController {
+
+    @Resource
+    private ZkxzSyncService zkxzSyncService;
+
+    @Operation(summary = "同步客户到zkxz")
+    @PostMapping("/addClient/{clientId}")
+    public Result<String> syncClient(@PathVariable String clientId) {
+        if (!zkxzSyncService.isEnabled()) {
+            return Result.error("zkxz同步未启用");
+        }
+        String result = zkxzSyncService.syncClient(new HashMap<>());
+        return Result.success(result);
+    }
+
+    @Operation(summary = "获取通知")
+    @PostMapping("/notice/{userLogin}")
+    public Result<String> getNotice(@PathVariable String userLogin) {
+        if (!zkxzSyncService.isEnabled()) {
+            return Result.success("zkxz未启用");
+        }
+        return Result.success("zkxz未启用");
+    }
+}

+ 8 - 20
zxyj-backend/src/main/java/com/zxyj/entity/AssessmentAppointment.java

@@ -15,33 +15,21 @@ public class AssessmentAppointment implements Serializable {
     @TableId(type = IdType.AUTO)
     private Long id;
 
-    private Long teacherId;
-
+    private Long userId;
     private Long familyId;
-
     private Long childId;
-
+    private Long teacherId;
     private Long guideId;
-
     private Long packageId;
-
     private Date appointmentDate;
-
     private Date appointmentTime;
-
-    private Integer durationMinutes;  // 测评时长
-
-    private String status;  // pending/confirmed/completed/cancelled/no_show
-
-    private String appointmentType;  // initial/follow_up/review
-
-    private String notes;  // 备注
-
+    private Integer durationMinutes;
+    private String status; // pending/confirmed/completed/cancelled/no_show
+    private String appointmentType;
+    private String notes;
+    private String remark;
     private String contactPhone;
-
-    private String meetLink;  // 会议链接
-
+    private String meetLink;
     private Date createdAt;
-
     private Date updatedAt;
 }

+ 6 - 0
zxyj-backend/src/main/java/com/zxyj/entity/GuidePackage.java

@@ -16,6 +16,12 @@ public class GuidePackage implements Serializable {
     @TableId(type = IdType.AUTO)
     private Long id;
 
+    private Long guideId;
+
+    private Long packageId;
+
+    private BigDecimal commissionRate;
+
     private String name;
 
     private String level;

+ 91 - 0
zxyj-backend/src/main/java/com/zxyj/service/AssessmentAppointmentService.java

@@ -0,0 +1,91 @@
+package com.zxyj.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.zxyj.entity.AssessmentAppointment;
+import com.zxyj.mapper.AssessmentAppointmentMapper;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.List;
+
+@Service
+public class AssessmentAppointmentService extends ServiceImpl<AssessmentAppointmentMapper, AssessmentAppointment> {
+
+    @Resource
+    private AssessmentAppointmentMapper assessmentAppointmentMapper;
+
+    public AssessmentAppointment createAppointment(Long userId, Long childId, Long guideId,
+                                                    Long packageId, String appointmentDate, String appointmentTime, String notes) {
+        AssessmentAppointment appointment = new AssessmentAppointment();
+        appointment.setUserId(userId);
+        appointment.setChildId(childId);
+        appointment.setGuideId(guideId);
+        appointment.setPackageId(packageId);
+        appointment.setAppointmentDate(parseDate(appointmentDate));
+        appointment.setAppointmentTime(parseDate(appointmentTime));
+        appointment.setTeacherId(guideId);
+        appointment.setStatus("pending");
+        appointment.setNotes(notes);
+        appointment.setCreatedAt(new Date());
+        appointment.setUpdatedAt(new Date());
+        this.save(appointment);
+        return appointment;
+    }
+
+    public boolean confirmAppointment(Long id, Long teacherId) {
+        AssessmentAppointment appointment = assessmentAppointmentMapper.selectById(id);
+        if (appointment == null) return false;
+        if (!teacherId.equals(appointment.getGuideId())) return false;
+        appointment.setStatus("confirmed");
+        appointment.setUpdatedAt(new Date());
+        return assessmentAppointmentMapper.updateById(appointment) > 0;
+    }
+
+    public boolean completeAppointment(Long id, Long teacherId) {
+        AssessmentAppointment appointment = assessmentAppointmentMapper.selectById(id);
+        if (appointment == null) return false;
+        if (!teacherId.equals(appointment.getGuideId())) return false;
+        appointment.setStatus("completed");
+        appointment.setUpdatedAt(new Date());
+        return assessmentAppointmentMapper.updateById(appointment) > 0;
+    }
+
+    public boolean cancelAppointment(Long id) {
+        AssessmentAppointment appointment = assessmentAppointmentMapper.selectById(id);
+        if (appointment == null) return false;
+        appointment.setStatus("cancelled");
+        appointment.setUpdatedAt(new Date());
+        return assessmentAppointmentMapper.updateById(appointment) > 0;
+    }
+
+    public List<AssessmentAppointment> getByUserId(Long userId) {
+        LambdaQueryWrapper<AssessmentAppointment> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(AssessmentAppointment::getUserId, userId);
+        wrapper.orderByDesc(AssessmentAppointment::getCreatedAt);
+        return assessmentAppointmentMapper.selectList(wrapper);
+    }
+
+    public List<AssessmentAppointment> getByGuideId(Long guideId) {
+        LambdaQueryWrapper<AssessmentAppointment> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(AssessmentAppointment::getGuideId, guideId);
+        wrapper.orderByDesc(AssessmentAppointment::getCreatedAt);
+        return assessmentAppointmentMapper.selectList(wrapper);
+    }
+
+    private Date parseDate(String dateStr) {
+        if (dateStr == null || dateStr.isEmpty()) return null;
+        try {
+            return new SimpleDateFormat("yyyy-MM-dd").parse(dateStr);
+        } catch (ParseException e) {
+            try {
+                return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(dateStr);
+            } catch (ParseException e2) {
+                return null;
+            }
+        }
+    }
+}

+ 12 - 6
zxyj-backend/src/main/java/com/zxyj/service/GuidePackageService.java

@@ -27,8 +27,8 @@ public class GuidePackageService extends ServiceImpl<GuidePackageMapper, GuidePa
     public boolean addGuidePackage(Long guideId, Long packageId, BigDecimal price, BigDecimal commissionRate) {
         // 检查是否已经存在
         LambdaQueryWrapper<GuidePackage> wrapper = new LambdaQueryWrapper<>();
-        wrapper.eq(GuidePackage::getId, guideId)
-               .eq(GuidePackage::getId, packageId);
+        wrapper.eq(GuidePackage::getGuideId, guideId)
+               .eq(GuidePackage::getPackageId, packageId);
         
         if (this.count(wrapper) > 0) {
             return false; // 已经存在
@@ -41,6 +41,9 @@ public class GuidePackageService extends ServiceImpl<GuidePackageMapper, GuidePa
         }
         
         GuidePackage guidePackage = new GuidePackage();
+        guidePackage.setGuideId(guideId);
+        guidePackage.setPackageId(packageId);
+        guidePackage.setCommissionRate(commissionRate);
         guidePackage.setName(taskPackage.getName());
         guidePackage.setPrice(price);
         guidePackage.setStatus("active");
@@ -53,7 +56,7 @@ public class GuidePackageService extends ServiceImpl<GuidePackageMapper, GuidePa
      */
     public List<GuidePackage> getGuidePackages(Long guideId) {
         LambdaQueryWrapper<GuidePackage> wrapper = new LambdaQueryWrapper<>();
-        wrapper.eq(GuidePackage::getId, guideId)
+        wrapper.eq(GuidePackage::getGuideId, guideId)
                .eq(GuidePackage::getStatus, "active")
                .orderByDesc(GuidePackage::getCreatedAt);
         
@@ -64,14 +67,17 @@ public class GuidePackageService extends ServiceImpl<GuidePackageMapper, GuidePa
      * 获取指导师对特定套餐的定价
      */
     public GuidePackage getGuidePackage(Long guideId, Long packageId) {
-        return this.getById(packageId);
+        LambdaQueryWrapper<GuidePackage> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(GuidePackage::getGuideId, guideId)
+               .eq(GuidePackage::getPackageId, packageId);
+        return this.getOne(wrapper);
     }
 
     /**
      * 更新指导师套餐价格
      */
     public boolean updateGuidePackagePrice(Long guideId, Long packageId, BigDecimal price) {
-        GuidePackage guidePackage = this.getById(packageId);
+        GuidePackage guidePackage = getGuidePackage(guideId, packageId);
         if (guidePackage == null) {
             return false;
         }
@@ -84,7 +90,7 @@ public class GuidePackageService extends ServiceImpl<GuidePackageMapper, GuidePa
      * 删除指导师套餐
      */
     public boolean deleteGuidePackage(Long guideId, Long packageId) {
-        GuidePackage guidePackage = this.getById(packageId);
+        GuidePackage guidePackage = getGuidePackage(guideId, packageId);
         if (guidePackage == null) {
             return false;
         }

+ 1 - 1
zxyj-backend/src/main/java/com/zxyj/service/PackagePurchaseService.java

@@ -126,7 +126,7 @@ public class PackagePurchaseService {
      * 获取可用的指导师列表(针对特定套餐)
      */
     public List<Map<String, Object>> getAvailableGuides(Long packageId) {
-        return new java.util.ArrayList<>();
+        return List.of();
     }
 
     /**

+ 2 - 1
zxyj-backend/src/main/java/com/zxyj/service/StreakService.java

@@ -7,6 +7,7 @@ import com.zxyj.entity.StreakMilestone;
 import com.zxyj.mapper.ChildMapper;
 import com.zxyj.mapper.PointsLogMapper;
 import com.zxyj.mapper.StreakMilestoneMapper;
+import com.zxyj.service.api.StreakServiceInterface;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 
@@ -16,7 +17,7 @@ import java.util.stream.Collectors;
 
 @Slf4j
 @Service
-public class StreakService {
+public class StreakService implements StreakServiceInterface {
 
     @Resource
     private StreakMilestoneMapper streakMilestoneMapper;

+ 1 - 2
zxyj-backend/src/main/java/com/zxyj/service/StreetService.java

@@ -3,7 +3,6 @@ package com.zxyj.service;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.zxyj.entity.Street;
 import com.zxyj.mapper.StreetMapper;
-import com.zxyj.service.api.StreetServiceInterface;
 import org.springframework.stereotype.Service;
 
 import javax.annotation.Resource;
@@ -14,7 +13,7 @@ import java.util.stream.Collectors;
  * 街道服务 - 支持四级地址和区域回退匹配
  */
 @Service
-public class StreetService implements StreetServiceInterface {
+public class StreetService {
 
     @Resource
     private StreetMapper streetMapper;

+ 2 - 1
zxyj-backend/src/main/java/com/zxyj/service/VerificationCodeService.java

@@ -3,6 +3,7 @@ package com.zxyj.service;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.zxyj.entity.VerificationCode;
 import com.zxyj.mapper.VerificationCodeMapper;
+import com.zxyj.service.api.VerificationCodeServiceInterface;
 import lombok.extern.slf4j.Slf4j;
 import javax.annotation.Resource;
 import org.springframework.stereotype.Service;
@@ -12,7 +13,7 @@ import java.util.Random;
 
 @Slf4j
 @Service
-public class VerificationCodeService {
+public class VerificationCodeService implements VerificationCodeServiceInterface {
 
     @Resource
     private VerificationCodeMapper verificationCodeMapper;

+ 2 - 1
zxyj-backend/src/main/java/com/zxyj/service/WechatService.java

@@ -8,13 +8,14 @@ import org.springframework.beans.factory.annotation.Value;
 import org.springframework.http.ResponseEntity;
 import org.springframework.stereotype.Service;
 import org.springframework.web.client.RestTemplate;
+import com.zxyj.service.api.WechatServiceInterface;
 
 import java.util.HashMap;
 import java.util.Map;
 import java.util.UUID;
 
 @Service
-public class WechatService {
+public class WechatService implements WechatServiceInterface {
 
     private static final Logger log = LoggerFactory.getLogger(WechatService.class);
 

+ 118 - 0
zxyj-backend/src/main/java/com/zxyj/service/ZkxzSyncService.java

@@ -0,0 +1,118 @@
+package com.zxyj.service;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+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.stereotype.Service;
+import org.springframework.web.client.RestTemplate;
+
+import javax.annotation.Resource;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+@Slf4j
+@Service
+public class ZkxzSyncService {
+
+    @Value("${zkxz.enabled:false}")
+    private boolean zkxzEnabled;
+
+    @Value("${zkxz.sync-api-url:}")
+    private String zkxzApiUrl;
+
+    @Value("${zkxz.zx-user:}")
+    private String zxUser;
+
+    @Value("${zkxz.zx-pass:}")
+    private String zxPass;
+
+    @Value("${zkxz.sync-client-url:}")
+    private String syncClientUrl;
+
+    @Value("${zkxz.sync-teacher-url:}")
+    private String syncTeacherUrl;
+
+    private static final String ZKXZ_KEY = "b354cab57768c7d4c1106eb4e82c75dc";
+    private static final String ZKXZ_VERSION = "v4";
+
+    @Resource
+    private RestTemplate restTemplate;
+
+    public boolean isEnabled() {
+        return zkxzEnabled && StringUtils.isNotBlank(zkxzApiUrl);
+    }
+
+    public String syncClient(Map<String, String> clientData) {
+        if (!isEnabled()) {
+            return "zkxz同步未启用";
+        }
+        Map<String, String> body = new HashMap<>(clientData);
+        HttpHeaders headers = buildHeaders();
+        body.put("username", zxUser);
+        body.put("password", zxPass);
+        if (StringUtils.isBlank(syncClientUrl)) {
+            return "未配置同步地址";
+        }
+        try {
+            HttpEntity<Map<String, String>> entity = new HttpEntity<>(body, headers);
+            String result = restTemplate.postForObject(zkxzApiUrl + syncClientUrl, entity, String.class);
+            log.info("zkxz client sync result: {}", result);
+            return "同步成功";
+        } catch (Exception e) {
+            log.error("zkxz client sync error", e);
+            return "同步失败: " + e.getMessage();
+        }
+    }
+
+    public String syncTeacher(Map<String, String> teacherData) {
+        if (!isEnabled()) {
+            return "zkxz同步未启用";
+        }
+        if (StringUtils.isBlank(syncTeacherUrl)) {
+            return "未配置同步地址";
+        }
+        HttpHeaders headers = buildHeaders();
+        HttpEntity<Map<String, String>> entity = new HttpEntity<>(teacherData, headers);
+        try {
+            String result = restTemplate.postForObject(zkxzApiUrl + syncTeacherUrl, entity, String.class);
+            log.info("zkxz teacher sync result: {}", result);
+            return "同步成功";
+        } catch (Exception e) {
+            log.error("zkxz teacher sync error", e);
+            return "同步失败: " + e.getMessage();
+        }
+    }
+
+    private HttpHeaders buildHeaders() {
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(MediaType.APPLICATION_JSON);
+        long timestamp = new Date().getTime() / 1000;
+        String nonceStr = StringUtils.right(UUID.randomUUID().toString(), 16);
+        String str = "timestamp=" + timestamp + "&nonceStr=" + nonceStr + "&key=" + ZKXZ_KEY + "&version=" + ZKXZ_VERSION;
+        String sign = md5Hex(str);
+        headers.add("version", ZKXZ_VERSION);
+        headers.add("x-token", sign);
+        return headers;
+    }
+
+    private String md5Hex(String input) {
+        try {
+            MessageDigest md = MessageDigest.getInstance("MD5");
+            byte[] digest = md.digest(input.getBytes(StandardCharsets.UTF_8));
+            StringBuilder sb = new StringBuilder();
+            for (byte b : digest) {
+                sb.append(String.format("%02x", b & 0xff));
+            }
+            return sb.toString();
+        } catch (Exception e) {
+            throw new RuntimeException("MD5 calculation failed", e);
+        }
+    }
+}

+ 9 - 0
zxyj-backend/src/main/java/com/zxyj/service/api/StreakServiceInterface.java

@@ -0,0 +1,9 @@
+package com.zxyj.service.api;
+
+import java.util.Map;
+
+public interface StreakServiceInterface {
+    Integer checkAndAwardMilestone(Long childId, int currentStreakDays);
+    Map<String, Object> getStreakProgress(Long childId);
+    void resetStreakIfNeeded();
+}

+ 15 - 0
zxyj-backend/src/main/java/com/zxyj/service/api/StreetServiceInterface.java

@@ -0,0 +1,15 @@
+package com.zxyj.service.api;
+
+import com.zxyj.entity.Street;
+import java.util.List;
+import java.util.Map;
+
+public interface StreetServiceInterface {
+    List<Street> getAllProvinces();
+    List<Street> getChildrenByParentId(Long parentId);
+    Street getById(Long id);
+    List<Street> search(String keyword);
+    List<Street> getAddressPath(Long streetId);
+    Map<String, Object> matchByStreetWithFallback(Long streetId);
+    Map<String, Object> getStatisticsByLevel(Integer level);
+}

+ 7 - 0
zxyj-backend/src/main/java/com/zxyj/service/api/VerificationCodeServiceInterface.java

@@ -0,0 +1,7 @@
+package com.zxyj.service.api;
+
+public interface VerificationCodeServiceInterface {
+    String generateCode(String phone, String type);
+    boolean verifyCode(String phone, String code, String type);
+    boolean checkCodeValid(String phone, String code, String type);
+}

+ 9 - 0
zxyj-backend/src/main/java/com/zxyj/service/api/WechatServiceInterface.java

@@ -0,0 +1,9 @@
+package com.zxyj.service.api;
+
+import java.util.Map;
+
+public interface WechatServiceInterface {
+    boolean isTestMode();
+    Map<String, String> code2Session(String code);
+    String getPhoneNumber(String code);
+}

+ 12 - 0
zxyj-backend/src/main/resources/application.yml

@@ -37,6 +37,18 @@ jwt:
   secret: zxyj-secret-key-2026-spring-boot-jwt-token
   expiration: 86400000
 
+zkxz:
+  enabled: false
+  sync-api-url: https://api.zkxz.org.cn/api/
+  login-url: member/login
+  sync-client-url: user/create
+  list-client-url: user/list
+  sync-teacher-url: teacher/create
+  user-report: user/report
+  report-info: report/info
+  zx-user: ""
+  zx-pass: ""
+
 wechat:
   appid: wx5ba8038ef16fb245
   secret: YOUR_APP_SECRET_HERE

+ 86 - 0
zxyj-backend/src/main/resources/migration_add_assessment_snapshot_fields.sql

@@ -0,0 +1,86 @@
+-- 为 dan_assessment_results 表添加孩子快照字段(测评时保存孩子完整信息)
+ALTER TABLE dan_assessment_results
+    ADD COLUMN IF NOT EXISTS child_name VARCHAR(50) COMMENT '孩子姓名快照',
+    ADD COLUMN IF NOT EXISTS child_birthday DATE COMMENT '出生日期快照',
+    ADD COLUMN IF NOT EXISTS child_gender VARCHAR(10) COMMENT '性别快照',
+    ADD COLUMN IF NOT EXISTS child_age INT COMMENT '年龄快照',
+    ADD COLUMN IF NOT EXISTS school VARCHAR(100) COMMENT '学校快照',
+    ADD COLUMN IF NOT EXISTS grade VARCHAR(20) COMMENT '年级快照',
+    ADD COLUMN IF NOT EXISTS grade_class VARCHAR(20) COMMENT '班级快照',
+    ADD COLUMN IF NOT EXISTS height INT COMMENT '身高(CM)快照',
+    ADD COLUMN IF NOT EXISTS weight INT COMMENT '体重(KG)快照',
+    ADD COLUMN IF NOT EXISTS family_status VARCHAR(50) COMMENT '家庭状况快照',
+    ADD COLUMN IF NOT EXISTS parent_degree VARCHAR(50) COMMENT '父母最高学历快照',
+    ADD COLUMN IF NOT EXISTS nation VARCHAR(20) COMMENT '民族快照',
+    ADD COLUMN IF NOT EXISTS province VARCHAR(50) COMMENT '省快照',
+    ADD COLUMN IF NOT EXISTS city VARCHAR(50) COMMENT '市快照',
+    ADD COLUMN IF NOT EXISTS district VARCHAR(50) COMMENT '区快照',
+    ADD COLUMN IF NOT EXISTS address VARCHAR(200) COMMENT '详细地址快照',
+    ADD COLUMN IF NOT EXISTS single_child VARCHAR(10) COMMENT '是否独生子女快照',
+    ADD COLUMN IF NOT EXISTS school_score VARCHAR(20) COMMENT '在校成绩快照',
+    ADD COLUMN IF NOT EXISTS urgent_name VARCHAR(50) COMMENT '紧急联系人姓名',
+    ADD COLUMN IF NOT EXISTS urgent_phone VARCHAR(20) COMMENT '紧急联系人电话',
+    ADD COLUMN IF NOT EXISTS pics TEXT COMMENT '图片IDs',
+    ADD COLUMN IF NOT EXISTS videos TEXT COMMENT '视频IDs',
+    ADD COLUMN IF NOT EXISTS audio VARCHAR(100) COMMENT '音频ID',
+    ADD COLUMN IF NOT EXISTS audio_url VARCHAR(255) COMMENT '音频URL',
+    ADD COLUMN IF NOT EXISTS process_desc TEXT COMMENT '过程描述',
+    ADD COLUMN IF NOT EXISTS result_desc TEXT COMMENT '结果简评';
+
+-- 为 assessment_appointments 表添加新字段
+ALTER TABLE assessment_appointments
+    ADD COLUMN IF NOT EXISTS user_id BIGINT COMMENT '用户ID',
+    ADD COLUMN IF NOT EXISTS guide_id BIGINT COMMENT '成长规划师ID',
+    ADD COLUMN IF NOT EXISTS package_id BIGINT COMMENT '套餐ID',
+    ADD COLUMN IF NOT EXISTS remark VARCHAR(500) COMMENT '备注';
+
+-- 创建 assessment_results 表(成长规划师录入的测评结果)
+CREATE TABLE IF NOT EXISTS assessment_results (
+    id BIGINT PRIMARY KEY AUTO_INCREMENT,
+    child_id BIGINT COMMENT '孩子ID',
+    teacher_id BIGINT COMMENT '成长规划师ID',
+    appointment_id BIGINT COMMENT '预约ID',
+    dan_level VARCHAR(10) COMMENT 'DAN等级',
+    attention_score INT COMMENT '注意力得分',
+    focus_score INT COMMENT '专注力得分',
+    memory_score INT COMMENT '记忆力得分',
+    logic_score INT COMMENT '逻辑思维得分',
+    overall_score INT COMMENT '综合得分',
+    assessment_date DATETIME COMMENT '测评日期',
+    duration_minutes INT COMMENT '测评时长',
+    analysis_report TEXT COMMENT '分析报告',
+    growth_suggestions TEXT COMMENT '成长建议',
+    next_review_date DATE COMMENT '下次复测日期',
+    status VARCHAR(20) DEFAULT 'draft' COMMENT '状态',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    child_name VARCHAR(50) COMMENT '孩子姓名快照',
+    child_birthday DATE COMMENT '出生日期快照',
+    child_gender VARCHAR(10) COMMENT '性别快照',
+    child_age INT COMMENT '年龄快照',
+    school VARCHAR(100) COMMENT '学校快照',
+    grade VARCHAR(20) COMMENT '年级快照',
+    grade_class VARCHAR(20) COMMENT '班级快照',
+    height INT COMMENT '身高(CM)快照',
+    weight INT COMMENT '体重(KG)快照',
+    family_status VARCHAR(50) COMMENT '家庭状况快照',
+    parent_degree VARCHAR(50) COMMENT '父母最高学历快照',
+    nation VARCHAR(20) COMMENT '民族快照',
+    province VARCHAR(50) COMMENT '省快照',
+    city VARCHAR(50) COMMENT '市快照',
+    district VARCHAR(50) COMMENT '区快照',
+    address VARCHAR(200) COMMENT '详细地址快照',
+    single_child VARCHAR(10) COMMENT '是否独生子女快照',
+    school_score VARCHAR(20) COMMENT '在校成绩快照',
+    urgent_name VARCHAR(50) COMMENT '紧急联系人姓名',
+    urgent_phone VARCHAR(20) COMMENT '紧急联系人电话',
+    pics TEXT COMMENT '图片IDs',
+    videos TEXT COMMENT '视频IDs',
+    audio VARCHAR(100) COMMENT '音频ID',
+    audio_url VARCHAR(255) COMMENT '音频URL',
+    process_desc TEXT COMMENT '过程描述',
+    result_desc TEXT COMMENT '结果简评',
+    INDEX idx_teacher (teacher_id),
+    INDEX idx_child (child_id),
+    INDEX idx_appointment (appointment_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='测评结果表(成长规划师录入)';

+ 24 - 0
zxyj-backend/src/main/resources/migration_assessment_order.sql

@@ -0,0 +1,24 @@
+-- 评估订单表
+CREATE TABLE IF NOT EXISTS assessment_orders (
+    id BIGINT PRIMARY KEY AUTO_INCREMENT,
+    order_no VARCHAR(64) NOT NULL UNIQUE COMMENT '订单号',
+    family_id BIGINT NOT NULL COMMENT '家庭ID',
+    user_id BIGINT NOT NULL COMMENT '用户ID',
+    child_id BIGINT COMMENT '孩子ID',
+    guide_id BIGINT NOT NULL COMMENT '成长规划师ID',
+    package_id BIGINT NOT NULL COMMENT '套餐ID',
+    guide_name VARCHAR(50) COMMENT '规划师姓名快照',
+    package_name VARCHAR(100) COMMENT '套餐名称快照',
+    total_price BIGINT DEFAULT 0 COMMENT '总价(分)',
+    discount_amount BIGINT DEFAULT 0 COMMENT '优惠金额(分)',
+    actual_price BIGINT DEFAULT 0 COMMENT '实付金额(分)',
+    status VARCHAR(20) DEFAULT 'pending' COMMENT '状态',
+    pay_time VARCHAR(50) COMMENT '支付时间',
+    pay_type VARCHAR(20) COMMENT '支付方式',
+    transaction_id VARCHAR(64) COMMENT '交易号',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    INDEX idx_family (family_id),
+    INDEX idx_guide (guide_id),
+    INDEX idx_status (status)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='评估订单表';