diff --git a/continew-server/src/main/resources/config/application.yml b/continew-server/src/main/resources/config/application.yml index 1116721d..37cb4306 100644 --- a/continew-server/src/main/resources/config/application.yml +++ b/continew-server/src/main/resources/config/application.yml @@ -249,6 +249,7 @@ sa-token: # 是否允许同一账号多地同时登录(为 true 时允许一起登录,为 false 时新登录挤掉旧登录) is-concurrent: true # 在多人登录同一账号时,是否共用一个 Token(为 true 时所有登录共用一个 Token,为 false 时每次登录新建一个 Token) + # 使用 jwt-simple 模式后,is-share 恒等于 false(目前本项目使用 jwt-simple 模式,可通过 sa-token.extension.enableJwt: false 关闭并自行提供 StpLogic 配置) is-share: false # 是否输出操作日志 is-log: false diff --git a/continew-server/src/main/resources/db/changelog/mysql/main_table.sql b/continew-server/src/main/resources/db/changelog/mysql/main_table.sql index 4e6df59e..c922210d 100644 --- a/continew-server/src/main/resources/db/changelog/mysql/main_table.sql +++ b/continew-server/src/main/resources/db/changelog/mysql/main_table.sql @@ -398,4 +398,12 @@ CREATE TABLE IF NOT EXISTS `sys_sms_log` ( PRIMARY KEY (`id`), INDEX `idx_config_id`(`config_id`), INDEX `idx_create_user`(`create_user`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='短信日志表'; \ No newline at end of file +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='短信日志表'; + +-- changeset KAI:20251125-01 +-- comment 追加sys_client表字段 +ALTER TABLE `sys_client` +ADD COLUMN `is_concurrent` tinyint(1) UNSIGNED NOT NULL DEFAULT 0 COMMENT '是否允许同一账号多地同时登录', +ADD COLUMN `max_login_count` int NOT NULL DEFAULT -1 COMMENT '同一账号最大登录数量,-1代表不限', +ADD COLUMN `replaced_range` varchar(20) DEFAULT 'ALL_DEVICE_TYPE' COMMENT '顶人下线的范围(CURR_DEVICE_TYPE=当前设备类型端;ALL_DEVICE_TYPE=所有设备类型端)', +ADD COLUMN `overflow_logout_mode` varchar(20) DEFAULT 'KICKOUT' COMMENT '溢出人数的注销方式(LOGOUT=注销下线;KICKOUT=踢人下线;REPLACED=顶人下线)'; \ No newline at end of file diff --git a/continew-server/src/main/resources/db/changelog/postgresql/main_table.sql b/continew-server/src/main/resources/db/changelog/postgresql/main_table.sql index 7ecc9e2f..213440d6 100644 --- a/continew-server/src/main/resources/db/changelog/postgresql/main_table.sql +++ b/continew-server/src/main/resources/db/changelog/postgresql/main_table.sql @@ -665,4 +665,17 @@ COMMENT ON COLUMN "sys_sms_log"."status" IS '发送状态(1:成功;2 COMMENT ON COLUMN "sys_sms_log"."res_msg" IS '返回数据'; COMMENT ON COLUMN "sys_sms_log"."create_user" IS '创建人'; COMMENT ON COLUMN "sys_sms_log"."create_time" IS '创建时间'; -COMMENT ON TABLE "sys_sms_log" IS '短信日志表'; \ No newline at end of file +COMMENT ON TABLE "sys_sms_log" IS '短信日志表'; + +-- changeset kai:20251125-01 +-- comment 追加sys_client表字段 +ALTER TABLE "sys_client" +ADD COLUMN "is_concurrent" bool NOT NULL DEFAULT false, +ADD COLUMN "max_login_count" int4 NOT NULL DEFAULT -1, +ADD COLUMN "replaced_range" varchar(20) NOT NULL DEFAULT 'ALL_DEVICE_TYPE', +ADD COLUMN "overflow_logout_mode" varchar(20) NOT NULL DEFAULT 'KICKOUT'; + +COMMENT ON COLUMN "sys_client"."is_concurrent" IS '是否允许同一账号多地同时登录'; +COMMENT ON COLUMN "sys_client"."max_login_count" IS '同一账号最大登录数量,-1代表不限'; +COMMENT ON COLUMN "sys_client"."replaced_range" IS '顶人下线的范围(CURR_DEVICE_TYPE=当前设备类型端;ALL_DEVICE_TYPE=所有设备类型端)'; +COMMENT ON COLUMN "sys_client"."overflow_logout_mode" IS '溢出人数的注销方式(LOGOUT=注销下线;KICKOUT=踢人下线;REPLACED=顶人下线)'; \ No newline at end of file diff --git a/continew-system/src/main/java/top/continew/admin/auth/AbstractLoginHandler.java b/continew-system/src/main/java/top/continew/admin/auth/AbstractLoginHandler.java index 46ceec68..13325946 100644 --- a/continew-system/src/main/java/top/continew/admin/auth/AbstractLoginHandler.java +++ b/continew-system/src/main/java/top/continew/admin/auth/AbstractLoginHandler.java @@ -18,6 +18,8 @@ package top.continew.admin.auth; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.stp.parameter.SaLoginParameter; +import cn.dev33.satoken.stp.parameter.enums.SaLogoutMode; +import cn.dev33.satoken.stp.parameter.enums.SaReplacedRange; import cn.hutool.core.bean.BeanUtil; import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletRequest; @@ -110,28 +112,33 @@ public abstract class AbstractLoginHandler implements LoginH return roles; }, threadPoolTaskExecutor); CompletableFuture passwordExpirationDaysFuture = CompletableFuture.supplyAsync(() -> optionService - .getValueByCode2Int(PASSWORD_EXPIRATION_DAYS.name()), threadPoolTaskExecutor); + .getValueByCode2Int(PASSWORD_EXPIRATION_DAYS.name()), threadPoolTaskExecutor); CompletableFuture.allOf(permissionFuture, roleFuture, passwordExpirationDaysFuture); UserContext userContext = new UserContext(permissionFuture.join(), roleFuture - .join(), passwordExpirationDaysFuture.join()); + .join(), passwordExpirationDaysFuture.join()); BeanUtil.copyProperties(user, userContext); // 设置登录配置参数 SaLoginParameter loginParameter = new SaLoginParameter(); loginParameter.setActiveTimeout(client.getActiveTimeout()); loginParameter.setTimeout(client.getTimeout()); loginParameter.setDeviceType(client.getClientType()); - userContext.setClientType(client.getClientType()); loginParameter.setExtra(CLIENT_ID, client.getClientId()); + // 设置并发登录相关参数 + loginParameter.setIsConcurrent(client.getIsConcurrent()); + loginParameter.setMaxLoginCount(client.getMaxLoginCount()); + loginParameter.setOverflowLogoutMode(SaLogoutMode.valueOf(client.getOverflowLogoutMode().getValue())); + loginParameter.setReplacedRange(SaReplacedRange.valueOf(client.getReplacedRange().getValue())); + userContext.setClientType(client.getClientType()); userContext.setClientId(client.getClientId()); userContext.setTenantId(tenantId); // 登录并缓存用户信息 StpUtil.login(userContext.getId(), loginParameter.setExtraData(BeanUtil - .beanToMap(new UserExtraContext(ServletUtils.getRequest())))); + .beanToMap(new UserExtraContext(ServletUtils.getRequest())))); UserContextHolder.setContext(userContext); return LoginResp.builder() - .token(StpUtil.getTokenValue()) - .tenantId(TenantContextHolder.isTenantEnabled() ? TenantContextHolder.getTenantId() : null) - .build(); + .token(StpUtil.getTokenValue()) + .tenantId(TenantContextHolder.isTenantEnabled() ? TenantContextHolder.getTenantId() : null) + .build(); } /** diff --git a/continew-system/src/main/java/top/continew/admin/system/enums/LogoutModeEnum.java b/continew-system/src/main/java/top/continew/admin/system/enums/LogoutModeEnum.java new file mode 100644 index 00000000..aebd4734 --- /dev/null +++ b/continew-system/src/main/java/top/continew/admin/system/enums/LogoutModeEnum.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package top.continew.admin.system.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import top.continew.starter.core.enums.BaseEnum; + +/** + * 注销模式枚举 + * + * @author KAI + * @since 2025-10-28 14:04 + */ +@Getter +@RequiredArgsConstructor +public enum LogoutModeEnum implements BaseEnum { + + /** + * 注销下线 + */ + LOGOUT("LOGOUT", "注销下线"), + + /** + * 踢人下线 + */ + KICKOUT("KICKOUT", "踢人下线"), + + /** + * 顶人下线 + */ + REPLACED("REPLACED", "顶人下线"); + + private final String value; + private final String description; + +} \ No newline at end of file diff --git a/continew-system/src/main/java/top/continew/admin/system/enums/ReplacedRangeEnum.java b/continew-system/src/main/java/top/continew/admin/system/enums/ReplacedRangeEnum.java new file mode 100644 index 00000000..cbe4512d --- /dev/null +++ b/continew-system/src/main/java/top/continew/admin/system/enums/ReplacedRangeEnum.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package top.continew.admin.system.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import top.continew.starter.core.enums.BaseEnum; + +/** + * 顶人下线的范围枚举 + * + * @author KAI + * @since 2025-10-28 14:05 + */ +@Getter +@RequiredArgsConstructor +public enum ReplacedRangeEnum implements BaseEnum { + + /** + * 当前指定的设备类型端 + */ + CURR_DEVICE_TYPE("CURR_DEVICE_TYPE", "当前指定的设备类型端"), + + /** + * 所有设备类型端 + */ + ALL_DEVICE_TYPE("ALL_DEVICE_TYPE", "所有设备类型端"); + + private final String value; + private final String description; + +} diff --git a/continew-system/src/main/java/top/continew/admin/system/model/entity/ClientDO.java b/continew-system/src/main/java/top/continew/admin/system/model/entity/ClientDO.java index 86969ebf..5ace04fb 100644 --- a/continew-system/src/main/java/top/continew/admin/system/model/entity/ClientDO.java +++ b/continew-system/src/main/java/top/continew/admin/system/model/entity/ClientDO.java @@ -20,8 +20,10 @@ import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; import lombok.Data; -import top.continew.admin.common.enums.DisEnableStatusEnum; import top.continew.admin.common.base.model.entity.BaseDO; +import top.continew.admin.common.enums.DisEnableStatusEnum; +import top.continew.admin.system.enums.LogoutModeEnum; +import top.continew.admin.system.enums.ReplacedRangeEnum; import java.io.Serial; import java.util.List; @@ -66,6 +68,26 @@ public class ClientDO extends BaseDO { */ private Long timeout; + /** + * 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) + */ + private Boolean isConcurrent; + + /** + * 同一账号最大登录数量,-1代表不限 (只有在 isConcurrent=true, isShare=false 时此配置项才有意义) + */ + private int maxLoginCount; + + /** + * 当 isConcurrent=false 时,顶人下线的范围 (CURR_DEVICE_TYPE=当前指定的设备类型端, ALL_DEVICE_TYPE=所有设备类型端) + */ + private ReplacedRangeEnum replacedRange; + + /** + * 溢出 maxLoginCount 的客户端,将以何种方式注销下线 (LOGOUT=注销下线, KICKOUT=踢人下线, REPLACED=顶人下线) + */ + private LogoutModeEnum overflowLogoutMode; + /** * 状态 */ diff --git a/continew-system/src/main/java/top/continew/admin/system/model/req/ClientReq.java b/continew-system/src/main/java/top/continew/admin/system/model/req/ClientReq.java index 92269e2c..c9c0c0ea 100644 --- a/continew-system/src/main/java/top/continew/admin/system/model/req/ClientReq.java +++ b/continew-system/src/main/java/top/continew/admin/system/model/req/ClientReq.java @@ -19,9 +19,12 @@ package top.continew.admin.system.model.req; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; import lombok.Data; import org.hibernate.validator.constraints.Length; import top.continew.admin.common.enums.DisEnableStatusEnum; +import top.continew.admin.system.enums.LogoutModeEnum; +import top.continew.admin.system.enums.ReplacedRangeEnum; import java.io.Serial; import java.io.Serializable; @@ -60,14 +63,44 @@ public class ClientReq implements Serializable { * Token 最低活跃频率(单位:秒,-1:不限制,永不冻结) */ @Schema(description = "Token 最低活跃频率(单位:秒,-1:不限制,永不冻结)", example = "1800") + @NotNull(message = "Token 最低活跃频率不能为空") private Long activeTimeout; /** * Token 有效期(单位:秒,-1:永不过期) */ @Schema(description = "Token 有效期(单位:秒,-1:永不过期)", example = "86400") + @NotNull(message = "Token 有效期不能为空") private Long timeout; + /** + * 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) + */ + @Schema(description = "是否允许同一账号多地同时登录", example = "true") + @NotNull(message = "是否运行同一账号多地同时登录不能为空") + private Boolean isConcurrent; + + /** + * 同一账号最大登录数量,-1代表不限 (只有在 isConcurrent=true, isShare=false 时此配置项才有意义) + */ + @Schema(description = "同一账号最大登录数量, -1代表不限", example = "-1") + @NotNull(message = "同一账号最大登录数量不能为空") + private int maxLoginCount; + + /** + * 当 isConcurrent=false 时,顶人下线的范围 (CURR_DEVICE_TYPE=当前指定的设备类型端, ALL_DEVICE_TYPE=所有设备类型端) + */ + @Schema(description = "顶人下线的范围", example = "ALL_DEVICE_TYPE") + @NotNull(message = "顶人下线的范围不能为空") + private ReplacedRangeEnum replacedRange; + + /** + * 溢出 maxLoginCount 的客户端,将以何种方式注销下线 (LOGOUT=注销下线, KICKOUT=踢人下线, REPLACED=顶人下线) + */ + @Schema(description = "溢出人数的注销方式", example = "KICKOUT") + @NotNull(message = "溢出人数的注销方式不能为空") + private LogoutModeEnum overflowLogoutMode; + /** * 状态 */ diff --git a/continew-system/src/main/java/top/continew/admin/system/model/resp/ClientResp.java b/continew-system/src/main/java/top/continew/admin/system/model/resp/ClientResp.java index e7ec1375..800ce38b 100644 --- a/continew-system/src/main/java/top/continew/admin/system/model/resp/ClientResp.java +++ b/continew-system/src/main/java/top/continew/admin/system/model/resp/ClientResp.java @@ -20,10 +20,12 @@ import cn.idev.excel.annotation.ExcelIgnoreUnannotated; import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +import top.continew.admin.common.base.model.resp.BaseDetailResp; import top.continew.admin.common.config.excel.DictExcelProperty; import top.continew.admin.common.config.excel.ExcelDictConverter; import top.continew.admin.common.enums.DisEnableStatusEnum; -import top.continew.admin.common.base.model.resp.BaseDetailResp; +import top.continew.admin.system.enums.LogoutModeEnum; +import top.continew.admin.system.enums.ReplacedRangeEnum; import top.continew.starter.excel.converter.ExcelBaseEnumConverter; import top.continew.starter.excel.converter.ExcelListConverter; @@ -81,10 +83,38 @@ public class ClientResp extends BaseDetailResp { @ExcelProperty(value = "Token 有效期", order = 7) private Long timeout; + /** + * 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) + */ + @Schema(description = "是否允许同一账号多地同时登录", example = "true") + @ExcelProperty(value = "是否允许同一账号多地同时登录", order = 8) + private Boolean isConcurrent; + + /** + * 同一账号最大登录数量,-1代表不限 (只有在 isConcurrent=true, isShare=false 时此配置项才有意义) + */ + @Schema(description = "同一账号最大登录数量, -1代表不限", example = "-1") + @ExcelProperty(value = "同一账号最大登录数量", order = 10) + private int maxLoginCount; + + /** + * 当 isConcurrent=false 时,顶人下线的范围 (CURR_DEVICE_TYPE=当前指定的设备类型端, ALL_DEVICE_TYPE=所有设备类型端) + */ + @Schema(description = "顶人下线的范围 (CURR_DEVICE_TYPE=当前指定的设备类型端, ALL_DEVICE_TYPE=所有设备类型端)", example = "ALL_DEVICE_TYPE") + @ExcelProperty(value = "顶人下线的范围", converter = ExcelBaseEnumConverter.class, order = 11) + private ReplacedRangeEnum replacedRange; + + /** + * 溢出 maxLoginCount 的客户端,将以何种方式注销下线 (LOGOUT=注销下线, KICKOUT=踢人下线, REPLACED=顶人下线) + */ + @Schema(description = "溢出人数的注销方式", example = "KICKOUT") + @ExcelProperty(value = "溢出人数的注销方式", converter = ExcelBaseEnumConverter.class, order = 12) + private LogoutModeEnum overflowLogoutMode; + /** * 状态 */ @Schema(description = "状态", example = "1") - @ExcelProperty(value = "状态", converter = ExcelBaseEnumConverter.class, order = 8) + @ExcelProperty(value = "状态", converter = ExcelBaseEnumConverter.class, order = 13) private DisEnableStatusEnum status; } \ No newline at end of file