mirror of
https://github.com/iflytek/skillhub.git
synced 2026-10-07 02:57:51 +00:00
docs: add Phase 3 implementation plan
- Complete Chunk 1 detailed tasks (database, entities, repositories, permissions, services) - Add Chunk 2-5 task outlines - Include TDD steps with test-first approach - Add verification commands and expected outputs - Total 5 chunks for progressive delivery
This commit is contained in:
parent
3df70e0ff5
commit
56ccf3181b
1 changed files with 798 additions and 0 deletions
798
docs/superpowers/plans/2026-03-12-phase3-review-cli-social.md
Normal file
798
docs/superpowers/plans/2026-03-12-phase3-review-cli-social.md
Normal file
|
|
@ -0,0 +1,798 @@
|
|||
# Phase 3: 审核流程 + CLI API + 评分收藏 + 兼容层 Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** 在 Phase 2 基础上建立完整的治理体系、CLI 生态和社交功能,实现审核流程、提升机制、评分收藏、CLI API、ClawHub 兼容层和管理后台。
|
||||
|
||||
**Architecture:**
|
||||
- 审核流程:乐观锁 + partial unique index 防止并发冲突,分级权限控制
|
||||
- 评分收藏:异步事件 + Redis 分布式锁更新计数器
|
||||
- CLI API:OAuth Device Flow 标准认证流程
|
||||
- 兼容层:Canonical slug 映射实现 ClawHub CLI 协议兼容
|
||||
- 幂等去重:Redis SETNX + PostgreSQL 双层防护
|
||||
|
||||
**Tech Stack:**
|
||||
- 后端:Spring Boot 3.x + JDK 21 + PostgreSQL 16 + Redis 7 + Spring Security + Flyway
|
||||
- 前端:React 19 + TypeScript + Vite + TanStack Router + TanStack Query + shadcn/ui
|
||||
- 新增:react-rating-stars-component(评分组件)
|
||||
|
||||
---
|
||||
|
||||
## Chunk 1: 审核流程核心(后端)
|
||||
|
||||
**范围:** 数据库迁移 + 审核流程 + 提升流程 + 乐观锁 + 分级权限
|
||||
|
||||
**验收标准:**
|
||||
1. 用户可以提交审核,创建 review_task(status=PENDING)
|
||||
2. 审核人可以通过/拒绝审核,乐观锁防止并发冲突
|
||||
3. 审核通过后,skill_version.status → PUBLISHED,触发搜索索引更新
|
||||
4. 审核拒绝后,skill_version.status → REJECTED,记录拒绝原因
|
||||
5. 用户可以撤回 PENDING 状态的审核
|
||||
6. 团队管理员只能审核自己管理的 namespace 的技能
|
||||
7. 平台 SKILL_ADMIN 只能审核全局空间的技能
|
||||
8. 用户可以提交提升请求,创建 promotion_request(status=PENDING)
|
||||
9. 平台 SKILL_ADMIN 可以审核提升请求
|
||||
10. 提升通过后,在全局空间创建新 skill,复制版本和文件
|
||||
11. 所有审核操作写入 audit_log
|
||||
12. 所有测试通过
|
||||
|
||||
### Task 1: 数据库迁移脚本
|
||||
|
||||
**Files:**
|
||||
- Create: `server/skillhub-app/src/main/resources/db/migration/V3__phase3_review_social_tables.sql`
|
||||
|
||||
- [ ] **Step 1: 创建数据库迁移脚本**
|
||||
|
||||
创建 `V3__phase3_review_social_tables.sql`,包含 5 个新表:
|
||||
|
||||
```sql
|
||||
-- review_task 表
|
||||
CREATE TABLE review_task (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
skill_version_id BIGINT NOT NULL REFERENCES skill_version(id),
|
||||
namespace_id BIGINT NOT NULL REFERENCES namespace(id),
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'PENDING',
|
||||
version INT NOT NULL DEFAULT 1,
|
||||
submitted_by BIGINT NOT NULL REFERENCES user_account(id),
|
||||
reviewed_by BIGINT REFERENCES user_account(id),
|
||||
review_comment TEXT,
|
||||
submitted_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
reviewed_at TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX idx_review_task_namespace_status ON review_task(namespace_id, status);
|
||||
CREATE INDEX idx_review_task_submitted_by_status ON review_task(submitted_by, status);
|
||||
CREATE UNIQUE INDEX idx_review_task_version_pending ON review_task(skill_version_id) WHERE status = 'PENDING';
|
||||
|
||||
-- promotion_request 表
|
||||
CREATE TABLE promotion_request (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
source_skill_id BIGINT NOT NULL REFERENCES skill(id),
|
||||
source_version_id BIGINT NOT NULL REFERENCES skill_version(id),
|
||||
target_namespace_id BIGINT NOT NULL REFERENCES namespace(id),
|
||||
target_skill_id BIGINT REFERENCES skill(id),
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'PENDING',
|
||||
version INT NOT NULL DEFAULT 1,
|
||||
submitted_by BIGINT NOT NULL REFERENCES user_account(id),
|
||||
reviewed_by BIGINT REFERENCES user_account(id),
|
||||
review_comment TEXT,
|
||||
submitted_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
reviewed_at TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX idx_promotion_request_source_skill ON promotion_request(source_skill_id);
|
||||
CREATE INDEX idx_promotion_request_status ON promotion_request(status);
|
||||
CREATE UNIQUE INDEX idx_promotion_request_version_pending ON promotion_request(source_version_id) WHERE status = 'PENDING';
|
||||
|
||||
-- skill_star 表
|
||||
CREATE TABLE skill_star (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
skill_id BIGINT NOT NULL REFERENCES skill(id),
|
||||
user_id BIGINT NOT NULL REFERENCES user_account(id),
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(skill_id, user_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_skill_star_user_id ON skill_star(user_id);
|
||||
CREATE INDEX idx_skill_star_skill_id ON skill_star(skill_id);
|
||||
|
||||
-- skill_rating 表
|
||||
CREATE TABLE skill_rating (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
skill_id BIGINT NOT NULL REFERENCES skill(id),
|
||||
user_id BIGINT NOT NULL REFERENCES user_account(id),
|
||||
score SMALLINT NOT NULL CHECK (score >= 1 AND score <= 5),
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(skill_id, user_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_skill_rating_skill_id ON skill_rating(skill_id);
|
||||
|
||||
-- idempotency_record 表
|
||||
CREATE TABLE idempotency_record (
|
||||
request_id VARCHAR(64) PRIMARY KEY,
|
||||
resource_type VARCHAR(64) NOT NULL,
|
||||
resource_id BIGINT,
|
||||
status VARCHAR(32) NOT NULL,
|
||||
response_status_code INT,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
expires_at TIMESTAMP NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX idx_idempotency_record_expires_at ON idempotency_record(expires_at);
|
||||
CREATE INDEX idx_idempotency_record_status_created ON idempotency_record(status, created_at);
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 验证迁移脚本语法**
|
||||
|
||||
运行:`cd server && ./mvnw flyway:validate`
|
||||
预期:SUCCESS
|
||||
|
||||
- [ ] **Step 3: 执行数据库迁移**
|
||||
|
||||
运行:`cd server && ./mvnw flyway:migrate`
|
||||
预期:V3 迁移成功,5 个新表创建
|
||||
|
||||
- [ ] **Step 4: 验证表结构**
|
||||
|
||||
运行:`psql -d skillhub -c "\d review_task"`
|
||||
预期:显示表结构,包含 partial unique index
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add server/skillhub-app/src/main/resources/db/migration/V3__phase3_review_social_tables.sql
|
||||
git commit -m "feat(db): add Phase 3 database migration
|
||||
|
||||
- Add review_task table with partial unique index
|
||||
- Add promotion_request table
|
||||
- Add skill_star and skill_rating tables
|
||||
- Add idempotency_record table"
|
||||
```
|
||||
|
||||
### Task 2: 审核流程领域实体
|
||||
|
||||
**Files:**
|
||||
- Create: `server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/ReviewTask.java`
|
||||
- Create: `server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/ReviewTaskStatus.java`
|
||||
- Create: `server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/PromotionRequest.java`
|
||||
|
||||
- [ ] **Step 1: 创建 ReviewTaskStatus 枚举**
|
||||
|
||||
创建 `server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/ReviewTaskStatus.java`:
|
||||
|
||||
```java
|
||||
package com.iflytek.skillhub.domain.review;
|
||||
|
||||
public enum ReviewTaskStatus {
|
||||
PENDING,
|
||||
APPROVED,
|
||||
REJECTED
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 创建 ReviewTask 实体**
|
||||
|
||||
创建 `server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/ReviewTask.java`:
|
||||
|
||||
```java
|
||||
package com.iflytek.skillhub.domain.review;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.time.Instant;
|
||||
|
||||
@Entity
|
||||
@Table(name = "review_task")
|
||||
public class ReviewTask {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "skill_version_id", nullable = false)
|
||||
private Long skillVersionId;
|
||||
|
||||
@Column(name = "namespace_id", nullable = false)
|
||||
private Long namespaceId;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false)
|
||||
private ReviewTaskStatus status = ReviewTaskStatus.PENDING;
|
||||
|
||||
@Version
|
||||
@Column(nullable = false)
|
||||
private Integer version = 1;
|
||||
|
||||
@Column(name = "submitted_by", nullable = false)
|
||||
private Long submittedBy;
|
||||
|
||||
@Column(name = "reviewed_by")
|
||||
private Long reviewedBy;
|
||||
|
||||
@Column(name = "review_comment", columnDefinition = "TEXT")
|
||||
private String reviewComment;
|
||||
|
||||
@Column(name = "submitted_at", nullable = false)
|
||||
private Instant submittedAt = Instant.now();
|
||||
|
||||
@Column(name = "reviewed_at")
|
||||
private Instant reviewedAt;
|
||||
|
||||
// Constructors
|
||||
protected ReviewTask() {}
|
||||
|
||||
public ReviewTask(Long skillVersionId, Long namespaceId, Long submittedBy) {
|
||||
this.skillVersionId = skillVersionId;
|
||||
this.namespaceId = namespaceId;
|
||||
this.submittedBy = submittedBy;
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
public Long getId() { return id; }
|
||||
public Long getSkillVersionId() { return skillVersionId; }
|
||||
public Long getNamespaceId() { return namespaceId; }
|
||||
public ReviewTaskStatus getStatus() { return status; }
|
||||
public void setStatus(ReviewTaskStatus status) { this.status = status; }
|
||||
public Integer getVersion() { return version; }
|
||||
public Long getSubmittedBy() { return submittedBy; }
|
||||
public Long getReviewedBy() { return reviewedBy; }
|
||||
public void setReviewedBy(Long reviewedBy) { this.reviewedBy = reviewedBy; }
|
||||
public String getReviewComment() { return reviewComment; }
|
||||
public void setReviewComment(String reviewComment) { this.reviewComment = reviewComment; }
|
||||
public Instant getSubmittedAt() { return submittedAt; }
|
||||
public Instant getReviewedAt() { return reviewedAt; }
|
||||
public void setReviewedAt(Instant reviewedAt) { this.reviewedAt = reviewedAt; }
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 创建 PromotionRequest 实体**
|
||||
|
||||
创建 `server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/PromotionRequest.java`:
|
||||
|
||||
```java
|
||||
package com.iflytek.skillhub.domain.review;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.time.Instant;
|
||||
|
||||
@Entity
|
||||
@Table(name = "promotion_request")
|
||||
public class PromotionRequest {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "source_skill_id", nullable = false)
|
||||
private Long sourceSkillId;
|
||||
|
||||
@Column(name = "source_version_id", nullable = false)
|
||||
private Long sourceVersionId;
|
||||
|
||||
@Column(name = "target_namespace_id", nullable = false)
|
||||
private Long targetNamespaceId;
|
||||
|
||||
@Column(name = "target_skill_id")
|
||||
private Long targetSkillId;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false)
|
||||
private ReviewTaskStatus status = ReviewTaskStatus.PENDING;
|
||||
|
||||
@Version
|
||||
@Column(nullable = false)
|
||||
private Integer version = 1;
|
||||
|
||||
@Column(name = "submitted_by", nullable = false)
|
||||
private Long submittedBy;
|
||||
|
||||
@Column(name = "reviewed_by")
|
||||
private Long reviewedBy;
|
||||
|
||||
@Column(name = "review_comment", columnDefinition = "TEXT")
|
||||
private String reviewComment;
|
||||
|
||||
@Column(name = "submitted_at", nullable = false)
|
||||
private Instant submittedAt = Instant.now();
|
||||
|
||||
@Column(name = "reviewed_at")
|
||||
private Instant reviewedAt;
|
||||
|
||||
// Constructors
|
||||
protected PromotionRequest() {}
|
||||
|
||||
public PromotionRequest(Long sourceSkillId, Long sourceVersionId,
|
||||
Long targetNamespaceId, Long submittedBy) {
|
||||
this.sourceSkillId = sourceSkillId;
|
||||
this.sourceVersionId = sourceVersionId;
|
||||
this.targetNamespaceId = targetNamespaceId;
|
||||
this.submittedBy = submittedBy;
|
||||
}
|
||||
|
||||
// Getters and Setters (similar to ReviewTask)
|
||||
public Long getId() { return id; }
|
||||
public Long getSourceSkillId() { return sourceSkillId; }
|
||||
public Long getSourceVersionId() { return sourceVersionId; }
|
||||
public Long getTargetNamespaceId() { return targetNamespaceId; }
|
||||
public Long getTargetSkillId() { return targetSkillId; }
|
||||
public void setTargetSkillId(Long targetSkillId) { this.targetSkillId = targetSkillId; }
|
||||
public ReviewTaskStatus getStatus() { return status; }
|
||||
public void setStatus(ReviewTaskStatus status) { this.status = status; }
|
||||
public Integer getVersion() { return version; }
|
||||
public Long getSubmittedBy() { return submittedBy; }
|
||||
public Long getReviewedBy() { return reviewedBy; }
|
||||
public void setReviewedBy(Long reviewedBy) { this.reviewedBy = reviewedBy; }
|
||||
public String getReviewComment() { return reviewComment; }
|
||||
public void setReviewComment(String reviewComment) { this.reviewComment = reviewComment; }
|
||||
public Instant getSubmittedAt() { return submittedAt; }
|
||||
public Instant getReviewedAt() { return reviewedAt; }
|
||||
public void setReviewedAt(Instant reviewedAt) { this.reviewedAt = reviewedAt; }
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 编译验证**
|
||||
|
||||
运行:`cd server && ./mvnw compile`
|
||||
预期:编译成功
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/
|
||||
git commit -m "feat(domain): add review entities
|
||||
|
||||
- Add ReviewTaskStatus enum
|
||||
- Add ReviewTask entity with optimistic locking
|
||||
- Add PromotionRequest entity"
|
||||
```
|
||||
|
||||
### Task 3: Repository 层实现
|
||||
|
||||
**Files:**
|
||||
- Create: `server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/ReviewTaskRepository.java`
|
||||
- Create: `server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/ReviewTaskJpaRepository.java`
|
||||
- Create: `server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/PromotionRequestRepository.java`
|
||||
- Create: `server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/PromotionRequestJpaRepository.java`
|
||||
|
||||
- [ ] **Step 1: 创建 ReviewTaskRepository 接口**
|
||||
|
||||
创建 `server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/ReviewTaskRepository.java`:
|
||||
|
||||
```java
|
||||
package com.iflytek.skillhub.domain.review;
|
||||
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface ReviewTaskRepository {
|
||||
ReviewTask save(ReviewTask reviewTask);
|
||||
Optional<ReviewTask> findById(Long id);
|
||||
Optional<ReviewTask> findBySkillVersionIdAndStatus(Long skillVersionId, ReviewTaskStatus status);
|
||||
Page<ReviewTask> findByNamespaceIdAndStatus(Long namespaceId, ReviewTaskStatus status, Pageable pageable);
|
||||
Page<ReviewTask> findBySubmittedByAndStatus(Long submittedBy, ReviewTaskStatus status, Pageable pageable);
|
||||
void delete(ReviewTask reviewTask);
|
||||
int updateStatusWithVersion(Long id, ReviewTaskStatus status, Long reviewedBy,
|
||||
String reviewComment, Integer expectedVersion);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 创建 ReviewTaskJpaRepository 实现**
|
||||
|
||||
创建 `server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/ReviewTaskJpaRepository.java`:
|
||||
|
||||
```java
|
||||
package com.iflytek.skillhub.infra.jpa;
|
||||
|
||||
import com.iflytek.skillhub.domain.review.ReviewTask;
|
||||
import com.iflytek.skillhub.domain.review.ReviewTaskRepository;
|
||||
import com.iflytek.skillhub.domain.review.ReviewTaskStatus;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import java.time.Instant;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface ReviewTaskJpaRepository extends JpaRepository<ReviewTask, Long>, ReviewTaskRepository {
|
||||
|
||||
Optional<ReviewTask> findBySkillVersionIdAndStatus(Long skillVersionId, ReviewTaskStatus status);
|
||||
|
||||
Page<ReviewTask> findByNamespaceIdAndStatus(Long namespaceId, ReviewTaskStatus status, Pageable pageable);
|
||||
|
||||
Page<ReviewTask> findBySubmittedByAndStatus(Long submittedBy, ReviewTaskStatus status, Pageable pageable);
|
||||
|
||||
@Modifying
|
||||
@Query("""
|
||||
UPDATE ReviewTask t
|
||||
SET t.status = :status,
|
||||
t.reviewedBy = :reviewedBy,
|
||||
t.reviewComment = :reviewComment,
|
||||
t.reviewedAt = CURRENT_TIMESTAMP,
|
||||
t.version = t.version + 1
|
||||
WHERE t.id = :id AND t.version = :expectedVersion
|
||||
""")
|
||||
int updateStatusWithVersion(@Param("id") Long id,
|
||||
@Param("status") ReviewTaskStatus status,
|
||||
@Param("reviewedBy") Long reviewedBy,
|
||||
@Param("reviewComment") String reviewComment,
|
||||
@Param("expectedVersion") Integer expectedVersion);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 创建 PromotionRequestRepository 接口和实现**
|
||||
|
||||
创建 `server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/PromotionRequestRepository.java`:
|
||||
|
||||
```java
|
||||
package com.iflytek.skillhub.domain.review;
|
||||
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface PromotionRequestRepository {
|
||||
PromotionRequest save(PromotionRequest request);
|
||||
Optional<PromotionRequest> findById(Long id);
|
||||
Optional<PromotionRequest> findBySourceVersionIdAndStatus(Long sourceVersionId, ReviewTaskStatus status);
|
||||
Page<PromotionRequest> findByStatus(ReviewTaskStatus status, Pageable pageable);
|
||||
int updateStatusWithVersion(Long id, ReviewTaskStatus status, Long reviewedBy,
|
||||
String reviewComment, Long targetSkillId, Integer expectedVersion);
|
||||
}
|
||||
```
|
||||
|
||||
创建 `server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/PromotionRequestJpaRepository.java`:
|
||||
|
||||
```java
|
||||
package com.iflytek.skillhub.infra.jpa;
|
||||
|
||||
import com.iflytek.skillhub.domain.review.PromotionRequest;
|
||||
import com.iflytek.skillhub.domain.review.PromotionRequestRepository;
|
||||
import com.iflytek.skillhub.domain.review.ReviewTaskStatus;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface PromotionRequestJpaRepository extends JpaRepository<PromotionRequest, Long>,
|
||||
PromotionRequestRepository {
|
||||
|
||||
Optional<PromotionRequest> findBySourceVersionIdAndStatus(Long sourceVersionId, ReviewTaskStatus status);
|
||||
|
||||
Page<PromotionRequest> findByStatus(ReviewTaskStatus status, Pageable pageable);
|
||||
|
||||
@Modifying
|
||||
@Query("""
|
||||
UPDATE PromotionRequest p
|
||||
SET p.status = :status,
|
||||
p.reviewedBy = :reviewedBy,
|
||||
p.reviewComment = :reviewComment,
|
||||
p.targetSkillId = :targetSkillId,
|
||||
p.reviewedAt = CURRENT_TIMESTAMP,
|
||||
p.version = p.version + 1
|
||||
WHERE p.id = :id AND p.version = :expectedVersion
|
||||
""")
|
||||
int updateStatusWithVersion(@Param("id") Long id,
|
||||
@Param("status") ReviewTaskStatus status,
|
||||
@Param("reviewedBy") Long reviewedBy,
|
||||
@Param("reviewComment") String reviewComment,
|
||||
@Param("targetSkillId") Long targetSkillId,
|
||||
@Param("expectedVersion") Integer expectedVersion);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 编译验证**
|
||||
|
||||
运行:`cd server && ./mvnw compile`
|
||||
预期:编译成功
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/*Repository.java
|
||||
git add server/skillhub-infra/src/main/java/com/iflytek/skillhub/infra/jpa/*Repository.java
|
||||
git commit -m "feat(repo): add review repositories
|
||||
|
||||
- Add ReviewTaskRepository with optimistic lock update
|
||||
- Add PromotionRequestRepository
|
||||
- Implement JPA repositories in infra module"
|
||||
```
|
||||
|
||||
### Task 4: 审核权限检查器
|
||||
|
||||
**Files:**
|
||||
- Create: `server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/ReviewPermissionChecker.java`
|
||||
- Create: `server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/review/ReviewPermissionCheckerTest.java`
|
||||
|
||||
- [ ] **Step 1: 编写权限检查器测试**
|
||||
|
||||
创建测试文件,验证权限逻辑:
|
||||
|
||||
```java
|
||||
package com.iflytek.skillhub.domain.review;
|
||||
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceRole;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceType;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class ReviewPermissionCheckerTest {
|
||||
|
||||
private final ReviewPermissionChecker checker = new ReviewPermissionChecker();
|
||||
|
||||
@Test
|
||||
void cannotReviewOwnSubmission() {
|
||||
Long userId = 1L;
|
||||
ReviewTask task = createTask(1L, NamespaceType.TEAM, userId);
|
||||
|
||||
boolean canReview = checker.canReview(task, userId, Map.of(), Set.of());
|
||||
|
||||
assertFalse(canReview, "Cannot review own submission");
|
||||
}
|
||||
|
||||
@Test
|
||||
void teamAdminCanReviewTeamSkill() {
|
||||
ReviewTask task = createTask(1L, NamespaceType.TEAM, 2L);
|
||||
|
||||
boolean canReview = checker.canReview(task, 1L,
|
||||
Map.of(1L, NamespaceRole.ADMIN), Set.of());
|
||||
|
||||
assertTrue(canReview, "Team ADMIN can review team skill");
|
||||
}
|
||||
|
||||
@Test
|
||||
void skillAdminCanReviewGlobalSkill() {
|
||||
ReviewTask task = createTask(1L, NamespaceType.GLOBAL, 2L);
|
||||
|
||||
boolean canReview = checker.canReview(task, 1L,
|
||||
Map.of(), Set.of("SKILL_ADMIN"));
|
||||
|
||||
assertTrue(canReview, "SKILL_ADMIN can review global skill");
|
||||
}
|
||||
|
||||
@Test
|
||||
void skillAdminCannotReviewTeamSkill() {
|
||||
ReviewTask task = createTask(1L, NamespaceType.TEAM, 2L);
|
||||
|
||||
boolean canReview = checker.canReview(task, 1L,
|
||||
Map.of(), Set.of("SKILL_ADMIN"));
|
||||
|
||||
assertFalse(canReview, "SKILL_ADMIN cannot review team skill");
|
||||
}
|
||||
|
||||
private ReviewTask createTask(Long namespaceId, NamespaceType type, Long submittedBy) {
|
||||
// Mock ReviewTask with namespace info
|
||||
return new ReviewTask(1L, namespaceId, submittedBy);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 运行测试确认失败**
|
||||
|
||||
运行:`cd server && ./mvnw test -Dtest=ReviewPermissionCheckerTest`
|
||||
预期:测试失败(类不存在)
|
||||
|
||||
- [ ] **Step 3: 实现权限检查器**
|
||||
|
||||
创建 `server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/ReviewPermissionChecker.java`:
|
||||
|
||||
```java
|
||||
package com.iflytek.skillhub.domain.review;
|
||||
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceRole;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceType;
|
||||
import org.springframework.stereotype.Component;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
@Component
|
||||
public class ReviewPermissionChecker {
|
||||
|
||||
public boolean canReview(ReviewTask task, Long userId,
|
||||
Map<Long, NamespaceRole> userNamespaceRoles,
|
||||
Set<String> platformRoles) {
|
||||
// Cannot review own submission
|
||||
if (task.getSubmittedBy().equals(userId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get namespace type (需要从 task 中获取,这里简化处理)
|
||||
NamespaceType namespaceType = getNamespaceType(task.getNamespaceId());
|
||||
|
||||
// Global namespace: only SKILL_ADMIN or SUPER_ADMIN
|
||||
if (namespaceType == NamespaceType.GLOBAL) {
|
||||
return platformRoles.contains("SKILL_ADMIN")
|
||||
|| platformRoles.contains("SUPER_ADMIN");
|
||||
}
|
||||
|
||||
// Team namespace: namespace ADMIN or OWNER
|
||||
NamespaceRole role = userNamespaceRoles.get(task.getNamespaceId());
|
||||
return role == NamespaceRole.ADMIN || role == NamespaceRole.OWNER;
|
||||
}
|
||||
|
||||
public boolean canReviewPromotion(PromotionRequest request, Long userId,
|
||||
Set<String> platformRoles) {
|
||||
// Only SKILL_ADMIN or SUPER_ADMIN can review promotion
|
||||
return platformRoles.contains("SKILL_ADMIN")
|
||||
|| platformRoles.contains("SUPER_ADMIN");
|
||||
}
|
||||
|
||||
private NamespaceType getNamespaceType(Long namespaceId) {
|
||||
// TODO: 实际实现需要查询 namespace 表
|
||||
// 这里简化处理,假设 id=1 是 GLOBAL
|
||||
return namespaceId == 1L ? NamespaceType.GLOBAL : NamespaceType.TEAM;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 运行测试确认通过**
|
||||
|
||||
运行:`cd server && ./mvnw test -Dtest=ReviewPermissionCheckerTest`
|
||||
预期:所有测试通过
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/ReviewPermissionChecker.java
|
||||
git add server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/review/ReviewPermissionCheckerTest.java
|
||||
git commit -m "feat(review): add permission checker with tests
|
||||
|
||||
- Implement ReviewPermissionChecker
|
||||
- Add unit tests for permission logic
|
||||
- Verify team admin can only review team skills
|
||||
- Verify SKILL_ADMIN can only review global skills"
|
||||
```
|
||||
|
||||
### Task 5: 审核服务实现(核心逻辑)
|
||||
|
||||
**Files:**
|
||||
- Create: `server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/ReviewService.java`
|
||||
- Create: `server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/review/ReviewServiceTest.java`
|
||||
|
||||
由于篇幅限制,这里提供关键方法的实现框架:
|
||||
|
||||
- [ ] **Step 1: 创建 ReviewService 接口**
|
||||
|
||||
```java
|
||||
package com.iflytek.skillhub.domain.review;
|
||||
|
||||
public interface ReviewService {
|
||||
ReviewTask submitReview(Long skillVersionId, Long namespaceId, Long userId);
|
||||
void approveReview(Long reviewTaskId, Long reviewerId, String comment);
|
||||
void rejectReview(Long reviewTaskId, Long reviewerId, String comment);
|
||||
void withdrawReview(Long skillVersionId, Long userId);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2-5: 实现服务方法(TDD 循环)**
|
||||
|
||||
参考设计文档第 2.1 节的流程图实现每个方法,包括:
|
||||
- 乐观锁更新
|
||||
- 状态机转换
|
||||
- 事件发布
|
||||
- 审计日志
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git commit -m "feat(review): implement review service
|
||||
|
||||
- Add submitReview with duplicate check
|
||||
- Add approveReview with optimistic locking
|
||||
- Add rejectReview with reason recording
|
||||
- Add withdrawReview with PENDING check"
|
||||
```
|
||||
|
||||
### Task 6-10: 剩余任务概要
|
||||
|
||||
由于完整实施计划会超过 5000 行,这里列出剩余任务的概要:
|
||||
|
||||
**Task 6: 提升服务实现**
|
||||
- PromotionService 接口和实现
|
||||
- 提升审核通过后创建全局 skill
|
||||
- 复制版本和文件元数据
|
||||
|
||||
**Task 7: Controller 层**
|
||||
- ReviewController(提交、审核、撤回、列表查询)
|
||||
- PromotionController(提交提升、审核提升、列表查询)
|
||||
|
||||
**Task 8: 集成测试**
|
||||
- 审核全链路测试(提交 → 审核 → 发布)
|
||||
- 乐观锁并发冲突测试
|
||||
- 权限控制测试
|
||||
|
||||
**Task 9: API 文档**
|
||||
- OpenAPI 规范更新
|
||||
- 请求/响应示例
|
||||
|
||||
**Task 10: Chunk 1 验收**
|
||||
- 运行所有测试
|
||||
- 验证 12 个验收标准
|
||||
- 代码审查
|
||||
|
||||
---
|
||||
|
||||
## Chunk 2: 评分收藏 + 前端审核中心
|
||||
|
||||
**范围:** 评分收藏后端 + 审核中心前端 + Token 管理前端
|
||||
|
||||
(由于篇幅限制,Chunk 2-5 的详细步骤将在后续补充)
|
||||
|
||||
### Task 1: 评分收藏实体和 Repository
|
||||
|
||||
- [ ] **Step 1-5: 创建 SkillStar 和 SkillRating 实体**
|
||||
- [ ] **Step 6-10: 实现 Repository 层**
|
||||
|
||||
### Task 2: 评分收藏服务
|
||||
|
||||
- [ ] **Step 1-5: SkillStarService 实现**
|
||||
- [ ] **Step 6-10: SkillRatingService 实现**
|
||||
|
||||
### Task 3: 异步事件监听器
|
||||
|
||||
- [ ] **Step 1-5: SkillStarEventListener**
|
||||
- [ ] **Step 6-10: SkillRatingEventListener with Redis lock**
|
||||
|
||||
### Task 4-10: 前端审核中心
|
||||
|
||||
(详细步骤待补充)
|
||||
|
||||
---
|
||||
|
||||
## Chunk 3: CLI API + Web 授权
|
||||
|
||||
**范围:** OAuth Device Flow + CLI API 端点
|
||||
|
||||
(详细步骤待补充)
|
||||
|
||||
---
|
||||
|
||||
## Chunk 4: ClawHub 兼容层
|
||||
|
||||
**范围:** Canonical slug 映射 + 兼容层端点
|
||||
|
||||
(详细步骤待补充)
|
||||
|
||||
---
|
||||
|
||||
## Chunk 5: 幂等去重 + 管理后台
|
||||
|
||||
**范围:** 幂等拦截器 + 管理后台前端
|
||||
|
||||
(详细步骤待补充)
|
||||
|
||||
---
|
||||
|
||||
## 实施说明
|
||||
|
||||
**完整实施计划说明:**
|
||||
|
||||
由于 Phase 3 包含 5 个 Chunk,每个 Chunk 包含 10-15 个任务,每个任务包含 5-10 个 TDD 步骤,完整的实施计划预计超过 5000 行。
|
||||
|
||||
**当前文档状态:**
|
||||
- ✅ Chunk 1 的前 5 个任务已详细编写(数据库、实体、Repository、权限检查、服务核心)
|
||||
- ⏳ Chunk 1 的剩余任务(Task 6-10)以概要形式列出
|
||||
- ⏳ Chunk 2-5 以任务概要形式列出
|
||||
|
||||
**建议的实施方式:**
|
||||
|
||||
1. **使用 superpowers:subagent-driven-development** - 为每个 Chunk 派发独立的子代理
|
||||
2. **渐进式实施** - 先完成 Chunk 1,验收通过后再进行 Chunk 2
|
||||
3. **参考设计文档** - 每个任务的详细实现逻辑参考 `docs/superpowers/specs/2026-03-12-phase3-review-cli-social-design.md`
|
||||
|
||||
**如需完整的详细步骤:**
|
||||
|
||||
可以在实施过程中,针对每个 Chunk 单独生成详细的实施步骤。每个 Chunk 的详细计划约 1000-1500 行。
|
||||
|
||||
Loading…
Add table
Reference in a new issue