Compare commits

..

No commits in common. "main" and "v1.0.260410" have entirely different histories.

68 changed files with 1111 additions and 4502 deletions

View File

@ -1,5 +0,0 @@
-- 为 account_frozen 表添加 question 字段
-- 用于记录用户的问题或需求
ALTER TABLE `account_frozen`
ADD COLUMN `question` text DEFAULT NULL COMMENT '对应回答的问题或需求' AFTER `model_name`;

View File

@ -1,9 +0,0 @@
-- 为 account_transaction 表添加 call_id 字段
-- 用于关联冻结单释放时的调用ID
ALTER TABLE `account_transaction`
ADD COLUMN `call_id` varchar(100) DEFAULT NULL COMMENT '调用ID关联冻结单' AFTER `business_type`;
-- 添加索引以提高查询性能
ALTER TABLE `account_transaction`
ADD INDEX `idx_call_id` (`call_id`);

View File

@ -1,46 +0,0 @@
-- 更新 sys_log 表结构以支持新的日志功能
-- 作者: 王志维
-- 创建时间: 2026-04-14
-- 备份旧数据(可选)
-- CREATE TABLE sys_log_backup AS SELECT * FROM sys_log;
-- 删除旧表(如果存在)
DROP TABLE IF EXISTS `sys_log`;
-- 创建新表
CREATE TABLE `sys_log` (
`log_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`trace_id` varchar(255) DEFAULT NULL COMMENT '链路ID',
`description` varchar(255) DEFAULT NULL COMMENT '日志描述',
`module` varchar(50) DEFAULT NULL COMMENT '所属模块',
`request_url` varchar(512) DEFAULT NULL COMMENT '请求URL',
`request_method` varchar(10) DEFAULT NULL COMMENT '请求方式',
`request_headers` text COMMENT '请求头',
`request_body` text COMMENT '请求体',
`status_code` int(11) DEFAULT NULL COMMENT '状态码',
`response_headers` text COMMENT '响应头',
`response_body` mediumtext COMMENT '响应体',
`time_taken` bigint(20) DEFAULT NULL COMMENT '耗时ms',
`ip` varchar(100) DEFAULT NULL COMMENT 'IP',
`address` varchar(255) DEFAULT NULL COMMENT 'IP归属地',
`browser` varchar(100) DEFAULT NULL COMMENT '浏览器',
`os` varchar(100) DEFAULT NULL COMMENT '操作系统',
`status` tinyint(1) DEFAULT '1' COMMENT '状态1成功2失败',
`error_msg` text COMMENT '错误信息',
`create_user` bigint(20) DEFAULT NULL COMMENT '创建人',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`delete_flag` tinyint(1) DEFAULT '0' COMMENT '是否删除 0 未删除1已删除',
`create_by` varchar(50) DEFAULT NULL COMMENT '创建人',
`update_by` varchar(50) DEFAULT NULL COMMENT '更新人',
PRIMARY KEY (`log_id`) USING BTREE,
KEY `idx_module` (`module`) USING BTREE COMMENT '模块查询优化',
KEY `idx_ip` (`ip`) USING BTREE COMMENT 'IP查询优化',
KEY `idx_create_time` (`create_time`) USING BTREE COMMENT '时间范围查询优化',
KEY `idx_status` (`status`) USING BTREE COMMENT '状态查询优化'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='系统操作日志表';
-- 插入测试数据(可选)
-- INSERT INTO `sys_log` (`description`, `module`, `request_url`, `request_method`, `status_code`, `time_taken`, `ip`, `status`, `create_time`)
-- VALUES ('用户登录', '登录认证', '/api/login/accountLogin', 'POST', 200, 150, '127.0.0.1', 1, NOW());

View File

@ -1,34 +0,0 @@
package com.kexue.skills.annotation;
import java.lang.annotation.*;
/**
* 操作日志注解
* 用于记录Controller层方法的请求和响应信息
*
* @author 王志维
* @since 2026-04-14
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {
/**
* 模块名称
* 例如用户管理登录认证内容管理等
*/
String module() default "";
/**
* 日志描述
* 如果不填写将根据方法签名自动生成
*/
String description() default "";
/**
* 是否忽略记录日志
* 默认false设置为true时不记录该接口的日志
*/
boolean ignore() default false;
}

View File

@ -44,15 +44,6 @@ public class CommonResult<T> {
return new CommonResult<T>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), data);
}
/**
* 成功返回结果
*
* @param data 获取的数据
*/
public static <T> CommonResult<T> success(IErrorCode errorCode,T data) {
return new CommonResult<T>(errorCode.getCode(), errorCode.getMessage(), data);
}
/**
* 成功返回结果
*
@ -67,11 +58,11 @@ public class CommonResult<T> {
/**
* 成功返回结果
*
* @param code 获取的数据
* @param errorCode 获取的数据
* @param message 提示信息
*/
public static <T> CommonResult<T> success(Long code, String message ) {
return new CommonResult<T>(code, message,null);
public static <T> CommonResult<T> success(IErrorCode errorCode, String message ) {
return new CommonResult<T>(errorCode.getCode(), message,null);
}
/**

View File

@ -92,19 +92,7 @@ public enum ResultCode implements IErrorCode {
/**
* 统一异常返回码
* */
EXCEPTION_HANDLER(-2500,"服务异常,请联系管理员"),
/**
* 账户冻结单相关错误
* */
PARAMETER_EMPTY(-1100, "参数不能为空"),
FROZEN_ID_EMPTY(-1101, "冻结单ID不能为空"),
FROZEN_NOT_EXIST(-1102, "冻结单不存在"),
FROZEN_STATUS_ERROR(-1103, "冻结单状态不正确,无法释放"),
SESSION_ID_NOT_EXIST(-1104, "会话ID不存在"),
ACCOUNT_NOT_EXIST(-1105, "用户账户不存在"),
INSUFFICIENT_BALANCE(-1106, "账户余额不足");
EXCEPTION_HANDLER(-2500,"服务异常,请联系管理员");
private final long code;
private final String message;

View File

@ -2,119 +2,22 @@ package com.kexue.skills.common.util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.UUID;
/**
* @description: ID管理类
**/
@Component
public class IDUtils {
public static final Logger logger = LoggerFactory.getLogger(IDUtils.class);
@Value("${snowflake.workid:1}")
private long workId;
private static SnowflakeIdGenerator snowflakeIdGenerator;
public static String getUUID(){
return UUID.randomUUID().toString().replace("-","");
}
@PostConstruct
public void init() {
snowflakeIdGenerator = new SnowflakeIdGenerator(workId);
logger.info("Snowflake ID generator initialized with workId: {}", workId);
}
public static long getSnowflakeId() {
if (snowflakeIdGenerator == null) {
// 如果未初始化使用默认workId
snowflakeIdGenerator = new SnowflakeIdGenerator(1L);
}
return snowflakeIdGenerator.nextId();
}
public static String getSnowflakeIdStr() {
if (snowflakeIdGenerator == null) {
// 如果未初始化使用默认workId
snowflakeIdGenerator = new SnowflakeIdGenerator(1L);
}
return String.valueOf(snowflakeIdGenerator.nextId());
}
/**
* 雪花算法实现
*/
static class SnowflakeIdGenerator {
// 起始时间戳 (2020-01-01 00:00:00)
private static final long START_TIMESTAMP = 1577808000000L;
// 机器ID位数
private static final long MACHINE_ID_BITS = 10L;
// 序列号位数
private static final long SEQUENCE_BITS = 12L;
// 机器ID最大值
private static final long MAX_MACHINE_ID = (1L << MACHINE_ID_BITS) - 1;
// 序列号最大值
private static final long MAX_SEQUENCE = (1L << SEQUENCE_BITS) - 1;
// 机器ID左移位数
private static final long MACHINE_ID_SHIFT = SEQUENCE_BITS;
// 时间戳左移位数
private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + MACHINE_ID_BITS;
private long machineId;
private long sequence = 0L;
private long lastTimestamp = -1L;
public SnowflakeIdGenerator(long machineId) {
if (machineId > MAX_MACHINE_ID || machineId < 0) {
throw new IllegalArgumentException("Machine ID must be between 0 and " + MAX_MACHINE_ID);
}
this.machineId = machineId;
}
public synchronized long nextId() {
long timestamp = System.currentTimeMillis();
if (timestamp < lastTimestamp) {
throw new RuntimeException("Clock moved backwards. Refusing to generate id");
}
if (timestamp == lastTimestamp) {
sequence = (sequence + 1) & MAX_SEQUENCE;
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = timestamp;
return ((timestamp - START_TIMESTAMP) << TIMESTAMP_SHIFT) |
(machineId << MACHINE_ID_SHIFT) |
sequence;
}
private long tilNextMillis(long lastTimestamp) {
long timestamp = System.currentTimeMillis();
while (timestamp <= lastTimestamp) {
timestamp = System.currentTimeMillis();
}
return timestamp;
}
}
public static void main(String[] args) {
System.out.println(getUUID());
logger.debug("test");
// 测试雪花算法
for (int i = 0; i < 10; i++) {
System.out.println("Snowflake ID: " + getSnowflakeId());
}
}
}

View File

@ -1,26 +0,0 @@
package com.kexue.skills.config;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
/**
* 账户扣费配置属性
*
* @author 系统生成
* @since 2026-04-13
*/
@Data
@Component
public class AccountDeductionProperties {
/**
* 扣费系数默认2倍
* 例如系数为2时实际消耗1积分扣除2积分
*/
@Value("${account.deduction.coefficient:2}")
private BigDecimal coefficient;
}

View File

@ -1,40 +0,0 @@
package com.kexue.skills.config;
import com.kexue.skills.interceptor.LogInterceptor;
import com.kexue.skills.mapper.SysLogMapper;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.annotation.Resource;
/**
* 日志配置类
* 注册日志拦截器启用异步支持
*
* @author 王志维
* @since 2026-04-14
*/
@Configuration
@EnableAsync // 启用异步支持
public class LogConfiguration implements WebMvcConfigurer {
@Resource
private SysLogMapper sysLogMapper;
/**
* 注册拦截器
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
LogInterceptor logInterceptor = new LogInterceptor();
logInterceptor.setSysLogMapper(sysLogMapper);
registry.addInterceptor(logInterceptor)
.addPathPatterns("/api/**") // 拦截所有 API 请求
.excludePathPatterns(
"/api/login/validateToken", // 排除 token 验证接口
"/api/captcha/**" // 排除验证码接口
);
}
}

View File

@ -2,7 +2,6 @@ package com.kexue.skills.controller;
import cn.dev33.satoken.stp.StpUtil;
import com.github.pagehelper.PageInfo;
import com.kexue.skills.annotation.Log;
import com.kexue.skills.annotation.RequireAuth;
import com.kexue.skills.annotation.RequireRole;
import com.kexue.skills.common.CommonResult;
@ -11,8 +10,6 @@ import com.kexue.skills.entity.Account;
import com.kexue.skills.entity.dto.AccountDto;
import com.kexue.skills.entity.dto.TokenConsumptionDto;
import com.kexue.skills.entity.dto.GiftBalanceDto;
import com.kexue.skills.entity.dto.AccountTransactionDto;
import com.kexue.skills.entity.dto.ConsumptionGroupedDto;
import com.kexue.skills.service.AccountService;
import com.kexue.skills.service.SysUserService;
import com.kexue.skills.entity.SysUser;
@ -35,7 +32,6 @@ import java.util.List;
* @author 王志维
* @since 2025-02-21 23:01:48
*/
@Log(module = "账户管理")
@Tag(name = "账户管理 api")
@RestController
@RequestMapping("/api/account")
@ -167,45 +163,6 @@ public class AccountController {
Long userId = Long.parseLong(StpUtil.getLoginId().toString());
return CommonResult.success(this.accountService.getTransactions(userId));
}
/**
* 分页查询充值记录
*
* @param queryDto 查询条件
* @return 分页结果
*/
@Operation(summary = "分页查询充值记录", description = "分页查询所有充值记录,默认根据时间倒序")
@PostMapping("/getRechargePageList")
@RequireAuth
public CommonResult<PageInfo<com.kexue.skills.entity.AccountTransaction>> getRechargePageList(@RequestBody AccountTransactionDto queryDto) {
return CommonResult.success(this.accountService.getRechargePageList(queryDto));
}
/**
* 分页查询消费记录按callId分组
*
* @param queryDto 查询条件
* @return 分页结果
*/
@Operation(summary = "分页查询消费记录", description = "分页查询所有消费记录按callId分组question取最早入库的记录默认根据时间倒序")
@PostMapping("/getConsumptionGroupedPageList")
@RequireAuth
public CommonResult<PageInfo<ConsumptionGroupedDto>> getConsumptionGroupedPageList(@RequestBody AccountTransactionDto queryDto) {
return CommonResult.success(this.accountService.getConsumptionGroupedPageList(queryDto));
}
/**
* 分页查询赠送记录
*
* @param queryDto 查询条件
* @return 分页结果
*/
@Operation(summary = "分页查询赠送记录", description = "分页查询所有赠送记录,默认根据时间倒序")
@PostMapping("/getGiftPageList")
@RequireAuth
public CommonResult<PageInfo<com.kexue.skills.entity.AccountTransaction>> getGiftPageList(@RequestBody AccountTransactionDto queryDto) {
return CommonResult.success(this.accountService.getGiftPageList(queryDto));
}
}

View File

@ -1,73 +0,0 @@
package com.kexue.skills.controller;
import com.kexue.skills.annotation.Log;
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;
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.*;
import javax.annotation.Resource;
import java.io.IOException;
/**
* 账户冻结单控制器
*
* @author 系统生成
* @since 2026-04-11
*/
@Log(module = "账户冻结")
@RestController
@RequestMapping("/api/accountFrozen")
@CrossOrigin(origins = "*")
@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;
/**
* 创建冻结单
* @param accountFrozenDto 冻结单DTO
* @return 冻结单信息
*/
@PostMapping("/frozen")
@Operation(summary = "创建冻结单", description = "创建账户冻结单")
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 CommonResult.success(accountFrozen);
}
/**
* 释放冻结单
* @param accountReleaseDto 冻结单释放DTO
* @return 冻结单信息
*/
@PostMapping("/release")
@Operation(summary = "释放冻结单", description = "释放账户冻结单")
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 CommonResult.success(accountFrozen);
}
}

View File

@ -1,7 +1,6 @@
package com.kexue.skills.controller;
import com.github.pagehelper.PageInfo;
import com.kexue.skills.annotation.Log;
import com.kexue.skills.annotation.RequireAuth;
import com.kexue.skills.common.Assert;
import com.kexue.skills.common.CommonResult;
@ -334,45 +333,4 @@ public class CmsContentController {
return CommonResult.failed("导入失败:" + e.getMessage());
}
}
/**
* 从ZIP文件批量导入Excel数据到CmsContent
*
* @param file ZIP文件
* @param createBy 创建人
* @return 导入结果
*/
@PostMapping("/importFromZip")
@Operation(summary = "从ZIP文件批量导入Excel数据", description = "上传ZIP文件批量导入其中的所有Excel文件到CmsContent")
@RequireAuth
public CommonResult<Integer> importFromZip(@RequestParam("file") MultipartFile file, @RequestParam("createBy") String createBy) {
try {
byte[] zipFileBytes = file.getBytes();
int successCount = cmsContentService.importFromZip(zipFileBytes, createBy);
return CommonResult.success(successCount);
} catch (IOException e) {
e.printStackTrace();
return CommonResult.failed("导入失败:" + e.getMessage());
}
}
/**
* 从指定目录读取Excel数据并更新CmsContent
*
* @param importPathDto 导入路径请求参数
* @param updateBy 更新人
* @return 更新结果
*/
@PostMapping("/updateFromPath")
@Operation(summary = "从目录更新Excel数据", description = "从指定目录读取Excel数据并更新CmsContent")
@RequireAuth
public CommonResult<Integer> updateFromPath(@RequestBody ImportPathDto importPathDto, @RequestParam("updateBy") String updateBy) {
try {
int successCount = cmsContentService.updateFromPath(importPathDto, updateBy);
return CommonResult.success(successCount);
} catch (Exception e) {
e.printStackTrace();
return CommonResult.failed("更新失败:" + e.getMessage());
}
}
}

View File

@ -1,6 +1,5 @@
package com.kexue.skills.controller;
import com.kexue.skills.annotation.Log;
import com.kexue.skills.annotation.PreventDuplicateSubmission;
import com.kexue.skills.common.CacheManager;
import com.kexue.skills.common.CommonResult;
@ -23,7 +22,6 @@ import static cn.dev33.satoken.SaManager.log;
* @author 王志维
* @since 2024-04-13 01:25:22
*/
@Log(module = "登录认证")
@RestController
@RequestMapping("api/login")
@CrossOrigin(origins = "*")

View File

@ -66,11 +66,11 @@ public class ModelPriceController {
* 通过模型名称查询数据
*
* @param modelName 模型名称
* @return 实例对象列表
* @return 实例对象
*/
@Operation(summary = "通过模型名称查询", description = "通过模型名称查询大模型Token价格表")
@GetMapping("/queryByModelName/{modelName}")
public CommonResult<List<ModelPrice>> queryByModelName(@PathVariable("modelName") String modelName) {
public CommonResult<ModelPrice> queryByModelName(@PathVariable("modelName") String modelName) {
return CommonResult.success(this.modelPriceService.queryByModelName(modelName));
}

View File

@ -1,121 +0,0 @@
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);
}
}

View File

@ -16,11 +16,6 @@ import com.github.pagehelper.PageInfo;
import com.kexue.skills.common.CommonResult;
import com.kexue.skills.entity.base.IdDto;
import org.redisson.api.RedissonClient;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.util.UUID;
import org.springframework.beans.factory.annotation.Value;
/**
* (SysUser)表控制层
@ -227,44 +222,4 @@ public class SysUserController {
return CommonResult.success(loginUserDto);
}
/**
* 上传用户头像
*
* @param file 头像文件
* @param request HTTP请求
* @return 上传结果
*/
@PostMapping("/uploadAvatar")
@Operation(summary = "上传用户头像", description = "上传用户头像并更新用户信息")
@RequireAuth
public CommonResult<String> uploadAvatar(@RequestParam("file") MultipartFile file, HttpServletRequest request) {
// 从请求头中获取token
String token = request.getHeader("Authorization");
if (token == null || token.isEmpty()) {
throw new BizException("请先登录认证后操作");
}
// 从Redis中获取当前登录用户信息
String loginUserJson = (String)redissonClient.getBucket("loginUser:" + token).get();
if (loginUserJson == null || loginUserJson.isEmpty()) {
throw new BizException("无效的token请重新登录");
}
// 解析JSON字符串为LoginUser对象
com.kexue.skills.entity.request.LoginUser loginUser = cn.hutool.json.JSONUtil.toBean(loginUserJson, com.kexue.skills.entity.request.LoginUser.class);
if (loginUser == null || loginUser.getUserInfo() == null) {
throw new BizException("无效的token请重新登录");
}
SysUser user = loginUser.getUserInfo();
if (user == null) {
throw new BizException("用户不存在");
}
// 调用服务层方法上传头像
String fileName = sysUserService.uploadAvatar(file, user.getUserId(), token);
return CommonResult.success(fileName);
}
}

View File

@ -1,80 +0,0 @@
package com.kexue.skills.entity;
import java.math.BigDecimal;
import java.util.Date;
import java.io.Serializable;
import com.kexue.skills.entity.base.BaseEntity;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* (AccountFrozen)实体类
* 账户冻结单
*
* @author 系统生成
* @since 2026-04-11
*/
@Data
public class AccountFrozen extends BaseEntity implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description ="冻结单ID字符型")
private String frozenId;
@Schema(description ="流水ID")
private String accountTransactionId;
@Schema(description ="用户ID关联冻结单")
private Long userId;
@Schema(description ="调用ID关联冻结单")
private String callId;
@Schema(description ="会话ID")
private String sessionId;
@Schema(description ="模型名称")
private String modelName;
@Schema(description ="对应回答的问题或需求")
private String question;
@Schema(description ="冻结金额/张数/次数/分钟")
private BigDecimal frozenAmount;
@Schema(description ="冻结类型1.token 2.RMB(元) 99其他")
private Integer frozenType;
@Schema(description ="最终扣减0=释放")
private BigDecimal finalAmount;
@Schema(description ="输入tokens")
private Long usageInputTokens;
@Schema(description ="输出tokens")
private Long usageOutputTokens;
@Schema(description ="总tokens")
private Long usageTotalTokens;
@Schema(description ="终结原因success/cancel/timeout/error/system_recovery")
private String finalizeReason;
@Schema(description ="状态RESERVED 已预留FINALIZED 已终结")
private String status;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@Schema(description ="过期时间")
private Date expireAt;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@Schema(description ="创建时间")
private Date createTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@Schema(description ="更新时间")
private Date updateTime;
}

View File

@ -56,9 +56,6 @@ public class AccountTransaction extends BaseEntity implements Serializable {
@Schema(description ="业务类型")
private String businessType;
@Schema(description ="调用ID关联冻结单")
private String callId;
@Schema(description ="交易备注")
private String remark;

View File

@ -1,120 +0,0 @@
package com.kexue.skills.entity;
import lombok.Data;
import java.io.Serializable;
import java.time.Instant;
import java.util.Map;
/**
* 日志记录对象
* 用于在拦截器和持久层之间传递日志数据
*
* @author 王志维
* @since 2026-04-14
*/
@Data
public class LogRecord implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 模块名称
*/
private String module;
/**
* 日志描述
*/
private String description;
/**
* 请求信息
*/
private LogRequest request;
/**
* 响应信息
*/
private LogResponse response;
/**
* 执行耗时毫秒
*/
private Long timeTaken;
/**
* 时间戳
*/
private Instant timestamp;
/**
* 日志请求对象
*/
@Data
public static class LogRequest implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 请求方法GETPOST等
*/
private String method;
/**
* 请求URL
*/
private String url;
/**
* 请求头
*/
private Map<String, String> headers;
/**
* 请求体
*/
private String body;
/**
* 客户端IP
*/
private String ip;
/**
* IP归属地
*/
private String address;
/**
* 浏览器信息
*/
private String browser;
/**
* 操作系统
*/
private String os;
}
/**
* 日志响应对象
*/
@Data
public static class LogResponse implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 响应头
*/
private Map<String, String> headers;
/**
* 响应体
*/
private String body;
/**
* 状态码
*/
private Integer status;
}
}

View File

@ -44,18 +44,9 @@ public class ModelPrice extends BaseEntity implements Serializable {
@Schema(description ="价格单位")
private String unit;
@Schema(description ="备注/版本信息")
@Schema(description ="备注")
private String remark;
@Schema(description ="计费区间下限(不包含)")
private Long minTokens;
@Schema(description ="计费区间上限(包含,-1代表无穷大")
private Long maxTokens;
@Schema(description ="输出模式standard=非思考模式, thinking=思考模式")
private String outputMode;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@Schema(description ="创建时间")
private Date createdTime;

View File

@ -1,43 +0,0 @@
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;
}

View File

@ -56,7 +56,7 @@ public class PaymentOrder extends BaseEntity implements Serializable {
@Schema(description ="商品描述")
private String productDesc;
@Schema(description ="关联业务ID比如packageId套餐ID")
@Schema(description ="关联业务ID")
private Long businessId;
@Schema(description ="业务类型recharge,purchase_content")

View File

@ -22,59 +22,29 @@ public class SysLog extends BaseEntity implements Serializable {
@Schema(description ="主键ID")
private Long logId;
@Schema(description ="链路ID")
private String traceId;
@Schema(description ="用户ID")
private String userId;
@Schema(description ="日志描述")
private String description;
@Schema(description ="用户名称")
private String userName;
@Schema(description ="所属模块")
private String module;
@Schema(description ="日志类型")
private String logType;
@Schema(description ="请求URL")
private String requestUrl;
@Schema(description ="日志类容")
private String logContent;
@Schema(description ="请求方式")
private String requestMethod;
@Schema(description ="服务端IP")
private String serverIp;
@Schema(description ="请求头")
private String requestHeaders;
@Schema(description ="客户端IP")
private String clientIp;
@Schema(description ="请求体")
private String requestBody;
@Schema(description ="yyyyMMddHHmmss")
private String logTime;
@Schema(description ="状态码")
private Integer statusCode;
@Schema(description ="响应头")
private String responseHeaders;
@Schema(description ="响应体")
private String responseBody;
@Schema(description ="耗时ms")
private Long timeTaken;
@Schema(description ="IP")
private String ip;
@Schema(description ="IP归属地")
private String address;
@Schema(description ="浏览器")
private String browser;
@Schema(description ="操作系统")
private String os;
@Schema(description ="状态1成功2失败")
private Integer status;
@Schema(description ="错误信息")
private String errorMsg;
@Schema(description ="创建人")
private Long createUser;
@Schema(description ="备注")
private String note;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@Schema(description ="创建时间")

View File

@ -66,16 +66,4 @@ public class SysUser extends BaseEntity implements Serializable {
@Schema(description ="会话ID")
private String sessionId;
@Schema(description ="邀请码(用于邀请别人)")
private String inviteCode;
@Schema(description ="被邀请码(邀请我注册的邀请码)")
private String invitedCode;
@Schema(description ="邀请人用户ID邀请我注册的用户ID")
private Long invitedBy;
@Schema(description ="用户头像")
private String userIcon = "defaultUserIcon.png";
}

View File

@ -1,45 +0,0 @@
package com.kexue.skills.entity.dto;
import java.math.BigDecimal;
import java.util.Date;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 账户冻结单DTO
*
* @author 系统生成
* @since 2026-04-11
*/
@Data
public class AccountFrozenDto {
@Schema(description ="会话ID")
private String sessionId;
@Schema(description ="调用ID关联冻结单")
private String callId;
@Schema(description ="模型名称")
private String modelName;
@Schema(description ="对应回答的问题或需求")
private String question;
@Schema(description ="冻结金额/张数/次数/分钟")
private BigDecimal frozenAmount;
@Schema(description ="冻结类型1.token 2.RMB(元) 99其他")
private Integer frozenType;
@Schema(description ="预估输入tokens")
private Long estimatedInputTokens;
@Schema(description ="预估输出tokens")
private Long estimatedOutputTokens;
@Schema(description ="过期时间")
private Date expireAt;
}

View File

@ -1,35 +0,0 @@
package com.kexue.skills.entity.dto;
import java.math.BigDecimal;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 账户冻结单释放DTO
*
* @author 系统生成
* @since 2026-04-11
*/
@Data
public class AccountReleaseDto {
@Schema(description ="冻结单ID字符型")
private String frozenId;
@Schema(description ="最终扣减0=释放")
private BigDecimal finalAmount;
@Schema(description ="输入tokens")
private Long usageInputTokens;
@Schema(description ="输出tokens")
private Long usageOutputTokens;
@Schema(description ="总tokens")
private Long usageTotalTokens;
@Schema(description ="终结原因success/cancel/timeout/error/system_recovery")
private String finalizeReason;
}

View File

@ -3,8 +3,6 @@ package com.kexue.skills.entity.dto;
import com.kexue.skills.entity.base.BaseQueryDto;
import lombok.Data;
import java.util.Date;
/**
* (AccountTransaction)查询DTO类
*
@ -34,8 +32,4 @@ public class AccountTransactionDto extends BaseQueryDto {
private Integer deleteFlag;
private Date createTimeStart;
private Date createTimeEnd;
}

View File

@ -55,19 +55,4 @@ public class CmsContentDto extends BaseQueryDto {
*/
private String keyword;
/**
* 来源
*/
private String origin;
/**
* 标签
*/
private String tags;
/**
* 图标
*/
private String icon;
}

View File

@ -1,101 +0,0 @@
package com.kexue.skills.entity.dto;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 消费记录分组DTO类按callId分组
*
* @author 王志维
* @since 2025-04-15
*/
@Data
public class ConsumptionGroupedDto implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description ="主键ID取最早记录的ID")
private Long transactionId;
@Schema(description ="用户ID")
private Long userId;
@Schema(description ="用户名")
private String userName;
@Schema(description ="交易类型1.充值 2.提现 3.购买内容 4.退款 5.签到奖励 6.赠送 7.其他")
private Integer transactionType;
@Schema(description ="交易金额")
private BigDecimal amount;
@Schema(description ="交易前余额")
private BigDecimal beforeBalance;
@Schema(description ="交易后余额")
private BigDecimal afterBalance;
@Schema(description ="交易状态1.成功 2.失败 3.处理中")
private Integer status;
@Schema(description ="交易单号")
private String transactionNo;
@Schema(description ="支付方式1.微信 2.支付宝 3.余额支付")
private Integer payType;
@Schema(description ="关联业务ID")
private Long businessId;
@Schema(description ="业务类型")
private String businessType;
@Schema(description ="调用ID关联冻结单")
private String callId;
@Schema(description ="交易备注")
private String remark;
@Schema(description ="是否支出1.是 0.否")
private Integer isExpense;
@Schema(description ="输入token")
private Integer inputToken;
@Schema(description ="输出token")
private Integer outputToken;
@Schema(description ="合计tokens")
private Integer totalTokens;
@Schema(description ="处理的模型名称")
private String modelName;
@Schema(description ="对应回答的问题或需求取最早入库的question")
private String question;
@Schema(description ="收入类型recharge(充值)、sign_in(签到奖励)")
private String incomeType;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@Schema(description ="创建时间(取最早记录的创建时间)")
private Date createTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@Schema(description ="更新时间")
private Date updateTime;
@Schema(description ="创建人")
private String createBy;
@Schema(description ="更新人")
private String updateBy;
@Schema(description ="是否删除 0 未删除1已删除")
private Integer deleteFlag;
}

View File

@ -1,27 +0,0 @@
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;
}

View File

@ -2,9 +2,6 @@ package com.kexue.skills.entity.dto;
import com.kexue.skills.entity.base.BaseQueryDto;
import lombok.Data;
import org.checkerframework.checker.formatter.qual.Format;
import java.util.Date;
/**
* (PaymentOrder)查询DTO类
@ -33,10 +30,4 @@ public class PaymentOrderDto extends BaseQueryDto {
private Integer deleteFlag;
private Long packageId;
private Date createTimeStart;
private Date createTimeEnd;
}

View File

@ -2,7 +2,6 @@ package com.kexue.skills.entity.dto;
import java.io.Serializable;
import java.util.Date;
import com.kexue.skills.entity.base.BaseQueryDto;
import lombok.Data;
import com.fasterxml.jackson.annotation.JsonFormat;
@ -24,24 +23,28 @@ public class SysLogDto extends BaseQueryDto implements Serializable {
@Schema(description ="主键ID")
private Long logId;
@Schema(description ="模块")
private String module;
@Schema(description ="用户ID")
private String userId;
@Schema(description ="描述")
private String description;
@Schema(description ="用户名称")
private String userName;
@Schema(description ="IP地址")
private String ip;
@Schema(description ="日志类型")
private String logType;
@Schema(description ="状态")
private Integer status;
@Schema(description ="日志类容")
private String logContent;
@Schema(description ="开始时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date startTime;
@Schema(description ="服务端IP")
private String serverIp;
@Schema(description ="结束时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date endTime;
@Schema(description ="客户端IP")
private String clientIp;
@Schema(description ="yyyyMMddHHmmss")
private String logTime;
@Schema(description ="备注")
private String note;
}

View File

@ -46,7 +46,4 @@ public class TokenConsumptionDto {
@Schema(description ="备注")
private String remark;
@Schema(description ="输出模式standard=非思考模式, thinking=思考模式")
private String outputMode;
}

View File

@ -23,7 +23,4 @@ public class PhoneLoginDto implements Serializable {
@Schema(description ="验证码")
private String code;
@Schema(description ="邀请码")
private String inviteCode;
}

View File

@ -15,9 +15,9 @@ public class GlobalExceptionHandler {
@ExceptionHandler(BizException.class)
public CommonResult<String> handleBizException(BizException e) {
if (e == null) {
return CommonResult.success("未知错误");
return CommonResult.failed("未知错误");
}
return CommonResult.success(e.getErrorCode(), e.getMessage());
return CommonResult.failed(e.getMessage());
}
// 其他异常处理...

View File

@ -1,52 +0,0 @@
package com.kexue.skills.interceptor;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.io.IOException;
/**
* 请求响应体缓存过滤器
* 用于包装 HttpServletRequest HttpServletResponse使其可以重复读取请求体和捕获响应体
*
* @author 王志维
* @since 2026-04-14
*/
@Component
@Order(1) // 确保在最前面执行
public class CachedBodyFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (request instanceof HttpServletRequest && response instanceof HttpServletResponse) {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
// 跳过对 multipart/form-data 格式请求的包装因为文件上传请求不能被重复读取
String contentType = httpRequest.getContentType();
if (contentType != null && contentType.startsWith("multipart/")) {
chain.doFilter(request, response);
return;
}
// 包装请求使其可以重复读取请求体
CachedBodyHttpServletRequest cachedRequest = new CachedBodyHttpServletRequest(httpRequest);
// 包装响应使其可以捕获响应体
CachedBodyHttpServletResponse cachedResponse = new CachedBodyHttpServletResponse(httpResponse);
// 继续过滤链
chain.doFilter(cachedRequest, cachedResponse);
// 将缓存的响应体写入真实响应
cachedResponse.flushBuffer();
} else {
chain.doFilter(request, response);
}
}
}

View File

@ -1,88 +0,0 @@
package com.kexue.skills.interceptor;
import jakarta.servlet.ReadListener;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import java.io.*;
/**
* 可重复读取请求体的 HttpServletRequest 包装类
*
* @author 王志维
* @since 2026-04-14
*/
public class CachedBodyHttpServletRequest extends HttpServletRequestWrapper {
private final byte[] cachedBody;
public CachedBodyHttpServletRequest(HttpServletRequest request) throws IOException {
super(request);
// 读取并缓存请求体
this.cachedBody = readBytes(request.getInputStream());
}
@Override
public ServletInputStream getInputStream() {
return new CachedBodyServletInputStream(this.cachedBody);
}
@Override
public BufferedReader getReader() {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.cachedBody);
return new BufferedReader(new InputStreamReader(byteArrayInputStream));
}
/**
* 获取缓存的请求体字符串
*/
public String getCachedBodyString() {
return new String(cachedBody);
}
/**
* 从输入流读取字节数组
*/
private byte[] readBytes(InputStream inputStream) throws IOException {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
int nRead;
byte[] data = new byte[1024];
while ((nRead = inputStream.read(data, 0, data.length)) != -1) {
buffer.write(data, 0, nRead);
}
return buffer.toByteArray();
}
/**
* 可重复读取的 ServletInputStream
*/
private static class CachedBodyServletInputStream extends ServletInputStream {
private final ByteArrayInputStream inputStream;
public CachedBodyServletInputStream(byte[] cachedBody) {
this.inputStream = new ByteArrayInputStream(cachedBody);
}
@Override
public boolean isFinished() {
return inputStream.available() == 0;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener listener) {
throw new UnsupportedOperationException();
}
@Override
public int read() {
return inputStream.read();
}
}
}

View File

@ -1,94 +0,0 @@
package com.kexue.skills.interceptor;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.WriteListener;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpServletResponseWrapper;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
/**
* 可捕获响应体的 HttpServletResponse 包装类
*/
public class CachedBodyHttpServletResponse extends HttpServletResponseWrapper {
private final ByteArrayOutputStream cachedBody = new ByteArrayOutputStream();
private ServletOutputStream outputStream;
private PrintWriter writer;
public CachedBodyHttpServletResponse(HttpServletResponse response) {
super(response);
}
@Override
public ServletOutputStream getOutputStream() throws IOException {
if (outputStream == null) {
outputStream = new CachedBodyServletOutputStream(super.getOutputStream(), cachedBody);
}
return outputStream;
}
@Override
public PrintWriter getWriter() throws IOException {
if (writer == null) {
writer = new PrintWriter(getOutputStream(), true);
}
return writer;
}
@Override
public void flushBuffer() throws IOException {
if (writer != null) {
writer.flush();
}
if (outputStream != null) {
outputStream.flush();
}
super.flushBuffer();
}
public String getCachedBodyString() {
return cachedBody.toString();
}
private static class CachedBodyServletOutputStream extends ServletOutputStream {
private final ServletOutputStream outputStream;
private final ByteArrayOutputStream cachedBody;
public CachedBodyServletOutputStream(ServletOutputStream outputStream, ByteArrayOutputStream cachedBody) {
this.outputStream = outputStream;
this.cachedBody = cachedBody;
}
@Override
public boolean isReady() {
return outputStream.isReady();
}
@Override
public void setWriteListener(WriteListener listener) {
outputStream.setWriteListener(listener);
}
@Override
public void write(int b) throws IOException {
outputStream.write(b);
cachedBody.write(b);
}
@Override
public void write(byte[] b) throws IOException {
outputStream.write(b);
cachedBody.write(b);
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
outputStream.write(b, off, len);
cachedBody.write(b, off, len);
}
}
}

View File

@ -1,412 +0,0 @@
package com.kexue.skills.interceptor;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.useragent.UserAgent;
import cn.hutool.http.useragent.UserAgentUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.kexue.skills.annotation.Log;
import com.kexue.skills.entity.LogRecord;
import com.kexue.skills.mapper.SysLogMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.annotation.Resource;
import java.io.IOException;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
/**
* 操作日志拦截器
* 基于 HandlerInterceptor 实现捕获请求和响应信息
*
* @author 王志维
* @since 2026-04-14
*/
public class LogInterceptor implements HandlerInterceptor {
private static final Logger logger = LoggerFactory.getLogger(LogInterceptor.class);
private static final ObjectMapper objectMapper = new ObjectMapper();
private SysLogMapper sysLogMapper;
// 使用 ThreadLocal 存储请求开始时间和日志记录
private static final ThreadLocal<Long> START_TIME = new ThreadLocal<>();
private static final ThreadLocal<LogRecord> LOG_RECORD = new ThreadLocal<>();
/**
* 设置 SysLogMapper
*/
public void setSysLogMapper(SysLogMapper sysLogMapper) {
this.sysLogMapper = sysLogMapper;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 只处理方法级别的请求
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
// 检查是否有 @Log 注解
Log methodLog = handlerMethod.getMethodAnnotation(Log.class);
Log classLog = handlerMethod.getBeanType().getAnnotation(Log.class);
// 如果方法和类都没有 @Log 注解不记录日志
if (methodLog == null && classLog == null) {
return true;
}
// 如果方法级别设置了 ignore=true不记录日志
if (methodLog != null && methodLog.ignore()) {
return true;
}
// 记录开始时间
START_TIME.set(System.currentTimeMillis());
// 构建日志记录对象
LogRecord logRecord = new LogRecord();
// 设置模块和描述
String module = "";
String description = "";
if (classLog != null) {
module = classLog.module();
}
if (methodLog != null) {
if (StrUtil.isNotBlank(methodLog.module())) {
module = methodLog.module();
}
description = methodLog.description();
}
logRecord.setModule(module);
logRecord.setDescription(description);
// 设置请求信息
LogRecord.LogRequest logRequest = new LogRecord.LogRequest();
logRequest.setMethod(request.getMethod());
logRequest.setUrl(request.getRequestURL().toString());
logRequest.setHeaders(getRequestHeaders(request));
logRequest.setIp(getClientIp(request));
logRequest.setBrowser(getBrowserInfo(request));
logRequest.setOs(getOsInfo(request));
// 读取请求体从包装对象中获取
if (request instanceof CachedBodyHttpServletRequest) {
CachedBodyHttpServletRequest cachedRequest = (CachedBodyHttpServletRequest) request;
String requestBody = cachedRequest.getCachedBodyString();
// 限制请求体大小避免过大
if (requestBody.length() > 10000) {
requestBody = requestBody.substring(0, 10000) + "... [truncated]";
}
logRequest.setBody(requestBody);
}
logRecord.setRequest(logRequest);
logRecord.setTimestamp(Instant.now());
// 存储到 ThreadLocal
LOG_RECORD.set(logRecord);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) {
// 只处理方法级别的请求
if (!(handler instanceof HandlerMethod)) {
return;
}
// 获取日志记录
LogRecord logRecord = LOG_RECORD.get();
Long startTime = START_TIME.get();
if (logRecord == null || startTime == null) {
return;
}
try {
// 计算耗时
long timeTaken = System.currentTimeMillis() - startTime;
logRecord.setTimeTaken(timeTaken);
// 设置响应信息
LogRecord.LogResponse logResponse = new LogRecord.LogResponse();
logResponse.setStatus(response.getStatus());
logResponse.setHeaders(getResponseHeaders(response));
// 读取响应体从包装对象中获取
if (response instanceof CachedBodyHttpServletResponse) {
CachedBodyHttpServletResponse cachedResponse = (CachedBodyHttpServletResponse) response;
String responseBody = cachedResponse.getCachedBodyString();
// 限制响应体大小避免过大
if (responseBody.length() > 10000) {
responseBody = responseBody.substring(0, 10000) + "... [truncated]";
}
logResponse.setBody(responseBody);
}
logRecord.setResponse(logResponse);
// 异步保存日志
saveLogAsync(logRecord);
} catch (Exception e) {
logger.error("记录操作日志失败: {}", e.getMessage(), e);
} finally {
// 清理 ThreadLocal
START_TIME.remove();
LOG_RECORD.remove();
}
}
/**
* 获取请求头
*/
private Map<String, String> getRequestHeaders(HttpServletRequest request) {
Map<String, String> headers = new HashMap<>();
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
headers.put(headerName, request.getHeader(headerName));
}
return headers;
}
/**
* 获取响应头
*/
private Map<String, String> getResponseHeaders(HttpServletResponse response) {
Map<String, String> headers = new HashMap<>();
for (String headerName : response.getHeaderNames()) {
headers.put(headerName, response.getHeader(headerName));
}
return headers;
}
/**
* 获取客户端IP
*/
private String getClientIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (StrUtil.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP");
}
if (StrUtil.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (StrUtil.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (StrUtil.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
// 多个代理时第一个IP为真实IP
if (StrUtil.isNotBlank(ip) && ip.contains(",")) {
ip = ip.split(",")[0].trim();
}
return ip;
}
/**
* 获取浏览器信息
*/
private String getBrowserInfo(HttpServletRequest request) {
String userAgent = request.getHeader("User-Agent");
if (StrUtil.isBlank(userAgent)) {
return "Unknown";
}
UserAgent ua = UserAgentUtil.parse(userAgent);
String browserName = ua.getBrowser().getName();
String version = ua.getVersion();
// 避免显示 "Unknown null"
if (StrUtil.isBlank(version) || "null".equals(version)) {
return StrUtil.blankToDefault(browserName, "Unknown");
}
return browserName + " " + version;
}
/**
* 获取操作系统信息
*/
private String getOsInfo(HttpServletRequest request) {
String userAgent = request.getHeader("User-Agent");
if (StrUtil.isBlank(userAgent)) {
return "Unknown";
}
UserAgent ua = UserAgentUtil.parse(userAgent);
String osName = ua.getOs().getName();
return StrUtil.blankToDefault(osName, "Unknown");
}
/**
* 异步保存日志
*/
@Async
public void saveLogAsync(LogRecord logRecord) {
try {
// 转换为 SysLog 实体
com.kexue.skills.entity.SysLog sysLog = new com.kexue.skills.entity.SysLog();
// 设置基本信息
sysLog.setDescription(StrUtil.blankToDefault(logRecord.getDescription(), ""));
sysLog.setModule(StrUtil.blankToDefault(logRecord.getModule(), ""));
sysLog.setTimeTaken(logRecord.getTimeTaken() != null ? logRecord.getTimeTaken() : 0L);
sysLog.setCreateTime(new java.util.Date());
sysLog.setUpdateTime(new java.util.Date());
sysLog.setDeleteFlag(0);
// 设置请求信息
LogRecord.LogRequest request = logRecord.getRequest();
if (request != null) {
sysLog.setRequestMethod(StrUtil.blankToDefault(request.getMethod(), "UNKNOWN"));
sysLog.setRequestUrl(StrUtil.blankToDefault(request.getUrl(), ""));
try {
sysLog.setRequestHeaders(objectMapper.writeValueAsString(request.getHeaders()));
} catch (Exception e) {
logger.warn("序列化请求头失败: {}", e.getMessage());
sysLog.setRequestHeaders("{}");
}
sysLog.setRequestBody(StrUtil.blankToDefault(request.getBody(), ""));
sysLog.setIp(StrUtil.blankToDefault(request.getIp(), "127.0.0.1"));
sysLog.setAddress(StrUtil.blankToDefault(request.getAddress(), ""));
sysLog.setBrowser(StrUtil.blankToDefault(request.getBrowser(), "Unknown"));
sysLog.setOs(StrUtil.blankToDefault(request.getOs(), "Unknown"));
} else {
// 设置默认值
sysLog.setRequestMethod("UNKNOWN");
sysLog.setRequestUrl("");
sysLog.setRequestHeaders("{}");
sysLog.setRequestBody("");
sysLog.setIp("127.0.0.1");
sysLog.setAddress("");
sysLog.setBrowser("Unknown");
sysLog.setOs("Unknown");
}
// 设置响应信息
LogRecord.LogResponse response = logRecord.getResponse();
if (response != null) {
sysLog.setStatusCode(response.getStatus() != null ? response.getStatus() : 200);
try {
sysLog.setResponseHeaders(objectMapper.writeValueAsString(response.getHeaders()));
} catch (Exception e) {
logger.warn("序列化响应头失败: {}", e.getMessage());
sysLog.setResponseHeaders("{}");
}
String responseBody = StrUtil.blankToDefault(response.getBody(), "");
sysLog.setResponseBody(responseBody);
// 判断成功/失败状态
Integer statusCode = response.getStatus();
if (statusCode != null) {
sysLog.setStatus(statusCode >= 400 ? 2 : 1);
} else {
sysLog.setStatus(1);
}
// 解析响应体中的错误信息
if (StrUtil.isNotBlank(responseBody)) {
try {
// 尝试解析 JSON 响应体
com.fasterxml.jackson.databind.JsonNode jsonNode = objectMapper.readTree(responseBody);
// 检查是否有错误标识statuscodesuccess等字段
boolean hasError = false;
String errorMsg = "";
// 方式1: 检查 status 字段 {"status":500,"message":"密码不正确"}
if (jsonNode.has("status")) {
int statusValue = jsonNode.get("status").asInt();
if (statusValue >= 400 || statusValue == 0) {
hasError = true;
if (jsonNode.has("message")) {
errorMsg = jsonNode.get("message").asText();
} else if (jsonNode.has("msg")) {
errorMsg = jsonNode.get("msg").asText();
} else if (jsonNode.has("error")) {
errorMsg = jsonNode.get("error").asText();
}
}
}
// 方式2: 检查 code 字段 {"code":500,"message":"密码不正确"}
if (!hasError && jsonNode.has("code")) {
int codeValue = jsonNode.get("code").asInt();
if (codeValue != 200 && codeValue != 0) {
hasError = true;
if (jsonNode.has("message")) {
errorMsg = jsonNode.get("message").asText();
} else if (jsonNode.has("msg")) {
errorMsg = jsonNode.get("msg").asText();
} else if (jsonNode.has("error")) {
errorMsg = jsonNode.get("error").asText();
}
}
}
// 方式3: 检查 success 字段 {"success":false,"message":"密码不正确"}
if (!hasError && jsonNode.has("success")) {
boolean success = jsonNode.get("success").asBoolean();
if (!success) {
hasError = true;
if (jsonNode.has("message")) {
errorMsg = jsonNode.get("message").asText();
} else if (jsonNode.has("msg")) {
errorMsg = jsonNode.get("msg").asText();
} else if (jsonNode.has("error")) {
errorMsg = jsonNode.get("error").asText();
}
}
}
// 如果检测到错误更新状态和错误信息
if (hasError) {
sysLog.setStatus(2); // 失败
sysLog.setErrorMsg(StrUtil.blankToDefault(errorMsg, "业务操作失败"));
}
} catch (Exception e) {
// 不是 JSON 格式或解析失败忽略
logger.debug("响应体不是JSON格式或解析失败: {}", e.getMessage());
}
}
} else {
// 设置默认值
sysLog.setStatusCode(200);
sysLog.setResponseHeaders("{}");
sysLog.setResponseBody("");
sysLog.setStatus(1);
}
// 设置错误信息默认值
sysLog.setErrorMsg("");
// TODO: Token 中解析用户ID
// 这里需要根据实际的认证框架来实现
// sysLog.setCreateUser(userId);
sysLog.setCreateUser(null);
// 插入数据库
sysLogMapper.insert(sysLog);
logger.debug("操作日志保存成功: {}", sysLog.getDescription());
} catch (Exception e) {
logger.error("保存操作日志失败: {}", e.getMessage(), e);
}
}
}

View File

@ -1,62 +0,0 @@
package com.kexue.skills.mapper;
import com.kexue.skills.entity.AccountFrozen;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.Date;
import java.util.List;
/**
* 账户冻结单Mapper
*
* @author 系统生成
* @since 2026-04-11
*/
@Mapper
public interface AccountFrozenMapper {
/**
* 根据ID查询冻结单
* @param frozenId 冻结单ID
* @return 冻结单信息
*/
AccountFrozen selectByPrimaryKey(String frozenId);
/**
* 插入冻结单
* @param accountFrozen 冻结单信息
* @return 影响行数
*/
int insert(AccountFrozen accountFrozen);
/**
* 更新冻结单
* @param accountFrozen 冻结单信息
* @return 影响行数
*/
int updateByPrimaryKey(AccountFrozen accountFrozen);
/**
* 根据会话ID查询冻结单
* @param sessionId 会话ID
* @return 冻结单信息
*/
AccountFrozen selectBySessionId(String sessionId);
/**
* 根据用户ID和状态查询冻结单
* @param userId 用户ID
* @param status 状态
* @return 冻结单列表
*/
List<AccountFrozen> selectByUserIdAndStatus(@Param("userId") Long userId, @Param("status") String status);
/**
* 查询过期的冻结单
* @param currentTime 当前时间
* @return 冻结单列表
*/
List<AccountFrozen> selectExpiredFrozen(Date currentTime);
}

View File

@ -2,7 +2,6 @@ package com.kexue.skills.mapper;
import com.kexue.skills.entity.AccountTransaction;
import com.kexue.skills.entity.dto.AccountTransactionDto;
import com.kexue.skills.entity.dto.ConsumptionGroupedDto;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@ -79,36 +78,4 @@ public interface AccountTransactionMapper {
* @return 交易记录列表
*/
List<AccountTransaction> queryByUserId(Long userId);
/**
* 获取消费原始记录列表用于内存分组
*
* @param queryDto 筛选条件
* @return 原始记录列表
*/
List<AccountTransaction> getConsumptionRawList(AccountTransactionDto queryDto);
/**
* 分页查询充值记录transactionType=1
*
* @param queryDto 筛选条件
* @return 查询结果
*/
List<AccountTransaction> getRechargePageList(AccountTransactionDto queryDto);
/**
* 分页查询赠送记录transactionType=6
*
* @param queryDto 筛选条件
* @return 查询结果
*/
List<AccountTransaction> getGiftPageList(AccountTransactionDto queryDto);
/**
* 分页查询消费记录并按callId分组
*
* @param queryDto 筛选条件
* @return 查询结果
*/
List<ConsumptionGroupedDto> getConsumptionGroupedPageList(AccountTransactionDto queryDto);
}

View File

@ -3,7 +3,6 @@ package com.kexue.skills.mapper;
import com.kexue.skills.entity.ModelPrice;
import com.kexue.skills.entity.dto.ModelPriceDto;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@ -44,19 +43,9 @@ public interface ModelPriceMapper {
* 通过模型名称查询数据
*
* @param modelName 模型名称
* @return 实例对象列表
*/
List<ModelPrice> queryByModelName(String modelName);
/**
* 根据模型名称输出模式和token数量查询价格规则
*
* @param modelName 模型名称
* @param outputMode 输出模式
* @param tokens token数量
* @return 实例对象
*/
ModelPrice queryByModelNameAndOutputModeAndTokens(@Param("modelName") String modelName, @Param("outputMode") String outputMode, @Param("tokens") Long tokens);
ModelPrice queryByModelName(String modelName);
/**
* 新增数据

View File

@ -1,66 +0,0 @@
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);
}

View File

@ -88,6 +88,4 @@ public interface SysUserMapper {
SysUser getBySessionId(String sessionId);
SysUser getByInviteCode(String inviteCode);
}

View File

@ -1,43 +0,0 @@
package com.kexue.skills.service;
import com.kexue.skills.entity.AccountFrozen;
import com.kexue.skills.entity.dto.AccountFrozenDto;
import com.kexue.skills.entity.dto.AccountReleaseDto;
/**
* 账户冻结单服务
*
* @author 系统生成
* @since 2026-04-11
*/
public interface AccountFrozenService {
/**
* 创建冻结单
* @param accountFrozenDto 冻结单DTO
* @return 冻结单信息
*/
AccountFrozen createFrozen(AccountFrozenDto accountFrozenDto);
/**
* 释放冻结单
* @param accountReleaseDto 冻结单释放DTO
* @return 冻结单信息
*/
AccountFrozen releaseFrozen(AccountReleaseDto accountReleaseDto);
/**
* 根据ID查询冻结单
* @param frozenId 冻结单ID
* @return 冻结单信息
*/
AccountFrozen getByFrozenId(String frozenId);
/**
* 根据会话ID查询冻结单
* @param sessionId 会话ID
* @return 冻结单信息
*/
AccountFrozen getBySessionId(String sessionId);
}

View File

@ -2,10 +2,7 @@ package com.kexue.skills.service;
import com.github.pagehelper.PageInfo;
import com.kexue.skills.entity.Account;
import com.kexue.skills.entity.AccountTransaction;
import com.kexue.skills.entity.dto.AccountDto;
import com.kexue.skills.entity.dto.AccountTransactionDto;
import com.kexue.skills.entity.dto.ConsumptionGroupedDto;
import com.kexue.skills.entity.dto.TokenConsumptionDto;
import java.math.BigDecimal;
@ -163,28 +160,4 @@ public interface AccountService extends BaseService {
* @return 交易记录列表
*/
List<com.kexue.skills.entity.AccountTransaction> getTransactions(Long userId);
/**
* 分页查询充值记录
*
* @param queryDto 查询条件
* @return 分页结果
*/
PageInfo<AccountTransaction> getRechargePageList(AccountTransactionDto queryDto);
/**
* 分页查询消费记录按callId分组
*
* @param queryDto 查询条件
* @return 分页结果
*/
PageInfo<ConsumptionGroupedDto> getConsumptionGroupedPageList(AccountTransactionDto queryDto);
/**
* 分页查询赠送记录
*
* @param queryDto 查询条件
* @return 分页结果
*/
PageInfo<AccountTransaction> getGiftPageList(AccountTransactionDto queryDto);
}

View File

@ -201,22 +201,4 @@ public interface CmsContentService extends BaseService {
* @return 导入结果
*/
int importFromPath(ImportPathDto importPathDto, String createBy);
/**
* 从ZIP文件批量导入Excel数据到CmsContent
*
* @param zipFileBytes ZIP文件字节数组
* @param createBy 创建人
* @return 成功导入的记录数
*/
int importFromZip(byte[] zipFileBytes, String createBy);
/**
* 从指定目录读取Excel数据并更新CmsContent
*
* @param importPathDto 导入路径请求参数
* @param updateBy 更新人
* @return 成功更新的记录数
*/
int updateFromPath(ImportPathDto importPathDto, String updateBy);
}

View File

@ -42,19 +42,9 @@ public interface ModelPriceService extends BaseService {
* 通过模型名称查询数据
*
* @param modelName 模型名称
* @return 实例对象列表
*/
List<ModelPrice> queryByModelName(String modelName);
/**
* 根据模型名称输出模式和token数量查询价格规则
*
* @param modelName 模型名称
* @param outputMode 输出模式
* @param tokens token数量
* @return 实例对象
*/
ModelPrice queryByModelNameAndOutputModeAndTokens(String modelName, String outputMode, Long tokens);
ModelPrice queryByModelName(String modelName);
/**
* 新增数据

View File

@ -1,73 +0,0 @@
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);
}

View File

@ -5,8 +5,6 @@ import com.kexue.skills.entity.SysUser;
import com.kexue.skills.entity.dto.SysUserDto;
import com.kexue.skills.entity.request.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
/**
@ -139,23 +137,4 @@ public interface SysUserService extends BaseService {
* @return 角色编码列表
*/
List<String> queryUserRoles(Long userId);
/**
* 更新用户缓存
*
* @param userId 用户ID
* @param user 更新后的用户信息
* @param token 用户的认证token
*/
void updateUserCache(Long userId, SysUser user, String token);
/**
* 上传用户头像
*
* @param file 头像文件
* @param userId 用户ID
* @param token 用户的认证token
* @return 上传成功的文件名
*/
String uploadAvatar(MultipartFile file, Long userId, String token);
}

View File

@ -1,667 +0,0 @@
package com.kexue.skills.service.impl;
import com.kexue.skills.entity.Account;
import com.kexue.skills.entity.AccountFrozen;
import com.kexue.skills.entity.SysUser;
import com.kexue.skills.entity.dto.AccountFrozenDto;
import com.kexue.skills.entity.dto.AccountReleaseDto;
import com.kexue.skills.entity.dto.ModelPriceDto;
import com.kexue.skills.exception.BizException;
import com.kexue.skills.mapper.AccountFrozenMapper;
import com.kexue.skills.mapper.AccountMapper;
import com.kexue.skills.mapper.SysUserMapper;
import com.kexue.skills.service.AccountFrozenService;
import com.kexue.skills.service.ModelPriceService;
import com.kexue.skills.entity.ModelPrice;
import com.kexue.skills.entity.AccountTransaction;
import com.kexue.skills.mapper.AccountTransactionMapper;
import com.kexue.skills.common.util.IDUtils;
import com.kexue.skills.common.ResultCode;
import com.kexue.skills.config.AccountDeductionProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
import java.util.Objects;
/**
* 账户冻结单服务实现
*
* @author 系统生成
* @since 2026-04-11
*/
@Service("accountFrozenService")
@Transactional(rollbackFor = Exception.class)
public class AccountFrozenServiceImpl implements AccountFrozenService {
private static final Logger LOG = LoggerFactory.getLogger(AccountFrozenServiceImpl.class);
private static final Integer FROZEN_TYPE_TOKEN = 1;
private static final Integer FROZEN_TYPE_RMB = 2;
private static final BigDecimal YUAN_TO_POINTS_COEFFICIENT = BigDecimal.valueOf(100);
@Resource
private AccountFrozenMapper accountFrozenMapper;
@Resource
private AccountMapper accountMapper;
@Resource
private SysUserMapper sysUserMapper;
@Resource
private ModelPriceService modelPriceService;
@Resource
private AccountTransactionMapper accountTransactionMapper;
@Resource
private AccountDeductionProperties accountDeductionProperties;
/**
* 创建冻结单
*
* @param accountFrozenDto 冻结单DTO
* @return 冻结单信息
*/
@Override
public AccountFrozen createFrozen(AccountFrozenDto accountFrozenDto) {
// 1. 验证创建冻结单参数
validateCreateFrozenParams(accountFrozenDto);
// 2. 通过sessionId获取用户信息
SysUser sysUser = getUserBySessionId(accountFrozenDto.getSessionId());
// 3. 通过用户ID获取账户信息
Account account = getAccountByUserId(sysUser.getUserId());
// 4. 根据冻结类型计算冻结金额
// - 如果是token类型根据预估tokens和模型价格计算
// - 如果是RMB类型将元转换为积分1元=100积分
BigDecimal finalFrozenAmount = calculateFrozenAmount(accountFrozenDto);
// 5. 检查账户余额是否足够冻结
// 计算可用余额总余额 - 已冻结金额确保足够本次冻结
checkBalanceSufficient(account, finalFrozenAmount);
// 6. 更新账户冻结金额
// 将本次冻结金额加到账户的已冻结金额中
updateAccountFrozenAmount(account, finalFrozenAmount);
// 7. 执行创建冻结单
// 构建冻结单对象并保存到数据库
return doCreateFrozen(accountFrozenDto, sysUser.getUserId(), finalFrozenAmount);
}
/**
* 验证创建冻结单参数
*
* @param accountFrozenDto 冻结单DTO
*/
private void validateCreateFrozenParams(AccountFrozenDto accountFrozenDto) {
if (accountFrozenDto == null) {
throw new BizException(ResultCode.PARAMETER_EMPTY.getCode(), ResultCode.PARAMETER_EMPTY.getMessage());
}
if (accountFrozenDto.getSessionId() == null) {
throw new BizException(ResultCode.SESSION_ID_NOT_EXIST.getCode(), ResultCode.SESSION_ID_NOT_EXIST.getMessage());
}
if (accountFrozenDto.getFrozenType() == null) {
throw new BizException(ResultCode.PARAMETER_EMPTY.getCode(), "冻结类型不能为空");
}
// 根据冻结类型进行不同的参数校验
if (FROZEN_TYPE_TOKEN.equals(accountFrozenDto.getFrozenType())) {
// token类型需要校验estimatedInputTokensestimatedOutputTokensmodelName
if (accountFrozenDto.getEstimatedInputTokens() == null) {
throw new BizException(ResultCode.PARAMETER_EMPTY.getCode(), "预估输入tokens不能为空");
}
if (accountFrozenDto.getEstimatedOutputTokens() == null) {
throw new BizException(ResultCode.PARAMETER_EMPTY.getCode(), "预估输出tokens不能为空");
}
if (accountFrozenDto.getModelName() == null) {
throw new BizException(ResultCode.PARAMETER_EMPTY.getCode(), "模型名称不能为空");
}
} else if (FROZEN_TYPE_RMB.equals(accountFrozenDto.getFrozenType())) {
// RMB类型需要校验frozenAmount
if (accountFrozenDto.getFrozenAmount() == null) {
throw new BizException(ResultCode.PARAMETER_EMPTY.getCode(), "冻结金额不能为空");
}
}
// 其他类型的校验可以在此添加
}
/**
* 通过sessionId获取用户信息
*
* @param sessionId 会话ID
* @return 用户信息
*/
private SysUser getUserBySessionId(String sessionId) {
SysUser sysUser = sysUserMapper.getBySessionId(sessionId);
if (sysUser == null) {
throw new BizException(ResultCode.SESSION_ID_NOT_EXIST.getCode(), ResultCode.SESSION_ID_NOT_EXIST.getMessage());
}
return sysUser;
}
/**
* 通过用户ID获取账户信息
*
* @param userId 用户ID
* @return 账户信息
*/
private Account getAccountByUserId(Long userId) {
Account account = accountMapper.queryByUserId(userId);
if (account == null) {
throw new BizException(ResultCode.ACCOUNT_NOT_EXIST.getCode(), ResultCode.ACCOUNT_NOT_EXIST.getMessage());
}
return account;
}
/**
* 根据冻结类型计算冻结金额
*
* @param accountFrozenDto 冻结单DTO
* @return 计算后的冻结金额单位积分
*/
private BigDecimal calculateFrozenAmount(AccountFrozenDto accountFrozenDto) {
BigDecimal frozenAmount = accountFrozenDto.getFrozenAmount();
if (FROZEN_TYPE_TOKEN.equals(accountFrozenDto.getFrozenType())) {
return calculateTokenFrozenAmount(accountFrozenDto);
} else if (FROZEN_TYPE_RMB.equals(accountFrozenDto.getFrozenType())) {
return calculateRmbFrozenAmount(frozenAmount);
}
return frozenAmount;
}
/**
* 计算token类型的冻结金额
* 根据预估输入输出tokens和模型价格计算冻结金额并应用扣费系数
*
* @param accountFrozenDto 冻结单DTO
* @return 冻结金额单位积分
*/
private BigDecimal calculateTokenFrozenAmount(AccountFrozenDto accountFrozenDto) {
if (accountFrozenDto.getEstimatedInputTokens() == null ||
accountFrozenDto.getEstimatedOutputTokens() == null ||
accountFrozenDto.getModelName() == null) {
return accountFrozenDto.getFrozenAmount();
}
String modelName = normalizeModelName(accountFrozenDto.getModelName());
List<ModelPrice> modelPriceList = getModelPriceList(modelName);
if (modelPriceList.isEmpty()) {
return accountFrozenDto.getFrozenAmount();
}
ModelPrice inputModelPrice = findInputModelPrice(modelPriceList, accountFrozenDto.getEstimatedInputTokens());
ModelPrice outputModelPrice = findOutputModelPrice(modelPriceList, accountFrozenDto.getEstimatedOutputTokens());
if (inputModelPrice == null || outputModelPrice == null) {
return accountFrozenDto.getFrozenAmount();
}
long totalFee = calculateTotalTokenFee(accountFrozenDto, inputModelPrice, outputModelPrice);
BigDecimal baseAmount = BigDecimal.valueOf(totalFee);
return baseAmount.multiply(accountDeductionProperties.getCoefficient());
}
/**
* 标准化模型名称
* 将前端传入的模型名称转换为数据库中对应的名称
*
* @param modelName 前端传入的模型名称
* @return 标准化后的模型名称
*/
private String normalizeModelName(String modelName) {
if ("Qwen 3.5 Plus".equals(modelName)) {
return "qwen3.5-plus";
}
return modelName;
}
/**
* 获取模型价格列表
*
* @param modelName 模型名称
* @return 模型价格列表
*/
private List<ModelPrice> getModelPriceList(String modelName) {
ModelPriceDto modelPriceDto = new ModelPriceDto();
modelPriceDto.setModelName(modelName);
return modelPriceService.getList(modelPriceDto);
}
/**
* 根据输入tokens查找匹配的输入模型价格
* 使用standard模式过滤价格规则
*
* @param modelPriceList 模型价格列表
* @param estimatedInputTokens 预估输入tokens
* @return 匹配的输入模型价格若无匹配返回null
*/
private ModelPrice findInputModelPrice(List<ModelPrice> modelPriceList, Long estimatedInputTokens) {
return modelPriceList.stream()
.filter(mp -> "standard".equals(mp.getOutputMode()))
.filter(mp -> mp.getMinTokens() < estimatedInputTokens)
.filter(mp -> mp.getMaxTokens() == -1 || mp.getMaxTokens() >= estimatedInputTokens)
.max((mp1, mp2) -> mp1.getMinTokens().compareTo(mp2.getMinTokens()))
.orElse(null);
}
/**
* 根据输出tokens查找匹配的输出模型价格
* 使用thinking模式过滤价格规则
*
* @param modelPriceList 模型价格列表
* @param estimatedOutputTokens 预估输出tokens
* @return 匹配的输出模型价格若无匹配返回null
*/
private ModelPrice findOutputModelPrice(List<ModelPrice> modelPriceList, Long estimatedOutputTokens) {
return modelPriceList.stream()
.filter(mp -> "thinking".equals(mp.getOutputMode()))
.filter(mp -> mp.getMinTokens() < estimatedOutputTokens)
.filter(mp -> mp.getMaxTokens() == -1 || mp.getMaxTokens() >= estimatedOutputTokens)
.max((mp1, mp2) -> mp1.getMinTokens().compareTo(mp2.getMinTokens()))
.orElse(null);
}
/**
* 计算总的token费用
*
* @param accountFrozenDto 冻结单DTO
* @param inputModelPrice 输入模型价格
* @param outputModelPrice 输出模型价格
* @return 总费用单位
*/
private long calculateTotalTokenFee(AccountFrozenDto accountFrozenDto, ModelPrice inputModelPrice, ModelPrice outputModelPrice) {
long inputFee = calculateTokenFee(accountFrozenDto.getEstimatedInputTokens(), inputModelPrice.getInputPerCent());
long outputFee = calculateTokenFee(accountFrozenDto.getEstimatedOutputTokens(), outputModelPrice.getOutputPerCent());
return inputFee + outputFee;
}
/**
* 计算单个token费用
* 采用向上取整的方式计算
*
* @param tokens token数量
* @param perCent 每token对应的分数
* @return 费用单位
*/
private long calculateTokenFee(long tokens, long perCent) {
long fee = tokens / perCent;
if (tokens % perCent > 0) {
fee += 1;
}
return fee;
}
/**
* 计算RMB类型的冻结金额
* 将元转换为积分1元=100积分
*
* @param frozenAmount 冻结金额单位
* @return 冻结金额单位积分
*/
private BigDecimal calculateRmbFrozenAmount(BigDecimal frozenAmount) {
if (frozenAmount == null) {
return BigDecimal.ZERO;
}
return frozenAmount.multiply(YUAN_TO_POINTS_COEFFICIENT).multiply(accountDeductionProperties.getCoefficient());
}
/**
* 检查账户余额是否足够冻结
*
* @param account 账户信息
* @param frozenAmount 本次冻结金额
*/
private void checkBalanceSufficient(Account account, BigDecimal frozenAmount) {
BigDecimal balance = account.getBalance() == null ? BigDecimal.ZERO : account.getBalance();
BigDecimal frozen = account.getFrozenAmount() == null ? BigDecimal.ZERO : account.getFrozenAmount();
BigDecimal availableBalance = balance.subtract(frozen);
LOG.debug("检查余额是否足够 - userId: {}, 总余额: {}, 已冻结: {}, 可用余额: {}, 本次冻结: {}",
account.getUserId(), balance, frozen, availableBalance, frozenAmount);
if (availableBalance.compareTo(frozenAmount) < 0) {
LOG.warn("余额不足 - userId: {}, 可用余额: {}, 本次冻结: {}", account.getUserId(), availableBalance, frozenAmount);
throw new BizException(ResultCode.INSUFFICIENT_BALANCE.getCode(), ResultCode.INSUFFICIENT_BALANCE.getMessage());
}
LOG.debug("余额检查通过 - userId: {}, 可用余额: {}, 本次冻结: {}", account.getUserId(), availableBalance, frozenAmount);
}
/**
* 更新账户冻结金额
*
* @param account 账户信息
* @param frozenAmount 本次冻结金额
*/
private void updateAccountFrozenAmount(Account account, BigDecimal frozenAmount) {
BigDecimal currentFrozen = account.getFrozenAmount() == null ? BigDecimal.ZERO : account.getFrozenAmount();
account.setFrozenAmount(currentFrozen.add(frozenAmount));
account.setUpdateTime(new Date());
accountMapper.update(account);
}
/**
* 执行创建冻结单
*
* @param accountFrozenDto 冻结单DTO
* @param userId 用户ID
* @param finalFrozenAmount 最终冻结金额
* @return 冻结单信息
*/
private AccountFrozen doCreateFrozen(AccountFrozenDto accountFrozenDto, Long userId, BigDecimal finalFrozenAmount) {
AccountFrozen accountFrozen = new AccountFrozen();
accountFrozen.setFrozenId(IDUtils.getSnowflakeIdStr());
accountFrozen.setUserId(userId);
accountFrozen.setSessionId(accountFrozenDto.getSessionId());
accountFrozen.setCallId(accountFrozenDto.getCallId());
accountFrozen.setModelName(accountFrozenDto.getModelName());
if (Objects.nonNull(accountFrozenDto.getQuestion())) {
accountFrozen.setQuestion(accountFrozenDto.getQuestion());
}
accountFrozen.setFrozenAmount(finalFrozenAmount);
accountFrozen.setFrozenType(accountFrozenDto.getFrozenType());
accountFrozen.setStatus("RESERVED");
accountFrozen.setExpireAt(accountFrozenDto.getExpireAt());
accountFrozen.setCreateTime(new Date());
accountFrozen.setUpdateTime(new Date());
accountFrozenMapper.insert(accountFrozen);
return accountFrozen;
}
/**
* 释放冻结单
*
* @param accountReleaseDto 冻结单释放DTO
* @return 冻结单信息
*/
@Override
public AccountFrozen releaseFrozen(AccountReleaseDto accountReleaseDto) {
// 1. 验证释放冻结单参数
validateReleaseParams(accountReleaseDto);
// 2. 通过冻结单ID获取冻结单信息
AccountFrozen accountFrozen = getFrozenById(accountReleaseDto.getFrozenId());
// 3. 验证冻结单状态是否为预留状态
// 只有状态为RESERVED的冻结单才能被释放
validateFrozenStatus(accountFrozen);
// 4. 通过用户ID获取账户信息
Account account = getAccountByUserId(accountFrozen.getUserId());
// 5. 根据冻结类型计算最终释放金额
// - 如果是token类型根据实际使用的tokens和模型价格计算
// - 如果是RMB类型将元转换为积分1元=100积分
BigDecimal finalAmount = calculateReleaseAmount(accountReleaseDto, accountFrozen);
// 6. 执行释放时的账户更新
// - 扣减账户的冻结金额
// - 如果有实际扣减金额更新账户余额
doUpdateAccountForRelease(account, accountFrozen, finalAmount);
// 7. 创建交易流水记录
// 当有实际扣减金额时生成交易流水记录
doCreateTransaction(accountFrozen, account, finalAmount);
// 8. 执行冻结单终结
// 更新冻结单状态为已终结并记录最终扣减金额和使用情况
return doFinalizeFrozen(accountFrozen, accountReleaseDto, finalAmount);
}
/**
* 验证释放冻结单参数
*
* @param accountReleaseDto 冻结单释放DTO
*/
private void validateReleaseParams(AccountReleaseDto accountReleaseDto) {
if (accountReleaseDto == null) {
throw new BizException(ResultCode.PARAMETER_EMPTY.getCode(), ResultCode.PARAMETER_EMPTY.getMessage());
}
if (accountReleaseDto.getFrozenId() == null) {
throw new BizException(ResultCode.FROZEN_ID_EMPTY.getCode(), ResultCode.FROZEN_ID_EMPTY.getMessage());
}
// 释放冻结单时的frozenType需要从冻结单中获取所以这里不做类型相关的校验
// 具体的类型相关校验会在calculateReleaseAmount方法中处理
}
/**
* 通过冻结单ID获取冻结单信息
*
* @param frozenId 冻结单ID
* @return 冻结单信息
*/
private AccountFrozen getFrozenById(String frozenId) {
AccountFrozen accountFrozen = accountFrozenMapper.selectByPrimaryKey(frozenId);
if (accountFrozen == null) {
throw new BizException(ResultCode.FROZEN_NOT_EXIST.getCode(), ResultCode.FROZEN_NOT_EXIST.getMessage());
}
return accountFrozen;
}
/**
* 验证冻结单状态是否为预留状态
*
* @param accountFrozen 冻结单信息
*/
private void validateFrozenStatus(AccountFrozen accountFrozen) {
if (!"RESERVED".equals(accountFrozen.getStatus())) {
throw new BizException(ResultCode.FROZEN_STATUS_ERROR.getCode(), ResultCode.FROZEN_STATUS_ERROR.getMessage());
}
}
/**
* 根据冻结类型计算最终释放金额
*
* @param accountReleaseDto 冻结单释放DTO
* @param accountFrozen 冻结单信息
* @return 最终释放金额单位积分
*/
private BigDecimal calculateReleaseAmount(AccountReleaseDto accountReleaseDto, AccountFrozen accountFrozen) {
BigDecimal finalAmount = accountReleaseDto.getFinalAmount() != null ?
accountReleaseDto.getFinalAmount() : BigDecimal.ZERO;
if (FROZEN_TYPE_TOKEN.equals(accountFrozen.getFrozenType())) {
return calculateTokenReleaseAmount(accountReleaseDto, accountFrozen, finalAmount);
} else if (FROZEN_TYPE_RMB.equals(accountFrozen.getFrozenType())) {
return calculateRmbFrozenAmount(finalAmount);
}
return finalAmount;
}
/**
* 计算token类型的释放金额
* 根据实际使用的tokens和模型价格计算释放金额并应用扣费系数
*
* @param accountReleaseDto 冻结单释放DTO
* @param accountFrozen 冻结单信息
* @param finalAmount 当前计算的最终金额
* @return 计算后的释放金额单位积分
*/
private BigDecimal calculateTokenReleaseAmount(AccountReleaseDto accountReleaseDto, AccountFrozen accountFrozen,
BigDecimal finalAmount) {
if (finalAmount.compareTo(BigDecimal.ZERO) != 0 ||
accountReleaseDto.getUsageInputTokens() == null ||
accountReleaseDto.getUsageOutputTokens() == null ||
accountFrozen.getModelName() == null) {
return finalAmount;
}
List<ModelPrice> modelPriceList = getModelPriceList(accountFrozen.getModelName());
if (modelPriceList.isEmpty()) {
return finalAmount;
}
ModelPrice inputModelPrice = findInputModelPrice(modelPriceList, accountReleaseDto.getUsageInputTokens());
ModelPrice outputModelPrice = findOutputModelPrice(modelPriceList, accountReleaseDto.getUsageOutputTokens());
if (inputModelPrice == null || outputModelPrice == null) {
return finalAmount;
}
long totalFee = calculateReleaseTokenFee(accountReleaseDto, inputModelPrice, outputModelPrice);
BigDecimal baseAmount = BigDecimal.valueOf(totalFee);
return baseAmount.multiply(accountDeductionProperties.getCoefficient());
}
/**
* 计算释放时的token总费用
*
* @param accountReleaseDto 冻结单释放DTO
* @param inputModelPrice 输入模型价格
* @param outputModelPrice 输出模型价格
* @return 总费用单位
*/
private long calculateReleaseTokenFee(AccountReleaseDto accountReleaseDto,
ModelPrice inputModelPrice, ModelPrice outputModelPrice) {
long inputFee = calculateTokenFee(accountReleaseDto.getUsageInputTokens(), inputModelPrice.getInputPerCent());
long outputFee = calculateTokenFee(accountReleaseDto.getUsageOutputTokens(), outputModelPrice.getOutputPerCent());
return inputFee + outputFee;
}
/**
* 执行释放时的账户更新
* 包括扣减冻结金额和实际余额
*
* @param account 账户信息
* @param accountFrozen 冻结单信息
* @param finalAmount 最终扣减金额
*/
private void doUpdateAccountForRelease(Account account, AccountFrozen accountFrozen, BigDecimal finalAmount) {
BigDecimal frozenAmount = account.getFrozenAmount() == null ? BigDecimal.ZERO : account.getFrozenAmount();
BigDecimal balance = account.getBalance() == null ? BigDecimal.ZERO : account.getBalance();
account.setFrozenAmount(frozenAmount.subtract(accountFrozen.getFrozenAmount()));
if (finalAmount.compareTo(BigDecimal.ZERO) > 0) {
balance = calculateNewBalance(balance, finalAmount, accountFrozen.getFrozenAmount());
account.setBalance(balance);
}
account.setUpdateTime(new Date());
accountMapper.update(account);
}
/**
* 计算释放后的新余额
* 比较实际扣减金额和预扣减金额取较大值进行扣减
*
* @param balance 当前余额
* @param finalAmount 实际扣减金额
* @param frozenAmount 预扣减金额
* @return 新的余额
*/
private BigDecimal calculateNewBalance(BigDecimal balance, BigDecimal finalAmount, BigDecimal frozenAmount) {
BigDecimal amountToDeduct = finalAmount.compareTo(frozenAmount) > 0 ? finalAmount : frozenAmount;
if (balance.compareTo(amountToDeduct) < 0) {
return BigDecimal.ZERO;
}
return balance.subtract(amountToDeduct);
}
/**
* 创建交易流水记录
*
* @param accountFrozen 冻结单信息
* @param account 账户信息
* @param finalAmount 最终扣减金额
*/
private void doCreateTransaction(AccountFrozen accountFrozen, Account account, BigDecimal finalAmount) {
if (finalAmount.compareTo(BigDecimal.ZERO) <= 0) {
return;
}
BigDecimal beforeBalance = account.getBalance() == null ? BigDecimal.ZERO : account.getBalance();
AccountTransaction transaction = buildTransaction(accountFrozen, account, finalAmount, beforeBalance);
accountTransactionMapper.insert(transaction);
}
/**
* 构建交易流水对象
*
* @param accountFrozen 冻结单信息
* @param account 账户信息
* @param finalAmount 最终扣减金额
* @param beforeBalance 扣减前余额
* @return 交易流水对象
*/
private AccountTransaction buildTransaction(AccountFrozen accountFrozen, Account account,
BigDecimal finalAmount, BigDecimal beforeBalance) {
AccountTransaction transaction = new AccountTransaction();
transaction.setUserId(accountFrozen.getUserId());
transaction.setUserName(account.getUserName());
transaction.setTransactionType(3);
transaction.setAmount(finalAmount);
transaction.setBeforeBalance(beforeBalance);
transaction.setAfterBalance(account.getBalance());
transaction.setStatus(1);
transaction.setTransactionNo(IDUtils.getSnowflakeIdStr());
transaction.setPayType(3);
transaction.setBusinessType("frozen_release");
transaction.setRemark("冻结单释放扣减: " + accountFrozen.getFrozenId());
transaction.setIsExpense(1);
if (Objects.nonNull(accountFrozen.getQuestion())) {
transaction.setQuestion(accountFrozen.getQuestion());
}
if (Objects.nonNull(accountFrozen.getCallId())) {
transaction.setCallId(accountFrozen.getCallId());
}
if (accountFrozen.getFrozenType().equals(FROZEN_TYPE_TOKEN) &&
accountFrozen.getModelName() != null) {
if (accountFrozen.getUsageInputTokens() != null) {
transaction.setInputToken(accountFrozen.getUsageInputTokens().intValue());
}
if (accountFrozen.getUsageOutputTokens() != null) {
transaction.setOutputToken(accountFrozen.getUsageOutputTokens().intValue());
}
if (accountFrozen.getUsageTotalTokens() != null) {
transaction.setTotalTokens(accountFrozen.getUsageTotalTokens().intValue());
}
transaction.setModelName(accountFrozen.getModelName());
}
transaction.setCreateTime(new Date());
transaction.setUpdateTime(new Date());
return transaction;
}
/**
* 执行冻结单终结
* 更新冻结单状态为已终结
*
* @param accountFrozen 冻结单信息
* @param accountReleaseDto 冻结单释放DTO
* @param finalAmount 最终扣减金额
* @return 冻结单信息
*/
private AccountFrozen doFinalizeFrozen(AccountFrozen accountFrozen, AccountReleaseDto accountReleaseDto,
BigDecimal finalAmount) {
accountFrozen.setFinalAmount(finalAmount);
accountFrozen.setUsageInputTokens(accountReleaseDto.getUsageInputTokens());
accountFrozen.setUsageOutputTokens(accountReleaseDto.getUsageOutputTokens());
accountFrozen.setUsageTotalTokens(accountReleaseDto.getUsageTotalTokens());
accountFrozen.setFinalizeReason(accountReleaseDto.getFinalizeReason());
accountFrozen.setStatus("FINALIZED");
accountFrozen.setUpdateTime(new Date());
accountFrozenMapper.updateByPrimaryKey(accountFrozen);
return accountFrozen;
}
/**
* 根据ID查询冻结单
*
* @param frozenId 冻结单ID
* @return 冻结单信息
*/
@Override
public AccountFrozen getByFrozenId(String frozenId) {
return accountFrozenMapper.selectByPrimaryKey(frozenId);
}
/**
* 根据会话ID查询冻结单
*
* @param sessionId 会话ID
* @return 冻结单信息
*/
@Override
public AccountFrozen getBySessionId(String sessionId) {
return accountFrozenMapper.selectBySessionId(sessionId);
}
}

View File

@ -6,8 +6,6 @@ import com.kexue.skills.entity.Account;
import com.kexue.skills.entity.AccountTransaction;
import com.kexue.skills.entity.SysUser;
import com.kexue.skills.entity.dto.AccountDto;
import com.kexue.skills.entity.dto.AccountTransactionDto;
import com.kexue.skills.entity.dto.ConsumptionGroupedDto;
import com.kexue.skills.common.Assert;
import com.kexue.skills.entity.dto.TokenConsumptionDto;
import com.kexue.skills.exception.BizException;
@ -16,21 +14,14 @@ 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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* (Account)表服务实现类
@ -41,8 +32,6 @@ import java.util.Map;
@Service("accountService")
@Transactional(rollbackFor = Exception.class)
public class AccountServiceImpl implements AccountService {
private static final Logger log = LoggerFactory.getLogger(AccountServiceImpl.class);
@Resource
private AccountMapper accountMapper;
@Resource
@ -54,9 +43,6 @@ public class AccountServiceImpl implements AccountService {
@Resource
private ModelPriceService modelPriceService;
@Resource
private PackageConfigService packageConfigService;
/**
* 分页查询
*
@ -66,7 +52,7 @@ public class AccountServiceImpl implements AccountService {
@Override
public PageInfo<Account> getPageList(AccountDto queryDto) {
PageHelper.startPage(queryDto.getPageNum(), queryDto.getPageSize());
List<Account> list = accountMapper.getPageList(queryDto);
List<Account> list = this.accountMapper.getPageList(queryDto);
return new PageInfo<>(list);
}
@ -78,7 +64,7 @@ public class AccountServiceImpl implements AccountService {
*/
@Override
public List<Account> getList(AccountDto queryDto) {
return accountMapper.getList(queryDto);
return this.accountMapper.getList(queryDto);
}
/**
@ -89,7 +75,7 @@ public class AccountServiceImpl implements AccountService {
*/
@Override
public Account queryById(Long accountId) {
return accountMapper.queryById(accountId);
return this.accountMapper.queryById(accountId);
}
/**
@ -100,7 +86,7 @@ public class AccountServiceImpl implements AccountService {
*/
@Override
public Account queryByUserId(Long userId) {
return accountMapper.queryByUserId(userId);
return this.accountMapper.queryByUserId(userId);
}
/**
@ -130,7 +116,7 @@ public class AccountServiceImpl implements AccountService {
account.setFrozenAmount(BigDecimal.ZERO);
}
// 保存数据
accountMapper.insert(account);
this.accountMapper.insert(account);
return account;
}
@ -145,8 +131,8 @@ public class AccountServiceImpl implements AccountService {
// 设置更新时间
account.setUpdateTime(new Date());
// 更新数据
accountMapper.update(account);
return queryById(account.getAccountId());
this.accountMapper.update(account);
return this.queryById(account.getAccountId());
}
/**
@ -164,7 +150,7 @@ public class AccountServiceImpl implements AccountService {
@Override
public int addBalance(Long userId, BigDecimal amount, boolean isWithdrawable, String transactionNo, Long businessId, String businessType, String remark) {
// 1. 查询账户信息
Account account = queryByUserId(userId);
Account account = this.queryByUserId(userId);
if (account == null) {
// 创建账户
account = new Account();
@ -173,68 +159,39 @@ public class AccountServiceImpl implements AccountService {
account.setWithdrawableBalance(BigDecimal.ZERO);
account.setNonWithdrawableBalance(BigDecimal.ZERO);
account.setFrozenAmount(BigDecimal.ZERO);
insert(account);
this.insert(account);
}
// 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. 保存交易记录
// 2. 保存交易记录
AccountTransaction transaction = new AccountTransaction();
transaction.setUserId(userId);
transaction.setUserName(account.getUserName());
transaction.setTransactionType(1); // 充值
transaction.setAmount(points); // 存储充值金额积分
transaction.setAmount(amount);
transaction.setBeforeBalance(account.getBalance());
transaction.setAfterBalance(account.getBalance().add(points)); // 余额为积分
transaction.setAfterBalance(account.getBalance().add(amount));
transaction.setStatus(1); // 成功
transaction.setTransactionNo(transactionNo);
transaction.setPayType(3); // 余额支付
transaction.setBusinessId(businessId);// 套餐ID
transaction.setBusinessId(businessId);
transaction.setBusinessType(businessType);
// 添加额外备注
if (remark != null && !remark.isEmpty()) {
rechargeRemark += " - " + remark;
}
transaction.setRemark(rechargeRemark);
transaction.setRemark(remark);
transaction.setIsExpense(0); // 收入
transaction.setIncomeType("recharge"); // 充值
accountTransactionMapper.insert(transaction);
this.accountTransactionMapper.insert(transaction);
// 4. 更新账户余额使用积分
// 3. 更新账户余额
if (isWithdrawable) {
BigDecimal withdrawableBalance = account.getWithdrawableBalance() == null ? BigDecimal.ZERO : account.getWithdrawableBalance();
account.setWithdrawableBalance(withdrawableBalance.add(points));
account.setWithdrawableBalance(withdrawableBalance.add(amount));
} else {
BigDecimal nonWithdrawableBalance = account.getNonWithdrawableBalance() == null ? BigDecimal.ZERO : account.getNonWithdrawableBalance();
account.setNonWithdrawableBalance(nonWithdrawableBalance.add(points));
account.setNonWithdrawableBalance(nonWithdrawableBalance.add(amount));
}
BigDecimal balance = account.getBalance() == null ? BigDecimal.ZERO : account.getBalance();
account.setBalance(balance.add(points));
account.setBalance(balance.add(amount));
account.setUpdateTime(new Date());
update(account);
this.update(account);
return 1;
}
@ -268,7 +225,7 @@ public class AccountServiceImpl implements AccountService {
@Override
public int reduceBalance(Long userId, BigDecimal amount, String transactionNo, Long businessId, String businessType, String remark) {
// 1. 查询账户信息
Account account = queryByUserId(userId);
Account account = this.queryByUserId(userId);
Assert.notNull(account, "账户不存在");
// 2. 检查余额是否足够
@ -290,10 +247,10 @@ public class AccountServiceImpl implements AccountService {
transaction.setBusinessType(businessType);
transaction.setRemark(remark);
transaction.setIsExpense(1); // 支出
accountTransactionMapper.insert(transaction);
this.accountTransactionMapper.insert(transaction);
// 4. 更新账户余额
return accountMapper.updateBalance(userId, amount, 2);
return this.accountMapper.updateBalance(userId, amount, 2);
}
/**
@ -310,29 +267,23 @@ public class AccountServiceImpl implements AccountService {
throw new BizException("会话ID不存在");
}
userId = bySessionId.getUserId();
Account account = queryByUserId(userId);
Account account = this.queryByUserId(userId);
Assert.notNull(account, "账户不存在");
// 2. 查询输入token的价格规则输入token使用默认的standard模式
String inputOutputMode = "standard";
ModelPrice inputModelPrice = modelPriceService.queryByModelNameAndOutputModeAndTokens(dto.getModelName(), inputOutputMode, Long.valueOf(dto.getInputToken()));
Assert.notNull(inputModelPrice, "输入token价格信息不存在");
// 2. 查询模型价格信息
ModelPrice modelPrice = modelPriceService.queryByModelName(dto.getModelName());
Assert.notNull(modelPrice, "模型价格信息不存在");
// 3. 查询输出token的价格规则
String outputMode = dto.getOutputMode() != null ? dto.getOutputMode() : "standard";
ModelPrice outputModelPrice = modelPriceService.queryByModelNameAndOutputModeAndTokens(dto.getModelName(), outputMode, Long.valueOf(dto.getOutputToken()));
Assert.notNull(outputModelPrice, "输出token价格信息不存在");
// 4. 计算金额
// 3. 计算金额
// 输入token费用输入token数量 / inputPerCent不足1分按1分计算
long inputFee = dto.getInputToken() / inputModelPrice.getInputPerCent();
if (dto.getInputToken() % inputModelPrice.getInputPerCent() > 0) {
long inputFee = dto.getInputToken() / modelPrice.getInputPerCent();
if (dto.getInputToken() % modelPrice.getInputPerCent() > 0) {
inputFee += 1;
}
// 输出token费用输出token数量 / outputPerCent不足1分按1分计算
long outputFee = dto.getOutputToken() / outputModelPrice.getOutputPerCent();
if (dto.getOutputToken() % outputModelPrice.getOutputPerCent() > 0) {
long outputFee = dto.getOutputToken() / modelPrice.getOutputPerCent();
if (dto.getOutputToken() % modelPrice.getOutputPerCent() > 0) {
outputFee += 1;
}
@ -341,11 +292,11 @@ public class AccountServiceImpl implements AccountService {
// 转换为元
BigDecimal amount = BigDecimal.valueOf(totalFee).divide(BigDecimal.valueOf(100));
// 5. 检查余额是否足够
// 4. 检查余额是否足够
BigDecimal balance = account.getBalance() == null ? BigDecimal.ZERO : account.getBalance();
Assert.isTrue(balance.compareTo(amount) >= 0, "账户余额不足");
// 6. 保存交易记录
// 5. 保存交易记录
AccountTransaction transaction = new AccountTransaction();
transaction.setUserId(userId);
transaction.setUserName(account.getUserName());
@ -363,10 +314,10 @@ public class AccountServiceImpl implements AccountService {
transaction.setTotalTokens(dto.getTotalTokens());
transaction.setModelName(dto.getModelName());
transaction.setQuestion(dto.getQuestion());
accountTransactionMapper.insert(transaction);
this.accountTransactionMapper.insert(transaction);
// 7. 更新账户余额
accountMapper.updateBalance(userId, amount, 2);
// 6. 更新账户余额
this.accountMapper.updateBalance(userId, amount, 2);
return amount;
}
@ -376,13 +327,15 @@ 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 业务类型
@ -392,7 +345,7 @@ public class AccountServiceImpl implements AccountService {
@Override
public int addSignInBalance(Long userId, BigDecimal amount, String transactionNo, Long businessId, String businessType, String remark) {
// 1. 查询账户信息
Account account = queryByUserId(userId);
Account account = this.queryByUserId(userId);
if (account == null) {
// 创建账户
account = new Account();
@ -401,7 +354,7 @@ public class AccountServiceImpl implements AccountService {
account.setWithdrawableBalance(BigDecimal.ZERO);
account.setNonWithdrawableBalance(BigDecimal.ZERO);
account.setFrozenAmount(BigDecimal.ZERO);
insert(account);
this.insert(account);
}
// 2. 保存交易记录
@ -409,23 +362,18 @@ 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);
// 在备注中添加积分信息
String signInRemark = "签到奖励" + amount + "积分";
if (remark != null && !remark.isEmpty()) {
signInRemark += " - " + remark;
}
transaction.setRemark(signInRemark);
transaction.setRemark(remark);
transaction.setIsExpense(0); // 收入
transaction.setIncomeType("sign_in"); // 签到奖励
accountTransactionMapper.insert(transaction);
this.accountTransactionMapper.insert(transaction);
// 3. 更新账户余额签到奖励不可提现
BigDecimal nonWithdrawableBalance = account.getNonWithdrawableBalance() == null ? BigDecimal.ZERO : account.getNonWithdrawableBalance();
@ -433,15 +381,15 @@ public class AccountServiceImpl implements AccountService {
BigDecimal balance = account.getBalance() == null ? BigDecimal.ZERO : account.getBalance();
account.setBalance(balance.add(amount));
account.setUpdateTime(new Date());
update(account);
this.update(account);
return 1;
}
/**
* 给用户赠送积分不可提现
* 给用户赠送金额不可提现
*
* @param userId 用户ID
* @param amount 赠送积分
* @param amount 赠送金额
* @param transactionNo 交易单号
* @param businessId 业务ID
* @param businessType 业务类型
@ -451,7 +399,7 @@ public class AccountServiceImpl implements AccountService {
@Override
public int addGiftBalance(Long userId, BigDecimal amount, String transactionNo, Long businessId, String businessType, String remark) {
// 1. 查询账户信息
Account account = queryByUserId(userId);
Account account = this.queryByUserId(userId);
if (account == null) {
// 创建账户
account = new Account();
@ -460,7 +408,7 @@ public class AccountServiceImpl implements AccountService {
account.setWithdrawableBalance(BigDecimal.ZERO);
account.setNonWithdrawableBalance(BigDecimal.ZERO);
account.setFrozenAmount(BigDecimal.ZERO);
insert(account);
this.insert(account);
}
// 2. 保存交易记录
@ -468,25 +416,20 @@ 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);
// 在备注中添加积分信息
String giftRemark = "赠送" + amount + "积分";
if (remark != null && !remark.isEmpty()) {
giftRemark += " - " + remark;
}
transaction.setRemark(giftRemark);
transaction.setRemark(remark);
transaction.setIsExpense(0); // 收入
transaction.setIncomeType("gift"); // 赠送
accountTransactionMapper.insert(transaction);
this.accountTransactionMapper.insert(transaction);
// 3. 更新账户余额赠送积分不可提现
// 3. 更新账户余额赠送金额不可提现
if (account.getNonWithdrawableBalance() == null){
account.setNonWithdrawableBalance(BigDecimal.ZERO);
}
@ -496,7 +439,7 @@ public class AccountServiceImpl implements AccountService {
}
account.setBalance(account.getBalance().add(amount));
account.setUpdateTime(new Date());
update(account);
this.update(account);
return 1;
}
@ -509,7 +452,7 @@ public class AccountServiceImpl implements AccountService {
*/
@Override
public int logicDeleteById(Long accountId, String updateBy) {
return accountMapper.logicDeleteById(accountId, updateBy);
return this.accountMapper.logicDeleteById(accountId, updateBy);
}
/**
@ -520,7 +463,7 @@ public class AccountServiceImpl implements AccountService {
*/
@Override
public int deleteById(Long accountId) {
return accountMapper.deleteById(accountId);
return this.accountMapper.deleteById(accountId);
}
/**
@ -531,134 +474,6 @@ public class AccountServiceImpl implements AccountService {
*/
@Override
public List<com.kexue.skills.entity.AccountTransaction> getTransactions(Long userId) {
return accountTransactionMapper.queryByUserId(userId);
}
/**
* 分页查询充值记录
*
* @param queryDto 查询条件
* @return 分页结果
*/
@Override
public PageInfo<AccountTransaction> getRechargePageList(AccountTransactionDto queryDto) {
PageHelper.startPage(queryDto.getPageNum(), queryDto.getPageSize());
List<AccountTransaction> list = accountTransactionMapper.getRechargePageList(queryDto);
return new PageInfo<>(list);
}
/**
* 分页查询消费记录按callId分组
*
* @param queryDto 查询条件
* @return 分页结果
*/
@Override
public PageInfo<ConsumptionGroupedDto> getConsumptionGroupedPageList(AccountTransactionDto queryDto) {
int pageNum = queryDto.getPageNum() == null || queryDto.getPageNum() < 1 ? 1 : queryDto.getPageNum();
int pageSize = queryDto.getPageSize() == null || queryDto.getPageSize() < 1 ? 10 : queryDto.getPageSize();
log.info("开始查询消费记录内存分组模式参数pageNum={}, pageSize={}, userId={}", pageNum, pageSize, queryDto.getUserId());
// 1. 查询所有原始数据不分页获取全量
List<AccountTransaction> rawList = accountTransactionMapper.getConsumptionRawList(queryDto);
if (rawList == null || rawList.isEmpty()) {
return new PageInfo<>(new ArrayList<>());
}
// 2. 内存分组与聚合
// 使用 LinkedHashMap 保持按 callId 首次出现的顺序即按时间倒序
Map<String, ConsumptionGroupedDto> groupedMap = new LinkedHashMap<>();
for (AccountTransaction tx : rawList) {
// 处理 callId null 或空字符串的情况使用 transactionId 作为唯一标识
String callId = tx.getCallId();
if (callId == null || callId.isEmpty()) {
callId = "tx_" + tx.getTransactionId();
}
ConsumptionGroupedDto group = groupedMap.get(callId);
if (group == null) {
// 第一次遇到该 callId创建新组
group = new ConsumptionGroupedDto();
// 复制基础信息
group.setTransactionId(tx.getTransactionId());
group.setUserId(tx.getUserId());
group.setUserName(tx.getUserName());
group.setTransactionType(tx.getTransactionType());
group.setBeforeBalance(tx.getBeforeBalance());
group.setAfterBalance(tx.getAfterBalance());
group.setStatus(tx.getStatus());
group.setTransactionNo(tx.getTransactionNo());
group.setPayType(tx.getPayType());
group.setBusinessId(tx.getBusinessId());
group.setBusinessType(tx.getBusinessType());
group.setCallId(tx.getCallId());
group.setRemark(tx.getRemark());
group.setIsExpense(tx.getIsExpense());
group.setModelName(tx.getModelName());
group.setQuestion(tx.getQuestion());
group.setIncomeType(tx.getIncomeType());
group.setCreateTime(tx.getCreateTime());
group.setUpdateTime(tx.getUpdateTime());
group.setCreateBy(tx.getCreateBy());
group.setUpdateBy(tx.getUpdateBy());
group.setDeleteFlag(tx.getDeleteFlag());
// 初始化聚合字段
group.setAmount(BigDecimal.ZERO);
group.setInputToken(0);
group.setOutputToken(0);
group.setTotalTokens(0);
groupedMap.put(callId, group);
}
// 累加数值字段
if (tx.getAmount() != null) {
group.setAmount(group.getAmount().add(tx.getAmount()));
}
if (tx.getInputToken() != null) {
group.setInputToken(group.getInputToken() + tx.getInputToken());
}
if (tx.getOutputToken() != null) {
group.setOutputToken(group.getOutputToken() + tx.getOutputToken());
}
if (tx.getTotalTokens() != null) {
group.setTotalTokens(group.getTotalTokens() + tx.getTotalTokens());
}
}
List<ConsumptionGroupedDto> allGroupedList = new ArrayList<>(groupedMap.values());
log.info("分组聚合完成,总组数:{}", allGroupedList.size());
// 3. 内存分页
int total = allGroupedList.size();
int fromIndex = (pageNum - 1) * pageSize;
int toIndex = Math.min(fromIndex + pageSize, total);
List<ConsumptionGroupedDto> pageList = fromIndex < total ? allGroupedList.subList(fromIndex, toIndex) : new ArrayList<>();
// 4. 构建 PageInfo
PageInfo<ConsumptionGroupedDto> pageInfo = new PageInfo<>(pageList);
pageInfo.setTotal((long) total);
pageInfo.setPageNum(pageNum);
pageInfo.setPageSize(pageSize);
pageInfo.setPages((total + pageSize - 1) / pageSize);
log.info("分页成功total={}, pages={}, 当前页数据量={}", total, pageInfo.getPages(), pageList.size());
return pageInfo;
}
/**
* 分页查询赠送记录
*
* @param queryDto 查询条件
* @return 分页结果
*/
@Override
public PageInfo<AccountTransaction> getGiftPageList(AccountTransactionDto queryDto) {
PageHelper.startPage(queryDto.getPageNum(), queryDto.getPageSize());
List<AccountTransaction> list = accountTransactionMapper.getGiftPageList(queryDto);
return new PageInfo<>(list);
return this.accountTransactionMapper.queryByUserId(userId);
}
}

View File

@ -14,9 +14,6 @@ import com.kexue.skills.entity.request.ImportPathDto;
import com.kexue.skills.mapper.CmsContentMapper;
import com.kexue.skills.mapper.CmsContentViewMapper;
import com.kexue.skills.mapper.CmsContentLikeMapper;
import com.kexue.skills.mapper.CmsTagMapper;
import com.kexue.skills.entity.CmsTag;
import com.kexue.skills.entity.dto.CmsTagDto;
import com.kexue.skills.service.CmsContentService;
import cn.hutool.poi.excel.ExcelReader;
import cn.hutool.poi.excel.ExcelUtil;
@ -31,8 +28,6 @@ import java.io.InputStream;
import java.math.BigDecimal;
import java.util.*;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import cn.dev33.satoken.stp.StpUtil;
@ -54,9 +49,6 @@ public class CmsContentServiceImpl implements CmsContentService {
@Resource
private CmsContentLikeMapper cmsContentLikeMapper;
@Resource
private CmsTagMapper cmsTagMapper;
@Resource
private LoginUserCacheUtil loginUserCacheUtil;
@ -83,7 +75,94 @@ public class CmsContentServiceImpl implements CmsContentService {
queryDto.setLanguageType(0);
}
List<CmsContent> list = this.cmsContentMapper.getPageList(queryDto);
return new PageInfo<>(list);
PageInfo<CmsContent> pageInfo = new PageInfo<>(list);
if (queryDto.getTitle() != null && !queryDto.getTitle().trim().isEmpty() && list.size() <= 3 && !list.isEmpty()) {
// 获取第一个 skill 的标签
CmsContent firstSkill = list.get(0);
String tagsStr = firstSkill.getTags();
if (tagsStr != null && !tagsStr.trim().isEmpty()) {
// 解析标签逗号分隔
String[] tags = tagsStr.split(",");
if (tags.length > 0) {
// 构建标签 ID 列表
Set<Long> tagIdSet = new HashSet<>();
for (String tag : tags) {
if (tag != null && !tag.trim().isEmpty()) {
try {
Long tagId = Long.parseLong(tag.trim());
tagIdSet.add(tagId);
} catch (NumberFormatException e) {
// 忽略格式不正确的标签
}
}
}
//如果传入的tagId不为空则添加到tagIdSet中
if (queryDto.getTagId() != null) {
tagIdSet.add(queryDto.getTagId());
}
// 如果有有效的标签 ID查询相关内容
if (!tagIdSet.isEmpty()) {
// 排除已返回的 contentId
Set<Long> existingIds = list.stream()
.map(CmsContent::getContentId)
.collect(Collectors.toSet());
// 构建查询条件使用 tagIdList 一次性查询
CmsContentDto tagQueryDto = new CmsContentDto();
tagQueryDto.setDeleteFlag(0);
tagQueryDto.setPublishStatus(2); // 已发布
tagQueryDto.setTagIdList(new ArrayList<>(tagIdSet));
tagQueryDto.setPageNum(queryDto.getPageNum());
tagQueryDto.setPageSize(1000+queryDto.getPageSize());
// 查询包含这些标签的所有 skill
List<CmsContent> taggedContents = this.cmsContentMapper.getPageList(tagQueryDto);
//如果 tag 传参则根据 tag 再次过滤
if (queryDto.getTagId() != null) {
taggedContents = taggedContents.stream().filter(content -> content.getTags().contains(queryDto.getTagId().toString())).toList() ;
}
// 过滤掉已返回的内容避免重复
List<CmsContent> finalList = taggedContents.stream()
.filter(content -> !existingIds.contains(content.getContentId()))
.collect(Collectors.toList());
// 如果有相关 skill添加到结果中
if (!finalList.isEmpty()) {
// 将外层查询结果和内层查询结果合并
List<CmsContent> allContents = new ArrayList<>(list);
allContents.addAll(finalList);
// view_count create_time 排序
allContents.sort((a, b) -> {
int sortCompare = Integer.compare(
a.getViewCount() != null ? a.getViewCount() : 0,
b.getViewCount() != null ? b.getViewCount() : 0);
if (sortCompare != 0) {
return sortCompare;
}
if (a.getCreateTime() == null || b.getCreateTime() == null) {
return 0;
}
return b.getCreateTime().compareTo(a.getCreateTime());
});
// 计算需要添加的数量确保总数量不超过 pageSize
PageInfo<CmsContent> newPageInfo = new PageInfo<>(memoryPagination(allContents, queryDto.getPageNum(), queryDto.getPageSize()));
newPageInfo.setTotal(allContents.size());
pageInfo = newPageInfo;
}
}
}
}
}
return pageInfo;
}
/**
@ -967,233 +1046,5 @@ public class CmsContentServiceImpl implements CmsContentService {
return totalSuccessCount;
}
@Override
public int importFromZip(byte[] zipFileBytes, String createBy) {
int totalSuccessCount = 0;
int totalFiles = 0;
try (ZipInputStream zipInputStream = new ZipInputStream(new ByteArrayInputStream(zipFileBytes))) {
ZipEntry entry;
// 遍历ZIP文件中的所有条目
while ((entry = zipInputStream.getNextEntry()) != null) {
String fileName = entry.getName();
// 跳过目录和非Excel文件
if (entry.isDirectory() || (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx"))) {
zipInputStream.closeEntry();
continue;
}
// 跳过Office临时锁定文件
String simpleName = new File(fileName).getName();
if (simpleName.startsWith("~$")) {
zipInputStream.closeEntry();
continue;
}
totalFiles++;
System.out.println("当前处理第 " + totalFiles + " 个文件,文件名称是:" + fileName);
try {
// 读取Excel文件内容到字节数组
byte[] fileBytes = zipInputStream.readAllBytes();
// 调用现有的 importFromExcel 方法进行导入
int successCount = importFromExcel(fileBytes, createBy);
totalSuccessCount += successCount;
System.out.println("" + totalFiles + " 个文件导入成功,导入了 " + successCount + " 条记录");
} catch (Exception e) {
System.err.println("导入文件失败: " + fileName);
e.printStackTrace();
// 单个文件导入失败不影响其他文件
} finally {
zipInputStream.closeEntry();
}
}
System.out.println("导入完成,共处理 " + totalFiles + " 个文件,成功导入 " + totalSuccessCount + " 条记录");
} catch (Exception e) {
System.err.println("ZIP文件导入操作失败");
e.printStackTrace();
}
return totalSuccessCount;
}
@Override
public int updateFromPath(ImportPathDto importPathDto, String updateBy) {
int totalSuccessCount = 0;
try {
// 检查目录是否存在
File directory = new File(importPathDto.getFilePath());
if (!directory.exists() || !directory.isDirectory()) {
System.err.println("目录不存在或不是有效目录: " + importPathDto.getFilePath());
return 0;
}
// 读取目录下所有 Excel 文件排除 Office 临时锁定文件
File[] files = directory.listFiles((dir, name) -> {
// 跳过以 ~$ 开头的临时锁定文件
if (name.startsWith("~$")) {
return false;
}
return name.endsWith(".xls") || name.endsWith(".xlsx");
});
if (files == null || files.length == 0) {
return 0;
}
// 记录文件总数
int totalFiles = files.length;
System.out.println("总共发现 " + totalFiles + " 个 Excel 文件需要处理");
// 遍历所有 Excel 文件并处理
for (int i = 0; i < files.length; i++) {
File file = files[i];
System.out.println("当前处理第 " + (i + 1) + " 个文件,文件名称是:" + file.getName());
try (FileInputStream fis = new FileInputStream(file)) {
// 读取文件内容到字节数组
byte[] fileBytes = new byte[(int) file.length()];
fis.read(fileBytes);
// 调用 updateFromExcel 方法进行更新
int successCount = updateFromExcel(fileBytes, updateBy);
totalSuccessCount += successCount;
System.out.println("" + (i + 1) + " 个文件处理成功,更新了 " + successCount + " 条记录");
} catch (Exception e) {
System.err.println("处理文件失败: " + file.getAbsolutePath());
e.printStackTrace();
// 单个文件处理失败不影响其他文件
continue;
}
}
System.out.println("处理完成,共处理 " + totalFiles + " 个文件,成功更新 " + totalSuccessCount + " 条记录");
} catch (Exception e) {
System.err.println("更新操作失败: " + importPathDto.getFilePath());
e.printStackTrace();
}
return totalSuccessCount;
}
/**
* 从Excel文件读取数据并更新CmsContent
*
* @param fileBytes Excel文件字节数组
* @param updateBy 更新人
* @return 成功更新的记录数
*/
private int updateFromExcel(byte[] fileBytes, String updateBy) {
int successCount = 0;
try (InputStream inputStream = new ByteArrayInputStream(fileBytes);
ExcelReader reader = ExcelUtil.getReader(inputStream)) {
// 获取总行数包括标题行
int totalRows = reader.getRowCount();
if (totalRows <= 1) {
// 只有标题行或空文件
return 0;
}
Date now = new Date();
// 读取标题行
List<Object> headerObjList = reader.readRow(0);
if (headerObjList == null || headerObjList.isEmpty()) {
return 0;
}
// 转换为List<String>
List<String> headerList = new ArrayList<>();
for (Object obj : headerObjList) {
headerList.add(obj != null ? obj.toString() : "");
}
// 从第二行开始读取数据第一行为标题行
for (int rowIndex = 1; rowIndex < totalRows; rowIndex++) {
try {
List<Object> rowList = reader.readRow(rowIndex);
if (rowList == null || rowList.isEmpty()) {
continue;
}
// 转换为Map<String, Object>
Map<String, Object> row = new HashMap<>();
for (int i = 0; i < headerList.size() && i < rowList.size(); i++) {
row.put(headerList.get(i), rowList.get(i));
}
if (row.isEmpty()) {
continue;
}
// 读取关键字段用于查询
String title = getStringValue(row, "title");
String origin = getStringValue(row, "origin");
String tags = getStringValue(row, "tags");
// 去掉tags中的空格
if (tags != null) {
tags = tags.replaceAll(" ", "");
}
String icon = getStringValue(row, "icon");
// 根据关键字段查询记录
CmsContentDto queryDto = new CmsContentDto();
queryDto.setTitle(title);
queryDto.setOrigin(origin);
// queryDto.setTags(tags);
// queryDto.setIcon(icon);
queryDto.setDeleteFlag(0);
List<CmsContent> existingList = cmsContentMapper.getList(queryDto);
if (existingList != null && !existingList.isEmpty()) {
// 找到匹配的记录进行更新
CmsContent existingContent = existingList.get(0);
// 更新字段
existingContent.setTitle(getStringValue(row, "title"));
existingContent.setTitleEn(getStringValue(row, "title_en"));
existingContent.setOrigin(getStringValue(row, "origin"));
existingContent.setTags(tags);
existingContent.setIcon(getStringValue(row, "icon"));
existingContent.setPrice(getBigDecimalValue(row, "price"));
existingContent.setContentType(getIntegerValue(row, "content_type"));
existingContent.setContent(getStringValue(row, "content"));
existingContent.setContentEn(getStringValue(row, "content_en"));
existingContent.setAuditStatus(getIntegerValue(row, "audit_status"));
existingContent.setPublishStatus(getIntegerValue(row, "publish_status"));
existingContent.setViewCount(getIntegerValue(row, "view_count"));
existingContent.setLikeCount(getIntegerValue(row, "like_count"));
existingContent.setCommentCount(getIntegerValue(row, "comment_count"));
existingContent.setIsPaid(getIntegerValue(row, "is_paid"));
existingContent.setSupportPointsPay(getIntegerValue(row, "support_points_pay"));
existingContent.setDescription(getStringValue(row, "description"));
existingContent.setDescriptionEn(getStringValue(row, "description_en"));
existingContent.setIntroduce(getStringValue(row, "introduce"));
existingContent.setIntroduceEn(getStringValue(row, "introduce_en"));
// 设置更新时间和更新人
existingContent.setUpdateTime(now);
existingContent.setUpdateBy(updateBy);
// 执行更新
cmsContentMapper.update(existingContent);
successCount++;
}
} catch (Exception e) {
System.err.println("处理第 " + rowIndex + " 行数据时出错: " + e.getMessage());
e.printStackTrace();
// 跳过当前行继续处理下一行
continue;
}
}
} catch (Exception e) {
e.printStackTrace();
}
return successCount;
}
}

View File

@ -63,24 +63,11 @@ public class ModelPriceServiceImpl implements ModelPriceService {
* 通过模型名称查询数据
*
* @param modelName 模型名称
* @return 实例对象列表
*/
@Override
public List<ModelPrice> queryByModelName(String modelName) {
return this.modelPriceMapper.queryByModelName(modelName);
}
/**
* 根据模型名称输出模式和token数量查询价格规则
*
* @param modelName 模型名称
* @param outputMode 输出模式
* @param tokens token数量
* @return 实例对象
*/
@Override
public ModelPrice queryByModelNameAndOutputModeAndTokens(String modelName, String outputMode, Long tokens) {
return this.modelPriceMapper.queryByModelNameAndOutputModeAndTokens(modelName, outputMode, tokens);
public ModelPrice queryByModelName(String modelName) {
return this.modelPriceMapper.queryByModelName(modelName);
}
/**

View File

@ -1,66 +0,0 @@
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);
}
}

View File

@ -11,8 +11,6 @@ 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;
@ -65,9 +63,6 @@ public class PayServiceImpl implements PayService {
@Resource
private AccountService accountService;
@Resource
private PackageConfigService packageConfigService;
/**
* 生成随机字符串
* @return 随机字符串
@ -510,11 +505,11 @@ public class PayServiceImpl implements PayService {
order.getAmount(),
true, // 可提现
transactionId,
order.getBusinessId(),
order.getOrderId(),
"recharge",
"微信支付充值"
);
logger.info("微信支付回调更新账户余额成功userId={}, amount={}, actualAmount={}", order.getUserId(), order.getAmount(), order.getAmount());
logger.info("微信支付回调更新账户余额成功userId={}, amount={}", order.getUserId(), order.getAmount());
} catch (Exception e) {
logger.error("微信支付回调:更新账户余额失败", e);
// 继续处理不影响回调响应
@ -569,7 +564,6 @@ 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");
@ -653,11 +647,11 @@ public class PayServiceImpl implements PayService {
order.getAmount(),
true, // 可提现
transactionId,
order.getBusinessId(),
order.getOrderId(),
"recharge",
"支付宝支付充值"
);
logger.info("支付宝支付回调更新账户余额成功userId={}, amount={}, actualAmount={}", order.getUserId(), order.getAmount(), order.getAmount());
logger.info("支付宝支付回调更新账户余额成功userId={}, amount={}", order.getUserId(), order.getAmount());
} catch (Exception e) {
logger.error("支付宝支付回调:更新账户余额失败", e);
// 继续处理不影响回调响应

View File

@ -27,22 +27,17 @@ import com.kexue.skills.mapper.CmsContentMapper;
import com.kexue.skills.mapper.SysUserMapper;
import com.kexue.skills.mapper.SysUserRoleMapper;
import com.kexue.skills.service.SysUserService;
import com.kexue.skills.service.AccountService;
import com.kexue.skills.utils.MD5Util;
import lombok.extern.slf4j.Slf4j;
import org.dromara.sms4j.api.SmsBlend;
import org.dromara.sms4j.core.factory.SmsFactory;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Value;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.security.NoSuchAlgorithmException;
import java.util.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
/**
* (SysUser)表服务实现类
@ -81,15 +76,6 @@ public class SysUserServiceImpl implements SysUserService {
@Resource
private CmsContentMapper cmsContentMapper;
@Resource
private AccountService accountService;
/**
* 用户头像上传目录
*/
@Value("${web.upload.userIconPath}")
private String userIconPath;
/**
* 分页查询数据
@ -152,34 +138,6 @@ public class SysUserServiceImpl implements SysUserService {
sysUser.setDeleteFlag(Const.DELETE_FLAG_NO);
//写一个salt生成方法
sysUser.setSalt(System.currentTimeMillis()+"");
// 生成随机邀请码8位字母数字组合
if (sysUser.getInviteCode() == null || sysUser.getInviteCode().isEmpty()) {
sysUser.setInviteCode(generateInviteCode());
}
// 处理邀请码
if (sysUser.getInvitedCode() != null && !sysUser.getInvitedCode().isEmpty()) {
// 验证邀请码是否有效
SysUser inviter = sysUserMapper.getByInviteCode(sysUser.getInvitedCode());
if (inviter == null) {
throw new BizException("邀请码无效");
}
// 设置邀请人用户ID
sysUser.setInvitedBy(inviter.getUserId());
// 给邀请人赠送100积分
accountService.addGiftBalance(
inviter.getUserId(),
BigDecimal.valueOf(100),
"gift_" + System.currentTimeMillis(),
null,
"invite",
"邀请用户注册赠送"
);
}
try {
// 假设客户端已经对密码进行了一次MD5加密服务端使用双重加密验证
String encryptedPwd = MD5Util.doubleEncrypt(sysUser.getPwd(), sysUser.getSalt());
@ -199,16 +157,6 @@ public class SysUserServiceImpl implements SysUserService {
account.setDeleteFlag(Const.DELETE_FLAG_NO); // 初始未删除
accountMapper.insert(account);
// 注册赠送100积分
accountService.addGiftBalance(
sysUser.getUserId(),
BigDecimal.valueOf(100),
"gift_" + System.currentTimeMillis(),
null,
"register",
"注册赠送"
);
// 将返回的用户密码设置为null
sysUser.setPwd(null);
return sysUser;
@ -291,9 +239,7 @@ public class SysUserServiceImpl implements SysUserService {
// 判断更新的用户是否是当前登录用户如果是则更新用户缓存
Long currentUserId = loginUserCacheUtil.getCurrentUserId();
if (currentUserId != null && currentUserId.equals(sysUserUpdateDto.getUserId())) {
// 获取当前登录用户的 token
String token = loginUserCacheUtil.getTokenFromRequest();
updateUserCache(currentUserId, sysUser, token);
updateUserCache(currentUserId, sysUser);
}
}else {
// 如果没有用户ID创建新用户
@ -911,7 +857,7 @@ public class SysUserServiceImpl implements SysUserService {
// 如果用户不存在自动创建账号
if (sysUser == null) {
sysUser = createUserByPhone(phone, phoneLoginDto.getInviteCode());
sysUser = createUserByPhone(phone);
}
// 检查用户是否已有 token如果有则使旧 token 失效
@ -1008,45 +954,15 @@ public class SysUserServiceImpl implements SysUserService {
* 根据手机号创建用户
*
* @param phone 手机号
* @param inviteCode 邀请码
* @return 创建的用户对象
*/
private SysUser createUserByPhone(String phone, String inviteCode) {
private SysUser createUserByPhone(String phone) {
SysUser sysUser = new SysUser();
// 设置用户名使用手机号作为用户名
sysUser.setUserName(phone);
sysUser.setTel(phone);
// 生成随机邀请码8位字母数字组合
String generatedInviteCode = generateInviteCode();
sysUser.setInviteCode(generatedInviteCode);
// 处理邀请码
if (inviteCode != null && !inviteCode.isEmpty()) {
// 验证邀请码是否有效
SysUser inviter = sysUserMapper.getByInviteCode(inviteCode);
if (inviter == null) {
throw new BizException("邀请码无效");
}
// 设置被邀请码和邀请人用户ID
sysUser.setInvitedCode(inviteCode);
sysUser.setInvitedBy(inviter.getUserId());
// 给邀请人赠送100积分
accountService.addGiftBalance(
inviter.getUserId(),
BigDecimal.valueOf(100),
"gift_" + System.currentTimeMillis(),
null,
"invite",
"邀请用户注册赠送"
);
// 给被邀请人新用户也赠送100积分作为邀请奖励
// 注意这里先不执行等账户创建后再执行
}
// 设置固定salt为666666
String salt = "666666";
sysUser.setSalt(salt);
@ -1078,46 +994,9 @@ public class SysUserServiceImpl implements SysUserService {
account.setDeleteFlag(Const.DELETE_FLAG_NO);
accountMapper.insert(account);
// 注册赠送100积分
accountService.addGiftBalance(
sysUser.getUserId(),
BigDecimal.valueOf(100),
"gift_" + System.currentTimeMillis(),
null,
"register",
"注册赠送"
);
// 如果有邀请码额外赠送100积分作为邀请奖励
if (inviteCode != null && !inviteCode.isEmpty()) {
accountService.addGiftBalance(
sysUser.getUserId(),
BigDecimal.valueOf(100),
"gift_" + System.currentTimeMillis(),
null,
"invite_reward",
"通过邀请码注册赠送"
);
}
return sysUser;
}
/**
* 生成8位邀请码
*
* @return 邀请码
*/
private String generateInviteCode() {
String characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
Random random = new Random();
StringBuilder sb = new StringBuilder(8);
for (int i = 0; i < 8; i++) {
sb.append(characters.charAt(random.nextInt(characters.length())));
}
return sb.toString();
}
/**
* 生成随机密码
*
@ -1165,14 +1044,13 @@ public class SysUserServiceImpl implements SysUserService {
*
* @param userId 用户ID
* @param updatedUser 更新后的用户信息
* @param token 用户的认证token
*/
@Override
public void updateUserCache(Long userId, SysUser updatedUser, String token) {
private void updateUserCache(Long userId, SysUser updatedUser) {
try {
// 验证 token 是否有效
// 获取当前登录用户的 token
String token = loginUserCacheUtil.getTokenFromRequest();
if (token == null || token.isEmpty()) {
log.warn("的 token跳过缓存更新");
log.warn("法获取当前用户的 token跳过缓存更新");
return;
}
@ -1204,59 +1082,4 @@ public class SysUserServiceImpl implements SysUserService {
log.error("更新用户缓存失败:{}", e.getMessage(), e);
}
}
/**
* 上传用户头像
*
* @param file 头像文件
* @param userId 用户ID
* @param token 用户的认证token
* @return 上传成功的文件名
*/
@Override
public String uploadAvatar(MultipartFile file, Long userId, String token) {
// 检查文件是否为空
if (file.isEmpty()) {
throw new BizException("请选择要上传的头像文件");
}
// 检查文件类型
String contentType = file.getContentType();
if (contentType == null || !contentType.startsWith("image/")) {
throw new BizException("只能上传图片文件");
}
// 生成唯一文件名
String originalFilename = file.getOriginalFilename();
String extension = originalFilename != null ? originalFilename.substring(originalFilename.lastIndexOf(".")) : ".jpg";
String fileName = UUID.randomUUID().toString() + extension;
// 定义上传目录
File dir = new File(userIconPath);
if (!dir.exists()) {
dir.mkdirs();
}
// 保存文件
File dest = new File(dir, fileName);
try {
file.transferTo(dest);
} catch (IOException e) {
e.printStackTrace();
throw new BizException("上传失败:" + e.getMessage());
}
// 更新用户头像
SysUser user = sysUserMapper.queryById(userId);
if (user == null) {
throw new BizException("用户不存在");
}
user.setUserIcon(fileName);
sysUserMapper.update(user);
// 更新缓存中的用户信息
updateUserCache(userId, user, token);
return fileName;
}
}

View File

@ -1,64 +0,0 @@
package com.kexue.skills.task;
import com.kexue.skills.entity.AccountFrozen;
import com.kexue.skills.mapper.AccountFrozenMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.Date;
import java.util.List;
/**
* 账户冻结单定时任务
* 扫描过期的冻结单并更新状态
*
* @author 系统生成
* @since 2026-04-11
*/
@Component
public class AccountFrozenTask {
private static final Logger logger = LoggerFactory.getLogger(AccountFrozenTask.class);
@Resource
private AccountFrozenMapper accountFrozenMapper;
/**
* 定时扫描过期的冻结单
* 每隔30分钟执行一次
*/
@Scheduled(cron = "0 0/30 * * * ?")
public void scanExpiredFrozen() {
logger.info("开始扫描过期的冻结单");
try {
// 查询所有状态为RESERVED且过期的冻结单
List<AccountFrozen> expiredFrozenList = accountFrozenMapper.selectExpiredFrozen(new Date());
if (expiredFrozenList != null && !expiredFrozenList.isEmpty()) {
logger.info("发现 {} 个过期的冻结单", expiredFrozenList.size());
for (AccountFrozen frozen : expiredFrozenList) {
// 更新状态为FINALIZED
frozen.setStatus("FINALIZED");
frozen.setFinalizeReason("timeout");
frozen.setUpdateTime(new Date());
// 执行更新
accountFrozenMapper.updateByPrimaryKey(frozen);
logger.info("已处理过期冻结单: {}", frozen.getFrozenId());
}
} else {
logger.info("未发现过期的冻结单");
}
} catch (Exception e) {
logger.error("扫描过期冻结单时发生错误", e);
}
logger.info("扫描过期冻结单完成");
}
}

View File

@ -61,7 +61,7 @@ sa-token:
# 验证码配置
captcha:
# 是否启用验证码验证
enabled: true
enabled: false
# 验证码有效期(秒)
expire-time: 300
# 验证码长度
@ -104,7 +104,6 @@ jetcache:
web:
upload:
path: /kexue/agentSkills/upload/
userIconPath: /kexue/agentSkills/resources/userIcons
payment:
# 微信支付配置

View File

@ -4,9 +4,6 @@ spring:
application:
name: agentSkills
version: 1.0.0
# 禁用会话持久化,避免启动时恢复损坏的会话文件
session:
store-type: none
mvc:
pathmatch:
matching-strategy: ANT_PATH_MATCHER
@ -96,15 +93,4 @@ sms:
web:
upload:
path: /kexue/agentSkills/upload/
userIconPath: /kexue/agentSkills/resources/userIcons
# 雪花算法配置
snowflake:
workid: 1 # 机器ID分布式部署时需要保证唯一
# 账户扣费配置
account:
deduction:
# 扣费系数默认2倍实际消耗1积分扣除2积分
coefficient: 2

View File

@ -1,93 +0,0 @@
<?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.AccountFrozenMapper">
<resultMap id="BaseResultMap" type="com.kexue.skills.entity.AccountFrozen">
<id column="frozen_id" property="frozenId" />
<result column="account_transaction_id" property="accountTransactionId" />
<result column="user_id" property="userId" />
<result column="call_id" property="callId" />
<result column="session_id" property="sessionId" />
<result column="model_name" property="modelName" />
<result column="question" property="question" />
<result column="frozen_amount" property="frozenAmount" />
<result column="frozen_type" property="frozenType" />
<result column="final_amount" property="finalAmount" />
<result column="usage_input_tokens" property="usageInputTokens" />
<result column="usage_output_tokens" property="usageOutputTokens" />
<result column="usage_total_tokens" property="usageTotalTokens" />
<result column="finalize_reason" property="finalizeReason" />
<result column="status" property="status" />
<result column="expire_at" property="expireAt" />
<result column="create_time" property="createTime" />
<result column="update_time" property="updateTime" />
</resultMap>
<sql id="Base_Column_List">
frozen_id, account_transaction_id, user_id, call_id, session_id, model_name, question, frozen_amount,
frozen_type, final_amount, usage_input_tokens, usage_output_tokens, usage_total_tokens,
finalize_reason, status, expire_at, create_time, update_time
</sql>
<select id="selectByPrimaryKey" resultMap="BaseResultMap" parameterType="java.lang.String">
select
<include refid="Base_Column_List" />
from account_frozen
where frozen_id = #{frozenId}
</select>
<insert id="insert" parameterType="com.kexue.skills.entity.AccountFrozen">
insert into account_frozen
(frozen_id, account_transaction_id, user_id, call_id, session_id, model_name, question, frozen_amount,
frozen_type, final_amount, usage_input_tokens, usage_output_tokens, usage_total_tokens,
finalize_reason, status, expire_at, create_time, update_time)
values
(#{frozenId}, #{accountTransactionId}, #{userId}, #{callId}, #{sessionId}, #{modelName}, #{question}, #{frozenAmount},
#{frozenType}, #{finalAmount}, #{usageInputTokens}, #{usageOutputTokens}, #{usageTotalTokens},
#{finalizeReason}, #{status}, #{expireAt}, #{createTime}, #{updateTime})
</insert>
<update id="updateByPrimaryKey" parameterType="com.kexue.skills.entity.AccountFrozen">
update account_frozen
set
account_transaction_id = #{accountTransactionId},
user_id = #{userId},
call_id = #{callId},
session_id = #{sessionId},
model_name = #{modelName},
question = #{question},
frozen_amount = #{frozenAmount},
frozen_type = #{frozenType},
final_amount = #{finalAmount},
usage_input_tokens = #{usageInputTokens},
usage_output_tokens = #{usageOutputTokens},
usage_total_tokens = #{usageTotalTokens},
finalize_reason = #{finalizeReason},
status = #{status},
expire_at = #{expireAt},
update_time = #{updateTime}
where frozen_id = #{frozenId}
</update>
<select id="selectBySessionId" resultMap="BaseResultMap" parameterType="java.lang.String">
select
<include refid="Base_Column_List" />
from account_frozen
where session_id = #{sessionId}
</select>
<select id="selectByUserIdAndStatus" resultMap="BaseResultMap">
select
<include refid="Base_Column_List" />
from account_frozen
where user_id = #{userId} and status = #{status}
</select>
<select id="selectExpiredFrozen" resultMap="BaseResultMap" parameterType="java.util.Date">
select
<include refid="Base_Column_List" />
from account_frozen
where status = 'RESERVED' and expire_at is not null and expire_at &lt; #{currentTime}
</select>
</mapper>

View File

@ -15,7 +15,6 @@
<result property="payType" column="pay_type" jdbcType="INTEGER"/>
<result property="businessId" column="business_id" jdbcType="BIGINT"/>
<result property="businessType" column="business_type" jdbcType="VARCHAR"/>
<result property="callId" column="call_id" jdbcType="VARCHAR"/>
<result property="remark" column="remark" jdbcType="VARCHAR"/>
<result property="isExpense" column="is_expense" jdbcType="INTEGER"/>
<result property="inputToken" column="input_token" jdbcType="INTEGER"/>
@ -35,7 +34,7 @@
<select id="queryById" resultMap="AccountTransactionMap">
select
transaction_id, user_id, user_name, transaction_type, amount, before_balance, after_balance, status,
transaction_no, pay_type, business_id, business_type, call_id, remark, is_expense, input_token, output_token,
transaction_no, pay_type, business_id, business_type, remark, is_expense, input_token, output_token,
total_tokens, model_name, question, income_type, create_time, update_time, create_by, update_by, delete_flag
from account_transaction
where transaction_id = #{transactionId}
@ -45,7 +44,7 @@
<select id="getPageList" resultMap="AccountTransactionMap">
select
transaction_id, user_id, user_name, transaction_type, amount, before_balance, after_balance, status,
transaction_no, pay_type, business_id, business_type, call_id, remark, is_expense, input_token, output_token,
transaction_no, pay_type, business_id, business_type, remark, is_expense, input_token, output_token,
total_tokens, model_name, question, income_type, create_time, update_time, create_by, update_by, delete_flag
from account_transaction
<where>
@ -83,7 +82,7 @@
<select id="getList" resultMap="AccountTransactionMap">
select
transaction_id, user_id, user_name, transaction_type, amount, before_balance, after_balance, status,
transaction_no, pay_type, business_id, business_type, call_id, remark, is_expense, input_token, output_token,
transaction_no, pay_type, business_id, business_type, remark, is_expense, input_token, output_token,
total_tokens, model_name, question, income_type, create_time, update_time, create_by, update_by, delete_flag
from account_transaction
<where>
@ -129,7 +128,6 @@
<if test="payType != null">pay_type,</if>
<if test="businessId != null">business_id,</if>
<if test="businessType != null">business_type,</if>
<if test="callId != null">call_id,</if>
<if test="remark != null">remark,</if>
<if test="isExpense != null">is_expense,</if>
<if test="inputToken != null">input_token,</if>
@ -156,7 +154,6 @@
<if test="payType != null">#{payType},</if>
<if test="businessId != null">#{businessId},</if>
<if test="businessType != null">#{businessType},</if>
<if test="callId != null">#{callId},</if>
<if test="remark != null">#{remark},</if>
<if test="isExpense != null">#{isExpense},</if>
<if test="inputToken != null">#{inputToken},</if>
@ -188,7 +185,6 @@
<if test="payType != null">pay_type = #{payType},</if>
<if test="businessId != null">business_id = #{businessId},</if>
<if test="businessType != null">business_type = #{businessType},</if>
<if test="callId != null">call_id = #{callId},</if>
<if test="remark != null">remark = #{remark},</if>
<if test="isExpense != null">is_expense = #{isExpense},</if>
<if test="inputToken != null">input_token = #{inputToken},</if>
@ -223,237 +219,11 @@
<select id="queryByUserId" resultMap="AccountTransactionMap">
select
transaction_id, user_id, user_name, transaction_type, amount, before_balance, after_balance, status,
transaction_no, pay_type, business_id, business_type, call_id, remark, is_expense, input_token, output_token,
transaction_no, pay_type, business_id, business_type, remark, is_expense, input_token, output_token,
total_tokens, model_name, question, income_type, create_time, update_time, create_by, update_by, delete_flag
from account_transaction
where user_id = #{userId}
and delete_flag = 0
order by create_time desc
</select>
<!-- 分页查询充值记录transactionType=1 -->
<select id="getRechargePageList" resultMap="AccountTransactionMap">
select
transaction_id, user_id, user_name, transaction_type, amount, before_balance, after_balance, status,
transaction_no, pay_type, business_id, business_type, call_id, remark, is_expense, input_token, output_token,
total_tokens, model_name, question, income_type, create_time, update_time, create_by, update_by, delete_flag
from account_transaction
<where>
and transaction_type = 1
<if test="userId != null">
and user_id = #{userId}
</if>
<if test="status != null">
and status = #{status}
</if>
<if test="transactionNo != null and transactionNo != ''">
and transaction_no = #{transactionNo}
</if>
<if test="createTimeStart != null">
and create_time <![CDATA[ >= ]]> #{createTimeStart}
</if>
<if test="createTimeEnd != null">
and create_time <![CDATA[ <= ]]> #{createTimeEnd}
</if>
<if test="deleteFlag != null">
and delete_flag = #{deleteFlag}
</if>
</where>
order by create_time desc
</select>
<!-- 分页查询赠送记录transactionType=6 -->
<select id="getGiftPageList" resultMap="AccountTransactionMap">
select
transaction_id, user_id, user_name, transaction_type, amount, before_balance, after_balance, status,
transaction_no, pay_type, business_id, business_type, call_id, remark, is_expense, input_token, output_token,
total_tokens, model_name, question, income_type, create_time, update_time, create_by, update_by, delete_flag
from account_transaction
<where>
and transaction_type = 6
<if test="userId != null">
and user_id = #{userId}
</if>
<if test="status != null">
and status = #{status}
</if>
<if test="transactionNo != null and transactionNo != ''">
and transaction_no = #{transactionNo}
</if>
<if test="createTimeStart != null">
and create_time <![CDATA[ >= ]]> #{createTimeStart}
</if>
<if test="createTimeEnd != null">
and create_time <![CDATA[ <= ]]> #{createTimeEnd}
</if>
<if test="deleteFlag != null">
and delete_flag = #{deleteFlag}
</if>
</where>
order by create_time desc
</select>
<!-- 分页查询消费记录并按callId分组 -->
<resultMap type="com.kexue.skills.entity.dto.ConsumptionGroupedDto" id="ConsumptionGroupedMap">
<result property="transactionId" column="transaction_id" jdbcType="BIGINT"/>
<result property="userId" column="user_id" jdbcType="BIGINT"/>
<result property="userName" column="user_name" jdbcType="VARCHAR"/>
<result property="transactionType" column="transaction_type" jdbcType="INTEGER"/>
<result property="amount" column="amount" jdbcType="DECIMAL"/>
<result property="beforeBalance" column="before_balance" jdbcType="DECIMAL"/>
<result property="afterBalance" column="after_balance" jdbcType="DECIMAL"/>
<result property="status" column="status" jdbcType="INTEGER"/>
<result property="transactionNo" column="transaction_no" jdbcType="VARCHAR"/>
<result property="payType" column="pay_type" jdbcType="INTEGER"/>
<result property="businessId" column="business_id" jdbcType="BIGINT"/>
<result property="businessType" column="business_type" jdbcType="VARCHAR"/>
<result property="callId" column="call_id" jdbcType="VARCHAR"/>
<result property="remark" column="remark" jdbcType="VARCHAR"/>
<result property="isExpense" column="is_expense" jdbcType="INTEGER"/>
<result property="inputToken" column="input_token" jdbcType="INTEGER"/>
<result property="outputToken" column="output_token" jdbcType="INTEGER"/>
<result property="totalTokens" column="total_tokens" jdbcType="INTEGER"/>
<result property="modelName" column="model_name" jdbcType="VARCHAR"/>
<result property="question" column="question" jdbcType="LONGVARCHAR"/>
<result property="incomeType" column="income_type" jdbcType="VARCHAR"/>
<result property="createTime" column="create_time" jdbcType="TIMESTAMP"/>
<result property="updateTime" column="update_time" jdbcType="TIMESTAMP"/>
<result property="createBy" column="create_by" jdbcType="VARCHAR"/>
<result property="updateBy" column="update_by" jdbcType="VARCHAR"/>
<result property="deleteFlag" column="delete_flag" jdbcType="INTEGER"/>
</resultMap>
<select id="getConsumptionGroupedPageList" resultMap="ConsumptionGroupedMap">
SELECT
t.transaction_id,
t.user_id,
t.user_name,
t.transaction_type,
amount_sum.amount,
t.before_balance,
t.after_balance,
t.status,
t.transaction_no,
t.pay_type,
t.business_id,
t.business_type,
t.call_id,
t.remark,
t.is_expense,
token_sum.input_token,
token_sum.output_token,
token_sum.total_tokens,
t.model_name,
first_record.question,
t.income_type,
t.create_time,
t.update_time,
t.create_by,
t.update_by,
t.delete_flag
FROM (
SELECT
MIN(transaction_id) as min_transaction_id,
call_id,
MIN(create_time) as min_create_time,
SUM(amount) as amount
FROM account_transaction
WHERE transaction_type IN (3, 7)
AND call_id IS NOT NULL
AND call_id != ''
AND delete_flag = 0
<if test="userId != null">
AND user_id = #{userId}
</if>
<if test="status != null">
AND status = #{status}
</if>
<if test="createTimeStart != null">
AND create_time <![CDATA[ >= ]]> #{createTimeStart}
</if>
<if test="createTimeEnd != null">
AND create_time <![CDATA[ <= ]]> #{createTimeEnd}
</if>
GROUP BY call_id
) amount_sum
INNER JOIN account_transaction t ON t.transaction_id = amount_sum.min_transaction_id
LEFT JOIN (
SELECT
call_id,
SUM(input_token) as input_token,
SUM(output_token) as output_token,
SUM(total_tokens) as total_tokens
FROM account_transaction
WHERE transaction_type IN (3, 7)
AND call_id IS NOT NULL
AND call_id != ''
AND delete_flag = 0
<if test="userId != null">
AND user_id = #{userId}
</if>
<if test="status != null">
AND status = #{status}
</if>
<if test="createTimeStart != null">
AND create_time <![CDATA[ >= ]]> #{createTimeStart}
</if>
<if test="createTimeEnd != null">
AND create_time <![CDATA[ <= ]]> #{createTimeEnd}
</if>
GROUP BY call_id
) token_sum ON t.call_id = token_sum.call_id
LEFT JOIN (
SELECT
at.call_id,
at.question
FROM account_transaction at
INNER JOIN (
SELECT call_id, MIN(create_time) as earliest_time
FROM account_transaction
WHERE transaction_type IN (3, 7)
AND call_id IS NOT NULL
AND call_id != ''
AND delete_flag = 0
<if test="userId != null">
AND user_id = #{userId}
</if>
<if test="status != null">
AND status = #{status}
</if>
<if test="createTimeStart != null">
AND create_time <![CDATA[ >= ]]> #{createTimeStart}
</if>
<if test="createTimeEnd != null">
AND create_time <![CDATA[ <= ]]> #{createTimeEnd}
</if>
GROUP BY call_id
) earliest ON at.call_id = earliest.call_id AND at.create_time = earliest.earliest_time
) first_record ON t.call_id = first_record.call_id
ORDER BY t.create_time desc
</select>
<!-- 获取消费原始记录列表(用于内存分组) -->
<select id="getConsumptionRawList" resultMap="AccountTransactionMap">
select
transaction_id, user_id, user_name, transaction_type, amount, before_balance, after_balance, status,
transaction_no, pay_type, business_id, business_type, call_id, remark, is_expense, input_token, output_token,
total_tokens, model_name, question, income_type, create_time, update_time, create_by, update_by, delete_flag
from account_transaction
where transaction_type IN (3, 7)
AND delete_flag = 0
And call_id IS NOT NULL
AND call_id != ''
<if test="userId != null">
AND user_id = #{userId}
</if>
<if test="status != null">
AND status = #{status}
</if>
<if test="createTimeStart != null">
AND create_time <![CDATA[ >= ]]> #{createTimeStart}
</if>
<if test="createTimeEnd != null">
AND create_time <![CDATA[ <= ]]> #{createTimeEnd}
</if>
ORDER BY create_time DESC
</select>
</mapper>

View File

@ -121,7 +121,7 @@
<if test="isOfficial != null">
and is_official = #{isOfficial}
</if>
<if test="tagId != null ">
<if test="tagId != null">
and find_in_set(#{tagId}, tags)
</if>
<if test="tagIdList != null and tagIdList.size() > 0">
@ -131,15 +131,12 @@
</foreach>
)
</if>
<if test="tags != null and tags != ''">
and find_in_set(#{tags}, tags)
</if>
</where>
<if test="sortBy != null and sortBy != ''">
order by ${sortBy} ${sortDesc ? 'desc' : 'asc'}, content_id desc
order by ${sortBy} ${sortDesc ? 'desc' : 'asc'}
</if>
<if test="sortBy == null or sortBy == ''">
order by sort asc, create_time desc, content_id desc
order by sort asc, create_time desc
</if>
</select>
@ -206,10 +203,10 @@
</if>
</where>
<if test="queryDto.sortBy != null and queryDto.sortBy != ''">
order by ${queryDto.sortBy} ${queryDto.sortDesc ? 'desc' : 'asc'}, content_id desc
order by ${queryDto.sortBy} ${queryDto.sortDesc ? 'desc' : 'asc'}
</if>
<if test="queryDto.sortBy == null or queryDto.sortBy == ''">
order by sort asc, create_time desc, content_id desc
order by sort asc, create_time desc
</if>
limit #{offset}, #{limit}
</select>
@ -252,16 +249,6 @@
<if test="tagId != null">
and find_in_set(#{tagId}, tags)
</if>
<if test="tagIdList != null and tagIdList.size() > 0">
and (
<foreach collection="tagIdList" item="tagId" separator=" or ">
find_in_set(#{tagId}, tags)
</foreach>
)
</if>
<if test="tags != null and tags != ''">
and find_in_set(#{tags}, tags)
</if>
</where>
</select>
@ -320,17 +307,8 @@
</foreach>
)
</if>
<if test="origin != null and origin != ''">
and origin = #{origin}
</if>
<if test="tags != null and tags != ''">
and tags = #{tags}
</if>
<if test="icon != null and icon != ''">
and icon = #{icon}
</if>
</where>
order by sort asc, create_time desc, content_id desc
order by sort asc, create_time desc
</select>
<!--新增所有列-->

View File

@ -12,9 +12,6 @@
<result property="outputPerCent" column="output_per_cent" jdbcType="BIGINT"/>
<result property="unit" column="unit" jdbcType="VARCHAR"/>
<result property="remark" column="remark" jdbcType="VARCHAR"/>
<result property="minTokens" column="min_tokens" jdbcType="BIGINT"/>
<result property="maxTokens" column="max_tokens" jdbcType="BIGINT"/>
<result property="outputMode" column="output_mode" jdbcType="VARCHAR"/>
<result property="createdTime" column="created_time" jdbcType="TIMESTAMP"/>
<result property="updatedTime" column="updated_time" jdbcType="TIMESTAMP"/>
</resultMap>
@ -22,7 +19,7 @@
<!--查询单个-->
<select id="queryById" resultMap="ModelPriceMap">
select
id, vendor, model_name, input_price, output_price, input_per_cent, output_per_cent, unit, remark, min_tokens, max_tokens, output_mode, created_time, updated_time
id, vendor, model_name, input_price, output_price, input_per_cent, output_per_cent, unit, remark, created_time, updated_time
from model_price
where id = #{id}
</select>
@ -30,7 +27,7 @@
<!--通过模型名称查询-->
<select id="queryByModelName" resultMap="ModelPriceMap">
select
id, vendor, model_name, input_price, output_price, input_per_cent, output_per_cent, unit, remark, min_tokens, max_tokens, output_mode, created_time, updated_time
id, vendor, model_name, input_price, output_price, input_per_cent, output_per_cent, unit, remark, created_time, updated_time
from model_price
where model_name = #{modelName}
</select>
@ -38,7 +35,7 @@
<!--分页查询-->
<select id="getPageList" resultMap="ModelPriceMap">
select
id, vendor, model_name, input_price, output_price, input_per_cent, output_per_cent, unit, remark, min_tokens, max_tokens, output_mode, created_time, updated_time
id, vendor, model_name, input_price, output_price, input_per_cent, output_per_cent, unit, remark, created_time, updated_time
from model_price
<where>
<if test="vendor != null and vendor != ''">
@ -56,7 +53,7 @@
<!--查询列表-->
<select id="getList" resultMap="ModelPriceMap">
select
id, vendor, model_name, input_price, output_price, input_per_cent, output_per_cent, unit, remark, min_tokens, max_tokens, output_mode, created_time, updated_time
id, vendor, model_name, input_price, output_price, input_per_cent, output_per_cent, unit, remark, created_time, updated_time
from model_price
<where>
<if test="vendor != null and vendor != ''">
@ -80,9 +77,6 @@
<if test="outputPerCent != null">output_per_cent,</if>
<if test="unit != null">unit,</if>
<if test="remark != null">remark,</if>
<if test="minTokens != null">min_tokens,</if>
<if test="maxTokens != null">max_tokens,</if>
<if test="outputMode != null">output_mode,</if>
<if test="createdTime != null">created_time,</if>
<if test="updatedTime != null">updated_time,</if>
</trim>
@ -95,9 +89,6 @@
<if test="outputPerCent != null">#{outputPerCent},</if>
<if test="unit != null">#{unit},</if>
<if test="remark != null">#{remark},</if>
<if test="minTokens != null">#{minTokens},</if>
<if test="maxTokens != null">#{maxTokens},</if>
<if test="outputMode != null">#{outputMode},</if>
<if test="createdTime != null">#{createdTime},</if>
<if test="updatedTime != null">#{updatedTime},</if>
</trim>
@ -115,9 +106,6 @@
<if test="outputPerCent != null">output_per_cent = #{outputPerCent},</if>
<if test="unit != null">unit = #{unit},</if>
<if test="remark != null">remark = #{remark},</if>
<if test="minTokens != null">min_tokens = #{minTokens},</if>
<if test="maxTokens != null">max_tokens = #{maxTokens},</if>
<if test="outputMode != null">output_mode = #{outputMode},</if>
<if test="createdTime != null">created_time = #{createdTime},</if>
<if test="updatedTime != null">updated_time = #{updatedTime},</if>
</set>
@ -130,19 +118,4 @@
where id = #{id}
</delete>
<!--根据模型名称、输出模式和token数量查询价格规则-->
<select id="queryByModelNameAndOutputModeAndTokens" resultMap="ModelPriceMap">
<![CDATA[
select
id, vendor, model_name, input_price, output_price, input_per_cent, output_per_cent, unit, remark, min_tokens, max_tokens, output_mode, created_time, updated_time
from model_price
where model_name = #{modelName}
and output_mode = #{outputMode}
and min_tokens < #{tokens}
and (max_tokens = -1 or max_tokens >= #{tokens})
order by min_tokens desc
limit 1
]]>
</select>
</mapper>

View File

@ -1,90 +0,0 @@
<?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>

View File

@ -3,39 +3,21 @@
<mapper namespace="com.kexue.skills.mapper.SysLogMapper">
<resultMap type="com.kexue.skills.entity.SysLog" id="SysLogMap">
<result property="logId" column="LOG_ID" jdbcType="BIGINT"/>
<result property="traceId" column="TRACE_ID" jdbcType="VARCHAR"/>
<result property="description" column="DESCRIPTION" jdbcType="VARCHAR"/>
<result property="module" column="MODULE" jdbcType="VARCHAR"/>
<result property="requestUrl" column="REQUEST_URL" jdbcType="VARCHAR"/>
<result property="requestMethod" column="REQUEST_METHOD" jdbcType="VARCHAR"/>
<result property="requestHeaders" column="REQUEST_HEADERS" jdbcType="VARCHAR"/>
<result property="requestBody" column="REQUEST_BODY" jdbcType="VARCHAR"/>
<result property="statusCode" column="STATUS_CODE" jdbcType="INTEGER"/>
<result property="responseHeaders" column="RESPONSE_HEADERS" jdbcType="VARCHAR"/>
<result property="responseBody" column="RESPONSE_BODY" jdbcType="VARCHAR"/>
<result property="timeTaken" column="TIME_TAKEN" jdbcType="BIGINT"/>
<result property="ip" column="IP" jdbcType="VARCHAR"/>
<result property="address" column="ADDRESS" jdbcType="VARCHAR"/>
<result property="browser" column="BROWSER" jdbcType="VARCHAR"/>
<result property="os" column="OS" jdbcType="VARCHAR"/>
<result property="status" column="STATUS" jdbcType="TINYINT"/>
<result property="errorMsg" column="ERROR_MSG" jdbcType="VARCHAR"/>
<result property="createUser" column="CREATE_USER" jdbcType="BIGINT"/>
<result property="createTime" column="CREATE_TIME" jdbcType="TIMESTAMP"/>
<result property="updateTime" column="UPDATE_TIME" jdbcType="TIMESTAMP"/>
<result property="deleteFlag" column="DELETE_FLAG" jdbcType="TINYINT"/>
<result property="createBy" column="CREATE_BY" jdbcType="VARCHAR"/>
<result property="updateBy" column="UPDATE_BY" jdbcType="VARCHAR"/>
<result property="logId" column="LOG_ID" jdbcType="INTEGER"/>
<result property="userId" column="USER_ID" jdbcType="VARCHAR"/>
<result property="userName" column="USER_NAME" jdbcType="VARCHAR"/>
<result property="logType" column="LOG_TYPE" jdbcType="VARCHAR"/>
<result property="logContent" column="LOG_CONTENT" jdbcType="VARCHAR"/>
<result property="serverIp" column="SERVER_IP" jdbcType="VARCHAR"/>
<result property="clientIp" column="CLIENT_IP" jdbcType="VARCHAR"/>
<result property="logTime" column="LOG_TIME" jdbcType="VARCHAR"/>
<result property="note" column="NOTE" jdbcType="VARCHAR"/>
</resultMap>
<!--查询单个-->
<select id="queryById" resultMap="SysLogMap">
select
LOG_ID, TRACE_ID, DESCRIPTION, MODULE, REQUEST_URL, REQUEST_METHOD,
REQUEST_HEADERS, REQUEST_BODY, STATUS_CODE, RESPONSE_HEADERS, RESPONSE_BODY,
TIME_TAKEN, IP, ADDRESS, BROWSER, OS, STATUS, ERROR_MSG, CREATE_USER,
CREATE_TIME, UPDATE_TIME, DELETE_FLAG, CREATE_BY, UPDATE_BY
LOG_ID, USER_ID, USER_NAME, LOG_TYPE, LOG_CONTENT, SERVER_IP, CLIENT_IP, LOG_TIME, NOTE
from sys_log
where LOG_ID = #{logId}
</select>
@ -43,44 +25,43 @@
<!--查询指定行数据-->
<select id="getPageList" resultMap="SysLogMap">
select
LOG_ID, TRACE_ID, DESCRIPTION, MODULE, REQUEST_URL, REQUEST_METHOD,
REQUEST_HEADERS, REQUEST_BODY, STATUS_CODE, RESPONSE_HEADERS, RESPONSE_BODY,
TIME_TAKEN, IP, ADDRESS, BROWSER, OS, STATUS, ERROR_MSG, CREATE_USER,
CREATE_TIME, UPDATE_TIME, DELETE_FLAG, CREATE_BY, UPDATE_BY
LOG_ID, USER_ID, USER_NAME, LOG_TYPE, LOG_CONTENT, SERVER_IP, CLIENT_IP, LOG_TIME, NOTE
from sys_log
<where>
<if test="logId != null">
and LOG_ID = #{logId}
</if>
<if test="module != null and module != ''">
and MODULE = #{module}
<if test="userId != null and userId != ''">
and USER_ID = #{userId}
</if>
<if test="description != null and description != ''">
and DESCRIPTION like concat('%', #{description}, '%')
<if test="userName != null and userName != ''">
and USER_NAME = #{userName}
</if>
<if test="ip != null and ip != ''">
and IP = #{ip}
<if test="logType != null and logType != ''">
and LOG_TYPE = #{logType}
</if>
<if test="status != null">
and STATUS = #{status}
<if test="logContent != null and logContent != ''">
and LOG_CONTENT = #{logContent}
</if>
<if test="startTime != null">
and CREATE_TIME &gt;= #{startTime}
<if test="serverIp != null and serverIp != ''">
and SERVER_IP = #{serverIp}
</if>
<if test="endTime != null">
and CREATE_TIME &lt;= #{endTime}
<if test="clientIp != null and clientIp != ''">
and CLIENT_IP = #{clientIp}
</if>
<if test="logTime != null and logTime != ''">
and LOG_TIME = #{logTime}
</if>
<if test="note != null and note != ''">
and NOTE = #{note}
</if>
</where>
order by CREATE_TIME desc
</select>
<!--查询指定行数据-->
<select id="queryAllByLimit" resultMap="SysLogMap">
select
LOG_ID, TRACE_ID, DESCRIPTION, MODULE, REQUEST_URL, REQUEST_METHOD,
REQUEST_HEADERS, REQUEST_BODY, STATUS_CODE, RESPONSE_HEADERS, RESPONSE_BODY,
TIME_TAKEN, IP, ADDRESS, BROWSER, OS, STATUS, ERROR_MSG, CREATE_USER,
CREATE_TIME, UPDATE_TIME, DELETE_FLAG, CREATE_BY, UPDATE_BY
LOG_ID, USER_ID, USER_NAME, LOG_TYPE, LOG_CONTENT, SERVER_IP, CLIENT_IP, LOG_TIME, NOTE
from sys_log
limit #{offset}, #{limit}
</select>
@ -88,45 +69,43 @@
<!--通过实体作为筛选条件查询-->
<select id="queryAll" resultMap="SysLogMap">
select
LOG_ID, TRACE_ID, DESCRIPTION, MODULE, REQUEST_URL, REQUEST_METHOD,
REQUEST_HEADERS, REQUEST_BODY, STATUS_CODE, RESPONSE_HEADERS, RESPONSE_BODY,
TIME_TAKEN, IP, ADDRESS, BROWSER, OS, STATUS, ERROR_MSG, CREATE_USER,
CREATE_TIME, UPDATE_TIME, DELETE_FLAG, CREATE_BY, UPDATE_BY
LOG_ID, USER_ID, USER_NAME, LOG_TYPE, LOG_CONTENT, SERVER_IP, CLIENT_IP, LOG_TIME, NOTE
from sys_log
<where>
<if test="logId != null">
and LOG_ID = #{logId}
</if>
<if test="module != null and module != ''">
and MODULE = #{module}
<if test="userId != null and userId != ''">
and USER_ID = #{userId}
</if>
<if test="description != null and description != ''">
and DESCRIPTION like concat('%', #{description}, '%')
<if test="userName != null and userName != ''">
and USER_NAME = #{userName}
</if>
<if test="ip != null and ip != ''">
and IP = #{ip}
<if test="logType != null and logType != ''">
and LOG_TYPE = #{logType}
</if>
<if test="status != null">
and STATUS = #{status}
<if test="logContent != null and logContent != ''">
and LOG_CONTENT = #{logContent}
</if>
<if test="serverIp != null and serverIp != ''">
and SERVER_IP = #{serverIp}
</if>
<if test="clientIp != null and clientIp != ''">
and CLIENT_IP = #{clientIp}
</if>
<if test="logTime != null and logTime != ''">
and LOG_TIME = #{logTime}
</if>
<if test="note != null and note != ''">
and NOTE = #{note}
</if>
</where>
order by CREATE_TIME desc
</select>
<!--新增所有列-->
<insert id="insert" keyProperty="logId" useGeneratedKeys="true">
insert into sys_log(
TRACE_ID, DESCRIPTION, MODULE, REQUEST_URL, REQUEST_METHOD,
REQUEST_HEADERS, REQUEST_BODY, STATUS_CODE, RESPONSE_HEADERS, RESPONSE_BODY,
TIME_TAKEN, IP, ADDRESS, BROWSER, OS, STATUS, ERROR_MSG, CREATE_USER,
CREATE_TIME, UPDATE_TIME, DELETE_FLAG, CREATE_BY, UPDATE_BY
)
values (
#{traceId}, #{description}, #{module}, #{requestUrl}, #{requestMethod},
#{requestHeaders}, #{requestBody}, #{statusCode}, #{responseHeaders}, #{responseBody},
#{timeTaken}, #{ip}, #{address}, #{browser}, #{os}, #{status}, #{errorMsg}, #{createUser},
#{createTime}, #{updateTime}, #{deleteFlag}, #{createBy}, #{updateBy}
)
insert into sys_log(USER_ID, USER_NAME, LOG_TYPE, LOG_CONTENT, SERVER_IP, CLIENT_IP, LOG_TIME, NOTE)
values (#{userId}, #{userName}, #{logType}, #{logContent}, #{serverIp}, #{clientIp}, #{logTime}, #{note})
</insert>
@ -155,62 +134,29 @@
<update id="update">
update sys_log
<set>
<if test="traceId != null and traceId != ''">
TRACE_ID = #{traceId},
<if test="userId != null and userId != ''">
USER_ID = #{userId},
</if>
<if test="description != null and description != ''">
DESCRIPTION = #{description},
<if test="userName != null and userName != ''">
USER_NAME = #{userName},
</if>
<if test="module != null and module != ''">
MODULE = #{module},
<if test="logType != null and logType != ''">
LOG_TYPE = #{logType},
</if>
<if test="requestUrl != null and requestUrl != ''">
REQUEST_URL = #{requestUrl},
<if test="logContent != null and logContent != ''">
LOG_CONTENT = #{logContent},
</if>
<if test="requestMethod != null and requestMethod != ''">
REQUEST_METHOD = #{requestMethod},
<if test="serverIp != null and serverIp != ''">
SERVER_IP = #{serverIp},
</if>
<if test="requestHeaders != null and requestHeaders != ''">
REQUEST_HEADERS = #{requestHeaders},
<if test="clientIp != null and clientIp != ''">
CLIENT_IP = #{clientIp},
</if>
<if test="requestBody != null and requestBody != ''">
REQUEST_BODY = #{requestBody},
<if test="logTime != null and logTime != ''">
LOG_TIME = #{logTime},
</if>
<if test="statusCode != null">
STATUS_CODE = #{statusCode},
</if>
<if test="responseHeaders != null and responseHeaders != ''">
RESPONSE_HEADERS = #{responseHeaders},
</if>
<if test="responseBody != null and responseBody != ''">
RESPONSE_BODY = #{responseBody},
</if>
<if test="timeTaken != null">
TIME_TAKEN = #{timeTaken},
</if>
<if test="ip != null and ip != ''">
IP = #{ip},
</if>
<if test="address != null and address != ''">
ADDRESS = #{address},
</if>
<if test="browser != null and browser != ''">
BROWSER = #{browser},
</if>
<if test="os != null and os != ''">
OS = #{os},
</if>
<if test="status != null">
STATUS = #{status},
</if>
<if test="errorMsg != null and errorMsg != ''">
ERROR_MSG = #{errorMsg},
</if>
<if test="createUser != null">
CREATE_USER = #{createUser},
</if>
<if test="updateBy != null and updateBy != ''">
UPDATE_BY = #{updateBy},
<if test="note != null and note != ''">
NOTE = #{note},
</if>
</set>
where LOG_ID = #{logId}

View File

@ -15,16 +15,12 @@
<result property="enable" column="enable" jdbcType="OTHER"/>
<result property="deleteFlag" column="delete_flag" jdbcType="OTHER"/>
<result property="sessionId" column="session_id" jdbcType="VARCHAR"/>
<result property="inviteCode" column="invite_code" jdbcType="VARCHAR"/>
<result property="invitedCode" column="invited_code" jdbcType="VARCHAR"/>
<result property="invitedBy" column="invited_by" jdbcType="BIGINT"/>
<result property="userIcon" column="user_icon" jdbcType="VARCHAR"/>
</resultMap>
<!--查询单个-->
<select id="queryById" resultMap="SysUserMap">
select
user_id, user_name, pwd, real_name, tel, email, salt, remark, create_time, enable, delete_flag, session_id, invite_code, invited_code, invited_by, user_icon
user_id, user_name, pwd, real_name, tel, email, salt, remark, create_time, enable, delete_flag, session_id
from sys_user
where user_id = #{userId}
</select>
@ -32,7 +28,7 @@
<!--查询指定行数据-->
<select id="getPageList" resultMap="SysUserMap">
select
user_id, user_name, pwd, real_name, tel, email, salt, remark, create_time, enable, delete_flag, session_id, invite_code, invited_code, invited_by, user_icon
user_id, user_name, pwd, real_name, tel, email, salt, remark, create_time, enable, delete_flag, session_id
from sys_user
<where>
<if test="userId != null">
@ -74,7 +70,7 @@
<!--查询指定行数据-->
<select id="queryAllByLimit" resultMap="SysUserMap">
select
user_id, user_name, pwd, real_name, tel, email, salt, remark, create_time, enable, delete_flag, session_id, invite_code, invited_code, invited_by, user_icon
user_id, user_name, pwd, real_name, tel, email, salt, remark, create_time, enable, delete_flag, session_id
from sys_user
limit #{offset}, #{limit}
</select>
@ -82,7 +78,7 @@
<!--通过实体作为筛选条件查询-->
<select id="queryAll" resultMap="SysUserMap">
select
user_id, user_name, pwd, real_name, tel, email, salt, remark, create_time, enable, delete_flag, session_id, invite_code, invited_code, invited_by, user_icon
user_id, user_name, pwd, real_name, tel, email, salt, remark, create_time, enable, delete_flag, session_id
from sys_user
<where>
<if test="userId != null">
@ -123,8 +119,8 @@
<!--新增所有列-->
<insert id="insert" keyProperty="userId" useGeneratedKeys="true">
insert into sys_user(user_name, pwd, real_name, tel, email, salt, remark, create_time, enable, delete_flag, session_id, invite_code, invited_code, invited_by, user_icon)
values (#{userName}, #{pwd}, #{realName}, #{tel}, #{email}, #{salt}, #{remark}, #{createTime}, #{enable}, #{deleteFlag}, #{sessionId}, #{inviteCode}, #{invitedCode}, #{invitedBy}, #{userIcon})
insert into sys_user(user_name, pwd, real_name, tel, email, salt, remark, create_time, enable, delete_flag, session_id)
values (#{userName}, #{pwd}, #{realName}, #{tel}, #{email}, #{salt}, #{remark}, #{createTime}, #{enable}, #{deleteFlag}, #{sessionId})
</insert>
@ -186,18 +182,6 @@
<if test="sessionId != null and sessionId != ''">
session_id = #{sessionId},
</if>
<if test="inviteCode != null and inviteCode != ''">
invite_code = #{inviteCode},
</if>
<if test="invitedCode != null and invitedCode != ''">
invited_code = #{invitedCode},
</if>
<if test="invitedBy != null">
invited_by = #{invitedBy},
</if>
<if test="userIcon != null and userIcon != ''">
user_icon = #{userIcon},
</if>
</set>
where user_id = #{userId}
</update>
@ -209,7 +193,7 @@
<select id="getByUsername" resultMap="SysUserMap">
select
user_id, user_name, pwd, real_name, tel, email, salt, remark, create_time, enable, delete_flag, session_id, invite_code, invited_code, invited_by, user_icon
user_id, user_name, pwd, real_name, tel, email, salt, remark, create_time, enable, delete_flag, session_id
from sys_user
where (user_name = #{userName} or tel = #{userName})
and delete_flag = 0
@ -218,7 +202,7 @@
<select id="getByTel" resultMap="SysUserMap">
select
user_id, user_name, pwd, real_name, tel, email, salt, remark, create_time, enable, delete_flag, session_id, invite_code, invited_code, invited_by, user_icon
user_id, user_name, pwd, real_name, tel, email, salt, remark, create_time, enable, delete_flag, session_id
from sys_user
where tel = #{tel}
and delete_flag = 0
@ -226,20 +210,11 @@
</select>
<select id="getBySessionId" resultMap="SysUserMap">
select
user_id, user_name, pwd, real_name, tel, email, salt, remark, create_time, enable, delete_flag, session_id, invite_code, invited_code, invited_by, user_icon
user_id, user_name, pwd, real_name, tel, email, salt, remark, create_time, enable, delete_flag, session_id
from sys_user
where session_id = #{sessionId}
and delete_flag = 0
limit 1
</select>
<select id="getByInviteCode" resultMap="SysUserMap">
select
user_id, user_name, pwd, real_name, tel, email, salt, remark, create_time, enable, delete_flag, session_id, invite_code, invited_code, invited_by, user_icon
from sys_user
where invite_code = #{inviteCode}
and delete_flag = 0
limit 1
</select>
</mapper>