mirror of
				https://github.com/continew-org/continew-admin.git
				synced 2025-10-31 00:57:13 +08:00 
			
		
		
		
	refactor(open): 重构及优化应用管理代码
This commit is contained in:
		| @@ -26,6 +26,7 @@ import top.continew.starter.core.util.validate.ValidationUtils; | ||||
| import top.continew.starter.security.crypto.autoconfigure.CryptoProperties; | ||||
| import top.continew.starter.security.crypto.encryptor.AesEncryptor; | ||||
| import top.continew.starter.security.crypto.encryptor.IEncryptor; | ||||
|  | ||||
| import java.util.List; | ||||
| import java.util.stream.Collectors; | ||||
|  | ||||
| @@ -40,6 +41,18 @@ public class SecureUtils { | ||||
|     private SecureUtils() { | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 公钥加密 | ||||
|      * | ||||
|      * @param data 要加密的内容 | ||||
|      * @return 加密后的内容 | ||||
|      */ | ||||
|     public static String encryptByRsaPublicKey(String data) { | ||||
|         String publicKey = RsaProperties.PUBLIC_KEY; | ||||
|         ValidationUtils.throwIfBlank(publicKey, "请配置 RSA 公钥"); | ||||
|         return encryptByRsaPublicKey(data, publicKey); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 私钥解密 | ||||
|      * | ||||
| @@ -52,6 +65,17 @@ public class SecureUtils { | ||||
|         return decryptByRsaPrivateKey(data, privateKey); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 公钥加密 | ||||
|      * | ||||
|      * @param data      要加密的内容 | ||||
|      * @param publicKey 公钥 | ||||
|      * @return 加密后的内容 | ||||
|      */ | ||||
|     public static String encryptByRsaPublicKey(String data, String publicKey) { | ||||
|         return new String(SecureUtil.rsa(null, publicKey).encrypt(data, KeyType.PublicKey)); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 私钥解密 | ||||
|      * | ||||
|   | ||||
| @@ -16,19 +16,20 @@ | ||||
|  | ||||
| package top.continew.admin.open.model.entity; | ||||
|  | ||||
| import java.io.Serial; | ||||
| import java.time.*; | ||||
|  | ||||
| import lombok.Data; | ||||
|  | ||||
| import com.baomidou.mybatisplus.annotation.TableName; | ||||
|  | ||||
| import lombok.Data; | ||||
| import top.continew.admin.common.enums.DisEnableStatusEnum; | ||||
| import top.continew.starter.extension.crud.model.entity.BaseDO; | ||||
| import top.continew.starter.security.crypto.annotation.FieldEncrypt; | ||||
|  | ||||
| import java.io.Serial; | ||||
| import java.time.LocalDateTime; | ||||
|  | ||||
| /** | ||||
|  * 应用实体 | ||||
|  * | ||||
|  * @author chengzi | ||||
|  * @author Charles7c | ||||
|  * @since 2024/10/17 16:03 | ||||
|  */ | ||||
| @Data | ||||
| @@ -39,42 +40,34 @@ public class AppDO extends BaseDO { | ||||
|     private static final long serialVersionUID = 1L; | ||||
|  | ||||
|     /** | ||||
|      * ID | ||||
|      */ | ||||
|     private Long id; | ||||
|  | ||||
|     /** | ||||
|      * 应用名称 | ||||
|      * 名称 | ||||
|      */ | ||||
|     private String name; | ||||
|  | ||||
|     /** | ||||
|      * APPKEY | ||||
|      * Access Key(访问密钥) | ||||
|      */ | ||||
|     private String appKey; | ||||
|     @FieldEncrypt | ||||
|     private String accessKey; | ||||
|  | ||||
|     /** | ||||
|      * APPSECRET | ||||
|      * Secret Key(私有密钥) | ||||
|      */ | ||||
|     private String appSecret; | ||||
|  | ||||
|     /** | ||||
|      * 应用状态 | ||||
|      */ | ||||
|     private String status; | ||||
|     @FieldEncrypt | ||||
|     private String secretKey; | ||||
|  | ||||
|     /** | ||||
|      * 失效时间 | ||||
|      */ | ||||
|     private LocalDateTime expirationTime; | ||||
|     private LocalDateTime expireTime; | ||||
|  | ||||
|     /** | ||||
|      * 应用描述 | ||||
|      * 描述 | ||||
|      */ | ||||
|     private String appDesc; | ||||
|     private String description; | ||||
|  | ||||
|     /** | ||||
|      * secret查看状态 | ||||
|      * 状态 | ||||
|      */ | ||||
|     private String secretStatus; | ||||
|     private DisEnableStatusEnum status; | ||||
| } | ||||
| @@ -16,20 +16,19 @@ | ||||
|  | ||||
| package top.continew.admin.open.model.query; | ||||
|  | ||||
| import java.io.Serial; | ||||
| import java.io.Serializable; | ||||
|  | ||||
| import lombok.Data; | ||||
|  | ||||
| import io.swagger.v3.oas.annotations.media.Schema; | ||||
|  | ||||
| import lombok.Data; | ||||
| import top.continew.starter.data.core.annotation.Query; | ||||
| import top.continew.starter.data.core.enums.QueryType; | ||||
|  | ||||
| import java.io.Serial; | ||||
| import java.io.Serializable; | ||||
|  | ||||
| /** | ||||
|  * 应用查询条件 | ||||
|  * | ||||
|  * @author chengzi | ||||
|  * @author Charles7c | ||||
|  * @since 2024/10/17 16:03 | ||||
|  */ | ||||
| @Data | ||||
| @@ -40,16 +39,9 @@ public class AppQuery implements Serializable { | ||||
|     private static final long serialVersionUID = 1L; | ||||
|  | ||||
|     /** | ||||
|      * 应用名称 | ||||
|      * 关键词 | ||||
|      */ | ||||
|     @Schema(description = "应用名称") | ||||
|     @Query(type = QueryType.LIKE) | ||||
|     private String name; | ||||
|  | ||||
|     /** | ||||
|      * APPKEY | ||||
|      */ | ||||
|     @Schema(description = "APPKEY") | ||||
|     @Query(type = QueryType.LIKE) | ||||
|     private String appKey; | ||||
|     @Schema(description = "关键词", example = "应用1") | ||||
|     @Query(columns = {"name", "description"}, type = QueryType.LIKE) | ||||
|     private String description; | ||||
| } | ||||
| @@ -16,67 +16,68 @@ | ||||
|  | ||||
| package top.continew.admin.open.model.req; | ||||
|  | ||||
| import java.io.Serial; | ||||
| import java.time.*; | ||||
|  | ||||
| import jakarta.validation.constraints.*; | ||||
|  | ||||
| import lombok.Data; | ||||
|  | ||||
| import io.swagger.v3.oas.annotations.media.Schema; | ||||
|  | ||||
| import jakarta.validation.constraints.Future; | ||||
| import jakarta.validation.constraints.NotBlank; | ||||
| import lombok.Data; | ||||
| import org.hibernate.validator.constraints.Length; | ||||
|  | ||||
| import top.continew.admin.common.enums.DisEnableStatusEnum; | ||||
| import top.continew.starter.extension.crud.model.req.BaseReq; | ||||
|  | ||||
| import java.io.Serial; | ||||
| import java.time.LocalDateTime; | ||||
|  | ||||
| /** | ||||
|  * 创建或修改应用信息 | ||||
|  * 创建或修改应用参数 | ||||
|  * | ||||
|  * @author chengzi | ||||
|  * @author Charles7c | ||||
|  * @since 2024/10/17 16:03 | ||||
|  */ | ||||
| @Data | ||||
| @Schema(description = "创建或修改应用信息") | ||||
| @Schema(description = "创建或修改应用参数") | ||||
| public class AppReq extends BaseReq { | ||||
|  | ||||
|     @Serial | ||||
|     private static final long serialVersionUID = 1L; | ||||
|  | ||||
|     /** | ||||
|      * 应用名称 | ||||
|      * 名称 | ||||
|      */ | ||||
|     @Schema(description = "应用名称") | ||||
|     @NotBlank(message = "应用名称不能为空") | ||||
|     @Length(max = 255, message = "应用名称长度不能超过 {max} 个字符") | ||||
|     @Schema(description = "名称", example = "应用1") | ||||
|     @NotBlank(message = "名称不能为空") | ||||
|     @Length(max = 100, message = "名称长度不能超过 {max} 个字符") | ||||
|     private String name; | ||||
|  | ||||
|     /** | ||||
|      * APPKEY | ||||
|      */ | ||||
|     @Schema(description = "应用密钥") | ||||
|     @NotBlank(message = "应用密钥不能为空") | ||||
|     @Length(max = 255, message = "应用密钥长度不能超过 {max} 个字符") | ||||
|     private String appKey; | ||||
|  | ||||
|     /** | ||||
|      * 应用状态 | ||||
|      */ | ||||
|     @Schema(description = "应用状态") | ||||
|     @NotBlank(message = "应用状态不能为空") | ||||
|     @Length(max = 255, message = "应用状态长度不能超过 {max} 个字符") | ||||
|     private String status; | ||||
|  | ||||
|     /** | ||||
|      * 失效时间 | ||||
|      */ | ||||
|     @Schema(description = "失效时间") | ||||
|     @NotNull(message = "失效时间不能为空") | ||||
|     private LocalDateTime expirationTime; | ||||
|     @Schema(description = "失效时间", example = "2023-08-08 23:59:59", type = "string") | ||||
|     @Future(message = "失效时间必须是未来时间") | ||||
|     private LocalDateTime expireTime; | ||||
|  | ||||
|     /** | ||||
|      * 应用描述 | ||||
|      * 描述 | ||||
|      */ | ||||
|     @Schema(description = "应用描述") | ||||
|     @Length(max = 255, message = "应用描述长度不能超过 {max} 个字符") | ||||
|     private String appDesc; | ||||
|     @Schema(description = "描述", example = "应用1描述信息") | ||||
|     @Length(max = 200, message = "描述长度不能超过 {max} 个字符") | ||||
|     private String description; | ||||
|  | ||||
|     /** | ||||
|      * 状态 | ||||
|      */ | ||||
|     @Schema(description = "状态", example = "1") | ||||
|     private DisEnableStatusEnum status; | ||||
|  | ||||
|     /** | ||||
|      * Access Key(访问密钥) | ||||
|      */ | ||||
|     @Schema(hidden = true) | ||||
|     private String accessKey; | ||||
|  | ||||
|     /** | ||||
|      * Secret Key(密钥) | ||||
|      */ | ||||
|     @Schema(hidden = true) | ||||
|     private String secretKey; | ||||
| } | ||||
| @@ -16,22 +16,22 @@ | ||||
|  | ||||
| package top.continew.admin.open.model.resp; | ||||
|  | ||||
| import java.io.Serial; | ||||
| import java.time.*; | ||||
|  | ||||
| import lombok.Data; | ||||
|  | ||||
| import io.swagger.v3.oas.annotations.media.Schema; | ||||
|  | ||||
| import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; | ||||
| import com.alibaba.excel.annotation.ExcelProperty; | ||||
|  | ||||
| import io.swagger.v3.oas.annotations.media.Schema; | ||||
| import lombok.Data; | ||||
| import top.continew.admin.common.enums.DisEnableStatusEnum; | ||||
| import top.continew.starter.extension.crud.model.resp.BaseDetailResp; | ||||
| import top.continew.starter.file.excel.converter.ExcelBaseEnumConverter; | ||||
|  | ||||
| import java.io.Serial; | ||||
| import java.time.LocalDateTime; | ||||
|  | ||||
| /** | ||||
|  * 应用详情信息 | ||||
|  * | ||||
|  * @author chengzi | ||||
|  * @author Charles7c | ||||
|  * @since 2024/10/17 16:03 | ||||
|  */ | ||||
| @Data | ||||
| @@ -43,37 +43,37 @@ public class AppDetailResp extends BaseDetailResp { | ||||
|     private static final long serialVersionUID = 1L; | ||||
|  | ||||
|     /** | ||||
|      * 应用名称 | ||||
|      * 名称 | ||||
|      */ | ||||
|     @Schema(description = "应用名称") | ||||
|     @ExcelProperty(value = "应用名称") | ||||
|     @Schema(description = "名称", example = "应用1") | ||||
|     @ExcelProperty(value = "名称", order = 2) | ||||
|     private String name; | ||||
|  | ||||
|     /** | ||||
|      * 应用密钥 | ||||
|      * Access Key(访问密钥) | ||||
|      */ | ||||
|     @Schema(description = "应用密钥") | ||||
|     @ExcelProperty(value = "应用密钥") | ||||
|     private String appKey; | ||||
|  | ||||
|     /** | ||||
|      * 应用状态 | ||||
|      */ | ||||
|     @Schema(description = "应用状态") | ||||
|     @ExcelProperty(value = "应用状态") | ||||
|     private String status; | ||||
|     @Schema(description = "Access Key(访问密钥)", example = "YjUyMGJjYjIxNTE0NDAxMWE1NmRiY2") | ||||
|     @ExcelProperty(value = "Access Key", order = 3) | ||||
|     private String accessKey; | ||||
|  | ||||
|     /** | ||||
|      * 失效时间 | ||||
|      */ | ||||
|     @Schema(description = "失效时间") | ||||
|     @ExcelProperty(value = "失效时间") | ||||
|     private LocalDateTime expirationTime; | ||||
|     @Schema(description = "失效时间", example = "2023-08-08 08:08:08", type = "string") | ||||
|     @ExcelProperty(value = "失效时间", order = 4) | ||||
|     private LocalDateTime expireTime; | ||||
|  | ||||
|     /** | ||||
|      * 应用描述 | ||||
|      * 状态 | ||||
|      */ | ||||
|     @Schema(description = "应用描述") | ||||
|     @ExcelProperty(value = "应用描述") | ||||
|     private String appDesc; | ||||
|     @Schema(description = "状态", example = "1") | ||||
|     @ExcelProperty(value = "状态", converter = ExcelBaseEnumConverter.class, order = 5) | ||||
|     private DisEnableStatusEnum status; | ||||
|  | ||||
|     /** | ||||
|      * 描述 | ||||
|      */ | ||||
|     @Schema(description = "描述", example = "应用1描述信息") | ||||
|     @ExcelProperty(value = "描述", order = 6) | ||||
|     private String description; | ||||
| } | ||||
| @@ -16,56 +16,55 @@ | ||||
|  | ||||
| package top.continew.admin.open.model.resp; | ||||
|  | ||||
| import java.io.Serial; | ||||
| import java.time.*; | ||||
|  | ||||
| import lombok.Data; | ||||
|  | ||||
| import io.swagger.v3.oas.annotations.media.Schema; | ||||
| import lombok.Data; | ||||
| import top.continew.admin.common.enums.DisEnableStatusEnum; | ||||
| import top.continew.starter.extension.crud.model.resp.BaseDetailResp; | ||||
|  | ||||
| import top.continew.starter.extension.crud.model.resp.BaseResp; | ||||
| import java.io.Serial; | ||||
| import java.time.LocalDateTime; | ||||
|  | ||||
| /** | ||||
|  * 应用信息 | ||||
|  * | ||||
|  * @author chengzi | ||||
|  * @author Charles7c | ||||
|  * @since 2024/10/17 16:03 | ||||
|  */ | ||||
| @Data | ||||
| @Schema(description = "应用信息") | ||||
| public class AppResp extends BaseResp { | ||||
| public class AppResp extends BaseDetailResp { | ||||
|  | ||||
|     @Serial | ||||
|     private static final long serialVersionUID = 1L; | ||||
|  | ||||
|     /** | ||||
|      * 应用名称 | ||||
|      * 名称 | ||||
|      */ | ||||
|     @Schema(description = "应用名称") | ||||
|     @Schema(description = "名称", example = "应用1") | ||||
|     private String name; | ||||
|  | ||||
|     /** | ||||
|      * APPKEY | ||||
|      * Access Key(访问密钥) | ||||
|      */ | ||||
|     @Schema(description = "应用密钥") | ||||
|     private String appKey; | ||||
|  | ||||
|     /** | ||||
|      * 应用状态 | ||||
|      */ | ||||
|     @Schema(description = "应用状态") | ||||
|     private String status; | ||||
|     @Schema(description = "Access Key(访问密钥)", example = "YjUyMGJjYjIxNTE0NDAxMWE1NmRiY2") | ||||
|     private String accessKey; | ||||
|  | ||||
|     /** | ||||
|      * 失效时间 | ||||
|      */ | ||||
|     @Schema(description = "失效时间") | ||||
|     private LocalDateTime expirationTime; | ||||
|     @Schema(description = "失效时间", example = "2023-08-08 08:08:08", type = "string") | ||||
|     private LocalDateTime expireTime; | ||||
|  | ||||
|     /** | ||||
|      * 应用描述 | ||||
|      * 状态 | ||||
|      */ | ||||
|     @Schema(description = "应用描述") | ||||
|     private String appDesc; | ||||
|     @Schema(description = "状态", example = "1") | ||||
|     private DisEnableStatusEnum status; | ||||
|  | ||||
|     /** | ||||
|      * 描述 | ||||
|      */ | ||||
|     @Schema(description = "描述", example = "应用1描述信息") | ||||
|     private String description; | ||||
| } | ||||
| @@ -20,28 +20,31 @@ import io.swagger.v3.oas.annotations.media.Schema; | ||||
| import lombok.Data; | ||||
| 
 | ||||
| import java.io.Serial; | ||||
| import java.io.Serializable; | ||||
| 
 | ||||
| /** | ||||
|  * 应用密钥/密码信息 | ||||
|  * 应用密钥信息 | ||||
|  * | ||||
|  * @author chengzi | ||||
|  * @author Charles7c | ||||
|  * @since 2024/10/17 16:03 | ||||
|  */ | ||||
| @Data | ||||
| @Schema(description = "应用密钥/密码信息") | ||||
| public class AppSecretGetResp { | ||||
| @Schema(description = "应用密钥信息") | ||||
| public class AppSecretResp implements Serializable { | ||||
| 
 | ||||
|     @Serial | ||||
|     private static final long serialVersionUID = 1L; | ||||
| 
 | ||||
|     /** | ||||
|      * 应用密钥 | ||||
|      * Access Key(访问密钥) | ||||
|      */ | ||||
|     @Schema(description = "应用密钥") | ||||
|     private String appKey; | ||||
|     @Schema(description = "Access Key(访问密钥)", example = "YjUyMGJjYjIxNTE0NDAxMWE1NmRiY2") | ||||
|     private String accessKey; | ||||
| 
 | ||||
|     /** | ||||
|      * 应用密码 | ||||
|      * Secret Key(私有密钥) | ||||
|      */ | ||||
|     @Schema(description = "应用密码") | ||||
|     private String appSecret; | ||||
|     @Schema(description = "Secret Key(私有密钥)", example = "") | ||||
|     private String secretKey; | ||||
| } | ||||
| @@ -16,71 +16,58 @@ | ||||
|  | ||||
| package top.continew.admin.open.service; | ||||
|  | ||||
| import top.continew.admin.open.model.resp.AppSecretGetResp; | ||||
| import top.continew.starter.extension.crud.service.BaseService; | ||||
| import top.continew.admin.open.model.query.AppQuery; | ||||
| import top.continew.admin.open.model.req.AppReq; | ||||
| import top.continew.admin.open.model.resp.AppDetailResp; | ||||
| import top.continew.admin.open.model.resp.AppResp; | ||||
| import top.continew.admin.open.model.resp.AppSecretResp; | ||||
| import top.continew.starter.extension.crud.service.BaseService; | ||||
|  | ||||
| /** | ||||
|  * 应用业务接口 | ||||
|  * | ||||
|  * @author chengzi | ||||
|  * @author Charles7c | ||||
|  * @since 2024/10/17 16:03 | ||||
|  */ | ||||
| public interface AppService extends BaseService<AppResp, AppDetailResp, AppQuery, AppReq> { | ||||
|     /** | ||||
|      * 根据ID查询应用密码 | ||||
|      * | ||||
|      * @param id ID | ||||
|      * @return 应用密码 | ||||
|      */ | ||||
|     AppSecretGetResp getAppSecretById(Long id); | ||||
|  | ||||
|     /** | ||||
|      * 根据ID重置应用密码查看状态 | ||||
|      * 获取密钥 | ||||
|      * | ||||
|      * @param id ID | ||||
|      * @return 密钥信息 | ||||
|      */ | ||||
|     AppSecretResp getSecret(Long id); | ||||
|  | ||||
|     /** | ||||
|      * 重置密钥 | ||||
|      * | ||||
|      * @param id ID | ||||
|      */ | ||||
|     void resetAppSecretStatusById(Long id, String status); | ||||
|     void resetSecret(Long id); | ||||
|  | ||||
|     /** | ||||
|      * 根据应用密钥重置应用密码查看状态 | ||||
|      * 根据 Access Key 获取 Secret Key | ||||
|      * | ||||
|      * @param appKey 应用密钥 | ||||
|      * @param accessKey Access Key | ||||
|      * @return Secret Key | ||||
|      */ | ||||
|     void resetAppSecretStatusByAppkey(String appKey, String status); | ||||
|     String getSecretKeyByAccessKey(String accessKey); | ||||
|  | ||||
|     /** | ||||
|      * 根据ID刷新应用密码 | ||||
|      * 判断应用是否存在 | ||||
|      * | ||||
|      * @param id ID | ||||
|      */ | ||||
|     void refreshAppSecretByID(Long id); | ||||
|  | ||||
|     /** | ||||
|      * 根据应用密钥获取应用密码 | ||||
|      * | ||||
|      * @param appKey 应用密钥 | ||||
|      * @return 应用密码 | ||||
|      */ | ||||
|     String getAppSecretByAppKey(String appKey); | ||||
|  | ||||
|     /** | ||||
|      * 判断应用密钥是否存在 | ||||
|      * | ||||
|      * @param appKey 应用密钥 | ||||
|      * @param accessKey Access Key | ||||
|      * @return 是否存在(true:存在;false:不存在) | ||||
|      */ | ||||
|     boolean isExistAppKey(String appKey); | ||||
|     boolean isAppExists(String accessKey); | ||||
|  | ||||
|     /** | ||||
|      * 判断应用密钥是否过期 | ||||
|      * | ||||
|      * @param appKey 应用密钥 | ||||
|      * @param accessKey Access Key | ||||
|      * @return 是否过期(true:已过期;false:未过期) | ||||
|      */ | ||||
|     boolean isExpireAppKey(String appKey); | ||||
|  | ||||
|     boolean isAppSecretExpired(String accessKey); | ||||
| } | ||||
| @@ -16,22 +16,22 @@ | ||||
|  | ||||
| package top.continew.admin.open.service.impl; | ||||
|  | ||||
| import cn.hutool.core.codec.Base64; | ||||
| import cn.hutool.core.date.DateUtil; | ||||
| import cn.hutool.core.date.LocalDateTimeUtil; | ||||
| import cn.hutool.core.util.IdUtil; | ||||
| import com.baomidou.mybatisplus.core.toolkit.Wrappers; | ||||
| import lombok.RequiredArgsConstructor; | ||||
|  | ||||
| import org.springframework.stereotype.Service; | ||||
|  | ||||
| import top.continew.admin.open.model.resp.AppSecretGetResp; | ||||
| import top.continew.starter.extension.crud.service.impl.BaseServiceImpl; | ||||
| import top.continew.admin.open.mapper.AppMapper; | ||||
| import top.continew.admin.open.model.entity.AppDO; | ||||
| import top.continew.admin.open.model.query.AppQuery; | ||||
| import top.continew.admin.open.model.req.AppReq; | ||||
| import top.continew.admin.open.model.resp.AppDetailResp; | ||||
| import top.continew.admin.open.model.resp.AppResp; | ||||
| import top.continew.admin.open.model.resp.AppSecretResp; | ||||
| import top.continew.admin.open.service.AppService; | ||||
| import top.continew.starter.core.constant.StringConstants; | ||||
| import top.continew.starter.extension.crud.service.impl.BaseServiceImpl; | ||||
|  | ||||
| import java.time.LocalDateTime; | ||||
|  | ||||
| @@ -39,64 +39,76 @@ import java.time.LocalDateTime; | ||||
|  * 应用业务实现 | ||||
|  * | ||||
|  * @author chengzi | ||||
|  * @author Charles7c | ||||
|  * @since 2024/10/17 16:03 | ||||
|  */ | ||||
| @Service | ||||
| @RequiredArgsConstructor | ||||
| public class AppServiceImpl extends BaseServiceImpl<AppMapper, AppDO, AppResp, AppDetailResp, AppQuery, AppReq> implements AppService { | ||||
|  | ||||
|     // 已激活 | ||||
|     private final static String APP_ENABLED_KEY = "1"; | ||||
|     // 未激活 | ||||
|     private final static String APP_DISABLED_KEY = "0"; | ||||
|     @Override | ||||
|     public void beforeAdd(AppReq req) { | ||||
|         req.setAccessKey(Base64.encode(IdUtil.fastSimpleUUID()) | ||||
|             .replace(StringConstants.SLASH, StringConstants.EMPTY) | ||||
|             .replace("+", StringConstants.EMPTY) | ||||
|             .substring(0, 30)); | ||||
|         req.setSecretKey(this.generateSecret()); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public AppSecretGetResp getAppSecretById(Long id) { | ||||
|         AppDO app = baseMapper.lambdaQuery().eq(AppDO::getId, id).one(); | ||||
|         String appSecret = "********"; | ||||
|         if (app.getSecretStatus().equals(APP_DISABLED_KEY)) { | ||||
|             appSecret = app.getAppSecret(); | ||||
|             this.resetAppSecretStatusById(id, APP_ENABLED_KEY); | ||||
|     public AppSecretResp getSecret(Long id) { | ||||
|         AppDO app = super.getById(id); | ||||
|         AppSecretResp appSecretResp = new AppSecretResp(); | ||||
|         appSecretResp.setAccessKey(app.getAccessKey()); | ||||
|         appSecretResp.setSecretKey(app.getSecretKey()); | ||||
|         return appSecretResp; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void resetSecret(Long id) { | ||||
|         super.getById(id); | ||||
|         AppDO app = new AppDO(); | ||||
|         app.setSecretKey(this.generateSecret()); | ||||
|         baseMapper.update(app, Wrappers.lambdaQuery(AppDO.class).eq(AppDO::getId, id)); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public String getSecretKeyByAccessKey(String accessKey) { | ||||
|         return baseMapper.lambdaQuery() | ||||
|             .select(AppDO::getSecretKey) | ||||
|             .eq(AppDO::getAccessKey, accessKey) | ||||
|             .oneOpt() | ||||
|             .map(AppDO::getSecretKey) | ||||
|             .orElse(null); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public boolean isAppExists(String accessKey) { | ||||
|         return baseMapper.lambdaQuery().eq(AppDO::getAccessKey, accessKey).exists(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public boolean isAppSecretExpired(String accessKey) { | ||||
|         LocalDateTime expireTime = baseMapper.lambdaQuery() | ||||
|             .select(AppDO::getExpireTime) | ||||
|             .eq(AppDO::getAccessKey, accessKey) | ||||
|             .oneOpt() | ||||
|             .map(AppDO::getExpireTime) | ||||
|             .orElse(null); | ||||
|         if (expireTime == null) { | ||||
|             return false; | ||||
|         } | ||||
|         AppSecretGetResp appSecretGetResp = new AppSecretGetResp(); | ||||
|         appSecretGetResp.setAppKey(app.getAppKey()); | ||||
|         appSecretGetResp.setAppSecret(appSecret); | ||||
|         return appSecretGetResp; | ||||
|         return expireTime.isBefore(DateUtil.date().toLocalDateTime()); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void resetAppSecretStatusById(Long id, String status) { | ||||
|         baseMapper.lambdaUpdate().set(AppDO::getSecretStatus, status).eq(AppDO::getId, id).update(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void resetAppSecretStatusByAppkey(String appKey, String status) { | ||||
|         baseMapper.lambdaUpdate().set(AppDO::getSecretStatus, status).eq(AppDO::getAppKey, appKey).update(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void refreshAppSecretByID(Long id) { | ||||
|         baseMapper.lambdaUpdate().set(AppDO::getAppSecret, IdUtil.simpleUUID()).eq(AppDO::getId, id).update(); | ||||
|         this.resetAppSecretStatusById(id, APP_DISABLED_KEY); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public String getAppSecretByAppKey(String appKey) { | ||||
|         return baseMapper.lambdaQuery().select(AppDO::getAppSecret).eq(AppDO::getAppKey, appKey).one().getAppSecret(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public boolean isExistAppKey(String appKey) { | ||||
|         return baseMapper.lambdaQuery().eq(AppDO::getAppKey, appKey).exists(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public boolean isExpireAppKey(String appKey) { | ||||
|         LocalDateTime expirationTime = baseMapper.lambdaQuery() | ||||
|             .select(AppDO::getExpirationTime) | ||||
|             .eq(AppDO::getAppKey, appKey) | ||||
|             .one() | ||||
|             .getExpirationTime(); | ||||
|         return expirationTime.isBefore(LocalDateTimeUtil.of(DateUtil.date())); | ||||
|     /** | ||||
|      * 生成密钥 | ||||
|      * | ||||
|      * @return 密钥 | ||||
|      */ | ||||
|     private String generateSecret() { | ||||
|         return Base64.encode(IdUtil.fastSimpleUUID()) | ||||
|             .replace(StringConstants.SLASH, StringConstants.EMPTY) | ||||
|             .replace("+", StringConstants.EMPTY); | ||||
|     } | ||||
| } | ||||
| @@ -28,12 +28,18 @@ import java.util.TreeMap; | ||||
|  | ||||
| import static cn.dev33.satoken.SaManager.log; | ||||
|  | ||||
| /** | ||||
|  * API 参数签名算法 | ||||
|  * | ||||
|  * @author chengzi | ||||
|  * @since 2024/10/17 16:03 | ||||
|  */ | ||||
| @Component | ||||
| @RequiredArgsConstructor | ||||
| public class OpenSignTemplate extends SaSignTemplate { | ||||
|  | ||||
|     private final AppService appService; | ||||
|     public static String appKey = "appkey"; | ||||
|     public static final String ACCESS_KEY = "accessKey"; | ||||
|  | ||||
|     @Override | ||||
|     public void checkParamMap(Map<String, String> paramMap) { | ||||
| @@ -41,19 +47,19 @@ public class OpenSignTemplate extends SaSignTemplate { | ||||
|         String timestampValue = paramMap.get(timestamp); | ||||
|         String nonceValue = paramMap.get(nonce); | ||||
|         String signValue = paramMap.get(sign); | ||||
|         String appKeyValue = paramMap.get(appKey); | ||||
|         String accessKeyValue = paramMap.get(ACCESS_KEY); | ||||
|  | ||||
|         // 参数非空校验 | ||||
|         SaSignException.notEmpty(timestampValue, "缺少 timestamp 字段"); | ||||
|         SaSignException.notEmpty(nonceValue, "缺少 nonce 字段"); | ||||
|         SaSignException.notEmpty(signValue, "缺少 sign 字段"); | ||||
|         SaSignException.notEmpty(appKeyValue, "缺少 appkey 字段"); | ||||
|         SaSignException.notEmpty(accessKeyValue, "缺少 accessKey 字段"); | ||||
|  | ||||
|         // 应用存在性校验 | ||||
|         SaSignException.notTrue(!appService.isExistAppKey(appKeyValue), "应用不存在"); | ||||
|         SaSignException.notTrue(!appService.isAppExists(ACCESS_KEY), "应用不存在"); | ||||
|  | ||||
|         // 应用是否过期校验 | ||||
|         SaSignException.notTrue(appService.isExpireAppKey(appKeyValue), "应用已过期"); | ||||
|         SaSignException.notTrue(appService.isAppSecretExpired(ACCESS_KEY), "应用已过期"); | ||||
|  | ||||
|         // 依次校验三个参数 | ||||
|         checkTimestamp(Long.parseLong(timestampValue)); | ||||
| @@ -67,7 +73,7 @@ public class OpenSignTemplate extends SaSignTemplate { | ||||
|     public String createSign(Map<String, ?> paramsMap) { | ||||
|         // 根据应用密钥获取对应的应用密码 | ||||
|         String appKey = (String)((Map)paramsMap).get("appkey"); | ||||
|         String secretKey = this.appService.getAppSecretByAppKey(appKey); | ||||
|         String secretKey = this.appService.getSecretKeyByAccessKey(appKey); | ||||
|         SaSignException.notEmpty(secretKey, "参与参数签名的秘钥不可为空", SaErrorCode.CODE_12201); | ||||
|  | ||||
|         // 如果调用者不小心传入了 sign 参数,则此处需要将 sign 参数排除在外 | ||||
|   | ||||
| @@ -41,10 +41,9 @@ | ||||
|         <dependency> | ||||
|             <groupId>top.continew</groupId> | ||||
|             <artifactId>continew-plugin-schedule</artifactId> | ||||
|             <version>${revision}</version> | ||||
|         </dependency> | ||||
|  | ||||
|         <!-- 能力开放插件(包括应用管理、API开放授权、API开发等) --> | ||||
|         <!-- 能力开放插件(后续会改造为独立插件) --> | ||||
|         <dependency> | ||||
|             <groupId>top.continew</groupId> | ||||
|             <artifactId>continew-plugin-open</artifactId> | ||||
|   | ||||
| @@ -20,27 +20,27 @@ import cn.dev33.satoken.annotation.SaCheckPermission; | ||||
| import io.swagger.v3.oas.annotations.Operation; | ||||
| import io.swagger.v3.oas.annotations.Parameter; | ||||
| import io.swagger.v3.oas.annotations.enums.ParameterIn; | ||||
| import lombok.RequiredArgsConstructor; | ||||
| import top.continew.admin.open.model.resp.AppSecretGetResp; | ||||
| import top.continew.starter.extension.crud.enums.Api; | ||||
|  | ||||
| import io.swagger.v3.oas.annotations.tags.Tag; | ||||
|  | ||||
| import org.springframework.web.bind.annotation.*; | ||||
|  | ||||
| import top.continew.starter.extension.crud.annotation.CrudRequestMapping; | ||||
| import top.continew.starter.extension.crud.controller.BaseController; | ||||
| import lombok.RequiredArgsConstructor; | ||||
| import org.springframework.web.bind.annotation.GetMapping; | ||||
| import org.springframework.web.bind.annotation.PatchMapping; | ||||
| import org.springframework.web.bind.annotation.PathVariable; | ||||
| import org.springframework.web.bind.annotation.RestController; | ||||
| import top.continew.admin.open.model.query.AppQuery; | ||||
| import top.continew.admin.open.model.req.AppReq; | ||||
| import top.continew.admin.open.model.resp.AppDetailResp; | ||||
| import top.continew.admin.open.model.resp.AppResp; | ||||
| import top.continew.admin.open.model.resp.AppSecretResp; | ||||
| import top.continew.admin.open.service.AppService; | ||||
| import top.continew.starter.extension.crud.model.resp.BaseIdResp; | ||||
| import top.continew.starter.extension.crud.annotation.CrudRequestMapping; | ||||
| import top.continew.starter.extension.crud.controller.BaseController; | ||||
| import top.continew.starter.extension.crud.enums.Api; | ||||
|  | ||||
| /** | ||||
|  * 应用管理 API | ||||
|  * | ||||
|  * @author chengzi | ||||
|  * @author Charles7c | ||||
|  * @since 2024/10/17 16:03 | ||||
|  */ | ||||
| @Tag(name = "应用管理 API") | ||||
| @@ -49,31 +49,19 @@ import top.continew.starter.extension.crud.model.resp.BaseIdResp; | ||||
| @CrudRequestMapping(value = "/open/app", api = {Api.PAGE, Api.DETAIL, Api.ADD, Api.UPDATE, Api.DELETE, Api.EXPORT}) | ||||
| public class AppController extends BaseController<AppService, AppResp, AppDetailResp, AppQuery, AppReq> { | ||||
|  | ||||
|     private final AppService appService; | ||||
|     private final static String APP_DISABLED_KEY = "0"; | ||||
|  | ||||
|     @Operation(summary = "刷新应用密码", description = "刷新应用密码") | ||||
|     @Parameter(name = "id", description = "ID", example = "test", in = ParameterIn.PATH) | ||||
|     @SaCheckPermission("open:app:refreshas") | ||||
|     @GetMapping(value = "/{id}/refreshas") | ||||
|     public void refreshAppSecret(@PathVariable Long id) { | ||||
|         appService.refreshAppSecretByID(id); | ||||
|     @Operation(summary = "获取密钥", description = "获取应用密钥") | ||||
|     @Parameter(name = "id", description = "ID", example = "1", in = ParameterIn.PATH) | ||||
|     @SaCheckPermission("open:app:secret") | ||||
|     @GetMapping("/{id}/secret") | ||||
|     public AppSecretResp getSecret(@PathVariable Long id) { | ||||
|         return baseService.getSecret(id); | ||||
|     } | ||||
|  | ||||
|     @Operation(summary = "获取应用密码", description = "获取应用密码") | ||||
|     @Parameter(name = "appKey", description = "应用密钥", example = "test", in = ParameterIn.PATH) | ||||
|     @SaCheckPermission("open:app:getas") | ||||
|     @GetMapping("/{id}/appsecret") | ||||
|     public AppSecretGetResp getAppSecret(@PathVariable Long id) { | ||||
|         return appService.getAppSecretById(id); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public BaseIdResp<Long> add(AppReq req) { | ||||
|         BaseIdResp<Long> baseIdResp = super.add(req); | ||||
|         Long appId = baseIdResp.getId(); | ||||
|         appService.refreshAppSecretByID(appId); | ||||
|         appService.resetAppSecretStatusById(appId, APP_DISABLED_KEY); | ||||
|         return baseIdResp; | ||||
|     @Operation(summary = "重置密钥", description = "重置应用密钥") | ||||
|     @Parameter(name = "id", description = "ID", example = "1", in = ParameterIn.PATH) | ||||
|     @SaCheckPermission("open:app:resetSecret") | ||||
|     @PatchMapping("/{id}/secret") | ||||
|     public void resetSecret(@PathVariable Long id) { | ||||
|         baseService.resetSecret(id); | ||||
|     } | ||||
| } | ||||
| @@ -7,8 +7,8 @@ databaseChangeLog: | ||||
|       file: db/changelog/mysql/main_data.sql | ||||
|   - include: | ||||
|       file: db/changelog/mysql/plugin/plugin_schedule.sql | ||||
| #  - include: | ||||
| #      file: db/changelog/mysql/plugin/plugin_open.sql | ||||
|   - include: | ||||
|       file: db/changelog/mysql/plugin/plugin_open.sql | ||||
|   - include: | ||||
|       file: db/changelog/mysql/plugin/plugin_generator.sql | ||||
| # PostgreSQL | ||||
|   | ||||
| @@ -4,19 +4,19 @@ | ||||
| -- comment 初始化能力开放插件 | ||||
| -- 初始化表结构 | ||||
| CREATE TABLE IF NOT EXISTS `sys_app`  ( | ||||
|     `id`              bigint(20)   NOT NULL AUTO_INCREMENT COMMENT 'ID', | ||||
|     `name`            varchar(255) DEFAULT NULL            COMMENT '应用名称', | ||||
|     `app_key`         varchar(255) DEFAULT NULL            COMMENT '应用密钥', | ||||
|     `app_secret`      varchar(255) DEFAULT NULL            COMMENT '应用密码', | ||||
|     `status`          varchar(255) DEFAULT NULL            COMMENT '应用状态 (0: 未激活;1: 激活)', | ||||
|     `expiration_time` datetime     DEFAULT NULL            COMMENT '失效时间', | ||||
|     `app_desc`        varchar(255) DEFAULT NULL            COMMENT '应用描述', | ||||
|     `secret_status`   varchar(255) DEFAULT NULL            COMMENT '应用密码查看状态 (0: 未查看;1: 已查看)', | ||||
|     `create_user`     bigint(20)   NOT NULL                COMMENT '创建人', | ||||
|     `create_time`     datetime     NOT NULL                COMMENT '创建时间', | ||||
|     `update_user`     bigint(20)   DEFAULT NULL            COMMENT '修改人', | ||||
|     `update_time`     datetime     DEFAULT NULL            COMMENT '修改时间', | ||||
|     `id`          bigint(20)   NOT NULL     AUTO_INCREMENT COMMENT 'ID', | ||||
|     `name`        varchar(100) NOT NULL                    COMMENT '名称', | ||||
|     `access_key`  varchar(255) NOT NULL                    COMMENT 'Access Key(访问密钥)', | ||||
|     `secret_key`  varchar(255) NOT NULL                    COMMENT 'Secret Key(私有密钥)', | ||||
|     `expire_time` datetime     DEFAULT NULL                COMMENT '失效时间', | ||||
|     `description` varchar(200) DEFAULT NULL                COMMENT '描述', | ||||
|     `status`      tinyint(1)   UNSIGNED NOT NULL DEFAULT 1 COMMENT '状态(1:启用;2:禁用)', | ||||
|     `create_user` bigint(20)   NOT NULL                    COMMENT '创建人', | ||||
|     `create_time` datetime     NOT NULL                    COMMENT '创建时间', | ||||
|     `update_user` bigint(20)   DEFAULT NULL                COMMENT '修改人', | ||||
|     `update_time` datetime     DEFAULT NULL                COMMENT '修改时间', | ||||
|     PRIMARY KEY (`id`), | ||||
|     UNIQUE INDEX `uk_access_key`(`access_key`), | ||||
|     INDEX `idx_create_user`(`create_user`), | ||||
|     INDEX `idx_update_user`(`update_user`) | ||||
| ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='应用表'; | ||||
| @@ -32,5 +32,6 @@ VALUES | ||||
| (7013, '新增', 7010, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'open:app:add', 3, 1, 1, NOW(), NULL, NULL), | ||||
| (7014, '修改', 7010, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'open:app:update', 4, 1, 1, NOW(), NULL, NULL), | ||||
| (7015, '删除', 7010, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'open:app:delete', 5, 1, 1, NOW(), NULL, NULL), | ||||
| (7016, '导出', 7010, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'open:app:export', 6, 1, 1, NOW(), NULL, NULL); | ||||
|  | ||||
| (7016, '导出', 7010, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'open:app:export', 6, 1, 1, NOW(), NULL, NULL), | ||||
| (7017, '查看密钥', 7010, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'open:app:secret', 7, 1, 1, NOW(), NULL, NULL), | ||||
| (7018, '重置密钥', 7010, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'open:app:resetSecret', 8, 1, 1, NOW(), NULL, NULL); | ||||
|   | ||||
| @@ -4,30 +4,29 @@ | ||||
| -- comment 初始化能力开放插件 | ||||
| -- 初始化表结构 | ||||
| CREATE TABLE IF NOT EXISTS "sys_app" ( | ||||
|     "id"              int8         DEFAULT NULL, | ||||
|     "name"            varchar(255) DEFAULT NULL, | ||||
|     "app_key"         varchar(255) DEFAULT NULL, | ||||
|     "app_secret"      varchar(255) DEFAULT NULL, | ||||
|     "status"          varchar(255) DEFAULT NULL, | ||||
|     "expiration_time" timestamp    DEFAULT NULL, | ||||
|     "app_desc"        varchar(255) DEFAULT NULL, | ||||
|     "secret_status"   varchar(255) DEFAULT NULL, | ||||
|     "create_user"     int8         NOT NULL, | ||||
|     "create_time"     timestamp    NOT NULL, | ||||
|     "update_user"     int8         NOT NULL, | ||||
|     "update_time"     timestamp    NOT NULL, | ||||
|     "id"          int8         NOT NULL, | ||||
|     "name"        varchar(100) NOT NULL, | ||||
|     "access_key"  varchar(255) NOT NULL, | ||||
|     "secret_key"  varchar(255) NOT NULL, | ||||
|     "expire_time" timestamp    DEFAULT NULL, | ||||
|     "description" varchar(200) DEFAULT NULL, | ||||
|     "status"      int2         NOT NULL DEFAULT 1, | ||||
|     "create_user" int8         NOT NULL, | ||||
|     "create_time" timestamp    NOT NULL, | ||||
|     "update_user" int8         DEFAULT NULL, | ||||
|     "update_time" timestamp    DEFAULT NULL, | ||||
|     PRIMARY KEY ("id") | ||||
| ); | ||||
| CREATE UNIQUE INDEX "uk_app_access_key" ON "sys_app" ("access_key"); | ||||
| CREATE INDEX "idx_app_create_user" ON "sys_app" ("create_user"); | ||||
| CREATE INDEX "idx_app_update_user" ON "sys_app" ("update_user"); | ||||
| COMMENT ON COLUMN "sys_app"."id"              IS 'ID'; | ||||
| COMMENT ON COLUMN "sys_app"."name"            IS '名称'; | ||||
| COMMENT ON COLUMN "sys_app"."app_key"         IS '应用密钥'; | ||||
| COMMENT ON COLUMN "sys_app"."app_secret"      IS '应用密码'; | ||||
| COMMENT ON COLUMN "sys_app"."status"          IS '应用状态 (0: 未激活;1: 激活)'; | ||||
| COMMENT ON COLUMN "sys_app"."expiration_time" IS '失效时间'; | ||||
| COMMENT ON COLUMN "sys_app"."app_desc"        IS '应用描述'; | ||||
| COMMENT ON COLUMN "sys_app"."secret_status"   IS '应用密码查看状态 (0: 未查看;1: 已查看)'; | ||||
| COMMENT ON COLUMN "sys_app"."access_key"      IS 'Access Key(访问密钥)'; | ||||
| COMMENT ON COLUMN "sys_app"."secret_key"      IS 'Secret Key(私有密钥)'; | ||||
| COMMENT ON COLUMN "sys_app"."expire_time"     IS '失效时间'; | ||||
| COMMENT ON COLUMN "sys_app"."description"     IS '描述'; | ||||
| COMMENT ON COLUMN "sys_app"."status"          IS '状态(1:启用;2:禁用)'; | ||||
| COMMENT ON COLUMN "sys_app"."create_user"     IS '创建人'; | ||||
| COMMENT ON COLUMN "sys_app"."create_time"     IS '创建时间'; | ||||
| COMMENT ON COLUMN "sys_app"."update_user"     IS '修改人'; | ||||
| @@ -45,5 +44,6 @@ VALUES | ||||
| (7013, '新增', 7010, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'open:app:add', 3, 1, 1, NOW(), NULL, NULL), | ||||
| (7014, '修改', 7010, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'open:app:update', 4, 1, 1, NOW(), NULL, NULL), | ||||
| (7015, '删除', 7010, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'open:app:delete', 5, 1, 1, NOW(), NULL, NULL), | ||||
| (7016, '导出', 7010, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'open:app:export', 6, 1, 1, NOW(), NULL, NULL); | ||||
|  | ||||
| (7016, '导出', 7010, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'open:app:export', 6, 1, 1, NOW(), NULL, NULL), | ||||
| (7017, '查看密钥', 7010, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'open:app:secret', 7, 1, 1, NOW(), NULL, NULL), | ||||
| (7018, '重置密钥', 7010, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 'open:app:resetSecret', 8, 1, 1, NOW(), NULL, NULL); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user