TaskService.java 96 KB

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