feat(content): 添加从目录导入Excel功能并优化内容管理

- 新增从指定目录批量导入Excel数据到CmsContent的功能
- 添加ImportPathDto请求参数实体类
- 实现importFromPath方法支持目录遍历和文件批量导入
- 添加truncateTable方法用于清空表数据
- 优化Excel导入逻辑增加异常处理和空值检查
- 调整批量处理大小从100改为10
- 更新审核状态和发布状态的描述文案
- 修复分享次数和官方标识字段的默认值设置
- 将Servlet API从javax迁移到jakarta
- 更新README.md完善项目文档
- 优化技能解析逻辑支持多层级目录结构
- 修复AI模型生成中的标签选择和参数验证问题
This commit is contained in:
wangzhiwei 2026-03-23 11:38:20 +08:00
parent 59a44f9c53
commit 3df611f809
19 changed files with 667 additions and 205 deletions

193
README.md
View File

@ -1,3 +1,192 @@
# agent-skill-backend
# 可学AI-skills平台后端
agent-skill-backend
## 项目简介
可学AI-skills平台是一个基于Spring Boot的智能技能管理系统提供技能生成、内容管理、用户认证、支付等功能。
## 技术栈
- **基础框架**Spring Boot 3.2.2
- **持久层**MyBatis 3.0.3
- **数据库**MySQL
- **缓存**Redis
- **认证**Sa-Token 1.38.0
- **模板引擎**Thymeleaf
- **API文档**Swagger 3.0.0
- **文件处理**sevenzipjbinding 16.02-2.01
- **短信服务**SMS4J 3.3.5
- **分布式锁**Redisson 3.23.5
- **AI集成**DeepSeek、GLM-4.6v
## 项目结构
```
backend/
├── .mvn/ # Maven包装器
├── db/ # 数据库脚本
├── src/
│ ├── main/
│ │ ├── java/com/kexue/skills/ # 主源码
│ │ │ ├── annotation/ # 自定义注解
│ │ │ ├── aspect/ # AOP切面
│ │ │ ├── common/ # 通用工具和常量
│ │ │ ├── config/ # 配置类
│ │ │ ├── controller/ # 控制器
│ │ │ ├── entity/ # 实体类
│ │ │ ├── exception/ # 异常处理
│ │ │ ├── interceptor/ # 拦截器
│ │ │ ├── mapper/ # 数据访问层
│ │ │ ├── service/ # 服务层
│ │ │ ├── task/ # 定时任务
│ │ │ ├── utils/ # 工具类
│ │ │ └── SkillsApp.java # 应用入口
│ │ └── resources/ # 资源文件
│ │ ├── mapper/ # MyBatis映射文件
│ │ ├── sql/ # SQL脚本
│ │ ├── static/ # 静态资源
│ │ ├── templates/ # Thymeleaf模板
│ │ ├── application-*.yml # 配置文件
│ │ └── logback-spring.xml # 日志配置
│ └── test/ # 测试代码
├── .gitignore # Git忽略文件
├── Dockerfile # Docker构建文件
├── README.md # 项目说明
├── mvnw.cmd # Maven包装器脚本
└── pom.xml # Maven依赖配置
```
## 核心功能
### 1. 用户认证与授权
- 基于Sa-Token的认证系统
- 支持账号密码登录
- 支持手机验证码登录
- 角色权限管理
- 防重复提交
### 2. 内容管理系统
- 内容分类管理
- 内容标签管理
- 内容发布与管理
- 内容点赞与浏览统计
### 3. 技能生成系统
- 技能上传与解析支持RAR等压缩格式
- 技能结构分析
- 技能介绍生成
### 4. 支付系统
- 微信支付集成
- 支付宝集成
- 支付订单管理
### 5. 账户管理
- 账户余额管理
- 积分管理
- 交易记录
### 6. 系统管理
- 菜单管理
- 角色管理
- 字典管理
- 系统日志
### 7. AI集成
- DeepSeek模型集成
- GLM-4.6v模型集成
- 智能内容生成
## 快速开始
### 环境要求
- JDK 17+
- Maven 3.6+
- MySQL 5.7+
- Redis 5.0+
### 配置说明
1. 修改 `application-dev.yml` 文件中的数据库连接信息
2. 修改 `application.yml` 文件中的Redis连接信息
3. 修改 `application.yml` 文件中的AI模型API密钥
4. 修改 `application.yml` 文件中的短信服务配置
### 数据库初始化
1. 执行 `db/create_tables.sql` 创建数据库表
2. 执行 `db/init_data.sql` 初始化基础数据
### 启动项目
```bash
# 编译项目
mvn clean compile
# 运行项目
mvn spring-boot:run
```
### 访问地址
- 项目首页http://localhost:8080
- Swagger文档http://localhost:8080/doc.html
## 主要API
### 用户认证
- `POST /api/login` - 用户登录
- `POST /api/logout` - 用户登出
- `GET /api/currentUser` - 获取当前用户信息
### 内容管理
- `GET /api/cms/content/list` - 获取内容列表
- `POST /api/cms/content/save` - 保存内容
- `DELETE /api/cms/content/delete` - 删除内容
### 技能管理
- `POST /api/skill/upload` - 上传技能
- `POST /api/skill/analyze` - 分析技能结构
- `POST /api/skill/genIntroduce` - 生成技能介绍
### 支付管理
- `POST /api/pay/wx` - 微信支付
- `POST /api/pay/alipay` - 支付宝支付
- `GET /api/payment/order/list` - 获取支付订单列表
## 部署说明
### Docker部署
1. 构建Docker镜像
```bash
docker build -t agent-skills .
```
2. 运行Docker容器
```bash
docker run -p 8080:8080 --name agent-skills agent-skills
```
### 生产环境部署
1. 打包项目
```bash
mvn clean package -DskipTests
配置文件直接打在jar包内
```
2. 部署jar包
```bash
java -jar agentSkills.jar --spring.profiles.active=prod
或者执行脚本启动
./start.sh
```
## 注意事项
1. 项目使用Redis作为缓存需要确保Redis服务正常运行
2. 项目使用阿里云短信服务,需要配置相关参数
3. 项目使用AI模型API需要配置相关API密钥
## 许可证
本项目仅供内部使用,未经授权不得用于商业用途。
## 联系方式
如有问题,请联系项目维护人员。

View File

@ -146,11 +146,11 @@
<version>1.3.2</version>
</dependency>
<!-- Servlet API for javax.servlet.http -->
<!-- Servlet API for jakarta.servlet.http (Spring Boot 3.x uses Jakarta EE 9+) -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>6.0.0</version>
<scope>provided</scope>
</dependency>

View File

@ -84,93 +84,7 @@ public class SkillZipParser {
return generateYaml(skillStructure);
}
/**
* 解析skill zip包并生成yaml描述
* @param zipFilePath zip文件路径
* @param author 作者
* @param defaultSkillName 默认技能名称
* @return 包含技能信息和yaml描述的Map
* @throws IOException 解析过程中的IO异常
* @throws SevenZipException 解析RAR文件时的异常
*/
public static Map<String, Object> parseSkillZip(String zipFilePath, String author, String defaultSkillName) throws IOException, SevenZipException {
// 检查文件扩展名
String fileExtension = "";
if (zipFilePath.contains(".")) {
int dotIndex = zipFilePath.lastIndexOf(".");
fileExtension = zipFilePath.substring(dotIndex).toLowerCase();
}
// 根据文件类型选择不同的解析方法
if (".rar".equals(fileExtension)) {
return parseRarFile(zipFilePath, author, defaultSkillName);
} else {
// 默认为zip文件
try (ZipFile zipFile = new ZipFile(zipFilePath, StandardCharsets.UTF_8)) {
// 从压缩文件中提取技能信息
Map<String, Object> skillInfo = extractSkillInfo(zipFile, defaultSkillName);
String skillName = (String) skillInfo.get("name");
String skillDescription = (String) skillInfo.get("description");
List<String> skillTags = (List<String>) skillInfo.get("tags");
// 生成技能包结构
Map<String, Object> skillStructure = generateSkillStructure(zipFile, skillName, skillDescription, skillTags, author);
// 生成yaml
String yamlContent = generateYaml(skillStructure);
// 构建返回结果
Map<String, Object> result = new LinkedHashMap<>();
result.put("name", skillName);
result.put("description", skillDescription);
result.put("tags", skillTags);
result.put("yamlContent", yamlContent);
// 包含md文件的完整内容
if (skillInfo.containsKey("skillMdText")) {
result.put("skillMdText", skillInfo.get("skillMdText"));
}
return result;
}
}
}
/**
* 解析rar文件
* @param rarFilePath rar文件路径
* @param author 作者
* @param defaultSkillName 默认技能名称
* @return 技能信息
* @throws IOException 解析过程中的IO异常
* @throws SevenZipException 解析RAR文件时的异常
*/
private static Map<String, Object> parseRarFile(String rarFilePath, String author, String defaultSkillName) throws IOException, SevenZipException {
// 从rar文件中提取技能信息
Map<String, Object> skillInfo = extractSkillInfoFromRar(rarFilePath, defaultSkillName);
String skillName = (String) skillInfo.get("name");
String skillDescription = (String) skillInfo.get("description");
List<String> skillTags = (List<String>) skillInfo.get("tags");
// 生成技能包结构
Map<String, Object> skillStructure = generateSkillStructureFromRar(rarFilePath, skillName, skillDescription, skillTags, author);
// 生成yaml
String yamlContent = generateYaml(skillStructure);
// 构建返回结果
Map<String, Object> result = new LinkedHashMap<>();
result.put("name", skillName);
result.put("description", skillDescription);
result.put("tags", skillTags);
result.put("yamlContent", yamlContent);
// 包含md文件的完整内容
if (skillInfo.containsKey("skillMdText")) {
result.put("skillMdText", skillInfo.get("skillMdText"));
}
return result;
}
/**
* 从rar文件中提取技能信息
* @param rarFilePath rar文件路径
@ -197,7 +111,7 @@ public class SkillZipParser {
// 使用简单接口
ISimpleInArchive simpleInArchive = archive.getSimpleInterface();
// 遍历所有文件条目
// 首先尝试在根目录查找md文件
for (ISimpleInArchiveItem item : simpleInArchive.getArchiveItems()) {
// 检查是否是根目录下的md文件
String path = item.getPath();
@ -224,6 +138,55 @@ public class SkillZipParser {
break; // 找到一个md文件后就停止
}
}
// 如果根目录没有找到md文件检查根目录下的文件夹
if (!foundMdFile) {
// 收集根目录下的文件夹
List<String> rootFolders = new ArrayList<>();
for (ISimpleInArchiveItem item : simpleInArchive.getArchiveItems()) {
if (item.isFolder()) {
String path = item.getPath();
// 确保是根目录下的文件夹路径中不包含/
if (!path.contains("/") && !path.contains("\\")) {
rootFolders.add(path);
}
}
}
// 检查每个根目录文件夹下的md文件
for (String folder : rootFolders) {
for (ISimpleInArchiveItem item : simpleInArchive.getArchiveItems()) {
if (!item.isFolder()) {
String path = item.getPath();
// 检查是否是该文件夹下的md文件
if (path.endsWith(".md") && (path.equals(folder + "/skill.md") || path.equals(folder + "/SKILL.md") ||
path.equals(folder + "/readme.md") || path.equals(folder + "/README.md"))) {
// 读取文件内容
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
item.extractSlow(data -> {
try {
outputStream.write(data);
} catch (IOException e) {
throw new RuntimeException(e);
}
return data.length;
});
String content = outputStream.toString(StandardCharsets.UTF_8);
// 存储md文件的完整内容
skillInfo.put("skillMdText", content);
// 解析md内容
parseSkillsMd(content, skillInfo);
foundMdFile = true;
break; // 找到一个md文件后就停止
}
}
}
if (foundMdFile) break; // 找到一个md文件后就停止
}
}
}
// 如果没有找到 md 文件使用默认值
@ -276,17 +239,59 @@ public class SkillZipParser {
}
}
}
// 如果根目录没有找到md文件检查根目录下的文件夹
if (!foundMdFile) {
// 收集根目录下的文件夹
List<String> rootFolders = new ArrayList<>();
entries = zipFile.entries();
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
if (entry.isDirectory()) {
String path = entry.getName();
// 确保是根目录下的文件夹路径中不包含/或者以/结尾且前面没有/
if (path.endsWith("/") && !path.substring(0, path.length() - 1).contains("/")) {
rootFolders.add(path.substring(0, path.length() - 1));
}
}
}
// 检查每个根目录文件夹下的md文件
for (String folder : rootFolders) {
entries = zipFile.entries();
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
if (!entry.isDirectory()) {
String path = entry.getName();
// 检查是否是该文件夹下的md文件
if (path.endsWith(".md") && (path.equals(folder + "/skill.md") || path.equals(folder + "/SKILL.md") ||
path.equals(folder + "/readme.md") || path.equals(folder + "/README.md"))) {
try (InputStream inputStream = zipFile.getInputStream(entry);
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
StringBuilder content = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
content.append(line).append("\n");
}
// 存储md文件的完整内容
skillInfo.put("skillMdText", content.toString());
// 解析md内容
parseSkillsMd(content.toString(), skillInfo);
foundMdFile = true;
break; // 找到一个md文件后就停止
}
}
}
}
if (foundMdFile) break; // 找到一个md文件后就停止
}
}
// 如果没有找到 md 文件使用默认值
if (!foundMdFile) {
skillInfo.put("name", defaultSkillName);
skillInfo.put("description", "Skill uploaded ");
skillInfo.put("tags", Arrays.asList("10001", "10002"));
} else {
// 确保 tags 不为 null
if (!skillInfo.containsKey("tags")) {
skillInfo.put("tags", Arrays.asList("10001"));
}
}
return skillInfo;

View File

@ -8,6 +8,7 @@ import com.kexue.skills.entity.CmsContent;
import com.kexue.skills.entity.base.IdDto;
import com.kexue.skills.entity.dto.CmsContentDto;
import com.kexue.skills.entity.dto.QueryContentDto;
import com.kexue.skills.entity.request.ImportPathDto;
import com.kexue.skills.service.CmsContentService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
@ -312,4 +313,24 @@ public class CmsContentController {
String title = cmsContentService.getTitle(contentId);
return CommonResult.success(title);
}
/**
* 从指定目录导入Excel数据到CmsContent
*
* @param importPathDto 导入路径请求参数
* @param createBy 创建人
* @return 导入结果
*/
@PostMapping("/importFromPath")
@Operation(summary = "从目录导入Excel数据", description = "从指定目录导入Excel数据到CmsContent")
@RequireAuth
public CommonResult<Integer> importFromPath(@RequestBody ImportPathDto importPathDto, @RequestParam("createBy") String createBy) {
try {
int successCount = cmsContentService.importFromPath(importPathDto, createBy);
return CommonResult.success(successCount);
} catch (Exception e) {
e.printStackTrace();
return CommonResult.failed("导入失败:" + e.getMessage());
}
}
}

View File

@ -10,8 +10,8 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Map;
/**

View File

@ -42,18 +42,6 @@ public class SkillGenController {
return CommonResult.success(skillGenService.preGenerateV2(request));
}
/**
* 生成技能V2
*
* @param request 生成请求
* @return 生成结果
*/
@PostMapping("/preGenerateV2")
@Operation(summary = "预生成技能V2", description = "使用新模型生成技能")
public CommonResult<SkillResponse> preGenerateV2(@RequestBody SkillPreGenRequest request) {
return CommonResult.success(skillGenService.preGenerateV2(request));
}
@PostMapping("/generate")
@Operation(summary = "生成技能", description = "生成技能")
public CommonResult<CmsContent> generate(@RequestBody SkillGenRequest request) {

View File

@ -3,9 +3,7 @@ package com.kexue.skills.controller;
import com.kexue.skills.annotation.RequireAuth;
import com.kexue.skills.entity.SysUser;
import com.kexue.skills.entity.dto.SysUserDto;
import com.kexue.skills.entity.request.ResetPasswordDto;
import com.kexue.skills.entity.request.ResetPwdDto;
import com.kexue.skills.entity.request.AdminResetPasswordDto;
import com.kexue.skills.entity.request.*;
import com.kexue.skills.exception.BizException;
import com.kexue.skills.service.SysUserService;
import org.springframework.web.bind.annotation.*;
@ -17,7 +15,6 @@ import com.kexue.skills.common.CacheManager;
import com.github.pagehelper.PageInfo;
import com.kexue.skills.common.CommonResult;
import com.kexue.skills.entity.base.IdDto;
import com.kexue.skills.entity.request.LoginUserDto;
import org.redisson.api.RedissonClient;
/**
@ -86,7 +83,7 @@ public class SysUserController {
*/
@PostMapping("/update")
@Operation(summary = "更新用户", description = "更新用户")
public CommonResult<SysUser> update(@RequestBody SysUser SysUser) {
public CommonResult<SysUser> update(@RequestBody SysUserUpdateDto SysUser) {
return CommonResult.success(sysUserService.update(SysUser));
}

View File

@ -97,14 +97,14 @@ public class CmsContent extends BaseEntity implements Serializable {
@Schema(description ="审核人名称")
private String reviewerName;
@Schema(description ="审核状态1草稿2待审核3审核通过4审核拒绝")
@Schema(description ="审核状态1未发布2待审核3审核通过4审核未通过")
private Integer auditStatus;
@Schema(description ="审核意见")
private String auditComment;
@Schema(description ="发布状态1未发布2已发布3已下架")
private Integer publishStatus;
@Schema(description ="发布状态1未发布2已发布3已下架--> 公有还是私有1私有2公有")
private Integer publishStatus;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@Schema(description ="发布时间")

View File

@ -0,0 +1,26 @@
package com.kexue.skills.entity.request;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
/**
* 导入路径请求参数
*
* @author 王志维
* @since 2025-02-21 23:01:48
*/
@Data
@Schema(name = "ImportPathDto", description = "导入路径请求参数")
public class ImportPathDto implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "是否追加true表示追加false表示清空表")
private boolean append;
@Schema(description = "文件目录")
private String filePath;
}

View File

@ -0,0 +1,38 @@
package com.kexue.skills.entity.request;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
/**
* 用户更新请求参数
*
* @author 王志维
* @since 2025-02-21 23:01:48
*/
@Data
@Schema(name = "SysUserUpdateDto", description = "用户更新请求参数")
public class SysUserUpdateDto implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "用户ID")
private Long userId;
@Schema(description = "用户名")
private String userName;
@Schema(description = "密码")
private String password;
@Schema(description = "邮箱")
private String email;
@Schema(description = "手机号")
private String tel;
@Schema(description = "状态1-正常0-禁用)")
private Integer enable;
}

View File

@ -225,4 +225,11 @@ public interface CmsContentMapper {
* @return 总记录数
*/
int getPageListByUserCreatedCount(@Param("userId") Long userId, @Param("publishStatus") Integer publishStatus);
/**
* 清空表数据
*
* @return 影响行数
*/
int truncateTable();
}

View File

@ -3,6 +3,7 @@ package com.kexue.skills.service;
import com.github.pagehelper.PageInfo;
import com.kexue.skills.entity.CmsContent;
import com.kexue.skills.entity.dto.CmsContentDto;
import com.kexue.skills.entity.request.ImportPathDto;
import java.util.List;
@ -191,4 +192,13 @@ public interface CmsContentService extends BaseService {
* @return title字段的内容
*/
String getTitle(Long contentId);
/**
* 从指定目录导入Excel数据到CmsContent
*
* @param importPathDto 导入路径请求参数
* @param createBy 创建人
* @return 导入结果
*/
int importFromPath(ImportPathDto importPathDto, String createBy);
}

View File

@ -1,7 +1,7 @@
package com.kexue.skills.service;
import com.kexue.skills.entity.PaymentOrder;
import javax.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Map;
/**

View File

@ -7,6 +7,7 @@ import com.kexue.skills.entity.request.LoginDto;
import com.kexue.skills.entity.request.LoginUserDto;
import com.kexue.skills.entity.request.PhoneLoginDto;
import com.kexue.skills.entity.request.ResetPasswordDto;
import com.kexue.skills.entity.request.SysUserUpdateDto;
import java.util.List;
@ -59,6 +60,14 @@ public interface SysUserService extends BaseService {
*/
SysUser update(SysUser sysUser);
/**
* 修改用户数据
*
* @param sysUserUpdateDto 用户更新请求参数
* @return 实例对象
*/
SysUser update(SysUserUpdateDto sysUserUpdateDto);
/**
* 通过主键删除数据
*

View File

@ -10,6 +10,7 @@ import com.kexue.skills.entity.CmsContentView;
import com.kexue.skills.entity.CmsContentLike;
import com.kexue.skills.entity.base.BaseQueryDto;
import com.kexue.skills.entity.dto.CmsContentDto;
import com.kexue.skills.entity.request.ImportPathDto;
import com.kexue.skills.mapper.CmsContentMapper;
import com.kexue.skills.mapper.CmsContentViewMapper;
import com.kexue.skills.mapper.CmsContentLikeMapper;
@ -21,6 +22,8 @@ import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.math.BigDecimal;
import java.util.*;
@ -311,9 +314,16 @@ public class CmsContentServiceImpl implements CmsContentService {
if (cmsContent.getCommentCount() == null) {
cmsContent.setCommentCount(0);
}
if (cmsContent.getShareCount() == null) {
cmsContent.setShareCount(0);
}
if (cmsContent.getSort() == null) {
cmsContent.setSort(0);
}
if (cmsContent.getIsOfficial() == null) {
cmsContent.setIsOfficial(false);
}
cmsContent.setAuthorId(StpUtil.getLoginIdAsLong());
// 保存数据
this.cmsContentMapper.insert(cmsContent);
return cmsContent;
@ -729,7 +739,7 @@ public class CmsContentServiceImpl implements CmsContentService {
@Override
public int importFromExcel(byte[] fileBytes, String createBy) {
int successCount = 0;
final int BATCH_SIZE = 100; // 批量处理大小
final int BATCH_SIZE = 10; // 批量处理大小
try (InputStream inputStream = new ByteArrayInputStream(fileBytes);
ExcelReader reader = ExcelUtil.getReader(inputStream)) {
@ -757,76 +767,91 @@ public class CmsContentServiceImpl implements CmsContentService {
// 从第二行开始读取数据第一行为标题行
for (int rowIndex = 1; rowIndex < totalRows; rowIndex++) {
List<Object> rowList = reader.readRow(rowIndex);
if (rowList == null || rowList.isEmpty()) {
continue;
}
// 转换为Map<String, Object>
Map<String, Object> row = new HashMap<>();
for (int i = 0; i < headerList.size() && i < rowList.size(); i++) {
row.put(headerList.get(i), rowList.get(i));
}
if (row.isEmpty()) {
continue;
}
CmsContent cmsContent = new CmsContent();
// 设置创建时间和更新时间
cmsContent.setCreateTime(now);
cmsContent.setUpdateTime(now);
cmsContent.setCreateBy(createBy);
cmsContent.setUpdateBy(createBy);
// 设置默认值
cmsContent.setDeleteFlag(0);
cmsContent.setAuditStatus(1); // 默认草稿状态
cmsContent.setPublishStatus(1); // 默认未发布状态
cmsContent.setViewCount(0);
cmsContent.setLikeCount(0);
cmsContent.setCommentCount(0);
cmsContent.setSort(0);
// 读取Excel中的字段
// content_id - 数据库自动生成不需要读取
cmsContent.setTitle(getStringValue(row, "title"));
cmsContent.setTitleEn(getStringValue(row, "title_en"));
cmsContent.setOrigin(getStringValue(row, "origin"));
cmsContent.setTags(Objects.requireNonNull(getStringValue(row, "tags")).replaceAll(" ",""));
cmsContent.setIcon(getStringValue(row, "icon"));
cmsContent.setIsOfficial(getBooleanValue(row, "is_official"));
cmsContent.setPrice(getBigDecimalValue(row, "price"));
cmsContent.setLikeCount(getIntegerValue(row, "like_count"));
cmsContent.setShareCount(getIntegerValue(row, "share_count"));
cmsContent.setContentType(getIntegerValue(row, "content_type"));
cmsContent.setContent(getStringValue(row, "content"));
cmsContent.setContentEn(getStringValue(row, "content_en"));
cmsContent.setAuditStatus(getIntegerValue(row, "audit_status"));
cmsContent.setPublishStatus(getIntegerValue(row, "publish_status"));
cmsContent.setPublishTime(getDateValue(row, "publish_time"));
cmsContent.setViewCount(getIntegerValue(row, "view_count"));
cmsContent.setCommentCount(getIntegerValue(row, "comment_count"));
cmsContent.setIsPaid(getIntegerValue(row, "is_paid"));
cmsContent.setSupportPointsPay(getIntegerValue(row, "support_points_pay"));
cmsContent.setDescription(getStringValue(row, "description"));
cmsContent.setDescriptionEn(getStringValue(row, "description_en"));
cmsContent.setIntroduce(getStringValue(row, "introduce"));
cmsContent.setIntroduceEn(getStringValue(row, "introduce_en"));
// create_time - 由系统生成不需要读取
// update_time - 由系统生成不需要读取
cmsContent.setDeleteFlag(getIntegerValue(row, "delete_flag"));
batchList.add(cmsContent);
// 达到批量大小或最后一行时执行批量插入
if (batchList.size() >= BATCH_SIZE || rowIndex == totalRows - 1) {
if (!batchList.isEmpty()) {
// 执行批量插入
this.cmsContentMapper.batchInsert(batchList);
successCount += batchList.size();
batchList.clear(); // 清空批次
try {
List<Object> rowList = reader.readRow(rowIndex);
if (rowList == null || rowList.isEmpty()) {
continue;
}
// 转换为Map<String, Object>
Map<String, Object> row = new HashMap<>();
for (int i = 0; i < headerList.size() && i < rowList.size(); i++) {
row.put(headerList.get(i), rowList.get(i));
}
if (row.isEmpty()) {
continue;
}
CmsContent cmsContent = new CmsContent();
// 设置创建时间和更新时间
cmsContent.setCreateTime(now);
cmsContent.setUpdateTime(now);
cmsContent.setCreateBy(createBy);
cmsContent.setUpdateBy(createBy);
// 设置默认值
cmsContent.setDeleteFlag(0);
cmsContent.setAuditStatus(1); // 默认草稿状态
cmsContent.setPublishStatus(1); // 默认未发布状态
cmsContent.setViewCount(0);
cmsContent.setLikeCount(0);
cmsContent.setCommentCount(0);
cmsContent.setSort(0);
// 读取Excel中的字段
// content_id - 数据库自动生成不需要读取
cmsContent.setTitle(getStringValue(row, "title"));
cmsContent.setTitleEn(getStringValue(row, "title_en"));
cmsContent.setOrigin(getStringValue(row, "origin"));
// 处理tags字段避免NullPointerException
String tags = getStringValue(row, "tags");
if (tags == null) {
System.err.println("" + rowIndex + " 行数据的tags字段为null跳过该条记录");
continue;
}
cmsContent.setTags(tags.replaceAll(" ",""));
cmsContent.setIcon(getStringValue(row, "icon"));
cmsContent.setIsOfficial(getBooleanValue(row, "is_official"));
cmsContent.setPrice(getBigDecimalValue(row, "price"));
cmsContent.setLikeCount(getIntegerValue(row, "like_count"));
cmsContent.setShareCount(getIntegerValue(row, "share_count"));
cmsContent.setContentType(getIntegerValue(row, "content_type"));
cmsContent.setContent(getStringValue(row, "content"));
cmsContent.setContentEn(getStringValue(row, "content_en"));
cmsContent.setAuditStatus(getIntegerValue(row, "audit_status"));
cmsContent.setPublishStatus(getIntegerValue(row, "publish_status"));
cmsContent.setPublishTime(getDateValue(row, "publish_time"));
cmsContent.setViewCount(getIntegerValue(row, "view_count"));
cmsContent.setCommentCount(getIntegerValue(row, "comment_count"));
cmsContent.setIsPaid(getIntegerValue(row, "is_paid"));
cmsContent.setSupportPointsPay(getIntegerValue(row, "support_points_pay"));
cmsContent.setDescription(getStringValue(row, "description"));
cmsContent.setDescriptionEn(getStringValue(row, "description_en"));
cmsContent.setIntroduce(getStringValue(row, "introduce"));
cmsContent.setIntroduceEn(getStringValue(row, "introduce_en"));
// create_time - 由系统生成不需要读取
// update_time - 由系统生成不需要读取
cmsContent.setDeleteFlag(getIntegerValue(row, "delete_flag"));
batchList.add(cmsContent);
// 达到批量大小或最后一行时执行批量插入
if (batchList.size() >= BATCH_SIZE || rowIndex == totalRows - 1) {
if (!batchList.isEmpty()) {
// 执行批量插入
this.cmsContentMapper.batchInsert(batchList);
successCount += batchList.size();
batchList.clear(); // 清空批次
}
}
} catch (Exception e) {
System.err.println("处理第 " + rowIndex + " 行数据时出错: " + e.getMessage());
e.printStackTrace();
// 跳过当前行继续处理下一行
continue;
}
}
} catch (Exception e) {
@ -942,4 +967,66 @@ public class CmsContentServiceImpl implements CmsContentService {
}
return cmsContent.getTitle();
}
@Override
public int importFromPath(ImportPathDto importPathDto, String createBy) {
int totalSuccessCount = 0;
try {
// 检查目录是否存在
File directory = new File(importPathDto.getFilePath());
if (!directory.exists() || !directory.isDirectory()) {
System.err.println("目录不存在或不是有效目录: " + importPathDto.getFilePath());
return 0;
}
// 如果不是追加模式清空表
if (!importPathDto.isAppend()) {
cmsContentMapper.truncateTable();
}
// 读取目录下所有 Excel 文件排除 Office 临时锁定文件
File[] files = directory.listFiles((dir, name) -> {
// 跳过以 ~$ 开头的临时锁定文件
if (name.startsWith("~$")) {
return false;
}
return name.endsWith(".xls") || name.endsWith(".xlsx");
});
if (files == null || files.length == 0) {
return 0;
}
// 记录文件总数
int totalFiles = files.length;
System.out.println("总共发现 " + totalFiles + " 个 Excel 文件需要导入");
// 遍历所有 Excel 文件并导入
for (int i = 0; i < files.length; i++) {
File file = files[i];
System.out.println("当前处理第 " + (i + 1) + " 个文件,文件名称是:" + file.getName());
try (FileInputStream fis = new FileInputStream(file)) {
// 读取文件内容到字节数组
byte[] fileBytes = new byte[(int) file.length()];
fis.read(fileBytes);
// 调用现有的 importFromExcel 方法进行导入
int successCount = importFromExcel(fileBytes, createBy);
totalSuccessCount += successCount;
System.out.println("" + (i + 1) + " 个文件导入成功,导入了 " + successCount + " 条记录");
} catch (Exception e) {
System.err.println("导入文件失败: " + file.getAbsolutePath());
e.printStackTrace();
// 单个文件导入失败不影响其他文件
continue;
}
}
System.out.println("导入完成,共处理 " + totalFiles + " 个文件,成功导入 " + totalSuccessCount + " 条记录");
} catch (Exception e) {
System.err.println("导入操作失败: " + importPathDto.getFilePath());
e.printStackTrace();
}
return totalSuccessCount;
}
}

View File

@ -14,8 +14,8 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;

View File

@ -4,6 +4,7 @@ import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.collection.CollectionUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.kexue.skills.common.Assert;
import com.kexue.skills.common.util.HttpUtil;
import com.kexue.skills.config.DeepSeekConfig;
import com.kexue.skills.config.GlmConfig;
@ -140,14 +141,14 @@ public class SkillGenServiceImpl implements SkillGenService {
StringBuilder tagsList = new StringBuilder();
for (int i = 0; i < tags.size(); i++) {
CmsTag tag = tags.get(i);
tagsList.append(tag.getTagName());
tagsList.append(tag.getTagId()+"."+tag.getTagName());
if (i < tags.size() - 1) {
tagsList.append("");
}
}
// 构建系统消息内容
String systemContent = "你是一个专业的AI技能设计助手。请根据agent skills撰写规范按照用户提出的主题描述及参考文件生成这个skill的名称、描述并从以下标签列表中选择一个或者多个标签:\"" + tagsList.toString() + "\"并简述这个skill的具体价值点。输出json格式仅输出以上所提到的名称、描述、标签、价值点节点名称分别为name、description、tags、value_point节点内容以中文形式返回。请严格按照指定的JSON格式输出仅包含要求的字段以中文形式返回。";
String systemContent = "你是一个专业的AI技能设计助手。请根据agent skills撰写规范按照用户提出的主题描述及参考文件生成这个skill的名称、描述并从以下标签列表中选择至少3个标签:\"" + tagsList.toString() + "\"tags只需要返回序号数组并简述这个skill的具体价值点。输出json格式仅输出以上所提到的名称、描述、标签、价值点节点名称分别为name、description、tags、value_point节点内容以中文形式返回。请严格按照指定的JSON格式输出仅包含要求的字段以中文形式返回。";
// 准备文件URL列表
List<String> fileUrls = new ArrayList<>();
@ -198,6 +199,11 @@ public class SkillGenServiceImpl implements SkillGenService {
@Override
public CmsContent generate(SkillGenRequest request) {
log.info("生成技能请求: {}", request);
// 参数验证确保每个参数都必须传递
Assert.notEmpty(request.getName(), "技能名称不能为空");
Assert.notEmpty(request.getDescription(), "技能描述不能为空");
Assert.notEmpty(request.getTags(), "技能标签不能为空");
Assert.notEmpty(request.getRequirement(), "技能摘要不能为空");
String url = deepSeekConfig.getBaseUrl() + "/v1/chat/completions";
// 从数据库中读取cms_tag表的标签信息
@ -223,7 +229,7 @@ public class SkillGenServiceImpl implements SkillGenService {
}
}
}
String systemContent = "你是一个专业的AI技能设计助手。请基于用户提供的Skill名称、描述、标签按照skills目录结构输出完整的skills内容包括skills.md本体内容、scripts目录中的脚本等并打包成一个YAML文件技能包。请严格遵循以下规范1. 包含必需的文件和目录2. 多行内容使用 | 字面块3. 内容从行首开始4. 空目录用 children: [] 表示5. 文件内容要实际有用6.输出一个完整的YAML文档概要含核心属性name、version、description、author、created、tags 等,其中 structure 为核心节点,用于描述 skill 包的文件目录结构structure 下的每个节点均含基础属性name、type、path、format、descriptioncontent 和 children 为互选属性,由 type 决定type 为 file 时,展示文件具体内容在 content 字段type 为 directory 时,展示子目录 / 文件节点在 children [] 数组中,无需其他额外说明。";
String systemContent = "你是一个专业的AI技能设计助手。请基于用户提供的Skill名称、描述、标签按照skills目录结构输出完整的skills内容包括skills.md本体内容、scripts目录中的脚本等并打包成一个YAML文件技能包。请严格遵循以下规范1. 包含必需的文件和目录2. 多行内容使用 | 字面块3. 内容从行首开始4. 空目录用 children: [] 表示5. 文件内容要实际有用6.输出一个完整的YAML文档概要含核心属性name、version、description、author、created、tags 等,其中 structure 为核心节点,用于描述 skill 包的文件目录结构structure 下的每个节点均含基础属性name、type、path、format、descriptioncontent 和 children 为互选属性,由 type 决定type 为 file 时,展示文件具体内容在 content 字段,必须保证content内容的缩进为2个字符type 为 directory 时,展示子目录 / 文件节点在 children [] 数组中,无需其他额外说明。";
String userContent = "请根据以下Skill信息生成skills.md文档内容Skill名称SKILL_NAME,Skill描述DESCRIPTION,Skill标签TAGS 摘要SUMMARY。";
userContent = userContent.replace("SKILL_NAME", request.getName()).replace("DESCRIPTION", request.getDescription()).replace("TAGS", tagsList.toString()).replace("SUMMARY", request.getRequirement());
SkillRequest skillRequest = new SkillRequest(true, deepSeekConfig.getChat().getModel(),systemContent,userContent,deepSeekConfig.getChat().getTemperature(), 8192,"text");
@ -467,7 +473,7 @@ public class SkillGenServiceImpl implements SkillGenService {
request.setName(defaultSkillName);
request.setDescription("未知");
request.setIntroduce("未知");
request.setTags(Arrays.asList("10001", "10002"));
request.setTags(Arrays.asList("1001", "1000"));
}
// 3. 使用SkillGenRequest中的信息生成yaml
@ -482,9 +488,19 @@ public class SkillGenServiceImpl implements SkillGenService {
// 4. 生成CmsContent对象
CmsContent cmsContent = getCmsContent(request, yamlContent, StpUtil.getLoginIdAsLong(), "");
List<CmsTag> list = tags.stream().filter(tag -> tag.getTagId() == Long.parseLong(cmsContent.getTags().split(",")[0])).toList();
if (CollectionUtil.isNotEmpty(list)){
cmsContent.setIcon(list.get(0).getIcon());
if (Objects.nonNull(cmsContent.getTags())) {
String s = cmsContent.getTags().split(",")[0];
long l = 1000;// 默认值
try {
l = Long.parseLong(s);
} catch (NumberFormatException e) {
// 异常是因为大模型返回的tag带中文了忽略
}
long finalL = l;
List<CmsTag> list = tags.stream().filter(tag -> tag.getTagId() == finalL).toList();
if (CollectionUtil.isNotEmpty(list)){
cmsContent.setIcon(list.get(0).getIcon());
}
}
// 删除临时文件
tempFile.delete();
@ -518,7 +534,7 @@ public class SkillGenServiceImpl implements SkillGenService {
- skill的核心描述description
- 详细功能介绍introduce
- 从指定标签列表中选取至少3个适配标签tagList标签范围TAG_LIST
- 选择标签的时候只需要输出对应的标签编号
- 选择标签的时候只需要输出对应的标签编号特别注意只需要标签编号
请将最终结果以JSON格式输出JSON结构必须包含name(技能名称),description技能描述introduce功能介绍tagList标签列表无需额外说明仅输出符合要求的JSON内容
""";
systemContent = systemContent.replace("TAG_LIST", tagsList.toString());
@ -554,7 +570,7 @@ public class SkillGenServiceImpl implements SkillGenService {
if (skillJson.containsKey("tagList")) {
request.setTags(skillJson.getJSONArray("tagList").toJavaList(String.class));
} else {
request.setTags(Arrays.asList("10001", "10002"));
request.setTags(Arrays.asList("1001", "1002"));
}
return request;
}

View File

@ -16,6 +16,7 @@ import com.kexue.skills.entity.dto.ContentPurchaseDto;
import com.kexue.skills.entity.dto.SessionDto;
import com.kexue.skills.entity.dto.SysUserDto;
import com.kexue.skills.entity.request.*;
import com.kexue.skills.entity.request.SysUserUpdateDto;
import com.kexue.skills.mapper.*;
import com.kexue.skills.service.SysUserService;
import com.kexue.skills.utils.MD5Util;
@ -194,6 +195,69 @@ public class SysUserServiceImpl implements SysUserService {
return queryById(sysUser.getUserId());
}
/**
* 修改用户数据
*
* @param sysUserUpdateDto 用户更新请求参数
* @return 实例对象
*/
@Override
@CacheInvalidate(name = "sysUser:", key = "#sysUserUpdateDto.userId")
@CacheInvalidate(name = "sysUser:username:", key = "#sysUserUpdateDto.userName")
public SysUser update(SysUserUpdateDto sysUserUpdateDto) {
if (Objects.nonNull(sysUserUpdateDto.getUserId())){
// 查询用户信息
SysUser sysUser = sysUserMapper.queryById(sysUserUpdateDto.getUserId());
Assert.notNull(sysUser, "用户不存在");
// 校验用户名是否已经存在
if (sysUserUpdateDto.getUserName() != null && !sysUserUpdateDto.getUserName().isEmpty()) {
SysUser existingUser = sysUserMapper.getByUsername(sysUserUpdateDto.getUserName());
Assert.isTrue(existingUser == null || existingUser.getUserId().equals(sysUserUpdateDto.getUserId()), "用户名已存在");
sysUser.setUserName(sysUserUpdateDto.getUserName());
}
// 更新邮箱
if (sysUserUpdateDto.getEmail() != null) {
sysUser.setEmail(sysUserUpdateDto.getEmail());
}
// 更新手机号
if (sysUserUpdateDto.getTel() != null) {
sysUser.setTel(sysUserUpdateDto.getTel());
}
// 更新状态
if (sysUserUpdateDto.getEnable() != null) {
sysUser.setEnable(sysUserUpdateDto.getEnable());
}
// 更新密码如果有
if (sysUserUpdateDto.getPassword() != null && !sysUserUpdateDto.getPassword().isEmpty()) {
try {
// 假设客户端已经对密码进行了一次MD5加密服务端使用双重加密验证
String encryptedPwd = MD5Util.doubleEncrypt(sysUserUpdateDto.getPassword(), sysUser.getSalt());
sysUser.setPwd(encryptedPwd);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
// 执行更新
sysUserMapper.update(sysUser);
}else {
// 如果没有用户ID创建新用户
SysUser sysUser = new SysUser();
sysUser.setUserName(sysUserUpdateDto.getUserName());
sysUser.setEmail(sysUserUpdateDto.getEmail());
sysUser.setTel(sysUserUpdateDto.getTel());
sysUser.setEnable(sysUserUpdateDto.getEnable());
sysUser.setPwd(sysUserUpdateDto.getPassword());
insert(sysUser);
}
return queryById(sysUserUpdateDto.getUserId());
}
/**
* 通过主键删除数据
*
@ -980,7 +1044,7 @@ public class SysUserServiceImpl implements SysUserService {
sessionDto.setNew(false);
} else {
// 没有会话创建新的会话ID
sessionId = java.util.UUID.randomUUID().toString().replaceAll("-","");
sessionId = java.util.UUID.randomUUID().toString();
sysUser.setSessionId(sessionId);
sysUserMapper.update(sysUser);
sessionDto.setSessionId(sessionId);

View File

@ -592,5 +592,10 @@
and publish_status = #{publishStatus}
</if>
</select>
<!--清空表数据-->
<update id="truncateTable">
truncate table cms_content
</update>
</mapper>