Skip to content

静态博客与提示词管理后台重构 Implementation Plan ​

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 删除数据库 CMS 与 Vue 管理端,保留 Markdown/JSON 静态博客,并交付一个支持单管理员认证及“提示词 + 单张图片”私有管理的 React/Spring Boot 系统。

Architecture: 公开博客继续完全由 VitePress、Markdown、JSON 和静态资源构建。管理系统使用 React SPA 调用同源 Spring Boot API;认证和提示词元数据存入 MySQL,原始图片存入私有 MinIO 桶。提示词服务通过明确的补偿流程协调数据库和对象存储,不引入发布、导出或后台 CMS 链路。

Tech Stack: VitePress、Node.js test runner、Java 17、Spring Boot 4.1.1、Spring Security、MyBatis XML、Flyway、MySQL 8.4、MinIO、React、TypeScript、Vite、React Router、Axios、Ant Design、Phosphor Icons、Vitest、Testing Library。


文件结构 ​

实现后关键文件职责如下:

  • backend/src/main/java/com/chris/blog/controller/AdminAuthController.java:登录、当前用户、CSRF、自助改密与退出。
  • backend/src/main/java/com/chris/blog/controller/PromptController.java:提示词创建、分页查询、详情与永久删除 HTTP 边界。
  • backend/src/main/java/com/chris/blog/service/PromptService.java:提示词业务用例接口。
  • backend/src/main/java/com/chris/blog/service/impl/PromptServiceImpl.java:规范化、唯一性、上传补偿和可重试删除编排。
  • backend/src/main/java/com/chris/blog/service/PromptImageInspector.java:图片真实格式、尺寸、像素数、大小与哈希校验。
  • backend/src/main/java/com/chris/blog/service/PromptObjectStorage.java:私有 MinIO 上传、签名预览与幂等删除边界。
  • backend/src/main/java/com/chris/blog/repository/PromptRepository.java:提示词持久化边界。
  • backend/src/main/resources/mapper/PromptMapper.xml:提示词 SQL;Java Mapper 不写注解 SQL。
  • backend/src/main/resources/db/migration/V1__admin_and_prompt_schema.sql:干净的单管理员和提示词数据库结构。
  • admin-frontend/src/auth/AuthContext.tsx:仅内存保存用户与 CSRF 状态。
  • admin-frontend/src/api/client.ts:统一 API 信封、CSRF 请求头和 401 跳转。
  • admin-frontend/src/pages/prompts/*:列表、新建与详情页面。
  • admin-frontend/src/styles/theme.ts、global.css:Ant Design 语义令牌、响应式布局和可访问状态。
  • ops/docker-compose.yml:MySQL、私有 prompt-results MinIO 桶和后端运行配置。

当前工作树在计划开始前已经包含未提交的后台原型和根目录修改。执行每个提交前必须运行 git diff --cached --check 与 git diff --cached --stat;对计划开始前已修改的跟踪文件(尤其是 README.md、package.json、.gitignore)使用 git add -p 只暂存本任务的 hunk,不得顺带提交用户无关修改。

Task 1:锁定静态博客为唯一公开内容源 ​

Files:

  • Create: tests/static-blog-content-source.test.mjs

  • Modify: package.json

  • Modify: README.md

  • Delete: scripts/export-backend-snapshot.mjs

  • Delete: tests/backend-snapshot-export.test.mjs

  • [ ] Step 1: 写失败的架构边界测试

js
import assert from 'node:assert/strict'
import { existsSync, readFileSync } from 'node:fs'
import test from 'node:test'

const packageJson = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'))

test('公开博客不再提供后端快照导出入口', () => {
  assert.equal(packageJson.scripts['export:backend'], undefined)
  assert.equal(existsSync(new URL('../scripts/export-backend-snapshot.mjs', import.meta.url)), false)
})

test('静态博客仍保留 GitHub 项目脚本和 JSON 内容源', () => {
  assert.equal(typeof packageJson.scripts['add-github-project'], 'string')
  assert.equal(existsSync(new URL('../docs/public/github-projects.json', import.meta.url)), true)
})
  • [ ] Step 2: 运行测试并确认失败

Run: node --test tests/static-blog-content-source.test.mjs

Expected: FAIL,因为 export:backend 和导出脚本仍存在。

  • [ ] Step 3: 删除导出入口并更新主 README

从 package.json 删除 export:backend,删除导出脚本及专用测试;在 README.md 的架构说明中明确:公开博客只读取 docs/**/*.md、docs/public/**/*.json 和静态资源,后端不提供文章正文。

  • [ ] Step 4: 验证静态站边界

Run: node --test tests/static-blog-content-source.test.mjs

Expected: 2 tests PASS。

Run: npm run build

Expected: VitePress build succeeds。

  • [ ] Step 5: 提交
bash
git add -p package.json README.md
git add tests/static-blog-content-source.test.mjs
git commit -m "refactor: make static files the only public content source"

Task 2:收敛为单管理员认证模型 ​

Files:

  • Rename: backend/src/main/resources/db/migration/V1__admin_auth_persistence.sql → backend/src/main/resources/db/migration/V1__admin_and_prompt_schema.sql

  • Delete: backend/src/main/resources/db/migration/V2__content_management.sql

  • Delete: backend/src/main/resources/db/migration/V3__immutable_publish_snapshot.sql

  • Delete: backend/src/main/resources/db/migration/V4__immutable_article_publication.sql

  • Modify: backend/src/main/java/com/chris/blog/service/AdminUserService.java

  • Modify: backend/src/main/java/com/chris/blog/service/impl/AdminUserServiceImpl.java

  • Modify: backend/src/main/java/com/chris/blog/controller/AdminAuthController.java

  • Modify: backend/src/main/java/com/chris/blog/repository/AdminUserRepository.java

  • Modify: backend/src/main/java/com/chris/blog/repository/impl/MyBatisAdminUserRepository.java

  • Modify: backend/src/main/java/com/chris/blog/mapper/AdminUserMapper.java

  • Modify: backend/src/main/resources/mapper/AdminUserMapper.xml

  • Delete: backend/src/main/java/com/chris/blog/controller/AdminUserController.java

  • Delete: backend/src/main/java/com/chris/blog/bean/AdminUserEnabledGuard.java

  • Delete: backend/src/main/java/com/chris/blog/enums/AdminUserEnabledChangeResult.java

  • Delete: backend/src/main/java/com/chris/blog/req/CreateAdminUserRequest.java

  • Delete: backend/src/main/java/com/chris/blog/req/UpdateAdminUserRequest.java

  • Delete: backend/src/main/java/com/chris/blog/req/ResetPasswordRequest.java

  • Delete: backend/src/main/java/com/chris/blog/req/ChangeEnabledRequest.java

  • Delete: backend/src/main/java/com/chris/blog/service/OperationLogService.java

  • Delete: backend/src/main/java/com/chris/blog/service/impl/OperationLogServiceImpl.java

  • Delete: backend/src/main/java/com/chris/blog/repository/OperationLogRepository.java

  • Delete: backend/src/main/java/com/chris/blog/repository/impl/MyBatisOperationLogRepository.java

  • Delete: backend/src/main/java/com/chris/blog/mapper/OperationLogMapper.java

  • Delete: backend/src/main/resources/mapper/OperationLogMapper.xml

  • Delete: backend/src/main/java/com/chris/blog/bean/OperationLog.java

  • Delete: backend/src/main/java/com/chris/blog/enums/OperationAction.java

  • Test: backend/src/test/java/com/chris/blog/persistence/AdminAuthenticationPersistenceContractTest.java

  • Test: backend/src/test/java/com/chris/blog/service/impl/AdminUserServiceImplTest.java

  • [ ] Step 1: 把认证契约测试改为单管理员边界

测试必须断言迁移只包含 admin_user,不包含 admin_user_enabled_guard 和 operation_log;AdminUserService 只暴露下列方法:

java
public interface AdminUserService {
    AdminUserVo getById(Long id);
    void changeOwnPassword(Long id, ChangePasswordRequest request);
    void bootstrap(String username, String password);
}

同时保留以下测试:BCrypt 72 字节边界、旧密码校验、凭据版本递增使旧会话失效、并发改密只能有一次成功、引导只在零账户时创建。

  • [ ] Step 2: 运行定向测试并确认失败

Run: cd backend && mvn -Dtest=AdminAuthenticationPersistenceContractTest,AdminUserServiceImplTest test

Expected: FAIL,因为多管理员与审计日志 API 仍存在。

  • [ ] Step 3: 重写管理员迁移与持久化接口

V1__admin_and_prompt_schema.sql 中先保留这一份管理员表:

sql
CREATE TABLE admin_user (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
    singleton_key TINYINT UNSIGNED NOT NULL DEFAULT 1,
    username VARCHAR(64) NOT NULL,
    display_name VARCHAR(64) NOT NULL,
    password_hash VARCHAR(100) NOT NULL,
    enabled TINYINT(1) NOT NULL DEFAULT 1,
    credential_version BIGINT UNSIGNED NOT NULL DEFAULT 1,
    failed_login_attempts INT UNSIGNED NOT NULL DEFAULT 0,
    locked_until DATETIME(6) NULL,
    created_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
    updated_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
    last_login_at DATETIME(6) NULL,
    version BIGINT UNSIGNED NOT NULL DEFAULT 0,
    PRIMARY KEY (id),
    UNIQUE KEY uk_admin_user_singleton (singleton_key),
    UNIQUE KEY uk_admin_user_username (username),
    KEY idx_admin_user_locked_until (locked_until),
    CONSTRAINT ck_admin_user_singleton CHECK (singleton_key = 1),
    CONSTRAINT ck_admin_user_enabled CHECK (enabled IN (0, 1))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

删除管理员列表、创建、资料修改、代改密码和启停相关 Mapper/Repository 方法。createIfNoAccount 直接尝试插入固定 singleton_key=1,将该唯一键的 DuplicateKeyException 解释为“已有管理员”,由数据库约束保证并发引导最多创建一个账户;不再维护 enabled guard 计数。

  • [ ] Step 4: 删除审计写入并保留运行日志

AdminUserServiceImpl 不再注入 OperationLogService。AdminAuthController.logout 直接清除安全上下文并失效会话;不得记录用户名、密码、CSRF 值或 MinIO 密钥到日志。

  • [ ] Step 5: 运行认证测试

Run: cd backend && mvn -Dtest='AdminAuthenticationPersistenceContractTest,AdminAuthenticationMySqlIntegrationTest,PasswordUpdateConcurrencySafetyTest,AdminUserServiceImplTest,AdminUserDetailsServiceTest,PersistentAdminCredentialVersionLookupTest' test

Expected: PASS。

  • [ ] Step 6: 提交
bash
git add backend/src/main backend/src/test/java/com/chris/blog/persistence backend/src/test/java/com/chris/blog/service/impl
git commit -m "refactor: reduce authentication to one administrator"

Task 3:删除后台 CMS、发布与通用媒体业务 ​

Files:

  • Delete: backend/src/main/java/com/chris/blog/bean/Article.java

  • Delete: backend/src/main/java/com/chris/blog/bean/ArticleRevision.java

  • Delete: backend/src/main/java/com/chris/blog/bean/PublishedArticle.java

  • Delete: backend/src/main/java/com/chris/blog/bean/Category.java

  • Delete: backend/src/main/java/com/chris/blog/bean/Tag.java

  • Delete: backend/src/main/java/com/chris/blog/bean/SiteConfig.java

  • Delete: backend/src/main/java/com/chris/blog/bean/GithubProject.java

  • Delete: backend/src/main/java/com/chris/blog/controller/ArticleController.java

  • Delete: backend/src/main/java/com/chris/blog/controller/CategoryController.java

  • Delete: backend/src/main/java/com/chris/blog/controller/TagController.java

  • Delete: backend/src/main/java/com/chris/blog/controller/SiteConfigController.java

  • Delete: backend/src/main/java/com/chris/blog/controller/GithubProjectController.java

  • Delete: backend/src/main/java/com/chris/blog/controller/PublishController.java

  • Delete: backend/src/main/java/com/chris/blog/controller/MediaController.java

  • Delete: backend/src/main/java/com/chris/blog/mapper/ArticleMapper.java

  • Delete: backend/src/main/java/com/chris/blog/mapper/ArticleRevisionMapper.java

  • Delete: backend/src/main/java/com/chris/blog/mapper/PublishedArticleMapper.java

  • Delete: backend/src/main/java/com/chris/blog/mapper/CategoryMapper.java

  • Delete: backend/src/main/java/com/chris/blog/mapper/TagMapper.java

  • Delete: backend/src/main/java/com/chris/blog/mapper/SiteConfigMapper.java

  • Delete: backend/src/main/java/com/chris/blog/mapper/GithubProjectMapper.java

  • Delete: backend/src/main/java/com/chris/blog/mapper/PublishSnapshotMapper.java

  • Delete: backend/src/main/java/com/chris/blog/repository/ArticleRepository.java

  • Delete: backend/src/main/java/com/chris/blog/repository/ArticleRevisionRepository.java

  • Delete: backend/src/main/java/com/chris/blog/repository/PublishedArticleRepository.java

  • Delete: backend/src/main/java/com/chris/blog/repository/CategoryRepository.java

  • Delete: backend/src/main/java/com/chris/blog/repository/TagRepository.java

  • Delete: backend/src/main/java/com/chris/blog/repository/SiteConfigRepository.java

  • Delete: backend/src/main/java/com/chris/blog/repository/GithubProjectRepository.java

  • Delete: backend/src/main/java/com/chris/blog/repository/impl/MyBatisArticleRepository.java

  • Delete: backend/src/main/java/com/chris/blog/repository/impl/MyBatisArticleRevisionRepository.java

  • Delete: backend/src/main/java/com/chris/blog/repository/impl/MyBatisPublishedArticleRepository.java

  • Delete: backend/src/main/java/com/chris/blog/repository/impl/MyBatisCategoryRepository.java

  • Delete: backend/src/main/java/com/chris/blog/repository/impl/MyBatisTagRepository.java

  • Delete: backend/src/main/java/com/chris/blog/repository/impl/MyBatisSiteConfigRepository.java

  • Delete: backend/src/main/java/com/chris/blog/repository/impl/MyBatisGithubProjectRepository.java

  • Delete: backend/src/main/java/com/chris/blog/req/CreateArticleRequest.java

  • Delete: backend/src/main/java/com/chris/blog/req/UpdateArticleRequest.java

  • Delete: backend/src/main/java/com/chris/blog/req/CreateCategoryRequest.java

  • Delete: backend/src/main/java/com/chris/blog/req/UpdateCategoryRequest.java

  • Delete: backend/src/main/java/com/chris/blog/req/CreateTagRequest.java

  • Delete: backend/src/main/java/com/chris/blog/req/UpdateTagRequest.java

  • Delete: backend/src/main/java/com/chris/blog/req/CreateSiteConfigRequest.java

  • Delete: backend/src/main/java/com/chris/blog/req/UpdateSiteConfigRequest.java

  • Delete: backend/src/main/java/com/chris/blog/req/CreateGithubProjectRequest.java

  • Delete: backend/src/main/java/com/chris/blog/req/UpdateGithubProjectRequest.java

  • Delete: backend/src/main/java/com/chris/blog/service/ArticleService.java

  • Delete: backend/src/main/java/com/chris/blog/service/CategoryService.java

  • Delete: backend/src/main/java/com/chris/blog/service/TagService.java

  • Delete: backend/src/main/java/com/chris/blog/service/SiteConfigService.java

  • Delete: backend/src/main/java/com/chris/blog/service/GithubProjectService.java

  • Delete: backend/src/main/java/com/chris/blog/service/PublishService.java

  • Delete: backend/src/main/java/com/chris/blog/service/MediaService.java

  • Delete: backend/src/main/java/com/chris/blog/service/impl/ArticleServiceImpl.java

  • Delete: backend/src/main/java/com/chris/blog/service/impl/CategoryServiceImpl.java

  • Delete: backend/src/main/java/com/chris/blog/service/impl/TagServiceImpl.java

  • Delete: backend/src/main/java/com/chris/blog/service/impl/SiteConfigServiceImpl.java

  • Delete: backend/src/main/java/com/chris/blog/service/impl/GithubProjectServiceImpl.java

  • Delete: backend/src/main/java/com/chris/blog/service/impl/PublishServiceImpl.java

  • Delete: backend/src/main/java/com/chris/blog/service/impl/MediaServiceImpl.java

  • Delete: backend/src/main/java/com/chris/blog/vo/ArticleVo.java

  • Delete: backend/src/main/java/com/chris/blog/vo/CategoryVo.java

  • Delete: backend/src/main/java/com/chris/blog/vo/TagVo.java

  • Delete: backend/src/main/java/com/chris/blog/vo/SiteConfigVo.java

  • Delete: backend/src/main/java/com/chris/blog/vo/GithubProjectVo.java

  • Delete: backend/src/main/java/com/chris/blog/vo/PublishSnapshotVo.java

  • Delete: backend/src/main/java/com/chris/blog/vo/MediaUploadVo.java

  • Delete: backend/src/main/java/com/chris/blog/enums/ArticleStatus.java

  • Delete: backend/src/main/java/com/chris/blog/enums/MediaType.java

  • Delete: backend/src/main/resources/mapper/ArticleMapper.xml

  • Delete: backend/src/main/resources/mapper/ArticleRevisionMapper.xml

  • Delete: backend/src/main/resources/mapper/PublishedArticleMapper.xml

  • Delete: backend/src/main/resources/mapper/CategoryMapper.xml

  • Delete: backend/src/main/resources/mapper/TagMapper.xml

  • Delete: backend/src/main/resources/mapper/SiteConfigMapper.xml

  • Delete: backend/src/main/resources/mapper/GithubProjectMapper.xml

  • Delete: backend/src/main/resources/mapper/PublishSnapshotMapper.xml

  • Delete: backend/src/main/java/com/chris/blog/config/JsonTypeHandler.java

  • Delete: backend/src/test/java/com/chris/blog/persistence/ArticlePublicationMigrationTest.java

  • Delete: backend/src/test/java/com/chris/blog/persistence/ContentApiMySqlIntegrationTest.java

  • Delete: backend/src/test/java/com/chris/blog/persistence/PublishSnapshotIntegrationTest.java

  • Delete: backend/src/test/java/com/chris/blog/persistence/SiteConfigMySqlIntegrationTest.java

  • Delete: backend/src/test/java/com/chris/blog/service/impl/MediaMinioIntegrationTest.java

  • Create: backend/src/test/java/com/chris/blog/config/RemovedCmsSurfaceTest.java

  • [ ] Step 1: 写废弃路由不存在的测试

使用 @SpringBootTest + MockMvc,认证后分别请求 /api/admin/articles、/categories、/tags、/site-config、/github-projects、/media/upload 和 /publish/snapshot,断言没有任何旧 Controller 映射;测试按 Spring 版本实际返回的 404 或拒绝信封锁定唯一结果,不允许返回 2xx。

  • [ ] Step 2: 运行测试并确认失败

Run: cd backend && mvn -Dtest=RemovedCmsSurfaceTest test

Expected: FAIL,因为旧路由仍存在。

  • [ ] Step 3: 删除旧业务代码与旧测试

使用 rg 确认剩余生产代码不再引用旧类型:

bash
rg -n "Article|Category|Tag|SiteConfig|GithubProject|PublishSnapshot|PublishedArticle|MediaService|OperationLog" backend/src/main

Expected: no matches,允许文档注释中与“图片 MIME media type”有关的普通单词,但不得保留旧业务类型引用。

  • [ ] Step 4: 验证后端可编译且旧路由消失

Run: cd backend && mvn test

Expected: 所有剩余测试 PASS。

  • [ ] Step 5: 提交
bash
git add -A backend/src/main backend/src/test
git commit -m "refactor: remove database cms and publishing modules"

Task 4:建立提示词表与持久化边界 ​

Files:

  • Modify: backend/src/main/resources/db/migration/V1__admin_and_prompt_schema.sql

  • Create: backend/src/main/java/com/chris/blog/bean/PromptRecord.java

  • Create: backend/src/main/java/com/chris/blog/enums/PromptDeleteState.java

  • Create: backend/src/main/java/com/chris/blog/mapper/PromptMapper.java

  • Create: backend/src/main/resources/mapper/PromptMapper.xml

  • Create: backend/src/main/java/com/chris/blog/repository/PromptRepository.java

  • Create: backend/src/main/java/com/chris/blog/repository/impl/MyBatisPromptRepository.java

  • Create: backend/src/main/java/com/chris/blog/vo/PageVo.java

  • Test: backend/src/test/java/com/chris/blog/persistence/PromptRepositoryMySqlIntegrationTest.java

  • [ ] Step 1: 写 MySQL 集成测试

覆盖:插入后完整读取、prompt_hash 唯一冲突、ACTIVE 列表排除 PENDING_DELETE、关键词 %/_ 按字面转义、默认 20/可选 50 分页、created_at DESC, id DESC 稳定排序、标记待删和条件物理删除。

  • [ ] Step 2: 运行测试并确认失败

Run: cd backend && mvn -Dtest=PromptRepositoryMySqlIntegrationTest test

Expected: FAIL,因为提示词表和仓储尚不存在。

  • [ ] Step 3: 在 V1 增加提示词表
sql
CREATE TABLE prompt_record (
    id CHAR(36) NOT NULL,
    prompt_text LONGTEXT NOT NULL,
    prompt_hash CHAR(64) NOT NULL,
    object_key VARCHAR(255) NOT NULL,
    mime_type VARCHAR(32) NOT NULL,
    width INT UNSIGNED NOT NULL,
    height INT UNSIGNED NOT NULL,
    file_size BIGINT UNSIGNED NOT NULL,
    image_sha256 CHAR(64) NOT NULL,
    delete_state VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
    created_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
    PRIMARY KEY (id),
    UNIQUE KEY uk_prompt_record_prompt_hash (prompt_hash),
    UNIQUE KEY uk_prompt_record_object_key (object_key),
    KEY idx_prompt_record_state_created (delete_state, created_at, id),
    CONSTRAINT ck_prompt_record_dimensions CHECK (width > 0 AND height > 0),
    CONSTRAINT ck_prompt_record_file_size CHECK (file_size > 0),
    CONSTRAINT ck_prompt_record_delete_state CHECK (delete_state IN ('ACTIVE', 'PENDING_DELETE'))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
  • [ ] Step 4: 实现明确的仓储接口
java
public interface PromptRepository {
    boolean existsByPromptHash(String promptHash);
    void insert(PromptRecord record);
    PromptRecord findById(String id);
    List<PromptRecord> findActivePage(String keyword, int limit, int offset);
    long countActive(String keyword);
    boolean markPendingDelete(String id);
    boolean deletePending(String id);
}

PromptMapper.xml 对关键词先在 Java 中转义 \\、%、_,SQL 使用 LIKE CONCAT('%', #{keyword}, '%') ESCAPE '\\'。所有字段和业务方法遵守仓库要求补充中文 Javadoc。

  • [ ] Step 5: 运行持久化测试

Run: cd backend && mvn -Dtest=PromptRepositoryMySqlIntegrationTest test

Expected: PASS。

  • [ ] Step 6: 提交
bash
git add backend/src/main/resources/db backend/src/main/resources/mapper/PromptMapper.xml backend/src/main/java/com/chris/blog/bean backend/src/main/java/com/chris/blog/enums backend/src/main/java/com/chris/blog/mapper/PromptMapper.java backend/src/main/java/com/chris/blog/repository backend/src/main/java/com/chris/blog/vo/PageVo.java backend/src/test/java/com/chris/blog/persistence/PromptRepositoryMySqlIntegrationTest.java
git commit -m "feat: add prompt persistence model"

Task 5:实现图片真实性校验与私有 MinIO 存储 ​

Files:

  • Modify: backend/pom.xml

  • Modify: backend/src/main/java/com/chris/blog/config/MinioConfig.java

  • Modify: backend/src/main/resources/application.yml

  • Modify: backend/src/main/resources/application-local.yml.example

  • Create: backend/src/main/java/com/chris/blog/dto/ValidatedPromptImage.java

  • Create: backend/src/main/java/com/chris/blog/service/PromptImageInspector.java

  • Create: backend/src/main/java/com/chris/blog/service/PromptObjectStorage.java

  • Create: backend/src/main/java/com/chris/blog/service/impl/DefaultPromptImageInspector.java

  • Create: backend/src/main/java/com/chris/blog/service/impl/MinioPromptObjectStorage.java

  • Test: backend/src/test/java/com/chris/blog/service/impl/DefaultPromptImageInspectorTest.java

  • Test: backend/src/test/java/com/chris/blog/service/impl/MinioPromptObjectStorageIntegrationTest.java

  • [ ] Step 1: 写图片校验单元测试

用测试资源生成最小 PNG/JPEG/WebP,覆盖允许格式、扩展名与内容不一致、GIF/SVG/PDF、空文件、实际流超过 10 MiB、宽或高超过 8192、总像素超过 40,000,000、不可解码内容,以及非 resettable 输入流。断言返回值包含由真实内容得到的 MIME、宽高、实际字节数、SHA-256 和字节流临时文件句柄。

  • [ ] Step 2: 写 MinIO 集成测试

Testcontainers 创建随机私有桶,覆盖上传、匿名 URL 返回 403、签名 URL 含 900 秒有效期且可读取、外部端点重写、删除存在对象、删除不存在对象仍成功。

  • [ ] Step 3: 运行测试并确认失败

Run: cd backend && mvn -Dtest='DefaultPromptImageInspectorTest,MinioPromptObjectStorageIntegrationTest' test

Expected: FAIL,因为图片检查器和提示词存储尚不存在。

  • [ ] Step 4: 添加 WebP ImageIO 解码器并实现检查器

在 pom.xml 添加 Maven Central 已存在的 TwelveMonkeys WebP ImageIO 插件:

xml
<dependency>
    <groupId>com.twelvemonkeys.imageio</groupId>
    <artifactId>imageio-webp</artifactId>
    <version>3.13.1</version>
</dependency>

实现必须把上传流复制到受控临时文件,同时累计实际长度和 SHA-256,超过 10 * 1024 * 1024 立即中止。使用 ImageIO reader 从内容读取格式与宽高,不信任原文件名或请求 MIME;只映射 PNG → image/png、JPEG → image/jpeg、WEBP → image/webp。在 finally 或 AutoCloseable.close() 中删除临时文件。

  • [ ] Step 5: 收敛 MinIO 配置和接口
java
public interface PromptObjectStorage {
    void upload(String objectKey, ValidatedPromptImage image);
    String createPreviewUrl(String objectKey, Duration validity);
    void deleteIfExists(String objectKey);
}

MinioProperties 只保留 endpoint、publicEndpoint、accessKey、secretKey 和默认值为 prompt-results 的 promptBucket。上传设置真实 Content-Type,不设置公共策略;预览固定由业务层传入 Duration.ofMinutes(15)。

  • [ ] Step 6: 运行图片与对象存储测试

Run: cd backend && mvn -Dtest='DefaultPromptImageInspectorTest,MinioPromptObjectStorageIntegrationTest' test

Expected: PASS。

  • [ ] Step 7: 提交
bash
git add backend/pom.xml backend/src/main/java/com/chris/blog/config/MinioConfig.java backend/src/main/java/com/chris/blog/dto backend/src/main/java/com/chris/blog/service backend/src/main/java/com/chris/blog/service/impl backend/src/main/resources/application.yml backend/src/main/resources/application-local.yml.example backend/src/test/java/com/chris/blog/service/impl
git commit -m "feat: validate and store private prompt images"

Task 6:实现提示词创建、查询与可重试删除用例 ​

Files:

  • Create: backend/src/main/java/com/chris/blog/service/PromptService.java

  • Create: backend/src/main/java/com/chris/blog/service/impl/PromptServiceImpl.java

  • Create: backend/src/main/java/com/chris/blog/vo/PromptListItemVo.java

  • Create: backend/src/main/java/com/chris/blog/vo/PromptDetailVo.java

  • Modify: backend/src/main/java/com/chris/blog/enums/ErrorCode.java

  • Modify: backend/src/main/java/com/chris/blog/config/GlobalExceptionHandler.java

  • Test: backend/src/test/java/com/chris/blog/service/impl/PromptServiceImplTest.java

  • [ ] Step 1: 写业务编排测试

覆盖以下精确行为;字符上限使用 Unicode code point 计数,不把一个补充平面字符误算成两个字符:

text
normalize = prompt.replace("\r\n", "\n").replace("\r", "\n").trim()
promptHash = SHA-256(UTF-8(normalize))
objectKey = "prompts/" + UUID + extensionFromVerifiedMime

测试空正文、50,001 字符、内部空白不变、规范化重复返回 409、同图不同提示词允许、上传后插库、插库失败补偿删对象、补偿失败只记录不覆盖原始错误、详情生成 15 分钟 URL、普通列表排除待删、首次删除标记后删对象再删行、MinIO 失败保留待删、重试成功、记录已不存在时 DELETE 仍成功。

  • [ ] Step 2: 运行测试并确认失败

Run: cd backend && mvn -Dtest=PromptServiceImplTest test

Expected: FAIL,因为业务服务尚不存在。

  • [ ] Step 3: 定义用例接口和响应模型
java
public interface PromptService {
    PromptDetailVo create(String promptText, MultipartFile image);
    PageVo<PromptListItemVo> list(String keyword, int page, int size);
    PromptDetailVo get(String id);
    void delete(String id);
}

只允许 size=20 或 size=50,page 从 1 开始。列表项返回短摘要和临时预览 URL;详情返回完整提示词与技术元数据。摘要只能用于展示,不另存数据库。

  • [ ] Step 4: 实现创建补偿和删除状态机

创建时先进行提示词与图片校验,再上传 MinIO,最后插入 MySQL。捕获任何数据库写入异常后调用 deleteIfExists(objectKey);唯一键冲突映射为 409 CONFLICT。删除时短事务标记 PENDING_DELETE,事务外删除 MinIO,再短事务物理删除;MinIO 不可用映射为 503 STORAGE_UNAVAILABLE。

  • [ ] Step 5: 运行业务测试

Run: cd backend && mvn -Dtest=PromptServiceImplTest test

Expected: PASS。

  • [ ] Step 6: 提交
bash
git add backend/src/main/java/com/chris/blog/service backend/src/main/java/com/chris/blog/vo backend/src/main/java/com/chris/blog/enums/ErrorCode.java backend/src/main/java/com/chris/blog/config/GlobalExceptionHandler.java backend/src/test/java/com/chris/blog/service/impl/PromptServiceImplTest.java
git commit -m "feat: add prompt lifecycle service"

Task 7:交付受会话与 CSRF 保护的提示词 API ​

Files:

  • Create: backend/src/main/java/com/chris/blog/controller/PromptController.java

  • Modify: backend/src/main/java/com/chris/blog/config/SecurityConfig.java

  • Test: backend/src/test/java/com/chris/blog/controller/PromptControllerTest.java

  • Test: backend/src/test/java/com/chris/blog/config/SecurityConfigBehaviorTest.java

  • Modify: backend/scripts/smoke-admin.py

  • [ ] Step 1: 写 Controller 与安全测试

覆盖:匿名 GET/POST/DELETE 返回 401;不存在匿名 /api/prompts;已登录 GET 可访问;POST/DELETE 缺 CSRF 返回 403;multipart 字段名固定且仅接受 prompt 和 image;列表参数默认 page=1&size=20;非法 size 返回 400;不存在详情返回 404;重复返回 409;存储不可用返回 503;PUT、批量、导入和公开接口没有映射;响应不包含堆栈、密钥和本地路径。

  • [ ] Step 2: 运行测试并确认失败

Run: cd backend && mvn -Dtest='PromptControllerTest,SecurityConfigBehaviorTest' test

Expected: FAIL,因为提示词 HTTP 路由尚不存在。

  • [ ] Step 3: 实现四个 API
java
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ApiResponse<PromptDetailVo> create(@RequestParam("prompt") String prompt,
                                           @RequestPart("image") MultipartFile image)

@GetMapping
public ApiResponse<PageVo<PromptListItemVo>> list(
        @RequestParam(defaultValue = "") String keyword,
        @RequestParam(defaultValue = "1") int page,
        @RequestParam(defaultValue = "20") int size)

@GetMapping("/{id}")
public ApiResponse<PromptDetailVo> get(@PathVariable String id)

@DeleteMapping("/{id}")
public ApiResponse<Void> delete(@PathVariable String id)

Controller 不处理对象存储或 SQL,只做 HTTP 参数绑定并调用 PromptService。

  • [ ] Step 4: 更新 smoke 脚本

脚本只验证登录、/auth/me、CSRF、自助改密可选流程和提示词 CRUD;彻底移除文章、分类、标签、配置、GitHub、媒体和发布检查。测试图片在脚本运行时生成到临时目录并在退出时清理。

  • [ ] Step 5: 运行后端全量测试

Run: cd backend && mvn test

Expected: PASS。

  • [ ] Step 6: 提交
bash
git add backend/src/main/java/com/chris/blog/controller/PromptController.java backend/src/main/java/com/chris/blog/config/SecurityConfig.java backend/src/test/java/com/chris/blog/controller backend/src/test/java/com/chris/blog/config/SecurityConfigBehaviorTest.java backend/scripts/smoke-admin.py
git commit -m "feat: expose secured prompt management api"

Task 8:更新 MySQL/MinIO 本地编排且不触碰既有对象 ​

Files:

  • Modify: ops/docker-compose.yml

  • Modify: ops/.env.example

  • Modify: ops/README.md

  • Test: tests/local-env-loader.test.mjs

  • [ ] Step 1: 增加编排契约断言

在现有 Node 测试中断言 compose 只初始化 prompt-results,执行 mc anonymous set none blog/prompt-results,后端环境使用 MINIO_PROMPT_BUCKET=prompt-results;不得出现 draft-media、public-media 或匿名下载策略。

  • [ ] Step 2: 运行测试并确认失败

Run: node --test tests/local-env-loader.test.mjs

Expected: FAIL,因为 compose 仍初始化旧媒体桶。

  • [ ] Step 3: 修改编排和操作说明

minio-init 使用 mc mb --ignore-existing blog/prompt-results,只设置私有策略,不删除任何已有桶或对象。README 写清楚:生产卷不可重建;仅隔离 QA 数据库 coding_blog_qa 可在本次迁移时删除重建。

  • [ ] Step 4: 验证 compose 配置

Run: docker compose --env-file ops/.env.example -f ops/docker-compose.yml config --quiet

Expected: exit 0。

Run: node --test tests/local-env-loader.test.mjs

Expected: PASS。

  • [ ] Step 5: 提交
bash
git add ops tests/local-env-loader.test.mjs
git commit -m "ops: provision private prompt image storage"

Task 9:把 Vue 管理端替换为 React 基础与认证壳 ​

Files:

  • Modify: admin-frontend/package.json

  • Modify: admin-frontend/package-lock.json

  • Modify: admin-frontend/vite.config.ts

  • Modify: admin-frontend/tsconfig.json

  • Modify: admin-frontend/index.html

  • Delete: admin-frontend/src/main.ts

  • Create: admin-frontend/src/main.tsx

  • Create: admin-frontend/src/App.tsx

  • Create: admin-frontend/src/api/types.ts

  • Create: admin-frontend/src/api/client.ts

  • Modify: admin-frontend/src/api/auth.ts

  • Create: admin-frontend/src/auth/AuthContext.tsx

  • Create: admin-frontend/src/auth/ProtectedRoute.tsx

  • Create: admin-frontend/src/layout/AdminLayout.tsx

  • Create: admin-frontend/src/pages/LoginPage.tsx

  • Create: admin-frontend/src/pages/PasswordPage.tsx

  • Create: admin-frontend/src/styles/theme.ts

  • Create: admin-frontend/src/styles/global.css

  • Create: admin-frontend/src/test/setup.ts

  • Delete: admin-frontend/src/App.vue

  • Delete: admin-frontend/src/components/MarkdownEditor.vue

  • Delete: admin-frontend/src/views/Dashboard.vue

  • Delete: admin-frontend/src/views/Layout.vue

  • Delete: admin-frontend/src/views/Login.vue

  • Delete: admin-frontend/src/views/article/ArticleEdit.vue

  • Delete: admin-frontend/src/views/article/ArticleList.vue

  • Delete: admin-frontend/src/views/category/CategoryList.vue

  • Delete: admin-frontend/src/views/config/SiteConfig.vue

  • Delete: admin-frontend/src/views/github/GithubProjectList.vue

  • Delete: admin-frontend/src/views/media/MediaLibrary.vue

  • Delete: admin-frontend/src/views/publish/PublishPage.vue

  • Delete: admin-frontend/src/views/tag/TagList.vue

  • Delete: admin-frontend/src/router/index.ts

  • Delete: admin-frontend/src/store/user.ts

  • Delete: admin-frontend/src/utils/request.ts

  • Delete: admin-frontend/src/api/article.ts

  • Delete: admin-frontend/src/api/category.ts

  • Delete: admin-frontend/src/api/config.ts

  • Delete: admin-frontend/src/api/github.ts

  • Delete: admin-frontend/src/api/media.ts

  • Delete: admin-frontend/src/api/publish.ts

  • Delete: admin-frontend/src/api/tag.ts

  • Delete: admin-frontend/tests/article-edit.test.mjs

  • Delete: admin-frontend/tests/article-list.test.mjs

  • Delete: admin-frontend/tests/dashboard.test.mjs

  • Delete: admin-frontend/tests/editor-upload.test.mjs

  • Delete: admin-frontend/tests/request.test.mjs

  • Delete: admin-frontend/tests/site-config.test.mjs

  • Test: admin-frontend/src/auth/AuthContext.test.tsx

  • Test: admin-frontend/src/pages/LoginPage.test.tsx

  • [ ] Step 1: 写 React 认证失败测试

Testing Library 覆盖:受保护路由启动时调用 /auth/me;匿名跳转 /login 并保留目标 URL;登录表单有可见标签、autocomplete=username/current-password、允许粘贴、显示密码按钮有 accessible name;成功登录仅在内存保存 CSRF;401 清空内存状态并回登录;不得读写 localStorage 中的凭据或 CSRF。

  • [ ] Step 2: 运行测试并确认失败

Run: cd admin-frontend && npm test -- --run

Expected: FAIL,因为 React 测试环境尚不存在。

  • [ ] Step 3: 替换依赖与 Vite 插件

运行:

bash
cd admin-frontend
npm remove vue vue-router pinia element-plus @element-plus/icons-vue marked highlight.js dompurify @vitejs/plugin-vue vue-tsc sass
npm install react react-dom react-router-dom axios antd @phosphor-icons/react
npm install -D @vitejs/plugin-react @types/react @types/react-dom vitest jsdom @testing-library/react @testing-library/user-event @testing-library/jest-dom

设置脚本:build 为 tsc -b && vite build,test 为 vitest。tsconfig.json 使用 "jsx": "react-jsx" 且不再包含 src/**/*.vue。vite.config.ts 使用 react(),保留 5173 端口与 /api 代理;index.html 入口改为 /src/main.tsx。

  • [ ] Step 4: 实现 API 客户端和内存认证状态

Axios withCredentials: true。写请求从 Context 注入当前 CSRF 值到后端返回的 header name;401 触发统一的 onUnauthorized,但不在 API 层直接渲染 toast。AuthContext 提供 user、status、login、logout、changePassword、refreshSession。

  • [ ] Step 5: 实现顶部导航、登录和改密页面

Ant Design ConfigProvider 使用 spec 中颜色令牌;顶部只显示“提示词管理”“提示词”和账号菜单。登录卡片最大宽 420px;错误字段使用 aria-describedby,提交错误使用 role="alert";改密成功后清空会话并导航登录。

  • [ ] Step 6: 验证认证壳

Run: cd admin-frontend && npm test -- --run

Expected: PASS。

Run: cd admin-frontend && npm run build

Expected: React production build succeeds,构建产物不包含 Vue/Element Plus chunk。

  • [ ] Step 7: 提交
bash
git add -A admin-frontend
git commit -m "refactor: rebuild admin shell with React"

Task 10:实现响应式提示词列表 ​

Files:

  • Create: admin-frontend/src/api/prompts.ts

  • Create: admin-frontend/src/pages/prompts/PromptListPage.tsx

  • Create: admin-frontend/src/pages/prompts/PromptCard.tsx

  • Create: admin-frontend/src/pages/prompts/listState.ts

  • Modify: admin-frontend/src/App.tsx

  • Modify: admin-frontend/src/styles/global.css

  • Test: admin-frontend/src/pages/prompts/PromptListPage.test.tsx

  • [ ] Step 1: 写列表行为测试

覆盖:首次请求 page=1&size=20;搜索提交重置为第 1 页;清空搜索;20/50 切换;加载骨架;空列表、无结果、失败重试三种状态;卡片展示图、摘要、尺寸、大小、时间;图片 loading="lazy" 且有固定 aspect-ratio;详情返回后从 session history state 恢复搜索、页码和滚动位置。

  • [ ] Step 2: 运行测试并确认失败

Run: cd admin-frontend && npm test -- --run src/pages/prompts/PromptListPage.test.tsx

Expected: FAIL,因为列表页面尚不存在。

  • [ ] Step 3: 实现类型安全 API 与列表状态
ts
export interface PromptListItem {
  id: string
  promptExcerpt: string
  previewUrl: string
  mimeType: 'image/png' | 'image/jpeg' | 'image/webp'
  width: number
  height: number
  fileSize: number
  createdAt: string
}

listState.ts 只负责 URL search params 与 history state 的序列化/恢复;API 层只发请求,不持有 UI 状态。

  • [ ] Step 4: 实现卡片网格和响应式 CSS

断点为 1 列默认、min-width: 768px 两列、min-width: 1024px 三列。删除入口不依赖 hover;空态提供“上传提示词”或“清空搜索”操作。所有图片使用描述性 alt,不以提示词全文作为超长 alt。

  • [ ] Step 5: 验证列表

Run: cd admin-frontend && npm test -- --run src/pages/prompts/PromptListPage.test.tsx

Expected: PASS。

Run: cd admin-frontend && npm run build

Expected: PASS。

  • [ ] Step 6: 提交
bash
git add admin-frontend/src
git commit -m "feat: add responsive prompt list"

Task 11:实现提示词与单图上传页面 ​

Files:

  • Create: admin-frontend/src/pages/prompts/PromptCreatePage.tsx

  • Create: admin-frontend/src/pages/prompts/ImagePicker.tsx

  • Create: admin-frontend/src/pages/prompts/promptValidation.ts

  • Modify: admin-frontend/src/api/prompts.ts

  • Modify: admin-frontend/src/App.tsx

  • Modify: admin-frontend/src/styles/global.css

  • Test: admin-frontend/src/pages/prompts/PromptCreatePage.test.tsx

  • [ ] Step 1: 写上传表单测试

覆盖:只接受一个 PNG/JPEG/WebP 候选;文件按钮是拖放的等价操作;本地预览 URL 在替换或卸载时 URL.revokeObjectURL;字符计数 0–50,000;空值和超长错误;客户端只做快速提示,后端错误仍是最终结果;提交生成字段名为 prompt/image 的 FormData;提交中按钮禁用;409、400、503 显示原因和恢复办法;失败保留正文与预览;多错误提交后聚焦错误摘要。

  • [ ] Step 2: 运行测试并确认失败

Run: cd admin-frontend && npm test -- --run src/pages/prompts/PromptCreatePage.test.tsx

Expected: FAIL,因为上传页面尚不存在。

  • [ ] Step 3: 实现图片选择器与预览

使用原生 <input type="file" accept="image/png,image/jpeg,image/webp"> 作为可访问基础;拖放层调用同一验证函数。浏览器可解码时显示自然宽高、文件名和格式化大小,预览区预留尺寸避免 CLS。

  • [ ] Step 4: 实现表单提交和错误恢复

提示词内部空白不在前端改写;只以 trim() 判断空值。后端字段错误映射到对应控件,非字段错误进入 role="alert" 的摘要。成功后跳转 /prompts/{id},失败不得 reset 表单。

  • [ ] Step 5: 验证上传页面

Run: cd admin-frontend && npm test -- --run src/pages/prompts/PromptCreatePage.test.tsx

Expected: PASS。

Run: cd admin-frontend && npm run build

Expected: PASS。

  • [ ] Step 6: 提交
bash
git add admin-frontend/src
git commit -m "feat: add prompt image upload flow"

Task 12:实现详情、复制与永久删除体验 ​

Files:

  • Create: admin-frontend/src/pages/prompts/PromptDetailPage.tsx

  • Create: admin-frontend/src/pages/prompts/DeletePromptDialog.tsx

  • Modify: admin-frontend/src/api/prompts.ts

  • Modify: admin-frontend/src/App.tsx

  • Modify: admin-frontend/src/styles/global.css

  • Test: admin-frontend/src/pages/prompts/PromptDetailPage.test.tsx

  • Test: admin-frontend/src/pages/prompts/DeletePromptDialog.test.tsx

  • [ ] Step 1: 写详情和删除测试

覆盖:完整提示词和技术元数据;复制调用 Clipboard API 并用 aria-live="polite" 通知;签名图片加载失败可重试刷新详情;删除按钮在危险区域;弹窗说明同时删除数据库与 MinIO 且不可恢复;取消为初始焦点;Tab 不离开弹窗;Escape 取消;关闭恢复触发按钮焦点;删除中禁用重复提交;503 保持弹窗并提供重试;成功回列表。

  • [ ] Step 2: 运行测试并确认失败

Run: cd admin-frontend && npm test -- --run src/pages/prompts/PromptDetailPage.test.tsx src/pages/prompts/DeletePromptDialog.test.tsx

Expected: FAIL,因为详情与删除组件尚不存在。

  • [ ] Step 3: 实现详情与安全删除

桌面双栏、移动端单列;提示词正文使用可换行且 overflow-wrap:anywhere 的只读区域。使用受控 Ant Design Modal,在 afterOpenChange(true) 中通过 ref 聚焦取消按钮;关闭后通过保存的触发按钮 ref 恢复焦点。

  • [ ] Step 4: 验证详情和删除

Run: cd admin-frontend && npm test -- --run src/pages/prompts/PromptDetailPage.test.tsx src/pages/prompts/DeletePromptDialog.test.tsx

Expected: PASS。

Run: cd admin-frontend && npm test -- --run && npm run build

Expected: 所有 React 测试和生产构建 PASS。

  • [ ] Step 5: 提交
bash
git add admin-frontend/src
git commit -m "feat: add prompt detail and permanent deletion"

Task 13:删除旧阶段文档并更新维护说明 ​

Files:

  • Delete: BACKEND_ADMIN_IMPLEMENTATION_STATUS.md

  • Delete: BACKEND_INTEGRATION_2026-09-22.md

  • Delete: FINAL_PROJECT_SUMMARY.md

  • Delete: FINAL_SUMMARY.md

  • Delete: IMPLEMENTATION_COMPLETE.md

  • Delete: PROJECT_COMPLETE_SUMMARY.md

  • Delete: PROJECT_FINAL_REPORT.md

  • Delete: STAGE1_SUMMARY.md

  • Delete: STAGE2A_SUMMARY.md

  • Delete: STAGE2B_SUMMARY.md

  • Delete: STAGE2C_SUMMARY.md

  • Delete: VUE_FRONTEND_PLAN.md

  • Modify: DEPLOYMENT.md

  • Modify: admin-frontend/README.md

  • Create: backend/README.md

  • [ ] Step 1: 删除会误导维护者的旧 CMS 报告

只删除设计规范第 4.2 节列出的旧报告;保留本设计与实施计划。若 backend/README.md 当前不存在,则创建它并说明模块启动、环境变量、API 范围和测试命令。

  • [ ] Step 2: 更新部署与开发说明

文档必须明确:公开博客静态部署;后台不发布博客;prompt-results 私有;签名 URL 15 分钟;生产数据库与 MinIO 卷不可随意重建;本地启动分别使用 npm run dev、cd backend && mvn spring-boot:run、cd admin-frontend && npm run dev。

  • [ ] Step 3: 检查废弃术语

Run:

bash
rg -n "Vue|Element Plus|文章管理|分类管理|标签管理|发布快照|export:backend|draft-media|public-media" README.md DEPLOYMENT.md backend admin-frontend ops package.json --glob '!**/target/**' --glob '!**/node_modules/**' --glob '!**/dist/**'

Expected: 仅允许设计/迁移说明中明确写着“已删除”的文字;运行代码与当前 README 中无旧功能入口。

  • [ ] Step 4: 提交
bash
git add DEPLOYMENT.md admin-frontend/README.md backend/README.md
git commit -m "docs: replace cms reports with prompt admin guidance"

Task 14:隔离 QA 重建与端到端验收 ​

Files:

  • Modify: backend/scripts/smoke-admin.py(仅在验收发现契约缺口时)

  • Modify: ops/README.md(仅记录实际验证命令或已确认限制)

  • [ ] Step 1: 运行所有静态检查和测试

Run:

bash
node --test tests/*.test.mjs
npm run build
cd backend && mvn clean test
cd ../admin-frontend && npm test -- --run
npm run build

Expected: 全部 PASS;若有与本次无关的既有失败,记录准确测试名与证据,不得跳过本次相关失败。

  • [ ] Step 2: 精确确认 QA 容器和数据库目标

Run:

bash
docker inspect --format '{{.Name}}' blog-qa-mysql-20260922
docker exec blog-qa-mysql-20260922 sh -lc 'mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -Nse "SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME='"'"'coding_blog_qa'"'"'"'

Expected: 容器名严格为 /blog-qa-mysql-20260922,查询只返回 coding_blog_qa。变量缺失时停止并从已有 QA 配置加载,不猜测密码。

  • [ ] Step 3: 只重建隔离 QA 数据库

在 Step 2 两项完全匹配后执行:

bash
docker exec blog-qa-mysql-20260922 sh -lc 'mysql -uroot -p"$MYSQL_ROOT_PASSWORD" -e "DROP DATABASE coding_blog_qa; CREATE DATABASE coding_blog_qa CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;"'

Expected: 只删除并重建 coding_blog_qa;不得删除容器、volume、其他 schema、MinIO bucket 或对象。

  • [ ] Step 4: 初始化私有桶并启动前后端

用当前 QA 环境变量启动后端和 React 前端;执行 mc mb --ignore-existing 与 mc anonymous set none 初始化 prompt-results。通过 MinIO 管理命令确认匿名访问为 none。

  • [ ] Step 5: 运行 smoke 与真实浏览器验收

Run: python3 backend/scripts/smoke-admin.py --base-url http://127.0.0.1:18080

Expected: 登录、CSRF、创建、分页搜索、详情、签名预览、永久删除、自助改密全部成功。

浏览器分别以 375、768、1024、1440 宽度验证:登录、上传 PNG/JPEG/WebP、重复提示词 409、非法图片、搜索、20/50 分页、详情、复制、删除失败重试、会话过期、键盘焦点、prefers-reduced-motion。确认公开 VitePress 博客在后端停止时仍可访问文章。

  • [ ] Step 6: 检查最终仓库状态和差异

Run:

bash
git status --short
git diff --check
git log --oneline --decorate -15
git diff --exit-code d4b6aae -- docs/public/gpt-image-prompts.json docs/public/gpt-image-prompts docs/gpt-image-prompts

Expected: 无意外生成物、无空白错误;只包含计划内变更,用户原有不相关修改保持不动;现有静态提示词档案没有被导入、改写或删除。

  • [ ] Step 7: 提交验收修正(如有)

若验收产生了必要修正:

bash
git add backend/scripts/smoke-admin.py ops/README.md
git commit -m "test: complete prompt admin acceptance coverage"

若没有文件变化,不创建空提交。

Last updated:

最后更新2026-09-22
觉得有帮助?把这个链接转给正在求职的朋友 · 用 Ctrl + K 全站搜索其它题