Compare commits

...

39 Commits

Author SHA1 Message Date
wangzhiwei a8faaebc79 refactor(account): 重构账户冻结功能实现
- 更新冻结类型枚举说明,统一为token和RMB类型
- 添加日志记录功能并引入常量定义
- 重构创建冻结单方法,增加参数验证和业务流程优化
- 重构释放冻结单方法,完善金额计算和账户更新逻辑
- 修改模型价格查询接口返回类型为列表
- 添加支付订单和系统日志查询条件字段
- 调整应用配置文件环境设置和Redis连接参数
2026-04-24 17:24:48 +08:00
wangzhiwei a608b7a754 refactor(account): 重构账户冻结功能实现
- 更新冻结类型枚举说明,统一为token和RMB类型
- 添加日志记录功能并引入常量定义
- 重构创建冻结单方法,增加参数验证和业务流程优化
- 重构释放冻结单方法,完善金额计算和账户更新逻辑
- 修改模型价格查询接口返回类型为列表
- 添加支付订单和系统日志查询条件字段
- 调整应用配置文件环境设置和Redis连接参数
2026-04-24 14:28:32 +08:00
wangzhiwei fc7ba98d6e feat(billing): 完善模型定价系统支持分段计费和不同输出模式
- 新增 minTokens、maxTokens 和 outputMode 字段到 ModelPrice 实体
- 实现基于 token 区间和输出模式的精细化计费逻辑
- 添加 queryByModelNameAndOutputModeAndTokens 方法支持动态定价查询
- 在 AccountFrozenServiceImpl 中实现 Qwen 3.5 Plus 模型名映射
- 优化账户冻结服务中的 token 费用计算流程
- 更新 AccountService 中的余额检查和交易记录逻辑
2026-04-22 17:04:27 +08:00
wangzhiwei eef2b68291 config(session): 禁用会话持久化并添加用户头像上传路径配置
- 禁用会话持久化,避免启动时恢复损坏的会话文件
- 添加 userIconPath 配置项用于用户头像上传路径
- 在开发环境配置中同步添加用户头像路径配置
2026-04-21 09:59:46 +08:00
wangzhiwei 891f60d5a5 feat(account): 添加账户交易记录分页查询功能
- 新增充值记录、消费记录和赠送记录的分页查询接口
- 实现消费记录按callId分组查询功能
- 添加账户交易记录DTO和分组查询DTO定义
- 在AccountController中添加日志注解和相关API端点
- 优化AccountServiceImpl中的数据库操作方法调用
- 修改AccountTransactionMapper.xml增加相应的SQL查询语句
- 添加创建时间范围筛选功能到交易记录查询中
- 实现CMS内容管理的路径更新功能和标签查询优化
2026-04-21 09:32:17 +08:00
wangzhiwei 0d934f7287 feat(account): 添加问题字段和调用ID关联功能
- 在AccountFrozen实体和DTO中添加question字段用于记录用户问题或需求
- 在AccountTransaction实体中添加callId字段用于关联冻结单调用
- 更新数据库映射文件中的查询和插入语句以支持新增字段
- 在账户冻结服务中实现question字段的赋值逻辑
- 在冻结单释放时将question和callId传递到交易记录中
- 添加数据库表结构变更SQL脚本为account_frozen表增加question字段
- 添加数据库表结构变更SQL脚本为account_transaction表增加call_id字段及索引
- 在用户注册流程中实现邀请奖励机制为被邀请用户赠送积分
2026-04-14 21:38:05 +08:00
wangzhiwei e5433853ec feat(CmsContent): 添加从ZIP文件批量导入Excel数据功能
- 新增 importFromZip 接口支持ZIP文件批量导入
- 实现ZIP文件解压并遍历内部Excel文件逻辑
- 集成现有Excel导入方法处理单个文件导入
- 添加Office临时文件过滤机制
- 实现导入进度和结果统计功能
- 提供详细的导入日志记录和错误处理
2026-04-13 15:14:31 +08:00
wangzhiwei 7841b94872 feat(account): 添加账户扣费系数配置和邀请码功能
- 在AccountFrozenController中添加跨域注解并修改请求路径为/api/accountFrozen
- 引入AccountDeductionProperties配置类,支持动态扣费系数设置
- 修改账户冻结逻辑,将原来的1元=100积分改为1分=1积分,并应用扣费系数
- 在SysUser实体中添加inviteCode、invitedCode、invitedBy字段
- 实现用户注册时自动生成邀请码功能
- 添加邀请码验证和奖励机制,邀请成功赠送100积分
- 优化PhoneLoginDto添加邀请码参数
- 完善数据库映射文件支持新字段操作
2026-04-13 14:50:13 +08:00
wangzhiwei 71edec48f7 fix(account): 修复账户余额计算逻辑
- 移除实际扣减小于预扣减时的余额调整逻辑
- 删除最终扣减为0时预扣减加回余额的处理
- 简化账户余额更新流程,避免重复计算
2026-04-11 21:49:55 +08:00
wangzhiwei e651e73fa2 feat(account): 实现账户积分系统及套餐功能
- 将账户余额系统改造为积分系统,充值金额按1元=100积分计算
- 新增套餐配置功能,支持套餐购买并获取基础额度和赠送额度
- 在账户冻结功能中集成模型价格计算,根据预估tokens自动计算冻结金额
- 更新支付流程以支持套餐ID关联和积分计算
- 修改全局异常处理器返回格式,统一使用CommonResult
- 优化账户交易记录的备注信息显示
- 添加雪花算法配置用于分布式ID生成
- 扩展账户冻结DTO添加预估tokens字段
- 重构账户服务中的金额处理逻辑为积分处理逻辑
- 实现套餐配置的CRUD操作接口和相关实体类
- 更新支付回调逻辑以正确处理套餐购买场景
2026-04-11 21:11:53 +08:00
wangzhiwei 51fce1ece6 feat(account): 添加账户冻结单功能和雪花算法ID生成器
- 在IDUtils中集成雪花算法ID生成器,支持分布式ID生成
- 添加AccountFrozen实体类,定义冻结单数据结构和字段
- 创建AccountFrozenController提供冻结单创建和释放接口
- 实现AccountFrozenService和AccountFrozenServiceImpl业务逻辑
- 添加AccountFrozenDto和AccountReleaseDto数据传输对象
- 创建AccountFrozenMapper和对应XML映射文件进行数据库操作
- 实现冻结单创建、释放、查询等完整业务流程
- 添加账户冻结单相关异常返回码定义
- 实现定时任务自动清理过期冻结单功能
- 支持基于tokens使用量的费用计算和扣减逻辑
2026-04-11 15:11:53 +08:00
wangzhiwei 8a80b31c2f chore(database): 重构数据库结构并移除旧的SQL脚本
- 移除旧的add_account_columns.sql脚本
- 移除旧的alter_cms_content.sql脚本
- 移除旧的init_cms_category.sql脚本
- 新增完整的技能系统数据库表结构脚本
- 包含账户、内容管理、支付、分类等完整表结构
- 整合了所有必要的数据库表定义和索引配置
2026-04-10 17:42:11 +08:00
wangzhiwei 071f6aafbc feat(skills): 优化文件内容读取逻辑并支持多种文件格式
- 添加 Base64 编码支持用于图片处理
- 区分文本文件和二进制文件,只读取文本文件内容
- 为图片格式的 Base64 字符串实现按行分割(每行80字符)
- 扩展文件格式识别,支持 js、ts、html、css、xml、csv 等格式
- 添加图片格式支持(png、jpg、jpeg、gif、bmp、webp、svg、ico)
- 增加 pdf、压缩包、音视频等二进制文件格式识别
- 实现二进制文件格式判断方法,避免读取非文本内容
2026-04-10 17:31:20 +08:00
wangzhiwei a5631caab3 feat(controller): 添加上传本地技能压缩包V2接口
- 在SkillGenController中新增uploadSkillV4方法,支持上传zip或rar格式技能包
- 新增CmsContent uploadSkillV4接口实现技能包解析和内容生成
- 集成SevenZipJBinding库支持rar格式解压
- 实现压缩包目录树结构解析功能
- 添加YAML内容生成和技能信息提取功能
- 完善异常处理和错误信息返回机制
2026-04-10 15:47:34 +08:00
wangzhiwei b548bfbc14 feat(controller): 添加根据技能描述生成介绍和上传yaml内容功能
- 新增 genIntroduceByDescription 接口用于根据技能描述生成技能介绍
- 新增 uploadSkillV3 接口支持直接上传yaml内容生成技能
- 添加 YamlContentDto 数据传输对象
- 实现 genIntroduceByDescription 服务方法调用Deepseek API生成技能介绍
- 实现 uploadSkillV3 方法解析yaml内容并保存到数据库
- 添加 YamlToMapUtil 工具类用于yaml文件和字符串解析
- 修改数据库插入逻辑,添加默认图标获取功能
2026-04-10 09:32:34 +08:00
wangzhiwei fd0e1c893f fix(payment): 更新支付宝支付回调地址配置
- 修改支付宝returnUrl从/api/pay/alipay/trade/return调整为/pay/alipay
- 保持notifyUrl、密钥和其他支付网关配置不变
- 确保支付流程的回调路径正确指向前端页面
2026-04-08 16:21:50 +08:00
wangzhiwei d527e1ad0f feat(skills): zip格式按照原样结构解析调整完成 2026-04-08 16:17:19 +08:00
wangzhiwei 713c28a534 feat(skills): rar格式按照原样结构解析调整完成 2026-04-08 15:30:21 +08:00
wangzhiwei bd252efd20 feat(user): 添加用户缓存更新功能
- 在用户信息更新后判断是否为当前登录用户并更新缓存
- 实现updateUserCache方法同步Redis中的LoginUser信息
- 添加token验证确保只更新当前用户的缓存
- 避免密码信息泄露通过设置pwd为null
- 添加异常处理和日志记录确保缓存更新稳定性
2026-04-07 14:40:22 +08:00
wangzhiwei 44c04f81e2 fix(auth): 修复用户登录时旧token清理逻辑
- 在用户登录时检查并使旧token失效的逻辑中添加异常处理
- 为Redis操作和Sa-Token操作分别添加try-catch异常捕获
- 增加详细的日志记录来跟踪token清理过程
- 统一中文化日志信息中的token相关术语
- 确保即使在操作失败时也能继续执行后续清理步骤
2026-04-07 14:13:16 +08:00
wangzhiwei e2fa8d7517 feat(login): 优化用户登出功能并增强日志记录
- 添加 CacheManager 导入以支持缓存管理
- 从 Redis 中删除用户信息后增加从缓存中移除 token 映射逻辑
- 添加用户名和 token 的双向缓存清理机制
- 集成 Sa-Token 登出流程并完善异常处理
- 添加登出成功和异常的日志记录功能
- 优化注释描述提高代码可读性
2026-04-07 14:12:36 +08:00
wangzhiwei 8e25f10b27 feat(login): 优化用户登出功能并增强日志记录
- 添加 CacheManager 导入以支持缓存管理
- 从 Redis 中删除用户信息后增加从缓存中移除 token 映射逻辑
- 添加用户名和 token 的双向缓存清理机制
- 集成 Sa-Token 登出流程并完善异常处理
- 添加登出成功和异常的日志记录功能
- 优化注释描述提高代码可读性
2026-04-07 14:11:53 +08:00
wangzhiwei 7ae6e19ae1 fix(content): 修复内容查询分页参数传递问题
- 修复了标签查询中分页参数未正确传递的问题
- 将硬编码的页码改为使用查询参数的页码值
- 修改页面大小计算方式为固定1000加上查询参数的页面大小
- 确保内容查询能够正确使用前端传入的分页参数
2026-04-07 14:10:23 +08:00
wangzhiwei 47831276ec fix(config): 更新支付宝支付回调URL配置
- 修改开发环境returnUrl从前端页面跳转改为后端接口地址
- 修改生产环境returnUrl从前端页面跳转改为后端接口地址
- 统一支付宝支付流程的返回地址到API接口处理
2026-04-03 11:02:16 +08:00
wangzhiwei bad416aeab feat(account): 实现账户功能完善与支付集成
- 集成Sa-Token框架实现用户身份认证和权限管理
- 修改账户查询接口,基于当前登录用户自动获取用户ID
- 优化账户余额操作,增加空值检查和BigDecimal精度处理
- 添加支付订单状态查询功能,支持按订单ID或订单号查询
- 实现支付回调处理,在微信和支付宝支付成功后自动更新账户余额
- 完善内容购买流程,基于当前登录用户进行权限验证
- 优化CMS内容推荐算法,改进标签匹配和去重逻辑
- 更新AI技能生成服务,优化YAML输出格式和目录结构规范
2026-04-02 18:14:29 +08:00
wangzhiwei f2b8a735f2 refactor(payment): 重构支付功能并优化token消费接口
- 修改reduceBalanceWithToken方法使用TokenConsumptionDto参数对象
- 添加通过会话ID查询用户的功能并验证用户会话有效性
- 在支付控制器中更新订单信息时保存二维码内容
- 为PaymentOrder实体添加codeUrl和qrCode字段支持
- 更新SysUserMapper添加getBySessionId查询方法
- 优化微信和支付宝支付回调日志记录
- 改进token消费参数验证逻辑
- 调整数据库映射文件以支持新增字段
2026-04-01 15:50:58 +08:00
wangzhiwei 770f50302e feat(account): 扩展账户余额管理功能
- 新增可提现余额和不可提现余额字段,完善账户余额结构
- 添加充值接口支持微信和支付宝支付方式
- 实现token消费转换扣费功能,支持AI模型调用计费
- 增加管理员赠送金额接口,仅管理员可调用
- 完善交易记录查询功能,支持用户查看历史交易明细
- 集成模型价格服务,实现token费用自动计算
- 重构余额增加逻辑,区分可提现和不可提现金额
- 优化账户实体类初始化逻辑,确保余额字段正确设置
- 更新交易记录实体类,新增token相关和收支类型字段
- 修改支付配置,更新微信和支付宝回调地址为生产环境域名
2026-04-01 11:52:33 +08:00
wangzhiwei 3df611f809 feat(content): 添加从目录导入Excel功能并优化内容管理
- 新增从指定目录批量导入Excel数据到CmsContent的功能
- 添加ImportPathDto请求参数实体类
- 实现importFromPath方法支持目录遍历和文件批量导入
- 添加truncateTable方法用于清空表数据
- 优化Excel导入逻辑增加异常处理和空值检查
- 调整批量处理大小从100改为10
- 更新审核状态和发布状态的描述文案
- 修复分享次数和官方标识字段的默认值设置
- 将Servlet API从javax迁移到jakarta
- 更新README.md完善项目文档
- 优化技能解析逻辑支持多层级目录结构
- 修复AI模型生成中的标签选择和参数验证问题
2026-03-23 11:38:20 +08:00
wangzhiwei 59a44f9c53 feat(skills): 新增技能包解析和标题获取功能
- 添加了CmsContentController的getTitle接口用于获取内容标题
- 实现了CmsContentService的getTitle方法支持内容标题查询
- 新增SkillZipParser工具类支持ZIP和RAR格式技能包解析
- 集成snakeyaml和sevenzipjbinding依赖处理YAML配置和压缩文件
- 实现SkillGenService的uploadSkillV2方法支持本地技能包上传
- 在SysUserController中增强token验证逻辑确保登录状态检查
- 支持从技能包中提取MD文件内容并自动生成YAML描述结构
2026-03-17 18:06:03 +08:00
wangzhiwei a92b668ac3 ```
feat(content): 添加内容服务参数验证和空值处理

- 在 CmsContentServiceImpl 中多个方法添加 Assert.notNull 参数验证
- 为 queryById、queryByIdWithPermission、update 等方法添加内容ID非空检查
- 处理点赞数为空时的默认值设置,避免空指针异常
- 在 controller 层修复注释参数名大小写问题
```
2026-03-16 11:09:07 +08:00
wangzhiwei ed220c9981 feat(content): 新增内容管理相关功能和优化技能生成服务
- 添加了获取CmsContent内容的接口和实现方法
- 新增QueryContentDto用于内容查询参数传递
- 修改SkillGenController中上传技能接口参数类型
- 在SkillGenRequest中添加技能说明字段
- 优化SkillGenServiceImpl中的API调用异常处理
- 添加对技能上传后图标设置的逻辑处理
- 在SysUser实体和数据库映射中添加会话ID字段
- 实现用户会话创建和管理功能
- 更新数据库查询语句以包含新增的session_id字段
- 添加了canvas-design技能包示例文件
2026-03-13 10:40:13 +08:00
wangzhiwei 6398b0495e feat(content): 添加多语言支持和Excel导入功能
- 在CmsContent实体类中增加英文标题、描述、介绍和内容字段
- 实现根据语言类型查询内容的功能,支持中英文切换
- 添加从Excel文件批量导入内容数据的功能
- 实现上传技能压缩包并解析生成技能内容的功能
- 优化分页查询逻辑,支持按标签过滤和内存分页
- 修改数据库映射配置以支持多语言字段存储
- 重构点赞功能的安全检查逻辑
2026-03-11 15:36:48 +08:00
wangzhiwei 1b0d102ef9 feat(content): 增强内容搜索功能支持关键词匹配和标签关联推荐
- 在 CmsContentDto 中新增 tagIdList 和 keyword 字段用于批量查询和全文搜索
- 修改数据库查询映射文件支持基于关键词的标题、描述、标签多字段模糊匹配
- 实现标签 ID 列表批量查询功能,支持多标签条件筛选
- 添加基于关键词搜索的智能关联推荐机制,根据首个结果的标签自动推荐相关内容
- 优化分页查询逻辑,在关键词搜索结果较少时自动补充相关技能内容
- 增强搜索结果排序算法,综合考虑排序权重和创建时间因素
2026-03-06 18:58:04 +08:00
wangzhiwei af0ae4bac1 feat(config): 添加上传配置并重构文件上传路径管理
- 新增 UploadConfig 配置类统一管理上传路径
- 将硬编码的上传目录路径改为配置驱动
- 添加 EscapeCharacterUtils 工具类处理转义字符
- 修复 application-dev.yml 和 application-prod.yml 中的上传路径格式
- 在 SkillGenServiceImpl 中集成转义字符清理功能
- 更新 CommonController 使用配置类管理上传目录
2026-03-04 14:48:50 +08:00
wangzhiwei 11bc1959f0 feat(content): 添加skill管理和支付配置功能
- 添加GLM大模型配置支持
- 配置生产环境Redis连接信息
- 更新支付回调URL路径配置
- 添加微信和支付宝支付配置到生产环境
- 修改异常处理器捕获BizException
- 添加内容详情、需求说明和介绍字段
- 将内容管理重命名为skill管理
- 添加取消收藏功能接口
- 添加用户历史查看、收藏、购买和创建内容列表接口
- 实现用户行为统计和个性化内容推荐功能
- 更新数据库映射文件以支持新字段和查询功能
2026-03-03 14:55:06 +08:00
wangzhiwei e16fbdf2d6 feat(payment): 添加支付功能支持微信和支付宝
- 集成微信支付和支付宝支付SDK
- 添加支付配置文件和配置类
- 实现支付控制器提供创建订单和处理回调接口
- 更新内容购买服务支持第三方支付方式
- 添加支付订单服务处理支付状态更新
- 修改CMS内容服务添加收藏和查看记录功能
- 更新应用配置文件适配开发环境和Redis连接
- 升级分页插件版本并添加统一SQL解析器依赖
2026-02-26 17:30:55 +08:00
wangzhiwei d33d567b81 fix(config): 更新Redis连接配置
- 注释掉本地Redis配置
- 添加生产环境Redis服务器地址43.248.97.19
- 设置Redis端口为16379
- 配置Redis密码为654321
2026-02-25 11:41:02 +08:00
wangzhiwei d8d1a4eaf4 feat(content): 重构内容管理系统增加标签功能
- 修改应用配置文件,统一token和session超时时间为24小时
- 配置Redis连接参数为动态引用公共配置
- 在内容实体类中替换分类相关字段为来源和标签字段
- 移除分类ID数组设置方法,优化实体映射
- 更新MyBatis映射文件中的字段映射关系
- 新增CmsCategoryTag实体类用于分类标签关联
- 实现标签服务接口,支持按分类查询标签列表
- 在内容控制器中添加标签列表获取接口
- 重构技能生成控制器,分离预生成和生成接口
- 更新技能请求参数类,支持标签列表传递
- 调整用户登录信息缓存时间至24小时
- 完善分类标签关联的数据访问层实现
2026-02-25 11:14:43 +08:00
wangzhiwei 8e00170ba1 fix(config): 更新生产环境数据库配置和搜索功能
- 将生产环境数据库连接URL从远程地址改为本地地址
- 修改CMS内容搜索逻辑,支持标题和摘要同时搜索
- 修复用户手机号验证正则表达式语法错误
2026-02-24 09:36:52 +08:00
165 changed files with 15602 additions and 1885 deletions

603
PAYMENT_GUIDE.md Normal file
View File

@ -0,0 +1,603 @@
# 支付功能使用指南
## 📋 概述
本项目已完整接入**微信支付**和**支付宝支付**,支持内容购买、账户充值等支付场景。
---
## 一、配置信息
### 1.1 微信支付配置
位置:`src/main/resources/application-dev.yml` 和 `src/main/resources/application-prod.yml`
```yaml
payment:
wechat:
appId: wx7d13d99de5be3bfa # 微信应用 ID
mchId: 1673321732 # 商户号
mchKey: UDuZXDcmy5Eb6o0nTNZhu6ek4DDh4K8B # 商户密钥
mchSerialNo: 5EFC47D3AA59BFD1AAE548F96B5E19E1C60F067D # 商户证书序列号
privateKeyPath: apiclient_key.pem # 商户私钥文件路径
domain: https://api.mch.weixin.qq.com # 微信服务器地址
notifyUrl: http://127.0.0.1:19001/api/pay/wx/notify # 支付回调地址
returnUrl: http://127.0.0.1:19001/api/pay/success # 支付成功跳转地址
```
### 1.2 支付宝支付配置
```yaml
payment:
alipay:
appId: 2021004138642603 # 支付宝应用 ID
publicKey: "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnakP04nUmsoFoveIvOhbLqkA1xQuYtvkrqq2AVvTsbtqpsEOTm9G095e2rBYLp89oDcf6L6BhtJPwdrhnA+qifUyVmACI9sprrsGeRYQgndK7y4c6spQcSnsnakSxlIp22j7pvBXNAZuqud2hQV+TOLKEUh1W3izTgMj/Ejoh3ZsCjgDRtTVgaytzSdHYrhNku+pIrl15/xVGJED99RYXkR8GHawxuK+vWVmxU0tiTCwTsqLz43v6TtCZ+/UfLL/luwp9B4ZvB+0qon82LILYr6oxs10kE2IAvryuDToAc1s/v/36jgt+7DXwqzfUDksHhVLHdJHChyc4ax5HmMsBwIDAQAB" # 支付宝公钥
privateKey: "" # 商户私钥(需配置)
notifyUrl: http://127.0.0.1:19001/api/pay/alipay/trade/notify # 支付回调地址
returnUrl: https://shuziren.xueai.art/alipay-success # 支付成功跳转地址
signType: RSA2 # 签名类型
charset: UTF-8 # 字符编码
gatewayUrl: https://openapi.alipay.com/gateway.do # 支付宝网关
```
---
## 二、API 接口说明
### 2.1 创建微信支付订单
**接口地址:** `POST /api/pay/wx/create`
**请求参数:**
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| orderNo | String | 否 | 订单号(不传则自动生成) |
| userId | Long | 是 | 用户 ID |
| userName | String | 是 | 用户名 |
| amount | BigDecimal | 是 | 支付金额(单位:元) |
| productName | String | 是 | 商品名称 |
| productDesc | String | 否 | 商品描述 |
| businessId | Long | 是 | 关联业务 ID |
| businessType | String | 是 | 业务类型recharge(充值)/purchase_content(购买内容) |
**请求示例:**
```bash
curl -X POST http://localhost:19001/api/pay/wx/create \
-H "Content-Type: application/json" \
-d '{
"userId": 123,
"userName": "张三",
"amount": 0.01,
"productName": "测试商品",
"productDesc": "商品描述",
"businessId": 456,
"businessType": "purchase_content"
}'
```
**响应示例:**
```json
{
"code": 200,
"message": "success",
"data": {
"code_url": "weixin://wxpay/bizpayurl?pr=xxx",
"order_no": "ORDER_20260331_001"
}
}
```
**字段说明:**
- `code_url`: 微信支付二维码链接,前端可使用此链接生成二维码
- `order_no`: 支付订单号,用于后续查询订单状态
---
### 2.2 创建支付宝支付订单
**接口地址:** `POST /api/pay/alipay/create`
**请求参数:** 与微信支付相同
**请求示例:**
```bash
curl -X POST http://localhost:19001/api/pay/alipay/create \
-H "Content-Type: application/json" \
-d '{
"userId": 123,
"userName": "张三",
"amount": 0.01,
"productName": "测试商品",
"businessId": 456,
"businessType": "purchase_content"
}'
```
**响应示例:**
```json
{
"code": 200,
"message": "success",
"data": "<form name=\"alipay\" method=\"post\" action=\"https://openapi.alipay.com/gateway.do\">...</form>"
}
```
**使用说明:**
- 返回的是 HTML 表单字符串
- 前端将此外壳写入页面后会自动提交跳转到支付宝支付页面
---
### 2.3 支付回调接口(系统自动处理)
#### 微信支付回调
**接口地址:** `POST /api/pay/wx/notify`
**说明:**
- 由微信支付服务器自动调用
- 处理支付结果并更新订单状态
- 返回 XML 格式响应给微信服务器
#### 支付宝支付回调
**异步回调:** `POST /api/pay/alipay/trade/notify`
**同步回调:** `GET /api/pay/alipay/trade/return`
**说明:**
- 异步回调由支付宝服务器自动调用
- 同步回调用于用户支付完成后跳转回指定页面
---
## 三、使用场景示例
### 3.1 场景 1购买付费内容
**业务流程:**
1. 用户点击购买按钮
2. 创建购买记录(待支付状态)
3. 创建支付订单
4. 用户扫码或跳转支付
5. 支付成功后更新订单和购买记录状态
**前端调用示例:**
```javascript
// 步骤 1创建购买记录
const purchaseResponse = await fetch('/api/content/purchase', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
userId: 123,
contentId: 456,
payType: 3 // 3=微信支付4=支付宝支付
})
});
const purchaseResult = await purchaseResponse.json();
// 步骤 2创建支付订单
const payResponse = await fetch('/api/pay/wx/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
userId: 123,
amount: 9.99,
productName: 'AI 技能教程',
productDesc: '高级 AI 技能培训内容',
businessId: 456,
businessType: 'purchase_content'
})
});
const payResult = await payResponse.json();
if (payResult.code === 200) {
// 步骤 3显示支付二维码
const codeUrl = payResult.data.code_url;
// 使用 QRCode 库生成二维码
new QRCode(document.getElementById('qrcode'), {
text: codeUrl,
width: 200,
height: 200
});
// 步骤 4轮询查询订单状态
const checkOrderStatus = setInterval(async () => {
const statusResponse = await fetch(`/api/pay/order/query?orderNo=${payResult.data.order_no}`);
const statusResult = await statusResponse.json();
if (statusResult.data.status === 2) { // 2=已支付
clearInterval(checkOrderStatus);
alert('支付成功!');
window.location.reload();
}
}, 3000); // 每 3 秒查询一次
// 设置超时15 分钟后停止查询)
setTimeout(() => {
clearInterval(checkOrderStatus);
alert('支付超时,请重新下单');
}, 900000);
}
```
---
### 3.2 场景 2账户充值
**业务流程:**
```javascript
// 创建充值订单
const rechargeResponse = await fetch('/api/pay/wx/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
userId: 123,
amount: 100.00, // 充值 100 元
productName: '账户充值',
businessId: 123,
businessType: 'recharge' // 充值业务类型
})
});
const result = await rechargeResponse.json();
if (result.code === 200) {
// 显示支付二维码
showQRCode(result.data.code_url);
}
```
**说明:**
- 支付成功后,系统会自动通过回调将充值金额添加到用户账户余额
---
### 3.3 场景 3支付宝网页支付
```javascript
// 创建支付宝订单
const aliPayResponse = await fetch('/api/pay/alipay/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
userId: 123,
amount: 0.01,
productName: '测试商品',
businessId: 456,
businessType: 'purchase_content'
})
});
const result = await aliPayResponse.json();
if (result.code === 200) {
// 将返回的 HTML 表单写入页面
document.body.innerHTML = result.data;
// 表单会自动提交,跳转到支付宝支付页面
}
```
---
## 四、前端集成完整示例
### 4.1 微信支付页面
```html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>微信支付</title>
<script src="https://cdn.jsdelivr.net/npm/qrcode@1.5.0/build/qrcode.min.js"></script>
<style>
.payment-container {
max-width: 600px;
margin: 50px auto;
padding: 20px;
text-align: center;
}
.qrcode-box {
border: 1px solid #ddd;
padding: 20px;
display: inline-block;
margin: 20px 0;
}
.status-tip {
color: #666;
font-size: 14px;
margin-top: 10px;
}
</style>
</head>
<body>
<div class="payment-container">
<h2>微信支付</h2>
<div class="qrcode-box">
<div id="qrcode"></div>
</div>
<p class="status-tip">请使用微信扫码支付</p>
<p class="status-tip" id="statusTip">等待支付...</p>
</div>
<script>
// 从 URL 参数获取商品信息
const urlParams = new URLSearchParams(window.location.search);
const productId = urlParams.get('productId');
const amount = urlParams.get('amount');
// 创建支付订单
async function createPayment() {
try {
const response = await fetch('/api/pay/wx/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
userId: 123, // 实际应从登录信息中获取
amount: parseFloat(amount),
productName: '商品-' + productId,
businessId: productId,
businessType: 'purchase_content'
})
});
const result = await response.json();
if (result.code === 200) {
// 生成二维码
QRCode.toCanvas(document.getElementById('qrcode'), result.data.code_url, {
width: 200,
height: 200
});
// 开始轮询订单状态
checkOrderStatus(result.data.order_no);
} else {
alert('创建订单失败:' + result.message);
}
} catch (error) {
console.error('创建订单失败:', error);
alert('网络错误,请稍后重试');
}
}
// 轮询订单状态
async function checkOrderStatus(orderNo) {
const maxAttempts = 300; // 最多查询 300 次15 分钟)
let attempts = 0;
const timer = setInterval(async () => {
try {
const response = await fetch(`/api/pay/order/query?orderNo=${orderNo}`);
const result = await response.json();
if (result.code === 200 && result.data) {
const status = result.data.status;
const statusText = document.getElementById('statusTip');
if (status === 2) { // 已支付
clearInterval(timer);
statusText.textContent = '✅ 支付成功!';
statusText.style.color = 'green';
setTimeout(() => {
window.location.href = '/pay-success.html?orderNo=' + orderNo;
}, 1000);
return;
} else if (status === 3 || status === 4) { // 支付失败或已取消
clearInterval(timer);
statusText.textContent = '❌ 支付失败';
statusText.style.color = 'red';
return;
}
attempts++;
if (attempts >= maxAttempts) {
clearInterval(timer);
statusText.textContent = '⏰ 支付超时,请重新下单';
statusText.style.color = 'orange';
}
}
} catch (error) {
console.error('查询订单状态失败:', error);
}
}, 3000); // 每 3 秒查询一次
}
// 页面加载时创建支付订单
createPayment();
</script>
</body>
</html>
```
### 4.2 支付宝支付页面
```html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>支付宝支付</title>
</head>
<body>
<div style="text-align: center; padding: 50px;">
<h2>正在跳转到支付宝支付页面...</h2>
<p>请稍候</p>
</div>
<script>
// 从 URL 获取商品信息
const urlParams = new URLSearchParams(window.location.search);
const productId = urlParams.get('productId');
const amount = urlParams.get('amount');
// 创建支付订单
async function createAlipay() {
try {
const response = await fetch('/api/pay/alipay/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
userId: 123,
amount: parseFloat(amount),
productName: '商品-' + productId,
businessId: productId,
businessType: 'purchase_content'
})
});
const result = await response.json();
if (result.code === 200) {
// 将返回的 HTML 表单写入页面并提交
document.body.innerHTML = result.data;
document.forms[0].submit();
} else {
alert('创建订单失败:' + result.message);
setTimeout(() => {
window.location.href = '/index.html';
}, 2000);
}
} catch (error) {
console.error('创建订单失败:', error);
alert('网络错误,请稍后重试');
}
}
createAlipay();
</script>
</body>
</html>
```
---
## 五、核心代码文件
| 文件路径 | 说明 |
|---------|------|
| `src/main/java/com/kexue/skills/controller/PayController.java` | 支付接口控制器 |
| `src/main/java/com/kexue/skills/service/PayService.java` | 支付服务接口 |
| `src/main/java/com/kexue/skills/service/impl/PayServiceImpl.java` | 支付服务实现类 |
| `src/main/java/com/kexue/skills/config/PaymentConfig.java` | 支付配置类 |
| `src/main/java/com/kexue/skills/entity/PaymentOrder.java` | 支付订单实体类 |
| `src/main/java/com/kexue/skills/service/PaymentOrderService.java` | 支付订单服务接口 |
| `src/main/java/com/kexue/skills/service/impl/PaymentOrderServiceImpl.java` | 支付订单服务实现类 |
---
## 六、注意事项
### 6.1 开发环境测试
⚠️ **重要提示:** 本地开发环境下,微信和支付宝无法直接访问到你的 localhost 回调地址。
**解决方案:**
1. **使用内网穿透工具**(推荐)
```bash
# 使用 ngrok
ngrok http 19001
# 将生成的域名配置到回调地址
# 例如https://abc123.ngrok.io/api/pay/wx/notify
```
2. **部署到测试服务器**
- 直接部署到具有公网 IP 的服务器进行测试
3. **修改配置文件**
```yaml
payment:
wechat:
notifyUrl: https://your-domain.ngrok.io/api/pay/wx/notify
alipay:
notifyUrl: https://your-domain.ngrok.io/api/pay/alipay/trade/notify
```
### 6.2 金额单位
- 前端传入金额单位:**元**
- 微信支付内部转换:**分**(代码自动处理)
- 建议最小金额0.01 元
### 6.3 业务类型说明
| 业务类型 | 说明 | 支付成功后操作 |
|---------|------|--------------|
| `recharge` | 账户充值 | 增加用户账户余额 |
| `purchase_content` | 购买内容 | 更新内容购买记录状态 |
### 6.4 支付状态码
| 状态码 | 说明 |
|-------|------|
| 1 | 待支付 |
| 2 | 已支付 |
| 3 | 支付失败 |
| 4 | 已取消 |
| 5 | 已退款 |
### 6.5 安全建议
1. **权限验证**:所有支付接口都应添加权限验证(已部分实现)
2. **签名验证**:确保支付回调的签名验证正确
3. **幂等性处理**:支付回调应支持重复通知(已实现)
4. **日志记录**:完整的支付日志便于问题排查
---
## 七、常见问题
### Q1: 如何查询订单状态?
```java
// 通过订单号查询
PaymentOrder order = paymentOrderService.queryByOrderNo(orderNo);
// 或通过主键查询
PaymentOrder order = paymentOrderService.queryById(orderId);
```
### Q2: 如何处理支付回调失败?
系统已实现幂等性处理,同一订单多次回调不会重复处理。如果回调失败,微信/支付宝会按一定频率重试。
### Q3: 如何申请退款?
当前版本暂未实现退款功能,如需退款,需要:
1. 调用微信/支付宝退款 API
2. 更新支付订单状态为已退款5
3. 恢复用户余额或购买权限
### Q4: 测试时使用真实资金吗?
是的,对接的是正式环境。如需测试环境,需要:
- 微信:申请沙箱环境
- 支付宝:使用测试账号
---
## 八、技术支持
如遇问题,请检查以下内容:
1. ✅ 配置文件中的商户号、密钥是否正确
2. ✅ 回调地址是否可被外网访问
3. ✅ 商户私钥文件是否存在且路径正确
4. ✅ 查看日志文件中的详细错误信息
5. ✅ 确认微信/支付宝商户号状态正常
---
**文档版本:** v1.0
**更新时间:** 2026-03-31
**维护人员:** 系统开发团队

193
README.md
View File

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

View File

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

View File

@ -0,0 +1,9 @@
-- 为 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

@ -0,0 +1,5 @@
-- 修改payment_order表为pay_type字段添加默认值
ALTER TABLE `payment_order` MODIFY COLUMN `pay_type` tinyint(1) NOT NULL DEFAULT 1 COMMENT '支付方式1.微信 2.支付宝';
-- 输出修改结果
SELECT '修改payment_order表pay_type字段默认值完成' AS result;

View File

@ -0,0 +1,46 @@
-- 更新 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

@ -12,7 +12,9 @@ CREATE TABLE `account` (
`account_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`user_id` bigint(20) NOT NULL COMMENT '用户ID',
`user_name` varchar(50) DEFAULT NULL COMMENT '用户名',
`balance` decimal(10,2) DEFAULT '0.00' COMMENT '账户余额',
`balance` decimal(10,2) DEFAULT '0.00' COMMENT '账户总余额',
`withdrawable_balance` decimal(10,2) DEFAULT '0.00' COMMENT '可提现余额',
`non_withdrawable_balance` decimal(10,2) DEFAULT '0.00' COMMENT '不可提现余额',
`frozen_amount` decimal(10,2) DEFAULT '0.00' COMMENT '冻结金额',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
@ -21,13 +23,36 @@ CREATE TABLE `account` (
KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='账户表,记录用户的账户信息';
-- 18. 提现记录表
DROP TABLE IF EXISTS `withdrawal_record`;
CREATE TABLE `withdrawal_record` (
`record_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`user_id` bigint(20) NOT NULL COMMENT '用户ID',
`user_name` varchar(50) DEFAULT NULL COMMENT '用户名',
`withdrawal_amount` decimal(10,2) NOT NULL COMMENT '提现金额',
`fee_amount` decimal(10,2) NOT NULL COMMENT '手续费',
`actual_amount` decimal(10,2) NOT NULL COMMENT '实际到账金额',
`status` tinyint(1) NOT NULL COMMENT '提现状态1.待处理 2.处理中 3.成功 4.失败',
`withdrawal_no` varchar(50) NOT NULL COMMENT '提现单号',
`bank_name` varchar(100) DEFAULT NULL COMMENT '银行名称',
`bank_account` varchar(100) DEFAULT NULL COMMENT '银行账号',
`bank_cardholder` varchar(50) DEFAULT NULL COMMENT '持卡人姓名',
`remark` varchar(255) 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已删除',
PRIMARY KEY (`record_id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_withdrawal_no` (`withdrawal_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='提现记录表,记录用户的提现记录';
-- 2. 账户流水表
DROP TABLE IF EXISTS `account_transaction`;
CREATE TABLE `account_transaction` (
`transaction_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`user_id` bigint(20) NOT NULL COMMENT '用户ID',
`user_name` varchar(50) DEFAULT NULL COMMENT '用户名',
`transaction_type` tinyint(1) NOT NULL COMMENT '交易类型1.充值 2.提现 3.购买内容 4.退款 5.其他',
`transaction_type` tinyint(1) NOT NULL COMMENT '交易类型1.充值 2.提现 3.购买内容 4.退款 5.签到奖励 6.赠送 7.其他',
`amount` decimal(10,2) NOT NULL COMMENT '交易金额',
`before_balance` decimal(10,2) NOT NULL COMMENT '交易前余额',
`after_balance` decimal(10,2) NOT NULL COMMENT '交易后余额',
@ -37,6 +62,13 @@ CREATE TABLE `account_transaction` (
`business_id` bigint(20) DEFAULT NULL COMMENT '关联业务ID',
`business_type` varchar(50) DEFAULT NULL COMMENT '业务类型',
`remark` varchar(255) DEFAULT NULL COMMENT '交易备注',
`is_expense` tinyint(1) NOT NULL COMMENT '是否支出1.是 0.否',
`input_token` int(11) DEFAULT NULL COMMENT '输入token',
`output_token` int(11) DEFAULT NULL COMMENT '输出token',
`total_tokens` int(11) DEFAULT NULL COMMENT '合计tokens',
`model_name` varchar(100) DEFAULT NULL COMMENT '处理的模型名称',
`question` text DEFAULT NULL COMMENT '对应回答的问题或需求',
`income_type` varchar(50) DEFAULT NULL COMMENT '收入类型recharge(充值)、sign_in(签到奖励)',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`create_by` varchar(50) DEFAULT NULL COMMENT '创建人',
@ -48,48 +80,7 @@ CREATE TABLE `account_transaction` (
KEY `idx_business_id` (`business_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='账户流水表,记录用户的账户交易记录';
-- 3. 积分账户表
DROP TABLE IF EXISTS `points_account`;
CREATE TABLE `points_account` (
`account_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`user_id` bigint(20) NOT NULL COMMENT '用户ID',
`user_name` varchar(50) DEFAULT NULL COMMENT '用户名',
`total_points` int(11) DEFAULT '0' COMMENT '总积分',
`available_points` int(11) DEFAULT '0' COMMENT '可用积分',
`frozen_points` int(11) DEFAULT '0' 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已删除',
PRIMARY KEY (`account_id`),
KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='积分账户表,记录用户的积分信息';
-- 4. 积分流水表
DROP TABLE IF EXISTS `points_transaction`;
CREATE TABLE `points_transaction` (
`transaction_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`user_id` bigint(20) NOT NULL COMMENT '用户ID',
`user_name` varchar(50) DEFAULT NULL COMMENT '用户名',
`transaction_type` tinyint(1) NOT NULL COMMENT '积分变动类型1.获取积分 2.消费积分 3.过期 4.其他',
`points` int(11) NOT NULL COMMENT '变动积分',
`before_points` int(11) NOT NULL COMMENT '变动前积分',
`after_points` int(11) NOT NULL COMMENT '变动后积分',
`status` tinyint(1) NOT NULL COMMENT '交易状态1.成功 2.失败 3.处理中',
`transaction_no` varchar(50) NOT NULL COMMENT '交易单号',
`pay_type` tinyint(1) DEFAULT NULL COMMENT '支付方式1.微信 2.支付宝 3.余额支付',
`business_id` bigint(20) DEFAULT NULL COMMENT '关联业务ID',
`business_type` varchar(50) DEFAULT NULL COMMENT '业务类型',
`remark` varchar(255) DEFAULT NULL COMMENT '备注',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`create_by` varchar(50) DEFAULT NULL COMMENT '创建人',
`update_by` varchar(50) DEFAULT NULL COMMENT '更新人',
`delete_flag` tinyint(1) DEFAULT '0' COMMENT '是否删除 0 未删除1已删除',
PRIMARY KEY (`transaction_id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_transaction_no` (`transaction_no`),
KEY `idx_business_id` (`business_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='积分流水表,记录用户的积分变动情况';
-- 5. 内容购买记录表
DROP TABLE IF EXISTS `content_purchase`;
@ -308,4 +299,24 @@ CREATE TABLE `sys_log` (
KEY `idx_log_time` (`log_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='系统日志表,记录系统操作日志';
-- 16. 大模型Token价格表
DROP TABLE IF EXISTS `model_price`;
CREATE TABLE `model_price` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`vendor` varchar(64) NOT NULL COMMENT '厂商',
`model_name` varchar(128) NOT NULL COMMENT '模型名称',
`input_price` decimal(10,4) NOT NULL COMMENT '输入价格:元/百万Token',
`output_price` decimal(10,4) NOT NULL COMMENT '输出价格:元/百万Token',
`input_per_cent` bigint NOT NULL COMMENT '1分钱可购买输入Token数',
`output_per_cent` bigint NOT NULL COMMENT '1分钱可购买输出Token数',
`unit` varchar(32) DEFAULT '百万Token' COMMENT '价格单位',
`remark` varchar(255) DEFAULT '' COMMENT '备注',
`created_time` datetime DEFAULT NULL,
`updated_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_vendor` (`vendor`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='大模型Token价格表';
SET FOREIGN_KEY_CHECKS = 1;

57
pom.xml
View File

@ -146,11 +146,11 @@
<version>1.3.2</version>
</dependency>
<!-- Servlet API for javax.servlet.http -->
<!-- Servlet API for jakarta.servlet.http (Spring Boot 3.x uses Jakarta EE 9+) -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>6.0.0</version>
<scope>provided</scope>
</dependency>
@ -164,7 +164,7 @@
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.4.2</version>
<version>1.4.6</version>
<exclusions>
<exclusion>
<groupId>org.mybatis</groupId>
@ -182,10 +182,6 @@
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</exclusion>
<exclusion>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</exclusion>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>log4j-over-slf4j</artifactId>
@ -195,12 +191,19 @@
<artifactId>jul-to-slf4j</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
<groupId>com.github.jsqlparser</groupId>
<artifactId>jsqlparser</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 统一的jsqlparser版本 -->
<dependency>
<groupId>com.github.jsqlparser</groupId>
<artifactId>jsqlparser</artifactId>
<version>4.4</version>
</dependency>
<!-- Sa-Token 核心依赖 -->
<dependency>
<groupId>cn.dev33</groupId>
@ -257,6 +260,38 @@
<version>1.2.83</version>
</dependency>
<!-- 微信支付SDK -->
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-pay</artifactId>
<version>4.4.0</version>
</dependency>
<!-- 支付宝SDK -->
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId>
<version>4.38.0.ALL</version>
</dependency>
<!-- YAML 配置文件解析器 -->
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>2.2</version>
</dependency>
<dependency>
<groupId>net.sf.sevenzipjbinding</groupId>
<artifactId>sevenzipjbinding</artifactId>
<version>16.02-2.01</version>
</dependency>
<dependency>
<groupId>net.sf.sevenzipjbinding</groupId>
<artifactId>sevenzipjbinding-all-platforms</artifactId>
<version>16.02-2.01</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>

View File

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

@ -15,7 +15,7 @@ import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import jakarta.servlet.http.HttpServletRequest;
import javax.annotation.Resource;
import jakarta.annotation.Resource;
/**
* @author 维哥
* @Description 登录认证切面
@ -67,11 +67,10 @@ public class AuthAspect {
// 设置用户上下文
UserContextHolder.setUserName(username);
return joinPoint.proceed();
} catch (Exception e) {
log.error("认证失败:{}", e.getMessage());
throw new BizException(ResultCode.TOKEN_FAILED.getCode(), "无效的token请重新登录");
}
return joinPoint.proceed();
}
}

View File

@ -16,6 +16,7 @@ import org.springframework.web.context.request.ServletRequestAttributes;
import jakarta.servlet.http.HttpServletRequest;
import javax.annotation.Resource;
import java.util.List;
/**
* @author 维哥
@ -85,14 +86,29 @@ public class RoleAspect {
// 获取用户的角色列表
String[] requiredRoles = requireRole.value();
if (requiredRoles != null && requiredRoles.length > 0) {
// 使用Sa-Token检查角色权限
cn.dev33.satoken.stp.StpUtil.checkRoleAnd(requiredRoles);
// 获取当前用户的角色列表
List<String> userRoles = cn.dev33.satoken.stp.StpUtil.getRoleList();
log.info("当前用户的角色列表:{}", String.join(",", userRoles));
// 检查用户是否拥有所有必需的角色
boolean hasAllRoles = true;
for (String role : requiredRoles) {
if (!userRoles.contains(role)) {
hasAllRoles = false;
log.error("用户缺少角色:{}", role);
break;
}
}
if (!hasAllRoles) {
throw new cn.dev33.satoken.exception.NotRoleException(requiredRoles[0]);
}
}
// 设置用户上下文
UserContextHolder.setUserName(username);
return joinPoint.proceed();
} catch (cn.dev33.satoken.exception.NotLoginException e) {
log.error("未登录:{}", e.getMessage());
throw new BizException(ResultCode.TOKEN_FAILED.getCode(), "请先登录认证后操作");
@ -103,5 +119,6 @@ public class RoleAspect {
log.error("权限验证失败:{}", e.getMessage());
throw new BizException(ResultCode.PERMISSION_DENIED.getCode(), "权限验证失败");
}
return joinPoint.proceed();
}
}

View File

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

View File

@ -0,0 +1,170 @@
package com.kexue.skills.common;
import com.kexue.skills.entity.request.LoginUser;
import com.kexue.skills.mapper.*;
import jakarta.servlet.http.HttpServletRequest;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
/**
* LoginUser缓存工具类
* 用于更新Redis中的LoginUser对象
*/
@Component
public class LoginUserCacheUtil {
@Resource
private RedissonClient redissonClient;
@Resource
private CmsContentLikeMapper cmsContentLikeMapper;
@Resource
private CmsContentViewMapper cmsContentViewMapper;
@Resource
private CmsContentMapper cmsContentMapper;
@Resource
private ContentPurchaseMapper contentPurchaseMapper;
/**
* 更新用户的收藏列表
*
* @param token 用户token
* @param userId 用户ID
*/
public void updateFavorites(String token, Long userId) {
updateLoginUserList(token, userId, "favorites", () -> cmsContentLikeMapper.queryRecentLikesByUserId(userId, 20));
}
/**
* 更新用户的查看历史列表
*
* @param token 用户token
* @param userId 用户ID
*/
public void updateHistory(String token, Long userId) {
updateLoginUserList(token, userId, "history", () -> cmsContentViewMapper.queryRecentViewsByUserId(userId, 20));
}
/**
* 更新用户的创建记录列表
*
* @param token 用户token
* @param userId 用户ID
*/
public void updateCreate(String token, Long userId) {
updateLoginUserList(token, userId, "create", () -> cmsContentMapper.queryRecentCreatedByUserId(userId, 20));
}
/**
* 更新用户的购买记录列表
*
* @param token 用户token
* @param userId 用户ID
*/
public void updateHas(String token, Long userId) {
updateLoginUserList(token, userId, "has", () -> {
List<Long> has = new ArrayList<>();
try {
com.kexue.skills.entity.dto.ContentPurchaseDto purchaseDto = new com.kexue.skills.entity.dto.ContentPurchaseDto();
purchaseDto.setUserId(userId);
List<com.kexue.skills.entity.ContentPurchase> purchases = contentPurchaseMapper.getList(purchaseDto);
if (purchases != null && !purchases.isEmpty()) {
java.util.LinkedHashSet<Long> contentIdSet = new java.util.LinkedHashSet<>();
for (com.kexue.skills.entity.ContentPurchase purchase : purchases) {
if (contentIdSet.size() < 20) {
contentIdSet.add(purchase.getContentId());
} else {
break;
}
}
has.addAll(contentIdSet);
}
} catch (Exception e) {
e.printStackTrace();
has = java.util.Collections.emptyList();
}
return has;
});
}
/**
* 更新LoginUser中的列表属性
*
* @param token 用户token
* @param userId 用户ID
* @param fieldName 字段名
* @param supplier 列表数据提供者
*/
private void updateLoginUserList(String token, Long userId, String fieldName, java.util.function.Supplier<List<Long>> supplier) {
try {
String key = "loginUser:" + token;
Object loginUserObj = redissonClient.getBucket(key).get();
if (loginUserObj != null) {
String loginUserJson = loginUserObj.toString();
com.kexue.skills.entity.request.LoginUser loginUser = com.alibaba.fastjson.JSON.parseObject(loginUserJson, com.kexue.skills.entity.request.LoginUser.class);
if (loginUser != null && loginUser.getUserInfo() != null && loginUser.getUserInfo().getUserId().equals(userId)) {
List<Long> list = supplier.get();
switch (fieldName) {
case "favorites":
loginUser.setFavorites(list);
break;
case "history":
loginUser.setHistory(list);
break;
case "create":
loginUser.setCreate(list);
break;
case "has":
loginUser.setHas(list);
break;
}
// 写回Redis
redissonClient.getBucket(key).set(com.alibaba.fastjson.JSON.toJSONString(loginUser));
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 从请求中获取token
*
* @return token
*/
public String getTokenFromRequest() {
org.springframework.web.context.request.RequestAttributes requestAttributes = org.springframework.web.context.request.RequestContextHolder.getRequestAttributes();
if (requestAttributes != null && requestAttributes instanceof org.springframework.web.context.request.ServletRequestAttributes) {
HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
if (request != null) {
return request.getHeader("Authorization");
}
}
return null;
}
/**
* 获取当前登录用户ID
*
* @return 用户ID
*/
public Long getCurrentUserId() {
try {
Object loginId = cn.dev33.satoken.stp.StpUtil.getLoginId();
return Long.parseLong(loginId.toString());
} catch (Exception e) {
return null;
}
}
}

View File

@ -92,7 +92,19 @@ public enum ResultCode implements IErrorCode {
/**
* 统一异常返回码
* */
EXCEPTION_HANDLER(-2500,"服务异常,请联系管理员");
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, "账户余额不足");
private final long code;
private final String message;

View File

@ -55,4 +55,49 @@ public class HttpUtil {
return response.body();
}
/**
* 发送 POST 请求到指定 URL
*
* @param url 请求 URL
* @param requestBody 请求体字符串
* @return 响应结果
* @throws Exception 异常信息
*/
public static String post(String url, String requestBody) throws Exception {
// 创建 HttpClient 实例
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(30))
.build();
return postWithClient(url, requestBody, client);
}
/**
* 发送 POST 请求到指定 URL使用指定的 HttpClient支持连接复用
*
* @param url 请求 URL
* @param requestBody 请求体字符串
* @param client HttpClient 实例
* @return 响应结果
* @throws Exception 异常信息
*/
public static String postWithClient(String url, String requestBody, HttpClient client) throws Exception {
// 构建 HttpRequest
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Content-Type", "application/xml")
.header("Accept", "application/xml")
.POST(HttpRequest.BodyPublishers.ofString(requestBody, StandardCharsets.UTF_8))
.build();
// 发送请求并获取响应
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
// 打印响应状态码和响应体
System.out.println("HTTP 状态码: " + response.statusCode());
System.out.println("响应体: " + response.body());
return response.body();
}
}

View File

@ -2,22 +2,119 @@ 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("-","");
}
public static void main(String[] args) {
@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());
}
}
}

File diff suppressed because it is too large Load Diff

View File

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

@ -0,0 +1,73 @@
package com.kexue.skills.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* GLM API配置类
*
* @author 维哥
* @since 2026-02-27
*/
@Component
@ConfigurationProperties(prefix = "spring.ai.glm")
public class GlmConfig {
private String baseUrl;
private String apiKey;
private ChatOptions chat;
public static class ChatOptions {
private String model;
private Double temperature;
private Integer maxTokens;
public String getModel() {
return model;
}
public void setModel(String model) {
this.model = model;
}
public Double getTemperature() {
return temperature;
}
public void setTemperature(Double temperature) {
this.temperature = temperature;
}
public Integer getMaxTokens() {
return maxTokens;
}
public void setMaxTokens(Integer maxTokens) {
this.maxTokens = maxTokens;
}
}
public String getBaseUrl() {
return baseUrl;
}
public void setBaseUrl(String baseUrl) {
this.baseUrl = baseUrl;
}
public String getApiKey() {
return apiKey;
}
public void setApiKey(String apiKey) {
this.apiKey = apiKey;
}
public ChatOptions getChat() {
return chat;
}
public void setChat(ChatOptions chat) {
this.chat = chat;
}
}

View File

@ -0,0 +1,26 @@
package com.kexue.skills.config;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import java.text.SimpleDateFormat;
import java.util.TimeZone;
/**
* Jackson 序列化配置
*/
@Configuration
public class JacksonConfig {
@Bean
public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
return builder.createXmlMapper(false)
.simpleDateFormat("yyyy-MM-dd HH:mm:ss")
.timeZone(TimeZone.getTimeZone("GMT+8"))
.featuresToDisable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.build();
}
}

View File

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

@ -0,0 +1,48 @@
package com.kexue.skills.config;
import com.kexue.skills.service.SysUserService;
import cn.dev33.satoken.stp.StpInterface;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.List;
/**
* Sa-Token 权限验证接口实现
*/
@Component
public class MyStpInterfaceImpl implements StpInterface {
@Resource
private SysUserService sysUserService;
/**
* 获取用户的角色列表
* @param loginId 用户登录ID
* @param loginType 登录类型
* @return 角色列表
*/
@Override
public List<String> getRoleList(Object loginId, String loginType) {
// 从数据库查询用户角色列表
try {
Long userId = Long.parseLong(loginId.toString());
return sysUserService.queryUserRoles(userId);
} catch (Exception e) {
e.printStackTrace();
return java.util.Collections.emptyList();
}
}
/**
* 获取用户的权限列表
* @param loginId 用户登录ID
* @param loginType 登录类型
* @return 权限列表
*/
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
// 暂时返回空列表后续可根据需要从数据库查询权限列表
return java.util.Collections.emptyList();
}
}

View File

@ -0,0 +1,66 @@
package com.kexue.skills.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 支付配置类
*/
@Component
@ConfigurationProperties(prefix = "payment")
@Data
public class PaymentConfig {
// 微信支付配置
private WechatPayConfig wechat;
// 支付宝支付配置
private AlipayConfig alipay;
/**
* 微信支付配置
*/
@Data
public static class WechatPayConfig {
// 应用ID
private String appId;
// 商户号
private String mchId;
// 商户密钥
private String mchKey;
// 商户API证书序列号
private String mchSerialNo;
// 商户私钥文件路径
private String privateKeyPath;
// 微信服务器地址
private String domain;
// 支付回调地址
private String notifyUrl;
// 支付成功跳转地址
private String returnUrl;
}
/**
* 支付宝支付配置
*/
@Data
public static class AlipayConfig {
// 应用ID
private String appId;
// 商户私钥
private String privateKey;
// 支付宝公钥
private String publicKey;
// 支付回调地址
private String notifyUrl;
// 支付成功跳转地址
private String returnUrl;
// 签名类型
private String signType;
// 字符编码
private String charset;
// 支付宝网关
private String gatewayUrl;
}
}

View File

@ -24,9 +24,9 @@ public class SaTokenConfig implements WebMvcConfigurer {
// 拦截所有请求除了登录注册文档等不需要认证的接口
SaRouter
// 放行登录接口
.match("/login/**").stop()
.match("/api/login/**").stop()
// 放行注册接口
.match("/register/**").stop()
.match("/api/register/**").stop()
// 放行Swagger文档
.match("/doc.html").stop()
.match("/swagger-ui/**").stop()

View File

@ -0,0 +1,48 @@
package com.kexue.skills.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 上传配置类
* 用于读取web.upload配置
*/
@Data
@Component
@ConfigurationProperties(prefix = "web.upload")
public class UploadConfig {
/**
* 上传路径
*/
private String path;
/**
* 文件上传目录
*/
public String getFileUploadDir() {
return path + "file/";
}
/**
* 图片上传目录
*/
public String getImgUploadDir() {
return path + "images/";
}
/**
* 文件访问路径前缀
*/
public String getFileUrlPrefix() {
return "/upload/file/";
}
/**
* 图片访问路径前缀
*/
public String getImgUrlPrefix() {
return "/upload/images/";
}
}

View File

@ -1,17 +1,31 @@
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;
import com.kexue.skills.common.Const;
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;
import com.kexue.skills.exception.BizException;
import com.kexue.skills.common.CacheManager;
import java.math.BigDecimal;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import java.util.List;
/**
@ -21,6 +35,7 @@ import java.util.List;
* @author 王志维
* @since 2025-02-21 23:01:48
*/
@Log(module = "账户管理")
@Tag(name = "账户管理 api")
@RestController
@RequestMapping("/api/account")
@ -31,6 +46,9 @@ public class AccountController {
@Resource
private AccountService accountService;
@Resource
private SysUserService sysUserService;
/**
* 分页查询
*
@ -63,7 +81,7 @@ public class AccountController {
* @param accountId 主键
* @return 单条数据
*/
@Operation(summary = "通过ID查询账户", description = "通过ID查询账户")
@Operation(summary = "通过查询账户", description = "通过ID查询账户")
@PostMapping("/queryById/{accountId}")
@RequireAuth
public CommonResult<Account> queryById(@Parameter(description = "账户ID") @PathVariable("accountId") Long accountId) {
@ -71,15 +89,123 @@ public class AccountController {
}
/**
* 通过用户ID查询单条数据
* 通过当前登录用户ID查询账户
*
* @param userId 用户ID
* @return 单条数据
*/
@Operation(summary = "通过用户ID查询账户", description = "通过用户ID查询账户")
@PostMapping("/queryByUserId/{userId}")
@Operation(summary = "通过当前登录用户查询账户", description = "通过当前登录用户查询账户")
@PostMapping("/currentAccount")
@RequireAuth
public CommonResult<Account> queryByUserId(@Parameter(description = "用户ID") @PathVariable("userId") Long userId) {
public CommonResult<Account> currentAccount() {
Long userId = Long.parseLong(StpUtil.getLoginId().toString());
return CommonResult.success(this.accountService.queryByUserId(userId));
}
/**
* 充值账户
*
* @param userId 用户ID
* @param amount 充值金额
* @param payType 支付方式1.微信 2.支付宝
* @return 充值结果
*/
@Operation(summary = "充值账户", description = "充值账户")
@PostMapping("/recharge")
@RequireAuth
public CommonResult<String> recharge(
@Parameter(description = "用户ID") @RequestParam("userId") Long userId,
@Parameter(description = "充值金额") @RequestParam("amount") java.math.BigDecimal amount,
@Parameter(description = "支付方式1.微信 2.支付宝") @RequestParam("payType") Integer payType) {
// 这里可以根据需要实现充值逻辑
// 实际的支付流程需要通过支付接口完成
return CommonResult.success("充值请求已提交,请通过支付接口完成支付");
}
/**
* 减少账户余额token消费转换
*
* @param tokenConsumptionDto token消费转换参数
* @return 消耗的金额
*/
@Operation(summary = "减少账户余额token消费转换", description = "减少账户余额token消费转换")
@PostMapping("/reduceBalanceWithToken")
public CommonResult<BigDecimal> reduceBalanceWithToken( @RequestBody TokenConsumptionDto tokenConsumptionDto) {
return CommonResult.success(this.accountService.reduceBalanceWithToken(tokenConsumptionDto));
}
/**
* 给用户赠送金额不可提现
* 只有管理员可以调用
*
* @param giftBalanceDto 赠送金额参数
* @return 影响行数
*/
@Operation(summary = "给用户赠送金额(不可提现)", description = "给用户赠送金额(不可提现),只有管理员可以调用")
@PostMapping("/addGiftBalance")
@RequireAuth
@RequireRole({"ADMIN"})
public CommonResult<Integer> addGiftBalance(@RequestBody GiftBalanceDto giftBalanceDto) {
return CommonResult.success(this.accountService.addGiftBalance(
giftBalanceDto.getUserId(),
giftBalanceDto.getAmount(),
giftBalanceDto.getTransactionNo(),
giftBalanceDto.getBusinessId(),
giftBalanceDto.getBusinessType(),
giftBalanceDto.getRemark()
));
}
/**
* 获取当前登录用户交易记录
*
* @return 交易记录列表
*/
@Operation(summary = "获取当前登录用户交易记录", description = "获取当前登录用户交易记录")
@PostMapping("/getTransactions")
@RequireAuth
public CommonResult<List<com.kexue.skills.entity.AccountTransaction>> getTransactions() {
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

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

@ -7,6 +7,8 @@ import com.kexue.skills.entity.CmsCategory;
import com.kexue.skills.entity.base.IdDto;
import com.kexue.skills.entity.dto.CmsCategoryDto;
import com.kexue.skills.service.CmsCategoryService;
import com.kexue.skills.service.CmsTagService;
import com.kexue.skills.service.CmsCategoryTagService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.*;
@ -29,6 +31,18 @@ public class CmsCategoryController {
*/
@Resource
private CmsCategoryService cmsCategoryService;
/**
* 标签服务对象
*/
@Resource
private CmsTagService cmsTagService;
/**
* 分类标签关联服务对象
*/
@Resource
private CmsCategoryTagService cmsCategoryTagService;
/**
* 分页查询
*
@ -126,4 +140,25 @@ public class CmsCategoryController {
public CommonResult<java.util.Map<Long, String>> getCategoryDict() {
return CommonResult.success(cmsCategoryService.getCategoryDict());
}
/**
* 获取标签列表
*
* @param categoryId 分类ID可选不传则返回所有标签
* @return 标签列表
*/
@GetMapping("/tagList")
@Operation(summary = "获取标签列表", description = "获取标签列表若传入分类ID则返回该分类下的标签否则返回所有标签")
public CommonResult<java.util.List<com.kexue.skills.entity.CmsTag>> getTagList(@RequestParam(value = "categoryId", required = false) Long categoryId) {
if (categoryId != null) {
// 根据分类ID查询标签列表
return CommonResult.success(cmsCategoryTagService.getTagsByCategoryId(categoryId));
} else {
// 查询所有标签
com.kexue.skills.entity.dto.CmsTagDto queryDto = new com.kexue.skills.entity.dto.CmsTagDto();
queryDto.setDeleteFlag(0);
queryDto.setStatus(1);
return CommonResult.success(cmsTagService.getList(queryDto));
}
}
}

View File

@ -1,17 +1,23 @@
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;
import com.kexue.skills.entity.CmsContent;
import com.kexue.skills.entity.base.IdDto;
import com.kexue.skills.entity.dto.CmsContentDto;
import com.kexue.skills.entity.dto.QueryContentDto;
import com.kexue.skills.entity.request.ImportPathDto;
import com.kexue.skills.service.CmsContentService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.io.IOException;
/**
* (CmsContent)表控制层
@ -21,7 +27,7 @@ import javax.annotation.Resource;
*/
@RestController
@RequestMapping("api/cmsContent")
@Tag(name = "内容skills管理 Api")
@Tag(name = "skillskills管理 Api")
@CrossOrigin(origins = "*")
public class CmsContentController {
/**
@ -60,7 +66,7 @@ public class CmsContentController {
* @return 单条数据
*/
@PostMapping("queryById/{contentId}")
@Operation(summary = "通过ID查询内容", description = "通过ID查询内容")
@Operation(summary = "通过ID查询skill", description = "通过ID查询skill")
public CommonResult<CmsContent> queryById(@PathVariable("contentId") Long contentId) {
// 增加阅读量
cmsContentService.increaseViewCount(contentId);
@ -74,7 +80,7 @@ public class CmsContentController {
* @return 新增结果
*/
@PostMapping("/insert")
@Operation(summary = "新增内容", description = "新增内容")
@Operation(summary = "新增skill", description = "新增skill")
@RequireAuth
public CommonResult<CmsContent> insert(@RequestBody CmsContent cmsContent) {
return CommonResult.success(cmsContentService.insert(cmsContent));
@ -87,7 +93,7 @@ public class CmsContentController {
* @return 编辑结果
*/
@PostMapping("/update")
@Operation(summary = "更新内容", description = "更新内容")
@Operation(summary = "更新skill", description = "更新skill")
@RequireAuth
public CommonResult<CmsContent> update(@RequestBody CmsContent cmsContent) {
return CommonResult.success(cmsContentService.update(cmsContent));
@ -96,7 +102,7 @@ public class CmsContentController {
/**
* 更新审核状态
*
* @param contentId 内容ID
* @param contentId skillID
* @param auditStatus 审核状态
* @param reviewerId 审核人ID
* @param reviewerName 审核人名称
@ -117,7 +123,7 @@ public class CmsContentController {
/**
* 更新发布状态
*
* @param contentId 内容ID
* @param contentId skillID
* @param publishStatus 发布状态
* @param publishTime 发布时间
* @param updateBy 更新人
@ -136,7 +142,7 @@ public class CmsContentController {
/**
* 增加阅读量
*
* @param contentId 内容ID
* @param contentId skillID
* @return 增加结果
*/
@PostMapping("/increaseViewCount/{contentId}")
@ -152,7 +158,7 @@ public class CmsContentController {
* @return 删除数据
*/
@PostMapping("/logicDeleteById")
@Operation(summary = "逻辑删除内容", description = "逻辑删除内容")
@Operation(summary = "逻辑删除skill", description = "逻辑删除skill")
@RequireAuth
public CommonResult<Boolean> logicDeleteById(@RequestBody IdDto idDto) {
return CommonResult.success(cmsContentService.logicDeleteById(idDto.getId(), "admin") > 0);
@ -165,9 +171,208 @@ public class CmsContentController {
* @return 删除数据
*/
@PostMapping("deleteById/{contentId}")
@Operation(summary = "物理删除内容", description = "物理删除内容")
@Operation(summary = "物理删除skill", description = "物理删除skill")
@RequireAuth
public CommonResult<Boolean> deleteById(@PathVariable("contentId") Long contentId) {
return CommonResult.success(cmsContentService.deleteById(contentId) > 0);
}
/**
* 添加收藏
*
* @param contentId skillID
* @return 操作结果
*/
@PostMapping("/addFavorite")
@Operation(summary = "添加收藏", description = "添加skill收藏")
@RequireAuth
public CommonResult<Boolean> addFavorite(@RequestParam("contentId") Long contentId) {
return CommonResult.success(cmsContentService.addFavorite(contentId) > 0);
}
/**
* 取消收藏
*
* @param contentId skillID
* @return 操作结果
*/
@PostMapping("/removeFavorite")
@Operation(summary = "取消收藏", description = "取消skill收藏")
@RequireAuth
public CommonResult<Boolean> removeFavorite(@RequestParam("contentId") Long contentId) {
return CommonResult.success(cmsContentService.removeFavorite(contentId) > 0);
}
/**
* 添加查看记录
*
* @param contentId skillID
* @return 操作结果
*/
@PostMapping("/addView")
@Operation(summary = "添加查看记录", description = "添加skill查看记录")
@RequireAuth
public CommonResult<Boolean> addView(@RequestParam("contentId") Long contentId) {
return CommonResult.success(cmsContentService.addView(contentId) > 0);
}
/**
* 获取用户历史查看的内容列表
*
* @param queryDto 筛选条件包含分页信息
* @return 查询结果
*/
@PostMapping("/getUserHistory")
@Operation(summary = "获取用户历史查看", description = "获取当前用户历史查看的内容列表,带分页")
@RequireAuth
public CommonResult<PageInfo<CmsContent>> getUserHistory(@RequestBody CmsContentDto queryDto) {
return CommonResult.success(cmsContentService.getPageListByUserHistory(queryDto));
}
/**
* 获取用户收藏的内容列表
*
* @param queryDto 筛选条件包含分页信息
* @return 查询结果
*/
@PostMapping("/getUserFavorites")
@Operation(summary = "获取用户收藏", description = "获取当前用户收藏的内容列表,带分页")
@RequireAuth
public CommonResult<PageInfo<CmsContent>> getUserFavorites(@RequestBody CmsContentDto queryDto) {
return CommonResult.success(cmsContentService.getPageListByUserFavorites(queryDto));
}
/**
* 获取用户购买的内容列表
*
* @param queryDto 筛选条件包含分页信息
* @return 查询结果
*/
@PostMapping("/getUserPurchases")
@Operation(summary = "获取用户拥有", description = "获取当前用户购买的内容列表,带分页")
@RequireAuth
public CommonResult<PageInfo<CmsContent>> getUserPurchases(@RequestBody CmsContentDto queryDto) {
return CommonResult.success(cmsContentService.getPageListByUserPurchases(queryDto));
}
/**
* 获取用户创建的内容列表
*
* @param queryDto 筛选条件包含分页信息和发布状态
* @return 查询结果
*/
@PostMapping("/getUserCreated")
@Operation(summary = "获取用户创建", description = "获取当前用户创建的内容列表,带分页,可查询已发布、未发布")
@RequireAuth
public CommonResult<PageInfo<CmsContent>> getUserCreated(@RequestBody CmsContentDto queryDto) {
return CommonResult.success(cmsContentService.getPageListByUserCreated(queryDto));
}
/**
* 导入Excel数据到CmsContent
*
* @param file Excel文件
* @param createBy 创建人
* @return 导入结果
*/
@PostMapping("/importFromExcel")
@Operation(summary = "导入Excel数据", description = "从Excel文件导入数据到CmsContent")
@RequireAuth
public CommonResult<Integer> importFromExcel(@RequestParam("file") MultipartFile file, @RequestParam("createBy") String createBy) {
try {
byte[] fileBytes = file.getBytes();
int successCount = cmsContentService.importFromExcel(fileBytes, createBy);
return CommonResult.success(successCount);
} catch (IOException e) {
e.printStackTrace();
return CommonResult.failed("导入失败:" + e.getMessage());
}
}
/**
* 获取CmsContent的content字段内容
*
* @param queryContentDto 包含contentId和languageType的DTO对象
* @return content或contentEn字段的内容
*/
@PostMapping("/getContent")
@Operation(summary = "获取内容详情", description = "根据languageType获取content或contentEn字段的内容")
public CommonResult<String> getContent(@RequestBody QueryContentDto queryContentDto) {
String content = cmsContentService.getContent(queryContentDto.getContentId(), queryContentDto.getLanguageType());
return CommonResult.success(content);
}
/**
* 获取CmsContent的title字段内容
*
* @param contentId 内容ID
* @return title字段的内容
*/
@PostMapping("/getTitle/{contentId}")
@Operation(summary = "获取标题", description = "根据contentId获取title字段的内容")
public CommonResult<String> getTitle(@PathVariable("contentId") Long contentId) {
String title = cmsContentService.getTitle(contentId);
return CommonResult.success(title);
}
/**
* 从指定目录导入Excel数据到CmsContent
*
* @param importPathDto 导入路径请求参数
* @param createBy 创建人
* @return 导入结果
*/
@PostMapping("/importFromPath")
@Operation(summary = "从目录导入Excel数据", description = "从指定目录导入Excel数据到CmsContent")
@RequireAuth
public CommonResult<Integer> importFromPath(@RequestBody ImportPathDto importPathDto, @RequestParam("createBy") String createBy) {
try {
int successCount = cmsContentService.importFromPath(importPathDto, createBy);
return CommonResult.success(successCount);
} catch (Exception e) {
e.printStackTrace();
return CommonResult.failed("导入失败:" + e.getMessage());
}
}
/**
* 从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,7 @@
package com.kexue.skills.controller;
import com.kexue.skills.common.CommonResult;
import com.kexue.skills.config.UploadConfig;
import com.kexue.skills.entity.request.UploadResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
@ -8,6 +9,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
@ -21,13 +23,13 @@ import java.nio.file.StandardCopyOption;
*/
@RestController
@RequestMapping("api/common")
@Tag(name = "公共 Api")
@Tag(name = "Common Api")
@CrossOrigin(origins = "*")
@Slf4j
public class CommonController {
private static final String FILE_UPLOAD_DIR = "/data/service/hyxp-portal/upload/file/";
private static final String IMG_UPLOAD_DIR = "/data/service/hyxp-portal/upload/images/";
@Resource
private UploadConfig uploadConfig;
/**
@ -49,7 +51,7 @@ public class CommonController {
try {
// 创建上传目录如果不存在
Path uploadPath = Paths.get(FILE_UPLOAD_DIR);
Path uploadPath = Paths.get(uploadConfig.getFileUploadDir());
if (!Files.exists(uploadPath)) {
Files.createDirectories(uploadPath);
}
@ -62,7 +64,7 @@ public class CommonController {
UploadResponse uploadResponse = new UploadResponse();
uploadResponse.setFileName(file.getOriginalFilename());
uploadResponse.setFileUrl("/upload/file/" + file.getOriginalFilename());
uploadResponse.setFileUrl(uploadConfig.getFileUrlPrefix() + file.getOriginalFilename());
return CommonResult.success(uploadResponse);
} catch (IOException e) {
e.printStackTrace();
@ -94,7 +96,7 @@ public class CommonController {
try {
// 创建上传目录如果不存在
Path uploadPath = Paths.get(IMG_UPLOAD_DIR);
Path uploadPath = Paths.get(uploadConfig.getImgUploadDir());
if (!Files.exists(uploadPath)) {
Files.createDirectories(uploadPath);
}
@ -107,7 +109,7 @@ public class CommonController {
UploadResponse uploadResponse = new UploadResponse();
uploadResponse.setFileName(image.getOriginalFilename());
uploadResponse.setFileUrl("/upload/images/" + image.getOriginalFilename());
uploadResponse.setFileUrl(uploadConfig.getImgUrlPrefix() + image.getOriginalFilename());
return CommonResult.success(uploadResponse);
} catch (IOException e) {
e.printStackTrace();

View File

@ -1,5 +1,6 @@
package com.kexue.skills.controller;
import cn.dev33.satoken.stp.StpUtil;
import com.github.pagehelper.PageInfo;
import com.kexue.skills.annotation.RequireAuth;
import com.kexue.skills.common.CommonResult;
@ -73,7 +74,6 @@ public class ContentPurchaseController {
/**
* 购买内容
*
* @param userId 用户ID
* @param contentId 内容ID
* @param payType 支付方式1.余额支付 2.积分支付
* @return 购买结果
@ -82,16 +82,15 @@ public class ContentPurchaseController {
@PostMapping("/purchase")
@RequireAuth
public CommonResult<ContentPurchase> purchaseContent(
@Parameter(description = "用户ID") @RequestParam("userId") Long userId,
@Parameter(description = "内容ID") @RequestParam("contentId") Long contentId,
@Parameter(description = "支付方式1.余额支付 2.积分支付") @RequestParam("payType") Integer payType) {
Long userId = Long.parseLong(StpUtil.getLoginId().toString());
return CommonResult.success(this.contentPurchaseService.purchaseContent(userId, contentId, payType));
}
/**
* 检查用户是否有权限访问内容
*
* @param userId 用户ID
* @param contentId 内容ID
* @return 是否有权限
*/
@ -99,8 +98,8 @@ public class ContentPurchaseController {
@PostMapping("/checkPermission")
@RequireAuth
public CommonResult<Boolean> checkAccessPermission(
@Parameter(description = "用户ID") @RequestParam("userId") Long userId,
@Parameter(description = "内容ID") @RequestParam("contentId") Long contentId) {
Long userId = Long.parseLong(StpUtil.getLoginId().toString());
boolean hasPermission = this.contentPurchaseService.checkAccessPermission(userId, contentId);
return CommonResult.success(hasPermission);
}

View File

@ -1,6 +1,8 @@
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;
import com.kexue.skills.entity.request.LoginDto;
import com.kexue.skills.entity.request.LoginUserDto;
@ -13,12 +15,15 @@ import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import org.redisson.api.RedissonClient;
import static cn.dev33.satoken.SaManager.log;
/**
* (SysUser)表控制层
*
* @author 王志维
* @since 2024-04-13 01:25:22
*/
@Log(module = "登录认证")
@RestController
@RequestMapping("api/login")
@CrossOrigin(origins = "*")
@ -58,20 +63,27 @@ public class LoginController {
@Operation(summary = "用户登出", description = "用户登出")
public CommonResult<String> logout() {
try {
// 获取当前用户的token
// 获取当前用户的 token
String token = cn.dev33.satoken.stp.StpUtil.getTokenValue();
// 使用Sa-Token登出
cn.dev33.satoken.stp.StpUtil.logout();
// 从Redis中删除用户信息
// Redis 中删除用户信息
if (token != null && !token.isEmpty()) {
redissonClient.getBucket("loginUser:" + token).delete();
// 从缓存中移除 token 映射
String username = CacheManager.getUsernameFromToken(token);
if (username != null) {
CacheManager.removeTokenFromCache(username);
}
}
// 使用 Sa-Token 登出这会清除 Sa-Token 内部的会话信息
cn.dev33.satoken.stp.StpUtil.logout();
log.info("用户登出成功token 已清理");
return CommonResult.success("登出成功");
} catch (Exception e) {
// 如果获取token失败仍然执行登出操作
log.error("登出异常:{}", e.getMessage());
// 如果获取 token 失败仍然执行登出操作
cn.dev33.satoken.stp.StpUtil.logout();
return CommonResult.success("登出成功");
}

View File

@ -0,0 +1,113 @@
package com.kexue.skills.controller;
import com.github.pagehelper.PageInfo;
import com.kexue.skills.entity.ModelPrice;
import com.kexue.skills.entity.dto.ModelPriceDto;
import com.kexue.skills.service.ModelPriceService;
import com.kexue.skills.common.CommonResult;
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;
import java.util.List;
/**
* (ModelPrice)表控制层
* 大模型Token价格表
*
* @author 王志维
* @since 2026-03-26 10:15:00
*/
@RestController
@RequestMapping("/api/modelPrice")
@Tag(name = "大模型Token价格表管理", description = "大模型Token价格表管理接口")
public class ModelPriceController {
@Resource
private ModelPriceService modelPriceService;
/**
* 分页查询
*
* @param queryDto 筛选条件
* @return 查询结果
*/
@Operation(summary = "分页查询", description = "分页查询大模型Token价格表")
@PostMapping("/getPageList")
public CommonResult<PageInfo<ModelPrice>> getPageList(@RequestBody ModelPriceDto queryDto) {
return CommonResult.success(this.modelPriceService.getPageList(queryDto));
}
/**
* 查询列表
*
* @param queryDto 筛选条件
* @return 查询结果
*/
@Operation(summary = "查询列表", description = "查询大模型Token价格表列表")
@PostMapping("/getList")
public CommonResult<List<ModelPrice>> getList(@RequestBody ModelPriceDto queryDto) {
return CommonResult.success(this.modelPriceService.getList(queryDto));
}
/**
* 通过主键查询单条数据
*
* @param id 主键
* @return 实例对象
*/
@Operation(summary = "通过主键查询", description = "通过主键查询大模型Token价格表")
@GetMapping("/queryById/{id}")
public CommonResult<ModelPrice> queryById(@PathVariable("id") Long id) {
return CommonResult.success(this.modelPriceService.queryById(id));
}
/**
* 通过模型名称查询数据
*
* @param modelName 模型名称
* @return 实例对象列表
*/
@Operation(summary = "通过模型名称查询", description = "通过模型名称查询大模型Token价格表")
@GetMapping("/queryByModelName/{modelName}")
public CommonResult<List<ModelPrice>> queryByModelName(@PathVariable("modelName") String modelName) {
return CommonResult.success(this.modelPriceService.queryByModelName(modelName));
}
/**
* 新增数据
*
* @param modelPrice 实例对象
* @return 实例对象
*/
@Operation(summary = "新增数据", description = "新增大模型Token价格表")
@PostMapping("/insert")
public CommonResult<ModelPrice> insert(@RequestBody ModelPrice modelPrice) {
return CommonResult.success(this.modelPriceService.insert(modelPrice));
}
/**
* 更新数据
*
* @param modelPrice 实例对象
* @return 实例对象
*/
@Operation(summary = "更新数据", description = "更新大模型Token价格表")
@PostMapping("/update")
public CommonResult<ModelPrice> update(@RequestBody ModelPrice modelPrice) {
return CommonResult.success(this.modelPriceService.update(modelPrice));
}
/**
* 通过主键删除数据
*
* @param id 主键
* @return 影响行数
*/
@Operation(summary = "通过主键删除", description = "通过主键删除大模型Token价格表")
@PostMapping("/deleteById/{id}")
public CommonResult<Integer> deleteById(@PathVariable("id") Long id) {
return CommonResult.success(this.modelPriceService.deleteById(id));
}
}

View File

@ -0,0 +1,121 @@
package com.kexue.skills.controller;
import com.github.pagehelper.PageInfo;
import com.kexue.skills.annotation.RequireAuth;
import com.kexue.skills.common.CommonResult;
import com.kexue.skills.entity.PackageConfig;
import com.kexue.skills.entity.base.IdDto;
import com.kexue.skills.entity.dto.PackageConfigDto;
import com.kexue.skills.service.PackageConfigService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
/**
* (PackageConfig)表控制层
*
* @author 系统生成
* @since 2026-04-11
*/
@RestController
@RequestMapping("api/packageConfig")
@Tag(name = "套餐配置管理 Api")
@CrossOrigin(origins = "*")
public class PackageConfigController {
/**
* 服务对象
*/
@Resource
private PackageConfigService packageConfigService;
/**
* 分页查询
*
* @param queryDto 筛选条件
* @return 查询结果
*/
@PostMapping("/getPageList")
@Operation(summary = "查询分页列表", description = "查询分页列表")
public CommonResult<PageInfo<PackageConfig>> getPageList(@RequestBody PackageConfigDto queryDto) {
return CommonResult.success(packageConfigService.getPageList(queryDto));
}
/**
* 查询列表
*
* @param queryDto 筛选条件
* @return 查询结果
*/
@PostMapping("/getList")
@Operation(summary = "查询列表", description = "查询列表")
public CommonResult<PageInfo<PackageConfig>> getList(@RequestBody PackageConfigDto queryDto) {
return CommonResult.success(new PageInfo<>(packageConfigService.getList(queryDto)));
}
/**
* 通过主键查询单条数据
*
* @param id 主键
* @return 单条数据
*/
@PostMapping("queryById/{id}")
@Operation(summary = "通过ID查询套餐", description = "通过ID查询套餐")
public CommonResult<PackageConfig> queryById(@PathVariable("id") Long id) {
return CommonResult.success(packageConfigService.queryById(id));
}
/**
* 新增数据
*
* @param packageConfig 实体
* @return 新增结果
*/
@PostMapping("/insert")
@Operation(summary = "新增套餐", description = "新增套餐")
@RequireAuth
public CommonResult<PackageConfig> insert(@RequestBody PackageConfig packageConfig) {
return CommonResult.success(packageConfigService.insert(packageConfig));
}
/**
* 编辑数据
*
* @param packageConfig 实体
* @return 编辑结果
*/
@PostMapping("/update")
@Operation(summary = "更新套餐", description = "更新套餐")
@RequireAuth
public CommonResult<PackageConfig> update(@RequestBody PackageConfig packageConfig) {
return CommonResult.success(packageConfigService.update(packageConfig));
}
/**
* 通过主键逻辑删除
*
* @param idDto 主键
* @return 删除数据
*/
@PostMapping("/logicDeleteById")
@Operation(summary = "逻辑删除套餐", description = "逻辑删除套餐")
@RequireAuth
public CommonResult<Boolean> logicDeleteById(@RequestBody IdDto idDto) {
return CommonResult.success(packageConfigService.logicDeleteById(idDto.getId(), "admin") > 0);
}
/**
* 删除数据
*
* @param id 主键
* @return 删除数据
*/
@PostMapping("deleteById/{id}")
@Operation(summary = "物理删除套餐", description = "物理删除套餐")
@RequireAuth
public CommonResult<Boolean> deleteById(@PathVariable("id") Long id) {
return CommonResult.success(packageConfigService.deleteById(id) > 0);
}
}

View File

@ -0,0 +1,192 @@
package com.kexue.skills.controller;
import com.kexue.skills.common.CommonResult;
import com.kexue.skills.entity.PaymentOrder;
import com.kexue.skills.service.PayService;
import com.kexue.skills.service.PaymentOrderService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import com.kexue.skills.entity.dto.OrderStatusDto;
import com.kexue.skills.entity.dto.OrderStatusQueryDto;
import java.util.Map;
import java.util.Objects;
/**
* 支付控制器
*/
@Tag(name = "支付管理 Api")
@RestController
@RequestMapping("/api/pay")
public class PayController {
private static final Logger logger = LoggerFactory.getLogger(PayController.class);
@Resource
private PayService payService;
@Resource
private PaymentOrderService paymentOrderService;
/**
* 创建微信支付订单
* @param order 支付订单信息
* @param request HTTP 请求用于获取用户 IP
* @return 微信支付参数
*/
@Operation(summary = "创建微信支付订单", description = "创建微信支付订单")
@PostMapping("/wx/create")
public CommonResult<Map<String, String>> createWechatPay(@RequestBody PaymentOrder order, HttpServletRequest request) {
try {
// 设置支付类型为微信支付1
order.setPayType(1);
// 创建支付订单
PaymentOrder createdOrder = paymentOrderService.createPaymentOrder(order);
// 获取用户真实 IP
String ipAddress = getUserIpAddress(request);
// 生成微信支付参数
Map<String, String> payParams = payService.createWechatPay(createdOrder, ipAddress);
return CommonResult.success(payParams);
} catch (IllegalArgumentException e) {
logger.error("参数错误:{}", e.getMessage());
return CommonResult.failed(e.getMessage());
} catch (Exception e) {
logger.error("创建微信支付订单失败", e);
return CommonResult.failed("系统繁忙,请稍后重试");
}
}
/**
* 获取用户真实 IP 地址
*/
private String getUserIpAddress(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
// 如果是多代理取第一个 IP
if (ip != null && !ip.isEmpty() && ip.contains(",")) {
ip = ip.split(",")[0].trim();
}
return ip;
}
/**
* 处理微信支付回调
* @param request HTTP请求
* @return 回调响应
*/
@Operation(summary = "处理微信支付回调", description = "处理微信支付回调")
@PostMapping("/wx/notify")
public String handleWechatPayNotify(HttpServletRequest request) {
return payService.handleWechatPayNotify(request);
}
/**
* 创建支付宝支付订单
* @param order 支付订单信息
* @return 支付宝支付表单
*/
@Operation(summary = "创建支付宝支付订单", description = "创建支付宝支付订单")
@PostMapping("/alipay/create")
public CommonResult<String> createAlipay(@RequestBody PaymentOrder order) {
try {
// 设置支付类型为支付宝2
order.setPayType(2);
// 创建支付订单
PaymentOrder createdOrder = paymentOrderService.createPaymentOrder(order);
// 生成支付宝支付表单
String form = payService.createAlipay(createdOrder);
// 更新订单信息
createdOrder.setQrCode( form);
paymentOrderService.update(createdOrder);
return CommonResult.success(form);
} catch (Exception e) {
logger.error("创建支付宝支付订单失败", e);
return CommonResult.failed("创建支付宝支付订单失败: " + e.getMessage());
}
}
/**
* 处理支付宝支付回调
* @param request HTTP请求
* @return 回调响应
*/
@Operation(summary = "处理支付宝支付回调", description = "处理支付宝支付回调")
@PostMapping("/alipay/trade/notify")
public String handleAlipayNotify(HttpServletRequest request) {
return payService.handleAlipayNotify(request);
}
/**
* 处理支付宝支付同步回调
* @param request HTTP请求
* @return 同步回调响应
*/
@Operation(summary = "处理支付宝支付同步回调", description = "处理支付宝支付同步回调")
@GetMapping("/alipay/trade/return")
public CommonResult<Map<String, Object>> handleAlipayReturn(HttpServletRequest request) {
Map<String, Object> result = payService.handleAlipayReturn(request);
if (result.get("success").equals(true)) {
return CommonResult.success(result);
} else {
return CommonResult.failed(result.get("message").toString());
}
}
/**
* 查询订单状态
* @param queryDto 查询参数包含 orderId orderNo
* @return 订单状态信息
*/
@Operation(summary = "查询订单状态", description = "根据订单 id 或 orderNo 查询订单状态")
@PostMapping("/queryOrderStatus")
public CommonResult<OrderStatusDto> queryOrderStatus(@RequestBody OrderStatusQueryDto queryDto) {
try {
// 检查参数是否有效
if (Objects.isNull(queryDto.getOrderId()) && Objects.isNull(queryDto.getOrderNo())) {
return CommonResult.failed("请提供 orderId 或 orderNo 参数");
}
PaymentOrder order = null;
// 根据订单 id 查询
if (queryDto.getOrderId() != null) {
order = paymentOrderService.queryById(queryDto.getOrderId());
}
// 根据订单号查询
else if (queryDto.getOrderNo() != null && !queryDto.getOrderNo().trim().isEmpty()) {
order = paymentOrderService.queryByOrderNo(queryDto.getOrderNo());
}
if (order == null) {
return CommonResult.failed("订单不存在");
}
// 构建响应数据
OrderStatusDto result = OrderStatusDto.fromPaymentOrder(order);
return CommonResult.success(result);
} catch (Exception e) {
logger.error("查询订单状态失败", e);
return CommonResult.failed("系统繁忙,请稍后重试");
}
}
}

View File

@ -1,85 +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.PointsAccount;
import com.kexue.skills.entity.dto.PointsAccountDto;
import com.kexue.skills.service.PointsAccountService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
/**
* (PointsAccount)表控制层
* 积分账户管理控制器
*
* @author 王志维
* @since 2025-02-21 23:01:48
*/
@Tag(name = "积分账户管理 Api")
@RestController
@RequestMapping("/api/pointsAccount")
public class PointsAccountController {
/**
* 服务对象
*/
@Resource
private PointsAccountService pointsAccountService;
/**
* 分页查询
*
* @param queryDto 查询参数
* @return 分页结果
*/
@Operation(summary = "分页查询积分账户", description = "分页查询积分账户")
@PostMapping("/pageList")
@RequireAuth
public CommonResult<PageInfo<PointsAccount>> getPageList(@RequestBody PointsAccountDto queryDto) {
return CommonResult.success(this.pointsAccountService.getPageList(queryDto));
}
/**
* 查询列表
*
* @param queryDto 查询参数
* @return 列表结果
*/
@Operation(summary = "查询积分账户列表", description = "查询积分账户列表")
@PostMapping("/list")
@RequireAuth
public CommonResult<List<PointsAccount>> getList(@RequestBody PointsAccountDto queryDto) {
return CommonResult.success(this.pointsAccountService.getList(queryDto));
}
/**
* 通过主键查询单条数据
*
* @param accountId 主键
* @return 单条数据
*/
@Operation(summary = "通过ID查询积分账户", description = "通过ID查询积分账户")
@PostMapping("/queryById/{accountId}")
@RequireAuth
public CommonResult<PointsAccount> queryById(@Parameter(description = "账户ID") @PathVariable("accountId") Long accountId) {
return CommonResult.success(this.pointsAccountService.queryById(accountId));
}
/**
* 通过用户ID查询单条数据
*
* @param userId 用户ID
* @return 单条数据
*/
@Operation(summary = "通过用户ID查询积分账户", description = "通过用户ID查询积分账户")
@PostMapping("/queryByUserId/{userId}")
@RequireAuth
public CommonResult<PointsAccount> queryByUserId(@Parameter(description = "用户ID") @PathVariable("userId") Long userId) {
return CommonResult.success(this.pointsAccountService.queryByUserId(userId));
}
}

View File

@ -0,0 +1,41 @@
package com.kexue.skills.controller;
import com.kexue.skills.common.CommonResult;
import com.kexue.skills.entity.dto.SessionDto;
import com.kexue.skills.service.SysUserService;
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;
/**
* Session管理控制器
*
* @author 王志维
* @since 2026-03-12
*/
@RestController
@RequestMapping("api/session")
@Tag(name = "Session管理 Api")
@CrossOrigin(origins = "*")
public class SessionController {
/**
* 服务对象
*/
@Resource
private SysUserService sysUserService;
/**
* 创建或获取用户会话
*
* @param userId 包含用户ID的DTO对象
* @return 会话信息包含sessionId和isNew字段
*/
@GetMapping("/createSession")
@Operation(summary = "创建或获取会话", description = "根据用户ID创建或获取会话信息")
public CommonResult<SessionDto> createSession(@RequestParam Long userId) {
SessionDto sessionDto = sysUserService.createSession(userId);
return CommonResult.success(sessionDto);
}
}

View File

@ -1,10 +1,18 @@
package com.kexue.skills.controller;
import com.kexue.skills.common.CommonResult;
import com.kexue.skills.entity.CmsContent;
import com.kexue.skills.entity.dto.YamlContentDto;
import com.kexue.skills.entity.request.GenIntroduceRequest;
import com.kexue.skills.entity.request.SkillGenRequest;
import com.kexue.skills.entity.request.SkillPreGenRequest;
import com.kexue.skills.entity.request.SkillUploadDto;
import com.kexue.skills.entity.response.SkillResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
/**
@ -28,10 +36,16 @@ public class SkillGenController {
* @param request 生成请求
* @return 生成结果
*/
@PostMapping("/preGenerate")
@Operation(summary = "预生成技能", description = "生成技能")
public CommonResult<SkillResponse> preGenerate(@RequestBody SkillPreGenRequest request) {
return CommonResult.success(skillGenService.preGenerateV2(request));
}
@PostMapping("/generate")
@Operation(summary = "生成技能", description = "生成技能")
public CommonResult<SkillResponse> generate(@RequestBody com.kexue.skills.entity.request.SkillGenRequest request) {
return CommonResult.success(skillGenService.generateSkill(request));
public CommonResult<CmsContent> generate(@RequestBody SkillGenRequest request) {
return CommonResult.success(skillGenService.generate(request));
}
/**
@ -45,4 +59,93 @@ public class SkillGenController {
public CommonResult<String> analyze(@RequestBody com.kexue.skills.entity.request.SkillAnalyzeRequest request) {
return CommonResult.success(skillGenService.analyzeSkill(request));
}
/**
* 生成技能介绍
*
* @param request 技能内容
* @return 技能介绍
*/
@PostMapping("/genIntroduce")
@Operation(summary = "生成技能介绍", description = "生成技能介绍")
public CommonResult<String> genIntroduce(@RequestBody GenIntroduceRequest request) {
return CommonResult.success(skillGenService.genIntroduce(request.getContent()));
}
/**
* 根据技能描述生成技能介绍
*
* @param request 技能描述
* @return 技能介绍
*/
@PostMapping("/genIntroduceByDescription")
@Operation(summary = "根据技能描述生成技能介绍", description = "根据技能描述生成技能介绍")
public CommonResult<String> genIntroduceByDescription(@RequestBody GenIntroduceRequest request) {
return CommonResult.success(skillGenService.genIntroduceByDescription(request.getContent()));
}
/**
* 上传技能压缩包
*
* @param skillUploadDto 技能压缩包URL
* @return 生成的技能内容
*/
@PostMapping("/uploadSkill")
@Operation(summary = "上传技能压缩包", description = "上传技能压缩包并生成技能")
public CommonResult<CmsContent> uploadSkill(@RequestBody SkillUploadDto skillUploadDto) {
return CommonResult.success(skillGenService.uploadSkill(skillUploadDto.getUrl()));
}
/**
* 上传本地技能压缩包V2
*
* @param file 技能压缩包文件
* @return 生成的技能内容
*/
@PostMapping("/uploadSkillV2")
@Operation(summary = "上传本地技能压缩包V2", description = "上传本地zip或rar文件并生成技能")
public CommonResult<CmsContent> uploadSkillV2(
@RequestParam("file") MultipartFile file) {
try {
byte[] fileBytes = file.getBytes();
String fileName = file.getOriginalFilename();
CmsContent cmsContent = skillGenService.uploadSkillV2(fileBytes, fileName);
return CommonResult.success(cmsContent);
} catch (Exception e) {
return CommonResult.failed("上传失败:" + e.getMessage());
}
}
/**
* 上传本地技能压缩包V3 直接传入yamlContent
*
* @param yamlContentDto 技能压缩包文件
* @return 生成的技能内容
*/
@PostMapping("/uploadSkillV3")
@Operation(summary = "上传本地技能压缩包V3,直接传入yamlContent", description = "直接传入yamlContent")
public CommonResult<CmsContent> uploadSkillV3(@RequestBody YamlContentDto yamlContentDto) {
CmsContent cmsContent = skillGenService.uploadSkillV3(yamlContentDto.getYamlContent());
return CommonResult.success(cmsContent);
}
/**
* 上传本地技能压缩包V2
*
* @param file 技能压缩包文件
* @return 生成的技能内容
*/
@PostMapping("/uploadSkillV4")
@Operation(summary = "上传本地技能压缩包V2", description = "上传本地zip或rar文件并生成技能")
public CommonResult<CmsContent> uploadSkillV4(
@RequestParam("file") MultipartFile file) {
try {
byte[] fileBytes = file.getBytes();
String fileName = file.getOriginalFilename();
CmsContent cmsContent = skillGenService.uploadSkillV4(fileBytes, fileName);
return CommonResult.success(cmsContent);
} catch (Exception e) {
return CommonResult.failed("上传失败:" + e.getMessage());
}
}
}

View File

@ -3,9 +3,7 @@ package com.kexue.skills.controller;
import com.kexue.skills.annotation.RequireAuth;
import com.kexue.skills.entity.SysUser;
import com.kexue.skills.entity.dto.SysUserDto;
import com.kexue.skills.entity.request.ResetPasswordDto;
import com.kexue.skills.entity.request.ResetPwdDto;
import com.kexue.skills.entity.request.AdminResetPasswordDto;
import com.kexue.skills.entity.request.*;
import com.kexue.skills.exception.BizException;
import com.kexue.skills.service.SysUserService;
import org.springframework.web.bind.annotation.*;
@ -17,8 +15,12 @@ import com.kexue.skills.common.CacheManager;
import com.github.pagehelper.PageInfo;
import com.kexue.skills.common.CommonResult;
import com.kexue.skills.entity.base.IdDto;
import com.kexue.skills.entity.request.LoginUserDto;
import org.redisson.api.RedissonClient;
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)表控制层
@ -86,7 +88,7 @@ public class SysUserController {
*/
@PostMapping("/update")
@Operation(summary = "更新用户", description = "更新用户")
public CommonResult<SysUser> update(@RequestBody SysUser SysUser) {
public CommonResult<SysUser> update(@RequestBody SysUserUpdateDto SysUser) {
return CommonResult.success(sysUserService.update(SysUser));
}
@ -111,25 +113,8 @@ public class SysUserController {
@PostMapping("/resetPassword")
@Operation(summary = "管理员帮助用户重置密码", description = "管理员帮助用户重置密码")
@RequireAuth
public CommonResult<Boolean> resetPasswordByAdmin(@RequestBody ResetPasswordDto resetPasswordDto, HttpServletRequest request) {
// 从请求头中获取token
String token = request.getHeader("Authorization");
if (token == null || token.isEmpty()) {
throw new BizException("请先登录认证后操作");
}
// 从缓存中获取当前登录用户
String username = CacheManager.getUsernameFromToken(token);
if (username == null) {
throw new BizException("无效的token请重新登录");
}
SysUser adminUser = sysUserService.getByUsername(username);
if (adminUser == null) {
throw new BizException("管理员不存在");
}
boolean result = sysUserService.resetPasswordByAdmin(resetPasswordDto);
public CommonResult<Boolean> resetPassword(@RequestBody ResetPwdDto resetPasswordDto) {
boolean result = sysUserService.resetPassword(resetPasswordDto);
return CommonResult.success(result);
}
@ -215,6 +200,13 @@ public class SysUserController {
throw new BizException("请先登录认证后操作");
}
// 使用Sa-Token检查token是否有效
try {
cn.dev33.satoken.stp.StpUtil.checkLogin();
} catch (Exception e) {
throw new BizException("无效的token请重新登录");
}
// 从Redis缓存中获取LoginUser对象
String loginUserJson = (String)redissonClient.getBucket("loginUser:" + token).get();
if (loginUserJson == null || loginUserJson.isEmpty()) {
@ -235,4 +227,44 @@ 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

@ -0,0 +1,129 @@
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.WithdrawalRecord;
import com.kexue.skills.entity.dto.WithdrawalRecordDto;
import com.kexue.skills.service.WithdrawalRecordService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
import java.math.BigDecimal;
/**
* 提现记录控制器
*
* @author 王志维
* @since 2026-03-25
*/
@Tag(name = "提现记录管理 Api")
@RestController
@RequestMapping("/api/withdrawalRecord")
public class WithdrawalRecordController {
/**
* 服务对象
*/
@Resource
private WithdrawalRecordService withdrawalRecordService;
/**
* 分页查询
*
* @param queryDto 查询参数
* @return 分页结果
*/
@Operation(summary = "分页查询提现记录", description = "分页查询提现记录")
@PostMapping("/pageList")
@RequireAuth
public CommonResult<PageInfo<WithdrawalRecord>> getPageList(@RequestBody WithdrawalRecordDto queryDto) {
return CommonResult.success(this.withdrawalRecordService.getPageList(queryDto));
}
/**
* 查询列表
*
* @param queryDto 查询参数
* @return 列表结果
*/
@Operation(summary = "查询提现记录列表", description = "查询提现记录列表")
@PostMapping("/list")
@RequireAuth
public CommonResult<List<WithdrawalRecord>> getList(@RequestBody WithdrawalRecordDto queryDto) {
return CommonResult.success(this.withdrawalRecordService.getList(queryDto));
}
/**
* 通过主键查询单条数据
*
* @param recordId 主键
* @return 单条数据
*/
@Operation(summary = "通过ID查询提现记录", description = "通过ID查询提现记录")
@PostMapping("/queryById/{recordId}")
@RequireAuth
public CommonResult<WithdrawalRecord> queryById(@Parameter(description = "提现记录ID") @PathVariable("recordId") Long recordId) {
return CommonResult.success(this.withdrawalRecordService.queryById(recordId));
}
/**
* 通过用户ID查询提现记录
*
* @param userId 用户ID
* @return 提现记录列表
*/
@Operation(summary = "通过用户ID查询提现记录", description = "通过用户ID查询提现记录")
@PostMapping("/queryByUserId/{userId}")
@RequireAuth
public CommonResult<List<WithdrawalRecord>> queryByUserId(@Parameter(description = "用户ID") @PathVariable("userId") Long userId) {
return CommonResult.success(this.withdrawalRecordService.queryByUserId(userId));
}
/**
* 提交提现申请
*
* @param userId 用户ID
* @param amount 提现金额
* @param bankName 银行名称
* @param bankAccount 银行账号
* @param bankCardholder 持卡人姓名
* @param remark 备注
* @return 提现记录
*/
@Operation(summary = "提交提现申请", description = "提交提现申请")
@PostMapping("/submit")
@RequireAuth
public CommonResult<WithdrawalRecord> submitWithdrawal(
@Parameter(description = "用户ID") @RequestParam("userId") Long userId,
@Parameter(description = "提现金额") @RequestParam("amount") BigDecimal amount,
@Parameter(description = "银行名称") @RequestParam("bankName") String bankName,
@Parameter(description = "银行账号") @RequestParam("bankAccount") String bankAccount,
@Parameter(description = "持卡人姓名") @RequestParam("bankCardholder") String bankCardholder,
@Parameter(description = "备注") @RequestParam("remark") String remark) {
WithdrawalRecord record = this.withdrawalRecordService.submitWithdrawal(userId, amount, bankName, bankAccount, bankCardholder, remark);
return CommonResult.success(record);
}
/**
* 处理提现
*
* @param recordId 记录ID
* @param status 状态
* @param remark 备注
* @return 处理结果
*/
@Operation(summary = "处理提现", description = "处理提现")
@PostMapping("/process")
@RequireAuth
public CommonResult<Integer> processWithdrawal(
@Parameter(description = "提现记录ID") @RequestParam("recordId") Long recordId,
@Parameter(description = "状态3.成功 4.失败") @RequestParam("status") Integer status,
@Parameter(description = "备注") @RequestParam("remark") String remark) {
int result = this.withdrawalRecordService.processWithdrawal(recordId, status, remark);
return CommonResult.success(result);
}
}

View File

@ -29,9 +29,15 @@ public class Account extends BaseEntity implements Serializable {
@Schema(description ="用户名")
private String userName;
@Schema(description ="账户余额")
@Schema(description ="账户余额")
private BigDecimal balance;
@Schema(description ="可提现余额")
private BigDecimal withdrawableBalance;
@Schema(description ="不可提现余额")
private BigDecimal nonWithdrawableBalance;
@Schema(description ="冻结金额")
private BigDecimal frozenAmount;

View File

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

@ -29,7 +29,7 @@ public class AccountTransaction extends BaseEntity implements Serializable {
@Schema(description ="用户名")
private String userName;
@Schema(description ="交易类型1.充值 2.提现 3.购买内容 4.退款 5.其他")
@Schema(description ="交易类型1.充值 2.提现 3.购买内容 4.退款 5.签到奖励 6.赠送 7.其他")
private Integer transactionType;
@Schema(description ="交易金额")
@ -56,9 +56,33 @@ public class AccountTransaction extends BaseEntity implements Serializable {
@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 ="对应回答的问题或需求")
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;

View File

@ -9,33 +9,23 @@ import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* (PointsAccount)实体类
* 积分账户表记录用户的积分信息
* (CmsCategoryTag)实体类
*
* @author 王志维
* @since 2025-02-21 23:01:48
*/
@Data
public class PointsAccount extends BaseEntity implements Serializable {
public class CmsCategoryTag extends BaseEntity implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description ="主键ID")
private Long accountId;
private Long id;
@Schema(description ="用户ID")
private Long userId;
@Schema(description ="分类ID")
private Long categoryId;
@Schema(description ="用户名")
private String userName;
@Schema(description ="总积分")
private Integer totalPoints;
@Schema(description ="可用积分")
private Integer availablePoints;
@Schema(description ="冻结积分")
private Integer frozenPoints;
@Schema(description ="标签ID")
private Long tagId;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@Schema(description ="创建时间")
@ -45,13 +35,13 @@ public class PointsAccount extends BaseEntity implements Serializable {
@Schema(description ="更新时间")
private Date updateTime;
@Schema(description ="是否删除 0 未删除1已删除")
private Integer deleteFlag;
@Schema(description ="创建人")
private String createBy;
@Schema(description ="更新人")
private String updateBy;
@Schema(description ="是否删除 0 未删除1已删除")
private Integer deleteFlag;
}

View File

@ -27,12 +27,12 @@ public class CmsContent extends BaseEntity implements Serializable {
@Schema(description ="标题")
private String title;
@Schema(description ="英文标题")
private String titleEn;
@Schema(description ="是否是官方0否1是")
private Boolean isOfficial;
@Schema(description ="分类ID列表逗号分隔")
private String categoryIds;
@Schema(description ="图标")
private String icon;
@ -45,6 +45,21 @@ public class CmsContent extends BaseEntity implements Serializable {
@Schema(description ="内容摘要")
private String summary;
@Schema(description ="详细描述")
private String description;
@Schema(description ="英文描述")
private String descriptionEn;
@Schema(description ="需求说明")
private String requirement;
@Schema(description ="介绍信息")
private String introduce;
@Schema(description ="英文介绍")
private String introduceEn;
@Schema(description ="分享数量")
private Integer shareCount;
@ -64,6 +79,9 @@ public class CmsContent extends BaseEntity implements Serializable {
@Schema(description ="内容详情")
private String content;
@Schema(description ="英文内容")
private String contentEn;
@Schema(description ="封面图片")
private String coverImage;
@ -79,14 +97,14 @@ public class CmsContent extends BaseEntity implements Serializable {
@Schema(description ="审核人名称")
private String reviewerName;
@Schema(description ="审核状态1草稿2待审核3审核通过4审核拒绝")
@Schema(description ="审核状态1未发布2待审核3审核通过4审核未通过")
private Integer auditStatus;
@Schema(description ="审核意见")
private String auditComment;
@Schema(description ="发布状态1未发布2已发布3已下架")
private Integer publishStatus;
@Schema(description ="发布状态1未发布2已发布3已下架--> 公有还是私有1私有2公有")
private Integer publishStatus;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@Schema(description ="发布时间")
@ -129,27 +147,10 @@ public class CmsContent extends BaseEntity implements Serializable {
@Schema(description ="副标题")
private String subtitle;
@Schema(description ="父分类ID")
private Long parentCategoryId;
@Schema(description ="来源")
private String origin;
// 用于接收前端发送的分类ID数组
@JsonProperty("categoryIds")
public void setCategoryIdsFromArray(List<Long> categoryIdList) {
if (categoryIdList != null && !categoryIdList.isEmpty()) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < categoryIdList.size(); i++) {
sb.append(categoryIdList.get(i));
if (i < categoryIdList.size() - 1) {
sb.append(",");
}
}
this.categoryIds = sb.toString();
}
}
// 用于接收前端发送的分类ID字符串
public void setCategoryIds(String categoryIds) {
this.categoryIds = categoryIds;
}
@Schema(description ="标签")
private String tags;
}

View File

@ -33,6 +33,9 @@ public class CmsTag extends BaseEntity implements Serializable {
@Schema(description ="状态1启用2禁用")
private Integer status;
@Schema(description ="标签图标")
private String icon;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@Schema(description ="创建时间")
private Date createTime;

View File

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

@ -0,0 +1,67 @@
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;
/**
* (ModelPrice)实体类
* 大模型Token价格表
*
* @author 王志维
* @since 2026-03-26 10:15:00
*/
@Data
public class ModelPrice extends BaseEntity implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description ="主键ID")
private Long id;
@Schema(description ="厂商")
private String vendor;
@Schema(description ="模型名称")
private String modelName;
@Schema(description ="输入价格:元/百万Token")
private BigDecimal inputPrice;
@Schema(description ="输出价格:元/百万Token")
private BigDecimal outputPrice;
@Schema(description ="1分钱可购买输入Token数")
private Long inputPerCent;
@Schema(description ="1分钱可购买输出Token数")
private Long outputPerCent;
@Schema(description ="价格单位")
private String unit;
@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;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@Schema(description ="更新时间")
private Date updatedTime;
}

View File

@ -0,0 +1,43 @@
package com.kexue.skills.entity;
import java.io.Serializable;
import com.kexue.skills.entity.base.BaseEntity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
/**
* (PackageConfig)实体类
* 套餐配置表
*
* @author 系统生成
* @since 2026-04-11
*/
@Data
public class PackageConfig extends BaseEntity implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description ="套餐ID")
private Long id;
@Schema(description ="套餐名称")
private String name;
@Schema(description ="价格")
private BigDecimal price;
@Schema(description ="基础额度")
private BigDecimal baseAmount;
@Schema(description ="赠送额度")
private BigDecimal giftAmount;
@Schema(description ="创建时间")
private Date createTime;
@Schema(description ="更新时间")
private Date updateTime;
}

View File

@ -44,16 +44,22 @@ public class PaymentOrder extends BaseEntity implements Serializable {
@Schema(description ="支付渠道订单号")
private String channelOrderNo;
@Schema(description ="微信二维码URL")
private String codeUrl;
@Schema(description ="支付宝二维码HTML内容")
private String qrCode;
@Schema(description ="商品名称")
private String productName;
@Schema(description ="商品描述")
private String productDesc;
@Schema(description ="关联业务ID")
@Schema(description ="关联业务ID比如packageId套餐ID")
private Long businessId;
@Schema(description ="业务类型")
@Schema(description ="业务类型recharge,purchase_content")
private String businessType;
@Schema(description ="支付回调地址")

View File

@ -1,78 +0,0 @@
package com.kexue.skills.entity;
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;
/**
* (PointsTransaction)实体类
* 积分流水表记录用户的积分变动情况
*
* @author 王志维
* @since 2025-02-21 23:01:48
*/
@Data
public class PointsTransaction extends BaseEntity implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description ="主键ID")
private Long transactionId;
@Schema(description ="用户ID")
private Long userId;
@Schema(description ="用户名")
private String userName;
@Schema(description ="积分变动类型1.获取积分 2.消费积分 3.过期 4.其他")
private Integer transactionType;
@Schema(description ="变动积分")
private Integer points;
@Schema(description ="变动前积分")
private Integer beforePoints;
@Schema(description ="变动后积分")
private Integer afterPoints;
@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 ="备注")
private String remark;
@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

@ -22,29 +22,59 @@ public class SysLog extends BaseEntity implements Serializable {
@Schema(description ="主键ID")
private Long logId;
@Schema(description ="用户ID")
private String userId;
@Schema(description ="链路ID")
private String traceId;
@Schema(description ="用户名称")
private String userName;
@Schema(description ="日志描述")
private String description;
@Schema(description ="日志类型")
private String logType;
@Schema(description ="所属模块")
private String module;
@Schema(description ="日志类容")
private String logContent;
@Schema(description ="请求URL")
private String requestUrl;
@Schema(description ="服务端IP")
private String serverIp;
@Schema(description ="请求方式")
private String requestMethod;
@Schema(description ="客户端IP")
private String clientIp;
@Schema(description ="请求头")
private String requestHeaders;
@Schema(description ="yyyyMMddHHmmss")
private String logTime;
@Schema(description ="请求体")
private String requestBody;
@Schema(description ="备注")
private String note;
@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;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@Schema(description ="创建时间")

View File

@ -63,4 +63,19 @@ public class SysUser extends BaseEntity implements Serializable {
@Schema(description ="更新人")
private String updateBy;
@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

@ -0,0 +1,75 @@
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;
/**
* 提现记录实体类
*
* @author 王志维
* @since 2026-03-25
*/
@Data
public class WithdrawalRecord extends BaseEntity implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description ="主键ID")
private Long recordId;
@Schema(description ="用户ID")
private Long userId;
@Schema(description ="用户名")
private String userName;
@Schema(description ="提现金额")
private BigDecimal withdrawalAmount;
@Schema(description ="手续费")
private BigDecimal feeAmount;
@Schema(description ="实际到账金额")
private BigDecimal actualAmount;
@Schema(description ="提现状态1.待处理 2.处理中 3.成功 4.失败")
private Integer status;
@Schema(description ="提现单号")
private String withdrawalNo;
@Schema(description ="银行名称")
private String bankName;
@Schema(description ="银行账号")
private String bankAccount;
@Schema(description ="持卡人姓名")
private String bankCardholder;
@Schema(description ="备注")
private String remark;
@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 ="是否删除 0 未删除1已删除")
private Integer deleteFlag;
@Schema(description ="创建人")
private String createBy;
@Schema(description ="更新人")
private String updateBy;
}

View File

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

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

View File

@ -0,0 +1,28 @@
package com.kexue.skills.entity.dto;
import com.kexue.skills.entity.base.BaseQueryDto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* (CmsCategoryTag)查询DTO
*
* @author 王志维
* @since 2025-02-21 23:01:48
*/
@Data
public class CmsCategoryTagDto extends BaseQueryDto {
@Schema(description ="主键ID")
private Long id;
@Schema(description ="分类ID")
private Long categoryId;
@Schema(description ="标签ID")
private Long tagId;
@Schema(description ="是否删除 0 未删除1已删除")
private Integer deleteFlag;
}

View File

@ -20,12 +20,6 @@ public class CmsContentDto extends BaseQueryDto {
private Integer contentType;
private String categoryIds;
private Long categoryId;
private List<String> categoryIdList;
private Boolean isOfficial;
private Integer shareCount;
@ -44,4 +38,36 @@ public class CmsContentDto extends BaseQueryDto {
private Long parentCategoryId;
private Long tagId;
/**
* 标签 ID 列表用于批量查询
*/
private List<Long> tagIdList;
/**
* 语言类型0 中文1 英文
*/
private Integer languageType;
/**
* 搜索关键字同时搜索 titledescriptiontags
*/
private String keyword;
/**
* 来源
*/
private String origin;
/**
* 标签
*/
private String tags;
/**
* 图标
*/
private String icon;
}

View File

@ -20,6 +20,8 @@ public class CmsTagDto extends BaseQueryDto {
private Integer status;
private String icon;
private Integer deleteFlag;
}

View File

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

@ -0,0 +1,66 @@
package com.kexue.skills.entity.dto;
import java.math.BigDecimal;
/**
* 赠送金额参数DTO
*
* @author 王志维
* @since 2026-03-26
*/
public class GiftBalanceDto {
private Long userId;
private BigDecimal amount;
private String transactionNo;
private Long businessId;
private String businessType;
private String remark;
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public BigDecimal getAmount() {
return amount;
}
public void setAmount(BigDecimal amount) {
this.amount = amount;
}
public String getTransactionNo() {
return transactionNo;
}
public void setTransactionNo(String transactionNo) {
this.transactionNo = transactionNo;
}
public Long getBusinessId() {
return businessId;
}
public void setBusinessId(Long businessId) {
this.businessId = businessId;
}
public String getBusinessType() {
return businessType;
}
public void setBusinessType(String businessType) {
this.businessType = businessType;
}
public String getRemark() {
return remark;
}
public void setRemark(String remark) {
this.remark = remark;
}
}

View File

@ -0,0 +1,53 @@
package com.kexue.skills.entity.dto;
import com.kexue.skills.entity.base.BaseQueryDto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
/**
* (ModelPrice)数据传输对象
* 大模型Token价格表
*
* @author 王志维
* @since 2026-03-26 10:15:00
*/
@Data
public class ModelPriceDto extends BaseQueryDto {
@Schema(description ="主键ID")
private Long id;
@Schema(description ="厂商")
private String vendor;
@Schema(description ="模型名称")
private String modelName;
@Schema(description ="输入价格:元/百万Token")
private BigDecimal inputPrice;
@Schema(description ="输出价格:元/百万Token")
private BigDecimal outputPrice;
@Schema(description ="1分钱可购买输入Token数")
private Long inputPerCent;
@Schema(description ="1分钱可购买输出Token数")
private Long outputPerCent;
@Schema(description ="价格单位")
private String unit;
@Schema(description ="备注")
private String remark;
@Schema(description ="创建时间")
private Date createdTime;
@Schema(description ="更新时间")
private Date updatedTime;
}

View File

@ -0,0 +1,77 @@
package com.kexue.skills.entity.dto;
import com.kexue.skills.entity.PaymentOrder;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
/**
* 订单状态DTO
*/
@Data
public class OrderStatusDto {
@Schema(description = "订单ID")
private Long orderId;
@Schema(description = "订单号")
private String orderNo;
@Schema(description = "状态码")
private Integer status;
@Schema(description = "状态文本")
private String statusText;
@Schema(description = "支付金额")
private BigDecimal amount;
@Schema(description = "支付方式1.微信 2.支付宝")
private Integer payType;
@Schema(description = "创建时间")
private Date createTime;
@Schema(description = "支付时间")
private Date payTime;
@Schema(description = "渠道订单号")
private String channelOrderNo;
/**
* 从PaymentOrder构建OrderStatusDto
* @param order 支付订单
* @return 订单状态DTO
*/
public static OrderStatusDto fromPaymentOrder(PaymentOrder order) {
OrderStatusDto dto = new OrderStatusDto();
dto.setOrderId(order.getOrderId());
dto.setOrderNo(order.getOrderNo());
dto.setStatus(order.getStatus());
dto.setStatusText(getStatusText(order.getStatus()));
dto.setAmount(order.getAmount());
dto.setPayType(order.getPayType());
dto.setCreateTime(order.getCreateTime());
dto.setPayTime(order.getPayTime());
dto.setChannelOrderNo(order.getChannelOrderNo());
return dto;
}
/**
* 获取状态文本
* @param status 状态码
* @return 状态文本
*/
private static String getStatusText(Integer status) {
switch (status) {
case 1: return "待支付";
case 2: return "已支付";
case 3: return "支付失败";
case 4: return "已取消";
case 5: return "已退款";
default: return "未知状态";
}
}
}

View File

@ -0,0 +1,18 @@
package com.kexue.skills.entity.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 订单状态查询DTO
*/
@Data
public class OrderStatusQueryDto {
@Schema(description = "订单ID")
private Long orderId;
@Schema(description = "订单号")
private String orderNo;
}

View File

@ -0,0 +1,27 @@
package com.kexue.skills.entity.dto;
import com.kexue.skills.entity.base.BaseQueryDto;
import lombok.Data;
import java.math.BigDecimal;
/**
* (PackageConfig)查询DTO类
*
* @author 系统生成
* @since 2026-04-11
*/
@Data
public class PackageConfigDto extends BaseQueryDto {
private Long id;
private String name;
private BigDecimal price;
private BigDecimal baseAmount;
private BigDecimal giftAmount;
}

View File

@ -2,6 +2,9 @@ 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类
@ -30,4 +33,10 @@ public class PaymentOrderDto extends BaseQueryDto {
private Integer deleteFlag;
private Long packageId;
private Date createTimeStart;
private Date createTimeEnd;
}

View File

@ -1,52 +0,0 @@
package com.kexue.skills.entity.dto;
import com.kexue.skills.entity.base.BaseQueryDto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.Date;
/**
* (PointsAccount)查询条件封装类
* 积分账户查询条件
*
* @author 王志维
* @since 2025-02-21 23:01:48
*/
@Data
public class PointsAccountDto extends BaseQueryDto {
@Schema(description ="主键ID")
private Long accountId;
@Schema(description ="用户ID")
private Long userId;
@Schema(description ="用户名")
private String userName;
@Schema(description ="总积分")
private Integer totalPoints;
@Schema(description ="可用积分")
private Integer availablePoints;
@Schema(description ="冻结积分")
private Integer frozenPoints;
@Schema(description ="创建时间开始")
private Date createTimeStart;
@Schema(description ="创建时间结束")
private Date createTimeEnd;
@Schema(description ="更新时间开始")
private Date updateTimeStart;
@Schema(description ="更新时间结束")
private Date updateTimeEnd;
@Schema(description ="是否删除 0 未删除1已删除")
private Integer deleteFlag;
}

View File

@ -1,73 +0,0 @@
package com.kexue.skills.entity.dto;
import com.kexue.skills.entity.base.BaseQueryDto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.Date;
/**
* (PointsTransaction)查询条件封装类
* 积分交易记录查询条件
*
* @author 王志维
* @since 2025-02-21 23:01:48
*/
@Data
public class PointsTransactionDto extends BaseQueryDto {
@Schema(description ="主键ID")
private Long transactionId;
@Schema(description ="用户ID")
private Long userId;
@Schema(description ="用户名")
private String userName;
@Schema(description ="交易类型1.获得积分 2.使用积分")
private Integer transactionType;
@Schema(description ="积分数量")
private Integer points;
@Schema(description ="交易前积分")
private Integer beforePoints;
@Schema(description ="交易后积分")
private Integer afterPoints;
@Schema(description ="状态1.成功 2.失败")
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 ="备注")
private String remark;
@Schema(description ="创建时间开始")
private Date createTimeStart;
@Schema(description ="创建时间结束")
private Date createTimeEnd;
@Schema(description ="更新时间开始")
private Date updateTimeStart;
@Schema(description ="更新时间结束")
private Date updateTimeEnd;
@Schema(description ="是否删除 0 未删除1已删除")
private Integer deleteFlag;
}

View File

@ -0,0 +1,18 @@
package com.kexue.skills.entity.dto;
import com.kexue.skills.entity.base.BaseQueryDto;
import lombok.Data;
import java.util.List;
/**
* (CmsContent)查询DTO类
*
* @author 王志维
* @since 2025-02-21 23:01:48
*/
@Data
public class QueryContentDto extends BaseQueryDto {
private Long contentId;
private Integer languageType;
}

View File

@ -0,0 +1,19 @@
package com.kexue.skills.entity.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 会话DTO类
*
* @author 王志维
* @since 2026-03-12
*/
@Data
public class SessionDto {
@Schema(description = "会话ID")
private String sessionId;
@Schema(description = "是否为新会话")
private boolean isNew;
}

View File

@ -0,0 +1,53 @@
package com.kexue.skills.entity.dto;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
/**
* 技能包信息DTO
*
* @author AI技能生成助手
* @since 2026-04-10
*/
@Data
public class SkillPackageInfoDto implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 技能名称
*/
private String name;
/**
* 版本号
*/
private String version;
/**
* 技能描述
*/
private String description;
/**
* 作者
*/
private String author;
/**
* 创建日期
*/
private String created;
/**
* 标签列表
*/
private List<String> tags;
/**
* 目录结构
*/
private List<SkillStructureNodeDto> structure;
}

View File

@ -0,0 +1,53 @@
package com.kexue.skills.entity.dto;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
/**
* 技能包目录结构节点DTO
*
* @author AI技能生成助手
* @since 2026-04-10
*/
@Data
public class SkillStructureNodeDto implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 节点名称文件或目录名
*/
private String name;
/**
* 节点类型directory file
*/
private String type;
/**
* 父级路径
*/
private String path;
/**
* 格式dirmarkdownpython
*/
private String format;
/**
* 节点描述
*/
private String description;
/**
* 子节点列表目录类型使用
*/
private List<SkillStructureNodeDto> children;
/**
* 文件内容文件类型使用
*/
private String content;
}

View File

@ -2,6 +2,7 @@ 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;
@ -23,28 +24,24 @@ public class SysLogDto extends BaseQueryDto implements Serializable {
@Schema(description ="主键ID")
private Long logId;
@Schema(description ="用户ID")
private String userId;
@Schema(description ="模块")
private String module;
@Schema(description ="用户名称")
private String userName;
@Schema(description ="描述")
private String description;
@Schema(description ="日志类型")
private String logType;
@Schema(description ="IP地址")
private String ip;
@Schema(description ="日志类容")
private String logContent;
@Schema(description ="状态")
private Integer status;
@Schema(description ="服务端IP")
private String serverIp;
@Schema(description ="开始时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date startTime;
@Schema(description ="客户端IP")
private String clientIp;
@Schema(description ="yyyyMMddHHmmss")
private String logTime;
@Schema(description ="备注")
private String note;
@Schema(description ="结束时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date endTime;
}

View File

@ -0,0 +1,52 @@
package com.kexue.skills.entity.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* Token消费转换DTO
* 用于传输token消费转换的参数
*
* @author 王志维
* @since 2026-03-26 15:00:00
*/
@Data
public class TokenConsumptionDto {
@Schema(description ="用户的会话ID")
private String sessionId;
@Schema(description ="用户ID" ,hidden = true)
private Long userId;
@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 ="对应回答的问题或需求")
private String question;
@Schema(description ="交易单号")
private String transactionNo;
@Schema(description ="业务ID" ,hidden = true)
private Long businessId;
@Schema(description ="业务类型",hidden = true)
private String businessType;
@Schema(description ="备注")
private String remark;
@Schema(description ="输出模式standard=非思考模式, thinking=思考模式")
private String outputMode;
}

View File

@ -0,0 +1,41 @@
package com.kexue.skills.entity.dto;
import com.kexue.skills.entity.base.BaseQueryDto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
/**
* 提现记录DTO类
*
* @author 王志维
* @since 2026-03-25
*/
@Data
public class WithdrawalRecordDto extends BaseQueryDto {
@Schema(description ="用户ID")
private Long userId;
@Schema(description ="用户名")
private String userName;
@Schema(description ="提现金额")
private BigDecimal withdrawalAmount;
@Schema(description ="状态")
private Integer status;
@Schema(description ="提现单号")
private String withdrawalNo;
@Schema(description ="银行名称")
private String bankName;
@Schema(description ="银行账号")
private String bankAccount;
@Schema(description ="持卡人姓名")
private String bankCardholder;
}

View File

@ -0,0 +1,14 @@
package com.kexue.skills.entity.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
@Data
@Schema(description = "yaml内容")
public class YamlContentDto implements Serializable {
@Schema(description = "yaml内容")
private String yamlContent;
}

View File

@ -0,0 +1,23 @@
package com.kexue.skills.entity.request;
import io.swagger.annotations.ApiModel;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
/**
* 技能生成请求参数
*
* @author 维哥
* @since 2026-01-28
*/
@Data
@ApiModel(value = "功能介绍生成请求参数")
public class GenIntroduceRequest implements Serializable {
@Schema(description = "yaml或者skill.md内容", required = true)
private String content;
}

View File

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

View File

@ -1,7 +1,6 @@
package com.kexue.skills.entity.request;
import com.kexue.skills.entity.Account;
import com.kexue.skills.entity.PointsAccount;
import com.kexue.skills.entity.SysUser;
import lombok.Data;
@ -35,11 +34,6 @@ public class LoginUser {
*/
private Account account;
/**
* 积分信息
*/
private PointsAccount pointsAccount;
/**
* token
*/

View File

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

View File

@ -15,7 +15,7 @@ import java.io.Serializable;
@ApiModel(value = "重置密码请求参数")
public class ResetPasswordDto implements Serializable {
@Schema(description ="用户名")
@Schema(description ="管理员用户名")
private String userName;
@Schema(description ="旧密码")

View File

@ -1,22 +1,23 @@
package com.kexue.skills.entity.request;
import io.swagger.annotations.ApiModel;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
/**
* 技能生成请求参数
*
* @author 维哥
* @since 2026-01-28
*/
@Data
@ApiModel(value = "技能生成请求参数")
public class SkillGenRequest implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "用户提示词")
private String prompt;
@Schema(description = "技能名称")
private String name;
@Schema(description = "技能描述")
private String description;
@Schema(description = "技能标签")
private List<String> tags;
@Schema(description = "技能说明")
private String introduce;
@Schema(description = "需求说明")
private String requirement;
}

View File

@ -0,0 +1,30 @@
package com.kexue.skills.entity.request;
import io.swagger.annotations.ApiModel;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
/**
* 技能生成请求参数
*
* @author 维哥
* @since 2026-01-28
*/
@Data
@ApiModel(value = "技能生成请求参数")
public class SkillPreGenRequest implements Serializable {
@Schema(description = "用户提示词")
private String prompt;
@Schema(description = "文件地址")
private String fileUrl;
@Schema(description = "文件地址列表")
private List<String> fileUrls;
}

View File

@ -32,7 +32,7 @@ public class SkillRequest implements Serializable {
Message userMessage = new Message();
userMessage.setRole("user");
userMessage.setContent("主题我想要做一个将数据库设计表转换成sql schema语言并迁移到数据库服务器中的skill。请根据agent skills撰写规范帮我生成这个skill的名称、描述并从以下标签列表中选择一个或者多个标签:\"软件开发系统集成网络工程云计算大数据人工智能物联网区块链信息安全运维服务测试认证IT 咨询,外包服务,电商技术,移动开发,前端开发,后端开发,全栈开发,数据库管理\"。输出json格式仅输出以上所提到的名称、描述、标签节点名称分别为name、description、tags节点内容以中文形式返回。");
userMessage.setContent("主题我想要做一个将数据库设计表转换成sql schema语言并迁移到数据库服务器中的skill。请根据agent skills撰写规范帮我生成这个skill的名称、描述并从数据库中选择一个或者多个标签。输出json格式仅输出以上所提到的名称、描述、标签节点名称分别为name、description、tags节点内容以中文形式返回。");
this.messages.add(userMessage);
this.temperature = 0.3;
@ -43,7 +43,7 @@ public class SkillRequest implements Serializable {
}
}
public SkillRequest(boolean useDefaultSettings, String model, Double temperature, Integer maxTokens,String prompt) {
public SkillRequest(boolean useDefaultSettings, String model, Double temperature, Integer maxTokens, String prompt, String tagsList) {
if (useDefaultSettings) {
this.model = model;
this.messages = new ArrayList<>();
@ -55,7 +55,7 @@ public class SkillRequest implements Serializable {
Message userMessage = new Message();
userMessage.setRole("user");
userMessage.setContent("主题:"+ prompt +"。请根据agent skills撰写规范帮我生成这个skill的名称、描述并从以下标签列表中选择一个或者多个标签\"软件开发系统集成网络工程云计算大数据人工智能物联网区块链信息安全运维服务测试认证IT 咨询,外包服务,电商技术,移动开发,前端开发,后端开发,全栈开发,数据库管理\"。输出json格式仅输出以上所提到的名称、描述、标签节点名称分别为name、description、tags节点内容以中文形式返回");
userMessage.setContent("主题:"+ prompt +"。请根据agent skills撰写规范帮我生成这个skill的名称、描述并从以下标签列表中选择一个或者多个标签\"" + tagsList + "\"。输出json格式仅输出以上所提到的名称、描述、标签节点名称分别为name、description、tags节点内容以中文形式返回tags只需要返回序号数组");
this.messages.add(userMessage);
this.temperature = temperature;
@ -66,6 +66,72 @@ public class SkillRequest implements Serializable {
}
}
public SkillRequest(boolean useDefaultSettings, String model, String systemContent,String userContent,Double temperature, Integer maxTokens,String type) {
if (useDefaultSettings) {
this.model = model;
this.messages = new ArrayList<>();
Message systemMessage = new Message();
systemMessage.setRole("system");
systemMessage.setContent(systemContent);
this.messages.add(systemMessage);
Message userMessage = new Message();
userMessage.setRole("user");
userMessage.setContent(userContent);
this.messages.add(userMessage);
this.temperature = temperature;
this.max_tokens = maxTokens;
this.response_format = new ResponseFormat();
this.response_format.setType(type);
}
}
// 新的构造方法支持文件URL列表
public SkillRequest(String model, String systemContent, String prompt, List<String> fileUrls, double temperature, int maxTokens) {
this.model = model;
this.messages = new ArrayList<>();
Message systemMessage = new Message();
systemMessage.setRole("system");
systemMessage.setContent(systemContent);
this.messages.add(systemMessage);
// 构建包含文件URL的用户消息
List<MessageContent> messageContents = new ArrayList<>();
// 添加文件URL
if (fileUrls != null && !fileUrls.isEmpty()) {
for (String fileUrl : fileUrls) {
MessageContent fileContent = new MessageContent();
fileContent.setType("file_url");
FileUrl fileUrlObj = new FileUrl();
fileUrlObj.setUrl(fileUrl);
fileContent.setFile_url(fileUrlObj);
messageContents.add(fileContent);
}
}
// 添加文本内容
MessageContent textContent = new MessageContent();
textContent.setType("text");
textContent.setText(prompt);
messageContents.add(textContent);
Message userMessage = new Message();
userMessage.setRole("user");
userMessage.setContent(messageContents);
this.messages.add(userMessage);
this.temperature = temperature;
this.max_tokens = maxTokens;
this.response_format = new ResponseFormat();
this.response_format.setType("json_object");
}
public static SkillRequest createDefault() {
return new SkillRequest(true);
}
@ -77,7 +143,27 @@ public class SkillRequest implements Serializable {
private static final long serialVersionUID = 1L;
private String role;
private String content;
private Object content; // 可以是String或List<MessageContent>
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class MessageContent implements Serializable {
private static final long serialVersionUID = 1L;
private String type;
private String text;
private FileUrl file_url;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class FileUrl implements Serializable {
private static final long serialVersionUID = 1L;
private String url;
}
@Data

View File

@ -0,0 +1,13 @@
package com.kexue.skills.entity.request;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
@Data
public class SkillUploadDto implements Serializable {
@Schema(description = "技能包地址", required = true)
private String url;
}

View File

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

View File

@ -12,4 +12,5 @@ public class SkillResponse implements Serializable {
private String name;
private String description;
private List<String> tags;
private String summary;
}

View File

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

View File

@ -10,7 +10,7 @@ import org.springframework.web.bind.annotation.ResponseBody;
@Slf4j
@ControllerAdvice
public class BizExceptionAdvice {
@ExceptionHandler(value = RuntimeException.class)
@ExceptionHandler(value = BizException.class)
@ResponseBody
public CommonResult handleException(BizException e){
e.printStackTrace();

View File

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

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

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

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

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

@ -99,4 +99,5 @@ public interface AccountMapper {
* @return 影响行数
*/
int deleteById(Long accountId);
}

View File

@ -2,6 +2,7 @@ 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;
@ -70,4 +71,44 @@ public interface AccountTransactionMapper {
* @return 影响行数
*/
int deleteById(Long transactionId);
/**
* 通过用户ID查询交易记录
*
* @param userId 用户ID
* @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

@ -0,0 +1,100 @@
package com.kexue.skills.mapper;
import com.kexue.skills.entity.CmsCategoryTag;
import com.kexue.skills.entity.dto.CmsCategoryTagDto;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* (CmsCategoryTag)表数据库访问层
*
* @author 王志维
* @since 2025-02-21 23:01:48
*/
@Mapper
public interface CmsCategoryTagMapper {
/**
* 分页查询
*
* @param queryDto 筛选条件
* @return 查询结果
*/
List<CmsCategoryTag> getPageList(CmsCategoryTagDto queryDto);
/**
* 查询列表
*
* @param queryDto 筛选条件
* @return 查询结果
*/
List<CmsCategoryTag> getList(CmsCategoryTagDto queryDto);
/**
* 通过ID查询单条数据
*
* @param id 主键
* @return 实例对象
*/
CmsCategoryTag queryById(Long id);
/**
* 新增数据
*
* @param cmsCategoryTag 实例对象
* @return 影响行数
*/
int insert(CmsCategoryTag cmsCategoryTag);
/**
* 更新数据
*
* @param cmsCategoryTag 实例对象
* @return 影响行数
*/
int update(CmsCategoryTag cmsCategoryTag);
/**
* 通过主键逻辑删除
*
* @param id 主键
* @param updateBy 更新人
* @return 影响行数
*/
int logicDeleteById(@Param("id") Long id, @Param("updateBy") String updateBy);
/**
* 通过主键物理删除
*
* @param id 主键
* @return 影响行数
*/
int deleteById(Long id);
/**
* 根据分类ID删除关联
*
* @param categoryId 分类ID
* @param updateBy 更新人
* @return 影响行数
*/
int deleteByCategoryId(@Param("categoryId") Long categoryId, @Param("updateBy") String updateBy);
/**
* 根据标签ID删除关联
*
* @param tagId 标签ID
* @param updateBy 更新人
* @return 影响行数
*/
int deleteByTagId(@Param("tagId") Long tagId, @Param("updateBy") String updateBy);
/**
* 批量插入关联
*
* @param categoryTagList 关联列表
* @return 影响行数
*/
int batchInsert(List<CmsCategoryTag> categoryTagList);
}

View File

@ -23,6 +23,24 @@ public interface CmsContentMapper {
*/
List<CmsContent> getPageList(CmsContentDto queryDto);
/**
* 带分页的查询
*
* @param queryDto 筛选条件
* @param offset 偏移量
* @param limit 限制数量
* @return 查询结果
*/
List<CmsContent> getPageListWithPagination(@Param("queryDto") CmsContentDto queryDto, @Param("offset") int offset, @Param("limit") int limit);
/**
* 查询总记录数
*
* @param queryDto 筛选条件
* @return 总记录数
*/
int getPageListCount(CmsContentDto queryDto);
/**
* 查询列表
*
@ -39,6 +57,14 @@ public interface CmsContentMapper {
*/
CmsContent queryById(Long contentId);
/**
* 通过ID和语言类型查询单条数据
*
* @param queryDto 筛选条件
* @return 实例对象
*/
CmsContent queryByIdWithLanguage(CmsContentDto queryDto);
/**
* 新增数据
*
@ -47,6 +73,14 @@ public interface CmsContentMapper {
*/
int insert(CmsContent cmsContent);
/**
* 批量新增数据
*
* @param cmsContentList 实例对象列表
* @return 影响行数
*/
int batchInsert(@Param("cmsContentList") List<CmsContent> cmsContentList);
/**
* 更新数据
*
@ -117,4 +151,85 @@ public interface CmsContentMapper {
* @return 内容ID列表
*/
List<Long> queryRecentCreatedByUserId(@Param("userId") Long userId, @Param("limit") int limit);
/**
* 获取用户历史查看的内容列表
*
* @param userId 用户ID
* @param offset 偏移量
* @param limit 限制数量
* @return 查询结果
*/
List<CmsContent> getPageListByUserHistory(@Param("userId") Long userId, @Param("offset") int offset, @Param("limit") int limit);
/**
* 获取用户历史查看的内容总数
*
* @param userId 用户ID
* @return 总记录数
*/
int getPageListByUserHistoryCount(@Param("userId") Long userId);
/**
* 获取用户收藏的内容列表
*
* @param userId 用户ID
* @param offset 偏移量
* @param limit 限制数量
* @return 查询结果
*/
List<CmsContent> getPageListByUserFavorites(@Param("userId") Long userId, @Param("offset") int offset, @Param("limit") int limit);
/**
* 获取用户收藏的内容总数
*
* @param userId 用户ID
* @return 总记录数
*/
int getPageListByUserFavoritesCount(@Param("userId") Long userId);
/**
* 获取用户购买的内容列表
*
* @param userId 用户ID
* @param offset 偏移量
* @param limit 限制数量
* @return 查询结果
*/
List<CmsContent> getPageListByUserPurchases(@Param("userId") Long userId, @Param("offset") int offset, @Param("limit") int limit);
/**
* 获取用户购买的内容总数
*
* @param userId 用户ID
* @return 总记录数
*/
int getPageListByUserPurchasesCount(@Param("userId") Long userId);
/**
* 获取用户创建的内容列表
*
* @param userId 用户ID
* @param publishStatus 发布状态
* @param offset 偏移量
* @param limit 限制数量
* @return 查询结果
*/
List<CmsContent> getPageListByUserCreated(@Param("userId") Long userId, @Param("publishStatus") Integer publishStatus, @Param("offset") int offset, @Param("limit") int limit);
/**
* 获取用户创建的内容总数
*
* @param userId 用户ID
* @param publishStatus 发布状态
* @return 总记录数
*/
int getPageListByUserCreatedCount(@Param("userId") Long userId, @Param("publishStatus") Integer publishStatus);
/**
* 清空表数据
*
* @return 影响行数
*/
int truncateTable();
}

View File

@ -70,4 +70,12 @@ public interface CmsTagMapper {
* @return 影响行数
*/
int deleteById(Long tagId);
/**
* 根据分类ID查询标签列表
*
* @param categoryId 分类ID
* @return 标签列表
*/
List<CmsTag> getTagsByCategoryId(@Param("categoryId") Long categoryId);
}

Some files were not shown because too many files have changed in this diff Show More