Просмотр исходного кода

feat(activity): 审核仅填五维权重并自动推导维度,活动标签自动生成

Xiaogang Liao 6 дней назад
Родитель
Сommit
ae2e60f108

+ 3 - 0
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -3104,6 +3104,9 @@ insertSysConfigSeed("gift_membership_pay_price", "198000", "送会员商品达
         // 迁移388: activities表添加intro列(活动简介,纯文本短摘要,用于活动列表卡片展示)
         // 迁移388: activities表添加intro列(活动简介,纯文本短摘要,用于活动列表卡片展示)
         ensureColumn("activities", "intro", "VARCHAR(200) DEFAULT NULL COMMENT '活动简介(列表卡片展示)'");
         ensureColumn("activities", "intro", "VARCHAR(200) DEFAULT NULL COMMENT '活动简介(列表卡片展示)'");
 
 
+        // 迁移389: activities表添加tags列(活动标签,逗号分隔,根据活动内容自动生成)
+        ensureColumn("activities", "tags", "VARCHAR(255) DEFAULT NULL COMMENT '活动标签,逗号分隔'");
+
         // 迁移382: health_status 添加 signature 列(用户手签名,base64 PNG data URL,用于问卷回答确认)
         // 迁移382: health_status 添加 signature 列(用户手签名,base64 PNG data URL,用于问卷回答确认)
         ensureColumn("health_status", "signature", "MEDIUMTEXT DEFAULT NULL COMMENT '用户手签名(base64 PNG data URL),确认问卷回答准确性'");
         ensureColumn("health_status", "signature", "MEDIUMTEXT DEFAULT NULL COMMENT '用户手签名(base64 PNG data URL),确认问卷回答准确性'");
 
 

+ 6 - 2
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminActivityController.java

@@ -75,15 +75,19 @@ public class AdminActivityController {
         Long id = ParamUtils.getLong(params.get("id"));
         Long id = ParamUtils.getLong(params.get("id"));
         String action = (String) params.get("action");
         String action = (String) params.get("action");
         String rejectReason = (String) params.get("rejectReason");
         String rejectReason = (String) params.get("rejectReason");
-        String dimensionCode = (String) params.get("dimensionCode");
         @SuppressWarnings("unchecked")
         @SuppressWarnings("unchecked")
         List<Map<String, Object>> dimensionWeights =
         List<Map<String, Object>> dimensionWeights =
                 (List<Map<String, Object>>) params.get("dimensionWeights");
                 (List<Map<String, Object>>) params.get("dimensionWeights");
         if (id == null || action == null) return Result.error("id和action不能为空");
         if (id == null || action == null) return Result.error("id和action不能为空");
-        activityAdminService.auditActivity(id, action, rejectReason, adminId, dimensionCode, dimensionWeights);
+        activityAdminService.auditActivity(id, action, rejectReason, adminId, dimensionWeights);
         return Result.success("已审核");
         return Result.success("已审核");
     }
     }
 
 
+    @PostMapping("/backfill-tags")
+    public Result<Integer> backfillTags() {
+        return Result.success(activityAdminService.backfillTags());
+    }
+
     @PostMapping("/withdraw")
     @PostMapping("/withdraw")
     public Result<String> withdraw(@RequestBody Map<String, Object> params) {
     public Result<String> withdraw(@RequestBody Map<String, Object> params) {
         Long id = ParamUtils.getLong(params.get("id"));
         Long id = ParamUtils.getLong(params.get("id"));

+ 2 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/ActivityDTO.java

@@ -11,6 +11,7 @@ public class ActivityDTO {
     private String title;
     private String title;
     private String description;
     private String description;
     private String intro;
     private String intro;
+    private String tags;
     private String coverImage;
     private String coverImage;
     private String dimensionCode;
     private String dimensionCode;
     /** JSON: {"body":0,"mind":0,"wisdom":0,"action":0,"wealth":0} */
     /** JSON: {"body":0,"mind":0,"wisdom":0,"action":0,"wealth":0} */
@@ -82,6 +83,7 @@ public class ActivityDTO {
         dto.setTitle(activity.getTitle());
         dto.setTitle(activity.getTitle());
         dto.setDescription(activity.getDescription());
         dto.setDescription(activity.getDescription());
         dto.setIntro(activity.getIntro());
         dto.setIntro(activity.getIntro());
+        dto.setTags(activity.getTags());
         dto.setCoverImage(activity.getCoverImage());
         dto.setCoverImage(activity.getCoverImage());
         dto.setDimensionCode(activity.getDimensionCode());
         dto.setDimensionCode(activity.getDimensionCode());
         dto.setDimensionWeights(activity.getDimensionWeights());
         dto.setDimensionWeights(activity.getDimensionWeights());

+ 3 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/Activity.java

@@ -22,6 +22,9 @@ public class Activity implements Serializable {
     /** 活动简介(纯文本短摘要,用于活动列表卡片展示) */
     /** 活动简介(纯文本短摘要,用于活动列表卡片展示) */
     private String intro;
     private String intro;
 
 
+    /** 活动标签,逗号分隔(根据活动内容自动生成,可人工修正) */
+    private String tags;
+
     private String coverImage;
     private String coverImage;
 
 
     /** body/mind/wisdom/action/wealth */
     /** body/mind/wisdom/action/wealth */

+ 32 - 6
cfc-backend/src/main/java/com/etotem/cfc/service/ActivityAdminService.java

@@ -33,6 +33,9 @@ public class ActivityAdminService {
     @Resource
     @Resource
     private DimensionWeightService dimensionWeightService;
     private DimensionWeightService dimensionWeightService;
 
 
+    @Resource
+    private ActivityTagService activityTagService;
+
     /**
     /**
      * 分页查询活动列表(包含草稿)
      * 分页查询活动列表(包含草稿)
      * 支持按 status / dimensionCode / auditStatus 过滤
      * 支持按 status / dimensionCode / auditStatus 过滤
@@ -136,14 +139,16 @@ public class ActivityAdminService {
         activity.setStatus("pending");
         activity.setStatus("pending");
         activity.setAuditStatus("pending");
         activity.setAuditStatus("pending");
         activityMapper.updateById(activity);
         activityMapper.updateById(activity);
+        // 提交审核时异步生成标签,便于审核页直接查看
+        activityTagService.generateIfEmptyAsync(id);
     }
     }
 
 
     /**
     /**
      * 审核活动(审核中心:ActivityAudit.vue 使用)
      * 审核活动(审核中心:ActivityAudit.vue 使用)
-     * @param params action: "approved" | "rejected", id: Long, dimensionCode: String(approved 时必填), rejectReason: String
+     * 审核通过时只需提交五维权重,所属维度自动取权重最高的维度。
      */
      */
     @Transactional
     @Transactional
-    public void auditActivity(Long id, String action, String auditReason, Long adminId, String dimensionCode,
+    public void auditActivity(Long id, String action, String auditReason, Long adminId,
                               List<Map<String, Object>> dimensionWeights) {
                               List<Map<String, Object>> dimensionWeights) {
         Activity activity = activityMapper.selectById(id);
         Activity activity = activityMapper.selectById(id);
         if (activity == null) {
         if (activity == null) {
@@ -158,13 +163,10 @@ public class ActivityAdminService {
         }
         }
         List<DimensionWeight> weights = null;
         List<DimensionWeight> weights = null;
         if ("approved".equals(normalizedAction)) {
         if ("approved".equals(normalizedAction)) {
-            if (dimensionCode == null || dimensionCode.trim().isEmpty()) {
-                throw new IllegalStateException("审核通过时必须选择所属维度");
-            }
             weights = parseAndValidateDimensionWeights(dimensionWeights);
             weights = parseAndValidateDimensionWeights(dimensionWeights);
             activity.setStatus("published");
             activity.setStatus("published");
             activity.setAuditStatus("approved");
             activity.setAuditStatus("approved");
-            activity.setDimensionCode(dimensionCode.trim());
+            activity.setDimensionCode(resolveTopDimension(weights));
             activity.setAuditorId(adminId);
             activity.setAuditorId(adminId);
             activity.setAuditedAt(new Date());
             activity.setAuditedAt(new Date());
         } else {
         } else {
@@ -178,9 +180,33 @@ public class ActivityAdminService {
         if (weights != null) {
         if (weights != null) {
             // 同时写入 dimension_weights 表与 activities.dimension_weights JSON 列
             // 同时写入 dimension_weights 表与 activities.dimension_weights JSON 列
             dimensionWeightService.saveWeights("activity", id, weights);
             dimensionWeightService.saveWeights("activity", id, weights);
+            // 审核通过时若标签仍为空则异步补标
+            activityTagService.generateIfEmptyAsync(id);
         }
         }
     }
     }
 
 
+    /**
+     * 所属维度取五维权重中占比最高的维度;并列时按 身/心/智/行/富 顺序取第一个。
+     */
+    private String resolveTopDimension(List<DimensionWeight> weights) {
+        String top = null;
+        int max = -1;
+        for (DimensionWeight weight : weights) {
+            if (weight.getWeight() != null && weight.getWeight() > max) {
+                max = weight.getWeight();
+                top = weight.getDimension();
+            }
+        }
+        return top;
+    }
+
+    /**
+     * 为已发布且标签为空的活动批量补标(异步)。
+     */
+    public int backfillTags() {
+        return activityTagService.backfillEmptyPublished();
+    }
+
     private static final List<String> REQUIRED_DIMENSIONS =
     private static final List<String> REQUIRED_DIMENSIONS =
             Arrays.asList("body", "mind", "wisdom", "action", "wealth");
             Arrays.asList("body", "mind", "wisdom", "action", "wealth");
 
 

+ 155 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/ActivityTagService.java

@@ -0,0 +1,155 @@
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.entity.Activity;
+import com.etotem.cfc.mapper.ActivityMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 活动标签:审核提交/通过时根据活动内容自动生成 3-5 个中文关键词。
+ * 与文章自动打标一致,走 AIService;失败仅记录日志,不影响活动流程。
+ */
+@Service
+public class ActivityTagService {
+
+    private static final Logger log = LoggerFactory.getLogger(ActivityTagService.class);
+    private static final String UPDATE_TAGS_SQL = "UPDATE activities SET tags = ? WHERE id = ?";
+
+    @Resource
+    private ActivityMapper activityMapper;
+
+    @Resource
+    private AIService aiService;
+
+    @Resource
+    private JdbcTemplate jdbcTemplate;
+
+    @Resource(name = "taskExecutor")
+    private ThreadPoolTaskExecutor taskExecutor;
+
+    /** 异步生成,不阻塞审核/提交流程 */
+    public void generateIfEmptyAsync(Long activityId) {
+        if (activityId == null) {
+            return;
+        }
+        try {
+            taskExecutor.execute(() -> {
+                try {
+                    generateIfEmpty(activityId);
+                } catch (Exception e) {
+                    log.warn("活动自动标签生成失败: activityId={}, error={}", activityId, e.getMessage());
+                }
+            });
+        } catch (Exception e) {
+            log.warn("活动自动标签任务提交失败: activityId={}, error={}", activityId, e.getMessage());
+        }
+    }
+
+    /** 仅当活动存在且标签为空时生成并写回 */
+    public void generateIfEmpty(Long activityId) {
+        if (activityId == null) {
+            return;
+        }
+        Activity activity = activityMapper.selectById(activityId);
+        if (activity == null) {
+            return;
+        }
+        if (activity.getTags() != null && !activity.getTags().trim().isEmpty()) {
+            return;
+        }
+        List<String> tags = generateTags(buildContent(activity));
+        if (tags.isEmpty()) {
+            return;
+        }
+        jdbcTemplate.update(UPDATE_TAGS_SQL, String.join(",", tags), activityId);
+    }
+
+    /**
+     * 调用 AI 从活动内容中提取 3-5 个中文关键词作为标签。
+     */
+    public List<String> generateTags(String content) {
+        if (content == null || content.trim().isEmpty()) {
+            return Collections.emptyList();
+        }
+        String plainText = content.replaceAll("<[^>]*>", " ")
+                .replaceAll("\\s+", " ")
+                .trim();
+        if (plainText.isEmpty()) {
+            return Collections.emptyList();
+        }
+        if (plainText.length() > 1500) {
+            plainText = plainText.substring(0, 1500);
+        }
+
+        String prompt = "你是一个家庭活动内容分析助手。请根据以下活动信息,提取 3 到 5 个最能代表活动主题的中文关键词作为标签。"
+                + "要求:1) 关键词为中文短语或名词,2-6 个字;2) 用英文逗号分隔;3) 不要包含编号、引号或解释文字;4) 只返回标签列表本身。"
+                + "\n\n活动信息:" + plainText;
+
+        try {
+            Map<String, Object> aiResult = aiService.sendMessage(prompt, "0", "", null);
+            Object answerObj = aiResult == null ? null : aiResult.get("answer");
+            if (answerObj == null || answerObj.toString().trim().isEmpty()) {
+                return Collections.emptyList();
+            }
+            List<String> tags = new ArrayList<>();
+            for (String raw : answerObj.toString().split("[,,、]")) {
+                String tag = raw.trim()
+                        .replaceAll("^[\\d\\.\\-\\s、。,]+", "")
+                        .replaceAll("[\\s、。,]+$", "")
+                        .trim();
+                if (!tag.isEmpty() && tag.length() <= 20 && !tags.contains(tag) && tags.size() < 5) {
+                    tags.add(tag);
+                }
+            }
+            return tags;
+        } catch (Exception e) {
+            log.warn("活动 AI 自动生成标签失败:{}", e.getMessage());
+            return Collections.emptyList();
+        }
+    }
+
+    /**
+     * 为已发布且标签为空的活动批量补标(异步,幂等)。
+     *
+     * @return 本次触发的活动数量
+     */
+    public int backfillEmptyPublished() {
+        List<Activity> activities = activityMapper.selectList(
+                new LambdaQueryWrapper<Activity>()
+                        .eq(Activity::getStatus, "published")
+                        .and(w -> w.isNull(Activity::getTags).or().apply("TRIM(tags) = ''")));
+        for (Activity activity : activities) {
+            generateIfEmptyAsync(activity.getId());
+        }
+        return activities.size();
+    }
+
+    private String buildContent(Activity activity) {
+        StringBuilder sb = new StringBuilder();
+        append(sb, activity.getTitle());
+        append(sb, activity.getIntro());
+        append(sb, activity.getDescription());
+        append(sb, activity.getLocation());
+        append(sb, activity.getActivityType());
+        return sb.toString();
+    }
+
+    private void append(StringBuilder sb, String value) {
+        if (value != null && !value.trim().isEmpty()) {
+            if (sb.length() > 0) {
+                sb.append(" ");
+            }
+            sb.append(value.trim());
+        }
+    }
+}

+ 1 - 0
cfc-backend/src/main/resources/schema.sql

@@ -2109,6 +2109,7 @@ CREATE TABLE IF NOT EXISTS activities (
     title            VARCHAR(100) NOT NULL COMMENT '活动标题',
     title            VARCHAR(100) NOT NULL COMMENT '活动标题',
     description      TEXT COMMENT '活动描述',
     description      TEXT COMMENT '活动描述',
     intro            VARCHAR(200) DEFAULT NULL COMMENT '活动简介(列表卡片展示)',
     intro            VARCHAR(200) DEFAULT NULL COMMENT '活动简介(列表卡片展示)',
+    tags             VARCHAR(255) DEFAULT NULL COMMENT '活动标签,逗号分隔',
     cover_image      VARCHAR(500) COMMENT '封面图片URL',
     cover_image      VARCHAR(500) COMMENT '封面图片URL',
     dimension_code   VARCHAR(20) COMMENT '维度代码: body/mind/wisdom/action/wealth',
     dimension_code   VARCHAR(20) COMMENT '维度代码: body/mind/wisdom/action/wealth',
     dimension_weights JSON DEFAULT NULL COMMENT '五维权重百分比: {"body":0,"mind":0,"wisdom":0,"action":0,"wealth":0}',
     dimension_weights JSON DEFAULT NULL COMMENT '五维权重百分比: {"body":0,"mind":0,"wisdom":0,"action":0,"wealth":0}',

+ 49 - 8
cfc-backend/src/test/java/com/etotem/cfc/unit/ActivityAuditTest.java

@@ -4,6 +4,7 @@ import com.etotem.cfc.entity.Activity;
 import com.etotem.cfc.entity.DimensionWeight;
 import com.etotem.cfc.entity.DimensionWeight;
 import com.etotem.cfc.mapper.ActivityMapper;
 import com.etotem.cfc.mapper.ActivityMapper;
 import com.etotem.cfc.service.ActivityAdminService;
 import com.etotem.cfc.service.ActivityAdminService;
+import com.etotem.cfc.service.ActivityTagService;
 import com.etotem.cfc.service.DimensionWeightService;
 import com.etotem.cfc.service.DimensionWeightService;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.Test;
@@ -21,21 +22,25 @@ import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.*;
 import static org.mockito.Mockito.*;
 
 
 /**
 /**
- * 审核中心活动审核:动作兼容 publish/reject,审核通过时必须补充五维权重。
+ * 审核中心活动审核:动作兼容 publish/reject;通过时只填五维权重,
+ * 所属维度由权重最高的维度自动推导。
  */
  */
 class ActivityAuditTest {
 class ActivityAuditTest {
 
 
     private ActivityAdminService service;
     private ActivityAdminService service;
     private ActivityMapper activityMapper;
     private ActivityMapper activityMapper;
     private DimensionWeightService dimensionWeightService;
     private DimensionWeightService dimensionWeightService;
+    private ActivityTagService activityTagService;
 
 
     @BeforeEach
     @BeforeEach
     void setUp() throws Exception {
     void setUp() throws Exception {
         service = new ActivityAdminService();
         service = new ActivityAdminService();
         activityMapper = mock(ActivityMapper.class);
         activityMapper = mock(ActivityMapper.class);
         dimensionWeightService = mock(DimensionWeightService.class);
         dimensionWeightService = mock(DimensionWeightService.class);
+        activityTagService = mock(ActivityTagService.class);
         set("activityMapper", activityMapper);
         set("activityMapper", activityMapper);
         set("dimensionWeightService", dimensionWeightService);
         set("dimensionWeightService", dimensionWeightService);
+        set("activityTagService", activityTagService);
     }
     }
 
 
     private void set(String name, Object value) throws Exception {
     private void set(String name, Object value) throws Exception {
@@ -73,7 +78,7 @@ class ActivityAuditTest {
         when(activityMapper.selectById(9L)).thenReturn(activity);
         when(activityMapper.selectById(9L)).thenReturn(activity);
         when(activityMapper.updateById(any(Activity.class))).thenReturn(1);
         when(activityMapper.updateById(any(Activity.class))).thenReturn(1);
 
 
-        service.auditActivity(9L, "publish", null, 1L, "body", weights(20, 20, 20, 20, 20));
+        service.auditActivity(9L, "publish", null, 1L, weights(20, 20, 20, 20, 20));
 
 
         assertEquals("published", activity.getStatus());
         assertEquals("published", activity.getStatus());
         assertEquals("approved", activity.getAuditStatus());
         assertEquals("approved", activity.getAuditStatus());
@@ -82,13 +87,35 @@ class ActivityAuditTest {
         assertNotNull(activity.getAuditedAt());
         assertNotNull(activity.getAuditedAt());
     }
     }
 
 
+    @Test
+    void audit_通过时按最高权重自动确定所属维度() {
+        Activity activity = pending();
+        when(activityMapper.selectById(9L)).thenReturn(activity);
+        when(activityMapper.updateById(any(Activity.class))).thenReturn(1);
+
+        service.auditActivity(9L, "approved", null, 1L, weights(10, 50, 10, 20, 10));
+
+        assertEquals("mind", activity.getDimensionCode());
+    }
+
+    @Test
+    void audit_最高权重并列时按身心灵知行顺序取第一个() {
+        Activity activity = pending();
+        when(activityMapper.selectById(9L)).thenReturn(activity);
+        when(activityMapper.updateById(any(Activity.class))).thenReturn(1);
+
+        service.auditActivity(9L, "approved", null, 1L, weights(50, 50, 0, 0, 0));
+
+        assertEquals("body", activity.getDimensionCode());
+    }
+
     @Test
     @Test
     void audit_通过时保存五维权重() {
     void audit_通过时保存五维权重() {
         Activity activity = pending();
         Activity activity = pending();
         when(activityMapper.selectById(9L)).thenReturn(activity);
         when(activityMapper.selectById(9L)).thenReturn(activity);
         when(activityMapper.updateById(any(Activity.class))).thenReturn(1);
         when(activityMapper.updateById(any(Activity.class))).thenReturn(1);
 
 
-        service.auditActivity(9L, "approved", null, 1L, "middle", weights(40, 15, 15, 15, 15));
+        service.auditActivity(9L, "approved", null, 1L, weights(40, 15, 15, 15, 15));
 
 
         @SuppressWarnings("unchecked")
         @SuppressWarnings("unchecked")
         ArgumentCaptor<List<DimensionWeight>> captor = ArgumentCaptor.forClass(List.class);
         ArgumentCaptor<List<DimensionWeight>> captor = ArgumentCaptor.forClass(List.class);
@@ -101,6 +128,20 @@ class ActivityAuditTest {
         assertNotNull(body);
         assertNotNull(body);
         assertEquals(40, body.getWeight());
         assertEquals(40, body.getWeight());
         assertTrue(body.getEnabled());
         assertTrue(body.getEnabled());
+        verify(activityTagService).generateIfEmptyAsync(9L);
+    }
+
+    @Test
+    void submitForReview_提交后异步生成标签() {
+        Activity activity = pending();
+        activity.setStatus("draft");
+        when(activityMapper.selectById(9L)).thenReturn(activity);
+        when(activityMapper.updateById(any(Activity.class))).thenReturn(1);
+
+        service.submitForReview(9L);
+
+        assertEquals("pending", activity.getStatus());
+        verify(activityTagService).generateIfEmptyAsync(9L);
     }
     }
 
 
     @Test
     @Test
@@ -109,7 +150,7 @@ class ActivityAuditTest {
         when(activityMapper.selectById(9L)).thenReturn(activity);
         when(activityMapper.selectById(9L)).thenReturn(activity);
 
 
         IllegalStateException e = assertThrows(IllegalStateException.class,
         IllegalStateException e = assertThrows(IllegalStateException.class,
-                () -> service.auditActivity(9L, "approved", null, 1L, "body", null));
+                () -> service.auditActivity(9L, "approved", null, 1L, null));
 
 
         assertTrue(e.getMessage().contains("五维权重"), e.getMessage());
         assertTrue(e.getMessage().contains("五维权重"), e.getMessage());
         verify(activityMapper, never()).updateById(any(Activity.class));
         verify(activityMapper, never()).updateById(any(Activity.class));
@@ -122,7 +163,7 @@ class ActivityAuditTest {
         when(activityMapper.selectById(9L)).thenReturn(activity);
         when(activityMapper.selectById(9L)).thenReturn(activity);
 
 
         IllegalStateException e = assertThrows(IllegalStateException.class,
         IllegalStateException e = assertThrows(IllegalStateException.class,
-                () -> service.auditActivity(9L, "approved", null, 1L, "body", weights(20, 20, 20, 20, 19)));
+                () -> service.auditActivity(9L, "approved", null, 1L, weights(20, 20, 20, 20, 19)));
 
 
         assertTrue(e.getMessage().contains("100"), e.getMessage());
         assertTrue(e.getMessage().contains("100"), e.getMessage());
         verify(activityMapper, never()).updateById(any(Activity.class));
         verify(activityMapper, never()).updateById(any(Activity.class));
@@ -137,7 +178,7 @@ class ActivityAuditTest {
         onlyFour.removeIf(m -> "wealth".equals(m.get("dimension")));
         onlyFour.removeIf(m -> "wealth".equals(m.get("dimension")));
 
 
         IllegalStateException e = assertThrows(IllegalStateException.class,
         IllegalStateException e = assertThrows(IllegalStateException.class,
-                () -> service.auditActivity(9L, "approved", null, 1L, "body", onlyFour));
+                () -> service.auditActivity(9L, "approved", null, 1L, onlyFour));
 
 
         assertTrue(e.getMessage().contains("五个维度"), e.getMessage());
         assertTrue(e.getMessage().contains("五个维度"), e.getMessage());
     }
     }
@@ -148,7 +189,7 @@ class ActivityAuditTest {
         when(activityMapper.selectById(9L)).thenReturn(activity);
         when(activityMapper.selectById(9L)).thenReturn(activity);
         when(activityMapper.updateById(any(Activity.class))).thenReturn(1);
         when(activityMapper.updateById(any(Activity.class))).thenReturn(1);
 
 
-        service.auditActivity(9L, "reject", "材料不全", 1L, null, null);
+        service.auditActivity(9L, "reject", "材料不全", 1L, null);
 
 
         assertEquals("rejected", activity.getStatus());
         assertEquals("rejected", activity.getStatus());
         assertEquals("rejected", activity.getAuditStatus());
         assertEquals("rejected", activity.getAuditStatus());
@@ -162,7 +203,7 @@ class ActivityAuditTest {
         when(activityMapper.selectById(9L)).thenReturn(activity);
         when(activityMapper.selectById(9L)).thenReturn(activity);
 
 
         IllegalStateException e = assertThrows(IllegalStateException.class,
         IllegalStateException e = assertThrows(IllegalStateException.class,
-                () -> service.auditActivity(9L, "unknown", null, 1L, "body", weights(20, 20, 20, 20, 20)));
+                () -> service.auditActivity(9L, "unknown", null, 1L, weights(20, 20, 20, 20, 20)));
 
 
         assertTrue(e.getMessage().contains("审核操作无效"), e.getMessage());
         assertTrue(e.getMessage().contains("审核操作无效"), e.getMessage());
     }
     }

+ 111 - 0
cfc-backend/src/test/java/com/etotem/cfc/unit/ActivityTagServiceTest.java

@@ -0,0 +1,111 @@
+package com.etotem.cfc.unit;
+
+import com.etotem.cfc.entity.Activity;
+import com.etotem.cfc.mapper.ActivityMapper;
+import com.etotem.cfc.service.AIService;
+import com.etotem.cfc.service.ActivityTagService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.jdbc.core.JdbcTemplate;
+
+import java.lang.reflect.Field;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * 活动自动标签:与文章一致,根据标题/简介/描述等内容由 AI 提取 3-5 个关键词。
+ */
+class ActivityTagServiceTest {
+
+    private ActivityTagService service;
+    private ActivityMapper activityMapper;
+    private AIService aiService;
+    private JdbcTemplate jdbcTemplate;
+
+    @BeforeEach
+    void setUp() throws Exception {
+        service = new ActivityTagService();
+        activityMapper = mock(ActivityMapper.class);
+        aiService = mock(AIService.class);
+        jdbcTemplate = mock(JdbcTemplate.class);
+        set("activityMapper", activityMapper);
+        set("aiService", aiService);
+        set("jdbcTemplate", jdbcTemplate);
+    }
+
+    private void set(String name, Object value) throws Exception {
+        Field f = ActivityTagService.class.getDeclaredField(name);
+        f.setAccessible(true);
+        f.set(service, value);
+    }
+
+    private Activity activity(Long id, String tags) {
+        Activity a = new Activity();
+        a.setId(id);
+        a.setTitle("周末亲子户外徒步");
+        a.setIntro("带孩子亲近自然");
+        a.setDescription("<p>亲子徒步、自然观察与野餐</p>");
+        a.setTags(tags);
+        return a;
+    }
+
+    @Test
+    void generateTags_解析AI返回的中文逗号与顿号() {
+        Map<String, Object> ai = new HashMap<>();
+        ai.put("answer", "1. 亲子活动,户外徒步、自然教育");
+        when(aiService.sendMessage(anyString(), anyString(), anyString(), any())).thenReturn(ai);
+
+        List<String> tags = service.generateTags("周末亲子户外徒步,带孩子亲近自然");
+
+        assertEquals(3, tags.size());
+        assertEquals("亲子活动", tags.get(0));
+        assertEquals("户外徒步", tags.get(1));
+        assertEquals("自然教育", tags.get(2));
+    }
+
+    @Test
+    void generateTags_空内容不调用AI() {
+        assertTrue(service.generateTags("   ").isEmpty());
+        verifyNoInteractions(aiService);
+    }
+
+    @Test
+    void generateIfEmpty_已有标签时不重复生成() {
+        when(activityMapper.selectById(9L)).thenReturn(activity(9L, "亲子,户外"));
+
+        service.generateIfEmpty(9L);
+
+        verifyNoInteractions(aiService);
+        verify(jdbcTemplate, never()).update(anyString(), any(Object[].class));
+    }
+
+    @Test
+    void generateIfEmpty_标签为空时生成并写回() {
+        when(activityMapper.selectById(9L)).thenReturn(activity(9L, null));
+        Map<String, Object> ai = new HashMap<>();
+        ai.put("answer", "亲子活动,户外徒步,自然教育");
+        when(aiService.sendMessage(anyString(), anyString(), anyString(), any())).thenReturn(ai);
+
+        service.generateIfEmpty(9L);
+
+        verify(jdbcTemplate).update("UPDATE activities SET tags = ? WHERE id = ?",
+                "亲子活动,户外徒步,自然教育", 9L);
+    }
+
+    @Test
+    void generateIfEmpty_AI无返回时不写库() {
+        when(activityMapper.selectById(9L)).thenReturn(activity(9L, ""));
+        when(aiService.sendMessage(anyString(), anyString(), anyString(), any()))
+                .thenReturn(Collections.emptyMap());
+
+        service.generateIfEmpty(9L);
+
+        verify(jdbcTemplate, never()).update(anyString(), any(Object[].class));
+    }
+}

+ 20 - 0
cfc-frontend/pages/activity/activity-detail/activity-detail.vue

@@ -28,6 +28,9 @@
 
 
       <view class="info-section">
       <view class="info-section">
         <text class="activity-title">{{ activity.title }}</text>
         <text class="activity-title">{{ activity.title }}</text>
+        <view class="activity-tags" v-if="tagList(activity.tags).length">
+          <text class="tag-chip" v-for="(t, ti) in tagList(activity.tags)" :key="ti">{{ t }}</text>
+        </view>
 
 
         <!-- 活动状态标签 -->
         <!-- 活动状态标签 -->
         <view class="status-row">
         <view class="status-row">
@@ -369,6 +372,9 @@ export default {
     this.fontScale = getFontScale()
     this.fontScale = getFontScale()
   },
   },
   methods: {
   methods: {
+    tagList: function(tags) {
+      return String(tags || '').split(/[,,、]/).map(function(t) { return t.trim() }).filter(function(t) { return !!t })
+    },
     // ===== 工具方法 =====
     // ===== 工具方法 =====
     getImageUrl: function(path) {
     getImageUrl: function(path) {
       if (!path || path === 'null' || path === 'undefined' || path === '/null') return ''
       if (!path || path === 'null' || path === 'undefined' || path === '/null') return ''
@@ -985,6 +991,20 @@ export default {
 .info-section {
 .info-section {
   padding: 30rpx;
   padding: 30rpx;
 }
 }
+.activity-tags {
+  display: flex;
+  flex-wrap: wrap;
+  margin-top: 16rpx;
+}
+.tag-chip {
+  font-size: 22rpx;
+  color: $brand-primary;
+  background: $uni-primary-pale;
+  padding: 3rpx 14rpx;
+  border-radius: 6rpx;
+  margin-right: 10rpx;
+  margin-bottom: 6rpx;
+}
 .activity-title {
 .activity-title {
   font-size: calc(36rpx * var(--font-scale, 1));
   font-size: calc(36rpx * var(--font-scale, 1));
   font-weight: bold;
   font-weight: bold;

+ 29 - 0
cfc-frontend/pages/activity/index.vue

@@ -56,6 +56,9 @@
              lazy-load />
              lazy-load />
             <view class="card-info">
             <view class="card-info">
               <text class="card-title">{{ act.title }}</text>
               <text class="card-title">{{ act.title }}</text>
+              <view class="card-tags" v-if="tagList(act.tags).length">
+                <text class="tag-chip" v-for="(t, ti) in tagList(act.tags)" :key="ti">{{ t }}</text>
+              </view>
               <text class="card-meta" v-if="act.registrantName">报名人:{{ act.registrantName }}</text>
               <text class="card-meta" v-if="act.registrantName">报名人:{{ act.registrantName }}</text>
               <text class="card-meta">开始 {{ formatDateTime(act.startTime) }}{{ act.endTime ? ' 结束 ' + formatDateTime(act.endTime) : '' }}</text>
               <text class="card-meta">开始 {{ formatDateTime(act.startTime) }}{{ act.endTime ? ' 结束 ' + formatDateTime(act.endTime) : '' }}</text>
               <text class="card-meta" v-if="act.location">📍 {{ act.location }}</text>
               <text class="card-meta" v-if="act.location">📍 {{ act.location }}</text>
@@ -130,6 +133,9 @@
              lazy-load />
              lazy-load />
             <view class="card-info">
             <view class="card-info">
               <text class="card-title">{{ act.title }}</text>
               <text class="card-title">{{ act.title }}</text>
+              <view class="card-tags" v-if="tagList(act.tags).length">
+                <text class="tag-chip" v-for="(t, ti) in tagList(act.tags)" :key="ti">{{ t }}</text>
+              </view>
               <text class="card-meta">开始 {{ formatDateTime(act.startTime) }}{{ act.endTime ? ' 结束 ' + formatDateTime(act.endTime) : '' }}</text>
               <text class="card-meta">开始 {{ formatDateTime(act.startTime) }}{{ act.endTime ? ' 结束 ' + formatDateTime(act.endTime) : '' }}</text>
               <text class="card-desc" v-if="act.intro || act.description">{{ act.intro || act.description }}</text>
               <text class="card-desc" v-if="act.intro || act.description">{{ act.intro || act.description }}</text>
               <text class="card-meta" v-if="act.location">📍 {{ act.location }}</text>
               <text class="card-meta" v-if="act.location">📍 {{ act.location }}</text>
@@ -195,6 +201,9 @@
              lazy-load />
              lazy-load />
             <view class="card-info">
             <view class="card-info">
               <text class="card-title">{{ act.title }}</text>
               <text class="card-title">{{ act.title }}</text>
+              <view class="card-tags" v-if="tagList(act.tags).length">
+                <text class="tag-chip" v-for="(t, ti) in tagList(act.tags)" :key="ti">{{ t }}</text>
+              </view>
               <text class="card-meta">开始 {{ formatDateTime(act.startTime) }}{{ act.endTime ? ' 结束 ' + formatDateTime(act.endTime) : '' }}</text>
               <text class="card-meta">开始 {{ formatDateTime(act.startTime) }}{{ act.endTime ? ' 结束 ' + formatDateTime(act.endTime) : '' }}</text>
               <text class="card-meta" v-if="act.location">📍 {{ act.location }}</text>
               <text class="card-meta" v-if="act.location">📍 {{ act.location }}</text>
               <view class="card-bottom">
               <view class="card-bottom">
@@ -273,6 +282,9 @@
              lazy-load />
              lazy-load />
             <view class="card-info">
             <view class="card-info">
               <text class="card-title">{{ act.title }}</text>
               <text class="card-title">{{ act.title }}</text>
+              <view class="card-tags" v-if="tagList(act.tags).length">
+                <text class="tag-chip" v-for="(t, ti) in tagList(act.tags)" :key="ti">{{ t }}</text>
+              </view>
               <text class="card-meta">{{ act.startTime }}</text>
               <text class="card-meta">{{ act.startTime }}</text>
               <view class="card-bottom">
               <view class="card-bottom">
                 <text class="card-status" :class="'status-' + (act.status || 'draft')">
                 <text class="card-status" :class="'status-' + (act.status || 'draft')">
@@ -386,6 +398,9 @@ export default {
     }
     }
   },
   },
   methods: {
   methods: {
+    tagList: function(tags) {
+      return String(tags || '').split(/[,,、]/).map(function(t) { return t.trim() }).filter(function(t) { return !!t })
+    },
     // 活动管理权限判定:与后端 ActivityController.hasActivityManageRole 对齐。
     // 活动管理权限判定:与后端 ActivityController.hasActivityManageRole 对齐。
     // 后台角色(admin / activity_admin / activity_manager)→ 可直接发布;
     // 后台角色(admin / activity_admin / activity_manager)→ 可直接发布;
     // 其余账号查服务商入驻状态,vendorType=activity_provider 且审核通过 → 可管理(只能提交审核)。
     // 其余账号查服务商入驻状态,vendorType=activity_provider 且审核通过 → 可管理(只能提交审核)。
@@ -994,6 +1009,20 @@ uni.showToast({ title: '请先选择家庭成员身份', icon: 'none' })
   flex-direction: column;
   flex-direction: column;
 }
 }
 
 
+.card-tags {
+  display: flex;
+  flex-wrap: wrap;
+  margin-top: 10rpx;
+}
+.tag-chip {
+  font-size: 22rpx;
+  color: $brand-primary;
+  background: $uni-primary-pale;
+  padding: 2rpx 12rpx;
+  border-radius: 6rpx;
+  margin-right: 8rpx;
+  margin-bottom: 4rpx;
+}
 .card-title {
 .card-title {
   font-size: calc(28rpx * var(--font-scale, 1));
   font-size: calc(28rpx * var(--font-scale, 1));
   font-weight: 600;
   font-weight: 600;

+ 4 - 5
cfc-web/src/router/index.js

@@ -696,11 +696,10 @@ const routes = [
           meta: { title: '通知模板管理', perm: 'admin:all' }
           meta: { title: '通知模板管理', perm: 'admin:all' }
         },
         },
         {
         {
-        path: 'activity-review',
-        name: 'ActivityReview',
-        component: () => import('@/views/admin/ActivityReview.vue'),
-        meta: { title: '活动审核', perm: 'activity:review' }
-      },
+          // 旧版活动审核页已由审核中心统一承接
+          path: 'activity-review',
+          redirect: '/review-center?tab=activity'
+        },
       {
       {
         path: 'activity-registration-review',
         path: 'activity-registration-review',
         name: 'ActivityRegistrationReview',
         name: 'ActivityRegistrationReview',

+ 11 - 2
cfc-web/src/views/admin/ActivityEdit.vue

@@ -79,6 +79,10 @@
           <dimension-weight-picker v-model="form.dimensionWeights" :show-presets="true" :readonly="readonly" />
           <dimension-weight-picker v-model="form.dimensionWeights" :show-presets="true" :readonly="readonly" />
         </el-form-item>
         </el-form-item>
 
 
+        <el-form-item label="活动标签">
+          <el-input v-model="form.tags" :disabled="readonly" placeholder="多个标签用逗号分隔;留空则在提交审核时自动生成" />
+        </el-form-item>
+
         <el-form-item label="活动地点">
         <el-form-item label="活动地点">
           <el-input v-model="form.location" placeholder="请输入活动地点" :disabled="readonly" />
           <el-input v-model="form.location" placeholder="请输入活动地点" :disabled="readonly" />
         </el-form-item>
         </el-form-item>
@@ -198,6 +202,7 @@ export default {
         description: '',
         description: '',
         dimensionCode: '',
         dimensionCode: '',
         dimensionWeights: [],
         dimensionWeights: [],
+        tags: '',
         location: '',
         location: '',
         maxParticipants: null,
         maxParticipants: null,
         priceYuan: null,
         priceYuan: null,
@@ -318,6 +323,7 @@ export default {
             description: d.description || '',
             description: d.description || '',
             dimensionCode: d.dimensionCode || '',
             dimensionCode: d.dimensionCode || '',
             dimensionWeights: [],
             dimensionWeights: [],
+            tags: d.tags || '',
             location: d.location || '',
             location: d.location || '',
             maxParticipants: d.maxParticipants || null,
             maxParticipants: d.maxParticipants || null,
             priceYuan: d.price ? d.price / 100 : null,
             priceYuan: d.price ? d.price / 100 : null,
@@ -397,7 +403,8 @@ export default {
           visibility: this.form.visibility,
           visibility: this.form.visibility,
           visibleScope: this.form.visibleScope,
           visibleScope: this.form.visibleScope,
           visibleTo: this.form.visibleTo,
           visibleTo: this.form.visibleTo,
-          checkinPoints: this.form.checkinPoints || 0
+          checkinPoints: this.form.checkinPoints || 0,
+          tags: this.form.tags || ''
         }
         }
         if (this.isEdit) {
         if (this.isEdit) {
           // 编辑模式:保存后直接返回列表
           // 编辑模式:保存后直接返回列表
@@ -439,6 +446,7 @@ export default {
         description: '',
         description: '',
         dimensionCode: '',
         dimensionCode: '',
         dimensionWeights: [],
         dimensionWeights: [],
+        tags: '',
         location: '',
         location: '',
         maxParticipants: null,
         maxParticipants: null,
         priceYuan: null,
         priceYuan: null,
@@ -504,7 +512,8 @@ export default {
           visibility: this.form.visibility,
           visibility: this.form.visibility,
           visibleScope: this.form.visibleScope,
           visibleScope: this.form.visibleScope,
           visibleTo: this.form.visibleTo,
           visibleTo: this.form.visibleTo,
-          checkinPoints: this.form.checkinPoints || 0
+          checkinPoints: this.form.checkinPoints || 0,
+          tags: this.form.tags || ''
         }
         }
         const activityId = this.form._activityId || this.$route.query.id
         const activityId = this.form._activityId || this.$route.query.id
         if (
         if (

+ 6 - 15
cfc-web/src/views/admin/review/ActivityAudit.vue

@@ -60,14 +60,8 @@
         <el-form-item label="活动内容">
         <el-form-item label="活动内容">
           <div style="white-space:pre-wrap;">{{ currentRow.description || currentRow.content || '-' }}</div>
           <div style="white-space:pre-wrap;">{{ currentRow.description || currentRow.content || '-' }}</div>
         </el-form-item>
         </el-form-item>
-        <el-form-item label="所属维度" v-if="currentRow && currentRow.auditStatus === 'pending'">
-          <el-select v-model="auditDimensionCode" placeholder="请选择维度" style="width:100%">
-            <el-option label="身 · 主动健康" value="body" />
-            <el-option label="心 · 情感丰盈" value="mind" />
-            <el-option label="智 · 赋能未来" value="wisdom" />
-            <el-option label="行 · 知行合一" value="action" />
-            <el-option label="富 · 物质精神" value="wealth" />
-          </el-select>
+        <el-form-item label="活动标签" v-if="currentRow && currentRow.tags">
+          <el-tag v-for="tag in tagList(currentRow.tags)" :key="tag" size="small" style="margin-right:6px;">{{ tag }}</el-tag>
         </el-form-item>
         </el-form-item>
         <el-form-item label="五维权重" v-if="currentRow && currentRow.auditStatus === 'pending'">
         <el-form-item label="五维权重" v-if="currentRow && currentRow.auditStatus === 'pending'">
           <dimension-weight-picker v-model="auditWeights" :show-presets="true" />
           <dimension-weight-picker v-model="auditWeights" :show-presets="true" />
@@ -102,7 +96,7 @@ export default {
       list: [], loading: false, keyword: '', statusFilter: 'pending',
       list: [], loading: false, keyword: '', statusFilter: 'pending',
       sortableColumns: { id: 'ID', title: '活动名称', auditReason: '原因' },
       sortableColumns: { id: 'ID', title: '活动名称', auditReason: '原因' },
       detailVisible: false, currentRow: null, auditLoading: false,
       detailVisible: false, currentRow: null, auditLoading: false,
-      auditDimensionCode: '', auditWeights: []
+      auditWeights: []
     }
     }
   },
   },
   mounted() { this.loadData() },
   mounted() { this.loadData() },
@@ -126,9 +120,11 @@ export default {
         dimension, weight: 20, enabled: true
         dimension, weight: 20, enabled: true
       }))
       }))
     },
     },
+    tagList(tags) {
+      return (tags || '').split(/[,,、]/).map(t => t.trim()).filter(Boolean)
+    },
     async showDetail(row) {
     async showDetail(row) {
       this.currentRow = row
       this.currentRow = row
-      this.auditDimensionCode = row.dimensionCode || ''
       this.auditWeights = this.defaultWeights()
       this.auditWeights = this.defaultWeights()
       this.detailVisible = true
       this.detailVisible = true
       if (row.auditStatus !== 'pending') return
       if (row.auditStatus !== 'pending') return
@@ -145,10 +141,6 @@ export default {
       }
       }
     },
     },
     handleApprove() {
     handleApprove() {
-      if (!this.auditDimensionCode) {
-        this.$message.warning('审核通过时必须选择所属维度')
-        return
-      }
       const weights = this.auditWeights || []
       const weights = this.auditWeights || []
       const missing = ['body', 'mind', 'wisdom', 'action', 'wealth']
       const missing = ['body', 'mind', 'wisdom', 'action', 'wealth']
         .some(d => !weights.find(w => w.dimension === d))
         .some(d => !weights.find(w => w.dimension === d))
@@ -166,7 +158,6 @@ export default {
       try {
       try {
         const params = { id: this.currentRow.id, action, rejectReason: action === 'rejected' ? reason : '' }
         const params = { id: this.currentRow.id, action, rejectReason: action === 'rejected' ? reason : '' }
         if (action === 'approved') {
         if (action === 'approved') {
-          params.dimensionCode = this.auditDimensionCode
           params.dimensionWeights = this.auditWeights
           params.dimensionWeights = this.auditWeights
         }
         }
         await auditActivity(params)
         await auditActivity(params)

+ 4 - 16
cfc-web/src/views/admin/review/__tests__/ActivityAudit.spec.js

@@ -1,6 +1,6 @@
 import { shallowMount, createLocalVue } from '@vue/test-utils'
 import { shallowMount, createLocalVue } from '@vue/test-utils'
 import ActivityAudit from '../ActivityAudit.vue'
 import ActivityAudit from '../ActivityAudit.vue'
-import { auditActivity, getActivityList } from '@/api/activity'
+import { auditActivity } from '@/api/activity'
 import { getDimensionWeights } from '@/api/dimension-weight'
 import { getDimensionWeights } from '@/api/dimension-weight'
 
 
 jest.mock('@/api/activity', () => ({
 jest.mock('@/api/activity', () => ({
@@ -42,20 +42,9 @@ describe('ActivityAudit 审核提交', () => {
     jest.clearAllMocks()
     jest.clearAllMocks()
   })
   })
 
 
-  it('未选择所属维度时不提交', () => {
-    const wrapper = mount()
-    wrapper.vm.currentRow = { id: 9, auditStatus: 'pending' }
-    wrapper.vm.auditDimensionCode = ''
-    wrapper.vm.handleApprove()
-
-    expect(wrapper.vm.$message.warning).toHaveBeenCalled()
-    expect(auditActivity).not.toHaveBeenCalled()
-  })
-
   it('五维权重合计不为 100% 时不提交', () => {
   it('五维权重合计不为 100% 时不提交', () => {
     const wrapper = mount()
     const wrapper = mount()
     wrapper.vm.currentRow = { id: 9, auditStatus: 'pending' }
     wrapper.vm.currentRow = { id: 9, auditStatus: 'pending' }
-    wrapper.vm.auditDimensionCode = 'body'
     wrapper.vm.auditWeights = validWeights().map((w, i) => ({ ...w, weight: i === 0 ? 19 : 20 }))
     wrapper.vm.auditWeights = validWeights().map((w, i) => ({ ...w, weight: i === 0 ? 19 : 20 }))
     wrapper.vm.handleApprove()
     wrapper.vm.handleApprove()
 
 
@@ -63,10 +52,9 @@ describe('ActivityAudit 审核提交', () => {
     expect(auditActivity).not.toHaveBeenCalled()
     expect(auditActivity).not.toHaveBeenCalled()
   })
   })
 
 
-  it('校验通过后以 approved 提交并携带维度与五维权重', async () => {
+  it('只需五维权重,不再提交所属维度', async () => {
     const wrapper = mount()
     const wrapper = mount()
     wrapper.vm.currentRow = { id: 9, auditStatus: 'pending' }
     wrapper.vm.currentRow = { id: 9, auditStatus: 'pending' }
-    wrapper.vm.auditDimensionCode = 'body'
     wrapper.vm.auditWeights = validWeights()
     wrapper.vm.auditWeights = validWeights()
     wrapper.vm.handleApprove()
     wrapper.vm.handleApprove()
     await flush()
     await flush()
@@ -75,9 +63,9 @@ describe('ActivityAudit 审核提交', () => {
       id: 9,
       id: 9,
       action: 'approved',
       action: 'approved',
       rejectReason: '',
       rejectReason: '',
-      dimensionCode: 'body',
       dimensionWeights: validWeights()
       dimensionWeights: validWeights()
     })
     })
+    expect(auditActivity.mock.calls[0][0].dimensionCode).toBeUndefined()
   })
   })
 
 
   it('驳回时以 rejected 提交并携带原因', async () => {
   it('驳回时以 rejected 提交并携带原因', async () => {
@@ -104,7 +92,7 @@ describe('ActivityAudit 审核提交', () => {
       ]
       ]
     })
     })
     const wrapper = mount()
     const wrapper = mount()
-    wrapper.vm.showDetail({ id: 9, auditStatus: 'pending', dimensionCode: 'body' })
+    wrapper.vm.showDetail({ id: 9, auditStatus: 'pending' })
     await flush()
     await flush()
 
 
     expect(getDimensionWeights).toHaveBeenCalledWith('activity', 9)
     expect(getDimensionWeights).toHaveBeenCalledWith('activity', 9)