feat(account): 实现账户积分系统及套餐功能
- 将账户余额系统改造为积分系统,充值金额按1元=100积分计算 - 新增套餐配置功能,支持套餐购买并获取基础额度和赠送额度 - 在账户冻结功能中集成模型价格计算,根据预估tokens自动计算冻结金额 - 更新支付流程以支持套餐ID关联和积分计算 - 修改全局异常处理器返回格式,统一使用CommonResult - 优化账户交易记录的备注信息显示 - 添加雪花算法配置用于分布式ID生成 - 扩展账户冻结DTO添加预估tokens字段 - 重构账户服务中的金额处理逻辑为积分处理逻辑 - 实现套餐配置的CRUD操作接口和相关实体类 - 更新支付回调逻辑以正确处理套餐购买场景
This commit is contained in:
parent
51fce1ece6
commit
e651e73fa2
|
|
@ -47,6 +47,15 @@ public class CommonResult<T> {
|
|||
/**
|
||||
* 成功返回结果
|
||||
*
|
||||
* @param data 获取的数据
|
||||
*/
|
||||
public static <T> CommonResult<T> success(IErrorCode errorCode,T data) {
|
||||
return new CommonResult<T>(errorCode.getCode(), errorCode.getMessage(), data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 成功返回结果
|
||||
*
|
||||
* @param errorCode 获取的编码
|
||||
* @param data 获取的数据
|
||||
* @param message 提示信息
|
||||
|
|
@ -58,11 +67,11 @@ public class CommonResult<T> {
|
|||
/**
|
||||
* 成功返回结果
|
||||
*
|
||||
* @param errorCode 获取的数据
|
||||
* @param code 获取的数据
|
||||
* @param message 提示信息
|
||||
*/
|
||||
public static <T> CommonResult<T> success(IErrorCode errorCode, String message ) {
|
||||
return new CommonResult<T>(errorCode.getCode(), message,null);
|
||||
public static <T> CommonResult<T> success(Long code, String message ) {
|
||||
return new CommonResult<T>(code, message,null);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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<AccountFrozen> createFrozen(@RequestBody AccountFrozenDto accountFrozenDto) {
|
||||
public CommonResult<AccountFrozen> createFrozen(@RequestBody AccountFrozenDto accountFrozenDto) {
|
||||
try {
|
||||
logger.info("创建冻结单入参: {}", objectMapper.writeValueAsString(accountFrozenDto));
|
||||
} catch (IOException e) {
|
||||
logger.error("创建冻结单入参序列化失败", e);
|
||||
}
|
||||
AccountFrozen accountFrozen = accountFrozenService.createFrozen(accountFrozenDto);
|
||||
return new Result<AccountFrozen>().ok().data(accountFrozen);
|
||||
return CommonResult.success(accountFrozen);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -47,9 +60,14 @@ public class AccountFrozenController {
|
|||
*/
|
||||
@PostMapping("/release")
|
||||
@Operation(summary = "释放冻结单", description = "释放账户冻结单")
|
||||
public Result<AccountFrozen> releaseFrozen(@RequestBody AccountReleaseDto accountReleaseDto) {
|
||||
public CommonResult<AccountFrozen> releaseFrozen(@RequestBody AccountReleaseDto accountReleaseDto) {
|
||||
try {
|
||||
logger.info("释放冻结单入参: {}", objectMapper.writeValueAsString(accountReleaseDto));
|
||||
} catch (IOException e) {
|
||||
logger.error("释放冻结单入参序列化失败", e);
|
||||
}
|
||||
AccountFrozen accountFrozen = accountFrozenService.releaseFrozen(accountReleaseDto);
|
||||
return new Result<AccountFrozen>().ok().data(accountFrozen);
|
||||
return CommonResult.success(accountFrozen);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<PageInfo<PackageConfig>> getPageList(@RequestBody PackageConfigDto queryDto) {
|
||||
return CommonResult.success(packageConfigService.getPageList(queryDto));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询列表
|
||||
*
|
||||
* @param queryDto 筛选条件
|
||||
* @return 查询结果
|
||||
*/
|
||||
@PostMapping("/getList")
|
||||
@Operation(summary = "查询列表", description = "查询列表")
|
||||
public CommonResult<PageInfo<PackageConfig>> 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<PackageConfig> queryById(@PathVariable("id") Long id) {
|
||||
return CommonResult.success(packageConfigService.queryById(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增数据
|
||||
*
|
||||
* @param packageConfig 实体
|
||||
* @return 新增结果
|
||||
*/
|
||||
@PostMapping("/insert")
|
||||
@Operation(summary = "新增套餐", description = "新增套餐")
|
||||
@RequireAuth
|
||||
public CommonResult<PackageConfig> insert(@RequestBody PackageConfig packageConfig) {
|
||||
return CommonResult.success(packageConfigService.insert(packageConfig));
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑数据
|
||||
*
|
||||
* @param packageConfig 实体
|
||||
* @return 编辑结果
|
||||
*/
|
||||
@PostMapping("/update")
|
||||
@Operation(summary = "更新套餐", description = "更新套餐")
|
||||
@RequireAuth
|
||||
public CommonResult<PackageConfig> update(@RequestBody PackageConfig packageConfig) {
|
||||
return CommonResult.success(packageConfigService.update(packageConfig));
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过主键逻辑删除
|
||||
*
|
||||
* @param idDto 主键
|
||||
* @return 删除数据
|
||||
*/
|
||||
@PostMapping("/logicDeleteById")
|
||||
@Operation(summary = "逻辑删除套餐", description = "逻辑删除套餐")
|
||||
@RequireAuth
|
||||
public CommonResult<Boolean> 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<Boolean> deleteById(@PathVariable("id") Long id) {
|
||||
return CommonResult.success(packageConfigService.deleteById(id) > 0);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
}
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
}
|
||||
|
|
@ -30,4 +30,6 @@ public class PaymentOrderDto extends BaseQueryDto {
|
|||
|
||||
private Integer deleteFlag;
|
||||
|
||||
private Long packageId;
|
||||
|
||||
}
|
||||
|
|
@ -15,9 +15,9 @@ public class GlobalExceptionHandler {
|
|||
@ExceptionHandler(BizException.class)
|
||||
public CommonResult<String> handleBizException(BizException e) {
|
||||
if (e == null) {
|
||||
return CommonResult.failed("未知错误");
|
||||
return CommonResult.success("未知错误");
|
||||
}
|
||||
return CommonResult.failed(e.getMessage());
|
||||
return CommonResult.success(e.getErrorCode(), e.getMessage());
|
||||
}
|
||||
|
||||
// 其他异常处理...
|
||||
|
|
|
|||
|
|
@ -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<PackageConfig> getPageList(PackageConfigDto queryDto);
|
||||
|
||||
/**
|
||||
* 查询列表
|
||||
*
|
||||
* @param queryDto 筛选条件
|
||||
* @return 查询结果
|
||||
*/
|
||||
List<PackageConfig> 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);
|
||||
|
||||
}
|
||||
|
|
@ -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<PackageConfig> getPageList(PackageConfigDto queryDto);
|
||||
|
||||
/**
|
||||
* 查询列表
|
||||
*
|
||||
* @param queryDto 筛选条件
|
||||
* @return 查询结果
|
||||
*/
|
||||
List<PackageConfig> 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);
|
||||
|
||||
}
|
||||
|
|
@ -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,11 +218,33 @@ public class AccountFrozenServiceImpl implements AccountFrozenService {
|
|||
|
||||
// 如果需要扣减余额
|
||||
if (finalAmount.compareTo(BigDecimal.ZERO) > 0) {
|
||||
// 检查实际扣减是否大于预扣减
|
||||
if (finalAmount.compareTo(accountFrozen.getFrozenAmount()) > 0) {
|
||||
// 实际扣减大于预扣减,根据实际扣减进行扣除
|
||||
// 如果余额不够实际扣减,将balance设置为0
|
||||
if (balance.compareTo(finalAmount) < 0) {
|
||||
throw new BizException(ResultCode.INSUFFICIENT_BALANCE.getCode(), ResultCode.INSUFFICIENT_BALANCE.getMessage());
|
||||
}
|
||||
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));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 最终扣减为0,将预扣减全部加回balance
|
||||
account.setBalance(balance.add(accountFrozen.getFrozenAmount()));
|
||||
}
|
||||
|
||||
account.setUpdateTime(new Date());
|
||||
accountMapper.update(account);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<PackageConfig> getPageList(PackageConfigDto queryDto) {
|
||||
PageHelper.startPage(queryDto.getPageNum(), queryDto.getPageSize());
|
||||
List<PackageConfig> list = packageConfigMapper.getList(queryDto);
|
||||
return new PageInfo<>(list);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PackageConfig> 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
// 继续处理,不影响回调响应
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
/**
|
||||
* 技能生成服务实现
|
||||
|
|
@ -105,7 +80,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(",");
|
||||
}
|
||||
|
|
@ -143,7 +118,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;
|
||||
|
|
@ -168,7 +143,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(",");
|
||||
}
|
||||
|
|
@ -218,7 +193,7 @@ public class SkillGenServiceImpl implements SkillGenService {
|
|||
}
|
||||
} 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());
|
||||
|
|
@ -349,7 +324,7 @@ public class SkillGenServiceImpl implements SkillGenService {
|
|||
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<CmsTag> 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());
|
||||
|
|
@ -438,11 +413,11 @@ public class SkillGenServiceImpl implements SkillGenService {
|
|||
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;
|
||||
|
|
@ -468,11 +443,11 @@ public class SkillGenServiceImpl implements SkillGenService {
|
|||
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(",");
|
||||
}
|
||||
|
|
@ -556,7 +531,7 @@ public class SkillGenServiceImpl implements SkillGenService {
|
|||
// 生成CmsContent对象
|
||||
CmsContent cmsContent = getCmsContent(request, skillJson.getString("content"), StpUtil.getLoginIdAsLong(), "");
|
||||
List<CmsTag> 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,7 +540,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;
|
||||
|
|
@ -645,7 +620,7 @@ public class SkillGenServiceImpl implements SkillGenService {
|
|||
}
|
||||
long finalL = l;
|
||||
List<CmsTag> list = tags.stream().filter(tag -> tag.getTagId() == finalL).toList();
|
||||
if (CollectionUtil.isNotEmpty(list)) {
|
||||
if (CollectionUtil.isNotEmpty(list)){
|
||||
cmsContent.setIcon(list.get(0).getIcon());
|
||||
}
|
||||
}
|
||||
|
|
@ -667,7 +642,7 @@ public class SkillGenServiceImpl implements SkillGenService {
|
|||
try {
|
||||
Map<String, Object> yamlMap = YamlToMapUtil.yamlTextToMap(yamlContent);
|
||||
|
||||
Map<String, Object> packageMap = (Map<String, Object>) yamlMap.get("package");
|
||||
Map<String, Object> packageMap = (Map<String, Object>)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<CmsTag> tags) {
|
||||
public SkillGenRequest parseSkillMdText(String skillMdText,List<CmsTag> 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<SkillStructureNodeDto> structureNodes = parseArchiveToStructure(fileBytes, fileName);
|
||||
|
||||
// 3. 从SKILL.md或README.md中提取描述信息(如果存在)
|
||||
String description = extractDescriptionFromStructure(structureNodes);
|
||||
if (description == null || description.isEmpty()) {
|
||||
description = "AI生成的技能包";
|
||||
}
|
||||
|
||||
// 4. 提取标签(如果没有则使用默认标签)
|
||||
List<String> 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<SkillStructureNodeDto> parseArchiveToStructure(byte[] fileBytes, String fileName) {
|
||||
Map<String, byte[]> 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<SkillStructureNodeDto> buildDirectoryTree(Map<String, byte[]> fileMap) {
|
||||
log.info("开始构建目录树,文件数量: {}", fileMap.size());
|
||||
|
||||
// 收集所有唯一的目录路径
|
||||
Set<String> 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<String, SkillStructureNodeDto> 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<SkillStructureNodeDto> rootChildren = new ArrayList<>();
|
||||
for (Map.Entry<String, SkillStructureNodeDto> 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<String, byte[]> 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<SkillStructureNodeDto> 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<SkillStructureNodeDto> 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<SkillStructureNodeDto> 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<SkillStructureNodeDto> 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<String> extractTagsFromStructure(List<SkillStructureNodeDto> 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<SkillStructureNodeDto> 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<String> tags) {
|
||||
if (tags == null || tags.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
String firstTagId = tags.get(0);
|
||||
CmsTagDto tagDto = new CmsTagDto();
|
||||
tagDto.setDeleteFlag(0);
|
||||
tagDto.setStatus(1);
|
||||
List<CmsTag> 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<String, byte[]> 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<String, byte[]> 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -94,3 +94,7 @@ web:
|
|||
upload:
|
||||
path: /kexue/agentSkills/upload/
|
||||
|
||||
# 雪花算法配置
|
||||
snowflake:
|
||||
workid: 1 # 机器ID,分布式部署时需要保证唯一
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,90 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.kexue.skills.mapper.PackageConfigMapper">
|
||||
|
||||
<resultMap type="com.kexue.skills.entity.PackageConfig" id="PackageConfigMap">
|
||||
<result property="id" column="id" jdbcType="BIGINT"/>
|
||||
<result property="name" column="name" jdbcType="VARCHAR"/>
|
||||
<result property="price" column="price" jdbcType="DECIMAL"/>
|
||||
<result property="baseAmount" column="base_amount" jdbcType="DECIMAL"/>
|
||||
<result property="giftAmount" column="gift_amount" jdbcType="DECIMAL"/>
|
||||
<result property="createTime" column="create_time" jdbcType="TIMESTAMP"/>
|
||||
<result property="updateTime" column="update_time" jdbcType="TIMESTAMP"/>
|
||||
</resultMap>
|
||||
|
||||
<!--查询单个-->
|
||||
<select id="queryById" resultMap="PackageConfigMap">
|
||||
select
|
||||
id, name, price, base_amount, gift_amount, create_time, update_time
|
||||
from package_config
|
||||
where id = #{id}
|
||||
</select>
|
||||
|
||||
<!--分页查询-->
|
||||
<select id="getPageList" resultMap="PackageConfigMap">
|
||||
select
|
||||
id, name, price, base_amount, gift_amount, create_time, update_time
|
||||
from package_config
|
||||
<where>
|
||||
<if test="name != null and name != ''">
|
||||
and name like concat('%', #{name}, '%')
|
||||
</if>
|
||||
</where>
|
||||
<if test="sortBy != null and sortBy != ''">
|
||||
order by ${sortBy} ${sortDesc ? 'desc' : 'asc'}
|
||||
</if>
|
||||
</select>
|
||||
|
||||
<!--查询列表-->
|
||||
<select id="getList" resultMap="PackageConfigMap">
|
||||
select
|
||||
id, name, price, base_amount, gift_amount, create_time, update_time
|
||||
from package_config
|
||||
<where>
|
||||
<if test="name != null and name != ''">
|
||||
and name like concat('%', #{name}, '%')
|
||||
</if>
|
||||
</where>
|
||||
</select>
|
||||
|
||||
<!--新增数据-->
|
||||
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
|
||||
insert into package_config
|
||||
<trim prefix="(" suffix=")" suffixOverrides=",">
|
||||
<if test="name != null">name,</if>
|
||||
<if test="price != null">price,</if>
|
||||
<if test="baseAmount != null">base_amount,</if>
|
||||
<if test="giftAmount != null">gift_amount,</if>
|
||||
<if test="createTime != null">create_time,</if>
|
||||
<if test="updateTime != null">update_time,</if>
|
||||
</trim>
|
||||
<trim prefix="values (" suffix=")" suffixOverrides=",">
|
||||
<if test="name != null">#{name},</if>
|
||||
<if test="price != null">#{price},</if>
|
||||
<if test="baseAmount != null">#{baseAmount},</if>
|
||||
<if test="giftAmount != null">#{giftAmount},</if>
|
||||
<if test="createTime != null">#{createTime},</if>
|
||||
<if test="updateTime != null">#{updateTime},</if>
|
||||
</trim>
|
||||
</insert>
|
||||
|
||||
<!--更新数据-->
|
||||
<update id="update">
|
||||
update package_config
|
||||
<set>
|
||||
<if test="name != null">name = #{name},</if>
|
||||
<if test="price != null">price = #{price},</if>
|
||||
<if test="baseAmount != null">base_amount = #{baseAmount},</if>
|
||||
<if test="giftAmount != null">gift_amount = #{giftAmount},</if>
|
||||
<if test="updateTime != null">update_time = #{updateTime},</if>
|
||||
</set>
|
||||
where id = #{id}
|
||||
</update>
|
||||
|
||||
<!--删除-->
|
||||
<delete id="deleteById">
|
||||
delete from package_config
|
||||
where id = #{id}
|
||||
</delete>
|
||||
|
||||
</mapper>
|
||||
Loading…
Reference in New Issue