Merge branch 'rouyi/master' into feature/notice_test

This commit is contained in:
budliang
2021-03-09 23:39:06 +08:00
18 changed files with 377 additions and 223 deletions

View File

@ -1,5 +1,7 @@
package cn.iocoder.dashboard.framework.mybatis.config;
import cn.iocoder.dashboard.framework.mybatis.core.handler.DefaultDBFieldHandler;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.apache.ibatis.annotations.Mapper;
@ -24,4 +26,9 @@ public class MybatisConfiguration {
return mybatisPlusInterceptor;
}
@Bean
public MetaObjectHandler defaultMetaObjectHandler(){
return new DefaultDBFieldHandler(); // 自动填充参数类
}
}

View File

@ -1,5 +1,7 @@
package cn.iocoder.dashboard.framework.mybatis.core.dataobject;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableLogic;
import lombok.Data;
@ -15,18 +17,26 @@ public class BaseDO implements Serializable {
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private Date createTime;
/**
* 最后更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
/**
* 创建者
* 创建者,目前使用 SysUser 的 id 编号
*
* 使用 String 类型的原因是,未来可能会存在非数值的情况,留好拓展性。
*/
@TableField(fill = FieldFill.INSERT)
private String creator;
/**
* 更新者
* 更新者,目前使用 SysUser 的 id 编号
*
* 使用 String 类型的原因是,未来可能会存在非数值的情况,留好拓展性。
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private String updater;
/**
* 是否删除

View File

@ -1,11 +1,10 @@
package cn.iocoder.dashboard.framework.mybatis.core.handle;
package cn.iocoder.dashboard.framework.mybatis.core.handler;
import cn.iocoder.dashboard.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.dashboard.framework.security.core.LoginUser;
import cn.iocoder.dashboard.framework.security.core.util.SecurityFrameworkUtils;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.Objects;
@ -17,7 +16,6 @@ import java.util.Objects;
*
* @author hexiaowu
*/
@Component
public class DefaultDBFieldHandler implements MetaObjectHandler {
@Override

View File

@ -56,7 +56,7 @@ public class SecurityFrameworkUtils {
if (authentication == null) {
return null;
}
return (LoginUser) authentication.getPrincipal();
return authentication.getPrincipal() instanceof LoginUser ? (LoginUser) authentication.getPrincipal() : null;
}
/**

View File

@ -1,11 +1,10 @@
package cn.iocoder.dashboard.modules.infra.controller.config;
import cn.iocoder.dashboard.common.exception.util.ServiceExceptionUtil;
import cn.iocoder.dashboard.common.pojo.CommonResult;
import cn.iocoder.dashboard.common.pojo.PageResult;
import cn.iocoder.dashboard.framework.excel.core.util.ExcelUtils;
import cn.iocoder.dashboard.framework.idempotent.core.annotation.Idempotent;
import cn.iocoder.dashboard.framework.idempotent.core.keyresolver.impl.ExpressionIdempotentKeyResolver;
import cn.iocoder.dashboard.framework.logger.operatelog.core.annotations.OperateLog;
import cn.iocoder.dashboard.modules.infra.controller.config.vo.*;
import cn.iocoder.dashboard.modules.infra.convert.config.InfConfigConvert;
import cn.iocoder.dashboard.modules.infra.dal.dataobject.config.InfConfigDO;
@ -13,50 +12,60 @@ import cn.iocoder.dashboard.modules.infra.service.config.InfConfigService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.io.IOException;
import java.util.List;
import static cn.iocoder.dashboard.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.dashboard.common.pojo.CommonResult.success;
import static cn.iocoder.dashboard.framework.logger.operatelog.core.enums.OperateTypeEnum.EXPORT;
import static cn.iocoder.dashboard.modules.infra.enums.InfErrorCodeConstants.CONFIG_GET_VALUE_ERROR_IF_SENSITIVE;
@Api(tags = "参数配置")
@RestController
@RequestMapping("/infra/config")
@Validated
public class InfConfigController {
@Resource
private InfConfigService configService;
@ApiOperation("获取参数配置分页")
@GetMapping("/page")
// @PreAuthorize("@ss.hasPermi('infra:config:list')")
public CommonResult<PageResult<InfConfigRespVO>> getConfigPage(@Validated InfConfigPageReqVO reqVO) {
PageResult<InfConfigDO> page = configService.getConfigPage(reqVO);
return success(InfConfigConvert.INSTANCE.convertPage(page));
@PostMapping("/create")
@ApiOperation("创建参数配置")
@PreAuthorize("@ss.hasPermission('infra:config:create')")
public CommonResult<Long> createConfig(@Valid @RequestBody InfConfigCreateReqVO reqVO) {
return success(configService.createConfig(reqVO));
}
@ApiOperation("导出参数配置")
@GetMapping("/export")
// @Log(title = "参数管理", businessType = BusinessType.EXPORT)
// @PreAuthorize("@ss.hasPermi('infra:config:export')")
public void exportSysConfig(HttpServletResponse response, @Validated InfConfigExportReqVO reqVO) throws IOException {
List<InfConfigDO> list = configService.getConfigList(reqVO);
// 拼接数据
List<InfConfigExcelVO> excelDataList = InfConfigConvert.INSTANCE.convertList(list);
// 输出
ExcelUtils.write(response, "参数配置.xls", "配置列表",
InfConfigExcelVO.class, excelDataList);
@ApiOperation("修改参数配置")
@PutMapping("/update")
@PreAuthorize("@ss.hasPermission('infra:config:update')")
@Idempotent(timeout = 60)
public CommonResult<Boolean> updateConfig(@Valid @RequestBody InfConfigUpdateReqVO reqVO) {
configService.updateConfig(reqVO);
return success(true);
}
@ApiOperation("删除参数配置")
@ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class)
@DeleteMapping("/delete")
@PreAuthorize("@ss.hasPermission('infra:config:delete')")
public CommonResult<Boolean> deleteConfig(@RequestParam("id") Long id) {
configService.deleteConfig(id);
return success(true);
}
@GetMapping(value = "/get")
@ApiOperation("获得参数配置")
@ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class)
@GetMapping(value = "/get")
// @PreAuthorize("@ss.hasPermi('infra:config:query')")
@PreAuthorize("@ss.hasPermission('infra:config:query')")
public CommonResult<InfConfigRespVO> getConfig(@RequestParam("id") Long id) {
return success(InfConfigConvert.INSTANCE.convert(configService.getConfig(id)));
}
@ -70,38 +79,30 @@ public class InfConfigController {
return null;
}
if (config.getSensitive()) {
throw ServiceExceptionUtil.exception(CONFIG_GET_VALUE_ERROR_IF_SENSITIVE);
throw exception(CONFIG_GET_VALUE_ERROR_IF_SENSITIVE);
}
return success(config.getValue());
}
@ApiOperation("新增参数配置")
@PostMapping("/create")
// @PreAuthorize("@ss.hasPermi('infra:config:add')")
// @Log(title = "参数管理", businessType = BusinessType.INSERT)
@Idempotent(timeout = 60, keyResolver = ExpressionIdempotentKeyResolver.class, keyArg = "#reqVO.key")
public CommonResult<Long> createConfig(@Validated @RequestBody InfConfigCreateReqVO reqVO) {
return success(configService.createConfig(reqVO));
@GetMapping("/page")
@ApiOperation("获取参数配置分页")
@PreAuthorize("@ss.hasPermission('infra:config:query')")
public CommonResult<PageResult<InfConfigRespVO>> getConfigPage(@Valid InfConfigPageReqVO reqVO) {
PageResult<InfConfigDO> page = configService.getConfigPage(reqVO);
return success(InfConfigConvert.INSTANCE.convertPage(page));
}
@ApiOperation("修改参数配置")
@PutMapping("/update")
// @PreAuthorize("@ss.hasPermi('infra:config:edit')")
// @Log(title = "参数管理", businessType = BusinessType.UPDATE)
@Idempotent(timeout = 60)
public CommonResult<Boolean> updateConfig(@Validated @RequestBody InfConfigUpdateReqVO reqVO) {
configService.updateConfig(reqVO);
return success(true);
}
@ApiOperation("删除参数配置")
@ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class)
@DeleteMapping("/delete")
// @PreAuthorize("@ss.hasPermi('infra:config:remove')")
// @Log(title = "参数管理", businessType = BusinessType.DELETE)
public CommonResult<Boolean> deleteConfig(@RequestParam("id") Long id) {
configService.deleteConfig(id);
return success(true);
@GetMapping("/export")
@ApiOperation("导出参数配置")
@PreAuthorize("@ss.hasPermission('infra:config:export')")
@OperateLog(type = EXPORT)
public void exportSysConfig(@Valid InfConfigExportReqVO reqVO,
HttpServletResponse response) throws IOException {
List<InfConfigDO> list = configService.getConfigList(reqVO);
// 拼接数据
List<InfConfigExcelVO> datas = InfConfigConvert.INSTANCE.convertList(list);
// 输出
ExcelUtils.write(response, "参数配置.xls", "数据", InfConfigExcelVO.class, datas);
}
}

View File

@ -11,6 +11,8 @@ import java.util.List;
/**
* 参数配置 Service 接口
*
* @author 芋道源码
*/
public interface InfConfigService {

View File

@ -10,6 +10,8 @@ import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Date;
/**
* 在线用户表
*
@ -36,6 +38,14 @@ public class SysUserSessionDO extends BaseDO {
* 关联 {@link SysUserDO#getId()}
*/
private Long userId;
/**
* 用户账号
*
* 冗余,因为账号可以变更
*/
private String username;
/**
* 用户 IP
*/
@ -44,5 +54,9 @@ public class SysUserSessionDO extends BaseDO {
* 浏览器 UA
*/
private String userAgent;
/**
* 会话超时时间
*/
private Date sessionTimeout;
}

View File

@ -8,6 +8,8 @@ import cn.iocoder.dashboard.modules.system.dal.dataobject.auth.SysUserSessionDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.Collection;
import java.util.Date;
import java.util.List;
@Mapper
public interface SysUserSessionMapper extends BaseMapperX<SysUserSessionDO> {
@ -18,4 +20,7 @@ public interface SysUserSessionMapper extends BaseMapperX<SysUserSessionDO> {
.likeIfPresent("user_ip", reqVO.getUserIp()));
}
default List<SysUserSessionDO> selectListBySessionTimoutLt() {
return selectList(new QueryWrapperX<SysUserSessionDO>().lt("session_timeout",new Date()));
}
}

View File

@ -16,7 +16,7 @@ public interface SysRedisKeyConstants {
RedisKeyDefine LOGIN_USER = new RedisKeyDefine("登陆用户的缓存",
"login_user:%s", // 参数为 sessionId
STRING, LoginUser.class, Duration.ofMinutes(30));
STRING, LoginUser.class, RedisKeyDefine.TimeoutTypeEnum.DYNAMIC);
RedisKeyDefine CAPTCHA_CODE = new RedisKeyDefine("验证码的缓存",
"captcha_code:%s", // 参数为 uuid

View File

@ -1,11 +1,13 @@
package cn.iocoder.dashboard.modules.system.dal.redis.auth;
import cn.iocoder.dashboard.framework.security.core.LoginUser;
import cn.iocoder.dashboard.modules.system.service.auth.SysUserSessionService;
import cn.iocoder.dashboard.util.json.JsonUtils;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Repository;
import javax.annotation.Resource;
import java.time.Duration;
import static cn.iocoder.dashboard.modules.system.dal.redis.SysRedisKeyConstants.LOGIN_USER;
@ -19,6 +21,8 @@ public class SysLoginUserRedisDAO {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private SysUserSessionService sysUserSessionService;
public LoginUser get(String sessionId) {
String redisKey = formatKey(sessionId);
@ -27,7 +31,8 @@ public class SysLoginUserRedisDAO {
public void set(String sessionId, LoginUser loginUser) {
String redisKey = formatKey(sessionId);
stringRedisTemplate.opsForValue().set(redisKey, JsonUtils.toJsonString(loginUser), LOGIN_USER.getTimeout());
stringRedisTemplate.opsForValue().set(redisKey, JsonUtils.toJsonString(loginUser),
Duration.ofMillis(sysUserSessionService.getSessionTimeoutMillis()));
}
public void delete(String sessionId) {

View File

@ -1,23 +1,30 @@
package cn.iocoder.dashboard.modules.system.job.auth;
import cn.iocoder.dashboard.framework.quartz.core.handler.JobHandler;
import cn.iocoder.dashboard.modules.system.service.auth.SysUserSessionService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 用户 Session 超时 Job
*
* @author 芋道源码
* @author
*/
@Component
@Slf4j
public class SysUserSessionTimeoutJob implements JobHandler {
@Resource
private SysUserSessionService sysUserSessionService;
@Override
public String execute(String param) throws Exception {
// System.out.println("执行了一次任务");
log.info("[execute][执行任务:{}]", param);
return null;
// 执行过期
Long timeoutCount = sysUserSessionService.clearSessionTimeout();
// 返回结果,记录每次的超时数量
return String.format("移除在线会话数量为 %s 个", timeoutCount);
}
}

View File

@ -60,4 +60,10 @@ public interface SysUserSessionService {
*/
PageResult<SysUserSessionDO> getUserSessionPage(SysUserSessionPageReqVO reqVO);
/**
* 移除超时的在线用户
*
* @return {@link Long } 移出的超时用户数量
**/
long clearSessionTimeout();
}

View File

@ -6,20 +6,28 @@ import cn.hutool.core.util.StrUtil;
import cn.iocoder.dashboard.common.pojo.PageResult;
import cn.iocoder.dashboard.framework.security.config.SecurityProperties;
import cn.iocoder.dashboard.framework.security.core.LoginUser;
import cn.iocoder.dashboard.framework.tracer.core.util.TracerUtils;
import cn.iocoder.dashboard.modules.system.controller.auth.vo.session.SysUserSessionPageReqVO;
import cn.iocoder.dashboard.modules.system.dal.mysql.auth.SysUserSessionMapper;
import cn.iocoder.dashboard.modules.system.controller.logger.vo.loginlog.SysLoginLogCreateReqVO;
import cn.iocoder.dashboard.modules.system.dal.dataobject.auth.SysUserSessionDO;
import cn.iocoder.dashboard.modules.system.dal.dataobject.user.SysUserDO;
import cn.iocoder.dashboard.modules.system.dal.mysql.auth.SysUserSessionMapper;
import cn.iocoder.dashboard.modules.system.dal.redis.auth.SysLoginUserRedisDAO;
import cn.iocoder.dashboard.modules.system.enums.logger.SysLoginLogTypeEnum;
import cn.iocoder.dashboard.modules.system.enums.logger.SysLoginResultEnum;
import cn.iocoder.dashboard.modules.system.service.auth.SysUserSessionService;
import cn.iocoder.dashboard.modules.system.service.logger.SysLoginLogService;
import cn.iocoder.dashboard.modules.system.service.user.SysUserService;
import com.google.common.collect.Lists;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Collection;
import java.util.Date;
import java.time.Duration;
import java.util.*;
import java.util.stream.Collectors;
import static cn.iocoder.dashboard.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.dashboard.util.date.DateUtils.addTime;
/**
* 在线用户 Session Service 实现类
@ -31,14 +39,14 @@ public class SysUserSessionServiceImpl implements SysUserSessionService {
@Resource
private SecurityProperties securityProperties;
@Resource
private SysLoginUserRedisDAO loginUserRedisDAO;
@Resource
private SysUserSessionMapper userSessionMapper;
@Resource
private SysUserService userService;
@Resource
private SysLoginLogService loginLogService;
@Override
public String createUserSession(LoginUser loginUser, String userIp, String userAgent) {
@ -49,7 +57,9 @@ public class SysUserSessionServiceImpl implements SysUserSessionService {
loginUserRedisDAO.set(sessionId, loginUser);
// 写入 DB 中
SysUserSessionDO userSession = SysUserSessionDO.builder().id(sessionId)
.userId(loginUser.getId()).userIp(userIp).userAgent(userAgent).build();
.userId(loginUser.getId()).userIp(userIp).userAgent(userAgent).username(loginUser.getUsername())
.sessionTimeout(addTime(Duration.ofMillis(getSessionTimeoutMillis())))
.build();
userSessionMapper.insert(userSession);
// 返回 Session 编号
return sessionId;
@ -62,7 +72,9 @@ public class SysUserSessionServiceImpl implements SysUserSessionService {
loginUserRedisDAO.set(sessionId, loginUser);
// 更新 DB 中
SysUserSessionDO updateObj = SysUserSessionDO.builder().id(sessionId).build();
updateObj.setUsername(loginUser.getUsername());
updateObj.setUpdateTime(new Date());
updateObj.setSessionTimeout(addTime(Duration.ofMillis(getSessionTimeoutMillis())));
userSessionMapper.updateById(updateObj);
}
@ -97,6 +109,36 @@ public class SysUserSessionServiceImpl implements SysUserSessionService {
return userSessionMapper.selectPage(reqVO, userIds);
}
@Override
public long clearSessionTimeout() {
// 获取db里已经超时的用户列表
List<SysUserSessionDO> sessionTimeoutDOS = userSessionMapper.selectListBySessionTimoutLt();
Map<String, SysUserSessionDO> timeoutSessionDOMap = sessionTimeoutDOS
.stream()
.filter(sessionDO -> loginUserRedisDAO.get(sessionDO.getId()) == null)
.collect(Collectors.toMap(SysUserSessionDO::getId, o -> o));
// 确认已经超时,按批次移出在线用户列表
if (CollUtil.isNotEmpty(timeoutSessionDOMap)) {
Lists.partition(new ArrayList<>(timeoutSessionDOMap.keySet()), 100).forEach(userSessionMapper::deleteBatchIds);
//记录用户超时退出日志
createTimeoutLogoutLog(timeoutSessionDOMap.values());
}
return timeoutSessionDOMap.size();
}
private void createTimeoutLogoutLog(Collection<SysUserSessionDO> timeoutSessionDOS) {
for (SysUserSessionDO timeoutSessionDO : timeoutSessionDOS) {
SysLoginLogCreateReqVO reqVO = new SysLoginLogCreateReqVO();
reqVO.setLogType(SysLoginLogTypeEnum.LOGOUT_TIMEOUT.getType());
reqVO.setTraceId(TracerUtils.getTraceId());
reqVO.setUsername(timeoutSessionDO.getUsername());
reqVO.setUserAgent(timeoutSessionDO.getUserAgent());
reqVO.setUserIp(timeoutSessionDO.getUserIp());
reqVO.setResult(SysLoginResultEnum.SUCCESS.getResult());
loginLogService.createLoginLog(reqVO);
}
}
/**
* 生成 Session 编号,目前采用 UUID 算法
*

View File

@ -0,0 +1,78 @@
package cn.iocoder.dashboard.modules.system.service.auth;
import cn.hutool.core.date.DateUtil;
import cn.iocoder.dashboard.BaseDbAndRedisUnitTest;
import cn.iocoder.dashboard.framework.mybatis.core.query.QueryWrapperX;
import cn.iocoder.dashboard.framework.security.config.SecurityProperties;
import cn.iocoder.dashboard.modules.system.dal.dataobject.auth.SysUserSessionDO;
import cn.iocoder.dashboard.modules.system.dal.mysql.auth.SysUserSessionMapper;
import cn.iocoder.dashboard.modules.system.dal.redis.auth.SysLoginUserRedisDAO;
import cn.iocoder.dashboard.modules.system.service.auth.impl.SysUserSessionServiceImpl;
import cn.iocoder.dashboard.modules.system.service.dept.impl.SysDeptServiceImpl;
import cn.iocoder.dashboard.modules.system.service.logger.impl.SysLoginLogServiceImpl;
import cn.iocoder.dashboard.modules.system.service.user.SysUserServiceImpl;
import cn.iocoder.dashboard.util.AssertUtils;
import cn.iocoder.dashboard.util.RandomUtils;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import;
import javax.annotation.Resource;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* SysUserSessionServiceImpl Tester.
*
* @author Lyon
* @version 1.0
* @since <pre>3月 8, 2021</pre>
*/
@Import(
SysUserSessionServiceImpl.class)
public class SysUserSessionServiceImplTest extends BaseDbAndRedisUnitTest {
@Resource
SysUserSessionServiceImpl sysUserSessionService;
@Resource
SysUserSessionMapper sysUserSessionMapper;
@MockBean
SecurityProperties securityProperties;
@MockBean
SysDeptServiceImpl sysDeptService;
@MockBean
SysUserServiceImpl sysUserService;
@MockBean
SysLoginLogServiceImpl sysLoginLogService;
@MockBean
SysLoginUserRedisDAO sysLoginUserRedisDAO;
@Test
public void testClearSessionTimeout_success() throws Exception {
// 准备超时数据 120 条, 在线用户 1 条
int expectedTimeoutCount = 120, expectedTotal = 1;
// 准备数据
List<SysUserSessionDO> prepareData = Stream
.iterate(0, i -> i)
.limit(expectedTimeoutCount)
.map(i -> RandomUtils.randomPojo(SysUserSessionDO.class, o -> o.setSessionTimeout(DateUtil.offsetSecond(new Date(), -1))))
.collect(Collectors.toList());
SysUserSessionDO sessionDO = RandomUtils.randomPojo(SysUserSessionDO.class, o -> o.setSessionTimeout(DateUtil.offsetMinute(new Date(), 30)));
prepareData.add(sessionDO);
prepareData.forEach(sysUserSessionMapper::insert);
//清空超时数据
long actualTimeoutCount = sysUserSessionService.clearSessionTimeout();
//校验
assertEquals(expectedTimeoutCount, actualTimeoutCount);
List<SysUserSessionDO> userSessionDOS = sysUserSessionMapper.selectList();
assertEquals(expectedTotal, userSessionDOS.size());
AssertUtils.assertPojoEquals(sessionDO, userSessionDOS.get(0), "updateTime");
}
}

View File

@ -8,3 +8,4 @@ DELETE FROM "sys_role";
DELETE FROM "sys_role_menu";
DELETE FROM "sys_menu";
DELETE FROM "sys_dict_type";
DELETE FROM "sys_user_session";

View File

@ -114,3 +114,18 @@ CREATE TABLE "sys_dict_type" (
"deleted" bit NOT NULL DEFAULT FALSE,
PRIMARY KEY ("id")
) COMMENT '字典类型表';
CREATE TABLE `sys_user_session` (
`id` varchar(32) NOT NULL,
`user_id` bigint DEFAULT NULL,
`username` varchar(50) NOT NULL DEFAULT '',
`user_ip` varchar(50) DEFAULT NULL,
`user_agent` varchar(512) DEFAULT NULL,
`session_timeout` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"creator" varchar(64) DEFAULT '',
"create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updater` varchar(64) DEFAULT '' ,
"update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted" bit NOT NULL DEFAULT FALSE,
PRIMARY KEY (`id`)
) COMMENT '用户在线 Session';