TaskService.java 39 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055
  1. package com.etotem.cfc.service;
  2. import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  3. import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
  4. import com.etotem.cfc.dto.CreateTaskDTO;
  5. import com.etotem.cfc.dto.TaskReviewDTO;
  6. import com.etotem.cfc.entity.*;
  7. import com.etotem.cfc.mapper.*;
  8. import com.etotem.cfc.service.api.TaskServiceInterface;
  9. import javax.annotation.Resource;
  10. import lombok.extern.slf4j.Slf4j;
  11. import org.springframework.stereotype.Service;
  12. import org.springframework.transaction.annotation.Transactional;
  13. import java.util.*;
  14. import com.etotem.cfc.util.SortUtil;
  15. @Slf4j
  16. @Service
  17. public class TaskService implements TaskServiceInterface {
  18. @Resource
  19. private TaskMapper taskMapper;
  20. @Resource
  21. private FamilyMemberMapper familyMemberMapper;
  22. @Resource
  23. private UserMapper userMapper;
  24. @Resource
  25. private PointsLogMapper pointsLogMapper;
  26. @Resource
  27. private UserService userService;
  28. @Resource
  29. private StreakService streakService;
  30. @Resource
  31. private MiniGameService miniGameService;
  32. @Resource
  33. private MiniGameMapper miniGameMapper;
  34. @Resource
  35. private EnergyService energyService;
  36. @Resource
  37. private OnboardingService onboardingService;
  38. @Resource
  39. private FamilyRelationshipScoreMapper familyRelationshipScoreMapper;
  40. @Resource
  41. private GrowthTaskService growthTaskService;
  42. private static final int EARLY_BONUS = 1; // 提前完成奖励
  43. private static final int PENALTY_MAX_DAILY = 5; // 每日最多扣分
  44. private static final int LATE_GRACE_MINUTES = 10; // 10分钟内不算迟到
  45. @Transactional
  46. public Long createTask(Long userId, CreateTaskDTO dto) {
  47. User currentUser = userService.getUserInfo(userId);
  48. // 处理执行者类型 - 支持孩子任务、家长任务、家庭成员任务
  49. String executorType = dto.getExecutorType();
  50. if (executorType == null || executorType.isEmpty()) {
  51. executorType = "child";
  52. }
  53. if ("member".equals(executorType)) {
  54. Long currentMemberId = getCurrentMemberId(userId);
  55. long familyMemberCount = familyMemberMapper.selectCount(
  56. new LambdaQueryWrapper<FamilyMember>()
  57. .eq(FamilyMember::getFamilyId, currentUser.getFamilyId()));
  58. if (familyMemberCount < 2) {
  59. throw new IllegalArgumentException("该任务需分配给其他家庭成员,当前家庭不足2人");
  60. }
  61. List<Long> assigneeIds = dto.getMemberIds();
  62. if (assigneeIds == null || assigneeIds.isEmpty()) {
  63. if (dto.getExecutorId() != null) {
  64. assigneeIds = Collections.singletonList(dto.getExecutorId());
  65. } else if (dto.getMemberId() != null) {
  66. assigneeIds = Collections.singletonList(dto.getMemberId());
  67. } else {
  68. assigneeIds = Collections.emptyList();
  69. }
  70. }
  71. for (Long assigneeId : assigneeIds) {
  72. if (assigneeId == null) continue;
  73. if (assigneeId.equals(currentMemberId)) {
  74. throw new IllegalArgumentException("不能给自己分配家庭成员任务");
  75. }
  76. Long count = familyMemberMapper.selectCount(
  77. new LambdaQueryWrapper<FamilyMember>()
  78. .eq(FamilyMember::getId, assigneeId)
  79. .eq(FamilyMember::getFamilyId, currentUser.getFamilyId()));
  80. if (count == 0) {
  81. throw new IllegalArgumentException("被分配成员不属于本家庭");
  82. }
  83. }
  84. }
  85. // 支持多孩子:为每个孩子创建独立任务实例
  86. List<Long> childIds = dto.getMemberIds();
  87. if (childIds == null || childIds.isEmpty()) {
  88. // 兼容旧版:使用单个childId
  89. if (dto.getMemberId() != null) {
  90. childIds = Collections.singletonList(dto.getMemberId());
  91. } else if (dto.getExecutorId() != null && "child".equals(executorType)) {
  92. childIds = Collections.singletonList(dto.getExecutorId());
  93. } else {
  94. childIds = Collections.emptyList();
  95. }
  96. }
  97. // 如果有多个孩子,为每个孩子创建任务
  98. if (!childIds.isEmpty() && childIds.size() > 1) {
  99. Long firstChildId = childIds.get(0);
  100. Task task = createSingleTask(currentUser, userId, executorType, firstChildId, dto);
  101. taskMapper.insert(task);
  102. // 为其他孩子创建任务实例
  103. for (int i = 1; i < childIds.size(); i++) {
  104. Task additionalTask = createSingleTask(currentUser, userId, executorType, childIds.get(i), dto);
  105. taskMapper.insert(additionalTask);
  106. }
  107. return task.getId();
  108. } else {
  109. // 单个孩子或无孩子
  110. Long memberId = childIds.isEmpty() ? null : childIds.get(0);
  111. Task task = createSingleTask(currentUser, userId, executorType, memberId, dto);
  112. taskMapper.insert(task);
  113. return task.getId();
  114. }
  115. }
  116. /**
  117. * 创建单个任务实例
  118. */
  119. private Task createSingleTask(User currentUser, Long userId, String executorType, Long memberId, CreateTaskDTO dto) {
  120. Task task = new Task();
  121. task.setFamilyId(currentUser.getFamilyId());
  122. task.setCreatorId(userId);
  123. task.setExecutorType(executorType);
  124. if ("child".equals(executorType)) {
  125. task.setExecutorId(memberId);
  126. task.setChildId(memberId);
  127. } else if ("member".equals(executorType)) {
  128. task.setExecutorId(memberId);
  129. task.setChildId(null);
  130. task.setStatus("assigned");
  131. task.setAcceptDeadline(dto.getDeadline());
  132. } else {
  133. task.setExecutorId(dto.getExecutorId() != null ? dto.getExecutorId() : userId);
  134. task.setChildId(null);
  135. }
  136. task.setTitle(dto.getTitle());
  137. task.setDescription(dto.getDescription());
  138. task.setPoints(dto.getPoints() != null ? dto.getPoints() : 2);
  139. task.setDuration(dto.getDuration());
  140. // 处理小游戏任务
  141. task.setMinigameCode(dto.getMinigameCode());
  142. if ("小游戏类".equals(dto.getCategory()) && dto.getMinigameCode() != null) {
  143. // 小游戏任务:从MiniGame获取默认积分
  144. MiniGame miniGame = miniGameMapper.selectOne(
  145. new LambdaQueryWrapper<MiniGame>()
  146. .eq(MiniGame::getGameCode, dto.getMinigameCode())
  147. .eq(MiniGame::getStatus, 1)
  148. );
  149. if (miniGame != null) {
  150. if (dto.getPoints() == null) {
  151. task.setPoints(miniGame.getDefaultPoints() != null ? miniGame.getDefaultPoints() : 5);
  152. }
  153. if (dto.getTitle() == null || dto.getTitle().isEmpty()) {
  154. task.setTitle(miniGame.getGameName());
  155. }
  156. }
  157. }
  158. // 如果没有指定截止时间,默认今天23:59
  159. if (dto.getDeadline() == null) {
  160. Calendar cal = Calendar.getInstance();
  161. cal.set(Calendar.HOUR_OF_DAY, 23);
  162. cal.set(Calendar.MINUTE, 59);
  163. cal.set(Calendar.SECOND, 59);
  164. task.setDeadline(cal.getTime());
  165. } else {
  166. task.setDeadline(dto.getDeadline());
  167. }
  168. task.setRepeatType(dto.getRepeatType() != null ? dto.getRepeatType() : "none");
  169. task.setCategory(dto.getCategory());
  170. // 任务性质
  171. task.setTaskType(dto.getTaskType());
  172. task.setFrequency(dto.getFrequency());
  173. task.setMaxFrequency(dto.getMaxFrequency());
  174. // 完成方式(转为JSON存储)
  175. if (dto.getCompleteTypes() != null && !dto.getCompleteTypes().isEmpty()) {
  176. task.setCompleteTypes(String.join(",", dto.getCompleteTypes()));
  177. }
  178. // 审核设置
  179. task.setReviewType(dto.getReviewType());
  180. // 时间设置
  181. task.setEarliestStart(dto.getEarliestStart());
  182. task.setLatestEnd(dto.getLatestEnd());
  183. // 如果是重复任务,标记为模板
  184. if (!"none".equals(task.getRepeatType())) {
  185. task.setIsTemplate(1); // 模板任务
  186. } else {
  187. task.setIsTemplate(0);
  188. }
  189. // 家长给孩子安排任务需要审核,孩子给家长安排任务也需要审核
  190. if ("child".equals(executorType)) {
  191. task.setNeedReview(dto.getNeedReview() != null ? dto.getNeedReview() : 0);
  192. } else {
  193. // 孩子给家长安排任务,默认需要审核
  194. task.setNeedReview(dto.getNeedReview() != null ? dto.getNeedReview() : 1);
  195. }
  196. task.setReviewByCategory(dto.getReviewByCategory() != null ? dto.getReviewByCategory() : 0);
  197. if (!"assigned".equals(task.getStatus())) {
  198. task.setStatus("pending");
  199. }
  200. task.setCreatedAt(new Date());
  201. task.setUpdatedAt(new Date());
  202. // 五维维度
  203. task.setDimensionCode(dto.getDimensionCode());
  204. task.setDimensionWeights(dto.getDimensionWeights());
  205. // B20: 会员任务标记透传(自建任务 memberOnly=0,不校验会员)
  206. task.setMemberOnly(dto.getMemberOnly() != null ? dto.getMemberOnly() : 0);
  207. return task;
  208. }
  209. private Long getCurrentMemberId(Long userId) {
  210. FamilyMember member = familyMemberMapper.selectOne(
  211. new LambdaQueryWrapper<FamilyMember>()
  212. .eq(FamilyMember::getUserId, userId)
  213. .last("LIMIT 1"));
  214. return member != null ? member.getId() : null;
  215. }
  216. /**
  217. * 实现接口:获取今日任务列表(按childId,不过滤维度)
  218. */
  219. @Override
  220. public List<Task> getTodayTasks(Long memberId) {
  221. return getTodayTasks(memberId, null);
  222. }
  223. public List<Task> getTodayTasks(Long memberId, String dimensionCode) {
  224. Calendar calendar = Calendar.getInstance();
  225. calendar.set(Calendar.HOUR_OF_DAY, 0);
  226. calendar.set(Calendar.MINUTE, 0);
  227. calendar.set(Calendar.SECOND, 0);
  228. calendar.set(Calendar.MILLISECOND, 0);
  229. Date startOfDay = calendar.getTime();
  230. calendar.add(Calendar.DAY_OF_MONTH, 1);
  231. Date endOfDay = calendar.getTime();
  232. LambdaQueryWrapper<Task> wrapper = new LambdaQueryWrapper<Task>()
  233. .eq(Task::getChildId, memberId)
  234. .ge(Task::getDeadline, startOfDay)
  235. .lt(Task::getDeadline, endOfDay)
  236. .in(Task::getStatus, Arrays.asList("pending", "completed"))
  237. .orderByAsc(Task::getDeadline);
  238. if (dimensionCode != null && !dimensionCode.isEmpty()) {
  239. wrapper.and(w -> w
  240. .apply("COALESCE(JSON_EXTRACT(dimension_weights, CONCAT('$.', {0})), 0) > 0", dimensionCode)
  241. .or().eq(Task::getDimensionCode, dimensionCode)
  242. );
  243. }
  244. SortUtil.applySort(wrapper);
  245. return taskMapper.selectList(wrapper);
  246. }
  247. /**
  248. * 按维度分类获取今日任务(五维页面筛选用)
  249. */
  250. public List<Task> getTodayTasksByCategory(Long memberId, String category) {
  251. Calendar calendar = Calendar.getInstance();
  252. calendar.set(Calendar.HOUR_OF_DAY, 0);
  253. calendar.set(Calendar.MINUTE, 0);
  254. calendar.set(Calendar.SECOND, 0);
  255. calendar.set(Calendar.MILLISECOND, 0);
  256. Date startOfDay = calendar.getTime();
  257. calendar.add(Calendar.DAY_OF_MONTH, 1);
  258. Date endOfDay = calendar.getTime();
  259. return taskMapper.selectList(new LambdaQueryWrapper<Task>()
  260. .eq(Task::getChildId, memberId)
  261. .eq(Task::getCategory, category)
  262. .ge(Task::getDeadline, startOfDay)
  263. .lt(Task::getDeadline, endOfDay)
  264. .in(Task::getStatus, Arrays.asList("pending", "completed"))
  265. .orderByAsc(Task::getDeadline));
  266. }
  267. /**
  268. * 获取孩子的任务统计数据
  269. */
  270. public Map<String, Object> getChildTaskStats(Long memberId) {
  271. Map<String, Object> stats = new HashMap<>();
  272. Date now = new Date();
  273. Calendar cal = Calendar.getInstance();
  274. cal.setTime(now);
  275. cal.set(Calendar.HOUR_OF_DAY, 0);
  276. cal.set(Calendar.MINUTE, 0);
  277. cal.set(Calendar.SECOND, 0);
  278. cal.set(Calendar.MILLISECOND, 0);
  279. Date todayStart = cal.getTime();
  280. cal.set(Calendar.HOUR_OF_DAY, 23);
  281. cal.set(Calendar.MINUTE, 59);
  282. cal.set(Calendar.SECOND, 59);
  283. cal.set(Calendar.MILLISECOND, 999);
  284. Date todayEnd = cal.getTime();
  285. List<Task> allTasks = taskMapper.selectList(
  286. new LambdaQueryWrapper<Task>()
  287. .eq(Task::getChildId, memberId)
  288. .between(Task::getDeadline, todayStart, todayEnd)
  289. );
  290. long completed = allTasks.stream()
  291. .filter(t -> "completed".equals(t.getStatus()))
  292. .count();
  293. int completionRate = allTasks.isEmpty() ? 0 : (int) (completed * 100 / allTasks.size());
  294. List<Task> pendingReview = taskMapper.selectList(
  295. new LambdaQueryWrapper<Task>()
  296. .eq(Task::getChildId, memberId)
  297. .eq(Task::getNeedReview, 1)
  298. .eq(Task::getStatus, "pending")
  299. );
  300. long habitCount = allTasks.stream()
  301. .filter(t -> "completed".equals(t.getStatus()))
  302. .filter(t -> "habit".equals(t.getCategory()))
  303. .count();
  304. stats.put("completionRate", completionRate);
  305. stats.put("pendingReviewCount", pendingReview.size());
  306. stats.put("habitCount", (int) habitCount);
  307. return stats;
  308. }
  309. @Transactional
  310. public Map<String, Object> completeTask(Long taskId, Long memberId, String photoUrl) {
  311. Task task = taskMapper.selectById(taskId);
  312. if (task == null || !task.getChildId().equals(memberId)) {
  313. return null;
  314. }
  315. if ("completed".equals(task.getStatus())) {
  316. return null; // 任务已完成,不能重复提交
  317. }
  318. Date now = new Date();
  319. int pointsEarned;
  320. // 检查是否为小游戏任务
  321. if ("小游戏类".equals(task.getCategory()) && task.getMinigameCode() != null) {
  322. // 小游戏任务不通过此接口完成,需要通过小游戏界面完成
  323. // 这里只返回null,实际完成逻辑在completeMinigameTask方法中
  324. return null;
  325. }
  326. pointsEarned = task.getPoints();
  327. boolean isEarly = now.before(task.getDeadline());
  328. boolean isLateGrace = false;
  329. // 检查是否在宽限期内
  330. if (!isEarly) {
  331. long diffMinutes = (now.getTime() - task.getDeadline().getTime()) / (1000 * 60);
  332. isLateGrace = diffMinutes <= LATE_GRACE_MINUTES;
  333. }
  334. FamilyMember child = familyMemberMapper.selectById(memberId);
  335. // POINT-002: 计算积分(支持自定义超额规则)
  336. String bonusRuleJson = task.getBonusRule();
  337. if (bonusRuleJson != null && !bonusRuleJson.isEmpty()) {
  338. // 解析自定义超额积分规则
  339. pointsEarned = calculatePointsWithBonusRule(task, now, isEarly, isLateGrace, child);
  340. } else {
  341. // 使用默认规则
  342. if (isEarly || isLateGrace) {
  343. // 提前完成或宽限期内完成,获得基础分 + 提前奖励
  344. pointsEarned += EARLY_BONUS;
  345. } else {
  346. // 超时,检查是否开启扣分
  347. pointsEarned = calculateLatePenalty(task, child, memberId);
  348. }
  349. }
  350. // 更新任务状态
  351. task.setStatus("completed");
  352. task.setCompletedAt(now);
  353. task.setUpdatedAt(new Date());
  354. taskMapper.updateById(task);
  355. // 更新孩子积分
  356. child.setTotalPoints(child.getTotalPoints() + pointsEarned);
  357. // 更新连续打卡
  358. Date lastTaskDate = child.getLastTaskDate();
  359. Date today = new Date();
  360. Calendar cal = Calendar.getInstance();
  361. cal.add(Calendar.DAY_OF_MONTH, -1);
  362. Date yesterday = cal.getTime();
  363. if (lastTaskDate == null || lastTaskDate.before(yesterday)) {
  364. child.setStreakDays(1); // 重新开始
  365. } else if (lastTaskDate != null && isSameDay(lastTaskDate, today)) {
  366. // 今天已经完成过任务,不增加天数
  367. } else {
  368. child.setStreakDays(child.getStreakDays() + 1);
  369. }
  370. child.setLastTaskDate(today);
  371. child.setUpdatedAt(new Date());
  372. familyMemberMapper.updateById(child);
  373. // 检查并发放打卡里程碑奖励
  374. int milestoneReward = streakService.checkAndAwardMilestone(memberId, child.getStreakDays());
  375. if (milestoneReward > 0) {
  376. pointsEarned += milestoneReward;
  377. log.info("孩子{}获得打卡里程碑奖励{}积分", memberId, milestoneReward);
  378. }
  379. // 记录积分流水
  380. PointsLog pointsLog = new PointsLog();
  381. pointsLog.setChildId(memberId);
  382. pointsLog.setTaskId(taskId);
  383. pointsLog.setAmount(pointsEarned);
  384. if (pointsEarned > 0) {
  385. pointsLog.setType(isEarly ? "bonus" : "earn");
  386. } else if (pointsEarned < 0) {
  387. pointsLog.setType("penalty");
  388. } else {
  389. pointsLog.setType("earn");
  390. }
  391. pointsLog.setDescription("完成任务: " + task.getTitle());
  392. pointsLog.setCreatedAt(new Date());
  393. // POINT-009: 设置积分过期时间
  394. if (pointsEarned > 0 && task.getPointsExpireDays() != null && task.getPointsExpireDays() > 0) {
  395. Calendar expireCal = Calendar.getInstance();
  396. expireCal.add(Calendar.DAY_OF_MONTH, task.getPointsExpireDays());
  397. pointsLog.setExpireAt(expireCal.getTime());
  398. }
  399. pointsLogMapper.insert(pointsLog);
  400. // 五维能量发放(与积分并行,独立系统,失败不影响积分)
  401. try {
  402. int energyAmount = Math.abs(pointsEarned);
  403. if (energyAmount > 0) {
  404. energyService.awardEnergy(memberId, "task", taskId, energyAmount,
  405. "完成任务: " + task.getTitle(), null);
  406. }
  407. } catch (Exception e) {
  408. log.warn("能量发放失败(不影响积分): memberId={}, taskId={}, error={}",
  409. memberId, taskId, e.getMessage());
  410. }
  411. try { growthTaskService.updateProgress(child.getUserId(), "DAILY_TASK", 1); } catch (Exception e) { log.warn("成长任务进度更新失败: memberId={}, error={}", memberId, e.getMessage()); }
  412. Map<String, Object> result = new HashMap<>();
  413. result.put("pointsEarned", pointsEarned);
  414. result.put("newBalance", child.getTotalPoints());
  415. result.put("needReview", task.getNeedReview() == 1);
  416. return result;
  417. }
  418. public boolean reviewTask(Long taskId, TaskReviewDTO dto) {
  419. Task task = taskMapper.selectById(taskId);
  420. if (task == null) {
  421. return false;
  422. }
  423. // 兼容两种审核字段:approved 和 result
  424. boolean isApproved;
  425. if (dto.getApproved() != null) {
  426. // 使用 approved 字段(推荐)
  427. isApproved = dto.getApproved();
  428. } else if ("pass".equals(dto.getResult())) {
  429. // 兼容 result 字段
  430. isApproved = true;
  431. } else {
  432. isApproved = false;
  433. }
  434. // 更新任务状态
  435. if (isApproved) {
  436. task.setStatus("completed");
  437. } else {
  438. task.setStatus("pending"); // 审核不通过,任务重置
  439. }
  440. task.setUpdatedAt(new Date());
  441. taskMapper.updateById(task);
  442. return true;
  443. }
  444. public Page<Task> getTaskHistory(Long memberId, Integer page, Integer size) {
  445. Page<Task> pageParam = new Page<>(page, size);
  446. return taskMapper.selectPage(pageParam, new LambdaQueryWrapper<Task>()
  447. .eq(Task::getChildId, memberId)
  448. .orderByDesc(Task::getCreatedAt));
  449. }
  450. public Page<Task> getTaskHistoryByCategory(Long memberId, Integer page, Integer size, String category) {
  451. Page<Task> pageParam = new Page<>(page, size);
  452. return taskMapper.selectPage(pageParam, new LambdaQueryWrapper<Task>()
  453. .eq(Task::getChildId, memberId)
  454. .eq(Task::getCategory, category)
  455. .orderByDesc(Task::getCreatedAt));
  456. }
  457. public boolean deleteTask(Long taskId, Long userId) {
  458. Task task = taskMapper.selectById(taskId);
  459. if (task == null || !task.getCreatorId().equals(userId)) {
  460. return false;
  461. }
  462. task.setStatus("cancelled");
  463. task.setUpdatedAt(new Date());
  464. taskMapper.updateById(task);
  465. return true;
  466. }
  467. /**
  468. * 获取待审核任务列表
  469. * 家长查看需要审核的任务
  470. */
  471. public List<Task> getPendingReviewTasks(Long userId) {
  472. // 获取用户信息
  473. User user = userService.getUserInfo(userId);
  474. if (user == null || !"parent".equals(user.getRole())) {
  475. return new ArrayList<>();
  476. }
  477. // 查询该家庭下所有需要审核且已完成的任务
  478. LambdaQueryWrapper<Task> wrapper = new LambdaQueryWrapper<>();
  479. wrapper.eq(Task::getFamilyId, user.getFamilyId())
  480. .eq(Task::getNeedReview, 1)
  481. .eq(Task::getStatus, "completed")
  482. .orderByDesc(Task::getCompletedAt);
  483. SortUtil.applySort(wrapper);
  484. return taskMapper.selectList(wrapper);
  485. }
  486. /**
  487. * 获取家长今日任务 (家长作为执行者的任务)
  488. */
  489. public List<Task> getTodayParentTasks(Long userId) {
  490. Calendar calendar = Calendar.getInstance();
  491. calendar.set(Calendar.HOUR_OF_DAY, 0);
  492. calendar.set(Calendar.MINUTE, 0);
  493. calendar.set(Calendar.SECOND, 0);
  494. calendar.set(Calendar.MILLISECOND, 0);
  495. Date startOfDay = calendar.getTime();
  496. calendar.add(Calendar.DAY_OF_MONTH, 1);
  497. Date endOfDay = calendar.getTime();
  498. User user = userService.getUserInfo(userId);
  499. if (user == null) {
  500. return new ArrayList<>();
  501. }
  502. return taskMapper.selectList(new LambdaQueryWrapper<Task>()
  503. .eq(Task::getFamilyId, user.getFamilyId())
  504. .eq(Task::getExecutorType, "parent")
  505. .eq(Task::getExecutorId, userId)
  506. .ge(Task::getDeadline, startOfDay)
  507. .lt(Task::getDeadline, endOfDay)
  508. .in(Task::getStatus, Arrays.asList("pending", "completed"))
  509. .orderByAsc(Task::getDeadline));
  510. }
  511. /**
  512. * 家长完成任务 (家长作为执行者完成任务)
  513. */
  514. @Transactional
  515. public Map<String, Object> completeParentTask(Long taskId, Long userId) {
  516. Task task = taskMapper.selectById(taskId);
  517. if (task == null || !("parent".equals(task.getExecutorType()) && task.getExecutorId().equals(userId))) {
  518. return null;
  519. }
  520. if ("completed".equals(task.getStatus())) {
  521. return null;
  522. }
  523. User user = userMapper.selectById(userId);
  524. if (user == null) {
  525. return null;
  526. }
  527. Date now = new Date();
  528. int pointsEarned = task.getPoints();
  529. boolean isEarly = now.before(task.getDeadline());
  530. // 提前完成获得奖励分
  531. if (isEarly) {
  532. pointsEarned += EARLY_BONUS;
  533. }
  534. // 更新任务状态
  535. task.setStatus("completed");
  536. task.setCompletedAt(now);
  537. task.setUpdatedAt(new Date());
  538. taskMapper.updateById(task);
  539. // 更新家长积分
  540. int newPoints = (user.getTotalPoints() != null ? user.getTotalPoints() : 0) + pointsEarned;
  541. user.setTotalPoints(newPoints);
  542. user.setUpdatedAt(new Date());
  543. userMapper.updateById(user);
  544. // 记录积分流水
  545. PointsLog log = new PointsLog();
  546. log.setChildId(task.getChildId());
  547. log.setAmount(pointsEarned);
  548. log.setType(isEarly ? "earn" : "earn");
  549. log.setDescription("完成任务: " + task.getTitle());
  550. log.setCreatedAt(new Date());
  551. pointsLogMapper.insert(log);
  552. Map<String, Object> result = new HashMap<>();
  553. result.put("pointsEarned", pointsEarned);
  554. try { FamilyMember child = familyMemberMapper.selectById(task.getChildId()); if (child != null) { onboardingService.completeTask(child.getUserId(), "FIRST_TASK"); } } catch (Exception e) { /* onboarding trigger failure should not block task completion */ }
  555. result.put("newBalance", user.getTotalPoints());
  556. result.put("needReview", task.getNeedReview() == 1);
  557. return result;
  558. }
  559. public void batchComplete(List<Long> taskIds, Long userId) {
  560. taskMapper.update(null, new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<Task>()
  561. .in(Task::getId, taskIds)
  562. .and(w -> w.eq(Task::getCreatorId, userId).or().eq(Task::getExecutorId, userId))
  563. .set(Task::getStatus, "completed")
  564. .set(Task::getCompletedAt, new Date()));
  565. }
  566. public void batchDelete(List<Long> taskIds, Long userId) {
  567. taskMapper.update(null, new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<Task>()
  568. .in(Task::getId, taskIds)
  569. .eq(Task::getCreatorId, userId)
  570. .set(Task::getStatus, "cancelled")
  571. .set(Task::getUpdatedAt, new Date()));
  572. }
  573. private boolean isSameDay(Date date1, Date date2) {
  574. Calendar cal1 = Calendar.getInstance();
  575. cal1.setTime(date1);
  576. Calendar cal2 = Calendar.getInstance();
  577. cal2.setTime(date2);
  578. return cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR)
  579. && cal1.get(Calendar.DAY_OF_YEAR) == cal2.get(Calendar.DAY_OF_YEAR);
  580. }
  581. /**
  582. * 获取孩子的待审核任务
  583. */
  584. public List<Task> getPendingReviewTasksForChild(Long memberId) {
  585. return taskMapper.selectList(new LambdaQueryWrapper<Task>()
  586. .eq(Task::getChildId, memberId)
  587. .eq(Task::getNeedReview, 1)
  588. .eq(Task::getStatus, "completed")
  589. .orderByDesc(Task::getCompletedAt));
  590. }
  591. /**
  592. * 获取孩子的所有任务
  593. */
  594. public List<Task> getAllTasksByChild(Long memberId) {
  595. return taskMapper.selectList(new LambdaQueryWrapper<Task>()
  596. .eq(Task::getChildId, memberId)
  597. .orderByDesc(Task::getCreatedAt));
  598. }
  599. /**
  600. * 获取孩子的任务历史(分页)
  601. */
  602. public Page<Task> getTaskHistoryByChild(Long memberId, Integer page, Integer size, String status) {
  603. Page<Task> pageParam = new Page<>(page, size);
  604. LambdaQueryWrapper<Task> wrapper = new LambdaQueryWrapper<Task>()
  605. .eq(Task::getChildId, memberId)
  606. .orderByDesc(Task::getCreatedAt);
  607. if (status != null && !status.isEmpty()) {
  608. wrapper.eq(Task::getStatus, status);
  609. }
  610. SortUtil.applySort(wrapper);
  611. return taskMapper.selectPage(pageParam, wrapper);
  612. }
  613. /**
  614. * POINT-002: 根据自定义规则计算超额积分
  615. * bonusRule JSON格式: {
  616. * "earlyBonus": 1, // 提前完成额外积分
  617. * "earlyMinutes": 30, // 提前多少分钟才算提前(可选)
  618. * "qualityBonus": 2, // 超质完成额外积分(家长评分)
  619. * "qualityThreshold": 90 // 质量评分阈值(可选)
  620. * }
  621. */
  622. private int calculatePointsWithBonusRule(Task task, Date completedAt, boolean isEarly, boolean isLateGrace, FamilyMember child) {
  623. int points = task.getPoints();
  624. try {
  625. // 解析JSON规则
  626. String json = task.getBonusRule();
  627. if (json == null || json.isEmpty()) {
  628. return isEarly || isLateGrace ? points + EARLY_BONUS : calculateLatePenalty(task, child, child.getId());
  629. }
  630. // 简单JSON解析(避免引入额外依赖)
  631. Integer earlyBonus = extractJsonValue(json, "earlyBonus");
  632. Integer earlyMinutes = extractJsonValue(json, "earlyMinutes");
  633. Integer qualityBonus = extractJsonValue(json, "qualityBonus");
  634. // 提前完成奖励
  635. if (isEarly || isLateGrace) {
  636. if (earlyBonus != null) {
  637. points += earlyBonus;
  638. } else {
  639. points += EARLY_BONUS; // 默认+1
  640. }
  641. // 如果设置了提前分钟数,检查是否满足条件
  642. if (earlyMinutes != null && task.getDeadline() != null) {
  643. long diffMinutes = (task.getDeadline().getTime() - completedAt.getTime()) / (1000 * 60);
  644. if (diffMinutes < earlyMinutes) {
  645. // 不满足提前分钟数,只给基础分
  646. points = task.getPoints();
  647. }
  648. }
  649. } else {
  650. // 超时,计算扣分
  651. points = calculateLatePenalty(task, child, child.getId());
  652. }
  653. // 超质完成奖励(需要家长后续评分,这里先不处理)
  654. // qualityBonus 会在家长审核时根据评分添加
  655. } catch (Exception e) {
  656. log.warn("解析超额积分规则失败: {}", e.getMessage());
  657. // 解析失败,使用默认规则
  658. if (isEarly || isLateGrace) {
  659. points += EARLY_BONUS;
  660. } else {
  661. points = calculateLatePenalty(task, child, child.getId());
  662. }
  663. }
  664. return points;
  665. }
  666. /**
  667. * 从简单JSON字符串中提取整数值
  668. */
  669. private Integer extractJsonValue(String json, String key) {
  670. try {
  671. String pattern = "\"" + key + "\"\\s*:\\s*(\\d+)";
  672. java.util.regex.Pattern p = java.util.regex.Pattern.compile(pattern);
  673. java.util.regex.Matcher m = p.matcher(json);
  674. if (m.find()) {
  675. return Integer.parseInt(m.group(1));
  676. }
  677. } catch (Exception e) {
  678. log.warn("提取JSON值失败: key={}", key);
  679. }
  680. return null;
  681. }
  682. /**
  683. * 计算超时扣分
  684. */
  685. private int calculateLatePenalty(Task task, FamilyMember child, Long memberId) {
  686. if (child == null) {
  687. return 0;
  688. }
  689. int points = task.getPoints();
  690. if (child.getPenaltyEnabled() != null && child.getPenaltyEnabled() == 1) {
  691. // 检查今日已扣分数
  692. Calendar cal = Calendar.getInstance();
  693. cal.set(Calendar.HOUR_OF_DAY, 0);
  694. cal.set(Calendar.MINUTE, 0);
  695. cal.set(Calendar.SECOND, 0);
  696. Date startOfDay = cal.getTime();
  697. Integer todayPenalty = pointsLogMapper.selectSum(
  698. new LambdaQueryWrapper<PointsLog>()
  699. .eq(PointsLog::getChildId, memberId)
  700. .eq(PointsLog::getType, "penalty")
  701. .ge(PointsLog::getCreatedAt, startOfDay)
  702. );
  703. int todayPenaltyTotal = todayPenalty != null ? Math.abs(todayPenalty) : 0;
  704. if (todayPenaltyTotal < PENALTY_MAX_DAILY) {
  705. int penalty = Math.min(task.getPoints(), PENALTY_MAX_DAILY - todayPenaltyTotal);
  706. return -penalty;
  707. } else {
  708. return 0; // 达到每日扣分上限
  709. }
  710. } else {
  711. return 0; // 扣分未开启
  712. }
  713. }
  714. /**
  715. * 完成小游戏任务
  716. * @param taskId 任务ID
  717. * @param memberId 孩子ID
  718. * @param completionTime 完成用时(秒)
  719. * @param score 得分(可选)
  720. * @return 积分信息
  721. */
  722. @Transactional
  723. public Map<String, Object> completeMinigameTask(Long taskId, Long memberId, Integer completionTime, Integer score) {
  724. Task task = taskMapper.selectById(taskId);
  725. if (task == null || !task.getChildId().equals(memberId)) {
  726. throw new RuntimeException("任务不存在或不属于该孩子");
  727. }
  728. if ("completed".equals(task.getStatus())) {
  729. throw new RuntimeException("任务已完成");
  730. }
  731. if (!"小游戏类".equals(task.getCategory()) || task.getMinigameCode() == null) {
  732. throw new RuntimeException("该任务不是小游戏任务");
  733. }
  734. // 调用MiniGameService计算积分
  735. Map<String, Object> gameResult = miniGameService.completeGame(memberId, task.getMinigameCode(), completionTime, score);
  736. int pointsEarned = (Integer) gameResult.get("pointsEarned");
  737. // 更新任务状态
  738. task.setStatus("completed");
  739. task.setCompletedAt(new Date());
  740. task.setUpdatedAt(new Date());
  741. taskMapper.updateById(task);
  742. // 更新连续打卡
  743. FamilyMember child = familyMemberMapper.selectById(memberId);
  744. Date lastTaskDate = child.getLastTaskDate();
  745. Date today = new Date();
  746. Calendar cal = Calendar.getInstance();
  747. cal.add(Calendar.DAY_OF_MONTH, -1);
  748. Date yesterday = cal.getTime();
  749. if (lastTaskDate == null || lastTaskDate.before(yesterday)) {
  750. child.setStreakDays(1); // 重新开始
  751. } else if (lastTaskDate != null && isSameDay(lastTaskDate, today)) {
  752. // 今天已经完成过任务,不增加天数
  753. } else {
  754. child.setStreakDays(child.getStreakDays() + 1);
  755. }
  756. child.setLastTaskDate(today);
  757. child.setUpdatedAt(new Date());
  758. familyMemberMapper.updateById(child);
  759. // 检查并发放打卡里程碑奖励
  760. int milestoneReward = streakService.checkAndAwardMilestone(memberId, child.getStreakDays());
  761. if (milestoneReward > 0) {
  762. pointsEarned += milestoneReward;
  763. log.info("孩子{}获得打卡里程碑奖励{}积分", memberId, milestoneReward);
  764. }
  765. Map<String, Object> result = new HashMap<>();
  766. result.put("pointsEarned", pointsEarned);
  767. result.put("newBalance", child.getTotalPoints());
  768. result.put("gameName", gameResult.get("gameName"));
  769. result.put("needReview", task.getNeedReview() == 1);
  770. return result;
  771. }
  772. /**
  773. * 获取家庭待办任务
  774. */
  775. public List<Task> getPendingTasksByFamily(Long familyId) {
  776. return taskMapper.selectList(new LambdaQueryWrapper<Task>()
  777. .eq(Task::getFamilyId, familyId)
  778. .in(Task::getStatus, Arrays.asList("pending", "in_progress"))
  779. .orderByAsc(Task::getDeadline));
  780. }
  781. // ==================== Family Member Task queries ====================
  782. /**
  783. * 获取指定家庭成员待审核的任务列表
  784. */
  785. public List<Task> getPendingReviewTasksForFamilyMember(Long familyMemberId) {
  786. return taskMapper.selectList(new LambdaQueryWrapper<Task>()
  787. .eq(Task::getFamilyMemberId, familyMemberId)
  788. .eq(Task::getNeedReview, 1)
  789. .in(Task::getStatus, Arrays.asList("completed", "pending"))
  790. .orderByDesc(Task::getCreatedAt));
  791. }
  792. /**
  793. * 获取指定家庭成员的全部任务
  794. */
  795. public List<Task> getAllTasksByFamilyMember(Long familyMemberId) {
  796. return taskMapper.selectList(new LambdaQueryWrapper<Task>()
  797. .eq(Task::getFamilyMemberId, familyMemberId)
  798. .orderByDesc(Task::getCreatedAt));
  799. }
  800. /**
  801. * 分页获取指定家庭成员的任务历史
  802. */
  803. public Page<Task> getTaskHistoryByFamilyMember(Long familyMemberId, Integer page, Integer size, String status) {
  804. LambdaQueryWrapper<Task> wrapper = new LambdaQueryWrapper<Task>()
  805. .eq(Task::getFamilyMemberId, familyMemberId);
  806. if (status != null && !status.isEmpty()) {
  807. wrapper.eq(Task::getStatus, status);
  808. }
  809. wrapper.orderByDesc(Task::getCreatedAt);
  810. SortUtil.applySort(wrapper);
  811. return taskMapper.selectPage(new Page<>(page, size), wrapper);
  812. }
  813. /**
  814. * 获取当前用户收到的待接受任务列表(executor_type='member' AND status='assigned')
  815. */
  816. public List<Task> getMyPendingTasks(Long memberId) {
  817. Calendar calendar = Calendar.getInstance();
  818. calendar.set(Calendar.HOUR_OF_DAY, 0);
  819. calendar.set(Calendar.MINUTE, 0);
  820. calendar.set(Calendar.SECOND, 0);
  821. calendar.set(Calendar.MILLISECOND, 0);
  822. Date startOfDay = calendar.getTime();
  823. calendar.add(Calendar.DAY_OF_MONTH, 1);
  824. Date endOfDay = calendar.getTime();
  825. return taskMapper.selectList(new LambdaQueryWrapper<Task>()
  826. .eq(Task::getExecutorType, "member")
  827. .eq(Task::getExecutorId, memberId)
  828. .eq(Task::getStatus, "assigned")
  829. .ge(Task::getDeadline, startOfDay)
  830. .lt(Task::getDeadline, endOfDay)
  831. .or()
  832. .and(w -> w.eq(Task::getExecutorType, "member")
  833. .eq(Task::getExecutorId, memberId)
  834. .eq(Task::getStatus, "assigned")
  835. .isNull(Task::getDeadline))
  836. .orderByAsc(Task::getDeadline));
  837. }
  838. /**
  839. * 接受任务:status 从 assigned 改为 in_progress
  840. */
  841. @Transactional
  842. public boolean acceptTask(Long taskId, Long memberId) {
  843. Task task = taskMapper.selectById(taskId);
  844. if (task == null || !task.getExecutorId().equals(memberId) || !"assigned".equals(task.getStatus())) {
  845. return false;
  846. }
  847. task.setStatus("in_progress");
  848. task.setAcceptDeadline(null);
  849. task.setUpdatedAt(new Date());
  850. taskMapper.updateById(task);
  851. return true;
  852. }
  853. /**
  854. * 拒绝任务:status 改为 rejected,调整 creator→assignee 和 assignee→creator 的关系分数
  855. */
  856. @Transactional
  857. public boolean rejectTask(Long taskId, Long memberId) {
  858. Task task = taskMapper.selectById(taskId);
  859. if (task == null || !task.getExecutorId().equals(memberId) || !"assigned".equals(task.getStatus())) {
  860. return false;
  861. }
  862. task.setStatus("rejected");
  863. task.setUpdatedAt(new Date());
  864. taskMapper.updateById(task);
  865. Long creatorId = task.getCreatorId();
  866. Long assigneeId = memberId;
  867. Long familyId = task.getFamilyId();
  868. if (creatorId == null || assigneeId == null || familyId == null) return true;
  869. int trustDelta = -1;
  870. int intimacyDelta = -1;
  871. int commDelta = -1;
  872. adjustRelationshipScore(familyId, creatorId, assigneeId, trustDelta, intimacyDelta, commDelta);
  873. adjustRelationshipScore(familyId, assigneeId, creatorId, 0, 0, 1);
  874. return true;
  875. }
  876. private void adjustRelationshipScore(Long familyId, Long fromId, Long toId, int trustDelta, int intimacyDelta, int commDelta) {
  877. FamilyRelationshipScore score = familyRelationshipScoreMapper.selectOne(
  878. new LambdaQueryWrapper<FamilyRelationshipScore>()
  879. .eq(FamilyRelationshipScore::getFamilyId, familyId)
  880. .eq(FamilyRelationshipScore::getFromMemberId, fromId)
  881. .eq(FamilyRelationshipScore::getToMemberId, toId));
  882. if (score == null) {
  883. FamilyMember from = familyMemberMapper.selectById(fromId);
  884. score = new FamilyRelationshipScore();
  885. score.setFamilyId(familyId);
  886. score.setFromMemberId(fromId);
  887. score.setToMemberId(toId);
  888. score.setTrustScore(from != null && from.getTrustScore() != null ? from.getTrustScore() : new java.math.BigDecimal("60.00"));
  889. score.setIntimacyScore(from != null && from.getIntimacyScore() != null ? from.getIntimacyScore() : new java.math.BigDecimal("60.00"));
  890. score.setCommunicationScore(from != null && from.getCommunicationScore() != null ? from.getCommunicationScore() : new java.math.BigDecimal("60.00"));
  891. try {
  892. familyRelationshipScoreMapper.insert(score);
  893. } catch (Exception e) {
  894. score = familyRelationshipScoreMapper.selectOne(
  895. new LambdaQueryWrapper<FamilyRelationshipScore>()
  896. .eq(FamilyRelationshipScore::getFamilyId, familyId)
  897. .eq(FamilyRelationshipScore::getFromMemberId, fromId)
  898. .eq(FamilyRelationshipScore::getToMemberId, toId));
  899. }
  900. }
  901. if (score == null) return;
  902. if (trustDelta != 0) score.setTrustScore(score.getTrustScore().add(java.math.BigDecimal.valueOf(trustDelta)));
  903. if (intimacyDelta != 0) score.setIntimacyScore(score.getIntimacyScore().add(java.math.BigDecimal.valueOf(intimacyDelta)));
  904. if (commDelta != 0) score.setCommunicationScore(score.getCommunicationScore().add(java.math.BigDecimal.valueOf(commDelta)));
  905. familyRelationshipScoreMapper.updateById(score);
  906. }
  907. }