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

merge(S-cfc-227): teacher.appointment.guard 使绿——测评预约状态守卫+未到(G-1)闭环

Agnes Agent 2 часов назад
Родитель
Сommit
ab349b35a1

+ 12 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/assessment/AssessmentAppointmentController.java

@@ -116,6 +116,18 @@ public class AssessmentAppointmentController {
         return Result.success(success);
     }
 
+    @Operation(summary = "标记预约未到(由成长规划师操作,status=4)")
+    @PostMapping("/no-show/{id}")
+    public Result<Boolean> noShowAppointment(
+            @RequestAttribute("userId") Long userId,
+            @PathVariable Long id) {
+        boolean success = appointmentService.noShowAppointment(id, userId);
+        if (!success) {
+            return Result.error("标记未到失败:预约状态不允许流转或您不是该预约的成长规划师");
+        }
+        return Result.success(true);
+    }
+
     @Operation(summary = "我的预约列表(本人视角)")
     @PostMapping("/my-list")
     public Result<List<Map<String, Object>>> getMyAppointments(@RequestAttribute("userId") Long userId) {

+ 26 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/AssessmentAppointmentService.java

@@ -57,6 +57,9 @@ public class AssessmentAppointmentService extends ServiceImpl<AssessmentAppointm
     public boolean confirmAppointment(Long id, Long userId) {
         AssessmentAppointment appointment = assessmentAppointmentMapper.selectById(id);
         if (appointment == null) return false;
+        // 状态守卫(teacher.appointment.guard / SC-T-09):仅待确认(0)可流转到已确认(1),
+        // 重复确认、已完成(2)/未到(4)等状态一律拒绝,且拒绝时不写库
+        if (appointment.getStatus() == null || appointment.getStatus() != 0) return false;
         // 允许被指派的测评师(assessorId)或规划师(guideId)确认
         Long expectedId = appointment.getAssessorId() != null ? appointment.getAssessorId() : appointment.getGuideId();
         if (!userId.equals(expectedId)) return false;
@@ -68,6 +71,9 @@ public class AssessmentAppointmentService extends ServiceImpl<AssessmentAppointm
     public boolean completeAppointment(Long id, Long teacherId) {
         AssessmentAppointment appointment = assessmentAppointmentMapper.selectById(id);
         if (appointment == null) return false;
+        // 状态守卫(teacher.appointment.guard / SC-T-10):仅已确认(1)可流转到已完成(2),
+        // 待确认(0)/已完成(2)/未到(4)一律拒绝,拒绝时不写库、不重复发放完成能量
+        if (appointment.getStatus() == null || appointment.getStatus() != 1) return false;
         if (!teacherId.equals(appointment.getGuideId())) return false;
         appointment.setStatus(2); // completed
         appointment.setUpdatedAt(new Date());
@@ -88,11 +94,31 @@ public class AssessmentAppointmentService extends ServiceImpl<AssessmentAppointm
     public boolean cancelAppointment(Long id) {
         AssessmentAppointment appointment = assessmentAppointmentMapper.selectById(id);
         if (appointment == null) return false;
+        // 状态守卫(teacher.appointment.guard / SC-T-10):仅待确认(0)/已确认(1)可取消;
+        // 已完成(2)/未到(4)等终态一律拒绝,且拒绝时不写库
+        if (appointment.getStatus() == null
+                || (appointment.getStatus() != 0 && appointment.getStatus() != 1)) return false;
         appointment.setStatus(3); // cancelled
         appointment.setUpdatedAt(new Date());
         return assessmentAppointmentMapper.updateById(appointment) > 0;
     }
 
+    /**
+     * 将预约标记为「未到」(status=4)—— 契约缺口 G-1:规划师「完成/未到」闭环的「未到」分支。
+     * 仅待确认(0)/已确认(1)可流转到未到(4);已完成(2)/已取消(3)/未到(4)等终态一律拒绝。
+     */
+    public boolean noShowAppointment(Long id, Long teacherId) {
+        AssessmentAppointment appointment = assessmentAppointmentMapper.selectById(id);
+        if (appointment == null) return false;
+        // 状态守卫:仅待确认(0)/已确认(1)可标记未到;终态一律拒绝且不写库
+        if (appointment.getStatus() == null
+                || (appointment.getStatus() != 0 && appointment.getStatus() != 1)) return false;
+        if (!teacherId.equals(appointment.getGuideId())) return false;
+        appointment.setStatus(4); // no-show
+        appointment.setUpdatedAt(new Date());
+        return assessmentAppointmentMapper.updateById(appointment) > 0;
+    }
+
     public List<AssessmentAppointment> getByUserId(Long userId) {
         LambdaQueryWrapper<AssessmentAppointment> wrapper = new LambdaQueryWrapper<>();
         wrapper.eq(AssessmentAppointment::getUserId, userId);

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
docs/superpowers/PROJECT-OVERVIEW.md


+ 2 - 1
docs/superpowers/api/API_REFERENCE.md

@@ -701,6 +701,7 @@ find cfc-backend/src/main/java -name "*XxxService.java" -o -name "*XxxController
 | `POST /api/assessment/appointment/create` | 预约测评 |
 | `POST /api/assessment/appointment/confirm/{id}` | 确认预约 |
 | `POST /api/assessment/appointment/cancel/{id}` | 取消预约 |
+| `POST /api/assessment/appointment/no-show/{id}` | 标记预约未到(status=4,规划师操作) |
 | `POST /api/assessment/appointment/my-list` | 我的预约 |
 | `POST /api/assessment/latest-result` | 最新测评结果 |
 | `POST /api/assessment/history` | 测评历史 |
@@ -2348,7 +2349,7 @@ Web 管理端定义活动海报模板(迁移381 `activity_poster_templates` 
 | SC-T-07 | 录入测评结果触发方案 ⭐ | `DanAssessmentController` `/api/dan-assessment/result/record`、`/record/create`、`/record/complete`、`/report/parse-preview` |
 | SC-T-08 | 成长计划 / 套餐创建 | `TrainingPlanController` `/api/guide/training-plans/*`(create/update/list/detail/delete);`GrowthPlanController` `/api/growth/plan/create` |
 | SC-T-09 | 测评预约确认(状态守卫) | `AssessmentAppointmentController` `/api/assessment/appointment/*`(create/confirm/{id}/cancel/{id}/my-list/guide-list/assessor-list);`DanAssessmentController` `/appointment/confirm` |
-| SC-T-10 | 测评预约完成/未到 | 完成 `DanAssessmentController /appointment/complete`;**未到(status=4)无独立端点 → 缺口 G-1** |
+| SC-T-10 | 测评预约完成/未到 | 完成 `DanAssessmentController /appointment/complete`;未到 `POST /api/assessment/appointment/no-show/{id}`(S-cfc-227 落地 G-1,仅待确认/已确认可流转 status=4) |
 | SC-T-11 | DAN 报告上传与解析 | `DanReportController` `/api/dan-report/*`(upload/{id}/confirm/{id}/discard/{id}/complete-analysis/list/compare) |
 | SC-T-12 | DAN 八类报告查看 | `AssessmentReportController` `/api/dan-assessment/result/*`(plans/list/upload/{resultId}/guidance);`DanAssessmentController` /result/{id}/my-results/child-results |
 | SC-T-13 | 规划师视角数据隔离 | 授权源 `guide_families.status='binding'`(`GuideFamilyService.isBound`)+ `JwtInterceptor` 角色隔离;`GuideFamilyQueryController /my-families`、`GuideFamilyTaskController /bound-families`、`PlanReviewController family-list`(JWT familyId 过滤) |

Некоторые файлы не были показаны из-за большого количества измененных файлов