feat(account): 扩展账户余额管理功能

- 新增可提现余额和不可提现余额字段,完善账户余额结构
- 添加充值接口支持微信和支付宝支付方式
- 实现token消费转换扣费功能,支持AI模型调用计费
- 增加管理员赠送金额接口,仅管理员可调用
- 完善交易记录查询功能,支持用户查看历史交易明细
- 集成模型价格服务,实现token费用自动计算
- 重构余额增加逻辑,区分可提现和不可提现金额
- 优化账户实体类初始化逻辑,确保余额字段正确设置
- 更新交易记录实体类,新增token相关和收支类型字段
- 修改支付配置,更新微信和支付宝回调地址为生产环境域名
This commit is contained in:
wangzhiwei 2026-04-01 11:52:33 +08:00
parent 3df611f809
commit 770f50302e
65 changed files with 4142 additions and 1533 deletions

603
PAYMENT_GUIDE.md Normal file
View File

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

View File

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

View File

@ -182,10 +182,6 @@
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</exclusion>
<exclusion>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</exclusion>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>log4j-over-slf4j</artifactId>
@ -194,10 +190,6 @@
<groupId>org.slf4j</groupId>
<artifactId>jul-to-slf4j</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
<exclusion>
<groupId>com.github.jsqlparser</groupId>
<artifactId>jsqlparser</artifactId>

View File

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

View File

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

View File

@ -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<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
// 打印响应状态码和响应体
System.out.println("HTTP 状态码: " + response.statusCode());
System.out.println("响应体: " + response.body());
return response.body();
}
}

View File

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

View File

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

View File

@ -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<Account> 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<String> recharge(
@Parameter(description = "用户ID") @RequestParam("userId") Long userId,
@Parameter(description = "充值金额") @RequestParam("amount") java.math.BigDecimal amount,
@Parameter(description = "支付方式1.微信 2.支付宝") @RequestParam("payType") Integer payType) {
// 这里可以根据需要实现充值逻辑
// 实际的支付流程需要通过支付接口完成
return CommonResult.success("充值请求已提交,请通过支付接口完成支付");
}
/**
* 减少账户余额token消费转换
*
* @param tokenConsumptionDto token消费转换参数
* @return 影响行数
*/
@Operation(summary = "减少账户余额token消费转换", description = "减少账户余额token消费转换")
@PostMapping("/reduceBalanceWithToken")
@RequireAuth
public CommonResult<Integer> 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<Integer> 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<List<com.kexue.skills.entity.AccountTransaction>> getTransactions(
@RequestBody java.util.Map<String, Object> params) {
Long userId = Long.valueOf(params.get("userId").toString());
return CommonResult.success(this.accountService.getTransactions(userId));
}
}

View File

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

View File

@ -33,24 +33,54 @@ public class PayController {
/**
* 创建微信支付订单
* @param order 支付订单信息
* @param request HTTP 请求用于获取用户 IP
* @return 微信支付参数
*/
@Operation(summary = "创建微信支付订单", description = "创建微信支付订单")
@PostMapping("/wx/create")
public CommonResult<Map<String, String>> createWechatPay(@RequestBody PaymentOrder order) {
public CommonResult<Map<String, String>> createWechatPay(@RequestBody PaymentOrder order, HttpServletRequest request) {
try {
// 设置支付类型为微信支付1
order.setPayType(1);
// 创建支付订单
PaymentOrder createdOrder = paymentOrderService.createPaymentOrder(order);
// 获取用户真实 IP
String ipAddress = getUserIpAddress(request);
// 生成微信支付参数
Map<String, String> payParams = payService.createWechatPay(createdOrder);
Map<String, String> payParams = payService.createWechatPay(createdOrder, ipAddress);
return CommonResult.success(payParams);
} catch (IllegalArgumentException e) {
logger.error("参数错误:{}", e.getMessage());
return CommonResult.failed(e.getMessage());
} catch (Exception e) {
logger.error("创建微信支付订单失败", e);
return CommonResult.failed("创建微信支付订单失败: " + 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;
}
/**
* 处理微信支付回调

View File

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

View File

@ -108,25 +108,8 @@ public class SysUserController {
@PostMapping("/resetPassword")
@Operation(summary = "管理员帮助用户重置密码", description = "管理员帮助用户重置密码")
@RequireAuth
public CommonResult<Boolean> resetPasswordByAdmin(@RequestBody ResetPasswordDto resetPasswordDto, HttpServletRequest request) {
// 从请求头中获取token
String token = request.getHeader("Authorization");
if (token == null || token.isEmpty()) {
throw new BizException("请先登录认证后操作");
}
// 从缓存中获取当前登录用户
String username = CacheManager.getUsernameFromToken(token);
if (username == null) {
throw new BizException("无效的token请重新登录");
}
SysUser adminUser = sysUserService.getByUsername(username);
if (adminUser == null) {
throw new BizException("管理员不存在");
}
boolean result = sysUserService.resetPasswordByAdmin(resetPasswordDto);
public CommonResult<Boolean> resetPassword(@RequestBody ResetPwdDto resetPasswordDto) {
boolean result = sysUserService.resetPassword(resetPasswordDto);
return CommonResult.success(result);
}

View File

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

View File

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

View File

@ -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;

View File

@ -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;
}

View File

@ -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 ="支付回调地址")

View File

@ -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;
}

View File

@ -1,78 +0,0 @@
package com.kexue.skills.entity;
import java.util.Date;
import java.io.Serializable;
import com.kexue.skills.entity.base.BaseEntity;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* (PointsTransaction)实体类
* 积分流水表记录用户的积分变动情况
*
* @author 王志维
* @since 2025-02-21 23:01:48
*/
@Data
public class PointsTransaction extends BaseEntity implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description ="主键ID")
private Long transactionId;
@Schema(description ="用户ID")
private Long userId;
@Schema(description ="用户名")
private String userName;
@Schema(description ="积分变动类型1.获取积分 2.消费积分 3.过期 4.其他")
private Integer transactionType;
@Schema(description ="变动积分")
private Integer points;
@Schema(description ="变动前积分")
private Integer beforePoints;
@Schema(description ="变动后积分")
private Integer afterPoints;
@Schema(description ="交易状态1.成功 2.失败 3.处理中")
private Integer status;
@Schema(description ="交易单号")
private String transactionNo;
@Schema(description ="支付方式1.微信 2.支付宝 3.余额支付")
private Integer payType;
@Schema(description ="关联业务ID")
private Long businessId;
@Schema(description ="业务类型")
private String businessType;
@Schema(description ="备注")
private String remark;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@Schema(description ="创建时间")
private Date createTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@Schema(description ="更新时间")
private Date updateTime;
@Schema(description ="创建人")
private String createBy;
@Schema(description ="更新人")
private String updateBy;
@Schema(description ="是否删除 0 未删除1已删除")
private Integer deleteFlag;
}

View File

@ -0,0 +1,75 @@
package com.kexue.skills.entity;
import java.math.BigDecimal;
import java.util.Date;
import java.io.Serializable;
import com.kexue.skills.entity.base.BaseEntity;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 提现记录实体类
*
* @author 王志维
* @since 2026-03-25
*/
@Data
public class WithdrawalRecord extends BaseEntity implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description ="主键ID")
private Long recordId;
@Schema(description ="用户ID")
private Long userId;
@Schema(description ="用户名")
private String userName;
@Schema(description ="提现金额")
private BigDecimal withdrawalAmount;
@Schema(description ="手续费")
private BigDecimal feeAmount;
@Schema(description ="实际到账金额")
private BigDecimal actualAmount;
@Schema(description ="提现状态1.待处理 2.处理中 3.成功 4.失败")
private Integer status;
@Schema(description ="提现单号")
private String withdrawalNo;
@Schema(description ="银行名称")
private String bankName;
@Schema(description ="银行账号")
private String bankAccount;
@Schema(description ="持卡人姓名")
private String bankCardholder;
@Schema(description ="备注")
private String remark;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@Schema(description ="创建时间")
private Date createTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@Schema(description ="更新时间")
private Date updateTime;
@Schema(description ="是否删除 0 未删除1已删除")
private Integer deleteFlag;
@Schema(description ="创建人")
private String createBy;
@Schema(description ="更新人")
private String updateBy;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -70,4 +70,12 @@ public interface AccountTransactionMapper {
* @return 影响行数
*/
int deleteById(Long transactionId);
/**
* 通过用户ID查询交易记录
*
* @param userId 用户ID
* @return 交易记录列表
*/
List<AccountTransaction> queryByUserId(Long userId);
}

View File

@ -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<ModelPrice> getPageList(ModelPriceDto queryDto);
/**
* 查询列表
*
* @param queryDto 筛选条件
* @return 查询结果
*/
List<ModelPrice> 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);
}

View File

@ -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<PointsAccount> getPageList(PointsAccountDto queryDto);
/**
* 查询列表
*
* @param queryDto 筛选条件
* @return 查询结果
*/
List<PointsAccount> 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);
}

View File

@ -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<PointsTransaction> getPageList(PointsTransactionDto queryDto);
/**
* 查询列表
*
* @param queryDto 筛选条件
* @return 查询结果
*/
List<PointsTransaction> 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);
}

View File

@ -84,4 +84,12 @@ public interface SysUserRoleMapper {
*/
int deleteById(Long roleId);
/**
* 通过用户 ID 查询角色编码列表关联查询 sys_role sys_user_role
*
* @param userId 用户 ID
* @return 角色编码列表
*/
List<String> queryRoleCodesByUserId(@Param("userId") Long userId);
}

View File

@ -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<WithdrawalRecord> queryByUserId(Long userId);
/**
* 通过提现单号查询提现记录
*
* @param withdrawalNo 提现单号
* @return 实例对象
*/
WithdrawalRecord queryByWithdrawalNo(String withdrawalNo);
/**
* 分页查询
*
* @param queryDto 筛选条件
* @return 查询结果
*/
List<WithdrawalRecord> getPageList(WithdrawalRecordDto queryDto);
/**
* 查询列表
*
* @param queryDto 筛选条件
* @return 查询结果
*/
List<WithdrawalRecord> getList(WithdrawalRecordDto queryDto);
/**
* 新增数据
*
* @param withdrawalRecord 实例对象
* @return 影响行数
*/
int insert(WithdrawalRecord withdrawalRecord);
/**
* 更新数据
*
* @param withdrawalRecord 实例对象
* @return 影响行数
*/
int update(WithdrawalRecord withdrawalRecord);
/**
* 更新提现状态
*
* @param params 参数
* @return 影响行数
*/
int updateStatus(Map<String, Object> params);
/**
* 通过主键逻辑删除
*
* @param params 参数
* @return 影响行数
*/
int logicDeleteById(Map<String, Object> params);
/**
* 通过主键物理删除
*
* @param recordId 主键
* @return 影响行数
*/
int deleteById(Long recordId);
}

View File

@ -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<com.kexue.skills.entity.AccountTransaction> getTransactions(Long userId);
}

View File

@ -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<ModelPrice> getPageList(ModelPriceDto queryDto);
/**
* 查询列表
*
* @param queryDto 筛选条件
* @return 查询结果
*/
List<ModelPrice> 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);
}

View File

@ -11,9 +11,10 @@ public interface PayService {
/**
* 创建微信支付订单
* @param order 支付订单信息
* @param ipAddress 用户真实 IP 地址 Controller 传入
* @return 微信支付参数
*/
Map<String, String> createWechatPay(PaymentOrder order);
Map<String, String> createWechatPay(PaymentOrder order, String ipAddress);
/**
* 处理微信支付回调

View File

@ -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<PointsAccount> getPageList(PointsAccountDto queryDto);
/**
* 查询列表
*
* @param queryDto 筛选条件
* @return 查询结果
*/
List<PointsAccount> 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);
}

View File

@ -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<String> queryUserRoles(Long userId);
}

View File

@ -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<WithdrawalRecord> getPageList(WithdrawalRecordDto queryDto);
/**
* 查询列表
*
* @param queryDto 筛选条件
* @return 查询结果
*/
List<WithdrawalRecord> getList(WithdrawalRecordDto queryDto);
/**
* 通过主键查询单条数据
*
* @param recordId 主键
* @return 实例对象
*/
WithdrawalRecord queryById(Long recordId);
/**
* 通过用户ID查询提现记录
*
* @param userId 用户ID
* @return 实例对象
*/
List<WithdrawalRecord> 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);
}

View File

@ -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<com.kexue.skills.entity.AccountTransaction> getTransactions(Long userId) {
return this.accountTransactionMapper.queryByUserId(userId);
}
}

View File

@ -775,13 +775,19 @@ public class CmsContentServiceImpl implements CmsContentService {
// 转换为Map<String, Object>
Map<String, Object> row = new HashMap<>();
for (int i = 0; i < headerList.size() && i < rowList.size(); i++) {
row.put(headerList.get(i), rowList.get(i));
}
if (row.isEmpty()) {
continue;
}
// 打印映射后的数据
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;
}
}

View File

@ -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());

View File

@ -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<ModelPrice> getPageList(ModelPriceDto queryDto) {
PageHelper.startPage(queryDto.getPageNum(), queryDto.getPageSize());
List<ModelPrice> list = this.modelPriceMapper.getPageList(queryDto);
return new PageInfo<>(list);
}
/**
* 查询列表
*
* @param queryDto 筛选条件
* @return 查询结果
*/
@Override
public List<ModelPrice> 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);
}
}

View File

@ -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("<xml>");
for (Map.Entry<String, String> entry : params.entrySet()) {
sb.append("<").append(entry.getKey()).append(">").append(entry.getValue()).append("</").append(entry.getKey()).append(">");
sb.append("<").append(entry.getKey()).append("><![CDATA[")
.append(entry.getValue())
.append("]]></").append(entry.getKey()).append(">");
}
sb.append("</xml>");
return sb.toString();
String xml = sb.toString();
logger.info("生成的XML: {}", xml);
return xml;
}
/**
@ -109,19 +140,44 @@ public class PayServiceImpl implements PayService {
private Map<String, String> xmlToMap(String xml) {
Map<String, String> map = new HashMap<>();
try {
// 简单的XML解析
String content = xml.replaceAll("<xml>", "").replaceAll("</xml>", "");
String[] elements = content.split("</");
for (String element : elements) {
if (element.isEmpty()) continue;
int idx = element.indexOf(">");
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>") || !xml.endsWith("</xml>")) {
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("</" + tagName + ">", startTagEnd);
if (endTagStart == -1) break;
String value = content.substring(startTagEnd + 1, endTagStart).trim();
// 处理CDATA标签
if (value.startsWith("<![CDATA[") && value.endsWith("]]>") ) {
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<String, String> createWechatPay(PaymentOrder order) {
public Map<String, String> createWechatPay(PaymentOrder order, String ipAddress) {
// 1. 参数校验
validatePaymentOrder(order);
try {
// 构建微信支付参数
Map<String, String> 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<String, String> 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<String, String> result = xmlToMap(xmlResult);
logger.info("解析后的微信支付响应: {}", result);
// 处理响应
if ("SUCCESS".equals(result.get("return_code")) && "SUCCESS".equals(result.get("result_code"))) {
Map<String, String> 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<String, String> buildWechatPayParams(PaymentOrder order, String ipAddress) {
Map<String, String> 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");
// 商品IDNATIVE支付必传
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("(<mch_key><!\\[CDATA\\[)(.*?)(]]></mch_key>)", "$1***$3")
.replaceAll("(<sign><!\\[CDATA\\[)(.*?)(]]></sign>)", "$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<String, String> processWechatResponse(String xmlResult, PaymentOrder order) {
try {
Map<String, String> 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<String, String> 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);
}
}

View File

@ -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; // 默认微信支付
}
}
/**
* 生成订单号

View File

@ -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<PointsAccount> getPageList(PointsAccountDto queryDto) {
PageHelper.startPage(queryDto.getPageNum(), queryDto.getPageSize());
List<PointsAccount> list = this.pointsAccountMapper.getPageList(queryDto);
return new PageInfo<>(list);
}
/**
* 查询列表
*
* @param queryDto 筛选条件
* @return 查询结果
*/
@Override
public List<PointsAccount> 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);
}
}

View File

@ -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、descriptioncontent 和 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` 节点必含子字段nameversiondescriptionauthorcreatedtagsstructure缺一不可
### 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: directorypath: /skillsformat: dirdescription: 技能包根目录
- /skills/skills.mdtype: filepath: /skills/skills.mdformat: markdowndescription: 技能说明文档
#### 2. Python脚本目录/文件判断逻辑
- 若用户提供的Skill描述Skill摘要**包含脚本代码执行运行处理计算** 等需执行逻辑的关键词
1. 必须新增 /skills/scripts 目录type: directorypath: /skills/scriptsformat: dirdescription: Python执行脚本目录
2. 必须在该目录下生成 /skills/scripts/main.pytype: fileformat: pythondescription: 技能核心Python脚本
- 若用户需求中**无任何执行逻辑相关描述**如纯文档纯说明类技能不生成 /skills/scripts 目录避免冗余
#### 3. 节点必含字段
- directory类型nametypepath绝对路径Unix风格 `/` /skills/scriptsformat固定"dir"descriptionchildren空目录写 `children: []`
- file类型nametypepath绝对路径format仅markdown/python两种descriptioncontent非空有实际可用内容
### 文件content内容规范Python脚本必实用
#### 1. /skills/skills.md结构固定
- # 技能名称不加多余符号居中可加空格但不强制
- ## 技能描述整合用户描述+摘要补充逻辑连贯性
- ## 标签格式`- 标签1\n- 标签2`对应tags数组内容
- ## 使用说明分点写适用场景操作步骤需脚本则写运行main.py脚本无需则写直接参考文档使用
- ## 目录结构用代码块 ``` 列出所有文件/目录路径
#### 2. /skills/scripts/main.pyPython脚本必含
- 必含依赖导入 `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空格packagestructurechildren每层+2空格
2. 路径全部为绝对路径Unix风格 `/` /skills/scripts/main.py禁止 `\\` `./`
3. 字符串特殊字符:#空格无需转义直接书写
4. 数组tags格式严格为 `tags: [标签1, 标签2]`逗号后加空格无多余逗号
5. content多行内容用 `|` 开头内容行首顶格内部遵循对应格式缩进Python用4空格
### 错误规避红线绝对不能触碰
1. 禁止顶层出现除 `package` 外的任何节点如nameversion不能直接在顶层
2. 禁止在YAML前后加任何多余文字生成完毕---分隔符
3. 禁止生成非Python脚本仅支持main.py.js/.sh文件
4. 禁止字段缺失如package必含structurefile必含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 {

View File

@ -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<String> 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<String> 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<String> queryUserRoles(Long userId) {
List<String> roles = new java.util.ArrayList<>();
@Override
public List<String> queryUserRoles(Long userId) {
try {
SysUserRole userRole = new SysUserRole();
userRole.setUserId(userId);
List<SysUserRole> 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<String> 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<String> 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<String> roles = new java.util.ArrayList<>();
try {
// 创建查询条件查询用户的角色关联记录
SysUserRole userRole = new SysUserRole();
userRole.setUserId(sysUser.getUserId());
List<SysUserRole> 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;
}

View File

@ -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<WithdrawalRecord> getPageList(WithdrawalRecordDto queryDto) {
PageHelper.startPage(queryDto.getPageNum(), queryDto.getPageSize());
List<WithdrawalRecord> list = this.withdrawalRecordMapper.getPageList(queryDto);
return new PageInfo<>(list);
}
/**
* 查询列表
*
* @param queryDto 筛选条件
* @return 查询结果
*/
@Override
public List<WithdrawalRecord> 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<WithdrawalRecord> 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<String, Object> 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<String, Object> 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<String, Object> 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;
}
}

View File

@ -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-----

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -16,6 +16,13 @@
<result property="businessId" column="business_id" jdbcType="BIGINT"/>
<result property="businessType" column="business_type" jdbcType="VARCHAR"/>
<result property="remark" column="remark" jdbcType="VARCHAR"/>
<result property="isExpense" column="is_expense" jdbcType="INTEGER"/>
<result property="inputToken" column="input_token" jdbcType="INTEGER"/>
<result property="outputToken" column="output_token" jdbcType="INTEGER"/>
<result property="totalTokens" column="total_tokens" jdbcType="INTEGER"/>
<result property="modelName" column="model_name" jdbcType="VARCHAR"/>
<result property="question" column="question" jdbcType="LONGVARCHAR"/>
<result property="incomeType" column="income_type" jdbcType="VARCHAR"/>
<result property="createTime" column="create_time" jdbcType="TIMESTAMP"/>
<result property="updateTime" column="update_time" jdbcType="TIMESTAMP"/>
<result property="createBy" column="create_by" jdbcType="VARCHAR"/>
@ -27,7 +34,8 @@
<select id="queryById" resultMap="AccountTransactionMap">
select
transaction_id, user_id, user_name, transaction_type, amount, before_balance, after_balance, status,
transaction_no, pay_type, business_id, business_type, 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
where transaction_id = #{transactionId}
</select>
@ -36,7 +44,8 @@
<select id="getPageList" resultMap="AccountTransactionMap">
select
transaction_id, user_id, user_name, transaction_type, amount, before_balance, after_balance, status,
transaction_no, pay_type, business_id, business_type, 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
<where>
<if test="userId != null">
@ -73,7 +82,8 @@
<select id="getList" resultMap="AccountTransactionMap">
select
transaction_id, user_id, user_name, transaction_type, amount, before_balance, after_balance, status,
transaction_no, pay_type, business_id, business_type, 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
<where>
<if test="userId != null">
@ -119,6 +129,13 @@
<if test="businessId != null">business_id,</if>
<if test="businessType != null">business_type,</if>
<if test="remark != null">remark,</if>
<if test="isExpense != null">is_expense,</if>
<if test="inputToken != null">input_token,</if>
<if test="outputToken != null">output_token,</if>
<if test="totalTokens != null">total_tokens,</if>
<if test="modelName != null">model_name,</if>
<if test="question != null">question,</if>
<if test="incomeType != null">income_type,</if>
<if test="createTime != null">create_time,</if>
<if test="updateTime != null">update_time,</if>
<if test="createBy != null">create_by,</if>
@ -138,6 +155,13 @@
<if test="businessId != null">#{businessId},</if>
<if test="businessType != null">#{businessType},</if>
<if test="remark != null">#{remark},</if>
<if test="isExpense != null">#{isExpense},</if>
<if test="inputToken != null">#{inputToken},</if>
<if test="outputToken != null">#{outputToken},</if>
<if test="totalTokens != null">#{totalTokens},</if>
<if test="modelName != null">#{modelName},</if>
<if test="question != null">#{question},</if>
<if test="incomeType != null">#{incomeType},</if>
<if test="createTime != null">#{createTime},</if>
<if test="updateTime != null">#{updateTime},</if>
<if test="createBy != null">#{createBy},</if>
@ -162,6 +186,13 @@
<if test="businessId != null">business_id = #{businessId},</if>
<if test="businessType != null">business_type = #{businessType},</if>
<if test="remark != null">remark = #{remark},</if>
<if test="isExpense != null">is_expense = #{isExpense},</if>
<if test="inputToken != null">input_token = #{inputToken},</if>
<if test="outputToken != null">output_token = #{outputToken},</if>
<if test="totalTokens != null">total_tokens = #{totalTokens},</if>
<if test="modelName != null">model_name = #{modelName},</if>
<if test="question != null">question = #{question},</if>
<if test="incomeType != null">income_type = #{incomeType},</if>
<if test="updateTime != null">update_time = #{updateTime},</if>
<if test="updateBy != null">update_by = #{updateBy},</if>
<if test="deleteFlag != null">delete_flag = #{deleteFlag},</if>
@ -183,4 +214,16 @@
delete from account_transaction
where transaction_id = #{transactionId}
</delete>
<!--通过用户ID查询交易记录-->
<select id="queryByUserId" resultMap="AccountTransactionMap">
select
transaction_id, user_id, user_name, transaction_type, amount, before_balance, after_balance, status,
transaction_no, pay_type, business_id, business_type, remark, is_expense, input_token, output_token,
total_tokens, model_name, question, income_type, create_time, update_time, create_by, update_by, delete_flag
from account_transaction
where user_id = #{userId}
and delete_flag = 0
order by create_time desc
</select>
</mapper>

View File

@ -0,0 +1,121 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.kexue.skills.mapper.ModelPriceMapper">
<resultMap type="com.kexue.skills.entity.ModelPrice" id="ModelPriceMap">
<result property="id" column="id" jdbcType="BIGINT"/>
<result property="vendor" column="vendor" jdbcType="VARCHAR"/>
<result property="modelName" column="model_name" jdbcType="VARCHAR"/>
<result property="inputPrice" column="input_price" jdbcType="DECIMAL"/>
<result property="outputPrice" column="output_price" jdbcType="DECIMAL"/>
<result property="inputPerCent" column="input_per_cent" jdbcType="BIGINT"/>
<result property="outputPerCent" column="output_per_cent" jdbcType="BIGINT"/>
<result property="unit" column="unit" jdbcType="VARCHAR"/>
<result property="remark" column="remark" jdbcType="VARCHAR"/>
<result property="createdTime" column="created_time" jdbcType="TIMESTAMP"/>
<result property="updatedTime" column="updated_time" jdbcType="TIMESTAMP"/>
</resultMap>
<!--查询单个-->
<select id="queryById" resultMap="ModelPriceMap">
select
id, vendor, model_name, input_price, output_price, input_per_cent, output_per_cent, unit, remark, created_time, updated_time
from model_price
where id = #{id}
</select>
<!--通过模型名称查询-->
<select id="queryByModelName" resultMap="ModelPriceMap">
select
id, vendor, model_name, input_price, output_price, input_per_cent, output_per_cent, unit, remark, created_time, updated_time
from model_price
where model_name = #{modelName}
</select>
<!--分页查询-->
<select id="getPageList" resultMap="ModelPriceMap">
select
id, vendor, model_name, input_price, output_price, input_per_cent, output_per_cent, unit, remark, created_time, updated_time
from model_price
<where>
<if test="vendor != null and vendor != ''">
and vendor = #{vendor}
</if>
<if test="modelName != null and modelName != ''">
and model_name like concat('%', #{modelName}, '%')
</if>
</where>
<if test="sortBy != null and sortBy != ''">
order by ${sortBy} ${sortDesc ? 'desc' : 'asc'}
</if>
</select>
<!--查询列表-->
<select id="getList" resultMap="ModelPriceMap">
select
id, vendor, model_name, input_price, output_price, input_per_cent, output_per_cent, unit, remark, created_time, updated_time
from model_price
<where>
<if test="vendor != null and vendor != ''">
and vendor = #{vendor}
</if>
<if test="modelName != null and modelName != ''">
and model_name like concat('%', #{modelName}, '%')
</if>
</where>
</select>
<!--新增数据-->
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
insert into model_price
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="vendor != null">vendor,</if>
<if test="modelName != null">model_name,</if>
<if test="inputPrice != null">input_price,</if>
<if test="outputPrice != null">output_price,</if>
<if test="inputPerCent != null">input_per_cent,</if>
<if test="outputPerCent != null">output_per_cent,</if>
<if test="unit != null">unit,</if>
<if test="remark != null">remark,</if>
<if test="createdTime != null">created_time,</if>
<if test="updatedTime != null">updated_time,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="vendor != null">#{vendor},</if>
<if test="modelName != null">#{modelName},</if>
<if test="inputPrice != null">#{inputPrice},</if>
<if test="outputPrice != null">#{outputPrice},</if>
<if test="inputPerCent != null">#{inputPerCent},</if>
<if test="outputPerCent != null">#{outputPerCent},</if>
<if test="unit != null">#{unit},</if>
<if test="remark != null">#{remark},</if>
<if test="createdTime != null">#{createdTime},</if>
<if test="updatedTime != null">#{updatedTime},</if>
</trim>
</insert>
<!--更新数据-->
<update id="update">
update model_price
<set>
<if test="vendor != null">vendor = #{vendor},</if>
<if test="modelName != null">model_name = #{modelName},</if>
<if test="inputPrice != null">input_price = #{inputPrice},</if>
<if test="outputPrice != null">output_price = #{outputPrice},</if>
<if test="inputPerCent != null">input_per_cent = #{inputPerCent},</if>
<if test="outputPerCent != null">output_per_cent = #{outputPerCent},</if>
<if test="unit != null">unit = #{unit},</if>
<if test="remark != null">remark = #{remark},</if>
<if test="createdTime != null">created_time = #{createdTime},</if>
<if test="updatedTime != null">updated_time = #{updatedTime},</if>
</set>
where id = #{id}
</update>
<!--删除数据-->
<delete id="deleteById">
delete from model_price
where id = #{id}
</delete>
</mapper>

View File

@ -1,156 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.kexue.skills.mapper.PointsAccountMapper">
<resultMap type="com.kexue.skills.entity.PointsAccount" id="PointsAccountMap">
<result property="accountId" column="account_id" jdbcType="BIGINT"/>
<result property="userId" column="user_id" jdbcType="BIGINT"/>
<result property="userName" column="user_name" jdbcType="VARCHAR"/>
<result property="totalPoints" column="total_points" jdbcType="INTEGER"/>
<result property="availablePoints" column="available_points" jdbcType="INTEGER"/>
<result property="frozenPoints" column="frozen_points" jdbcType="INTEGER"/>
<result property="createTime" column="create_time" jdbcType="TIMESTAMP"/>
<result property="updateTime" column="update_time" jdbcType="TIMESTAMP"/>
<result property="createBy" column="create_by" jdbcType="VARCHAR"/>
<result property="updateBy" column="update_by" jdbcType="VARCHAR"/>
<result property="deleteFlag" column="delete_flag" jdbcType="INTEGER"/>
</resultMap>
<!--查询单个-->
<select id="queryById" resultMap="PointsAccountMap">
select
account_id, user_id, user_name, total_points, available_points, frozen_points, create_time, update_time, create_by, update_by, delete_flag
from points_account
where account_id = #{accountId}
</select>
<!--通过用户ID查询-->
<select id="queryByUserId" resultMap="PointsAccountMap">
select
account_id, user_id, user_name, total_points, available_points, frozen_points, create_time, update_time, create_by, update_by, delete_flag
from points_account
where user_id = #{userId}
</select>
<!--分页查询-->
<select id="getPageList" resultMap="PointsAccountMap">
select
account_id, user_id, user_name, total_points, available_points, frozen_points, create_time, update_time, create_by, update_by, delete_flag
from points_account
<where>
<if test="userId != null">
and user_id = #{userId}
</if>
<if test="userName != null and userName != ''">
and user_name like concat('%', #{userName}, '%')
</if>
<if test="totalPoints != null">
and total_points <![CDATA[ >= ]]> #{totalPoints}
</if>
<if test="deleteFlag != null">
and delete_flag = #{deleteFlag}
</if>
</where>
<if test="sortBy != null and sortBy != ''">
order by ${sortBy} ${sortDesc ? 'desc' : 'asc'}
</if>
</select>
<!--查询列表-->
<select id="getList" resultMap="PointsAccountMap">
select
account_id, user_id, user_name, total_points, available_points, frozen_points, create_time, update_time, create_by, update_by, delete_flag
from points_account
<where>
<if test="userId != null">
and user_id = #{userId}
</if>
<if test="userName != null and userName != ''">
and user_name like concat('%', #{userName}, '%')
</if>
<if test="totalPoints != null">
and total_points <![CDATA[ >= ]]> #{totalPoints}
</if>
<if test="deleteFlag != null">
and delete_flag = #{deleteFlag}
</if>
</where>
</select>
<!--新增数据-->
<insert id="insert" useGeneratedKeys="true" keyProperty="accountId">
insert into points_account
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="userId != null">user_id,</if>
<if test="userName != null">user_name,</if>
<if test="totalPoints != null">total_points,</if>
<if test="availablePoints != null">available_points,</if>
<if test="frozenPoints != null">frozen_points,</if>
<if test="createTime != null">create_time,</if>
<if test="updateTime != null">update_time,</if>
<if test="createBy != null">create_by,</if>
<if test="updateBy != null">update_by,</if>
<if test="deleteFlag != null">delete_flag,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="userId != null">#{userId},</if>
<if test="userName != null">#{userName},</if>
<if test="totalPoints != null">#{totalPoints},</if>
<if test="availablePoints != null">#{availablePoints},</if>
<if test="frozenPoints != null">#{frozenPoints},</if>
<if test="createTime != null">#{createTime},</if>
<if test="updateTime != null">#{updateTime},</if>
<if test="createBy != null">#{createBy},</if>
<if test="updateBy != null">#{updateBy},</if>
<if test="deleteFlag != null">#{deleteFlag},</if>
</trim>
</insert>
<!--更新数据-->
<update id="update">
update points_account
<set>
<if test="userId != null">user_id = #{userId},</if>
<if test="userName != null">user_name = #{userName},</if>
<if test="totalPoints != null">total_points = #{totalPoints},</if>
<if test="availablePoints != null">available_points = #{availablePoints},</if>
<if test="frozenPoints != null">frozen_points = #{frozenPoints},</if>
<if test="updateTime != null">update_time = #{updateTime},</if>
<if test="updateBy != null">update_by = #{updateBy},</if>
<if test="deleteFlag != null">delete_flag = #{deleteFlag},</if>
</set>
where account_id = #{accountId}
</update>
<!--更新积分-->
<update id="updatePoints">
update points_account
<set>
<if test="type == 1">
total_points = total_points + #{points},
available_points = available_points + #{points}
</if>
<if test="type == 2">
total_points = total_points - #{points},
available_points = available_points - #{points}
</if>
update_time = now()
</set>
where user_id = #{userId}
</update>
<!--逻辑删除-->
<update id="logicDeleteById">
update points_account
set delete_flag = 1,
update_by = #{updateBy},
update_time = now()
where account_id = #{accountId}
</update>
<!--物理删除-->
<delete id="deleteById">
delete from points_account
where account_id = #{accountId}
</delete>
</mapper>

View File

@ -1,186 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.kexue.skills.mapper.PointsTransactionMapper">
<resultMap type="com.kexue.skills.entity.PointsTransaction" id="PointsTransactionMap">
<result property="transactionId" column="transaction_id" jdbcType="BIGINT"/>
<result property="userId" column="user_id" jdbcType="BIGINT"/>
<result property="userName" column="user_name" jdbcType="VARCHAR"/>
<result property="transactionType" column="transaction_type" jdbcType="INTEGER"/>
<result property="points" column="points" jdbcType="INTEGER"/>
<result property="beforePoints" column="before_points" jdbcType="INTEGER"/>
<result property="afterPoints" column="after_points" jdbcType="INTEGER"/>
<result property="status" column="status" jdbcType="INTEGER"/>
<result property="transactionNo" column="transaction_no" jdbcType="VARCHAR"/>
<result property="payType" column="pay_type" jdbcType="INTEGER"/>
<result property="businessId" column="business_id" jdbcType="BIGINT"/>
<result property="businessType" column="business_type" jdbcType="VARCHAR"/>
<result property="remark" column="remark" jdbcType="VARCHAR"/>
<result property="createTime" column="create_time" jdbcType="TIMESTAMP"/>
<result property="updateTime" column="update_time" jdbcType="TIMESTAMP"/>
<result property="createBy" column="create_by" jdbcType="VARCHAR"/>
<result property="updateBy" column="update_by" jdbcType="VARCHAR"/>
<result property="deleteFlag" column="delete_flag" jdbcType="INTEGER"/>
</resultMap>
<!--查询单个-->
<select id="queryById" resultMap="PointsTransactionMap">
select
transaction_id, 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
from points_transaction
where transaction_id = #{transactionId}
</select>
<!--分页查询-->
<select id="getPageList" resultMap="PointsTransactionMap">
select
transaction_id, 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
from points_transaction
<where>
<if test="userId != null">
and user_id = #{userId}
</if>
<if test="transactionType != null">
and transaction_type = #{transactionType}
</if>
<if test="status != null">
and status = #{status}
</if>
<if test="transactionNo != null and transactionNo != ''">
and transaction_no = #{transactionNo}
</if>
<if test="businessType != null and businessType != ''">
and business_type = #{businessType}
</if>
<if test="createTimeStart != null">
and create_time <![CDATA[ >= ]]> #{createTimeStart}
</if>
<if test="createTimeEnd != null">
and create_time <![CDATA[ <= ]]> #{createTimeEnd}
</if>
<if test="deleteFlag != null">
and delete_flag = #{deleteFlag}
</if>
</where>
<if test="sortBy != null and sortBy != ''">
order by ${sortBy} ${sortDesc ? 'desc' : 'asc'}
</if>
</select>
<!--查询列表-->
<select id="getList" resultMap="PointsTransactionMap">
select
transaction_id, 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
from points_transaction
<where>
<if test="userId != null">
and user_id = #{userId}
</if>
<if test="transactionType != null">
and transaction_type = #{transactionType}
</if>
<if test="status != null">
and status = #{status}
</if>
<if test="transactionNo != null and transactionNo != ''">
and transaction_no = #{transactionNo}
</if>
<if test="businessType != null and businessType != ''">
and business_type = #{businessType}
</if>
<if test="createTimeStart != null">
and create_time <![CDATA[ >= ]]> #{createTimeStart}
</if>
<if test="createTimeEnd != null">
and create_time <![CDATA[ <= ]]> #{createTimeEnd}
</if>
<if test="deleteFlag != null">
and delete_flag = #{deleteFlag}
</if>
</where>
</select>
<!--新增数据-->
<insert id="insert" useGeneratedKeys="true" keyProperty="transactionId">
insert into points_transaction
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="userId != null">user_id,</if>
<if test="userName != null">user_name,</if>
<if test="transactionType != null">transaction_type,</if>
<if test="points != null">points,</if>
<if test="beforePoints != null">before_points,</if>
<if test="afterPoints != null">after_points,</if>
<if test="status != null">status,</if>
<if test="transactionNo != null">transaction_no,</if>
<if test="payType != null">pay_type,</if>
<if test="businessId != null">business_id,</if>
<if test="businessType != null">business_type,</if>
<if test="remark != null">remark,</if>
<if test="createTime != null">create_time,</if>
<if test="updateTime != null">update_time,</if>
<if test="createBy != null">create_by,</if>
<if test="updateBy != null">update_by,</if>
<if test="deleteFlag != null">delete_flag,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="userId != null">#{userId},</if>
<if test="userName != null">#{userName},</if>
<if test="transactionType != null">#{transactionType},</if>
<if test="points != null">#{points},</if>
<if test="beforePoints != null">#{beforePoints},</if>
<if test="afterPoints != null">#{afterPoints},</if>
<if test="status != null">#{status},</if>
<if test="transactionNo != null">#{transactionNo},</if>
<if test="payType != null">#{payType},</if>
<if test="businessId != null">#{businessId},</if>
<if test="businessType != null">#{businessType},</if>
<if test="remark != null">#{remark},</if>
<if test="createTime != null">#{createTime},</if>
<if test="updateTime != null">#{updateTime},</if>
<if test="createBy != null">#{createBy},</if>
<if test="updateBy != null">#{updateBy},</if>
<if test="deleteFlag != null">#{deleteFlag},</if>
</trim>
</insert>
<!--更新数据-->
<update id="update">
update points_transaction
<set>
<if test="userId != null">user_id = #{userId},</if>
<if test="userName != null">user_name = #{userName},</if>
<if test="transactionType != null">transaction_type = #{transactionType},</if>
<if test="points != null">points = #{points},</if>
<if test="beforePoints != null">before_points = #{beforePoints},</if>
<if test="afterPoints != null">after_points = #{afterPoints},</if>
<if test="status != null">status = #{status},</if>
<if test="transactionNo != null">transaction_no = #{transactionNo},</if>
<if test="payType != null">pay_type = #{payType},</if>
<if test="businessId != null">business_id = #{businessId},</if>
<if test="businessType != null">business_type = #{businessType},</if>
<if test="remark != null">remark = #{remark},</if>
<if test="updateTime != null">update_time = #{updateTime},</if>
<if test="updateBy != null">update_by = #{updateBy},</if>
<if test="deleteFlag != null">delete_flag = #{deleteFlag},</if>
</set>
where transaction_id = #{transactionId}
</update>
<!--逻辑删除-->
<update id="logicDeleteById">
update points_transaction
set delete_flag = 1,
update_by = #{updateBy},
update_time = now()
where transaction_id = #{transactionId}
</update>
<!--物理删除-->
<delete id="deleteById">
delete from points_transaction
where transaction_id = #{transactionId}
</delete>
</mapper>

View File

@ -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
</select>

View File

@ -94,4 +94,13 @@
delete from sys_user_role where role_id = #{roleId}
</delete>
<!--通过用户 ID 查询角色编码列表(关联查询)-->
<select id="queryRoleCodesByUserId" resultType="java.lang.String">
select r.role_code
from sys_user_role ur
inner join sys_role r on ur.role_id = r.role_id
where ur.user_id = #{userId}
and r.delete_flag = '0'
</select>
</mapper>

View File

@ -0,0 +1,139 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.kexue.skills.mapper.WithdrawalRecordMapper">
<resultMap id="BaseResultMap" type="com.kexue.skills.entity.WithdrawalRecord">
<id column="record_id" property="recordId" jdbcType="BIGINT"/>
<result column="user_id" property="userId" jdbcType="BIGINT"/>
<result column="user_name" property="userName" jdbcType="VARCHAR"/>
<result column="withdrawal_amount" property="withdrawalAmount" jdbcType="DECIMAL"/>
<result column="fee_amount" property="feeAmount" jdbcType="DECIMAL"/>
<result column="actual_amount" property="actualAmount" jdbcType="DECIMAL"/>
<result column="status" property="status" jdbcType="TINYINT"/>
<result column="withdrawal_no" property="withdrawalNo" jdbcType="VARCHAR"/>
<result column="bank_name" property="bankName" jdbcType="VARCHAR"/>
<result column="bank_account" property="bankAccount" jdbcType="VARCHAR"/>
<result column="bank_cardholder" property="bankCardholder" jdbcType="VARCHAR"/>
<result column="remark" property="remark" jdbcType="VARCHAR"/>
<result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
<result column="update_time" property="updateTime" jdbcType="TIMESTAMP"/>
<result column="delete_flag" property="deleteFlag" jdbcType="TINYINT"/>
<result column="create_by" property="createBy" jdbcType="VARCHAR"/>
<result column="update_by" property="updateBy" jdbcType="VARCHAR"/>
</resultMap>
<sql id="Base_Column_List">
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
</sql>
<select id="queryById" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM withdrawal_record
WHERE record_id = #{recordId} AND delete_flag = 0
</select>
<select id="queryByUserId" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM withdrawal_record
WHERE user_id = #{userId} AND delete_flag = 0
ORDER BY create_time DESC
</select>
<select id="queryByWithdrawalNo" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM withdrawal_record
WHERE withdrawal_no = #{withdrawalNo} AND delete_flag = 0
</select>
<select id="getPageList" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM withdrawal_record
WHERE delete_flag = 0
<if test="userId != null">
AND user_id = #{userId}
</if>
<if test="userName != null and userName != ''">
AND user_name LIKE CONCAT('%', #{userName}, '%')
</if>
<if test="status != null">
AND status = #{status}
</if>
<if test="withdrawalNo != null and withdrawalNo != ''">
AND withdrawal_no LIKE CONCAT('%', #{withdrawalNo}, '%')
</if>
ORDER BY create_time DESC
</select>
<select id="getList" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column_List"/>
FROM withdrawal_record
WHERE delete_flag = 0
<if test="userId != null">
AND user_id = #{userId}
</if>
<if test="userName != null and userName != ''">
AND user_name LIKE CONCAT('%', #{userName}, '%')
</if>
<if test="status != null">
AND status = #{status}
</if>
<if test="withdrawalNo != null and withdrawalNo != ''">
AND withdrawal_no LIKE CONCAT('%', #{withdrawalNo}, '%')
</if>
ORDER BY create_time DESC
</select>
<insert id="insert" parameterType="com.kexue.skills.entity.WithdrawalRecord">
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}
)
</insert>
<update id="update" parameterType="com.kexue.skills.entity.WithdrawalRecord">
UPDATE withdrawal_record
<set>
<if test="userId != null">user_id = #{userId},</if>
<if test="userName != null">user_name = #{userName},</if>
<if test="withdrawalAmount != null">withdrawal_amount = #{withdrawalAmount},</if>
<if test="feeAmount != null">fee_amount = #{feeAmount},</if>
<if test="actualAmount != null">actual_amount = #{actualAmount},</if>
<if test="status != null">status = #{status},</if>
<if test="withdrawalNo != null">withdrawal_no = #{withdrawalNo},</if>
<if test="bankName != null">bank_name = #{bankName},</if>
<if test="bankAccount != null">bank_account = #{bankAccount},</if>
<if test="bankCardholder != null">bank_cardholder = #{bankCardholder},</if>
<if test="remark != null">remark = #{remark},</if>
<if test="createBy != null">create_by = #{createBy},</if>
<if test="updateBy != null">update_by = #{updateBy},</if>
update_time = #{updateTime}
</set>
WHERE record_id = #{recordId}
</update>
<update id="updateStatus" parameterType="java.util.Map">
UPDATE withdrawal_record
SET status = #{status}, update_time = CURRENT_TIMESTAMP
WHERE record_id = #{recordId}
</update>
<update id="logicDeleteById" parameterType="java.util.Map">
UPDATE withdrawal_record
SET delete_flag = 1, update_time = CURRENT_TIMESTAMP
WHERE record_id = #{recordId}
</update>
<delete id="deleteById" parameterType="java.lang.Long">
DELETE FROM withdrawal_record
WHERE record_id = #{recordId}
</delete>
</mapper>

View File

@ -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 '不可提现余额';

View File

@ -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<String, String> 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========== 测试 9IP 地址为 null应使用默认 127.0.0.1 ==========");
testOrder.setOrderNo("TEST_NULL_IP_" + System.currentTimeMillis());
try {
Map<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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");
}
}

View File

@ -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);
}
}