From d154aab9ed617962e6aaf3719977fafbc7f7215b Mon Sep 17 00:00:00 2001 From: lensferno Date: Fri, 19 Aug 2022 22:08:35 +0800 Subject: [PATCH] =?UTF-8?q?=E7=99=BB=E5=BD=95=E3=80=81=E6=B3=A8=E5=86=8C?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=EF=BC=8C=E6=B3=A8=E5=86=8C=E6=94=B9=E4=B8=BA?= =?UTF-8?q?=E6=B3=A8=E5=86=8C=E7=A0=81=E6=9C=BA=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- API-Document.md | 143 +++++------------- .../dscape/annotation/NotInvalidChar.java | 45 ++++++ .../configure/MybatisPlusConfigure.java | 2 + .../lensfrex/dscape/dao/entity/BlackList.java | 57 +++++++ .../dscape/dao/entity/RegisterApplies.java | 57 +++++++ .../dscape/dao/mapper/BlackListMapper.java | 15 ++ .../dao/mapper/RegisterAppliesMapper.java | 15 ++ .../dscape/dao/service/BlackListService.java | 13 ++ .../dao/service/RegisterAppliesService.java | 13 ++ .../service/impl/BlackListServiceImpl.java | 16 ++ .../impl/RegisterAppliesServiceImpl.java | 14 ++ .../dto/request/UserLoginRequestBody.java | 4 +- .../dto/request/user/RegisterRequestBody.java | 9 +- .../dscape/dto/response/general/Response.java | 6 +- .../dto/response/general/ResponseCode.java | 10 +- .../dscape/enums/user/BlackListTypeEnum.java | 20 +++ .../dscape/enums/user/UserRoleEnum.java | 4 +- .../lensfrex/dscape/utils/NetworkUtil.java | 20 +++ .../dscape/utils/validate/TextCheckUtil.java | 57 +++++++ .../web/controllers/user/UserController.java | 19 ++- .../user/admin/AdminController.java | 64 ++++++-- .../dscape/web/service/user/UserService.java | 103 +++++++++++-- .../web/service/user/admin/AdminService.java | 112 ++++++++++++++ 23 files changed, 672 insertions(+), 146 deletions(-) create mode 100644 src/main/java/net/lensfrex/dscape/annotation/NotInvalidChar.java create mode 100644 src/main/java/net/lensfrex/dscape/dao/entity/BlackList.java create mode 100644 src/main/java/net/lensfrex/dscape/dao/entity/RegisterApplies.java create mode 100644 src/main/java/net/lensfrex/dscape/dao/mapper/BlackListMapper.java create mode 100644 src/main/java/net/lensfrex/dscape/dao/mapper/RegisterAppliesMapper.java create mode 100644 src/main/java/net/lensfrex/dscape/dao/service/BlackListService.java create mode 100644 src/main/java/net/lensfrex/dscape/dao/service/RegisterAppliesService.java create mode 100644 src/main/java/net/lensfrex/dscape/dao/service/impl/BlackListServiceImpl.java create mode 100644 src/main/java/net/lensfrex/dscape/dao/service/impl/RegisterAppliesServiceImpl.java create mode 100644 src/main/java/net/lensfrex/dscape/enums/user/BlackListTypeEnum.java create mode 100644 src/main/java/net/lensfrex/dscape/utils/NetworkUtil.java create mode 100644 src/main/java/net/lensfrex/dscape/utils/validate/TextCheckUtil.java create mode 100644 src/main/java/net/lensfrex/dscape/web/service/user/admin/AdminService.java diff --git a/API-Document.md b/API-Document.md index 7ef34da..9652740 100644 --- a/API-Document.md +++ b/API-Document.md @@ -30,8 +30,7 @@ - [添加用户](#添加用户) - [修改密码](#修改密码) - [封禁、删除用户](#封禁删除用户) - - [管理员获取待审批注册用户](#管理员获取待审批注册用户) - - [审核用户注册](#审核用户注册) + - [管理员发放注册码](#管理员发放注册码) --- @@ -61,10 +60,10 @@ - 请求参数: - | 参数 | 值类型 | 可空 | 说明 | - | -------- | ------ | ----- | ------------------------------------------------ | + | 参数 | 值类型 | 可空 | 说明 | + | --------- | ------ | ----- | ------------------------------------------------ | | user_name | string | false | 用户登录名,非空 | - | password | string | false | 用户登录密码,非空,其值为明文密码两次sha256加密 | + | password | string | false | 用户登录密码,非空,其值为明文密码两次sha256加密 | 例如: ``` json @@ -102,35 +101,33 @@ | code | 错误 | | ----- | ---------------------- | - | 40301 | 用户名或密码错误 | - | 40302 | 用户已被封禁 | - | 40305 | 上级管理员未认证该用户 | + | 60101 | 用户名或密码错误 | + | 60102 | 用户已被封禁 | + | 60103 | 上级管理员未认证该用户 | --- ### 用户注册(公共) -> 用户注册可由管理员手动创建,也可以由用户自行注册,但是需要提供管理员名称,并且由管理员确认信息后才能使用 -> -> 在系统中首次注册的用户即为管理员,管理员注册后普通用户必须要提供上级管理员才能注册 +> 在系统中首次注册的用户即为管理员,管理员注册后普通用户必须要提供上级管理员提供的注册码才能注册 - URL路径:`/user/register` - 方法:`POST` - 请求参数: - | 参数 | 值类型 | 可空 | 说明 | - | -------- | ------ | ----- | ------------------------------------------------ | - | user_name | string | false | 用户登录名,非空 | - | password | string | false | 用户登录密码,非空,其值为明文密码两次sha256加密 | - | superior | string | true | 上级管理员用户名 | + | 参数 | 值类型 | 可空 | 说明 | + | ----------- | ------ | ----- | ------------------------------------------------ | + | user_name | string | false | 用户登录名,非空 | + | password | string | false | 用户登录密码,非空,其值为明文密码两次sha256加密 | + | invite_code | string | true | 上级管理员发放的注册码 | 例如: ``` json { - "user_name": "admin", + "user_name": "lensfrex", "password": "e723fb2ff93afb010960ac20c05439f1cdd1ecbb533947e7de9f43656a612052", - "superior": "lensfrex" + "invite_code": "asdf" } ``` @@ -156,7 +153,8 @@ | code | 错误 | | ----- | ---------- | | 40300 | 权限不足 | - | 20001 | 用户已存在 | + | 60201 | 用户已存在 | + | 60202 | 注册码不正确 | > 管理员添加用户,详见[添加用户](#添加用户) @@ -382,12 +380,12 @@ - 请求参数: - | 参数 | 值类型 | 可空 | 说明 | - | -------- | -------------- | -------------------- | ------------------------------------------------ | + | 参数 | 值类型 | 可空 | 说明 | + | --------- | -------------- | -------------------- | ------------------------------------------------ | | user_name | string | false | 用户登录名,非空 | - | password | string | false | 用户登录密码,非空,其值为明文密码两次sha256加密 | - | role | int | true | 用户角色 | - | token | string(header) | 管理员的access_token | + | password | string | false | 用户登录密码,非空,其值为明文密码两次sha256加密 | + | role | int | true | 用户角色 | + | token | string(header) | 管理员的access_token | 例如: ``` json @@ -474,7 +472,7 @@ ### 封禁、删除用户 -- URL路径:`/user/admin/modifyStatus/{uid}` +- URL路径:`/user/admin/modifyStatus/{uid}?status={status}` - 方法:`POST` - 请求参数: @@ -482,17 +480,9 @@ | 参数 | 值类型 | 可空 | 说明 | | ------ | -------------- | ----------------- | ------------------------------------------------------------------------ | | uid | string(url) | false | 欲修改状态的用户uid | - | status | int | false | 修改状态,0:正常;1:彻底封禁(无法登录);2:注销(销号);3:计算封禁 | + | status | int(url) | false | 修改状态,0:正常;1:彻底封禁(无法登录);2:注销(销号);3:计算封禁 | | token | string(header) | 当前对应用户的uid | - 例如: - ``` json - { - "uid": "9a6d777c-997d-a7e5-35f4-8471b7582ac0", - "new_password": 1 - } - ``` - - 返回数据 > data部分无返回数据 @@ -514,24 +504,17 @@ --- -### 管理员获取待审批注册用户 +### 管理员发放注册码 -- URL路径:`/user/admin/applications/list?offset={offset}&limit={limit}&page={page}` +- URL路径:`/user/admin/inviteCode?expired={expired}&count={count}` - 方法:`GET` - 请求参数: | 参数 | 值类型 | 可空 | 说明 | | ------ | -------------- | ----- | ------------------- | - | offset | int(url) | false | 分页数据偏移量 | - | limit | int(url) | false | 每页数据数量 | - | page | int(url) | false | 分页页码 | - | token | string(header) | false | 请求token,必须提供 | - - 例如,需要第三页数据,每页10条数据: - ``` - https://dscape.lensfrex.net/api/v1/user/admin/apply?offset=20&limit=10&page=3 - ``` + | expired | long(url) | false | 过期时间,留空或设为0表示不过期,以时间戳为格式 | + | count | int(url) | false | 获取数量,留空只获取一条,每位管理员最大可申请1024条(有效的) | - 返回数据 @@ -539,84 +522,26 @@ | 参数 | 值类型 | 可空 | 说明 | | ---- | ---------------- | ------------------ | ---- | - | id | int | 审批记录id | - | uid | string | 待审批用户uid | - | name | string | 待审批用户名 | - | ip | string | 用户发送审批时的ip | - | time | datetime(string) | 申请时间 | - + 例如: ``` json { "code": 20000, "msg": "success", "data" : [ - { - "id": 64888, - "uid": "9a6d777c-997d-a7e5-35f4-8471b7582ac0", - "name": "lensfrex2", - "ip": "127.0.0.1", - "time": "2022-07-10 08:00:00" - }, - { - "id": 64889, - "uid": "9a6d777c-997d-a7e5-35f4-8471b7582ac1", - "name": "lensfrex2", - "ip": "127.0.0.1", - "time": "2022-07-10 08:00:00" - }, + "asdf", + "zxcv", + "qwer" // ... ] } ``` - -- 错误信息 - - | code | 错误 | - | ----- | -------- | - | 40300 | 权限不足 | - ---- - -### 审核用户注册 - -- URL路径:`/user/admin/applications/deal/{uid}` -- 方法:`POST` - -- 请求参数: - - | 参数 | 值类型 | 可空 | 说明 | - | ----- | -------------- | ----------------- | --------------------------------- | - | uid | string(url) | false | 处理的用户uid | - | idea | boolean | false | 处理意见,true:通过;false:拒绝 | - | token | string(header) | 当前对应用户的uid | - - 例如: - ``` json - { - "uid": "9a6d777c-997d-a7e5-35f4-8471b7582ac0", - "idea": true - } - ``` - -- 返回数据 - - > data部分无返回数据 - - 例如: - ``` json - { - "code": 20000, - "msg": "success", - "data" : null - } - ``` - - 错误信息 | code | 错误 | | ----- | -------- | | 40300 | 权限不足 | + | 60401 | 申请数量超过上限 | ---- +--- \ No newline at end of file diff --git a/src/main/java/net/lensfrex/dscape/annotation/NotInvalidChar.java b/src/main/java/net/lensfrex/dscape/annotation/NotInvalidChar.java new file mode 100644 index 0000000..164ec76 --- /dev/null +++ b/src/main/java/net/lensfrex/dscape/annotation/NotInvalidChar.java @@ -0,0 +1,45 @@ +///* +// * Class created by lensfrex. +// */ +// +//package net.lensfrex.dscape.annotation; +// +//import net.lensfrex.dscape.utils.validate.TextCheckUtil; +// +//import java.lang.annotation.Documented; +//import java.lang.annotation.Repeatable; +//import java.lang.annotation.Retention; +//import java.lang.annotation.Target; +// +//import static java.lang.annotation.ElementType.*; +//import static java.lang.annotation.RetentionPolicy.RUNTIME; +// +//@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE}) +//@Retention(RUNTIME) +//@Repeatable(NotInvalidChar.List.class) +//@Documented +//@Constraint(validatedBy = TextCheckUtil.class)//标明由哪个类执行校验逻辑 +//public @interface NotInvalidChar { +// String message() default "value not in enum values."; +// +// Class[] groups() default {}; +// +//// Class[] payload() default {}; +// +// /** +// * @return date must in this value array +// */ +// String[] value(); +// +// /** +// * Defines several {@link NotInvalidChar} annotations on the same element. +// * +// * @see NotInvalidChar +// */ +// @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE}) +// @Retention(RUNTIME) +// @Documented +// @interface List { +// NotInvalidChar[] value(); +// } +//} diff --git a/src/main/java/net/lensfrex/dscape/configure/MybatisPlusConfigure.java b/src/main/java/net/lensfrex/dscape/configure/MybatisPlusConfigure.java index c4c661d..0d8fe13 100644 --- a/src/main/java/net/lensfrex/dscape/configure/MybatisPlusConfigure.java +++ b/src/main/java/net/lensfrex/dscape/configure/MybatisPlusConfigure.java @@ -7,6 +7,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.annotation.EnableTransactionManagement; import javax.annotation.Resource; @@ -15,6 +16,7 @@ import javax.annotation.Resource; */ @Slf4j @Configuration +@EnableTransactionManagement public class MybatisPlusConfigure { @Resource DataSourceProperties dataSourceProperties; diff --git a/src/main/java/net/lensfrex/dscape/dao/entity/BlackList.java b/src/main/java/net/lensfrex/dscape/dao/entity/BlackList.java new file mode 100644 index 0000000..bbfb32d --- /dev/null +++ b/src/main/java/net/lensfrex/dscape/dao/entity/BlackList.java @@ -0,0 +1,57 @@ +package net.lensfrex.dscape.dao.entity; + +import lombok.Data; +import java.util.Date; +import java.util.List; +import java.io.Serializable; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; + +/** + * @description black_list + * @author lensfrex + * @date 2022-08-19 + */ +@Data +public class BlackList implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.AUTO) + /** + * id + */ + private Integer id; + + /** + * 黑名单数据类型;0:ip;1:uid + */ + private int type; + + /** + * 黑名单条目名称,可空 + */ + private String name; + + /** + * 黑名单数据 + */ + private String data; + + /** + * create_time + */ + private Date createTime; + + /** + * edit_time + */ + private Date editTime; + + /** + * is_deleted + */ + private int isDeleted; + + public BlackList() {} +} \ No newline at end of file diff --git a/src/main/java/net/lensfrex/dscape/dao/entity/RegisterApplies.java b/src/main/java/net/lensfrex/dscape/dao/entity/RegisterApplies.java new file mode 100644 index 0000000..3fa262c --- /dev/null +++ b/src/main/java/net/lensfrex/dscape/dao/entity/RegisterApplies.java @@ -0,0 +1,57 @@ +package net.lensfrex.dscape.dao.entity; + +import lombok.Data; + +import java.time.LocalDateTime; +import java.io.Serializable; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; + +/** + * @description register_applies + * @author lensfrex + * @date 2022-08-18 + */ +@Data +public class RegisterApplies implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * id + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 管理员uid + */ + private String admin; + + /** + * 申请认证的用户 + */ + private String applyUser; + + /** + * 申请状态 + */ + private int status; + + /** + * create_time + */ + private LocalDateTime createTime; + + /** + * edit_time + */ + private LocalDateTime editTime; + + /** + * is_deleted + */ + private int isDeleted; + + public RegisterApplies() {} +} \ No newline at end of file diff --git a/src/main/java/net/lensfrex/dscape/dao/mapper/BlackListMapper.java b/src/main/java/net/lensfrex/dscape/dao/mapper/BlackListMapper.java new file mode 100644 index 0000000..8061560 --- /dev/null +++ b/src/main/java/net/lensfrex/dscape/dao/mapper/BlackListMapper.java @@ -0,0 +1,15 @@ +package net.lensfrex.dscape.dao.mapper; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Select; +import net.lensfrex.dscape.dao.entity.BlackList; +import java.util.List; +/** + * @description black_listMapper + * @author lensfrex + * @date 2022-08-19 + */ +@Mapper +public interface BlackListMapper extends BaseMapper { + +} \ No newline at end of file diff --git a/src/main/java/net/lensfrex/dscape/dao/mapper/RegisterAppliesMapper.java b/src/main/java/net/lensfrex/dscape/dao/mapper/RegisterAppliesMapper.java new file mode 100644 index 0000000..9830ffd --- /dev/null +++ b/src/main/java/net/lensfrex/dscape/dao/mapper/RegisterAppliesMapper.java @@ -0,0 +1,15 @@ +package net.lensfrex.dscape.dao.mapper; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Select; +import net.lensfrex.dscape.dao.entity.RegisterApplies; +import java.util.List; +/** + * @description register_appliesMapper + * @author lensfrex + * @date 2022-08-18 + */ +@Mapper +public interface RegisterAppliesMapper extends BaseMapper { + +} \ No newline at end of file diff --git a/src/main/java/net/lensfrex/dscape/dao/service/BlackListService.java b/src/main/java/net/lensfrex/dscape/dao/service/BlackListService.java new file mode 100644 index 0000000..74d9840 --- /dev/null +++ b/src/main/java/net/lensfrex/dscape/dao/service/BlackListService.java @@ -0,0 +1,13 @@ +package net.lensfrex.dscape.dao.service; +import net.lensfrex.dscape.dao.entity.BlackList; +import org.springframework.stereotype.Service; +import com.baomidou.mybatisplus.extension.service.IService; +/** + * @description black_list服务层 + * @author lensfrex + * @date 2022-08-19 + */ +@Service +public interface BlackListService extends IService { + +} \ No newline at end of file diff --git a/src/main/java/net/lensfrex/dscape/dao/service/RegisterAppliesService.java b/src/main/java/net/lensfrex/dscape/dao/service/RegisterAppliesService.java new file mode 100644 index 0000000..420e0ec --- /dev/null +++ b/src/main/java/net/lensfrex/dscape/dao/service/RegisterAppliesService.java @@ -0,0 +1,13 @@ +package net.lensfrex.dscape.dao.service; +import net.lensfrex.dscape.dao.entity.RegisterApplies; +import org.springframework.stereotype.Service; +import com.baomidou.mybatisplus.extension.service.IService; +/** + * @description register_applies服务层 + * @author lensfrex + * @date 2022-08-18 + */ +@Service +public interface RegisterAppliesService extends IService { + +} \ No newline at end of file diff --git a/src/main/java/net/lensfrex/dscape/dao/service/impl/BlackListServiceImpl.java b/src/main/java/net/lensfrex/dscape/dao/service/impl/BlackListServiceImpl.java new file mode 100644 index 0000000..abe66c9 --- /dev/null +++ b/src/main/java/net/lensfrex/dscape/dao/service/impl/BlackListServiceImpl.java @@ -0,0 +1,16 @@ +/* + * Class created by lensfrex. + */ + +package net.lensfrex.dscape.dao.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import net.lensfrex.dscape.dao.entity.BlackList; +import net.lensfrex.dscape.dao.mapper.BlackListMapper; +import net.lensfrex.dscape.dao.service.BlackListService; +import org.springframework.stereotype.Service; + +@Service +public class BlackListServiceImpl extends ServiceImpl implements BlackListService { + +} diff --git a/src/main/java/net/lensfrex/dscape/dao/service/impl/RegisterAppliesServiceImpl.java b/src/main/java/net/lensfrex/dscape/dao/service/impl/RegisterAppliesServiceImpl.java new file mode 100644 index 0000000..2433383 --- /dev/null +++ b/src/main/java/net/lensfrex/dscape/dao/service/impl/RegisterAppliesServiceImpl.java @@ -0,0 +1,14 @@ +/* + * Class created by lensfrex. + */ + +package net.lensfrex.dscape.dao.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import net.lensfrex.dscape.dao.entity.RegisterApplies; +import net.lensfrex.dscape.dao.mapper.RegisterAppliesMapper; +import net.lensfrex.dscape.dao.service.RegisterAppliesService; + +public class RegisterAppliesServiceImpl extends ServiceImpl implements RegisterAppliesService { + +} diff --git a/src/main/java/net/lensfrex/dscape/dto/request/UserLoginRequestBody.java b/src/main/java/net/lensfrex/dscape/dto/request/UserLoginRequestBody.java index c1a7785..4f54063 100644 --- a/src/main/java/net/lensfrex/dscape/dto/request/UserLoginRequestBody.java +++ b/src/main/java/net/lensfrex/dscape/dto/request/UserLoginRequestBody.java @@ -8,8 +8,8 @@ import lombok.Data; @Data public class UserLoginRequestBody { - private String password; - @JsonProperty("user_name") private String userName; + + private String password; } diff --git a/src/main/java/net/lensfrex/dscape/dto/request/user/RegisterRequestBody.java b/src/main/java/net/lensfrex/dscape/dto/request/user/RegisterRequestBody.java index 24b18d9..bb4bdfa 100644 --- a/src/main/java/net/lensfrex/dscape/dto/request/user/RegisterRequestBody.java +++ b/src/main/java/net/lensfrex/dscape/dto/request/user/RegisterRequestBody.java @@ -1,10 +1,15 @@ package net.lensfrex.dscape.dto.request.user; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; @Data public class RegisterRequestBody { - private String password; - private String superior; + @JsonProperty("user_name") private String userName; + + private String password; + + @JsonProperty("invite_code") + private String inviteCode; } \ No newline at end of file diff --git a/src/main/java/net/lensfrex/dscape/dto/response/general/Response.java b/src/main/java/net/lensfrex/dscape/dto/response/general/Response.java index 43ccd9f..94a5f89 100644 --- a/src/main/java/net/lensfrex/dscape/dto/response/general/Response.java +++ b/src/main/java/net/lensfrex/dscape/dto/response/general/Response.java @@ -36,8 +36,12 @@ public class Response { return success(null); } + public static Response error(int code, String message) { + return new Response<>(code, message, null); + } + public static Response error(ResponseCode code, String message) { - return new Response<>(code.getCode(), message, null); + return error(code.getCode(), message); } public static Response error(ResponseCode code) { diff --git a/src/main/java/net/lensfrex/dscape/dto/response/general/ResponseCode.java b/src/main/java/net/lensfrex/dscape/dto/response/general/ResponseCode.java index 819992f..6f3e17f 100644 --- a/src/main/java/net/lensfrex/dscape/dto/response/general/ResponseCode.java +++ b/src/main/java/net/lensfrex/dscape/dto/response/general/ResponseCode.java @@ -14,10 +14,14 @@ public enum ResponseCode { SERVER_INTERNAL_ERROR(50000, "服务器内部错误"), API_NOT_IMPLEMENT(0, "接口未实现"), - USERNAME_OR_PASSWORD_WRONG(40301, "用户名或密码不正确"), - USER_WAS_BANNED(40302, "用户已被封禁"), - USER_WAS_NOT_IDENTIFIED(40301, "上级管理员未认证该用户"), + USERNAME_OR_PASSWORD_WRONG(60101, "用户名或密码不正确"), + USER_WAS_BANNED(60102, "用户已被封禁"), + USER_WAS_NOT_IDENTIFIED(60103, "上级管理员未认证该用户"), + USER_NAME_EXISTS(60204, "用户名已经被使用"), + INVITE_CODE_WRONG(60202, "注册码不正确"), + + INVITE_CODE_REACHED_LIMIT(60401, "注册码已达上限") ; private final int code; diff --git a/src/main/java/net/lensfrex/dscape/enums/user/BlackListTypeEnum.java b/src/main/java/net/lensfrex/dscape/enums/user/BlackListTypeEnum.java new file mode 100644 index 0000000..68c1138 --- /dev/null +++ b/src/main/java/net/lensfrex/dscape/enums/user/BlackListTypeEnum.java @@ -0,0 +1,20 @@ +package net.lensfrex.dscape.enums.user; + +import com.baomidou.mybatisplus.annotation.EnumValue; +import lombok.Getter; + +@Getter +public enum BlackListTypeEnum { + + IP_ADDRESS(0, "IP地址"), + UID(1, "用户UID"),; + + @EnumValue + private final int code; + private final String name; + + BlackListTypeEnum(int code, String name) { + this.code = code; + this.name = name; + } +} diff --git a/src/main/java/net/lensfrex/dscape/enums/user/UserRoleEnum.java b/src/main/java/net/lensfrex/dscape/enums/user/UserRoleEnum.java index 6b3c5f1..26d677b 100644 --- a/src/main/java/net/lensfrex/dscape/enums/user/UserRoleEnum.java +++ b/src/main/java/net/lensfrex/dscape/enums/user/UserRoleEnum.java @@ -5,8 +5,8 @@ import lombok.Getter; @Getter public enum UserRoleEnum { - ADMIN(1, "admin"), - NORMAL_USER(0, "normal"); + NORMAL_USER(0, "normal"), + ADMIN(1, "admin"); @EnumValue private final int code; diff --git a/src/main/java/net/lensfrex/dscape/utils/NetworkUtil.java b/src/main/java/net/lensfrex/dscape/utils/NetworkUtil.java new file mode 100644 index 0000000..d27e8af --- /dev/null +++ b/src/main/java/net/lensfrex/dscape/utils/NetworkUtil.java @@ -0,0 +1,20 @@ +package net.lensfrex.dscape.utils; + +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +import javax.servlet.http.HttpServletRequest; + +@Component +@Scope("singleton") +public class NetworkUtil { + + public static String getRealIP(HttpServletRequest request) { + String forward = request.getHeader("X-Forwarded-For"); + if (forward != null) { + return forward.split(",")[0]; + } else { + return request.getRemoteAddr(); + } + } +} diff --git a/src/main/java/net/lensfrex/dscape/utils/validate/TextCheckUtil.java b/src/main/java/net/lensfrex/dscape/utils/validate/TextCheckUtil.java new file mode 100644 index 0000000..7ec96e8 --- /dev/null +++ b/src/main/java/net/lensfrex/dscape/utils/validate/TextCheckUtil.java @@ -0,0 +1,57 @@ +/* + * Class created by lensfrex. + */ + +/* + * Class created by lensfrex. + */ + +package net.lensfrex.dscape.utils.validate; + +public class TextCheckUtil { + + private static final String ABC_AND_CHINESE_REGEX = "[^a-zA-Z0-9\\_\\u4e00-\\u9fa5]"; + + private static final String ABC_REGEX = "[^a-zA-Z0-9\\_]"; + + // [^a-zA-Z0-9\_\[\];',./{}:"<>?/*\-\\!@#$%^&()\+\u4e00-\u9fa5] + private static final String ALL_CHARACTER_REGEX = "[^a-zA-Z0-9\\_\\[\\];',./{}:\"<>?/*\\-\\\\!@#$%^&()+=~`\\u4e00-\\u9fa5]"; + + /** + * 检测非字母和中文符号(下划线"_"除外) + * @param source 待检测字符串 + * @return 是否含有非字母字符 + */ + public static boolean checkNoneAbcAndChinese(String source) { + return !source.matches(ABC_AND_CHINESE_REGEX); + } + + /** + * 批量检测非字母和中文符号(下划线"_"除外) + * @param source 待检测字符串 + * @return 是否含有非字母字符 + */ + public static boolean checkNoneAbcAndChinese(String... source) { + for (String text : source) { + if (text.matches(ABC_AND_CHINESE_REGEX)) { + return true; + } + } + + return false; + } + + public static boolean checkNoneCharacter(String source) { + return !source.matches(ALL_CHARACTER_REGEX); + } + + public static boolean checkNoneCharacter(String... source) { + for (String text : source) { + if (text.matches(ALL_CHARACTER_REGEX)) { + return true; + } + } + + return false; + } +} \ No newline at end of file diff --git a/src/main/java/net/lensfrex/dscape/web/controllers/user/UserController.java b/src/main/java/net/lensfrex/dscape/web/controllers/user/UserController.java index 8838afb..23acee3 100644 --- a/src/main/java/net/lensfrex/dscape/web/controllers/user/UserController.java +++ b/src/main/java/net/lensfrex/dscape/web/controllers/user/UserController.java @@ -14,9 +14,15 @@ import net.lensfrex.dscape.dto.response.general.Response; import net.lensfrex.dscape.dto.response.general.ResponseCode; import net.lensfrex.dscape.dto.response.user.RegisterResponseBody; import net.lensfrex.dscape.exception.GlobalException; +import net.lensfrex.dscape.utils.NetworkUtil; +import net.lensfrex.dscape.utils.validate.TextCheckUtil; import net.lensfrex.dscape.web.service.user.UserService; import org.springframework.web.bind.annotation.*; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import java.net.http.HttpClient; + @JsonSerialize(using = ToStringSerializer.class) @RestController @RequestMapping("/user") @@ -41,17 +47,22 @@ public class UserController { } @GetMapping(value = "/checkLogin", produces = "application/json") - public Response testLogin() { - return Response.success(StpUtil.isLogin()); + public Response testLogin() { + return Response.success(userService.checkLogin()); } @PostMapping(value = "/register", produces = "application/json") - public Response register(@RequestBody RegisterRequestBody request) { + public Response register(@RequestBody RegisterRequestBody request, HttpServletRequest httpServletRequest) { if (request.getUserName() == null || request.getPassword() == null) { throw new GlobalException(ResponseCode.PARAM_WRONG); } - return Response.success(userService.register(request)); + // 这里的密码也用checkNoneAbcAndChinese是因为正常从前端发来的话是sha256,实际上是可以任意符号的 + if (TextCheckUtil.checkNoneAbcAndChinese(request.getUserName(), request.getPassword())) { + throw new GlobalException(ResponseCode.INVALID_REQUEST); + } + + return Response.success(userService.register(request, NetworkUtil.getRealIP(httpServletRequest))); } @PostMapping(value = "/modifyPassword/{uid}", produces = "application/json") diff --git a/src/main/java/net/lensfrex/dscape/web/controllers/user/admin/AdminController.java b/src/main/java/net/lensfrex/dscape/web/controllers/user/admin/AdminController.java index 9399c26..e89fa30 100644 --- a/src/main/java/net/lensfrex/dscape/web/controllers/user/admin/AdminController.java +++ b/src/main/java/net/lensfrex/dscape/web/controllers/user/admin/AdminController.java @@ -4,35 +4,71 @@ package net.lensfrex.dscape.web.controllers.user.admin; +import cn.dev33.satoken.stp.StpUtil; +import lombok.extern.slf4j.Slf4j; import net.lensfrex.dscape.dto.response.general.Response; import net.lensfrex.dscape.dto.response.general.ResponseCode; +import net.lensfrex.dscape.enums.user.UserRoleEnum; +import net.lensfrex.dscape.exception.GlobalException; +import net.lensfrex.dscape.web.service.user.admin.AdminService; import org.springframework.web.bind.annotation.*; +import java.util.List; + +@Slf4j @RestController @RequestMapping("/user/admin") public class AdminController { + + private final AdminService adminService; + + public AdminController(AdminService adminService) { + this.adminService = adminService; + } + @PostMapping(value = "/add", produces = "application/json") public Response addUser(@RequestBody String body, @RequestHeader String token) { return Response.error(ResponseCode.API_NOT_IMPLEMENT); } - @PostMapping(value = "/modifyStatus/{uid}", produces = "application/json") - public Response modifyStatus(@RequestBody String body, @PathVariable String uid, - @RequestHeader String token) { - return Response.error(ResponseCode.API_NOT_IMPLEMENT); - } + @PostMapping(value = "/modifyStatus", produces = "application/json") + public Response modifyStatus(@RequestParam String username, @RequestParam int status) { + if (!StpUtil.hasRole(String.valueOf(UserRoleEnum.ADMIN.getCode()))) { + throw new GlobalException(ResponseCode.PERMISSION_DENIED); + } - @GetMapping(value = "/application/list", produces = "application/json") - public Response listApplication( - @RequestParam String offset, @RequestParam String limit, @RequestParam String page, - @RequestHeader String token) { - return Response.error(ResponseCode.API_NOT_IMPLEMENT); + switch (status) { + case 0: + case 1: + adminService.modifyUserStatus(username, status); + break; + case 2: + adminService.deleteUser(username); + break; + case 3: + adminService.addUserToBlackList(username); + break; + default: + throw new GlobalException(ResponseCode.PARAM_WRONG); + } + + return Response.success(); } - @PostMapping(value = "/application/deal/{uid}", produces = "application/json") - public Response dealApplication(@RequestBody String body, @PathVariable String uid, - @RequestHeader String token) { - return Response.error(ResponseCode.API_NOT_IMPLEMENT); + @GetMapping(value = "/inviteCode", produces = "application/json") + public Response> generateInviteCodes(@RequestParam(required = false, defaultValue = "-1") long expired, + @RequestParam(required = false, defaultValue = "1") int count) { + if (!StpUtil.hasRole(String.valueOf(UserRoleEnum.ADMIN.getCode()))) { + throw new GlobalException(ResponseCode.PERMISSION_DENIED); + } + + if (count > 1024) { + throw new GlobalException(ResponseCode.INVALID_REQUEST); + } + + List result = adminService.generateInviteCode(expired, count); + + return Response.success(result); } } \ No newline at end of file diff --git a/src/main/java/net/lensfrex/dscape/web/service/user/UserService.java b/src/main/java/net/lensfrex/dscape/web/service/user/UserService.java index 527658b..577f045 100644 --- a/src/main/java/net/lensfrex/dscape/web/service/user/UserService.java +++ b/src/main/java/net/lensfrex/dscape/web/service/user/UserService.java @@ -9,11 +9,14 @@ import cn.dev33.satoken.stp.StpUtil; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import lombok.extern.slf4j.Slf4j; import net.lensfrex.dscape.dao.entity.UserBasic; +import net.lensfrex.dscape.dao.entity.UserRole; import net.lensfrex.dscape.dao.service.UserBasicService; +import net.lensfrex.dscape.dao.service.UserRoleService; import net.lensfrex.dscape.dto.request.UserLoginRequestBody; import net.lensfrex.dscape.dto.request.user.RegisterRequestBody; import net.lensfrex.dscape.dto.response.general.ResponseCode; import net.lensfrex.dscape.dto.response.user.RegisterResponseBody; +import net.lensfrex.dscape.enums.user.UserRoleEnum; import net.lensfrex.dscape.enums.user.UserStatusEnum; import net.lensfrex.dscape.exception.GlobalException; import net.lensfrex.dscape.utils.ObjectJsonSerializer; @@ -21,6 +24,9 @@ import org.mindrot.jbcrypt.BCrypt; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.UUID; @Slf4j @Service @@ -28,20 +34,30 @@ public class UserService { private static final String USER_INFO_CACHE_KEY = "dscape:user:basic"; + private static final String REGISTER_INVITE_CODE_KEY = "dscape:user:inviteCodes"; + private final ObjectJsonSerializer objectJsonSerializer; private final RedisTemplate redis; private final UserBasicService userBasicService; + private final UserRoleService userRoleService; + @Autowired public UserService(ObjectJsonSerializer objectJsonSerializer, RedisTemplate redis, - UserBasicService userBasicService) { + UserBasicService userBasicService, + UserRoleService userRoleService) { this.objectJsonSerializer = objectJsonSerializer; this.redis = redis; this.userBasicService = userBasicService; + this.userRoleService = userRoleService; + } + + public SaTokenInfo checkLogin() { + return StpUtil.getTokenInfo(); } public SaTokenInfo login(UserLoginRequestBody requestBody) { @@ -49,32 +65,37 @@ public class UserService { String password = requestBody.getPassword(); UserBasic userBasic; - if (Boolean.TRUE.equals(redis.hasKey(USER_INFO_CACHE_KEY))) { - String data = (String) redis.opsForHash().get(USER_INFO_CACHE_KEY, username); - userBasic = objectJsonSerializer.deserialize(data, UserBasic.class); + // 找缓存 + String cacheData = (String) redis.opsForHash().get(USER_INFO_CACHE_KEY, username); + if (cacheData != null) { + userBasic = objectJsonSerializer.deserialize(cacheData, UserBasic.class); } else { + // 进数据库里边查 QueryWrapper wrapper = new QueryWrapper<>(); wrapper.lambda() -// .select(UserBasic::getUid, UserBasic::getPassword, UserBasic::getUserName, UserBasic::getStatus) .eq(UserBasic::getUserName, username) .eq(UserBasic::getIsDeleted, 0); userBasic = userBasicService.getOne(wrapper); try { - redis.opsForHash().put(USER_INFO_CACHE_KEY, username, objectJsonSerializer.serialize(userBasic)); + if (userBasic != null) { + redis.opsForHash().put(USER_INFO_CACHE_KEY, username, objectJsonSerializer.serialize(userBasic)); + } } catch (Exception e) { log.info("将用户数据存入redis缓存时发生错误", e); } } + // 验证密码 if (userBasic != null && BCrypt.checkpw(password, userBasic.getPassword())) { + // 检查用户状态 if (userBasic.getStatus() == UserStatusEnum.BANNED.getCode()) { throw new GlobalException(ResponseCode.USER_WAS_BANNED); } else if (userBasic.getStatus() == UserStatusEnum.NOT_IDENTIFIED.getCode()) { throw new GlobalException(ResponseCode.USER_WAS_NOT_IDENTIFIED); } - StpUtil.login(requestBody.getUserName()); + StpUtil.login(userBasic.getUid()); } else { throw new GlobalException(ResponseCode.USERNAME_OR_PASSWORD_WRONG); } @@ -82,7 +103,71 @@ public class UserService { return StpUtil.getTokenInfo(); } - public RegisterResponseBody register(RegisterRequestBody requestBody) { - return new RegisterResponseBody(); + private static final String ADMIN_INVITE_CODE_COUNTER_KEY = "dscape:admin:inviteCodeCounter:%s"; + + @Transactional + public RegisterResponseBody register(RegisterRequestBody requestBody, String registerIp) { + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.lambda().eq(UserBasic::getUserName, requestBody.getUserName()) + .eq(UserBasic::getIsDeleted, 0); + // 检查用户名是否被占用 + if (userBasicService.count(wrapper) != 0) { + throw new GlobalException(ResponseCode.USER_NAME_EXISTS); + } + + RegisterResponseBody registerResponseBody = new RegisterResponseBody(); + + // 注册码验证 + String inviteCode = requestBody.getInviteCode(); + if (inviteCode == null) { + // 验证是否注册为初始管理员 + if (userBasicService.count() != 0) { + throw new GlobalException(ResponseCode.PERMISSION_DENIED); + } + + String uid = addUserToDatabase(requestBody.getUserName(), requestBody.getPassword(), "null", UserRoleEnum.ADMIN, registerIp); + registerResponseBody.setUid(uid); + } else { + String superior = (String) redis.opsForHash().get(REGISTER_INVITE_CODE_KEY, inviteCode); + if (superior == null) { + throw new GlobalException(ResponseCode.INVITE_CODE_WRONG); + } + + String uid = addUserToDatabase(requestBody.getUserName(), requestBody.getPassword(), superior, UserRoleEnum.NORMAL_USER, registerIp); + registerResponseBody.setUid(uid); + + redis.opsForHash().delete(REGISTER_INVITE_CODE_KEY, inviteCode); + redis.opsForValue().decrement(String.format(ADMIN_INVITE_CODE_COUNTER_KEY, superior)); + } + + return registerResponseBody; + } + + /** + * 添加一个用户到数据库 + * @param userName 用户名 + * @param password 密码(未用bcrypt加密的) + * @param superior 上级管理员,本身是管理员时可为空 + * @param userRoleEnum 用户角色 + * @return 用户uid + */ + private String addUserToDatabase(String userName, String password, String superior, UserRoleEnum userRoleEnum, String userIp) { + UserBasic userBasic = new UserBasic(); + + String uid = UUID.randomUUID().toString(); + userBasic.setUid(uid); + userBasic.setPassword(BCrypt.hashpw(password, BCrypt.gensalt())); + userBasic.setUserName(userName); + userBasic.setStatus(UserStatusEnum.NORMAL.getCode()); + userBasic.setSuperior(superior); + userBasic.setCreateIp(userIp); + userBasicService.save(userBasic); + + UserRole userRole = new UserRole(); + userRole.setRole(userRoleEnum.getCode()); + userRole.setUid(uid); + userRoleService.save(userRole); + + return uid; } } \ No newline at end of file diff --git a/src/main/java/net/lensfrex/dscape/web/service/user/admin/AdminService.java b/src/main/java/net/lensfrex/dscape/web/service/user/admin/AdminService.java new file mode 100644 index 0000000..29b927a --- /dev/null +++ b/src/main/java/net/lensfrex/dscape/web/service/user/admin/AdminService.java @@ -0,0 +1,112 @@ +/* + * Class created by lensfrex. + */ + +package net.lensfrex.dscape.web.service.user.admin; + +import cn.dev33.satoken.stp.StpUtil; +import cn.dev33.satoken.util.SaFoxUtil; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import net.lensfrex.dscape.dao.entity.BlackList; +import net.lensfrex.dscape.dao.entity.UserBasic; +import net.lensfrex.dscape.dao.service.BlackListService; +import net.lensfrex.dscape.dao.service.UserBasicService; +import net.lensfrex.dscape.dto.response.general.ResponseCode; +import net.lensfrex.dscape.enums.user.BlackListTypeEnum; +import net.lensfrex.dscape.exception.GlobalException; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +@Service +public class AdminService { + private final RedisTemplate redis; + + private final UserBasicService userBasicService; + + private final BlackListService blackListService; + + public AdminService(RedisTemplate redis, + UserBasicService userBasicService, + BlackListService blackListService) { + + this.redis = redis; + this.userBasicService = userBasicService; + this.blackListService = blackListService; + } + + private static final int INVITE_CODE_LENGTH = 6; + + private static final String REGISTER_INVITE_CODE_KEY = "dscape:user:inviteCodes"; + + private static final String ADMIN_INVITE_CODE_COUNTER_KEY = "dscape:admin:inviteCodeCounter:%s"; + + public List generateInviteCode(long expired, int count) { + String adminUid = StpUtil.getLoginIdAsString(); + String adminInviteCodeCounterKey = String.format(ADMIN_INVITE_CODE_COUNTER_KEY, adminUid); + + String countResult = redis.opsForValue().get(adminInviteCodeCounterKey); + + if (countResult != null && Integer.parseInt(countResult) > 1024) { + throw new GlobalException(ResponseCode.INVITE_CODE_REACHED_LIMIT); + } + + List inviteCodes = new ArrayList<>(count); + + for (int i = 0; i < count; i++) { + inviteCodes.add(SaFoxUtil.getRandomString(INVITE_CODE_LENGTH)); + } + HashMap codeAdmin = new HashMap<>(count); + inviteCodes.forEach(inviteCode -> codeAdmin.put(inviteCode, adminUid)); + + redis.opsForHash().putAll(REGISTER_INVITE_CODE_KEY, codeAdmin); + redis.opsForValue().increment(adminInviteCodeCounterKey, count); + + return inviteCodes; + } + + private static final String USER_INFO_CACHE_KEY = "dscape:user:basic"; + + public void modifyUserStatus(String username, int status) { + UpdateWrapper wrapper = new UpdateWrapper<>(); + wrapper.lambda() + .eq(UserBasic::getUserName, username) + .eq(UserBasic::getSuperior, StpUtil.getLoginIdAsString()) + .set(UserBasic::getStatus, status); + + boolean success = userBasicService.update(wrapper); + + if (success) { + this.clearUserBasicCache(username); + } + } + + public void addUserToBlackList(String username) { + BlackList blackList = new BlackList(); + blackList.setData(username); + blackList.setType(BlackListTypeEnum.UID.getCode()); + + boolean success = blackListService.save(blackList); + } + + public void deleteUser(String username) { + UpdateWrapper wrapper = new UpdateWrapper<>(); + wrapper.lambda() + .eq(UserBasic::getUserName, username) + .set(UserBasic::getIsDeleted, 1); + + boolean success = userBasicService.update(wrapper); + + if (success) { + this.clearUserBasicCache(username); + } + } + + private void clearUserBasicCache(String username) { + redis.opsForHash().delete(USER_INFO_CACHE_KEY, username); + } +} \ No newline at end of file