From e651e73fa2413f382bc7cb6d120642cf65567400 Mon Sep 17 00:00:00 2001 From: wangzhiwei Date: Sat, 11 Apr 2026 21:11:53 +0800 Subject: [PATCH] =?UTF-8?q?feat(account):=20=E5=AE=9E=E7=8E=B0=E8=B4=A6?= =?UTF-8?q?=E6=88=B7=E7=A7=AF=E5=88=86=E7=B3=BB=E7=BB=9F=E5=8F=8A=E5=A5=97?= =?UTF-8?q?=E9=A4=90=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将账户余额系统改造为积分系统,充值金额按1元=100积分计算 - 新增套餐配置功能,支持套餐购买并获取基础额度和赠送额度 - 在账户冻结功能中集成模型价格计算,根据预估tokens自动计算冻结金额 - 更新支付流程以支持套餐ID关联和积分计算 - 修改全局异常处理器返回格式,统一使用CommonResult - 优化账户交易记录的备注信息显示 - 添加雪花算法配置用于分布式ID生成 - 扩展账户冻结DTO添加预估tokens字段 - 重构账户服务中的金额处理逻辑为积分处理逻辑 - 实现套餐配置的CRUD操作接口和相关实体类 - 更新支付回调逻辑以正确处理套餐购买场景 --- .../com/kexue/skills/common/CommonResult.java | 15 +- .../controller/AccountFrozenController.java | 26 +- .../controller/PackageConfigController.java | 121 +++ .../kexue/skills/entity/PackageConfig.java | 43 + .../com/kexue/skills/entity/PaymentOrder.java | 2 +- .../skills/entity/dto/AccountFrozenDto.java | 6 + .../skills/entity/dto/PackageConfigDto.java | 27 + .../skills/entity/dto/PaymentOrderDto.java | 2 + .../exception/GlobalExceptionHandler.java | 4 +- .../skills/mapper/PackageConfigMapper.java | 66 ++ .../skills/service/PackageConfigService.java | 73 ++ .../impl/AccountFrozenServiceImpl.java | 74 +- .../service/impl/AccountServiceImpl.java | 86 +- .../impl/PackageConfigServiceImpl.java | 66 ++ .../skills/service/impl/PayServiceImpl.java | 14 +- .../service/impl/SkillGenServiceImpl.java | 972 +++--------------- src/main/resources/application.yml | 4 + .../resources/mapper/PackageConfigMapper.xml | 90 ++ 18 files changed, 835 insertions(+), 856 deletions(-) create mode 100644 src/main/java/com/kexue/skills/controller/PackageConfigController.java create mode 100644 src/main/java/com/kexue/skills/entity/PackageConfig.java create mode 100644 src/main/java/com/kexue/skills/entity/dto/PackageConfigDto.java create mode 100644 src/main/java/com/kexue/skills/mapper/PackageConfigMapper.java create mode 100644 src/main/java/com/kexue/skills/service/PackageConfigService.java create mode 100644 src/main/java/com/kexue/skills/service/impl/PackageConfigServiceImpl.java create mode 100644 src/main/resources/mapper/PackageConfigMapper.xml diff --git a/src/main/java/com/kexue/skills/common/CommonResult.java b/src/main/java/com/kexue/skills/common/CommonResult.java index 6aeb76c..7965833 100644 --- a/src/main/java/com/kexue/skills/common/CommonResult.java +++ b/src/main/java/com/kexue/skills/common/CommonResult.java @@ -44,6 +44,15 @@ public class CommonResult { return new CommonResult(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), data); } + /** + * 成功返回结果 + * + * @param data 获取的数据 + */ + public static CommonResult success(IErrorCode errorCode,T data) { + return new CommonResult(errorCode.getCode(), errorCode.getMessage(), data); + } + /** * 成功返回结果 * @@ -58,11 +67,11 @@ public class CommonResult { /** * 成功返回结果 * - * @param errorCode 获取的数据 + * @param code 获取的数据 * @param message 提示信息 */ - public static CommonResult success(IErrorCode errorCode, String message ) { - return new CommonResult(errorCode.getCode(), message,null); + public static CommonResult success(Long code, String message ) { + return new CommonResult(code, message,null); } /** diff --git a/src/main/java/com/kexue/skills/controller/AccountFrozenController.java b/src/main/java/com/kexue/skills/controller/AccountFrozenController.java index 943411c..49c659a 100644 --- a/src/main/java/com/kexue/skills/controller/AccountFrozenController.java +++ b/src/main/java/com/kexue/skills/controller/AccountFrozenController.java @@ -1,5 +1,6 @@ package com.kexue.skills.controller; +import com.kexue.skills.common.CommonResult; import com.kexue.skills.entity.AccountFrozen; import com.kexue.skills.entity.dto.AccountFrozenDto; import com.kexue.skills.entity.dto.AccountReleaseDto; @@ -7,12 +8,16 @@ import com.kexue.skills.service.AccountFrozenService; import com.kexue.skills.common.Result; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; +import java.io.IOException; /** * 账户冻结单控制器 @@ -25,6 +30,9 @@ import javax.annotation.Resource; @Tag(name = "账户冻结单", description = "账户冻结单管理接口") public class AccountFrozenController { + private static final Logger logger = LoggerFactory.getLogger(AccountFrozenController.class); + private static final ObjectMapper objectMapper = new ObjectMapper(); + @Resource private AccountFrozenService accountFrozenService; @@ -35,9 +43,14 @@ public class AccountFrozenController { */ @PostMapping("/frozen") @Operation(summary = "创建冻结单", description = "创建账户冻结单") - public Result createFrozen(@RequestBody AccountFrozenDto accountFrozenDto) { + public CommonResult createFrozen(@RequestBody AccountFrozenDto accountFrozenDto) { + try { + logger.info("创建冻结单入参: {}", objectMapper.writeValueAsString(accountFrozenDto)); + } catch (IOException e) { + logger.error("创建冻结单入参序列化失败", e); + } AccountFrozen accountFrozen = accountFrozenService.createFrozen(accountFrozenDto); - return new Result().ok().data(accountFrozen); + return CommonResult.success(accountFrozen); } /** @@ -47,9 +60,14 @@ public class AccountFrozenController { */ @PostMapping("/release") @Operation(summary = "释放冻结单", description = "释放账户冻结单") - public Result releaseFrozen(@RequestBody AccountReleaseDto accountReleaseDto) { + public CommonResult releaseFrozen(@RequestBody AccountReleaseDto accountReleaseDto) { + try { + logger.info("释放冻结单入参: {}", objectMapper.writeValueAsString(accountReleaseDto)); + } catch (IOException e) { + logger.error("释放冻结单入参序列化失败", e); + } AccountFrozen accountFrozen = accountFrozenService.releaseFrozen(accountReleaseDto); - return new Result().ok().data(accountFrozen); + return CommonResult.success(accountFrozen); } } diff --git a/src/main/java/com/kexue/skills/controller/PackageConfigController.java b/src/main/java/com/kexue/skills/controller/PackageConfigController.java new file mode 100644 index 0000000..7f0b11f --- /dev/null +++ b/src/main/java/com/kexue/skills/controller/PackageConfigController.java @@ -0,0 +1,121 @@ +package com.kexue.skills.controller; + +import com.github.pagehelper.PageInfo; +import com.kexue.skills.annotation.RequireAuth; +import com.kexue.skills.common.CommonResult; +import com.kexue.skills.entity.PackageConfig; +import com.kexue.skills.entity.base.IdDto; +import com.kexue.skills.entity.dto.PackageConfigDto; +import com.kexue.skills.service.PackageConfigService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; + +/** + * (PackageConfig)表控制层 + * + * @author 系统生成 + * @since 2026-04-11 + */ +@RestController +@RequestMapping("api/packageConfig") +@Tag(name = "套餐配置管理 Api") +@CrossOrigin(origins = "*") +public class PackageConfigController { + /** + * 服务对象 + */ + @Resource + private PackageConfigService packageConfigService; + + /** + * 分页查询 + * + * @param queryDto 筛选条件 + * @return 查询结果 + */ + @PostMapping("/getPageList") + @Operation(summary = "查询分页列表", description = "查询分页列表") + public CommonResult> getPageList(@RequestBody PackageConfigDto queryDto) { + return CommonResult.success(packageConfigService.getPageList(queryDto)); + } + + /** + * 查询列表 + * + * @param queryDto 筛选条件 + * @return 查询结果 + */ + @PostMapping("/getList") + @Operation(summary = "查询列表", description = "查询列表") + public CommonResult> getList(@RequestBody PackageConfigDto queryDto) { + return CommonResult.success(new PageInfo<>(packageConfigService.getList(queryDto))); + } + + /** + * 通过主键查询单条数据 + * + * @param id 主键 + * @return 单条数据 + */ + @PostMapping("queryById/{id}") + @Operation(summary = "通过ID查询套餐", description = "通过ID查询套餐") + public CommonResult queryById(@PathVariable("id") Long id) { + return CommonResult.success(packageConfigService.queryById(id)); + } + + /** + * 新增数据 + * + * @param packageConfig 实体 + * @return 新增结果 + */ + @PostMapping("/insert") + @Operation(summary = "新增套餐", description = "新增套餐") + @RequireAuth + public CommonResult insert(@RequestBody PackageConfig packageConfig) { + return CommonResult.success(packageConfigService.insert(packageConfig)); + } + + /** + * 编辑数据 + * + * @param packageConfig 实体 + * @return 编辑结果 + */ + @PostMapping("/update") + @Operation(summary = "更新套餐", description = "更新套餐") + @RequireAuth + public CommonResult update(@RequestBody PackageConfig packageConfig) { + return CommonResult.success(packageConfigService.update(packageConfig)); + } + + /** + * 通过主键逻辑删除 + * + * @param idDto 主键 + * @return 删除数据 + */ + @PostMapping("/logicDeleteById") + @Operation(summary = "逻辑删除套餐", description = "逻辑删除套餐") + @RequireAuth + public CommonResult logicDeleteById(@RequestBody IdDto idDto) { + return CommonResult.success(packageConfigService.logicDeleteById(idDto.getId(), "admin") > 0); + } + + /** + * 删除数据 + * + * @param id 主键 + * @return 删除数据 + */ + @PostMapping("deleteById/{id}") + @Operation(summary = "物理删除套餐", description = "物理删除套餐") + @RequireAuth + public CommonResult deleteById(@PathVariable("id") Long id) { + return CommonResult.success(packageConfigService.deleteById(id) > 0); + } + +} diff --git a/src/main/java/com/kexue/skills/entity/PackageConfig.java b/src/main/java/com/kexue/skills/entity/PackageConfig.java new file mode 100644 index 0000000..8707a35 --- /dev/null +++ b/src/main/java/com/kexue/skills/entity/PackageConfig.java @@ -0,0 +1,43 @@ +package com.kexue.skills.entity; + +import java.io.Serializable; +import com.kexue.skills.entity.base.BaseEntity; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Date; + +/** + * (PackageConfig)实体类 + * 套餐配置表 + * + * @author 系统生成 + * @since 2026-04-11 + */ +@Data +public class PackageConfig extends BaseEntity implements Serializable { + private static final long serialVersionUID = 1L; + + @Schema(description ="套餐ID") + private Long id; + + @Schema(description ="套餐名称") + private String name; + + @Schema(description ="价格") + private BigDecimal price; + + @Schema(description ="基础额度") + private BigDecimal baseAmount; + + @Schema(description ="赠送额度") + private BigDecimal giftAmount; + + @Schema(description ="创建时间") + private Date createTime; + + @Schema(description ="更新时间") + private Date updateTime; + +} diff --git a/src/main/java/com/kexue/skills/entity/PaymentOrder.java b/src/main/java/com/kexue/skills/entity/PaymentOrder.java index 0ddb54a..b95ffdf 100644 --- a/src/main/java/com/kexue/skills/entity/PaymentOrder.java +++ b/src/main/java/com/kexue/skills/entity/PaymentOrder.java @@ -56,7 +56,7 @@ public class PaymentOrder extends BaseEntity implements Serializable { @Schema(description ="商品描述") private String productDesc; - @Schema(description ="关联业务ID") + @Schema(description ="关联业务ID,比如packageId(套餐ID)") private Long businessId; @Schema(description ="业务类型:recharge,purchase_content") diff --git a/src/main/java/com/kexue/skills/entity/dto/AccountFrozenDto.java b/src/main/java/com/kexue/skills/entity/dto/AccountFrozenDto.java index fa36224..03e83a9 100644 --- a/src/main/java/com/kexue/skills/entity/dto/AccountFrozenDto.java +++ b/src/main/java/com/kexue/skills/entity/dto/AccountFrozenDto.java @@ -30,6 +30,12 @@ public class AccountFrozenDto { @Schema(description ="冻结类型:1余额 2图片张数 3时间 4次数 5积分 99其他") private Integer frozenType; + @Schema(description ="预估输入tokens") + private Long estimatedInputTokens; + + @Schema(description ="预估输出tokens") + private Long estimatedOutputTokens; + @Schema(description ="过期时间") private Date expireAt; diff --git a/src/main/java/com/kexue/skills/entity/dto/PackageConfigDto.java b/src/main/java/com/kexue/skills/entity/dto/PackageConfigDto.java new file mode 100644 index 0000000..df97e87 --- /dev/null +++ b/src/main/java/com/kexue/skills/entity/dto/PackageConfigDto.java @@ -0,0 +1,27 @@ +package com.kexue.skills.entity.dto; + +import com.kexue.skills.entity.base.BaseQueryDto; +import lombok.Data; + +import java.math.BigDecimal; + +/** + * (PackageConfig)查询DTO类 + * + * @author 系统生成 + * @since 2026-04-11 + */ +@Data +public class PackageConfigDto extends BaseQueryDto { + + private Long id; + + private String name; + + private BigDecimal price; + + private BigDecimal baseAmount; + + private BigDecimal giftAmount; + +} diff --git a/src/main/java/com/kexue/skills/entity/dto/PaymentOrderDto.java b/src/main/java/com/kexue/skills/entity/dto/PaymentOrderDto.java index a79e862..5b5af4f 100644 --- a/src/main/java/com/kexue/skills/entity/dto/PaymentOrderDto.java +++ b/src/main/java/com/kexue/skills/entity/dto/PaymentOrderDto.java @@ -30,4 +30,6 @@ public class PaymentOrderDto extends BaseQueryDto { private Integer deleteFlag; + private Long packageId; + } \ No newline at end of file diff --git a/src/main/java/com/kexue/skills/exception/GlobalExceptionHandler.java b/src/main/java/com/kexue/skills/exception/GlobalExceptionHandler.java index e1caffc..0d150ff 100644 --- a/src/main/java/com/kexue/skills/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/kexue/skills/exception/GlobalExceptionHandler.java @@ -15,9 +15,9 @@ public class GlobalExceptionHandler { @ExceptionHandler(BizException.class) public CommonResult handleBizException(BizException e) { if (e == null) { - return CommonResult.failed("未知错误"); + return CommonResult.success("未知错误"); } - return CommonResult.failed(e.getMessage()); + return CommonResult.success(e.getErrorCode(), e.getMessage()); } // 其他异常处理... diff --git a/src/main/java/com/kexue/skills/mapper/PackageConfigMapper.java b/src/main/java/com/kexue/skills/mapper/PackageConfigMapper.java new file mode 100644 index 0000000..f3bdbf4 --- /dev/null +++ b/src/main/java/com/kexue/skills/mapper/PackageConfigMapper.java @@ -0,0 +1,66 @@ +package com.kexue.skills.mapper; + +import com.kexue.skills.entity.PackageConfig; +import com.kexue.skills.entity.dto.PackageConfigDto; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * (PackageConfig)表数据库访问层 + * + * @author 系统生成 + * @since 2026-04-11 + */ +@Mapper +public interface PackageConfigMapper { + /** + * 分页查询 + * + * @param queryDto 筛选条件 + * @return 查询结果 + */ + List getPageList(PackageConfigDto queryDto); + + /** + * 查询列表 + * + * @param queryDto 筛选条件 + * @return 查询结果 + */ + List getList(PackageConfigDto queryDto); + + /** + * 通过主键查询单条数据 + * + * @param id 主键 + * @return 实例对象 + */ + PackageConfig queryById(Long id); + + /** + * 新增数据 + * + * @param packageConfig 实例对象 + * @return 影响行数 + */ + int insert(PackageConfig packageConfig); + + /** + * 更新数据 + * + * @param packageConfig 实例对象 + * @return 影响行数 + */ + int update(PackageConfig packageConfig); + + /** + * 通过主键删除 + * + * @param id 主键 + * @return 影响行数 + */ + int deleteById(Long id); + +} diff --git a/src/main/java/com/kexue/skills/service/PackageConfigService.java b/src/main/java/com/kexue/skills/service/PackageConfigService.java new file mode 100644 index 0000000..03db44f --- /dev/null +++ b/src/main/java/com/kexue/skills/service/PackageConfigService.java @@ -0,0 +1,73 @@ +package com.kexue.skills.service; + +import com.github.pagehelper.PageInfo; +import com.kexue.skills.entity.PackageConfig; +import com.kexue.skills.entity.dto.PackageConfigDto; + +import java.util.List; + +/** + * (PackageConfig)表服务接口 + * + * @author 系统生成 + * @since 2026-04-11 + */ +public interface PackageConfigService extends BaseService { + /** + * 分页查询 + * + * @param queryDto 筛选条件 + * @return 查询结果 + */ + PageInfo getPageList(PackageConfigDto queryDto); + + /** + * 查询列表 + * + * @param queryDto 筛选条件 + * @return 查询结果 + */ + List getList(PackageConfigDto queryDto); + + /** + * 通过主键查询单条数据 + * + * @param id 主键 + * @return 实例对象 + */ + PackageConfig queryById(Long id); + + /** + * 新增数据 + * + * @param packageConfig 实例对象 + * @return 实例对象 + */ + PackageConfig insert(PackageConfig packageConfig); + + /** + * 更新数据 + * + * @param packageConfig 实例对象 + * @return 实例对象 + */ + PackageConfig update(PackageConfig packageConfig); + + /** + * 通过主键逻辑删除 + * + * @param id 主键 + * @param updateBy 更新人 + * @return 影响行数 + */ + int logicDeleteById(Long id, String updateBy); + + /** + * 通过主键物理删除 + * + * @param id 主键 + * @return 影响行数 + */ + int deleteById(Long id); + +} diff --git a/src/main/java/com/kexue/skills/service/impl/AccountFrozenServiceImpl.java b/src/main/java/com/kexue/skills/service/impl/AccountFrozenServiceImpl.java index e609454..d613049 100644 --- a/src/main/java/com/kexue/skills/service/impl/AccountFrozenServiceImpl.java +++ b/src/main/java/com/kexue/skills/service/impl/AccountFrozenServiceImpl.java @@ -62,9 +62,6 @@ public class AccountFrozenServiceImpl implements AccountFrozenService { if (accountFrozenDto.getSessionId() == null) { throw new BizException(ResultCode.SESSION_ID_NOT_EXIST.getCode(), ResultCode.SESSION_ID_NOT_EXIST.getMessage()); } - if (accountFrozenDto.getFrozenAmount() == null || accountFrozenDto.getFrozenAmount().compareTo(BigDecimal.ZERO) <= 0) { - throw new BizException(ResultCode.PARAMETER_EMPTY.getCode(), "冻结金额必须大于0"); - } if (accountFrozenDto.getFrozenType() == null) { throw new BizException(ResultCode.PARAMETER_EMPTY.getCode(), "冻结类型不能为空"); } @@ -82,16 +79,47 @@ public class AccountFrozenServiceImpl implements AccountFrozenService { throw new BizException(ResultCode.ACCOUNT_NOT_EXIST.getCode(), ResultCode.ACCOUNT_NOT_EXIST.getMessage()); } - // 4. 检查余额是否足够(账户总余额 - 已冻结金额 >= 本次冻结金额) + // 4. 当冻结类型为余额时,根据预估tokens计算冻结金额 + BigDecimal finalFrozenAmount = accountFrozenDto.getFrozenAmount(); + if (accountFrozenDto.getFrozenType() != null && accountFrozenDto.getFrozenType() == 1) { + if (accountFrozenDto.getEstimatedInputTokens() != null && + accountFrozenDto.getEstimatedOutputTokens() != null && + accountFrozenDto.getModelName() != null) { + + // 查询模型价格信息 + ModelPrice modelPrice = modelPriceService.queryByModelName(accountFrozenDto.getModelName()); + if (modelPrice != null) { + // 计算token费用 + long inputFee = accountFrozenDto.getEstimatedInputTokens() / modelPrice.getInputPerCent(); + if (accountFrozenDto.getEstimatedInputTokens() % modelPrice.getInputPerCent() > 0) { + inputFee += 1; + } + + long outputFee = accountFrozenDto.getEstimatedOutputTokens() / modelPrice.getOutputPerCent(); + if (accountFrozenDto.getEstimatedOutputTokens() % modelPrice.getOutputPerCent() > 0) { + outputFee += 1; + } + + // 总费用(分) + long totalFee = inputFee + outputFee; + // 转换为元 + BigDecimal feeInYuan = BigDecimal.valueOf(totalFee).divide(BigDecimal.valueOf(100)); + // 转换为积分(1元=100积分) + finalFrozenAmount = feeInYuan.multiply(BigDecimal.valueOf(100)); + } + } + } + + // 5. 检查余额是否足够(账户总余额 - 已冻结金额 >= 本次冻结金额) BigDecimal balance = account.getBalance() == null ? BigDecimal.ZERO : account.getBalance(); BigDecimal frozenAmount = account.getFrozenAmount() == null ? BigDecimal.ZERO : account.getFrozenAmount(); BigDecimal availableBalance = balance.subtract(frozenAmount); - if (availableBalance.compareTo(accountFrozenDto.getFrozenAmount()) < 0) { + if (availableBalance.compareTo(finalFrozenAmount) < 0) { throw new BizException(ResultCode.INSUFFICIENT_BALANCE.getCode(), ResultCode.INSUFFICIENT_BALANCE.getMessage()); } // 5. 更新账户冻结金额 - account.setFrozenAmount(frozenAmount.add(accountFrozenDto.getFrozenAmount())); + account.setFrozenAmount(frozenAmount.add(finalFrozenAmount)); account.setUpdateTime(new Date()); accountMapper.update(account); @@ -102,7 +130,7 @@ public class AccountFrozenServiceImpl implements AccountFrozenService { accountFrozen.setSessionId(accountFrozenDto.getSessionId()); accountFrozen.setCallId(accountFrozenDto.getCallId()); accountFrozen.setModelName(accountFrozenDto.getModelName()); - accountFrozen.setFrozenAmount(accountFrozenDto.getFrozenAmount()); + accountFrozen.setFrozenAmount(finalFrozenAmount); accountFrozen.setFrozenType(accountFrozenDto.getFrozenType()); accountFrozen.setStatus("RESERVED"); accountFrozen.setExpireAt(accountFrozenDto.getExpireAt()); @@ -173,7 +201,9 @@ public class AccountFrozenServiceImpl implements AccountFrozenService { // 总费用(分) long totalFee = inputFee + outputFee; // 转换为元 - finalAmount = BigDecimal.valueOf(totalFee).divide(BigDecimal.valueOf(100)); + BigDecimal feeInYuan = BigDecimal.valueOf(totalFee).divide(BigDecimal.valueOf(100)); + // 转换为积分(1元=100积分) + finalAmount = feeInYuan.multiply(BigDecimal.valueOf(100)); } } } @@ -188,10 +218,32 @@ public class AccountFrozenServiceImpl implements AccountFrozenService { // 如果需要扣减余额 if (finalAmount.compareTo(BigDecimal.ZERO) > 0) { - if (balance.compareTo(finalAmount) < 0) { - throw new BizException(ResultCode.INSUFFICIENT_BALANCE.getCode(), ResultCode.INSUFFICIENT_BALANCE.getMessage()); + // 检查实际扣减是否大于预扣减 + if (finalAmount.compareTo(accountFrozen.getFrozenAmount()) > 0) { + // 实际扣减大于预扣减,根据实际扣减进行扣除 + // 如果余额不够实际扣减,将balance设置为0 + if (balance.compareTo(finalAmount) < 0) { + account.setBalance(BigDecimal.ZERO); + } else { + account.setBalance(balance.subtract(finalAmount)); + } + } else { + // 实际扣减小于等于预扣减,正常扣减 + if (balance.compareTo(finalAmount) < 0) { + account.setBalance(BigDecimal.ZERO); + } else { + account.setBalance(balance.subtract(finalAmount)); + } + + // 实际扣减小于预扣减,将剩余预扣减加回balance + if (finalAmount.compareTo(accountFrozen.getFrozenAmount()) < 0) { + BigDecimal remainingFrozen = accountFrozen.getFrozenAmount().subtract(finalAmount); + account.setBalance(account.getBalance().add(remainingFrozen)); + } } - account.setBalance(balance.subtract(finalAmount)); + } else { + // 最终扣减为0,将预扣减全部加回balance + account.setBalance(balance.add(accountFrozen.getFrozenAmount())); } account.setUpdateTime(new Date()); diff --git a/src/main/java/com/kexue/skills/service/impl/AccountServiceImpl.java b/src/main/java/com/kexue/skills/service/impl/AccountServiceImpl.java index 481e8e9..75921a4 100644 --- a/src/main/java/com/kexue/skills/service/impl/AccountServiceImpl.java +++ b/src/main/java/com/kexue/skills/service/impl/AccountServiceImpl.java @@ -14,7 +14,9 @@ import com.kexue.skills.mapper.AccountTransactionMapper; import com.kexue.skills.mapper.SysUserMapper; import com.kexue.skills.service.AccountService; import com.kexue.skills.service.ModelPriceService; +import com.kexue.skills.service.PackageConfigService; import com.kexue.skills.entity.ModelPrice; +import com.kexue.skills.entity.PackageConfig; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -43,6 +45,9 @@ public class AccountServiceImpl implements AccountService { @Resource private ModelPriceService modelPriceService; + @Resource + private PackageConfigService packageConfigService; + /** * 分页查询 * @@ -162,34 +167,63 @@ public class AccountServiceImpl implements AccountService { this.insert(account); } - // 2. 保存交易记录 + // 2. 计算积分 + BigDecimal points = BigDecimal.ZERO; + String rechargeRemark = ""; + + // 检查是否有套餐ID + if (businessId != null) { + // 查询套餐配置 + PackageConfig packageConfig = packageConfigService.queryById(businessId); + if (packageConfig != null) { + // 套餐的baseAmount和giftAmount已经是积分,直接使用 + points = packageConfig.getBaseAmount().add(packageConfig.getGiftAmount()); + // 根据套餐信息生成remark + rechargeRemark = "购买套餐:" + packageConfig.getName() + ",获得" + points + "积分"; + } else { + // 没有找到套餐,按照原来的逻辑计算积分 + points = amount.multiply(BigDecimal.valueOf(100)); + rechargeRemark = "充值" + amount + "元,获得" + points + "积分"; + } + } else { + // 没有套餐ID,按照原来的逻辑计算积分 + points = amount.multiply(BigDecimal.valueOf(100)); + rechargeRemark = "充值" + amount + "元,获得" + points + "积分"; + } + + // 3. 保存交易记录 AccountTransaction transaction = new AccountTransaction(); transaction.setUserId(userId); transaction.setUserName(account.getUserName()); transaction.setTransactionType(1); // 充值 - transaction.setAmount(amount); + transaction.setAmount(points); // 存储充值金额(积分) transaction.setBeforeBalance(account.getBalance()); - transaction.setAfterBalance(account.getBalance().add(amount)); + transaction.setAfterBalance(account.getBalance().add(points)); // 余额为积分 transaction.setStatus(1); // 成功 transaction.setTransactionNo(transactionNo); transaction.setPayType(3); // 余额支付 - transaction.setBusinessId(businessId); + transaction.setBusinessId(businessId);// 套餐ID transaction.setBusinessType(businessType); - transaction.setRemark(remark); + + // 添加额外备注 + if (remark != null && !remark.isEmpty()) { + rechargeRemark += " - " + remark; + } + transaction.setRemark(rechargeRemark); transaction.setIsExpense(0); // 收入 transaction.setIncomeType("recharge"); // 充值 this.accountTransactionMapper.insert(transaction); - // 3. 更新账户余额 + // 4. 更新账户余额(使用积分) if (isWithdrawable) { BigDecimal withdrawableBalance = account.getWithdrawableBalance() == null ? BigDecimal.ZERO : account.getWithdrawableBalance(); - account.setWithdrawableBalance(withdrawableBalance.add(amount)); + account.setWithdrawableBalance(withdrawableBalance.add(points)); } else { BigDecimal nonWithdrawableBalance = account.getNonWithdrawableBalance() == null ? BigDecimal.ZERO : account.getNonWithdrawableBalance(); - account.setNonWithdrawableBalance(nonWithdrawableBalance.add(amount)); + account.setNonWithdrawableBalance(nonWithdrawableBalance.add(points)); } BigDecimal balance = account.getBalance() == null ? BigDecimal.ZERO : account.getBalance(); - account.setBalance(balance.add(amount)); + account.setBalance(balance.add(points)); account.setUpdateTime(new Date()); this.update(account); return 1; @@ -327,15 +361,13 @@ public class AccountServiceImpl implements AccountService { Assert.notNull(dto.getQuestion(), "问题不能为空"); Assert.notNull(dto.getModelName(), "模型名称不能为空"); Assert.notNull(dto.getQuestion(), "问题不能为空"); - Assert.notNull(dto.getModelName(), "模型名称不能为空"); - Assert.notNull(dto.getQuestion(), "问题不能为空"); } /** - * 增加账户余额(签到奖励token转换) + * 增加账户积分(签到奖励token转换) * * @param userId 用户ID - * @param amount 增加金额 + * @param amount 增加积分 * @param transactionNo 交易单号 * @param businessId 业务ID * @param businessType 业务类型 @@ -362,15 +394,20 @@ public class AccountServiceImpl implements AccountService { transaction.setUserId(userId); transaction.setUserName(account.getUserName()); transaction.setTransactionType(5); // 签到奖励 - transaction.setAmount(amount); + transaction.setAmount(amount); // 存储签到奖励积分 transaction.setBeforeBalance(account.getBalance()); - transaction.setAfterBalance(account.getBalance().add(amount)); + transaction.setAfterBalance(account.getBalance().add(amount)); // 余额为积分 transaction.setStatus(1); // 成功 transaction.setTransactionNo(transactionNo); transaction.setPayType(3); // 余额支付 transaction.setBusinessId(businessId); transaction.setBusinessType(businessType); - transaction.setRemark(remark); + // 在备注中添加积分信息 + String signInRemark = "签到奖励" + amount + "积分"; + if (remark != null && !remark.isEmpty()) { + signInRemark += " - " + remark; + } + transaction.setRemark(signInRemark); transaction.setIsExpense(0); // 收入 transaction.setIncomeType("sign_in"); // 签到奖励 this.accountTransactionMapper.insert(transaction); @@ -386,10 +423,10 @@ public class AccountServiceImpl implements AccountService { } /** - * 给用户赠送金额(不可提现) + * 给用户赠送积分(不可提现) * * @param userId 用户ID - * @param amount 赠送金额 + * @param amount 赠送积分 * @param transactionNo 交易单号 * @param businessId 业务ID * @param businessType 业务类型 @@ -416,20 +453,25 @@ public class AccountServiceImpl implements AccountService { transaction.setUserId(userId); transaction.setUserName(account.getUserName()); transaction.setTransactionType(6); // 赠送 - transaction.setAmount(amount); + transaction.setAmount(amount); // 存储赠送积分 transaction.setBeforeBalance(account.getBalance()); - transaction.setAfterBalance(account.getBalance().add(amount)); + transaction.setAfterBalance(account.getBalance().add(amount)); // 余额为积分 transaction.setStatus(1); // 成功 transaction.setTransactionNo(transactionNo); transaction.setPayType(3); // 余额支付 transaction.setBusinessId(businessId); transaction.setBusinessType(businessType); - transaction.setRemark(remark); + // 在备注中添加积分信息 + String giftRemark = "赠送" + amount + "积分"; + if (remark != null && !remark.isEmpty()) { + giftRemark += " - " + remark; + } + transaction.setRemark(giftRemark); transaction.setIsExpense(0); // 收入 transaction.setIncomeType("gift"); // 赠送 this.accountTransactionMapper.insert(transaction); - // 3. 更新账户余额(赠送金额不可提现) + // 3. 更新账户余额(赠送积分不可提现) if (account.getNonWithdrawableBalance() == null){ account.setNonWithdrawableBalance(BigDecimal.ZERO); } diff --git a/src/main/java/com/kexue/skills/service/impl/PackageConfigServiceImpl.java b/src/main/java/com/kexue/skills/service/impl/PackageConfigServiceImpl.java new file mode 100644 index 0000000..6bf7c34 --- /dev/null +++ b/src/main/java/com/kexue/skills/service/impl/PackageConfigServiceImpl.java @@ -0,0 +1,66 @@ +package com.kexue.skills.service.impl; + +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import com.kexue.skills.entity.PackageConfig; +import com.kexue.skills.entity.dto.PackageConfigDto; +import com.kexue.skills.mapper.PackageConfigMapper; +import com.kexue.skills.service.PackageConfigService; +import org.springframework.stereotype.Service; + +import jakarta.annotation.Resource; +import java.util.List; + +/** + * (PackageConfig)表服务实现类 + * + * @author 系统生成 + * @since 2026-04-11 + */ +@Service +public class PackageConfigServiceImpl implements PackageConfigService { + + @Resource + private PackageConfigMapper packageConfigMapper; + + @Override + public PageInfo getPageList(PackageConfigDto queryDto) { + PageHelper.startPage(queryDto.getPageNum(), queryDto.getPageSize()); + List list = packageConfigMapper.getList(queryDto); + return new PageInfo<>(list); + } + + @Override + public List getList(PackageConfigDto queryDto) { + return packageConfigMapper.getList(queryDto); + } + + @Override + public PackageConfig queryById(Long id) { + return packageConfigMapper.queryById(id); + } + + @Override + public PackageConfig insert(PackageConfig packageConfig) { + packageConfigMapper.insert(packageConfig); + return packageConfig; + } + + @Override + public PackageConfig update(PackageConfig packageConfig) { + packageConfigMapper.update(packageConfig); + return packageConfig; + } + + @Override + public int logicDeleteById(Long id, String updateBy) { + // 由于package_config表没有delete_flag字段,这里直接调用物理删除 + return packageConfigMapper.deleteById(id); + } + + @Override + public int deleteById(Long id) { + return packageConfigMapper.deleteById(id); + } + +} diff --git a/src/main/java/com/kexue/skills/service/impl/PayServiceImpl.java b/src/main/java/com/kexue/skills/service/impl/PayServiceImpl.java index c3bfe5a..45b9be7 100644 --- a/src/main/java/com/kexue/skills/service/impl/PayServiceImpl.java +++ b/src/main/java/com/kexue/skills/service/impl/PayServiceImpl.java @@ -11,6 +11,8 @@ import com.kexue.skills.entity.PaymentOrder; import com.kexue.skills.service.PayService; import com.kexue.skills.service.PaymentOrderService; import com.kexue.skills.service.AccountService; +import com.kexue.skills.service.PackageConfigService; +import com.kexue.skills.entity.PackageConfig; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; @@ -63,6 +65,9 @@ public class PayServiceImpl implements PayService { @Resource private AccountService accountService; + @Resource + private PackageConfigService packageConfigService; + /** * 生成随机字符串 * @return 随机字符串 @@ -505,11 +510,11 @@ public class PayServiceImpl implements PayService { order.getAmount(), true, // 可提现 transactionId, - order.getOrderId(), + order.getBusinessId(), "recharge", "微信支付充值" ); - logger.info("微信支付回调:更新账户余额成功,userId={}, amount={}", order.getUserId(), order.getAmount()); + logger.info("微信支付回调:更新账户余额成功,userId={}, amount={}, actualAmount={}", order.getUserId(), order.getAmount(), order.getAmount()); } catch (Exception e) { logger.error("微信支付回调:更新账户余额失败", e); // 继续处理,不影响回调响应 @@ -564,6 +569,7 @@ public class PayServiceImpl implements PayService { bizContent.put("out_trade_no", order.getOrderNo()); bizContent.put("total_amount", order.getAmount().toString()); bizContent.put("subject", order.getProductName()); + bizContent.put("businessId", order.getBusinessId()); bizContent.put("body", order.getProductDesc()); bizContent.put("timeout_express", "30m"); bizContent.put("product_code", "FAST_INSTANT_TRADE_PAY"); @@ -647,11 +653,11 @@ public class PayServiceImpl implements PayService { order.getAmount(), true, // 可提现 transactionId, - order.getOrderId(), + order.getBusinessId(), "recharge", "支付宝支付充值" ); - logger.info("支付宝支付回调:更新账户余额成功,userId={}, amount={}", order.getUserId(), order.getAmount()); + logger.info("支付宝支付回调:更新账户余额成功,userId={}, amount={}, actualAmount={}", order.getUserId(), order.getAmount(), order.getAmount()); } catch (Exception e) { logger.error("支付宝支付回调:更新账户余额失败", e); // 继续处理,不影响回调响应 diff --git a/src/main/java/com/kexue/skills/service/impl/SkillGenServiceImpl.java b/src/main/java/com/kexue/skills/service/impl/SkillGenServiceImpl.java index 2dc3b6b..58ec230 100644 --- a/src/main/java/com/kexue/skills/service/impl/SkillGenServiceImpl.java +++ b/src/main/java/com/kexue/skills/service/impl/SkillGenServiceImpl.java @@ -11,8 +11,6 @@ import com.kexue.skills.config.GlmConfig; import com.kexue.skills.entity.CmsContent; import com.kexue.skills.entity.CmsTag; import com.kexue.skills.entity.dto.CmsTagDto; -import com.kexue.skills.entity.dto.SkillPackageInfoDto; -import com.kexue.skills.entity.dto.SkillStructureNodeDto; import com.kexue.skills.entity.request.SkillAnalyzeRequest; import com.kexue.skills.entity.request.SkillGenRequest; import com.kexue.skills.entity.request.SkillPreGenRequest; @@ -24,43 +22,20 @@ import com.kexue.skills.service.CmsTagService; import com.kexue.skills.service.SkillGenService; import com.kexue.skills.utils.EscapeCharacterUtils; import com.kexue.skills.utils.YamlToMapUtil; -import com.kexue.skills.utils.YamlUtil; import jodd.util.StringUtil; import lombok.extern.slf4j.Slf4j; -import net.sf.sevenzipjbinding.IInArchive; -import net.sf.sevenzipjbinding.SevenZip; -import net.sf.sevenzipjbinding.impl.RandomAccessFileInStream; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.yaml.snakeyaml.error.YAMLException; import javax.annotation.Resource; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; -import java.io.InputStream; -import java.io.RandomAccessFile; import java.math.BigDecimal; -import java.nio.charset.StandardCharsets; -import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Base64; -import java.util.Date; -import java.util.Enumeration; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.TreeSet; +import java.util.*; import java.util.stream.Collectors; -import java.util.zip.ZipEntry; -import java.util.zip.ZipInputStream; /** * 技能生成服务实现 @@ -74,10 +49,10 @@ public class SkillGenServiceImpl implements SkillGenService { @Autowired private DeepSeekConfig deepSeekConfig; - + @Autowired private GlmConfig glmConfig; - + @Autowired private CmsTagService cmsTagService; @@ -94,25 +69,25 @@ public class SkillGenServiceImpl implements SkillGenService { public SkillResponse preGenerate(SkillPreGenRequest request) { log.info("生成技能请求: {}", request); String url = deepSeekConfig.getBaseUrl() + "/v1/chat/completions"; - + // 从数据库中读取cms_tag表的标签信息 CmsTagDto tagDto = new CmsTagDto(); tagDto.setDeleteFlag(0); tagDto.setStatus(1); List tags = cmsTagService.getList(tagDto); - + // 将标签名称拼接成逗号分隔的字符串 StringBuilder tagsList = new StringBuilder(); for (int i = 0; i < tags.size(); i++) { CmsTag tag = tags.get(i); - tagsList.append(tag.getTagId() + "." + tag.getTagName()); + tagsList.append(tag.getTagId()+"."+tag.getTagName()); if (i < tags.size() - 1) { tagsList.append(","); } } - - SkillRequest skillRequest = new SkillRequest(true, deepSeekConfig.getChat().getModel(), - deepSeekConfig.getChat().getTemperature(), deepSeekConfig.getChat().getMaxTokens(), + + SkillRequest skillRequest = new SkillRequest(true, deepSeekConfig.getChat().getModel(), + deepSeekConfig.getChat().getTemperature(), deepSeekConfig.getChat().getMaxTokens(), request.getPrompt(), tagsList.toString()); String deepseekResponse = ""; @@ -124,26 +99,26 @@ public class SkillGenServiceImpl implements SkillGenService { // 解析deepseek返回结果 JSONObject responseJson = JSON.parseObject(deepseekResponse); List choices = responseJson.getJSONArray("choices").toJavaList(JSONObject.class); - + if (choices != null && !choices.isEmpty()) { // 获取最新的choice(这里是第一个,因为只有一个) JSONObject latestChoice = choices.get(0); JSONObject message = latestChoice.getJSONObject("message"); String content = message.getString("content"); - + // 解析content中的JSON JSONObject skillJson = JSON.parseObject(content); SkillResponse skillResponse = new SkillResponse(); skillResponse.setName(skillJson.getString("name")); skillResponse.setDescription(skillJson.getString("description")); skillResponse.setTags(skillJson.getJSONArray("tags").toJavaList(String.class)); - + log.info("解析技能响应: {}", skillResponse); return skillResponse; } } catch (Exception e) { log.error("调用Deepseek API失败: {}", e.getMessage(), e); - throw new BizException("调用Deepseek API失败:" + e.getMessage() + " Deepseek API响应:" + deepseekResponse); + throw new BizException("调用Deepseek API失败:"+ e.getMessage()+" Deepseek API响应:"+ deepseekResponse); } return null; @@ -157,26 +132,26 @@ public class SkillGenServiceImpl implements SkillGenService { String model = glmConfig.getChat().getModel(); double temperature = glmConfig.getChat().getTemperature(); int maxTokens = glmConfig.getChat().getMaxTokens(); - + // 从数据库中读取cms_tag表的标签信息 CmsTagDto tagDto = new CmsTagDto(); tagDto.setDeleteFlag(0); tagDto.setStatus(1); List tags = cmsTagService.getList(tagDto); - + // 将标签名称拼接成逗号分隔的字符串 StringBuilder tagsList = new StringBuilder(); for (int i = 0; i < tags.size(); i++) { CmsTag tag = tags.get(i); - tagsList.append(tag.getTagId() + "." + tag.getTagName()); + tagsList.append(tag.getTagId()+"."+tag.getTagName()); if (i < tags.size() - 1) { tagsList.append(","); } } - + // 构建系统消息内容 String systemContent = "你是一个专业的AI技能设计助手。请根据agent skills撰写规范,按照用户提出的主题描述及参考文件,生成这个skill的名称、描述,并从以下标签列表中选择至少3个标签:\"" + tagsList.toString() + "\",tags只需要返回序号数组,并简述这个skill的具体价值点。输出json格式,仅输出以上所提到的名称、描述、标签、价值点,节点名称分别为name、description、tags、value_point,节点内容以中文形式返回。请严格按照指定的JSON格式输出,仅包含要求的字段,以中文形式返回。"; - + // 准备文件URL列表 List fileUrls = new ArrayList<>(); if (request.getFileUrl() != null && !request.getFileUrl().isEmpty()) { @@ -185,7 +160,7 @@ public class SkillGenServiceImpl implements SkillGenService { if (request.getFileUrls() != null && !request.getFileUrls().isEmpty()) { fileUrls.addAll(request.getFileUrls()); } - + // 创建技能请求 SkillRequest skillRequest = new SkillRequest(model, systemContent, request.getPrompt(), fileUrls, temperature, maxTokens); @@ -198,13 +173,13 @@ public class SkillGenServiceImpl implements SkillGenService { // 解析返回结果 JSONObject responseJson = JSON.parseObject(response); List choices = responseJson.getJSONArray("choices").toJavaList(JSONObject.class); - + if (choices != null && !choices.isEmpty()) { // 获取最新的choice(这里是第一个,因为只有一个) JSONObject latestChoice = choices.get(0); JSONObject message = latestChoice.getJSONObject("message"); String content = message.getString("content"); - + // 解析content中的JSON JSONObject skillJson = JSON.parseObject(content); SkillResponse skillResponse = new SkillResponse(); @@ -212,13 +187,13 @@ public class SkillGenServiceImpl implements SkillGenService { skillResponse.setDescription(skillJson.getString("description")); skillResponse.setTags(skillJson.getJSONArray("tags").toJavaList(String.class)); skillResponse.setSummary(skillJson.getString("value_point")); - + log.info("解析技能响应: {}", skillResponse); return skillResponse; } } catch (Exception e) { log.error("调用API失败: {}", e.getMessage(), e); - throw new BizException("调用Deepseek API失败:" + e.getMessage() + " Deepseek API响应:" + response); + throw new BizException("调用Deepseek API失败:"+ e.getMessage()+" Deepseek API响应:"+ response); } return null; } @@ -246,8 +221,8 @@ public class SkillGenServiceImpl implements SkillGenService { String defaultIcon = ""; for (int i = 0; i < tags.size(); i++) { CmsTag tag = tags.get(i); - if (tags1.contains(tag.getTagId() + "")) { - if (StringUtil.isEmpty(defaultIcon)) { + if (tags1.contains(tag.getTagId()+"")) { + if (StringUtil.isEmpty(defaultIcon)){ defaultIcon = tag.getIcon(); } tagsList.append(tag.getTagName()); @@ -257,99 +232,99 @@ public class SkillGenServiceImpl implements SkillGenService { } } String systemContent = """ - 你是AI技能包设计专家,仅输出【完整的纯YAML文本】,输出内容是完整可解析的技能YAML描述文件,绝非片段,不包含任何多余文字(无解释、无注释、无引言、无结尾)。 - - ### 一、YAML顶层强制规则(仅一个节点:package) - 1. 顶层只能有 package 一个节点,所有信息(名称、版本、目录结构等)均嵌套在 package 下 - 2. package 节点必含子字段:name、version、description、author、created、tags、structure(缺一不可) - 3. 最终必须输出完整闭合的YAML结构,禁止输出残缺片段、部分节点 - - ### 二、package子字段规范(固定格式) - 1. name:技能名称(与用户提供的Skill名称完全一致,不修改) - 2. version:固定为 "1.0.0" - 3. description:用户提供的Skill描述(完整复制,不增删任何内容) - 4. author:固定为 "AI技能生成助手" - 5. created:格式为 "YYYY-MM-DD"(使用当前日期,如2026-04-01) - 6. tags:数组格式,值为用户提供的Skill标签(中文,自动去重,逗号后加空格) - 7. structure:技能包目录树,根目录固定为 /,structure下直接编写根目录的一级子文件/子文件夹,禁止重复描述根目录本身 - - ### 三、structure目录树规则(仅Python脚本,无其他语言) - #### 1. 基础必含文件(所有技能通用) - - 根目录 / 下必生成:skills.md 文件,归属父路径 /,文件必须保留后缀名 .md - - #### 2. structure核心编写规则 - 1. structure节点下不写根目录/的描述,直接从一级子文件/子文件夹开始编写 - 2. 【path路径强制规则】path只写父级目录路径,不拼接文件名称/目录名称 - - 目录自身名称使用 name 字段体现,目录一律无后缀名 - - 文件自身名称使用 name 字段体现,文件必须保留标准后缀名 - - 示例:scripts目录在根目录下 → path: /,name: scripts - - 示例:skills.md在根目录下 → path: /,name: skills.md - - 示例:main.py在scripts目录下 → path: /scripts,name: main.py - 3. 所有directory/file节点均为根目录/的直接/间接子节点 - - #### 3. Python脚本目录/文件判断逻辑 - - 若用户提供的“Skill描述”“Skill摘要”中包含“脚本”“代码”“执行”“运行”“处理”“计算”等需执行逻辑的关键词: - 1. 必须新增 scripts 目录,该目录为文件夹,无任何后缀名,path: /,name: scripts - 2. 必须在该目录下固定生成 main.py 文件,文件必须带 .py 后缀,不允许省略或修改文件名 - - 若用户需求中无任何执行逻辑相关描述(如纯文档、纯说明类技能):不生成 scripts 目录,避免冗余 - - #### 4. 节点必含字段 - - directory类型:name、type、path(仅父级路径)、format(固定"dir")、description、children(空目录写 children: []) - - file类型:name、type、path(仅父级路径)、format(仅markdown/python两种)、description、content(非空,有实际可用内容) - - ### 四、文件content内容规范(Python脚本必实用) - #### 1. 根目录下 skills.md - - # 技能名称(不加多余符号,居中可加空格但不强制) - - ## 技能描述(整合用户“描述+摘要”,补充逻辑连贯性) - - ## 标签(格式:- 标签1\n- 标签2) - - ## 使用说明(分点写:适用场景、操作步骤,需脚本则写“运行scripts/main.py脚本”,无需则写“直接参考文档使用”) - - ## 目录结构(用代码块 ``` 列出所有文件/目录路径) - - #### 2. scripts目录下 main.py - - 必含依赖导入(如 import pandas as pd,无依赖则不写) - - 必含入口函数 def execute(params: dict) -> dict:(参数为dict,返回dict结果) - - 函数内必含: - 1. 参数校验(判断必填键是否存在,缺失返回错误) - 2. 核心逻辑(匹配技能需求,如数据处理、文本分析) - 3. 结果返回(成功:{"status": "success", "data": 结果};失败:{"status": "fail", "error": 信息}) - - 必含注释:函数说明、参数/返回值说明、示例调用(if __name__ == "__main__": 块) - - 禁止空函数、语法错误,确保复制后可直接运行 - - ### 五、YAML语法死规定(100%无解析错误) - 1. 缩进:统一2个空格(禁止Tab,禁止1/3/4空格,嵌套层级严格对齐) - - package 下子字段缩进2空格 - - structure 下目录/文件节点缩进4空格(package→structure→children,每层+2空格) - 2. 路径:全部遵循path只写父目录规则,Unix风格 /,禁止 \\ 或 ./ - 3. 字符串:特殊字符(:、#、空格)无需转义,直接书写 - 4. 数组:tags格式严格为 tags: [标签1, 标签2](逗号后加空格,无多余逗号) - 5. content:多行内容用 | 开头,内容行首顶格,内部遵循对应格式缩进(Python用4空格) - - ### 六、错误规避红线(绝对不能触碰) - 1. 禁止顶层出现除 package 外的任何节点 - 2. 禁止在YAML前后加任何多余文字 - 3. 禁止生成非Python脚本,禁止生成无后缀的脚本文件 - 4. 禁止字段缺失 - 5. 禁止输出YAML片段,必须输出完整可解析文件 - 6. structure禁止描述根目录/,直接从一级子节点开始 - 7. 严禁path中携带文件/目录名称,必须只写父级路径 - 8. 严禁将scripts目录错误添加后缀名,严禁修改Python脚本名为非main.py - - 最终输出仅完整纯YAML,直接可复制存储、解析使用,无任何冗余或格式问题! - """; + 你是AI技能包设计专家,仅输出【完整的纯YAML文本】,输出内容是完整可解析的技能YAML描述文件,绝非片段,不包含任何多余文字(无解释、无注释、无引言、无结尾)。 + + ### 一、YAML顶层强制规则(仅一个节点:package) + 1. 顶层只能有 package 一个节点,所有信息(名称、版本、目录结构等)均嵌套在 package 下 + 2. package 节点必含子字段:name、version、description、author、created、tags、structure(缺一不可) + 3. 最终必须输出完整闭合的YAML结构,禁止输出残缺片段、部分节点 + + ### 二、package子字段规范(固定格式) + 1. name:技能名称(与用户提供的Skill名称完全一致,不修改) + 2. version:固定为 "1.0.0" + 3. description:用户提供的Skill描述(完整复制,不增删任何内容) + 4. author:固定为 "AI技能生成助手" + 5. created:格式为 "YYYY-MM-DD"(使用当前日期,如2026-04-01) + 6. tags:数组格式,值为用户提供的Skill标签(中文,自动去重,逗号后加空格) + 7. structure:技能包目录树,根目录固定为 /,structure下直接编写根目录的一级子文件/子文件夹,禁止重复描述根目录本身 + + ### 三、structure目录树规则(仅Python脚本,无其他语言) + #### 1. 基础必含文件(所有技能通用) + - 根目录 / 下必生成:skills.md 文件,归属父路径 /,文件必须保留后缀名 .md + + #### 2. structure核心编写规则 + 1. structure节点下不写根目录/的描述,直接从一级子文件/子文件夹开始编写 + 2. 【path路径强制规则】path只写父级目录路径,不拼接文件名称/目录名称 + - 目录自身名称使用 name 字段体现,目录一律无后缀名 + - 文件自身名称使用 name 字段体现,文件必须保留标准后缀名 + - 示例:scripts目录在根目录下 → path: /,name: scripts + - 示例:skills.md在根目录下 → path: /,name: skills.md + - 示例:main.py在scripts目录下 → path: /scripts,name: main.py + 3. 所有directory/file节点均为根目录/的直接/间接子节点 + + #### 3. Python脚本目录/文件判断逻辑 + - 若用户提供的“Skill描述”“Skill摘要”中包含“脚本”“代码”“执行”“运行”“处理”“计算”等需执行逻辑的关键词: + 1. 必须新增 scripts 目录,该目录为文件夹,无任何后缀名,path: /,name: scripts + 2. 必须在该目录下固定生成 main.py 文件,文件必须带 .py 后缀,不允许省略或修改文件名 + - 若用户需求中无任何执行逻辑相关描述(如纯文档、纯说明类技能):不生成 scripts 目录,避免冗余 + + #### 4. 节点必含字段 + - directory类型:name、type、path(仅父级路径)、format(固定"dir")、description、children(空目录写 children: []) + - file类型:name、type、path(仅父级路径)、format(仅markdown/python两种)、description、content(非空,有实际可用内容) + + ### 四、文件content内容规范(Python脚本必实用) + #### 1. 根目录下 skills.md + - # 技能名称(不加多余符号,居中可加空格但不强制) + - ## 技能描述(整合用户“描述+摘要”,补充逻辑连贯性) + - ## 标签(格式:- 标签1\n- 标签2) + - ## 使用说明(分点写:适用场景、操作步骤,需脚本则写“运行scripts/main.py脚本”,无需则写“直接参考文档使用”) + - ## 目录结构(用代码块 ``` 列出所有文件/目录路径) + + #### 2. scripts目录下 main.py + - 必含依赖导入(如 import pandas as pd,无依赖则不写) + - 必含入口函数 def execute(params: dict) -> dict:(参数为dict,返回dict结果) + - 函数内必含: + 1. 参数校验(判断必填键是否存在,缺失返回错误) + 2. 核心逻辑(匹配技能需求,如数据处理、文本分析) + 3. 结果返回(成功:{"status": "success", "data": 结果};失败:{"status": "fail", "error": 信息}) + - 必含注释:函数说明、参数/返回值说明、示例调用(if __name__ == "__main__": 块) + - 禁止空函数、语法错误,确保复制后可直接运行 + + ### 五、YAML语法死规定(100%无解析错误) + 1. 缩进:统一2个空格(禁止Tab,禁止1/3/4空格,嵌套层级严格对齐) + - package 下子字段缩进2空格 + - structure 下目录/文件节点缩进4空格(package→structure→children,每层+2空格) + 2. 路径:全部遵循path只写父目录规则,Unix风格 /,禁止 \\ 或 ./ + 3. 字符串:特殊字符(:、#、空格)无需转义,直接书写 + 4. 数组:tags格式严格为 tags: [标签1, 标签2](逗号后加空格,无多余逗号) + 5. content:多行内容用 | 开头,内容行首顶格,内部遵循对应格式缩进(Python用4空格) + + ### 六、错误规避红线(绝对不能触碰) + 1. 禁止顶层出现除 package 外的任何节点 + 2. 禁止在YAML前后加任何多余文字 + 3. 禁止生成非Python脚本,禁止生成无后缀的脚本文件 + 4. 禁止字段缺失 + 5. 禁止输出YAML片段,必须输出完整可解析文件 + 6. structure禁止描述根目录/,直接从一级子节点开始 + 7. 严禁path中携带文件/目录名称,必须只写父级路径 + 8. 严禁将scripts目录错误添加后缀名,严禁修改Python脚本名为非main.py + + 最终输出仅完整纯YAML,直接可复制存储、解析使用,无任何冗余或格式问题! + """; String userContent = """ - 基于以下信息生成技能包YAML,严格遵守system指令(仅Python脚本,无其他语言): - 1. Skill名称:%s - 2. Skill描述:%s - 3. Skill标签:%s(中文,直接使用,不修改) - 4. Skill摘要/需求:%s(用于完善skills.md的“使用说明”章节) - """.formatted( + 基于以下信息生成技能包YAML,严格遵守system指令(仅Python脚本,无其他语言): + 1. Skill名称:%s + 2. Skill描述:%s + 3. Skill标签:%s(中文,直接使用,不修改) + 4. Skill摘要/需求:%s(用于完善skills.md的“使用说明”章节) + """.formatted( request.getName(), request.getDescription(), tagsList.toString(), request.getRequirement() ); - SkillRequest skillRequest = new SkillRequest(true, deepSeekConfig.getChat().getModel(), systemContent, userContent, deepSeekConfig.getChat().getTemperature(), 8192, "text"); + SkillRequest skillRequest = new SkillRequest(true, deepSeekConfig.getChat().getModel(),systemContent,userContent,deepSeekConfig.getChat().getTemperature(), 8192,"text"); String deepseekResponse = ""; try { // 发送HTTP请求到deepseek API @@ -357,10 +332,10 @@ public class SkillGenServiceImpl implements SkillGenService { log.info("Deepseek API响应: {}", deepseekResponse); JSONObject responseJson = JSON.parseObject(deepseekResponse); String content = responseJson.getJSONArray("choices").toJavaList(JSONObject.class).get(0).getJSONObject("message").getString("content"); - content = EscapeCharacterUtils.removeEscapeCharacters(content);//去除转义字符 - CmsContent cmsContent = getCmsContent(request, content, StpUtil.getLoginIdAsLong(), defaultIcon); + content = EscapeCharacterUtils.removeEscapeCharacters( content);//去除转义字符 + CmsContent cmsContent = getCmsContent(request, content, StpUtil.getLoginIdAsLong(),defaultIcon); List list = tags.stream().filter(tag -> tag.getTagId() == Long.parseLong(cmsContent.getTags().split(",")[0])).toList(); - if (CollectionUtil.isNotEmpty(list)) { + if (CollectionUtil.isNotEmpty( list)){ cmsContent.setIcon(list.get(0).getIcon()); } // 保存到数据库 @@ -368,11 +343,11 @@ public class SkillGenServiceImpl implements SkillGenService { return cmsContent; } catch (Exception e) { log.error("调用Deepseek API失败: {}", e.getMessage(), e); - throw new BizException("调用Deepseek API失败:" + e.getMessage() + " Deepseek API响应:" + deepseekResponse); + throw new BizException("调用Deepseek API失败:"+ e.getMessage()+" Deepseek API响应:"+ deepseekResponse); } } - private CmsContent getCmsContent(SkillGenRequest request, String CmsContent, Long userId, String defaultIcon) { + private CmsContent getCmsContent(SkillGenRequest request, String CmsContent,Long userId,String defaultIcon){ CmsContent cmsContent = new CmsContent(); cmsContent.setTitle(request.getName()); cmsContent.setDescription(request.getDescription()); @@ -394,8 +369,8 @@ public class SkillGenServiceImpl implements SkillGenService { cmsContent.setCreateTime(new Date()); cmsContent.setUpdateTime(new Date()); cmsContent.setContentType(1); - cmsContent.setCreateBy(userId + ""); - cmsContent.setUpdateBy(userId + ""); + cmsContent.setCreateBy(userId+""); + cmsContent.setUpdateBy(userId+""); cmsContent.setDeleteFlag(0); cmsContent.setIcon(defaultIcon); cmsContent.setRequirement(request.getRequirement()); @@ -422,7 +397,7 @@ public class SkillGenServiceImpl implements SkillGenService { public String genIntroduce(String content) { log.info("生成技能介绍请求: {}", content); String url = deepSeekConfig.getBaseUrl() + "/v1/chat/completions"; - + String systemContent = "你是一个专业的AI技能设计助手。我会给你提供一个完整的skill的内容,请你帮我总结出skill的作用,能够解决的问题,输出一段描述文本"; SkillRequest skillRequest = new SkillRequest(true, deepSeekConfig.getChat().getModel(), systemContent, content, 0.3, 500, "text"); String deepseekResponse = ""; @@ -430,29 +405,29 @@ public class SkillGenServiceImpl implements SkillGenService { // 发送HTTP请求到deepseek API deepseekResponse = HttpUtil.sendPostRequest(url, skillRequest, deepSeekConfig.getApiKey(), null); log.info("Deepseek API响应: {}", deepseekResponse); - + // 解析返回结果 JSONObject responseJson = JSON.parseObject(deepseekResponse); List choices = responseJson.getJSONArray("choices").toJavaList(JSONObject.class); - + if (choices != null && !choices.isEmpty()) { JSONObject latestChoice = choices.get(0); JSONObject message = latestChoice.getJSONObject("message"); - return EscapeCharacterUtils.removeEscapeCharacters(message.getString("content"));//去除转义字符 + return EscapeCharacterUtils.removeEscapeCharacters( message.getString("content"));//去除转义字符 } } catch (Exception e) { log.error("调用Deepseek API失败: {}", e.getMessage(), e); - throw new BizException("调用Deepseek API失败:" + e.getMessage() + " Deepseek API响应:" + deepseekResponse); + throw new BizException("调用Deepseek API失败:"+ e.getMessage()+" Deepseek API响应:"+ deepseekResponse); } - + return null; } - + @Override public String genIntroduceByDescription(String description) { log.info("根据技能描述生成技能介绍请求: {}", description); String url = deepSeekConfig.getBaseUrl() + "/v1/chat/completions"; - + String systemContent = "你是一个专业的AI技能设计助手。我会给你提供一个skill的描述,请你基于这个描述,生成一段详细的技能介绍,包括技能的作用、能够解决的问题、使用场景等,输出一段完整的描述文本"; SkillRequest skillRequest = new SkillRequest(true, deepSeekConfig.getChat().getModel(), systemContent, description, 0.3, 500, "text"); String deepseekResponse = ""; @@ -460,21 +435,21 @@ public class SkillGenServiceImpl implements SkillGenService { // 发送HTTP请求到deepseek API deepseekResponse = HttpUtil.sendPostRequest(url, skillRequest, deepSeekConfig.getApiKey(), null); log.info("Deepseek API响应: {}", deepseekResponse); - + // 解析返回结果 JSONObject responseJson = JSON.parseObject(deepseekResponse); List choices = responseJson.getJSONArray("choices").toJavaList(JSONObject.class); - + if (choices != null && !choices.isEmpty()) { JSONObject latestChoice = choices.get(0); JSONObject message = latestChoice.getJSONObject("message"); - return EscapeCharacterUtils.removeEscapeCharacters(message.getString("content"));//去除转义字符 + return EscapeCharacterUtils.removeEscapeCharacters( message.getString("content"));//去除转义字符 } } catch (Exception e) { log.error("调用Deepseek API失败: {}", e.getMessage(), e); - throw new BizException("调用Deepseek API失败:" + e.getMessage() + " Deepseek API响应:" + deepseekResponse); + throw new BizException("调用Deepseek API失败:"+ e.getMessage()+" Deepseek API响应:"+ deepseekResponse); } - + return null; } @@ -493,7 +468,7 @@ 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.getTagId() + "." + tag.getTagName()); + tagsList.append(tag.getTagId()+"."+tag.getTagName()); if (i < tags.size() - 1) { tagsList.append(","); } @@ -520,10 +495,10 @@ public class SkillGenServiceImpl implements SkillGenService { 请将最终结果以JSON格式输出,JSON结构必须包含:name(技能名称),description(技能描述)、introduce(功能介绍)、tagList(标签列表)、content(完整YAML格式技能包内容),无需额外说明,仅输出符合要求的JSON内容。 """; systemContent = systemContent.replace("TAG_LIST", tagsList.toString()); - + // 构建用户消息内容 String userContent = skillUrl; - + // 创建技能请求 SkillRequest skillRequest = new SkillRequest(true, deepSeekConfig.getChat().getModel(), systemContent, userContent, 0.5, 8192, "json_object"); @@ -532,31 +507,31 @@ public class SkillGenServiceImpl implements SkillGenService { // 发送HTTP请求到DeepSeek API deepseekResponse = HttpUtil.sendPostRequest(url, skillRequest, deepSeekConfig.getApiKey(), null); log.info("Deepseek API响应: {}", deepseekResponse); - + // 解析返回结果 JSONObject responseJson = JSON.parseObject(deepseekResponse); List choices = responseJson.getJSONArray("choices").toJavaList(JSONObject.class); - + if (choices != null && !choices.isEmpty()) { // 获取最新的choice JSONObject latestChoice = choices.get(0); JSONObject message = latestChoice.getJSONObject("message"); String content = message.getString("content"); - + // 解析content中的JSON JSONObject skillJson = JSON.parseObject(content); - + // 构建技能生成请求 SkillGenRequest request = new SkillGenRequest(); request.setName(skillJson.getString("name")); request.setDescription(skillJson.getString("description")); request.setIntroduce(skillJson.getString("introduce")); request.setTags(skillJson.getJSONArray("tagList").toJavaList(String.class)); - + // 生成CmsContent对象 CmsContent cmsContent = getCmsContent(request, skillJson.getString("content"), StpUtil.getLoginIdAsLong(), ""); List list = tags.stream().filter(tag -> tag.getTagId() == Long.parseLong(cmsContent.getTags().split(",")[0])).toList(); - if (CollectionUtil.isNotEmpty(list)) { + if (CollectionUtil.isNotEmpty( list)){ cmsContent.setIcon(list.get(0).getIcon()); } // 保存到数据库 @@ -565,16 +540,16 @@ public class SkillGenServiceImpl implements SkillGenService { } } catch (Exception e) { log.error("调用Deepseek API失败: {}", e.getMessage(), e); - throw new BizException("调用Deepseek API失败:" + e.getMessage() + " Deepseek API响应:" + deepseekResponse); + throw new BizException("调用Deepseek API失败:"+ e.getMessage()+" Deepseek API响应:"+ deepseekResponse); } - + return null; } - + @Override public CmsContent uploadSkillV2(byte[] fileBytes, String fileName) { log.info("上传本地技能压缩包请求: {}", fileName); - + try { // 创建临时文件 String fileExtension = ""; @@ -588,7 +563,7 @@ public class SkillGenServiceImpl implements SkillGenService { try (FileOutputStream fos = new FileOutputStream(tempFile)) { fos.write(fileBytes); } - + // 生成默认参数 String author = StpUtil.getLoginIdAsString(); // 从文件名中提取技能名称 @@ -599,11 +574,11 @@ public class SkillGenServiceImpl implements SkillGenService { defaultSkillName = fileName.substring(0, dotIndex); } } - + // 1. 提取压缩包中的skillMdText Map skillInfo = com.kexue.skills.common.util.SkillZipParser.extractSkillMdText(tempFile.getAbsolutePath(), defaultSkillName); String skillMdText = (String) skillInfo.get("skillMdText"); - + // 从数据库中读取cms_tag表的标签信息 CmsTagDto tagDto = new CmsTagDto(); tagDto.setDeleteFlag(0); @@ -625,11 +600,11 @@ public class SkillGenServiceImpl implements SkillGenService { // 3. 使用SkillGenRequest中的信息生成yaml String yamlContent = com.kexue.skills.common.util.SkillZipParser.generateYamlFromSkillInfo( - tempFile.getAbsolutePath(), - author, - request.getName(), - request.getDescription(), - request.getTags() + tempFile.getAbsolutePath(), + author, + request.getName(), + request.getDescription(), + request.getTags() ); log.info("解析出skill 压缩包内容:{}", yamlContent); @@ -645,7 +620,7 @@ public class SkillGenServiceImpl implements SkillGenService { } long finalL = l; List list = tags.stream().filter(tag -> tag.getTagId() == finalL).toList(); - if (CollectionUtil.isNotEmpty(list)) { + if (CollectionUtil.isNotEmpty(list)){ cmsContent.setIcon(list.get(0).getIcon()); } } @@ -653,7 +628,7 @@ public class SkillGenServiceImpl implements SkillGenService { // cmsContentMapper.insert(cmsContent); // 删除临时文件 tempFile.delete(); - + return cmsContent; } catch (Exception e) { log.error("上传本地技能压缩包失败: {}", e.getMessage(), e); @@ -667,7 +642,7 @@ public class SkillGenServiceImpl implements SkillGenService { try { Map yamlMap = YamlToMapUtil.yamlTextToMap(yamlContent); - Map packageMap = (Map) yamlMap.get("package"); + Map packageMap = (Map)yamlMap.get("package"); String skillName = (String) packageMap.get("name"); String skillDescription = (String) packageMap.get("description"); List tagList = (List) packageMap.get("tags"); @@ -680,11 +655,11 @@ public class SkillGenServiceImpl implements SkillGenService { request.setIntroduce(genIntroduceByDescription(skillDescription)); return getCmsContent(request, yamlContent, StpUtil.getLoginIdAsLong(), skillIcon); } catch (YAMLException e) { - throw new BizException("yaml解析失败:" + e.getMessage()); + throw new BizException("yaml解析失败:"+ e.getMessage()); } } - private String getDefaultIcon(String tagId) { + private String getDefaultIcon(String tagId){ CmsTagDto tagDto = new CmsTagDto(); tagDto.setDeleteFlag(0); tagDto.setStatus(1); @@ -693,8 +668,8 @@ public class SkillGenServiceImpl implements SkillGenService { String defaultIcon = ""; for (int i = 0; i < tagList.size(); i++) { CmsTag tag = tagList.get(i); - if (tagId.contains(tag.getTagId() + "")) { - if (StringUtil.isEmpty(defaultIcon)) { + if (tagId.contains(tag.getTagId()+"")) { + if (StringUtil.isEmpty(defaultIcon)){ defaultIcon = tag.getIcon(); break; } @@ -703,14 +678,14 @@ public class SkillGenServiceImpl implements SkillGenService { return defaultIcon; } - public SkillGenRequest parseSkillMdText(String skillMdText, List tags) { + public SkillGenRequest parseSkillMdText(String skillMdText,List tags) { String url = deepSeekConfig.getBaseUrl() + "/v1/chat/completions"; // 将标签名称拼接成逗号分隔的字符串 StringBuilder tagsList = new StringBuilder(); for (int i = 0; i < tags.size(); i++) { CmsTag tag = tags.get(i); - tagsList.append(tag.getTagId() + "." + tag.getTagName()); + tagsList.append(tag.getTagId()+"."+tag.getTagName()); if (i < tags.size() - 1) { tagsList.append(","); } @@ -767,7 +742,7 @@ public class SkillGenServiceImpl implements SkillGenService { } } catch (Exception e) { log.error("调用Deepseek API失败: {}", e.getMessage(), e); - throw new BizException("调用Deepseek API失败:" + e.getMessage() + " Deepseek API响应:" + deepseekResponse); + throw new BizException("调用Deepseek API失败:"+ e.getMessage()+" Deepseek API响应:"+ deepseekResponse); } return null; @@ -776,627 +751,6 @@ public class SkillGenServiceImpl implements SkillGenService { @Override public CmsContent uploadSkillV4(byte[] fileBytes, String fileName) { - log.info("上传技能压缩包V4请求: {}", fileName); - - try { - // 1. 从文件名提取技能名称 - String skillName = extractSkillName(fileName); - - // 2. 解析压缩包,构建目录树结构 - List structureNodes = parseArchiveToStructure(fileBytes, fileName); - - // 3. 从SKILL.md或README.md中提取描述信息(如果存在) - String description = extractDescriptionFromStructure(structureNodes); - if (description == null || description.isEmpty()) { - description = "AI生成的技能包"; - } - - // 4. 提取标签(如果没有则使用默认标签) - List tags = extractTagsFromStructure(structureNodes); - if (tags == null || tags.isEmpty()) { - tags = Arrays.asList("通用"); - } - - // 5. 构建packageInfo - SkillPackageInfoDto packageInfo = new SkillPackageInfoDto(); - packageInfo.setName(skillName); - packageInfo.setVersion("1.0.0"); - packageInfo.setDescription(description); - packageInfo.setAuthor("AI技能生成助手"); - packageInfo.setCreated(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))); - packageInfo.setTags(tags); - packageInfo.setStructure(structureNodes); - - // 6. 生成YAML内容 - String yamlContent = generateYamlContent(packageInfo); - log.info("生成的YAML内容:\n{}", yamlContent); - - // 7. 构建SkillGenRequest - SkillGenRequest request = new SkillGenRequest(); - request.setName(skillName); - request.setDescription(description); - // request.setIntroduce(genIntroduceByDescription(description)); - request.setTags(tags); - - // 8. 生成CmsContent - CmsContent cmsContent = getCmsContent(request, yamlContent, StpUtil.getLoginIdAsLong(), ""); - - // 9. 设置图标 - setSkillIcon(cmsContent, tags); - - cmsContentMapper.insert(cmsContent); - - return cmsContent; - } catch (Exception e) { - log.error("上传技能压缩包V4失败: {}", e.getMessage(), e); - throw new BizException("上传技能压缩包V4失败:" + e.getMessage()); - } - } - - /** - * 解析压缩包为目录树结构 - */ - private List parseArchiveToStructure(byte[] fileBytes, String fileName) { - Map fileMap = new HashMap<>(); - - // 解压文件 - if (fileName.toLowerCase().endsWith(".zip")) { - extractZipFiles(fileBytes, fileMap); - } else if (fileName.toLowerCase().endsWith(".rar")) { - extractRarFiles(fileBytes, fileMap); - } else { - throw new BizException("不支持的压缩包格式,仅支持 zip 或 rar"); - } - - // 构建目录树 - return buildDirectoryTree(fileMap); - } - - /** - * 构建目录树结构 - */ - private List buildDirectoryTree(Map fileMap) { - log.info("开始构建目录树,文件数量: {}", fileMap.size()); - - // 收集所有唯一的目录路径 - Set directorySet = new TreeSet<>(); - for (String filePath : fileMap.keySet()) { - String normalizedPath = filePath.replace("\\", "/"); - // 从文件路径提取所有父目录 - int lastSlashIndex = normalizedPath.lastIndexOf("/"); - while (lastSlashIndex > 0) { - String parentDir = normalizedPath.substring(0, lastSlashIndex); - directorySet.add(parentDir); - lastSlashIndex = parentDir.lastIndexOf("/"); - } - } - - log.info("检测到目录: {}", directorySet); - - // 创建节点映射:path -> node - Map pathToNodeMap = new HashMap<>(); - - // 先创建所有目录节点 - for (String dirPath : directorySet) { - String[] parts = dirPath.split("/"); - String dirName = parts[parts.length - 1]; - String parentPath = dirPath.contains("/") ? dirPath.substring(0, dirPath.lastIndexOf("/")) : ""; - - SkillStructureNodeDto dirNode = new SkillStructureNodeDto(); - dirNode.setName(dirName); - dirNode.setType("directory"); - dirNode.setPath(parentPath.isEmpty() ? "/" : parentPath); - dirNode.setFormat("dir"); - dirNode.setDescription(dirName + " 目录"); - dirNode.setChildren(new ArrayList<>()); - - pathToNodeMap.put(dirPath, dirNode); - log.debug("创建目录节点: name={}, path={}", dirName, dirNode.getPath()); - } - - // 将目录节点添加到父节点 - List rootChildren = new ArrayList<>(); - for (Map.Entry entry : pathToNodeMap.entrySet()) { - String dirPath = entry.getKey(); - SkillStructureNodeDto node = entry.getValue(); - - String parentPath = dirPath.contains("/") ? dirPath.substring(0, dirPath.lastIndexOf("/")) : ""; - if (parentPath.isEmpty()) { - // 一级目录,直接添加到根节点 - rootChildren.add(node); - } else { - // 找到父目录并添加 - SkillStructureNodeDto parentNode = pathToNodeMap.get(parentPath); - if (parentNode != null && parentNode.getChildren() != null) { - parentNode.getChildren().add(node); - } else { - log.warn("找不到父目录: {}, 将{}添加到根节点", parentPath, dirPath); - rootChildren.add(node); - } - } - } - - // 再处理所有文件 - for (Map.Entry entry : fileMap.entrySet()) { - String filePath = entry.getKey().replace("\\", "/"); - if (filePath.endsWith("/")) { - continue; // 跳过目录条目 - } - - int lastSlashIndex = filePath.lastIndexOf("/"); - String fileName = lastSlashIndex >= 0 ? filePath.substring(lastSlashIndex + 1) : filePath; - String parentPath = lastSlashIndex >= 0 ? filePath.substring(0, lastSlashIndex) : ""; - - SkillStructureNodeDto fileNode = new SkillStructureNodeDto(); - fileNode.setName(fileName); - fileNode.setType("file"); - fileNode.setPath(parentPath.isEmpty() ? "/" : parentPath); - fileNode.setFormat(getFileFormat(fileName)); - fileNode.setDescription(fileName + " 文件"); - - // 读取文件内容(仅文本文件,图片等二进制文件不读取内容) - String fileFormat = getFileFormat(fileName); - boolean isBinaryFile = isBinaryFormat(fileFormat); - - if (!isBinaryFile) { - // 文本文件,读取内容 - try { - String content = new String(entry.getValue(), StandardCharsets.UTF_8); - fileNode.setContent(content); - } catch (Exception e) { - log.warn("读取文件内容失败: {}", filePath, e); - fileNode.setContent(""); - } - } else { - // 二进制文件(如图片),不读取内容,仅记录文件大小信息 - int fileSize = entry.getValue().length; - fileNode.setContent(""); - log.debug("二进制文件: {} ({} bytes)", fileName, fileSize); - } - - // 添加到父节点 - if (parentPath.isEmpty()) { - // 根目录下的文件 - rootChildren.add(fileNode); - } else { - SkillStructureNodeDto parentNode = pathToNodeMap.get(parentPath); - if (parentNode != null && parentNode.getChildren() != null) { - parentNode.getChildren().add(fileNode); - } else { - log.warn("找不到文件的父目录: {}, 将{}添加到根节点", parentPath, filePath); - rootChildren.add(fileNode); - } - } - - log.debug("创建文件节点: name={}, path={}", fileName, fileNode.getPath()); - } - - // 删除:不再强制生成skills.md,保持压缩包原有结构 - // 之前的代码:ensureSkillsMdExists(rootChildren); - - // 如果只有一个根目录节点,将其children提升到根层级(去掉最外层技能包目录) - if (rootChildren.size() == 1 && "directory".equals(rootChildren.get(0).getType())) { - SkillStructureNodeDto rootDir = rootChildren.get(0); - String removedDirName = rootDir.getName(); - log.info("检测到单个根目录节点: {},将其children提升到根层级", removedDirName); - - // 获取其children作为新的根节点 - List newRootChildren = rootDir.getChildren(); - if (newRootChildren != null && !newRootChildren.isEmpty()) { - rootChildren = newRootChildren; - - // 递归更新所有节点的path,去除被移除的目录名前缀 - updatePathsAfterFlattening(rootChildren, removedDirName); - } - } - - log.info("目录树构建完成,根节点数量: {}", rootChildren.size()); - for (SkillStructureNodeDto node : rootChildren) { - log.info(" 根节点: {} ({})", node.getName(), node.getType()); - } - return rootChildren; - } - - /** - * 扁平化后递归更新所有节点的path - */ - private void updatePathsAfterFlattening(List nodes, String removedDirPrefix) { - if (nodes == null || nodes.isEmpty()) { - return; - } - - for (SkillStructureNodeDto node : nodes) { - String oldPath = node.getPath(); - String newPath; - - // 如果path是被移除的目录名,改为 "/" - if (removedDirPrefix.equals(oldPath)) { - newPath = "/"; - } else if (oldPath.startsWith(removedDirPrefix + "/")) { - // 如果被移除目录名是前缀,去掉它 - newPath = "/" + oldPath.substring(removedDirPrefix.length() + 1); - } else { - // 其他情况保持不变 - newPath = oldPath; - } - - node.setPath(newPath); - - // 递归处理子节点 - if ("directory".equals(node.getType()) && node.getChildren() != null) { - updatePathsAfterFlattening(node.getChildren(), removedDirPrefix); - } - } - } - - /** - * 确保根目录下有skills.md文件 - */ - private void ensureSkillsMdExists(List rootChildren) { - boolean hasSkillsMd = false; - for (SkillStructureNodeDto node : rootChildren) { - if ("skills.md".equals(node.getName())) { - hasSkillsMd = true; - break; - } - } - - if (!hasSkillsMd) { - SkillStructureNodeDto skillsMd = new SkillStructureNodeDto(); - skillsMd.setName("skills.md"); - skillsMd.setType("file"); - skillsMd.setPath("/"); - skillsMd.setFormat("markdown"); - skillsMd.setDescription("技能说明文档"); - skillsMd.setContent("# 技能说明\n\n请在此处填写技能的详细说明。\n"); - rootChildren.add(skillsMd); - } - } - - /** - * 从目录结构中提取描述 - */ - private String extractDescriptionFromStructure(List structureNodes) { - // 查找SKILL.md或README.md文件 - for (SkillStructureNodeDto node : structureNodes) { - if ("file".equals(node.getType())) { - String lowerName = node.getName().toLowerCase(); - if ((lowerName.equals("skill.md") || lowerName.equals("readme.md")) && node.getContent() != null) { - // 提取前200个字符作为描述 - String content = node.getContent(); - if (content.length() > 200) { - return content.substring(0, 200); - } - return content; - } - } else if ("directory".equals(node.getType()) && node.getChildren() != null) { - // 递归查找子目录 - String desc = extractDescriptionFromStructure(node.getChildren()); - if (desc != null && !desc.isEmpty()) { - return desc; - } - } - } return null; } - - /** - * 从目录结构中提取标签 - */ - private List extractTagsFromStructure(List structureNodes) { - // 这里可以解析SKILL.md中的front matter提取标签 - // 暂时返回空列表,由调用方设置默认值 - return new ArrayList<>(); - } - - /** - * 生成YAML内容 - */ - private String generateYamlContent(SkillPackageInfoDto packageInfo) { - StringBuilder yaml = new StringBuilder(); - yaml.append("package:\n"); - yaml.append(" name: ").append(quoteIfNecessary(packageInfo.getName())).append("\n"); - yaml.append(" version: ").append(quoteIfNecessary(packageInfo.getVersion())).append("\n"); - - // description可能包含特殊字符,使用块标量 - String description = packageInfo.getDescription(); - if (description != null && !description.isEmpty()) { - yaml.append(" description: |\n"); - String[] lines = description.split("\n"); - for (String line : lines) { - yaml.append(" ").append(line).append("\n"); - } - } else { - yaml.append(" description: \"\"\n"); - } - - yaml.append(" author: ").append(quoteIfNecessary(packageInfo.getAuthor())).append("\n"); - yaml.append(" created: ").append(quoteIfNecessary(packageInfo.getCreated())).append("\n"); - - // tags数组 - yaml.append(" tags:\n"); - if (packageInfo.getTags() != null) { - for (String tag : packageInfo.getTags()) { - yaml.append(" - ").append(quoteIfNecessary(tag)).append("\n"); - } - } - - // structure目录树 - yaml.append(" structure:\n"); - if (packageInfo.getStructure() != null) { - appendStructureNodes(yaml, packageInfo.getStructure(), 2); - } - - return yaml.toString(); - } - - /** - * 判断是否需要引号,需要则加双引号 - */ - private String quoteIfNecessary(String value) { - if (value == null) { - return "\"\""; - } - // 如果包含特殊字符或可能产生歧义,加双引号 - if (value.contains(":") || value.contains("#") || value.contains("{") || - value.contains("}") || value.contains("[") || value.contains("]") || - value.contains(",") || value.contains("&") || value.contains("*") || - value.contains("?") || value.contains("|") || value.contains("-") || - value.contains("<") || value.contains(">") || value.contains("=") || - value.contains("!") || value.contains("%") || value.contains("@") || - value.contains("`") || value.startsWith("'") || value.startsWith(" ") || - value.endsWith(" ")) { - return "\"" + value.replace("\\", "\\\\").replace("\"", "\\\"") + "\""; - } - return value; - } - - /** - * 递归追加structure节点到YAML - */ - private void appendStructureNodes(StringBuilder yaml, List nodes, int indentLevel) { - if (nodes == null || nodes.isEmpty()) { - return; - } - - String indent = " ".repeat(indentLevel); - - for (SkillStructureNodeDto node : nodes) { - yaml.append(indent).append("- name: ").append(node.getName()).append("\n"); - yaml.append(indent).append(" type: ").append(node.getType()).append("\n"); - yaml.append(indent).append(" path: ").append(node.getPath()).append("\n"); - yaml.append(indent).append(" format: ").append(node.getFormat()).append("\n"); - - if ("file".equals(node.getType())) { - // 文件类型,添加content - if (node.getContent() != null && !node.getContent().isEmpty()) { - String rawContent = node.getContent(); - - // 如果是图片格式,Base64字符串需要按行分割(每行80字符) - if ("image".equals(node.getFormat())) { - int lineLength = 80; - StringBuilder base64Lines = new StringBuilder(); - for (int i = 0; i < rawContent.length(); i += lineLength) { - if (i > 0) { - base64Lines.append("\n"); - } - int end = Math.min(i + lineLength, rawContent.length()); - base64Lines.append(rawContent.substring(i, end)); - } - rawContent = base64Lines.toString(); - } - - // 将Tab替换为4个空格(YAML不允许Tab) - rawContent = rawContent.replace("\t", " "); - - // 分割成行 - String[] lines = rawContent.split("\n", -1); - - // 去除末尾的空行 - int lastNonEmpty = lines.length - 1; - while (lastNonEmpty >= 0 && lines[lastNonEmpty].trim().isEmpty()) { - lastNonEmpty--; - } - - if (lastNonEmpty >= 0) { - yaml.append(indent).append(" content: |"); - // 如果原内容有尾随换行,保留一个 - if (lastNonEmpty < lines.length - 1) { - yaml.append("\n"); - } else { - yaml.append("-\n"); // |- 表示去掉末尾换行 - } - - // 输出内容行,每行添加统一的缩进 - for (int i = 0; i <= lastNonEmpty; i++) { - yaml.append(indent).append(" ").append(lines[i]).append("\n"); - } - } - } - } else if ("directory".equals(node.getType())) { - // 目录类型,添加children - if (node.getChildren() != null && !node.getChildren().isEmpty()) { - yaml.append(indent).append(" children:\n"); - appendStructureNodes(yaml, node.getChildren(), indentLevel + 1); - } else { - yaml.append(indent).append(" children: []\n"); - } - } - } - } - - /** - * 转义YAML字符串中的特殊字符 - */ - private String escapeYamlString(String str) { - if (str == null) { - return ""; - } - // 转义双引号、反斜杠等特殊字符 - return str.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n"); - } - - /** - * 获取文件格式 - */ - private String getFileFormat(String fileName) { - if (fileName == null) { - return "unknown"; - } - String lowerName = fileName.toLowerCase(); - if (lowerName.endsWith(".md")) { - return "markdown"; - } else if (lowerName.endsWith(".py")) { - return "python"; - } else if (lowerName.endsWith(".txt")) { - return "text"; - } else if (lowerName.endsWith(".json")) { - return "json"; - } else if (lowerName.endsWith(".yaml") || lowerName.endsWith(".yml")) { - return "yaml"; - } else if (lowerName.endsWith(".js") || lowerName.endsWith(".ts")) { - return "javascript"; - } else if (lowerName.endsWith(".html")) { - return "html"; - } else if (lowerName.endsWith(".css")) { - return "css"; - } else if (lowerName.endsWith(".xml")) { - return "xml"; - } else if (lowerName.endsWith(".csv")) { - return "csv"; - } else if (lowerName.endsWith(".png") || lowerName.endsWith(".jpg") || lowerName.endsWith(".jpeg") || - lowerName.endsWith(".gif") || lowerName.endsWith(".bmp") || lowerName.endsWith(".webp") || - lowerName.endsWith(".svg") || lowerName.endsWith(".ico")) { - return "image"; - } else if (lowerName.endsWith(".pdf")) { - return "pdf"; - } else if (lowerName.endsWith(".zip") || lowerName.endsWith(".rar") || lowerName.endsWith(".7z")) { - return "archive"; - } else if (lowerName.endsWith(".mp4") || lowerName.endsWith(".avi") || lowerName.endsWith(".mov")) { - return "video"; - } else if (lowerName.endsWith(".mp3") || lowerName.endsWith(".wav") || lowerName.endsWith(".ogg")) { - return "audio"; - } - return "unknown"; - } - - /** - * 判断是否为二进制文件格式(不需要读取文本内容) - * 注意:image 类型不在此列,因为图片需要转Base64 - */ - private boolean isBinaryFormat(String format) { - if (format == null) { - return false; - } - return "pdf".equals(format) || "archive".equals(format) || - "video".equals(format) || "audio".equals(format) || "unknown".equals(format); - } - - /** - * 设置技能图标 - */ - private void setSkillIcon(CmsContent cmsContent, List tags) { - if (tags == null || tags.isEmpty()) { - return; - } - - try { - String firstTagId = tags.get(0); - CmsTagDto tagDto = new CmsTagDto(); - tagDto.setDeleteFlag(0); - tagDto.setStatus(1); - List tagList = cmsTagService.getList(tagDto); - - for (CmsTag tag : tagList) { - if (firstTagId.contains(tag.getTagId() + "")) { - cmsContent.setIcon(tag.getIcon()); - break; - } - } - } catch (Exception e) { - log.warn("设置技能图标失败: {}", e.getMessage()); - } - } - - /** - * 解压 ZIP 文件 - */ - private void extractZipFiles(byte[] fileBytes, Map fileMap) { - try (ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(fileBytes))) { - ZipEntry entry; - while ((entry = zis.getNextEntry()) != null) { - if (!entry.isDirectory()) { - String entryName = entry.getName(); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - byte[] buffer = new byte[1024]; - int len; - while ((len = zis.read(buffer)) > 0) { - baos.write(buffer, 0, len); - } - fileMap.put(entryName, baos.toByteArray()); - } - zis.closeEntry(); - } - } catch (Exception e) { - log.error("解压 ZIP 文件失败: {}", e.getMessage(), e); - throw new BizException("解压 ZIP 文件失败:" + e.getMessage()); - } - } - - /** - * 解压 RAR 文件 - */ - private void extractRarFiles(byte[] fileBytes, Map fileMap) { - File tempFile = null; - try { - // 创建临时文件 - tempFile = File.createTempFile("skill_rar_", ".rar"); - try (FileOutputStream fos = new FileOutputStream(tempFile)) { - fos.write(fileBytes); - } - - // 使用 SevenZipJBinding 解压 RAR - RandomAccessFile randomAccessFile = new RandomAccessFile(tempFile, "r"); - IInArchive archive = SevenZip.openInArchive(null, new RandomAccessFileInStream(randomAccessFile)); - - int itemCount = archive.getNumberOfItems(); - for (int i = 0; i < itemCount; i++) { - Object isFolder = archive.getProperty(i, net.sf.sevenzipjbinding.PropID.IS_FOLDER); - if (!(isFolder instanceof Boolean) || !((Boolean) isFolder)) { - String path = (String) archive.getProperty(i, net.sf.sevenzipjbinding.PropID.PATH); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - archive.extractSlow(i, data -> { - try { - baos.write(data); - return data.length; - } catch (Exception e) { - throw new RuntimeException(e); - } - }); - fileMap.put(path, baos.toByteArray()); - } - } - - archive.close(); - randomAccessFile.close(); - } catch (Exception e) { - log.error("解压 RAR 文件失败: {}", e.getMessage(), e); - throw new BizException("解压 RAR 文件失败:" + e.getMessage()); - } finally { - if (tempFile != null && tempFile.exists()) { - tempFile.delete(); - } - } - } - - /** - * 从文件名提取技能名称 - */ - private String extractSkillName(String fileName) { - if (fileName.contains(".")) { - int dotIndex = fileName.lastIndexOf("."); - return fileName.substring(0, dotIndex); - } - return fileName; - } -} \ No newline at end of file +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index f9fbc2b..2a8738c 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -94,3 +94,7 @@ web: upload: path: /kexue/agentSkills/upload/ +# 雪花算法配置 +snowflake: + workid: 1 # 机器ID,分布式部署时需要保证唯一 + diff --git a/src/main/resources/mapper/PackageConfigMapper.xml b/src/main/resources/mapper/PackageConfigMapper.xml new file mode 100644 index 0000000..2d91f74 --- /dev/null +++ b/src/main/resources/mapper/PackageConfigMapper.xml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + insert into package_config + + name, + price, + base_amount, + gift_amount, + create_time, + update_time, + + + #{name}, + #{price}, + #{baseAmount}, + #{giftAmount}, + #{createTime}, + #{updateTime}, + + + + + + update package_config + + name = #{name}, + price = #{price}, + base_amount = #{baseAmount}, + gift_amount = #{giftAmount}, + update_time = #{updateTime}, + + where id = #{id} + + + + + delete from package_config + where id = #{id} + + +