Initial commit

This commit is contained in:
Developer
2026-03-17 12:09:43 +08:00
commit 70bedcf241
211 changed files with 31464 additions and 0 deletions

View File

@@ -0,0 +1,432 @@
# Skill服务开发文档
## 一、Entity 实体类
### Skill.java
```java
package com.openclaw.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
@TableName("skills")
public class Skill {
@TableId(type = IdType.AUTO)
private Long id;
private Long creatorId;
private String name;
private String description;
private String coverImageUrl;
private Integer categoryId;
private BigDecimal price;
private Boolean isFree;
private String status; // draft/pending/approved/rejected/offline
private String rejectReason;
private Long auditorId; // 审核人ID
private LocalDateTime auditedAt; // 审核时间
private Integer downloadCount;
private BigDecimal rating;
private Integer ratingCount;
private String version;
private Long fileSize;
private String fileUrl;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
@TableLogic
private LocalDateTime deletedAt;
}
```
### SkillCategory.java
```java
package com.openclaw.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("skill_categories")
public class SkillCategory {
@TableId(type = IdType.AUTO)
private Integer id;
private String name;
private Integer parentId;
private String iconUrl;
private Integer sortOrder;
private LocalDateTime createdAt;
}
```
### SkillReview.java
```java
package com.openclaw.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("skill_reviews")
public class SkillReview {
@TableId(type = IdType.AUTO)
private Long id;
private Long skillId;
private Long userId;
private Long orderId;
private Integer rating;
private String content;
private String images; // JSON字符串
private Integer helpfulCount;
private String status; // pending/approved/rejected
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
```
## 二、DTO / VO
### SkillQueryDTO.java列表查询参数
```java
package com.openclaw.dto;
import lombok.Data;
@Data
public class SkillQueryDTO {
private Integer categoryId; // 分类筛选
private String keyword; // 关键词搜索
private Boolean isFree; // 是否免费
private String sort; // newest/hottest/rating/price_asc/price_desc
private Integer pageNum = 1;
private Integer pageSize = 10;
}
```
### SkillCreateDTO.java
```java
package com.openclaw.dto;
import jakarta.validation.constraints.*;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class SkillCreateDTO {
@NotBlank(message = "Skill名称不能为空")
private String name;
private String description;
private String coverImageUrl; // 腾讯云COS URL
@NotNull(message = "分类不能为空")
private Integer categoryId;
private BigDecimal price = BigDecimal.ZERO;
private Boolean isFree = false;
private String version;
private String fileUrl; // 腾讯云COS URL
private Long fileSize;
}
```
### SkillReviewDTO.java
```java
package com.openclaw.dto;
import jakarta.validation.constraints.*;
import lombok.Data;
import java.util.List;
@Data
public class SkillReviewDTO {
@NotNull @Min(1) @Max(5)
private Integer rating;
private String content;
private List<String> images; // 腾讯云COS URL列表
}
```
### SkillVO.java
```java
package com.openclaw.vo;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
public class SkillVO {
private Long id;
private String name;
private String description;
private String coverImageUrl;
private Integer categoryId;
private String categoryName;
private BigDecimal price;
private Boolean isFree;
private Integer downloadCount;
private BigDecimal rating;
private Integer ratingCount;
private String version;
private Long fileSize;
private String creatorNickname;
private Boolean owned; // 当前用户是否已拥有
private LocalDateTime createdAt;
}
```
## 三、Service 接口
### SkillService.java
```java
package com.openclaw.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.openclaw.dto.*;
import com.openclaw.vo.*;
public interface SkillService {
IPage<SkillVO> listSkills(SkillQueryDTO query, Long currentUserId);
SkillVO getSkillDetail(Long skillId, Long currentUserId);
SkillVO createSkill(Long userId, SkillCreateDTO dto);
void submitReview(Long skillId, Long userId, SkillReviewDTO dto);
boolean hasOwned(Long userId, Long skillId);
void grantAccess(Long userId, Long skillId, Long orderId, String type);
}
```
## 四、Service 实现
### SkillServiceImpl.java
```java
package com.openclaw.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.openclaw.constant.ErrorCode;
import com.openclaw.dto.*;
import com.openclaw.entity.*;
import com.openclaw.exception.BusinessException;
import com.openclaw.repository.*;
import com.openclaw.service.SkillService;
import com.openclaw.vo.*;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
public class SkillServiceImpl implements SkillService {
private final SkillRepository skillRepository;
private final SkillCategoryRepository categoryRepository;
private final SkillReviewRepository reviewRepository;
private final SkillDownloadRepository downloadRepository;
private final UserRepository userRepository;
@Override
public IPage<SkillVO> listSkills(SkillQueryDTO query, Long currentUserId) {
Page<Skill> page = new Page<>(query.getPageNum(), query.getPageSize());
LambdaQueryWrapper<Skill> wrapper = new LambdaQueryWrapper<>()
.eq(Skill::getStatus, "approved")
.eq(query.getCategoryId() != null, Skill::getCategoryId, query.getCategoryId())
.eq(query.getIsFree() != null, Skill::getIsFree, query.getIsFree())
.and(query.getKeyword() != null, w ->
w.like(Skill::getName, query.getKeyword())
.or().like(Skill::getDescription, query.getKeyword()));
// 排序
switch (query.getSort() == null ? "newest" : query.getSort()) {
case "hottest" -> wrapper.orderByDesc(Skill::getDownloadCount);
case "rating" -> wrapper.orderByDesc(Skill::getRating);
case "price_asc" -> wrapper.orderByAsc(Skill::getPrice);
case "price_desc" -> wrapper.orderByDesc(Skill::getPrice);
default -> wrapper.orderByDesc(Skill::getCreatedAt);
}
IPage<Skill> result = skillRepository.selectPage(page, wrapper);
return result.convert(skill -> toVO(skill, currentUserId));
}
@Override
public SkillVO getSkillDetail(Long skillId, Long currentUserId) {
Skill skill = skillRepository.selectById(skillId);
if (skill == null || "offline".equals(skill.getStatus()))
throw new BusinessException(ErrorCode.SKILL_NOT_FOUND);
return toVO(skill, currentUserId);
}
@Override
@Transactional
public SkillVO createSkill(Long userId, SkillCreateDTO dto) {
Skill skill = new Skill();
skill.setCreatorId(userId);
skill.setName(dto.getName());
skill.setDescription(dto.getDescription());
skill.setCoverImageUrl(dto.getCoverImageUrl());
skill.setCategoryId(dto.getCategoryId());
skill.setPrice(dto.getPrice());
skill.setIsFree(dto.getIsFree());
skill.setVersion(dto.getVersion());
skill.setFileUrl(dto.getFileUrl());
skill.setFileSize(dto.getFileSize());
skill.setStatus("pending"); // 提交审核
skill.setDownloadCount(0);
skillRepository.insert(skill);
return toVO(skill, userId);
}
@Override
@Transactional
public void submitReview(Long skillId, Long userId, SkillReviewDTO dto) {
// 检查是否已购买
if (!hasOwned(userId, skillId)) throw new BusinessException(ErrorCode.SKILL_NOT_FOUND);
SkillReview review = new SkillReview();
review.setSkillId(skillId);
review.setUserId(userId);
review.setRating(dto.getRating());
review.setContent(dto.getContent());
if (dto.getImages() != null) {
review.setImages(dto.getImages().toString());
}
review.setStatus("approved");
reviewRepository.insert(review);
// 更新Skill平均评分
updateSkillRating(skillId);
}
@Override
public boolean hasOwned(Long userId, Long skillId) {
if (userId == null) return false;
return downloadRepository.selectCount(
new LambdaQueryWrapper<SkillDownload>()
.eq(SkillDownload::getUserId, userId)
.eq(SkillDownload::getSkillId, skillId)) > 0;
}
@Override
@Transactional
public void grantAccess(Long userId, Long skillId, Long orderId, String type) {
SkillDownload d = new SkillDownload();
d.setUserId(userId);
d.setSkillId(skillId);
d.setOrderId(orderId);
d.setDownloadType(type);
downloadRepository.insert(d);
// 更新下载次数
skillRepository.incrementDownloadCount(skillId);
}
private void updateSkillRating(Long skillId) {
// 重新计算平均分
Double avg = reviewRepository.avgRatingBySkillId(skillId);
Integer cnt = reviewRepository.countBySkillId(skillId);
if (avg != null) {
skillRepository.updateRating(skillId,
java.math.BigDecimal.valueOf(avg).setScale(2, java.math.RoundingMode.HALF_UP), cnt);
}
}
private SkillVO toVO(Skill skill, Long currentUserId) {
SkillVO vo = new SkillVO();
vo.setId(skill.getId());
vo.setName(skill.getName());
vo.setDescription(skill.getDescription());
vo.setCoverImageUrl(skill.getCoverImageUrl());
vo.setCategoryId(skill.getCategoryId());
vo.setPrice(skill.getPrice());
vo.setIsFree(skill.getIsFree());
vo.setDownloadCount(skill.getDownloadCount());
vo.setRating(skill.getRating());
vo.setRatingCount(skill.getRatingCount());
vo.setVersion(skill.getVersion());
vo.setFileSize(skill.getFileSize());
vo.setCreatedAt(skill.getCreatedAt());
// 分类名
SkillCategory cat = categoryRepository.selectById(skill.getCategoryId());
if (cat != null) vo.setCategoryName(cat.getName());
// 创建者昵称
User creator = userRepository.selectById(skill.getCreatorId());
if (creator != null) vo.setCreatorNickname(creator.getNickname());
// 是否已拥有
vo.setOwned(hasOwned(currentUserId, skill.getId()));
return vo;
}
}
```
## 五、Controller
### SkillController.java
```java
package com.openclaw.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.openclaw.common.Result;
import com.openclaw.dto.*;
import com.openclaw.service.SkillService;
import com.openclaw.util.UserContext;
import com.openclaw.vo.*;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/v1/skills")
@RequiredArgsConstructor
public class SkillController {
private final SkillService skillService;
/** Skill列表公开支持分页/筛选/排序) */
@GetMapping
public Result<IPage<SkillVO>> listSkills(SkillQueryDTO query) {
Long userId = UserContext.getUserId(); // 未登录为null
return Result.ok(skillService.listSkills(query, userId));
}
/** Skill详情公开 */
@GetMapping("/{id}")
public Result<SkillVO> getDetail(@PathVariable Long id) {
return Result.ok(skillService.getSkillDetail(id, UserContext.getUserId()));
}
/** 上传Skill需登录 */
@PostMapping
public Result<SkillVO> createSkill(@Valid @RequestBody SkillCreateDTO dto) {
return Result.ok(skillService.createSkill(UserContext.getUserId(), dto));
}
/** 发表评价(需登录且已拥有) */
@PostMapping("/{id}/reviews")
public Result<Void> submitReview(
@PathVariable Long id,
@Valid @RequestBody SkillReviewDTO dto) {
skillService.submitReview(id, UserContext.getUserId(), dto);
return Result.ok();
}
}
```
---
**文档版本**v1.0
**创建日期**2026-03-16