| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223 |
- package com.etotem.cfc.service;
- import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
- import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
- import com.etotem.cfc.common.Result;
- import com.etotem.cfc.dto.CreateProductOrderDTO;
- import com.etotem.cfc.dto.CreateTaskDTO;
- import com.etotem.cfc.dto.ProductOrderDTO;
- import com.etotem.cfc.dto.TaskReviewDTO;
- import com.etotem.cfc.entity.*;
- import com.etotem.cfc.mapper.*;
- import com.etotem.cfc.service.api.TaskServiceInterface;
- import com.alibaba.fastjson.JSON;
- import com.alibaba.fastjson.JSONObject;
- import com.etotem.cfc.service.OrchestrationEngine;
- import javax.annotation.Resource;
- import lombok.extern.slf4j.Slf4j;
- import org.springframework.context.annotation.Lazy;
- import org.springframework.stereotype.Service;
- import org.springframework.transaction.annotation.Transactional;
- import java.util.*;
- import com.etotem.cfc.util.DimensionCodes;
- import com.etotem.cfc.util.SortUtil;
- import com.etotem.cfc.service.NotificationDispatchService;
- @Slf4j
- @Service
- public class TaskService implements TaskServiceInterface {
- @Resource
- private TaskMapper taskMapper;
- @Resource
- private NotificationDispatchService notificationDispatchService;
- @Resource
- private TaskReminderConfigService taskReminderConfigService;
- @Resource
- private TaskExecutionMapper taskExecutionMapper;
- @Resource
- private FamilyMemberMapper familyMemberMapper;
- @Resource
- private UserMapper userMapper;
- @Resource
- private PointsLogMapper pointsLogMapper;
- @Resource
- @Lazy
- private UserService userService;
- @Resource
- private StreakService streakService;
- @Resource
- private MiniGameService miniGameService;
- @Resource
- private MiniGameMapper miniGameMapper;
- @Resource
- private EnergyService energyService;
- @Resource
- private FamilyRelationshipScoreMapper familyRelationshipScoreMapper;
- @Resource
- private CartService cartService;
- @Resource
- private ProductOrderService productOrderService;
- @Resource
- private ConsigneeMapper consigneeMapper;
- @Resource
- private SurveyService surveyService;
- @Resource
- private SurveyTemplateMapper surveyTemplateMapper;
- @Resource
- @Lazy
- private OrchestrationEngine orchestrationEngine;
- @Resource
- private TaskTemplateMapper taskTemplateMapper;
- @Resource
- private CfPathwayEarnService cfPathwayEarnService;
- private static final int EARLY_BONUS = 1; // 提前完成奖励
- private static final int PENALTY_MAX_DAILY = 5; // 每日最多扣分
- private static final int LATE_GRACE_MINUTES = 10; // 10分钟内不算迟到
- // ==================== SC-P-10 创建任务校验(S-cfc-12) ====================
- private static final Set<String> VALID_FREQUENCIES = Collections.unmodifiableSet(
- new HashSet<>(Arrays.asList("none", "daily", "weekly", "monthly", "every_other_day", "hourly")));
- /** 幂等窗口:同一创建请求在 30 秒内重复提交仅生成 1 条任务(重复提交不重复发分) */
- private static final long TASK_DEDUP_WINDOW_MS = 30_000L;
- /** 按任务去重键持有的 JVM 内锁,保证并发双发时 check-then-insert 原子性 */
- private static final java.util.concurrent.ConcurrentHashMap<String, Object> TASK_DEDUP_LOCKS =
- new java.util.concurrent.ConcurrentHashMap<>();
- /** JVM 内刚创建任务登记表:dedupKey → long[]{taskId, createdAtMillis}(不依赖 DB 事务提交时机) */
- private static final java.util.concurrent.ConcurrentHashMap<String, long[]> RECENT_TASKS =
- new java.util.concurrent.ConcurrentHashMap<>();
- @Transactional
- public Long createTask(Long userId, CreateTaskDTO dto) {
- User currentUser = userService.getUserInfo(userId);
- // ===== SC-P-10 创建任务参数校验(异常流:空标题/负积分/非法频率/超期截止 → 400)=====
- if (dto.getTitle() == null || dto.getTitle().trim().isEmpty()) {
- throw new IllegalArgumentException("任务标题不能为空");
- }
- if (dto.getPoints() != null && dto.getPoints() < 0) {
- throw new IllegalArgumentException("任务积分不能为负数");
- }
- if (dto.getFrequency() != null && !dto.getFrequency().isEmpty()
- && !VALID_FREQUENCIES.contains(dto.getFrequency())) {
- throw new IllegalArgumentException("任务频率不合法: " + dto.getFrequency());
- }
- if (dto.getDeadline() != null && dto.getDeadline().before(new Date())) {
- throw new IllegalArgumentException("任务截止时间不能早于当前时间");
- }
-
- // 处理执行者类型 - 支持家庭成员任务、家庭主账号任务、家庭成员任务
- String executorType = dto.getExecutorType();
- if (executorType == null || executorType.isEmpty()) {
- executorType = "child";
- }
- // SC-P-10 权限流加固:child 下发的执行人(memberId/executorId)必须属于本家庭,
- // 防横向越权向他人家庭成员注入任务(等价 member 分支的「被分配成员不属于本家庭」校验)
- if ("child".equals(executorType)) {
- List<Long> assigneeIds = new ArrayList<>();
- if (dto.getMemberIds() != null) assigneeIds.addAll(dto.getMemberIds());
- if (dto.getMemberId() != null) assigneeIds.add(dto.getMemberId());
- if (dto.getExecutorId() != null) assigneeIds.add(dto.getExecutorId());
- for (Long assigneeId : assigneeIds) {
- if (assigneeId == null) continue;
- Long count = familyMemberMapper.selectCount(
- new LambdaQueryWrapper<FamilyMember>()
- .eq(FamilyMember::getId, assigneeId)
- .eq(FamilyMember::getFamilyId, currentUser.getFamilyId()));
- if (count == 0) {
- throw new IllegalArgumentException("被分配成员不属于本家庭");
- }
- }
- }
- if ("member".equals(executorType)) {
- Long currentMemberId = getCurrentMemberId(userId);
- long familyMemberCount = familyMemberMapper.selectCount(
- new LambdaQueryWrapper<FamilyMember>()
- .eq(FamilyMember::getFamilyId, currentUser.getFamilyId()));
- if (familyMemberCount < 2) {
- throw new IllegalArgumentException("该任务需分配给其他家庭成员,当前家庭不足2人");
- }
- List<Long> assigneeIds = dto.getMemberIds();
- if (assigneeIds == null || assigneeIds.isEmpty()) {
- if (dto.getExecutorId() != null) {
- assigneeIds = Collections.singletonList(dto.getExecutorId());
- } else if (dto.getMemberId() != null) {
- assigneeIds = Collections.singletonList(dto.getMemberId());
- } else {
- assigneeIds = Collections.emptyList();
- }
- }
- for (Long assigneeId : assigneeIds) {
- if (assigneeId == null) continue;
- if (assigneeId.equals(currentMemberId)) {
- throw new IllegalArgumentException("不能给自己分配家庭成员任务");
- }
- Long count = familyMemberMapper.selectCount(
- new LambdaQueryWrapper<FamilyMember>()
- .eq(FamilyMember::getId, assigneeId)
- .eq(FamilyMember::getFamilyId, currentUser.getFamilyId()));
- if (count == 0) {
- throw new IllegalArgumentException("被分配成员不属于本家庭");
- }
- }
- }
- // 支持多家庭成员:为每个家庭成员创建独立任务实例
- List<Long> memberIds = dto.getMemberIds();
- if (memberIds == null || memberIds.isEmpty()) {
- // 兼容旧版:使用单个memberId
- if (dto.getMemberId() != null) {
- memberIds = Collections.singletonList(dto.getMemberId());
- } else if (dto.getExecutorId() != null && "child".equals(executorType)) {
- memberIds = Collections.singletonList(dto.getExecutorId());
- } else {
- memberIds = Collections.emptyList();
- }
- }
-
- // 如果有多个家庭成员,为每个家庭成员创建任务
- if (!memberIds.isEmpty() && memberIds.size() > 1) {
- Long firstTaskId = null;
- for (int i = 0; i < memberIds.size(); i++) {
- Task memberTask = createSingleTask(currentUser, userId, executorType, memberIds.get(i), dto);
- Long insertedId = insertTaskWithDedup(memberTask);
- if (i == 0) firstTaskId = insertedId;
- }
- return firstTaskId;
- } else {
- // 单个家庭成员或无家庭成员
- Long memberId = memberIds.isEmpty() ? null : memberIds.get(0);
- Task task = createSingleTask(currentUser, userId, executorType, memberId, dto);
- return insertTaskWithDedup(task);
- }
- }
- /**
- * SC-P-10 幂等插入:同一创建请求(家庭+执行人+标题+积分+频率+截止)在窗口内重复提交
- * 复用已有任务 id。并发场景按任务键加 JVM 内锁,先查内存登记表(不依赖 DB 事务提交时机),
- * 再查 DB 窗口内同内容任务(兜底跨重启/多实例),均未命中才插入并登记。
- */
- private Long insertTaskWithDedup(Task task) {
- String key = dedupKeyFor(task);
- Object lock = TASK_DEDUP_LOCKS.computeIfAbsent(key, k -> new Object());
- synchronized (lock) {
- // 1) JVM 内登记表(并发双发:第一个请求尚未提交 DB 也能命中)
- long[] recent = RECENT_TASKS.get(key);
- if (recent != null && System.currentTimeMillis() - recent[1] < TASK_DEDUP_WINDOW_MS) {
- log.info("SC-P-10 幂等:JVM 登记表命中重复创建(key={}),复用任务 id={}", key, recent[0]);
- return recent[0];
- }
- // 2) DB 窗口内同内容任务(跨重启/多实例兜底)
- Long dupId = findRecentDuplicateTask(task);
- if (dupId != null) {
- RECENT_TASKS.put(key, new long[]{dupId, System.currentTimeMillis()});
- log.info("SC-P-10 幂等:DB 命中重复创建(familyId={}, executor={}, title={}),复用任务 id={}",
- task.getFamilyId(), task.getExecutorId(), task.getTitle(), dupId);
- return dupId;
- }
- taskMapper.insert(task);
- ensureSurveyTemplateForTask(task);
- RECENT_TASKS.put(key, new long[]{task.getId(), System.currentTimeMillis()});
- return task.getId();
- }
- }
- /** 去重键(截止时间按秒归一化:DB DATETIME 无毫秒,避免毫秒差导致幂等漏判) */
- private String dedupKeyFor(Task task) {
- String deadline = "";
- if (task.getDeadline() != null) {
- deadline = String.valueOf(task.getDeadline().getTime() / 1000);
- }
- return task.getFamilyId() + "|" + task.getExecutorType() + "|" + task.getExecutorId()
- + "|" + task.getTitle() + "|" + task.getPoints() + "|" + task.getFrequency()
- + "|" + deadline;
- }
- /** 在幂等窗口内查找刚创建过的同内容任务(仅查窗口内的同 key 任务,不误伤历史重复任务) */
- private Long findRecentDuplicateTask(Task candidate) {
- Date windowStart = new Date(System.currentTimeMillis() - TASK_DEDUP_WINDOW_MS);
- LambdaQueryWrapper<Task> wrapper = new LambdaQueryWrapper<Task>()
- .eq(Task::getFamilyId, candidate.getFamilyId())
- .eq(Task::getTitle, candidate.getTitle())
- .eq(Task::getPoints, candidate.getPoints())
- .ge(Task::getCreatedAt, windowStart);
- if (candidate.getFrequency() != null) wrapper.eq(Task::getFrequency, candidate.getFrequency());
- if (candidate.getExecutorType() != null) wrapper.eq(Task::getExecutorType, candidate.getExecutorType());
- if (candidate.getExecutorId() != null) wrapper.eq(Task::getExecutorId, candidate.getExecutorId());
- if (candidate.getFamilyMemberId() != null) wrapper.eq(Task::getFamilyMemberId, candidate.getFamilyMemberId());
- if (candidate.getDeadline() != null) {
- // 截止时间按秒精度比对(DB DATETIME 无毫秒)
- java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
- wrapper.apply("DATE_FORMAT(deadline, '%Y-%m-%d %H:%i:%s') = {0}", sdf.format(candidate.getDeadline()));
- }
- List<Task> hits = taskMapper.selectList(wrapper.last("LIMIT 1"));
- return hits.isEmpty() ? null : hits.get(0).getId();
- }
-
- /**
- * 创建单个任务实例
- */
- private Task createSingleTask(User currentUser, Long userId, String executorType, Long memberId, CreateTaskDTO dto) {
- Task task = new Task();
- task.setFamilyId(currentUser.getFamilyId());
- task.setCreatorId(userId);
- task.setExecutorType(executorType);
-
- if ("child".equals(executorType)) {
- task.setExecutorId(memberId);
- task.setFamilyMemberId(memberId);
- task.setMemberId(memberId);
- } else if ("member".equals(executorType)) {
- task.setExecutorId(memberId);
- task.setMemberId(null);
- task.setStatus("assigned");
- task.setAcceptDeadline(dto.getDeadline());
- } else {
- task.setExecutorId(dto.getExecutorId() != null ? dto.getExecutorId() : userId);
- task.setMemberId(null);
- }
-
- task.setTitle(dto.getTitle());
- task.setDescription(dto.getDescription());
- task.setPoints(dto.getPoints() != null ? dto.getPoints() : 2);
- task.setDuration(dto.getDuration());
- // 处理小游戏任务
- task.setMinigameCode(dto.getMinigameCode());
- if ("小游戏类".equals(dto.getCategory()) && dto.getMinigameCode() != null) {
- // 小游戏任务:从MiniGame获取默认积分
- MiniGame miniGame = miniGameMapper.selectOne(
- new LambdaQueryWrapper<MiniGame>()
- .eq(MiniGame::getGameCode, dto.getMinigameCode())
- .eq(MiniGame::getStatus, 1)
- );
- if (miniGame != null) {
- if (dto.getPoints() == null) {
- task.setPoints(miniGame.getDefaultPoints() != null ? miniGame.getDefaultPoints() : 5);
- }
- if (dto.getTitle() == null || dto.getTitle().isEmpty()) {
- task.setTitle(miniGame.getGameName());
- }
- }
- }
- // 如果没有指定截止时间,默认今天23:59
- if (dto.getDeadline() == null) {
- Calendar cal = Calendar.getInstance();
- cal.set(Calendar.HOUR_OF_DAY, 23);
- cal.set(Calendar.MINUTE, 59);
- cal.set(Calendar.SECOND, 59);
- task.setDeadline(cal.getTime());
- } else {
- task.setDeadline(dto.getDeadline());
- }
-
- task.setRepeatType(dto.getRepeatType() != null ? dto.getRepeatType() : "none");
- task.setCategory(dto.getCategory());
- // 任务性质
- task.setTaskType(dto.getTaskType());
- task.setFrequency(dto.getFrequency());
- task.setMaxFrequency(dto.getMaxFrequency());
- // 完成方式(转为JSON存储)
- if (dto.getCompleteTypes() != null && !dto.getCompleteTypes().isEmpty()) {
- task.setCompleteTypes(String.join(",", dto.getCompleteTypes()));
- }
- // 审核设置
- task.setReviewType(dto.getReviewType());
-
- // 时间设置
- task.setEarliestStart(dto.getEarliestStart());
- task.setLatestEnd(dto.getLatestEnd());
- // 如果是重复任务,标记为模板
- if (!"none".equals(task.getRepeatType())) {
- task.setIsTemplate(1); // 模板任务
- } else {
- task.setIsTemplate(0);
- }
- // 家庭主账号给家庭成员安排任务需要审核,家庭成员给家庭主账号安排任务也需要审核
- if ("child".equals(executorType)) {
- task.setNeedReview(dto.getNeedReview() != null ? dto.getNeedReview() : 0);
- } else {
- // 家庭成员给家庭主账号安排任务,默认需要审核
- task.setNeedReview(dto.getNeedReview() != null ? dto.getNeedReview() : 1);
- }
- task.setReviewByCategory(dto.getReviewByCategory() != null ? dto.getReviewByCategory() : 0);
- if (!"assigned".equals(task.getStatus())) {
- // TASK-SYS-248: 有前置任务且前置未完成 → 初始状态 locked
- if (dto.getPrerequisiteTaskId() != null) {
- Task prereq = taskMapper.selectById(dto.getPrerequisiteTaskId());
- if (prereq == null || !"completed".equals(prereq.getStatus())) {
- task.setStatus("locked");
- } else {
- task.setStatus("pending");
- }
- } else {
- task.setStatus("pending");
- }
- }
- task.setCreatedAt(new Date());
- task.setUpdatedAt(new Date());
- // 五维维度
- task.setDimensionCode(dto.getDimensionCode());
- task.setDimensionWeights(dto.getDimensionWeights());
- // B20: 会员任务标记透传(自建任务 memberOnly=0,不校验会员)
- task.setMemberOnly(dto.getMemberOnly() != null ? dto.getMemberOnly() : 0);
- // TASK-SYS-248: 行为调度中心扩展字段透传
- task.setPrerequisiteTaskId(dto.getPrerequisiteTaskId());
- task.setActionType(dto.getActionType());
- task.setActionConfig(dto.getActionConfig());
- task.setRequireInput(dto.getRequireInput() != null ? dto.getRequireInput() : 0);
- task.setStartRequired(dto.getStartRequired() != null ? dto.getStartRequired() : 0);
- task.setStartCount(0);
- task.setMinDurationSeconds(dto.getMinDurationSeconds() != null ? dto.getMinDurationSeconds() : 0);
- // TASK-SYS-248: 自动计算是否为流程任务
- // 有前置任务 或 有后置任务 → 标记为流程任务
- if (dto.getPrerequisiteTaskId() != null
- || (dto.getSuccessorTaskIds() != null && !dto.getSuccessorTaskIds().isEmpty())) {
- task.setIsFlowTask(1);
- } else {
- task.setIsFlowTask(0);
- }
- task.setSuccessorTaskIds(dto.getSuccessorTaskIds());
- task.setSourceType(dto.getSourceType() != null ? dto.getSourceType() : "self");
- task.setSourceId(dto.getSourceId());
- task.setIsDailyProgress(dto.getIsDailyProgress() != null ? dto.getIsDailyProgress() : 0);
- task.setTargetValue(dto.getTargetValue());
- // 进度型任务无 deadline(原 growth_task DAILY:每日重置,无截止时间)
- if (task.getIsDailyProgress() != null && task.getIsDailyProgress() == 1) {
- task.setDeadline(null);
- }
- return task;
- }
- private Long getCurrentMemberId(Long userId) {
- FamilyMember member = familyMemberMapper.selectOne(
- new LambdaQueryWrapper<FamilyMember>()
- .eq(FamilyMember::getUserId, userId)
- .last("LIMIT 1"));
- return member != null ? member.getId() : null;
- }
- /**
- * 实现接口:获取今日任务列表(按memberId,不过滤维度)
- */
- @Override
- public List<Task> getTodayTasks(Long memberId) {
- return getTodayTasks(memberId, null);
- }
- public List<Task> getTodayTasks(Long memberId, String dimensionCode) {
- Calendar calendar = Calendar.getInstance();
- calendar.set(Calendar.HOUR_OF_DAY, 0);
- calendar.set(Calendar.MINUTE, 0);
- calendar.set(Calendar.SECOND, 0);
- calendar.set(Calendar.MILLISECOND, 0);
- Date startOfDay = calendar.getTime();
- calendar.add(Calendar.DAY_OF_MONTH, 1);
- Date endOfDay = calendar.getTime();
- LambdaQueryWrapper<Task> wrapper = new LambdaQueryWrapper<Task>()
- .eq(Task::getFamilyMemberId, memberId)
- .in(Task::getStatus, Arrays.asList("pending", "completed", "locked", "pending_review"))
- .and(w -> w
- // 普通任务:按 deadline 判断今日
- .and(w2 -> w2.isNotNull(Task::getDeadline)
- .ge(Task::getDeadline, startOfDay)
- .lt(Task::getDeadline, endOfDay))
- // 进度型任务:按 createdAt 判断今日(deadline 为 NULL)
- .or(w3 -> w3.eq(Task::getIsDailyProgress, 1)
- .ge(Task::getCreatedAt, startOfDay)
- .lt(Task::getCreatedAt, endOfDay)))
- .orderByAsc(Task::getDeadline);
- if (dimensionCode != null && !dimensionCode.isEmpty() && DimensionCodes.isValid(dimensionCode)) {
- String dim = dimensionCode.trim();
- wrapper.and(w -> w
- .apply("COALESCE(JSON_EXTRACT(dimension_weights, CONCAT('$.', {0})), 0) > 0", dim)
- .or().eq(Task::getDimensionCode, dim)
- );
- }
- SortUtil.applySort(wrapper);
- List<Task> all = taskMapper.selectList(wrapper);
- // 过滤掉 latestEnd 已过期的任务(有执行时间窗口的任务:过了结束时间不再显示为待完成)
- Date now = new Date();
- all.removeIf(t -> t.getLatestEnd() != null && t.getLatestEnd().before(now) && "pending".equals(t.getStatus()));
- fillProgress(all, memberId);
- return filterLockedChildren(all);
- }
- /**
- * 家庭维度今日任务(本人端总览:方案分解任务可能挂在任意家庭成员上)
- */
- public List<Task> getTodayTasksByFamily(Long familyId) {
- if (familyId == null) {
- return new ArrayList<>();
- }
- Calendar calendar = Calendar.getInstance();
- calendar.set(Calendar.HOUR_OF_DAY, 0);
- calendar.set(Calendar.MINUTE, 0);
- calendar.set(Calendar.SECOND, 0);
- calendar.set(Calendar.MILLISECOND, 0);
- Date startOfDay = calendar.getTime();
- calendar.add(Calendar.DAY_OF_MONTH, 1);
- Date endOfDay = calendar.getTime();
- // 对齐 getTodayTasks 口径:包含 in_progress 状态 + isDailyProgress 任务
- List<Task> all = taskMapper.selectList(new LambdaQueryWrapper<Task>()
- .eq(Task::getFamilyId, familyId)
- .in(Task::getStatus, Arrays.asList("pending", "completed", "locked", "pending_review", "in_progress"))
- .and(w -> w
- .and(w2 -> w2.isNotNull(Task::getDeadline)
- .ge(Task::getDeadline, startOfDay)
- .lt(Task::getDeadline, endOfDay))
- .or(w3 -> w3.eq(Task::getIsDailyProgress, 1)
- .ge(Task::getCreatedAt, startOfDay)
- .lt(Task::getCreatedAt, endOfDay)))
- .orderByAsc(Task::getDeadline)
- .orderByAsc(Task::getId));
- return filterLockedChildren(all);
- }
- /**
- * 移除"父任务未完成"的子任务(购买任务→每日使用子任务链)。
- * 子任务在父购买任务完成(status=completed)前保持锁定,不显示在今日任务中。
- */
- private List<Task> filterLockedChildren(List<Task> tasks) {
- if (tasks == null || tasks.isEmpty()) return tasks;
- java.util.Set<Long> parentIds = new java.util.HashSet<>();
- for (Task t : tasks) {
- if (t.getParentTaskId() != null) parentIds.add(t.getParentTaskId());
- }
- if (parentIds.isEmpty()) return tasks;
- Map<Long, Task> parents = new HashMap<>();
- if (!parentIds.isEmpty()) {
- List<Task> plist = taskMapper.selectBatchIds(parentIds);
- for (Task p : plist) parents.put(p.getId(), p);
- }
- List<Task> result = new java.util.ArrayList<>();
- for (Task t : tasks) {
- if (t.getParentTaskId() != null) {
- Task parent = parents.get(t.getParentTaskId());
- if (parent == null || !"completed".equals(parent.getStatus())) {
- continue; // 父购买任务未完成,子任务锁定不显示
- }
- }
- result.add(t);
- }
- return result;
- }
- /**
- * 按维度分类获取今日任务(五维页面筛选用)
- */
- public List<Task> getTodayTasksByCategory(Long memberId, String category) {
- Calendar calendar = Calendar.getInstance();
- calendar.set(Calendar.HOUR_OF_DAY, 0);
- calendar.set(Calendar.MINUTE, 0);
- calendar.set(Calendar.SECOND, 0);
- calendar.set(Calendar.MILLISECOND, 0);
- Date startOfDay = calendar.getTime();
- calendar.add(Calendar.DAY_OF_MONTH, 1);
- Date endOfDay = calendar.getTime();
- return filterLockedChildren(taskMapper.selectList(new LambdaQueryWrapper<Task>()
- .eq(Task::getFamilyMemberId, memberId)
- .eq(Task::getCategory, category)
- .ge(Task::getDeadline, startOfDay)
- .lt(Task::getDeadline, endOfDay)
- .in(Task::getStatus, Arrays.asList("pending", "completed", "pending_review"))
- .orderByAsc(Task::getDeadline)));
- }
- /**
- * 获取家庭成员的任务统计数据
- */
- public Map<String, Object> getChildTaskStats(Long memberId) {
- Map<String, Object> stats = new HashMap<>();
- Date now = new Date();
- Calendar cal = Calendar.getInstance();
- cal.setTime(now);
- cal.set(Calendar.HOUR_OF_DAY, 0);
- cal.set(Calendar.MINUTE, 0);
- cal.set(Calendar.SECOND, 0);
- cal.set(Calendar.MILLISECOND, 0);
- Date todayStart = cal.getTime();
- cal.set(Calendar.HOUR_OF_DAY, 23);
- cal.set(Calendar.MINUTE, 59);
- cal.set(Calendar.SECOND, 59);
- cal.set(Calendar.MILLISECOND, 999);
- Date todayEnd = cal.getTime();
- List<Task> allTasks = taskMapper.selectList(
- new LambdaQueryWrapper<Task>()
- .eq(Task::getFamilyMemberId, memberId)
- .between(Task::getDeadline, todayStart, todayEnd)
- );
- long completed = allTasks.stream()
- .filter(t -> "completed".equals(t.getStatus()))
- .count();
- int completionRate = allTasks.isEmpty() ? 0 : (int) (completed * 100 / allTasks.size());
- List<Task> pendingReview = taskMapper.selectList(
- new LambdaQueryWrapper<Task>()
- .eq(Task::getFamilyMemberId, memberId)
- .eq(Task::getNeedReview, 1)
- .eq(Task::getStatus, "pending")
- );
- long habitCount = allTasks.stream()
- .filter(t -> "completed".equals(t.getStatus()))
- .filter(t -> "habit".equals(t.getCategory()))
- .count();
- stats.put("completionRate", completionRate);
- stats.put("pendingReviewCount", pendingReview.size());
- stats.put("habitCount", (int) habitCount);
- return stats;
- }
- /**
- * TASK-SYS-248: 开始任务(两阶段任务入口)
- * 校验前置/会员/频率后:pending→in_progress,创建 task_executions(started)
- * 返回 executionId + 任务信息(前端据此跳转联动页面)
- */
- @Transactional
- public Map<String, Object> startTask(Long taskId, Long memberId) {
- Task task = taskMapper.selectById(taskId);
- if (task == null || !Objects.equals(task.getFamilyMemberId(), memberId)) {
- throw new IllegalArgumentException("任务不存在或不属于该成员");
- }
- if ("completed".equals(task.getStatus())) {
- throw new IllegalArgumentException("任务已完成");
- }
- // 前置任务校验
- if (task.getPrerequisiteTaskId() != null) {
- Task prereq = taskMapper.selectById(task.getPrerequisiteTaskId());
- if (prereq == null || !"completed".equals(prereq.getStatus())) {
- throw new IllegalArgumentException("需先完成前置任务:" + (prereq != null ? prereq.getTitle() : "未知任务"));
- }
- }
- Date now = new Date();
- // 状态流转:locked→pending 也允许开始(前端已解锁);其它状态置 in_progress
- task.setStatus("in_progress");
- task.setStartCount((task.getStartCount() == null ? 0 : task.getStartCount()) + 1);
- task.setUpdatedAt(now);
- taskMapper.updateById(task);
- // 创建执行记录
- TaskExecution exec = new TaskExecution();
- exec.setTaskId(taskId);
- exec.setMemberId(memberId);
- exec.setStatus("started");
- exec.setStartedAt(now);
- exec.setCreatedAt(now);
- taskExecutionMapper.insert(exec);
- // TASK-SYS-248: 购物任务联动——cart/order 动作自动加购/下单
- try {
- handleShoppingAction(task, memberId, exec, now);
- } catch (Exception e) {
- log.warn("购物任务联动失败(不影响任务开始): taskId={}, error={}", taskId, e.getMessage());
- }
- Map<String, Object> result = new HashMap<>();
- result.put("executionId", exec.getId());
- result.put("taskId", taskId);
- result.put("actionType", task.getActionType());
- result.put("actionConfig", task.getActionConfig());
- return result;
- }
- @Transactional
- public Map<String, Object> completeTask(Long taskId, Long memberId, String photoUrl, String content, String contentType) {
- Task task = taskMapper.selectById(taskId);
- if (task == null) {
- return null; // 任务不存在
- }
- // ===== T24-A 幂等完成(生产 500 热修) =====
- // 背景:睡眠/运动/饮食/饮水打卡属"每日进度型任务",POST /api/health/*/create 落库后会
- // 按【任务归属】自动完成(updateCheckinTaskProgress → awardTaskCompletionRewards,
- // status 已真落库为 completed,见 TaskService:awardTaskCompletionRewards)。
- // 前端历史版本在 create 成功后还会冗余调用一次 POST /api/tasks/{id}/complete,
- // 且传入的是"操作视角成员" M_front。
- // 双身份(family_members 同 (user_id,family_id) 存在多条"本人"行)下
- // M_front ≠ task.familyMemberId → 命中下方旧 587 行;
- // 无双身份时任务已是 completed → 命中旧 591 行。
- // 两种触发旧代码都 return null → TaskController:139 吐 500
- // ("任务不存在、无权完成或状态已变更"),但用户其实已打卡成功、首页卡片已消失。
- // 处理:这两种情况一律幂等返回成功(复用既有 result 结构),不改数据、不重复发奖。
- if ("completed".equals(task.getStatus()) || "pending_review".equals(task.getStatus())) {
- // 已完成 / 已提交待审核:幂等成功(pointsEarned=0,不重复发奖)
- return buildIdempotentCompleteResult(task, memberId);
- }
- if (!Objects.equals(task.getFamilyMemberId(), memberId)) {
- // 成员不匹配:可能是双身份下的解析漂移,也可能是真正的越权。
- // 此处不修改任何数据、不发奖,仅返回幂等成功,避免打卡回写竞态把用户卡在 500。
- log.warn("completeTask 成员不匹配,按幂等成功返回(疑似双身份解析漂移): taskId={}, taskMemberId={}, reqMemberId={}",
- taskId, task.getFamilyMemberId(), memberId);
- return buildIdempotentCompleteResult(task, memberId);
- }
- // TASK-SYS-248: 前置任务校验——前置未完成则拒绝
- if (task.getPrerequisiteTaskId() != null) {
- Task prereq = taskMapper.selectById(task.getPrerequisiteTaskId());
- if (prereq == null || !"completed".equals(prereq.getStatus())) {
- throw new IllegalArgumentException("需先完成前置任务:" + (prereq != null ? prereq.getTitle() : "未知任务"));
- }
- }
- // TASK-SYS-248: 填写式完成强校验——require_input=1 必须带内容
- if (task.getRequireInput() != null && task.getRequireInput() == 1) {
- boolean hasContent = (content != null && !content.trim().isEmpty()) || (photoUrl != null && !photoUrl.isEmpty());
- if (!hasContent) {
- throw new IllegalArgumentException("该任务需填写内容后才能完成");
- }
- }
- // TASK-SYS-248: 两阶段任务校验——start_required=1 必须已开始(存在 in_progress 的 execution)
- if (task.getStartRequired() != null && task.getStartRequired() == 1) {
- Long executionCount = taskExecutionMapper.selectCount(
- new LambdaQueryWrapper<TaskExecution>()
- .eq(TaskExecution::getTaskId, taskId)
- .eq(TaskExecution::getMemberId, memberId)
- .eq(TaskExecution::getStatus, "started"));
- if (executionCount == null || executionCount == 0) {
- throw new IllegalArgumentException("该任务需先开始才能完成");
- }
- // 最短持续时长校验
- if (task.getMinDurationSeconds() != null && task.getMinDurationSeconds() > 0) {
- TaskExecution exec = taskExecutionMapper.selectOne(
- new LambdaQueryWrapper<TaskExecution>()
- .eq(TaskExecution::getTaskId, taskId)
- .eq(TaskExecution::getMemberId, memberId)
- .eq(TaskExecution::getStatus, "started")
- .last("LIMIT 1"));
- if (exec != null && exec.getStartedAt() != null) {
- long elapsed = (System.currentTimeMillis() - exec.getStartedAt().getTime()) / 1000;
- if (elapsed < task.getMinDurationSeconds()) {
- throw new IllegalArgumentException("该任务需持续至少 " + task.getMinDurationSeconds() + " 秒后才能完成");
- }
- }
- }
- }
- Date now = new Date();
- // 检查是否为小游戏任务
- if ("小游戏类".equals(task.getCategory()) && task.getMinigameCode() != null) {
- return null;
- }
- // 需审核的任务:提交后进入待审核,审核通过后再发放积分
- if (task.getNeedReview() != null && task.getNeedReview() == 1) {
- return submitTaskForReview(task, memberId, now, content, contentType);
- }
- recordTaskExecution(task, memberId, now, content, contentType);
- int pointsEarned = awardTaskCompletionRewards(task, memberId, now);
- FamilyMember child = familyMemberMapper.selectById(memberId);
- Map<String, Object> result = new HashMap<>();
- result.put("pointsEarned", pointsEarned);
- result.put("newBalance", child != null ? child.getTotalPoints() : 0);
- result.put("needReview", false);
- try {
- com.etotem.cfc.entity.TaskReminderConfig cfg = taskReminderConfigService.loadAllConfigs().get(task.getExecutorId());
- if (cfg != null && Integer.valueOf(1).equals(cfg.getCompletionPraiseEnabled())) {
- java.util.HashMap<String, Object> vars = new java.util.HashMap<String, Object>();
- vars.put("title", task.getTitle());
- String chans = cfg.getCompletionPraiseChannels();
- if (chans == null || chans.isEmpty()) chans = "wechat,in_app";
- notificationDispatchService.sendByTemplate("task_completion_praise", task.getExecutorId(), vars, "task", task.getId(),
- "任务完成,太棒了!",
- "太棒了!任务「" + task.getTitle() + "」已完成,继续保持,加油~");
- }
- } catch (Exception e) {}
- return result;
- }
- /**
- * T24-A:幂等完成结果(复用 completeTask 既定 result 结构)。
- * <p>用于"任务已完成/已提交待审核"或"成员不匹配(双身份解析漂移)"两种触发,
- * 让重复调用不再返回 null → TaskController:139 的 500;不修改任务、不发奖。</p>
- *
- * @param task 已存在的任务
- * @param memberId 请求方传入的成员(为空时退化为任务归属成员)
- */
- private Map<String, Object> buildIdempotentCompleteResult(Task task, Long memberId) {
- Long balanceMemberId = memberId != null ? memberId : task.getFamilyMemberId();
- FamilyMember child = balanceMemberId != null ? familyMemberMapper.selectById(balanceMemberId) : null;
- Map<String, Object> result = new HashMap<>();
- result.put("pointsEarned", 0);
- result.put("newBalance", child != null && child.getTotalPoints() != null ? child.getTotalPoints() : 0);
- // 已完成 → false(与既有成功结构一致);已提交待审核 → true(保持前端"待审核"提示)
- result.put("needReview", "pending_review".equals(task.getStatus()));
- result.put("alreadyCompleted", true);
- return result;
- }
- @Transactional
- public boolean reviewTask(Long taskId, TaskReviewDTO dto) {
- Task task = taskMapper.selectById(taskId);
- if (task == null) {
- return false;
- }
- if (!"pending_review".equals(task.getStatus())) {
- return "completed".equals(task.getStatus());
- }
- boolean isApproved;
- if (dto.getApproved() != null) {
- isApproved = dto.getApproved();
- } else if ("pass".equals(dto.getResult())) {
- isApproved = true;
- } else {
- isApproved = false;
- }
- if (isApproved) {
- Date completionTime = task.getCompletedAt() != null ? task.getCompletedAt() : new Date();
- awardTaskCompletionRewards(task, task.getFamilyMemberId(), completionTime);
- } else {
- task.setStatus("pending");
- task.setCompletedAt(null);
- task.setUpdatedAt(new Date());
- taskMapper.updateById(task);
- }
- return true;
- }
- public Page<Task> getTaskHistory(Long memberId, Integer page, Integer size) {
- Page<Task> pageParam = new Page<>(page, size);
- return taskMapper.selectPage(pageParam, new LambdaQueryWrapper<Task>()
- .eq(Task::getFamilyMemberId, memberId)
- .orderByDesc(Task::getCreatedAt));
- }
- public Page<Task> getTaskHistoryByCategory(Long memberId, Integer page, Integer size, String category) {
- Page<Task> pageParam = new Page<>(page, size);
- return taskMapper.selectPage(pageParam, new LambdaQueryWrapper<Task>()
- .eq(Task::getFamilyMemberId, memberId)
- .eq(Task::getCategory, category)
- .orderByDesc(Task::getCreatedAt));
- }
- public boolean deleteTask(Long taskId, Long userId) {
- Task task = taskMapper.selectById(taskId);
- if (task == null || !task.getCreatorId().equals(userId)) {
- return false;
- }
- task.setStatus("cancelled");
- task.setUpdatedAt(new Date());
- taskMapper.updateById(task);
- return true;
- }
- /**
- * 获取待审核任务列表
- * 家庭主账号查看需要审核的任务
- */
- public List<Task> getPendingReviewTasks(Long userId) {
- // 获取用户信息
- User user = userService.getUserInfo(userId);
- if (user == null || !"parent".equals(user.getRole())) {
- return new ArrayList<>();
- }
- // 查询该家庭下所有需要审核且已完成的任务
- LambdaQueryWrapper<Task> wrapper = new LambdaQueryWrapper<>();
- wrapper.eq(Task::getFamilyId, user.getFamilyId())
- .eq(Task::getNeedReview, 1)
- .eq(Task::getStatus, "pending_review")
- .orderByDesc(Task::getCompletedAt);
- SortUtil.applySort(wrapper);
- return taskMapper.selectList(wrapper);
- }
- /**
- * 获取家庭主账号今日任务 (家庭主账号作为执行者的任务)
- */
- public List<Task> getTodayParentTasks(Long userId) {
- Calendar calendar = Calendar.getInstance();
- calendar.set(Calendar.HOUR_OF_DAY, 0);
- calendar.set(Calendar.MINUTE, 0);
- calendar.set(Calendar.SECOND, 0);
- calendar.set(Calendar.MILLISECOND, 0);
- Date startOfDay = calendar.getTime();
- calendar.add(Calendar.DAY_OF_MONTH, 1);
- Date endOfDay = calendar.getTime();
- User user = userService.getUserInfo(userId);
- if (user == null) {
- return new ArrayList<>();
- }
- return taskMapper.selectList(new LambdaQueryWrapper<Task>()
- .eq(Task::getFamilyId, user.getFamilyId())
- .eq(Task::getExecutorType, "parent")
- .eq(Task::getExecutorId, userId)
- .ge(Task::getDeadline, startOfDay)
- .lt(Task::getDeadline, endOfDay)
- .in(Task::getStatus, Arrays.asList("pending", "completed"))
- .orderByAsc(Task::getDeadline));
- }
- /**
- * 家庭主账号完成任务 (家庭主账号作为执行者完成任务)
- */
- @Transactional
- public Map<String, Object> completeParentTask(Long taskId, Long userId) {
- Task task = taskMapper.selectById(taskId);
- if (task == null || !("parent".equals(task.getExecutorType()) && task.getExecutorId().equals(userId))) {
- return null;
- }
- if ("completed".equals(task.getStatus())) {
- return null;
- }
- User user = userMapper.selectById(userId);
- if (user == null) {
- return null;
- }
- Date now = new Date();
- int pointsEarned = task.getPoints();
- boolean isEarly = now.before(task.getDeadline());
- // 提前完成获得奖励分
- if (isEarly) {
- pointsEarned += EARLY_BONUS;
- }
- // 更新任务状态
- task.setStatus("completed");
- task.setCompletedAt(now);
- task.setUpdatedAt(new Date());
- taskMapper.updateById(task);
- // 仅编排节点任务生效:通知编排引擎流程评估(非编排节点任务零开销)
- try {
- orchestrationEngine.onTaskCompleted(taskId);
- } catch (Exception e) {
- log.warn("orchestration onTaskCompleted failed: {}", e.getMessage());
- }
- // 更新家庭主账号积分
- int newPoints = (user.getTotalPoints() != null ? user.getTotalPoints() : 0) + pointsEarned;
- user.setTotalPoints(newPoints);
- user.setUpdatedAt(new Date());
- userMapper.updateById(user);
- // 记录积分流水
- PointsLog log = new PointsLog();
- log.setMemberId(task.getFamilyMemberId());
- log.setAmount(pointsEarned);
- log.setType(isEarly ? "earn" : "earn");
- log.setDescription("完成任务: " + task.getTitle());
- log.setCreatedAt(new Date());
- pointsLogMapper.insert(log);
- Map<String, Object> result = new HashMap<>();
- result.put("pointsEarned", pointsEarned);
- 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 */ }
- result.put("newBalance", user.getTotalPoints());
- result.put("needReview", task.getNeedReview() == 1);
- try {
- Long receiver = userId;
- com.etotem.cfc.entity.TaskReminderConfig cfg = taskReminderConfigService.loadAllConfigs().get(receiver);
- if (cfg != null && Integer.valueOf(1).equals(cfg.getCompletionPraiseEnabled())) {
- java.util.HashMap<String, Object> vars = new java.util.HashMap<String, Object>();
- vars.put("title", task.getTitle());
- notificationDispatchService.sendByTemplate("task_completion_praise", receiver, vars, "task", task.getId(),
- "任务完成,太棒了!",
- "太棒了!任务「" + task.getTitle() + "」已完成,继续保持,加油~");
- }
- } catch (Exception e) {}
- return result;
- }
- public void batchComplete(List<Long> taskIds, Long userId) {
- taskMapper.update(null, new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<Task>()
- .in(Task::getId, taskIds)
- .and(w -> w.eq(Task::getCreatorId, userId).or().eq(Task::getExecutorId, userId))
- .set(Task::getStatus, "completed")
- .set(Task::getCompletedAt, new Date()));
- }
- public void batchDelete(List<Long> taskIds, Long userId) {
- taskMapper.update(null, new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<Task>()
- .in(Task::getId, taskIds)
- .eq(Task::getCreatorId, userId)
- .set(Task::getStatus, "cancelled")
- .set(Task::getUpdatedAt, new Date()));
- }
- private Map<String, Object> submitTaskForReview(Task task, Long memberId, Date now, String content, String contentType) {
- task.setStatus("pending_review");
- task.setCompletedAt(now);
- task.setUpdatedAt(now);
- taskMapper.updateById(task);
- recordTaskExecution(task, memberId, now, content, contentType);
- FamilyMember child = familyMemberMapper.selectById(memberId);
- Map<String, Object> result = new HashMap<>();
- result.put("pointsEarned", 0);
- result.put("newBalance", child != null ? child.getTotalPoints() : 0);
- result.put("needReview", true);
- result.put("message", "已提交,待审核");
- return result;
- }
- private int awardTaskCompletionRewards(Task task, Long memberId, Date completionTime) {
- Long taskId = task.getId();
- int pointsEarned = task.getPoints() != null ? task.getPoints() : 0;
- boolean isEarly = task.getDeadline() != null && completionTime.before(task.getDeadline());
- boolean isLateGrace = false;
- if (!isEarly && task.getDeadline() != null) {
- long diffMinutes = (completionTime.getTime() - task.getDeadline().getTime()) / (1000 * 60);
- isLateGrace = diffMinutes <= LATE_GRACE_MINUTES;
- }
- FamilyMember child = familyMemberMapper.selectById(memberId);
- if (child == null) {
- return 0;
- }
- String bonusRuleJson = task.getBonusRule();
- if (bonusRuleJson != null && !bonusRuleJson.isEmpty()) {
- pointsEarned = calculatePointsWithBonusRule(task, completionTime, isEarly, isLateGrace, child);
- } else if (isEarly || isLateGrace) {
- pointsEarned += EARLY_BONUS;
- } else {
- pointsEarned = calculateLatePenalty(task, child, memberId);
- }
- task.setStatus("completed");
- task.setCompletedAt(completionTime);
- task.setUpdatedAt(new Date());
- taskMapper.updateById(task);
- child.setTotalPoints(child.getTotalPoints() + pointsEarned);
- Date lastTaskDate = child.getLastTaskDate();
- Date today = new Date();
- Calendar cal = Calendar.getInstance();
- cal.add(Calendar.DAY_OF_MONTH, -1);
- Date yesterday = cal.getTime();
- if (lastTaskDate == null || lastTaskDate.before(yesterday)) {
- child.setStreakDays(1);
- } else if (!isSameDay(lastTaskDate, today)) {
- child.setStreakDays(child.getStreakDays() + 1);
- }
- child.setLastTaskDate(today);
- child.setUpdatedAt(new Date());
- familyMemberMapper.updateById(child);
- int milestoneReward = streakService.checkAndAwardMilestone(memberId, child.getStreakDays());
- if (milestoneReward > 0) {
- pointsEarned += milestoneReward;
- log.info("家庭成员{}获得打卡里程碑奖励{}积分", memberId, milestoneReward);
- }
- PointsLog pointsLog = new PointsLog();
- pointsLog.setMemberId(memberId);
- pointsLog.setTaskId(taskId);
- pointsLog.setAmount(pointsEarned);
- if (pointsEarned > 0) {
- pointsLog.setType(isEarly ? "bonus" : "earn");
- } else if (pointsEarned < 0) {
- pointsLog.setType("penalty");
- } else {
- pointsLog.setType("earn");
- }
- pointsLog.setDescription("完成任务: " + task.getTitle());
- pointsLog.setCreatedAt(new Date());
- if (pointsEarned > 0 && task.getPointsExpireDays() != null && task.getPointsExpireDays() > 0) {
- Calendar expireCal = Calendar.getInstance();
- expireCal.add(Calendar.DAY_OF_MONTH, task.getPointsExpireDays());
- pointsLog.setExpireAt(expireCal.getTime());
- }
- pointsLogMapper.insert(pointsLog);
- try {
- int energyAmount = Math.abs(pointsEarned);
- if (energyAmount > 0) {
- energyService.awardEnergy(memberId, "task", taskId, energyAmount,
- "完成任务: " + task.getTitle(), null);
- }
- } catch (Exception e) {
- log.warn("能量发放失败(不影响积分): memberId={}, taskId={}, error={}",
- memberId, taskId, e.getMessage());
- }
- try {
- if (child.getUserId() != null) {
- this.updateProgress(child.getUserId(), child.getId(), "DAILY_TASK", 1);
- }
- } catch (Exception e) {
- log.warn("成长任务进度更新失败: memberId={}, error={}", memberId, e.getMessage());
- }
- // D5: 打卡完成发放个人 CF 值(F22 统一入口)。
- // earnByRule 按 (member_id, task_complete, task_id) 幂等去重,审核重复触发不会双记。
- // CF 额度取任务配置(当前 tasks.points),不含连胜里程碑加成与迟到惩罚,避免奖励被放大/倒挂。
- // 家庭池不再承接 CF 值:原个人 + 家庭池双发已收敛为个人成员账本单发。
- try {
- int cfAmount = task.getPoints() != null && task.getPoints() > 0 ? task.getPoints() : 0;
- // 注册/绑定推荐人为引流类任务,不发可提现的CF值(积分照发,防止刷CF)
- if (cfAmount > 0 && ("REGISTER".equals(task.getTitle()) || "BIND_REFERRAL".equals(task.getTitle()))) {
- cfAmount = 0;
- }
- if (cfAmount > 0) {
- cfPathwayEarnService.earnTaskCompletionCf(memberId, taskId, cfAmount, task.getTitle());
- }
- } catch (Exception e) {
- log.warn("CF值发放失败(不影响积分): taskId={}, error={}", taskId, e.getMessage());
- }
- unlockPrerequisiteTasks(taskId);
- return pointsEarned;
- }
- private void recordTaskExecution(Task task, Long memberId, Date now, String content, String contentType) {
- Long taskId = task.getId();
- try {
- TaskExecution activeExec = null;
- if (task.getStartRequired() != null && task.getStartRequired() == 1) {
- activeExec = taskExecutionMapper.selectOne(
- new LambdaQueryWrapper<TaskExecution>()
- .eq(TaskExecution::getTaskId, taskId)
- .eq(TaskExecution::getMemberId, memberId)
- .eq(TaskExecution::getStatus, "started")
- .last("LIMIT 1"));
- }
- if (activeExec != null) {
- activeExec.setStatus("finished");
- activeExec.setFinishedAt(now);
- activeExec.setContent(content);
- activeExec.setContentType(contentType);
- taskExecutionMapper.updateById(activeExec);
- } else if (content != null && !content.trim().isEmpty()) {
- TaskExecution exec = new TaskExecution();
- exec.setTaskId(taskId);
- exec.setMemberId(memberId);
- exec.setStatus("finished");
- exec.setStartedAt(now);
- exec.setFinishedAt(now);
- exec.setContent(content);
- exec.setContentType(contentType);
- exec.setCreatedAt(now);
- taskExecutionMapper.insert(exec);
- }
- } catch (Exception e) {
- log.warn("任务执行进度记录失败(不影响完成): taskId={}, error={}", taskId, e.getMessage());
- }
- }
- private void unlockPrerequisiteTasks(Long taskId) {
- try {
- List<Task> nextTasks = taskMapper.selectList(
- new LambdaQueryWrapper<Task>()
- .eq(Task::getPrerequisiteTaskId, taskId));
- for (Task next : nextTasks) {
- if ("locked".equals(next.getStatus())) {
- next.setStatus("pending");
- next.setUpdatedAt(new Date());
- taskMapper.updateById(next);
- log.info("前置任务{}完成,解锁后置任务{}", taskId, next.getId());
- }
- }
- } catch (Exception e) {
- log.warn("后置任务解锁失败: taskId={}, error={}", taskId, e.getMessage());
- }
- }
- /**
- * 管理员/平台查询待审核任务
- */
- public List<Task> getAllPendingReviewTasks(Long familyId) {
- LambdaQueryWrapper<Task> wrapper = new LambdaQueryWrapper<Task>()
- .eq(Task::getNeedReview, 1)
- .eq(Task::getStatus, "pending_review")
- .orderByDesc(Task::getCompletedAt);
- if (familyId != null) {
- wrapper.eq(Task::getFamilyId, familyId);
- }
- SortUtil.applySort(wrapper);
- return taskMapper.selectList(wrapper);
- }
- private boolean isSameDay(Date date1, Date date2) {
- Calendar cal1 = Calendar.getInstance();
- cal1.setTime(date1);
- Calendar cal2 = Calendar.getInstance();
- cal2.setTime(date2);
- return cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR)
- && cal1.get(Calendar.DAY_OF_YEAR) == cal2.get(Calendar.DAY_OF_YEAR);
- }
- /**
- * 获取家庭成员的待审核任务
- */
- public List<Task> getPendingReviewTasksForChild(Long memberId) {
- return taskMapper.selectList(new LambdaQueryWrapper<Task>()
- .eq(Task::getFamilyMemberId, memberId)
- .eq(Task::getNeedReview, 1)
- .eq(Task::getStatus, "pending_review")
- .orderByDesc(Task::getCompletedAt));
- }
- /**
- * 获取家庭成员的所有任务
- */
- public List<Task> getAllTasksByChild(Long memberId) {
- return taskMapper.selectList(new LambdaQueryWrapper<Task>()
- .eq(Task::getFamilyMemberId, memberId)
- .orderByDesc(Task::getCreatedAt));
- }
- /**
- * 获取家庭成员的任务历史(分页)
- */
- public Page<Task> getTaskHistoryByChild(Long memberId, Integer page, Integer size, String status) {
- Page<Task> pageParam = new Page<>(page, size);
- LambdaQueryWrapper<Task> wrapper = new LambdaQueryWrapper<Task>()
- .eq(Task::getFamilyMemberId, memberId)
- .orderByDesc(Task::getCreatedAt);
-
- if (status != null && !status.isEmpty()) {
- wrapper.eq(Task::getStatus, status);
- }
- SortUtil.applySort(wrapper);
- return taskMapper.selectPage(pageParam, wrapper);
- }
- /**
- * POINT-002: 根据自定义规则计算超额积分
- * bonusRule JSON格式: {
- * "earlyBonus": 1, // 提前完成额外积分
- * "earlyMinutes": 30, // 提前多少分钟才算提前(可选)
- * "qualityBonus": 2, // 超质完成额外积分(家庭主账号评分)
- * "qualityThreshold": 90 // 质量评分阈值(可选)
- * }
- */
- private int calculatePointsWithBonusRule(Task task, Date completedAt, boolean isEarly, boolean isLateGrace, FamilyMember child) {
- int points = task.getPoints();
- try {
- // 解析JSON规则
- String json = task.getBonusRule();
- if (json == null || json.isEmpty()) {
- return isEarly || isLateGrace ? points + EARLY_BONUS : calculateLatePenalty(task, child, child.getId());
- }
- // 简单JSON解析(避免引入额外依赖)
- Integer earlyBonus = extractJsonValue(json, "earlyBonus");
- Integer earlyMinutes = extractJsonValue(json, "earlyMinutes");
- Integer qualityBonus = extractJsonValue(json, "qualityBonus");
- // 提前完成奖励
- if (isEarly || isLateGrace) {
- if (earlyBonus != null) {
- points += earlyBonus;
- } else {
- points += EARLY_BONUS; // 默认+1
- }
- // 如果设置了提前分钟数,检查是否满足条件
- if (earlyMinutes != null && task.getDeadline() != null) {
- long diffMinutes = (task.getDeadline().getTime() - completedAt.getTime()) / (1000 * 60);
- if (diffMinutes < earlyMinutes) {
- // 不满足提前分钟数,只给基础分
- points = task.getPoints();
- }
- }
- } else {
- // 超时,计算扣分
- points = calculateLatePenalty(task, child, child.getId());
- }
- // 超质完成奖励(需要家庭主账号后续评分,这里先不处理)
- // qualityBonus 会在家庭主账号审核时根据评分添加
- } catch (Exception e) {
- log.warn("解析超额积分规则失败: {}", e.getMessage());
- // 解析失败,使用默认规则
- if (isEarly || isLateGrace) {
- points += EARLY_BONUS;
- } else {
- points = calculateLatePenalty(task, child, child.getId());
- }
- }
- return points;
- }
- /**
- * 从简单JSON字符串中提取整数值
- */
- private Integer extractJsonValue(String json, String key) {
- try {
- String pattern = "\"" + key + "\"\\s*:\\s*(\\d+)";
- java.util.regex.Pattern p = java.util.regex.Pattern.compile(pattern);
- java.util.regex.Matcher m = p.matcher(json);
- if (m.find()) {
- return Integer.parseInt(m.group(1));
- }
- } catch (Exception e) {
- log.warn("提取JSON值失败: key={}", key);
- }
- return null;
- }
- /**
- * 计算超时扣分
- */
- private int calculateLatePenalty(Task task, FamilyMember child, Long memberId) {
- if (child == null) {
- return 0;
- }
- int points = task.getPoints() != null ? task.getPoints() : 0;
- // 无 deadline 的每日进度型任务(饮食/睡眠/运动打卡等)不存在"迟到"概念,发放基础分
- if (task.getDeadline() == null) {
- return points;
- }
- if (child.getPenaltyEnabled() != null && child.getPenaltyEnabled() == 1) {
- // 检查今日已扣分数
- Calendar cal = Calendar.getInstance();
- cal.set(Calendar.HOUR_OF_DAY, 0);
- cal.set(Calendar.MINUTE, 0);
- cal.set(Calendar.SECOND, 0);
- Date startOfDay = cal.getTime();
- Integer todayPenalty = pointsLogMapper.selectSum(
- new LambdaQueryWrapper<PointsLog>()
- .eq(PointsLog::getMemberId, memberId)
- .eq(PointsLog::getType, "penalty")
- .ge(PointsLog::getCreatedAt, startOfDay)
- );
- int todayPenaltyTotal = todayPenalty != null ? Math.abs(todayPenalty) : 0;
- if (todayPenaltyTotal < PENALTY_MAX_DAILY) {
- int penalty = Math.min(task.getPoints(), PENALTY_MAX_DAILY - todayPenaltyTotal);
- return -penalty;
- } else {
- return points; // 达到每日扣分上限:不扣分,任务完成仍发放基础分
- }
- } else {
- return points; // 扣分未开启:任务完成发放基础分
- }
- }
- /**
- * 完成小游戏任务
- * @param taskId 任务ID
- * @param memberId 家庭成员ID
- * @param completionTime 完成用时(秒)
- * @param score 得分(可选)
- * @return 积分信息
- */
- @Transactional
- public Map<String, Object> completeMinigameTask(Long taskId, Long memberId, Integer completionTime, Integer score) {
- Task task = taskMapper.selectById(taskId);
- if (task == null || !Objects.equals(task.getFamilyMemberId(), memberId)) {
- throw new RuntimeException("任务不存在或不属于该成员");
- }
- if ("completed".equals(task.getStatus())) {
- throw new RuntimeException("任务已完成");
- }
- if (!"小游戏类".equals(task.getCategory()) || task.getMinigameCode() == null) {
- throw new RuntimeException("该任务不是小游戏任务");
- }
- // 调用MiniGameService计算积分
- Map<String, Object> gameResult = miniGameService.completeGame(memberId, task.getMinigameCode(), completionTime, score);
- int pointsEarned = (Integer) gameResult.get("pointsEarned");
- // 更新任务状态
- task.setStatus("completed");
- task.setCompletedAt(new Date());
- task.setUpdatedAt(new Date());
- taskMapper.updateById(task);
- // 更新连续打卡
- FamilyMember child = familyMemberMapper.selectById(memberId);
- Date lastTaskDate = child.getLastTaskDate();
- Date today = new Date();
- Calendar cal = Calendar.getInstance();
- cal.add(Calendar.DAY_OF_MONTH, -1);
- Date yesterday = cal.getTime();
- if (lastTaskDate == null || lastTaskDate.before(yesterday)) {
- child.setStreakDays(1); // 重新开始
- } else if (lastTaskDate != null && isSameDay(lastTaskDate, today)) {
- // 今天已经完成过任务,不增加天数
- } else {
- child.setStreakDays(child.getStreakDays() + 1);
- }
- child.setLastTaskDate(today);
- child.setUpdatedAt(new Date());
- familyMemberMapper.updateById(child);
- // 检查并发放打卡里程碑奖励
- int milestoneReward = streakService.checkAndAwardMilestone(memberId, child.getStreakDays());
- if (milestoneReward > 0) {
- pointsEarned += milestoneReward;
- log.info("家庭成员{}获得打卡里程碑奖励{}积分", memberId, milestoneReward);
- }
- Map<String, Object> result = new HashMap<>();
- result.put("pointsEarned", pointsEarned);
- result.put("newBalance", child.getTotalPoints());
- result.put("gameName", gameResult.get("gameName"));
- result.put("needReview", task.getNeedReview() == 1);
- return result;
- }
- /**
- * 获取家庭待办任务
- */
- public List<Task> getPendingTasksByFamily(Long familyId) {
- return taskMapper.selectList(new LambdaQueryWrapper<Task>()
- .eq(Task::getFamilyId, familyId)
- .in(Task::getStatus, Arrays.asList("pending", "in_progress"))
- .orderByAsc(Task::getDeadline));
- }
- // ==================== Family Member Task queries ====================
- /**
- * 获取指定家庭成员待审核的任务列表
- */
- public List<Task> getPendingReviewTasksForFamilyMember(Long familyMemberId) {
- return taskMapper.selectList(new LambdaQueryWrapper<Task>()
- .eq(Task::getFamilyMemberId, familyMemberId)
- .eq(Task::getNeedReview, 1)
- .eq(Task::getStatus, "pending_review")
- .orderByDesc(Task::getCompletedAt));
- }
- /**
- * 获取指定家庭成员的全部任务
- */
- public List<Task> getAllTasksByFamilyMember(Long familyMemberId) {
- return taskMapper.selectList(new LambdaQueryWrapper<Task>()
- .eq(Task::getFamilyMemberId, familyMemberId)
- .orderByDesc(Task::getCreatedAt));
- }
- /**
- * 分页获取指定家庭成员的任务历史
- */
- public Page<Task> getTaskHistoryByFamilyMember(Long familyMemberId, Integer page, Integer size, String status) {
- LambdaQueryWrapper<Task> wrapper = new LambdaQueryWrapper<Task>()
- .eq(Task::getFamilyMemberId, familyMemberId);
- if (status != null && !status.isEmpty()) {
- wrapper.eq(Task::getStatus, status);
- }
- wrapper.orderByDesc(Task::getCreatedAt);
- SortUtil.applySort(wrapper);
- return taskMapper.selectPage(new Page<>(page, size), wrapper);
- }
- /**
- * 获取当前用户收到的待接受任务列表(executor_type='member' AND status='assigned')
- */
- public List<Task> getMyPendingTasks(Long memberId) {
- Calendar calendar = Calendar.getInstance();
- calendar.set(Calendar.HOUR_OF_DAY, 0);
- calendar.set(Calendar.MINUTE, 0);
- calendar.set(Calendar.SECOND, 0);
- calendar.set(Calendar.MILLISECOND, 0);
- Date startOfDay = calendar.getTime();
- calendar.add(Calendar.DAY_OF_MONTH, 1);
- Date endOfDay = calendar.getTime();
- return taskMapper.selectList(new LambdaQueryWrapper<Task>()
- .eq(Task::getExecutorType, "member")
- .eq(Task::getExecutorId, memberId)
- .eq(Task::getStatus, "assigned")
- .ge(Task::getDeadline, startOfDay)
- .lt(Task::getDeadline, endOfDay)
- .or()
- .and(w -> w.eq(Task::getExecutorType, "member")
- .eq(Task::getExecutorId, memberId)
- .eq(Task::getStatus, "assigned")
- .isNull(Task::getDeadline))
- .orderByAsc(Task::getDeadline));
- }
- /**
- * 接受任务:status 从 assigned 改为 in_progress
- */
- @Transactional
- public boolean acceptTask(Long taskId, Long memberId) {
- Task task = taskMapper.selectById(taskId);
- if (task == null || !task.getExecutorId().equals(memberId) || !"assigned".equals(task.getStatus())) {
- return false;
- }
- task.setStatus("in_progress");
- task.setAcceptDeadline(null);
- task.setUpdatedAt(new Date());
- taskMapper.updateById(task);
- return true;
- }
- /**
- * 拒绝任务:status 改为 rejected,调整 creator→assignee 和 assignee→creator 的关系分数
- */
- @Transactional
- public boolean rejectTask(Long taskId, Long memberId) {
- Task task = taskMapper.selectById(taskId);
- if (task == null || !task.getExecutorId().equals(memberId) || !"assigned".equals(task.getStatus())) {
- return false;
- }
- task.setStatus("rejected");
- task.setUpdatedAt(new Date());
- taskMapper.updateById(task);
- Long creatorId = task.getCreatorId();
- Long assigneeId = memberId;
- Long familyId = task.getFamilyId();
- if (creatorId == null || assigneeId == null || familyId == null) return true;
- int trustDelta = -1;
- int intimacyDelta = -1;
- int commDelta = -1;
- adjustRelationshipScore(familyId, creatorId, assigneeId, trustDelta, intimacyDelta, commDelta);
- adjustRelationshipScore(familyId, assigneeId, creatorId, 0, 0, 1);
- return true;
- }
- private void adjustRelationshipScore(Long familyId, Long fromId, Long toId, int trustDelta, int intimacyDelta, int commDelta) {
- FamilyRelationshipScore score = familyRelationshipScoreMapper.selectOne(
- new LambdaQueryWrapper<FamilyRelationshipScore>()
- .eq(FamilyRelationshipScore::getFamilyId, familyId)
- .eq(FamilyRelationshipScore::getFromMemberId, fromId)
- .eq(FamilyRelationshipScore::getToMemberId, toId));
- if (score == null) {
- FamilyMember from = familyMemberMapper.selectById(fromId);
- score = new FamilyRelationshipScore();
- score.setFamilyId(familyId);
- score.setFromMemberId(fromId);
- score.setToMemberId(toId);
- score.setTrustScore(from != null && from.getTrustScore() != null ? from.getTrustScore() : new java.math.BigDecimal("60.00"));
- score.setIntimacyScore(from != null && from.getIntimacyScore() != null ? from.getIntimacyScore() : new java.math.BigDecimal("60.00"));
- score.setCommunicationScore(from != null && from.getCommunicationScore() != null ? from.getCommunicationScore() : new java.math.BigDecimal("60.00"));
- try {
- familyRelationshipScoreMapper.insert(score);
- } catch (Exception e) {
- score = familyRelationshipScoreMapper.selectOne(
- new LambdaQueryWrapper<FamilyRelationshipScore>()
- .eq(FamilyRelationshipScore::getFamilyId, familyId)
- .eq(FamilyRelationshipScore::getFromMemberId, fromId)
- .eq(FamilyRelationshipScore::getToMemberId, toId));
- }
- }
- if (score == null) return;
- if (trustDelta != 0) score.setTrustScore(score.getTrustScore().add(java.math.BigDecimal.valueOf(trustDelta)));
- if (intimacyDelta != 0) score.setIntimacyScore(score.getIntimacyScore().add(java.math.BigDecimal.valueOf(intimacyDelta)));
- if (commDelta != 0) score.setCommunicationScore(score.getCommunicationScore().add(java.math.BigDecimal.valueOf(commDelta)));
- familyRelationshipScoreMapper.updateById(score);
- }
- /**
- * TASK-SYS-248: 购物任务联动(cart/order 动作的自动加购/下单)
- * 在任务开始(startTask)时被调用,将 actionConfig 解析后驱动 CartService / ProductOrderService
- * actionConfig JSON 格式示例:
- * cart: {"productId": 123, "quantity": 1, "skuId": 456}
- * order: {"productId": 123, "quantity": 1, "skuId": 456, "consigneeId": 789, "deliveryMethod": 1}
- */
- private void handleShoppingAction(Task task, Long memberId, TaskExecution exec, Date now) {
- String actionType = task.getActionType();
- if (actionType == null || actionType.isEmpty()) {
- return;
- }
- if (!"cart".equals(actionType) && !"order".equals(actionType)) {
- return;
- }
- JSONObject config = parseActionConfig(task.getActionConfig());
- Long productId = config != null ? config.getLong("productId") : null;
- Integer quantity = config != null && config.getInteger("quantity") != null ? config.getInteger("quantity") : 1;
- Long skuId = config != null ? config.getLong("skuId") : null;
- if (productId == null) {
- log.warn("购物任务缺少 productId: taskId={}", task.getId());
- return;
- }
- // 从 member 获取 userId
- FamilyMember member = familyMemberMapper.selectById(memberId);
- Long userId = member != null ? member.getUserId() : null;
- if (userId == null) {
- log.warn("购物任务无法找到 userId: memberId={}", memberId);
- return;
- }
- JSONObject extra = new JSONObject();
- extra.put("actionType", actionType);
- extra.put("productId", productId);
- extra.put("quantity", quantity);
- if (skuId != null) {
- extra.put("skuId", skuId);
- }
- try {
- // 加购(cart 和 order 都需要)
- cartService.addToCart(userId, productId, quantity, skuId);
- extra.put("cartAdded", true);
- if ("order".equals(actionType)) {
- // 创建 pending 订单
- Long consigneeId = config != null ? config.getLong("consigneeId") : null;
- Integer deliveryMethod = config != null ? config.getInteger("deliveryMethod") : null;
- // 未指定收货地址时取用户默认地址
- if (consigneeId == null) {
- try {
- Consignee defaultConsignee = consigneeMapper.selectOne(
- new LambdaQueryWrapper<Consignee>()
- .eq(Consignee::getUserId, userId)
- .eq(Consignee::getIsDefault, 1)
- .last("LIMIT 1"));
- if (defaultConsignee != null) {
- consigneeId = defaultConsignee.getId();
- }
- } catch (Exception e) {
- log.warn("查询默认收货地址失败: userId={}", userId);
- }
- }
- CreateProductOrderDTO orderDto = new CreateProductOrderDTO();
- orderDto.setProductId(productId);
- orderDto.setQuantity(quantity);
- orderDto.setPaymentMethod("wechat");
- if (deliveryMethod != null) {
- orderDto.setDeliveryMethod(deliveryMethod);
- }
- if (consigneeId != null) {
- orderDto.setConsigneeId(consigneeId);
- }
- Result<ProductOrderDTO> orderResult = productOrderService.create(orderDto, userId);
- if (orderResult.getCode() == 200 && orderResult.getData() != null) {
- extra.put("orderId", orderResult.getData().getId());
- extra.put("orderNo", orderResult.getData().getOrderNo());
- exec.setExtra(JSON.toJSONString(extra));
- taskExecutionMapper.updateById(exec);
- log.info("购物任务下单成功: taskId={}, orderNo={}", task.getId(), orderResult.getData().getOrderNo());
- } else {
- // 下单失败(通常因缺地址/库存),保留加购状态
- extra.put("orderFailed", true);
- extra.put("orderError", orderResult.getMessage());
- exec.setExtra(JSON.toJSONString(extra));
- taskExecutionMapper.updateById(exec);
- log.warn("购物任务下单失败: taskId={}, reason={}", task.getId(), orderResult.getMessage());
- }
- } else {
- // 纯加购
- exec.setExtra(JSON.toJSONString(extra));
- taskExecutionMapper.updateById(exec);
- }
- } catch (Exception e) {
- log.error("购物任务联动失败: taskId={}, error={}", task.getId(), e.getMessage(), e);
- extra.put("error", e.getMessage());
- exec.setExtra(JSON.toJSONString(extra));
- taskExecutionMapper.updateById(exec);
- }
- }
- private com.alibaba.fastjson.JSONObject parseActionConfig(String json) {
- if (json == null || json.trim().isEmpty()) {
- return null;
- }
- try {
- return JSON.parseObject(json);
- } catch (Exception e) {
- log.warn("解析 actionConfig 失败: {}", json);
- return null;
- }
- }
- /**
- * TASK-SYS-248: 调研任务自动模板生成(actionType=survey 时,若 actionConfig 无 surveyId 则调 AI 生成)
- * 在 createTask 的 taskMapper.insert() 之后调用
- */
- private void ensureSurveyTemplateForTask(Task task) {
- if (!"survey".equals(task.getActionType())) {
- return;
- }
- JSONObject config = parseActionConfig(task.getActionConfig());
- // 已有 surveyId → 跳过
- if (config != null && config.get("surveyId") != null) {
- return;
- }
- // 从 config 或 task 属性取维度和主题
- String dimension = null;
- String topic = null;
- if (config != null) {
- dimension = config.getString("dimension");
- topic = config.getString("topic");
- }
- if (dimension == null || dimension.isEmpty()) {
- dimension = task.getDimensionCode();
- }
- if (topic == null || topic.isEmpty()) {
- topic = task.getTitle();
- }
- if (topic == null || topic.isEmpty()) {
- topic = "综合评估";
- }
- try {
- String questionsJson = surveyService.aiGenerateQuestions(dimension, topic);
- SurveyTemplate tpl = new SurveyTemplate();
- tpl.setTitle(task.getTitle() != null ? task.getTitle() + " - 调研" : "定期调研");
- tpl.setDimension(dimension);
- tpl.setQuestionsJson(questionsJson);
- tpl.setCycleType("weekly");
- tpl.setEnabled(1);
- tpl.setAiGenerated(1);
- tpl.setCreatedAt(new Date());
- tpl.setUpdatedAt(new Date());
- surveyTemplateMapper.insert(tpl);
- // 回填 actionConfig
- if (config == null) {
- config = new JSONObject();
- }
- config.put("surveyId", tpl.getId());
- task.setActionConfig(config.toJSONString());
- taskMapper.updateById(task);
- log.info("调研任务自动生成模板: taskId={}, templateId={}, dimension={}, topic={}",
- task.getId(), tpl.getId(), dimension, topic);
- } catch (Exception e) {
- log.warn("调研任务自动生成模板失败(不影响任务创建): taskId={}, error={}", task.getId(), e.getMessage());
- }
- }
- // ==================== 统一任务系统:成长任务迁移(原 GrowthTaskService) ====================
- /**
- * 回填进度型任务的当前进度(progress/todayDone,不入库)
- */
- private void fillProgress(List<Task> tasks, Long memberId) {
- if (tasks == null || tasks.isEmpty()) {
- return;
- }
- for (Task t : tasks) {
- if (t.getIsDailyProgress() != null && t.getIsDailyProgress() == 1) {
- TaskExecution exec = taskExecutionMapper.selectOne(new LambdaQueryWrapper<TaskExecution>()
- .eq(TaskExecution::getTaskId, t.getId())
- .eq(TaskExecution::getMemberId, memberId)
- .last("LIMIT 1"));
- int progress = 0;
- if (exec != null && exec.getExtra() != null) {
- try {
- progress = JSON.parseObject(exec.getExtra()).getIntValue("progress");
- } catch (Exception ignore) {
- }
- }
- t.setProgress(progress);
- t.setTodayDone("completed".equals(t.getStatus()) ? 1 : 0);
- }
- }
- }
- /**
- * 统一任务系统:更新每日进度型任务的进度(原 growth_task updateProgress)
- * 按 (userId→familyMemberId, taskKey) 找当日任务实例,累计进度,达标自动完成+发奖
- * taskKey 匹配约定:title 精确匹配 OR title 以 "[taskKey]" 前缀匹配(迁移时 task_key 拼入 title)
- */
- @Transactional
- public void updateProgress(Long userId, String taskKey, int increment) {
- updateProgress(userId, null, taskKey, increment);
- }
- /**
- * 统一任务系统:更新每日进度型任务的进度(带家庭成员精确定位)
- * 按 (familyMemberId 或 userId→familyMemberId, taskKey) 找当日任务实例,累计进度,达标自动完成+发奖
- * taskKey 匹配约定:title 精确匹配 OR title 以 "[taskKey]" 前缀匹配(迁移时 task_key 拼入 title)
- *
- * 打卡类调用必须显式传 familyMemberId:同一 userId 可能绑定多个家庭成员(多家庭场景),
- * 仅按 user_id 反查(LIMIT 1 无排序)会命中错误成员,导致进度记到他人任务、本人任务永久 pending。
- */
- @Transactional
- public void updateProgress(Long userId, Long familyMemberId, String taskKey, int increment) {
- if (userId == null) {
- // 未绑定登录账号的家庭成员(如仅建档的家庭成员)无成长任务进度
- return;
- }
- FamilyMember member;
- if (familyMemberId != null) {
- member = familyMemberMapper.selectById(familyMemberId);
- if (member == null || !Long.valueOf(userId).equals(member.getUserId())) {
- log.warn("统一任务进度更新:成员{}不存在或不属于用户{},跳过 taskKey={}",
- familyMemberId, userId, taskKey);
- return;
- }
- } else {
- member = familyMemberMapper.selectOne(
- new LambdaQueryWrapper<FamilyMember>()
- .eq(FamilyMember::getUserId, userId)
- .last("LIMIT 1"));
- }
- if (member == null) {
- return;
- }
- Calendar cal = Calendar.getInstance();
- cal.set(Calendar.HOUR_OF_DAY, 0);
- cal.set(Calendar.MINUTE, 0);
- cal.set(Calendar.SECOND, 0);
- cal.set(Calendar.MILLISECOND, 0);
- Date startOfDay = cal.getTime();
- cal.add(Calendar.DAY_OF_MONTH, 1);
- Date endOfDay = cal.getTime();
- Task task = taskMapper.selectOne(new LambdaQueryWrapper<Task>()
- .eq(Task::getFamilyMemberId, member.getId())
- .eq(Task::getIsDailyProgress, 1)
- .ge(Task::getCreatedAt, startOfDay)
- .lt(Task::getCreatedAt, endOfDay)
- .and(w -> w.eq(Task::getTitle, taskKey)
- .or().likeRight(Task::getTitle, "[" + taskKey + "]"))
- .last("LIMIT 1"));
- if (task == null) {
- log.warn("统一任务进度更新:未找到今日进度型任务 taskKey={}, userId={}", taskKey, userId);
- return;
- }
- if ("completed".equals(task.getStatus())) {
- return;
- }
- // 读当前进度(task_executions.extra.progress),累加
- TaskExecution exec = taskExecutionMapper.selectOne(new LambdaQueryWrapper<TaskExecution>()
- .eq(TaskExecution::getTaskId, task.getId())
- .eq(TaskExecution::getMemberId, member.getId())
- .last("LIMIT 1"));
- int progress = 0;
- if (exec != null && exec.getExtra() != null) {
- try {
- progress = JSON.parseObject(exec.getExtra()).getIntValue("progress");
- } catch (Exception ignore) {
- }
- }
- progress += increment;
- int target = task.getTargetValue() != null ? task.getTargetValue() : 1;
- if (exec == null) {
- exec = new TaskExecution();
- exec.setTaskId(task.getId());
- exec.setMemberId(member.getId());
- exec.setStatus("started");
- exec.setStartedAt(new Date());
- exec.setCreatedAt(new Date());
- }
- exec.setStatus("finished");
- exec.setFinishedAt(new Date());
- exec.setExtra("{\"progress\":" + progress + ",\"target\":" + target + "}");
- if (exec.getId() == null) {
- taskExecutionMapper.insert(exec);
- } else {
- taskExecutionMapper.updateById(exec);
- }
- // 达标自动完成 + 发奖(复用现有奖励链路)
- if (progress >= target) {
- awardTaskCompletionRewards(task, member.getId(), new Date());
- }
- }
- /**
- * 统一任务系统:打卡回写——按【具体任务实例】累计每日进度,达标后完成该实例并发放奖励。
- *
- * 与按 taskKey 匹配的 updateProgress 不同:前端「我的任务」卡片点击后会把 taskId 透传到
- * 打卡接口,本方法精确关闭该实例,保证打卡成功后首页该卡片立即从待办消失
- * (首页 getTodayTasks 按 status 过滤,前端只保留 status === 'pending' 的卡片)。
- *
- * 为什么不能沿用 updateProgress 的 title LIKE '[taskKey]' 匹配:
- * 1. LIMIT 1 无排序——同一 taskKey 当日若存在多个实例(模板实例 + 内置实例/重复数据)时
- * 完成的是任意一条,用户点击的那条可能永远 pending,首页卡片不关闭;
- * 2. 若实例 created_at 不在今日窗口(首页按 deadline 分支亦会返回该任务),task==null 直接
- * 跳过,点击的任务实例永不被关闭。
- *
- * @param taskId 前端透传的具体任务实例 ID
- * @param userId 登录账号 ID
- * @param familyMemberId 打卡归属家庭成员 ID
- * @param increment 本次累加进度(打卡类恒为 1)
- */
- @Transactional
- public void updateCheckinTaskProgress(Long taskId, Long userId, Long familyMemberId, int increment) {
- if (taskId == null || familyMemberId == null || userId == null) {
- return;
- }
- // 成员校验:必须属于同一登录账号(多家庭场景防串号)
- FamilyMember member = familyMemberMapper.selectById(familyMemberId);
- if (member == null || !Long.valueOf(userId).equals(member.getUserId())) {
- log.warn("打卡回写:成员{}不存在或不属于用户{},跳过 taskId={}", familyMemberId, userId, taskId);
- return;
- }
- Task task = taskMapper.selectById(taskId);
- if (task == null || task.getFamilyMemberId() == null || !task.getFamilyMemberId().equals(familyMemberId)) {
- log.warn("打卡回写:任务{}不存在或不属于成员{},跳过", taskId, familyMemberId);
- return;
- }
- // 仅处理每日进度型任务(一次性任务由 completeTask 链路关闭)
- if (task.getIsDailyProgress() == null || task.getIsDailyProgress() != 1) {
- log.warn("打卡回写:任务{}非每日进度型,跳过", taskId);
- return;
- }
- // 幂等:已关闭则不再重复发奖
- if ("completed".equals(task.getStatus())) {
- return;
- }
- // 读当前进度(task_executions.extra.progress),累加
- TaskExecution exec = taskExecutionMapper.selectOne(new LambdaQueryWrapper<TaskExecution>()
- .eq(TaskExecution::getTaskId, task.getId())
- .eq(TaskExecution::getMemberId, member.getId())
- .last("LIMIT 1"));
- int progress = 0;
- if (exec != null && exec.getExtra() != null) {
- try {
- progress = JSON.parseObject(exec.getExtra()).getIntValue("progress");
- } catch (Exception ignore) {
- }
- }
- progress += increment;
- int target = task.getTargetValue() != null ? task.getTargetValue() : 1;
- if (exec == null) {
- exec = new TaskExecution();
- exec.setTaskId(task.getId());
- exec.setMemberId(member.getId());
- exec.setStatus("started");
- exec.setStartedAt(new Date());
- exec.setCreatedAt(new Date());
- }
- exec.setStatus("finished");
- exec.setFinishedAt(new Date());
- exec.setExtra("{\"progress\":" + progress + ",\"target\":" + target + "}");
- if (exec.getId() == null) {
- taskExecutionMapper.insert(exec);
- } else {
- taskExecutionMapper.updateById(exec);
- }
- // 达标自动完成 + 发奖(复用现有奖励链路):status 写入 completed,
- // 首页刷新后该卡片不再展示为待办;次日由 RepeatTaskGenerator 重新生成当日实例
- if (progress >= target) {
- awardTaskCompletionRewards(task, member.getId(), new Date());
- log.info("每日打卡任务已关闭: taskId={}, title={}, progress={}/target={}",
- taskId, task.getTitle(), progress, target);
- }
- }
- /**
- * 统一任务系统:AI 对话生成任务(原 growth_task createDynamicTask)
- * 直接写入 tasks 表,source_type=ai
- */
- @Transactional
- public Long createDynamicTask(Long userId, String title, String description,
- String dimension, Integer rewardPoints, String sourceConversationId) {
- User user = userService.getUserInfo(userId);
- Task task = new Task();
- task.setFamilyId(user.getFamilyId());
- task.setCreatorId(userId);
- task.setExecutorType("child");
- // 执行者:AI 任务发给当前用户对应家庭成员(若有)
- FamilyMember member = familyMemberMapper.selectOne(
- new LambdaQueryWrapper<FamilyMember>()
- .eq(FamilyMember::getUserId, userId)
- .last("LIMIT 1"));
- if (member != null) {
- task.setExecutorId(member.getId());
- task.setFamilyMemberId(member.getId());
- task.setMemberId(member.getId());
- }
- task.setTitle(title);
- task.setDescription(description);
- task.setPoints(rewardPoints != null ? rewardPoints : 10);
- task.setCategory("成长");
- task.setStatus("pending");
- task.setNeedReview(0);
- task.setIsTemplate(0);
- task.setSourceType("ai");
- task.setSourceId(0L);
- task.setDimensionCode(dimension != null ? dimension : "body");
- task.setMemberOnly(0);
- task.setDeadline(new Date(System.currentTimeMillis() + 7L * 24 * 3600 * 1000)); // 7天有效期
- task.setCreatedAt(new Date());
- task.setUpdatedAt(new Date());
- taskMapper.insert(task);
- log.info("创建AI动态任务: userId={}, taskId={}, title={}", userId, task.getId(), title);
- return task.getId();
- }
- /**
- * 统一任务系统:查询用户每日进度型任务列表(原 growth-task/list)
- * type: DAILY(NEWBIE 在 Task 5 处理)
- */
- public List<Map<String, Object>> getGrowthTaskList(Long userId, String type, String dimension) {
- FamilyMember member = familyMemberMapper.selectOne(
- new LambdaQueryWrapper<FamilyMember>()
- .eq(FamilyMember::getUserId, userId)
- .last("LIMIT 1"));
- List<Map<String, Object>> result = new ArrayList<>();
- if (member == null || !"DAILY".equals(type)) {
- return result;
- }
- Calendar cal = Calendar.getInstance();
- cal.set(Calendar.HOUR_OF_DAY, 0);
- cal.set(Calendar.MINUTE, 0);
- cal.set(Calendar.SECOND, 0);
- cal.set(Calendar.MILLISECOND, 0);
- Date startOfDay = cal.getTime();
- cal.add(Calendar.DAY_OF_MONTH, 1);
- Date endOfDay = cal.getTime();
- List<Task> tasks = taskMapper.selectList(new LambdaQueryWrapper<Task>()
- .eq(Task::getFamilyMemberId, member.getId())
- .eq(Task::getIsDailyProgress, 1)
- .ge(Task::getCreatedAt, startOfDay)
- .lt(Task::getCreatedAt, endOfDay)
- .orderByAsc(Task::getId));
- for (Task task : tasks) {
- Map<String, Object> item = new LinkedHashMap<>();
- item.put("taskLogId", task.getId());
- item.put("id", task.getId());
- item.put("type", "DAILY");
- item.put("title", task.getTitle());
- item.put("description", task.getDescription());
- item.put("rewardPoints", task.getPoints());
- item.put("rewardEnergy", 0);
- item.put("targetValue", task.getTargetValue() != null ? task.getTargetValue() : 1);
- item.put("dimension", task.getDimensionCode());
- TaskExecution exec = taskExecutionMapper.selectOne(new LambdaQueryWrapper<TaskExecution>()
- .eq(TaskExecution::getTaskId, task.getId())
- .eq(TaskExecution::getMemberId, member.getId())
- .last("LIMIT 1"));
- int progress = 0;
- if (exec != null && exec.getExtra() != null) {
- try {
- progress = JSON.parseObject(exec.getExtra()).getIntValue("progress");
- } catch (Exception ignore) {
- }
- }
- item.put("progress", progress);
- item.put("completed", "completed".equals(task.getStatus()) ? 1 : 0);
- item.put("claimed", 0); // 统一任务系统:自动发放,无 claimed
- result.add(item);
- }
- return result;
- }
- // ==================== 统一任务系统:新人引导迁移(原 OnboardingService) ====================
- /**
- * 统一任务系统:新用户注册初始化新人引导任务
- * 按 admin_task_templates 中 onboarding_sequence 有值的模板生成 tasks(source_type=onboarding)
- * 顺序链:sequence 递增,前序未完成则后序 locked(复用 prerequisite_task_id + locked 状态)
- */
- @Transactional
- public void initOnboardingTasks(Long userId) {
- User user = userService.getUserInfo(userId);
- if (user == null || user.getFamilyId() == null) {
- return;
- }
- // 幂等:该用户已有 onboarding 任务则跳过
- Long existing = taskMapper.selectCount(new LambdaQueryWrapper<Task>()
- .eq(Task::getCreatorId, userId)
- .eq(Task::getSourceType, "onboarding"));
- if (existing != null && existing > 0) {
- return;
- }
- List<TaskTemplate> templates = taskTemplateMapper.selectList(
- new LambdaQueryWrapper<TaskTemplate>()
- .eq(TaskTemplate::getIsActive, 1)
- .isNotNull(TaskTemplate::getOnboardingSequence)
- .orderByAsc(TaskTemplate::getOnboardingSequence));
- if (templates.isEmpty()) {
- return;
- }
- FamilyMember member = familyMemberMapper.selectOne(
- new LambdaQueryWrapper<FamilyMember>()
- .eq(FamilyMember::getUserId, userId)
- .last("LIMIT 1"));
- Date now = new Date();
- Long prevTaskId = null;
- for (TaskTemplate tpl : templates) {
- Task task = new Task();
- task.setFamilyId(user.getFamilyId());
- task.setCreatorId(userId);
- task.setExecutorType("child");
- if (member != null) {
- task.setExecutorId(member.getId());
- task.setFamilyMemberId(member.getId());
- task.setMemberId(member.getId());
- }
- task.setTitle(tpl.getTitle());
- task.setDescription(tpl.getDescription());
- task.setPoints(tpl.getPoints() != null ? tpl.getPoints() : 2);
- task.setCategory(tpl.getCategory());
- task.setStatus(prevTaskId == null ? "pending" : "locked"); // 首任务可直接做,后续需解锁
- task.setNeedReview(tpl.getNeedReview() != null ? tpl.getNeedReview() : 0);
- task.setIsTemplate(0);
- task.setSourceType("onboarding");
- task.setTemplateId(tpl.getId());
- task.setPrerequisiteTaskId(prevTaskId); // 顺序链
- task.setMemberOnly(0);
- task.setDeadline(new Date(now.getTime() + 30L * 24 * 3600 * 1000)); // 30天有效期
- task.setCreatedAt(now);
- task.setUpdatedAt(now);
- taskMapper.insert(task);
- prevTaskId = task.getId();
- }
- log.info("新用户{}初始化{}条新人引导任务", userId, templates.size());
- }
- /**
- * 统一任务系统:完成指定新人引导任务(原 OnboardingService.completeTask)
- * 按 source_type=onboarding + title 精确匹配(title 约定 = 原 taskType,如 FIRST_TASK)
- */
- public void completeOnboardingTask(Long userId, String templateTitle) {
- FamilyMember member = familyMemberMapper.selectOne(
- new LambdaQueryWrapper<FamilyMember>()
- .eq(FamilyMember::getUserId, userId)
- .last("LIMIT 1"));
- if (member == null) {
- return;
- }
- Task task = taskMapper.selectOne(new LambdaQueryWrapper<Task>()
- .eq(Task::getFamilyMemberId, member.getId())
- .eq(Task::getSourceType, "onboarding")
- .eq(Task::getTitle, templateTitle)
- .in(Task::getStatus, "pending", "locked")
- .last("LIMIT 1"));
- if (task != null) {
- awardTaskCompletionRewards(task, member.getId(), new Date());
- unlockPrerequisiteTasks(task.getId());
- }
- }
- /**
- * 统一任务系统:查询用户的新人引导任务列表(原 OnboardingService.getProgress)
- * 返回前端兼容结构:taskType=title、status(completed→CLAIMED,其余→PENDING)、rewardPoints、rewardEnergy
- */
- public List<Map<String, Object>> getOnboardingTasks(Long userId) {
- FamilyMember member = familyMemberMapper.selectOne(
- new LambdaQueryWrapper<FamilyMember>()
- .eq(FamilyMember::getUserId, userId)
- .last("LIMIT 1"));
- List<Map<String, Object>> result = new ArrayList<>();
- if (member == null) {
- return result;
- }
- List<Task> tasks = taskMapper.selectList(new LambdaQueryWrapper<Task>()
- .eq(Task::getFamilyMemberId, member.getId())
- .eq(Task::getSourceType, "onboarding")
- .orderByAsc(Task::getId));
- for (Task task : tasks) {
- Map<String, Object> item = new LinkedHashMap<>();
- item.put("taskType", task.getTitle());
- item.put("rewardPoints", task.getPoints() != null ? task.getPoints() : 0);
- item.put("rewardEnergy", task.getPoints() != null ? task.getPoints() : 0); // 能量≈积分(与 awardTaskCompletionRewards 一致)
- // 状态映射:completed→CLAIMED(奖励已自动发放),其余→PENDING
- item.put("status", "completed".equals(task.getStatus()) ? "CLAIMED" : "PENDING");
- result.add(item);
- }
- return result;
- }
- /**
- * 获取家庭成员可关联的任务列表(含所有状态),供创建任务时选择前置/后置任务
- */
- public List<Task> getRelatedTaskList(Long familyId, Long memberId, String titleKeyword) {
- LambdaQueryWrapper<Task> wrapper = new LambdaQueryWrapper<Task>()
- .eq(Task::getFamilyId, familyId)
- .ne(Task::getStatus, "cancelled")
- .orderByDesc(Task::getCreatedAt);
- if (memberId != null) {
- wrapper.eq(Task::getFamilyMemberId, memberId);
- }
- if (titleKeyword != null && !titleKeyword.trim().isEmpty()) {
- wrapper.like(Task::getTitle, titleKeyword.trim());
- }
- return taskMapper.selectList(wrapper);
- }
- }
|