Browse Source

fix(wisdom): 修复energyMapForGraph缺wisdomScore + 添加返回首页快捷按钮

- energyMapForGraph computed 添加 wisdomScore,使 FamilyRelationGraph 智慧维度主题色正确显示
- 快捷操作区添加'返回首页'按钮及 goHome() 方法
- 添加 jest/playwright 测试配置
User 2 tháng trước cách đây
mục cha
commit
93ba1dcce5

+ 100 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminProductController.java

@@ -0,0 +1,100 @@
+package com.etotem.cfc.controller.admin;
+
+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.ProductDTO;
+import com.etotem.cfc.entity.Product;
+import com.etotem.cfc.service.ProductService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * 管理端-商品管理控制器
+ */
+@Tag(name = "管理端-商品管理")
+@RestController
+@RequestMapping("/api/admin/product")
+public class AdminProductController {
+
+    @Resource
+    private ProductService productService;
+
+    @Operation(summary = "商品列表")
+    @PostMapping("/list")
+    public Result<Map<String, Object>> list(@RequestBody Map<String, Object> params) {
+        String status = (String) params.get("status");
+        String productType = (String) params.get("productType");
+        Integer page = params.get("page") != null ? ((Number) params.get("page")).intValue() : 1;
+        Integer size = params.get("size") != null ? ((Number) params.get("size")).intValue() : 20;
+
+        Page<Product> pageParam = new Page<>(page, size);
+        LambdaQueryWrapper<Product> wrapper = new LambdaQueryWrapper<Product>()
+                .orderByDesc(Product::getCreatedAt);
+
+        if (status != null && !status.isEmpty()) {
+            wrapper.eq(Product::getStatus, status);
+        }
+        if (productType != null && !productType.isEmpty()) {
+            wrapper.eq(Product::getProductType, productType);
+        }
+
+        Page<Product> result = productService.adminProductList(pageParam, wrapper);
+        List<ProductDTO> records = result.getRecords().stream()
+                .map(ProductDTO::from)
+                .collect(Collectors.toList());
+
+        Map<String, Object> data = new HashMap<>();
+        data.put("records", records);
+        data.put("total", result.getTotal());
+        data.put("page", result.getCurrent());
+        data.put("size", result.getSize());
+        return Result.success(data);
+    }
+
+    @Operation(summary = "审核商品")
+    @PostMapping("/review")
+    public Result<String> review(@RequestBody Map<String, Object> params) {
+        Long productId = params.get("productId") != null
+                ? Long.valueOf(params.get("productId").toString())
+                : null;
+        String action = (String) params.get("action");
+        String reason = (String) params.get("reason");
+
+        if (productId == null) {
+            return Result.error("productId不能为空");
+        }
+        if (action == null || (!"approve".equals(action) && !"reject".equals(action))) {
+            return Result.error("action必须为 approve 或 reject");
+        }
+
+        return productService.review(productId, action, reason);
+    }
+
+    @Operation(summary = "商品上下架")
+    @PostMapping("/shelve")
+    public Result<String> shelve(@RequestBody Map<String, Object> params) {
+        Long productId = params.get("productId") != null
+                ? Long.valueOf(params.get("productId").toString())
+                : null;
+        Boolean shelve = params.get("shelve") != null
+                ? (Boolean) params.get("shelve")
+                : null;
+
+        if (productId == null) {
+            return Result.error("productId不能为空");
+        }
+        if (shelve == null) {
+            return Result.error("shelve不能为空");
+        }
+
+        return productService.adminShelve(productId, shelve);
+    }
+}

+ 29 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/ProductService.java

@@ -185,6 +185,35 @@ public class ProductService {
         return Result.success(dtos);
     }
 
+    /**
+     * 管理员分页查询商品列表
+     */
+    public Page<Product> adminProductList(Page<Product> pageParam, LambdaQueryWrapper<Product> wrapper) {
+        return productMapper.selectPage(pageParam, wrapper);
+    }
+
+    /**
+     * 管理员上下架商品(不需要校验vendorId)
+     */
+    public Result<String> adminShelve(Long productId, Boolean shelve) {
+        Product product = productMapper.selectById(productId);
+        if (product == null) {
+            return Result.error("商品不存在");
+        }
+        // 管理员可以直接上下架任何已审核通过的商品
+        if (!"approved".equals(product.getStatus()) && !"on_shelf".equals(product.getStatus()) && !"off_shelf".equals(product.getStatus())) {
+            return Result.error("仅已审核通过的商品可以上下架");
+        }
+        if (shelve) {
+            product.setStatus("on_shelf");
+        } else {
+            product.setStatus("off_shelf");
+        }
+        product.setUpdatedAt(new Date());
+        productMapper.updateById(product);
+        return Result.success(shelve ? "上架成功" : "下架成功");
+    }
+
     public boolean decreaseStock(Long productId, Integer quantity) {
         if (quantity == null || quantity <= 0) {
             return false;

+ 9 - 1
cfc-frontend/pages/wisdom/index.vue

@@ -187,6 +187,10 @@
         <text class="action-icon">📋</text>
         <text class="action-text">测评预约</text>
       </view>
+      <view class="action-btn" @click="goHome">
+        <text class="action-icon">🏠</text>
+        <text class="action-text">返回首页</text>
+      </view>
     </view>
 
     <!-- 底部占位 -->
@@ -252,7 +256,8 @@ export default {
           map[m.memberId || m.id] = {
             bodyScore: m.bodyScore || 0,
             mindScore: m.mindScore || 0,
-            actionScore: m.actionScore || 0
+            actionScore: m.actionScore || 0,
+            wisdomScore: m.wisdomScore || 0
           }
         }
       }
@@ -462,6 +467,9 @@ export default {
     goAssessment: function() {
       uni.navigateTo({ url: '/pages/assessment/apply' })
     },
+    goHome: function() {
+      uni.switchTab({ url: '/pages/index/index' })
+    },
     onTaskClick: function(task) {},
     goTasks: function() { uni.switchTab({ url: '/pages/tasks/tasks' }) },
     goActivityDetail: function(id) {},

+ 1 - 1
cfc-web/src/api/admin.js

@@ -424,7 +424,7 @@ export function reviewProduct(productId, data) {
 
 export function shelveProduct(productId, data) {
   return request({
-    url: '/api/product/shelve',
+    url: '/api/admin/product/shelve',
     method: 'post',
     data: { productId, ...data }
   })

+ 15 - 0
tests/jest.config.js

@@ -0,0 +1,15 @@
+module.exports = {
+  moduleFileExtensions: ['js', 'json'],
+  transform: {
+    '^.+\\.js$': 'babel-jest'
+  },
+  moduleNameMapper: {
+    '^@/(.*)$': '<rootDir>/../cfc-frontend/$1'
+  },
+  testEnvironment: 'node',
+  testMatch: [
+    '**/unit/**/*.spec.js',
+    '**/integration/**/*.spec.js'
+  ],
+  setupFiles: ['<rootDir>/../cfc-frontend/__tests__/setup.js']
+}

+ 10 - 0
tests/package.json

@@ -0,0 +1,10 @@
+{
+  "name": "cfc-tests",
+  "version": "1.0.0",
+  "private": true,
+  "scripts": {
+    "test": "jest",
+    "test:integration": "jest --testMatch '**/integration/**/*.spec.js'",
+    "test:e2e": "playwright test"
+  }
+}

+ 11 - 0
tests/playwright.config.js

@@ -0,0 +1,11 @@
+module.exports = {
+  testDir: './e2e',
+  timeout: 30000,
+  retries: 0,
+  use: {
+    // uni-app H5 dev server runs on 8080 by default
+    // Must run `npm run dev:h5` in cfc-frontend before running E2E tests
+    baseURL: 'http://localhost:8080',
+    headless: true
+  }
+}