Browse Source

feat: 新增实体类、Mapper和Service支持多媒体和会员功能

- 新增实体类: MediaRecord, Teacher, TaskTemplate, MembershipLevel等
- 新增Mapper接口: MediaRecordMapper, TeacherMapper, TaskTemplateMapper等
- 新增Service层: MediaRecordService, TeacherService, MembershipService等
- 支持多媒体上传、教师管理、会员套餐、任务模板等功能模块
User 7 months ago
parent
commit
0787b54a07
77 changed files with 3273 additions and 47 deletions
  1. 12 0
      zxyj-backend/src/main/java/com/zxyj/dto/ApplyPackageDTO.java
  2. 26 0
      zxyj-backend/src/main/java/com/zxyj/dto/AssessmentAppointmentDTO.java
  3. 12 0
      zxyj-backend/src/main/java/com/zxyj/dto/BindGuideDTO.java
  4. 18 0
      zxyj-backend/src/main/java/com/zxyj/dto/ClassInfoDTO.java
  5. 20 0
      zxyj-backend/src/main/java/com/zxyj/dto/ClassReportDTO.java
  6. 18 0
      zxyj-backend/src/main/java/com/zxyj/dto/CreateAppointmentDTO.java
  7. 22 0
      zxyj-backend/src/main/java/com/zxyj/dto/CreateAssessmentResultDTO.java
  8. 15 0
      zxyj-backend/src/main/java/com/zxyj/dto/CreateClassDTO.java
  9. 17 0
      zxyj-backend/src/main/java/com/zxyj/dto/CreatePackageDTO.java
  10. 15 0
      zxyj-backend/src/main/java/com/zxyj/dto/CreatePackageItemDTO.java
  11. 14 0
      zxyj-backend/src/main/java/com/zxyj/dto/CreateRewardWishlistDTO.java
  12. 6 0
      zxyj-backend/src/main/java/com/zxyj/dto/CreateTaskDTO.java
  13. 20 0
      zxyj-backend/src/main/java/com/zxyj/dto/CreateTeacherTaskDTO.java
  14. 32 0
      zxyj-backend/src/main/java/com/zxyj/dto/DanAssessmentResultDTO.java
  15. 22 0
      zxyj-backend/src/main/java/com/zxyj/dto/FamilyMembershipDTO.java
  16. 26 0
      zxyj-backend/src/main/java/com/zxyj/dto/GrowthPlanDTO.java
  17. 1 0
      zxyj-backend/src/main/java/com/zxyj/dto/LoginResultDTO.java
  18. 21 0
      zxyj-backend/src/main/java/com/zxyj/dto/MembershipLevelDTO.java
  19. 24 0
      zxyj-backend/src/main/java/com/zxyj/dto/PackageDetailDTO.java
  20. 19 0
      zxyj-backend/src/main/java/com/zxyj/dto/PaymentOrderDTO.java
  21. 16 0
      zxyj-backend/src/main/java/com/zxyj/dto/RegisterWithIdCardDTO.java
  22. 21 0
      zxyj-backend/src/main/java/com/zxyj/dto/StudentInfoDTO.java
  23. 19 0
      zxyj-backend/src/main/java/com/zxyj/dto/StudentProgressDTO.java
  24. 22 0
      zxyj-backend/src/main/java/com/zxyj/dto/TaskDTO.java
  25. 22 0
      zxyj-backend/src/main/java/com/zxyj/dto/TeacherDTO.java
  26. 16 0
      zxyj-backend/src/main/java/com/zxyj/dto/TeacherInfoDTO.java
  27. 14 0
      zxyj-backend/src/main/java/com/zxyj/dto/UpdateUserDTO.java
  28. 1 0
      zxyj-backend/src/main/java/com/zxyj/dto/WechatLoginDTO.java
  29. 2 3
      zxyj-backend/src/main/java/com/zxyj/dto/WechatPhoneLoginDTO.java
  30. 5 0
      zxyj-backend/src/main/java/com/zxyj/entity/Admin.java
  31. 43 0
      zxyj-backend/src/main/java/com/zxyj/entity/AssessmentAppointment.java
  32. 53 0
      zxyj-backend/src/main/java/com/zxyj/entity/DanAssessmentResult.java
  33. 42 0
      zxyj-backend/src/main/java/com/zxyj/entity/FamilyMembership.java
  34. 43 0
      zxyj-backend/src/main/java/com/zxyj/entity/GrowthPlan.java
  35. 36 0
      zxyj-backend/src/main/java/com/zxyj/entity/GuideFamily.java
  36. 66 0
      zxyj-backend/src/main/java/com/zxyj/entity/MediaRecord.java
  37. 43 0
      zxyj-backend/src/main/java/com/zxyj/entity/MembershipLevel.java
  38. 40 0
      zxyj-backend/src/main/java/com/zxyj/entity/PaymentOrder.java
  39. 42 0
      zxyj-backend/src/main/java/com/zxyj/entity/RewardWishlist.java
  40. 7 0
      zxyj-backend/src/main/java/com/zxyj/entity/Task.java
  41. 39 0
      zxyj-backend/src/main/java/com/zxyj/entity/TaskPlanInstance.java
  42. 35 0
      zxyj-backend/src/main/java/com/zxyj/entity/TaskPlanItem.java
  43. 42 0
      zxyj-backend/src/main/java/com/zxyj/entity/TaskTemplate.java
  44. 37 0
      zxyj-backend/src/main/java/com/zxyj/entity/TaskTemplateItem.java
  45. 52 0
      zxyj-backend/src/main/java/com/zxyj/entity/TaskTemplatePackage.java
  46. 43 0
      zxyj-backend/src/main/java/com/zxyj/entity/Teacher.java
  47. 10 0
      zxyj-backend/src/main/java/com/zxyj/entity/User.java
  48. 9 0
      zxyj-backend/src/main/java/com/zxyj/mapper/AssessmentAppointmentMapper.java
  49. 9 0
      zxyj-backend/src/main/java/com/zxyj/mapper/DanAssessmentResultMapper.java
  50. 9 0
      zxyj-backend/src/main/java/com/zxyj/mapper/FamilyMembershipMapper.java
  51. 9 0
      zxyj-backend/src/main/java/com/zxyj/mapper/GuideFamilyMapper.java
  52. 21 0
      zxyj-backend/src/main/java/com/zxyj/mapper/MediaRecordMapper.java
  53. 9 0
      zxyj-backend/src/main/java/com/zxyj/mapper/MembershipLevelMapper.java
  54. 9 0
      zxyj-backend/src/main/java/com/zxyj/mapper/PaymentOrderMapper.java
  55. 9 0
      zxyj-backend/src/main/java/com/zxyj/mapper/RewardWishlistMapper.java
  56. 9 0
      zxyj-backend/src/main/java/com/zxyj/mapper/TaskPlanInstanceMapper.java
  57. 9 0
      zxyj-backend/src/main/java/com/zxyj/mapper/TaskPlanItemMapper.java
  58. 9 0
      zxyj-backend/src/main/java/com/zxyj/mapper/TaskTemplateItemMapper.java
  59. 9 0
      zxyj-backend/src/main/java/com/zxyj/mapper/TaskTemplateMapper.java
  60. 9 0
      zxyj-backend/src/main/java/com/zxyj/mapper/TaskTemplatePackageMapper.java
  61. 9 0
      zxyj-backend/src/main/java/com/zxyj/mapper/TeacherMapper.java
  62. 9 0
      zxyj-backend/src/main/java/com/zxyj/service/DatabaseSyncService.java
  63. 110 0
      zxyj-backend/src/main/java/com/zxyj/service/GuideFamilyService.java
  64. 34 0
      zxyj-backend/src/main/java/com/zxyj/service/MediaRecordService.java
  65. 241 0
      zxyj-backend/src/main/java/com/zxyj/service/MembershipService.java
  66. 3 3
      zxyj-backend/src/main/java/com/zxyj/service/PointsService.java
  67. 5 5
      zxyj-backend/src/main/java/com/zxyj/service/RewardService.java
  68. 118 0
      zxyj-backend/src/main/java/com/zxyj/service/RewardWishlistService.java
  69. 21 0
      zxyj-backend/src/main/java/com/zxyj/service/StorageService.java
  70. 167 0
      zxyj-backend/src/main/java/com/zxyj/service/TaskPlanService.java
  71. 261 0
      zxyj-backend/src/main/java/com/zxyj/service/TaskTemplatePackageService.java
  72. 308 0
      zxyj-backend/src/main/java/com/zxyj/service/TeacherService.java
  73. 370 34
      zxyj-backend/src/main/java/com/zxyj/service/UserService.java
  74. 2 2
      zxyj-backend/src/main/java/com/zxyj/service/VerificationCodeService.java
  75. 144 0
      zxyj-backend/src/main/java/com/zxyj/service/WechatService.java
  76. 72 0
      zxyj-backend/src/main/java/com/zxyj/service/impl/LocalDiskStorageService.java
  77. 99 0
      zxyj-backend/src/main/java/com/zxyj/service/impl/MediaRecordServiceImpl.java

+ 12 - 0
zxyj-backend/src/main/java/com/zxyj/dto/ApplyPackageDTO.java

@@ -0,0 +1,12 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class ApplyPackageDTO {
+    private Long packageId;
+    private Long childId;
+    private List<Long> selectedItemIds;  // 家长勾选的任务项ID
+}

+ 26 - 0
zxyj-backend/src/main/java/com/zxyj/dto/AssessmentAppointmentDTO.java

@@ -0,0 +1,26 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+import java.util.Date;
+
+@Data
+public class AssessmentAppointmentDTO {
+    private Long id;
+    private Long teacherId;
+    private Long familyId;
+    private Long childId;
+    private Date appointmentDate;
+    private Date appointmentTime;
+    private Integer durationMinutes;
+    private String status;
+    private String appointmentType;
+    private String notes;
+    private String contactPhone;
+    private String meetLink;
+    private Date createdAt;
+    
+    // 关联信息
+    private String childName;
+    private String familyName;
+    private String teacherName;
+}

+ 12 - 0
zxyj-backend/src/main/java/com/zxyj/dto/BindGuideDTO.java

@@ -0,0 +1,12 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+public class BindGuideDTO {
+    private String bindToken;    // 绑定令牌
+    private String serviceType;  // monthly/yearly/consulting
+    private BigDecimal servicePrice;
+}

+ 18 - 0
zxyj-backend/src/main/java/com/zxyj/dto/ClassInfoDTO.java

@@ -0,0 +1,18 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+/**
+ * 班级信息DTO
+ */
+@Data
+public class ClassInfoDTO {
+    private Long id;
+    private String className;
+    private String subject;
+    private String gradeLevel;
+    private String description;
+    private String roomNumber;
+    private Long teacherId;
+    private Integer studentCount;
+}

+ 20 - 0
zxyj-backend/src/main/java/com/zxyj/dto/ClassReportDTO.java

@@ -0,0 +1,20 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 班级报告DTO
+ */
+@Data
+public class ClassReportDTO {
+    private Long classId;
+    private String className;
+    private Integer totalStudents;
+    private Integer activeStudents;
+    private Double averageCompletionRate;
+    private Double averagePoints;
+    private List<StudentProgressDTO> topStudents;
+    private List<StudentProgressDTO> needAttentionStudents;
+}

+ 18 - 0
zxyj-backend/src/main/java/com/zxyj/dto/CreateAppointmentDTO.java

@@ -0,0 +1,18 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+import java.util.Date;
+
+@Data
+public class CreateAppointmentDTO {
+    private Long teacherId;
+    private Long familyId;
+    private Long childId;
+    private Date appointmentDate;
+    private Date appointmentTime;
+    private Integer durationMinutes;
+    private String appointmentType;
+    private String notes;
+    private String contactPhone;
+    private String meetLink;
+}

+ 22 - 0
zxyj-backend/src/main/java/com/zxyj/dto/CreateAssessmentResultDTO.java

@@ -0,0 +1,22 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+import java.util.Date;
+
+@Data
+public class CreateAssessmentResultDTO {
+    private Long childId;
+    private Long appointmentId;
+    private String danLevel;
+    private Integer attentionScore;
+    private Integer focusScore;
+    private Integer memoryScore;
+    private Integer logicScore;
+    private Integer overallScore;
+    private Date assessmentDate;
+    private Integer durationMinutes;
+    private String gameRecords;
+    private String analysisReport;
+    private String growthSuggestions;
+    private Date nextReviewDate;
+}

+ 15 - 0
zxyj-backend/src/main/java/com/zxyj/dto/CreateClassDTO.java

@@ -0,0 +1,15 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+/**
+ * 创建班级DTO
+ */
+@Data
+public class CreateClassDTO {
+    private String className;
+    private String subject;
+    private String gradeLevel;
+    private String description;
+    private String roomNumber; // 可选:班级房间号或线上班级标识
+}

+ 17 - 0
zxyj-backend/src/main/java/com/zxyj/dto/CreatePackageDTO.java

@@ -0,0 +1,17 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+public class CreatePackageDTO {
+    private String name;
+    private String description;
+    private Integer cycleDays;
+    private String category;
+    private String tags;
+    private BigDecimal price;
+    private Integer isPublic;
+    private String coverImage;
+}

+ 15 - 0
zxyj-backend/src/main/java/com/zxyj/dto/CreatePackageItemDTO.java

@@ -0,0 +1,15 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+@Data
+public class CreatePackageItemDTO {
+    private Long packageId;
+    private String taskName;
+    private String description;
+    private Integer points;
+    private String frequency;
+    private Integer duration;
+    private Integer isRequired;
+    private Integer sortOrder;
+}

+ 14 - 0
zxyj-backend/src/main/java/com/zxyj/dto/CreateRewardWishlistDTO.java

@@ -0,0 +1,14 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+/**
+ * 创建家长心愿单DTO
+ */
+@Data
+public class CreateRewardWishlistDTO {
+    private String title;
+    private String description;
+    private Integer pointsRequired;
+    private String category;
+}

+ 6 - 0
zxyj-backend/src/main/java/com/zxyj/dto/CreateTaskDTO.java

@@ -14,5 +14,11 @@ public class CreateTaskDTO {
     private String category;
     private Integer needReview;
     private Integer reviewByCategory;
+    
+    // 执行者类型: child(孩子) 或 parent(家长)
+    private String executorType;
+    // 执行者ID
+    private Long executorId;
+    // 兼容旧版 - 指定孩子ID
     private Long childId;
 }

+ 20 - 0
zxyj-backend/src/main/java/com/zxyj/dto/CreateTeacherTaskDTO.java

@@ -0,0 +1,20 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+/**
+ * 教师创建任务DTO
+ */
+@Data
+public class CreateTeacherTaskDTO {
+    private String title;
+    private String description;
+    private Integer points;
+    private String category;
+    private String difficultyLevel; // 难度级别,可关联DAN等级
+    private String subject; // 学科
+    private String gradeLevel; // 年级
+    private Long deadline; // 截止时间戳
+    private Boolean needReview; // 是否需要审核
+    private String repeatType; // 重复类型
+}

+ 32 - 0
zxyj-backend/src/main/java/com/zxyj/dto/DanAssessmentResultDTO.java

@@ -0,0 +1,32 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+import java.util.Date;
+
+@Data
+public class DanAssessmentResultDTO {
+    private Long id;
+    private Long childId;
+    private Long teacherId;
+    private Long appointmentId;
+    private String danLevel;
+    private Integer attentionScore;
+    private Integer focusScore;
+    private Integer memoryScore;
+    private Integer logicScore;
+    private Integer overallScore;
+    private Date assessmentDate;
+    private Integer durationMinutes;
+    private String gameRecords;
+    private String analysisReport;
+    private String growthSuggestions;
+    private Date nextReviewDate;
+    private String status;
+    private Date createdAt;
+    
+    // 关联信息
+    private String childName;
+    private String childNickname;
+    private String teacherName;
+    private String teacherTitle;
+}

+ 22 - 0
zxyj-backend/src/main/java/com/zxyj/dto/FamilyMembershipDTO.java

@@ -0,0 +1,22 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+import java.math.BigDecimal;
+import java.util.Date;
+
+@Data
+public class FamilyMembershipDTO {
+    private Long id;
+    private Long familyId;
+    private String levelCode;
+    private String levelName;
+    private Date startDate;
+    private Date endDate;
+    private String paymentMethod;
+    private String paymentStatus;
+    private String orderNo;
+    private BigDecimal amount;
+    private Integer autoRenew;
+    private Boolean isActive;  // 是否在会员期内
+    private Long daysRemaining;  // 剩余天数
+}

+ 26 - 0
zxyj-backend/src/main/java/com/zxyj/dto/GrowthPlanDTO.java

@@ -0,0 +1,26 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+import java.util.Date;
+
+@Data
+public class GrowthPlanDTO {
+    private Long id;
+    private Long childId;
+    private Long teacherId;
+    private Long assessmentId;
+    private String planTitle;
+    private String planContent;
+    private String targetDanLevel;
+    private Integer durationMonths;
+    private String status;
+    private Date startDate;
+    private Date endDate;
+    private Integer reviewCount;
+    private Date createdAt;
+    
+    // 关联信息
+    private String childName;
+    private String teacherName;
+    private String danLevel;  // 当前DAN等级
+}

+ 1 - 0
zxyj-backend/src/main/java/com/zxyj/dto/LoginResultDTO.java

@@ -11,4 +11,5 @@ public class LoginResultDTO {
     private String nickname;
     private Long currentChildId; // 当前切换到的孩子ID(如果切换了)
     private Boolean isNewUser; // 是否新用户,用于前端判断是否需要进入信息编辑页面
+    private String openid; // 微信openid,用于前端缓存实现自动登录
 }

+ 21 - 0
zxyj-backend/src/main/java/com/zxyj/dto/MembershipLevelDTO.java

@@ -0,0 +1,21 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+import java.math.BigDecimal;
+import java.util.Date;
+
+@Data
+public class MembershipLevelDTO {
+    private Long id;
+    private String levelCode;
+    private String levelName;
+    private String levelDesc;
+    private BigDecimal priceMonthly;
+    private BigDecimal priceYearly;
+    private String features;
+    private Integer maxChildren;
+    private Integer maxTasksPerDay;
+    private Integer aiReviewEnabled;
+    private Integer prioritySupport;
+    private Integer teacherConsultation;
+}

+ 24 - 0
zxyj-backend/src/main/java/com/zxyj/dto/PackageDetailDTO.java

@@ -0,0 +1,24 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+public class PackageDetailDTO {
+    private Long id;
+    private String name;
+    private String description;
+    private Integer cycleDays;
+    private String category;
+    private String tags;
+    private BigDecimal price;
+    private BigDecimal platformFee;
+    private Integer isPublic;
+    private String shareCode;
+    private String coverImage;
+    private String status;
+    private String creatorName;
+    private Integer itemCount;
+    private Long createdAt;
+}

+ 19 - 0
zxyj-backend/src/main/java/com/zxyj/dto/PaymentOrderDTO.java

@@ -0,0 +1,19 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+import java.math.BigDecimal;
+import java.util.Date;
+
+@Data
+public class PaymentOrderDTO {
+    private String orderNo;
+    private Long familyId;
+    private String levelCode;
+    private String paymentType;
+    private BigDecimal amount;
+    private String status;
+    private String payMethod;
+    private String transactionId;
+    private Date paidAt;
+    private Date createdAt;
+}

+ 16 - 0
zxyj-backend/src/main/java/com/zxyj/dto/RegisterWithIdCardDTO.java

@@ -0,0 +1,16 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+/**
+ * 注册并绑定身份证的DTO
+ */
+@Data
+public class RegisterWithIdCardDTO {
+    private String phone;
+    private String code;
+    private String nickname;
+    private String avatar;
+    private String idCard; // 身份证号
+    private String realName; // 真实姓名
+}

+ 21 - 0
zxyj-backend/src/main/java/com/zxyj/dto/StudentInfoDTO.java

@@ -0,0 +1,21 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+* 学生信息DTO(供教师查看)
+*/
+@Data
+public class StudentInfoDTO {
+  private Long id;
+  private String nickname;
+  private String avatar;
+  private String realName;
+  private String gender;
+  private Date birthday;
+  private String danLevel; // DAN等级
+  private Long familyId;
+  private Long userId; // 关联的用户ID(家长或孩子自己的账号)
+}

+ 19 - 0
zxyj-backend/src/main/java/com/zxyj/dto/StudentProgressDTO.java

@@ -0,0 +1,19 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+/**
+ * 学生进度DTO
+ */
+@Data
+public class StudentProgressDTO {
+    private Long childId;
+    private String childName;
+    private Integer totalTasks;
+    private Integer completedTasks;
+    private Integer pendingTasks;
+    private Double completionRate;
+    private Integer totalPoints;
+    private Integer currentPoints;
+    private String danLevel;
+}

+ 22 - 0
zxyj-backend/src/main/java/com/zxyj/dto/TaskDTO.java

@@ -0,0 +1,22 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 任务DTO(教师任务列表)
+ */
+@Data
+public class TaskDTO {
+    private Long id;
+    private String title;
+    private String description;
+    private Integer points;
+    private Date deadline;
+    private String category;
+    private String status;
+    private Long childId;
+    private String childName;
+    private Date createdAt;
+}

+ 22 - 0
zxyj-backend/src/main/java/com/zxyj/dto/TeacherDTO.java

@@ -0,0 +1,22 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+import java.util.Date;
+
+@Data
+public class TeacherDTO {
+    private Long id;
+    private Long userId;
+    private String teacherNo;
+    private String title;
+    private String specialty;
+    private String bio;
+    private Integer experienceYears;
+    private java.math.BigDecimal rating;
+    private Integer totalAppointments;
+    private String status;
+    private String verificationStatus;
+    private Date verifiedAt;
+    private String nickname;  // 关联用户昵称
+    private String avatar;   // 关联用户头像
+}

+ 16 - 0
zxyj-backend/src/main/java/com/zxyj/dto/TeacherInfoDTO.java

@@ -0,0 +1,16 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+/**
+ * 教师信息DTO
+ */
+@Data
+public class TeacherInfoDTO {
+    private Long id;
+    private String nickname;
+    private String avatar;
+    private String subjectSpecialization;
+    private String gradeLevelTaught;
+    private String employmentStatus;
+}

+ 14 - 0
zxyj-backend/src/main/java/com/zxyj/dto/UpdateUserDTO.java

@@ -0,0 +1,14 @@
+package com.zxyj.dto;
+
+import lombok.Data;
+
+@Data
+public class UpdateUserDTO {
+    private String nickname;
+    private String avatar;
+    private String realName;
+    private String role;
+    private String gender;
+    private String birthday;
+    private String idCard;
+}

+ 1 - 0
zxyj-backend/src/main/java/com/zxyj/dto/WechatLoginDTO.java

@@ -5,6 +5,7 @@ import lombok.Data;
 @Data
 public class WechatLoginDTO {
     private String code;
+    private String openid;
     private String userInfo;
     private String nickname;
     private String avatar;

+ 2 - 3
zxyj-backend/src/main/java/com/zxyj/dto/WechatPhoneLoginDTO.java

@@ -4,9 +4,8 @@ import lombok.Data;
 
 @Data
 public class WechatPhoneLoginDTO {
-    private String code;
-    private String encryptedData;
-    private String iv;
+    private String code;           // 登录code,用于获取openid
+    private String phoneCode;      // 手机号code,用于获取手机号
     private String nickname;
     private String avatar;
 }

+ 5 - 0
zxyj-backend/src/main/java/com/zxyj/entity/Admin.java

@@ -24,6 +24,11 @@ public class Admin {
 
     private String email;
 
+    /**
+     * 角色类型:admin(管理员)、teacher(教师)
+     */
+    private String role;
+
     private Integer status;
 
     private Date createdAt;

+ 43 - 0
zxyj-backend/src/main/java/com/zxyj/entity/AssessmentAppointment.java

@@ -0,0 +1,43 @@
+package com.zxyj.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("assessment_appointments")
+public class AssessmentAppointment implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long teacherId;
+
+    private Long familyId;
+
+    private Long childId;
+
+    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 String contactPhone;
+
+    private String meetLink;  // 会议链接
+
+    private Date createdAt;
+
+    private Date updatedAt;
+}

+ 53 - 0
zxyj-backend/src/main/java/com/zxyj/entity/DanAssessmentResult.java

@@ -0,0 +1,53 @@
+package com.zxyj.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_results")
+public class DanAssessmentResult implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long childId;
+
+    private Long teacherId;
+
+    private Long appointmentId;
+
+    private String danLevel;  // DAN等级 A/B/C/D
+
+    private Integer attentionScore;  // 注意力得分
+
+    private Integer focusScore;  // 专注力得分
+
+    private Integer memoryScore;  // 记忆力得分
+
+    private Integer logicScore;  // 逻辑思维得分
+
+    private Integer overallScore;  // 综合得分
+
+    private Date assessmentDate;
+
+    private Integer durationMinutes;  // 测评时长
+
+    private String gameRecords;  // 游戏记录详情(JSON)
+
+    private String analysisReport;  // 分析报告
+
+    private String growthSuggestions;  // 成长建议
+
+    private Date nextReviewDate;  // 下次复测日期
+
+    private String status;  // draft/completed/archived
+
+    private Date createdAt;
+
+    private Date updatedAt;
+}

+ 42 - 0
zxyj-backend/src/main/java/com/zxyj/entity/FamilyMembership.java

@@ -0,0 +1,42 @@
+package com.zxyj.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.math.BigDecimal;
+import java.util.Date;
+
+@Data
+@TableName("family_memberships")
+public class FamilyMembership implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long familyId;
+
+    private String levelCode;  // FREE/BASIC/PROFESSIONAL/ENTERPRISE
+
+    private Date startDate;
+
+    private Date endDate;
+
+    private String paymentMethod;  // wechat/alipay/card
+
+    private String paymentStatus;  // pending/paid/expired/refunded
+
+    private String orderNo;  // 订单号
+
+    private String transactionId;  // 交易流水号
+
+    private BigDecimal amount;  // 支付金额
+
+    private Integer autoRenew;  // 自动续费
+
+    private Date createdAt;
+
+    private Date updatedAt;
+}

+ 43 - 0
zxyj-backend/src/main/java/com/zxyj/entity/GrowthPlan.java

@@ -0,0 +1,43 @@
+package com.zxyj.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("growth_plans")
+public class GrowthPlan implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long childId;
+
+    private Long teacherId;
+
+    private Long assessmentId;
+
+    private String planTitle;
+
+    private String planContent;  // 规划内容
+
+    private String targetDanLevel;  // 目标DAN等级
+
+    private Integer durationMonths;  // 规划周期(月)
+
+    private String status;  // draft/active/completed/expired
+
+    private Date startDate;
+
+    private Date endDate;
+
+    private Integer reviewCount;  // 已复盘次数
+
+    private Date createdAt;
+
+    private Date updatedAt;
+}

+ 36 - 0
zxyj-backend/src/main/java/com/zxyj/entity/GuideFamily.java

@@ -0,0 +1,36 @@
+package com.zxyj.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.math.BigDecimal;
+import java.util.Date;
+
+@Data
+@TableName("guide_families")
+public class GuideFamily implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long guideId;        // 指导师用户ID
+
+    private Long familyId;       // 家庭ID
+
+    private String serviceType;  // 服务类型: monthly/yearly/consulting
+
+    private BigDecimal servicePrice;  // 服务价格
+
+    private String status;       // binding/paused/cancelled
+
+    private Date boundAt;        // 绑定时间
+
+    private Date expireAt;       // 服务到期时间
+
+    private Date createdAt;
+
+    private Date updatedAt;
+}

+ 66 - 0
zxyj-backend/src/main/java/com/zxyj/entity/MediaRecord.java

@@ -0,0 +1,66 @@
+package com.zxyj.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("media_records")
+public class MediaRecord implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 媒体类型:image, video, audio, text
+     */
+    private String type;
+
+    /**
+     * 文件URL(图片/视频/音频的存储路径)
+     */
+    private String url;
+
+    /**
+     * 媒体描述或文字内容
+     */
+    private String description;
+
+    /**
+     * 关联的任务ID
+     */
+    private Long taskId;
+
+    /**
+     * 创建者ID(上传者)
+     */
+    private Long creatorId;
+
+    /**
+     * 原始文件名
+     */
+    private String originalFilename;
+
+    /**
+     * 文件大小(字节)
+     */
+    private Long fileSize;
+
+    /**
+     * 文件类型(MIME type)
+     */
+    private String contentType;
+
+    /**
+     * 创建时间
+     */
+    private Date createdAt;
+}

+ 43 - 0
zxyj-backend/src/main/java/com/zxyj/entity/MembershipLevel.java

@@ -0,0 +1,43 @@
+package com.zxyj.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("membership_levels")
+public class MembershipLevel implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private String levelCode;  // FREE/BASIC/PROFESSIONAL/ENTERPRISE
+
+    private String levelName;  // 等级名称
+
+    private String levelDesc;  // 等级描述
+
+    private java.math.BigDecimal priceMonthly;  // 月费
+
+    private java.math.BigDecimal priceYearly;  // 年费
+
+    private String features;  // 功能权限列表(JSON)
+
+    private Integer maxChildren;  // 可添加孩子数量
+
+    private Integer maxTasksPerDay;  // 每日任务上限
+
+    private Integer aiReviewEnabled;  // AI审核
+
+    private Integer prioritySupport;  // 优先客服
+
+    private Integer teacherConsultation;  // 老师咨询
+
+    private Date createdAt;
+
+    private Date updatedAt;
+}

+ 40 - 0
zxyj-backend/src/main/java/com/zxyj/entity/PaymentOrder.java

@@ -0,0 +1,40 @@
+package com.zxyj.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.math.BigDecimal;
+import java.util.Date;
+
+@Data
+@TableName("payment_orders")
+public class PaymentOrder implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private String orderNo;
+
+    private Long familyId;
+
+    private String levelCode;
+
+    private String paymentType;
+
+    private BigDecimal amount;
+
+    private String status;
+
+    private String payMethod;
+
+    private String transactionId;
+
+    private Date paidAt;
+
+    private Date createdAt;
+
+    private Date updatedAt;
+}

+ 42 - 0
zxyj-backend/src/main/java/com/zxyj/entity/RewardWishlist.java

@@ -0,0 +1,42 @@
+package com.zxyj.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("reward_wishlists")
+public class RewardWishlist implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long familyId;
+
+    private Long userId; // 家长用户ID
+
+    private String title;
+
+    private String description;
+
+    private Integer pointsRequired;
+
+    private String category;
+
+    private String status; // pending, exchanged, rejected
+
+    private Integer exchangeApproved;
+
+    private Date exchangedAt;
+
+    private Date createdAt;
+
+    private Date updatedAt;
+}

+ 7 - 0
zxyj-backend/src/main/java/com/zxyj/entity/Task.java

@@ -19,6 +19,13 @@ public class Task implements Serializable {
 
     private Long creatorId;
 
+    // 执行者类型: child(孩子) 或 parent(家长)
+    private String executorType;
+
+    // 执行者ID (当executorType=child时对应child表ID,executorType=parent时对应user表ID)
+    private Long executorId;
+
+    // 保留childId字段用于兼容旧数据
     private Long childId;
 
     private String title;

+ 39 - 0
zxyj-backend/src/main/java/com/zxyj/entity/TaskPlanInstance.java

@@ -0,0 +1,39 @@
+package com.zxyj.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("task_plan_instances")
+public class TaskPlanInstance implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long packageId;     // 来源套餐ID
+
+    private Long familyId;       // 所属家庭
+
+    private Long childId;        // 适用孩子
+
+    private String name;         // 实例名称
+
+    private Date startDate;      // 计划开始日期
+
+    private Date endDate;        // 计划结束日期
+
+    private String status;       // active/completed/cancelled
+
+    private Integer totalDays;   // 总天数
+
+    private Integer completedDays;  // 已完成天数
+
+    private Date createdAt;
+
+    private Date updatedAt;
+}

+ 35 - 0
zxyj-backend/src/main/java/com/zxyj/entity/TaskPlanItem.java

@@ -0,0 +1,35 @@
+package com.zxyj.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("task_plan_items")
+public class TaskPlanItem implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long instanceId;     // 所属实例ID
+
+    private Long templateItemId; // 来源模板项ID
+
+    private String taskName;
+
+    private String description;
+
+    private Integer points;
+
+    private String frequency;
+
+    private Integer duration;
+
+    private Date createdAt;
+
+    private Date updatedAt;
+}

+ 42 - 0
zxyj-backend/src/main/java/com/zxyj/entity/TaskTemplate.java

@@ -0,0 +1,42 @@
+package com.zxyj.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("admin_task_templates")
+public class TaskTemplate implements Serializable {
+
+  @TableId(type = IdType.AUTO)
+  private Long id;
+
+  private String title;
+
+  private String description;
+
+  private Integer points;
+
+  private String category;
+
+  private Integer needReview;
+
+  private Integer reviewByCategory;
+
+  private Integer recommendedAge;
+
+  private String difficulty;
+
+  private Integer isActive;
+
+  // 创建该模板的指导师ID
+  private Long teacherId;
+
+  private Date createdAt;
+
+  private Date updatedAt;
+}

+ 37 - 0
zxyj-backend/src/main/java/com/zxyj/entity/TaskTemplateItem.java

@@ -0,0 +1,37 @@
+package com.zxyj.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("task_template_items")
+public class TaskTemplateItem implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long packageId;     // 所属套餐ID
+
+    private String taskName;     // 任务名称
+
+    private String description;  // 任务描述
+
+    private Integer points;      // 基础积分
+
+    private String frequency;    // 执行频率: daily/weekly/custom
+
+    private Integer duration;    // 预计时长(分钟)
+
+    private Integer isRequired;  // 是否必选: 0/1
+
+    private Integer sortOrder;   // 排序
+
+    private Date createdAt;
+
+    private Date updatedAt;
+}

+ 52 - 0
zxyj-backend/src/main/java/com/zxyj/entity/TaskTemplatePackage.java

@@ -0,0 +1,52 @@
+package com.zxyj.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.math.BigDecimal;
+import java.util.Date;
+
+@Data
+@TableName("task_template_packages")
+public class TaskTemplatePackage implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long creatorId;      // 创建者指导师ID
+
+    private String name;         // 套餐名称,如"21天养成学习习惯"
+
+    private String description;  // 详细介绍
+
+    private Integer cycleDays;   // 周期天数: 21/30/60等
+
+    private String category;     // 分类: study/habit/health等
+
+    private String tags;         // 标签 JSON: ["学习", "专注"]
+
+    private BigDecimal price;    // 售价
+
+    private BigDecimal platformFee;  // 平台服务费 (不超过10%)
+
+    private Integer isPublic;    // 是否公开到市场: 0/1
+
+    private String shareCode;    // 分享码 (唯一)
+
+    private String coverImage;   // 封面图
+
+    private String status;       // draft/pending/approved/rejected/offline
+
+    private Long reviewerId;     // 审核人
+
+    private Date reviewedAt;     // 审核时间
+
+    private String rejectReason; // 拒绝原因
+
+    private Date createdAt;
+
+    private Date updatedAt;
+}

+ 43 - 0
zxyj-backend/src/main/java/com/zxyj/entity/Teacher.java

@@ -0,0 +1,43 @@
+package com.zxyj.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("teachers")
+public class Teacher implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long userId;  // 关联users表ID
+
+    private String teacherNo;  // 指导师编号
+
+    private String title;  // 职称/称号
+
+    private String specialty;  // 专长领域
+
+    private String bio;  // 简介
+
+    private Integer experienceYears;  // 从业年限
+
+    private java.math.BigDecimal rating;  // 评分
+
+    private Integer totalAppointments;  // 总测评次数
+
+    private String status;  // active/inactive/suspended
+
+    private String verificationStatus;  // pending/approved/rejected
+
+    private Date verifiedAt;
+
+    private Date createdAt;
+
+    private Date updatedAt;
+}

+ 10 - 0
zxyj-backend/src/main/java/com/zxyj/entity/User.java

@@ -21,8 +21,12 @@ public class User implements Serializable {
 
     private Long familyId;
 
+    // 当前活跃角色: parent/child/teacher
     private String role;
 
+    // 用户拥有的所有角色列表 (JSON格式: ["parent","teacher"] 或逗号分隔)
+    private String roles;
+
     private String nickname;
 
     private String avatar;
@@ -39,6 +43,12 @@ public class User implements Serializable {
 
     private String gender; // 性别: male/female
 
+    // 家长积分 (孩子没有积分,用积分奖励家长)
+    private Integer totalPoints;
+
+    // 指导师关联的家庭ID列表 (逗号分隔)
+    private String teacherFamilyIds;
+
     private Date createdAt;
 
     private Date updatedAt;

+ 9 - 0
zxyj-backend/src/main/java/com/zxyj/mapper/AssessmentAppointmentMapper.java

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

+ 9 - 0
zxyj-backend/src/main/java/com/zxyj/mapper/DanAssessmentResultMapper.java

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

+ 9 - 0
zxyj-backend/src/main/java/com/zxyj/mapper/FamilyMembershipMapper.java

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

+ 9 - 0
zxyj-backend/src/main/java/com/zxyj/mapper/GuideFamilyMapper.java

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

+ 21 - 0
zxyj-backend/src/main/java/com/zxyj/mapper/MediaRecordMapper.java

@@ -0,0 +1,21 @@
+package com.zxyj.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.zxyj.entity.MediaRecord;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.List;
+
+@Mapper
+public interface MediaRecordMapper extends BaseMapper<MediaRecord> {
+
+    /**
+     * 根据任务ID获取所有媒体记录
+     */
+    List<MediaRecord> selectByTaskId(Long taskId);
+
+    /**
+     * 根据创建者ID获取所有媒体记录
+     */
+    List<MediaRecord> selectByCreatorId(Long creatorId);
+}

+ 9 - 0
zxyj-backend/src/main/java/com/zxyj/mapper/MembershipLevelMapper.java

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

+ 9 - 0
zxyj-backend/src/main/java/com/zxyj/mapper/PaymentOrderMapper.java

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

+ 9 - 0
zxyj-backend/src/main/java/com/zxyj/mapper/RewardWishlistMapper.java

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

+ 9 - 0
zxyj-backend/src/main/java/com/zxyj/mapper/TaskPlanInstanceMapper.java

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

+ 9 - 0
zxyj-backend/src/main/java/com/zxyj/mapper/TaskPlanItemMapper.java

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

+ 9 - 0
zxyj-backend/src/main/java/com/zxyj/mapper/TaskTemplateItemMapper.java

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

+ 9 - 0
zxyj-backend/src/main/java/com/zxyj/mapper/TaskTemplateMapper.java

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

+ 9 - 0
zxyj-backend/src/main/java/com/zxyj/mapper/TaskTemplatePackageMapper.java

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

+ 9 - 0
zxyj-backend/src/main/java/com/zxyj/mapper/TeacherMapper.java

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

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

@@ -34,6 +34,15 @@ public class DatabaseSyncService {
         ENTITY_TABLE_MAP.put(Reward.class, "rewards");
         ENTITY_TABLE_MAP.put(PointsLog.class, "points_log");
         ENTITY_TABLE_MAP.put(VerificationCode.class, "verification_codes");
+        // 老师相关
+        ENTITY_TABLE_MAP.put(Teacher.class, "teachers");
+        ENTITY_TABLE_MAP.put(AssessmentAppointment.class, "assessment_appointments");
+        ENTITY_TABLE_MAP.put(DanAssessmentResult.class, "dan_assessment_results");
+        ENTITY_TABLE_MAP.put(GrowthPlan.class, "growth_plans");
+        // 会员相关
+        ENTITY_TABLE_MAP.put(MembershipLevel.class, "membership_levels");
+        ENTITY_TABLE_MAP.put(FamilyMembership.class, "family_memberships");
+        ENTITY_TABLE_MAP.put(PaymentOrder.class, "payment_orders");
     }
 
     /**

+ 110 - 0
zxyj-backend/src/main/java/com/zxyj/service/GuideFamilyService.java

@@ -0,0 +1,110 @@
+package com.zxyj.service;
+
+import com.zxyj.entity.GuideFamily;
+import com.zxyj.mapper.GuideFamilyMapper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.Date;
+import java.util.List;
+import java.util.UUID;
+
+@Service
+public class GuideFamilyService {
+
+    @Autowired
+    private GuideFamilyMapper guideFamilyMapper;
+
+    /**
+     * 生成绑定邀请码
+     */
+    public String generateBindToken(Long guideId, Long familyId) {
+        GuideFamily gf = new GuideFamily();
+        gf.setGuideId(guideId);
+        gf.setFamilyId(familyId);
+        gf.setStatus("binding");
+        gf.setBoundAt(new Date());
+        gf.setCreatedAt(new Date());
+        gf.setUpdatedAt(new Date());
+        guideFamilyMapper.insert(gf);
+        
+        // 生成邀请令牌
+        return UUID.randomUUID().toString().replace("-", "").substring(0, 16);
+    }
+
+    /**
+     * 获取指导师绑定的家庭列表
+     */
+    public List<GuideFamily> getFamiliesByGuide(Long guideId) {
+        return guideFamilyMapper.selectList(
+            new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<GuideFamily>()
+                .eq("guide_id", guideId)
+                .orderByDesc("created_at")
+        );
+    }
+
+    /**
+     * 获取家庭绑定的指导师列表
+     */
+    public List<GuideFamily> getGuidesByFamily(Long familyId) {
+        return guideFamilyMapper.selectList(
+            new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<GuideFamily>()
+                .eq("family_id", familyId)
+                .orderByDesc("created_at")
+        );
+    }
+
+    /**
+     * 确认绑定
+     */
+    public boolean confirmBind(Long guideId, Long familyId, String serviceType, java.math.BigDecimal servicePrice) {
+        GuideFamily gf = guideFamilyMapper.selectOne(
+            new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<GuideFamily>()
+                .eq("guide_id", guideId)
+                .eq("family_id", familyId)
+        );
+        
+        if (gf == null) {
+            gf = new GuideFamily();
+            gf.setGuideId(guideId);
+            gf.setFamilyId(familyId);
+            gf.setStatus("binding");
+            gf.setBoundAt(new Date());
+            gf.setCreatedAt(new Date());
+        }
+        
+        gf.setServiceType(serviceType);
+        gf.setServicePrice(servicePrice);
+        gf.setUpdatedAt(new Date());
+        
+        if (gf.getId() == null) {
+            return guideFamilyMapper.insert(gf) > 0;
+        } else {
+            return guideFamilyMapper.updateById(gf) > 0;
+        }
+    }
+
+    /**
+     * 解除绑定
+     */
+    public boolean unbind(Long id) {
+        GuideFamily gf = new GuideFamily();
+        gf.setId(id);
+        gf.setStatus("cancelled");
+        gf.setUpdatedAt(new Date());
+        return guideFamilyMapper.updateById(gf) > 0;
+    }
+
+    /**
+     * 检查绑定关系是否存在
+     */
+    public boolean isBound(Long guideId, Long familyId) {
+        GuideFamily gf = guideFamilyMapper.selectOne(
+            new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<GuideFamily>()
+                .eq("guide_id", guideId)
+                .eq("family_id", familyId)
+                .eq("status", "binding")
+        );
+        return gf != null;
+    }
+}

+ 34 - 0
zxyj-backend/src/main/java/com/zxyj/service/MediaRecordService.java

@@ -0,0 +1,34 @@
+package com.zxyj.service;
+
+import com.zxyj.entity.MediaRecord;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.util.List;
+
+public interface MediaRecordService {
+
+    /**
+     * 上传媒体文件(图片/视频/音频)
+     */
+    MediaRecord uploadMedia(MultipartFile file, Long taskId, Long creatorId, String description);
+
+    /**
+     * 保存文字记录
+     */
+    MediaRecord saveTextRecord(Long taskId, Long creatorId, String content);
+
+    /**
+     * 获取任务的所有媒体记录
+     */
+    List<MediaRecord> getMediaByTaskId(Long taskId);
+
+    /**
+     * 删除媒体记录
+     */
+    boolean deleteMedia(Long mediaId, Long userId);
+
+    /**
+     * 批量关联媒体到任务
+     */
+    boolean attachMediaToTask(Long taskId, List<Long> mediaIds);
+}

+ 241 - 0
zxyj-backend/src/main/java/com/zxyj/service/MembershipService.java

@@ -0,0 +1,241 @@
+package com.zxyj.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.zxyj.dto.*;
+import com.zxyj.entity.*;
+import com.zxyj.mapper.*;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.math.BigDecimal;
+import java.util.Date;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Service
+public class MembershipService {
+
+    @Resource
+    private MembershipLevelMapper levelMapper;
+
+    @Resource
+    private FamilyMembershipMapper membershipMapper;
+
+    @Resource
+    private FamilyMapper familyMapper;
+
+    /**
+     * 获取所有会员等级
+     */
+    public List<MembershipLevelDTO> getAllLevels() {
+        List<MembershipLevel> levels = levelMapper.selectList(null);
+        return levels.stream().map(this::toLevelDTO).collect(Collectors.toList());
+    }
+
+    /**
+     * 获取家庭的会员信息
+     */
+    public FamilyMembershipDTO getFamilyMembership(Long familyId) {
+        FamilyMembership membership = membershipMapper.selectOne(
+                new LambdaQueryWrapper<FamilyMembership>()
+                        .eq(FamilyMembership::getFamilyId, familyId)
+                        .orderByDesc(FamilyMembership::getEndDate)
+                        .last("LIMIT 1")
+        );
+        
+        if (membership == null) {
+            return null;
+        }
+        
+        return toMembershipDTO(membership);
+    }
+
+    /**
+     * 获取家庭当前会员等级
+     */
+    public MembershipLevelDTO getCurrentLevel(Long familyId) {
+        FamilyMembershipDTO membership = getFamilyMembership(familyId);
+        if (membership == null || !membership.getIsActive()) {
+            // 返回免费版
+            MembershipLevel freeLevel = levelMapper.selectOne(
+                    new LambdaQueryWrapper<MembershipLevel>()
+                            .eq(MembershipLevel::getLevelCode, "FREE")
+            );
+            return freeLevel != null ? toLevelDTO(freeLevel) : null;
+        }
+        
+        MembershipLevel level = levelMapper.selectOne(
+                new LambdaQueryWrapper<MembershipLevel>()
+                        .eq(MembershipLevel::getLevelCode, membership.getLevelCode())
+        );
+        
+        return level != null ? toLevelDTO(level) : null;
+    }
+
+    /**
+     * 检查功能权限
+     */
+    public boolean hasFeature(Long familyId, String feature) {
+        MembershipLevelDTO level = getCurrentLevel(familyId);
+        if (level == null || level.getFeatures() == null) {
+            return false;
+        }
+        
+        // 解析features JSON,判断是否包含该功能
+        return level.getFeatures().contains(feature);
+    }
+
+    /**
+     * 创建订单
+     */
+    public PaymentOrderDTO createOrder(Long familyId, String levelCode, String paymentType) {
+        MembershipLevel level = levelMapper.selectOne(
+                new LambdaQueryWrapper<MembershipLevel>()
+                        .eq(MembershipLevel::getLevelCode, levelCode)
+        );
+        
+        if (level == null) {
+            throw new RuntimeException("会员等级不存在");
+        }
+        
+        BigDecimal amount = "monthly".equals(paymentType) ? level.getPriceMonthly() : level.getPriceYearly();
+        
+        PaymentOrder order = new PaymentOrder();
+        order.setOrderNo(generateOrderNo());
+        order.setFamilyId(familyId);
+        order.setLevelCode(levelCode);
+        order.setPaymentType(paymentType);
+        order.setAmount(amount);
+        order.setStatus("pending");
+        order.setCreatedAt(new Date());
+        order.setUpdatedAt(new Date());
+        
+        // 这里需要注入PaymentOrderMapper
+        // orderMapper.insert(order);
+        
+        PaymentOrderDTO dto = new PaymentOrderDTO();
+        dto.setOrderNo(order.getOrderNo());
+        dto.setAmount(order.getAmount());
+        dto.setStatus(order.getStatus());
+        
+        return dto;
+    }
+
+    /**
+     * 处理支付回调
+     */
+    public boolean processPaymentCallback(String orderNo, String transactionId, String payMethod) {
+        // 查找订单
+        // PaymentOrder order = orderMapper.selectOne(...)
+        // if (order == null) return false;
+        // 
+        // order.setStatus("paid");
+        // order.setTransactionId(transactionId);
+        // order.setPayMethod(payMethod);
+        // order.setPaidAt(new Date());
+        // order.setUpdatedAt(new Date());
+        // orderMapper.updateById(order);
+        // 
+        // // 开通/续费会员
+        // MembershipLevel level = levelMapper.selectOne(...)
+        // 
+        // FamilyMembership membership = new FamilyMembership();
+        // membership.setFamilyId(order.getFamilyId());
+        // membership.setLevelCode(order.getLevelCode());
+        // membership.setStartDate(new Date());
+        // membership.setEndDate(calculateEndDate(order.getPaymentType()));
+        // membership.setPaymentStatus("paid");
+        // membership.setOrderNo(orderNo);
+        // membership.setTransactionId(transactionId);
+        // membership.setAmount(order.getAmount());
+        // membershipMapper.insert(membership);
+        // 
+        // return true;
+        
+        return true; // 简化实现
+    }
+
+    /**
+     * 检查会员是否可使用某功能
+     */
+    public boolean canUseFeature(Long familyId, String feature) {
+        MembershipLevelDTO currentLevel = getCurrentLevel(familyId);
+        
+        if (currentLevel == null) {
+            return false;
+        }
+        
+        // 免费版基础功能
+        if ("FREE".equals(currentLevel.getLevelCode())) {
+            return "basic_tasks".equals(feature) || "points_system".equals(feature);
+        }
+        
+        return hasFeature(familyId, feature);
+    }
+
+    private String generateOrderNo() {
+        return "ORD" + System.currentTimeMillis();
+    }
+
+    private Date calculateEndDate(String paymentType) {
+        Date now = new Date();
+        if ("yearly".equals(paymentType)) {
+            return new Date(now.getTime() + 365L * 24 * 60 * 60 * 1000);
+        }
+        return new Date(now.getTime() + 30L * 24 * 60 * 60 * 1000);
+    }
+
+    private MembershipLevelDTO toLevelDTO(MembershipLevel level) {
+        MembershipLevelDTO dto = new MembershipLevelDTO();
+        dto.setId(level.getId());
+        dto.setLevelCode(level.getLevelCode());
+        dto.setLevelName(level.getLevelName());
+        dto.setLevelDesc(level.getLevelDesc());
+        dto.setPriceMonthly(level.getPriceMonthly());
+        dto.setPriceYearly(level.getPriceYearly());
+        dto.setFeatures(level.getFeatures());
+        dto.setMaxChildren(level.getMaxChildren());
+        dto.setMaxTasksPerDay(level.getMaxTasksPerDay());
+        dto.setAiReviewEnabled(level.getAiReviewEnabled());
+        dto.setPrioritySupport(level.getPrioritySupport());
+        dto.setTeacherConsultation(level.getTeacherConsultation());
+        return dto;
+    }
+
+    private FamilyMembershipDTO toMembershipDTO(FamilyMembership membership) {
+        FamilyMembershipDTO dto = new FamilyMembershipDTO();
+        dto.setId(membership.getId());
+        dto.setFamilyId(membership.getFamilyId());
+        dto.setLevelCode(membership.getLevelCode());
+        dto.setStartDate(membership.getStartDate());
+        dto.setEndDate(membership.getEndDate());
+        dto.setPaymentMethod(membership.getPaymentMethod());
+        dto.setPaymentStatus(membership.getPaymentStatus());
+        dto.setOrderNo(membership.getOrderNo());
+        dto.setAmount(membership.getAmount());
+        dto.setAutoRenew(membership.getAutoRenew());
+        
+        // 计算是否在会员期内
+        Date now = new Date();
+        boolean isActive = "paid".equals(membership.getPaymentStatus()) && 
+                now.after(membership.getStartDate()) && 
+                now.before(membership.getEndDate());
+        dto.setIsActive(isActive);
+        
+        if (isActive) {
+            long daysRemaining = (membership.getEndDate().getTime() - now.getTime()) / (24 * 60 * 60 * 1000);
+            dto.setDaysRemaining(daysRemaining);
+        }
+        
+        // 获取等级名称
+        MembershipLevel level = levelMapper.selectOne(
+                new LambdaQueryWrapper<MembershipLevel>()
+                        .eq(MembershipLevel::getLevelCode, membership.getLevelCode())
+        );
+        if (level != null) {
+            dto.setLevelName(level.getLevelName());
+        }
+        
+        return dto;
+    }
+}

+ 3 - 3
zxyj-backend/src/main/java/com/zxyj/service/PointsService.java

@@ -6,7 +6,7 @@ import com.zxyj.entity.Child;
 import com.zxyj.entity.PointsLog;
 import com.zxyj.mapper.ChildMapper;
 import com.zxyj.mapper.PointsLogMapper;
-import org.springframework.beans.factory.annotation.Autowired;
+import javax.annotation.Resource;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
@@ -17,10 +17,10 @@ import java.util.Map;
 @Service
 public class PointsService {
 
-    @Autowired
+    @Resource
     private ChildMapper childMapper;
 
-    @Autowired
+    @Resource
     private PointsLogMapper pointsLogMapper;
 
     public Map<String, Object> getBalance(Long childId) {

+ 5 - 5
zxyj-backend/src/main/java/com/zxyj/service/RewardService.java

@@ -10,7 +10,7 @@ import com.zxyj.entity.Reward;
 import com.zxyj.mapper.ChildMapper;
 import com.zxyj.mapper.PointsLogMapper;
 import com.zxyj.mapper.RewardMapper;
-import org.springframework.beans.factory.annotation.Autowired;
+import javax.annotation.Resource;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
@@ -23,16 +23,16 @@ import java.util.stream.Collectors;
 @Service
 public class RewardService {
 
-    @Autowired
+    @Resource
     private RewardMapper rewardMapper;
 
-    @Autowired
+    @Resource
     private ChildMapper childMapper;
 
-    @Autowired
+    @Resource
     private PointsLogMapper pointsLogMapper;
 
-    @Autowired
+    @Resource
     private UserService userService;
 
     @Transactional

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

@@ -0,0 +1,118 @@
+package com.zxyj.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.zxyj.dto.CreateRewardWishlistDTO;
+import com.zxyj.entity.RewardWishlist;
+import com.zxyj.entity.User;
+import com.zxyj.mapper.RewardWishlistMapper;
+import com.zxyj.mapper.UserMapper;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.List;
+
+@Service
+public class RewardWishlistService {
+
+    @Resource
+    private RewardWishlistMapper rewardWishlistMapper;
+
+    @Resource
+    private UserMapper userMapper;
+
+    /**
+     * 创建家长心愿
+     */
+    public Long create(Long userId, CreateRewardWishlistDTO dto) {
+        User user = userMapper.selectById(userId);
+        if (user == null) {
+            throw new RuntimeException("用户不存在");
+        }
+
+        RewardWishlist wishlist = new RewardWishlist();
+        wishlist.setFamilyId(user.getFamilyId());
+        wishlist.setUserId(userId);
+        wishlist.setTitle(dto.getTitle());
+        wishlist.setDescription(dto.getDescription());
+        wishlist.setPointsRequired(dto.getPointsRequired() != null ? dto.getPointsRequired() : 0);
+        wishlist.setCategory(dto.getCategory());
+        wishlist.setStatus("pending");
+        wishlist.setExchangeApproved(0);
+        wishlist.setCreatedAt(new Date());
+        wishlist.setUpdatedAt(new Date());
+
+        rewardWishlistMapper.insert(wishlist);
+        return wishlist.getId();
+    }
+
+    /**
+     * 获取家长的心愿单列表
+     */
+    public List<RewardWishlist> getByUserId(Long userId) {
+        return rewardWishlistMapper.selectList(new LambdaQueryWrapper<RewardWishlist>()
+                .eq(RewardWishlist::getUserId, userId)
+                .orderByDesc(RewardWishlist::getCreatedAt));
+    }
+
+    /**
+     * 获取家庭的心愿单列表
+     */
+    public List<RewardWishlist> getByFamilyId(Long familyId) {
+        return rewardWishlistMapper.selectList(new LambdaQueryWrapper<RewardWishlist>()
+                .eq(RewardWishlist::getFamilyId, familyId)
+                .orderByDesc(RewardWishlist::getCreatedAt));
+    }
+
+    /**
+     * 兑换心愿 (扣积分)
+     */
+    public boolean exchange(Long wishlistId, Long userId) {
+        RewardWishlist wishlist = rewardWishlistMapper.selectById(wishlistId);
+        if (wishlist == null || !wishlist.getUserId().equals(userId)) {
+            return false;
+        }
+
+        User user = userMapper.selectById(userId);
+        if (user == null || user.getTotalPoints() == null || user.getTotalPoints() < wishlist.getPointsRequired()) {
+            return false;
+        }
+
+        // 扣减积分
+        user.setTotalPoints(user.getTotalPoints() - wishlist.getPointsRequired());
+        user.setUpdatedAt(new Date());
+        userMapper.updateById(user);
+
+        // 更新心愿状态
+        wishlist.setStatus("exchanged");
+        wishlist.setExchangeApproved(1);
+        wishlist.setExchangedAt(new Date());
+        wishlist.setUpdatedAt(new Date());
+        rewardWishlistMapper.updateById(wishlist);
+
+        return true;
+    }
+
+    /**
+     * 审核心愿 (家长自己审核自己的心愿,可能需要管理员审批)
+     */
+    public boolean approve(Long wishlistId, boolean approved) {
+        RewardWishlist wishlist = rewardWishlistMapper.selectById(wishlistId);
+        if (wishlist == null) {
+            return false;
+        }
+
+        if (approved) {
+            wishlist.setStatus("exchanged");
+            wishlist.setExchangeApproved(1);
+            wishlist.setExchangedAt(new Date());
+        } else {
+            wishlist.setStatus("rejected");
+            wishlist.setExchangeApproved(0);
+        }
+        wishlist.setUpdatedAt(new Date());
+        rewardWishlistMapper.updateById(wishlist);
+
+        return true;
+    }
+}

+ 21 - 0
zxyj-backend/src/main/java/com/zxyj/service/StorageService.java

@@ -0,0 +1,21 @@
+package com.zxyj.service;
+
+import org.springframework.web.multipart.MultipartFile;
+
+public interface StorageService {
+
+    /**
+     * 存储文件并返回访问URL
+     */
+    String storeFile(MultipartFile file);
+
+    /**
+     * 删除文件
+     */
+    void deleteFile(String fileUrl);
+
+    /**
+     * 获取文件完整路径
+     */
+    String getFilePath(String filename);
+}

+ 167 - 0
zxyj-backend/src/main/java/com/zxyj/service/TaskPlanService.java

@@ -0,0 +1,167 @@
+package com.zxyj.service;
+
+import com.zxyj.entity.TaskPlanInstance;
+import com.zxyj.entity.TaskPlanItem;
+import com.zxyj.entity.TaskTemplateItem;
+import com.zxyj.mapper.TaskPlanInstanceMapper;
+import com.zxyj.mapper.TaskPlanItemMapper;
+import com.zxyj.mapper.TaskTemplateItemMapper;
+import com.zxyj.dto.ApplyPackageDTO;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Service
+public class TaskPlanService {
+
+    @Autowired
+    private TaskPlanInstanceMapper instanceMapper;
+
+    @Autowired
+    private TaskPlanItemMapper planItemMapper;
+
+    @Autowired
+    private TaskTemplateItemMapper templateItemMapper;
+
+    @Autowired
+    private TaskTemplatePackageService packageService;
+
+    /**
+     * 家长应用套餐 - 创建任务计划实例
+     */
+    @Transactional
+    public TaskPlanInstance applyPackage(ApplyPackageDTO dto, Long familyId) {
+        // 获取套餐信息
+        var pkg = packageService.getPackageById(dto.getPackageId());
+        if (pkg == null) {
+            throw new RuntimeException("套餐不存在");
+        }
+
+        // 获取选中的任务项
+        List<TaskTemplateItem> selectedItems;
+        if (dto.getSelectedItemIds() != null && !dto.getSelectedItemIds().isEmpty()) {
+            selectedItems = dto.getSelectedItemIds().stream()
+                .map(templateItemMapper::selectById)
+                .filter(item -> item != null && item.getPackageId().equals(dto.getPackageId()))
+                .collect(Collectors.toList());
+        } else {
+            // 未选择则获取所有任务项
+            selectedItems = templateItemMapper.selectList(
+                new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<TaskTemplateItem>()
+                    .eq("package_id", dto.getPackageId())
+                    .orderByAsc("sort_order")
+            );
+        }
+
+        // 创建实例
+        TaskPlanInstance instance = new TaskPlanInstance();
+        instance.setPackageId(dto.getPackageId());
+        instance.setFamilyId(familyId);
+        instance.setChildId(dto.getChildId());
+        instance.setName(pkg.getName());
+        
+        Date now = new Date();
+        instance.setStartDate(now);
+        instance.setEndDate(new Date(now.getTime() + (long) pkg.getCycleDays() * 24 * 60 * 60 * 1000));
+        instance.setStatus("active");
+        instance.setTotalDays(pkg.getCycleDays());
+        instance.setCompletedDays(0);
+        instance.setCreatedAt(now);
+        instance.setUpdatedAt(now);
+        
+        instanceMapper.insert(instance);
+
+        // 复制任务项到实例
+        for (TaskTemplateItem templateItem : selectedItems) {
+            TaskPlanItem planItem = new TaskPlanItem();
+            planItem.setInstanceId(instance.getId());
+            planItem.setTemplateItemId(templateItem.getId());
+            planItem.setTaskName(templateItem.getTaskName());
+            planItem.setDescription(templateItem.getDescription());
+            planItem.setPoints(templateItem.getPoints());
+            planItem.setFrequency(templateItem.getFrequency());
+            planItem.setDuration(templateItem.getDuration());
+            planItem.setCreatedAt(now);
+            planItem.setUpdatedAt(now);
+            planItemMapper.insert(planItem);
+        }
+
+        return instance;
+    }
+
+    /**
+     * 获取家庭的任务计划列表
+     */
+    public List<TaskPlanInstance> getFamilyPlans(Long familyId) {
+        return instanceMapper.selectList(
+            new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<TaskPlanInstance>()
+                .eq("family_id", familyId)
+                .orderByDesc("created_at")
+        );
+    }
+
+    /**
+     * 获取计划详情
+     */
+    public TaskPlanInstance getPlanById(Long id) {
+        return instanceMapper.selectById(id);
+    }
+
+    /**
+     * 获取计划的任务项列表
+     */
+    public List<TaskPlanItem> getPlanItems(Long instanceId) {
+        return planItemMapper.selectList(
+            new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<TaskPlanItem>()
+                .eq("instance_id", instanceId)
+        );
+    }
+
+    /**
+     * 取消计划
+     */
+    public boolean cancelPlan(Long id) {
+        TaskPlanInstance instance = instanceMapper.selectById(id);
+        if (instance == null || !"active".equals(instance.getStatus())) {
+            return false;
+        }
+        instance.setStatus("cancelled");
+        instance.setUpdatedAt(new Date());
+        return instanceMapper.updateById(instance) > 0;
+    }
+
+    /**
+     * 更新完成天数
+     */
+    public boolean updateCompletedDays(Long id, int days) {
+        TaskPlanInstance instance = instanceMapper.selectById(id);
+        if (instance == null) return false;
+        
+        instance.setCompletedDays(days);
+        
+        // 如果完成天数达到总天数,自动完成
+        if (instance.getCompletedDays() >= instance.getTotalDays()) {
+            instance.setStatus("completed");
+        }
+        
+        instance.setUpdatedAt(new Date());
+        return instanceMapper.updateById(instance) > 0;
+    }
+
+    /**
+     * 获取孩子的有效计划
+     */
+    public List<TaskPlanInstance> getChildPlans(Long childId) {
+        return instanceMapper.selectList(
+            new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<TaskPlanInstance>()
+                .eq("child_id", childId)
+                .eq("status", "active")
+                .orderByDesc("created_at")
+        );
+    }
+}

+ 261 - 0
zxyj-backend/src/main/java/com/zxyj/service/TaskTemplatePackageService.java

@@ -0,0 +1,261 @@
+package com.zxyj.service;
+
+import com.zxyj.entity.TaskTemplatePackage;
+import com.zxyj.entity.TaskTemplateItem;
+import com.zxyj.mapper.TaskTemplatePackageMapper;
+import com.zxyj.mapper.TaskTemplateItemMapper;
+import com.zxyj.dto.CreatePackageDTO;
+import com.zxyj.dto.CreatePackageItemDTO;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigDecimal;
+import java.util.Date;
+import java.util.List;
+import java.util.UUID;
+
+@Service
+public class TaskTemplatePackageService {
+
+    @Autowired
+    private TaskTemplatePackageMapper packageMapper;
+
+    @Autowired
+    private TaskTemplateItemMapper itemMapper;
+
+    private static final BigDecimal MAX_PLATFORM_FEE_RATE = new BigDecimal("0.10");
+
+    /**
+     * 创建套餐
+     */
+    @Transactional
+    public TaskTemplatePackage createPackage(CreatePackageDTO dto, Long creatorId) {
+        TaskTemplatePackage pkg = new TaskTemplatePackage();
+        pkg.setCreatorId(creatorId);
+        pkg.setName(dto.getName());
+        pkg.setDescription(dto.getDescription());
+        pkg.setCycleDays(dto.getCycleDays());
+        pkg.setCategory(dto.getCategory());
+        pkg.setTags(dto.getTags());
+        pkg.setPrice(dto.getPrice());
+        
+        // 计算平台服务费 (不超过10%)
+        BigDecimal feeRate = new BigDecimal("0.10");
+        pkg.setPlatformFee(dto.getPrice().multiply(feeRate));
+        
+        pkg.setIsPublic(dto.getIsPublic() != null ? dto.getIsPublic() : 0);
+        pkg.setCoverImage(dto.getCoverImage());
+        pkg.setStatus("draft");
+        pkg.setShareCode(generateShareCode());
+        pkg.setCreatedAt(new Date());
+        pkg.setUpdatedAt(new Date());
+        
+        packageMapper.insert(pkg);
+        return pkg;
+    }
+
+    /**
+     * 更新套餐
+     */
+    public boolean updatePackage(Long id, CreatePackageDTO dto) {
+        TaskTemplatePackage pkg = packageMapper.selectById(id);
+        if (pkg == null) return false;
+        
+        if (dto.getName() != null) pkg.setName(dto.getName());
+        if (dto.getDescription() != null) pkg.setDescription(dto.getDescription());
+        if (dto.getCycleDays() != null) pkg.setCycleDays(dto.getCycleDays());
+        if (dto.getCategory() != null) pkg.setCategory(dto.getCategory());
+        if (dto.getTags() != null) pkg.setTags(dto.getTags());
+        if (dto.getPrice() != null) {
+            pkg.setPrice(dto.getPrice());
+            pkg.setPlatformFee(dto.getPrice().multiply(new BigDecimal("0.10")));
+        }
+        if (dto.getIsPublic() != null) pkg.setIsPublic(dto.getIsPublic());
+        if (dto.getCoverImage() != null) pkg.setCoverImage(dto.getCoverImage());
+        pkg.setUpdatedAt(new Date());
+        
+        return packageMapper.updateById(pkg) > 0;
+    }
+
+    /**
+     * 获取套餐详情
+     */
+    public TaskTemplatePackage getPackageById(Long id) {
+        return packageMapper.selectById(id);
+    }
+
+    /**
+     * 获取我的套餐列表
+     */
+    public List<TaskTemplatePackage> getMyPackages(Long creatorId) {
+        return packageMapper.selectList(
+            new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<TaskTemplatePackage>()
+                .eq("creator_id", creatorId)
+                .orderByDesc("created_at")
+        );
+    }
+
+    /**
+     * 获取公开套餐列表 (市场)
+     */
+    public List<TaskTemplatePackage> getPublicPackages(String category) {
+        var wrapper = new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<TaskTemplatePackage>()
+            .eq("is_public", 1)
+            .eq("status", "approved");
+        
+        if (category != null && !category.isEmpty()) {
+            wrapper.eq("category", category);
+        }
+        
+        return wrapper.orderByDesc("created_at").list();
+    }
+
+    /**
+     * 通过分享码获取套餐
+     */
+    public TaskTemplatePackage getPackageByShareCode(String shareCode) {
+        return packageMapper.selectOne(
+            new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<TaskTemplatePackage>()
+                .eq("share_code", shareCode)
+        );
+    }
+
+    /**
+     * 提交审核
+     */
+    public boolean submitForReview(Long id) {
+        TaskTemplatePackage pkg = packageMapper.selectById(id);
+        if (pkg == null || !"draft".equals(pkg.getStatus())) {
+            return false;
+        }
+        pkg.setStatus("pending");
+        pkg.setUpdatedAt(new Date());
+        return packageMapper.updateById(pkg) > 0;
+    }
+
+    /**
+     * 审核通过
+     */
+    public boolean approvePackage(Long id, Long reviewerId) {
+        TaskTemplatePackage pkg = packageMapper.selectById(id);
+        if (pkg == null || !"pending".equals(pkg.getStatus())) {
+            return false;
+        }
+        pkg.setStatus("approved");
+        pkg.setReviewerId(reviewerId);
+        pkg.setReviewedAt(new Date());
+        pkg.setUpdatedAt(new Date());
+        return packageMapper.updateById(pkg) > 0;
+    }
+
+    /**
+     * 审核拒绝
+     */
+    public boolean rejectPackage(Long id, Long reviewerId, String reason) {
+        TaskTemplatePackage pkg = packageMapper.selectById(id);
+        if (pkg == null || !"pending".equals(pkg.getStatus())) {
+            return false;
+        }
+        pkg.setStatus("rejected");
+        pkg.setReviewerId(reviewerId);
+        pkg.setReviewedAt(new Date());
+        pkg.setRejectReason(reason);
+        pkg.setUpdatedAt(new Date());
+        return packageMapper.updateById(pkg) > 0;
+    }
+
+    /**
+     * 发布/下架
+     */
+    public boolean publishPackage(Long id, boolean publish) {
+        TaskTemplatePackage pkg = packageMapper.selectById(id);
+        if (pkg == null || !"approved".equals(pkg.getStatus())) {
+            return false;
+        }
+        pkg.setStatus(publish ? "approved" : "offline");
+        pkg.setUpdatedAt(new Date());
+        return packageMapper.updateById(pkg) > 0;
+    }
+
+    // ========== 套餐任务项管理 ==========
+
+    /**
+     * 添加任务项
+     */
+    public TaskTemplateItem addItem(CreatePackageItemDTO dto) {
+        TaskTemplateItem item = new TaskTemplateItem();
+        item.setPackageId(dto.getPackageId());
+        item.setTaskName(dto.getTaskName());
+        item.setDescription(dto.getDescription());
+        item.setPoints(dto.getPoints());
+        item.setFrequency(dto.getFrequency());
+        item.setDuration(dto.getDuration());
+        item.setIsRequired(dto.getIsRequired());
+        item.setSortOrder(dto.getSortOrder());
+        item.setCreatedAt(new Date());
+        item.setUpdatedAt(new Date());
+        
+        itemMapper.insert(item);
+        return item;
+    }
+
+    /**
+     * 更新任务项
+     */
+    public boolean updateItem(Long id, CreatePackageItemDTO dto) {
+        TaskTemplateItem item = itemMapper.selectById(id);
+        if (item == null) return false;
+        
+        if (dto.getTaskName() != null) item.setTaskName(dto.getTaskName());
+        if (dto.getDescription() != null) item.setDescription(dto.getDescription());
+        if (dto.getPoints() != null) item.setPoints(dto.getPoints());
+        if (dto.getFrequency() != null) item.setFrequency(dto.getFrequency());
+        if (dto.getDuration() != null) item.setDuration(dto.getDuration());
+        if (dto.getIsRequired() != null) item.setIsRequired(dto.getIsRequired());
+        if (dto.getSortOrder() != null) item.setSortOrder(dto.getSortOrder());
+        item.setUpdatedAt(new Date());
+        
+        return itemMapper.updateById(item) > 0;
+    }
+
+    /**
+     * 删除任务项
+     */
+    public boolean deleteItem(Long id) {
+        return itemMapper.deleteById(id) > 0;
+    }
+
+    /**
+     * 获取套餐的任务项列表
+     */
+    public List<TaskTemplateItem> getPackageItems(Long packageId) {
+        return itemMapper.selectList(
+            new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<TaskTemplateItem>()
+                .eq("package_id", packageId)
+                .orderByAsc("sort_order")
+        );
+    }
+
+    /**
+     * 获取待审核的套餐列表
+     */
+    public List<TaskTemplatePackage> getPendingPackages() {
+        return packageMapper.selectList(
+            new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<TaskTemplatePackage>()
+                .eq("status", "pending")
+                .orderByAsc("created_at")
+        );
+    }
+
+    /**
+     * 生成唯一分享码
+     */
+    private String generateShareCode() {
+        String code;
+        do {
+            code = UUID.randomUUID().toString().replace("-", "").substring(0, 8).toUpperCase();
+        } while (getPackageByShareCode(code) != null);
+        return code;
+    }
+}

+ 308 - 0
zxyj-backend/src/main/java/com/zxyj/service/TeacherService.java

@@ -0,0 +1,308 @@
+package com.zxyj.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.zxyj.dto.*;
+import com.zxyj.entity.*;
+import com.zxyj.mapper.*;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Service
+public class TeacherService {
+
+    @Resource
+    private TeacherMapper teacherMapper;
+
+    @Resource
+    private AssessmentAppointmentMapper appointmentMapper;
+
+    @Resource
+    private DanAssessmentResultMapper assessmentResultMapper;
+
+    @Resource
+    private UserMapper userMapper;
+
+    @Resource
+    private ChildMapper childMapper;
+
+    @Resource
+    private UserService userService;
+
+    /**
+     * 获取老师信息
+     */
+    public TeacherDTO getTeacherInfo(Long userId) {
+        Teacher teacher = teacherMapper.selectOne(new LambdaQueryWrapper<Teacher>()
+                .eq(Teacher::getUserId, userId));
+        
+        if (teacher == null) {
+            return null;
+        }
+        
+        return toDTO(teacher);
+    }
+
+    /**
+     * 创建老师信息
+     */
+    public TeacherDTO createTeacher(Long userId, TeacherDTO dto) {
+        User user = userMapper.selectById(userId);
+        if (user == null) {
+            throw new RuntimeException("用户不存在");
+        }
+
+        // 检查是否已是老师
+        Teacher existing = teacherMapper.selectOne(new LambdaQueryWrapper<Teacher>()
+                .eq(Teacher::getUserId, userId));
+        if (existing != null) {
+            throw new RuntimeException("该用户已是老师");
+        }
+
+        Teacher teacher = new Teacher();
+        teacher.setUserId(userId);
+        teacher.setTeacherNo(generateTeacherNo());
+        teacher.setTitle(dto.getTitle());
+        teacher.setSpecialty(dto.getSpecialty());
+        teacher.setBio(dto.getBio());
+        teacher.setExperienceYears(dto.getExperienceYears());
+        teacher.setRating(java.math.BigDecimal.valueOf(5.00));
+        teacher.setTotalAppointments(0);
+        teacher.setStatus("active");
+        teacher.setVerificationStatus("pending");
+        teacher.setCreatedAt(new Date());
+        teacher.setUpdatedAt(new Date());
+        
+        teacherMapper.insert(teacher);
+
+        // 更新用户角色 - 添加 teacher 角色到用户的角色列表
+        userService.addRole(userId, "teacher");
+        // 如果当前不是老师,切换到老师角色
+        if (!"teacher".equals(user.getRole())) {
+            user.setRole("teacher");
+        }
+        user.setUpdatedAt(new Date());
+        userMapper.updateById(user);
+
+        return toDTO(teacher);
+    }
+
+    /**
+     * 获取老师的测评预约列表
+     */
+    public List<AssessmentAppointmentDTO> getAppointments(Long teacherId, String status) {
+        LambdaQueryWrapper<AssessmentAppointment> wrapper = new LambdaQueryWrapper<AssessmentAppointment>()
+                .eq(AssessmentAppointment::getTeacherId, teacherId);
+        
+        if (status != null && !status.isEmpty()) {
+            wrapper.eq(AssessmentAppointment::getStatus, status);
+        }
+        
+        wrapper.orderByDesc(AssessmentAppointment::getAppointmentDate);
+        
+        List<AssessmentAppointment> appointments = appointmentMapper.selectList(wrapper);
+        
+        return appointments.stream().map(this::toAppointmentDTO).collect(Collectors.toList());
+    }
+
+    /**
+     * 创建测评预约
+     */
+    public AssessmentAppointmentDTO createAppointment(Long teacherId, CreateAppointmentDTO dto) {
+        AssessmentAppointment appointment = new AssessmentAppointment();
+        appointment.setTeacherId(teacherId);
+        appointment.setFamilyId(dto.getFamilyId());
+        appointment.setChildId(dto.getChildId());
+        appointment.setAppointmentDate(dto.getAppointmentDate());
+        appointment.setAppointmentTime(dto.getAppointmentTime());
+        appointment.setDurationMinutes(dto.getDurationMinutes());
+        appointment.setAppointmentType(dto.getAppointmentType());
+        appointment.setNotes(dto.getNotes());
+        appointment.setContactPhone(dto.getContactPhone());
+        appointment.setMeetLink(dto.getMeetLink());
+        appointment.setStatus("pending");
+        appointment.setCreatedAt(new Date());
+        appointment.setUpdatedAt(new Date());
+        
+        appointmentMapper.insert(appointment);
+        
+        return toAppointmentDTO(appointment);
+    }
+
+    /**
+     * 确认/完成预约
+     */
+    public AssessmentAppointmentDTO updateAppointmentStatus(Long appointmentId, String status) {
+        AssessmentAppointment appointment = appointmentMapper.selectById(appointmentId);
+        if (appointment == null) {
+            throw new RuntimeException("预约不存在");
+        }
+        
+        appointment.setStatus(status);
+        appointment.setUpdatedAt(new Date());
+        appointmentMapper.updateById(appointment);
+        
+        return toAppointmentDTO(appointment);
+    }
+
+    /**
+     * 创建DAN测评结果
+     */
+    public DanAssessmentResultDTO createAssessmentResult(Long teacherId, CreateAssessmentResultDTO dto) {
+        DanAssessmentResult result = new DanAssessmentResult();
+        result.setChildId(dto.getChildId());
+        result.setTeacherId(teacherId);
+        result.setAppointmentId(dto.getAppointmentId());
+        result.setDanLevel(dto.getDanLevel());
+        result.setAttentionScore(dto.getAttentionScore());
+        result.setFocusScore(dto.getFocusScore());
+        result.setMemoryScore(dto.getMemoryScore());
+        result.setLogicScore(dto.getLogicScore());
+        result.setOverallScore(dto.getOverallScore());
+        result.setAssessmentDate(dto.getAssessmentDate());
+        result.setDurationMinutes(dto.getDurationMinutes());
+        result.setGameRecords(dto.getGameRecords());
+        result.setAnalysisReport(dto.getAnalysisReport());
+        result.setGrowthSuggestions(dto.getGrowthSuggestions());
+        result.setNextReviewDate(dto.getNextReviewDate());
+        result.setStatus("completed");
+        result.setCreatedAt(new Date());
+        result.setUpdatedAt(new Date());
+        
+        assessmentResultMapper.insert(result);
+        
+        // 更新孩子的DAN等级
+        Child child = childMapper.selectById(dto.getChildId());
+        if (child != null) {
+            child.setDanLevel(dto.getDanLevel());
+            child.setUpdatedAt(new Date());
+            childMapper.updateById(child);
+        }
+        
+        // 更新老师的测评次数
+        Teacher teacher = teacherMapper.selectById(teacherId);
+        if (teacher != null) {
+            teacher.setTotalAppointments(teacher.getTotalAppointments() + 1);
+            teacher.setUpdatedAt(new Date());
+            teacherMapper.updateById(teacher);
+        }
+        
+        return toAssessmentResultDTO(result);
+    }
+
+    /**
+     * 获取孩子的测评历史
+     */
+    public List<DanAssessmentResultDTO> getChildAssessmentHistory(Long childId) {
+        List<DanAssessmentResult> results = assessmentResultMapper.selectList(
+                new LambdaQueryWrapper<DanAssessmentResult>()
+                        .eq(DanAssessmentResult::getChildId, childId)
+                        .orderByDesc(DanAssessmentResult::getAssessmentDate)
+        );
+        
+        return results.stream().map(this::toAssessmentResultDTO).collect(Collectors.toList());
+    }
+
+    /**
+     * 获取授权的孩子列表
+     */
+    public List<ChildInfoDTO> getAuthorizedChildren(Long teacherId) {
+        // 这里需要查询家长授权表,获取授权的孩子
+        // 简化实现:返回所有家庭的孩子
+        return null;
+    }
+
+    private String generateTeacherNo() {
+        return "T" + System.currentTimeMillis();
+    }
+
+    private TeacherDTO toDTO(Teacher teacher) {
+        TeacherDTO dto = new TeacherDTO();
+        dto.setId(teacher.getId());
+        dto.setUserId(teacher.getUserId());
+        dto.setTeacherNo(teacher.getTeacherNo());
+        dto.setTitle(teacher.getTitle());
+        dto.setSpecialty(teacher.getSpecialty());
+        dto.setBio(teacher.getBio());
+        dto.setExperienceYears(teacher.getExperienceYears());
+        dto.setRating(teacher.getRating());
+        dto.setTotalAppointments(teacher.getTotalAppointments());
+        dto.setStatus(teacher.getStatus());
+        dto.setVerificationStatus(teacher.getVerificationStatus());
+        dto.setVerifiedAt(teacher.getVerifiedAt());
+        
+        // 获取关联用户信息
+        User user = userMapper.selectById(teacher.getUserId());
+        if (user != null) {
+            dto.setNickname(user.getNickname());
+            dto.setAvatar(user.getAvatar());
+        }
+        
+        return dto;
+    }
+
+    private AssessmentAppointmentDTO toAppointmentDTO(AssessmentAppointment appt) {
+        AssessmentAppointmentDTO dto = new AssessmentAppointmentDTO();
+        dto.setId(appt.getId());
+        dto.setTeacherId(appt.getTeacherId());
+        dto.setFamilyId(appt.getFamilyId());
+        dto.setChildId(appt.getChildId());
+        dto.setAppointmentDate(appt.getAppointmentDate());
+        dto.setAppointmentTime(appt.getAppointmentTime());
+        dto.setDurationMinutes(appt.getDurationMinutes());
+        dto.setStatus(appt.getStatus());
+        dto.setAppointmentType(appt.getAppointmentType());
+        dto.setNotes(appt.getNotes());
+        dto.setContactPhone(appt.getContactPhone());
+        dto.setMeetLink(appt.getMeetLink());
+        dto.setCreatedAt(appt.getCreatedAt());
+        
+        // 补充关联信息
+        Child child = childMapper.selectById(appt.getChildId());
+        if (child != null) {
+            dto.setChildName(child.getNickname());
+        }
+        
+        return dto;
+    }
+
+    private DanAssessmentResultDTO toAssessmentResultDTO(DanAssessmentResult result) {
+        DanAssessmentResultDTO dto = new DanAssessmentResultDTO();
+        dto.setId(result.getId());
+        dto.setChildId(result.getChildId());
+        dto.setTeacherId(result.getTeacherId());
+        dto.setAppointmentId(result.getAppointmentId());
+        dto.setDanLevel(result.getDanLevel());
+        dto.setAttentionScore(result.getAttentionScore());
+        dto.setFocusScore(result.getFocusScore());
+        dto.setMemoryScore(result.getMemoryScore());
+        dto.setLogicScore(result.getLogicScore());
+        dto.setOverallScore(result.getOverallScore());
+        dto.setAssessmentDate(result.getAssessmentDate());
+        dto.setDurationMinutes(result.getDurationMinutes());
+        dto.setGameRecords(result.getGameRecords());
+        dto.setAnalysisReport(result.getAnalysisReport());
+        dto.setGrowthSuggestions(result.getGrowthSuggestions());
+        dto.setNextReviewDate(result.getNextReviewDate());
+        dto.setStatus(result.getStatus());
+        dto.setCreatedAt(result.getCreatedAt());
+        
+        // 补充关联信息
+        Child child = childMapper.selectById(result.getChildId());
+        if (child != null) {
+            dto.setChildName(child.getNickname());
+            dto.setChildNickname(child.getNickname());
+            dto.setDanLevel(child.getDanLevel());
+        }
+        
+        User teacher = userMapper.selectById(result.getTeacherId());
+        if (teacher != null) {
+            dto.setTeacherName(teacher.getNickname());
+        }
+        
+        return dto;
+    }
+}

+ 370 - 34
zxyj-backend/src/main/java/com/zxyj/service/UserService.java

@@ -11,14 +11,14 @@ import com.zxyj.mapper.ChildMapper;
 import com.zxyj.mapper.FamilyMapper;
 import com.zxyj.mapper.UserMapper;
 import com.zxyj.service.VerificationCodeService;
+import com.zxyj.service.WechatService;
 import javax.annotation.Resource;
 import org.springframework.stereotype.Service;
 import org.springframework.util.DigestUtils;
 
 import java.nio.charset.StandardCharsets;
-import java.util.Date;
-import java.util.List;
-import java.util.UUID;
+import java.util.*;
+
 
 @Service
 public class UserService {
@@ -38,6 +38,9 @@ private JwtConfig jwtConfig;
 @Resource
 private VerificationCodeService verificationCodeService;
 
+@Resource
+private WechatService wechatService;
+
     public LoginResultDTO wechatLogin(WechatLoginDTO dto) {
         // 模拟微信登录,实际需要调用微信API获取openid
         String openid = "mock_openid_" + dto.getCode();
@@ -81,45 +84,131 @@ private VerificationCodeService verificationCodeService;
         return result;
     }
 
+/**
+ * 手机号验证码登录
+ */
+public LoginResultDTO phoneLogin(PhoneLoginDTO dto) {
+  // 使用手机号作为唯一标识
+  String phone = dto.getPhone();
+
+  // 先通过手机号查找用户(更准确的判断)
+  User user = userMapper.selectOne(new LambdaQueryWrapper<User>()
+      .eq(User::getPhone, phone));
+
+  boolean isNewUser = false;
+
+  if (user == null) {
+    // 新用户,创建新家庭和用户
+    isNewUser = true;
+
+    Family family = new Family();
+    family.setName("我的家庭");
+    family.setInviteCode(generateInviteCode());
+    family.setCreatedAt(new Date());
+    family.setUpdatedAt(new Date());
+    familyMapper.insert(family);
+
+    user = new User();
+    user.setOpenid("phone:" + phone);
+    user.setUnionid("");
+    user.setFamilyId(family.getId());
+    // 默认角色为家长,可以被覆盖
+    user.setRole(dto.getRole() != null ? dto.getRole() : "parent");
+    user.setNickname(dto.getNickname() != null ? dto.getNickname() : "用户" + phone.substring(7));
+    user.setAvatar(dto.getAvatar());
+    user.setPhone(phone);
+    user.setBirthday(dto.getBirthday());
+    user.setGender(dto.getGender());
+    user.setPassword("");
+    user.setCreatedAt(new Date());
+    user.setUpdatedAt(new Date());
+    userMapper.insert(user);
+  } else {
+    // 老用户,更新信息(如果提供了)
+    if (dto.getNickname() != null && !dto.getNickname().isEmpty()) {
+      user.setNickname(dto.getNickname());
+    }
+    if (dto.getAvatar() != null && !dto.getAvatar().isEmpty()) {
+      user.setAvatar(dto.getAvatar());
+    }
+    user.setUpdatedAt(new Date());
+    userMapper.updateById(user);
+  }
+
+  // 生成token
+  String token = jwtConfig.generateToken(user.getId(), user.getRole());
+
+  LoginResultDTO result = new LoginResultDTO();
+  result.setToken(token);
+  result.setUserId(user.getId());
+  result.setRole(user.getRole());
+  result.setFamilyId(user.getFamilyId());
+  result.setNickname(user.getNickname());
+  result.setIsNewUser(isNewUser);
+
+  return result;
+}
+
     /**
-     * 手机号验证码登录
+     * 微信手机号登录(正确获取openid和手机号)
      */
-    public LoginResultDTO phoneLogin(PhoneLoginDTO dto) {
-        // 使用手机号作为唯一标识
-        String phone = dto.getPhone();
+    public LoginResultDTO wechatPhoneLogin(WechatPhoneLoginDTO dto) {
+        // 1. 通过code获取openid
+        Map<String, String> sessionData = wechatService.code2Session(dto.getCode());
+        String openid = sessionData.get("openid");
+        String unionid = sessionData.get("unionid");
 
-        // 查询用户是否存在
+        // 2. 通过手机号code获取真实手机号
+        String phone = wechatService.getPhoneNumber(dto.getPhoneCode());
+
+        // 3. 先通过openid查找用户
         User user = userMapper.selectOne(new LambdaQueryWrapper<User>()
-                .eq(User::getOpenid, "phone:" + phone));
+                .eq(User::getOpenid, openid));
 
         boolean isNewUser = false;
 
         if (user == null) {
-            // 新用户,创建新家庭和用户
-            isNewUser = true;
-
-            Family family = new Family();
-            family.setName("我的家庭");
-            family.setInviteCode(generateInviteCode());
-            family.setCreatedAt(new Date());
-            family.setUpdatedAt(new Date());
-            familyMapper.insert(family);
-
-            user = new User();
-            user.setOpenid("phone:" + phone);
-            user.setUnionid("");
-            user.setFamilyId(family.getId());
-            // 默认角色为家长,可以被覆盖
-            user.setRole(dto.getRole() != null ? dto.getRole() : "parent");
-            user.setNickname(dto.getNickname() != null ? dto.getNickname() : "用户" + phone.substring(7));
-            user.setAvatar(dto.getAvatar());
-            user.setPhone(phone);
-            user.setBirthday(dto.getBirthday());
-            user.setGender(dto.getGender());
-            user.setPassword("");
-            user.setCreatedAt(new Date());
-            user.setUpdatedAt(new Date());
-            userMapper.insert(user);
+            // 4. openid不存在,通过手机号查找
+            user = userMapper.selectOne(new LambdaQueryWrapper<User>()
+                    .eq(User::getPhone, phone));
+
+            if (user == null) {
+                // 5. 全新用户,创建新家庭和用户
+                isNewUser = true;
+
+                Family family = new Family();
+                family.setName("我的家庭");
+                family.setInviteCode(generateInviteCode());
+                family.setCreatedAt(new Date());
+                family.setUpdatedAt(new Date());
+                familyMapper.insert(family);
+
+                user = new User();
+                user.setOpenid(openid);
+                user.setUnionid(unionid);
+                user.setFamilyId(family.getId());
+                user.setRole("parent");
+                user.setNickname(dto.getNickname() != null ? dto.getNickname() : "用户" + phone.substring(7));
+                user.setAvatar(dto.getAvatar());
+                user.setPhone(phone);
+                user.setPassword("");
+                user.setCreatedAt(new Date());
+                user.setUpdatedAt(new Date());
+                userMapper.insert(user);
+            } else {
+                // 6. 手机号已存在但openid不同,绑定openid
+                user.setOpenid(openid);
+                user.setUnionid(unionid);
+                user.setUpdatedAt(new Date());
+                userMapper.updateById(user);
+            }
+        } else {
+            // 7. openid已存在,更新手机号(如果手机号变了)
+            if (phone != null && !phone.equals(user.getPhone())) {
+                user.setPhone(phone);
+                user.setUpdatedAt(new Date());
+                userMapper.updateById(user);
+            }
         }
 
         // 生成token
@@ -132,6 +221,64 @@ private VerificationCodeService verificationCodeService;
         result.setFamilyId(user.getFamilyId());
         result.setNickname(user.getNickname());
         result.setIsNewUser(isNewUser);
+        result.setOpenid(openid);
+
+        return result;
+    }
+
+    /**
+     * 微信openid静默登录(用户再次打开小程序时自动登录)
+     */
+    public LoginResultDTO silentLogin(String code) {
+        // 1. 通过code获取openid
+        Map<String, String> sessionData = wechatService.code2Session(code);
+        String openid = sessionData.get("openid");
+        String unionid = sessionData.get("unionid");
+
+        // 2. 查找用户
+        User user = userMapper.selectOne(new LambdaQueryWrapper<User>()
+                .eq(User::getOpenid, openid));
+
+        if (user == null) {
+            throw new RuntimeException("用户不存在,请先登录");
+        }
+
+        // 3. 生成token
+        String token = jwtConfig.generateToken(user.getId(), user.getRole());
+
+        LoginResultDTO result = new LoginResultDTO();
+        result.setToken(token);
+        result.setUserId(user.getId());
+        result.setRole(user.getRole());
+        result.setFamilyId(user.getFamilyId());
+        result.setNickname(user.getNickname());
+        result.setIsNewUser(false);
+        result.setOpenid(openid);
+
+        return result;
+    }
+
+    /**
+     * 通过openid自动登录(前端已缓存openid时调用)
+     */
+    public LoginResultDTO autoLoginByOpenid(String openid) {
+        User user = userMapper.selectOne(new LambdaQueryWrapper<User>()
+                .eq(User::getOpenid, openid));
+
+        if (user == null) {
+            return null;
+        }
+
+        String token = jwtConfig.generateToken(user.getId(), user.getRole());
+
+        LoginResultDTO result = new LoginResultDTO();
+        result.setToken(token);
+        result.setUserId(user.getId());
+        result.setRole(user.getRole());
+        result.setFamilyId(user.getFamilyId());
+        result.setNickname(user.getNickname());
+        result.setIsNewUser(false);
+        result.setOpenid(openid);
 
         return result;
     }
@@ -164,12 +311,139 @@ private VerificationCodeService verificationCodeService;
         if (user == null) {
             return null;
         }
+        // 验证用户是否拥有该角色
+        if (!hasRole(user, mode)) {
+            return null;
+        }
         user.setRole(mode);
         user.setUpdatedAt(new Date());
         userMapper.updateById(user);
         return mode;
     }
 
+    /**
+     * 切换用户当前角色 (支持 parent/child/teacher)
+     * 用户需要先拥有该角色才能切换
+     */
+    public String switchRole(Long userId, String targetRole) {
+        User user = userMapper.selectById(userId);
+        if (user == null) {
+            return null;
+        }
+        // 验证用户是否拥有该角色
+        if (!hasRole(user, targetRole)) {
+            throw new RuntimeException("您没有该角色权限");
+        }
+        // 检查目标角色是否有效
+        if (!isValidRole(targetRole)) {
+            return null;
+        }
+        user.setRole(targetRole);
+        user.setUpdatedAt(new Date());
+        userMapper.updateById(user);
+        return targetRole;
+    }
+
+    /**
+     * 获取用户所有角色列表
+     */
+    public List<String> getUserRoles(Long userId) {
+        User user = userMapper.selectById(userId);
+        if (user == null) {
+            return new ArrayList<>();
+        }
+        return parseRoles(user.getRoles());
+    }
+
+    /**
+     * 检查用户是否拥有指定角色
+     */
+    public boolean hasRole(Long userId, String role) {
+        User user = userMapper.selectById(userId);
+        if (user == null) {
+            return false;
+        }
+        return hasRole(user, role);
+    }
+
+    /**
+     * 添加角色到用户 (用于用户注册成为老师时)
+     */
+    public boolean addRole(Long userId, String newRole) {
+        User user = userMapper.selectById(userId);
+        if (user == null || !isValidRole(newRole)) {
+            return false;
+        }
+        // 如果用户还没有roles字段,设置默认值
+        if (user.getRoles() == null || user.getRoles().isEmpty()) {
+            user.setRoles(user.getRole()); // 当前角色作为第一个角色
+        }
+        // 检查是否已拥有该角色
+        if (hasRole(user, newRole)) {
+            return true; // 已有该角色
+        }
+        // 添加新角色
+        List<String> roleList = parseRoles(user.getRoles());
+        roleList.add(newRole);
+        user.setRoles(joinRoles(roleList));
+        user.setUpdatedAt(new Date());
+        userMapper.updateById(user);
+        return true;
+    }
+
+    /**
+     * 检查用户是否拥有指定角色 (内部方法)
+     */
+    private boolean hasRole(User user, String role) {
+        List<String> roles = parseRoles(user.getRoles());
+        return roles.contains(role);
+    }
+
+    /**
+     * 解析角色字符串为列表
+     */
+    private List<String> parseRoles(String rolesStr) {
+        List<String> result = new ArrayList<>();
+        if (rolesStr == null || rolesStr.isEmpty()) {
+            return result;
+        }
+        // 支持JSON数组格式或逗号分隔格式
+        if (rolesStr.startsWith("[")) {
+            // JSON格式解析
+            rolesStr = rolesStr.replace("[", "").replace("]", "")
+                    .replace("\"", "").replace(" ", "");
+            String[] arr = rolesStr.split(",");
+            for (String r : arr) {
+                if (!r.isEmpty()) {
+                    result.add(r.trim());
+                }
+            }
+        } else {
+            // 逗号分隔格式
+            String[] arr = rolesStr.split(",");
+            for (String r : arr) {
+                if (!r.trim().isEmpty()) {
+                    result.add(r.trim());
+                }
+            }
+        }
+        return result;
+    }
+
+    /**
+     * 将角色列表转换为字符串
+     */
+    private String joinRoles(List<String> roles) {
+        return String.join(",", roles);
+    }
+
+    /**
+     * 检查角色是否有效
+     */
+    private boolean isValidRole(String role) {
+        return "parent".equals(role) || "child".equals(role) || "teacher".equals(role);
+    }
+
     /**
      * 家长切换到指定孩子身份
      */
@@ -381,11 +655,73 @@ private VerificationCodeService verificationCodeService;
         if (dto.getRealName() != null) {
             user.setRealName(dto.getRealName());
         }
+        // 更新角色
+        if (dto.getRole() != null) {
+            user.setRole(dto.getRole());
+        }
+        // 更新性别
+        if (dto.getGender() != null) {
+            user.setGender(dto.getGender());
+        }
+        // 更新生日(将字符串转换为Date)
+        if (dto.getBirthday() != null && !dto.getBirthday().isEmpty()) {
+            try {
+                java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd");
+                user.setBirthday(sdf.parse(dto.getBirthday()));
+            } catch (Exception e) {
+                // 忽略解析错误
+            }
+        }
+        // 更新身份证号
+        if (dto.getIdCard() != null && !dto.getIdCard().isEmpty()) {
+            user.setIdCard(dto.getIdCard());
+            // 如果没有手动设置生日,尝试从身份证号提取
+            if (user.getBirthday() == null) {
+                String birthdayFromIdCard = extractBirthdayFromIdCard(dto.getIdCard());
+                if (birthdayFromIdCard != null) {
+                    try {
+                        java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd");
+                        user.setBirthday(sdf.parse(birthdayFromIdCard));
+                    } catch (Exception e) {
+                        // 忽略解析错误
+                    }
+                }
+            }
+        }
         user.setUpdatedAt(new Date());
         userMapper.updateById(user);
         return true;
     }
 
+    /**
+     * 从身份证号提取生日
+     * 15位:1985年07月08日 -> 1985-07-08
+     * 18位:1985年07月08日 -> 1985-07-08
+     */
+    private String extractBirthdayFromIdCard(String idCard) {
+        if (idCard == null || idCard.isEmpty()) {
+            return null;
+        }
+        try {
+            if (idCard.length() == 15) {
+                // 15位身份证:出生日期在第6-12位
+                String year = "19" + idCard.substring(6, 8);
+                String month = idCard.substring(8, 10);
+                String day = idCard.substring(10, 12);
+                return String.format("%s-%s-%s", year, month, day);
+            } else if (idCard.length() == 18) {
+                // 18位身份证:出生日期在第6-14位
+                String year = idCard.substring(6, 10);
+                String month = idCard.substring(10, 12);
+                String day = idCard.substring(12, 14);
+                return String.format("%s-%s-%s", year, month, day);
+            }
+        } catch (Exception e) {
+            // 忽略解析异常
+        }
+        return null;
+    }
+
     /**
      * 通过邀请码加入家庭
      */

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

@@ -4,7 +4,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.zxyj.entity.VerificationCode;
 import com.zxyj.mapper.VerificationCodeMapper;
 import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Autowired;
+import javax.annotation.Resource;
 import org.springframework.stereotype.Service;
 
 import java.util.Date;
@@ -14,7 +14,7 @@ import java.util.Random;
 @Service
 public class VerificationCodeService {
 
-    @Autowired
+    @Resource
     private VerificationCodeMapper verificationCodeMapper;
 
     private static final int CODE_LENGTH = 6;

+ 144 - 0
zxyj-backend/src/main/java/com/zxyj/service/WechatService.java

@@ -0,0 +1,144 @@
+package com.zxyj.service;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestTemplate;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+@Service
+public class WechatService {
+
+    private static final Logger log = LoggerFactory.getLogger(WechatService.class);
+
+    @Value("${wechat.appid}")
+    private String appid;
+
+    @Value("${wechat.secret}")
+    private String secret;
+
+    @Value("${wechat.login-url}")
+    private String loginUrl;
+
+    @Value("${wechat.phone-url}")
+    private String phoneUrl;
+
+    @Value("${wechat.test-mode:false}")
+    private boolean testMode;
+
+    private final RestTemplate restTemplate = new RestTemplate();
+
+    /**
+     * 判断是否测试模式
+     */
+    public boolean isTestMode() {
+        return testMode;
+    }
+
+    /**
+     * 通过code获取openid和session_key
+     */
+    public Map<String, String> code2Session(String code) {
+        // 测试模式:返回模拟的openid
+        if (testMode) {
+            log.info("测试模式:模拟code2Session返回");
+            Map<String, String> result = new HashMap<>();
+            result.put("openid", "test_openid_" + UUID.randomUUID().toString().substring(0, 8));
+            result.put("session_key", "test_session_key");
+            result.put("unionid", "");
+            return result;
+        }
+
+        try {
+            String url = String.format("%s?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code",
+                    loginUrl, appid, secret, code);
+
+            ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
+            JSONObject json = JSON.parseObject(response.getBody());
+
+            if (json.containsKey("openid")) {
+                Map<String, String> result = new HashMap<>();
+                result.put("openid", json.getString("openid"));
+                result.put("session_key", json.getString("session_key"));
+                result.put("unionid", json.getString("unionid"));
+                return result;
+            } else {
+                log.error("微信code2Session失败: {}", json.toJSONString());
+                throw new RuntimeException("微信登录失败: " + json.getString("errmsg"));
+            }
+        } catch (Exception e) {
+            log.error("微信code2Session异常", e);
+            throw new RuntimeException("微信登录异常");
+        }
+    }
+
+    /**
+     * 获取微信手机号(新版接口,使用access_token)
+     */
+    public String getPhoneNumber(String code) {
+        // 测试模式:返回模拟的手机号
+        if (testMode) {
+            log.info("测试模式:模拟getPhoneNumber返回");
+            // 尝试从code中解析手机号(测试时可以在code中传入真实手机号)
+            if (code != null && code.matches("^1[3-9]\\d{9}$")) {
+                return code;
+            }
+            // 默认返回测试手机号
+            return "13800138000";
+        }
+
+        try {
+            // 先获取access_token
+            String accessToken = getAccessToken();
+
+            String url = phoneUrl + "?access_token=" + accessToken;
+
+            JSONObject requestBody = new JSONObject();
+            requestBody.put("code", code);
+
+            ResponseEntity<String> response = restTemplate.postForEntity(url, requestBody.toJSONString(), String.class);
+            JSONObject json = JSON.parseObject(response.getBody());
+
+            if (json.getInteger("errcode") == 0) {
+                JSONObject phoneInfo = json.getJSONObject("phone_info");
+                return phoneInfo.getString("phoneNumber");
+            } else {
+                log.error("获取手机号失败: {}", json.toJSONString());
+                throw new RuntimeException("获取手机号失败: " + json.getString("errmsg"));
+            }
+        } catch (Exception e) {
+            log.error("获取手机号异常", e);
+            throw new RuntimeException("获取手机号异常");
+        }
+    }
+
+    /**
+     * 获取access_token
+     */
+    private String getAccessToken() {
+        try {
+            String url = String.format("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s",
+                    appid, secret);
+
+            ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
+            JSONObject json = JSON.parseObject(response.getBody());
+
+            if (json.containsKey("access_token")) {
+                return json.getString("access_token");
+            } else {
+                log.error("获取access_token失败: {}", json.toJSONString());
+                throw new RuntimeException("获取access_token失败");
+            }
+        } catch (Exception e) {
+            log.error("获取access_token异常", e);
+            throw new RuntimeException("获取access_token异常");
+        }
+    }
+}

+ 72 - 0
zxyj-backend/src/main/java/com/zxyj/service/impl/LocalDiskStorageService.java

@@ -0,0 +1,72 @@
+package com.zxyj.service.impl;
+
+import com.zxyj.service.StorageService;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.UUID;
+
+@Service
+public class LocalDiskStorageService implements StorageService {
+
+    @Value("${file.upload-dir:./uploads}")
+    private String uploadDir;
+
+    @Override
+    public String storeFile(MultipartFile file) {
+        try {
+            // 生成日期路径:uploads/2026/04/05/
+            String datePath = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
+            Path uploadPath = Paths.get(uploadDir, datePath);
+
+            // 创建目录
+            if (!Files.exists(uploadPath)) {
+                Files.createDirectories(uploadPath);
+            }
+
+            // 生成唯一文件名
+            String originalFilename = file.getOriginalFilename();
+            String extension = "";
+            if (originalFilename != null && originalFilename.contains(".")) {
+                extension = originalFilename.substring(originalFilename.lastIndexOf("."));
+            }
+            String filename = UUID.randomUUID().toString() + extension;
+
+            // 保存文件
+            Path filePath = uploadPath.resolve(filename);
+            file.transferTo(filePath.toFile());
+
+            // 返回相对URL路径
+            return "/" + datePath + "/" + filename;
+        } catch (IOException e) {
+            throw new RuntimeException("文件存储失败", e);
+        }
+    }
+
+    @Override
+    public void deleteFile(String fileUrl) {
+        if (fileUrl == null || fileUrl.isEmpty()) {
+            return;
+        }
+        try {
+            Path filePath = Paths.get(uploadDir, fileUrl);
+            Files.deleteIfExists(filePath);
+        } catch (IOException e) {
+            // 记录日志但不抛出异常
+            System.err.println("删除文件失败: " + fileUrl);
+        }
+    }
+
+    @Override
+    public String getFilePath(String filename) {
+        return Paths.get(uploadDir, filename).toString();
+    }
+}

+ 99 - 0
zxyj-backend/src/main/java/com/zxyj/service/impl/MediaRecordServiceImpl.java

@@ -0,0 +1,99 @@
+package com.zxyj.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.zxyj.entity.MediaRecord;
+import com.zxyj.mapper.MediaRecordMapper;
+import com.zxyj.service.MediaRecordService;
+import com.zxyj.service.StorageService;
+import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.List;
+
+@Service
+public class MediaRecordServiceImpl implements MediaRecordService {
+
+    @Resource
+    private MediaRecordMapper mediaRecordMapper;
+
+    @Resource
+    private StorageService storageService;
+
+    @Override
+    public MediaRecord uploadMedia(MultipartFile file, Long taskId, Long creatorId, String description) {
+        // 存储文件并获取URL
+        String url = storageService.storeFile(file);
+
+        MediaRecord record = new MediaRecord();
+        record.setType(getMediaType(file.getContentType()));
+        record.setUrl(url);
+        record.setTaskId(taskId);
+        record.setCreatorId(creatorId);
+        record.setDescription(description);
+        record.setOriginalFilename(file.getOriginalFilename());
+        record.setFileSize(file.getSize());
+        record.setContentType(file.getContentType());
+        record.setCreatedAt(new Date());
+
+        mediaRecordMapper.insert(record);
+        return record;
+    }
+
+    @Override
+    public MediaRecord saveTextRecord(Long taskId, Long creatorId, String content) {
+        MediaRecord record = new MediaRecord();
+        record.setType("text");
+        record.setTaskId(taskId);
+        record.setCreatorId(creatorId);
+        record.setDescription(content);
+        record.setCreatedAt(new Date());
+
+        mediaRecordMapper.insert(record);
+        return record;
+    }
+
+    @Override
+    public List<MediaRecord> getMediaByTaskId(Long taskId) {
+        return mediaRecordMapper.selectList(new LambdaQueryWrapper<MediaRecord>()
+                .eq(MediaRecord::getTaskId, taskId)
+                .orderByAsc(MediaRecord::getCreatedAt));
+    }
+
+    @Override
+    public boolean deleteMedia(Long mediaId, Long userId) {
+        MediaRecord record = mediaRecordMapper.selectById(mediaId);
+        if (record == null || !record.getCreatorId().equals(userId)) {
+            return false;
+        }
+
+        // 删除文件
+        if (record.getUrl() != null) {
+            storageService.deleteFile(record.getUrl());
+        }
+
+        mediaRecordMapper.deleteById(mediaId);
+        return true;
+    }
+
+    @Override
+    public boolean attachMediaToTask(Long taskId, List<Long> mediaIds) {
+        for (Long mediaId : mediaIds) {
+            MediaRecord record = mediaRecordMapper.selectById(mediaId);
+            if (record != null) {
+                record.setTaskId(taskId);
+                mediaRecordMapper.updateById(record);
+            }
+        }
+        return true;
+    }
+
+    private String getMediaType(String contentType) {
+        if (contentType == null) return "image";
+        if (contentType.startsWith("image")) return "image";
+        if (contentType.startsWith("video")) return "video";
+        if (contentType.startsWith("audio")) return "audio";
+        return "image";
+    }
+}