feat(account): 添加账户交易记录分页查询功能

- 新增充值记录、消费记录和赠送记录的分页查询接口
- 实现消费记录按callId分组查询功能
- 添加账户交易记录DTO和分组查询DTO定义
- 在AccountController中添加日志注解和相关API端点
- 优化AccountServiceImpl中的数据库操作方法调用
- 修改AccountTransactionMapper.xml增加相应的SQL查询语句
- 添加创建时间范围筛选功能到交易记录查询中
- 实现CMS内容管理的路径更新功能和标签查询优化
This commit is contained in:
wangzhiwei 2026-04-21 09:32:17 +08:00
parent 0d934f7287
commit 891f60d5a5
29 changed files with 2070 additions and 222 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,6 @@
package com.kexue.skills.controller;
import com.kexue.skills.annotation.Log;
import com.kexue.skills.common.CommonResult;
import com.kexue.skills.entity.AccountFrozen;
import com.kexue.skills.entity.dto.AccountFrozenDto;
@ -22,6 +23,7 @@ import java.io.IOException;
* @author 系统生成
* @since 2026-04-11
*/
@Log(module = "账户冻结")
@RestController
@RequestMapping("/api/accountFrozen")
@CrossOrigin(origins = "*")

View File

@ -1,6 +1,7 @@
package com.kexue.skills.controller;
import com.github.pagehelper.PageInfo;
import com.kexue.skills.annotation.Log;
import com.kexue.skills.annotation.RequireAuth;
import com.kexue.skills.common.Assert;
import com.kexue.skills.common.CommonResult;
@ -354,4 +355,24 @@ public class CmsContentController {
return CommonResult.failed("导入失败:" + e.getMessage());
}
}
/**
* 从指定目录读取Excel数据并更新CmsContent
*
* @param importPathDto 导入路径请求参数
* @param updateBy 更新人
* @return 更新结果
*/
@PostMapping("/updateFromPath")
@Operation(summary = "从目录更新Excel数据", description = "从指定目录读取Excel数据并更新CmsContent")
@RequireAuth
public CommonResult<Integer> updateFromPath(@RequestBody ImportPathDto importPathDto, @RequestParam("updateBy") String updateBy) {
try {
int successCount = cmsContentService.updateFromPath(importPathDto, updateBy);
return CommonResult.success(successCount);
} catch (Exception e) {
e.printStackTrace();
return CommonResult.failed("更新失败:" + e.getMessage());
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -75,4 +75,7 @@ public class SysUser extends BaseEntity implements Serializable {
@Schema(description ="邀请人用户ID邀请我注册的用户ID")
private Long invitedBy;
@Schema(description ="用户头像")
private String userIcon = "defaultUserIcon.png";
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -210,4 +210,13 @@ public interface CmsContentService extends BaseService {
* @return 成功导入的记录数
*/
int importFromZip(byte[] zipFileBytes, String createBy);
/**
* 从指定目录读取Excel数据并更新CmsContent
*
* @param importPathDto 导入路径请求参数
* @param updateBy 更新人
* @return 成功更新的记录数
*/
int updateFromPath(ImportPathDto importPathDto, String updateBy);
}

View File

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

View File

@ -6,6 +6,8 @@ import com.kexue.skills.entity.Account;
import com.kexue.skills.entity.AccountTransaction;
import com.kexue.skills.entity.SysUser;
import com.kexue.skills.entity.dto.AccountDto;
import com.kexue.skills.entity.dto.AccountTransactionDto;
import com.kexue.skills.entity.dto.ConsumptionGroupedDto;
import com.kexue.skills.common.Assert;
import com.kexue.skills.entity.dto.TokenConsumptionDto;
import com.kexue.skills.exception.BizException;
@ -17,13 +19,18 @@ import com.kexue.skills.service.ModelPriceService;
import com.kexue.skills.service.PackageConfigService;
import com.kexue.skills.entity.ModelPrice;
import com.kexue.skills.entity.PackageConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* (Account)表服务实现类
@ -34,6 +41,8 @@ import java.util.List;
@Service("accountService")
@Transactional(rollbackFor = Exception.class)
public class AccountServiceImpl implements AccountService {
private static final Logger log = LoggerFactory.getLogger(AccountServiceImpl.class);
@Resource
private AccountMapper accountMapper;
@Resource
@ -57,7 +66,7 @@ public class AccountServiceImpl implements AccountService {
@Override
public PageInfo<Account> getPageList(AccountDto queryDto) {
PageHelper.startPage(queryDto.getPageNum(), queryDto.getPageSize());
List<Account> list = this.accountMapper.getPageList(queryDto);
List<Account> list = accountMapper.getPageList(queryDto);
return new PageInfo<>(list);
}
@ -69,7 +78,7 @@ public class AccountServiceImpl implements AccountService {
*/
@Override
public List<Account> getList(AccountDto queryDto) {
return this.accountMapper.getList(queryDto);
return accountMapper.getList(queryDto);
}
/**
@ -80,7 +89,7 @@ public class AccountServiceImpl implements AccountService {
*/
@Override
public Account queryById(Long accountId) {
return this.accountMapper.queryById(accountId);
return accountMapper.queryById(accountId);
}
/**
@ -91,7 +100,7 @@ public class AccountServiceImpl implements AccountService {
*/
@Override
public Account queryByUserId(Long userId) {
return this.accountMapper.queryByUserId(userId);
return accountMapper.queryByUserId(userId);
}
/**
@ -121,7 +130,7 @@ public class AccountServiceImpl implements AccountService {
account.setFrozenAmount(BigDecimal.ZERO);
}
// 保存数据
this.accountMapper.insert(account);
accountMapper.insert(account);
return account;
}
@ -136,8 +145,8 @@ public class AccountServiceImpl implements AccountService {
// 设置更新时间
account.setUpdateTime(new Date());
// 更新数据
this.accountMapper.update(account);
return this.queryById(account.getAccountId());
accountMapper.update(account);
return queryById(account.getAccountId());
}
/**
@ -155,7 +164,7 @@ public class AccountServiceImpl implements AccountService {
@Override
public int addBalance(Long userId, BigDecimal amount, boolean isWithdrawable, String transactionNo, Long businessId, String businessType, String remark) {
// 1. 查询账户信息
Account account = this.queryByUserId(userId);
Account account = queryByUserId(userId);
if (account == null) {
// 创建账户
account = new Account();
@ -164,7 +173,7 @@ public class AccountServiceImpl implements AccountService {
account.setWithdrawableBalance(BigDecimal.ZERO);
account.setNonWithdrawableBalance(BigDecimal.ZERO);
account.setFrozenAmount(BigDecimal.ZERO);
this.insert(account);
insert(account);
}
// 2. 计算积分
@ -212,7 +221,7 @@ public class AccountServiceImpl implements AccountService {
transaction.setRemark(rechargeRemark);
transaction.setIsExpense(0); // 收入
transaction.setIncomeType("recharge"); // 充值
this.accountTransactionMapper.insert(transaction);
accountTransactionMapper.insert(transaction);
// 4. 更新账户余额使用积分
if (isWithdrawable) {
@ -225,7 +234,7 @@ public class AccountServiceImpl implements AccountService {
BigDecimal balance = account.getBalance() == null ? BigDecimal.ZERO : account.getBalance();
account.setBalance(balance.add(points));
account.setUpdateTime(new Date());
this.update(account);
update(account);
return 1;
}
@ -259,7 +268,7 @@ public class AccountServiceImpl implements AccountService {
@Override
public int reduceBalance(Long userId, BigDecimal amount, String transactionNo, Long businessId, String businessType, String remark) {
// 1. 查询账户信息
Account account = this.queryByUserId(userId);
Account account = queryByUserId(userId);
Assert.notNull(account, "账户不存在");
// 2. 检查余额是否足够
@ -281,10 +290,10 @@ public class AccountServiceImpl implements AccountService {
transaction.setBusinessType(businessType);
transaction.setRemark(remark);
transaction.setIsExpense(1); // 支出
this.accountTransactionMapper.insert(transaction);
accountTransactionMapper.insert(transaction);
// 4. 更新账户余额
return this.accountMapper.updateBalance(userId, amount, 2);
return accountMapper.updateBalance(userId, amount, 2);
}
/**
@ -301,7 +310,7 @@ public class AccountServiceImpl implements AccountService {
throw new BizException("会话ID不存在");
}
userId = bySessionId.getUserId();
Account account = this.queryByUserId(userId);
Account account = queryByUserId(userId);
Assert.notNull(account, "账户不存在");
// 2. 查询模型价格信息
@ -348,10 +357,10 @@ public class AccountServiceImpl implements AccountService {
transaction.setTotalTokens(dto.getTotalTokens());
transaction.setModelName(dto.getModelName());
transaction.setQuestion(dto.getQuestion());
this.accountTransactionMapper.insert(transaction);
accountTransactionMapper.insert(transaction);
// 6. 更新账户余额
this.accountMapper.updateBalance(userId, amount, 2);
accountMapper.updateBalance(userId, amount, 2);
return amount;
}
@ -377,7 +386,7 @@ public class AccountServiceImpl implements AccountService {
@Override
public int addSignInBalance(Long userId, BigDecimal amount, String transactionNo, Long businessId, String businessType, String remark) {
// 1. 查询账户信息
Account account = this.queryByUserId(userId);
Account account = queryByUserId(userId);
if (account == null) {
// 创建账户
account = new Account();
@ -386,7 +395,7 @@ public class AccountServiceImpl implements AccountService {
account.setWithdrawableBalance(BigDecimal.ZERO);
account.setNonWithdrawableBalance(BigDecimal.ZERO);
account.setFrozenAmount(BigDecimal.ZERO);
this.insert(account);
insert(account);
}
// 2. 保存交易记录
@ -410,7 +419,7 @@ public class AccountServiceImpl implements AccountService {
transaction.setRemark(signInRemark);
transaction.setIsExpense(0); // 收入
transaction.setIncomeType("sign_in"); // 签到奖励
this.accountTransactionMapper.insert(transaction);
accountTransactionMapper.insert(transaction);
// 3. 更新账户余额签到奖励不可提现
BigDecimal nonWithdrawableBalance = account.getNonWithdrawableBalance() == null ? BigDecimal.ZERO : account.getNonWithdrawableBalance();
@ -418,7 +427,7 @@ public class AccountServiceImpl implements AccountService {
BigDecimal balance = account.getBalance() == null ? BigDecimal.ZERO : account.getBalance();
account.setBalance(balance.add(amount));
account.setUpdateTime(new Date());
this.update(account);
update(account);
return 1;
}
@ -436,7 +445,7 @@ public class AccountServiceImpl implements AccountService {
@Override
public int addGiftBalance(Long userId, BigDecimal amount, String transactionNo, Long businessId, String businessType, String remark) {
// 1. 查询账户信息
Account account = this.queryByUserId(userId);
Account account = queryByUserId(userId);
if (account == null) {
// 创建账户
account = new Account();
@ -445,7 +454,7 @@ public class AccountServiceImpl implements AccountService {
account.setWithdrawableBalance(BigDecimal.ZERO);
account.setNonWithdrawableBalance(BigDecimal.ZERO);
account.setFrozenAmount(BigDecimal.ZERO);
this.insert(account);
insert(account);
}
// 2. 保存交易记录
@ -469,7 +478,7 @@ public class AccountServiceImpl implements AccountService {
transaction.setRemark(giftRemark);
transaction.setIsExpense(0); // 收入
transaction.setIncomeType("gift"); // 赠送
this.accountTransactionMapper.insert(transaction);
accountTransactionMapper.insert(transaction);
// 3. 更新账户余额赠送积分不可提现
if (account.getNonWithdrawableBalance() == null){
@ -481,7 +490,7 @@ public class AccountServiceImpl implements AccountService {
}
account.setBalance(account.getBalance().add(amount));
account.setUpdateTime(new Date());
this.update(account);
update(account);
return 1;
}
@ -494,7 +503,7 @@ public class AccountServiceImpl implements AccountService {
*/
@Override
public int logicDeleteById(Long accountId, String updateBy) {
return this.accountMapper.logicDeleteById(accountId, updateBy);
return accountMapper.logicDeleteById(accountId, updateBy);
}
/**
@ -505,7 +514,7 @@ public class AccountServiceImpl implements AccountService {
*/
@Override
public int deleteById(Long accountId) {
return this.accountMapper.deleteById(accountId);
return accountMapper.deleteById(accountId);
}
/**
@ -516,6 +525,134 @@ public class AccountServiceImpl implements AccountService {
*/
@Override
public List<com.kexue.skills.entity.AccountTransaction> getTransactions(Long userId) {
return this.accountTransactionMapper.queryByUserId(userId);
return accountTransactionMapper.queryByUserId(userId);
}
/**
* 分页查询充值记录
*
* @param queryDto 查询条件
* @return 分页结果
*/
@Override
public PageInfo<AccountTransaction> getRechargePageList(AccountTransactionDto queryDto) {
PageHelper.startPage(queryDto.getPageNum(), queryDto.getPageSize());
List<AccountTransaction> list = accountTransactionMapper.getRechargePageList(queryDto);
return new PageInfo<>(list);
}
/**
* 分页查询消费记录按callId分组
*
* @param queryDto 查询条件
* @return 分页结果
*/
@Override
public PageInfo<ConsumptionGroupedDto> getConsumptionGroupedPageList(AccountTransactionDto queryDto) {
int pageNum = queryDto.getPageNum() == null || queryDto.getPageNum() < 1 ? 1 : queryDto.getPageNum();
int pageSize = queryDto.getPageSize() == null || queryDto.getPageSize() < 1 ? 10 : queryDto.getPageSize();
log.info("开始查询消费记录内存分组模式参数pageNum={}, pageSize={}, userId={}", pageNum, pageSize, queryDto.getUserId());
// 1. 查询所有原始数据不分页获取全量
List<AccountTransaction> rawList = accountTransactionMapper.getConsumptionRawList(queryDto);
if (rawList == null || rawList.isEmpty()) {
return new PageInfo<>(new ArrayList<>());
}
// 2. 内存分组与聚合
// 使用 LinkedHashMap 保持按 callId 首次出现的顺序即按时间倒序
Map<String, ConsumptionGroupedDto> groupedMap = new LinkedHashMap<>();
for (AccountTransaction tx : rawList) {
// 处理 callId null 或空字符串的情况使用 transactionId 作为唯一标识
String callId = tx.getCallId();
if (callId == null || callId.isEmpty()) {
callId = "tx_" + tx.getTransactionId();
}
ConsumptionGroupedDto group = groupedMap.get(callId);
if (group == null) {
// 第一次遇到该 callId创建新组
group = new ConsumptionGroupedDto();
// 复制基础信息
group.setTransactionId(tx.getTransactionId());
group.setUserId(tx.getUserId());
group.setUserName(tx.getUserName());
group.setTransactionType(tx.getTransactionType());
group.setBeforeBalance(tx.getBeforeBalance());
group.setAfterBalance(tx.getAfterBalance());
group.setStatus(tx.getStatus());
group.setTransactionNo(tx.getTransactionNo());
group.setPayType(tx.getPayType());
group.setBusinessId(tx.getBusinessId());
group.setBusinessType(tx.getBusinessType());
group.setCallId(tx.getCallId());
group.setRemark(tx.getRemark());
group.setIsExpense(tx.getIsExpense());
group.setModelName(tx.getModelName());
group.setQuestion(tx.getQuestion());
group.setIncomeType(tx.getIncomeType());
group.setCreateTime(tx.getCreateTime());
group.setUpdateTime(tx.getUpdateTime());
group.setCreateBy(tx.getCreateBy());
group.setUpdateBy(tx.getUpdateBy());
group.setDeleteFlag(tx.getDeleteFlag());
// 初始化聚合字段
group.setAmount(BigDecimal.ZERO);
group.setInputToken(0);
group.setOutputToken(0);
group.setTotalTokens(0);
groupedMap.put(callId, group);
}
// 累加数值字段
if (tx.getAmount() != null) {
group.setAmount(group.getAmount().add(tx.getAmount()));
}
if (tx.getInputToken() != null) {
group.setInputToken(group.getInputToken() + tx.getInputToken());
}
if (tx.getOutputToken() != null) {
group.setOutputToken(group.getOutputToken() + tx.getOutputToken());
}
if (tx.getTotalTokens() != null) {
group.setTotalTokens(group.getTotalTokens() + tx.getTotalTokens());
}
}
List<ConsumptionGroupedDto> allGroupedList = new ArrayList<>(groupedMap.values());
log.info("分组聚合完成,总组数:{}", allGroupedList.size());
// 3. 内存分页
int total = allGroupedList.size();
int fromIndex = (pageNum - 1) * pageSize;
int toIndex = Math.min(fromIndex + pageSize, total);
List<ConsumptionGroupedDto> pageList = fromIndex < total ? allGroupedList.subList(fromIndex, toIndex) : new ArrayList<>();
// 4. 构建 PageInfo
PageInfo<ConsumptionGroupedDto> pageInfo = new PageInfo<>(pageList);
pageInfo.setTotal((long) total);
pageInfo.setPageNum(pageNum);
pageInfo.setPageSize(pageSize);
pageInfo.setPages((total + pageSize - 1) / pageSize);
log.info("分页成功total={}, pages={}, 当前页数据量={}", total, pageInfo.getPages(), pageList.size());
return pageInfo;
}
/**
* 分页查询赠送记录
*
* @param queryDto 查询条件
* @return 分页结果
*/
@Override
public PageInfo<AccountTransaction> getGiftPageList(AccountTransactionDto queryDto) {
PageHelper.startPage(queryDto.getPageNum(), queryDto.getPageSize());
List<AccountTransaction> list = accountTransactionMapper.getGiftPageList(queryDto);
return new PageInfo<>(list);
}
}

View File

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

View File

@ -34,11 +34,15 @@ import org.dromara.sms4j.api.SmsBlend;
import org.dromara.sms4j.core.factory.SmsFactory;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Value;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.security.NoSuchAlgorithmException;
import java.util.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
/**
* (SysUser)表服务实现类
@ -80,6 +84,12 @@ public class SysUserServiceImpl implements SysUserService {
@Resource
private AccountService accountService;
/**
* 用户头像上传目录
*/
@Value("${web.upload.userIconPath}")
private String userIconPath;
/**
* 分页查询数据
@ -281,7 +291,9 @@ public class SysUserServiceImpl implements SysUserService {
// 判断更新的用户是否是当前登录用户如果是则更新用户缓存
Long currentUserId = loginUserCacheUtil.getCurrentUserId();
if (currentUserId != null && currentUserId.equals(sysUserUpdateDto.getUserId())) {
updateUserCache(currentUserId, sysUser);
// 获取当前登录用户的 token
String token = loginUserCacheUtil.getTokenFromRequest();
updateUserCache(currentUserId, sysUser, token);
}
}else {
// 如果没有用户ID创建新用户
@ -1153,13 +1165,14 @@ public class SysUserServiceImpl implements SysUserService {
*
* @param userId 用户ID
* @param updatedUser 更新后的用户信息
* @param token 用户的认证token
*/
private void updateUserCache(Long userId, SysUser updatedUser) {
@Override
public void updateUserCache(Long userId, SysUser updatedUser, String token) {
try {
// 获取当前登录用户的 token
String token = loginUserCacheUtil.getTokenFromRequest();
// 验证 token 是否有效
if (token == null || token.isEmpty()) {
log.warn("法获取当前用户的 token跳过缓存更新");
log.warn("的 token跳过缓存更新");
return;
}
@ -1191,4 +1204,59 @@ public class SysUserServiceImpl implements SysUserService {
log.error("更新用户缓存失败:{}", e.getMessage(), e);
}
}
/**
* 上传用户头像
*
* @param file 头像文件
* @param userId 用户ID
* @param token 用户的认证token
* @return 上传成功的文件名
*/
@Override
public String uploadAvatar(MultipartFile file, Long userId, String token) {
// 检查文件是否为空
if (file.isEmpty()) {
throw new BizException("请选择要上传的头像文件");
}
// 检查文件类型
String contentType = file.getContentType();
if (contentType == null || !contentType.startsWith("image/")) {
throw new BizException("只能上传图片文件");
}
// 生成唯一文件名
String originalFilename = file.getOriginalFilename();
String extension = originalFilename != null ? originalFilename.substring(originalFilename.lastIndexOf(".")) : ".jpg";
String fileName = UUID.randomUUID().toString() + extension;
// 定义上传目录
File dir = new File(userIconPath);
if (!dir.exists()) {
dir.mkdirs();
}
// 保存文件
File dest = new File(dir, fileName);
try {
file.transferTo(dest);
} catch (IOException e) {
e.printStackTrace();
throw new BizException("上传失败:" + e.getMessage());
}
// 更新用户头像
SysUser user = sysUserMapper.queryById(userId);
if (user == null) {
throw new BizException("用户不存在");
}
user.setUserIcon(fileName);
sysUserMapper.update(user);
// 更新缓存中的用户信息
updateUserCache(userId, user, token);
return fileName;
}
}

View File

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

View File

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

View File

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

View File

@ -18,12 +18,13 @@
<result property="inviteCode" column="invite_code" jdbcType="VARCHAR"/>
<result property="invitedCode" column="invited_code" jdbcType="VARCHAR"/>
<result property="invitedBy" column="invited_by" jdbcType="BIGINT"/>
<result property="userIcon" column="user_icon" jdbcType="VARCHAR"/>
</resultMap>
<!--查询单个-->
<select id="queryById" resultMap="SysUserMap">
select
user_id, user_name, pwd, real_name, tel, email, salt, remark, create_time, enable, delete_flag, session_id, invite_code, invited_code, invited_by
user_id, user_name, pwd, real_name, tel, email, salt, remark, create_time, enable, delete_flag, session_id, invite_code, invited_code, invited_by, user_icon
from sys_user
where user_id = #{userId}
</select>
@ -31,7 +32,7 @@
<!--查询指定行数据-->
<select id="getPageList" resultMap="SysUserMap">
select
user_id, user_name, pwd, real_name, tel, email, salt, remark, create_time, enable, delete_flag, session_id, invite_code, invited_code, invited_by
user_id, user_name, pwd, real_name, tel, email, salt, remark, create_time, enable, delete_flag, session_id, invite_code, invited_code, invited_by, user_icon
from sys_user
<where>
<if test="userId != null">
@ -73,7 +74,7 @@
<!--查询指定行数据-->
<select id="queryAllByLimit" resultMap="SysUserMap">
select
user_id, user_name, pwd, real_name, tel, email, salt, remark, create_time, enable, delete_flag, session_id, invite_code, invited_code, invited_by
user_id, user_name, pwd, real_name, tel, email, salt, remark, create_time, enable, delete_flag, session_id, invite_code, invited_code, invited_by, user_icon
from sys_user
limit #{offset}, #{limit}
</select>
@ -81,7 +82,7 @@
<!--通过实体作为筛选条件查询-->
<select id="queryAll" resultMap="SysUserMap">
select
user_id, user_name, pwd, real_name, tel, email, salt, remark, create_time, enable, delete_flag, session_id, invite_code, invited_code, invited_by
user_id, user_name, pwd, real_name, tel, email, salt, remark, create_time, enable, delete_flag, session_id, invite_code, invited_code, invited_by, user_icon
from sys_user
<where>
<if test="userId != null">
@ -122,8 +123,8 @@
<!--新增所有列-->
<insert id="insert" keyProperty="userId" useGeneratedKeys="true">
insert into sys_user(user_name, pwd, real_name, tel, email, salt, remark, create_time, enable, delete_flag, session_id, invite_code, invited_code, invited_by)
values (#{userName}, #{pwd}, #{realName}, #{tel}, #{email}, #{salt}, #{remark}, #{createTime}, #{enable}, #{deleteFlag}, #{sessionId}, #{inviteCode}, #{invitedCode}, #{invitedBy})
insert into sys_user(user_name, pwd, real_name, tel, email, salt, remark, create_time, enable, delete_flag, session_id, invite_code, invited_code, invited_by, user_icon)
values (#{userName}, #{pwd}, #{realName}, #{tel}, #{email}, #{salt}, #{remark}, #{createTime}, #{enable}, #{deleteFlag}, #{sessionId}, #{inviteCode}, #{invitedCode}, #{invitedBy}, #{userIcon})
</insert>
@ -194,6 +195,9 @@
<if test="invitedBy != null">
invited_by = #{invitedBy},
</if>
<if test="userIcon != null and userIcon != ''">
user_icon = #{userIcon},
</if>
</set>
where user_id = #{userId}
</update>
@ -205,7 +209,7 @@
<select id="getByUsername" resultMap="SysUserMap">
select
user_id, user_name, pwd, real_name, tel, email, salt, remark, create_time, enable, delete_flag, session_id, invite_code, invited_code, invited_by
user_id, user_name, pwd, real_name, tel, email, salt, remark, create_time, enable, delete_flag, session_id, invite_code, invited_code, invited_by, user_icon
from sys_user
where (user_name = #{userName} or tel = #{userName})
and delete_flag = 0
@ -214,7 +218,7 @@
<select id="getByTel" resultMap="SysUserMap">
select
user_id, user_name, pwd, real_name, tel, email, salt, remark, create_time, enable, delete_flag, session_id, invite_code, invited_code, invited_by
user_id, user_name, pwd, real_name, tel, email, salt, remark, create_time, enable, delete_flag, session_id, invite_code, invited_code, invited_by, user_icon
from sys_user
where tel = #{tel}
and delete_flag = 0
@ -222,7 +226,7 @@
</select>
<select id="getBySessionId" resultMap="SysUserMap">
select
user_id, user_name, pwd, real_name, tel, email, salt, remark, create_time, enable, delete_flag, session_id, invite_code, invited_code, invited_by
user_id, user_name, pwd, real_name, tel, email, salt, remark, create_time, enable, delete_flag, session_id, invite_code, invited_code, invited_by, user_icon
from sys_user
where session_id = #{sessionId}
and delete_flag = 0
@ -231,7 +235,7 @@
<select id="getByInviteCode" resultMap="SysUserMap">
select
user_id, user_name, pwd, real_name, tel, email, salt, remark, create_time, enable, delete_flag, session_id, invite_code, invited_code, invited_by
user_id, user_name, pwd, real_name, tel, email, salt, remark, create_time, enable, delete_flag, session_id, invite_code, invited_code, invited_by, user_icon
from sys_user
where invite_code = #{inviteCode}
and delete_flag = 0