TaskService.java 82 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994
  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.common.Result;
  5. import com.etotem.cfc.dto.CreateProductOrderDTO;
  6. import com.etotem.cfc.dto.CreateTaskDTO;
  7. import com.etotem.cfc.dto.ProductOrderDTO;
  8. import com.etotem.cfc.dto.TaskReviewDTO;
  9. import com.etotem.cfc.entity.*;
  10. import com.etotem.cfc.mapper.*;
  11. import com.etotem.cfc.service.api.TaskServiceInterface;
  12. import com.alibaba.fastjson.JSON;
  13. import com.alibaba.fastjson.JSONObject;
  14. import com.etotem.cfc.service.OrchestrationEngine;
  15. import javax.annotation.Resource;
  16. import lombok.extern.slf4j.Slf4j;
  17. import org.springframework.context.annotation.Lazy;
  18. import org.springframework.stereotype.Service;
  19. import org.springframework.transaction.annotation.Transactional;
  20. import java.util.*;
  21. import com.etotem.cfc.util.DimensionCodes;
  22. import com.etotem.cfc.util.SortUtil;
  23. import com.etotem.cfc.service.NotificationDispatchService;
  24. @Slf4j
  25. @Service
  26. public class TaskService implements TaskServiceInterface {
  27. @Resource
  28. private TaskMapper taskMapper;
  29. @Resource
  30. private NotificationDispatchService notificationDispatchService;
  31. @Resource
  32. private TaskReminderConfigService taskReminderConfigService;
  33. @Resource
  34. private TaskExecutionMapper taskExecutionMapper;
  35. @Resource
  36. private FamilyMemberMapper familyMemberMapper;
  37. @Resource
  38. private UserMapper userMapper;
  39. @Resource
  40. private PointsLogMapper pointsLogMapper;
  41. @Resource
  42. @Lazy
  43. private UserService userService;
  44. @Resource
  45. private StreakService streakService;
  46. @Resource
  47. private MiniGameService miniGameService;
  48. @Resource
  49. private MiniGameMapper miniGameMapper;
  50. @Resource
  51. private EnergyService energyService;
  52. @Resource
  53. private FamilyRelationshipScoreMapper familyRelationshipScoreMapper;
  54. @Resource
  55. private CartService cartService;
  56. @Resource
  57. private ProductOrderService productOrderService;
  58. @Resource
  59. private ConsigneeMapper consigneeMapper;
  60. @Resource
  61. private SurveyService surveyService;
  62. @Resource
  63. private SurveyTemplateMapper surveyTemplateMapper;
  64. @Resource
  65. @Lazy
  66. private OrchestrationEngine orchestrationEngine;
  67. @Resource
  68. private TaskTemplateMapper taskTemplateMapper;
  69. @Resource
  70. private PlatformPointsService platformPointsService;
  71. @Resource
  72. private FamilyPlatformPointsService familyPlatformPointsService;
  73. private static final int EARLY_BONUS = 1; // 提前完成奖励
  74. private static final int PENALTY_MAX_DAILY = 5; // 每日最多扣分
  75. private static final int LATE_GRACE_MINUTES = 10; // 10分钟内不算迟到
  76. @Transactional
  77. public Long createTask(Long userId, CreateTaskDTO dto) {
  78. User currentUser = userService.getUserInfo(userId);
  79. // 处理执行者类型 - 支持孩子任务、家长任务、家庭成员任务
  80. String executorType = dto.getExecutorType();
  81. if (executorType == null || executorType.isEmpty()) {
  82. executorType = "child";
  83. }
  84. if ("member".equals(executorType)) {
  85. Long currentMemberId = getCurrentMemberId(userId);
  86. long familyMemberCount = familyMemberMapper.selectCount(
  87. new LambdaQueryWrapper<FamilyMember>()
  88. .eq(FamilyMember::getFamilyId, currentUser.getFamilyId()));
  89. if (familyMemberCount < 2) {
  90. throw new IllegalArgumentException("该任务需分配给其他家庭成员,当前家庭不足2人");
  91. }
  92. List<Long> assigneeIds = dto.getMemberIds();
  93. if (assigneeIds == null || assigneeIds.isEmpty()) {
  94. if (dto.getExecutorId() != null) {
  95. assigneeIds = Collections.singletonList(dto.getExecutorId());
  96. } else if (dto.getMemberId() != null) {
  97. assigneeIds = Collections.singletonList(dto.getMemberId());
  98. } else {
  99. assigneeIds = Collections.emptyList();
  100. }
  101. }
  102. for (Long assigneeId : assigneeIds) {
  103. if (assigneeId == null) continue;
  104. if (assigneeId.equals(currentMemberId)) {
  105. throw new IllegalArgumentException("不能给自己分配家庭成员任务");
  106. }
  107. Long count = familyMemberMapper.selectCount(
  108. new LambdaQueryWrapper<FamilyMember>()
  109. .eq(FamilyMember::getId, assigneeId)
  110. .eq(FamilyMember::getFamilyId, currentUser.getFamilyId()));
  111. if (count == 0) {
  112. throw new IllegalArgumentException("被分配成员不属于本家庭");
  113. }
  114. }
  115. }
  116. // 支持多孩子:为每个孩子创建独立任务实例
  117. List<Long> childIds = dto.getMemberIds();
  118. if (childIds == null || childIds.isEmpty()) {
  119. // 兼容旧版:使用单个childId
  120. if (dto.getMemberId() != null) {
  121. childIds = Collections.singletonList(dto.getMemberId());
  122. } else if (dto.getExecutorId() != null && "child".equals(executorType)) {
  123. childIds = Collections.singletonList(dto.getExecutorId());
  124. } else {
  125. childIds = Collections.emptyList();
  126. }
  127. }
  128. // 如果有多个孩子,为每个孩子创建任务
  129. if (!childIds.isEmpty() && childIds.size() > 1) {
  130. Long firstChildId = childIds.get(0);
  131. Task task = createSingleTask(currentUser, userId, executorType, firstChildId, dto);
  132. taskMapper.insert(task);
  133. ensureSurveyTemplateForTask(task);
  134. // 为其他孩子创建任务实例
  135. for (int i = 1; i < childIds.size(); i++) {
  136. Task additionalTask = createSingleTask(currentUser, userId, executorType, childIds.get(i), dto);
  137. taskMapper.insert(additionalTask);
  138. ensureSurveyTemplateForTask(additionalTask);
  139. }
  140. return task.getId();
  141. } else {
  142. // 单个孩子或无孩子
  143. Long memberId = childIds.isEmpty() ? null : childIds.get(0);
  144. Task task = createSingleTask(currentUser, userId, executorType, memberId, dto);
  145. taskMapper.insert(task);
  146. ensureSurveyTemplateForTask(task);
  147. return task.getId();
  148. }
  149. }
  150. /**
  151. * 创建单个任务实例
  152. */
  153. private Task createSingleTask(User currentUser, Long userId, String executorType, Long memberId, CreateTaskDTO dto) {
  154. Task task = new Task();
  155. task.setFamilyId(currentUser.getFamilyId());
  156. task.setCreatorId(userId);
  157. task.setExecutorType(executorType);
  158. if ("child".equals(executorType)) {
  159. task.setExecutorId(memberId);
  160. task.setFamilyMemberId(memberId);
  161. task.setChildId(memberId);
  162. } else if ("member".equals(executorType)) {
  163. task.setExecutorId(memberId);
  164. task.setChildId(null);
  165. task.setStatus("assigned");
  166. task.setAcceptDeadline(dto.getDeadline());
  167. } else {
  168. task.setExecutorId(dto.getExecutorId() != null ? dto.getExecutorId() : userId);
  169. task.setChildId(null);
  170. }
  171. task.setTitle(dto.getTitle());
  172. task.setDescription(dto.getDescription());
  173. task.setPoints(dto.getPoints() != null ? dto.getPoints() : 2);
  174. task.setDuration(dto.getDuration());
  175. // 处理小游戏任务
  176. task.setMinigameCode(dto.getMinigameCode());
  177. if ("小游戏类".equals(dto.getCategory()) && dto.getMinigameCode() != null) {
  178. // 小游戏任务:从MiniGame获取默认积分
  179. MiniGame miniGame = miniGameMapper.selectOne(
  180. new LambdaQueryWrapper<MiniGame>()
  181. .eq(MiniGame::getGameCode, dto.getMinigameCode())
  182. .eq(MiniGame::getStatus, 1)
  183. );
  184. if (miniGame != null) {
  185. if (dto.getPoints() == null) {
  186. task.setPoints(miniGame.getDefaultPoints() != null ? miniGame.getDefaultPoints() : 5);
  187. }
  188. if (dto.getTitle() == null || dto.getTitle().isEmpty()) {
  189. task.setTitle(miniGame.getGameName());
  190. }
  191. }
  192. }
  193. // 如果没有指定截止时间,默认今天23:59
  194. if (dto.getDeadline() == null) {
  195. Calendar cal = Calendar.getInstance();
  196. cal.set(Calendar.HOUR_OF_DAY, 23);
  197. cal.set(Calendar.MINUTE, 59);
  198. cal.set(Calendar.SECOND, 59);
  199. task.setDeadline(cal.getTime());
  200. } else {
  201. task.setDeadline(dto.getDeadline());
  202. }
  203. task.setRepeatType(dto.getRepeatType() != null ? dto.getRepeatType() : "none");
  204. task.setCategory(dto.getCategory());
  205. // 任务性质
  206. task.setTaskType(dto.getTaskType());
  207. task.setFrequency(dto.getFrequency());
  208. task.setMaxFrequency(dto.getMaxFrequency());
  209. // 完成方式(转为JSON存储)
  210. if (dto.getCompleteTypes() != null && !dto.getCompleteTypes().isEmpty()) {
  211. task.setCompleteTypes(String.join(",", dto.getCompleteTypes()));
  212. }
  213. // 审核设置
  214. task.setReviewType(dto.getReviewType());
  215. // 时间设置
  216. task.setEarliestStart(dto.getEarliestStart());
  217. task.setLatestEnd(dto.getLatestEnd());
  218. // 如果是重复任务,标记为模板
  219. if (!"none".equals(task.getRepeatType())) {
  220. task.setIsTemplate(1); // 模板任务
  221. } else {
  222. task.setIsTemplate(0);
  223. }
  224. // 家长给孩子安排任务需要审核,孩子给家长安排任务也需要审核
  225. if ("child".equals(executorType)) {
  226. task.setNeedReview(dto.getNeedReview() != null ? dto.getNeedReview() : 0);
  227. } else {
  228. // 孩子给家长安排任务,默认需要审核
  229. task.setNeedReview(dto.getNeedReview() != null ? dto.getNeedReview() : 1);
  230. }
  231. task.setReviewByCategory(dto.getReviewByCategory() != null ? dto.getReviewByCategory() : 0);
  232. if (!"assigned".equals(task.getStatus())) {
  233. // TASK-SYS-248: 有前置任务且前置未完成 → 初始状态 locked
  234. if (dto.getPrerequisiteTaskId() != null) {
  235. Task prereq = taskMapper.selectById(dto.getPrerequisiteTaskId());
  236. if (prereq == null || !"completed".equals(prereq.getStatus())) {
  237. task.setStatus("locked");
  238. } else {
  239. task.setStatus("pending");
  240. }
  241. } else {
  242. task.setStatus("pending");
  243. }
  244. }
  245. task.setCreatedAt(new Date());
  246. task.setUpdatedAt(new Date());
  247. // 五维维度
  248. task.setDimensionCode(dto.getDimensionCode());
  249. task.setDimensionWeights(dto.getDimensionWeights());
  250. // B20: 会员任务标记透传(自建任务 memberOnly=0,不校验会员)
  251. task.setMemberOnly(dto.getMemberOnly() != null ? dto.getMemberOnly() : 0);
  252. // TASK-SYS-248: 行为调度中心扩展字段透传
  253. task.setPrerequisiteTaskId(dto.getPrerequisiteTaskId());
  254. task.setActionType(dto.getActionType());
  255. task.setActionConfig(dto.getActionConfig());
  256. task.setRequireInput(dto.getRequireInput() != null ? dto.getRequireInput() : 0);
  257. task.setStartRequired(dto.getStartRequired() != null ? dto.getStartRequired() : 0);
  258. task.setStartCount(0);
  259. task.setMinDurationSeconds(dto.getMinDurationSeconds() != null ? dto.getMinDurationSeconds() : 0);
  260. // TASK-SYS-248: 自动计算是否为流程任务
  261. // 有前置任务 或 有后置任务 → 标记为流程任务
  262. if (dto.getPrerequisiteTaskId() != null
  263. || (dto.getSuccessorTaskIds() != null && !dto.getSuccessorTaskIds().isEmpty())) {
  264. task.setIsFlowTask(1);
  265. } else {
  266. task.setIsFlowTask(0);
  267. }
  268. task.setSuccessorTaskIds(dto.getSuccessorTaskIds());
  269. task.setSourceType(dto.getSourceType() != null ? dto.getSourceType() : "self");
  270. task.setSourceId(dto.getSourceId());
  271. task.setIsDailyProgress(dto.getIsDailyProgress() != null ? dto.getIsDailyProgress() : 0);
  272. task.setTargetValue(dto.getTargetValue());
  273. // 进度型任务无 deadline(原 growth_task DAILY:每日重置,无截止时间)
  274. if (task.getIsDailyProgress() != null && task.getIsDailyProgress() == 1) {
  275. task.setDeadline(null);
  276. }
  277. return task;
  278. }
  279. private Long getCurrentMemberId(Long userId) {
  280. FamilyMember member = familyMemberMapper.selectOne(
  281. new LambdaQueryWrapper<FamilyMember>()
  282. .eq(FamilyMember::getUserId, userId)
  283. .last("LIMIT 1"));
  284. return member != null ? member.getId() : null;
  285. }
  286. /**
  287. * 实现接口:获取今日任务列表(按childId,不过滤维度)
  288. */
  289. @Override
  290. public List<Task> getTodayTasks(Long memberId) {
  291. return getTodayTasks(memberId, null);
  292. }
  293. public List<Task> getTodayTasks(Long memberId, String dimensionCode) {
  294. Calendar calendar = Calendar.getInstance();
  295. calendar.set(Calendar.HOUR_OF_DAY, 0);
  296. calendar.set(Calendar.MINUTE, 0);
  297. calendar.set(Calendar.SECOND, 0);
  298. calendar.set(Calendar.MILLISECOND, 0);
  299. Date startOfDay = calendar.getTime();
  300. calendar.add(Calendar.DAY_OF_MONTH, 1);
  301. Date endOfDay = calendar.getTime();
  302. LambdaQueryWrapper<Task> wrapper = new LambdaQueryWrapper<Task>()
  303. .eq(Task::getFamilyMemberId, memberId)
  304. .in(Task::getStatus, Arrays.asList("pending", "completed", "locked", "pending_review"))
  305. .and(w -> w
  306. // 普通任务:按 deadline 判断今日
  307. .and(w2 -> w2.isNotNull(Task::getDeadline)
  308. .ge(Task::getDeadline, startOfDay)
  309. .lt(Task::getDeadline, endOfDay))
  310. // 进度型任务:按 createdAt 判断今日(deadline 为 NULL)
  311. .or(w3 -> w3.eq(Task::getIsDailyProgress, 1)
  312. .ge(Task::getCreatedAt, startOfDay)
  313. .lt(Task::getCreatedAt, endOfDay)))
  314. .orderByAsc(Task::getDeadline);
  315. if (dimensionCode != null && !dimensionCode.isEmpty() && DimensionCodes.isValid(dimensionCode)) {
  316. String dim = dimensionCode.trim();
  317. wrapper.and(w -> w
  318. .apply("COALESCE(JSON_EXTRACT(dimension_weights, CONCAT('$.', {0})), 0) > 0", dim)
  319. .or().eq(Task::getDimensionCode, dim)
  320. );
  321. }
  322. SortUtil.applySort(wrapper);
  323. List<Task> all = taskMapper.selectList(wrapper);
  324. fillProgress(all, memberId);
  325. return filterLockedChildren(all);
  326. }
  327. /**
  328. * 家庭维度今日任务(家长端总览:方案分解任务可能挂在任意家庭成员上)
  329. */
  330. public List<Task> getTodayTasksByFamily(Long familyId) {
  331. if (familyId == null) {
  332. return new ArrayList<>();
  333. }
  334. Calendar calendar = Calendar.getInstance();
  335. calendar.set(Calendar.HOUR_OF_DAY, 0);
  336. calendar.set(Calendar.MINUTE, 0);
  337. calendar.set(Calendar.SECOND, 0);
  338. calendar.set(Calendar.MILLISECOND, 0);
  339. Date startOfDay = calendar.getTime();
  340. calendar.add(Calendar.DAY_OF_MONTH, 1);
  341. Date endOfDay = calendar.getTime();
  342. List<Task> all = taskMapper.selectList(new LambdaQueryWrapper<Task>()
  343. .eq(Task::getFamilyId, familyId)
  344. .ge(Task::getDeadline, startOfDay)
  345. .lt(Task::getDeadline, endOfDay)
  346. .in(Task::getStatus, Arrays.asList("pending", "completed", "locked", "pending_review"))
  347. .orderByAsc(Task::getDeadline)
  348. .orderByAsc(Task::getId));
  349. return filterLockedChildren(all);
  350. }
  351. /**
  352. * 移除"父任务未完成"的子任务(购买任务→每日使用子任务链)。
  353. * 子任务在父购买任务完成(status=completed)前保持锁定,不显示在今日任务中。
  354. */
  355. private List<Task> filterLockedChildren(List<Task> tasks) {
  356. if (tasks == null || tasks.isEmpty()) return tasks;
  357. java.util.Set<Long> parentIds = new java.util.HashSet<>();
  358. for (Task t : tasks) {
  359. if (t.getParentTaskId() != null) parentIds.add(t.getParentTaskId());
  360. }
  361. if (parentIds.isEmpty()) return tasks;
  362. Map<Long, Task> parents = new HashMap<>();
  363. if (!parentIds.isEmpty()) {
  364. List<Task> plist = taskMapper.selectBatchIds(parentIds);
  365. for (Task p : plist) parents.put(p.getId(), p);
  366. }
  367. List<Task> result = new java.util.ArrayList<>();
  368. for (Task t : tasks) {
  369. if (t.getParentTaskId() != null) {
  370. Task parent = parents.get(t.getParentTaskId());
  371. if (parent == null || !"completed".equals(parent.getStatus())) {
  372. continue; // 父购买任务未完成,子任务锁定不显示
  373. }
  374. }
  375. result.add(t);
  376. }
  377. return result;
  378. }
  379. /**
  380. * 按维度分类获取今日任务(五维页面筛选用)
  381. */
  382. public List<Task> getTodayTasksByCategory(Long memberId, String category) {
  383. Calendar calendar = Calendar.getInstance();
  384. calendar.set(Calendar.HOUR_OF_DAY, 0);
  385. calendar.set(Calendar.MINUTE, 0);
  386. calendar.set(Calendar.SECOND, 0);
  387. calendar.set(Calendar.MILLISECOND, 0);
  388. Date startOfDay = calendar.getTime();
  389. calendar.add(Calendar.DAY_OF_MONTH, 1);
  390. Date endOfDay = calendar.getTime();
  391. return filterLockedChildren(taskMapper.selectList(new LambdaQueryWrapper<Task>()
  392. .eq(Task::getFamilyMemberId, memberId)
  393. .eq(Task::getCategory, category)
  394. .ge(Task::getDeadline, startOfDay)
  395. .lt(Task::getDeadline, endOfDay)
  396. .in(Task::getStatus, Arrays.asList("pending", "completed", "pending_review"))
  397. .orderByAsc(Task::getDeadline)));
  398. }
  399. /**
  400. * 获取孩子的任务统计数据
  401. */
  402. public Map<String, Object> getChildTaskStats(Long memberId) {
  403. Map<String, Object> stats = new HashMap<>();
  404. Date now = new Date();
  405. Calendar cal = Calendar.getInstance();
  406. cal.setTime(now);
  407. cal.set(Calendar.HOUR_OF_DAY, 0);
  408. cal.set(Calendar.MINUTE, 0);
  409. cal.set(Calendar.SECOND, 0);
  410. cal.set(Calendar.MILLISECOND, 0);
  411. Date todayStart = cal.getTime();
  412. cal.set(Calendar.HOUR_OF_DAY, 23);
  413. cal.set(Calendar.MINUTE, 59);
  414. cal.set(Calendar.SECOND, 59);
  415. cal.set(Calendar.MILLISECOND, 999);
  416. Date todayEnd = cal.getTime();
  417. List<Task> allTasks = taskMapper.selectList(
  418. new LambdaQueryWrapper<Task>()
  419. .eq(Task::getFamilyMemberId, memberId)
  420. .between(Task::getDeadline, todayStart, todayEnd)
  421. );
  422. long completed = allTasks.stream()
  423. .filter(t -> "completed".equals(t.getStatus()))
  424. .count();
  425. int completionRate = allTasks.isEmpty() ? 0 : (int) (completed * 100 / allTasks.size());
  426. List<Task> pendingReview = taskMapper.selectList(
  427. new LambdaQueryWrapper<Task>()
  428. .eq(Task::getFamilyMemberId, memberId)
  429. .eq(Task::getNeedReview, 1)
  430. .eq(Task::getStatus, "pending")
  431. );
  432. long habitCount = allTasks.stream()
  433. .filter(t -> "completed".equals(t.getStatus()))
  434. .filter(t -> "habit".equals(t.getCategory()))
  435. .count();
  436. stats.put("completionRate", completionRate);
  437. stats.put("pendingReviewCount", pendingReview.size());
  438. stats.put("habitCount", (int) habitCount);
  439. return stats;
  440. }
  441. /**
  442. * TASK-SYS-248: 开始任务(两阶段任务入口)
  443. * 校验前置/会员/频率后:pending→in_progress,创建 task_executions(started)
  444. * 返回 executionId + 任务信息(前端据此跳转联动页面)
  445. */
  446. @Transactional
  447. public Map<String, Object> startTask(Long taskId, Long memberId) {
  448. Task task = taskMapper.selectById(taskId);
  449. if (task == null || !Objects.equals(task.getFamilyMemberId(), memberId)) {
  450. throw new IllegalArgumentException("任务不存在或不属于该成员");
  451. }
  452. if ("completed".equals(task.getStatus())) {
  453. throw new IllegalArgumentException("任务已完成");
  454. }
  455. // 前置任务校验
  456. if (task.getPrerequisiteTaskId() != null) {
  457. Task prereq = taskMapper.selectById(task.getPrerequisiteTaskId());
  458. if (prereq == null || !"completed".equals(prereq.getStatus())) {
  459. throw new IllegalArgumentException("需先完成前置任务:" + (prereq != null ? prereq.getTitle() : "未知任务"));
  460. }
  461. }
  462. Date now = new Date();
  463. // 状态流转:locked→pending 也允许开始(前端已解锁);其它状态置 in_progress
  464. task.setStatus("in_progress");
  465. task.setStartCount((task.getStartCount() == null ? 0 : task.getStartCount()) + 1);
  466. task.setUpdatedAt(now);
  467. taskMapper.updateById(task);
  468. // 创建执行记录
  469. TaskExecution exec = new TaskExecution();
  470. exec.setTaskId(taskId);
  471. exec.setMemberId(memberId);
  472. exec.setStatus("started");
  473. exec.setStartedAt(now);
  474. exec.setCreatedAt(now);
  475. taskExecutionMapper.insert(exec);
  476. // TASK-SYS-248: 购物任务联动——cart/order 动作自动加购/下单
  477. try {
  478. handleShoppingAction(task, memberId, exec, now);
  479. } catch (Exception e) {
  480. log.warn("购物任务联动失败(不影响任务开始): taskId={}, error={}", taskId, e.getMessage());
  481. }
  482. Map<String, Object> result = new HashMap<>();
  483. result.put("executionId", exec.getId());
  484. result.put("taskId", taskId);
  485. result.put("actionType", task.getActionType());
  486. result.put("actionConfig", task.getActionConfig());
  487. return result;
  488. }
  489. @Transactional
  490. public Map<String, Object> completeTask(Long taskId, Long memberId, String photoUrl, String content, String contentType) {
  491. Task task = taskMapper.selectById(taskId);
  492. if (task == null || !Objects.equals(task.getFamilyMemberId(), memberId)) {
  493. return null;
  494. }
  495. if ("completed".equals(task.getStatus()) || "pending_review".equals(task.getStatus())) {
  496. return null; // 任务已完成或已提交待审核,不能重复提交
  497. }
  498. // TASK-SYS-248: 前置任务校验——前置未完成则拒绝
  499. if (task.getPrerequisiteTaskId() != null) {
  500. Task prereq = taskMapper.selectById(task.getPrerequisiteTaskId());
  501. if (prereq == null || !"completed".equals(prereq.getStatus())) {
  502. throw new IllegalArgumentException("需先完成前置任务:" + (prereq != null ? prereq.getTitle() : "未知任务"));
  503. }
  504. }
  505. // TASK-SYS-248: 填写式完成强校验——require_input=1 必须带内容
  506. if (task.getRequireInput() != null && task.getRequireInput() == 1) {
  507. boolean hasContent = (content != null && !content.trim().isEmpty()) || (photoUrl != null && !photoUrl.isEmpty());
  508. if (!hasContent) {
  509. throw new IllegalArgumentException("该任务需填写内容后才能完成");
  510. }
  511. }
  512. // TASK-SYS-248: 两阶段任务校验——start_required=1 必须已开始(存在 in_progress 的 execution)
  513. if (task.getStartRequired() != null && task.getStartRequired() == 1) {
  514. Long executionCount = taskExecutionMapper.selectCount(
  515. new LambdaQueryWrapper<TaskExecution>()
  516. .eq(TaskExecution::getTaskId, taskId)
  517. .eq(TaskExecution::getMemberId, memberId)
  518. .eq(TaskExecution::getStatus, "started"));
  519. if (executionCount == null || executionCount == 0) {
  520. throw new IllegalArgumentException("该任务需先开始才能完成");
  521. }
  522. // 最短持续时长校验
  523. if (task.getMinDurationSeconds() != null && task.getMinDurationSeconds() > 0) {
  524. TaskExecution exec = taskExecutionMapper.selectOne(
  525. new LambdaQueryWrapper<TaskExecution>()
  526. .eq(TaskExecution::getTaskId, taskId)
  527. .eq(TaskExecution::getMemberId, memberId)
  528. .eq(TaskExecution::getStatus, "started")
  529. .last("LIMIT 1"));
  530. if (exec != null && exec.getStartedAt() != null) {
  531. long elapsed = (System.currentTimeMillis() - exec.getStartedAt().getTime()) / 1000;
  532. if (elapsed < task.getMinDurationSeconds()) {
  533. throw new IllegalArgumentException("该任务需持续至少 " + task.getMinDurationSeconds() + " 秒后才能完成");
  534. }
  535. }
  536. }
  537. }
  538. Date now = new Date();
  539. // 检查是否为小游戏任务
  540. if ("小游戏类".equals(task.getCategory()) && task.getMinigameCode() != null) {
  541. return null;
  542. }
  543. // 需审核的任务:提交后进入待审核,审核通过后再发放积分
  544. if (task.getNeedReview() != null && task.getNeedReview() == 1) {
  545. return submitTaskForReview(task, memberId, now, content, contentType);
  546. }
  547. recordTaskExecution(task, memberId, now, content, contentType);
  548. int pointsEarned = awardTaskCompletionRewards(task, memberId, now);
  549. FamilyMember child = familyMemberMapper.selectById(memberId);
  550. Map<String, Object> result = new HashMap<>();
  551. result.put("pointsEarned", pointsEarned);
  552. result.put("newBalance", child != null ? child.getTotalPoints() : 0);
  553. result.put("needReview", false);
  554. try {
  555. com.etotem.cfc.entity.TaskReminderConfig cfg = taskReminderConfigService.loadAllConfigs().get(task.getExecutorId());
  556. if (cfg != null && Integer.valueOf(1).equals(cfg.getCompletionPraiseEnabled())) {
  557. java.util.HashMap<String, Object> vars = new java.util.HashMap<String, Object>();
  558. vars.put("title", task.getTitle());
  559. String chans = cfg.getCompletionPraiseChannels();
  560. if (chans == null || chans.isEmpty()) chans = "wechat,in_app";
  561. notificationDispatchService.sendByTemplate("task_completion_praise", task.getExecutorId(), vars, "task", task.getId(),
  562. "任务完成,太棒了!",
  563. "太棒了!任务「" + task.getTitle() + "」已完成,继续保持,加油~");
  564. }
  565. } catch (Exception e) {}
  566. return result;
  567. }
  568. @Transactional
  569. public boolean reviewTask(Long taskId, TaskReviewDTO dto) {
  570. Task task = taskMapper.selectById(taskId);
  571. if (task == null) {
  572. return false;
  573. }
  574. if (!"pending_review".equals(task.getStatus())) {
  575. return "completed".equals(task.getStatus());
  576. }
  577. boolean isApproved;
  578. if (dto.getApproved() != null) {
  579. isApproved = dto.getApproved();
  580. } else if ("pass".equals(dto.getResult())) {
  581. isApproved = true;
  582. } else {
  583. isApproved = false;
  584. }
  585. if (isApproved) {
  586. Date completionTime = task.getCompletedAt() != null ? task.getCompletedAt() : new Date();
  587. awardTaskCompletionRewards(task, task.getFamilyMemberId(), completionTime);
  588. } else {
  589. task.setStatus("pending");
  590. task.setCompletedAt(null);
  591. task.setUpdatedAt(new Date());
  592. taskMapper.updateById(task);
  593. }
  594. return true;
  595. }
  596. public Page<Task> getTaskHistory(Long memberId, Integer page, Integer size) {
  597. Page<Task> pageParam = new Page<>(page, size);
  598. return taskMapper.selectPage(pageParam, new LambdaQueryWrapper<Task>()
  599. .eq(Task::getFamilyMemberId, memberId)
  600. .orderByDesc(Task::getCreatedAt));
  601. }
  602. public Page<Task> getTaskHistoryByCategory(Long memberId, Integer page, Integer size, String category) {
  603. Page<Task> pageParam = new Page<>(page, size);
  604. return taskMapper.selectPage(pageParam, new LambdaQueryWrapper<Task>()
  605. .eq(Task::getFamilyMemberId, memberId)
  606. .eq(Task::getCategory, category)
  607. .orderByDesc(Task::getCreatedAt));
  608. }
  609. public boolean deleteTask(Long taskId, Long userId) {
  610. Task task = taskMapper.selectById(taskId);
  611. if (task == null || !task.getCreatorId().equals(userId)) {
  612. return false;
  613. }
  614. task.setStatus("cancelled");
  615. task.setUpdatedAt(new Date());
  616. taskMapper.updateById(task);
  617. return true;
  618. }
  619. /**
  620. * 获取待审核任务列表
  621. * 家长查看需要审核的任务
  622. */
  623. public List<Task> getPendingReviewTasks(Long userId) {
  624. // 获取用户信息
  625. User user = userService.getUserInfo(userId);
  626. if (user == null || !"parent".equals(user.getRole())) {
  627. return new ArrayList<>();
  628. }
  629. // 查询该家庭下所有需要审核且已完成的任务
  630. LambdaQueryWrapper<Task> wrapper = new LambdaQueryWrapper<>();
  631. wrapper.eq(Task::getFamilyId, user.getFamilyId())
  632. .eq(Task::getNeedReview, 1)
  633. .eq(Task::getStatus, "pending_review")
  634. .orderByDesc(Task::getCompletedAt);
  635. SortUtil.applySort(wrapper);
  636. return taskMapper.selectList(wrapper);
  637. }
  638. /**
  639. * 获取家长今日任务 (家长作为执行者的任务)
  640. */
  641. public List<Task> getTodayParentTasks(Long userId) {
  642. Calendar calendar = Calendar.getInstance();
  643. calendar.set(Calendar.HOUR_OF_DAY, 0);
  644. calendar.set(Calendar.MINUTE, 0);
  645. calendar.set(Calendar.SECOND, 0);
  646. calendar.set(Calendar.MILLISECOND, 0);
  647. Date startOfDay = calendar.getTime();
  648. calendar.add(Calendar.DAY_OF_MONTH, 1);
  649. Date endOfDay = calendar.getTime();
  650. User user = userService.getUserInfo(userId);
  651. if (user == null) {
  652. return new ArrayList<>();
  653. }
  654. return taskMapper.selectList(new LambdaQueryWrapper<Task>()
  655. .eq(Task::getFamilyId, user.getFamilyId())
  656. .eq(Task::getExecutorType, "parent")
  657. .eq(Task::getExecutorId, userId)
  658. .ge(Task::getDeadline, startOfDay)
  659. .lt(Task::getDeadline, endOfDay)
  660. .in(Task::getStatus, Arrays.asList("pending", "completed"))
  661. .orderByAsc(Task::getDeadline));
  662. }
  663. /**
  664. * 家长完成任务 (家长作为执行者完成任务)
  665. */
  666. @Transactional
  667. public Map<String, Object> completeParentTask(Long taskId, Long userId) {
  668. Task task = taskMapper.selectById(taskId);
  669. if (task == null || !("parent".equals(task.getExecutorType()) && task.getExecutorId().equals(userId))) {
  670. return null;
  671. }
  672. if ("completed".equals(task.getStatus())) {
  673. return null;
  674. }
  675. User user = userMapper.selectById(userId);
  676. if (user == null) {
  677. return null;
  678. }
  679. Date now = new Date();
  680. int pointsEarned = task.getPoints();
  681. boolean isEarly = now.before(task.getDeadline());
  682. // 提前完成获得奖励分
  683. if (isEarly) {
  684. pointsEarned += EARLY_BONUS;
  685. }
  686. // 更新任务状态
  687. task.setStatus("completed");
  688. task.setCompletedAt(now);
  689. task.setUpdatedAt(new Date());
  690. taskMapper.updateById(task);
  691. // 仅编排节点任务生效:通知编排引擎流程评估(非编排节点任务零开销)
  692. try {
  693. orchestrationEngine.onTaskCompleted(taskId);
  694. } catch (Exception e) {
  695. log.warn("orchestration onTaskCompleted failed: {}", e.getMessage());
  696. }
  697. // 更新家长积分
  698. int newPoints = (user.getTotalPoints() != null ? user.getTotalPoints() : 0) + pointsEarned;
  699. user.setTotalPoints(newPoints);
  700. user.setUpdatedAt(new Date());
  701. userMapper.updateById(user);
  702. // 记录积分流水
  703. PointsLog log = new PointsLog();
  704. log.setChildId(task.getFamilyMemberId());
  705. log.setAmount(pointsEarned);
  706. log.setType(isEarly ? "earn" : "earn");
  707. log.setDescription("完成任务: " + task.getTitle());
  708. log.setCreatedAt(new Date());
  709. pointsLogMapper.insert(log);
  710. Map<String, Object> result = new HashMap<>();
  711. result.put("pointsEarned", pointsEarned);
  712. try { FamilyMember child = familyMemberMapper.selectById(task.getFamilyMemberId()); if (child != null) { this.completeOnboardingTask(child.getUserId(), "FIRST_TASK"); } } catch (Exception e) { /* onboarding trigger failure should not block task completion */ }
  713. result.put("newBalance", user.getTotalPoints());
  714. result.put("needReview", task.getNeedReview() == 1);
  715. try {
  716. Long receiver = userId;
  717. com.etotem.cfc.entity.TaskReminderConfig cfg = taskReminderConfigService.loadAllConfigs().get(receiver);
  718. if (cfg != null && Integer.valueOf(1).equals(cfg.getCompletionPraiseEnabled())) {
  719. java.util.HashMap<String, Object> vars = new java.util.HashMap<String, Object>();
  720. vars.put("title", task.getTitle());
  721. notificationDispatchService.sendByTemplate("task_completion_praise", receiver, vars, "task", task.getId(),
  722. "任务完成,太棒了!",
  723. "太棒了!任务「" + task.getTitle() + "」已完成,继续保持,加油~");
  724. }
  725. } catch (Exception e) {}
  726. return result;
  727. }
  728. public void batchComplete(List<Long> taskIds, Long userId) {
  729. taskMapper.update(null, new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<Task>()
  730. .in(Task::getId, taskIds)
  731. .and(w -> w.eq(Task::getCreatorId, userId).or().eq(Task::getExecutorId, userId))
  732. .set(Task::getStatus, "completed")
  733. .set(Task::getCompletedAt, new Date()));
  734. }
  735. public void batchDelete(List<Long> taskIds, Long userId) {
  736. taskMapper.update(null, new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<Task>()
  737. .in(Task::getId, taskIds)
  738. .eq(Task::getCreatorId, userId)
  739. .set(Task::getStatus, "cancelled")
  740. .set(Task::getUpdatedAt, new Date()));
  741. }
  742. private Map<String, Object> submitTaskForReview(Task task, Long memberId, Date now, String content, String contentType) {
  743. task.setStatus("pending_review");
  744. task.setCompletedAt(now);
  745. task.setUpdatedAt(now);
  746. taskMapper.updateById(task);
  747. recordTaskExecution(task, memberId, now, content, contentType);
  748. FamilyMember child = familyMemberMapper.selectById(memberId);
  749. Map<String, Object> result = new HashMap<>();
  750. result.put("pointsEarned", 0);
  751. result.put("newBalance", child != null ? child.getTotalPoints() : 0);
  752. result.put("needReview", true);
  753. result.put("message", "已提交,待审核");
  754. return result;
  755. }
  756. private int awardTaskCompletionRewards(Task task, Long memberId, Date completionTime) {
  757. Long taskId = task.getId();
  758. int pointsEarned = task.getPoints() != null ? task.getPoints() : 0;
  759. boolean isEarly = task.getDeadline() != null && completionTime.before(task.getDeadline());
  760. boolean isLateGrace = false;
  761. if (!isEarly && task.getDeadline() != null) {
  762. long diffMinutes = (completionTime.getTime() - task.getDeadline().getTime()) / (1000 * 60);
  763. isLateGrace = diffMinutes <= LATE_GRACE_MINUTES;
  764. }
  765. FamilyMember child = familyMemberMapper.selectById(memberId);
  766. if (child == null) {
  767. return 0;
  768. }
  769. String bonusRuleJson = task.getBonusRule();
  770. if (bonusRuleJson != null && !bonusRuleJson.isEmpty()) {
  771. pointsEarned = calculatePointsWithBonusRule(task, completionTime, isEarly, isLateGrace, child);
  772. } else if (isEarly || isLateGrace) {
  773. pointsEarned += EARLY_BONUS;
  774. } else {
  775. pointsEarned = calculateLatePenalty(task, child, memberId);
  776. }
  777. task.setStatus("completed");
  778. task.setCompletedAt(completionTime);
  779. task.setUpdatedAt(new Date());
  780. taskMapper.updateById(task);
  781. child.setTotalPoints(child.getTotalPoints() + pointsEarned);
  782. Date lastTaskDate = child.getLastTaskDate();
  783. Date today = new Date();
  784. Calendar cal = Calendar.getInstance();
  785. cal.add(Calendar.DAY_OF_MONTH, -1);
  786. Date yesterday = cal.getTime();
  787. if (lastTaskDate == null || lastTaskDate.before(yesterday)) {
  788. child.setStreakDays(1);
  789. } else if (!isSameDay(lastTaskDate, today)) {
  790. child.setStreakDays(child.getStreakDays() + 1);
  791. }
  792. child.setLastTaskDate(today);
  793. child.setUpdatedAt(new Date());
  794. familyMemberMapper.updateById(child);
  795. int milestoneReward = streakService.checkAndAwardMilestone(memberId, child.getStreakDays());
  796. if (milestoneReward > 0) {
  797. pointsEarned += milestoneReward;
  798. log.info("孩子{}获得打卡里程碑奖励{}积分", memberId, milestoneReward);
  799. }
  800. PointsLog pointsLog = new PointsLog();
  801. pointsLog.setChildId(memberId);
  802. pointsLog.setTaskId(taskId);
  803. pointsLog.setAmount(pointsEarned);
  804. if (pointsEarned > 0) {
  805. pointsLog.setType(isEarly ? "bonus" : "earn");
  806. } else if (pointsEarned < 0) {
  807. pointsLog.setType("penalty");
  808. } else {
  809. pointsLog.setType("earn");
  810. }
  811. pointsLog.setDescription("完成任务: " + task.getTitle());
  812. pointsLog.setCreatedAt(new Date());
  813. if (pointsEarned > 0 && task.getPointsExpireDays() != null && task.getPointsExpireDays() > 0) {
  814. Calendar expireCal = Calendar.getInstance();
  815. expireCal.add(Calendar.DAY_OF_MONTH, task.getPointsExpireDays());
  816. pointsLog.setExpireAt(expireCal.getTime());
  817. }
  818. pointsLogMapper.insert(pointsLog);
  819. try {
  820. int energyAmount = Math.abs(pointsEarned);
  821. if (energyAmount > 0) {
  822. energyService.awardEnergy(memberId, "task", taskId, energyAmount,
  823. "完成任务: " + task.getTitle(), null);
  824. }
  825. } catch (Exception e) {
  826. log.warn("能量发放失败(不影响积分): memberId={}, taskId={}, error={}",
  827. memberId, taskId, e.getMessage());
  828. }
  829. try {
  830. if (child.getUserId() != null) {
  831. this.updateProgress(child.getUserId(), child.getId(), "DAILY_TASK", 1);
  832. }
  833. } catch (Exception e) {
  834. log.warn("成长任务进度更新失败: memberId={}, error={}", memberId, e.getMessage());
  835. }
  836. // D5: 打卡完成发放 CF 值(用户级 + 家庭级)。
  837. // earn() 按 (id, ref_type, ref_id) 幂等去重,审核重复触发不会双记。
  838. // CF 额度取任务基础积分,不含连胜里程碑加成与迟到惩罚,避免奖励被放大/倒挂。
  839. try {
  840. int cfAmount = task.getPoints() != null && task.getPoints() > 0 ? task.getPoints() : 0;
  841. // 注册/绑定推荐人为引流类任务,不发可提现的CF值(积分照发,防止刷CF)
  842. if (cfAmount > 0 && ("REGISTER".equals(task.getTitle()) || "BIND_REFERRAL".equals(task.getTitle()))) {
  843. cfAmount = 0;
  844. }
  845. if (cfAmount > 0) {
  846. String remark = "完成任务: " + task.getTitle();
  847. if (child != null && child.getUserId() != null) {
  848. platformPointsService.earn(child.getUserId(), cfAmount, "task_complete", taskId, remark);
  849. }
  850. if (task.getFamilyId() != null) {
  851. familyPlatformPointsService.earn(task.getFamilyId(), cfAmount, "task_complete", taskId, remark);
  852. }
  853. }
  854. } catch (Exception e) {
  855. log.warn("CF值发放失败(不影响积分): taskId={}, error={}", taskId, e.getMessage());
  856. }
  857. unlockPrerequisiteTasks(taskId);
  858. return pointsEarned;
  859. }
  860. private void recordTaskExecution(Task task, Long memberId, Date now, String content, String contentType) {
  861. Long taskId = task.getId();
  862. try {
  863. TaskExecution activeExec = null;
  864. if (task.getStartRequired() != null && task.getStartRequired() == 1) {
  865. activeExec = taskExecutionMapper.selectOne(
  866. new LambdaQueryWrapper<TaskExecution>()
  867. .eq(TaskExecution::getTaskId, taskId)
  868. .eq(TaskExecution::getMemberId, memberId)
  869. .eq(TaskExecution::getStatus, "started")
  870. .last("LIMIT 1"));
  871. }
  872. if (activeExec != null) {
  873. activeExec.setStatus("finished");
  874. activeExec.setFinishedAt(now);
  875. activeExec.setContent(content);
  876. activeExec.setContentType(contentType);
  877. taskExecutionMapper.updateById(activeExec);
  878. } else if (content != null && !content.trim().isEmpty()) {
  879. TaskExecution exec = new TaskExecution();
  880. exec.setTaskId(taskId);
  881. exec.setMemberId(memberId);
  882. exec.setStatus("finished");
  883. exec.setStartedAt(now);
  884. exec.setFinishedAt(now);
  885. exec.setContent(content);
  886. exec.setContentType(contentType);
  887. exec.setCreatedAt(now);
  888. taskExecutionMapper.insert(exec);
  889. }
  890. } catch (Exception e) {
  891. log.warn("任务执行进度记录失败(不影响完成): taskId={}, error={}", taskId, e.getMessage());
  892. }
  893. }
  894. private void unlockPrerequisiteTasks(Long taskId) {
  895. try {
  896. List<Task> nextTasks = taskMapper.selectList(
  897. new LambdaQueryWrapper<Task>()
  898. .eq(Task::getPrerequisiteTaskId, taskId));
  899. for (Task next : nextTasks) {
  900. if ("locked".equals(next.getStatus())) {
  901. next.setStatus("pending");
  902. next.setUpdatedAt(new Date());
  903. taskMapper.updateById(next);
  904. log.info("前置任务{}完成,解锁后置任务{}", taskId, next.getId());
  905. }
  906. }
  907. } catch (Exception e) {
  908. log.warn("后置任务解锁失败: taskId={}, error={}", taskId, e.getMessage());
  909. }
  910. }
  911. /**
  912. * 管理员/平台查询待审核任务
  913. */
  914. public List<Task> getAllPendingReviewTasks(Long familyId) {
  915. LambdaQueryWrapper<Task> wrapper = new LambdaQueryWrapper<Task>()
  916. .eq(Task::getNeedReview, 1)
  917. .eq(Task::getStatus, "pending_review")
  918. .orderByDesc(Task::getCompletedAt);
  919. if (familyId != null) {
  920. wrapper.eq(Task::getFamilyId, familyId);
  921. }
  922. SortUtil.applySort(wrapper);
  923. return taskMapper.selectList(wrapper);
  924. }
  925. private boolean isSameDay(Date date1, Date date2) {
  926. Calendar cal1 = Calendar.getInstance();
  927. cal1.setTime(date1);
  928. Calendar cal2 = Calendar.getInstance();
  929. cal2.setTime(date2);
  930. return cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR)
  931. && cal1.get(Calendar.DAY_OF_YEAR) == cal2.get(Calendar.DAY_OF_YEAR);
  932. }
  933. /**
  934. * 获取孩子的待审核任务
  935. */
  936. public List<Task> getPendingReviewTasksForChild(Long memberId) {
  937. return taskMapper.selectList(new LambdaQueryWrapper<Task>()
  938. .eq(Task::getFamilyMemberId, memberId)
  939. .eq(Task::getNeedReview, 1)
  940. .eq(Task::getStatus, "pending_review")
  941. .orderByDesc(Task::getCompletedAt));
  942. }
  943. /**
  944. * 获取孩子的所有任务
  945. */
  946. public List<Task> getAllTasksByChild(Long memberId) {
  947. return taskMapper.selectList(new LambdaQueryWrapper<Task>()
  948. .eq(Task::getFamilyMemberId, memberId)
  949. .orderByDesc(Task::getCreatedAt));
  950. }
  951. /**
  952. * 获取孩子的任务历史(分页)
  953. */
  954. public Page<Task> getTaskHistoryByChild(Long memberId, Integer page, Integer size, String status) {
  955. Page<Task> pageParam = new Page<>(page, size);
  956. LambdaQueryWrapper<Task> wrapper = new LambdaQueryWrapper<Task>()
  957. .eq(Task::getFamilyMemberId, memberId)
  958. .orderByDesc(Task::getCreatedAt);
  959. if (status != null && !status.isEmpty()) {
  960. wrapper.eq(Task::getStatus, status);
  961. }
  962. SortUtil.applySort(wrapper);
  963. return taskMapper.selectPage(pageParam, wrapper);
  964. }
  965. /**
  966. * POINT-002: 根据自定义规则计算超额积分
  967. * bonusRule JSON格式: {
  968. * "earlyBonus": 1, // 提前完成额外积分
  969. * "earlyMinutes": 30, // 提前多少分钟才算提前(可选)
  970. * "qualityBonus": 2, // 超质完成额外积分(家长评分)
  971. * "qualityThreshold": 90 // 质量评分阈值(可选)
  972. * }
  973. */
  974. private int calculatePointsWithBonusRule(Task task, Date completedAt, boolean isEarly, boolean isLateGrace, FamilyMember child) {
  975. int points = task.getPoints();
  976. try {
  977. // 解析JSON规则
  978. String json = task.getBonusRule();
  979. if (json == null || json.isEmpty()) {
  980. return isEarly || isLateGrace ? points + EARLY_BONUS : calculateLatePenalty(task, child, child.getId());
  981. }
  982. // 简单JSON解析(避免引入额外依赖)
  983. Integer earlyBonus = extractJsonValue(json, "earlyBonus");
  984. Integer earlyMinutes = extractJsonValue(json, "earlyMinutes");
  985. Integer qualityBonus = extractJsonValue(json, "qualityBonus");
  986. // 提前完成奖励
  987. if (isEarly || isLateGrace) {
  988. if (earlyBonus != null) {
  989. points += earlyBonus;
  990. } else {
  991. points += EARLY_BONUS; // 默认+1
  992. }
  993. // 如果设置了提前分钟数,检查是否满足条件
  994. if (earlyMinutes != null && task.getDeadline() != null) {
  995. long diffMinutes = (task.getDeadline().getTime() - completedAt.getTime()) / (1000 * 60);
  996. if (diffMinutes < earlyMinutes) {
  997. // 不满足提前分钟数,只给基础分
  998. points = task.getPoints();
  999. }
  1000. }
  1001. } else {
  1002. // 超时,计算扣分
  1003. points = calculateLatePenalty(task, child, child.getId());
  1004. }
  1005. // 超质完成奖励(需要家长后续评分,这里先不处理)
  1006. // qualityBonus 会在家长审核时根据评分添加
  1007. } catch (Exception e) {
  1008. log.warn("解析超额积分规则失败: {}", e.getMessage());
  1009. // 解析失败,使用默认规则
  1010. if (isEarly || isLateGrace) {
  1011. points += EARLY_BONUS;
  1012. } else {
  1013. points = calculateLatePenalty(task, child, child.getId());
  1014. }
  1015. }
  1016. return points;
  1017. }
  1018. /**
  1019. * 从简单JSON字符串中提取整数值
  1020. */
  1021. private Integer extractJsonValue(String json, String key) {
  1022. try {
  1023. String pattern = "\"" + key + "\"\\s*:\\s*(\\d+)";
  1024. java.util.regex.Pattern p = java.util.regex.Pattern.compile(pattern);
  1025. java.util.regex.Matcher m = p.matcher(json);
  1026. if (m.find()) {
  1027. return Integer.parseInt(m.group(1));
  1028. }
  1029. } catch (Exception e) {
  1030. log.warn("提取JSON值失败: key={}", key);
  1031. }
  1032. return null;
  1033. }
  1034. /**
  1035. * 计算超时扣分
  1036. */
  1037. private int calculateLatePenalty(Task task, FamilyMember child, Long memberId) {
  1038. if (child == null) {
  1039. return 0;
  1040. }
  1041. int points = task.getPoints() != null ? task.getPoints() : 0;
  1042. // 无 deadline 的每日进度型任务(饮食/睡眠/运动打卡等)不存在"迟到"概念,发放基础分
  1043. if (task.getDeadline() == null) {
  1044. return points;
  1045. }
  1046. if (child.getPenaltyEnabled() != null && child.getPenaltyEnabled() == 1) {
  1047. // 检查今日已扣分数
  1048. Calendar cal = Calendar.getInstance();
  1049. cal.set(Calendar.HOUR_OF_DAY, 0);
  1050. cal.set(Calendar.MINUTE, 0);
  1051. cal.set(Calendar.SECOND, 0);
  1052. Date startOfDay = cal.getTime();
  1053. Integer todayPenalty = pointsLogMapper.selectSum(
  1054. new LambdaQueryWrapper<PointsLog>()
  1055. .eq(PointsLog::getChildId, memberId)
  1056. .eq(PointsLog::getType, "penalty")
  1057. .ge(PointsLog::getCreatedAt, startOfDay)
  1058. );
  1059. int todayPenaltyTotal = todayPenalty != null ? Math.abs(todayPenalty) : 0;
  1060. if (todayPenaltyTotal < PENALTY_MAX_DAILY) {
  1061. int penalty = Math.min(task.getPoints(), PENALTY_MAX_DAILY - todayPenaltyTotal);
  1062. return -penalty;
  1063. } else {
  1064. return points; // 达到每日扣分上限:不扣分,任务完成仍发放基础分
  1065. }
  1066. } else {
  1067. return points; // 扣分未开启:任务完成发放基础分
  1068. }
  1069. }
  1070. /**
  1071. * 完成小游戏任务
  1072. * @param taskId 任务ID
  1073. * @param memberId 孩子ID
  1074. * @param completionTime 完成用时(秒)
  1075. * @param score 得分(可选)
  1076. * @return 积分信息
  1077. */
  1078. @Transactional
  1079. public Map<String, Object> completeMinigameTask(Long taskId, Long memberId, Integer completionTime, Integer score) {
  1080. Task task = taskMapper.selectById(taskId);
  1081. if (task == null || !Objects.equals(task.getFamilyMemberId(), memberId)) {
  1082. throw new RuntimeException("任务不存在或不属于该孩子");
  1083. }
  1084. if ("completed".equals(task.getStatus())) {
  1085. throw new RuntimeException("任务已完成");
  1086. }
  1087. if (!"小游戏类".equals(task.getCategory()) || task.getMinigameCode() == null) {
  1088. throw new RuntimeException("该任务不是小游戏任务");
  1089. }
  1090. // 调用MiniGameService计算积分
  1091. Map<String, Object> gameResult = miniGameService.completeGame(memberId, task.getMinigameCode(), completionTime, score);
  1092. int pointsEarned = (Integer) gameResult.get("pointsEarned");
  1093. // 更新任务状态
  1094. task.setStatus("completed");
  1095. task.setCompletedAt(new Date());
  1096. task.setUpdatedAt(new Date());
  1097. taskMapper.updateById(task);
  1098. // 更新连续打卡
  1099. FamilyMember child = familyMemberMapper.selectById(memberId);
  1100. Date lastTaskDate = child.getLastTaskDate();
  1101. Date today = new Date();
  1102. Calendar cal = Calendar.getInstance();
  1103. cal.add(Calendar.DAY_OF_MONTH, -1);
  1104. Date yesterday = cal.getTime();
  1105. if (lastTaskDate == null || lastTaskDate.before(yesterday)) {
  1106. child.setStreakDays(1); // 重新开始
  1107. } else if (lastTaskDate != null && isSameDay(lastTaskDate, today)) {
  1108. // 今天已经完成过任务,不增加天数
  1109. } else {
  1110. child.setStreakDays(child.getStreakDays() + 1);
  1111. }
  1112. child.setLastTaskDate(today);
  1113. child.setUpdatedAt(new Date());
  1114. familyMemberMapper.updateById(child);
  1115. // 检查并发放打卡里程碑奖励
  1116. int milestoneReward = streakService.checkAndAwardMilestone(memberId, child.getStreakDays());
  1117. if (milestoneReward > 0) {
  1118. pointsEarned += milestoneReward;
  1119. log.info("孩子{}获得打卡里程碑奖励{}积分", memberId, milestoneReward);
  1120. }
  1121. Map<String, Object> result = new HashMap<>();
  1122. result.put("pointsEarned", pointsEarned);
  1123. result.put("newBalance", child.getTotalPoints());
  1124. result.put("gameName", gameResult.get("gameName"));
  1125. result.put("needReview", task.getNeedReview() == 1);
  1126. return result;
  1127. }
  1128. /**
  1129. * 获取家庭待办任务
  1130. */
  1131. public List<Task> getPendingTasksByFamily(Long familyId) {
  1132. return taskMapper.selectList(new LambdaQueryWrapper<Task>()
  1133. .eq(Task::getFamilyId, familyId)
  1134. .in(Task::getStatus, Arrays.asList("pending", "in_progress"))
  1135. .orderByAsc(Task::getDeadline));
  1136. }
  1137. // ==================== Family Member Task queries ====================
  1138. /**
  1139. * 获取指定家庭成员待审核的任务列表
  1140. */
  1141. public List<Task> getPendingReviewTasksForFamilyMember(Long familyMemberId) {
  1142. return taskMapper.selectList(new LambdaQueryWrapper<Task>()
  1143. .eq(Task::getFamilyMemberId, familyMemberId)
  1144. .eq(Task::getNeedReview, 1)
  1145. .eq(Task::getStatus, "pending_review")
  1146. .orderByDesc(Task::getCompletedAt));
  1147. }
  1148. /**
  1149. * 获取指定家庭成员的全部任务
  1150. */
  1151. public List<Task> getAllTasksByFamilyMember(Long familyMemberId) {
  1152. return taskMapper.selectList(new LambdaQueryWrapper<Task>()
  1153. .eq(Task::getFamilyMemberId, familyMemberId)
  1154. .orderByDesc(Task::getCreatedAt));
  1155. }
  1156. /**
  1157. * 分页获取指定家庭成员的任务历史
  1158. */
  1159. public Page<Task> getTaskHistoryByFamilyMember(Long familyMemberId, Integer page, Integer size, String status) {
  1160. LambdaQueryWrapper<Task> wrapper = new LambdaQueryWrapper<Task>()
  1161. .eq(Task::getFamilyMemberId, familyMemberId);
  1162. if (status != null && !status.isEmpty()) {
  1163. wrapper.eq(Task::getStatus, status);
  1164. }
  1165. wrapper.orderByDesc(Task::getCreatedAt);
  1166. SortUtil.applySort(wrapper);
  1167. return taskMapper.selectPage(new Page<>(page, size), wrapper);
  1168. }
  1169. /**
  1170. * 获取当前用户收到的待接受任务列表(executor_type='member' AND status='assigned')
  1171. */
  1172. public List<Task> getMyPendingTasks(Long memberId) {
  1173. Calendar calendar = Calendar.getInstance();
  1174. calendar.set(Calendar.HOUR_OF_DAY, 0);
  1175. calendar.set(Calendar.MINUTE, 0);
  1176. calendar.set(Calendar.SECOND, 0);
  1177. calendar.set(Calendar.MILLISECOND, 0);
  1178. Date startOfDay = calendar.getTime();
  1179. calendar.add(Calendar.DAY_OF_MONTH, 1);
  1180. Date endOfDay = calendar.getTime();
  1181. return taskMapper.selectList(new LambdaQueryWrapper<Task>()
  1182. .eq(Task::getExecutorType, "member")
  1183. .eq(Task::getExecutorId, memberId)
  1184. .eq(Task::getStatus, "assigned")
  1185. .ge(Task::getDeadline, startOfDay)
  1186. .lt(Task::getDeadline, endOfDay)
  1187. .or()
  1188. .and(w -> w.eq(Task::getExecutorType, "member")
  1189. .eq(Task::getExecutorId, memberId)
  1190. .eq(Task::getStatus, "assigned")
  1191. .isNull(Task::getDeadline))
  1192. .orderByAsc(Task::getDeadline));
  1193. }
  1194. /**
  1195. * 接受任务:status 从 assigned 改为 in_progress
  1196. */
  1197. @Transactional
  1198. public boolean acceptTask(Long taskId, Long memberId) {
  1199. Task task = taskMapper.selectById(taskId);
  1200. if (task == null || !task.getExecutorId().equals(memberId) || !"assigned".equals(task.getStatus())) {
  1201. return false;
  1202. }
  1203. task.setStatus("in_progress");
  1204. task.setAcceptDeadline(null);
  1205. task.setUpdatedAt(new Date());
  1206. taskMapper.updateById(task);
  1207. return true;
  1208. }
  1209. /**
  1210. * 拒绝任务:status 改为 rejected,调整 creator→assignee 和 assignee→creator 的关系分数
  1211. */
  1212. @Transactional
  1213. public boolean rejectTask(Long taskId, Long memberId) {
  1214. Task task = taskMapper.selectById(taskId);
  1215. if (task == null || !task.getExecutorId().equals(memberId) || !"assigned".equals(task.getStatus())) {
  1216. return false;
  1217. }
  1218. task.setStatus("rejected");
  1219. task.setUpdatedAt(new Date());
  1220. taskMapper.updateById(task);
  1221. Long creatorId = task.getCreatorId();
  1222. Long assigneeId = memberId;
  1223. Long familyId = task.getFamilyId();
  1224. if (creatorId == null || assigneeId == null || familyId == null) return true;
  1225. int trustDelta = -1;
  1226. int intimacyDelta = -1;
  1227. int commDelta = -1;
  1228. adjustRelationshipScore(familyId, creatorId, assigneeId, trustDelta, intimacyDelta, commDelta);
  1229. adjustRelationshipScore(familyId, assigneeId, creatorId, 0, 0, 1);
  1230. return true;
  1231. }
  1232. private void adjustRelationshipScore(Long familyId, Long fromId, Long toId, int trustDelta, int intimacyDelta, int commDelta) {
  1233. FamilyRelationshipScore score = familyRelationshipScoreMapper.selectOne(
  1234. new LambdaQueryWrapper<FamilyRelationshipScore>()
  1235. .eq(FamilyRelationshipScore::getFamilyId, familyId)
  1236. .eq(FamilyRelationshipScore::getFromMemberId, fromId)
  1237. .eq(FamilyRelationshipScore::getToMemberId, toId));
  1238. if (score == null) {
  1239. FamilyMember from = familyMemberMapper.selectById(fromId);
  1240. score = new FamilyRelationshipScore();
  1241. score.setFamilyId(familyId);
  1242. score.setFromMemberId(fromId);
  1243. score.setToMemberId(toId);
  1244. score.setTrustScore(from != null && from.getTrustScore() != null ? from.getTrustScore() : new java.math.BigDecimal("60.00"));
  1245. score.setIntimacyScore(from != null && from.getIntimacyScore() != null ? from.getIntimacyScore() : new java.math.BigDecimal("60.00"));
  1246. score.setCommunicationScore(from != null && from.getCommunicationScore() != null ? from.getCommunicationScore() : new java.math.BigDecimal("60.00"));
  1247. try {
  1248. familyRelationshipScoreMapper.insert(score);
  1249. } catch (Exception e) {
  1250. score = familyRelationshipScoreMapper.selectOne(
  1251. new LambdaQueryWrapper<FamilyRelationshipScore>()
  1252. .eq(FamilyRelationshipScore::getFamilyId, familyId)
  1253. .eq(FamilyRelationshipScore::getFromMemberId, fromId)
  1254. .eq(FamilyRelationshipScore::getToMemberId, toId));
  1255. }
  1256. }
  1257. if (score == null) return;
  1258. if (trustDelta != 0) score.setTrustScore(score.getTrustScore().add(java.math.BigDecimal.valueOf(trustDelta)));
  1259. if (intimacyDelta != 0) score.setIntimacyScore(score.getIntimacyScore().add(java.math.BigDecimal.valueOf(intimacyDelta)));
  1260. if (commDelta != 0) score.setCommunicationScore(score.getCommunicationScore().add(java.math.BigDecimal.valueOf(commDelta)));
  1261. familyRelationshipScoreMapper.updateById(score);
  1262. }
  1263. /**
  1264. * TASK-SYS-248: 购物任务联动(cart/order 动作的自动加购/下单)
  1265. * 在任务开始(startTask)时被调用,将 actionConfig 解析后驱动 CartService / ProductOrderService
  1266. * actionConfig JSON 格式示例:
  1267. * cart: {"productId": 123, "quantity": 1, "skuId": 456}
  1268. * order: {"productId": 123, "quantity": 1, "skuId": 456, "consigneeId": 789, "deliveryMethod": 1}
  1269. */
  1270. private void handleShoppingAction(Task task, Long memberId, TaskExecution exec, Date now) {
  1271. String actionType = task.getActionType();
  1272. if (actionType == null || actionType.isEmpty()) {
  1273. return;
  1274. }
  1275. if (!"cart".equals(actionType) && !"order".equals(actionType)) {
  1276. return;
  1277. }
  1278. JSONObject config = parseActionConfig(task.getActionConfig());
  1279. Long productId = config != null ? config.getLong("productId") : null;
  1280. Integer quantity = config != null && config.getInteger("quantity") != null ? config.getInteger("quantity") : 1;
  1281. Long skuId = config != null ? config.getLong("skuId") : null;
  1282. if (productId == null) {
  1283. log.warn("购物任务缺少 productId: taskId={}", task.getId());
  1284. return;
  1285. }
  1286. // 从 member 获取 userId
  1287. FamilyMember member = familyMemberMapper.selectById(memberId);
  1288. Long userId = member != null ? member.getUserId() : null;
  1289. if (userId == null) {
  1290. log.warn("购物任务无法找到 userId: memberId={}", memberId);
  1291. return;
  1292. }
  1293. JSONObject extra = new JSONObject();
  1294. extra.put("actionType", actionType);
  1295. extra.put("productId", productId);
  1296. extra.put("quantity", quantity);
  1297. if (skuId != null) {
  1298. extra.put("skuId", skuId);
  1299. }
  1300. try {
  1301. // 加购(cart 和 order 都需要)
  1302. cartService.addToCart(userId, productId, quantity, skuId);
  1303. extra.put("cartAdded", true);
  1304. if ("order".equals(actionType)) {
  1305. // 创建 pending 订单
  1306. Long consigneeId = config != null ? config.getLong("consigneeId") : null;
  1307. Integer deliveryMethod = config != null ? config.getInteger("deliveryMethod") : null;
  1308. // 未指定收货地址时取用户默认地址
  1309. if (consigneeId == null) {
  1310. try {
  1311. Consignee defaultConsignee = consigneeMapper.selectOne(
  1312. new LambdaQueryWrapper<Consignee>()
  1313. .eq(Consignee::getUserId, userId)
  1314. .eq(Consignee::getIsDefault, 1)
  1315. .last("LIMIT 1"));
  1316. if (defaultConsignee != null) {
  1317. consigneeId = defaultConsignee.getId();
  1318. }
  1319. } catch (Exception e) {
  1320. log.warn("查询默认收货地址失败: userId={}", userId);
  1321. }
  1322. }
  1323. CreateProductOrderDTO orderDto = new CreateProductOrderDTO();
  1324. orderDto.setProductId(productId);
  1325. orderDto.setQuantity(quantity);
  1326. orderDto.setPaymentMethod("wechat");
  1327. if (deliveryMethod != null) {
  1328. orderDto.setDeliveryMethod(deliveryMethod);
  1329. }
  1330. if (consigneeId != null) {
  1331. orderDto.setConsigneeId(consigneeId);
  1332. }
  1333. Result<ProductOrderDTO> orderResult = productOrderService.create(orderDto, userId);
  1334. if (orderResult.getCode() == 200 && orderResult.getData() != null) {
  1335. extra.put("orderId", orderResult.getData().getId());
  1336. extra.put("orderNo", orderResult.getData().getOrderNo());
  1337. exec.setExtra(JSON.toJSONString(extra));
  1338. taskExecutionMapper.updateById(exec);
  1339. log.info("购物任务下单成功: taskId={}, orderNo={}", task.getId(), orderResult.getData().getOrderNo());
  1340. } else {
  1341. // 下单失败(通常因缺地址/库存),保留加购状态
  1342. extra.put("orderFailed", true);
  1343. extra.put("orderError", orderResult.getMessage());
  1344. exec.setExtra(JSON.toJSONString(extra));
  1345. taskExecutionMapper.updateById(exec);
  1346. log.warn("购物任务下单失败: taskId={}, reason={}", task.getId(), orderResult.getMessage());
  1347. }
  1348. } else {
  1349. // 纯加购
  1350. exec.setExtra(JSON.toJSONString(extra));
  1351. taskExecutionMapper.updateById(exec);
  1352. }
  1353. } catch (Exception e) {
  1354. log.error("购物任务联动失败: taskId={}, error={}", task.getId(), e.getMessage(), e);
  1355. extra.put("error", e.getMessage());
  1356. exec.setExtra(JSON.toJSONString(extra));
  1357. taskExecutionMapper.updateById(exec);
  1358. }
  1359. }
  1360. private com.alibaba.fastjson.JSONObject parseActionConfig(String json) {
  1361. if (json == null || json.trim().isEmpty()) {
  1362. return null;
  1363. }
  1364. try {
  1365. return JSON.parseObject(json);
  1366. } catch (Exception e) {
  1367. log.warn("解析 actionConfig 失败: {}", json);
  1368. return null;
  1369. }
  1370. }
  1371. /**
  1372. * TASK-SYS-248: 调研任务自动模板生成(actionType=survey 时,若 actionConfig 无 surveyId 则调 AI 生成)
  1373. * 在 createTask 的 taskMapper.insert() 之后调用
  1374. */
  1375. private void ensureSurveyTemplateForTask(Task task) {
  1376. if (!"survey".equals(task.getActionType())) {
  1377. return;
  1378. }
  1379. JSONObject config = parseActionConfig(task.getActionConfig());
  1380. // 已有 surveyId → 跳过
  1381. if (config != null && config.get("surveyId") != null) {
  1382. return;
  1383. }
  1384. // 从 config 或 task 属性取维度和主题
  1385. String dimension = null;
  1386. String topic = null;
  1387. if (config != null) {
  1388. dimension = config.getString("dimension");
  1389. topic = config.getString("topic");
  1390. }
  1391. if (dimension == null || dimension.isEmpty()) {
  1392. dimension = task.getDimensionCode();
  1393. }
  1394. if (topic == null || topic.isEmpty()) {
  1395. topic = task.getTitle();
  1396. }
  1397. if (topic == null || topic.isEmpty()) {
  1398. topic = "综合评估";
  1399. }
  1400. try {
  1401. String questionsJson = surveyService.aiGenerateQuestions(dimension, topic);
  1402. SurveyTemplate tpl = new SurveyTemplate();
  1403. tpl.setTitle(task.getTitle() != null ? task.getTitle() + " - 调研" : "定期调研");
  1404. tpl.setDimension(dimension);
  1405. tpl.setQuestionsJson(questionsJson);
  1406. tpl.setCycleType("weekly");
  1407. tpl.setEnabled(1);
  1408. tpl.setAiGenerated(1);
  1409. tpl.setCreatedAt(new Date());
  1410. tpl.setUpdatedAt(new Date());
  1411. surveyTemplateMapper.insert(tpl);
  1412. // 回填 actionConfig
  1413. if (config == null) {
  1414. config = new JSONObject();
  1415. }
  1416. config.put("surveyId", tpl.getId());
  1417. task.setActionConfig(config.toJSONString());
  1418. taskMapper.updateById(task);
  1419. log.info("调研任务自动生成模板: taskId={}, templateId={}, dimension={}, topic={}",
  1420. task.getId(), tpl.getId(), dimension, topic);
  1421. } catch (Exception e) {
  1422. log.warn("调研任务自动生成模板失败(不影响任务创建): taskId={}, error={}", task.getId(), e.getMessage());
  1423. }
  1424. }
  1425. // ==================== 统一任务系统:成长任务迁移(原 GrowthTaskService) ====================
  1426. /**
  1427. * 回填进度型任务的当前进度(progress/todayDone,不入库)
  1428. */
  1429. private void fillProgress(List<Task> tasks, Long memberId) {
  1430. if (tasks == null || tasks.isEmpty()) {
  1431. return;
  1432. }
  1433. for (Task t : tasks) {
  1434. if (t.getIsDailyProgress() != null && t.getIsDailyProgress() == 1) {
  1435. TaskExecution exec = taskExecutionMapper.selectOne(new LambdaQueryWrapper<TaskExecution>()
  1436. .eq(TaskExecution::getTaskId, t.getId())
  1437. .eq(TaskExecution::getMemberId, memberId)
  1438. .last("LIMIT 1"));
  1439. int progress = 0;
  1440. if (exec != null && exec.getExtra() != null) {
  1441. try {
  1442. progress = JSON.parseObject(exec.getExtra()).getIntValue("progress");
  1443. } catch (Exception ignore) {
  1444. }
  1445. }
  1446. t.setProgress(progress);
  1447. t.setTodayDone("completed".equals(t.getStatus()) ? 1 : 0);
  1448. }
  1449. }
  1450. }
  1451. /**
  1452. * 统一任务系统:更新每日进度型任务的进度(原 growth_task updateProgress)
  1453. * 按 (userId→familyMemberId, taskKey) 找当日任务实例,累计进度,达标自动完成+发奖
  1454. * taskKey 匹配约定:title 精确匹配 OR title 以 "[taskKey]" 前缀匹配(迁移时 task_key 拼入 title)
  1455. */
  1456. @Transactional
  1457. public void updateProgress(Long userId, String taskKey, int increment) {
  1458. updateProgress(userId, null, taskKey, increment);
  1459. }
  1460. /**
  1461. * 统一任务系统:更新每日进度型任务的进度(带家庭成员精确定位)
  1462. * 按 (familyMemberId 或 userId→familyMemberId, taskKey) 找当日任务实例,累计进度,达标自动完成+发奖
  1463. * taskKey 匹配约定:title 精确匹配 OR title 以 "[taskKey]" 前缀匹配(迁移时 task_key 拼入 title)
  1464. *
  1465. * 打卡类调用必须显式传 familyMemberId:同一 userId 可能绑定多个家庭成员(多家庭场景),
  1466. * 仅按 user_id 反查(LIMIT 1 无排序)会命中错误成员,导致进度记到他人任务、本人任务永久 pending。
  1467. */
  1468. @Transactional
  1469. public void updateProgress(Long userId, Long familyMemberId, String taskKey, int increment) {
  1470. if (userId == null) {
  1471. // 未绑定登录账号的家庭成员(如仅建档的孩子)无成长任务进度
  1472. return;
  1473. }
  1474. FamilyMember member;
  1475. if (familyMemberId != null) {
  1476. member = familyMemberMapper.selectById(familyMemberId);
  1477. if (member == null || !Long.valueOf(userId).equals(member.getUserId())) {
  1478. log.warn("统一任务进度更新:成员{}不存在或不属于用户{},跳过 taskKey={}",
  1479. familyMemberId, userId, taskKey);
  1480. return;
  1481. }
  1482. } else {
  1483. member = familyMemberMapper.selectOne(
  1484. new LambdaQueryWrapper<FamilyMember>()
  1485. .eq(FamilyMember::getUserId, userId)
  1486. .last("LIMIT 1"));
  1487. }
  1488. if (member == null) {
  1489. return;
  1490. }
  1491. Calendar cal = Calendar.getInstance();
  1492. cal.set(Calendar.HOUR_OF_DAY, 0);
  1493. cal.set(Calendar.MINUTE, 0);
  1494. cal.set(Calendar.SECOND, 0);
  1495. cal.set(Calendar.MILLISECOND, 0);
  1496. Date startOfDay = cal.getTime();
  1497. cal.add(Calendar.DAY_OF_MONTH, 1);
  1498. Date endOfDay = cal.getTime();
  1499. Task task = taskMapper.selectOne(new LambdaQueryWrapper<Task>()
  1500. .eq(Task::getFamilyMemberId, member.getId())
  1501. .eq(Task::getIsDailyProgress, 1)
  1502. .ge(Task::getCreatedAt, startOfDay)
  1503. .lt(Task::getCreatedAt, endOfDay)
  1504. .and(w -> w.eq(Task::getTitle, taskKey)
  1505. .or().likeRight(Task::getTitle, "[" + taskKey + "]"))
  1506. .last("LIMIT 1"));
  1507. if (task == null) {
  1508. log.warn("统一任务进度更新:未找到今日进度型任务 taskKey={}, userId={}", taskKey, userId);
  1509. return;
  1510. }
  1511. if ("completed".equals(task.getStatus())) {
  1512. return;
  1513. }
  1514. // 读当前进度(task_executions.extra.progress),累加
  1515. TaskExecution exec = taskExecutionMapper.selectOne(new LambdaQueryWrapper<TaskExecution>()
  1516. .eq(TaskExecution::getTaskId, task.getId())
  1517. .eq(TaskExecution::getMemberId, member.getId())
  1518. .last("LIMIT 1"));
  1519. int progress = 0;
  1520. if (exec != null && exec.getExtra() != null) {
  1521. try {
  1522. progress = JSON.parseObject(exec.getExtra()).getIntValue("progress");
  1523. } catch (Exception ignore) {
  1524. }
  1525. }
  1526. progress += increment;
  1527. int target = task.getTargetValue() != null ? task.getTargetValue() : 1;
  1528. if (exec == null) {
  1529. exec = new TaskExecution();
  1530. exec.setTaskId(task.getId());
  1531. exec.setMemberId(member.getId());
  1532. exec.setStatus("started");
  1533. exec.setStartedAt(new Date());
  1534. exec.setCreatedAt(new Date());
  1535. }
  1536. exec.setStatus("finished");
  1537. exec.setFinishedAt(new Date());
  1538. exec.setExtra("{\"progress\":" + progress + ",\"target\":" + target + "}");
  1539. if (exec.getId() == null) {
  1540. taskExecutionMapper.insert(exec);
  1541. } else {
  1542. taskExecutionMapper.updateById(exec);
  1543. }
  1544. // 达标自动完成 + 发奖(复用现有奖励链路)
  1545. if (progress >= target) {
  1546. awardTaskCompletionRewards(task, member.getId(), new Date());
  1547. }
  1548. }
  1549. /**
  1550. * 统一任务系统:AI 对话生成任务(原 growth_task createDynamicTask)
  1551. * 直接写入 tasks 表,source_type=ai
  1552. */
  1553. @Transactional
  1554. public Long createDynamicTask(Long userId, String title, String description,
  1555. String dimension, Integer rewardPoints, String sourceConversationId) {
  1556. User user = userService.getUserInfo(userId);
  1557. Task task = new Task();
  1558. task.setFamilyId(user.getFamilyId());
  1559. task.setCreatorId(userId);
  1560. task.setExecutorType("child");
  1561. // 执行者:AI 任务发给当前用户对应家庭成员(若有)
  1562. FamilyMember member = familyMemberMapper.selectOne(
  1563. new LambdaQueryWrapper<FamilyMember>()
  1564. .eq(FamilyMember::getUserId, userId)
  1565. .last("LIMIT 1"));
  1566. if (member != null) {
  1567. task.setExecutorId(member.getId());
  1568. task.setFamilyMemberId(member.getId());
  1569. task.setChildId(member.getId());
  1570. }
  1571. task.setTitle(title);
  1572. task.setDescription(description);
  1573. task.setPoints(rewardPoints != null ? rewardPoints : 10);
  1574. task.setCategory("成长");
  1575. task.setStatus("pending");
  1576. task.setNeedReview(0);
  1577. task.setIsTemplate(0);
  1578. task.setSourceType("ai");
  1579. task.setSourceId(0L);
  1580. task.setDimensionCode(dimension != null ? dimension : "body");
  1581. task.setMemberOnly(0);
  1582. task.setDeadline(new Date(System.currentTimeMillis() + 7L * 24 * 3600 * 1000)); // 7天有效期
  1583. task.setCreatedAt(new Date());
  1584. task.setUpdatedAt(new Date());
  1585. taskMapper.insert(task);
  1586. log.info("创建AI动态任务: userId={}, taskId={}, title={}", userId, task.getId(), title);
  1587. return task.getId();
  1588. }
  1589. /**
  1590. * 统一任务系统:查询用户每日进度型任务列表(原 growth-task/list)
  1591. * type: DAILY(NEWBIE 在 Task 5 处理)
  1592. */
  1593. public List<Map<String, Object>> getGrowthTaskList(Long userId, String type, String dimension) {
  1594. FamilyMember member = familyMemberMapper.selectOne(
  1595. new LambdaQueryWrapper<FamilyMember>()
  1596. .eq(FamilyMember::getUserId, userId)
  1597. .last("LIMIT 1"));
  1598. List<Map<String, Object>> result = new ArrayList<>();
  1599. if (member == null || !"DAILY".equals(type)) {
  1600. return result;
  1601. }
  1602. Calendar cal = Calendar.getInstance();
  1603. cal.set(Calendar.HOUR_OF_DAY, 0);
  1604. cal.set(Calendar.MINUTE, 0);
  1605. cal.set(Calendar.SECOND, 0);
  1606. cal.set(Calendar.MILLISECOND, 0);
  1607. Date startOfDay = cal.getTime();
  1608. cal.add(Calendar.DAY_OF_MONTH, 1);
  1609. Date endOfDay = cal.getTime();
  1610. List<Task> tasks = taskMapper.selectList(new LambdaQueryWrapper<Task>()
  1611. .eq(Task::getFamilyMemberId, member.getId())
  1612. .eq(Task::getIsDailyProgress, 1)
  1613. .ge(Task::getCreatedAt, startOfDay)
  1614. .lt(Task::getCreatedAt, endOfDay)
  1615. .orderByAsc(Task::getId));
  1616. for (Task task : tasks) {
  1617. Map<String, Object> item = new LinkedHashMap<>();
  1618. item.put("taskLogId", task.getId());
  1619. item.put("id", task.getId());
  1620. item.put("type", "DAILY");
  1621. item.put("title", task.getTitle());
  1622. item.put("description", task.getDescription());
  1623. item.put("rewardPoints", task.getPoints());
  1624. item.put("rewardEnergy", 0);
  1625. item.put("targetValue", task.getTargetValue() != null ? task.getTargetValue() : 1);
  1626. item.put("dimension", task.getDimensionCode());
  1627. TaskExecution exec = taskExecutionMapper.selectOne(new LambdaQueryWrapper<TaskExecution>()
  1628. .eq(TaskExecution::getTaskId, task.getId())
  1629. .eq(TaskExecution::getMemberId, member.getId())
  1630. .last("LIMIT 1"));
  1631. int progress = 0;
  1632. if (exec != null && exec.getExtra() != null) {
  1633. try {
  1634. progress = JSON.parseObject(exec.getExtra()).getIntValue("progress");
  1635. } catch (Exception ignore) {
  1636. }
  1637. }
  1638. item.put("progress", progress);
  1639. item.put("completed", "completed".equals(task.getStatus()) ? 1 : 0);
  1640. item.put("claimed", 0); // 统一任务系统:自动发放,无 claimed
  1641. result.add(item);
  1642. }
  1643. return result;
  1644. }
  1645. // ==================== 统一任务系统:新人引导迁移(原 OnboardingService) ====================
  1646. /**
  1647. * 统一任务系统:新用户注册初始化新人引导任务
  1648. * 按 admin_task_templates 中 onboarding_sequence 有值的模板生成 tasks(source_type=onboarding)
  1649. * 顺序链:sequence 递增,前序未完成则后序 locked(复用 prerequisite_task_id + locked 状态)
  1650. */
  1651. @Transactional
  1652. public void initOnboardingTasks(Long userId) {
  1653. User user = userService.getUserInfo(userId);
  1654. if (user == null || user.getFamilyId() == null) {
  1655. return;
  1656. }
  1657. // 幂等:该用户已有 onboarding 任务则跳过
  1658. Long existing = taskMapper.selectCount(new LambdaQueryWrapper<Task>()
  1659. .eq(Task::getCreatorId, userId)
  1660. .eq(Task::getSourceType, "onboarding"));
  1661. if (existing != null && existing > 0) {
  1662. return;
  1663. }
  1664. List<TaskTemplate> templates = taskTemplateMapper.selectList(
  1665. new LambdaQueryWrapper<TaskTemplate>()
  1666. .eq(TaskTemplate::getIsActive, 1)
  1667. .isNotNull(TaskTemplate::getOnboardingSequence)
  1668. .orderByAsc(TaskTemplate::getOnboardingSequence));
  1669. if (templates.isEmpty()) {
  1670. return;
  1671. }
  1672. FamilyMember member = familyMemberMapper.selectOne(
  1673. new LambdaQueryWrapper<FamilyMember>()
  1674. .eq(FamilyMember::getUserId, userId)
  1675. .last("LIMIT 1"));
  1676. Date now = new Date();
  1677. Long prevTaskId = null;
  1678. for (TaskTemplate tpl : templates) {
  1679. Task task = new Task();
  1680. task.setFamilyId(user.getFamilyId());
  1681. task.setCreatorId(userId);
  1682. task.setExecutorType("child");
  1683. if (member != null) {
  1684. task.setExecutorId(member.getId());
  1685. task.setFamilyMemberId(member.getId());
  1686. task.setChildId(member.getId());
  1687. }
  1688. task.setTitle(tpl.getTitle());
  1689. task.setDescription(tpl.getDescription());
  1690. task.setPoints(tpl.getPoints() != null ? tpl.getPoints() : 2);
  1691. task.setCategory(tpl.getCategory());
  1692. task.setStatus(prevTaskId == null ? "pending" : "locked"); // 首任务可直接做,后续需解锁
  1693. task.setNeedReview(tpl.getNeedReview() != null ? tpl.getNeedReview() : 0);
  1694. task.setIsTemplate(0);
  1695. task.setSourceType("onboarding");
  1696. task.setTemplateId(tpl.getId());
  1697. task.setPrerequisiteTaskId(prevTaskId); // 顺序链
  1698. task.setMemberOnly(0);
  1699. task.setDeadline(new Date(now.getTime() + 30L * 24 * 3600 * 1000)); // 30天有效期
  1700. task.setCreatedAt(now);
  1701. task.setUpdatedAt(now);
  1702. taskMapper.insert(task);
  1703. prevTaskId = task.getId();
  1704. }
  1705. log.info("新用户{}初始化{}条新人引导任务", userId, templates.size());
  1706. }
  1707. /**
  1708. * 统一任务系统:完成指定新人引导任务(原 OnboardingService.completeTask)
  1709. * 按 source_type=onboarding + title 精确匹配(title 约定 = 原 taskType,如 FIRST_TASK)
  1710. */
  1711. public void completeOnboardingTask(Long userId, String templateTitle) {
  1712. FamilyMember member = familyMemberMapper.selectOne(
  1713. new LambdaQueryWrapper<FamilyMember>()
  1714. .eq(FamilyMember::getUserId, userId)
  1715. .last("LIMIT 1"));
  1716. if (member == null) {
  1717. return;
  1718. }
  1719. Task task = taskMapper.selectOne(new LambdaQueryWrapper<Task>()
  1720. .eq(Task::getFamilyMemberId, member.getId())
  1721. .eq(Task::getSourceType, "onboarding")
  1722. .eq(Task::getTitle, templateTitle)
  1723. .in(Task::getStatus, "pending", "locked")
  1724. .last("LIMIT 1"));
  1725. if (task != null) {
  1726. awardTaskCompletionRewards(task, member.getId(), new Date());
  1727. unlockPrerequisiteTasks(task.getId());
  1728. }
  1729. }
  1730. /**
  1731. * 统一任务系统:查询用户的新人引导任务列表(原 OnboardingService.getProgress)
  1732. * 返回前端兼容结构:taskType=title、status(completed→CLAIMED,其余→PENDING)、rewardPoints、rewardEnergy
  1733. */
  1734. public List<Map<String, Object>> getOnboardingTasks(Long userId) {
  1735. FamilyMember member = familyMemberMapper.selectOne(
  1736. new LambdaQueryWrapper<FamilyMember>()
  1737. .eq(FamilyMember::getUserId, userId)
  1738. .last("LIMIT 1"));
  1739. List<Map<String, Object>> result = new ArrayList<>();
  1740. if (member == null) {
  1741. return result;
  1742. }
  1743. List<Task> tasks = taskMapper.selectList(new LambdaQueryWrapper<Task>()
  1744. .eq(Task::getFamilyMemberId, member.getId())
  1745. .eq(Task::getSourceType, "onboarding")
  1746. .orderByAsc(Task::getId));
  1747. for (Task task : tasks) {
  1748. Map<String, Object> item = new LinkedHashMap<>();
  1749. item.put("taskType", task.getTitle());
  1750. item.put("rewardPoints", task.getPoints() != null ? task.getPoints() : 0);
  1751. item.put("rewardEnergy", task.getPoints() != null ? task.getPoints() : 0); // 能量≈积分(与 awardTaskCompletionRewards 一致)
  1752. // 状态映射:completed→CLAIMED(奖励已自动发放),其余→PENDING
  1753. item.put("status", "completed".equals(task.getStatus()) ? "CLAIMED" : "PENDING");
  1754. result.add(item);
  1755. }
  1756. return result;
  1757. }
  1758. /**
  1759. * 获取家庭成员可关联的任务列表(含所有状态),供创建任务时选择前置/后置任务
  1760. */
  1761. public List<Task> getRelatedTaskList(Long familyId, Long memberId, String titleKeyword) {
  1762. LambdaQueryWrapper<Task> wrapper = new LambdaQueryWrapper<Task>()
  1763. .eq(Task::getFamilyId, familyId)
  1764. .ne(Task::getStatus, "cancelled")
  1765. .orderByDesc(Task::getCreatedAt);
  1766. if (memberId != null) {
  1767. wrapper.eq(Task::getFamilyMemberId, memberId);
  1768. }
  1769. if (titleKeyword != null && !titleKeyword.trim().isEmpty()) {
  1770. wrapper.like(Task::getTitle, titleKeyword.trim());
  1771. }
  1772. return taskMapper.selectList(wrapper);
  1773. }
  1774. }