From 770f50302e040446c25a8a899c6d489eb90e01eb Mon Sep 17 00:00:00 2001 From: wangzhiwei Date: Wed, 1 Apr 2026 11:52:33 +0800 Subject: [PATCH] =?UTF-8?q?feat(account):=20=E6=89=A9=E5=B1=95=E8=B4=A6?= =?UTF-8?q?=E6=88=B7=E4=BD=99=E9=A2=9D=E7=AE=A1=E7=90=86=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增可提现余额和不可提现余额字段,完善账户余额结构 - 添加充值接口支持微信和支付宝支付方式 - 实现token消费转换扣费功能,支持AI模型调用计费 - 增加管理员赠送金额接口,仅管理员可调用 - 完善交易记录查询功能,支持用户查看历史交易明细 - 集成模型价格服务,实现token费用自动计算 - 重构余额增加逻辑,区分可提现和不可提现金额 - 优化账户实体类初始化逻辑,确保余额字段正确设置 - 更新交易记录实体类,新增token相关和收支类型字段 - 修改支付配置,更新微信和支付宝回调地址为生产环境域名 --- PAYMENT_GUIDE.md | 603 ++++++++++++++++++ db/create_tables.sql | 97 +-- pom.xml | 8 - .../com/kexue/skills/aspect/AuthAspect.java | 5 +- .../com/kexue/skills/aspect/RoleAspect.java | 23 +- .../kexue/skills/common/util/HttpUtil.java | 28 +- .../kexue/skills/config/JacksonConfig.java | 26 + .../skills/config/MyStpInterfaceImpl.java | 48 ++ .../skills/controller/AccountController.java | 86 +++ .../controller/ModelPriceController.java | 113 ++++ .../skills/controller/PayController.java | 36 +- .../controller/PointsAccountController.java | 85 --- .../skills/controller/SysUserController.java | 21 +- .../WithdrawalRecordController.java | 129 ++++ .../java/com/kexue/skills/entity/Account.java | 8 +- .../skills/entity/AccountTransaction.java | 23 +- .../com/kexue/skills/entity/ModelPrice.java | 58 ++ .../com/kexue/skills/entity/PaymentOrder.java | 2 +- .../kexue/skills/entity/PointsAccount.java | 57 -- .../skills/entity/PointsTransaction.java | 78 --- .../kexue/skills/entity/WithdrawalRecord.java | 75 +++ .../skills/entity/dto/GiftBalanceDto.java | 66 ++ .../skills/entity/dto/ModelPriceDto.java | 53 ++ .../skills/entity/dto/PointsAccountDto.java | 52 -- .../entity/dto/PointsTransactionDto.java | 73 --- .../entity/dto/TokenConsumptionDto.java | 46 ++ .../entity/dto/WithdrawalRecordDto.java | 41 ++ .../skills/entity/request/LoginUser.java | 6 - .../entity/request/ResetPasswordDto.java | 2 +- .../mapper/AccountTransactionMapper.java | 8 + .../kexue/skills/mapper/ModelPriceMapper.java | 74 +++ .../skills/mapper/PointsAccountMapper.java | 91 --- .../mapper/PointsTransactionMapper.java | 73 --- .../skills/mapper/SysUserRoleMapper.java | 8 + .../skills/mapper/WithdrawalRecordMapper.java | 94 +++ .../kexue/skills/service/AccountService.java | 65 ++ .../skills/service/ModelPriceService.java | 73 +++ .../com/kexue/skills/service/PayService.java | 3 +- .../skills/service/PointsAccountService.java | 106 --- .../kexue/skills/service/SysUserService.java | 18 +- .../service/WithdrawalRecordService.java | 119 ++++ .../service/impl/AccountServiceImpl.java | 236 ++++++- .../service/impl/CmsContentServiceImpl.java | 28 +- .../impl/ContentPurchaseServiceImpl.java | 20 - .../service/impl/ModelPriceServiceImpl.java | 114 ++++ .../skills/service/impl/PayServiceImpl.java | 369 +++++++++-- .../service/impl/PaymentOrderServiceImpl.java | 125 +++- .../impl/PointsAccountServiceImpl.java | 229 ------- .../service/impl/SkillGenServiceImpl.java | 85 ++- .../service/impl/SysUserServiceImpl.java | 205 +++--- .../impl/WithdrawalRecordServiceImpl.java | 288 +++++++++ src/main/resources/apiclient_key.pem | 28 + src/main/resources/application-dev.yml | 4 +- src/main/resources/application-prod.yml | 10 +- src/main/resources/application.yml | 2 +- .../mapper/AccountTransactionMapper.xml | 49 +- .../resources/mapper/ModelPriceMapper.xml | 121 ++++ .../resources/mapper/PointsAccountMapper.xml | 156 ----- .../mapper/PointsTransactionMapper.xml | 186 ------ src/main/resources/mapper/SysUserMapper.xml | 2 +- .../resources/mapper/SysUserRoleMapper.xml | 9 + .../mapper/WithdrawalRecordMapper.xml | 139 ++++ .../resources/sql/add_account_columns.sql | 2 + .../java/com/kexue/skills/WechatPayTest.java | 336 ++++++++++ .../kexue/skills/WechatPayValidationTest.java | 252 ++++++++ 65 files changed, 4142 insertions(+), 1533 deletions(-) create mode 100644 PAYMENT_GUIDE.md create mode 100644 src/main/java/com/kexue/skills/config/JacksonConfig.java create mode 100644 src/main/java/com/kexue/skills/config/MyStpInterfaceImpl.java create mode 100644 src/main/java/com/kexue/skills/controller/ModelPriceController.java delete mode 100644 src/main/java/com/kexue/skills/controller/PointsAccountController.java create mode 100644 src/main/java/com/kexue/skills/controller/WithdrawalRecordController.java create mode 100644 src/main/java/com/kexue/skills/entity/ModelPrice.java delete mode 100644 src/main/java/com/kexue/skills/entity/PointsAccount.java delete mode 100644 src/main/java/com/kexue/skills/entity/PointsTransaction.java create mode 100644 src/main/java/com/kexue/skills/entity/WithdrawalRecord.java create mode 100644 src/main/java/com/kexue/skills/entity/dto/GiftBalanceDto.java create mode 100644 src/main/java/com/kexue/skills/entity/dto/ModelPriceDto.java delete mode 100644 src/main/java/com/kexue/skills/entity/dto/PointsAccountDto.java delete mode 100644 src/main/java/com/kexue/skills/entity/dto/PointsTransactionDto.java create mode 100644 src/main/java/com/kexue/skills/entity/dto/TokenConsumptionDto.java create mode 100644 src/main/java/com/kexue/skills/entity/dto/WithdrawalRecordDto.java create mode 100644 src/main/java/com/kexue/skills/mapper/ModelPriceMapper.java delete mode 100644 src/main/java/com/kexue/skills/mapper/PointsAccountMapper.java delete mode 100644 src/main/java/com/kexue/skills/mapper/PointsTransactionMapper.java create mode 100644 src/main/java/com/kexue/skills/mapper/WithdrawalRecordMapper.java create mode 100644 src/main/java/com/kexue/skills/service/ModelPriceService.java delete mode 100644 src/main/java/com/kexue/skills/service/PointsAccountService.java create mode 100644 src/main/java/com/kexue/skills/service/WithdrawalRecordService.java create mode 100644 src/main/java/com/kexue/skills/service/impl/ModelPriceServiceImpl.java delete mode 100644 src/main/java/com/kexue/skills/service/impl/PointsAccountServiceImpl.java create mode 100644 src/main/java/com/kexue/skills/service/impl/WithdrawalRecordServiceImpl.java create mode 100644 src/main/resources/apiclient_key.pem create mode 100644 src/main/resources/mapper/ModelPriceMapper.xml delete mode 100644 src/main/resources/mapper/PointsAccountMapper.xml delete mode 100644 src/main/resources/mapper/PointsTransactionMapper.xml create mode 100644 src/main/resources/mapper/WithdrawalRecordMapper.xml create mode 100644 src/main/resources/sql/add_account_columns.sql create mode 100644 src/test/java/com/kexue/skills/WechatPayTest.java create mode 100644 src/test/java/com/kexue/skills/WechatPayValidationTest.java diff --git a/PAYMENT_GUIDE.md b/PAYMENT_GUIDE.md new file mode 100644 index 0000000..c004fcb --- /dev/null +++ b/PAYMENT_GUIDE.md @@ -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": "
...
" +} +``` + +**使用说明:** +- 返回的是 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 + + + + + 微信支付 + + + + +
+

微信支付

+
+
+
+

请使用微信扫码支付

+

等待支付...

+
+ + + + +``` + +### 4.2 支付宝支付页面 + +```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 +**维护人员:** 系统开发团队 diff --git a/db/create_tables.sql b/db/create_tables.sql index c309b66..3416c1a 100644 --- a/db/create_tables.sql +++ b/db/create_tables.sql @@ -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; \ No newline at end of file diff --git a/pom.xml b/pom.xml index a5089f3..ba83ffa 100644 --- a/pom.xml +++ b/pom.xml @@ -182,10 +182,6 @@ org.slf4j slf4j-api - - ch.qos.logback - logback-classic - org.slf4j log4j-over-slf4j @@ -194,10 +190,6 @@ org.slf4j jul-to-slf4j - - org.springframework.boot - spring-boot-starter-logging - com.github.jsqlparser jsqlparser diff --git a/src/main/java/com/kexue/skills/aspect/AuthAspect.java b/src/main/java/com/kexue/skills/aspect/AuthAspect.java index 8754658..9c024ad 100644 --- a/src/main/java/com/kexue/skills/aspect/AuthAspect.java +++ b/src/main/java/com/kexue/skills/aspect/AuthAspect.java @@ -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(); } } \ No newline at end of file diff --git a/src/main/java/com/kexue/skills/aspect/RoleAspect.java b/src/main/java/com/kexue/skills/aspect/RoleAspect.java index ee3c19e..0efafc3 100644 --- a/src/main/java/com/kexue/skills/aspect/RoleAspect.java +++ b/src/main/java/com/kexue/skills/aspect/RoleAspect.java @@ -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 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(); } } \ No newline at end of file diff --git a/src/main/java/com/kexue/skills/common/util/HttpUtil.java b/src/main/java/com/kexue/skills/common/util/HttpUtil.java index 2ee86b5..452b3e0 100644 --- a/src/main/java/com/kexue/skills/common/util/HttpUtil.java +++ b/src/main/java/com/kexue/skills/common/util/HttpUtil.java @@ -57,29 +57,47 @@ public class HttpUtil { } /** - * 发送POST请求到指定URL + * 发送 POST 请求到指定 URL * - * @param url 请求URL + * @param url 请求 URL * @param requestBody 请求体字符串 * @return 响应结果 * @throws Exception 异常信息 */ public static String post(String url, String requestBody) throws Exception { - // 创建HttpClient实例 + // 创建 HttpClient 实例 HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(30)) .build(); + + return postWithClient(url, requestBody, client); + } - // 构建HttpRequest + /** + * 发送 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 response = client.send(request, HttpResponse.BodyHandlers.ofString()); + // 打印响应状态码和响应体 + System.out.println("HTTP 状态码: " + response.statusCode()); + System.out.println("响应体: " + response.body()); + return response.body(); } } \ No newline at end of file diff --git a/src/main/java/com/kexue/skills/config/JacksonConfig.java b/src/main/java/com/kexue/skills/config/JacksonConfig.java new file mode 100644 index 0000000..edea504 --- /dev/null +++ b/src/main/java/com/kexue/skills/config/JacksonConfig.java @@ -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(); + } +} diff --git a/src/main/java/com/kexue/skills/config/MyStpInterfaceImpl.java b/src/main/java/com/kexue/skills/config/MyStpInterfaceImpl.java new file mode 100644 index 0000000..52059f8 --- /dev/null +++ b/src/main/java/com/kexue/skills/config/MyStpInterfaceImpl.java @@ -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 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 getPermissionList(Object loginId, String loginType) { + // 暂时返回空列表,后续可根据需要从数据库查询权限列表 + return java.util.Collections.emptyList(); + } +} diff --git a/src/main/java/com/kexue/skills/controller/AccountController.java b/src/main/java/com/kexue/skills/controller/AccountController.java index b1fd59f..e597777 100644 --- a/src/main/java/com/kexue/skills/controller/AccountController.java +++ b/src/main/java/com/kexue/skills/controller/AccountController.java @@ -2,9 +2,13 @@ package com.kexue.skills.controller; import com.github.pagehelper.PageInfo; 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.service.AccountService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -82,4 +86,86 @@ public class AccountController { public CommonResult queryByUserId(@Parameter(description = "用户ID") @PathVariable("userId") Long userId) { 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 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") + @RequireAuth + public CommonResult reduceBalanceWithToken( + @RequestBody TokenConsumptionDto tokenConsumptionDto) { + return CommonResult.success(this.accountService.reduceBalanceWithToken( + tokenConsumptionDto.getUserId(), + tokenConsumptionDto.getInputToken(), + tokenConsumptionDto.getOutputToken(), + tokenConsumptionDto.getTotalTokens(), + tokenConsumptionDto.getModelName(), + tokenConsumptionDto.getQuestion(), + tokenConsumptionDto.getTransactionNo(), + tokenConsumptionDto.getBusinessId(), + tokenConsumptionDto.getBusinessType(), + tokenConsumptionDto.getRemark() + )); + } + + /** + * 给用户赠送金额(不可提现) + * 只有管理员可以调用 + * + * @param giftBalanceDto 赠送金额参数 + * @return 影响行数 + */ + @Operation(summary = "给用户赠送金额(不可提现)", description = "给用户赠送金额(不可提现),只有管理员可以调用") + @PostMapping("/addGiftBalance") + @RequireAuth + @RequireRole({"ADMIN"}) + public CommonResult addGiftBalance(@RequestBody GiftBalanceDto giftBalanceDto) { + return CommonResult.success(this.accountService.addGiftBalance( + giftBalanceDto.getUserId(), + giftBalanceDto.getAmount(), + giftBalanceDto.getTransactionNo(), + giftBalanceDto.getBusinessId(), + giftBalanceDto.getBusinessType(), + giftBalanceDto.getRemark() + )); + } + + /** + * 获取用户交易记录 + * + * @param userId 用户ID + * @return 交易记录列表 + */ + @Operation(summary = "获取用户交易记录", description = "获取用户交易记录") + @PostMapping("/getTransactions") + @RequireAuth + public CommonResult> getTransactions( + @RequestBody java.util.Map params) { + Long userId = Long.valueOf(params.get("userId").toString()); + return CommonResult.success(this.accountService.getTransactions(userId)); + } } \ No newline at end of file diff --git a/src/main/java/com/kexue/skills/controller/ModelPriceController.java b/src/main/java/com/kexue/skills/controller/ModelPriceController.java new file mode 100644 index 0000000..4440aca --- /dev/null +++ b/src/main/java/com/kexue/skills/controller/ModelPriceController.java @@ -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> getPageList(@RequestBody ModelPriceDto queryDto) { + return CommonResult.success(this.modelPriceService.getPageList(queryDto)); + } + + /** + * 查询列表 + * + * @param queryDto 筛选条件 + * @return 查询结果 + */ + @Operation(summary = "查询列表", description = "查询大模型Token价格表列表") + @PostMapping("/getList") + public CommonResult> getList(@RequestBody ModelPriceDto queryDto) { + return CommonResult.success(this.modelPriceService.getList(queryDto)); + } + + /** + * 通过主键查询单条数据 + * + * @param id 主键 + * @return 实例对象 + */ + @Operation(summary = "通过主键查询", description = "通过主键查询大模型Token价格表") + @GetMapping("/queryById/{id}") + public CommonResult queryById(@PathVariable("id") Long id) { + return CommonResult.success(this.modelPriceService.queryById(id)); + } + + /** + * 通过模型名称查询数据 + * + * @param modelName 模型名称 + * @return 实例对象 + */ + @Operation(summary = "通过模型名称查询", description = "通过模型名称查询大模型Token价格表") + @GetMapping("/queryByModelName/{modelName}") + public CommonResult queryByModelName(@PathVariable("modelName") String modelName) { + return CommonResult.success(this.modelPriceService.queryByModelName(modelName)); + } + + /** + * 新增数据 + * + * @param modelPrice 实例对象 + * @return 实例对象 + */ + @Operation(summary = "新增数据", description = "新增大模型Token价格表") + @PostMapping("/insert") + public CommonResult insert(@RequestBody ModelPrice modelPrice) { + return CommonResult.success(this.modelPriceService.insert(modelPrice)); + } + + /** + * 更新数据 + * + * @param modelPrice 实例对象 + * @return 实例对象 + */ + @Operation(summary = "更新数据", description = "更新大模型Token价格表") + @PostMapping("/update") + public CommonResult update(@RequestBody ModelPrice modelPrice) { + return CommonResult.success(this.modelPriceService.update(modelPrice)); + } + + /** + * 通过主键删除数据 + * + * @param id 主键 + * @return 影响行数 + */ + @Operation(summary = "通过主键删除", description = "通过主键删除大模型Token价格表") + @PostMapping("/deleteById/{id}") + public CommonResult deleteById(@PathVariable("id") Long id) { + return CommonResult.success(this.modelPriceService.deleteById(id)); + } + +} diff --git a/src/main/java/com/kexue/skills/controller/PayController.java b/src/main/java/com/kexue/skills/controller/PayController.java index 3a6656b..a0fab51 100644 --- a/src/main/java/com/kexue/skills/controller/PayController.java +++ b/src/main/java/com/kexue/skills/controller/PayController.java @@ -33,24 +33,54 @@ public class PayController { /** * 创建微信支付订单 * @param order 支付订单信息 + * @param request HTTP 请求(用于获取用户 IP) * @return 微信支付参数 */ @Operation(summary = "创建微信支付订单", description = "创建微信支付订单") @PostMapping("/wx/create") - public CommonResult> createWechatPay(@RequestBody PaymentOrder order) { + public CommonResult> createWechatPay(@RequestBody PaymentOrder order, HttpServletRequest request) { try { // 设置支付类型为微信支付(1) order.setPayType(1); // 创建支付订单 PaymentOrder createdOrder = paymentOrderService.createPaymentOrder(order); + // 获取用户真实 IP + String ipAddress = getUserIpAddress(request); // 生成微信支付参数 - Map payParams = payService.createWechatPay(createdOrder); + Map 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("创建微信支付订单失败: " + e.getMessage()); + 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; + } /** * 处理微信支付回调 diff --git a/src/main/java/com/kexue/skills/controller/PointsAccountController.java b/src/main/java/com/kexue/skills/controller/PointsAccountController.java deleted file mode 100644 index 10bf26e..0000000 --- a/src/main/java/com/kexue/skills/controller/PointsAccountController.java +++ /dev/null @@ -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> getPageList(@RequestBody PointsAccountDto queryDto) { - return CommonResult.success(this.pointsAccountService.getPageList(queryDto)); - } - - /** - * 查询列表 - * - * @param queryDto 查询参数 - * @return 列表结果 - */ - @Operation(summary = "查询积分账户列表", description = "查询积分账户列表") - @PostMapping("/list") - @RequireAuth - public CommonResult> 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 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 queryByUserId(@Parameter(description = "用户ID") @PathVariable("userId") Long userId) { - return CommonResult.success(this.pointsAccountService.queryByUserId(userId)); - } -} \ No newline at end of file diff --git a/src/main/java/com/kexue/skills/controller/SysUserController.java b/src/main/java/com/kexue/skills/controller/SysUserController.java index e3b32a5..f5eedc6 100644 --- a/src/main/java/com/kexue/skills/controller/SysUserController.java +++ b/src/main/java/com/kexue/skills/controller/SysUserController.java @@ -108,25 +108,8 @@ public class SysUserController { @PostMapping("/resetPassword") @Operation(summary = "管理员帮助用户重置密码", description = "管理员帮助用户重置密码") @RequireAuth - public CommonResult 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 resetPassword(@RequestBody ResetPwdDto resetPasswordDto) { + boolean result = sysUserService.resetPassword(resetPasswordDto); return CommonResult.success(result); } diff --git a/src/main/java/com/kexue/skills/controller/WithdrawalRecordController.java b/src/main/java/com/kexue/skills/controller/WithdrawalRecordController.java new file mode 100644 index 0000000..f447218 --- /dev/null +++ b/src/main/java/com/kexue/skills/controller/WithdrawalRecordController.java @@ -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> getPageList(@RequestBody WithdrawalRecordDto queryDto) { + return CommonResult.success(this.withdrawalRecordService.getPageList(queryDto)); + } + + /** + * 查询列表 + * + * @param queryDto 查询参数 + * @return 列表结果 + */ + @Operation(summary = "查询提现记录列表", description = "查询提现记录列表") + @PostMapping("/list") + @RequireAuth + public CommonResult> 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 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> 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 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 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); + } +} \ No newline at end of file diff --git a/src/main/java/com/kexue/skills/entity/Account.java b/src/main/java/com/kexue/skills/entity/Account.java index f42726d..228bbe3 100644 --- a/src/main/java/com/kexue/skills/entity/Account.java +++ b/src/main/java/com/kexue/skills/entity/Account.java @@ -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; diff --git a/src/main/java/com/kexue/skills/entity/AccountTransaction.java b/src/main/java/com/kexue/skills/entity/AccountTransaction.java index 64f3859..1aee812 100644 --- a/src/main/java/com/kexue/skills/entity/AccountTransaction.java +++ b/src/main/java/com/kexue/skills/entity/AccountTransaction.java @@ -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 ="交易金额") @@ -59,6 +59,27 @@ public class AccountTransaction extends BaseEntity implements Serializable { @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; diff --git a/src/main/java/com/kexue/skills/entity/ModelPrice.java b/src/main/java/com/kexue/skills/entity/ModelPrice.java new file mode 100644 index 0000000..6e90ef6 --- /dev/null +++ b/src/main/java/com/kexue/skills/entity/ModelPrice.java @@ -0,0 +1,58 @@ +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; + + @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; + +} diff --git a/src/main/java/com/kexue/skills/entity/PaymentOrder.java b/src/main/java/com/kexue/skills/entity/PaymentOrder.java index f8aeb91..901c6d4 100644 --- a/src/main/java/com/kexue/skills/entity/PaymentOrder.java +++ b/src/main/java/com/kexue/skills/entity/PaymentOrder.java @@ -53,7 +53,7 @@ public class PaymentOrder extends BaseEntity implements Serializable { @Schema(description ="关联业务ID") private Long businessId; - @Schema(description ="业务类型") + @Schema(description ="业务类型:recharge,purchase_content") private String businessType; @Schema(description ="支付回调地址") diff --git a/src/main/java/com/kexue/skills/entity/PointsAccount.java b/src/main/java/com/kexue/skills/entity/PointsAccount.java deleted file mode 100644 index c10f32f..0000000 --- a/src/main/java/com/kexue/skills/entity/PointsAccount.java +++ /dev/null @@ -1,57 +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; - -/** - * (PointsAccount)实体类 - * 积分账户表,记录用户的积分信息 - * - * @author 王志维 - * @since 2025-02-21 23:01:48 - */ -@Data -public class PointsAccount extends BaseEntity implements Serializable { - private static final long serialVersionUID = 1L; - - @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; - - @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; - -} \ No newline at end of file diff --git a/src/main/java/com/kexue/skills/entity/PointsTransaction.java b/src/main/java/com/kexue/skills/entity/PointsTransaction.java deleted file mode 100644 index e888708..0000000 --- a/src/main/java/com/kexue/skills/entity/PointsTransaction.java +++ /dev/null @@ -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; - -} \ No newline at end of file diff --git a/src/main/java/com/kexue/skills/entity/WithdrawalRecord.java b/src/main/java/com/kexue/skills/entity/WithdrawalRecord.java new file mode 100644 index 0000000..9a557e7 --- /dev/null +++ b/src/main/java/com/kexue/skills/entity/WithdrawalRecord.java @@ -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; + +} \ No newline at end of file diff --git a/src/main/java/com/kexue/skills/entity/dto/GiftBalanceDto.java b/src/main/java/com/kexue/skills/entity/dto/GiftBalanceDto.java new file mode 100644 index 0000000..b7903f1 --- /dev/null +++ b/src/main/java/com/kexue/skills/entity/dto/GiftBalanceDto.java @@ -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; + } +} \ No newline at end of file diff --git a/src/main/java/com/kexue/skills/entity/dto/ModelPriceDto.java b/src/main/java/com/kexue/skills/entity/dto/ModelPriceDto.java new file mode 100644 index 0000000..40e40ff --- /dev/null +++ b/src/main/java/com/kexue/skills/entity/dto/ModelPriceDto.java @@ -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; + +} diff --git a/src/main/java/com/kexue/skills/entity/dto/PointsAccountDto.java b/src/main/java/com/kexue/skills/entity/dto/PointsAccountDto.java deleted file mode 100644 index 4cbea68..0000000 --- a/src/main/java/com/kexue/skills/entity/dto/PointsAccountDto.java +++ /dev/null @@ -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; - -} \ No newline at end of file diff --git a/src/main/java/com/kexue/skills/entity/dto/PointsTransactionDto.java b/src/main/java/com/kexue/skills/entity/dto/PointsTransactionDto.java deleted file mode 100644 index 81eb60e..0000000 --- a/src/main/java/com/kexue/skills/entity/dto/PointsTransactionDto.java +++ /dev/null @@ -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; - -} \ No newline at end of file diff --git a/src/main/java/com/kexue/skills/entity/dto/TokenConsumptionDto.java b/src/main/java/com/kexue/skills/entity/dto/TokenConsumptionDto.java new file mode 100644 index 0000000..03f5ce9 --- /dev/null +++ b/src/main/java/com/kexue/skills/entity/dto/TokenConsumptionDto.java @@ -0,0 +1,46 @@ +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 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") + private Long businessId; + + @Schema(description ="业务类型") + private String businessType; + + @Schema(description ="备注") + private String remark; + +} diff --git a/src/main/java/com/kexue/skills/entity/dto/WithdrawalRecordDto.java b/src/main/java/com/kexue/skills/entity/dto/WithdrawalRecordDto.java new file mode 100644 index 0000000..c9af45b --- /dev/null +++ b/src/main/java/com/kexue/skills/entity/dto/WithdrawalRecordDto.java @@ -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; + +} \ No newline at end of file diff --git a/src/main/java/com/kexue/skills/entity/request/LoginUser.java b/src/main/java/com/kexue/skills/entity/request/LoginUser.java index e0d6e44..1a3915c 100644 --- a/src/main/java/com/kexue/skills/entity/request/LoginUser.java +++ b/src/main/java/com/kexue/skills/entity/request/LoginUser.java @@ -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 */ diff --git a/src/main/java/com/kexue/skills/entity/request/ResetPasswordDto.java b/src/main/java/com/kexue/skills/entity/request/ResetPasswordDto.java index 193b453..ff9d9dc 100644 --- a/src/main/java/com/kexue/skills/entity/request/ResetPasswordDto.java +++ b/src/main/java/com/kexue/skills/entity/request/ResetPasswordDto.java @@ -15,7 +15,7 @@ import java.io.Serializable; @ApiModel(value = "重置密码请求参数") public class ResetPasswordDto implements Serializable { - @Schema(description ="用户名") + @Schema(description ="管理员用户名") private String userName; @Schema(description ="旧密码") diff --git a/src/main/java/com/kexue/skills/mapper/AccountTransactionMapper.java b/src/main/java/com/kexue/skills/mapper/AccountTransactionMapper.java index 615af13..0e5e143 100644 --- a/src/main/java/com/kexue/skills/mapper/AccountTransactionMapper.java +++ b/src/main/java/com/kexue/skills/mapper/AccountTransactionMapper.java @@ -70,4 +70,12 @@ public interface AccountTransactionMapper { * @return 影响行数 */ int deleteById(Long transactionId); + + /** + * 通过用户ID查询交易记录 + * + * @param userId 用户ID + * @return 交易记录列表 + */ + List queryByUserId(Long userId); } \ No newline at end of file diff --git a/src/main/java/com/kexue/skills/mapper/ModelPriceMapper.java b/src/main/java/com/kexue/skills/mapper/ModelPriceMapper.java new file mode 100644 index 0000000..2e74058 --- /dev/null +++ b/src/main/java/com/kexue/skills/mapper/ModelPriceMapper.java @@ -0,0 +1,74 @@ +package com.kexue.skills.mapper; + +import com.kexue.skills.entity.ModelPrice; +import com.kexue.skills.entity.dto.ModelPriceDto; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * (ModelPrice)表数据库访问层 + * 大模型Token价格表 + * + * @author 王志维 + * @since 2026-03-26 10:15:00 + */ +@Mapper +public interface ModelPriceMapper { + /** + * 分页查询 + * + * @param queryDto 筛选条件 + * @return 查询结果 + */ + List getPageList(ModelPriceDto queryDto); + + /** + * 查询列表 + * + * @param queryDto 筛选条件 + * @return 查询结果 + */ + List getList(ModelPriceDto queryDto); + + /** + * 通过主键查询单条数据 + * + * @param id 主键 + * @return 实例对象 + */ + ModelPrice queryById(Long id); + + /** + * 通过模型名称查询数据 + * + * @param modelName 模型名称 + * @return 实例对象 + */ + ModelPrice queryByModelName(String modelName); + + /** + * 新增数据 + * + * @param modelPrice 实例对象 + * @return 影响行数 + */ + int insert(ModelPrice modelPrice); + + /** + * 更新数据 + * + * @param modelPrice 实例对象 + * @return 影响行数 + */ + int update(ModelPrice modelPrice); + + /** + * 通过主键删除数据 + * + * @param id 主键 + * @return 影响行数 + */ + int deleteById(Long id); + +} diff --git a/src/main/java/com/kexue/skills/mapper/PointsAccountMapper.java b/src/main/java/com/kexue/skills/mapper/PointsAccountMapper.java deleted file mode 100644 index 5cca8aa..0000000 --- a/src/main/java/com/kexue/skills/mapper/PointsAccountMapper.java +++ /dev/null @@ -1,91 +0,0 @@ -package com.kexue.skills.mapper; - -import com.kexue.skills.entity.PointsAccount; -import com.kexue.skills.entity.dto.PointsAccountDto; -import org.apache.ibatis.annotations.Mapper; -import org.apache.ibatis.annotations.Param; - -import java.util.List; - -/** - * (PointsAccount)表数据库访问层 - * - * @author 王志维 - * @since 2025-02-21 23:01:48 - */ -@Mapper -public interface PointsAccountMapper { - /** - * 分页查询 - * - * @param queryDto 筛选条件 - * @return 查询结果 - */ - List getPageList(PointsAccountDto queryDto); - - /** - * 查询列表 - * - * @param queryDto 筛选条件 - * @return 查询结果 - */ - List getList(PointsAccountDto queryDto); - - /** - * 通过主键查询单条数据 - * - * @param accountId 主键 - * @return 实例对象 - */ - PointsAccount queryById(Long accountId); - - /** - * 通过用户ID查询积分账户 - * - * @param userId 用户ID - * @return 实例对象 - */ - PointsAccount queryByUserId(Long userId); - - /** - * 新增数据 - * - * @param pointsAccount 实例对象 - * @return 影响行数 - */ - int insert(PointsAccount pointsAccount); - - /** - * 更新数据 - * - * @param pointsAccount 实例对象 - * @return 影响行数 - */ - int update(PointsAccount pointsAccount); - - /** - * 更新积分 - * - * @param userId 用户ID - * @param points 变动积分 - * @param type 变动类型:1.增加 2.减少 - * @return 影响行数 - */ - int updatePoints(@Param("userId") Long userId, @Param("points") Integer points, @Param("type") Integer type); - - /** - * 通过主键逻辑删除 - * - * @param accountId 主键 - * @return 影响行数 - */ - int logicDeleteById(@Param("accountId") Long accountId, @Param("updateBy") String updateBy); - - /** - * 通过主键物理删除 - * - * @param accountId 主键 - * @return 影响行数 - */ - int deleteById(Long accountId); -} \ No newline at end of file diff --git a/src/main/java/com/kexue/skills/mapper/PointsTransactionMapper.java b/src/main/java/com/kexue/skills/mapper/PointsTransactionMapper.java deleted file mode 100644 index c4f3383..0000000 --- a/src/main/java/com/kexue/skills/mapper/PointsTransactionMapper.java +++ /dev/null @@ -1,73 +0,0 @@ -package com.kexue.skills.mapper; - -import com.kexue.skills.entity.PointsTransaction; -import com.kexue.skills.entity.dto.PointsTransactionDto; -import org.apache.ibatis.annotations.Mapper; -import org.apache.ibatis.annotations.Param; - -import java.util.List; - -/** - * (PointsTransaction)表数据库访问层 - * - * @author 王志维 - * @since 2025-02-21 23:01:48 - */ -@Mapper -public interface PointsTransactionMapper { - /** - * 分页查询 - * - * @param queryDto 筛选条件 - * @return 查询结果 - */ - List getPageList(PointsTransactionDto queryDto); - - /** - * 查询列表 - * - * @param queryDto 筛选条件 - * @return 查询结果 - */ - List getList(PointsTransactionDto queryDto); - - /** - * 通过主键查询单条数据 - * - * @param transactionId 主键 - * @return 实例对象 - */ - PointsTransaction queryById(Long transactionId); - - /** - * 新增数据 - * - * @param transaction 实例对象 - * @return 影响行数 - */ - int insert(PointsTransaction transaction); - - /** - * 更新数据 - * - * @param transaction 实例对象 - * @return 影响行数 - */ - int update(PointsTransaction transaction); - - /** - * 通过主键逻辑删除 - * - * @param transactionId 主键 - * @return 影响行数 - */ - int logicDeleteById(@Param("transactionId") Long transactionId, @Param("updateBy") String updateBy); - - /** - * 通过主键物理删除 - * - * @param transactionId 主键 - * @return 影响行数 - */ - int deleteById(Long transactionId); -} \ No newline at end of file diff --git a/src/main/java/com/kexue/skills/mapper/SysUserRoleMapper.java b/src/main/java/com/kexue/skills/mapper/SysUserRoleMapper.java index 07673cf..3d5308d 100644 --- a/src/main/java/com/kexue/skills/mapper/SysUserRoleMapper.java +++ b/src/main/java/com/kexue/skills/mapper/SysUserRoleMapper.java @@ -84,4 +84,12 @@ public interface SysUserRoleMapper { */ int deleteById(Long roleId); + /** + * 通过用户 ID 查询角色编码列表(关联查询 sys_role 和 sys_user_role) + * + * @param userId 用户 ID + * @return 角色编码列表 + */ + List queryRoleCodesByUserId(@Param("userId") Long userId); + } diff --git a/src/main/java/com/kexue/skills/mapper/WithdrawalRecordMapper.java b/src/main/java/com/kexue/skills/mapper/WithdrawalRecordMapper.java new file mode 100644 index 0000000..8459e2b --- /dev/null +++ b/src/main/java/com/kexue/skills/mapper/WithdrawalRecordMapper.java @@ -0,0 +1,94 @@ +package com.kexue.skills.mapper; + +import com.kexue.skills.entity.WithdrawalRecord; +import com.kexue.skills.entity.dto.WithdrawalRecordDto; +import java.util.List; +import java.util.Map; + +/** + * 提现记录Mapper接口 + * + * @author 王志维 + * @since 2026-03-25 + */ +public interface WithdrawalRecordMapper { + /** + * 通过ID查询单条数据 + * + * @param recordId 主键 + * @return 实例对象 + */ + WithdrawalRecord queryById(Long recordId); + + /** + * 通过用户ID查询提现记录 + * + * @param userId 用户ID + * @return 实例对象 + */ + List queryByUserId(Long userId); + + /** + * 通过提现单号查询提现记录 + * + * @param withdrawalNo 提现单号 + * @return 实例对象 + */ + WithdrawalRecord queryByWithdrawalNo(String withdrawalNo); + + /** + * 分页查询 + * + * @param queryDto 筛选条件 + * @return 查询结果 + */ + List getPageList(WithdrawalRecordDto queryDto); + + /** + * 查询列表 + * + * @param queryDto 筛选条件 + * @return 查询结果 + */ + List getList(WithdrawalRecordDto queryDto); + + /** + * 新增数据 + * + * @param withdrawalRecord 实例对象 + * @return 影响行数 + */ + int insert(WithdrawalRecord withdrawalRecord); + + /** + * 更新数据 + * + * @param withdrawalRecord 实例对象 + * @return 影响行数 + */ + int update(WithdrawalRecord withdrawalRecord); + + /** + * 更新提现状态 + * + * @param params 参数 + * @return 影响行数 + */ + int updateStatus(Map params); + + /** + * 通过主键逻辑删除 + * + * @param params 参数 + * @return 影响行数 + */ + int logicDeleteById(Map params); + + /** + * 通过主键物理删除 + * + * @param recordId 主键 + * @return 影响行数 + */ + int deleteById(Long recordId); +} \ No newline at end of file diff --git a/src/main/java/com/kexue/skills/service/AccountService.java b/src/main/java/com/kexue/skills/service/AccountService.java index 171b4de..0f619c2 100644 --- a/src/main/java/com/kexue/skills/service/AccountService.java +++ b/src/main/java/com/kexue/skills/service/AccountService.java @@ -67,6 +67,20 @@ public interface AccountService extends BaseService { * * @param userId 用户ID * @param amount 增加金额 + * @param isWithdrawable 是否可提现 + * @param transactionNo 交易单号 + * @param businessId 业务ID + * @param businessType 业务类型 + * @param remark 备注 + * @return 影响行数 + */ + int addBalance(Long userId, BigDecimal amount, boolean isWithdrawable, String transactionNo, Long businessId, String businessType, String remark); + + /** + * 增加账户余额(默认不可提现) + * + * @param userId 用户ID + * @param amount 增加金额 * @param transactionNo 交易单号 * @param businessId 业务ID * @param businessType 业务类型 @@ -88,6 +102,49 @@ public interface AccountService extends BaseService { */ int reduceBalance(Long userId, BigDecimal amount, String transactionNo, Long businessId, String businessType, String remark); + /** + * 减少账户余额(token消费转换) + * + * @param userId 用户ID + * @param inputToken 输入token + * @param outputToken 输出token + * @param totalTokens 合计tokens + * @param modelName 处理的模型名称 + * @param question 对应回答的问题或需求 + * @param transactionNo 交易单号 + * @param businessId 业务ID + * @param businessType 业务类型 + * @param remark 备注 + * @return 影响行数 + */ + int reduceBalanceWithToken(Long userId, Integer inputToken, Integer outputToken, Integer totalTokens, String modelName, String question, String transactionNo, Long businessId, String businessType, String remark); + + /** + * 增加账户余额(签到奖励token转换) + * + * @param userId 用户ID + * @param amount 增加金额 + * @param transactionNo 交易单号 + * @param businessId 业务ID + * @param businessType 业务类型 + * @param remark 备注 + * @return 影响行数 + */ + int addSignInBalance(Long userId, BigDecimal amount, String transactionNo, Long businessId, String businessType, String remark); + + /** + * 给用户赠送金额(不可提现) + * + * @param userId 用户ID + * @param amount 赠送金额 + * @param transactionNo 交易单号 + * @param businessId 业务ID + * @param businessType 业务类型 + * @param remark 备注 + * @return 影响行数 + */ + int addGiftBalance(Long userId, BigDecimal amount, String transactionNo, Long businessId, String businessType, String remark); + /** * 通过主键逻辑删除 * @@ -104,4 +161,12 @@ public interface AccountService extends BaseService { * @return 影响行数 */ int deleteById(Long accountId); + + /** + * 获取用户交易记录 + * + * @param userId 用户ID + * @return 交易记录列表 + */ + List getTransactions(Long userId); } \ No newline at end of file diff --git a/src/main/java/com/kexue/skills/service/ModelPriceService.java b/src/main/java/com/kexue/skills/service/ModelPriceService.java new file mode 100644 index 0000000..9808881 --- /dev/null +++ b/src/main/java/com/kexue/skills/service/ModelPriceService.java @@ -0,0 +1,73 @@ +package com.kexue.skills.service; + +import com.github.pagehelper.PageInfo; +import com.kexue.skills.entity.ModelPrice; +import com.kexue.skills.entity.dto.ModelPriceDto; + +import java.util.List; + +/** + * (ModelPrice)表服务接口 + * 大模型Token价格表 + * + * @author 王志维 + * @since 2026-03-26 10:15:00 + */ +public interface ModelPriceService extends BaseService { + /** + * 分页查询 + * + * @param queryDto 筛选条件 + * @return 查询结果 + */ + PageInfo getPageList(ModelPriceDto queryDto); + + /** + * 查询列表 + * + * @param queryDto 筛选条件 + * @return 查询结果 + */ + List getList(ModelPriceDto queryDto); + + /** + * 通过主键查询单条数据 + * + * @param id 主键 + * @return 实例对象 + */ + ModelPrice queryById(Long id); + + /** + * 通过模型名称查询数据 + * + * @param modelName 模型名称 + * @return 实例对象 + */ + ModelPrice queryByModelName(String modelName); + + /** + * 新增数据 + * + * @param modelPrice 实例对象 + * @return 实例对象 + */ + ModelPrice insert(ModelPrice modelPrice); + + /** + * 更新数据 + * + * @param modelPrice 实例对象 + * @return 实例对象 + */ + ModelPrice update(ModelPrice modelPrice); + + /** + * 通过主键删除数据 + * + * @param id 主键 + * @return 影响行数 + */ + int deleteById(Long id); + +} diff --git a/src/main/java/com/kexue/skills/service/PayService.java b/src/main/java/com/kexue/skills/service/PayService.java index f62750e..09ac6e1 100644 --- a/src/main/java/com/kexue/skills/service/PayService.java +++ b/src/main/java/com/kexue/skills/service/PayService.java @@ -11,9 +11,10 @@ public interface PayService { /** * 创建微信支付订单 * @param order 支付订单信息 + * @param ipAddress 用户真实 IP 地址(从 Controller 传入) * @return 微信支付参数 */ - Map createWechatPay(PaymentOrder order); + Map createWechatPay(PaymentOrder order, String ipAddress); /** * 处理微信支付回调 diff --git a/src/main/java/com/kexue/skills/service/PointsAccountService.java b/src/main/java/com/kexue/skills/service/PointsAccountService.java deleted file mode 100644 index fc8824d..0000000 --- a/src/main/java/com/kexue/skills/service/PointsAccountService.java +++ /dev/null @@ -1,106 +0,0 @@ -package com.kexue.skills.service; - -import com.github.pagehelper.PageInfo; -import com.kexue.skills.entity.PointsAccount; -import com.kexue.skills.entity.dto.PointsAccountDto; - -import java.util.List; - -/** - * (PointsAccount)表服务接口 - * - * @author 王志维 - * @since 2025-02-21 23:01:48 - */ -public interface PointsAccountService extends BaseService { - /** - * 分页查询 - * - * @param queryDto 筛选条件 - * @return 查询结果 - */ - PageInfo getPageList(PointsAccountDto queryDto); - - /** - * 查询列表 - * - * @param queryDto 筛选条件 - * @return 查询结果 - */ - List getList(PointsAccountDto queryDto); - - /** - * 通过主键查询单条数据 - * - * @param accountId 主键 - * @return 实例对象 - */ - PointsAccount queryById(Long accountId); - - /** - * 通过用户ID查询积分账户 - * - * @param userId 用户ID - * @return 实例对象 - */ - PointsAccount queryByUserId(Long userId); - - /** - * 新增数据 - * - * @param pointsAccount 实例对象 - * @return 实例对象 - */ - PointsAccount insert(PointsAccount pointsAccount); - - /** - * 更新数据 - * - * @param pointsAccount 实例对象 - * @return 实例对象 - */ - PointsAccount update(PointsAccount pointsAccount); - - /** - * 增加积分 - * - * @param userId 用户ID - * @param points 增加积分 - * @param transactionNo 交易单号 - * @param businessId 业务ID - * @param businessType 业务类型 - * @param remark 备注 - * @return 影响行数 - */ - int addPoints(Long userId, Integer points, String transactionNo, Long businessId, String businessType, String remark); - - /** - * 减少积分 - * - * @param userId 用户ID - * @param points 减少积分 - * @param transactionNo 交易单号 - * @param businessId 业务ID - * @param businessType 业务类型 - * @param remark 备注 - * @return 影响行数 - */ - int reducePoints(Long userId, Integer points, String transactionNo, Long businessId, String businessType, String remark); - - /** - * 通过主键逻辑删除 - * - * @param accountId 主键 - * @param updateBy 更新人 - * @return 影响行数 - */ - int logicDeleteById(Long accountId, String updateBy); - - /** - * 通过主键物理删除 - * - * @param accountId 主键 - * @return 影响行数 - */ - int deleteById(Long accountId); -} \ No newline at end of file diff --git a/src/main/java/com/kexue/skills/service/SysUserService.java b/src/main/java/com/kexue/skills/service/SysUserService.java index 734d418..e5fe348 100644 --- a/src/main/java/com/kexue/skills/service/SysUserService.java +++ b/src/main/java/com/kexue/skills/service/SysUserService.java @@ -3,11 +3,7 @@ import com.github.pagehelper.PageInfo; import com.kexue.skills.entity.SysUser; import com.kexue.skills.entity.dto.SysUserDto; -import com.kexue.skills.entity.request.LoginDto; -import com.kexue.skills.entity.request.LoginUserDto; -import com.kexue.skills.entity.request.PhoneLoginDto; -import com.kexue.skills.entity.request.ResetPasswordDto; -import com.kexue.skills.entity.request.SysUserUpdateDto; +import com.kexue.skills.entity.request.*; import java.util.List; @@ -78,9 +74,7 @@ public interface SysUserService extends BaseService { LoginUserDto login(LoginDto loginDto); - boolean resetPassword(ResetPasswordDto resetPasswordDto); - - boolean resetPasswordByAdmin(ResetPasswordDto resetPasswordDto); + boolean resetPassword(ResetPwdDto resetPasswordDto); SysUser getByUsername(String username); @@ -135,4 +129,12 @@ public interface SysUserService extends BaseService { * @return 会话信息 */ com.kexue.skills.entity.dto.SessionDto createSession(Long userId); + + /** + * 查询用户角色列表 + * + * @param userId 用户ID + * @return 角色编码列表 + */ + List queryUserRoles(Long userId); } diff --git a/src/main/java/com/kexue/skills/service/WithdrawalRecordService.java b/src/main/java/com/kexue/skills/service/WithdrawalRecordService.java new file mode 100644 index 0000000..0e32aea --- /dev/null +++ b/src/main/java/com/kexue/skills/service/WithdrawalRecordService.java @@ -0,0 +1,119 @@ +package com.kexue.skills.service; + +import com.github.pagehelper.PageInfo; +import com.kexue.skills.entity.WithdrawalRecord; +import com.kexue.skills.entity.dto.WithdrawalRecordDto; +import java.util.List; + +/** + * 提现记录Service接口 + * + * @author 王志维 + * @since 2026-03-25 + */ +public interface WithdrawalRecordService { + /** + * 分页查询 + * + * @param queryDto 筛选条件 + * @return 查询结果 + */ + PageInfo getPageList(WithdrawalRecordDto queryDto); + + /** + * 查询列表 + * + * @param queryDto 筛选条件 + * @return 查询结果 + */ + List getList(WithdrawalRecordDto queryDto); + + /** + * 通过主键查询单条数据 + * + * @param recordId 主键 + * @return 实例对象 + */ + WithdrawalRecord queryById(Long recordId); + + /** + * 通过用户ID查询提现记录 + * + * @param userId 用户ID + * @return 实例对象 + */ + List queryByUserId(Long userId); + + /** + * 通过提现单号查询提现记录 + * + * @param withdrawalNo 提现单号 + * @return 实例对象 + */ + WithdrawalRecord queryByWithdrawalNo(String withdrawalNo); + + /** + * 新增数据 + * + * @param withdrawalRecord 实例对象 + * @return 实例对象 + */ + WithdrawalRecord insert(WithdrawalRecord withdrawalRecord); + + /** + * 更新数据 + * + * @param withdrawalRecord 实例对象 + * @return 实例对象 + */ + WithdrawalRecord update(WithdrawalRecord withdrawalRecord); + + /** + * 更新提现状态 + * + * @param recordId 记录ID + * @param status 状态 + * @return 影响行数 + */ + int updateStatus(Long recordId, Integer status); + + /** + * 通过主键逻辑删除 + * + * @param recordId 主键 + * @param updateBy 更新人 + * @return 影响行数 + */ + int logicDeleteById(Long recordId, String updateBy); + + /** + * 通过主键物理删除 + * + * @param recordId 主键 + * @return 影响行数 + */ + int deleteById(Long recordId); + + /** + * 提交提现申请 + * + * @param userId 用户ID + * @param amount 提现金额 + * @param bankName 银行名称 + * @param bankAccount 银行账号 + * @param bankCardholder 持卡人姓名 + * @param remark 备注 + * @return 提现记录 + */ + WithdrawalRecord submitWithdrawal(Long userId, java.math.BigDecimal amount, String bankName, String bankAccount, String bankCardholder, String remark); + + /** + * 处理提现 + * + * @param recordId 记录ID + * @param status 状态 + * @param remark 备注 + * @return 影响行数 + */ + int processWithdrawal(Long recordId, Integer status, String remark); +} \ No newline at end of file diff --git a/src/main/java/com/kexue/skills/service/impl/AccountServiceImpl.java b/src/main/java/com/kexue/skills/service/impl/AccountServiceImpl.java index 2651673..1a90d21 100644 --- a/src/main/java/com/kexue/skills/service/impl/AccountServiceImpl.java +++ b/src/main/java/com/kexue/skills/service/impl/AccountServiceImpl.java @@ -10,6 +10,8 @@ import com.kexue.skills.exception.BizException; import com.kexue.skills.mapper.AccountMapper; import com.kexue.skills.mapper.AccountTransactionMapper; import com.kexue.skills.service.AccountService; +import com.kexue.skills.service.ModelPriceService; +import com.kexue.skills.entity.ModelPrice; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -33,6 +35,9 @@ public class AccountServiceImpl implements AccountService { @Resource private AccountTransactionMapper accountTransactionMapper; + @Resource + private ModelPriceService modelPriceService; + /** * 分页查询 * @@ -96,6 +101,12 @@ public class AccountServiceImpl implements AccountService { if (account.getBalance() == null) { account.setBalance(BigDecimal.ZERO); } + if (account.getWithdrawableBalance() == null) { + account.setWithdrawableBalance(BigDecimal.ZERO); + } + if (account.getNonWithdrawableBalance() == null) { + account.setNonWithdrawableBalance(BigDecimal.ZERO); + } if (account.getFrozenAmount() == null) { account.setFrozenAmount(BigDecimal.ZERO); } @@ -124,6 +135,7 @@ public class AccountServiceImpl implements AccountService { * * @param userId 用户ID * @param amount 增加金额 + * @param isWithdrawable 是否可提现 * @param transactionNo 交易单号 * @param businessId 业务ID * @param businessType 业务类型 @@ -131,7 +143,7 @@ public class AccountServiceImpl implements AccountService { * @return 影响行数 */ @Override - public int addBalance(Long userId, BigDecimal amount, String transactionNo, Long businessId, String businessType, String remark) { + public int addBalance(Long userId, BigDecimal amount, boolean isWithdrawable, String transactionNo, Long businessId, String businessType, String remark) { // 1. 查询账户信息 Account account = this.queryByUserId(userId); if (account == null) { @@ -139,6 +151,8 @@ public class AccountServiceImpl implements AccountService { account = new Account(); account.setUserId(userId); account.setBalance(BigDecimal.ZERO); + account.setWithdrawableBalance(BigDecimal.ZERO); + account.setNonWithdrawableBalance(BigDecimal.ZERO); account.setFrozenAmount(BigDecimal.ZERO); this.insert(account); } @@ -157,10 +171,36 @@ public class AccountServiceImpl implements AccountService { transaction.setBusinessId(businessId); transaction.setBusinessType(businessType); transaction.setRemark(remark); + transaction.setIsExpense(0); // 收入 + transaction.setIncomeType("recharge"); // 充值 this.accountTransactionMapper.insert(transaction); // 3. 更新账户余额 - return this.accountMapper.updateBalance(userId, amount, 1); + if (isWithdrawable) { + account.setWithdrawableBalance(account.getWithdrawableBalance().add(amount)); + } else { + account.setNonWithdrawableBalance(account.getNonWithdrawableBalance().add(amount)); + } + account.setBalance(account.getBalance().add(amount)); + account.setUpdateTime(new Date()); + this.update(account); + return 1; + } + + /** + * 增加账户余额(默认不可提现) + * + * @param userId 用户ID + * @param amount 增加金额 + * @param transactionNo 交易单号 + * @param businessId 业务ID + * @param businessType 业务类型 + * @param remark 备注 + * @return 影响行数 + */ + @Override + public int addBalance(Long userId, BigDecimal amount, String transactionNo, Long businessId, String businessType, String remark) { + return addBalance(userId, amount, false, transactionNo, businessId, businessType, remark); } /** @@ -197,12 +237,193 @@ public class AccountServiceImpl implements AccountService { transaction.setBusinessId(businessId); transaction.setBusinessType(businessType); transaction.setRemark(remark); + transaction.setIsExpense(1); // 支出 this.accountTransactionMapper.insert(transaction); // 4. 更新账户余额 return this.accountMapper.updateBalance(userId, amount, 2); } + /** + * 减少账户余额(token消费转换) + * + * @param userId 用户ID + * @param amount 减少金额 + * @param inputToken 输入token + * @param outputToken 输出token + * @param totalTokens 合计tokens + * @param modelName 处理的模型名称 + * @param question 对应回答的问题或需求 + * @param transactionNo 交易单号 + * @param businessId 业务ID + * @param businessType 业务类型 + * @param remark 备注 + * @return 影响行数 + */ + @Override + public int reduceBalanceWithToken(Long userId, Integer inputToken, Integer outputToken, Integer totalTokens, String modelName, String question, String transactionNo, Long businessId, String businessType, String remark) { + // 1. 查询账户信息 + Account account = this.queryByUserId(userId); + Assert.notNull(account, "账户不存在"); + + // 2. 查询模型价格信息 + ModelPrice modelPrice = modelPriceService.queryByModelName(modelName); + Assert.notNull(modelPrice, "模型价格信息不存在"); + + // 3. 计算金额 + // 输入token费用:输入token数量 / inputPerCent,不足1分按1分计算 + long inputFee = inputToken / modelPrice.getInputPerCent(); + if (inputToken % modelPrice.getInputPerCent() > 0) { + inputFee += 1; + } + + // 输出token费用:输出token数量 / outputPerCent,不足1分按1分计算 + long outputFee = outputToken / modelPrice.getOutputPerCent(); + if (outputToken % modelPrice.getOutputPerCent() > 0) { + outputFee += 1; + } + + // 总费用(分) + long totalFee = inputFee + outputFee; + // 转换为元 + BigDecimal amount = BigDecimal.valueOf(totalFee).divide(BigDecimal.valueOf(100)); + + // 4. 检查余额是否足够 + Assert.isTrue(account.getBalance().compareTo(amount) >= 0, "账户余额不足"); + + // 5. 保存交易记录 + AccountTransaction transaction = new AccountTransaction(); + transaction.setUserId(userId); + transaction.setUserName(account.getUserName()); + transaction.setTransactionType(3); // 购买内容 + transaction.setAmount(amount); + transaction.setBeforeBalance(account.getBalance()); + transaction.setAfterBalance(account.getBalance().subtract(amount)); + transaction.setStatus(1); // 成功 + transaction.setTransactionNo(transactionNo); + transaction.setPayType(3); // 余额支付 + transaction.setBusinessId(businessId); + transaction.setBusinessType(businessType); + transaction.setRemark(remark); + transaction.setIsExpense(1); // 支出 + transaction.setInputToken(inputToken); + transaction.setOutputToken(outputToken); + transaction.setTotalTokens(totalTokens); + transaction.setModelName(modelName); + transaction.setQuestion(question); + this.accountTransactionMapper.insert(transaction); + + // 6. 更新账户余额 + return this.accountMapper.updateBalance(userId, amount, 2); + } + + /** + * 增加账户余额(签到奖励token转换) + * + * @param userId 用户ID + * @param amount 增加金额 + * @param transactionNo 交易单号 + * @param businessId 业务ID + * @param businessType 业务类型 + * @param remark 备注 + * @return 影响行数 + */ + @Override + public int addSignInBalance(Long userId, BigDecimal amount, String transactionNo, Long businessId, String businessType, String remark) { + // 1. 查询账户信息 + Account account = this.queryByUserId(userId); + if (account == null) { + // 创建账户 + account = new Account(); + account.setUserId(userId); + account.setBalance(BigDecimal.ZERO); + account.setWithdrawableBalance(BigDecimal.ZERO); + account.setNonWithdrawableBalance(BigDecimal.ZERO); + account.setFrozenAmount(BigDecimal.ZERO); + this.insert(account); + } + + // 2. 保存交易记录 + AccountTransaction transaction = new AccountTransaction(); + transaction.setUserId(userId); + transaction.setUserName(account.getUserName()); + transaction.setTransactionType(5); // 签到奖励 + transaction.setAmount(amount); + transaction.setBeforeBalance(account.getBalance()); + transaction.setAfterBalance(account.getBalance().add(amount)); + transaction.setStatus(1); // 成功 + transaction.setTransactionNo(transactionNo); + transaction.setPayType(3); // 余额支付 + transaction.setBusinessId(businessId); + transaction.setBusinessType(businessType); + transaction.setRemark(remark); + transaction.setIsExpense(0); // 收入 + transaction.setIncomeType("sign_in"); // 签到奖励 + this.accountTransactionMapper.insert(transaction); + + // 3. 更新账户余额(签到奖励不可提现) + account.setNonWithdrawableBalance(account.getNonWithdrawableBalance().add(amount)); + account.setBalance(account.getBalance().add(amount)); + account.setUpdateTime(new Date()); + this.update(account); + return 1; + } + + /** + * 给用户赠送金额(不可提现) + * + * @param userId 用户ID + * @param amount 赠送金额 + * @param transactionNo 交易单号 + * @param businessId 业务ID + * @param businessType 业务类型 + * @param remark 备注 + * @return 影响行数 + */ + @Override + public int addGiftBalance(Long userId, BigDecimal amount, String transactionNo, Long businessId, String businessType, String remark) { + // 1. 查询账户信息 + Account account = this.queryByUserId(userId); + if (account == null) { + // 创建账户 + account = new Account(); + account.setUserId(userId); + account.setBalance(BigDecimal.ZERO); + account.setWithdrawableBalance(BigDecimal.ZERO); + account.setNonWithdrawableBalance(BigDecimal.ZERO); + account.setFrozenAmount(BigDecimal.ZERO); + this.insert(account); + } + + // 2. 保存交易记录 + AccountTransaction transaction = new AccountTransaction(); + transaction.setUserId(userId); + transaction.setUserName(account.getUserName()); + transaction.setTransactionType(6); // 赠送 + transaction.setAmount(amount); + transaction.setBeforeBalance(account.getBalance()); + transaction.setAfterBalance(account.getBalance().add(amount)); + transaction.setStatus(1); // 成功 + transaction.setTransactionNo(transactionNo); + transaction.setPayType(3); // 余额支付 + transaction.setBusinessId(businessId); + transaction.setBusinessType(businessType); + transaction.setRemark(remark); + transaction.setIsExpense(0); // 收入 + transaction.setIncomeType("gift"); // 赠送 + this.accountTransactionMapper.insert(transaction); + + // 3. 更新账户余额(赠送金额不可提现) + if (account.getNonWithdrawableBalance() == null){ + account.setNonWithdrawableBalance(BigDecimal.ZERO); + } + account.setNonWithdrawableBalance(account.getNonWithdrawableBalance().add(amount)); + account.setBalance(account.getBalance().add(amount)); + account.setUpdateTime(new Date()); + this.update(account); + return 1; + } + /** * 通过主键逻辑删除 * @@ -225,4 +446,15 @@ public class AccountServiceImpl implements AccountService { public int deleteById(Long accountId) { return this.accountMapper.deleteById(accountId); } + + /** + * 获取用户交易记录 + * + * @param userId 用户ID + * @return 交易记录列表 + */ + @Override + public List getTransactions(Long userId) { + return this.accountTransactionMapper.queryByUserId(userId); + } } \ No newline at end of file diff --git a/src/main/java/com/kexue/skills/service/impl/CmsContentServiceImpl.java b/src/main/java/com/kexue/skills/service/impl/CmsContentServiceImpl.java index cbd47e4..a06c15e 100644 --- a/src/main/java/com/kexue/skills/service/impl/CmsContentServiceImpl.java +++ b/src/main/java/com/kexue/skills/service/impl/CmsContentServiceImpl.java @@ -775,13 +775,19 @@ public class CmsContentServiceImpl implements CmsContentService { // 转换为Map Map row = new HashMap<>(); + + + for (int i = 0; i < headerList.size() && i < rowList.size(); i++) { row.put(headerList.get(i), rowList.get(i)); } if (row.isEmpty()) { continue; } - + + // 打印映射后的数据 + System.out.println("映射后的数据:" + row); + CmsContent cmsContent = new CmsContent(); // 设置创建时间和更新时间 @@ -798,6 +804,7 @@ public class CmsContentServiceImpl implements CmsContentService { cmsContent.setLikeCount(0); cmsContent.setCommentCount(0); cmsContent.setSort(0); + cmsContent.setIsOfficial(true); // 固定设置为1 // 读取Excel中的字段 // content_id - 数据库自动生成,不需要读取 @@ -814,16 +821,16 @@ public class CmsContentServiceImpl implements CmsContentService { cmsContent.setTags(tags.replaceAll(" ","")); cmsContent.setIcon(getStringValue(row, "icon")); - cmsContent.setIsOfficial(getBooleanValue(row, "is_official")); + cmsContent.setIsOfficial(true); // 固定设置为1 cmsContent.setPrice(getBigDecimalValue(row, "price")); - cmsContent.setLikeCount(getIntegerValue(row, "like_count")); - cmsContent.setShareCount(getIntegerValue(row, "share_count")); + cmsContent.setLikeCount(100 + new Random().nextInt(3000)); + cmsContent.setShareCount(100 + new Random().nextInt(1000)); cmsContent.setContentType(getIntegerValue(row, "content_type")); cmsContent.setContent(getStringValue(row, "content")); cmsContent.setContentEn(getStringValue(row, "content_en")); cmsContent.setAuditStatus(getIntegerValue(row, "audit_status")); cmsContent.setPublishStatus(getIntegerValue(row, "publish_status")); - cmsContent.setPublishTime(getDateValue(row, "publish_time")); + cmsContent.setPublishTime(now); // 设置为与update_time一致 cmsContent.setViewCount(getIntegerValue(row, "view_count")); cmsContent.setCommentCount(getIntegerValue(row, "comment_count")); cmsContent.setIsPaid(getIntegerValue(row, "is_paid")); @@ -839,7 +846,7 @@ public class CmsContentServiceImpl implements CmsContentService { batchList.add(cmsContent); // 达到批量大小或最后一行时,执行批量插入 - if (batchList.size() >= BATCH_SIZE || rowIndex == totalRows - 1) { + if (batchList.size() >= BATCH_SIZE) { if (!batchList.isEmpty()) { // 执行批量插入 this.cmsContentMapper.batchInsert(batchList); @@ -854,6 +861,13 @@ public class CmsContentServiceImpl implements CmsContentService { continue; } } + + // 处理最后一批不足BATCH_SIZE的数据 + if (!batchList.isEmpty()) { + this.cmsContentMapper.batchInsert(batchList); + successCount += batchList.size(); + batchList.clear(); + } } catch (Exception e) { e.printStackTrace(); } @@ -1029,4 +1043,6 @@ public class CmsContentServiceImpl implements CmsContentService { return totalSuccessCount; } + + } \ No newline at end of file diff --git a/src/main/java/com/kexue/skills/service/impl/ContentPurchaseServiceImpl.java b/src/main/java/com/kexue/skills/service/impl/ContentPurchaseServiceImpl.java index 64196ec..af083b5 100644 --- a/src/main/java/com/kexue/skills/service/impl/ContentPurchaseServiceImpl.java +++ b/src/main/java/com/kexue/skills/service/impl/ContentPurchaseServiceImpl.java @@ -12,7 +12,6 @@ import com.kexue.skills.mapper.ContentPurchaseMapper; import com.kexue.skills.service.AccountService; import com.kexue.skills.service.CmsContentService; import com.kexue.skills.service.ContentPurchaseService; -import com.kexue.skills.service.PointsAccountService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -41,9 +40,6 @@ public class ContentPurchaseServiceImpl implements ContentPurchaseService { @Resource private AccountService accountService; - - @Resource - private PointsAccountService pointsAccountService; @Resource private LoginUserCacheUtil loginUserCacheUtil; @@ -211,22 +207,6 @@ public class ContentPurchaseServiceImpl implements ContentPurchaseService { // 扣除用户余额 this.accountService.reduceBalance(userId, price, transactionNo, contentId, "purchase_content", "购买内容:" + content.getTitle()); - // 6. 更新购买记录状态 - purchase.setStatus(2); // 已支付 - purchase.setPurchaseTime(new Date()); - this.insert(purchase); - isPaid = true; - } else if (payType == 2) { - // 积分支付 - Assert.isTrue(content.getSupportPointsPay() == 1, "该内容不支持积分支付"); - - Integer points = content.getRequiredPoints(); - purchase.setAmount(BigDecimal.ZERO); - purchase.setPoints(points); - - // 扣除用户积分 - this.pointsAccountService.reducePoints(userId, points, transactionNo, contentId, "purchase_content", "购买内容:" + content.getTitle()); - // 6. 更新购买记录状态 purchase.setStatus(2); // 已支付 purchase.setPurchaseTime(new Date()); diff --git a/src/main/java/com/kexue/skills/service/impl/ModelPriceServiceImpl.java b/src/main/java/com/kexue/skills/service/impl/ModelPriceServiceImpl.java new file mode 100644 index 0000000..057adf4 --- /dev/null +++ b/src/main/java/com/kexue/skills/service/impl/ModelPriceServiceImpl.java @@ -0,0 +1,114 @@ +package com.kexue.skills.service.impl; + +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import com.kexue.skills.entity.ModelPrice; +import com.kexue.skills.entity.dto.ModelPriceDto; +import com.kexue.skills.mapper.ModelPriceMapper; +import com.kexue.skills.service.ModelPriceService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.Date; +import java.util.List; + +/** + * (ModelPrice)表服务实现类 + * 大模型Token价格表 + * + * @author 王志维 + * @since 2026-03-26 10:15:00 + */ +@Service("modelPriceService") +public class ModelPriceServiceImpl implements ModelPriceService { + @Resource + private ModelPriceMapper modelPriceMapper; + + /** + * 分页查询 + * + * @param queryDto 筛选条件 + * @return 查询结果 + */ + @Override + public PageInfo getPageList(ModelPriceDto queryDto) { + PageHelper.startPage(queryDto.getPageNum(), queryDto.getPageSize()); + List list = this.modelPriceMapper.getPageList(queryDto); + return new PageInfo<>(list); + } + + /** + * 查询列表 + * + * @param queryDto 筛选条件 + * @return 查询结果 + */ + @Override + public List getList(ModelPriceDto queryDto) { + return this.modelPriceMapper.getList(queryDto); + } + + /** + * 通过主键查询单条数据 + * + * @param id 主键 + * @return 实例对象 + */ + @Override + public ModelPrice queryById(Long id) { + return this.modelPriceMapper.queryById(id); + } + + /** + * 通过模型名称查询数据 + * + * @param modelName 模型名称 + * @return 实例对象 + */ + @Override + public ModelPrice queryByModelName(String modelName) { + return this.modelPriceMapper.queryByModelName(modelName); + } + + /** + * 新增数据 + * + * @param modelPrice 实例对象 + * @return 实例对象 + */ + @Override + public ModelPrice insert(ModelPrice modelPrice) { + // 设置创建时间 + modelPrice.setCreatedTime(new Date()); + // 保存数据 + this.modelPriceMapper.insert(modelPrice); + return modelPrice; + } + + /** + * 更新数据 + * + * @param modelPrice 实例对象 + * @return 实例对象 + */ + @Override + public ModelPrice update(ModelPrice modelPrice) { + // 设置更新时间 + modelPrice.setUpdatedTime(new Date()); + // 更新数据 + this.modelPriceMapper.update(modelPrice); + return this.queryById(modelPrice.getId()); + } + + /** + * 通过主键删除数据 + * + * @param id 主键 + * @return 影响行数 + */ + @Override + public int deleteById(Long id) { + return this.modelPriceMapper.deleteById(id); + } + +} diff --git a/src/main/java/com/kexue/skills/service/impl/PayServiceImpl.java b/src/main/java/com/kexue/skills/service/impl/PayServiceImpl.java index 241c3e7..79b151b 100644 --- a/src/main/java/com/kexue/skills/service/impl/PayServiceImpl.java +++ b/src/main/java/com/kexue/skills/service/impl/PayServiceImpl.java @@ -20,9 +20,13 @@ import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; import java.io.UnsupportedEncodingException; +import java.net.http.HttpClient; +import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.time.Duration; import java.util.*; +import java.math.BigDecimal; /** * 支付服务实现类 @@ -31,6 +35,23 @@ import java.util.*; public class PayServiceImpl implements PayService { private static final Logger logger = LoggerFactory.getLogger(PayServiceImpl.class); + + // 单例 HTTP 客户端,提升性能 + private static final HttpClient HTTP_CLIENT = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(30)) + .build(); + + // 微信支付下单 URL + private static final String WECHAT_PAY_URL = "https://api.mch.weixin.qq.com/pay/unifiedorder"; + + // 最大金额限制(元) + private static final BigDecimal MAX_AMOUNT = new BigDecimal("100000"); + + // 最小金额限制(元) + private static final BigDecimal MIN_AMOUNT = new BigDecimal("0.01"); + + // 商品描述最大长度(字节) + private static final int MAX_BODY_LENGTH = 128; @Resource private PaymentConfig paymentConfig; @@ -66,6 +87,10 @@ public class PayServiceImpl implements PayService { } } sb.append("key=").append(key); + + // 打印签名字符串(注意:生产环境应移除) + logger.info("生成签名的字符串: {}", sb.toString()); + logger.info("使用的密钥: {}", key); // 生成MD5 try { @@ -79,7 +104,9 @@ public class PayServiceImpl implements PayService { } result.append(hex); } - return result.toString().toUpperCase(); + String signature = result.toString().toUpperCase(); + logger.info("生成的签名: {}", signature); + return signature; } catch (NoSuchAlgorithmException | UnsupportedEncodingException e) { logger.error("生成签名失败", e); throw new RuntimeException("生成签名失败", e); @@ -95,10 +122,14 @@ public class PayServiceImpl implements PayService { StringBuilder sb = new StringBuilder(); sb.append(""); for (Map.Entry entry : params.entrySet()) { - sb.append("<").append(entry.getKey()).append(">").append(entry.getValue()).append(""); + sb.append("<").append(entry.getKey()).append(">"); } sb.append(""); - return sb.toString(); + String xml = sb.toString(); + logger.info("生成的XML: {}", xml); + return xml; } /** @@ -109,19 +140,44 @@ public class PayServiceImpl implements PayService { private Map xmlToMap(String xml) { Map map = new HashMap<>(); try { - // 简单的XML解析 - String content = xml.replaceAll("", "").replaceAll("", ""); - String[] elements = content.split(""); - if (idx == -1) continue; - String key = element.substring(1, idx); - String value = element.substring(idx + 1); - map.put(key, value); + // 使用更健壮的XML解析方法 + xml = xml.trim(); + if (!xml.startsWith("") || !xml.endsWith("")) { + throw new RuntimeException("无效的XML格式"); } + + // 移除xml标签 + String content = xml.substring(5, xml.length() - 6).trim(); + + // 按标签分割 + int pos = 0; + while (pos < content.length()) { + // 找到开始标签 + int startTagStart = content.indexOf('<', pos); + if (startTagStart == -1) break; + int startTagEnd = content.indexOf('>', startTagStart); + if (startTagEnd == -1) break; + + String tagName = content.substring(startTagStart + 1, startTagEnd).trim(); + + // 找到结束标签 + int endTagStart = content.indexOf("", startTagEnd); + if (endTagStart == -1) break; + + String value = content.substring(startTagEnd + 1, endTagStart).trim(); + + // 处理CDATA标签 + if (value.startsWith("") ) { + value = value.substring(9, value.length() - 3).trim(); + } + + map.put(tagName, value); + pos = endTagStart + tagName.length() + 3; + } + + logger.info("XML解析结果: {}", map); } catch (Exception e) { - logger.error("XML解析失败", e); + logger.error("XML解析失败: {}", xml, e); throw new RuntimeException("XML解析失败", e); } return map; @@ -130,64 +186,255 @@ public class PayServiceImpl implements PayService { /** * 创建微信支付订单 * @param order 支付订单信息 + * @param ipAddress 用户真实 IP 地址(从 Controller 传入) * @return 微信支付参数 + * @throws IllegalArgumentException 参数错误时抛出 + * @throws RuntimeException 网络异常或微信侧错误时抛出 */ @Override - public Map createWechatPay(PaymentOrder order) { + public Map createWechatPay(PaymentOrder order, String ipAddress) { + // 1. 参数校验 + validatePaymentOrder(order); + try { - // 构建微信支付参数 - Map params = new HashMap<>(); - params.put("appid", paymentConfig.getWechat().getAppId()); - params.put("mch_id", paymentConfig.getWechat().getMchId()); - params.put("nonce_str", generateNonceStr()); - params.put("body", order.getProductName()); - params.put("out_trade_no", order.getOrderNo()); - params.put("total_fee", String.valueOf((int) (order.getAmount().doubleValue() * 100))); - params.put("spbill_create_ip", "127.0.0.1"); - params.put("notify_url", paymentConfig.getWechat().getNotifyUrl()); - params.put("trade_type", "NATIVE"); - - // 生成签名 + // 2. 构建微信支付参数 + Map params = buildWechatPayParams(order, ipAddress); + + // 3. 生成签名并构建 XML String sign = generateSignature(params, paymentConfig.getWechat().getMchKey()); params.put("sign", sign); - - // 构建请求XML + String xml = mapToXml(params); - logger.info("微信支付请求XML: {}", xml); - - // 发送请求到微信支付接口 - String xmlResult = null; - try { - logger.info("开始发送微信支付请求到: https://api.mch.weixin.qq.com/pay/unifiedorder"); - xmlResult = HttpUtil.post("https://api.mch.weixin.qq.com/pay/unifiedorder", xml); - logger.info("微信支付接口返回XML: {}", xmlResult); - } catch (Exception e) { - logger.error("发送微信支付请求失败", e); - throw new RuntimeException("发送微信支付请求失败: " + e.getMessage()); - } + logRequest(xml); - if (xmlResult == null || xmlResult.isEmpty()) { - logger.error("微信支付接口返回空响应"); - throw new RuntimeException("微信支付接口返回空响应"); - } + // 4. 发送请求到微信支付接口 + String xmlResult = sendWechatRequest(xml); - Map result = xmlToMap(xmlResult); - logger.info("解析后的微信支付响应: {}", result); - - // 处理响应 - if ("SUCCESS".equals(result.get("return_code")) && "SUCCESS".equals(result.get("result_code"))) { - Map payParams = new HashMap<>(); - payParams.put("code_url", result.get("code_url")); - payParams.put("order_no", order.getOrderNo()); - return payParams; - } else { - String returnMsg = result.get("return_msg") != null ? result.get("return_msg") : "未知错误"; - logger.error("微信支付下单失败: {}", returnMsg); - throw new RuntimeException("微信支付下单失败: " + returnMsg); - } + // 5. 解析并处理响应 + return processWechatResponse(xmlResult, order); + + } catch (IllegalArgumentException e) { + logger.error("参数校验失败:{}", e.getMessage()); + throw e; } catch (Exception e) { logger.error("创建微信支付订单失败", e); - throw new RuntimeException("创建微信支付订单失败", e); + throw new RuntimeException("系统繁忙,请稍后重试", e); + } + } + + /** + * 校验支付订单参数 + */ + private void validatePaymentOrder(PaymentOrder order) { + if (order == null) { + throw new IllegalArgumentException("支付订单不能为空"); + } + + // 校验金额 + if (order.getAmount() == null) { + throw new IllegalArgumentException("支付金额不能为空"); + } + if (order.getAmount().compareTo(MIN_AMOUNT) < 0) { + throw new IllegalArgumentException("支付金额不能小于 0.01 元"); + } + if (order.getAmount().compareTo(MAX_AMOUNT) > 0) { + throw new IllegalArgumentException("单笔支付金额不能超过 10 万元"); + } + + // 校验订单号 + if (order.getOrderNo() == null || order.getOrderNo().trim().isEmpty()) { + throw new IllegalArgumentException("订单号不能为空"); + } + if (order.getOrderNo().length() > 32) { + throw new IllegalArgumentException("订单号长度不能超过 32 位"); + } + if (!order.getOrderNo().matches("^[a-zA-Z0-9_]+$")) { + throw new IllegalArgumentException("订单号只能包含字母、数字和下划线"); + } + + // 校验商品名称 + if (order.getProductName() == null || order.getProductName().trim().isEmpty()) { + throw new IllegalArgumentException("商品名称不能为空"); + } + } + + /** + * 构建微信支付请求参数 + */ + private Map buildWechatPayParams(PaymentOrder order, String ipAddress) { + Map params = new HashMap<>(); + + // 应用ID + String appId = paymentConfig.getWechat().getAppId(); + logger.info("使用的appId: {}", appId); + params.put("appid", appId); + + // 商户号 + String mchId = paymentConfig.getWechat().getMchId(); + logger.info("使用的mchId: {}", mchId); + params.put("mch_id", mchId); + + // 随机字符串 + String nonceStr = generateNonceStr(); + logger.info("生成的nonce_str: {}", nonceStr); + params.put("nonce_str", nonceStr); + + params.put("sign_type", "MD5"); + + // 商品描述 + String body = limitBodyLength(order.getProductName()); + logger.info("商品描述: {}", body); + params.put("body", body); + + // 商户订单号 + String outTradeNo = order.getOrderNo(); + logger.info("商户订单号: {}", outTradeNo); + params.put("out_trade_no", outTradeNo); + + // 金额转换:元转分 + BigDecimal totalFee = order.getAmount() + .multiply(BigDecimal.valueOf(100)) + .setScale(0, BigDecimal.ROUND_HALF_UP); + String totalFeeStr = String.valueOf(totalFee.intValue()); + logger.info("计算的 total_fee: {} 分", totalFeeStr); + params.put("total_fee", totalFeeStr); + + // 使用传入的 IP 地址,如果为空则使用默认值 + String ip = ipAddress != null ? ipAddress : "127.0.0.1"; + // 转换IPv6地址为IPv4 + if (ip.equals("0:0:0:0:0:0:0:1")) { + ip = "127.0.0.1"; + } + // 确保IP地址格式正确 + ip = ip.trim(); + logger.info("使用的IP地址: {}", ip); + params.put("spbill_create_ip", ip); + + // 使用配置的回调地址 + String notifyUrl = paymentConfig.getWechat().getNotifyUrl(); + // 确保回调地址格式正确 + notifyUrl = notifyUrl.trim(); + logger.info("使用的回调地址: {}", notifyUrl); + params.put("notify_url", notifyUrl); + + // 交易类型 + params.put("trade_type", "NATIVE"); + + // 商品ID(NATIVE支付必传) + params.put("product_id", order.getOrderNo()); + + // 打印构建的参数 + logger.info("构建的微信支付参数: {}", params); + + return params; + } + + /** + * 限制商品描述长度(最多 128 字节) + */ + private String limitBodyLength(String body) { + if (body == null) { + return ""; + } + try { + // 移除特殊字符,只保留中文、英文、数字和常见标点 + body = body.replaceAll("[^\u4e00-\u9fa5a-zA-Z0-9\\s,.!?!,。]", ""); + + byte[] bytes = body.getBytes(StandardCharsets.UTF_8); + if (bytes.length <= MAX_BODY_LENGTH) { + return body; + } + // 截断并添加省略号 + String truncated = new String(bytes, 0, Math.min(MAX_BODY_LENGTH - 3, bytes.length), StandardCharsets.UTF_8); + return truncated + "..."; + } catch (Exception e) { + logger.warn("商品描述编码失败,使用默认值", e); + return body.length() > 60 ? body.substring(0, 60) + "..." : body; + } + } + + /** + * 记录请求日志(敏感信息脱敏) + */ + private void logRequest(String xml) { + if (logger.isDebugEnabled()) { + logger.debug("微信支付请求 XML: {}", maskSensitiveInfo(xml)); + } + logger.info("开始发送微信支付请求到:{}", WECHAT_PAY_URL); + } + + /** + * 脱敏敏感信息 + */ + private String maskSensitiveInfo(String xml) { + return xml.replaceAll("()", "$1***$3") + .replaceAll("()", "$1***$3"); + } + + /** + * 发送微信支付请求 + */ + private String sendWechatRequest(String xml) throws Exception { + logger.info("请求参数:appId={}, mchId={}", + paymentConfig.getWechat().getAppId(), + paymentConfig.getWechat().getMchId()); + + String xmlResult = HttpUtil.postWithClient(WECHAT_PAY_URL, xml, HTTP_CLIENT); + + if (xmlResult == null || xmlResult.trim().isEmpty()) { + logger.error("微信支付接口返回空响应"); + throw new RuntimeException("微信支付接口返回空响应"); + } + + logger.info("微信支付接口返回 XML: {}", xmlResult); + return xmlResult; + } + + /** + * 处理微信支付响应 + */ + private Map processWechatResponse(String xmlResult, PaymentOrder order) { + try { + Map result = xmlToMap(xmlResult); + logger.info("解析后的微信支付响应:{}", result); + + String returnCode = result.get("return_code"); + String resultCode = result.get("result_code"); + String returnMsg = result.get("return_msg"); + String errCode = result.get("err_code"); + String errCodeDes = result.get("err_code_des"); + + logger.info("微信支付返回码:return_code={}, result_code={}, return_msg={}, err_code={}, err_code_des={}", + returnCode, resultCode, returnMsg, errCode, errCodeDes); + + // 检查通信标识 + if (!"SUCCESS".equals(returnCode)) { + String errorMsg = "通信失败:" + (returnMsg != null ? returnMsg : "未知错误"); + logger.error(errorMsg); + throw new RuntimeException(errorMsg); + } + + // 检查业务结果 + if (!"SUCCESS".equals(resultCode)) { + String errorMsg = returnMsg != null ? returnMsg : "未知错误"; + if (errCodeDes != null) { + errorMsg += " (" + errCodeDes + ")"; + } + logger.error("微信支付下单失败:err_code={}, error_msg={}", errCode, errorMsg); + throw new RuntimeException("支付下单失败:" + errorMsg); + } + + // 返回成功结果 + Map payParams = new HashMap<>(); + payParams.put("code_url", result.get("code_url")); + payParams.put("order_no", order.getOrderNo()); + return payParams; + + } catch (IllegalArgumentException e) { + throw e; + } catch (Exception e) { + logger.error("解析微信支付响应失败", e); + throw new RuntimeException("解析响应失败:" + e.getMessage(), e); } } diff --git a/src/main/java/com/kexue/skills/service/impl/PaymentOrderServiceImpl.java b/src/main/java/com/kexue/skills/service/impl/PaymentOrderServiceImpl.java index c4191ed..5a02b61 100644 --- a/src/main/java/com/kexue/skills/service/impl/PaymentOrderServiceImpl.java +++ b/src/main/java/com/kexue/skills/service/impl/PaymentOrderServiceImpl.java @@ -36,6 +36,9 @@ public class PaymentOrderServiceImpl implements PaymentOrderService { @Resource private com.kexue.skills.service.ContentPurchaseService contentPurchaseService; + + @Resource + private com.kexue.skills.service.CmsContentService cmsContentService; /** * 分页查询 @@ -199,36 +202,130 @@ public class PaymentOrderServiceImpl implements PaymentOrderService { // 查询订单 PaymentOrder order = this.queryByOrderNo(orderNo); if (order == null) { + logger.warn("支付回调订单不存在:{}", orderNo); return false; } - + // 更新订单状态 this.updateStatus(order.getOrderId(), status, channelOrderNo); - + // 如果支付成功,处理相关业务逻辑 if (status == 2) { // 2.已支付 + logger.info("支付订单回调成功,orderNo: {}, businessType: {}", orderNo, order.getBusinessType()); + // 根据业务类型处理不同的业务逻辑 if ("recharge".equals(order.getBusinessType())) { // 充值业务,增加用户余额 - this.accountService.addBalance(order.getUserId(), order.getAmount(), orderNo, - order.getBusinessId(), order.getBusinessType(), "充值成功"); - } else if ("purchase_content".equals(order.getBusinessType())) { - // 购买内容业务,更新购买记录状态 try { - // 查找对应的购买记录并更新状态 - com.kexue.skills.entity.ContentPurchase purchase = contentPurchaseService.queryByUserIdAndContentId(order.getUserId(), order.getBusinessId()); - if (purchase != null && purchase.getStatus() == 1) { // 1.待支付 - contentPurchaseService.updateStatus(purchase.getPurchaseId(), 2); // 2.已支付 - } + this.accountService.addBalance(order.getUserId(), order.getAmount(), orderNo, + order.getBusinessId(), order.getBusinessType(), "充值成功"); + logger.info("充值业务处理完成,userId: {}, amount: {}", order.getUserId(), order.getAmount()); } catch (Exception e) { - // 记录错误但不影响支付回调的处理 - logger.error("更新购买记录状态失败: {}", e.getMessage(), e); + logger.error("充值业务处理失败:{}", e.getMessage(), e); + return false; + } + } else if ("purchase_content".equals(order.getBusinessType())) { + // 购买内容业务,创建或更新购买记录 + try { + handleContentPurchaseCallback(order); + } catch (Exception e) { + logger.error("购买内容业务处理失败:{}", e.getMessage(), e); + return false; } } } - + return true; } + + /** + * 处理内容购买回调逻辑 + * + * @param order 支付订单 + */ + private void handleContentPurchaseCallback(PaymentOrder order) { + Long userId = order.getUserId(); + Long contentId = order.getBusinessId(); + + // 1. 先查询是否已存在购买记录 + com.kexue.skills.entity.ContentPurchase existingPurchase = + contentPurchaseService.queryByUserIdAndContentId(userId, contentId); + + if (existingPurchase != null) { + // 2. 如果已存在购买记录且状态为待支付,更新为已支付 + if (existingPurchase.getStatus() == 1) { // 1.待支付 + contentPurchaseService.updateStatus(existingPurchase.getPurchaseId(), 2); // 2.已支付 + logger.info("更新购买记录状态为已支付,userId: {}, contentId: {}", userId, contentId); + } else { + logger.warn("购买记录已存在且状态为已支付,无需更新,userId: {}, contentId: {}", userId, contentId); + } + } else { + // 3. 如果不存在购买记录,创建新的购买记录 + logger.info("购买记录不存在,创建新记录,userId: {}, contentId: {}", userId, contentId); + createContentPurchaseRecord(order); + } + } + + /** + * 创建内容购买记录 + * + * @param order 支付订单 + */ + private void createContentPurchaseRecord(PaymentOrder order) { + try { + // 查询内容信息获取标题 + String contentTitle = order.getProductName(); + if (order.getBusinessId() != null) { + com.kexue.skills.entity.CmsContent content = cmsContentService.queryById(order.getBusinessId()); + if (content != null) { + contentTitle = content.getTitle(); + } + } + + // 创建购买记录 + com.kexue.skills.entity.ContentPurchase purchase = new com.kexue.skills.entity.ContentPurchase(); + purchase.setUserId(order.getUserId()); + purchase.setContentId(order.getBusinessId()); + purchase.setContentTitle(contentTitle); + purchase.setPayType(mapPayType(order.getPayType())); // 转换支付方式类型 + purchase.setAmount(order.getAmount()); + purchase.setPoints(0); // 微信/支付宝支付不计积分 + purchase.setStatus(2); // 2.已支付 + purchase.setPurchaseTime(new Date()); + purchase.setCreateTime(new Date()); + purchase.setUpdateTime(new Date()); + purchase.setDeleteFlag(0); + + contentPurchaseService.insert(purchase); + logger.info("创建购买记录成功,userId: {}, contentId: {}, purchaseId: {}", + order.getUserId(), order.getBusinessId(), purchase.getPurchaseId()); + } catch (Exception e) { + logger.error("创建购买记录失败:{}", e.getMessage(), e); + throw new RuntimeException("创建购买记录失败:" + e.getMessage(), e); + } + } + + /** + * 映射支付方式类型 + * payment_order.pay_type: 1.微信 2.支付宝 + * content_purchase.pay_type: 3.微信支付 4.支付宝支付 + * + * @param payType 支付订单的支付方式 + * @return 购买记录的支付方式 + */ + private int mapPayType(Integer payType) { + if (payType == null) { + return 3; // 默认微信支付 + } + switch (payType) { + case 1: // 微信 + return 3; + case 2: // 支付宝 + return 4; + default: + return 3; // 默认微信支付 + } + } /** * 生成订单号 diff --git a/src/main/java/com/kexue/skills/service/impl/PointsAccountServiceImpl.java b/src/main/java/com/kexue/skills/service/impl/PointsAccountServiceImpl.java deleted file mode 100644 index c0516b6..0000000 --- a/src/main/java/com/kexue/skills/service/impl/PointsAccountServiceImpl.java +++ /dev/null @@ -1,229 +0,0 @@ -package com.kexue.skills.service.impl; - -import com.github.pagehelper.PageHelper; -import com.github.pagehelper.PageInfo; -import com.kexue.skills.entity.PointsAccount; -import com.kexue.skills.entity.PointsTransaction; -import com.kexue.skills.entity.dto.PointsAccountDto; -import com.kexue.skills.common.Assert; -import com.kexue.skills.exception.BizException; -import com.kexue.skills.mapper.PointsAccountMapper; -import com.kexue.skills.mapper.PointsTransactionMapper; -import com.kexue.skills.service.PointsAccountService; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import javax.annotation.Resource; -import java.util.Date; -import java.util.List; - -/** - * (PointsAccount)表服务实现类 - * - * @author 王志维 - * @since 2025-02-21 23:01:48 - */ -@Service("pointsAccountService") -@Transactional(rollbackFor = Exception.class) -public class PointsAccountServiceImpl implements PointsAccountService { - @Resource - private PointsAccountMapper pointsAccountMapper; - - @Resource - private PointsTransactionMapper pointsTransactionMapper; - - /** - * 分页查询 - * - * @param queryDto 筛选条件 - * @return 查询结果 - */ - @Override - public PageInfo getPageList(PointsAccountDto queryDto) { - PageHelper.startPage(queryDto.getPageNum(), queryDto.getPageSize()); - List list = this.pointsAccountMapper.getPageList(queryDto); - return new PageInfo<>(list); - } - - /** - * 查询列表 - * - * @param queryDto 筛选条件 - * @return 查询结果 - */ - @Override - public List getList(PointsAccountDto queryDto) { - return this.pointsAccountMapper.getList(queryDto); - } - - /** - * 通过主键查询单条数据 - * - * @param accountId 主键 - * @return 实例对象 - */ - @Override - public PointsAccount queryById(Long accountId) { - return this.pointsAccountMapper.queryById(accountId); - } - - /** - * 通过用户ID查询积分账户 - * - * @param userId 用户ID - * @return 实例对象 - */ - @Override - public PointsAccount queryByUserId(Long userId) { - return this.pointsAccountMapper.queryByUserId(userId); - } - - /** - * 新增数据 - * - * @param pointsAccount 实例对象 - * @return 实例对象 - */ - @Override - public PointsAccount insert(PointsAccount pointsAccount) { - // 设置创建时间和更新时间 - Date now = new Date(); - pointsAccount.setCreateTime(now); - pointsAccount.setUpdateTime(now); - // 设置默认值 - pointsAccount.setDeleteFlag(0); - if (pointsAccount.getTotalPoints() == null) { - pointsAccount.setTotalPoints(0); - } - if (pointsAccount.getAvailablePoints() == null) { - pointsAccount.setAvailablePoints(0); - } - if (pointsAccount.getFrozenPoints() == null) { - pointsAccount.setFrozenPoints(0); - } - // 保存数据 - this.pointsAccountMapper.insert(pointsAccount); - return pointsAccount; - } - - /** - * 更新数据 - * - * @param pointsAccount 实例对象 - * @return 实例对象 - */ - @Override - public PointsAccount update(PointsAccount pointsAccount) { - // 设置更新时间 - pointsAccount.setUpdateTime(new Date()); - // 更新数据 - this.pointsAccountMapper.update(pointsAccount); - return this.queryById(pointsAccount.getAccountId()); - } - - /** - * 增加积分 - * - * @param userId 用户ID - * @param points 增加积分 - * @param transactionNo 交易单号 - * @param businessId 业务ID - * @param businessType 业务类型 - * @param remark 备注 - * @return 影响行数 - */ - @Override - public int addPoints(Long userId, Integer points, String transactionNo, Long businessId, String businessType, String remark) { - // 1. 查询积分账户信息 - PointsAccount account = this.queryByUserId(userId); - if (account == null) { - // 创建积分账户 - account = new PointsAccount(); - account.setUserId(userId); - account.setTotalPoints(0); - account.setAvailablePoints(0); - account.setFrozenPoints(0); - this.insert(account); - } - - // 2. 保存积分交易记录 - PointsTransaction transaction = new PointsTransaction(); - transaction.setUserId(userId); - transaction.setUserName(account.getUserName()); - transaction.setTransactionType(1); // 1.获得积分 - transaction.setPoints(points); - transaction.setBeforePoints(account.getAvailablePoints()); - transaction.setAfterPoints(account.getAvailablePoints() + points); - transaction.setStatus(1); // 成功 - transaction.setTransactionNo(transactionNo); - transaction.setBusinessId(businessId); - transaction.setBusinessType(businessType); - transaction.setRemark(remark); - this.pointsTransactionMapper.insert(transaction); - - // 3. 更新积分账户 - return this.pointsAccountMapper.updatePoints(userId, points, 1); - } - - /** - * 减少积分 - * - * @param userId 用户ID - * @param points 减少积分 - * @param transactionNo 交易单号 - * @param businessId 业务ID - * @param businessType 业务类型 - * @param remark 备注 - * @return 影响行数 - */ - @Override - public int reducePoints(Long userId, Integer points, String transactionNo, Long businessId, String businessType, String remark) { - // 1. 查询积分账户信息 - PointsAccount account = this.queryByUserId(userId); - Assert.notNull(account, "积分账户不存在"); - - // 2. 检查积分是否足够 - Assert.isTrue(account.getAvailablePoints() >= points, "积分不足"); - - // 3. 保存积分交易记录 - PointsTransaction transaction = new PointsTransaction(); - transaction.setUserId(userId); - transaction.setUserName(account.getUserName()); - transaction.setTransactionType(2); // 2.使用积分 - transaction.setPoints(points); - transaction.setBeforePoints(account.getAvailablePoints()); - transaction.setAfterPoints(account.getAvailablePoints() - points); - transaction.setStatus(1); // 成功 - transaction.setTransactionNo(transactionNo); - transaction.setBusinessId(businessId); - transaction.setBusinessType(businessType); - transaction.setRemark(remark); - this.pointsTransactionMapper.insert(transaction); - - // 4. 更新积分账户 - return this.pointsAccountMapper.updatePoints(userId, points, 2); - } - - /** - * 通过主键逻辑删除 - * - * @param accountId 主键 - * @param updateBy 更新人 - * @return 影响行数 - */ - @Override - public int logicDeleteById(Long accountId, String updateBy) { - return this.pointsAccountMapper.logicDeleteById(accountId, updateBy); - } - - /** - * 通过主键物理删除 - * - * @param accountId 主键 - * @return 影响行数 - */ - @Override - public int deleteById(Long accountId) { - return this.pointsAccountMapper.deleteById(accountId); - } -} \ No newline at end of file diff --git a/src/main/java/com/kexue/skills/service/impl/SkillGenServiceImpl.java b/src/main/java/com/kexue/skills/service/impl/SkillGenServiceImpl.java index 0bc8a99..17c32ba 100644 --- a/src/main/java/com/kexue/skills/service/impl/SkillGenServiceImpl.java +++ b/src/main/java/com/kexue/skills/service/impl/SkillGenServiceImpl.java @@ -225,13 +225,90 @@ public class SkillGenServiceImpl implements SkillGenService { } tagsList.append(tag.getTagName()); if (i < tags.size() - 1) { - tagsList.append(","); + tagsList.append(","); } } } - String systemContent = "你是一个专业的AI技能设计助手。请基于用户提供的Skill名称、描述、标签,按照skills目录结构输出完整的skills内容,包括skills.md本体内容、scripts目录中的脚本等,并打包成一个YAML文件技能包。请严格遵循以下规范:1. 包含必需的文件和目录;2. 多行内容使用 | 字面块;3. 内容从行首开始;4. 空目录用 children: [] 表示;5. 文件内容要实际有用;6.输出一个完整的YAML文档,概要含核心属性:name、version、description、author、created、tags 等,其中 structure 为核心节点,用于描述 skill 包的文件目录结构;structure 下的每个节点均含基础属性:name、type、path、format、description,content 和 children 为互选属性,由 type 决定:type 为 file 时,展示文件具体内容在 content 字段,必须保证content内容的缩进为2个字符;type 为 directory 时,展示子目录 / 文件节点在 children [] 数组中,无需其他额外说明。"; - String userContent = "请根据以下Skill信息生成skills.md文档内容:Skill名称:SKILL_NAME,Skill描述:DESCRIPTION,Skill标签:TAGS ,摘要:SUMMARY。"; - userContent = userContent.replace("SKILL_NAME", request.getName()).replace("DESCRIPTION", request.getDescription()).replace("TAGS", tagsList.toString()).replace("SUMMARY", request.getRequirement()); + String systemContent = """ + 你是AI技能包设计专家,仅输出【纯YAML文本】,不包含任何多余文字(无解释、无注释、无引言、无结尾)。 + + ### 一、YAML顶层强制规则(仅一个节点:package) + 1. 顶层只能有 `package` 一个节点,所有信息(名称、版本、目录结构等)均嵌套在 `package` 下 + 2. `package` 节点必含子字段:name、version、description、author、created、tags、structure(缺一不可) + + ### 二、package子字段规范(固定格式) + 1. name:技能名称(与用户提供的Skill名称完全一致,不修改) + 2. version:固定为 "1.0.0" + 3. description:用户提供的Skill描述(完整复制,不增删任何内容) + 4. author:固定为 "AI技能生成助手" + 5. created:格式为 "YYYY-MM-DD"(使用当前日期,如2026-04-01) + 6. tags:数组格式,值为用户提供的Skill标签(中文,自动去重,逗号后加空格) + 7. structure:技能包目录树根节点(必为directory类型,统一命名"skills") + + ### 三、structure目录树规则(仅Python脚本,无其他语言) + #### 1. 基础必含文件(所有技能通用) + - /skills(根目录,type: directory,path: /skills,format: dir,description: 技能包根目录) + - /skills/skills.md(type: file,path: /skills/skills.md,format: markdown,description: 技能说明文档) + + #### 2. Python脚本目录/文件判断逻辑 + - 若用户提供的“Skill描述”“Skill摘要”中**包含“脚本”“代码”“执行”“运行”“处理”“计算”** 等需执行逻辑的关键词: + 1. 必须新增 /skills/scripts 目录(type: directory,path: /skills/scripts,format: dir,description: Python执行脚本目录) + 2. 必须在该目录下生成 /skills/scripts/main.py(type: file,format: python,description: 技能核心Python脚本) + - 若用户需求中**无任何执行逻辑相关描述**(如纯文档、纯说明类技能):不生成 /skills/scripts 目录,避免冗余 + + #### 3. 节点必含字段 + - directory类型:name、type、path(绝对路径,Unix风格 `/`,如 /skills/scripts)、format(固定"dir")、description、children(空目录写 `children: []`) + - file类型:name、type、path(绝对路径)、format(仅markdown/python两种)、description、content(非空,有实际可用内容) + + ### 四、文件content内容规范(Python脚本必实用) + #### 1. /skills/skills.md(结构固定) + - # 技能名称(不加多余符号,居中可加空格但不强制) + - ## 技能描述(整合用户“描述+摘要”,补充逻辑连贯性) + - ## 标签(格式:`- 标签1\n- 标签2`,对应tags数组内容) + - ## 使用说明(分点写:适用场景、操作步骤,需脚本则写“运行main.py脚本”,无需则写“直接参考文档使用”) + - ## 目录结构(用代码块 ``` 列出所有文件/目录路径) + + #### 2. /skills/scripts/main.py(Python脚本必含) + - 必含依赖导入(如 `import pandas as pd`,无依赖则不写) + - 必含入口函数 `def execute(params: dict) -> dict:`(参数为dict,返回dict结果) + - 函数内必含: + 1. 参数校验(判断必填键是否存在,缺失返回错误) + 2. 核心逻辑(匹配技能需求,如数据处理、文本分析) + 3. 结果返回(成功:`{"status": "success", "data": 结果}`;失败:`{"status": "fail", "error": 信息}`) + - 必含注释:函数说明、参数/返回值说明、示例调用(`if __name__ == "__main__":` 块) + - 禁止空函数、语法错误,确保复制后可直接运行 + + ### 五、YAML语法死规定(100%无解析错误) + 1. 缩进:统一2个空格(禁止Tab,禁止1/3/4空格,嵌套层级严格对齐) + - package 下子字段缩进2空格 + - structure 下目录/文件节点缩进4空格(package→structure→children,每层+2空格) + 2. 路径:全部为绝对路径,Unix风格 `/`(如 /skills/scripts/main.py),禁止 `\\` 或 `./` + 3. 字符串:特殊字符(:、#、空格)无需转义,直接书写 + 4. 数组:tags格式严格为 `tags: [标签1, 标签2]`(逗号后加空格,无多余逗号) + 5. content:多行内容用 `|` 开头,内容行首顶格,内部遵循对应格式缩进(Python用4空格) + + ### 六、错误规避红线(绝对不能触碰) + 1. 禁止顶层出现除 `package` 外的任何节点(如name、version不能直接在顶层) + 2. 禁止在YAML前后加任何多余文字(如“生成完毕”“---”分隔符) + 3. 禁止生成非Python脚本(仅支持main.py,无.js/.sh文件) + 4. 禁止字段缺失(如package必含structure,file必含content) + 5. 禁止目录结构错误(脚本必须在/skills/scripts下) + + 最终输出仅纯YAML,直接可复制存储、解析使用,无任何冗余或格式问题! + """; + + String userContent = """ + 基于以下信息生成技能包YAML,严格遵守system指令(仅Python脚本,无其他语言): + 1. Skill名称:%s + 2. Skill描述:%s + 3. Skill标签:%s(中文,直接使用,不修改) + 4. Skill摘要/需求:%s(用于完善skills.md的“使用说明”章节) + """.formatted( + request.getName(), + request.getDescription(), + tagsList.toString(), + request.getRequirement() + ); SkillRequest skillRequest = new SkillRequest(true, deepSeekConfig.getChat().getModel(),systemContent,userContent,deepSeekConfig.getChat().getTemperature(), 8192,"text"); String deepseekResponse = ""; try { diff --git a/src/main/java/com/kexue/skills/service/impl/SysUserServiceImpl.java b/src/main/java/com/kexue/skills/service/impl/SysUserServiceImpl.java index 5f0d230..e061f84 100644 --- a/src/main/java/com/kexue/skills/service/impl/SysUserServiceImpl.java +++ b/src/main/java/com/kexue/skills/service/impl/SysUserServiceImpl.java @@ -1,6 +1,5 @@ package com.kexue.skills.service.impl; -import cn.hutool.core.bean.BeanUtil; import com.alicp.jetcache.anno.CacheInvalidate; import com.alicp.jetcache.anno.CachePenetrationProtect; import com.alicp.jetcache.anno.Cached; @@ -11,29 +10,34 @@ import com.kexue.skills.common.CacheManager; import com.kexue.skills.common.Const; import com.kexue.skills.common.LoginUserCacheUtil; import com.kexue.skills.config.CaptchaConfig; -import com.kexue.skills.entity.*; +import com.kexue.skills.entity.Account; +import com.kexue.skills.entity.ContentPurchase; +import com.kexue.skills.entity.SysUser; +import com.kexue.skills.entity.SysUserRole; import com.kexue.skills.entity.dto.ContentPurchaseDto; import com.kexue.skills.entity.dto.SessionDto; import com.kexue.skills.entity.dto.SysUserDto; import com.kexue.skills.entity.request.*; -import com.kexue.skills.entity.request.SysUserUpdateDto; -import com.kexue.skills.mapper.*; +import com.kexue.skills.exception.BizException; +import com.kexue.skills.mapper.AccountMapper; +import com.kexue.skills.mapper.ContentPurchaseMapper; +import com.kexue.skills.mapper.CmsContentLikeMapper; +import com.kexue.skills.mapper.CmsContentViewMapper; +import com.kexue.skills.mapper.CmsContentMapper; +import com.kexue.skills.mapper.SysUserMapper; +import com.kexue.skills.mapper.SysUserRoleMapper; import com.kexue.skills.service.SysUserService; import com.kexue.skills.utils.MD5Util; import lombok.extern.slf4j.Slf4j; import org.dromara.sms4j.api.SmsBlend; import org.dromara.sms4j.core.factory.SmsFactory; -import org.redisson.api.RBucket; import org.redisson.api.RedissonClient; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.math.BigDecimal; import java.security.NoSuchAlgorithmException; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Objects; -import java.util.Random; +import java.util.*; /** * (SysUser)表服务实现类 @@ -48,17 +52,13 @@ public class SysUserServiceImpl implements SysUserService { @Resource private SysUserMapper sysUserMapper; - - @Resource private RedissonClient redissonClient; @Resource private CaptchaConfig captchaConfig; - @Resource - private PointsAccountMapper pointsAccountMapper; - + @Resource private AccountMapper accountMapper; @@ -157,16 +157,6 @@ public class SysUserServiceImpl implements SysUserService { account.setDeleteFlag(Const.DELETE_FLAG_NO); // 初始未删除 accountMapper.insert(account); - // 初始化积分账户 - PointsAccount pointsAccount = new PointsAccount(); - pointsAccount.setUserId(sysUser.getUserId()); - pointsAccount.setUserName(sysUser.getUserName()); - pointsAccount.setTotalPoints(0); // 初始总积分为0 - pointsAccount.setAvailablePoints(0); // 初始可用积分为0 - pointsAccount.setFrozenPoints(0); // 初始冻结积分为0 - pointsAccount.setDeleteFlag(Const.DELETE_FLAG_NO); // 初始未删除 - pointsAccountMapper.insert(pointsAccount); - // 将返回的用户密码设置为null sysUser.setPwd(null); return sysUser; @@ -304,8 +294,13 @@ public class SysUserServiceImpl implements SysUserService { log.info("用户:{}的旧token已失效", sysUser.getUserName()); } - // 生成token - String token = generateToken(sysUser.getUserId()); + // 查询用户角色列表 + List roles = queryUserRoles(sysUser.getUserId()); + log.info("用户{}的角色列表:{}", sysUser.getUserName(), String.join(",", roles)); + + // 生成token并设置角色 + String token = generateToken(sysUser.getUserId(), roles); + log.info("设置后Sa-Token中的角色列表:{}", String.join(",", cn.dev33.satoken.stp.StpUtil.getRoleList())); // 构建登录用户信息 LoginUser loginUser = buildLoginUser(sysUser, token); @@ -402,10 +397,16 @@ public class SysUserServiceImpl implements SysUserService { * 生成token * * @param userId 用户ID + * @param roles 角色列表 * @return token */ - private String generateToken(Long userId) { + private String generateToken(Long userId, List roles) { + // 登录用户 cn.dev33.satoken.stp.StpUtil.login(userId); + // 添加角色 + if (!roles.isEmpty()) { + cn.dev33.satoken.stp.StpUtil.getRoleList().addAll(roles); + } return cn.dev33.satoken.stp.StpUtil.getTokenValue(); } @@ -432,8 +433,7 @@ public class SysUserServiceImpl implements SysUserService { // 查询并设置用户账户信息 loginUser.setAccount(queryUserAccount(sysUser.getUserId())); - // 查询并设置用户积分信息 - loginUser.setPointsAccount(queryUserPointsAccount(sysUser.getUserId())); + // 查询并设置用户最近点赞记录 loginUser.setFavorites(queryRecentFavorites(sysUser.getUserId())); @@ -456,24 +456,18 @@ public class SysUserServiceImpl implements SysUserService { /** * 查询用户角色列表 * - * @param userId 用户ID - * @return 角色列表 + * @param userId 用户 ID + * @return 角色编码列表 */ - private List queryUserRoles(Long userId) { - List roles = new java.util.ArrayList<>(); + @Override + public List queryUserRoles(Long userId) { try { - SysUserRole userRole = new SysUserRole(); - userRole.setUserId(userId); - List userRoles = sysUserRoleMapper.queryAll(userRole); - - for (SysUserRole ur : userRoles) { - roles.add("ROLE_" + ur.getRoleId()); - } + // 直接通过关联查询获取角色编码列表 + return sysUserRoleMapper.queryRoleCodesByUserId(userId); } catch (Exception e) { log.error("查询用户角色列表失败:{}", e.getMessage()); - roles = java.util.Collections.emptyList(); + return java.util.Collections.emptyList(); } - return roles; } /** @@ -509,16 +503,8 @@ public class SysUserServiceImpl implements SysUserService { return accountMapper.queryByUserId(userId); } - /** - * 查询用户积分信息 - * - * @param userId 用户ID - * @return 积分信息 - */ - private PointsAccount queryUserPointsAccount(Long userId) { - return pointsAccountMapper.queryByUserId(userId); - } - + + /** * 查询用户最近点赞记录 * @@ -626,34 +612,31 @@ public class SysUserServiceImpl implements SysUserService { private LoginUserCacheUtil loginUserCacheUtil; @Override - public boolean resetPassword(ResetPasswordDto resetPasswordDto) { - Assert.notNull(resetPasswordDto.getUserName(), "用户名或手机号不能位空"); - Assert.notNull(resetPasswordDto.getOldPassword(), "旧密码不能位空"); - Assert.notNull(resetPasswordDto.getNewPassword(), "新密码不能位空"); - - // 获取当前登录用户ID + public boolean resetPassword(ResetPwdDto resetPasswordDto) { + // 1. 检查登录状态 Long currentUserId = loginUserCacheUtil.getCurrentUserId(); Assert.notNull(currentUserId, "请先登录"); - // 查询用户(支持用户名或手机号) - SysUser sysUser = getUserByUsernameOrPhone(resetPasswordDto.getUserName()); + // 2. 检查参数 + Assert.notNull(resetPasswordDto.getUserId(), "用户ID不能为空"); + Assert.notNull(resetPasswordDto.getNewPassword(), "新密码不能为空"); - // 检查是否是用户自己修改密码,或者是管理员 - boolean isAdmin = Const.ADMIN_USER_LIST.contains(sysUser.getUserName().toLowerCase()); - boolean isSelf = currentUserId.equals(sysUser.getUserId()); - Assert.isTrue(isAdmin || isSelf, "只能修改自己的密码"); + // 3. 检查是否是管理员 + boolean isAdmin = isAdminUser(currentUserId); + if (!isAdmin) { + throw new BizException("只有管理员才能重置密码"); + } + + // 4. 查询用户(支持用户名或手机号) + SysUser sysUser = sysUserMapper.queryById(resetPasswordDto.getUserId()); try { - // 假设客户端已经对密码进行了一次MD5加密,服务端使用双重加密验证 - String oldEncryptedPwd = MD5Util.doubleEncrypt(resetPasswordDto.getOldPassword(), sysUser.getSalt()); - Assert.equals(oldEncryptedPwd, sysUser.getPwd(), "旧密码不正确"); - - // 对新密码进行双重加密 + // 5. 参照密码创建时候的逻辑,对新密码进行双重加密 String newEncryptedPwd = MD5Util.doubleEncrypt(resetPasswordDto.getNewPassword(), sysUser.getSalt()); sysUser.setPwd(newEncryptedPwd); sysUserMapper.update(sysUser); - // 清除旧的token + // 6. 清除旧的token CacheManager.removeTokenFromCache(sysUser.getUserName()); return true; @@ -662,27 +645,29 @@ public class SysUserServiceImpl implements SysUserService { } } - @Override - public boolean resetPasswordByAdmin(ResetPasswordDto resetPasswordDto) { - Assert.notNull(resetPasswordDto.getUserName(), "用户名或手机号不能位空"); - Assert.notNull(resetPasswordDto.getNewPassword(), "新密码不能位空"); - - // 查询用户(支持用户名或手机号) - SysUser sysUser = getUserByUsernameOrPhone(resetPasswordDto.getUserName()); - - try { - // 假设客户端已经对密码进行了一次MD5加密,服务端使用双重加密验证 - String newEncryptedPwd = MD5Util.doubleEncrypt(resetPasswordDto.getNewPassword(), sysUser.getSalt()); - sysUser.setPwd(newEncryptedPwd); - sysUserMapper.update(sysUser); - - // 清除旧的token - CacheManager.removeTokenFromCache(sysUser.getUserName()); - + /** + * 检查用户是否是管理员 + * + * @param userId 用户 ID + * @return 是否是管理员 + */ + private boolean isAdminUser(Long userId) { + // 先检查是否在管理员用户列表中 + SysUser currentUser = sysUserMapper.queryById(userId); + if (currentUser != null && Const.ADMIN_USER_LIST.contains(currentUser.getUserName().toLowerCase())) { return true; - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException(e); } + + // 检查用户是否有管理员角色(通过角色编码判断) + List roles = queryUserRoles(userId); + // 只要有一个角色编码是 "admin",就是管理员 + for (String role : roles) { + if ("admin".equalsIgnoreCase(role)) { + return true; + } + } + + return false; } @Override @@ -870,10 +855,13 @@ public class SysUserServiceImpl implements SysUserService { log.info("用户:{}的旧token已失效", sysUser.getUserName()); } - // 使用Sa-Token登录,生成token - cn.dev33.satoken.stp.StpUtil.login(sysUser.getUserId()); - // 获取生成的token - String token = cn.dev33.satoken.stp.StpUtil.getTokenValue(); + // 查询用户角色列表 + List roles = queryUserRoles(sysUser.getUserId()); + log.info("用户{}的角色列表:{}", sysUser.getUserName(), String.join(",", roles)); + + // 生成token并设置角色 + String token = generateToken(sysUser.getUserId(), roles); + log.info("设置后Sa-Token中的角色列表:{}", String.join(",", cn.dev33.satoken.stp.StpUtil.getRoleList())); // 创建LoginUser对象 LoginUser loginUser = new LoginUser(); @@ -881,23 +869,6 @@ public class SysUserServiceImpl implements SysUserService { // 设置用户基本信息 sysUser.setPwd(null); loginUser.setUserInfo(sysUser); - - // 查询用户角色列表 - List roles = new java.util.ArrayList<>(); - try { - // 创建查询条件,查询用户的角色关联记录 - SysUserRole userRole = new SysUserRole(); - userRole.setUserId(sysUser.getUserId()); - List userRoles = sysUserRoleMapper.queryAll(userRole); - - // 遍历角色关联记录,获取角色名称 - for (SysUserRole ur : userRoles) { - roles.add("ROLE_" + ur.getRoleId()); - } - } catch (Exception e) { - log.error("查询用户角色列表失败:{}", e.getMessage()); - roles = java.util.Collections.emptyList(); - } loginUser.setRoles(roles); // 查询用户已购买的内容ID列表 @@ -922,10 +893,6 @@ public class SysUserServiceImpl implements SysUserService { Account account = accountMapper.queryByUserId(sysUser.getUserId()); loginUser.setAccount(account); - // 查询用户积分信息 - PointsAccount pointsAccount = pointsAccountMapper.queryByUserId(sysUser.getUserId()); - loginUser.setPointsAccount(pointsAccount); - // 查询并设置用户最近点赞记录 loginUser.setFavorites(queryRecentFavorites(sysUser.getUserId())); @@ -999,16 +966,6 @@ public class SysUserServiceImpl implements SysUserService { account.setDeleteFlag(Const.DELETE_FLAG_NO); accountMapper.insert(account); - // 初始化积分账户 - PointsAccount pointsAccount = new PointsAccount(); - pointsAccount.setUserId(sysUser.getUserId()); - pointsAccount.setUserName(sysUser.getUserName()); - pointsAccount.setTotalPoints(0); - pointsAccount.setAvailablePoints(0); - pointsAccount.setFrozenPoints(0); - pointsAccount.setDeleteFlag(Const.DELETE_FLAG_NO); - pointsAccountMapper.insert(pointsAccount); - return sysUser; } diff --git a/src/main/java/com/kexue/skills/service/impl/WithdrawalRecordServiceImpl.java b/src/main/java/com/kexue/skills/service/impl/WithdrawalRecordServiceImpl.java new file mode 100644 index 0000000..313b92c --- /dev/null +++ b/src/main/java/com/kexue/skills/service/impl/WithdrawalRecordServiceImpl.java @@ -0,0 +1,288 @@ +package com.kexue.skills.service.impl; + +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import com.kexue.skills.entity.Account; +import com.kexue.skills.entity.WithdrawalRecord; +import com.kexue.skills.entity.dto.WithdrawalRecordDto; +import com.kexue.skills.common.Assert; +import com.kexue.skills.exception.BizException; +import com.kexue.skills.mapper.WithdrawalRecordMapper; +import com.kexue.skills.service.AccountService; +import com.kexue.skills.service.WithdrawalRecordService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.math.BigDecimal; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.HashMap; +import java.util.UUID; + +/** + * 提现记录Service实现类 + * + * @author 王志维 + * @since 2026-03-25 + */ +@Service("withdrawalRecordService") +@Transactional(rollbackFor = Exception.class) +public class WithdrawalRecordServiceImpl implements WithdrawalRecordService { + @Resource + private WithdrawalRecordMapper withdrawalRecordMapper; + + @Resource + private AccountService accountService; + + /** + * 提现手续费比例 + */ + private static final BigDecimal FEE_RATE = new BigDecimal("0.02"); + + /** + * 分页查询 + * + * @param queryDto 筛选条件 + * @return 查询结果 + */ + @Override + public PageInfo getPageList(WithdrawalRecordDto queryDto) { + PageHelper.startPage(queryDto.getPageNum(), queryDto.getPageSize()); + List list = this.withdrawalRecordMapper.getPageList(queryDto); + return new PageInfo<>(list); + } + + /** + * 查询列表 + * + * @param queryDto 筛选条件 + * @return 查询结果 + */ + @Override + public List getList(WithdrawalRecordDto queryDto) { + return this.withdrawalRecordMapper.getList(queryDto); + } + + /** + * 通过主键查询单条数据 + * + * @param recordId 主键 + * @return 实例对象 + */ + @Override + public WithdrawalRecord queryById(Long recordId) { + return this.withdrawalRecordMapper.queryById(recordId); + } + + /** + * 通过用户ID查询提现记录 + * + * @param userId 用户ID + * @return 实例对象 + */ + @Override + public List queryByUserId(Long userId) { + return this.withdrawalRecordMapper.queryByUserId(userId); + } + + /** + * 通过提现单号查询提现记录 + * + * @param withdrawalNo 提现单号 + * @return 实例对象 + */ + @Override + public WithdrawalRecord queryByWithdrawalNo(String withdrawalNo) { + return this.withdrawalRecordMapper.queryByWithdrawalNo(withdrawalNo); + } + + /** + * 新增数据 + * + * @param withdrawalRecord 实例对象 + * @return 实例对象 + */ + @Override + public WithdrawalRecord insert(WithdrawalRecord withdrawalRecord) { + // 设置创建时间和更新时间 + Date now = new Date(); + withdrawalRecord.setCreateTime(now); + withdrawalRecord.setUpdateTime(now); + // 设置默认值 + withdrawalRecord.setDeleteFlag(0); + // 保存数据 + this.withdrawalRecordMapper.insert(withdrawalRecord); + return withdrawalRecord; + } + + /** + * 更新数据 + * + * @param withdrawalRecord 实例对象 + * @return 实例对象 + */ + @Override + public WithdrawalRecord update(WithdrawalRecord withdrawalRecord) { + // 设置更新时间 + withdrawalRecord.setUpdateTime(new Date()); + // 更新数据 + this.withdrawalRecordMapper.update(withdrawalRecord); + return this.queryById(withdrawalRecord.getRecordId()); + } + + /** + * 更新提现状态 + * + * @param recordId 记录ID + * @param status 状态 + * @return 影响行数 + */ + @Override + public int updateStatus(Long recordId, Integer status) { + Map params = new HashMap<>(); + params.put("recordId", recordId); + params.put("status", status); + return this.withdrawalRecordMapper.updateStatus(params); + } + + /** + * 通过主键逻辑删除 + * + * @param recordId 主键 + * @param updateBy 更新人 + * @return 影响行数 + */ + @Override + public int logicDeleteById(Long recordId, String updateBy) { + Map params = new HashMap<>(); + params.put("recordId", recordId); + return this.withdrawalRecordMapper.logicDeleteById(params); + } + + /** + * 通过主键物理删除 + * + * @param recordId 主键 + * @return 影响行数 + */ + @Override + public int deleteById(Long recordId) { + return this.withdrawalRecordMapper.deleteById(recordId); + } + + /** + * 提交提现申请 + * + * @param userId 用户ID + * @param amount 提现金额 + * @param bankName 银行名称 + * @param bankAccount 银行账号 + * @param bankCardholder 持卡人姓名 + * @param remark 备注 + * @return 提现记录 + */ + @Override + public WithdrawalRecord submitWithdrawal(Long userId, BigDecimal amount, String bankName, String bankAccount, String bankCardholder, String remark) { + // 1. 查询账户信息 + Account account = accountService.queryByUserId(userId); + Assert.notNull(account, "账户不存在"); + + // 2. 检查可提现余额是否足够 + Assert.isTrue(account.getWithdrawableBalance().compareTo(amount) >= 0, "可提现余额不足"); + + // 3. 计算手续费和实际到账金额 + BigDecimal feeAmount = amount.multiply(FEE_RATE); + BigDecimal actualAmount = amount.subtract(feeAmount); + + // 4. 生成提现单号 + String withdrawalNo = generateWithdrawalNo(); + + // 5. 创建提现记录 + WithdrawalRecord record = new WithdrawalRecord(); + record.setUserId(userId); + record.setUserName(account.getUserName()); + record.setWithdrawalAmount(amount); + record.setFeeAmount(feeAmount); + record.setActualAmount(actualAmount); + record.setStatus(1); // 1.待处理 + record.setWithdrawalNo(withdrawalNo); + record.setBankName(bankName); + record.setBankAccount(bankAccount); + record.setBankCardholder(bankCardholder); + record.setRemark(remark); + record.setCreateTime(new Date()); + record.setUpdateTime(new Date()); + record.setDeleteFlag(0); + this.withdrawalRecordMapper.insert(record); + + // 6. 冻结可提现余额 + account.setWithdrawableBalance(account.getWithdrawableBalance().subtract(amount)); + account.setFrozenAmount(account.getFrozenAmount().add(amount)); + account.setUpdateTime(new Date()); + accountService.update(account); + + return record; + } + + /** + * 处理提现 + * + * @param recordId 记录ID + * @param status 状态 + * @param remark 备注 + * @return 影响行数 + */ + @Override + public int processWithdrawal(Long recordId, Integer status, String remark) { + // 1. 查询提现记录 + WithdrawalRecord record = this.queryById(recordId); + Assert.notNull(record, "提现记录不存在"); + + // 2. 检查状态是否为待处理 + Assert.isTrue(record.getStatus() == 1, "提现记录状态不正确"); + + // 3. 更新提现状态 + Map params = new HashMap<>(); + params.put("recordId", recordId); + params.put("status", status); + int result = this.withdrawalRecordMapper.updateStatus(params); + + // 4. 如果提现成功,减少可提现余额,增加冻结金额 + if (status == 3) { // 3.成功 + Account account = accountService.queryByUserId(record.getUserId()); + if (account != null) { + // 减少冻结金额 + account.setFrozenAmount(account.getFrozenAmount().subtract(record.getWithdrawalAmount())); + // 减少总余额 + account.setBalance(account.getBalance().subtract(record.getWithdrawalAmount())); + account.setUpdateTime(new Date()); + accountService.update(account); + } + } else if (status == 4) { // 4.失败 + // 解冻金额 + Account account = accountService.queryByUserId(record.getUserId()); + if (account != null) { + account.setFrozenAmount(account.getFrozenAmount().subtract(record.getWithdrawalAmount())); + account.setWithdrawableBalance(account.getWithdrawableBalance().add(record.getWithdrawalAmount())); + account.setUpdateTime(new Date()); + accountService.update(account); + } + } + + return result; + } + + /** + * 生成提现单号 + * + * @return 提现单号 + */ + private String generateWithdrawalNo() { + // 提现单号生成规则:W + 时间戳 + 6位随机数 + String timestamp = String.valueOf(System.currentTimeMillis()); + String random = UUID.randomUUID().toString().substring(0, 6).replaceAll("-", ""); + return "W" + timestamp + random; + } +} \ No newline at end of file diff --git a/src/main/resources/apiclient_key.pem b/src/main/resources/apiclient_key.pem new file mode 100644 index 0000000..1b7c534 --- /dev/null +++ b/src/main/resources/apiclient_key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDHxqhqf/A5f+Ya +Qpbu9/vVwlvbMQULdcpzBXgDAQ6WyEz3/0kYRW8Kg3iAkQyjCH3nPiaC5XwWtItu +CmNYz8gon3N5xLS0y0usD+aQWGMFplm4cj/UgwwhZqiapTNPncKhVFsaLFkDxDlJ +vd58H9LD3TnJt1wlNQ/gUSfHx0BtKNj0c3tV3sa7lY4gTvVyCjK1Okfm9xYzFJmL +dnBQUOUNnMwpsUV7BI6yFZZWAz/n+BXK84FLRrLxg5ErZ9d/qV8VdIbHD1+pVFHt +g6HQcV7+DEPyffOeF/4QWUAGGUDnii/emcwARn9sDuMo3pihDJ9uhh/GmZDchGos +MvfGbe5vAgMBAAECggEAKJESie3I3iQ2mYaEF6qLnPCGro/ZsmYM4iZuJE4GpF+w +IXvZX/BZiA0CXzkVE9YZmudn8pSfCg1HcuTxH4ux3W3jiQqEl2Hgz+O6sf0Avpj1 +BxtMEt85i17JRf3d2YDzkMcMaNgwiy9BYtQHZbUgm11E6s0tDPEldCuGpG61inSx +FX+LPonQ60IicBoTacZqcSFpEDOdW45anfq3xmwVE3lbCvbVeOGkakp/6Tf0yzx1 +E89b3r2uoi3JIjtGYZptX+HboFwALUytAnl8W42GJltLBVQPWTonOFJ2bbAjjYgu +b+fEshrqzYa9r97tNSJ5Yn2KkDluPBfebW5BmiizQQKBgQDpbe7uwk0TMECBjHeQ +iFwwyOntEuFHP6+cPyX1b/xglHlIEljuWS+upCmmnymLUqc0n0HFXEH2T0UYhmVb +jP1gOikiU3+9aE8dD5IExPj2iS5n5QcVQz1dazUNnGGJFWezgQ/WBtluiXyrGCJB +5elHm+pLdUsNuYNkMSdJhu+J9wKBgQDbF7QCucp/ggo5jEpwJlrmxRL8idGuV24a +r2U4wfBrjRkUb6Spn7uI89uU2KHhdVv0K9b7c2m9owFjo08cXSZcEiwB/pwqbHqB +SneFtcsD95IKhCIPk2FtvGx5M1gggzFp+TxPgO+MmyKpgsIWyZwtVHAG1ljGltgx +gdebT69hSQKBgQDVlUYC5nvZa9QJ9TnNYEdiR+NqjVTdeVM4VrtnqHC2+gNCw67l +X2t2kzSdBZLgrN7bEkD+0Vx1f7CMMSR6sTWBH5ZVlysRZmjFAWMsxAG8qmZwn6ls +dhqm6Johew3vfUtmfle8EIZQUrJkZm+p9jEN2YZ2RrtGspCbUzJMX4+7nwKBgQCh +YN8+Fr3al92SMAzweMACNW1byORC57F5RHJpkSjW/7JWhDmkm7yWDxFRnRP4LurR +eq06v/NGNNgkHTl7af2EWfpCadl7wjWmIETTn2lvfZ770gIIuQVNwDmiOLiUEi6G +oYfUA+PvDKJGe8Mc59n65bQyxRXVCW0rYjl+8/35yQKBgDFiaVey4oLTFAGwCFSu +FDccbNdhSXnljgmU03uUf/5F/BMWAfHxjIg5vmcKq+ULi/sUGqnZmR5R4+FsACcm +xRnU2YUbfYg9PX8CcyCrks5IP+ut07Pu7WxT0KxI01cHfIyO3Bx0AZpx1ceMwJOX +i1aeYnJYFT1jitBgYWIH2Lx5 +-----END PRIVATE KEY----- diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index b856945..fb61799 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -105,14 +105,12 @@ web: upload: path: /kexue/agentSkills/upload/ -# 支付配置 - payment: # 微信支付配置 wechat: appId: wx7d13d99de5be3bfa mchId: 1673321732 - mchKey: UDuZXDcmy5Eb6o0nTNZhu6ek4DDh4K8B + mchKey: e10adc3949ba59abbe56e057f20f883e mchSerialNo: 5EFC47D3AA59BFD1AAE548F96B5E19E1C60F067D privateKeyPath: apiclient_key.pem domain: https://api.mch.weixin.qq.com diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 251e350..0dcdb2a 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -107,19 +107,19 @@ payment: wechat: appId: wx7d13d99de5be3bfa mchId: 1673321732 - mchKey: UDuZXDcmy5Eb6o0nTNZhu6ek4DDh4K8B + mchKey: e10adc3949ba59abbe56e057f20f883e mchSerialNo: 5EFC47D3AA59BFD1AAE548F96B5E19E1C60F067D privateKeyPath: apiclient_key.pem domain: https://api.mch.weixin.qq.com - notifyUrl: http://43.248.97.19:19000/pay/wx/notify - returnUrl: http://43.248.97.19:19000/pay/success + notifyUrl: https://skills.xueai.art/api/pay/wx/notify + returnUrl: https://skills.xueai.art/pay/success # 支付宝支付配置 alipay: appId: 2021004138642603 privateKey: "MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCBzqh/sBfx8UsiKIzSaGm3QwMape+PEmHaFYsP0XxRLyRe5d3H1r4JBvE/GBCDarXkZMvJ3PKeUpO61i7LZtNBN7M6XbMvs/eiaipUmWCNF4IK+ilhOqr8GNryjb6tBnO0uzd1c7UmAvxF+MKkxti0qfscP+Tr6pkuF14DofOyITY56x0y36DmEc85rr213llQ/bSX5nVPHWfddtrqB+TNFvrnAN10NxnUurDK+wd7fKq3TgMjYNDINPGxFYbYezeIGvYj6E34mFS3b3wyxTcB5DeL4GV1FAWqJ0Yk+3hHT3OdmK/UnNEr55w+r3liQBCDRBoE4fldbG5CtW2/ER0lAgMBAAECggEAC2kgNMFFCZaddS49Ws2k5WA1qKUHjvsdsO8N32EZ3YUYXGM2gLem0uJSWKqD4RmDTcVyiJcsmLBHnjfvux+Z2HTOA4ZzFvFqBlPwzqkA7MYxP0fIVWyz1R9WN9Yv+cPEbhG7CU8XkHTYuknoylVUfWUn1s7jD736oyuYrxcQdgsHOHpLHngvsELLa1pv2EURohvr4p+zirMjFGuz3BVaGgVpWegn0nJ/8n1y0ZTM04Mvm/zXpGQQxfuUxrK2owQMFViY7BrRQXlPVeUM/IPAx7cvxLkR5hl9UgAl+nH6FCsm5osEvUln6VLhGTmNFBLCN9piX6sZaPEKZKRBCz0GQQKBgQDD4Dq+dGQAoCmnXdkzI6DHGXOePa+sGnj1Y55dRcrPWPKtCPeEnIPxhVCJ2+cqYK49K6youhLYzs3h8y/M4lh0JQLKP4zTSyZnARmdYHW/SyTu9BcHCdiPwZBrmn7bCWotGf3r5QRIJT6ilZEj8cLnF+9+gd0YyLyRE7Rmihq2UQKBgQCpps7qghRv3JIB0Hb4nBCbAeyPMjHj+7BSUsui2Dhdg3MeTk48a7RLl+r139pMzgTm5Pj0VG6qslUeqP2HlQ46o0Z2bPeohfXH4zMJi2amh4MvAFp4t8eNCc6faeqpJPTTQj3hS4drFnHEHBeFfgFCXZKhjYeP7SP2WVLOQvUAlQKBgFIq6fmjEaBBj7ep4sdVFsjuoFWtQthLcppd47z03hMFGSgFLu/uSFs0tYhfOyXH0M/QVmmhRO62Mh+qyE6GVNzD+dultQmd6Mok5/3gzQQmHaQvuMk3FCWZ6V96O+Temi+5S499TsKE/TVu0Kfnbv9KRykmiP0wmAmz3mV1YadBAoGABc5quIX5MxbmfF9pIvscamG3efMq1/WuRDMHOyyRSUoNb5UYgmLhSdEKPp4Jt6U5b7mYd6xIGVl/Jkx8WN6WHRWnfLggBcmH7u5sub/mpH5w0/P8JLONhds3Eieq210jb/ONcJ+II/chr6eSeoQkgOP498SDRj7Eg1LtTZfnEL0CgYBnMeXQWKUae+xES5NsEX7D0lwSCotb7attTHeE6vZOI77TzsURb4jOEAhYVsdJ8lm+J1Sv1Wjnr2yMxiRH2G+I1tUxGcI8OkRT26FxFdMl1RdbTf+gDM8IjMiu+Li+plIzb28VtF8q/Umgd+5LTlSBmM2yoiL8RKtmStjr5iIuhA==" publicKey: "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnakP04nUmsoFoveIvOhbLqkA1xQuYtvkrqq2AVvTsbtqpsEOTm9G095e2rBYLp89oDcf6L6BhtJPwdrhnA+qifUyVmACI9sprrsGeRYQgndK7y4c6spQcSnsnakSxlIp22j7pvBXNAZuqud2hQV+TOLKEUh1W3izTgMj/Ejoh3ZsCjgDRtTVgaytzSdHYrhNku+pIrl15/xVGJED99RYXkR8GHawxuK+vWVmxU0tiTCwTsqLz43v6TtCZ+/UfLL/luwp9B4ZvB+0qon82LILYr6oxs10kE2IAvryuDToAc1s/v/36jgt+7DXwqzfUDksHhVLHdJHChyc4ax5HmMsBwIDAQAB" - notifyUrl: http://43.248.97.19:19000/pay/ali-pay/trade/notify - returnUrl: https://shuziren.xueai.art/alipay-success + notifyUrl: https://skills.xueai.art/api/pay/alipay/trade/notify + returnUrl: https://skills.xueai.art/alipay-success signType: RSA2 charset: UTF-8 gatewayUrl: https://openapi.alipay.com/gateway.do \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 399a085..f9fbc2b 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -8,7 +8,7 @@ spring: pathmatch: matching-strategy: ANT_PATH_MATCHER static-path-pattern: /** - date-format: yyyy-MM-dd + date-format: yyyy-MM-dd HH:mm:ss favicon: enabled: true thymeleaf: diff --git a/src/main/resources/mapper/AccountTransactionMapper.xml b/src/main/resources/mapper/AccountTransactionMapper.xml index 667bbfb..465bf4d 100644 --- a/src/main/resources/mapper/AccountTransactionMapper.xml +++ b/src/main/resources/mapper/AccountTransactionMapper.xml @@ -16,6 +16,13 @@ + + + + + + + @@ -27,7 +34,8 @@ @@ -36,7 +44,8 @@ select transaction_id, user_id, user_name, transaction_type, amount, before_balance, after_balance, status, - transaction_no, pay_type, business_id, business_type, remark, create_time, update_time, create_by, update_by, delete_flag + transaction_no, pay_type, business_id, business_type, remark, is_expense, input_token, output_token, + total_tokens, model_name, question, income_type, create_time, update_time, create_by, update_by, delete_flag from account_transaction @@ -119,6 +129,13 @@ business_id, business_type, remark, + is_expense, + input_token, + output_token, + total_tokens, + model_name, + question, + income_type, create_time, update_time, create_by, @@ -138,6 +155,13 @@ #{businessId}, #{businessType}, #{remark}, + #{isExpense}, + #{inputToken}, + #{outputToken}, + #{totalTokens}, + #{modelName}, + #{question}, + #{incomeType}, #{createTime}, #{updateTime}, #{createBy}, @@ -162,6 +186,13 @@ business_id = #{businessId}, business_type = #{businessType}, remark = #{remark}, + is_expense = #{isExpense}, + input_token = #{inputToken}, + output_token = #{outputToken}, + total_tokens = #{totalTokens}, + model_name = #{modelName}, + question = #{question}, + income_type = #{incomeType}, update_time = #{updateTime}, update_by = #{updateBy}, delete_flag = #{deleteFlag}, @@ -183,4 +214,16 @@ delete from account_transaction where transaction_id = #{transactionId} + + + \ No newline at end of file diff --git a/src/main/resources/mapper/ModelPriceMapper.xml b/src/main/resources/mapper/ModelPriceMapper.xml new file mode 100644 index 0000000..73463e7 --- /dev/null +++ b/src/main/resources/mapper/ModelPriceMapper.xml @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + insert into model_price + + vendor, + model_name, + input_price, + output_price, + input_per_cent, + output_per_cent, + unit, + remark, + created_time, + updated_time, + + + #{vendor}, + #{modelName}, + #{inputPrice}, + #{outputPrice}, + #{inputPerCent}, + #{outputPerCent}, + #{unit}, + #{remark}, + #{createdTime}, + #{updatedTime}, + + + + + + update model_price + + vendor = #{vendor}, + model_name = #{modelName}, + input_price = #{inputPrice}, + output_price = #{outputPrice}, + input_per_cent = #{inputPerCent}, + output_per_cent = #{outputPerCent}, + unit = #{unit}, + remark = #{remark}, + created_time = #{createdTime}, + updated_time = #{updatedTime}, + + where id = #{id} + + + + + delete from model_price + where id = #{id} + + + diff --git a/src/main/resources/mapper/PointsAccountMapper.xml b/src/main/resources/mapper/PointsAccountMapper.xml deleted file mode 100644 index 3ba8171..0000000 --- a/src/main/resources/mapper/PointsAccountMapper.xml +++ /dev/null @@ -1,156 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - insert into points_account - - user_id, - user_name, - total_points, - available_points, - frozen_points, - create_time, - update_time, - create_by, - update_by, - delete_flag, - - - #{userId}, - #{userName}, - #{totalPoints}, - #{availablePoints}, - #{frozenPoints}, - #{createTime}, - #{updateTime}, - #{createBy}, - #{updateBy}, - #{deleteFlag}, - - - - - - update points_account - - user_id = #{userId}, - user_name = #{userName}, - total_points = #{totalPoints}, - available_points = #{availablePoints}, - frozen_points = #{frozenPoints}, - update_time = #{updateTime}, - update_by = #{updateBy}, - delete_flag = #{deleteFlag}, - - where account_id = #{accountId} - - - - - update points_account - - - total_points = total_points + #{points}, - available_points = available_points + #{points} - - - total_points = total_points - #{points}, - available_points = available_points - #{points} - - update_time = now() - - where user_id = #{userId} - - - - - update points_account - set delete_flag = 1, - update_by = #{updateBy}, - update_time = now() - where account_id = #{accountId} - - - - - delete from points_account - where account_id = #{accountId} - - \ No newline at end of file diff --git a/src/main/resources/mapper/PointsTransactionMapper.xml b/src/main/resources/mapper/PointsTransactionMapper.xml deleted file mode 100644 index 335e91d..0000000 --- a/src/main/resources/mapper/PointsTransactionMapper.xml +++ /dev/null @@ -1,186 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - insert into points_transaction - - user_id, - user_name, - transaction_type, - points, - before_points, - after_points, - status, - transaction_no, - pay_type, - business_id, - business_type, - remark, - create_time, - update_time, - create_by, - update_by, - delete_flag, - - - #{userId}, - #{userName}, - #{transactionType}, - #{points}, - #{beforePoints}, - #{afterPoints}, - #{status}, - #{transactionNo}, - #{payType}, - #{businessId}, - #{businessType}, - #{remark}, - #{createTime}, - #{updateTime}, - #{createBy}, - #{updateBy}, - #{deleteFlag}, - - - - - - update points_transaction - - user_id = #{userId}, - user_name = #{userName}, - transaction_type = #{transactionType}, - points = #{points}, - before_points = #{beforePoints}, - after_points = #{afterPoints}, - status = #{status}, - transaction_no = #{transactionNo}, - pay_type = #{payType}, - business_id = #{businessId}, - business_type = #{businessType}, - remark = #{remark}, - update_time = #{updateTime}, - update_by = #{updateBy}, - delete_flag = #{deleteFlag}, - - where transaction_id = #{transactionId} - - - - - update points_transaction - set delete_flag = 1, - update_by = #{updateBy}, - update_time = now() - where transaction_id = #{transactionId} - - - - - delete from points_transaction - where transaction_id = #{transactionId} - - \ No newline at end of file diff --git a/src/main/resources/mapper/SysUserMapper.xml b/src/main/resources/mapper/SysUserMapper.xml index 0010084..6ae2ba9 100644 --- a/src/main/resources/mapper/SysUserMapper.xml +++ b/src/main/resources/mapper/SysUserMapper.xml @@ -195,7 +195,7 @@ select user_id, user_name, pwd, real_name, tel, email, salt, remark, create_time, enable, delete_flag, session_id from sys_user - where user_name = #{userName} + where (user_name = #{userName} or tel = #{userName}) and delete_flag = 0 limit 1 diff --git a/src/main/resources/mapper/SysUserRoleMapper.xml b/src/main/resources/mapper/SysUserRoleMapper.xml index e5b417a..1abb346 100644 --- a/src/main/resources/mapper/SysUserRoleMapper.xml +++ b/src/main/resources/mapper/SysUserRoleMapper.xml @@ -94,4 +94,13 @@ delete from sys_user_role where role_id = #{roleId} + + + diff --git a/src/main/resources/mapper/WithdrawalRecordMapper.xml b/src/main/resources/mapper/WithdrawalRecordMapper.xml new file mode 100644 index 0000000..8e7bf9d --- /dev/null +++ b/src/main/resources/mapper/WithdrawalRecordMapper.xml @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + record_id, user_id, user_name, withdrawal_amount, fee_amount, actual_amount, status, withdrawal_no, bank_name, bank_account, bank_cardholder, remark, create_time, update_time, delete_flag, create_by, update_by + + + + + + + + + + + + + + INSERT INTO withdrawal_record ( + user_id, user_name, withdrawal_amount, fee_amount, actual_amount, status, withdrawal_no, bank_name, bank_account, bank_cardholder, remark, create_time, update_time, delete_flag, create_by, update_by + ) VALUES ( + #{userId}, #{userName}, #{withdrawalAmount}, #{feeAmount}, #{actualAmount}, #{status}, #{withdrawalNo}, #{bankName}, #{bankAccount}, #{bankCardholder}, #{remark}, #{createTime}, #{updateTime}, #{deleteFlag}, #{createBy}, #{updateBy} + ) + + + + UPDATE withdrawal_record + + user_id = #{userId}, + user_name = #{userName}, + withdrawal_amount = #{withdrawalAmount}, + fee_amount = #{feeAmount}, + actual_amount = #{actualAmount}, + status = #{status}, + withdrawal_no = #{withdrawalNo}, + bank_name = #{bankName}, + bank_account = #{bankAccount}, + bank_cardholder = #{bankCardholder}, + remark = #{remark}, + create_by = #{createBy}, + update_by = #{updateBy}, + update_time = #{updateTime} + + WHERE record_id = #{recordId} + + + + UPDATE withdrawal_record + SET status = #{status}, update_time = CURRENT_TIMESTAMP + WHERE record_id = #{recordId} + + + + UPDATE withdrawal_record + SET delete_flag = 1, update_time = CURRENT_TIMESTAMP + WHERE record_id = #{recordId} + + + + DELETE FROM withdrawal_record + WHERE record_id = #{recordId} + + + \ No newline at end of file diff --git a/src/main/resources/sql/add_account_columns.sql b/src/main/resources/sql/add_account_columns.sql new file mode 100644 index 0000000..6a4bc41 --- /dev/null +++ b/src/main/resources/sql/add_account_columns.sql @@ -0,0 +1,2 @@ +ALTER TABLE account ADD COLUMN withdrawable_balance DECIMAL(18,2) DEFAULT 0.00 COMMENT '可提现余额'; +ALTER TABLE account ADD COLUMN non_withdrawable_balance DECIMAL(18,2) DEFAULT 0.00 COMMENT '不可提现余额'; diff --git a/src/test/java/com/kexue/skills/WechatPayTest.java b/src/test/java/com/kexue/skills/WechatPayTest.java new file mode 100644 index 0000000..ac159d7 --- /dev/null +++ b/src/test/java/com/kexue/skills/WechatPayTest.java @@ -0,0 +1,336 @@ +package com.kexue.skills; + +import com.kexue.skills.entity.PaymentOrder; +import com.kexue.skills.service.PayService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +import jakarta.annotation.Resource; +import java.math.BigDecimal; +import java.util.Map; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * 微信支付接口自测类 + */ +@SpringBootTest +@ActiveProfiles("dev") // 使用开发环境配置 +class WechatPayTest { + + @Resource + private PayService payService; + + private PaymentOrder testOrder; + + @BeforeEach + void setUp() { + // 准备测试订单数据 + testOrder = new PaymentOrder(); + testOrder.setUserId(1L); + testOrder.setUserName("测试用户"); + testOrder.setProductName("测试商品-" + System.currentTimeMillis()); + testOrder.setProductDesc("这是一个测试商品"); + testOrder.setAmount(new BigDecimal("0.01")); // 测试金额 0.01 元 + testOrder.setBusinessId(1L); + testOrder.setBusinessType("purchase_content"); + } + + /** + * 测试 1:创建微信支付订单 - 正常场景 + */ + @Test + void testCreateWechatPay_Success() { + System.out.println("========== 测试 1:创建微信支付订单(正常场景) =========="); + + try { + // 生成唯一订单号 + String orderNo = "TEST_" + System.currentTimeMillis(); + testOrder.setOrderNo(orderNo); + + // 调用支付接口(传入测试 IP) + Map result = payService.createWechatPay(testOrder, "192.168.1.100"); + + // 验证返回结果 + assertNotNull(result, "返回结果不应为空"); + assertTrue(result.containsKey("code_url"), "应包含 code_url"); + assertTrue(result.containsKey("order_no"), "应包含 order_no"); + + System.out.println("✅ 测试通过!"); + System.out.println("订单号:" + result.get("order_no")); + System.out.println("二维码链接:" + result.get("code_url")); + + } catch (Exception e) { + System.err.println("❌ 测试失败:" + e.getMessage()); + e.printStackTrace(); + fail("创建微信支付订单失败:" + e.getMessage()); + } + } + + /** + * 测试 2:创建微信支付订单 - 金额为空 + */ + @Test + void testCreateWechatPay_NullAmount() { + System.out.println("\n========== 测试 2:金额为空(应抛出异常) =========="); + + testOrder.setOrderNo("TEST_NULL_AMOUNT_" + System.currentTimeMillis()); + testOrder.setAmount(null); // 设置金额为空 + + assertThrows(IllegalArgumentException.class, () -> { + payService.createWechatPay(testOrder, "192.168.1.100"); + }, "金额为空时应抛出 IllegalArgumentException"); + + System.out.println("✅ 测试通过:正确捕获了金额为空的异常"); + } + + /** + * 测试 3:创建微信支付订单 - 金额过小 + */ + @Test + void testCreateWechatPay_AmountTooSmall() { + System.out.println("\n========== 测试 3:金额过小(应抛出异常) =========="); + + testOrder.setOrderNo("TEST_SMALL_AMOUNT_" + System.currentTimeMillis()); + testOrder.setAmount(new BigDecimal("0.001")); // 小于 0.01 元 + + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + payService.createWechatPay(testOrder, "192.168.1.100"); + }); + + System.out.println("✅ 测试通过:捕获异常 - " + exception.getMessage()); + } + + /** + * 测试 4:创建微信支付订单 - 金额过大 + */ + @Test + void testCreateWechatPay_AmountTooLarge() { + System.out.println("\n========== 测试 4:金额过大(应抛出异常) =========="); + + testOrder.setOrderNo("TEST_LARGE_AMOUNT_" + System.currentTimeMillis()); + testOrder.setAmount(new BigDecimal("200000")); // 超过 10 万元 + + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + payService.createWechatPay(testOrder, "192.168.1.100"); + }); + + System.out.println("✅ 测试通过:捕获异常 - " + exception.getMessage()); + } + + /** + * 测试 5:创建微信支付订单 - 订单号为空 + */ + @Test + void testCreateWechatPay_NullOrderNo() { + System.out.println("\n========== 测试 5:订单号为空(应抛出异常) =========="); + + testOrder.setOrderNo(null); // 订单号为空 + + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + payService.createWechatPay(testOrder, "192.168.1.100"); + }); + + System.out.println("✅ 测试通过:捕获异常 - " + exception.getMessage()); + } + + /** + * 测试 6:创建微信支付订单 - 订单号格式不正确 + */ + @Test + void testCreateWechatPay_InvalidOrderNo() { + System.out.println("\n========== 测试 6:订单号格式不正确(应抛出异常) =========="); + + testOrder.setOrderNo("TEST@INVALID#ORDER"); // 包含特殊字符 + + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + payService.createWechatPay(testOrder, "192.168.1.100"); + }); + + System.out.println("✅ 测试通过:捕获异常 - " + exception.getMessage()); + } + + /** + * 测试 7:创建微信支付订单 - 订单号超长 + */ + @Test + void testCreateWechatPay_OrderNoTooLong() { + System.out.println("\n========== 测试 7:订单号超长(应抛出异常) =========="); + + testOrder.setOrderNo("TEST_" + UUID.randomUUID().toString().replace("-", "")); // 超过 32 位 + + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + payService.createWechatPay(testOrder, "192.168.1.100"); + }); + + System.out.println("✅ 测试通过:捕获异常 - " + exception.getMessage()); + } + + /** + * 测试 8:创建微信支付订单 - 商品名称为空 + */ + @Test + void testCreateWechatPay_NullProductName() { + System.out.println("\n========== 测试 8:商品名称为空(应抛出异常) =========="); + + testOrder.setOrderNo("TEST_NULL_PRODUCT_" + System.currentTimeMillis()); + testOrder.setProductName(null); // 商品名称为空 + + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + payService.createWechatPay(testOrder, "192.168.1.100"); + }); + + System.out.println("✅ 测试通过:捕获异常 - " + exception.getMessage()); + } + + /** + * 测试 9:创建微信支付订单 - IP 地址为 null(应使用默认值) + */ + @Test + void testCreateWechatPay_NullIpAddress() { + System.out.println("\n========== 测试 9:IP 地址为 null(应使用默认 127.0.0.1) =========="); + + testOrder.setOrderNo("TEST_NULL_IP_" + System.currentTimeMillis()); + + try { + Map result = payService.createWechatPay(testOrder, null); + assertNotNull(result, "即使 IP 为 null,也应返回结果"); + System.out.println("✅ 测试通过:IP 为 null 时使用了默认值"); + } catch (Exception e) { + fail("IP 为 null 时不应抛出异常:" + e.getMessage()); + } + } + + /** + * 测试 10:创建微信支付订单 - 长商品描述(应自动截断) + */ + @Test + void testCreateWechatPay_LongProductDescription() { + System.out.println("\n========== 测试 10:长商品描述(应自动截断) =========="); + + testOrder.setOrderNo("TEST_LONG_DESC_" + System.currentTimeMillis()); + + // 创建一个很长的商品描述(超过 128 字节) + StringBuilder longDesc = new StringBuilder(); + for (int i = 0; i < 100; i++) { + longDesc.append("这是非常长的商品描述测试"); + } + testOrder.setProductName(longDesc.toString()); + + try { + Map result = payService.createWechatPay(testOrder, "192.168.1.100"); + assertNotNull(result, "长描述应该也能成功创建订单"); + System.out.println("✅ 测试通过:长商品描述已自动处理"); + } catch (Exception e) { + fail("长商品描述不应导致失败:" + e.getMessage()); + } + } + + /** + * 测试 11:边界值测试 - 最小金额 + */ + @Test + void testCreateWechatPay_MinimumAmount() { + System.out.println("\n========== 测试 11:最小金额边界测试(0.01 元) =========="); + + testOrder.setOrderNo("TEST_MIN_AMOUNT_" + System.currentTimeMillis()); + testOrder.setAmount(new BigDecimal("0.01")); // 最小金额 + + try { + Map result = payService.createWechatPay(testOrder, "192.168.1.100"); + assertNotNull(result, "最小金额应该可以成功下单"); + System.out.println("✅ 测试通过:0.01 元可以正常下单"); + } catch (Exception e) { + fail("最小金额不应抛出异常:" + e.getMessage()); + } + } + + /** + * 测试 12:边界值测试 - 最大金额 + */ + @Test + void testCreateWechatPay_MaximumAmount() { + System.out.println("\n========== 测试 12:最大金额边界测试(100000 元) =========="); + + testOrder.setOrderNo("TEST_MAX_AMOUNT_" + System.currentTimeMillis()); + testOrder.setAmount(new BigDecimal("100000")); // 最大金额限制 + + try { + Map result = payService.createWechatPay(testOrder, "192.168.1.100"); + assertNotNull(result, "最大金额应该可以成功下单"); + System.out.println("✅ 测试通过:100000 元可以正常下单"); + } catch (Exception e) { + fail("最大金额不应抛出异常:" + e.getMessage()); + } + } + + /** + * 测试 13:真实场景模拟 - 购买内容 + */ + @Test + void testCreateWechatPay_PurchaseContent() { + System.out.println("\n========== 测试 13:真实场景 - 购买内容 =========="); + + PaymentOrder order = new PaymentOrder(); + order.setOrderNo("BUY_" + System.currentTimeMillis()); + order.setUserId(1L); + order.setUserName("张三"); + order.setAmount(new BigDecimal("9.99")); // 9.99 元 + order.setProductName("AI 技能高级教程"); + order.setProductDesc("这是一套完整的 AI 技能培训课程,包含从入门到精通的所有内容。"); + order.setBusinessId(100L); + order.setBusinessType("purchase_content"); + + try { + Map result = payService.createWechatPay(order, "10.0.0.1"); + assertNotNull(result, "购买内容场景应成功"); + System.out.println("✅ 测试通过:购买内容场景模拟成功"); + System.out.println("订单号:" + result.get("order_no")); + } catch (Exception e) { + fail("购买内容场景不应失败:" + e.getMessage()); + } + } + + /** + * 测试 14:真实场景模拟 - 账户充值 + */ + @Test + void testCreateWechatPay_Recharge() { + System.out.println("\n========== 测试 14:真实场景 - 账户充值 =========="); + + PaymentOrder order = new PaymentOrder(); + order.setOrderNo("RECHARGE_" + System.currentTimeMillis()); + order.setUserId(1L); + order.setUserName("李四"); + order.setAmount(new BigDecimal("100.00")); // 充值 100 元 + order.setProductName("账户充值"); + order.setProductDesc("用户账户余额充值"); + order.setBusinessId(1L); + order.setBusinessType("recharge"); + + try { + Map result = payService.createWechatPay(order, "172.16.0.100"); + assertNotNull(result, "充值场景应成功"); + System.out.println("✅ 测试通过:账户充值场景模拟成功"); + System.out.println("充值金额:¥" + order.getAmount()); + } catch (Exception e) { + fail("充值场景不应失败:" + e.getMessage()); + } + } + + /** + * 辅助方法:打印测试结果 + */ + private void printTestResult(String testName, boolean success, String message) { + System.out.println("==========================================="); + System.out.println("测试:" + testName); + System.out.println("状态:" + (success ? "✅ 通过" : "❌ 失败")); + if (!success) { + System.out.println("原因:" + message); + } + System.out.println("===========================================\n"); + } +} diff --git a/src/test/java/com/kexue/skills/WechatPayValidationTest.java b/src/test/java/com/kexue/skills/WechatPayValidationTest.java new file mode 100644 index 0000000..3649589 --- /dev/null +++ b/src/test/java/com/kexue/skills/WechatPayValidationTest.java @@ -0,0 +1,252 @@ +package com.kexue.skills; + +import com.kexue.skills.entity.PaymentOrder; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * 微信支付参数校验单元测试(不依赖 Spring 容器) + */ +@DisplayName("微信支付参数校验测试") +class WechatPayValidationTest { + + private PaymentOrder testOrder; + + @BeforeEach + void setUp() { + testOrder = new PaymentOrder(); + testOrder.setUserId(1L); + testOrder.setUserName("测试用户"); + testOrder.setProductName("测试商品"); + testOrder.setProductDesc("这是一个测试商品"); + testOrder.setAmount(new BigDecimal("0.01")); + testOrder.setBusinessId(1L); + testOrder.setBusinessType("purchase_content"); + } + + @Test + @DisplayName("订单号格式验证 - 正常情况") + void testOrderNo_Valid() { + // 有效的订单号 + assertTrue(isValidOrderNo("ORDER_20260331_001")); + assertTrue(isValidOrderNo("TEST_1234567890")); + assertTrue(isValidOrderNo("PAY_abc_123_XYZ")); + assertTrue(isValidOrderNo("a1b2c3d4e5f6")); + } + + @Test + @DisplayName("订单号格式验证 - 无效情况") + void testOrderNo_Invalid() { + // 包含特殊字符 + assertFalse(isValidOrderNo("ORDER@2026")); + assertFalse(isValidOrderNo("TEST#ORDER")); + assertFalse(isValidOrderNo("PAY$123")); + + // 包含中文 + assertFalse(isValidOrderNo("订单_123")); + + // 空字符串 + assertFalse(isValidOrderNo("")); + assertFalse(isValidOrderNo(" ")); + } + + @Test + @DisplayName("订单号长度验证") + void testOrderNo_Length() { + // 正好 32 位 + assertTrue(isValidOrderNo("12345678901234567890123456789012")); + + // 超过 32 位 + assertFalse(isValidOrderNo("123456789012345678901234567890123")); + + // 很长的 UUID(去掉横杠后通常超过 32 位) + String longUuid = "12345678-1234-1234-1234-123456789012".replace("-", ""); + assertFalse(isValidOrderNo(longUuid)); + } + + @Test + @DisplayName("金额验证 - 有效金额") + void testAmount_Valid() { + // 最小金额 + assertTrue(isValidAmount(new BigDecimal("0.01"))); + + // 正常金额 + assertTrue(isValidAmount(new BigDecimal("1.00"))); + assertTrue(isValidAmount(new BigDecimal("9.99"))); + assertTrue(isValidAmount(new BigDecimal("100.00"))); + + // 最大金额 + assertTrue(isValidAmount(new BigDecimal("100000.00"))); + } + + @Test + @DisplayName("金额验证 - 无效金额") + void testAmount_Invalid() { + // 金额为 null + assertFalse(isValidAmount(null)); + + // 金额过小 + assertFalse(isValidAmount(new BigDecimal("0.001"))); + assertFalse(isValidAmount(new BigDecimal("0.00"))); + + // 负数金额 + assertFalse(isValidAmount(new BigDecimal("-1.00"))); + + // 金额过大 + assertFalse(isValidAmount(new BigDecimal("100000.01"))); + assertFalse(isValidAmount(new BigDecimal("200000.00"))); + } + + @Test + @DisplayName("商品描述长度验证") + void testProductDescription_Length() { + // 短描述(正常) + StringBuilder shortDesc = new StringBuilder(); + for (int i = 0; i < 10; i++) { + shortDesc.append("测试"); + } + assertTrue(getByteLength(shortDesc.toString()) <= 128); + + // 长描述(应截断) + StringBuilder longDesc = new StringBuilder(); + for (int i = 0; i < 100; i++) { + longDesc.append("这是非常长的商品描述"); + } + + // 原始长度应该超过 128 字节 + assertTrue(getByteLength(longDesc.toString()) > 128); + + // 处理后应该不超过 128 字节 + String processed = limitBodyLength(longDesc.toString()); + assertTrue(getByteLength(processed) <= 128, "处理后的描述不应超过 128 字节"); + assertTrue(processed.endsWith("..."), "处理后的描述应以省略号结尾"); + } + + @Test + @DisplayName("IP 地址格式验证") + void testIpAddress_Format() { + // IPv4 地址 + assertTrue(isValidIpAddress("192.168.1.100")); + assertTrue(isValidIpAddress("10.0.0.1")); + assertTrue(isValidIpAddress("172.16.0.100")); + assertTrue(isValidIpAddress("127.0.0.1")); + + // 无效 IP + assertFalse(isValidIpAddress("256.256.256.256")); + assertFalse(isValidIpAddress("192.168.1")); + assertFalse(isValidIpAddress("invalid.ip")); + } + + @Test + @DisplayName("业务类型验证") + void testBusinessType_Valid() { + assertTrue(isValidBusinessType("recharge")); + assertTrue(isValidBusinessType("purchase_content")); + } + + @Test + @DisplayName("业务类型验证 - 无效") + void testBusinessType_Invalid() { + assertFalse(isValidBusinessType(null)); + assertFalse(isValidBusinessType("")); + assertFalse(isValidBusinessType("unknown_type")); + assertFalse(isValidBusinessType("RECHARGE")); // 区分大小写 + } + + // ===== 辅助验证方法 ===== + + /** + * 验证订单号格式 + */ + private boolean isValidOrderNo(String orderNo) { + if (orderNo == null || orderNo.trim().isEmpty()) { + return false; + } + if (orderNo.length() > 32) { + return false; + } + return orderNo.matches("^[a-zA-Z0-9_]+$"); + } + + /** + * 验证金额 + */ + private boolean isValidAmount(BigDecimal amount) { + if (amount == null) { + return false; + } + if (amount.compareTo(new BigDecimal("0.01")) < 0) { + return false; + } + if (amount.compareTo(new BigDecimal("100000")) > 0) { + return false; + } + return true; + } + + /** + * 获取字符串的字节长度(UTF-8 编码) + */ + private int getByteLength(String text) { + try { + return text.getBytes(java.nio.charset.StandardCharsets.UTF_8).length; + } catch (Exception e) { + return text.length(); + } + } + + /** + * 限制商品描述长度(模拟服务层逻辑) + */ + private String limitBodyLength(String body) { + if (body == null) { + return ""; + } + try { + byte[] bytes = body.getBytes(java.nio.charset.StandardCharsets.UTF_8); + if (bytes.length <= 128) { + return body; + } + String truncated = new String(bytes, 0, Math.min(128 - 3, bytes.length), java.nio.charset.StandardCharsets.UTF_8); + return truncated + "..."; + } catch (Exception e) { + return body.length() > 60 ? body.substring(0, 60) + "..." : body; + } + } + + /** + * 验证 IP 地址格式 + */ + private boolean isValidIpAddress(String ip) { + if (ip == null || ip.isEmpty()) { + return false; + } + String[] parts = ip.split("\\."); + if (parts.length != 4) { + return false; + } + try { + for (String part : parts) { + int num = Integer.parseInt(part); + if (num < 0 || num > 255) { + return false; + } + } + return true; + } catch (NumberFormatException e) { + return false; + } + } + + /** + * 验证业务类型 + */ + private boolean isValidBusinessType(String businessType) { + return "recharge".equals(businessType) || "purchase_content".equals(businessType); + } +}