Appearance
静态博客与提示词管理后台重构 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-resultsMinIO 桶和后端运行配置。
当前工作树在计划开始前已经包含未提交的后台原型和根目录修改。执行每个提交前必须运行 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.mjsModify:
package.jsonModify:
README.mdDelete:
scripts/export-backend-snapshot.mjsDelete:
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.sqlDelete:
backend/src/main/resources/db/migration/V2__content_management.sqlDelete:
backend/src/main/resources/db/migration/V3__immutable_publish_snapshot.sqlDelete:
backend/src/main/resources/db/migration/V4__immutable_article_publication.sqlModify:
backend/src/main/java/com/chris/blog/service/AdminUserService.javaModify:
backend/src/main/java/com/chris/blog/service/impl/AdminUserServiceImpl.javaModify:
backend/src/main/java/com/chris/blog/controller/AdminAuthController.javaModify:
backend/src/main/java/com/chris/blog/repository/AdminUserRepository.javaModify:
backend/src/main/java/com/chris/blog/repository/impl/MyBatisAdminUserRepository.javaModify:
backend/src/main/java/com/chris/blog/mapper/AdminUserMapper.javaModify:
backend/src/main/resources/mapper/AdminUserMapper.xmlDelete:
backend/src/main/java/com/chris/blog/controller/AdminUserController.javaDelete:
backend/src/main/java/com/chris/blog/bean/AdminUserEnabledGuard.javaDelete:
backend/src/main/java/com/chris/blog/enums/AdminUserEnabledChangeResult.javaDelete:
backend/src/main/java/com/chris/blog/req/CreateAdminUserRequest.javaDelete:
backend/src/main/java/com/chris/blog/req/UpdateAdminUserRequest.javaDelete:
backend/src/main/java/com/chris/blog/req/ResetPasswordRequest.javaDelete:
backend/src/main/java/com/chris/blog/req/ChangeEnabledRequest.javaDelete:
backend/src/main/java/com/chris/blog/service/OperationLogService.javaDelete:
backend/src/main/java/com/chris/blog/service/impl/OperationLogServiceImpl.javaDelete:
backend/src/main/java/com/chris/blog/repository/OperationLogRepository.javaDelete:
backend/src/main/java/com/chris/blog/repository/impl/MyBatisOperationLogRepository.javaDelete:
backend/src/main/java/com/chris/blog/mapper/OperationLogMapper.javaDelete:
backend/src/main/resources/mapper/OperationLogMapper.xmlDelete:
backend/src/main/java/com/chris/blog/bean/OperationLog.javaDelete:
backend/src/main/java/com/chris/blog/enums/OperationAction.javaTest:
backend/src/test/java/com/chris/blog/persistence/AdminAuthenticationPersistenceContractTest.javaTest:
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.javaDelete:
backend/src/main/java/com/chris/blog/bean/ArticleRevision.javaDelete:
backend/src/main/java/com/chris/blog/bean/PublishedArticle.javaDelete:
backend/src/main/java/com/chris/blog/bean/Category.javaDelete:
backend/src/main/java/com/chris/blog/bean/Tag.javaDelete:
backend/src/main/java/com/chris/blog/bean/SiteConfig.javaDelete:
backend/src/main/java/com/chris/blog/bean/GithubProject.javaDelete:
backend/src/main/java/com/chris/blog/controller/ArticleController.javaDelete:
backend/src/main/java/com/chris/blog/controller/CategoryController.javaDelete:
backend/src/main/java/com/chris/blog/controller/TagController.javaDelete:
backend/src/main/java/com/chris/blog/controller/SiteConfigController.javaDelete:
backend/src/main/java/com/chris/blog/controller/GithubProjectController.javaDelete:
backend/src/main/java/com/chris/blog/controller/PublishController.javaDelete:
backend/src/main/java/com/chris/blog/controller/MediaController.javaDelete:
backend/src/main/java/com/chris/blog/mapper/ArticleMapper.javaDelete:
backend/src/main/java/com/chris/blog/mapper/ArticleRevisionMapper.javaDelete:
backend/src/main/java/com/chris/blog/mapper/PublishedArticleMapper.javaDelete:
backend/src/main/java/com/chris/blog/mapper/CategoryMapper.javaDelete:
backend/src/main/java/com/chris/blog/mapper/TagMapper.javaDelete:
backend/src/main/java/com/chris/blog/mapper/SiteConfigMapper.javaDelete:
backend/src/main/java/com/chris/blog/mapper/GithubProjectMapper.javaDelete:
backend/src/main/java/com/chris/blog/mapper/PublishSnapshotMapper.javaDelete:
backend/src/main/java/com/chris/blog/repository/ArticleRepository.javaDelete:
backend/src/main/java/com/chris/blog/repository/ArticleRevisionRepository.javaDelete:
backend/src/main/java/com/chris/blog/repository/PublishedArticleRepository.javaDelete:
backend/src/main/java/com/chris/blog/repository/CategoryRepository.javaDelete:
backend/src/main/java/com/chris/blog/repository/TagRepository.javaDelete:
backend/src/main/java/com/chris/blog/repository/SiteConfigRepository.javaDelete:
backend/src/main/java/com/chris/blog/repository/GithubProjectRepository.javaDelete:
backend/src/main/java/com/chris/blog/repository/impl/MyBatisArticleRepository.javaDelete:
backend/src/main/java/com/chris/blog/repository/impl/MyBatisArticleRevisionRepository.javaDelete:
backend/src/main/java/com/chris/blog/repository/impl/MyBatisPublishedArticleRepository.javaDelete:
backend/src/main/java/com/chris/blog/repository/impl/MyBatisCategoryRepository.javaDelete:
backend/src/main/java/com/chris/blog/repository/impl/MyBatisTagRepository.javaDelete:
backend/src/main/java/com/chris/blog/repository/impl/MyBatisSiteConfigRepository.javaDelete:
backend/src/main/java/com/chris/blog/repository/impl/MyBatisGithubProjectRepository.javaDelete:
backend/src/main/java/com/chris/blog/req/CreateArticleRequest.javaDelete:
backend/src/main/java/com/chris/blog/req/UpdateArticleRequest.javaDelete:
backend/src/main/java/com/chris/blog/req/CreateCategoryRequest.javaDelete:
backend/src/main/java/com/chris/blog/req/UpdateCategoryRequest.javaDelete:
backend/src/main/java/com/chris/blog/req/CreateTagRequest.javaDelete:
backend/src/main/java/com/chris/blog/req/UpdateTagRequest.javaDelete:
backend/src/main/java/com/chris/blog/req/CreateSiteConfigRequest.javaDelete:
backend/src/main/java/com/chris/blog/req/UpdateSiteConfigRequest.javaDelete:
backend/src/main/java/com/chris/blog/req/CreateGithubProjectRequest.javaDelete:
backend/src/main/java/com/chris/blog/req/UpdateGithubProjectRequest.javaDelete:
backend/src/main/java/com/chris/blog/service/ArticleService.javaDelete:
backend/src/main/java/com/chris/blog/service/CategoryService.javaDelete:
backend/src/main/java/com/chris/blog/service/TagService.javaDelete:
backend/src/main/java/com/chris/blog/service/SiteConfigService.javaDelete:
backend/src/main/java/com/chris/blog/service/GithubProjectService.javaDelete:
backend/src/main/java/com/chris/blog/service/PublishService.javaDelete:
backend/src/main/java/com/chris/blog/service/MediaService.javaDelete:
backend/src/main/java/com/chris/blog/service/impl/ArticleServiceImpl.javaDelete:
backend/src/main/java/com/chris/blog/service/impl/CategoryServiceImpl.javaDelete:
backend/src/main/java/com/chris/blog/service/impl/TagServiceImpl.javaDelete:
backend/src/main/java/com/chris/blog/service/impl/SiteConfigServiceImpl.javaDelete:
backend/src/main/java/com/chris/blog/service/impl/GithubProjectServiceImpl.javaDelete:
backend/src/main/java/com/chris/blog/service/impl/PublishServiceImpl.javaDelete:
backend/src/main/java/com/chris/blog/service/impl/MediaServiceImpl.javaDelete:
backend/src/main/java/com/chris/blog/vo/ArticleVo.javaDelete:
backend/src/main/java/com/chris/blog/vo/CategoryVo.javaDelete:
backend/src/main/java/com/chris/blog/vo/TagVo.javaDelete:
backend/src/main/java/com/chris/blog/vo/SiteConfigVo.javaDelete:
backend/src/main/java/com/chris/blog/vo/GithubProjectVo.javaDelete:
backend/src/main/java/com/chris/blog/vo/PublishSnapshotVo.javaDelete:
backend/src/main/java/com/chris/blog/vo/MediaUploadVo.javaDelete:
backend/src/main/java/com/chris/blog/enums/ArticleStatus.javaDelete:
backend/src/main/java/com/chris/blog/enums/MediaType.javaDelete:
backend/src/main/resources/mapper/ArticleMapper.xmlDelete:
backend/src/main/resources/mapper/ArticleRevisionMapper.xmlDelete:
backend/src/main/resources/mapper/PublishedArticleMapper.xmlDelete:
backend/src/main/resources/mapper/CategoryMapper.xmlDelete:
backend/src/main/resources/mapper/TagMapper.xmlDelete:
backend/src/main/resources/mapper/SiteConfigMapper.xmlDelete:
backend/src/main/resources/mapper/GithubProjectMapper.xmlDelete:
backend/src/main/resources/mapper/PublishSnapshotMapper.xmlDelete:
backend/src/main/java/com/chris/blog/config/JsonTypeHandler.javaDelete:
backend/src/test/java/com/chris/blog/persistence/ArticlePublicationMigrationTest.javaDelete:
backend/src/test/java/com/chris/blog/persistence/ContentApiMySqlIntegrationTest.javaDelete:
backend/src/test/java/com/chris/blog/persistence/PublishSnapshotIntegrationTest.javaDelete:
backend/src/test/java/com/chris/blog/persistence/SiteConfigMySqlIntegrationTest.javaDelete:
backend/src/test/java/com/chris/blog/service/impl/MediaMinioIntegrationTest.javaCreate:
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/mainExpected: 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.sqlCreate:
backend/src/main/java/com/chris/blog/bean/PromptRecord.javaCreate:
backend/src/main/java/com/chris/blog/enums/PromptDeleteState.javaCreate:
backend/src/main/java/com/chris/blog/mapper/PromptMapper.javaCreate:
backend/src/main/resources/mapper/PromptMapper.xmlCreate:
backend/src/main/java/com/chris/blog/repository/PromptRepository.javaCreate:
backend/src/main/java/com/chris/blog/repository/impl/MyBatisPromptRepository.javaCreate:
backend/src/main/java/com/chris/blog/vo/PageVo.javaTest:
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.xmlModify:
backend/src/main/java/com/chris/blog/config/MinioConfig.javaModify:
backend/src/main/resources/application.ymlModify:
backend/src/main/resources/application-local.yml.exampleCreate:
backend/src/main/java/com/chris/blog/dto/ValidatedPromptImage.javaCreate:
backend/src/main/java/com/chris/blog/service/PromptImageInspector.javaCreate:
backend/src/main/java/com/chris/blog/service/PromptObjectStorage.javaCreate:
backend/src/main/java/com/chris/blog/service/impl/DefaultPromptImageInspector.javaCreate:
backend/src/main/java/com/chris/blog/service/impl/MinioPromptObjectStorage.javaTest:
backend/src/test/java/com/chris/blog/service/impl/DefaultPromptImageInspectorTest.javaTest:
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.javaCreate:
backend/src/main/java/com/chris/blog/service/impl/PromptServiceImpl.javaCreate:
backend/src/main/java/com/chris/blog/vo/PromptListItemVo.javaCreate:
backend/src/main/java/com/chris/blog/vo/PromptDetailVo.javaModify:
backend/src/main/java/com/chris/blog/enums/ErrorCode.javaModify:
backend/src/main/java/com/chris/blog/config/GlobalExceptionHandler.javaTest:
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.javaModify:
backend/src/main/java/com/chris/blog/config/SecurityConfig.javaTest:
backend/src/test/java/com/chris/blog/controller/PromptControllerTest.javaTest:
backend/src/test/java/com/chris/blog/config/SecurityConfigBehaviorTest.javaModify:
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.ymlModify:
ops/.env.exampleModify:
ops/README.mdTest:
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.jsonModify:
admin-frontend/package-lock.jsonModify:
admin-frontend/vite.config.tsModify:
admin-frontend/tsconfig.jsonModify:
admin-frontend/index.htmlDelete:
admin-frontend/src/main.tsCreate:
admin-frontend/src/main.tsxCreate:
admin-frontend/src/App.tsxCreate:
admin-frontend/src/api/types.tsCreate:
admin-frontend/src/api/client.tsModify:
admin-frontend/src/api/auth.tsCreate:
admin-frontend/src/auth/AuthContext.tsxCreate:
admin-frontend/src/auth/ProtectedRoute.tsxCreate:
admin-frontend/src/layout/AdminLayout.tsxCreate:
admin-frontend/src/pages/LoginPage.tsxCreate:
admin-frontend/src/pages/PasswordPage.tsxCreate:
admin-frontend/src/styles/theme.tsCreate:
admin-frontend/src/styles/global.cssCreate:
admin-frontend/src/test/setup.tsDelete:
admin-frontend/src/App.vueDelete:
admin-frontend/src/components/MarkdownEditor.vueDelete:
admin-frontend/src/views/Dashboard.vueDelete:
admin-frontend/src/views/Layout.vueDelete:
admin-frontend/src/views/Login.vueDelete:
admin-frontend/src/views/article/ArticleEdit.vueDelete:
admin-frontend/src/views/article/ArticleList.vueDelete:
admin-frontend/src/views/category/CategoryList.vueDelete:
admin-frontend/src/views/config/SiteConfig.vueDelete:
admin-frontend/src/views/github/GithubProjectList.vueDelete:
admin-frontend/src/views/media/MediaLibrary.vueDelete:
admin-frontend/src/views/publish/PublishPage.vueDelete:
admin-frontend/src/views/tag/TagList.vueDelete:
admin-frontend/src/router/index.tsDelete:
admin-frontend/src/store/user.tsDelete:
admin-frontend/src/utils/request.tsDelete:
admin-frontend/src/api/article.tsDelete:
admin-frontend/src/api/category.tsDelete:
admin-frontend/src/api/config.tsDelete:
admin-frontend/src/api/github.tsDelete:
admin-frontend/src/api/media.tsDelete:
admin-frontend/src/api/publish.tsDelete:
admin-frontend/src/api/tag.tsDelete:
admin-frontend/tests/article-edit.test.mjsDelete:
admin-frontend/tests/article-list.test.mjsDelete:
admin-frontend/tests/dashboard.test.mjsDelete:
admin-frontend/tests/editor-upload.test.mjsDelete:
admin-frontend/tests/request.test.mjsDelete:
admin-frontend/tests/site-config.test.mjsTest:
admin-frontend/src/auth/AuthContext.test.tsxTest:
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.tsCreate:
admin-frontend/src/pages/prompts/PromptListPage.tsxCreate:
admin-frontend/src/pages/prompts/PromptCard.tsxCreate:
admin-frontend/src/pages/prompts/listState.tsModify:
admin-frontend/src/App.tsxModify:
admin-frontend/src/styles/global.cssTest:
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.tsxCreate:
admin-frontend/src/pages/prompts/ImagePicker.tsxCreate:
admin-frontend/src/pages/prompts/promptValidation.tsModify:
admin-frontend/src/api/prompts.tsModify:
admin-frontend/src/App.tsxModify:
admin-frontend/src/styles/global.cssTest:
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.tsxCreate:
admin-frontend/src/pages/prompts/DeletePromptDialog.tsxModify:
admin-frontend/src/api/prompts.tsModify:
admin-frontend/src/App.tsxModify:
admin-frontend/src/styles/global.cssTest:
admin-frontend/src/pages/prompts/PromptDetailPage.test.tsxTest:
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.mdDelete:
BACKEND_INTEGRATION_2026-09-22.mdDelete:
FINAL_PROJECT_SUMMARY.mdDelete:
FINAL_SUMMARY.mdDelete:
IMPLEMENTATION_COMPLETE.mdDelete:
PROJECT_COMPLETE_SUMMARY.mdDelete:
PROJECT_FINAL_REPORT.mdDelete:
STAGE1_SUMMARY.mdDelete:
STAGE2A_SUMMARY.mdDelete:
STAGE2B_SUMMARY.mdDelete:
STAGE2C_SUMMARY.mdDelete:
VUE_FRONTEND_PLAN.mdModify:
DEPLOYMENT.mdModify:
admin-frontend/README.mdCreate:
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 buildExpected: 全部 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-promptsExpected: 无意外生成物、无空白错误;只包含计划内变更,用户原有不相关修改保持不动;现有静态提示词档案没有被导入、改写或删除。
- [ ] Step 7: 提交验收修正(如有)
若验收产生了必要修正:
bash
git add backend/scripts/smoke-admin.py ops/README.md
git commit -m "test: complete prompt admin acceptance coverage"若没有文件变化,不创建空提交。