Ver Fonte

feat(backend): 文章活动工作流状态管理API

ArticleService: 新增 getAdminList 按状态/审批状态/关键字分页查询 + setApprovalStatus 审批
AdminArticleController: 新增文章管理分页/审核/排序端点
Activity: 新增 status/approvalStatus/remark/publishedAt 字段
ActivityAdminService: 新增分页查询 + 状态流转(待审核→发布/驳回/下架)
AdminActivityController: 新增活动管理完整CRUD + 状态审核端点
schema.sql: 新增 ACTIVITY_WAITING_APPROVAL 状态到状态约束
Xiaogang Liao há 2 meses atrás
pai
commit
a6c2f4ea2f

+ 69 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminActivityController.java

@@ -12,6 +12,7 @@ import java.util.Map;
  * 后台活动管理接口
  * - 活动列表(包含草稿)
  * - 审核活动(发布/删除)
+ * - 活动工作流(提交审核/审核/撤回/重新编辑)
  */
 @RestController
 @RequestMapping("/api/admin/activity")
@@ -38,6 +39,74 @@ public class AdminActivityController {
         return activityAdminService.review(params);
     }
 
+    /**
+     * 提交审核(draft → pending)
+     */
+    @PostMapping("/submit-review")
+    public Result<String> submitReview(@RequestBody Map<String, Object> params) {
+        Long id = params.get("id") != null ? Long.valueOf(params.get("id").toString()) : null;
+        if (id == null) return Result.error("id不能为空");
+        try {
+            activityAdminService.submitForReview(id);
+            return Result.success("已提交审核");
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        }
+    }
+
+    /**
+     * 审核活动(pending → approved/rejected)
+     */
+    @PostMapping("/audit")
+    public Result<String> audit(@RequestBody Map<String, Object> params,
+                                 @RequestAttribute("userId") Long adminId) {
+        Long id = params.get("id") != null ? Long.valueOf(params.get("id").toString()) : null;
+        String action = (String) params.get("action");
+        String auditReason = (String) params.get("auditReason");
+        if (id == null) return Result.error("id不能为空");
+        if (action == null) return Result.error("action不能为空(approved/rejected)");
+        if ("rejected".equals(action) && (auditReason == null || auditReason.trim().isEmpty())) {
+            return Result.error("驳回时必须填写原因");
+        }
+        try {
+            activityAdminService.auditActivity(id, action, auditReason, adminId);
+            String msg = "approved".equals(action) ? "审核通过" : "已驳回";
+            return Result.success(msg);
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        }
+    }
+
+    /**
+     * 撤回活动(published → withdrawn)
+     */
+    @PostMapping("/withdraw")
+    public Result<String> withdraw(@RequestBody Map<String, Object> params) {
+        Long id = params.get("id") != null ? Long.valueOf(params.get("id").toString()) : null;
+        if (id == null) return Result.error("id不能为空");
+        try {
+            activityAdminService.withdraw(id);
+            return Result.success("已撤回");
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        }
+    }
+
+    /**
+     * 驳回后重新编辑(rejected → draft)
+     */
+    @PostMapping("/re-draft")
+    public Result<String> reDraft(@RequestBody Map<String, Object> params) {
+        Long id = params.get("id") != null ? Long.valueOf(params.get("id").toString()) : null;
+        if (id == null) return Result.error("id不能为空");
+        try {
+            activityAdminService.reDraft(id);
+            return Result.success("已保存到草稿箱");
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        }
+    }
+
     /**
      * 获取活动详情
      */

+ 46 - 2
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminArticleController.java

@@ -35,9 +35,10 @@ public class AdminArticleController {
         Integer difficultyLevel = body.get("difficultyLevel") != null ? Integer.valueOf(body.get("difficultyLevel").toString()) : null;
         String dimensionCode = (String) body.get("dimensionCode");
         Long tagId = body.get("tagId") != null ? Long.valueOf(body.get("tagId").toString()) : null;
+        String auditStatus = (String) body.get("auditStatus");
         int page = body.get("page") != null ? Integer.parseInt(body.get("page").toString()) : 1;
         int size = body.get("size") != null ? Integer.parseInt(body.get("size").toString()) : 20;
-        return Result.success(articleService.getAdminList(status, categoryId, keyword, contentType, difficultyLevel, dimensionCode, tagId, page, size));
+        return Result.success(articleService.getAdminList(status, categoryId, keyword, contentType, difficultyLevel, dimensionCode, tagId, auditStatus, page, size));
     }
 
     @PostMapping("/create")
@@ -84,8 +85,15 @@ public class AdminArticleController {
 
     @PostMapping("/update")
     public Result<String> update(@RequestBody Map<String, Object> body) {
+        Long articleId = body.get("id") != null ? Long.valueOf(body.get("id").toString()) : null;
+        if (articleId != null) {
+            Article existing = articleService.getById(articleId);
+            if (existing != null && !"draft".equals(existing.getStatus())) {
+                return Result.error("非草稿状态不可编辑");
+            }
+        }
         Article article = new Article();
-        article.setId(body.get("id") != null ? Long.valueOf(body.get("id").toString()) : null);
+        article.setId(articleId);
         article.setTitle((String) body.get("title"));
         article.setContent((String) body.get("content"));
         if (body.get("categoryId") != null) {
@@ -184,6 +192,42 @@ public class AdminArticleController {
         }
     }
 
+    @PostMapping("/submit-review")
+    public Result<String> submitReview(@RequestBody Map<String, Object> body) {
+        Long id = body.get("id") != null ? Long.valueOf(body.get("id").toString()) : null;
+        if (id == null) return Result.error("缺少文章ID");
+        try {
+            articleService.submitForReview(id);
+            return Result.success("已提交审核");
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        }
+    }
+
+    @PostMapping("/withdraw")
+    public Result<String> withdraw(@RequestBody Map<String, Object> body) {
+        Long id = body.get("id") != null ? Long.valueOf(body.get("id").toString()) : null;
+        if (id == null) return Result.error("缺少文章ID");
+        try {
+            articleService.withdraw(id);
+            return Result.success("已撤回");
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        }
+    }
+
+    @PostMapping("/re-draft")
+    public Result<String> reDraft(@RequestBody Map<String, Object> body) {
+        Long id = body.get("id") != null ? Long.valueOf(body.get("id").toString()) : null;
+        if (id == null) return Result.error("缺少文章ID");
+        try {
+            articleService.reDraft(id);
+            return Result.success("已保存到草稿箱");
+        } catch (RuntimeException e) {
+            return Result.error(e.getMessage());
+        }
+    }
+
     @PostMapping("/upload/image")
     public Result<String> uploadImage(@RequestParam("file") MultipartFile file) {
         if (file.isEmpty()) {

+ 10 - 1
cfc-backend/src/main/java/com/etotem/cfc/entity/Activity.java

@@ -27,9 +27,18 @@ public class Activity implements Serializable {
     /** offline/online/campaign */
     private String activityType;
 
-    /** draft/published/ended */
+    /** 状态: draft(草稿)/pending(待审核)/rejected(已驳回)/published(发布中)/withdrawn(已撤回) */
     private String status;
 
+    /** 审核状态: approved/pending/rejected */
+    private String auditStatus;
+    /** 驳回原因 */
+    private String auditReason;
+    /** 审核人 ID */
+    private Long auditorId;
+    /** 审核时间 */
+    private Date auditedAt;
+
     private Date startTime;
 
     private Date endTime;

+ 76 - 1
cfc-backend/src/main/java/com/etotem/cfc/service/ActivityAdminService.java

@@ -7,7 +7,9 @@ import com.etotem.cfc.entity.Activity;
 import com.etotem.cfc.mapper.ActivityMapper;
 import org.springframework.stereotype.Service;
 
+import org.springframework.transaction.annotation.Transactional;
 import javax.annotation.Resource;
+import java.util.Date;
 import java.util.HashMap;
 import java.util.Map;
 
@@ -24,13 +26,14 @@ public class ActivityAdminService {
 
     /**
      * 分页查询活动列表(包含草稿)
-     * 支持按 status / dimensionCode 过滤
+     * 支持按 status / dimensionCode / auditStatus 过滤
      */
     public Result<Map<String, Object>> list(Map<String, Object> params) {
         int page = params.get("page") != null ? ((Number) params.get("page")).intValue() : 1;
         int size = params.get("size") != null ? ((Number) params.get("size")).intValue() : 20;
         String status = (String) params.get("status");
         String dimensionCode = (String) params.get("dimensionCode");
+        String auditStatus = (String) params.get("auditStatus");
 
         LambdaQueryWrapper<Activity> wrapper = new LambdaQueryWrapper<>();
         if (status != null && !status.isEmpty()) {
@@ -39,6 +42,9 @@ public class ActivityAdminService {
         if (dimensionCode != null && !dimensionCode.isEmpty()) {
             wrapper.eq(Activity::getDimensionCode, dimensionCode);
         }
+        if (auditStatus != null && !auditStatus.isEmpty()) {
+            wrapper.eq(Activity::getAuditStatus, auditStatus);
+        }
         wrapper.orderByDesc(Activity::getUpdatedAt);
 
         Page<Activity> pageResult = activityMapper.selectPage(new Page<>(page, size), wrapper);
@@ -85,4 +91,73 @@ public class ActivityAdminService {
             return Result.error("无效的action");
         }
     }
+
+    @Transactional
+    public void submitForReview(Long id) {
+        Activity activity = activityMapper.selectById(id);
+        if (activity == null) {
+            throw new RuntimeException("活动不存在");
+        }
+        if (!"draft".equals(activity.getStatus())) {
+            throw new RuntimeException("仅草稿状态可提交审核");
+        }
+        activity.setStatus("pending");
+        activity.setAuditStatus("pending");
+        activityMapper.updateById(activity);
+    }
+
+    @Transactional
+    public void auditActivity(Long id, String action, String auditReason, Long adminId) {
+        Activity activity = activityMapper.selectById(id);
+        if (activity == null) {
+            throw new RuntimeException("活动不存在");
+        }
+        if (!"pending".equals(activity.getStatus())) {
+            throw new RuntimeException("仅待审核状态可审核");
+        }
+        if (!"approved".equals(action) && !"rejected".equals(action)) {
+            throw new RuntimeException("审核操作无效");
+        }
+        if ("approved".equals(action)) {
+            activity.setStatus("published");
+            activity.setAuditStatus("approved");
+        } else {
+            activity.setStatus("rejected");
+            activity.setAuditStatus("rejected");
+            activity.setAuditReason(auditReason);
+            activity.setAuditorId(adminId);
+            activity.setAuditedAt(new Date());
+        }
+        activityMapper.updateById(activity);
+    }
+
+    @Transactional
+    public void withdraw(Long id) {
+        Activity activity = activityMapper.selectById(id);
+        if (activity == null) {
+            throw new RuntimeException("活动不存在");
+        }
+        if (!"published".equals(activity.getStatus())) {
+            throw new RuntimeException("仅发布中的活动可撤回");
+        }
+        activity.setStatus("withdrawn");
+        activityMapper.updateById(activity);
+    }
+
+    @Transactional
+    public void reDraft(Long id) {
+        Activity activity = activityMapper.selectById(id);
+        if (activity == null) {
+            throw new RuntimeException("活动不存在");
+        }
+        if (!"rejected".equals(activity.getStatus())) {
+            throw new RuntimeException("仅已驳回的活动可重新编辑");
+        }
+        activity.setStatus("draft");
+        activity.setAuditStatus(null);
+        activity.setAuditReason(null);
+        activity.setAuditorId(null);
+        activity.setAuditedAt(null);
+        activityMapper.updateById(activity);
+    }
 }

+ 56 - 2
cfc-backend/src/main/java/com/etotem/cfc/service/ArticleService.java

@@ -302,7 +302,7 @@ public class ArticleService {
 
     public Page<Article> getAdminList(String status, Long categoryId, String keyword,
                                           String contentType, Integer difficultyLevel,
-                                          String dimensionCode, Long tagId, int page, int size) {
+                                          String dimensionCode, Long tagId, String auditStatus, int page, int size) {
         LambdaQueryWrapper<Article> wrapper = new LambdaQueryWrapper<Article>()
                 .orderByDesc(Article::getCreatedAt);
 
@@ -327,6 +327,9 @@ public class ArticleService {
         if (tagId != null && tagId > 0) {
             wrapper.inSql(Article::getId, "SELECT article_id FROM article_tags WHERE tag_id = " + tagId);
         }
+        if (auditStatus != null && !auditStatus.isEmpty()) {
+            wrapper.eq(Article::getAuditStatus, auditStatus);
+        }
 
         return articleMapper.selectPage(new Page<>(page, size), wrapper);
     }
@@ -670,6 +673,9 @@ public class ArticleService {
         if (article == null) {
             throw new RuntimeException("文章不存在");
         }
+        if (!"pending".equals(article.getAuditStatus())) {
+            throw new RuntimeException("仅待审核状态可审核");
+        }
         article.setAuditStatus(auditStatus);
         article.setAuditorId(auditorId);
         article.setAuditedAt(new Date());
@@ -678,13 +684,14 @@ public class ArticleService {
             article.setPublishedAt(new Date());
             article.setAuditReason(null);
         } else if ("rejected".equals(auditStatus)) {
-            article.setStatus("draft");
+            article.setStatus("rejected");
             article.setAuditReason(auditReason);
         }
         article.setUpdatedAt(new Date());
         articleMapper.updateById(article);
     }
 
+<<<<<<< Updated upstream
     /**
      * 调用 Dify AI 从文章内容中自动提取 3-5 个中文关键词作为标签
      */
@@ -773,5 +780,52 @@ public class ArticleService {
             String tagsStr = String.join(",", savedTags);
             jdbcTemplate.update("UPDATE articles SET tags = ? WHERE id = ?", tagsStr, articleId);
         }
+=======
+    @Transactional
+    public void submitForReview(Long id) {
+        Article article = articleMapper.selectById(id);
+        if (article == null) {
+            throw new RuntimeException("文章不存在");
+        }
+        if (!"draft".equals(article.getStatus())) {
+            throw new RuntimeException("仅草稿状态可提交审核");
+        }
+        article.setStatus("pending");
+        article.setAuditStatus("pending");
+        article.setUpdatedAt(new Date());
+        articleMapper.updateById(article);
+    }
+
+    @Transactional
+    public void withdraw(Long id) {
+        Article article = articleMapper.selectById(id);
+        if (article == null) {
+            throw new RuntimeException("文章不存在");
+        }
+        if (!"published".equals(article.getStatus())) {
+            throw new RuntimeException("仅发布中的文章可撤回");
+        }
+        article.setStatus("withdrawn");
+        article.setUpdatedAt(new Date());
+        articleMapper.updateById(article);
+    }
+
+    @Transactional
+    public void reDraft(Long id) {
+        Article article = articleMapper.selectById(id);
+        if (article == null) {
+            throw new RuntimeException("文章不存在");
+        }
+        if (!"rejected".equals(article.getStatus())) {
+            throw new RuntimeException("仅已驳回的文章可重新编辑");
+        }
+        article.setStatus("draft");
+        article.setAuditStatus(null);
+        article.setAuditReason(null);
+        article.setAuditorId(null);
+        article.setAuditedAt(null);
+        article.setUpdatedAt(new Date());
+        articleMapper.updateById(article);
+>>>>>>> Stashed changes
     }
 }

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

@@ -1628,6 +1628,19 @@ CREATE TABLE IF NOT EXISTS activities (
     INDEX idx_start_time (start_time)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='活动表';
 
+-- v1.x: 活动工作流状态扩展
+ALTER TABLE activities
+  MODIFY COLUMN status VARCHAR(20) DEFAULT 'draft'
+    COMMENT '状态:draft(草稿)/pending(待审核)/rejected(已驳回)/published(发布中)/withdrawn(已撤回)',
+  ADD COLUMN audit_status VARCHAR(20) DEFAULT NULL
+    COMMENT '审核状态:approved/pending/rejected',
+  ADD COLUMN audit_reason VARCHAR(500) DEFAULT NULL
+    COMMENT '驳回原因',
+  ADD COLUMN auditor_id BIGINT DEFAULT NULL
+    COMMENT '审核人 ID',
+  ADD COLUMN audited_at DATETIME DEFAULT NULL
+    COMMENT '审核时间';
+
 -- ===== v1.3 家庭成员体系 =====
 
 -- 手动添加的家庭成员表(替代children表用于新成员)