GuideFamilyTaskController.java 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532
  1. package com.etotem.cfc.controller.guide;
  2. import javax.annotation.Resource;
  3. import io.swagger.v3.oas.annotations.responses.ApiResponse;
  4. import io.swagger.v3.oas.annotations.responses.ApiResponses;
  5. import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
  6. import com.etotem.cfc.common.Result;
  7. import com.etotem.cfc.dto.TaskReviewDTO;
  8. import com.etotem.cfc.entity.FamilyMember;
  9. import com.etotem.cfc.entity.GuideFamily;
  10. import com.etotem.cfc.entity.Task;
  11. import com.etotem.cfc.entity.User;
  12. import com.etotem.cfc.mapper.FamilyMemberMapper;
  13. import com.etotem.cfc.mapper.TaskMapper;
  14. import com.etotem.cfc.mapper.UserMapper;
  15. import com.etotem.cfc.service.GuideFamilyAccessGuard;
  16. import com.etotem.cfc.service.GuideFamilyService;
  17. import com.etotem.cfc.service.MembershipService;
  18. import com.etotem.cfc.service.TaskService;
  19. import io.swagger.v3.oas.annotations.Operation;
  20. import io.swagger.v3.oas.annotations.tags.Tag;
  21. import org.springframework.web.bind.annotation.*;
  22. import java.time.LocalDate;
  23. import java.time.ZoneId;
  24. import java.util.*;
  25. import java.util.stream.Collectors;
  26. import com.etotem.cfc.util.ParamUtils;
  27. @Tag(name = "指导师-家庭任务管理", description = "指导师查看家庭任务、审核任务等接口")
  28. @RestController
  29. @RequestMapping("/api/guide/families")
  30. public class GuideFamilyTaskController {
  31. @Resource
  32. private TaskService taskService;
  33. @Resource
  34. private FamilyMemberMapper familyMemberMapper;
  35. @Resource
  36. private TaskMapper taskMapper;
  37. @Resource
  38. private UserMapper userMapper;
  39. @Resource
  40. private MembershipService membershipService;
  41. @Resource
  42. private GuideFamilyService guideFamilyService;
  43. @Resource
  44. private GuideFamilyAccessGuard guideFamilyAccessGuard;
  45. /**
  46. * 规划师解除家庭绑定(仅限解除属于自己的绑定)
  47. */
  48. @Operation(summary = "规划师解除家庭绑定")
  49. @PostMapping("/unbind")
  50. public Result<Map<String, Object>> unbindFamily(@RequestAttribute("userId") Long guideId,
  51. @RequestAttribute("role") String role,
  52. @RequestBody Map<String, Object> params) {
  53. if (!"teacher".equals(role)) {
  54. return Result.error("Access denied");
  55. }
  56. Long id = ParamUtils.getLong(params.get("id"));
  57. if (id == null) {
  58. return Result.error("缺少绑定记录ID");
  59. }
  60. try {
  61. GuideFamily gf = guideFamilyService.unbindByGuide(id, guideId);
  62. Map<String, Object> result = new HashMap<>();
  63. result.put("familyId", gf.getFamilyId());
  64. result.put("serviceType", gf.getServiceType());
  65. return Result.success(result);
  66. } catch (RuntimeException e) {
  67. return Result.error(400, e.getMessage());
  68. }
  69. }
  70. /**
  71. * 会员任务校验:task.memberOnly==1 且家庭非会员 → 拒绝(提示开通会员)
  72. */
  73. private Result<?> checkMemberOnly(Long taskId) {
  74. Task task = taskMapper.selectById(taskId);
  75. if (task == null) {
  76. return Result.error("任务不存在");
  77. }
  78. if (task.getMemberOnly() != null && task.getMemberOnly() == 1) {
  79. com.etotem.cfc.dto.MembershipLevelDTO level = membershipService.getCurrentLevel(task.getFamilyId());
  80. boolean isMember = level != null && !"FREE".equals(level.getLevelCode());
  81. if (!isMember) {
  82. return Result.error("该任务是会员任务,请先开通会员后再操作");
  83. }
  84. }
  85. return null;
  86. }
  87. /**
  88. * 获取指导师关联的家庭列表(带简要任务统计)
  89. */
  90. @Operation(summary = "获取关联家庭列表")
  91. @ApiResponses({
  92. @ApiResponse(responseCode = "200", description = "成功返回家庭列表"),
  93. @ApiResponse(responseCode = "403", description = "Access denied - 非教师"),
  94. @ApiResponse(responseCode = "401", description = "未登录")
  95. })
  96. @PostMapping("/bound-families")
  97. public Result<List<Map<String, Object>>> getBoundFamilies(@RequestAttribute("userId") Long guideId, @RequestAttribute("role") String role) {
  98. if (!"teacher".equals(role)) { return Result.error("Access denied"); }
  99. // 获取当前指导师信息
  100. User guide = userMapper.selectById(guideId);
  101. if (guide == null || guide.getTeacherFamilyIds() == null || guide.getTeacherFamilyIds().isEmpty()) {
  102. return Result.success(new ArrayList<>());
  103. }
  104. // 解析家庭ID列表(逗号分隔)
  105. List<Long> familyIds = Arrays.stream(guide.getTeacherFamilyIds().split(","))
  106. .map(String::trim)
  107. .filter(s -> !s.isEmpty())
  108. .map(Long::parseLong)
  109. .collect(Collectors.toList());
  110. if (familyIds.isEmpty()) {
  111. return Result.success(new ArrayList<>());
  112. }
  113. List<Map<String, Object>> result = new ArrayList<>();
  114. for (Long familyId : familyIds) {
  115. // 获取家庭的家长用户
  116. List<User> parents = userMapper.selectList(
  117. new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<User>()
  118. .eq("family_id", familyId)
  119. .eq("role", "parent")
  120. );
  121. if (parents.isEmpty()) {
  122. continue;
  123. }
  124. User parent = parents.get(0);
  125. Map<String, Object> item = new HashMap<>();
  126. item.put("familyId", familyId);
  127. item.put("familyName", "家庭" + familyId);
  128. item.put("parentName", parent.getNickname());
  129. // 获取家庭孩子列表
  130. List<FamilyMember> children = familyMemberMapper.selectList(
  131. new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<FamilyMember>()
  132. .eq("family_id", familyId)
  133. );
  134. item.put("childrenCount", children.size());
  135. // 获取待审核任务数量
  136. long pendingCount = 0;
  137. for (FamilyMember child : children) {
  138. List<Task> pendingTasks = taskService.getPendingReviewTasksForFamilyMember(child.getId());
  139. pendingCount += pendingTasks.size();
  140. }
  141. item.put("pendingReviewCount", pendingCount);
  142. result.add(item);
  143. }
  144. return Result.success(result);
  145. }
  146. /**
  147. * 获取指定家庭的所有孩子任务概览
  148. */
  149. @Operation(summary = "获取家庭任务概览")
  150. @ApiResponses({
  151. @ApiResponse(responseCode = "200", description = "成功返回概览"),
  152. @ApiResponse(responseCode = "403", description = "Access denied"),
  153. @ApiResponse(responseCode = "401", description = "未登录")
  154. })
  155. @PostMapping("/{familyId}/overview")
  156. public Result<Map<String, Object>> getFamilyTaskOverview(
  157. @RequestAttribute("userId") Long guideId,
  158. @RequestAttribute("role") String role,
  159. @PathVariable Long familyId) {
  160. if (!"teacher".equals(role)) { return Result.error("Access denied"); }
  161. Result<Void> denied = requireBound(guideId, familyId);
  162. if (denied != null) return Result.error(denied.getCode(), denied.getMessage());
  163. List<FamilyMember> children = familyMemberMapper.selectList(
  164. new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<FamilyMember>()
  165. .eq("family_id", familyId)
  166. );
  167. List<Map<String, Object>> childTaskList = new ArrayList<>();
  168. int totalPending = 0;
  169. int totalCompleted = 0;
  170. int totalOverdue = 0;
  171. for (FamilyMember child : children) {
  172. Map<String, Object> childInfo = new HashMap<>();
  173. childInfo.put("memberId", child.getId());
  174. childInfo.put("childName", child.getNickname());
  175. childInfo.put("totalPoints", child.getTotalPoints());
  176. childInfo.put("streakDays", child.getStreakDays());
  177. // 获取今日任务
  178. List<Task> todayTasks = taskService.getTodayTasks(child.getId());
  179. childInfo.put("todayTaskCount", todayTasks.size());
  180. childInfo.put("todayCompletedCount", todayTasks.stream()
  181. .filter(t -> "completed".equals(t.getStatus()))
  182. .count());
  183. // 获取待审核任务
  184. List<Task> pendingTasks = taskService.getPendingReviewTasksForFamilyMember(child.getId());
  185. childInfo.put("pendingReviewCount", pendingTasks.size());
  186. totalPending += pendingTasks.size();
  187. // 统计已完成和超时
  188. List<Task> allTasks = taskService.getAllTasksByFamilyMember(child.getId());
  189. totalCompleted += allTasks.stream()
  190. .filter(t -> "completed".equals(t.getStatus()))
  191. .count();
  192. totalOverdue += allTasks.stream()
  193. .filter(t -> "overdue".equals(t.getStatus()))
  194. .count();
  195. childTaskList.add(childInfo);
  196. }
  197. Map<String, Object> result = new HashMap<>();
  198. result.put("familyId", familyId);
  199. result.put("children", childTaskList);
  200. result.put("totalFamilyMemberren", children.size());
  201. result.put("totalPendingReview", totalPending);
  202. result.put("totalCompleted", totalCompleted);
  203. result.put("totalOverdue", totalOverdue);
  204. return Result.success(result);
  205. }
  206. /**
  207. * 获取家庭孩子的任务列表
  208. */
  209. @Operation(summary = "获取孩子任务列表")
  210. @ApiResponses({
  211. @ApiResponse(responseCode = "200", description = "成功返回分页任务"),
  212. @ApiResponse(responseCode = "403", description = "Access denied"),
  213. @ApiResponse(responseCode = "401", description = "未登录")
  214. })
  215. @PostMapping("/{familyId}/children/{memberId}/tasks")
  216. public Result<Page<Task>> getFamilyMemberTasks(
  217. @RequestAttribute("userId") Long guideId,
  218. @PathVariable Long familyId,
  219. @PathVariable Long memberId,
  220. @RequestAttribute("role") String role,
  221. @RequestBody(required = false) Map<String, Object> params) {
  222. if (!"teacher".equals(role)) { return Result.error("Access denied"); }
  223. Result<Void> denied = requireBound(guideId, familyId);
  224. if (denied != null) return Result.error(denied.getCode(), denied.getMessage());
  225. Integer page = params != null && params.get("page") != null ? Integer.valueOf(params.get("page").toString()) : 1;
  226. Integer size = params != null && params.get("size") != null ? Integer.valueOf(params.get("size").toString()) : 10;
  227. String status = params != null && params.get("status") != null ? params.get("status").toString() : null;
  228. Page<Task> tasks = taskService.getTaskHistoryByFamilyMember(memberId, page, size, status);
  229. return Result.success(tasks);
  230. }
  231. /**
  232. * 获取家庭孩子的待审核任务
  233. */
  234. @Operation(summary = "获取待审核任务列表")
  235. @ApiResponses({
  236. @ApiResponse(responseCode = "200", description = "成功返回待审核任务"),
  237. @ApiResponse(responseCode = "403", description = "Access denied"),
  238. @ApiResponse(responseCode = "401", description = "未登录")
  239. })
  240. @PostMapping("/{familyId}/children/{memberId}/pending-review")
  241. public Result<List<Task>> getPendingReviewTasks(
  242. @RequestAttribute("userId") Long guideId,
  243. @PathVariable Long familyId,
  244. @PathVariable Long memberId,
  245. @RequestAttribute("role") String role) {
  246. if (!"teacher".equals(role)) { return Result.error("Access denied"); }
  247. Result<Void> denied = requireBound(guideId, familyId);
  248. if (denied != null) return Result.error(denied.getCode(), denied.getMessage());
  249. List<Task> tasks = taskService.getPendingReviewTasksForFamilyMember(memberId);
  250. return Result.success(tasks);
  251. }
  252. /**
  253. * 审核任务(指导师)
  254. */
  255. @Operation(summary = "审核任务")
  256. @ApiResponses({
  257. @ApiResponse(responseCode = "200", description = "审核成功返回布尔值"),
  258. @ApiResponse(responseCode = "403", description = "Access denied"),
  259. @ApiResponse(responseCode = "401", description = "未登录")
  260. })
  261. @PostMapping("/{familyId}/tasks/{taskId}/review")
  262. public Result<Boolean> reviewTask(
  263. @RequestAttribute("userId") Long guideId,
  264. @PathVariable Long familyId,
  265. @PathVariable Long taskId,
  266. @RequestAttribute("role") String role,
  267. @RequestBody TaskReviewDTO dto) {
  268. if (!"teacher".equals(role)) { return Result.error("Access denied"); }
  269. Result<Void> denied = requireBound(guideId, familyId);
  270. if (denied != null) return Result.error(denied.getCode(), denied.getMessage());
  271. Result<?> memberCheck = checkMemberOnly(taskId);
  272. if (memberCheck != null) {
  273. return Result.error(memberCheck.getMessage());
  274. }
  275. boolean success = taskService.reviewTask(taskId, dto);
  276. return Result.success(success);
  277. }
  278. /**
  279. * 批量审核任务
  280. */
  281. @Operation(summary = "批量审核任务")
  282. @ApiResponses({
  283. @ApiResponse(responseCode = "200", description = "批量审核成功返回统计"),
  284. @ApiResponse(responseCode = "403", description = "Access denied"),
  285. @ApiResponse(responseCode = "401", description = "未登录")
  286. })
  287. @PostMapping("/{familyId}/tasks/batch-review")
  288. public Result<Map<String, Object>> batchReviewTasks(
  289. @RequestAttribute("userId") Long guideId,
  290. @PathVariable Long familyId,
  291. @RequestAttribute("role") String role,
  292. @RequestBody Map<String, Object> params) {
  293. @SuppressWarnings("unchecked")
  294. List<Object> rawTaskIds = (List<Object>) params.get("taskIds");
  295. List<Long> taskIds = new ArrayList<>();
  296. if (rawTaskIds != null) {
  297. for (Object taskId : rawTaskIds) {
  298. taskIds.add(Long.valueOf(taskId.toString()));
  299. }
  300. }
  301. Boolean approved = params.get("approved") != null ? Boolean.valueOf(params.get("approved").toString()) : Boolean.FALSE;
  302. if (!"teacher".equals(role)) { return Result.error("Access denied"); }
  303. Result<Void> denied = requireBound(guideId, familyId);
  304. if (denied != null) return Result.error(denied.getCode(), denied.getMessage());
  305. int successCount = 0;
  306. int failCount = 0;
  307. for (Long taskId : taskIds) {
  308. try {
  309. // 会员任务校验:审核通过会员任务需家庭为会员
  310. Result<?> memberCheck = checkMemberOnly(taskId);
  311. if (memberCheck != null) {
  312. failCount++;
  313. continue;
  314. }
  315. TaskReviewDTO dto = new TaskReviewDTO();
  316. dto.setApproved(approved);
  317. dto.setComment(approved ? "指导师审核通过" : "指导师审核拒绝");
  318. if (taskService.reviewTask(taskId, dto)) {
  319. successCount++;
  320. } else {
  321. failCount++;
  322. }
  323. } catch (Exception e) {
  324. failCount++;
  325. }
  326. }
  327. Map<String, Object> result = new HashMap<>();
  328. result.put("successCount", successCount);
  329. result.put("failCount", failCount);
  330. return Result.success(result);
  331. }
  332. /**
  333. * 指导师修改任务
  334. */
  335. @Operation(summary = "修改任务")
  336. @ApiResponses({
  337. @ApiResponse(responseCode = "200", description = "修改成功"),
  338. @ApiResponse(responseCode = "403", description = "Access denied"),
  339. @ApiResponse(responseCode = "404", description = "任务不存在")
  340. })
  341. @PostMapping("/{familyId}/tasks/{taskId}")
  342. public Result<Boolean> updateTask(
  343. @RequestAttribute("userId") Long guideId,
  344. @PathVariable Long familyId,
  345. @PathVariable Long taskId,
  346. @RequestAttribute("role") String role,
  347. @RequestBody Map<String, Object> taskData) {
  348. if (!"teacher".equals(role)) { return Result.error("Access denied"); }
  349. Result<Void> denied = requireBound(guideId, familyId);
  350. if (denied != null) return Result.error(denied.getCode(), denied.getMessage());
  351. Task task = taskMapper.selectById(taskId);
  352. if (task == null) {
  353. return Result.error("任务不存在");
  354. }
  355. // 验证任务属于该家庭
  356. if (!familyId.equals(task.getFamilyId())) {
  357. return Result.error("任务不属于该家庭");
  358. }
  359. // 更新任务字段
  360. if (taskData.containsKey("title")) {
  361. task.setTitle((String) taskData.get("title"));
  362. }
  363. if (taskData.containsKey("description")) {
  364. task.setDescription((String) taskData.get("description"));
  365. }
  366. if (taskData.containsKey("points")) {
  367. task.setPoints((Integer) taskData.get("points"));
  368. }
  369. if (taskData.containsKey("deadline")) {
  370. task.setDeadline(new java.util.Date((Long) taskData.get("deadline")));
  371. }
  372. if (taskData.containsKey("status")) {
  373. task.setStatus((String) taskData.get("status"));
  374. }
  375. task.setUpdatedAt(new java.util.Date());
  376. taskMapper.updateById(task);
  377. return Result.success(true);
  378. }
  379. /**
  380. * 指导师删除任务
  381. */
  382. @Operation(summary = "删除任务")
  383. @ApiResponses({
  384. @ApiResponse(responseCode = "200", description = "删除成功"),
  385. @ApiResponse(responseCode = "403", description = "Access denied"),
  386. @ApiResponse(responseCode = "404", description = "任务不存在")
  387. })
  388. @PostMapping("/{familyId}/tasks/{taskId}/delete")
  389. public Result<Boolean> deleteTask(
  390. @RequestAttribute("userId") Long guideId,
  391. @PathVariable Long familyId,
  392. @PathVariable Long taskId,
  393. @RequestAttribute("role") String role) {
  394. if (!"teacher".equals(role)) { return Result.error("Access denied"); }
  395. Result<Void> denied = requireBound(guideId, familyId);
  396. if (denied != null) return Result.error(denied.getCode(), denied.getMessage());
  397. Task task = taskMapper.selectById(taskId);
  398. if (task == null) {
  399. return Result.error("任务不存在");
  400. }
  401. // 验证任务属于该家庭
  402. if (!familyId.equals(task.getFamilyId())) {
  403. return Result.error("任务不属于该家庭");
  404. }
  405. // 指导师可以删除任何任务(不只是创建者)
  406. task.setStatus("cancelled");
  407. task.setUpdatedAt(new java.util.Date());
  408. taskMapper.updateById(task);
  409. return Result.success(true);
  410. }
  411. /**
  412. * 获取规划师首页统计(实际数据)
  413. */
  414. @Operation(summary = "获取规划师首页统计")
  415. @PostMapping("/dashboard-stats")
  416. public Result<Map<String, Object>> getDashboardStats(@RequestAttribute("userId") Long guideId, @RequestAttribute("role") String role) {
  417. if (!"teacher".equals(role)) { return Result.error("Access denied"); }
  418. User guide = userMapper.selectById(guideId);
  419. long totalFamilies = 0;
  420. long todayCompleted = 0;
  421. long pendingReview = 0;
  422. if (guide != null && guide.getTeacherFamilyIds() != null && !guide.getTeacherFamilyIds().isEmpty()) {
  423. List<Long> familyIds = Arrays.stream(guide.getTeacherFamilyIds().split(","))
  424. .map(String::trim).filter(s -> !s.isEmpty()).map(Long::parseLong)
  425. .collect(Collectors.toList());
  426. totalFamilies = familyIds.size();
  427. // 今天0点
  428. Date todayStart = Date.from(LocalDate.now().atStartOfDay(ZoneId.systemDefault()).toInstant());
  429. for (Long familyId : familyIds) {
  430. List<FamilyMember> children = familyMemberMapper.selectList(
  431. new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<FamilyMember>()
  432. .eq(FamilyMember::getFamilyId, familyId));
  433. for (FamilyMember child : children) {
  434. // 今日完成任务:status=completed 且 completedAt >= todayStart
  435. long completed = taskMapper.selectCount(
  436. new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<Task>()
  437. .eq(Task::getFamilyMemberId, child.getId())
  438. .eq(Task::getStatus, "completed")
  439. .ge(Task::getCompletedAt, todayStart));
  440. todayCompleted += completed;
  441. // 待审核任务
  442. long pending = taskMapper.selectCount(
  443. new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<Task>()
  444. .eq(Task::getFamilyMemberId, child.getId())
  445. .eq(Task::getNeedReview, 1)
  446. .eq(Task::getStatus, "pending_review"));
  447. pendingReview += pending;
  448. }
  449. }
  450. }
  451. Map<String, Object> stats = new HashMap<>();
  452. stats.put("totalFamilies", totalFamilies);
  453. stats.put("todayCompletedTasks", todayCompleted);
  454. stats.put("pendingReviewTasks", pendingReview);
  455. return Result.success(stats);
  456. }
  457. /**
  458. * 家庭归属校验;返回 null 表示放行,非 null 为 403 响应。
  459. */
  460. private Result<Void> requireBound(Long guideId, Long familyId) {
  461. return guideFamilyAccessGuard.checkBinding(guideId, familyId);
  462. }
  463. }