Browse Source

feat(task): 购物任务联动-cart/order 动作自动加购/下单

E2E Test Bot 2 months ago
parent
commit
0b0eaf6887

+ 137 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/TaskService.java

@@ -2,11 +2,16 @@ 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 javax.annotation.Resource;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
@@ -58,6 +63,15 @@ public class TaskService implements TaskServiceInterface {
     @Resource
     private GrowthTaskService growthTaskService;
 
+    @Resource
+    private CartService cartService;
+
+    @Resource
+    private ProductOrderService productOrderService;
+
+    @Resource
+    private ConsigneeMapper consigneeMapper;
+
     private static final int EARLY_BONUS = 1; // 提前完成奖励
     private static final int PENALTY_MAX_DAILY = 5; // 每日最多扣分
     private static final int LATE_GRACE_MINUTES = 10; // 10分钟内不算迟到
@@ -459,6 +473,13 @@ public List<Task> getTodayTasks(Long memberId, String dimensionCode) {
         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);
@@ -1249,4 +1270,120 @@ return 0; // 达到每日扣分上限
         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;
+        }
+    }
 }

+ 24 - 0
docs/superpowers/api/API_REFERENCE.md

@@ -183,6 +183,7 @@ find cfc-backend/src/main/java -name "*XxxService.java" -o -name "*XxxController
 | `POST /api/tasks/minigame-options` | 小游戏选项 |
 | `POST /api/tasks/create` | 创建任务 |
 | `POST /api/tasks/today` | 今日任务 |
+| `POST /api/tasks/{id}/start` | 开始任务(两阶段入口,含购物任务自动加购/下单) |
 | `POST /api/tasks/{id}/complete` | 完成任务 |
 | `POST /api/tasks/{id}/review` | 审核任务 |
 | `POST /api/tasks/history` | 历史任务 |
@@ -196,6 +197,29 @@ find cfc-backend/src/main/java -name "*XxxService.java" -o -name "*XxxController
 | `POST /api/tasks/accept` | 接受任务 |
 | `POST /api/tasks/reject/{id}` | 拒绝任务 |
 
+#### 4.3.1 购物任务联动(`actionType = cart/order`)
+
+任务创建时通过 `actionType` + `actionConfig` 声明购物联动;调用 `POST /api/tasks/{id}/start` 时,后端自动执行加购/下单。
+
+**actionConfig JSON 格式:**
+
+| 字段 | 必填 | 说明 |
+|------|------|------|
+| `productId` | ✅ | 商品ID |
+| `quantity` | ❌ | 数量,默认 1 |
+| `skuId` | ❌ | SKU ID(可选) |
+| `consigneeId` | ❌ | 收货地址ID(`order` 模式下可选;未提供时取用户默认地址) |
+| `deliveryMethod` | ❌ | 配送方式:1=快递/2=自提/4=线上可选人/5=线上单人 |
+
+**actionType 行为:**
+
+| actionType | 行为 | 返回字段 |
+|-----------|------|---------|
+| `cart` | 自动加购,返回加购结果 | `execution.extra` 含 `cartAdded=true`、`productId`、`quantity` |
+| `order` | 先加购,再创建 pending 订单 | `execution.extra` 含 `orderId`、`orderNo`(成功时)或 `orderFailed=true`、`orderError`(失败时) |
+
+**下单失败常见原因:** 缺默认收货地址、商品已下架、库存不足、配送方式校验失败。失败时加购结果保留,任务状态正常置 `in_progress`,前端可引导用户去购物车/结算页手动完成。
+
 ### 4.4 心愿(`/api/wishes`)
 
 | 路径 | 说明 |