Browse Source

feat: 规划师模块新增首页统计和家庭信息接口

- GuideFamilyTaskController: 新增dashboard-stats端点(今日完成任务/待审核/家庭数)

- GuideManagementController: 完善邀请码生成和规划师申请流程

- TeacherMessageController: 老师消息回复接口优化

- GuideFamilyService/GuideApplicationService: 家庭绑定和申请服务增强

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
User 5 months ago
parent
commit
06a7954a60

+ 55 - 0
zxyj-backend/src/main/java/com/zxyj/controller/guide/GuideFamilyTaskController.java

@@ -18,6 +18,8 @@ import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import org.springframework.web.bind.annotation.*;
 
+import java.time.LocalDate;
+import java.time.ZoneId;
 import java.util.*;
 import java.util.stream.Collectors;
 
@@ -379,4 +381,57 @@ public class GuideFamilyTaskController {
         
         return Result.success(true);
     }
+
+    /**
+     * 获取规划师首页统计(实际数据)
+     */
+    @Operation(summary = "获取规划师首页统计")
+    @PostMapping("/dashboard-stats")
+    public Result<Map<String, Object>> getDashboardStats(@RequestAttribute("userId") Long guideId, @RequestAttribute("role") String role) {
+        if (!"teacher".equals(role)) { return Result.error("Access denied"); }
+
+        User guide = userMapper.selectById(guideId);
+        long totalFamilies = 0;
+        long todayCompleted = 0;
+        long pendingReview = 0;
+
+        if (guide != null && guide.getTeacherFamilyIds() != null && !guide.getTeacherFamilyIds().isEmpty()) {
+            List<Long> familyIds = Arrays.stream(guide.getTeacherFamilyIds().split(","))
+                    .map(String::trim).filter(s -> !s.isEmpty()).map(Long::parseLong)
+                    .collect(Collectors.toList());
+            totalFamilies = familyIds.size();
+
+            // 今天0点
+            Date todayStart = Date.from(LocalDate.now().atStartOfDay(ZoneId.systemDefault()).toInstant());
+
+            for (Long familyId : familyIds) {
+                List<Child> children = childMapper.selectList(
+                    new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<Child>()
+                        .eq(Child::getFamilyId, familyId));
+                for (Child child : children) {
+                    // 今日完成任务:status=completed 且 completedAt >= todayStart
+                    long completed = taskMapper.selectCount(
+                        new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<Task>()
+                            .eq(Task::getChildId, child.getId())
+                            .eq(Task::getStatus, "completed")
+                            .ge(Task::getCompletedAt, todayStart));
+                    todayCompleted += completed;
+
+                    // 待审核任务
+                    long pending = taskMapper.selectCount(
+                        new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<Task>()
+                            .eq(Task::getChildId, child.getId())
+                            .eq(Task::getNeedReview, 1)
+                            .eq(Task::getStatus, "completed"));
+                    pendingReview += pending;
+                }
+            }
+        }
+
+        Map<String, Object> stats = new HashMap<>();
+        stats.put("totalFamilies", totalFamilies);
+        stats.put("todayCompletedTasks", todayCompleted);
+        stats.put("pendingReviewTasks", pendingReview);
+        return Result.success(stats);
+    }
 }

+ 117 - 22
zxyj-backend/src/main/java/com/zxyj/controller/guide/GuideManagementController.java

@@ -3,12 +3,15 @@ package com.zxyj.controller.guide;
 import com.zxyj.common.Result;
 import com.zxyj.dto.GuideRegisterDTO;
 import com.zxyj.dto.PackagePurchaseDTO;
+import com.zxyj.entity.GuideApplication;
+import com.zxyj.entity.GuideApplicationRecord;
 import com.zxyj.entity.GuideInviteCode;
 import com.zxyj.entity.GuidePackage;
 import com.zxyj.entity.GuideHierarchy;
 import com.zxyj.entity.PackageOrder;
 import com.zxyj.entity.User;
 import com.zxyj.mapper.UserMapper;
+import com.zxyj.service.GuideApplicationService;
 import com.zxyj.service.GuideHierarchyService;
 import com.zxyj.service.GuideInviteCodeService;
 import com.zxyj.service.GuidePackageService;
@@ -46,8 +49,11 @@ public class GuideManagementController {
     @Resource
     private UserService userService;
 
+    @Resource
+    private GuideApplicationService guideApplicationService;
+
     /**
-     * 成长规划师注册
+     * 成长规划师注册(保存草稿)
      */
     @Operation(summary = "成长规划师注册")
     @PostMapping("/register")
@@ -58,20 +64,6 @@ public class GuideManagementController {
             return Result.error("用户不存在");
         }
 
-        // 如果有邀请码,验证并建立层级关系
-        if (dto.getInviteCode() != null && !dto.getInviteCode().isEmpty()) {
-            GuideInviteCode inviteCode = guideInviteCodeService.validateInviteCode(dto.getInviteCode());
-            if (inviteCode == null) {
-                return Result.error("邀请码无效或已过期");
-            }
-
-            // 建立层级关系
-            guideHierarchyService.addChildGuide(inviteCode.getGuideId(), dto.getUserId());
-
-            // 使用邀请码
-            guideInviteCodeService.useInviteCode(dto.getInviteCode(), dto.getUserId());
-        }
-
         // 更新用户信息
         user.setTeacherNo(dto.getTeacherNo());
         user.setTeacherPhoto(dto.getTeacherPhoto());
@@ -79,24 +71,128 @@ public class GuideManagementController {
         user.setRealName(dto.getRealName());
         user.setPhone(dto.getPhone());
         user.setIdCard(dto.getIdCard());
-        user.setTeacherStatus("pending"); // 待审核
+        user.setAddress(dto.getAddress());
+        user.setTeacherStatus("pending");
         user.setUpdatedAt(new java.util.Date());
         userMapper.updateById(user);
 
-        // 将teacher角色添加到用户的角色列表中(审核通过前不可切换为teacher)
+        // 创建申请记录
+        guideApplicationService.createDraft(
+            dto.getUserId(), "new_apply", "junior", null,
+            dto.getRealName(), dto.getPhone(), dto.getIdCard(),
+            dto.getTeacherNo(), dto.getTeacherPhoto(), dto.getCertificateImage(),
+            dto.getAddress(), dto.getBankCard(), dto.getBankName()
+        );
+
+        // 将teacher角色添加到用户的角色列表中
         userService.addRole(user.getId(), "teacher");
 
         return Result.success(true);
     }
 
+    /**
+     * 提交申请(draft -> pending)
+     */
+    @Operation(summary = "提交成长规划师申请")
+    @PostMapping("/application/submit")
+    public Result<Boolean> submitApplication(@RequestAttribute("userId") Long userId) {
+        // 查找用户的草稿申请
+        List<GuideApplication> apps = guideApplicationService.getUserApplications(userId);
+        GuideApplication draft = null;
+        for (GuideApplication app : apps) {
+            if ("draft".equals(app.getStatus())) {
+                draft = app;
+                break;
+            }
+        }
+        if (draft == null) {
+            return Result.error("未找到草稿申请");
+        }
+        boolean success = guideApplicationService.submitApplication(draft.getId(), userId);
+        return success ? Result.success(true) : Result.error("提交失败");
+    }
+
+    /**
+     * 撤回申请
+     */
+    @Operation(summary = "撤回成长规划师申请")
+    @PostMapping("/application/withdraw")
+    public Result<Boolean> withdrawApplication(@RequestAttribute("userId") Long userId) {
+        List<GuideApplication> apps = guideApplicationService.getUserApplications(userId);
+        GuideApplication pending = null;
+        for (GuideApplication app : apps) {
+            if ("pending".equals(app.getStatus())) {
+                pending = app;
+                break;
+            }
+        }
+        if (pending == null) {
+            return Result.error("未找到待审核的申请");
+        }
+        boolean success = guideApplicationService.withdrawApplication(pending.getId(), userId);
+        return success ? Result.success(true) : Result.error("撤回失败");
+    }
+
+    /**
+     * 重新提交申请
+     */
+    @Operation(summary = "重新提交成长规划师申请")
+    @PostMapping("/application/resubmit")
+    public Result<Boolean> resubmitApplication(@RequestAttribute("userId") Long userId) {
+        List<GuideApplication> apps = guideApplicationService.getUserApplications(userId);
+        GuideApplication target = null;
+        for (GuideApplication app : apps) {
+            if ("withdrawn".equals(app.getStatus()) || "rejected".equals(app.getStatus())) {
+                target = app;
+                break;
+            }
+        }
+        if (target == null) {
+            return Result.error("未找到可重新提交的申请");
+        }
+        boolean success = guideApplicationService.resubmitApplication(target.getId(), userId);
+        return success ? Result.success(true) : Result.error("重新提交失败");
+    }
+
+    /**
+     * 获取用户申请列表
+     */
+    @Operation(summary = "获取用户申请列表")
+    @PostMapping("/applications")
+    public Result<List<GuideApplication>> getUserApplications(@RequestAttribute("userId") Long userId) {
+        return Result.success(guideApplicationService.getUserApplications(userId));
+    }
+
+    /**
+     * 获取申请处理记录
+     */
+    @Operation(summary = "获取申请处理记录")
+    @PostMapping("/application/{id}/records")
+    public Result<List<GuideApplicationRecord>> getApplicationRecords(@PathVariable Long id) {
+        return Result.success(guideApplicationService.getApplicationRecords(id));
+    }
+
+    /**
+     * 如果有邀请码,验证并建立层级关系
+     */
+    private void processInviteCode(String inviteCode, Long userId) {
+        if (inviteCode != null && !inviteCode.isEmpty()) {
+            GuideInviteCode code = guideInviteCodeService.validateInviteCode(inviteCode);
+            if (code != null) {
+                guideHierarchyService.addChildGuide(code.getGuideId(), userId);
+                guideInviteCodeService.useInviteCode(inviteCode, userId);
+            }
+        }
+    }
+
     /**
      * 生成邀请码
      */
     @Operation(summary = "生成邀请码")
     @PostMapping("/invite-code/generate")
     public Result<GuideInviteCode> generateInviteCode(@RequestAttribute("userId") Long userId,
-                                                      @RequestParam(defaultValue = "1") Integer maxUseCount,
-                                                      @RequestParam(defaultValue = "30") Integer expireDays) {
+                                                       @RequestParam(defaultValue = "1") Integer maxUseCount,
+                                                       @RequestParam(defaultValue = "30") Integer expireDays) {
         GuideInviteCode inviteCode = guideInviteCodeService.generateInviteCode(userId, maxUseCount, expireDays);
         return Result.success(inviteCode);
     }
@@ -118,7 +214,7 @@ public class GuideManagementController {
     @PostMapping("/invite-code/validate")
     public Result<Map<String, Object>> validateInviteCode(@RequestParam String code) {
         GuideInviteCode inviteCode = guideInviteCodeService.validateInviteCode(code);
-        
+
         if (inviteCode == null) {
             return Result.error("邀请码无效或已过期");
         }
@@ -130,7 +226,6 @@ public class GuideManagementController {
         result.put("useCount", inviteCode.getUseCount());
         result.put("expireAt", inviteCode.getExpireAt());
 
-        // 获取成长规划师信息
         User guide = userMapper.selectById(inviteCode.getGuideId());
         if (guide != null) {
             result.put("guideName", guide.getNickname());
@@ -179,7 +274,7 @@ public class GuideManagementController {
                                           @RequestBody Map<String, Object> params) {
         Long packageId = Long.valueOf(params.get("packageId").toString());
         java.math.BigDecimal price = new java.math.BigDecimal(params.get("price").toString());
-        java.math.BigDecimal commissionRate = params.containsKey("commissionRate") 
+        java.math.BigDecimal commissionRate = params.containsKey("commissionRate")
             ? new java.math.BigDecimal(params.get("commissionRate").toString())
             : new java.math.BigDecimal("0.1");
 

+ 8 - 2
zxyj-backend/src/main/java/com/zxyj/controller/guide/TeacherMessageController.java

@@ -33,10 +33,16 @@ public class TeacherMessageController {
     @PostMapping("/send")
     public Result<Long> sendMessage(@RequestAttribute("userId") Long parentId,
                                      @RequestBody Map<String, Object> body) {
+        if (body.get("familyId") == null || body.get("teacherId") == null) {
+            return Result.error("缺少必要参数: familyId/teacherId");
+        }
+        String content = (String) body.get("content");
+        if (content == null || content.trim().isEmpty()) {
+            return Result.error("消息内容不能为空");
+        }
         Long familyId = Long.valueOf(body.get("familyId").toString());
         Long teacherId = Long.valueOf(body.get("teacherId").toString());
-        String content = (String) body.get("content");
-        
+
         Long messageId = teacherMessageService.sendMessage(familyId, parentId, teacherId, content);
         return Result.success(messageId);
     }

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

@@ -6,17 +6,118 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.zxyj.entity.Guide;
 import com.zxyj.entity.GuideApplication;
+import com.zxyj.entity.GuideApplicationRecord;
 import com.zxyj.mapper.GuideApplicationMapper;
+import com.zxyj.mapper.GuideApplicationRecordMapper;
 import com.zxyj.mapper.GuideMapper;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
+import javax.annotation.Resource;
 import java.util.Date;
 import java.util.List;
 
 @Service
 public class GuideApplicationService extends ServiceImpl<GuideApplicationMapper, GuideApplication> {
 
+    @Resource
+    private GuideApplicationRecordMapper guideApplicationRecordMapper;
+
+    @Resource
+    private GuideMapper guideMapper;
+
+    /**
+     * 创建申请(初始状态:draft)
+     */
+    public boolean createDraft(Long userId, String type, String targetLevel, Long packageId,
+                                String realName, String phone, String idCard,
+                                String teacherNo, String teacherPhoto, String certificateImage,
+                                String address, String bankCard, String bankName) {
+        GuideApplication application = new GuideApplication();
+        application.setUserId(userId);
+        application.setType(type);
+        application.setTargetLevel(targetLevel);
+        application.setPackageId(packageId);
+        application.setStatus("draft");
+        application.setAddress(address);
+        application.setBankCard(bankCard);
+        application.setBankName(bankName);
+        application.setCreatedAt(new Date());
+        boolean saved = this.save(application);
+
+        if (saved) {
+            addRecord(application.getId(), userId, "create", null, "draft", "创建申请");
+        }
+
+        return saved;
+    }
+
+    /**
+     * 提交申请(draft -> pending)
+     */
+    @Transactional
+    public boolean submitApplication(Long applicationId, Long userId) {
+        GuideApplication application = this.getById(applicationId);
+        if (application == null || !"draft".equals(application.getStatus())) {
+            return false;
+        }
+
+        application.setStatus("pending");
+        application.setUpdatedAt(new Date());
+        boolean updated = this.updateById(application);
+
+        if (updated) {
+            addRecord(applicationId, userId, "submit", "draft", "pending", "提交审核");
+        }
+
+        return updated;
+    }
+
+    /**
+     * 撤回申请(pending -> withdrawn)
+     */
+    @Transactional
+    public boolean withdrawApplication(Long applicationId, Long userId) {
+        GuideApplication application = this.getById(applicationId);
+        if (application == null || !"pending".equals(application.getStatus())) {
+            return false;
+        }
+
+        application.setStatus("withdrawn");
+        application.setUpdatedAt(new Date());
+        boolean updated = this.updateById(application);
+
+        if (updated) {
+            addRecord(applicationId, userId, "withdraw", "pending", "withdrawn", "申请人撤回");
+        }
+
+        return updated;
+    }
+
+    /**
+     * 重新提交(withdrawn/rejected -> pending)
+     */
+    @Transactional
+    public boolean resubmitApplication(Long applicationId, Long userId) {
+        GuideApplication application = this.getById(applicationId);
+        if (application == null) return false;
+        String currentStatus = application.getStatus();
+        if (!"withdrawn".equals(currentStatus) && !"rejected".equals(currentStatus)) {
+            return false;
+        }
+
+        application.setStatus("pending");
+        application.setRejectReason(null);
+        application.setUpdatedAt(new Date());
+        boolean updated = this.updateById(application);
+
+        if (updated) {
+            addRecord(applicationId, userId, "resubmit", currentStatus, "pending", "重新提交审核");
+        }
+
+        return updated;
+    }
+
     public boolean applyNew(Long userId, String targetLevel, Long packageId, String paymentProof) {
         GuideApplication application = new GuideApplication();
         application.setUserId(userId);
@@ -26,7 +127,13 @@ public class GuideApplicationService extends ServiceImpl<GuideApplicationMapper,
         application.setPaymentProof(paymentProof);
         application.setStatus("pending");
         application.setCreatedAt(new Date());
-        return this.save(application);
+        boolean saved = this.save(application);
+
+        if (saved) {
+            addRecord(application.getId(), userId, "submit", null, "pending", "提交新申请");
+        }
+
+        return saved;
     }
 
     public boolean applyUpgrade(Long userId, String fromLevel, String toLevel, String paymentProof) {
@@ -38,7 +145,13 @@ public class GuideApplicationService extends ServiceImpl<GuideApplicationMapper,
         application.setPaymentProof(paymentProof);
         application.setStatus("pending");
         application.setCreatedAt(new Date());
-        return this.save(application);
+        boolean saved = this.save(application);
+
+        if (saved) {
+            addRecord(application.getId(), userId, "submit", null, "pending", "提交升级申请");
+        }
+
+        return saved;
     }
 
     public List<GuideApplication> getPendingApplications() {
@@ -48,27 +161,70 @@ public class GuideApplicationService extends ServiceImpl<GuideApplicationMapper,
         return this.list(wrapper);
     }
 
+    public List<GuideApplication> getUserApplications(Long userId) {
+        LambdaQueryWrapper<GuideApplication> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(GuideApplication::getUserId, userId)
+               .orderByDesc(GuideApplication::getCreatedAt);
+        return this.list(wrapper);
+    }
+
     @Transactional
     public boolean approveApplication(Long applicationId, Long reviewedBy) {
         GuideApplication application = this.getById(applicationId);
         if (application == null) return false;
 
+        String fromStatus = application.getStatus();
         application.setStatus("approved");
         application.setReviewedAt(new Date());
         application.setReviewedBy(reviewedBy);
-        this.updateById(application);
+        boolean updated = this.updateById(application);
+
+        if (updated) {
+            addRecord(applicationId, reviewedBy, "approve", fromStatus, "approved", "审核通过");
+        }
 
         return true;
     }
 
+    @Transactional
     public boolean rejectApplication(Long applicationId, Long reviewedBy, String reason) {
         GuideApplication application = this.getById(applicationId);
         if (application == null) return false;
 
+        String fromStatus = application.getStatus();
         application.setStatus("rejected");
         application.setReviewedAt(new Date());
         application.setReviewedBy(reviewedBy);
         application.setRejectReason(reason);
-        return this.updateById(application);
+        boolean updated = this.updateById(application);
+
+        if (updated) {
+            addRecord(applicationId, reviewedBy, "reject", fromStatus, "rejected", reason);
+        }
+
+        return updated;
+    }
+
+    /**
+     * 获取申请的处理记录
+     */
+    public List<GuideApplicationRecord> getApplicationRecords(Long applicationId) {
+        LambdaQueryWrapper<GuideApplicationRecord> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(GuideApplicationRecord::getApplicationId, applicationId)
+               .orderByAsc(GuideApplicationRecord::getCreatedAt);
+        return guideApplicationRecordMapper.selectList(wrapper);
+    }
+
+    private void addRecord(Long applicationId, Long userId, String action,
+                           String fromStatus, String toStatus, String remark) {
+        GuideApplicationRecord record = new GuideApplicationRecord();
+        record.setApplicationId(applicationId);
+        record.setUserId(userId);
+        record.setAction(action);
+        record.setFromStatus(fromStatus);
+        record.setToStatus(toStatus);
+        record.setRemark(remark);
+        record.setCreatedAt(new Date());
+        guideApplicationRecordMapper.insert(record);
     }
-}
+}

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

@@ -60,6 +60,17 @@ public class GuideFamilyService {
      * 确认绑定
      */
     public boolean confirmBind(Long guideId, Long familyId, String serviceType, java.math.BigDecimal servicePrice) {
+        // 检查家庭是否已绑定其他规划师
+        GuideFamily existing = guideFamilyMapper.selectOne(
+            new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<GuideFamily>()
+                .eq("family_id", familyId)
+                .eq("status", "binding")
+                .ne("guide_id", guideId)
+        );
+        if (existing != null) {
+            throw new RuntimeException("该家庭已绑定其他成长规划师,请先解除当前绑定");
+        }
+
         GuideFamily gf = guideFamilyMapper.selectOne(
             new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<GuideFamily>()
                 .eq("guide_id", guideId)
@@ -86,6 +97,25 @@ public class GuideFamilyService {
         }
     }
 
+    /**
+     * 家长解除绑定,返回解除的 GuideFamily 记录(含 guideId)
+     */
+    public GuideFamily unbindAndGet(Long id, Long familyId) {
+        GuideFamily gf = guideFamilyMapper.selectOne(
+            new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<GuideFamily>()
+                .eq("id", id)
+                .eq("family_id", familyId)
+                .eq("status", "binding")
+        );
+        if (gf == null) {
+            throw new RuntimeException("绑定关系不存在或已解除");
+        }
+        gf.setStatus("cancelled");
+        gf.setUpdatedAt(new Date());
+        guideFamilyMapper.updateById(gf);
+        return gf;
+    }
+
     /**
      * 解除绑定
      */