This commit is contained in:
六如
2024-10-04 21:48:38 +08:00
parent 00da3cc0a9
commit c08fec74c9
987 changed files with 24735 additions and 22137 deletions

View File

@@ -3,18 +3,13 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>com.gitee.sop</groupId>
<artifactId>sop-parent</artifactId>
<version>5.0.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath> <!-- lookup parent from repository -->
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>com.gitee.sop</groupId>
<artifactId>sop-admin</artifactId>
<version>5.0.0-SNAPSHOT</version>
<packaging>pom</packaging>
<modules>
<module>sop-admin-server</module>
<module>sop-admin-backend</module>
</modules>
</project>

33
sop-admin/sop-admin-backend/.gitignore vendored Normal file
View File

@@ -0,0 +1,33 @@
HELP.md
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/

View File

@@ -0,0 +1,294 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.15</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.gitee.sop</groupId>
<artifactId>sop-admin-backend</artifactId>
<version>5.0.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- springboot 版本-->
<spring-boot.version>2.6.15</spring-boot.version>
<!-- spring cloud 版本 -->
<spring-cloud.version>2021.0.5</spring-cloud.version>
<!-- spring cloud alibaba 版本 -->
<!-- 具体版本对应关系见https://github.com/alibaba/spring-cloud-alibaba/wiki/%E7%89%88%E6%9C%AC%E8%AF%B4%E6%98%8E -->
<spring-cloud-alibaba.version>2021.0.5.0</spring-cloud-alibaba.version>
<!-- dubbo版本 -->
<dubbo.version>3.2.10</dubbo.version>
<junit.version>4.11</junit.version>
<commons-io.version>2.5</commons-io.version>
<commons-fileupload.version>1.3.3</commons-fileupload.version>
<commons-collection.version>3.2.2</commons-collection.version>
<commons-lang3.version>3.8.1</commons-lang3.version>
<commons-codec.version>1.11</commons-codec.version>
<commons-logging.version>1.2</commons-logging.version>
<validation-api.version>2.0.1.Final</validation-api.version>
<hibernate-validator.version>6.0.13.Final</hibernate-validator.version>
<fastmybatis.version>3.0.10</fastmybatis.version>
</properties>
<dependencies>
<dependency>
<groupId>com.gitee.sop</groupId>
<artifactId>sop-service-support</artifactId>
<version>5.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
</dependency>
<!-- nacos注册中心 -->
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-nacos-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>io.gitee.durcframework</groupId>
<artifactId>fastmybatis-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
</dependency>
<!-- test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- provided -->
<!-- 仅在开发中使用 -->
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-dependencies-zookeeper-curator5</artifactId>
<version>${dubbo.version}</version>
<type>pom</type>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<!-- 加了这个就不需要加版本号了 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-bom</artifactId>
<version>${dubbo.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>io.gitee.durcframework</groupId>
<artifactId>fastmybatis-spring-boot-starter</artifactId>
<version>${fastmybatis.version}</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>3.14.7</version>
</dependency>
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>${validation-api.version}</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>${hibernate-validator.version}</version>
</dependency>
<!-- commons -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${commons-lang3.version}</version>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>${commons-collection.version}</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>${commons-io.version}</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>${commons-codec.version}</version>
</dependency>
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>${commons-fileupload.version}</version>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>${commons-logging.version}</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
</dependency>
</dependencies>
</dependencyManagement>
<repositories>
<repository>
<id>aliyun</id>
<name>aliyun</name>
<url>https://maven.aliyun.com/repository/public</url>
</repository>
</repositories>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<!-- 打包时跳过测试 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.12.4</version>
<configuration>
<skipTests>true</skipTests>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,15 @@
package com.gitee.sop.adminbackend;
import org.apache.dubbo.config.spring.context.annotation.EnableDubbo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@EnableDubbo
public class SopAdminBackendApplication {
public static void main(String[] args) {
SpringApplication.run(SopAdminBackendApplication.class, args);
}
}

View File

@@ -0,0 +1,22 @@
package com.gitee.sop.adminbackend.common;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @author 六如
*/
@AllArgsConstructor
@Getter
public enum ConfigKeyEnum {
PASSWORD_SALT("admin.password-salt", ""),
JWT_TIMEOUT_DAYS("admin.jwt-timeout-days", "365"),
JWT_SECRET("admin.jwt.secret", ""),
;
private final String key;
private final String defaultValue;
}

View File

@@ -0,0 +1,9 @@
package com.gitee.sop.adminbackend.common;
public interface IConfig {
String getConfig(String key);
String getConfig(String key, String defaultValue);
}

View File

@@ -0,0 +1,46 @@
package com.gitee.sop.adminbackend.common;
import lombok.Data;
import java.util.Objects;
/**
* @author thc
*/
@Data
public class Result<T> {
private static final Result<?> RESULT = new Result<>();
private String code = "0";
private T data;
private String msg = "success";
public static Result<?> ok() {
return RESULT;
}
public static <E> Result<E> ok(E obj) {
Result<E> result = new Result<>();
result.setData(obj);
return result;
}
public static <E> Result<E> err(String msg) {
Result<E> result = new Result<>();
result.setCode("1");
result.setMsg(msg);
return result;
}
public static <E> Result<E> err(String code, String msg) {
Result<E> result = new Result<>();
result.setCode(code);
result.setMsg(msg);
return result;
}
public boolean getSuccess() {
return Objects.equals("0", code);
}
}

View File

@@ -0,0 +1,32 @@
package com.gitee.sop.adminbackend.common;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationEvent;
/**
* @author 六如
*/
public class SpringContext {
private static ApplicationContext ctx;
public static <T> T getBean(Class<T> clazz) {
return ctx.getBean(clazz);
}
public static Object getBean(String beanName) {
return ctx.getBean(beanName);
}
public static void setApplicationContext(ApplicationContext ctx) {
SpringContext.ctx = ctx;
}
public static ApplicationContext getApplicationContext() {
return ctx;
}
public static void publishEvent(ApplicationEvent event) {
ctx.publishEvent(event);
}
}

View File

@@ -0,0 +1,24 @@
package com.gitee.sop.adminbackend.common;
/**
* 登录用户信息
* @author 六如
*/
public interface User {
/**
* 用户id
* @return
*/
Long getUserId();
/**
* 昵称
* @return
*/
String getNickname();
Integer getStatus();
String getToken();
}

View File

@@ -0,0 +1,10 @@
package com.gitee.sop.adminbackend.common.exception;
/**
* @author tanghc
*/
public class BizException extends RuntimeException {
public BizException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,30 @@
package com.gitee.sop.adminbackend.common.exception;
/**
* @author tanghc
*/
public enum ErrorCode {
// 1000: 登录失败
LOGIN_FAIL("1000", "login error"),
JWT_CREATE("1000", "create token error"),
JWT_ERROR("1000", "invalid token"),
JWT_EXPIRED("1000", "token expired"),
SET_PASSWORD("2000", "set password"),
;
ErrorCode(String code, String msg) {
this.code = code;
this.msg = msg;
}
private final String code;
private final String msg;
public String getCode() {
return code;
}
public String getMsg() {
return msg;
}
}

View File

@@ -0,0 +1,7 @@
package com.gitee.sop.adminbackend.common.exception;
/**
* @author tanghc
*/
public class ErrorTokenException extends Exception {
}

View File

@@ -0,0 +1,8 @@
package com.gitee.sop.adminbackend.common.exception;
/**
* @author tanghc
*/
public interface ExceptionCode {
ErrorCode getCode();
}

View File

@@ -0,0 +1,11 @@
package com.gitee.sop.adminbackend.common.exception;
/**
* @author tanghc
*/
public class JwtCreateException extends RuntimeException implements ExceptionCode {
@Override
public ErrorCode getCode() {
return ErrorCode.JWT_CREATE;
}
}

View File

@@ -0,0 +1,12 @@
package com.gitee.sop.adminbackend.common.exception;
/**
* @author tanghc
*/
public class JwtErrorException extends Exception {
@Override
public String getMessage() {
return "jwt verify error";
}
}

View File

@@ -0,0 +1,11 @@
package com.gitee.sop.adminbackend.common.exception;
/**
* @author tanghc
*/
public class JwtExpiredException extends Exception {
@Override
public String getMessage() {
return "jwt expired";
}
}

View File

@@ -0,0 +1,18 @@
package com.gitee.sop.adminbackend.common.exception;
/**
* @author tanghc
*/
public class LoginFailureException extends RuntimeException implements ExceptionCode {
@Override
public ErrorCode getCode() {
return ErrorCode.LOGIN_FAIL;
}
public LoginFailureException(String message) {
super(message);
}
public LoginFailureException() {
}
}

View File

@@ -0,0 +1,11 @@
package com.gitee.sop.adminbackend.common.exception;
/**
* @author tanghc
*/
public class SetPasswordException extends RuntimeException implements ExceptionCode {
@Override
public ErrorCode getCode() {
return ErrorCode.SET_PASSWORD;
}
}

View File

@@ -0,0 +1,17 @@
package com.gitee.sop.adminbackend.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* @author 六如
*/
@Configuration
@ConfigurationProperties(prefix = "admin")
@Data
public class AdminConfig {
private int jwtTimeoutDays;
}

View File

@@ -0,0 +1,46 @@
package com.gitee.sop.adminbackend.config;
import com.gitee.sop.adminbackend.common.ConfigKeyEnum;
import com.gitee.sop.adminbackend.common.IConfig;
import com.gitee.sop.adminbackend.common.SpringContext;
import java.util.function.Supplier;
/**
* @author 六如
*/
public class Configs {
/**
* 获取配置参数
*
* @param keyGetter 配置key
* @return 返回配参数没有则返回null
*/
public static String getValue(ConfigKeyEnum keyGetter) {
return getValue(keyGetter, keyGetter.getDefaultValue());
}
/**
* 获取配置参数
*
* @param keyGetter 配置key
* @param defaultValue 默认值
* @return 返回配参数,没有则返回默认值
*/
public static String getValue(ConfigKeyEnum keyGetter, String defaultValue) {
return SpringContext.getBean(IConfig.class).getConfig(keyGetter.getKey(), defaultValue);
}
/**
* 获取配置参数
*
* @param keyGetter 配置key
* @param defaultValue 默认值
* @return 返回配参数,没有则返回默认值
*/
public static String getValue(ConfigKeyEnum keyGetter, Supplier<String> defaultValue) {
return getValue(keyGetter, defaultValue.get());
}
}

View File

@@ -0,0 +1,18 @@
package com.gitee.sop.adminbackend.config;
import com.gitee.sop.adminbackend.common.SpringContext;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Configuration;
/**
* @author 六如
*/
@Configuration
public class SopAdminConfiguration implements ApplicationContextAware {
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
SpringContext.setApplicationContext(applicationContext);
}
}

View File

@@ -0,0 +1,52 @@
package com.gitee.sop.adminbackend.controller;
import com.gitee.sop.adminbackend.common.Result;
import com.gitee.sop.adminbackend.common.exception.BizException;
import com.gitee.sop.adminbackend.common.exception.ErrorCode;
import com.gitee.sop.adminbackend.common.exception.ExceptionCode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
import java.util.stream.Collectors;
/**
* 全局错误处理
*
* @author tanghc
*/
@RestControllerAdvice
@Slf4j
public class ExceptionHandlerController {
@ExceptionHandler(Exception.class)
public Object exceptionHandler(HttpServletRequest request, Exception e) {
if (e instanceof ExceptionCode) {
ExceptionCode exceptionCode = (ExceptionCode) e;
ErrorCode errorCode = exceptionCode.getCode();
log.error("报错code:{}, msg:{}", errorCode.getCode(), errorCode.getMsg(), e);
return Result.err(errorCode.getCode(), errorCode.getMsg());
}
if (e instanceof BizException || e instanceof IllegalArgumentException) {
RuntimeException bizException = (RuntimeException) e;
return Result.err(bizException.getMessage());
}
// 处理JSR-303错误
if (e instanceof MethodArgumentNotValidException) {
MethodArgumentNotValidException exception = (MethodArgumentNotValidException) e;
List<ObjectError> allErrors = exception.getBindingResult().getAllErrors();
String msg = allErrors.stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.collect(Collectors.joining(", "));
return Result.err(msg);
}
log.error("未知错误URI{}HttpMethod{}", request.getRequestURI(), request.getMethod(), e);
return Result.err("系统错误,请查看日志");
}
}

View File

@@ -0,0 +1,47 @@
package com.gitee.sop.adminbackend.controller.sys;
import com.gitee.sop.adminbackend.common.Result;
import com.gitee.sop.adminbackend.controller.sys.req.LoginParam;
import com.gitee.sop.adminbackend.controller.sys.resp.LoginResultVO;
import com.gitee.sop.adminbackend.service.sys.login.LoginService;
import com.gitee.sop.adminbackend.service.sys.login.dto.LoginDTO;
import com.gitee.sop.adminbackend.service.sys.login.dto.LoginUser;
import com.gitee.sop.adminbackend.service.sys.login.enums.RegTypeEnum;
import com.gitee.sop.adminbackend.util.CopyUtil;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
/**
* 登录相关接口
*
* @author 六如
*/
@RestController
@RequestMapping("sys")
public class LoginController {
@Resource
private LoginService loginService;
/**
* 用户登录
*
* @param param
* @return
*/
@PostMapping("login")
public Result<LoginResultVO> login(@Validated @RequestBody LoginParam param) {
LoginDTO loginDTO = new LoginDTO();
loginDTO.setUsername(param.getUsername());
loginDTO.setPassword(param.getPassword());
loginDTO.setRegType(RegTypeEnum.BACKEND);
LoginUser loginUser = loginService.login(loginDTO);
LoginResultVO loginResult = CopyUtil.copyBean(loginUser, LoginResultVO::new);
return Result.ok(loginResult);
}
}

View File

@@ -0,0 +1,76 @@
package com.gitee.sop.adminbackend.controller.sys;
import com.gitee.fastmybatis.core.PageInfo;
import com.gitee.fastmybatis.core.query.Query;
import com.gitee.fastmybatis.core.query.param.PageParam;
import com.gitee.sop.adminbackend.common.Result;
import com.gitee.sop.adminbackend.dao.entity.SysAdminUser;
import com.gitee.sop.adminbackend.service.sys.SysAdminUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author 六如
*/
@RestController
@RequestMapping("sys/adminuser")
public class SysAdminUserController {
@Autowired
private SysAdminUserService sysAdminUserService;
/**
* 分页查询
*
* @param param
* @return
*/
@GetMapping("/page")
public Result<PageInfo<SysAdminUser>> page(PageParam param) {
Query query = param.toQuery();
PageInfo<SysAdminUser> pageInfo = sysAdminUserService.page(query);
return Result.ok(pageInfo);
}
/**
* 新增记录
*
* @param user
* @return
*/
@PostMapping("/save")
public Result<Long> save(SysAdminUser user) {
sysAdminUserService.save(user);
// 返回添加后的主键值
return Result.ok(user.getId());
}
/**
* 修改记录
*
* @param user 表单数据
* @return
*/
@PutMapping("/update")
public Result<Integer> update(SysAdminUser user) {
return Result.ok(sysAdminUserService.update(user));
}
/**
* 删除记录
*
* @param id 主键id
* @return
*/
@DeleteMapping("/delete")
public Result<?> delete(Long id) {
return Result.ok(sysAdminUserService.deleteById(id));
}
}

View File

@@ -0,0 +1,14 @@
package com.gitee.sop.adminbackend.controller.sys.req;
import lombok.Data;
import javax.validation.constraints.NotBlank;
@Data
public class LoginParam {
@NotBlank(message = "用户名不能为空")
private String username;
@NotBlank(message = "密码不能为空")
private String password;
}

View File

@@ -0,0 +1,53 @@
package com.gitee.sop.adminbackend.controller.sys.resp;
import lombok.Data;
import java.util.Set;
/**
* @author 六如
*/
@Data
public class LoginResultVO {
private Long id;
/**
* 用户名
*/
private String username;
/**
* 用户名
*/
private String nickname;
/**
* 密码
*/
private String password;
/**
* 头像
*/
private String avatar;
/**
* 注册类型
*/
private Integer regType;
/**
* 状态1启用0禁用
*/
private Integer status;
private Set<String> roles;
private Set<String> permissions;
private String accessToken;
private String refreshToken;
private String expires;
}

View File

@@ -0,0 +1,73 @@
package com.gitee.sop.adminbackend.dao.entity;
import com.gitee.fastmybatis.annotation.Pk;
import com.gitee.fastmybatis.annotation.PkStrategy;
import com.gitee.fastmybatis.annotation.Table;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 表名api_info
* 备注:接口信息表
*
* @author 六如
*/
@Table(name = "api_info", pk = @Pk(name = "id", strategy = PkStrategy.INCREMENT))
@Data
public class ApiInfo {
private Long id;
/**
* 应用名称
*/
private String application;
/**
* 接口名称
*/
private String apiName;
/**
* 版本号
*/
private String apiVersion;
/**
* 接口class
*/
private String interfaceClassName;
/**
* 方法名称
*/
private String methodName;
/**
* 参数信息
*/
private String paramInfo;
/**
* 接口是否需要授权访问
*/
private Integer isPermission;
/**
* 是否需要appAuthToken
*/
private Integer isNeedToken;
/**
* 状态,1-启用,0-禁用
*/
private Integer status;
private LocalDateTime addTime;
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,43 @@
package com.gitee.sop.adminbackend.dao.entity;
import com.gitee.fastmybatis.annotation.Pk;
import com.gitee.fastmybatis.annotation.PkStrategy;
import com.gitee.fastmybatis.annotation.Table;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 表名isv_info
* 备注isv信息表
*
* @author 六如
*/
@Table(name = "isv_info", pk = @Pk(name = "id", strategy = PkStrategy.INCREMENT))
@Data
public class IsvInfo {
private Long id;
/**
* appKey
*/
private String appId;
/**
* 1启用2禁用
*/
private Integer status;
/**
* 备注
*/
private String remark;
private LocalDateTime addTime;
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,55 @@
package com.gitee.sop.adminbackend.dao.entity;
import com.gitee.fastmybatis.annotation.Pk;
import com.gitee.fastmybatis.annotation.PkStrategy;
import com.gitee.fastmybatis.annotation.Table;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 表名isv_keys
* 备注ISV秘钥管理
*
* @author 六如
*/
@Table(name = "isv_keys", pk = @Pk(name = "id", strategy = PkStrategy.INCREMENT))
@Data
public class IsvKeys {
private Long id;
private String appId;
/**
* 秘钥格式1PKCS8(JAVA适用)2PKCS1(非JAVA适用)
*/
private Integer keyFormat;
/**
* 开发者生成的公钥
*/
private String publicKeyIsv;
/**
* 开发者生成的私钥(交给开发者)
*/
private String privateKeyIsv;
/**
* 平台生成的公钥(交给开发者)
*/
private String publicKeyPlatform;
/**
* 平台生成的私钥
*/
private String privateKeyPlatform;
private LocalDateTime addTime;
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,64 @@
package com.gitee.sop.adminbackend.dao.entity;
import java.time.LocalDateTime;
import com.gitee.fastmybatis.annotation.Pk;
import com.gitee.fastmybatis.annotation.PkStrategy;
import com.gitee.fastmybatis.annotation.Table;
import lombok.Data;
/**
* 表名sys_admin_user
* 备注:系统用户表
*
* @author 六如
*/
@Table(name = "sys_admin_user", pk = @Pk(name = "id", strategy = PkStrategy.INCREMENT))
@Data
public class SysAdminUser {
private Long id;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 用户名
*/
private String nickname;
/**
* 邮箱
*/
private String email;
/**
* 头像
*/
private String avatar;
/**
* 状态1启用0禁用
*/
private Integer status;
/**
* 注册类型
*/
private String regType;
private LocalDateTime addTime;
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,38 @@
package com.gitee.sop.adminbackend.dao.entity;
import java.time.LocalDateTime;
import com.gitee.fastmybatis.annotation.Pk;
import com.gitee.fastmybatis.annotation.PkStrategy;
import com.gitee.fastmybatis.annotation.Table;
import lombok.Data;
/**
* 表名sys_config
* 备注:系统配置表
*
* @author 六如
*/
@Table(name = "sys_config", pk = @Pk(name = "id", strategy = PkStrategy.INCREMENT))
@Data
public class SysConfig {
private Long id;
private String configKey;
private String configValue;
private String remark;
@com.gitee.fastmybatis.annotation.Column(logicDelete = true)
private Integer isDeleted;
private LocalDateTime addTime;
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,19 @@
package com.gitee.sop.adminbackend.dao.mapper;
import com.gitee.fastmybatis.core.mapper.BaseMapper;
import com.gitee.sop.adminbackend.dao.entity.ApiInfo;
/**
* @author 六如
*/
public interface ApiInfoMapper extends BaseMapper<ApiInfo> {
default ApiInfo getByNameVersion(String apiName, String apiVersion) {
return this.query()
.eq(ApiInfo::getApiName, apiName)
.eq(ApiInfo::getApiVersion, apiVersion)
.get();
}
}

View File

@@ -0,0 +1,15 @@
package com.gitee.sop.adminbackend.dao.mapper;
import com.gitee.fastmybatis.core.mapper.BaseMapper;
import com.gitee.sop.adminbackend.dao.entity.IsvInfo;
/**
* @author 六如
*/
public interface IsvInfoMapper extends BaseMapper<IsvInfo> {
default IsvInfo getByAppId(String appId) {
return this.get(IsvInfo::getAppId, appId);
}
}

View File

@@ -0,0 +1,15 @@
package com.gitee.sop.adminbackend.dao.mapper;
import com.gitee.fastmybatis.core.mapper.BaseMapper;
import com.gitee.sop.adminbackend.dao.entity.IsvKeys;
/**
* @author 六如
*/
public interface IsvKeysMapper extends BaseMapper<IsvKeys> {
default IsvKeys getByAppId(String appId) {
return this.get(IsvKeys::getAppId, appId);
}
}

View File

@@ -0,0 +1,11 @@
package com.gitee.sop.adminbackend.dao.mapper;
import com.gitee.fastmybatis.core.mapper.BaseMapper;
import com.gitee.sop.adminbackend.dao.entity.SysAdminUser;
/**
* @author 六如
*/
public interface SysAdminUserMapper extends BaseMapper<SysAdminUser> {
}

View File

@@ -0,0 +1,11 @@
package com.gitee.sop.adminbackend.dao.mapper;
import com.gitee.fastmybatis.core.mapper.BaseMapper;
import com.gitee.sop.adminbackend.dao.entity.SysConfig;
/**
* @author 六如
*/
public interface SysConfigMapper extends BaseMapper<SysConfig> {
}

View File

@@ -0,0 +1,27 @@
package com.gitee.sop.adminbackend.dao.mapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
import java.util.Map;
/**
* @author tanghc
*/
@Mapper
public interface UpgradeMapper {
void runSql(@Param("sql") String sql);
/**
* 查看MYSQL表字段信息
* @param tableName 表名
* @return 返回字段信息
*/
List<Map<String, Object>> listColumnInfo(@Param("tableName") String tableName);
List<String> listTableName();
List<Map<String, Object>> listTableIndex(@Param("tableName") String tableName);
}

View File

@@ -0,0 +1,40 @@
package com.gitee.sop.adminbackend.service.sys;
import com.gitee.fastmybatis.core.support.LambdaService;
import com.gitee.sop.adminbackend.common.ConfigKeyEnum;
import com.gitee.sop.adminbackend.config.AdminConfig;
import com.gitee.sop.adminbackend.config.Configs;
import com.gitee.sop.adminbackend.dao.entity.SysAdminUser;
import com.gitee.sop.adminbackend.dao.mapper.SysAdminUserMapper;
import com.gitee.sop.adminbackend.util.GenerateUtil;
import org.springframework.stereotype.Service;
/**
* @author 六如
*/
@Service
public class SysAdminUserService implements LambdaService<SysAdminUser, SysAdminUserMapper> {
public SysAdminUser getByUsername(String username) {
return this.get(SysAdminUser::getUsername, username);
}
public String getDbPassword(String username, String password) {
return getDbPassword(username, password, getPasswordSalt());
}
public String getDbPassword(String username) {
return this.query().eq(SysAdminUser::getUsername, username).getValue(SysAdminUser::getPassword);
}
public String getDbPassword(String username, String password, String salt) {
return GenerateUtil.getUserPassword(username, password, salt);
}
private String getPasswordSalt() {
return Configs.getValue(ConfigKeyEnum.PASSWORD_SALT);
}
}

View File

@@ -0,0 +1,102 @@
package com.gitee.sop.adminbackend.service.sys;
import com.gitee.fastmybatis.core.support.BaseLambdaService;
import com.gitee.sop.adminbackend.common.IConfig;
import com.gitee.sop.adminbackend.dao.entity.SysConfig;
import com.gitee.sop.adminbackend.dao.mapper.SysConfigMapper;
import com.gitee.sop.adminbackend.service.sys.dto.SystemConfigDTO;
import com.gitee.sop.adminbackend.util.CopyUtil;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
/**
* @author 六如
*/
@Service
public class SysConfigService extends BaseLambdaService<SysConfig, SysConfigMapper> implements IConfig {
@Resource
private Environment environment;
// key: configKey, value: configValue
private final LoadingCache<String, Optional<String>> configCache = CacheBuilder.newBuilder()
.expireAfterAccess(15, TimeUnit.MINUTES)
.build(new CacheLoader<String, Optional<String>>() {
@Override
public Optional<String> load(String key) throws Exception {
return Optional.ofNullable(getConfigValue(key, null));
}
});
public String getRawValue(String key) {
return this.query()
.eq(SysConfig::getConfigKey, key)
.getValue(SysConfig::getConfigValue);
}
public void setConfig(String key, String value) {
setConfig(key, value, "");
}
public void setConfig(String key, String value, String remark) {
SystemConfigDTO systemConfigDTO = new SystemConfigDTO();
systemConfigDTO.setConfigKey(key);
systemConfigDTO.setConfigValue(value);
systemConfigDTO.setRemark(remark);
setConfig(systemConfigDTO);
}
public void setConfig(SystemConfigDTO systemConfigDTO) {
Objects.requireNonNull(systemConfigDTO.getConfigKey(), "need key");
Objects.requireNonNull(systemConfigDTO.getConfigValue(), "need value");
SysConfig systemConfig = get(SysConfig::getConfigKey, systemConfigDTO.getConfigKey());
if (systemConfig == null) {
systemConfig = CopyUtil.copyBean(systemConfigDTO, SysConfig::new);
this.save(systemConfig);
} else {
CopyUtil.copyPropertiesIgnoreNull(systemConfigDTO, systemConfig);
this.update(systemConfig);
}
configCache.invalidate(systemConfigDTO.getConfigKey());
}
/**
* 获取配置信息
* <pre>
* 优先级:
* 数据库
* Environment
* 默认配置
* </pre>
*
* @param key 配置key
* @param defaultValue 没有获取到返回的默认值
* @return 返回配置信息,如果没有获取到值,则返回默认值
*/
public String getConfigValue(String key, String defaultValue) {
Objects.requireNonNull(key, "need key");
SysConfig systemConfig = get(SysConfig::getConfigKey, key);
return Optional.ofNullable(systemConfig)
.map(SysConfig::getConfigValue)
.orElseGet(() -> environment.getProperty(key, defaultValue));
}
@Override
public String getConfig(String key) {
return configCache.getUnchecked(key).orElse(null);
}
@Override
public String getConfig(String key, String defaultValue) {
return configCache.getUnchecked(key).orElse(defaultValue);
}
}

View File

@@ -0,0 +1,67 @@
package com.gitee.sop.adminbackend.service.sys;
import com.gitee.sop.adminbackend.common.ConfigKeyEnum;
import com.gitee.sop.adminbackend.dao.entity.SysAdminUser;
import com.gitee.sop.adminbackend.dao.mapper.UpgradeMapper;
import com.gitee.sop.adminbackend.service.sys.login.enums.RegTypeEnum;
import com.gitee.sop.adminbackend.util.PasswordUtil;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.crypto.bcrypt.BCrypt;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
/**
* @author 六如
*/
@Service
public class UpgradeService {
@Resource
private SysConfigService sysConfigService;
@Resource
private SysAdminUserService sysAdminUserService;
@Resource
private UpgradeMapper upgradeMapper;
@PostConstruct
public void init() {
this.initJwtSecret();
this.insertAdmin();
}
private void initJwtSecret() {
String configKey = ConfigKeyEnum.JWT_SECRET.getKey();
String value = sysConfigService.getRawValue(configKey);
if (StringUtils.isBlank(value)) {
value = PasswordUtil.getRandomSimplePassword(30);
sysConfigService.setConfig(configKey, value);
}
}
public void insertAdmin() {
SysAdminUser userInfo = sysAdminUserService.getByUsername("admin");
if (userInfo != null) {
return;
}
String username = "admin";
String tpl = "INSERT INTO `sys_admin_user` ( `username`, `password`, `nickname`, `reg_type`) VALUES \n" +
"\t('%s','%s','%s','%s');";
// 初始密码
String defPassword = "123456";
defPassword = DigestUtils.sha256Hex(defPassword);
String encodedPassword = BCrypt.hashpw(defPassword, BCrypt.gensalt());
String sql = String.format(tpl, username, encodedPassword, username, RegTypeEnum.BACKEND.getValue());
runSql(sql);
}
protected void runSql(String sql) {
upgradeMapper.runSql(sql);
}
}

View File

@@ -0,0 +1,18 @@
package com.gitee.sop.adminbackend.service.sys.dto;
import lombok.Data;
/**
* @author tanghc
*/
@Data
public class SystemConfigDTO {
/** 数据库字段config_key */
private String configKey;
/** 数据库字段config_value */
private String configValue;
/** 数据库字段remark */
private String remark;
}

View File

@@ -0,0 +1,153 @@
package com.gitee.sop.adminbackend.service.sys.login;
import com.alibaba.nacos.shaded.com.google.common.collect.Sets;
import com.gitee.fastmybatis.core.query.Query;
import com.gitee.sop.adminbackend.common.ConfigKeyEnum;
import com.gitee.sop.adminbackend.common.exception.BizException;
import com.gitee.sop.adminbackend.config.Configs;
import com.gitee.sop.adminbackend.dao.entity.SysAdminUser;
import com.gitee.sop.adminbackend.service.sys.SysAdminUserService;
import com.gitee.sop.adminbackend.service.sys.login.dto.LoginDTO;
import com.gitee.sop.adminbackend.service.sys.login.dto.LoginForm;
import com.gitee.sop.adminbackend.service.sys.login.dto.LoginResult;
import com.gitee.sop.adminbackend.service.sys.login.dto.LoginUser;
import com.gitee.sop.adminbackend.service.sys.login.enums.AdminUserStatusEnum;
import com.gitee.sop.adminbackend.service.sys.login.enums.RegTypeEnum;
import com.gitee.sop.adminbackend.util.CopyUtil;
import com.gitee.sop.adminbackend.util.GenerateUtil;
import com.gitee.sop.adminbackend.util.JwtUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.math.NumberUtils;
import org.springframework.security.crypto.bcrypt.BCrypt;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
/**
* @author 六如
*/
@Service
@Slf4j
public class LoginService {
@Resource
private SysAdminUserService sysAdminUserService;
@Resource
private UserCacheManager userCacheManager;
public LoginUser login(LoginDTO loginDTO) {
String username = loginDTO.getUsername();
String password = loginDTO.getPassword();
RegTypeEnum regType = loginDTO.getRegType();
SysAdminUser userInfo;
switch (regType) {
case FORM:
throw new UnsupportedOperationException("第三方登录暂未支持");
case LDAP:
// LDAP登录
throw new UnsupportedOperationException("LDAP登录登录暂未支持");
default: {
// 默认注册账号登录
userInfo = this.doDatabaseLogin(username, password);
}
}
LoginUser loginUser = buildLoginUser(userInfo);
// 保存到缓存
userCacheManager.saveUser(loginUser);
return loginUser;
}
private LoginUser buildLoginUser(SysAdminUser userInfo) {
if (AdminUserStatusEnum.of(userInfo.getStatus()) == AdminUserStatusEnum.DISABLED) {
throw new BizException("账号已禁用,请联系管理员");
}
// 登录成功
LoginUser loginUser = CopyUtil.copyBean(userInfo, LoginUser::new);
// 创建token
String token = this.createToken(userInfo.getId());
loginUser.setAccessToken(token);
if ("admin".equals(userInfo.getUsername())) {
// ROLE
loginUser.setRoles(Sets.newHashSet("admin"));
// *:*:* 表示所有权限
loginUser.setPermissions(Sets.newHashSet("*:*:*"));
} else {
// TODO:其它角色权限
loginUser.setRoles(Sets.newHashSet());
loginUser.setPermissions(Sets.newHashSet());
}
// 设置token过期时间
String value = Configs.getValue(ConfigKeyEnum.JWT_TIMEOUT_DAYS);
LocalDateTime expireDate = LocalDateTime.now().plusDays(NumberUtils.toInt(value));
loginUser.setExpires(expireDate.format(DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss")));
return loginUser;
}
private String createToken(long userId) {
Map<String, String> data = new HashMap<>(4);
data.put("id", String.valueOf(userId));
String value = Configs.getValue(ConfigKeyEnum.JWT_TIMEOUT_DAYS);
return JwtUtil.createJwt(data, NumberUtils.toInt(value), getJwtSecret());
}
public static String getJwtSecret() {
return Configs.getValue(ConfigKeyEnum.JWT_SECRET);
}
private SysAdminUser doThirdPartyLogin(ThirdPartyLoginManager thirdPartyLoginManager, String username, String password) {
LoginForm loginForm = new LoginForm();
loginForm.setUsername(username);
loginForm.setPassword(password);
LoginResult loginResult;
try {
loginResult = thirdPartyLoginManager.login(loginForm);
} catch (Exception e) {
log.error("第三方登录失败", e);
throw new BizException(e.getMessage());
}
SysAdminUser userInfo = sysAdminUserService.getByUsername(username);
// 用户第一次登录则插入到user_info表
if (userInfo == null) {
userInfo = new SysAdminUser();
userInfo.setUsername(username);
userInfo.setPassword(GenerateUtil.getUUID());
userInfo.setNickname(loginResult.getNickname());
userInfo.setAvatar("");
userInfo.setStatus(AdminUserStatusEnum.ENABLE.getStatus());
userInfo.setRegType(loginResult.getRegTypeEnum().getValue());
userInfo.setEmail(loginResult.getEmail());
sysAdminUserService.save(userInfo);
} else {
String email = loginResult.getEmail();
// 如果更改了邮箱
if (StringUtils.hasText(email) && !Objects.equals(email, userInfo.getEmail())) {
userInfo.setEmail(email);
sysAdminUserService.update(userInfo);
}
}
return userInfo;
}
private SysAdminUser doDatabaseLogin(String username, String password) {
SysAdminUser sysAdminUser = sysAdminUserService.getByUsername(username);
Assert.notNull(sysAdminUser, () -> "用户名密码不正确");
String encodedPasswordDb = sysAdminUser.getPassword();
// 校验
boolean flag = BCrypt.checkpw(password, encodedPasswordDb);
Assert.isTrue(flag, () -> "用户名密码不正确");
return sysAdminUser;
}
}

View File

@@ -0,0 +1,40 @@
package com.gitee.sop.adminbackend.service.sys.login;
/**
* @author thc
*/
public class NotNullStringBuilder {
private final StringBuilder stringBuilder = new StringBuilder();
public NotNullStringBuilder append(Number o) {
stringBuilder.append(formatValue(o));
return this;
}
public NotNullStringBuilder append(String o) {
stringBuilder.append(formatValue(o));
return this;
}
public NotNullStringBuilder append(String o, String defaultValue) {
if (o == null || o.length() == 0) {
o = defaultValue;
}
stringBuilder.append(formatValue(o));
return this;
}
private static String formatValue(Number o) {
return o == null ? "0" : String.valueOf(o);
}
private static String formatValue(String o) {
return o == null ? "" : o;
}
@Override
public String toString() {
return stringBuilder.toString();
}
}

View File

@@ -0,0 +1,17 @@
package com.gitee.sop.adminbackend.service.sys.login;
import com.gitee.sop.adminbackend.service.sys.login.dto.LoginForm;
import com.gitee.sop.adminbackend.service.sys.login.dto.LoginResult;
/**
* @author 六如
*/
public interface ThirdPartyLoginManager {
/**
* 第三方登录
* @param loginForm 登录表单
* @return 返回登录结果
*/
LoginResult login(LoginForm loginForm) throws Exception;
}

View File

@@ -0,0 +1,21 @@
package com.gitee.sop.adminbackend.service.sys.login;
import com.gitee.sop.adminbackend.common.User;
/**
* @author 六如
*/
public interface UserCacheManager {
/**
* 返回用户信息
* @param userId 用户id
* @return 查不到返回null
*/
User getUser(long userId);
/**
* 保存用户
* @param user 用户
*/
void saveUser(User user);
}

View File

@@ -0,0 +1,11 @@
package com.gitee.sop.adminbackend.service.sys.login.dto;
import com.gitee.sop.adminbackend.service.sys.login.enums.RegTypeEnum;
import lombok.Data;
@Data
public class LoginDTO {
private String username;
private String password;
private RegTypeEnum regType = RegTypeEnum.BACKEND;
}

View File

@@ -0,0 +1,12 @@
package com.gitee.sop.adminbackend.service.sys.login.dto;
import lombok.Data;
/**
* @author 六如
*/
@Data
public class LoginForm {
private String username;
private String password;
}

View File

@@ -0,0 +1,20 @@
package com.gitee.sop.adminbackend.service.sys.login.dto;
import com.gitee.sop.adminbackend.service.sys.login.enums.RegTypeEnum;
import lombok.Data;
/**
* @author 六如
*/
@Data
public class LoginResult {
private String username;
private String nickname;
private String email;
private RegTypeEnum regTypeEnum = RegTypeEnum.FORM;
}

View File

@@ -0,0 +1,83 @@
package com.gitee.sop.adminbackend.service.sys.login.dto;
import com.gitee.sop.adminbackend.common.User;
import lombok.Data;
import java.util.Set;
/**
* @author 六如
*/
@Data
public class LoginUser implements User {
/*
avatar: "https://avatars.githubusercontent.com/u/44761321",
username: "admin",
nickname: "小铭",
// 一个用户可能有多个角色
roles: ["admin"],
// 按钮级别权限
permissions: ["*:*:*"],
accessToken: "eyJhbGciOiJIUzUxMiJ9.admin",
refreshToken: "eyJhbGciOiJIUzUxMiJ9.adminRefresh",
expires: "2030/10/30 00:00:00"
*/
private Long id;
/**
* 用户名
*/
private String username;
/**
* 用户名
*/
private String nickname;
/**
* 密码
*/
private String password;
/**
* 头像
*/
private String avatar;
/**
* 注册类型
*/
private Integer regType;
/**
* 状态1启用0禁用
*/
private Integer status;
private Set<String> roles;
private Set<String> permissions;
private String accessToken;
private String refreshToken;
private String expires;
@Override
public Long getUserId() {
return id;
}
@Override
public Integer getStatus() {
return status;
}
@Override
public String getToken() {
return accessToken;
}
}

View File

@@ -0,0 +1,34 @@
package com.gitee.sop.adminbackend.service.sys.login.enums;
import java.util.Objects;
/**
* @author 六如
*/
public enum AdminUserStatusEnum {
DISABLED((byte)0),
ENABLE((byte)1),
SET_PWD((byte)2),
;
private final int status;
public static AdminUserStatusEnum of(Integer value) {
for (AdminUserStatusEnum adminUserStatusEnum : AdminUserStatusEnum.values()) {
if (Objects.equals(adminUserStatusEnum.status, value)) {
return adminUserStatusEnum;
}
}
return DISABLED;
}
AdminUserStatusEnum(byte style) {
this.status = style;
}
public int getStatus() {
return status;
}
}

View File

@@ -0,0 +1,30 @@
package com.gitee.sop.adminbackend.service.sys.login.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Objects;
/**
* @author 六如
*/
@AllArgsConstructor
@Getter
public enum RegTypeEnum {
BACKEND("backend"),
FORM("form"),
OAUTH("oauth"),
LDAP("ldap"),
;
public static RegTypeEnum of(String source) {
for (RegTypeEnum value : RegTypeEnum.values()) {
if (Objects.equals(source, value.value)) {
return value;
}
}
return RegTypeEnum.BACKEND;
}
private final String value;
}

View File

@@ -0,0 +1,90 @@
package com.gitee.sop.adminbackend.service.sys.login.impl;
import com.gitee.sop.adminbackend.common.User;
import com.gitee.sop.adminbackend.dao.entity.SysAdminUser;
import com.gitee.sop.adminbackend.service.sys.SysAdminUserService;
import com.gitee.sop.adminbackend.service.sys.login.UserCacheManager;
import com.gitee.sop.adminbackend.service.sys.login.dto.LoginUser;
import com.gitee.sop.adminbackend.service.sys.login.enums.AdminUserStatusEnum;
import com.gitee.sop.adminbackend.util.CopyUtil;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
/**
* @author 六如
*/
@Service
@Slf4j
public class DefaultUserCacheManager implements UserCacheManager, InitializingBean {
@Autowired
private SysAdminUserService sysAdminUserService;
@Value("${admin.user-cache-timeout-minutes:15}")
private int timeoutMinutes;
// key: userId
private LoadingCache<Long, Optional<User>> userCache;
@Override
public User getUser(long userId) {
return userCache.getUnchecked(userId).orElse(null);
}
@Override
public void saveUser(User user) {
if (user == null) {
return;
}
userCache.put(user.getUserId(), Optional.of(user));
}
private LoadingCache<Long, Optional<User>> buildCache(int timeout) {
if (timeout <= 0) {
throw new IllegalArgumentException("timeout must be gt 0");
}
CacheBuilder<Object, Object> cacheBuilder = CacheBuilder.newBuilder();
cacheBuilder.expireAfterAccess(timeout, TimeUnit.MINUTES);
return cacheBuilder
.build(new CacheLoader<Long, Optional<User>>() {
@Override
public Optional<User> load(Long id) throws Exception {
User user = getLoginUser(id);
return Optional.ofNullable(user);
}
});
}
/**
* 获取登陆用户
* @param id
* @return
*/
private User getLoginUser(long id) {
SysAdminUser userInfo = sysAdminUserService.getById(id);
if (userInfo == null) {
log.warn("登录用户不存在userId{}", id);
return null;
}
if (userInfo.getStatus() == AdminUserStatusEnum.DISABLED.getStatus()) {
log.warn("用户被禁用, userId:{}, username:{}, nickname:{}", userInfo.getId(), userInfo.getUsername(), userInfo.getNickname());
return null;
}
return CopyUtil.copyBean(userInfo, LoginUser::new);
}
@Override
public void afterPropertiesSet() throws Exception {
userCache = buildCache(timeoutMinutes);
}
}

View File

@@ -0,0 +1,344 @@
package com.gitee.sop.adminbackend.util;
import com.alibaba.fastjson2.JSON;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.FatalBeanException;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
/**
* 属性拷贝工具类
*
* @author 六如
*/
public class CopyUtil extends BeanUtils {
/**
* 属性拷贝,第一个参数中的属性值拷贝到第二个参数中<br>
* 注意:当第一个参数中的属性有null值时,不会拷贝进去
*
* @param from 源对象
* @param to 目标对象
* @param ignoreProperties 忽略的字段
* @throws BeansException
*/
public static void copyPropertiesIgnoreNull(Object from, Object to, String... ignoreProperties)
throws BeansException {
Assert.notNull(from, "Source must not be null");
Assert.notNull(to, "Target must not be null");
Class<?> actualEditable = to.getClass();
PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);
List<String> ignoreList = (ignoreProperties != null ? Arrays.asList(ignoreProperties) : Collections.emptyList());
for (PropertyDescriptor targetPd : targetPds) {
if (ignoreList.contains(targetPd.getName())) {
continue;
}
Method writeMethod = targetPd.getWriteMethod();
if (writeMethod != null) {
PropertyDescriptor sourcePd = getPropertyDescriptor(from.getClass(), targetPd.getName());
if (sourcePd != null) {
Method readMethod = sourcePd.getReadMethod();
if (readMethod != null &&
ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())) {
try {
if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {
readMethod.setAccessible(true);
}
Object value = readMethod.invoke(from);
if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
writeMethod.setAccessible(true);
}
// 这里判断value是否为空 当然这里也能进行一些特殊要求的处理
// 例如绑定时格式转换等等
if (value != null) {
writeMethod.invoke(to, value);
}
} catch (Throwable ex) {
throw new FatalBeanException(
"Could not copy property '" + targetPd.getName() + "' from source to target", ex);
}
}
}
}
}
}
/**
* 拷贝指定的字段
*
* @param from 源对象
* @param to 目标对象
* @param includeFields 指定字段
*/
public static void copyPropertiesInclude(Object from, Object to, Set<String> includeFields) {
Objects.requireNonNull(includeFields, "includeFields can not null");
Assert.notNull(from, "Source must not be null");
Assert.notNull(to, "Target must not be null");
if (includeFields.isEmpty()) {
return;
}
Class<?> actualEditable = to.getClass();
PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);
for (PropertyDescriptor targetPd : targetPds) {
if (!includeFields.contains(targetPd.getName())) {
continue;
}
Method writeMethod = targetPd.getWriteMethod();
if (writeMethod != null) {
PropertyDescriptor sourcePd = getPropertyDescriptor(from.getClass(), targetPd.getName());
if (sourcePd != null) {
Method readMethod = sourcePd.getReadMethod();
if (readMethod != null &&
ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())) {
try {
if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {
readMethod.setAccessible(true);
}
Object value = readMethod.invoke(from);
if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
writeMethod.setAccessible(true);
}
writeMethod.invoke(to, value);
} catch (Throwable ex) {
throw new FatalBeanException(
"Could not copy property '" + targetPd.getName() + "' from source to target", ex);
}
}
}
}
}
}
/**
* 拷贝属性
*
* @param from 被拷贝类
* @param to 目标类
*/
public static void copyProperties(Object from, Object to) {
BeanUtils.copyProperties(from, to);
}
/**
* 拷贝bean成为一个新类
*
* @param from 被拷贝类
* @param supplier 新的类获取回调
* @param <T> 新的类
* @return 返回新的类实例from为null时返回null
*/
public static <T> T copyBean(Object from, Supplier<T> supplier) {
if (from == null) {
return null;
}
T to = supplier.get();
BeanUtils.copyProperties(from, to);
return to;
}
/**
* 拷贝实例
*
* @param from 被拷贝类
* @param supplier 新的类获取回调
* @param after 对新的类最后续处理回调
* @param <T> 新的类
* @return 返回新的类
*/
public static <T> T copyBean(Object from, Supplier<T> supplier, Consumer<T> after) {
if (from == null) {
return null;
}
T to = supplier.get();
BeanUtils.copyProperties(from, to);
after.accept(to);
return to;
}
/**
* 拷贝List将list中的类转换成新的对象
*
* @param collection 被拷贝的集合
* @param toElement List新元素
* @param <T> 新元素类型
* @return 返回新的List
*/
public static <T> List<T> copyList(Collection<?> collection, Supplier<T> toElement) {
if (collection == null || collection.isEmpty()) {
return new ArrayList<>();
}
return collection.stream()
.map(source -> {
T target = toElement.get();
BeanUtils.copyProperties(source, target);
return target;
})
.collect(Collectors.toList());
}
public static <E, R> List<R> copyList(Collection<E> fromList, Function<E, R> function) {
if (fromList == null) {
return new ArrayList<>();
}
return fromList.stream()
.map(source -> {
R target = function.apply(source);
BeanUtils.copyProperties(source, target);
return target;
})
.collect(Collectors.toList());
}
/**
* 拷贝List并做后续处理
*
* @param fromList 被拷贝的list
* @param toElement 新元素
* @param after 对新元素做后续处理
* @param <T> 新类型
* @return 返回新的List
*/
public static <T> List<T> copyList(Collection<?> fromList, Supplier<T> toElement, Consumer<T> after) {
if (fromList == null) {
return new ArrayList<>();
}
return fromList.stream()
.map(source -> {
T target = toElement.get();
BeanUtils.copyProperties(source, target);
after.accept(target);
return target;
})
.collect(Collectors.toList());
}
/**
* 拷贝List并做后续处理
*
* @param fromList 被拷贝的list
* @param toElement 新元素
* @param after 对新元素做后续处理
* @param <T> 新类型
* @return 返回新的List
*/
public static <T, F> List<T> copyList(Collection<F> fromList, Supplier<T> toElement, CopyConsumer<F, T> after) {
if (fromList == null) {
return new ArrayList<>();
}
return fromList.stream()
.map(source -> {
T target = toElement.get();
BeanUtils.copyProperties(source, target);
after.apply(source, target);
return target;
})
.collect(Collectors.toList());
}
/**
* 深层次拷贝通过json转换的方式实现
*
* @param from 待转换的类
* @param toClass 目标类class
* @param <T> 目标类
* @return 返回目标类
*/
public static <T> T deepCopy(Object from, Class<T> toClass) {
String json = JSON.toJSONString(from);
return JSON.parseObject(json, toClass);
}
/**
* 深层次拷贝通过json转换的方式实现
*
* @param from 待转换的类
* @param toClass 目标类class
* @param <T> 目标类
* @return 返回目标类
*/
public static <T> List<T> deepCopyList(Object from, Class<T> toClass) {
String json = JSON.toJSONString(from);
return JSON.parseArray(json, toClass);
}
/**
* 拷贝map
*
* @param srcMap 原map
* @param valueGetter 值转换
* @param <K> Key类型
* @param <V> Value类型
* @return 返回新map
*/
public static <K, V> Map<K, V> copyMap(Map<K, ?> srcMap, Supplier<V> valueGetter) {
Map<K, V> ret = new LinkedHashMap<>(srcMap.size() * 2);
for (Map.Entry<K, ?> entry : srcMap.entrySet()) {
V value = copyBean(entry.getValue(), valueGetter);
ret.put(entry.getKey(), value);
}
return ret;
}
/**
* 拷贝map
*
* @param srcMap 原map
* @param function 值转换
* @param <K> Key类型
* @param <V> Value类型
* @return 返回新map
*/
public static <K, V, V0> Map<K, V> copyMap(Map<K, V0> srcMap, Function<V0, V> function) {
Map<K, V> ret = new LinkedHashMap<>(srcMap.size() * 2);
for (Map.Entry<K, V0> entry : srcMap.entrySet()) {
V value = function.apply(entry.getValue());
ret.put(entry.getKey(), value);
}
return ret;
}
/**
* 拷贝map,value是list
*
* @param srcMap 原map
* @param valueGetter 值转换
* @param <K> Key类型
* @param <V> Value类型
* @return 返回新map
*/
public static <K, V, V2> Map<K, List<V2>> copyMapList(Map<K, List<V>> srcMap, Function<List<V>, List<V2>> valueGetter) {
Map<K, List<V2>> ret = new LinkedHashMap<>(srcMap.size() * 2);
for (Map.Entry<K, List<V>> entry : srcMap.entrySet()) {
List<V2> value = valueGetter.apply(entry.getValue());
ret.put(entry.getKey(), value);
}
return ret;
}
public interface CopyConsumer<F, T> {
void apply(F from, T to);
}
}

View File

@@ -0,0 +1,22 @@
package com.gitee.sop.adminbackend.util;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.DigestUtils;
import java.nio.charset.StandardCharsets;
/**
* @author 六如
*/
@Slf4j
public class GenerateUtil {
public static String getUserPassword(String username, String password, String salt) {
return DigestUtils.md5DigestAsHex((username + password + salt).getBytes(StandardCharsets.UTF_8));
}
public static String getUUID() {
return IdGen.uuid();
}
}

View File

@@ -0,0 +1,171 @@
package com.gitee.sop.adminbackend.util;
import java.util.UUID;
public class IdGen {
private static long workId = 0;
private static final SnowflakeIdWorker worker = new SnowflakeIdWorker(workId++, 0);
public static long genId() {
return worker.nextId();
}
/**
* 生成唯一id
* @return
*/
public static String nextId() {
return String.valueOf(genId());
}
public static String uuid() {
return UUID.randomUUID().toString().replace("-", "");
}
/**
* Twitter_Snowflake<br>
* SnowFlake的结构如下(每部分用-分开):<br>
* 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000 <br>
* 1位标识由于long基本类型在Java中是带符号的最高位是符号位正数是0负数是1所以id一般是正数最高位是0<br>
* 41位时间截(毫秒级)注意41位时间截不是存储当前时间的时间截而是存储时间截的差值当前时间截 - 开始时间截)
* 得到的值这里的的开始时间截一般是我们的id生成器开始使用的时间由我们程序来指定的如下下面程序IdWorker类的startTime属性。41位的时间截可以使用69年年T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69<br>
* 10位的数据机器位可以部署在1024个节点包括5位datacenterId和5位workerId<br>
* 12位序列毫秒内的计数12位的计数顺序号支持每个节点每毫秒(同一机器,同一时间截)产生4096个ID序号<br>
* 加起来刚好64位为一个Long型。<br>
* SnowFlake的优点是整体上按照时间自增排序并且整个分布式系统内不会产生ID碰撞(由数据中心ID和机器ID作区分)并且效率较高经测试SnowFlake每秒能够产生26万ID左右。
*/
public static class SnowflakeIdWorker {
// ==============================Fields===========================================
/** 开始时间截 (2015-01-01) */
private final long twepoch = 1420041100000L;
/** 机器id所占的位数 */
private final long workerIdBits = 5L;
/** 数据标识id所占的位数 */
private final long datacenterIdBits = 5L;
/** 支持的最大机器id结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数) */
private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
/** 支持的最大数据标识id结果是31 */
private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
/** 序列在id中占的位数 */
private final long sequenceBits = 12L;
/** 机器ID向左移12位 */
private final long workerIdShift = sequenceBits;
/** 数据标识id向左移17位(12+5) */
private final long datacenterIdShift = sequenceBits + workerIdBits;
/** 时间截向左移22位(5+5+12) */
private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
/** 生成序列的掩码这里为4095 (0b111111111111=0xfff=4095) */
private final long sequenceMask = -1L ^ (-1L << sequenceBits);
/** 工作机器ID(0~31) */
private long workerId;
/** 数据中心ID(0~31) */
private long datacenterId;
/** 毫秒内序列(0~4095) */
private long sequence = 0L;
/** 上次生成ID的时间截 */
private long lastTimestamp = -1L;
//==============================Constructors=====================================
/**
* 构造函数
* @param workerId 工作ID (0~31)
* @param datacenterId 数据中心ID (0~31)
*/
public SnowflakeIdWorker(long workerId, long datacenterId) {
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
}
if (datacenterId > maxDatacenterId || datacenterId < 0) {
throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
}
this.workerId = workerId;
this.datacenterId = datacenterId;
}
// ==============================Methods==========================================
/**
* 获得下一个ID (该方法是线程安全的)
* @return SnowflakeId
*/
public synchronized long nextId() {
long timestamp = timeGen();
//如果当前时间小于上一次ID生成的时间戳说明系统时钟回退过这个时候应当抛出异常
if (timestamp < lastTimestamp) {
throw new RuntimeException(
String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
}
//如果是同一时间生成的,则进行毫秒内序列
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & sequenceMask;
//毫秒内序列溢出
if (sequence == 0) {
//阻塞到下一个毫秒,获得新的时间戳
timestamp = tilNextMillis(lastTimestamp);
}
}
//时间戳改变,毫秒内序列重置
else {
sequence = 0L;
}
//上次生成ID的时间截
lastTimestamp = timestamp;
//移位并通过或运算拼到一起组成64位的ID
return ((timestamp - twepoch) << timestampLeftShift)
| (datacenterId << datacenterIdShift)
| (workerId << workerIdShift)
| sequence;
}
/**
* 阻塞到下一个毫秒,直到获得新的时间戳
* @param lastTimestamp 上次生成ID的时间截
* @return 当前时间戳
*/
protected long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
/**
* 返回以毫秒为单位的当前时间
* @return 当前时间(毫秒)
*/
protected long timeGen() {
return System.currentTimeMillis();
}
}
/*//==============================Test=============================================
*/
/** 测试
public static void main(String[] args) {
SnowflakeIdWorker idWorker = new SnowflakeIdWorker(0, 0);
for (int i = 0; i < 100; i++) {
long id = idWorker.nextId();
System.out.println("id:" + id);
System.out.println("toBinaryString:" + Long.toBinaryString(id));
}
}
*/
}

View File

@@ -0,0 +1,97 @@
package com.gitee.sop.adminbackend.util;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.gitee.sop.adminbackend.common.exception.JwtCreateException;
import com.gitee.sop.adminbackend.common.exception.JwtErrorException;
import com.gitee.sop.adminbackend.common.exception.JwtExpiredException;
import lombok.extern.slf4j.Slf4j;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* @author tanghc
*/
@Slf4j
public class JwtUtil {
private static final Map<String, Object> headerClaims = new HashMap<>();
/**
* 登录成功后跳转页面
*/
private static final String SUCCESS_HTML = String.join("\n", Arrays.asList(
"<html><head><script>",
"localStorage.setItem('torna.token', '%s');",
"location.href = '%s';",
"</script></head><body></body></html>"
));
static {
headerClaims.put("typ", "JWT");
headerClaims.put("alg", "HS256");
}
public static String createJwt(Map<String, String> data, int timeoutDays, String secret) {
JWTCreator.Builder builder = JWT.create().withHeader(headerClaims);
Set<Map.Entry<String, String>> entrySet = data.entrySet();
for (Map.Entry<String, String> entry : entrySet) {
builder.withClaim(entry.getKey(), entry.getValue());
}
LocalDateTime expiredDay = LocalDateTime.now().plusDays(timeoutDays);
Date expiredDate = Date.from(expiredDay.atZone(ZoneId.systemDefault()).toInstant());
try {
return builder
// 过期时间
.withExpiresAt(expiredDate)
// 创建时间
.withIssuedAt(new Date())
// 签名
.sign(Algorithm.HMAC256(secret));
} catch (Exception e) {
log.error(e.getMessage(), e);
throw new JwtCreateException();
}
}
public static Map<String, Claim> verifyJwt(String token, String secret) throws JwtExpiredException, JwtErrorException {
JWTVerifier verifier = null;
try {
verifier = JWT.require(Algorithm.HMAC256(secret)).build();
} catch (Exception e) {
log.error("验证jwt失败", e);
throw new JwtErrorException();
}
DecodedJWT jwt;
try {
jwt = verifier.verify(token);
} catch (TokenExpiredException e) {
throw new JwtExpiredException();
} catch (Exception e) {
log.error("验证jwt失败", e);
throw new JwtErrorException();
}
return jwt.getClaims();
}
public static String getJumpPageHtml(String token) {
return getJumpPageHtml(token, "/");
}
public static String getJumpPageHtml(String token, String redirectUrl) {
return String.format(SUCCESS_HTML, token, redirectUrl);
}
}

View File

@@ -0,0 +1,64 @@
package com.gitee.sop.adminbackend.util;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;
public class PasswordUtil {
private static final List<Character> CHARS;
private static final List<Character> SIMPLE_CHARS;
static {
CHARS = getCharList("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890~!@#$%^&*-=,.<>");
SIMPLE_CHARS = getCharList("abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789");
}
private static List<Character> getCharList(String str) {
char[] chars = str.toCharArray();
List<Character> list = new ArrayList<>(chars.length);
for (char c : chars) {
list.add(c);
}
Collections.shuffle(list);
return list;
}
public static void main(String[] args) {
String password = getRandomPassword(32);
System.out.println(password);
}
/**
* 随机密码生成,仅字母数字
* @param len 密码长度必须大于等于4
*/
public static String getRandomSimplePassword(int len) {
if (len < 4) {
throw new IllegalArgumentException("'len' must >= 4");
}
StringBuilder sb = new StringBuilder();
Random r = new Random();
for (int x = 0; x < len; ++x) {
sb.append(SIMPLE_CHARS.get(r.nextInt(SIMPLE_CHARS.size())));
}
return sb.toString();
}
/**
* 随机密码生成
* @param len 密码长度必须大于等于6
*/
public static String getRandomPassword(int len) {
if (len < 6) {
throw new IllegalArgumentException("'len' must >= 6");
}
StringBuilder sb = new StringBuilder();
Random r = new Random();
for (int x = 0; x < len; ++x) {
sb.append(CHARS.get(r.nextInt(CHARS.size())));
}
return sb.toString();
}
}

View File

@@ -0,0 +1,8 @@
dubbo.registry.address=zookeeper://localhost:2181
mybatis.print-sql=true
# mysql config
mysql.host=127.0.0.1:3306
mysql.username=root
mysql.password=root

View File

@@ -0,0 +1,2 @@
dubbo.registry.address=nacos://localhost:8848

View File

@@ -0,0 +1,33 @@
server.port=8082
spring.profiles.active=dev
spring.application.name=sop-admin-backend
dubbo.protocol.name=dubbo
dubbo.protocol.port=-1
dubbo.application.qos-enable=false
dubbo.registry.address=zookeeper://localhost:2181
####### mysql config #######
mysql.host=127.0.0.1:3306
mysql.username=
mysql.password=
mysql.db=sop
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://${mysql.host}/${mysql.db}?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai
spring.datasource.username=${mysql.username}
spring.datasource.password=${mysql.password}
####### mybatis config #######
mybatis.fill.com.gitee.fastmybatis.core.support.LocalDateTimeFillInsert=add_time
mybatis.fill.com.gitee.fastmybatis.core.support.LocalDateTimeFillUpdate=update_time
# mybatis config file
mybatis.config-location=classpath:mybatis/mybatisConfig.xml
mybatis.mapper-locations=classpath:mybatis/mapper/*.xml
# print SQL
logging.level.com.gitee.sop.adminbackend.dao=error
logging.level.com.gitee.fastmybatis=info
mybatis.print-sql=false

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.gitee.sop.adminbackend.dao.mapper.UpgradeMapper">
<update id="runSql">
${sql}
</update>
<select id="listColumnInfo" resultType="java.util.Map">
SHOW COLUMNS FROM ${tableName}
</select>
<select id="listTableName" resultType="String">
SHOW TABLES
</select>
<select id="listTableIndex" resultType="java.util.Map">
show index from ${tableName}
</select>
</mapper>

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<!-- 全局映射器启用缓存 -->
<setting name="cacheEnabled" value="true" />
<!-- 查询时,关闭关联对象即时加载以提高性能 -->
<setting name="lazyLoadingEnabled" value="true" />
<!-- 对于未知的SQL查询允许返回不同的结果集以达到通用的效果 -->
<setting name="multipleResultSetsEnabled" value="true" />
<!-- 允许使用列标签代替列名 -->
<setting name="useColumnLabel" value="true" />
<!-- 允许使用自定义的主键值(比如由程序生成的UUID 32位编码作为键值)数据表的PK生成策略将被覆盖 -->
<setting name="useGeneratedKeys" value="false" />
<!-- 对于批量更新操作缓存SQL以提高性能:BATCH -->
<setting name="defaultExecutorType" value="SIMPLE" />
<!-- 超时设置 -->
<setting name="defaultStatementTimeout" value="25000" />
</settings>
<plugins>
<plugin interceptor="com.gitee.fastmybatis.core.support.plugin.SqlFormatterPlugin">
</plugin>
</plugins>
</configuration>

View File

@@ -0,0 +1,13 @@
package com.gitee.sop.adminbackend;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class BaseTest {
@Test
void contextLoads() {
}
}

View File

@@ -0,0 +1,44 @@
package com.gitee.sop.adminbackend.service;
import com.alibaba.fastjson2.JSON;
import com.gitee.sop.adminbackend.BaseTest;
import com.gitee.sop.adminbackend.service.sys.login.LoginService;
import com.gitee.sop.adminbackend.service.sys.login.dto.LoginDTO;
import com.gitee.sop.adminbackend.service.sys.login.dto.LoginUser;
import com.gitee.sop.adminbackend.service.sys.login.enums.RegTypeEnum;
import javax.annotation.Resource;
import org.apache.commons.codec.digest.DigestUtils;
import org.junit.jupiter.api.Test;
import org.springframework.security.crypto.bcrypt.BCrypt;
import org.springframework.util.Assert;
/**
* @author 六如
*/
public class LoginServiceTest extends BaseTest {
@Resource
LoginService loginService;
@Test
public void login() {
LoginDTO loginDTO = new LoginDTO();
loginDTO.setUsername("admin");
loginDTO.setPassword("123456");
loginDTO.setRegType(RegTypeEnum.BACKEND);
LoginUser loginUser = loginService.login(loginDTO);
Assert.notNull(loginUser, "not null");
System.out.println(JSON.toJSONString(loginUser));
}
@Test
public void resetAdminPwd() {
// 初始密码
String defPassword = "123456";
defPassword = DigestUtils.sha256Hex(defPassword);
String encodedPassword = BCrypt.hashpw(defPassword, BCrypt.gensalt());
}
}

View File

@@ -0,0 +1,37 @@
package com.gitee.sop.adminbackend.service;
import com.gitee.sop.adminbackend.BaseTest;
import com.gitee.sop.adminbackend.dao.entity.SysAdminUser;
import com.gitee.sop.adminbackend.service.sys.SysAdminUserService;
import org.apache.commons.codec.digest.DigestUtils;
import org.junit.jupiter.api.Test;
import org.springframework.security.crypto.bcrypt.BCrypt;
import javax.annotation.Resource;
/**
* @author 六如
*/
public class PasswordTest extends BaseTest {
@Resource
SysAdminUserService sysAdminUserService;
/**
* 重置admin密码
*/
@Test
public void resetAdminPwd() {
String username = "admin";
String defPassword = "123456";
defPassword = DigestUtils.sha256Hex(defPassword);
String encodedPassword = BCrypt.hashpw(defPassword, BCrypt.gensalt());
System.out.println("数据库保存:" + encodedPassword);
sysAdminUserService.query()
.eq(SysAdminUser::getUsername, username)
.set(SysAdminUser::getPassword, encodedPassword)
.update();
}
}

View File

@@ -0,0 +1,4 @@
> 1%
last 2 versions
not dead
not ie 11

View File

@@ -0,0 +1,21 @@
node_modules
.DS_Store
dist
dist-ssr
*.local
.eslintcache
report.html
yarn.lock
npm-debug.log*
.pnpm-error.log*
.pnpm-debug.log
tests/**/coverage/
# Editor directories and files
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
tsconfig.tsbuildinfo

View File

@@ -0,0 +1,14 @@
# http://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
insert_final_newline = false
trim_trailing_whitespace = false

View File

@@ -0,0 +1,5 @@
# 平台本地运行端口号
VITE_PORT = 9123
# 是否隐藏首页 隐藏 true 不隐藏 false 勿删除VITE_HIDE_HOME只需在.env文件配置
VITE_HIDE_HOME = false

View File

@@ -1 +1,8 @@
VITE_API_BASE_URL= 'http://localhost:8080'
# 平台本地运行端口号
VITE_PORT = 9123
# 开发环境读取配置文件路径
VITE_PUBLIC_PATH = /
# 开发环境路由历史模式Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数"
VITE_ROUTER_HISTORY = "hash"

View File

@@ -0,0 +1,13 @@
# 线上环境平台打包路径
VITE_PUBLIC_PATH = /
# 线上环境路由历史模式Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数"
VITE_ROUTER_HISTORY = "hash"
# 是否在打包时使用cdn替换本地库 替换 true 不替换 false
VITE_CDN = false
# 是否启用gzip压缩或brotli压缩分两种情况删除原始文件和不删除原始文件
# 压缩时不删除原始文件的配置gzip、brotli、both同时开启 gzip 与 brotli 压缩、none不开启压缩默认
# 压缩时删除原始文件的配置gzip-clear、brotli-clear、both-clear同时开启 gzip 与 brotli 压缩、none不开启压缩默认
VITE_COMPRESSION = "none"

View File

@@ -0,0 +1,16 @@
# 预发布也需要生产环境的行为
# https://cn.vitejs.dev/guide/env-and-mode.html#modes
# NODE_ENV = development
VITE_PUBLIC_PATH = /
# 预发布环境路由历史模式Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数"
VITE_ROUTER_HISTORY = "hash"
# 是否在打包时使用cdn替换本地库 替换 true 不替换 false
VITE_CDN = true
# 是否启用gzip压缩或brotli压缩分两种情况删除原始文件和不删除原始文件
# 压缩时不删除原始文件的配置gzip、brotli、both同时开启 gzip 与 brotli 压缩、none不开启压缩默认
# 压缩时删除原始文件的配置gzip-clear、brotli-clear、both-clear同时开启 gzip 与 brotli 压缩、none不开启压缩默认
VITE_COMPRESSION = "none"

View File

@@ -1,3 +0,0 @@
/*.json
/*.js
dist

View File

@@ -1,70 +0,0 @@
// eslint-disable-next-line @typescript-eslint/no-var-requires
const path = require('path');
module.exports = {
root: true,
parser: 'vue-eslint-parser',
parserOptions: {
// Parser that checks the content of the <script> tag
parser: '@typescript-eslint/parser',
sourceType: 'module',
ecmaVersion: 2020,
ecmaFeatures: {
jsx: true,
},
},
env: {
'browser': true,
'node': true,
'vue/setup-compiler-macros': true,
},
plugins: ['@typescript-eslint'],
extends: [
// Airbnb JavaScript Style Guide https://github.com/airbnb/javascript
'airbnb-base',
'plugin:@typescript-eslint/recommended',
'plugin:import/recommended',
'plugin:import/typescript',
'plugin:vue/vue3-recommended',
'plugin:prettier/recommended',
],
settings: {
'import/resolver': {
typescript: {
project: path.resolve(__dirname, './tsconfig.json'),
},
},
},
rules: {
'prettier/prettier': 1,
// Vue: Recommended rules to be closed or modify
'vue/require-default-prop': 0,
'vue/singleline-html-element-content-newline': 0,
'vue/max-attributes-per-line': 0,
// Vue: Add extra rules
'vue/custom-event-name-casing': [2, 'camelCase'],
'vue/no-v-text': 1,
'vue/padding-line-between-blocks': 1,
'vue/require-direct-export': 1,
'vue/multi-word-component-names': 0,
// Allow @ts-ignore comment
'@typescript-eslint/ban-ts-comment': 0,
'@typescript-eslint/no-unused-vars': 1,
'@typescript-eslint/no-empty-function': 1,
'@typescript-eslint/no-explicit-any': 0,
'import/extensions': [
2,
'ignorePackages',
{
js: 'never',
jsx: 'never',
ts: 'never',
tsx: 'never',
},
],
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
'no-param-reassign': 0,
'prefer-regex-literals': 0,
'import/no-extraneous-dependencies': 0,
},
};

View File

@@ -3,8 +3,20 @@ node_modules
dist
dist-ssr
*.local
node_modules
.DS_Store
dist
dist-ssr
*.local
.eslintcache
report.html
vite.config.*.timestamp*
yarn.lock
npm-debug.log*
.pnpm-error.log*
.pnpm-debug.log
tests/**/coverage/
# Editor directories and files
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
tsconfig.tsbuildinfo

View File

@@ -1,4 +1,8 @@
#!/bin/sh
# shellcheck source=./_/husky.sh
. "$(dirname "$0")/_/husky.sh"
pnpm commitlint --edit $1
PATH="/usr/local/bin:$PATH"
npx --no-install commitlint --edit "$1"

View File

@@ -0,0 +1,9 @@
#!/bin/sh
command_exists () {
command -v "$1" >/dev/null 2>&1
}
# Workaround for Windows 10, Git Bash and Pnpm
if command_exists winpty && test -t 1; then
exec < /dev/tty
fi

View File

@@ -1,4 +1,10 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
. "$(dirname "$0")/common.sh"
npm run lint-staged
[ -n "$CI" ] && exit 0
PATH="/usr/local/bin:$PATH"
# Perform lint check on files in the staging area through .lintstagedrc configuration
pnpm exec lint-staged

View File

@@ -0,0 +1,20 @@
{
"*.{js,jsx,ts,tsx}": [
"prettier --cache --ignore-unknown --write",
"eslint --cache --fix"
],
"{!(package)*.json,*.code-snippets,.!({browserslist,npm,nvm})*rc}": [
"prettier --cache --write--parser json"
],
"package.json": ["prettier --cache --write"],
"*.vue": [
"prettier --write",
"eslint --cache --fix",
"stylelint --fix --allow-empty-input"
],
"*.{css,scss,html}": [
"prettier --cache --ignore-unknown --write",
"stylelint --fix --allow-empty-input"
],
"*.md": ["prettier --cache --ignore-unknown --write"]
}

View File

@@ -0,0 +1,11 @@
{
"default": true,
"MD003": false,
"MD033": false,
"MD013": false,
"MD001": false,
"MD025": false,
"MD024": false,
"MD007": { "indent": 4 },
"no-hard-tabs": false
}

View File

@@ -0,0 +1,4 @@
shell-emulator=true
shamefully-hoist=true
enable-pre-post-scripts=false
strict-peer-dependencies=false

View File

@@ -0,0 +1 @@
v20.15.0

View File

@@ -1,7 +0,0 @@
/dist/*
.local
.output.js
/node_modules/**
**/*.svg
**/*.sh

View File

@@ -1,9 +1,9 @@
module.exports = {
tabWidth: 2,
semi: true,
printWidth: 80,
singleQuote: true,
quoteProps: 'consistent',
htmlWhitespaceSensitivity: 'strict',
vueIndentScriptAndStyle: true,
// @ts-check
/** @type {import("prettier").Config} */
export default {
bracketSpacing: true,
singleQuote: false,
arrowParens: "avoid",
trailingComma: "none"
};

View File

@@ -0,0 +1,4 @@
/dist/*
/public/*
public/*
src/style/reset.scss

View File

@@ -1,30 +0,0 @@
module.exports = {
extends: [
'stylelint-config-standard',
'stylelint-config-rational-order',
'stylelint-config-prettier',
'stylelint-config-recommended-vue',
],
defaultSeverity: 'warning',
plugins: ['stylelint-order'],
rules: {
'at-rule-no-unknown': [
true,
{
ignoreAtRules: ['plugin'],
},
],
'rule-empty-line-before': [
'always',
{
except: ['after-single-line-comment', 'first-nested'],
},
],
'selector-pseudo-class-no-unknown': [
true,
{
ignorePseudoClasses: ['deep'],
},
],
},
};

View File

@@ -0,0 +1,19 @@
{
"recommendations": [
"christian-kohler.path-intellisense",
"warmthsea.vscode-custom-code-color",
"vscode-icons-team.vscode-icons",
"davidanson.vscode-markdownlint",
"ms-azuretools.vscode-docker",
"stylelint.vscode-stylelint",
"bradlc.vscode-tailwindcss",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"redhat.vscode-yaml",
"csstools.postcss",
"mikestead.dotenv",
"eamodio.gitlens",
"antfu.iconify",
"Vue.volar"
]
}

View File

@@ -0,0 +1,43 @@
{
"editor.formatOnType": true,
"editor.formatOnSave": true,
"[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"editor.tabSize": 2,
"editor.formatOnPaste": true,
"editor.guides.bracketPairs": "active",
"files.autoSave": "afterDelay",
"git.confirmSync": false,
"workbench.startupEditor": "newUntitledFile",
"editor.suggestSelection": "first",
"editor.acceptSuggestionOnCommitCharacter": false,
"css.lint.propertyIgnoredDueToDisplay": "ignore",
"editor.quickSuggestions": {
"other": true,
"comments": true,
"strings": true
},
"files.associations": {
"editor.snippetSuggestions": "top"
},
"[css]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"iconify.excludes": [
"el"
],
"vscodeCustomCodeColor.highlightValue": [
"v-loading",
"v-auth",
"v-copy",
"v-longpress",
"v-optimize",
"v-perms",
"v-ripple"
],
"vscodeCustomCodeColor.highlightValueColor": "#b392f0",
}

View File

@@ -0,0 +1,22 @@
{
"Vue3.0快速生成模板": {
"scope": "vue",
"prefix": "Vue3.0",
"body": [
"<template>",
"\t<div>test</div>",
"</template>\n",
"<script lang='ts'>",
"export default {",
"\tsetup() {",
"\t\treturn {}",
"\t}",
"}",
"</script>\n",
"<style lang='scss' scoped>\n",
"</style>",
"$2"
],
"description": "Vue3.0"
}
}

View File

@@ -0,0 +1,17 @@
{
"Vue3.2+快速生成模板": {
"scope": "vue",
"prefix": "Vue3.2+",
"body": [
"<script setup lang='ts'>",
"</script>\n",
"<template>",
"\t<div>test</div>",
"</template>\n",
"<style lang='scss' scoped>\n",
"</style>",
"$2"
],
"description": "Vue3.2+"
}
}

View File

@@ -0,0 +1,20 @@
{
"Vue3.3+defineOptions快速生成模板": {
"scope": "vue",
"prefix": "Vue3.3+",
"body": [
"<script setup lang='ts'>",
"defineOptions({",
"\tname: ''",
"})",
"</script>\n",
"<template>",
"\t<div>test</div>",
"</template>\n",
"<style lang='scss' scoped>\n",
"</style>",
"$2"
],
"description": "Vue3.3+defineOptions快速生成模板"
}
}

View File

@@ -0,0 +1,20 @@
FROM node:20-alpine as build-stage
WORKDIR /app
RUN corepack enable
RUN corepack prepare pnpm@latest --activate
RUN npm config set registry https://registry.npmmirror.com
COPY .npmrc package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm build
FROM nginx:stable-alpine as production-stage
COPY --from=build-stage /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020-present, pure-admin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,51 @@
<h1>vue-pure-admin精简版国际化版本</h1>
[![license](https://img.shields.io/github/license/pure-admin/vue-pure-admin.svg)](LICENSE)
**中文** | [English](./README.en-US.md)
## 介绍
精简版是基于 [vue-pure-admin](https://github.com/pure-admin/vue-pure-admin) 提炼出的架子,包含主体功能,更适合实际项目开发,打包后的大小在全局引入 [element-plus](https://element-plus.org) 的情况下仍然低于 `2.3MB`,并且会永久同步完整版的代码。开启 `brotli` 压缩和 `cdn` 替换本地库模式后,打包大小低于 `350kb`
## 版本选择
当前是国际化版本,如果您需要非国际化版本 [请点击](https://github.com/pure-admin/pure-admin-thin)
## `js` 版本
[点我查看 js 版本](https://pure-admin.github.io/pure-admin-doc/pages/js/)
## `max` 版本
[点我查看 max 版本](https://github.com/pure-admin/vue-pure-admin-max)
## 配套视频
[点我查看 UI 设计](https://www.bilibili.com/video/BV17g411T7rq)
[点我查看快速开发教程](https://www.bilibili.com/video/BV1kg411v7QT)
## 配套保姆级文档
[点我查看 vue-pure-admin 文档](https://pure-admin.github.io/pure-admin-doc)
[点我查看 @pureadmin/utils 文档](https://pure-admin-utils.netlify.app)
## 优质服务、软件外包、赞助支持
[点我查看详情](https://pure-admin.github.io/pure-admin-doc/pages/service/)
## 预览
[查看预览](https://pure-admin-thin.netlify.app/#/login)
## 维护者
[xiaoxian521](https://github.com/xiaoxian521)
## ⚠️ 注意
精简版不接受任何 `issues``pr`,如果有问题请到完整版 [issues](https://github.com/pure-admin/vue-pure-admin/issues/new/choose) 去提,谢谢!
## 许可证
[MIT © 2020-present, pure-admin](./LICENSE)

View File

@@ -1,3 +0,0 @@
module.exports = {
plugins: ['@vue/babel-plugin-jsx'],
};

View File

@@ -0,0 +1,60 @@
import { Plugin as importToCDN } from "vite-plugin-cdn-import";
/**
* @description 打包时采用`cdn`模式仅限外网使用默认不采用如果需要采用cdn模式请在 .env.production 文件,将 VITE_CDN 设置成true
* 平台采用国内cdnhttps://www.bootcdn.cn当然你也可以选择 https://unpkg.com 或者 https://www.jsdelivr.com
* 注意上面提到的仅限外网使用也不是完全肯定的如果你们公司内网部署的有相关js、css文件也可以将下面配置对应改一下整一套内网版cdn
*/
export const cdn = importToCDN({
//prodUrl解释 name: 对应下面modules的nameversion: 自动读取本地package.json中dependencies依赖中对应包的版本号path: 对应下面modules的path当然也可写完整路径会替换prodUrl
prodUrl: "https://cdn.bootcdn.net/ajax/libs/{name}/{version}/{path}",
modules: [
{
name: "vue",
var: "Vue",
path: "vue.global.prod.min.js"
},
{
name: "vue-router",
var: "VueRouter",
path: "vue-router.global.min.js"
},
{
name: "vue-i18n",
var: "VueI18n",
path: "vue-i18n.runtime.global.prod.min.js"
},
// 项目中没有直接安装vue-demi但是pinia用到了所以需要在引入pinia前引入vue-demihttps://github.com/vuejs/pinia/blob/v2/packages/pinia/package.json#L77
{
name: "vue-demi",
var: "VueDemi",
path: "index.iife.min.js"
},
{
name: "pinia",
var: "Pinia",
path: "pinia.iife.min.js"
},
{
name: "element-plus",
var: "ElementPlus",
path: "index.full.min.js",
css: "index.min.css"
},
{
name: "axios",
var: "axios",
path: "axios.min.js"
},
{
name: "dayjs",
var: "dayjs",
path: "dayjs.min.js"
},
{
name: "echarts",
var: "echarts",
path: "echarts.min.js"
}
]
});

View File

@@ -0,0 +1,63 @@
import type { Plugin } from "vite";
import { isArray } from "@pureadmin/utils";
import compressPlugin from "vite-plugin-compression";
export const configCompressPlugin = (
compress: ViteCompression
): Plugin | Plugin[] => {
if (compress === "none") return null;
const gz = {
// 生成的压缩包后缀
ext: ".gz",
// 体积大于threshold才会被压缩
threshold: 0,
// 默认压缩.js|mjs|json|css|html后缀文件设置成true压缩全部文件
filter: () => true,
// 压缩后是否删除原始文件
deleteOriginFile: false
};
const br = {
ext: ".br",
algorithm: "brotliCompress",
threshold: 0,
filter: () => true,
deleteOriginFile: false
};
const codeList = [
{ k: "gzip", v: gz },
{ k: "brotli", v: br },
{ k: "both", v: [gz, br] }
];
const plugins: Plugin[] = [];
codeList.forEach(item => {
if (compress.includes(item.k)) {
if (compress.includes("clear")) {
if (isArray(item.v)) {
item.v.forEach(vItem => {
plugins.push(
compressPlugin(Object.assign(vItem, { deleteOriginFile: true }))
);
});
} else {
plugins.push(
compressPlugin(Object.assign(item.v, { deleteOriginFile: true }))
);
}
} else {
if (isArray(item.v)) {
item.v.forEach(vItem => {
plugins.push(compressPlugin(vItem));
});
} else {
plugins.push(compressPlugin(item.v));
}
}
}
});
return plugins;
};

View File

@@ -0,0 +1,57 @@
import type { Plugin } from "vite";
import { getPackageSize } from "./utils";
import dayjs, { type Dayjs } from "dayjs";
import duration from "dayjs/plugin/duration";
import gradientString from "gradient-string";
import boxen, { type Options as BoxenOptions } from "boxen";
dayjs.extend(duration);
const welcomeMessage = gradientString("cyan", "magenta").multiline(
`您好! 欢迎使用 pure-admin 开源项目\n我们为您精心准备了下面两个贴心的保姆级文档\nhttps://pure-admin.github.io/pure-admin-doc\nhttps://pure-admin-utils.netlify.app`
);
const boxenOptions: BoxenOptions = {
padding: 0.5,
borderColor: "cyan",
borderStyle: "round"
};
export function viteBuildInfo(): Plugin {
let config: { command: string };
let startTime: Dayjs;
let endTime: Dayjs;
let outDir: string;
return {
name: "vite:buildInfo",
configResolved(resolvedConfig) {
config = resolvedConfig;
outDir = resolvedConfig.build?.outDir ?? "dist";
},
buildStart() {
console.log(boxen(welcomeMessage, boxenOptions));
if (config.command === "build") {
startTime = dayjs(new Date());
}
},
closeBundle() {
if (config.command === "build") {
endTime = dayjs(new Date());
getPackageSize({
folder: outDir,
callback: (size: string) => {
console.log(
boxen(
gradientString("cyan", "magenta").multiline(
`🎉 恭喜打包完成(总用时${dayjs
.duration(endTime.diff(startTime))
.format("mm分ss秒")},打包后的大小为${size}`
),
boxenOptions
)
);
}
});
}
}
};
}

View File

@@ -0,0 +1,34 @@
/**
* 此文件作用于 `vite.config.ts` 的 `optimizeDeps.include` 依赖预构建配置项
* 依赖预构建,`vite` 启动时会将下面 include 里的模块,编译成 esm 格式并缓存到 node_modules/.vite 文件夹,页面加载到对应模块时如果浏览器有缓存就读取浏览器缓存,如果没有会读取本地缓存并按需加载
* 尤其当您禁用浏览器缓存时(这种情况只应该发生在调试阶段)必须将对应模块加入到 include里否则会遇到开发环境切换页面卡顿的问题vite 会认为它是一个新的依赖包会重新加载并强制刷新页面),因为它既无法使用浏览器缓存,又没有在本地 node_modules/.vite 里缓存
* 温馨提示:如果您使用的第三方库是全局引入,也就是引入到 src/main.ts 文件里,就不需要再添加到 include 里了,因为 vite 会自动将它们缓存到 node_modules/.vite
*/
const include = [
"qs",
"mitt",
"dayjs",
"axios",
"pinia",
"vue-i18n",
"vue-types",
"js-cookie",
"vue-tippy",
"pinyin-pro",
"sortablejs",
"@vueuse/core",
"@pureadmin/utils",
"responsive-storage"
];
/**
* 在预构建中强制排除的依赖项
* 温馨提示:所有以 `@iconify-icons/` 开头引入的的本地图标模块,都应该加入到下面的 `exclude` 里,因为平台推荐的使用方式是哪里需要哪里引入而且都是单个的引入,不需要预构建,直接让浏览器加载就好
*/
const exclude = [
"@iconify-icons/ep",
"@iconify-icons/ri",
"@pureadmin/theme/dist/browser-utils"
];
export { include, exclude };

View File

@@ -0,0 +1,76 @@
import { cdn } from "./cdn";
import vue from "@vitejs/plugin-vue";
import { pathResolve } from "./utils";
import { viteBuildInfo } from "./info";
import svgLoader from "vite-svg-loader";
import type { PluginOption } from "vite";
import checker from "vite-plugin-checker";
import vueJsx from "@vitejs/plugin-vue-jsx";
import Inspector from "vite-plugin-vue-inspector";
import { configCompressPlugin } from "./compress";
import removeNoMatch from "vite-plugin-router-warn";
import { visualizer } from "rollup-plugin-visualizer";
import removeConsole from "vite-plugin-remove-console";
import { themePreprocessorPlugin } from "@pureadmin/theme";
import VueI18nPlugin from "@intlify/unplugin-vue-i18n/vite";
import { genScssMultipleScopeVars } from "../src/layout/theme";
import { vitePluginFakeServer } from "vite-plugin-fake-server";
export function getPluginsList(
VITE_CDN: boolean,
VITE_COMPRESSION: ViteCompression
): PluginOption[] {
const lifecycle = process.env.npm_lifecycle_event;
return [
vue(),
// jsx、tsx语法支持
vueJsx(),
VueI18nPlugin({
jitCompilation: false,
include: [pathResolve("../locales/**")]
}),
checker({
typescript: true,
vueTsc: true,
eslint: {
lintCommand: `eslint ${pathResolve("../{src,mock,build}/**/*.{vue,js,ts,tsx}")}`,
useFlatConfig: true
},
terminal: false,
enableBuild: false
}),
// 按下Command(⌘)+Shift(⇧)然后点击页面元素会自动打开本地IDE并跳转到对应的代码位置
Inspector(),
viteBuildInfo(),
/**
* 开发环境下移除非必要的vue-router动态路由警告No match found for location with path
* 非必要具体看 https://github.com/vuejs/router/issues/521 和 https://github.com/vuejs/router/issues/359
* vite-plugin-router-warn只在开发环境下启用只处理vue-router文件并且只在服务启动或重启时运行一次性能消耗可忽略不计
*/
removeNoMatch(),
// mock支持
vitePluginFakeServer({
logger: false,
include: "mock",
infixName: false,
enableProd: true
}),
// 自定义主题
themePreprocessorPlugin({
scss: {
multipleScopeVars: genScssMultipleScopeVars(),
extract: true
}
}),
// svg组件化支持
svgLoader(),
VITE_CDN ? cdn : null,
configCompressPlugin(VITE_COMPRESSION),
// 线上环境删除console
removeConsole({ external: ["src/assets/iconfont/iconfont.js"] }),
// 打包分析
lifecycle === "report"
? visualizer({ open: true, brotliSize: true, filename: "report.html" })
: (null as any)
];
}

Some files were not shown because too many files have changed in this diff Show More