欢迎光临
我们一直在努力

华为云码道 CodeArts 实战:AI 智能体独立开发一个多平台内容分发管理台(架构设计 + 开发实录)

一键开通华为云码道 CodeArts 代码智能体:https://developer.huaweicloud.com/codeartsco.html?source=dmzntgwatomgit1&sourcead=dmzntgwatomgithd

摘要:本文是华为云码道 AI 编程挑战赛的参赛实测记录。我用 CodeArts 代码智能体独立开发了一个全栈 Web 应用「ArticlePilot 多平台内容分发管理台」,解决技术创作者同一篇文章要发多个平台、格式与发布状态全靠人肉维护的问题。文章完整记录选题背景、架构设计(平台适配器模式 + 稿件状态机 + RBAC)、AI 辅助开发的实操流程、功能演示与踩坑记录,源码已在 AtomGit 开源。

技术栈版本:码道 CodeArts 代码智能体(2026-09 挑战赛版本)| Spring Boot 3.5.x | Java 21 | Vue 3.5 + TypeScript | MySQL 8 | Docker Compose | 实测时间: 2026-09

在这里插入图片描述

一、项目背景:一个真实的多平台发布痛点

先交代我为什么要做这个项目。作为一名技术博主,我的工作流是这样的:一篇文章写完后,要发布到 CSDN、51CTO、腾讯云、阿里云、华为云、掘金、微信公众号等多个平台。每个平台的标签体系不同、封面尺寸不同、摘要字数限制不同、发布状态也各不相同。

在此之前,这套流程全靠 Excel 表格和本地文件夹人肉维护。哪篇稿子发到了哪个平台、哪个平台被拒稿了、哪个平台的标签还没适配,全靠记忆和翻记录。稿子一多,这套管理方式立刻崩坏——漏发、错发、状态对不上是常态。

所以这次参加华为云码道 AI 编程挑战赛,我决定不做一个"演示型"作品,而是直接把这条真实工作流工具化:做一个多平台内容分发管理台,让选题、稿件、平台适配、发布状态在一个地方闭环。这个选题有个额外好处:开发过程中遇到的每一个痛点,都是我真实经历过的,文章里不需要编任何故事。

1.1 作品功能一览

模块功能
用户与权限 JWT 登录、管理员/作者/访客三角色 RBAC
选题库 选题增删改查、状态流转(IDEA → PLANNED → WRITING → ARCHIVED)
稿件管理 Markdown 编辑预览、乐观锁、版本快照、稿件状态机
平台适配 七大平台适配器、适配字段校验、token 加密存储
发布追踪 发布计划管理、状态看板、统计概览

技术栈:Vue 3 + TypeScript + Element Plus + Pinia(前端),Spring Boot 3 + MyBatis-Plus + Spring Security + MySQL 8(后端),Docker Compose 一键部署。

二、架构设计:三个核心决策

在动手让 AI 生成代码之前,我先把架构决策想清楚了——这一步很重要,AI 智能体擅长执行明确的架构约束,但不擅长替你做架构决策。评审维度里架构设计占 30%,这部分必须在投喂话术之前由人定好。

2.1 决策一:平台适配用适配器模式

多平台分发的核心复杂度在于:每个平台的发布要求都不一样。CSDN 要求 1~10 个标签,掘金必须传封面和摘要,公众号要求作者和 120 字以内的摘要。如果把每个平台的校验逻辑 if-else 地写死在业务代码里,每接入一个新平台就要改一遍核心流程。

所以我在设计上把它抽象成 PlatformAdapter 接口,每个平台一个实现类,通过 Spring 的依赖注入自动收集成注册表。新增平台 = 新增一个实现类,核心流程零改动。

动手前我把三个候选方案摆在一起比过:

方案新增平台成本校验逻辑内聚性结论
if-else / switch 按平台码分支 改核心流程,回归风险大 校验散落各分支 否决,违反开闭原则
配置化(平台规则存表驱动校验) 不改代码,只加配置 复杂校验(如标签数量区间)表达不了,要写大量特判 否决,规则复杂后配置本身变成代码
适配器模式 新增一个实现类 校验、组装逻辑收敛在各自类里 ✅ 采用

// src/main/java/com/articlepilot/adapter/PlatformAdapter.java
/**
* 平台适配器接口。新增平台只需新增实现类,Spring 自动收集注入 PlatformAdapterRegistry —— 开放扩展原则。
*/

public interface PlatformAdapter {

String platformCode();

Set<String> requiredFields();

ValidationResult validate(PublishDraft draft);

PublishPlanData buildPlan(PublishDraft draft);
}

以 CSDN 适配器为例,校验规则精确到字段与原因(标签 1~10 个、标题 ≤100 字):

// src/main/java/com/articlepilot/adapter/impl/CsdnAdapter.java(节选)
@Component
@RequiredArgsConstructor
public class CsdnAdapter implements PlatformAdapter {

@Override
public String platformCode() {
return "CSDN";
}

@Override
public Set<String> requiredFields() {
return Set.of("title", "contentMd", "tags");
}

@Override
public ValidationResult validate(PublishDraft d) {
if (d.getTitle() == null || d.getTitle().isBlank()) {
return ValidationResult.fail("title 不能为空");
}
if (d.getTitle().length() > 100) {
return ValidationResult.fail("title 长度不能超过 100");
}
int tagCount = d.getTags() == null ? 0 : d.getTags().size();
if (tagCount < 1) {
return ValidationResult.fail("tags 至少 1 个");
}
if (tagCount > 10) {
return ValidationResult.fail("tags 最多 10 个");
}
return ValidationResult.ok();
}
// buildPlan(…) 略:组装发布计划并序列化为 JSON 落库
}

#mermaid-svg-noq0K0FESHy1uG7H{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-noq0K0FESHy1uG7H .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-noq0K0FESHy1uG7H .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-noq0K0FESHy1uG7H .error-icon{fill:#552222;}#mermaid-svg-noq0K0FESHy1uG7H .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-noq0K0FESHy1uG7H .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-noq0K0FESHy1uG7H .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-noq0K0FESHy1uG7H .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-noq0K0FESHy1uG7H .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-noq0K0FESHy1uG7H .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-noq0K0FESHy1uG7H .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-noq0K0FESHy1uG7H .marker{fill:#333333;stroke:#333333;}#mermaid-svg-noq0K0FESHy1uG7H .marker.cross{stroke:#333333;}#mermaid-svg-noq0K0FESHy1uG7H svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-noq0K0FESHy1uG7H p{margin:0;}#mermaid-svg-noq0K0FESHy1uG7H .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-noq0K0FESHy1uG7H .cluster-label text{fill:#333;}#mermaid-svg-noq0K0FESHy1uG7H .cluster-label span{color:#333;}#mermaid-svg-noq0K0FESHy1uG7H .cluster-label span p{background-color:transparent;}#mermaid-svg-noq0K0FESHy1uG7H .label text,#mermaid-svg-noq0K0FESHy1uG7H span{fill:#333;color:#333;}#mermaid-svg-noq0K0FESHy1uG7H .node rect,#mermaid-svg-noq0K0FESHy1uG7H .node circle,#mermaid-svg-noq0K0FESHy1uG7H .node ellipse,#mermaid-svg-noq0K0FESHy1uG7H .node polygon,#mermaid-svg-noq0K0FESHy1uG7H .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-noq0K0FESHy1uG7H .rough-node .label text,#mermaid-svg-noq0K0FESHy1uG7H .node .label text,#mermaid-svg-noq0K0FESHy1uG7H .image-shape .label,#mermaid-svg-noq0K0FESHy1uG7H .icon-shape .label{text-anchor:middle;}#mermaid-svg-noq0K0FESHy1uG7H .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-noq0K0FESHy1uG7H .rough-node .label,#mermaid-svg-noq0K0FESHy1uG7H .node .label,#mermaid-svg-noq0K0FESHy1uG7H .image-shape .label,#mermaid-svg-noq0K0FESHy1uG7H .icon-shape .label{text-align:center;}#mermaid-svg-noq0K0FESHy1uG7H .node.clickable{cursor:pointer;}#mermaid-svg-noq0K0FESHy1uG7H .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-noq0K0FESHy1uG7H .arrowheadPath{fill:#333333;}#mermaid-svg-noq0K0FESHy1uG7H .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-noq0K0FESHy1uG7H .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-noq0K0FESHy1uG7H .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-noq0K0FESHy1uG7H .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-noq0K0FESHy1uG7H .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-noq0K0FESHy1uG7H .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-noq0K0FESHy1uG7H .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-noq0K0FESHy1uG7H .cluster text{fill:#333;}#mermaid-svg-noq0K0FESHy1uG7H .cluster span{color:#333;}#mermaid-svg-noq0K0FESHy1uG7H div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-noq0K0FESHy1uG7H .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-noq0K0FESHy1uG7H rect.text{fill:none;stroke-width:0;}#mermaid-svg-noq0K0FESHy1uG7H .icon-shape,#mermaid-svg-noq0K0FESHy1uG7H .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-noq0K0FESHy1uG7H .icon-shape p,#mermaid-svg-noq0K0FESHy1uG7H .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-noq0K0FESHy1uG7H .icon-shape .label rect,#mermaid-svg-noq0K0FESHy1uG7H .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-noq0K0FESHy1uG7H .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-noq0K0FESHy1uG7H .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-noq0K0FESHy1uG7H :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

存储层

平台适配层

业务层

按 platformCode 路由

实现

实现

实现

稿件服务状态校验 READY

发布计划服务编排与持久化

PlatformAdapter 接口platformCode / requiredFieldsvalidate / buildPlan

CsdnAdapter标签 1~10 个

JuejinAdapter封面与摘要必填

WechatMpAdapter摘要 ≤120 字

MySQL 8publish_plan 表

适配器模式的收益在代码上体现为两个"零":核心发布流程对接新平台的改动量为零,平台校验规则的维护彼此干扰为零。这个设计也是本文后面单元测试的落点——每个适配器都能独立测。

2.2 决策二:状态流转用集中式状态机

选题和稿件都有生命周期,如果任由前端"想改成什么状态就改成什么状态",脏数据迟早泛滥(比如把一个还停在 IDEA 的选题直接改成已发布)。所以我把合法流转表集中定义在 Service 层,非法流转直接拒绝并说明当前状态与目标状态。

状态校验的落点也考虑过两个位置:

方案一致性测试难度结论
前端路由守卫拦截 只约束本前端,绕过前端直接调 API 就穿透 无法单测 否决,只能当体验优化
注解/AOP 式声明校验 分散在各切面,流转规则不直观 可测但调试链路长 否决,过度设计
集中式状态机类(纯静态方法) 全局唯一流转表 纯函数,单测最简单 ✅ 采用

#mermaid-svg-K7HARQBmzfYFDNem{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-K7HARQBmzfYFDNem .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-K7HARQBmzfYFDNem .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-K7HARQBmzfYFDNem .error-icon{fill:#552222;}#mermaid-svg-K7HARQBmzfYFDNem .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-K7HARQBmzfYFDNem .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-K7HARQBmzfYFDNem .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-K7HARQBmzfYFDNem .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-K7HARQBmzfYFDNem .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-K7HARQBmzfYFDNem .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-K7HARQBmzfYFDNem .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-K7HARQBmzfYFDNem .marker{fill:#333333;stroke:#333333;}#mermaid-svg-K7HARQBmzfYFDNem .marker.cross{stroke:#333333;}#mermaid-svg-K7HARQBmzfYFDNem svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-K7HARQBmzfYFDNem p{margin:0;}#mermaid-svg-K7HARQBmzfYFDNem defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-K7HARQBmzfYFDNem g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-K7HARQBmzfYFDNem g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-K7HARQBmzfYFDNem g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-K7HARQBmzfYFDNem g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-K7HARQBmzfYFDNem g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-K7HARQBmzfYFDNem .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-K7HARQBmzfYFDNem .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-K7HARQBmzfYFDNem .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-K7HARQBmzfYFDNem .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-K7HARQBmzfYFDNem .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-K7HARQBmzfYFDNem .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-K7HARQBmzfYFDNem .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-K7HARQBmzfYFDNem .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-K7HARQBmzfYFDNem .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-K7HARQBmzfYFDNem .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-K7HARQBmzfYFDNem .edgeLabel .label text{fill:#333;}#mermaid-svg-K7HARQBmzfYFDNem .label div .edgeLabel{color:#333;}#mermaid-svg-K7HARQBmzfYFDNem .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-K7HARQBmzfYFDNem .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-K7HARQBmzfYFDNem .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-K7HARQBmzfYFDNem .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-K7HARQBmzfYFDNem .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-K7HARQBmzfYFDNem .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-K7HARQBmzfYFDNem .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-K7HARQBmzfYFDNem #statediagram-barbEnd{fill:#333333;}#mermaid-svg-K7HARQBmzfYFDNem .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-K7HARQBmzfYFDNem .cluster-label,#mermaid-svg-K7HARQBmzfYFDNem .nodeLabel{color:#131300;}#mermaid-svg-K7HARQBmzfYFDNem .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-K7HARQBmzfYFDNem .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-K7HARQBmzfYFDNem .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-K7HARQBmzfYFDNem .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-K7HARQBmzfYFDNem .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-K7HARQBmzfYFDNem .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-K7HARQBmzfYFDNem .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-K7HARQBmzfYFDNem .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-K7HARQBmzfYFDNem .note-edge{stroke-dasharray:5;}#mermaid-svg-K7HARQBmzfYFDNem .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-K7HARQBmzfYFDNem .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-K7HARQBmzfYFDNem .statediagram-note text{fill:black;}#mermaid-svg-K7HARQBmzfYFDNem .statediagram-note .nodeLabel{color:black;}#mermaid-svg-K7HARQBmzfYFDNem .statediagram .edgeLabel{color:red;}#mermaid-svg-K7HARQBmzfYFDNem #dependencyStart,#mermaid-svg-K7HARQBmzfYFDNem #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-K7HARQBmzfYFDNem .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-K7HARQBmzfYFDNem :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

创建稿件

提交审核

审核通过

发布

归档

归档

归档

DRAFT

IN_REVIEW

READY

PUBLISHED

ARCHIVED

稿件状态机保证了一个不变量:只有 READY 状态的稿件才能创建发布计划。这条约束在前端 UI 上也有对应(流转按钮只展示合法目标状态),但真正的防线在后端——前端约束只是体验优化,不能当安全边界。

流转表在 Service 层只有一份,非法流转直接抛业务异常,错误信息带上传入的两个状态:

// src/main/java/com/articlepilot/service/article/ArticleStatusMachine.java(节选)
public final class ArticleStatusMachine {

public static final int INVALID_TRANSITION_CODE = 1006;

private static final Map<ArticleStatus, Set<ArticleStatus>> ALLOWED =
new EnumMap<>(ArticleStatus.class);

static {
ALLOWED.put(ArticleStatus.DRAFT, Set.of(ArticleStatus.IN_REVIEW, ArticleStatus.ARCHIVED));
ALLOWED.put(ArticleStatus.IN_REVIEW, Set.of(ArticleStatus.READY, ArticleStatus.ARCHIVED));
ALLOWED.put(ArticleStatus.READY, Set.of(ArticleStatus.PUBLISHED, ArticleStatus.ARCHIVED));
ALLOWED.put(ArticleStatus.PUBLISHED, Set.of(ArticleStatus.ARCHIVED));
ALLOWED.put(ArticleStatus.ARCHIVED, Set.of());
}

public static void validateTransition(ArticleStatus from, ArticleStatus to) {
Set<ArticleStatus> targets = ALLOWED.getOrDefault(from, Set.of());
if (!targets.contains(to)) {
throw new BusinessException(
INVALID_TRANSITION_CODE,
"稿件状态非法流转: " + from + " → " + to);
}
}
}

2.3 决策三:安全边界前置到设计

AI 生成的代码在业务逻辑上表现不错,但安全细节需要人主动提出约束,否则它会给你"能跑但不安全"的默认实现。本项目在设计阶段就定下四条安全红线,贯穿全部开发过程。其中密钥管理在"环境变量注入"和"配置文件加密(Jasypt)"之间权衡过:后者对单机部署更省事,但密文的主密钥还是得落 somewhere,等于把问题往后挪了一层;环境变量 + .env.example 模板在 Docker Compose 场景下零额外依赖,最终选了前者。四条红线如下:

红线实现
密钥不入库 JWT_SECRET、数据库密码、加密密钥全部环境变量注入
token 不泄露 平台 token AES/GCM 加密落库,接口只返回掩码(sk-****ab12)
越权有防线 @PreAuthorize 接口级鉴权 + Service 层数据归属校验,双层防线
注入有防护 持久层全部走 MyBatis-Plus Wrapper 预编译,排序字段白名单

三、用 CodeArts 开发:我的实操流程

3.1 开发前的准备

在打开 CodeArts 之前,我做了三件"人肉"准备工作:在 AtomGit 创建公开仓库并放好 LICENSE、README 骨架和 .gitignore。

同时把架构决策(2.1~2.3)整理成带验收标准的投喂话术,并规划好七天的开发里程碑(D1 骨架 → D2 认证 → D3 稿件 → D4 适配器 → D5 收口 → D6 文章 → D7 投稿)。

这里分享一个最重要的使用心得:一次只投一个话术块,验收通过再投下一个。AI 在明确的小范围约束下表现非常稳定,但让它一次性"把整个系统写出来"时,质量会明显失控。

我给每个话术块都写死了目录结构、接口签名和验收标准(如 curl 实测场景、mvn test 全绿),AI 的任务是在约束内高质量实现,而不是自由发挥。

在这里插入图片描述
开发现场:AtomCode Agent 工作台,articlepilot 仓库已建立代码库索引,Agent 基于索引读写工程文件

在这里插入图片描述
Agent 会话列表:2 个会话贯穿 D1-D5 的全部话术投喂与验收往返,本身就是一份开发日志

3.2 一个典型话术块长什么样

以 D4 的平台适配器话术为例(节选),可以看到我把"硬性要求"和"验收动作"都写在了投喂内容里:

实现多平台适配器体系,要求:

1. 定义接口 PlatformAdapter:platformCode() / requiredFields() /
validate(PublishDraft) / buildPlan(PublishDraft);
2. 所有实现通过 Spring 注入收集为 Map<platformCode, adapter>,
新增平台只需新增实现类,不改核心流程;
3. 本期实现 CsdnAdapter、JuejinAdapter、WechatMpAdapter 三个适配器,
校验规则各不相同,失败信息要具体到字段与原因;
4. 完成后 mvn test 报告结果,单元测试覆盖每个适配器的合法/非法用例。

AI 对这类结构化约束的执行质量很高——三个适配器一次生成即通过编译,校验逻辑与话术要求完全一致。真正需要我介入修正的,反而是话术没写到的细节,下一章的踩坑记录会具体展开。
在这里插入图片描述

D4 适配器话术投喂后的会话现场:Agent 按约束生成三个适配器并回报 mvn test 结果

四、功能演示:从选题到发布追踪的完整闭环

项目完成后,我用 docker compose up -d –build 一条命令拉起整站(MySQL + 后端 + nginx 托管的前端),浏览器访问 8081 端口完成以下全链路验证。

4.1 登录与三角色权限

本地起服务只需两步:IDEA 里 Debug 启动 ArticlePilotApplication(后端 8081),前端目录执行 yarn dev 起 Vite 调试服务(5173)。之后使用预置的三个测试账号(管理员/作者/访客)分别登录,验证 RBAC 生效:作者看不到用户管理入口,访客对看板以外的写操作被后端 403 拦截,越权请求返回统一的错误格式而非堆栈。

在这里插入图片描述
后端启动:IDEA 以 Debug 方式运行 ArticlePilotApplication,可直接断点排查请求链路

在这里插入图片描述
前端启动:yarn dev 后 Vite 不到 1 秒即就绪(ready in 482 ms),本地调试地址 http://localhost:5173/

在这里插入图片描述
登录页:admin / author / guest 三个测试账号直接列在页面下方,点「填入」免输入即可体验三种角色

4.2 选题库与稿件管理

新建选题后按状态机流转(IDEA → PLANNED → WRITING),再关联创建稿件。稿件侧支持编辑与版本快照,两个账号并发编辑同一稿件时,后者会收到"已被他人修改"的 409 提示,乐观锁生效。

在这里插入图片描述
选题库列表:按状态筛选,每条选题带摘要与来源(用户征稿/热点事件/自发),标题即后续稿件的雏形

在这里插入图片描述
稿件管理:DRAFT / IN_REVIEW / PUBLISHED / ARCHIVED 各状态稿件同屏可见,状态机流转的结果一目了然

4.3 平台适配与发布追踪

稿件流转到 READY 后,为它逐平台填写适配字段(表单项按适配器 requiredFields 动态渲染),提交发布计划。缺封面、摘要超长这类校验错误会精确提示到字段。发布计划状态更新为已发布并填入链接后,追踪看板的统计卡片实时同步。

发布计划自身的生命周期也是一台状态机,与稿件状态机的区别在于它多出 FAILED 和 CANCELLED 两个"异常出口"——发布动作可能因平台校验失败而失败,也可能被作者主动取消:

#mermaid-svg-yW28mtzSUTclaJmw{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-yW28mtzSUTclaJmw .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-yW28mtzSUTclaJmw .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-yW28mtzSUTclaJmw .error-icon{fill:#552222;}#mermaid-svg-yW28mtzSUTclaJmw .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-yW28mtzSUTclaJmw .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-yW28mtzSUTclaJmw .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-yW28mtzSUTclaJmw .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-yW28mtzSUTclaJmw .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-yW28mtzSUTclaJmw .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-yW28mtzSUTclaJmw .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-yW28mtzSUTclaJmw .marker{fill:#333333;stroke:#333333;}#mermaid-svg-yW28mtzSUTclaJmw .marker.cross{stroke:#333333;}#mermaid-svg-yW28mtzSUTclaJmw svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-yW28mtzSUTclaJmw p{margin:0;}#mermaid-svg-yW28mtzSUTclaJmw defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-yW28mtzSUTclaJmw g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-yW28mtzSUTclaJmw g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-yW28mtzSUTclaJmw g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-yW28mtzSUTclaJmw g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-yW28mtzSUTclaJmw g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-yW28mtzSUTclaJmw .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-yW28mtzSUTclaJmw .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-yW28mtzSUTclaJmw .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-yW28mtzSUTclaJmw .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-yW28mtzSUTclaJmw .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-yW28mtzSUTclaJmw .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-yW28mtzSUTclaJmw .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-yW28mtzSUTclaJmw .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-yW28mtzSUTclaJmw .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-yW28mtzSUTclaJmw .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-yW28mtzSUTclaJmw .edgeLabel .label text{fill:#333;}#mermaid-svg-yW28mtzSUTclaJmw .label div .edgeLabel{color:#333;}#mermaid-svg-yW28mtzSUTclaJmw .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-yW28mtzSUTclaJmw .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-yW28mtzSUTclaJmw .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-yW28mtzSUTclaJmw .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-yW28mtzSUTclaJmw .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-yW28mtzSUTclaJmw .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-yW28mtzSUTclaJmw .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-yW28mtzSUTclaJmw #statediagram-barbEnd{fill:#333333;}#mermaid-svg-yW28mtzSUTclaJmw .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-yW28mtzSUTclaJmw .cluster-label,#mermaid-svg-yW28mtzSUTclaJmw .nodeLabel{color:#131300;}#mermaid-svg-yW28mtzSUTclaJmw .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-yW28mtzSUTclaJmw .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-yW28mtzSUTclaJmw .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-yW28mtzSUTclaJmw .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-yW28mtzSUTclaJmw .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-yW28mtzSUTclaJmw .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-yW28mtzSUTclaJmw .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-yW28mtzSUTclaJmw .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-yW28mtzSUTclaJmw .note-edge{stroke-dasharray:5;}#mermaid-svg-yW28mtzSUTclaJmw .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-yW28mtzSUTclaJmw .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-yW28mtzSUTclaJmw .statediagram-note text{fill:black;}#mermaid-svg-yW28mtzSUTclaJmw .statediagram-note .nodeLabel{color:black;}#mermaid-svg-yW28mtzSUTclaJmw .statediagram .edgeLabel{color:red;}#mermaid-svg-yW28mtzSUTclaJmw #dependencyStart,#mermaid-svg-yW28mtzSUTclaJmw #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-yW28mtzSUTclaJmw .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-yW28mtzSUTclaJmw :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

适配校验通过创建发布计划

填入发布链接

平台校验/发布失败

作者取消

PENDING

PUBLISHED

FAILED

CANCELLED

稿件状态机(§2.2)管"这篇稿子能不能发",发布计划状态机管"发到这个平台的动作结果如何"——两台状态机通过"只有 READY 稿件才能创建 PENDING 计划"这条不变量衔接。

在这里插入图片描述
发布追踪:每篇稿件一张卡片,卡片上按平台展示状态徽标(CSDN/JUEJIN/WECHAT_MP/TENCENT_CLOUD…),哪个平台发过、哪个平台还没发一眼可辨

在这里插入图片描述

工作台首页:素材库累计稿件、选题总数、近 7 天新增趋势与选题/文章状态分布图,创作管道的健康度尽收一屏

在这里插入图片描述
平台配置页:各平台 Token 以掩码形式回显(对应 §2.3 安全红线),待配置的平台显示「去配置」入口

平台 token 的安全展示在配置页完成:保存时 AES/GCM 加密落库,页面只回显末 4 位掩码(对应 §2.3 的安全红线与 §5.3 的踩坑修复)。

4.4 接口文档

后端集成 springdoc-openapi,全部接口可在 Swagger 页面浏览与调试,统一返回体结构一目了然。

在这里插入图片描述
Swagger 首页(OAS 3.0):Admin-User / Article / Topic 等按 Controller 分组,接口支持在页面直接调试,稿件更新接口标注了乐观锁 409 语义

在这里插入图片描述
接口分组一览:Auth / Dashboard / Health / Manuscript / PlatformConfig(标注 token 加密存储、掩码返回)/ PublishPlan / Topic,统一返回体结构在每个接口的 Schema 中可见

五、踩坑记录:AI 写对了九成,剩下一成靠人

如实记录开发过程中真实遇到的问题。AI 智能体大幅提效是事实,但"生成的代码能编译通过"和"能正确运行"之间的缝隙,仍然需要人填。

5.1 JWT 过滤器链顺序不对,登录后所有请求 401

现象:登录接口正常返回 token,但带着 token 访问任何业务接口都是 401。

根因:AI 生成的 SecurityConfig 中,自定义 JwtAuthenticationFilter 注册的位置不对——被加在了默认过滤器链之后,请求到达它之前就已经被认证逻辑拒掉了。

修复:显式指定过滤器插入位置,把 JwtAuthenticationFilter 挂到 UsernamePasswordAuthenticationFilter 之前,重新验证三角色场景后恢复正常。这个问题 AI 两次修改才修对,第二次我按"先定位根因再动手"的纪律要求它先解释过滤器链的执行顺序,确认理解一致后一次修复。修复后的关键配置如下:

// src/main/java/com/articlepilot/config/SecurityConfig.java(节选)
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers(PUBLIC_PATHS).permitAll()
.anyRequest().authenticated()
)
.exceptionHandling(eh -> eh
.authenticationEntryPoint((request, response, authException) ->
writeJson(response, 401, "未认证或令牌已失效"))
.accessDeniedHandler((request, response, accessDeniedException) ->
writeJson(response, 403, "无权访问该接口"))
)
// 坑一的修复点:自定义过滤器必须挂在用户名密码过滤器之前,
// 否则请求先被默认认证逻辑拒掉,永远到不了 JWT 解析
.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider),
UsernamePasswordAuthenticationFilter.class)
.formLogin(form -> form.disable())
.httpBasic(basic -> basic.disable());
return http.build();
}

在这里插入图片描述
坑一现场:401 问题在 Agent 会话中往返定位,Agent 先解释过滤器链执行顺序再动手修复

5.2 数据库增量 SQL 没生效,新接口集体报表不存在

现象:D3 之后新增的选题、稿件接口全部报"Table doesn’t exist",但 SQL 文明明明提交了。

根因:MySQL 容器挂载的初始化脚本只在 volume 首次创建时执行,后续追加的 migration 文件对已经在运行的库不会自动生效。

修复:两个动作——对运行中的库手动应用增量 SQL;同时把所有建表语句加固为幂等写法(CREATE TABLE IF NOT EXISTS),并验证 docker compose down -v 全新重建后所有表自动创建。这个坑的教训是:容器化项目里,“文件在仓库里"不等于"在数据库里”。
在这里插入图片描述

坑二现场:“Table doesn’t exist” 的根因分析与幂等加固方案都在会话中留痕

5.3 AI 默认实现的 token 返回,把加密后的密文原样吐给了前端

现象:安全自查时发现,平台配置接口返回的 token 字段虽然是密文,但密文本身完整返回了——加密等于白做。

根因:话术里写了"AES 加密落库",但没写死"接口返回掩码",AI 按它自己的默认理解实现了。

修复:补充掩码规则(仅展示末 4 位),并在 DTO 层切断 entity 直出。这次踩坑让我把投喂话术的纪律升级了一条:安全要求必须写成可验收的明确规则,不能只给方向。
在这里插入图片描述
坑三现场:安全自查发现密文直出后在会话中补充掩码规则并切断 entity 直出

5.4 小结

三个坑的共同点是:AI 的失误都发生在"话术没约束到的地方",没有一个坑是因为 AI 写不好"话术约束到的代码"。这印证了我的使用策略——架构决策、安全规则、验收标准由人写死,AI 在约束内高效执行,这个分工下的协作质量最高。

补一句质量收口的实测数据(mvn verify 本地实测,JaCoCo 0.8.12):

  • 后端单元测试 9 个测试类、123 个用例,全部通过、0 失败;
  • 核心业务包行覆盖率:状态机(service.article / service.topic)100%,平台适配注册表(adapter)88%;
  • 全项目整体行覆盖率约 14%——这个数字不高,如实说明原因:Controller、DTO、Entity 层未纳入单测范围(Controller 靠 docs/smoke-test.md 的全链路冒烟兜底),单测火力集中在业务逻辑与安全组件上。

在这里插入图片描述
mvn verify 生成的 JaCoCo 报告(target/site/jacoco):service.article / service.topic 状态机包 100%,adapter 88%,common 49%,Total 14%——与上文数字一致,未覆盖部分主要是 Controller/DTO/Entity 层

六、提效数据:AI 辅助开发到底快了多少

如实给出本次开发的提效记录(基于开发日志的实际耗时统计,为个人单项目样本,仅供参考,不构成普适结论)。

开发阶段纯手写预估AI 辅助实际提效来源
前后端项目骨架 + Docker 部署 约 2 天 约 2.5 小时 脚手架/配置类工作收益最大
JWT 认证 + RBAC 约 1.5 天 约 3 小时 模板化安全代码,人负责验收
选题/稿件模块 + 状态机 约 2.5 天 约 5 小时 CRUD 由 AI 生成,状态机测试人工把关
平台适配器体系 约 1 天 约 2.5 小时 设计已定,实现即翻译
质量收口(安全自查/补测试/README) 约 1 天 约 4 小时 自查清单执行快,修复靠人

在这里插入图片描述
AtomGit 个人工作台:articlepilot 与其他在维项目并列,Agent 会话记录即开发日志的原始凭证

两点如实说明:一是上表"纯手写预估"基于我以往同类项目的经验估算,存在主观误差;二是提效并非免费的——验收环节(curl 实测、点页面、看测试报告)的耗时包含在内,这部分是 AI 替代不了的人工成本。综合感受:整体工期压缩到传统方式的四分之一左右,且代码质量的下限反而更稳(约束写死的情况下)。

七、开源地址与运行方式

项目已开源,包含可运行源代码、README(作品介绍)与开源协议:

项目仓库:https://atomgit.com/dickeryang/articlepilot.git

本地运行只需三步(依赖 Docker):

# 1. 克隆仓库
git clone https://atomgit.com/dickeryang/articlepilot.git
cd articlepilot

# 2. 一条命令拉起整站(MySQL + 后端 + nginx 前端)
docker compose up -d –build

# 3. 浏览器访问 http://localhost:8081
# 默认测试账号:admin / author / guest,密码 Articlepilot@2026

仓库内附 docs/architecture.md(架构设计文档)、docs/testing.md(测试清单与覆盖率)、docs/smoke-test.md(全链路冒烟记录),按 README 从零环境一次跑通是投稿前的验收门槛。项目采用 Apache-2.0 协议开源,欢迎参考与反馈。

八、总结与平台使用建议

8.1 结论

回到开头的问题:AI 智能体能不能独立完成一个全栈应用?这次实测的答案是——在"人定架构与验收、AI 做实现"的分工下,可以,且提效显著。

三个核心设计(适配器模式、集中状态机、安全红线前置)全部由人预先决策,AI 在这套约束内完成了约九成的代码量。评审关注的架构清晰度、测试覆盖、安全合规,恰恰是靠"投喂前的人肉准备"保住的,而不是靠 AI 自觉。

8.2 给平台的建议

一周深度使用下来,对 CodeArts 代码智能体的几点优化建议(如实反馈,供官方参考):

  • 多文件改动的事务感:一次话术涉及十几个文件时,中途失败会留下半成品状态,希望提供"本轮改动整体回滚"的操作;
  • 安全默认值:建议对密钥硬编码、SQL 拼接这类问题在生成时就主动告警,而不是依赖开发者话术约束;
  • 长会话上下文:跨天开发时,前一天的架构约束需要重新投喂,若能持久化项目级约定会省不少重复工作。
  • 8.3 写在最后

    这次参赛对我而言最大的收获不是这个管理台本身,而是验证了一套可复制的 AI 协作工作流:架构决策人做、约束写进话术、每块验收兜底。

    这套方法已经整理成完整的执行手册,后续我会在专栏中展开。如果你也在参加本期华为云码道 AI 编程挑战赛,欢迎在评论区交流投喂话术的写法与踩坑经验。

    觉得有用就点个赞,有疑问或不同看法欢迎评论区交流。

    赞(0)
    未经允许不得转载:171主机测评 » 华为云码道 CodeArts 实战:AI 智能体独立开发一个多平台内容分发管理台(架构设计 + 开发实录)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址