u
This commit is contained in:
1 parent
52112bd277
commit
1da52dc4f4
66 files changed
+3294
-721
No files matched your search
@@ -0,0 +1,39 @@
|
||||
target/
|
||||
!.mvn/wrapper/maven-wrapper.jar
|
||||
!**/src/main/**/target/
|
||||
!**/src/test/**/target/
|
||||
.kotlin
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea/modules.xml
|
||||
.idea/jarRepositories.xml
|
||||
.idea/compiler.xml
|
||||
.idea/libraries/
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
|
||||
### Eclipse ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
build/
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
|
||||
### Mac OS ###
|
||||
.DS_Store
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
@@ -0,0 +1,5 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
|
||||
</state>
|
||||
</component>
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Encoding">
|
||||
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
|
||||
</component>
|
||||
</project>
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="MavenProjectsManager">
|
||||
<option name="originalFiles">
|
||||
<list>
|
||||
<option value="$PROJECT_DIR$/pom.xml" />
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
</project>
|
||||
Generated
+6
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$/../../.." vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -0,0 +1,130 @@
|
||||
<?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>
|
||||
|
||||
<groupId>cn.link56</groupId>
|
||||
<artifactId>link56-backend</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>2.7.18</version>
|
||||
</parent>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>8</maven.compiler.source>
|
||||
<maven.compiler.target>8</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
|
||||
<jwt.version>3.10.3</jwt.version>
|
||||
<sqlserver.version>1.3.1</sqlserver.version>
|
||||
<druid.version>1.2.23</druid.version>
|
||||
<mybatisplus.version>3.5.10.1</mybatisplus.version>
|
||||
<apachecommons.version>3.12.0</apachecommons.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<!-- lombok -->
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<!-- 测试 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<!-- web -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<!-- aop -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-aop</artifactId>
|
||||
</dependency>
|
||||
<!-- 校验 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
<!-- 邮件 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-mail</artifactId>
|
||||
</dependency>
|
||||
<!--thymeleaf依赖-->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
||||
</dependency>
|
||||
<!-- sqlserver -->
|
||||
<dependency>
|
||||
<groupId>net.sourceforge.jtds</groupId>
|
||||
<artifactId>jtds</artifactId>
|
||||
<version>${jtds.version}</version>
|
||||
</dependency>
|
||||
<!-- druid -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>druid-spring-boot-starter</artifactId>
|
||||
<version>${druid.version}</version>
|
||||
</dependency>
|
||||
<!-- mybatisplus -->
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-boot-starter</artifactId>
|
||||
<version>${mybatisplus.version}</version>
|
||||
</dependency>
|
||||
<!-- mybatisplus-sqlserver -->
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-jsqlparser-4.9</artifactId>
|
||||
<version>${mybatisplus.version}</version>
|
||||
</dependency>
|
||||
<!-- jwt -->
|
||||
<dependency>
|
||||
<groupId>com.auth0</groupId>
|
||||
<artifactId>java-jwt</artifactId>
|
||||
<version>${jwt.version}</version>
|
||||
</dependency>
|
||||
<!--工具类-->
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
<version>${apachecommons.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
|
||||
<!-- 阿里云maven仓库 -->
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>public</id>
|
||||
<name>aliyun nexus</name>
|
||||
<url>https://maven.aliyun.com/repository/public/</url>
|
||||
<releases>
|
||||
<enabled>true</enabled>
|
||||
</releases>
|
||||
</repository>
|
||||
</repositories>
|
||||
<pluginRepositories>
|
||||
<pluginRepository>
|
||||
<id>public</id>
|
||||
<name>aliyun nexus</name>
|
||||
<url>https://maven.aliyun.com/repository/public/</url>
|
||||
<releases>
|
||||
<enabled>true</enabled>
|
||||
</releases>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</pluginRepository>
|
||||
</pluginRepositories>
|
||||
</project>
|
||||
@@ -0,0 +1,13 @@
|
||||
package cn.link56;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableAsync
|
||||
public class Link56Application {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(Link56Application.class,args);
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package cn.link56.auth.code.controller;
|
||||
|
||||
import cn.link56.auth.code.model.AuthCodeChannel;
|
||||
import cn.link56.auth.code.model.AuthCodePurpose;
|
||||
import cn.link56.auth.code.model.dto.AuthCodeSendDTO;
|
||||
import cn.link56.auth.code.model.dto.AuthCodeVerifyDTO;
|
||||
import cn.link56.auth.code.service.VerificationCodeService;
|
||||
import cn.link56.common.Result;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/auth/code")
|
||||
public class AuthCodeController {
|
||||
@Resource
|
||||
private VerificationCodeService verificationCodeService;
|
||||
|
||||
@PostMapping("/send")
|
||||
public Result<Void> send(@RequestBody AuthCodeSendDTO authCodeSendDTO) {
|
||||
AuthCodePurpose purpose = authCodeSendDTO.getPurpose();
|
||||
AuthCodeChannel channel = authCodeSendDTO.getChannel();
|
||||
String target = authCodeSendDTO.getTarget();
|
||||
|
||||
boolean flag = verificationCodeService.sendCode(purpose, channel, target);
|
||||
|
||||
return flag ? Result.success() : Result.error();
|
||||
}
|
||||
|
||||
@PostMapping("/verify")
|
||||
public Result<Void> verify(@RequestBody AuthCodeVerifyDTO authCodeVerifyDTO) {
|
||||
AuthCodePurpose purpose = authCodeVerifyDTO.getPurpose();
|
||||
AuthCodeChannel channel = authCodeVerifyDTO.getChannel();
|
||||
String target = authCodeVerifyDTO.getTarget();
|
||||
String code = authCodeVerifyDTO.getCode();
|
||||
|
||||
boolean flag = verificationCodeService.verifyCode(purpose, channel, target, code);
|
||||
|
||||
return flag ? Result.success() : Result.error();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package cn.link56.auth.code.model;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class AuthCode {
|
||||
private String code;
|
||||
private Instant expireAt;
|
||||
private Instant lastSentAt;
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package cn.link56.auth.code.model;
|
||||
|
||||
public enum AuthCodeChannel {
|
||||
EMAIL,
|
||||
SMS
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package cn.link56.auth.code.model;
|
||||
|
||||
/**
|
||||
* 验证码用途 / 业务场景
|
||||
*/
|
||||
public enum AuthCodePurpose {
|
||||
REGISTER, // 注册
|
||||
LOGIN, // 登录
|
||||
RESET_PASSWORD // 找回密码
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package cn.link56.auth.code.model.dto;
|
||||
|
||||
import cn.link56.auth.code.model.AuthCodeChannel;
|
||||
import cn.link56.auth.code.model.AuthCodePurpose;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class AuthCodeSendDTO {
|
||||
private AuthCodePurpose purpose;
|
||||
|
||||
private AuthCodeChannel channel;
|
||||
|
||||
/**
|
||||
* 邮箱 / 手机号
|
||||
*/
|
||||
private String target;
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package cn.link56.auth.code.model.dto;
|
||||
|
||||
import cn.link56.auth.code.model.AuthCodeChannel;
|
||||
import cn.link56.auth.code.model.AuthCodePurpose;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class AuthCodeVerifyDTO {
|
||||
private AuthCodePurpose purpose;
|
||||
|
||||
private AuthCodeChannel channel;
|
||||
|
||||
/**
|
||||
* 邮箱 / 手机号
|
||||
*/
|
||||
private String target;
|
||||
|
||||
private String code;
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package cn.link56.auth.code.sender;
|
||||
|
||||
import cn.link56.auth.code.model.AuthCodePurpose;
|
||||
import cn.link56.common.utils.EmailUtils;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
public class EmailAuthCodeSender {
|
||||
@Resource
|
||||
private EmailUtils emailUtils;
|
||||
|
||||
public boolean send(String email, String code, AuthCodePurpose purpose) {
|
||||
String subject = "";
|
||||
String templateHtml = "";
|
||||
|
||||
Map<String,Object> payload = new HashMap<>();
|
||||
|
||||
if (purpose.equals(AuthCodePurpose.LOGIN)){
|
||||
subject = "登录";
|
||||
}else if(purpose.equals(AuthCodePurpose.REGISTER)){
|
||||
subject = "注册";
|
||||
templateHtml = "Register.html";
|
||||
}else if(purpose.equals(AuthCodePurpose.RESET_PASSWORD)){
|
||||
subject = "找回密码";
|
||||
templateHtml = "ForgetPassword.html";
|
||||
}
|
||||
|
||||
payload.put("Code",code);
|
||||
|
||||
return emailUtils.sendSync(templateHtml,subject,payload,email);
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package cn.link56.auth.code.sender;
|
||||
|
||||
import cn.link56.auth.code.model.AuthCodePurpose;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class SmsAuthCodeSender {
|
||||
public boolean send(String email, String code, AuthCodePurpose purpose) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
package cn.link56.auth.code.service;
|
||||
|
||||
import cn.link56.auth.code.model.AuthCodeChannel;
|
||||
import cn.link56.auth.code.model.AuthCode;
|
||||
import cn.link56.auth.code.model.AuthCodePurpose;
|
||||
import cn.link56.auth.code.sender.EmailAuthCodeSender;
|
||||
import cn.link56.auth.code.sender.SmsAuthCodeSender;
|
||||
import cn.link56.auth.code.store.AuthCodeStore;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
@Service
|
||||
public class VerificationCodeService {
|
||||
@Resource
|
||||
private AuthCodeStore codeStore;
|
||||
|
||||
@Resource
|
||||
private EmailAuthCodeSender emailAuthCodeSender;
|
||||
|
||||
@Resource
|
||||
private SmsAuthCodeSender smsAuthCodeSender;
|
||||
|
||||
public boolean sendCode(
|
||||
AuthCodePurpose purpose,
|
||||
AuthCodeChannel channel,
|
||||
String target
|
||||
) {
|
||||
AuthCode code = codeStore.generate(purpose, channel, target);
|
||||
|
||||
boolean success;
|
||||
|
||||
switch (channel) {
|
||||
case EMAIL:
|
||||
success = emailAuthCodeSender.send(target, code.getCode(), purpose);
|
||||
break;
|
||||
case SMS:
|
||||
success = smsAuthCodeSender.send(target, code.getCode(), purpose);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("不支持类型: " + channel);
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
public boolean verifyCode(
|
||||
AuthCodePurpose purpose,
|
||||
AuthCodeChannel channel,
|
||||
String target,
|
||||
String inputCode
|
||||
) {
|
||||
return codeStore.verify(purpose, channel, target, inputCode);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package cn.link56.auth.code.store;
|
||||
|
||||
import cn.link56.auth.code.model.AuthCodeChannel;
|
||||
import cn.link56.auth.code.model.AuthCode;
|
||||
import cn.link56.auth.code.model.AuthCodePurpose;
|
||||
import cn.link56.common.exception.OperationTooFrequentException;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.naming.OperationNotSupportedException;
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@Component
|
||||
public class AuthCodeStore {
|
||||
private final Map<String, AuthCode> store = new ConcurrentHashMap<>();
|
||||
|
||||
private static final long EXPIRE_SECONDS = 600;
|
||||
private static final long MIN_INTERVAL_SECONDS = 60;
|
||||
|
||||
public AuthCode generate(
|
||||
AuthCodePurpose purpose,
|
||||
AuthCodeChannel channel,
|
||||
String target
|
||||
) {
|
||||
String key = buildKey(purpose, channel, target);
|
||||
Instant now = Instant.now();
|
||||
|
||||
AuthCode existing = store.get(key);
|
||||
if (existing != null &&
|
||||
existing.getLastSentAt().plusSeconds(MIN_INTERVAL_SECONDS).isAfter(now)) {
|
||||
throw new OperationTooFrequentException("验证码发送过于频繁,请稍后再试");
|
||||
}
|
||||
|
||||
AuthCode code = new AuthCode(
|
||||
randomCode(6),
|
||||
now.plusSeconds(EXPIRE_SECONDS),
|
||||
now
|
||||
);
|
||||
|
||||
store.put(key, code);
|
||||
return code;
|
||||
}
|
||||
|
||||
public boolean verify(
|
||||
AuthCodePurpose purpose,
|
||||
AuthCodeChannel channel,
|
||||
String target,
|
||||
String inputCode
|
||||
) {
|
||||
String key = buildKey(purpose, channel, target);
|
||||
AuthCode code = store.get(key);
|
||||
|
||||
if (code == null || Instant.now().isAfter(code.getExpireAt())) {
|
||||
store.remove(key);
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean matched = code.getCode().equals(inputCode);
|
||||
if (matched) {
|
||||
store.remove(key); // 一次性使用
|
||||
}
|
||||
return matched;
|
||||
}
|
||||
|
||||
private String buildKey(
|
||||
AuthCodePurpose purpose,
|
||||
AuthCodeChannel channel,
|
||||
String target
|
||||
) {
|
||||
return purpose.name() + ":" + channel.name() + ":" + target;
|
||||
}
|
||||
|
||||
private String randomCode(int length) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < length; i++) {
|
||||
sb.append((int) (Math.random() * 10));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package cn.link56.auth.identity.model;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@TableName("b_company")
|
||||
@Accessors(chain = true)
|
||||
public class BCompany {
|
||||
@TableId(value = "b_id")
|
||||
private Long id;
|
||||
@TableField("b_name")
|
||||
private String name;
|
||||
@TableField("b_email")
|
||||
private String email;
|
||||
@TableField("b_phone")
|
||||
private String phone;
|
||||
@TableField("b_address")
|
||||
private String address;
|
||||
@TableField("b_industry")
|
||||
private String industry;
|
||||
@TableField("b_logo")
|
||||
private String logo;
|
||||
@TableField("b_profile")
|
||||
private String profile;
|
||||
@TableField("b_description")
|
||||
private String description;
|
||||
@TableField("b_create_by")
|
||||
private Long createBy;
|
||||
@TableField("b_create_time")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date createTime;
|
||||
@TableField("b_update_by")
|
||||
private Long updateBy;
|
||||
@TableField("b_update_time")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date updateTime;
|
||||
@TableField("b_status")
|
||||
private Integer status;
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package cn.link56.auth.identity.model;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@TableName("b_company_user")
|
||||
@Accessors(chain = true)
|
||||
public class BCompanyUser {
|
||||
@TableId(value = "b_id")
|
||||
private Long id;
|
||||
@TableId(value = "b_company_id")
|
||||
private Long companyId;
|
||||
@TableField("b_email")
|
||||
private String email;
|
||||
@TableField("b_password")
|
||||
private String password;
|
||||
@TableField("b_phone")
|
||||
private String phone;
|
||||
@TableField("b_role")
|
||||
private Integer role;
|
||||
@TableField("b_position")
|
||||
private String position;
|
||||
@TableField("b_create_by")
|
||||
private Long createBy;
|
||||
@TableField("b_create_time")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date createTime;
|
||||
@TableField("b_update_by")
|
||||
private Long updateBy;
|
||||
@TableField("b_update_time")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date updateTime;
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
package cn.link56.auth.identity.model.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import javax.validation.constraints.Email;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class AuthRegisterDTO {
|
||||
private Company company;
|
||||
private Admin admin;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public static class Company {
|
||||
@NotBlank(message = "公司名称不能为空")
|
||||
private String name;
|
||||
@NotBlank(message = "所属行业不能为空")
|
||||
private String industry;
|
||||
@NotBlank(message = "公司地址不能为空")
|
||||
private String address;
|
||||
private String phone;
|
||||
private String email;
|
||||
private String profile;
|
||||
}
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public static class Admin {
|
||||
@NotBlank(message = "管理员名称不能为空")
|
||||
private String name;
|
||||
private String phone;
|
||||
@NotBlank(message = "邮箱不能为空")
|
||||
@Email(message = "邮箱格式有误")
|
||||
private String email;
|
||||
@NotBlank(message = "密码不能为空")
|
||||
private String password;
|
||||
private String position;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package cn.link56.auth.token.model;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Data
|
||||
public class AuthToken {
|
||||
private String token; // 实际的 token 字符串
|
||||
private Instant expireAt; // 过期时间
|
||||
private TokenPayload payload; // 携带用户信息
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package cn.link56.auth.token.model;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class TokenPayload {
|
||||
private Long userId; // 用户唯一标识
|
||||
private Long companyId; // 用户所属公司ID
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
package cn.link56.auth.token.service;
|
||||
|
||||
import cn.link56.auth.token.model.AuthToken;
|
||||
import cn.link56.auth.token.model.TokenPayload;
|
||||
import com.auth0.jwt.JWT;
|
||||
import com.auth0.jwt.JWTVerifier;
|
||||
import com.auth0.jwt.algorithms.Algorithm;
|
||||
import com.auth0.jwt.exceptions.JWTVerificationException;
|
||||
import com.auth0.jwt.interfaces.DecodedJWT;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Date;
|
||||
|
||||
@Service
|
||||
public class AuthTokenService {
|
||||
private static final String SECRET_KEY = "dasdasdad661d65a1s65da65ds!";
|
||||
private static final long TOKEN_EXPIRE_SECONDS = 30L * 24 * 3600; // 30天
|
||||
private static final ObjectMapper objectMapper = new ObjectMapper();
|
||||
private static final Algorithm algorithm = Algorithm.HMAC256(SECRET_KEY);
|
||||
|
||||
/**
|
||||
* 生成 JWT Token
|
||||
*/
|
||||
public AuthToken generateToken(TokenPayload payload) {
|
||||
try {
|
||||
String payloadJson = objectMapper.writeValueAsString(payload);
|
||||
Instant now = Instant.now();
|
||||
Instant expireAt = now.plusSeconds(TOKEN_EXPIRE_SECONDS);
|
||||
|
||||
String token = JWT.create()
|
||||
.withSubject(payload.getUserId().toString())
|
||||
.withClaim("payload", payloadJson)
|
||||
.withIssuedAt(Date.from(now))
|
||||
.withExpiresAt(Date.from(expireAt))
|
||||
.sign(algorithm);
|
||||
|
||||
return new AuthToken(token, expireAt, payload);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to generate token", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 Token 是否有效
|
||||
*/
|
||||
public boolean validateToken(String token) {
|
||||
try {
|
||||
JWTVerifier verifier = JWT.require(algorithm).build();
|
||||
verifier.verify(token);
|
||||
return true;
|
||||
} catch (JWTVerificationException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 Token Payload
|
||||
*/
|
||||
public TokenPayload parsePayload(String token) {
|
||||
try {
|
||||
DecodedJWT decoded = JWT.require(algorithm).build().verify(token);
|
||||
String payloadJson = decoded.getClaim("payload").asString();
|
||||
return objectMapper.readValue(payloadJson, TokenPayload.class);
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Token 过期时间
|
||||
*/
|
||||
public Instant getExpireAt(String token) {
|
||||
try {
|
||||
DecodedJWT decoded = JWT.require(algorithm).build().verify(token);
|
||||
return decoded.getExpiresAt().toInstant();
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package cn.link56.base.controller;
|
||||
|
||||
import cn.link56.base.model.vo.BaseDataVO;
|
||||
import cn.link56.base.service.BaseDataService;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/base")
|
||||
public class BaseDataController {
|
||||
@Resource
|
||||
private BaseDataService baseDataService;
|
||||
|
||||
|
||||
@GetMapping("/init")
|
||||
public BaseDataVO initBaseData() {
|
||||
return baseDataService.getBaseData();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package cn.link56.base.mapper;
|
||||
|
||||
import cn.link56.base.model.BIndustry;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface IndustryMapper extends BaseMapper<BIndustry> {
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package cn.link56.base.model;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@TableName("b_industry")
|
||||
@Accessors(chain = true)
|
||||
public class BIndustry {
|
||||
@TableId(value = "b_id")
|
||||
private Long id;
|
||||
@TableField("b_xh")
|
||||
private Integer xh;
|
||||
@TableField("b_name")
|
||||
private String name;
|
||||
@TableField("b_canuse")
|
||||
private String canuse;
|
||||
@TableField("b_bz")
|
||||
private String bz;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package cn.link56.base.model.vo;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class BaseDataVO {
|
||||
private List<Industry> industries;
|
||||
private List<Area> areas;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public static class Industry {
|
||||
private Long id;
|
||||
private Integer xh;
|
||||
private String name;
|
||||
}
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public static class Area {
|
||||
private String id;
|
||||
private Integer xh;
|
||||
private String name;
|
||||
private String fatherId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package cn.link56.base.service;
|
||||
|
||||
import cn.link56.base.mapper.IndustryMapper;
|
||||
import cn.link56.base.model.BIndustry;
|
||||
import cn.link56.base.model.vo.BaseDataVO;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class BaseDataService {
|
||||
@Resource
|
||||
private IndustryMapper industryMapper;
|
||||
|
||||
public BaseDataVO getBaseData() {
|
||||
BaseDataVO baseDataVO = new BaseDataVO();
|
||||
|
||||
// 查询可用行业
|
||||
List<BaseDataVO.Industry> industries = industryMapper.selectList(
|
||||
new LambdaQueryWrapper<BIndustry>()
|
||||
.eq(BIndustry::getCanuse, "1")
|
||||
).stream()
|
||||
.map(industry -> new BaseDataVO.Industry(
|
||||
industry.getId(),
|
||||
industry.getXh(),
|
||||
industry.getName()
|
||||
))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
|
||||
baseDataVO.setIndustries(industries);
|
||||
|
||||
return baseDataVO;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package cn.link56.common;
|
||||
|
||||
import cn.link56.common.constants.ResponseStatusConstants;
|
||||
import lombok.Data;
|
||||
import lombok.NonNull;
|
||||
|
||||
@Data
|
||||
public class Result<T> {
|
||||
private final Integer code;
|
||||
private final String message;
|
||||
private final T data;
|
||||
|
||||
// 私有构造函数,强制通过静态工厂方法创建实例
|
||||
private Result(Integer code, String message, T data) {
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
// 成功响应(无数据)
|
||||
public static <T> Result<T> success() {
|
||||
return new Result<>(ResponseStatusConstants.SUCCESS,"请求成功", null);
|
||||
}
|
||||
|
||||
public static <T> Result<T> success(String message) {
|
||||
return new Result<>(ResponseStatusConstants.SUCCESS, message, null);
|
||||
}
|
||||
|
||||
// 成功响应(带数据)
|
||||
public static <T> Result<T> success(@NonNull T data) {
|
||||
return new Result<>(ResponseStatusConstants.SUCCESS,"请求成功", data);
|
||||
}
|
||||
|
||||
// 成功响应(带数据)
|
||||
public static <T> Result<T> success(@NonNull T data,String message) {
|
||||
return new Result<>(ResponseStatusConstants.SUCCESS, message, data);
|
||||
}
|
||||
|
||||
|
||||
public static <T> Result<T> error(Integer code,String message){
|
||||
return new Result<>(code, message, null);
|
||||
}
|
||||
|
||||
public static <T> Result<T> error(String message){
|
||||
return new Result<>(ResponseStatusConstants.ERROR, message, null);
|
||||
}
|
||||
|
||||
public static <T> Result<T> error(){
|
||||
return new Result<>(ResponseStatusConstants.ERROR, null,null);
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package cn.link56.common.config;
|
||||
|
||||
import cn.link56.common.utils.snowflake.contract.IdGeneratorOptions;
|
||||
import cn.link56.common.utils.snowflake.idgen.IdGenerator;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class IdGeneratorInitializer {
|
||||
@EventListener(ApplicationReadyEvent.class)
|
||||
public void init() {
|
||||
IdGeneratorOptions options = new IdGeneratorOptions();
|
||||
options.WorkerId = 1;
|
||||
IdGenerator.setIdGenerator(options);
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package cn.link56.common.config;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
@MapperScan("cn.link56.**.mapper")
|
||||
public class MybatisPlusConfig {
|
||||
|
||||
/**
|
||||
* 添加分页插件
|
||||
*/
|
||||
@Bean
|
||||
public MybatisPlusInterceptor mybatisPlusInterceptor() {
|
||||
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
|
||||
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.SQL_SERVER2005)); // 如果配置多个插件, 切记分页最后添加
|
||||
// 如果有多数据源可以不配具体类型, 否则都建议配上具体的 DbType
|
||||
return interceptor;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package cn.link56.common.config;
|
||||
|
||||
import cn.link56.common.interceptor.AuthInterceptor;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
@Configuration
|
||||
public class WebMvcConfig implements WebMvcConfigurer {
|
||||
private final AuthInterceptor authInterceptor;
|
||||
|
||||
public WebMvcConfig(AuthInterceptor authInterceptor) {
|
||||
this.authInterceptor = authInterceptor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(authInterceptor)
|
||||
.addPathPatterns("/**")
|
||||
.excludePathPatterns(
|
||||
"/auth/**",
|
||||
"/company/getCompanyPage",
|
||||
"/company/getPublicDetail",
|
||||
"/company/getRecommendList",
|
||||
"/company/getRecommendRightList",
|
||||
|
||||
"/forum/post/page",
|
||||
"/forum/post/detail",
|
||||
"/forum/post/getRecommendList",
|
||||
"/forum/post/getHotList",
|
||||
"/forum/post/comment/list",
|
||||
|
||||
"/base/**",
|
||||
|
||||
"/error/**",
|
||||
"/favicon.ico"
|
||||
);
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package cn.link56.common.constants;
|
||||
|
||||
public class ResponseStatusConstants {
|
||||
/**
|
||||
* 操作成功
|
||||
*/
|
||||
public static final Integer SUCCESS = 200;
|
||||
|
||||
/**
|
||||
* 系统内部错误
|
||||
*/
|
||||
public static final Integer ERROR = 500;
|
||||
|
||||
/**
|
||||
* 缺少参数,参数格式有误,请求路径有误等
|
||||
*/
|
||||
public static final Integer BAD_REQUEST = 400;
|
||||
|
||||
/**
|
||||
* 未找到资源,或者该资源已丢失
|
||||
*/
|
||||
public static final Integer NOT_FOUND = 404;
|
||||
|
||||
/**
|
||||
* 登录失效或已过期
|
||||
*/
|
||||
public static final Integer UNAUTHORIZED = 401;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package cn.link56.common.context;
|
||||
|
||||
/**
|
||||
* 用户信息上下文(ThreadLocal)
|
||||
* 独立,不依赖 JwtUtils 或拦截器
|
||||
*/
|
||||
public class UserContext {
|
||||
|
||||
private static final ThreadLocal<Long> USER_ID = new ThreadLocal<>();
|
||||
private static final ThreadLocal<Long> COMPANY_ID = new ThreadLocal<>();
|
||||
|
||||
public static void setUserId(Long userId) {
|
||||
USER_ID.set(userId);
|
||||
}
|
||||
|
||||
public static Long getUserId() {
|
||||
return USER_ID.get();
|
||||
}
|
||||
|
||||
public static void removeUserId() {
|
||||
USER_ID.remove();
|
||||
}
|
||||
|
||||
public static void setCompanyId(Long companyId) {
|
||||
COMPANY_ID.set(companyId);
|
||||
}
|
||||
|
||||
public static Long getCompanyId() {
|
||||
return COMPANY_ID.get();
|
||||
}
|
||||
|
||||
public static void removeCompanyId() {
|
||||
COMPANY_ID.remove();
|
||||
}
|
||||
|
||||
public static void clear() {
|
||||
removeUserId();
|
||||
removeCompanyId();
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package cn.link56.common.exception;
|
||||
|
||||
public class BusinessException extends RuntimeException {
|
||||
public BusinessException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
package cn.link56.common.exception;
|
||||
|
||||
import cn.link56.common.Result;
|
||||
import cn.link56.common.constants.ResponseStatusConstants;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
import org.springframework.validation.BindException;
|
||||
import org.springframework.web.HttpRequestMethodNotSupportedException;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.MissingPathVariableException;
|
||||
import org.springframework.web.bind.MissingServletRequestParameterException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.validation.ConstraintViolationException;
|
||||
import java.util.Objects;
|
||||
|
||||
@Slf4j
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
/**
|
||||
* 请求方式不支持
|
||||
*/
|
||||
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
|
||||
public Result<Void> handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException e,
|
||||
HttpServletRequest request)
|
||||
{
|
||||
String requestURI = request.getRequestURI();
|
||||
String message = "请求地址 '" + requestURI + "' 不支持 '" + e.getMethod() + " ' 请求";
|
||||
return Result.error(ResponseStatusConstants.BAD_REQUEST,message);
|
||||
}
|
||||
/**
|
||||
* 请求路径中缺少必需的路径变量
|
||||
*/
|
||||
@ExceptionHandler(MissingPathVariableException.class)
|
||||
public Result<Void> handleMissingPathVariableException(MissingPathVariableException e, HttpServletRequest request)
|
||||
{
|
||||
return Result.error(ResponseStatusConstants.BAD_REQUEST,String.format("请求路径中缺少必需的路径变量[%s]", e.getVariableName()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义验证异常
|
||||
*/
|
||||
@ExceptionHandler(BindException.class)
|
||||
public Result<Void> handleBindException(BindException e) {
|
||||
log.error(e.getMessage(), e);
|
||||
return Result.error(ResponseStatusConstants.BAD_REQUEST,"自定义校验异常");
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义验证异常
|
||||
*/
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public Result<Void> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
|
||||
String message = Objects.requireNonNull(e.getBindingResult().getFieldError()).getDefaultMessage();
|
||||
return Result.error(ResponseStatusConstants.BAD_REQUEST,message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 缺少参数异常
|
||||
*/
|
||||
@ExceptionHandler(HttpMessageNotReadableException.class)
|
||||
public Result<Void> handleHttpMessageNotReadableException(HttpMessageNotReadableException e) {
|
||||
return Result.error(ResponseStatusConstants.BAD_REQUEST,"缺少请求体:" + e.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 缺少参数异常
|
||||
*/
|
||||
@ExceptionHandler(MissingServletRequestParameterException.class)
|
||||
public Result<Void> handleMissingServletRequestParameterException(MissingServletRequestParameterException e) {
|
||||
return Result.error(ResponseStatusConstants.BAD_REQUEST,"缺少参数:" + e.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 缺少参数异常
|
||||
*/
|
||||
@ExceptionHandler(ConstraintViolationException.class)
|
||||
public Result<Void> handleConstraintViolationException(ConstraintViolationException e) {
|
||||
return Result.error(ResponseStatusConstants.BAD_REQUEST,e.getMessage());
|
||||
}
|
||||
|
||||
// 自定义异常
|
||||
@ExceptionHandler(OperationTooFrequentException.class)
|
||||
public Result<Void> handleOperationTooFrequentException(OperationTooFrequentException e) {
|
||||
return Result.error(e.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理系统异常
|
||||
*/
|
||||
@ExceptionHandler(value = Exception.class)
|
||||
public Result<Void> handleSystemException(Exception e) {
|
||||
e.printStackTrace();
|
||||
log.error("捕获到全局异常:{}",e.getMessage());
|
||||
return Result.error(e.getMessage());
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package cn.link56.common.exception;
|
||||
|
||||
public class OperationTooFrequentException extends BusinessException {
|
||||
public OperationTooFrequentException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public OperationTooFrequentException() {
|
||||
super("操作过于频繁,请稍后再试");
|
||||
}
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
package cn.link56.common.interceptor;
|
||||
|
||||
import cn.link56.auth.token.model.TokenPayload;
|
||||
import cn.link56.auth.token.service.AuthTokenService;
|
||||
import cn.link56.common.Result;
|
||||
import cn.link56.common.constants.ResponseStatusConstants;
|
||||
import cn.link56.common.context.UserContext;
|
||||
import cn.link56.common.utils.JsonUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.PrintWriter;
|
||||
|
||||
@Component
|
||||
public class AuthInterceptor implements HandlerInterceptor {
|
||||
|
||||
private final AuthTokenService authTokenService;
|
||||
|
||||
public AuthInterceptor(AuthTokenService authTokenService) {
|
||||
this.authTokenService = authTokenService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
||||
|
||||
// 预检请求放行
|
||||
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
|
||||
return true;
|
||||
}
|
||||
System.out.println(request.getRequestURI());
|
||||
System.out.println(request.getRequestURL());
|
||||
|
||||
try {
|
||||
String token = request.getHeader("Authorization");
|
||||
if (StringUtils.isBlank(token)) {
|
||||
return writeUnauthorized(response);
|
||||
}
|
||||
|
||||
boolean flag = authTokenService.validateToken(token);
|
||||
|
||||
if (!flag){
|
||||
return writeUnauthorized(response);
|
||||
}
|
||||
|
||||
// 解析 token
|
||||
TokenPayload tokenPayload = authTokenService.parsePayload(token);
|
||||
|
||||
// 对比信息
|
||||
String companyIdStr = request.getHeader("X-Company-Id");
|
||||
String userIdStr = request.getHeader("X-User-Id");
|
||||
|
||||
if ((StringUtils.isNotBlank(companyIdStr) && !companyIdStr.equals(tokenPayload.getCompanyId().toString()))
|
||||
|| (StringUtils.isNotBlank(userIdStr) && !userIdStr.equals(tokenPayload.getUserId().toString()))) {
|
||||
return writeUnauthorized(response);
|
||||
}
|
||||
|
||||
// 注入到 UserContext
|
||||
UserContext.setUserId(tokenPayload.getUserId());
|
||||
UserContext.setCompanyId(tokenPayload.getCompanyId());
|
||||
|
||||
return true;
|
||||
|
||||
} catch (Exception e) {
|
||||
return writeUnauthorized(response);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
|
||||
UserContext.clear(); // 清理 ThreadLocal
|
||||
}
|
||||
|
||||
private boolean writeUnauthorized(HttpServletResponse response) throws Exception {
|
||||
Result<Void> result = Result.error(ResponseStatusConstants.UNAUTHORIZED, "登录过期或未授权");
|
||||
String jsonString = JsonUtils.toJson(result);
|
||||
|
||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
response.setContentType("application/json;charset=utf-8");
|
||||
|
||||
try (PrintWriter out = response.getWriter()) {
|
||||
out.println(jsonString);
|
||||
out.flush();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package cn.link56.common.utils;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.mail.javamail.JavaMailSender;
|
||||
import org.springframework.mail.javamail.MimeMessageHelper;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.thymeleaf.TemplateEngine;
|
||||
import org.thymeleaf.context.Context;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.mail.MessagingException;
|
||||
import javax.mail.internet.MimeMessage;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
@Component
|
||||
public class EmailUtils {
|
||||
|
||||
@Value("${spring.mail.username}")
|
||||
private String fromEmail;
|
||||
|
||||
@Resource
|
||||
private JavaMailSender mailSender;
|
||||
|
||||
@Resource
|
||||
private TemplateEngine templateEngine;
|
||||
|
||||
/**
|
||||
* 异步发送邮件
|
||||
*/
|
||||
@Async
|
||||
public CompletableFuture<Boolean> sendAsync(String templateHtml, String subject, Map<String, Object> mapValue, String toEmail) {
|
||||
try {
|
||||
MimeMessage message = createMimeMessage(templateHtml, subject, mapValue, toEmail);
|
||||
mailSender.send(message);
|
||||
return CompletableFuture.completedFuture(true);
|
||||
} catch (Exception e) {
|
||||
System.err.println("Error sending async email: " + e.getMessage());
|
||||
return CompletableFuture.completedFuture(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步发送邮件
|
||||
*/
|
||||
public boolean sendSync(String templateHtml, String subject, Map<String, Object> mapValue, String toEmail) {
|
||||
try {
|
||||
MimeMessage message = createMimeMessage(templateHtml, subject, mapValue, toEmail);
|
||||
mailSender.send(message);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
System.err.println("Error sending sync email: " + e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 MimeMessage
|
||||
*/
|
||||
private MimeMessage createMimeMessage(String templateHtml, String subject, Map<String, Object> mapValue, String toEmail) throws MessagingException {
|
||||
Context context = new Context();
|
||||
if (mapValue != null) {
|
||||
mapValue.forEach(context::setVariable);
|
||||
}
|
||||
|
||||
String emailContent = templateEngine.process(templateHtml, context);
|
||||
MimeMessage message = mailSender.createMimeMessage();
|
||||
MimeMessageHelper helper = new MimeMessageHelper(message, true);
|
||||
|
||||
helper.setFrom(fromEmail);
|
||||
helper.setTo(toEmail);
|
||||
helper.setSubject(subject);
|
||||
helper.setText(emailContent, true);
|
||||
|
||||
return message;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package cn.link56.common.utils;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
public class JsonUtils {
|
||||
private static final ObjectMapper objectMapper = new ObjectMapper();
|
||||
// 判断字符串是否为有效的 JSON 格式
|
||||
public static boolean isTypeJSON(String msgText) {
|
||||
try {
|
||||
// 尝试将字符串解析为 JsonNode,如果能解析,说明是有效的 JSON
|
||||
JsonNode node = objectMapper.readTree(msgText);
|
||||
return node != null;
|
||||
} catch (Exception e) {
|
||||
return false; // 如果抛出异常,说明不是有效的 JSON
|
||||
}
|
||||
}
|
||||
|
||||
// 将 JSON 字符串转换为指定类型的 Java 对象
|
||||
public static <T> T toBean(String json, Class<T> clazz) {
|
||||
try {
|
||||
// 使用 ObjectMapper 将 JSON 字符串转换为指定类型的对象
|
||||
return objectMapper.readValue(json, clazz);
|
||||
} catch (JsonProcessingException e) {
|
||||
e.printStackTrace(); // 打印异常信息
|
||||
return null; // 如果转换失败,返回 null
|
||||
}
|
||||
}
|
||||
|
||||
// 将对象转换为 JSON 字符串
|
||||
public static String toJson(Object obj) {
|
||||
try {
|
||||
return objectMapper.writeValueAsString(obj); // 将对象转换为 JSON 字符串
|
||||
} catch (JsonProcessingException e) {
|
||||
System.out.println(e.getMessage());
|
||||
return null; // 如果出现异常,返回 null
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
package cn.link56.common.utils.snowflake.contract;
|
||||
|
||||
public interface IIdGenerator {
|
||||
long newLong() throws IdGeneratorException;
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
package cn.link56.common.utils.snowflake.contract;
|
||||
|
||||
public interface ISnowWorker {
|
||||
long nextId() throws IdGeneratorException;
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package cn.link56.common.utils.snowflake.contract;
|
||||
|
||||
public class IdGeneratorException extends RuntimeException {
|
||||
|
||||
public IdGeneratorException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public IdGeneratorException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public IdGeneratorException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public IdGeneratorException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public IdGeneratorException(String msgFormat, Object... args) {
|
||||
super(String.format(msgFormat, args));
|
||||
}
|
||||
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package cn.link56.common.utils.snowflake.contract;
|
||||
|
||||
public class IdGeneratorOptions {
|
||||
/**
|
||||
* 雪花计算方法
|
||||
* (1-漂移算法|2-传统算法),默认1
|
||||
*/
|
||||
public short Method = 1;
|
||||
|
||||
/**
|
||||
* 基础时间(ms单位)
|
||||
* 不能超过当前系统时间
|
||||
*/
|
||||
public long BaseTime = 1582136402000L;
|
||||
|
||||
/**
|
||||
* 机器码
|
||||
* 必须由外部设定,最大值 2^WorkerIdBitLength-1
|
||||
*/
|
||||
public short WorkerId = 0;
|
||||
|
||||
/**
|
||||
* 机器码位长
|
||||
* 默认值6,取值范围 [1, 15](要求:序列数位长+机器码位长不超过22)
|
||||
*/
|
||||
public byte WorkerIdBitLength = 6;
|
||||
|
||||
/**
|
||||
* 序列数位长
|
||||
* 默认值6,取值范围 [3, 21](要求:序列数位长+机器码位长不超过22)
|
||||
*/
|
||||
public byte SeqBitLength = 6;
|
||||
|
||||
/**
|
||||
* 最大序列数(含)
|
||||
* 设置范围 [MinSeqNumber, 2^SeqBitLength-1],默认值0,表示最大序列数取最大值(2^SeqBitLength-1])
|
||||
*/
|
||||
public short MaxSeqNumber = 0;
|
||||
|
||||
/**
|
||||
* 最小序列数(含)
|
||||
* 默认值5,取值范围 [5, MaxSeqNumber],每毫秒的前5个序列数对应编号是0-4是保留位,其中1-4是时间回拨相应预留位,0是手工新值预留位
|
||||
*/
|
||||
public short MinSeqNumber = 5;
|
||||
|
||||
/**
|
||||
* 最大漂移次数(含)
|
||||
* 默认2000,推荐范围500-10000(与计算能力有关)
|
||||
*/
|
||||
public short TopOverCostCount = 2000;
|
||||
|
||||
public IdGeneratorOptions() {
|
||||
|
||||
}
|
||||
|
||||
public IdGeneratorOptions(short workerId) {
|
||||
WorkerId = workerId;
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package cn.link56.common.utils.snowflake.contract;
|
||||
/**
|
||||
* Id生成时回调参数
|
||||
*/
|
||||
public class OverCostActionArg {
|
||||
/**
|
||||
* 事件类型
|
||||
* 1-开始,2-结束,8-漂移
|
||||
*/
|
||||
public int ActionType = 0;
|
||||
|
||||
/**
|
||||
* 时间戳
|
||||
*/
|
||||
public long TimeTick = 0;
|
||||
|
||||
/**
|
||||
* 机器码
|
||||
*/
|
||||
public short WorkerId = 0;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public int OverCostCountInOneTerm = 0;
|
||||
|
||||
/**
|
||||
* 漂移期间生产ID个数
|
||||
*/
|
||||
public int GenCountInOneTerm = 0;
|
||||
|
||||
/**
|
||||
* 漂移周期
|
||||
*/
|
||||
public int TermIndex = 0;
|
||||
|
||||
public OverCostActionArg(short workerId, long timeTick, int actionType, int overCostCountInOneTerm, int genCountWhenOverCost, int index) {
|
||||
ActionType = actionType;
|
||||
TimeTick = timeTick;
|
||||
WorkerId = workerId;
|
||||
OverCostCountInOneTerm = overCostCountInOneTerm;
|
||||
GenCountInOneTerm = genCountWhenOverCost;
|
||||
TermIndex = index;
|
||||
}
|
||||
}
|
||||
+229
@@ -0,0 +1,229 @@
|
||||
package cn.link56.common.utils.snowflake.core;
|
||||
|
||||
import cn.link56.common.utils.snowflake.contract.ISnowWorker;
|
||||
import cn.link56.common.utils.snowflake.contract.IdGeneratorException;
|
||||
import cn.link56.common.utils.snowflake.contract.IdGeneratorOptions;
|
||||
import cn.link56.common.utils.snowflake.contract.OverCostActionArg;
|
||||
|
||||
public class SnowWorkerM1 implements ISnowWorker {
|
||||
/**
|
||||
* 基础时间
|
||||
*/
|
||||
protected final long BaseTime;
|
||||
|
||||
/**
|
||||
* 机器码
|
||||
*/
|
||||
protected final short WorkerId;
|
||||
|
||||
/**
|
||||
* 机器码位长
|
||||
*/
|
||||
protected final byte WorkerIdBitLength;
|
||||
|
||||
/**
|
||||
* 自增序列数位长
|
||||
*/
|
||||
protected final byte SeqBitLength;
|
||||
|
||||
/**
|
||||
* 最大序列数(含)
|
||||
*/
|
||||
protected final int MaxSeqNumber;
|
||||
|
||||
/**
|
||||
* 最小序列数(含)
|
||||
*/
|
||||
protected final short MinSeqNumber;
|
||||
|
||||
/**
|
||||
* 最大漂移次数(含)
|
||||
*/
|
||||
protected final int TopOverCostCount;
|
||||
|
||||
protected final byte _TimestampShift;
|
||||
protected final static byte[] _SyncLock = new byte[0];
|
||||
|
||||
protected short _CurrentSeqNumber;
|
||||
protected long _LastTimeTick = 0;
|
||||
protected long _TurnBackTimeTick = 0;
|
||||
protected byte _TurnBackIndex = 0;
|
||||
|
||||
protected boolean _IsOverCost = false;
|
||||
protected int _OverCostCountInOneTerm = 0;
|
||||
protected int _GenCountInOneTerm = 0;
|
||||
protected int _TermIndex = 0;
|
||||
|
||||
public SnowWorkerM1(IdGeneratorOptions options) {
|
||||
BaseTime = options.BaseTime != 0 ? options.BaseTime : 1582136402000L;
|
||||
WorkerIdBitLength = options.WorkerIdBitLength == 0 ? 6 : options.WorkerIdBitLength;
|
||||
WorkerId = options.WorkerId;
|
||||
SeqBitLength = options.SeqBitLength == 0 ? 6 : options.SeqBitLength;
|
||||
MaxSeqNumber = options.MaxSeqNumber <= 0 ? (1 << SeqBitLength) - 1 : options.MaxSeqNumber;
|
||||
MinSeqNumber = options.MinSeqNumber;
|
||||
// TopOverCostCount = options.TopOverCostCount == 0 ? 2000 : options.TopOverCostCount;
|
||||
TopOverCostCount = options.TopOverCostCount;
|
||||
_TimestampShift = (byte) (WorkerIdBitLength + SeqBitLength);
|
||||
_CurrentSeqNumber = MinSeqNumber;
|
||||
}
|
||||
|
||||
private void DoGenIdAction(OverCostActionArg arg) {
|
||||
|
||||
}
|
||||
|
||||
private void BeginOverCostAction(long useTimeTick) {
|
||||
|
||||
}
|
||||
|
||||
private void EndOverCostAction(long useTimeTick) {
|
||||
// if (_TermIndex > 10000) {
|
||||
// _TermIndex = 0;
|
||||
// }
|
||||
}
|
||||
|
||||
private void BeginTurnBackAction(long useTimeTick) {
|
||||
|
||||
}
|
||||
|
||||
private void EndTurnBackAction(long useTimeTick) {
|
||||
|
||||
}
|
||||
|
||||
private long NextOverCostId() {
|
||||
long currentTimeTick = GetCurrentTimeTick();
|
||||
|
||||
if (currentTimeTick > _LastTimeTick) {
|
||||
EndOverCostAction(currentTimeTick);
|
||||
|
||||
_LastTimeTick = currentTimeTick;
|
||||
_CurrentSeqNumber = MinSeqNumber;
|
||||
_IsOverCost = false;
|
||||
_OverCostCountInOneTerm = 0;
|
||||
_GenCountInOneTerm = 0;
|
||||
|
||||
return CalcId(_LastTimeTick);
|
||||
}
|
||||
|
||||
if (_OverCostCountInOneTerm >= TopOverCostCount) {
|
||||
EndOverCostAction(currentTimeTick);
|
||||
|
||||
_LastTimeTick = GetNextTimeTick();
|
||||
_CurrentSeqNumber = MinSeqNumber;
|
||||
_IsOverCost = false;
|
||||
_OverCostCountInOneTerm = 0;
|
||||
_GenCountInOneTerm = 0;
|
||||
|
||||
return CalcId(_LastTimeTick);
|
||||
}
|
||||
|
||||
if (_CurrentSeqNumber > MaxSeqNumber) {
|
||||
_LastTimeTick++;
|
||||
_CurrentSeqNumber = MinSeqNumber;
|
||||
_IsOverCost = true;
|
||||
_OverCostCountInOneTerm++;
|
||||
_GenCountInOneTerm++;
|
||||
|
||||
return CalcId(_LastTimeTick);
|
||||
}
|
||||
|
||||
_GenCountInOneTerm++;
|
||||
return CalcId(_LastTimeTick);
|
||||
}
|
||||
|
||||
private long NextNormalId() throws IdGeneratorException {
|
||||
long currentTimeTick = GetCurrentTimeTick();
|
||||
|
||||
if (currentTimeTick < _LastTimeTick) {
|
||||
if (_TurnBackTimeTick < 1) {
|
||||
_TurnBackTimeTick = _LastTimeTick - 1;
|
||||
_TurnBackIndex++;
|
||||
// 每毫秒序列数的前5位是预留位,0用于手工新值,1-4是时间回拨次序
|
||||
// 支持4次回拨次序(避免回拨重叠导致ID重复),可无限次回拨(次序循环使用)。
|
||||
if (_TurnBackIndex > 4) {
|
||||
_TurnBackIndex = 1;
|
||||
}
|
||||
//BeginTurnBackAction(_TurnBackTimeTick);
|
||||
}
|
||||
|
||||
// try {
|
||||
// Thread.sleep(1);
|
||||
// } catch (InterruptedException e) {
|
||||
// e.printStackTrace();
|
||||
// }
|
||||
|
||||
return CalcTurnBackId(_TurnBackTimeTick);
|
||||
}
|
||||
|
||||
// 时间追平时,_TurnBackTimeTick清零
|
||||
if (_TurnBackTimeTick > 0) {
|
||||
EndTurnBackAction(_TurnBackTimeTick);
|
||||
_TurnBackTimeTick = 0;
|
||||
}
|
||||
|
||||
if (currentTimeTick > _LastTimeTick) {
|
||||
_LastTimeTick = currentTimeTick;
|
||||
_CurrentSeqNumber = MinSeqNumber;
|
||||
|
||||
return CalcId(_LastTimeTick);
|
||||
}
|
||||
|
||||
if (_CurrentSeqNumber > MaxSeqNumber) {
|
||||
BeginOverCostAction(currentTimeTick);
|
||||
|
||||
_TermIndex++;
|
||||
_LastTimeTick++;
|
||||
_CurrentSeqNumber = MinSeqNumber;
|
||||
_IsOverCost = true;
|
||||
_OverCostCountInOneTerm = 1;
|
||||
_GenCountInOneTerm = 1;
|
||||
|
||||
return CalcId(_LastTimeTick);
|
||||
}
|
||||
|
||||
return CalcId(_LastTimeTick);
|
||||
}
|
||||
|
||||
private long CalcId(long useTimeTick) {
|
||||
long result = ((useTimeTick << _TimestampShift) +
|
||||
((long) WorkerId << SeqBitLength) +
|
||||
(int) _CurrentSeqNumber);
|
||||
|
||||
_CurrentSeqNumber++;
|
||||
return result;
|
||||
}
|
||||
|
||||
private long CalcTurnBackId(long useTimeTick) {
|
||||
long result = ((useTimeTick << _TimestampShift) +
|
||||
((long) WorkerId << SeqBitLength) + _TurnBackIndex);
|
||||
|
||||
_TurnBackTimeTick--;
|
||||
return result;
|
||||
}
|
||||
|
||||
protected long GetCurrentTimeTick() {
|
||||
long millis = System.currentTimeMillis();
|
||||
return millis - BaseTime;
|
||||
}
|
||||
|
||||
protected long GetNextTimeTick() {
|
||||
long tempTimeTicker = GetCurrentTimeTick();
|
||||
|
||||
while (tempTimeTicker <= _LastTimeTick) {
|
||||
try {
|
||||
Thread.sleep(1);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
tempTimeTicker = GetCurrentTimeTick();
|
||||
}
|
||||
|
||||
return tempTimeTicker;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long nextId() {
|
||||
synchronized (_SyncLock) {
|
||||
return _IsOverCost ? NextOverCostId() : NextNormalId();
|
||||
}
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package cn.link56.common.utils.snowflake.core;
|
||||
|
||||
import cn.link56.common.utils.snowflake.contract.IdGeneratorException;
|
||||
import cn.link56.common.utils.snowflake.contract.IdGeneratorOptions;
|
||||
|
||||
public class SnowWorkerM2 extends SnowWorkerM1 {
|
||||
public SnowWorkerM2(IdGeneratorOptions options) {
|
||||
super(options);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long nextId() {
|
||||
synchronized (_SyncLock) {
|
||||
long currentTimeTick = GetCurrentTimeTick();
|
||||
|
||||
if (_LastTimeTick == currentTimeTick) {
|
||||
if (_CurrentSeqNumber++ > MaxSeqNumber) {
|
||||
_CurrentSeqNumber = MinSeqNumber;
|
||||
currentTimeTick = GetNextTimeTick();
|
||||
}
|
||||
} else {
|
||||
_CurrentSeqNumber = MinSeqNumber;
|
||||
}
|
||||
|
||||
if (currentTimeTick < _LastTimeTick) {
|
||||
throw new IdGeneratorException("Time error for {0} milliseconds", _LastTimeTick - currentTimeTick);
|
||||
}
|
||||
|
||||
_LastTimeTick = currentTimeTick;
|
||||
long result = ((currentTimeTick << _TimestampShift) + ((long) WorkerId << SeqBitLength) + (int) _CurrentSeqNumber);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
package cn.link56.common.utils.snowflake.idgen;
|
||||
|
||||
import cn.link56.common.utils.snowflake.contract.IIdGenerator;
|
||||
import cn.link56.common.utils.snowflake.contract.ISnowWorker;
|
||||
import cn.link56.common.utils.snowflake.contract.IdGeneratorException;
|
||||
import cn.link56.common.utils.snowflake.contract.IdGeneratorOptions;
|
||||
import cn.link56.common.utils.snowflake.core.SnowWorkerM1;
|
||||
import cn.link56.common.utils.snowflake.core.SnowWorkerM2;
|
||||
|
||||
public class DefaultIdGenerator implements IIdGenerator {
|
||||
private static ISnowWorker _SnowWorker = null;
|
||||
|
||||
public DefaultIdGenerator(IdGeneratorOptions options) throws IdGeneratorException {
|
||||
if (options == null) {
|
||||
throw new IdGeneratorException("options error.");
|
||||
}
|
||||
|
||||
// 1.BaseTime
|
||||
if (options.BaseTime < 315504000000L || options.BaseTime > System.currentTimeMillis()) {
|
||||
throw new IdGeneratorException("BaseTime error.");
|
||||
}
|
||||
|
||||
// 2.WorkerIdBitLength
|
||||
if (options.WorkerIdBitLength <= 0) {
|
||||
throw new IdGeneratorException("WorkerIdBitLength error.(range:[1, 21])");
|
||||
}
|
||||
if (options.WorkerIdBitLength + options.SeqBitLength > 22) {
|
||||
throw new IdGeneratorException("error:WorkerIdBitLength + SeqBitLength <= 22");
|
||||
}
|
||||
|
||||
// 3.WorkerId
|
||||
int maxWorkerIdNumber = (1 << options.WorkerIdBitLength) - 1;
|
||||
if (maxWorkerIdNumber == 0) {
|
||||
maxWorkerIdNumber = 63;
|
||||
}
|
||||
if (options.WorkerId < 0 || options.WorkerId > maxWorkerIdNumber) {
|
||||
throw new IdGeneratorException(
|
||||
"WorkerId error. (range:[0, " + (maxWorkerIdNumber > 0 ? maxWorkerIdNumber : 63) + "]");
|
||||
}
|
||||
|
||||
// 4.SeqBitLength
|
||||
if (options.SeqBitLength < 2 || options.SeqBitLength > 21) {
|
||||
throw new IdGeneratorException("SeqBitLength error. (range:[2, 21])");
|
||||
}
|
||||
|
||||
// 5.MaxSeqNumber
|
||||
int maxSeqNumber = (1 << options.SeqBitLength) - 1;
|
||||
if (maxSeqNumber == 0) {
|
||||
maxSeqNumber = 63;
|
||||
}
|
||||
if (options.MaxSeqNumber < 0 || options.MaxSeqNumber > maxSeqNumber) {
|
||||
throw new IdGeneratorException("MaxSeqNumber error. (range:[1, " + maxSeqNumber + "]");
|
||||
}
|
||||
|
||||
// 6.MinSeqNumber
|
||||
if (options.MinSeqNumber < 5 || options.MinSeqNumber > maxSeqNumber) {
|
||||
throw new IdGeneratorException("MinSeqNumber error. (range:[5, " + maxSeqNumber + "]");
|
||||
}
|
||||
|
||||
// 7.TopOverCostCount
|
||||
if (options.TopOverCostCount < 0 || options.TopOverCostCount > 10000) {
|
||||
throw new IdGeneratorException("TopOverCostCount error. (range:[0, 10000]");
|
||||
}
|
||||
|
||||
switch (options.Method) {
|
||||
case 2:
|
||||
_SnowWorker = new SnowWorkerM2(options);
|
||||
break;
|
||||
case 1:
|
||||
default:
|
||||
_SnowWorker = new SnowWorkerM1(options);
|
||||
break;
|
||||
}
|
||||
|
||||
if (options.Method == 1) {
|
||||
try {
|
||||
Thread.sleep(500);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long newLong() {
|
||||
return _SnowWorker.nextId();
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package cn.link56.common.utils.snowflake.idgen;
|
||||
|
||||
import cn.link56.common.utils.snowflake.contract.IIdGenerator;
|
||||
import cn.link56.common.utils.snowflake.contract.IdGeneratorException;
|
||||
import cn.link56.common.utils.snowflake.contract.IdGeneratorOptions;
|
||||
import lombok.Getter;
|
||||
|
||||
public class IdGenerator {
|
||||
@Getter
|
||||
private static IIdGenerator idGenInstance = null;
|
||||
|
||||
/**
|
||||
* 设置参数,建议程序初始化时执行一次
|
||||
*/
|
||||
public static void setIdGenerator(IdGeneratorOptions options) throws IdGeneratorException {
|
||||
idGenInstance = new DefaultIdGenerator(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成新的Id
|
||||
* 调用本方法前,请确保调用了 SetIdGenerator 方法做初始化。
|
||||
*
|
||||
*/
|
||||
public static long nextId() throws IdGeneratorException {
|
||||
// if (idGenInstance == null) {
|
||||
// idGenInstance = new DefaultIdGenerator(new IdGeneratorOptions((short) 1));
|
||||
// }
|
||||
|
||||
if (idGenInstance == null)
|
||||
throw new IdGeneratorException("Please initialize Yitter.IdGeneratorOptions first.");
|
||||
|
||||
return idGenInstance.newLong();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
server:
|
||||
port: 8084
|
||||
spring:
|
||||
datasource:
|
||||
type: com.alibaba.druid.pool.DruidDataSource
|
||||
driver-class-name: net.sourceforge.jtds.jdbc.Driver
|
||||
url: jdbc:jtds:sqlserver://192.168.1.189:1433;databaseName=G3-Link
|
||||
username: sa
|
||||
password: "g3soft"
|
||||
# druid 连接池管理
|
||||
druid:
|
||||
# 初始化时建立物理连接的个数
|
||||
initial-size: 5
|
||||
# 连接池的最小空闲数量
|
||||
min-idle: 5
|
||||
# 连接池最大连接数量
|
||||
max-active: 20
|
||||
# 获取连接时最大等待时间,单位毫秒
|
||||
max-wait: 60000
|
||||
# 申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
|
||||
test-while-idle: true
|
||||
# 既作为检测的间隔时间又作为testWhileIdel执行的依据
|
||||
time-between-eviction-runs-millis: 60000
|
||||
# 销毁线程时检测当前连接的最后活动时间和当前时间差大于该值时,关闭当前连接(配置连接在池中的最小生存时间)
|
||||
min-evictable-idle-time-millis: 30000
|
||||
# 用来检测数据库连接是否有效的sql 必须是一个查询语句(oracle中为 select 1 from dual) validation-query: select 'x'
|
||||
# 申请连接时会执行validationQuery检测连接是否有效,开启会降低性能,默认为true
|
||||
test-on-borrow: false
|
||||
# 归还连接时会执行validationQuery检测连接是否有效,开启会降低性能,默认为true
|
||||
test-on-return: false
|
||||
# 是否缓存preparedStatement, 也就是PSCache,PSCache对支持游标的数据库性能提升巨大,比如说oracle,在mysql下建议关闭。
|
||||
pool-prepared-statements: false
|
||||
# 置监控统计拦截的filters,去掉后监控界面sql无法统计,stat: 监控统计、Slf4j:日志记录、waLL: 防御sqL注入
|
||||
filters: stat,wall,slf4j
|
||||
# 要启用PSCache,必须配置大于0,当大于0时,poolPreparedStatements自动触发修改为true。在Druid中,不会存在Oracle下PSCache占用内存过多的问题,可以把这个数值配置大一些,比如说100
|
||||
max-pool-prepared-statement-per-connection-size: -1
|
||||
# 合并多个DruidDataSource的监控数据
|
||||
use-global-data-source-stat: true
|
||||
# 通过connectProperties属性来打开mergeSql功能;慢SQL记录
|
||||
connect-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
|
||||
|
||||
stat-view-servlet:
|
||||
# 是否启用StatViewServlet默认值true
|
||||
enabled: true
|
||||
# 访问路径为/druid时,跳转到StatViewServlet
|
||||
url-pattern: /druid/*
|
||||
# 是否能够重置数据
|
||||
reset-enable: false
|
||||
# 需要账号密码才能访问控制台,默认为root
|
||||
login-username: root
|
||||
login-password: root
|
||||
# IP白名单
|
||||
allow: 127.0.0.1
|
||||
# IP黑名单(共同存在时,deny优先于allow)
|
||||
deny:
|
||||
@@ -0,0 +1,36 @@
|
||||
server:
|
||||
servlet:
|
||||
context-path: / # 应用的访问路径
|
||||
tomcat:
|
||||
uri-encoding: UTF-8 # URI编码
|
||||
accept-count: 1000 # 连接数满后的排队数
|
||||
threads:
|
||||
max: 800 # 最大线程数
|
||||
min-spare: 100 # 初始化的线程数
|
||||
max-http-header-size: 64KB
|
||||
logging:
|
||||
level:
|
||||
com.client: debug # 调试级别的日志
|
||||
org.springframework: warn # 仅显示警告级别及以上的Spring日志
|
||||
|
||||
spring:
|
||||
profiles:
|
||||
active: dev
|
||||
mail:
|
||||
host: smtp.163.com #邮箱服务器地址
|
||||
username: hi_itao@163.com #邮箱账号
|
||||
password: GVdEzKAee9MByXdy #邮箱授权码
|
||||
default-encoding: utf-8 #默认编码
|
||||
|
||||
mybatis-plus:
|
||||
# 对应的 XML 文件位置
|
||||
mapperLocations: classpath*:mapper/**/*Mapper.xml
|
||||
# 实体扫描,多个package用逗号或者分号分隔
|
||||
typeAliasesPackage: cn.link56.domain
|
||||
# configLocation: classpath:mybatis/mybatis-config.xml
|
||||
configuration:
|
||||
cache-enabled: true
|
||||
use-generated-keys: true
|
||||
default-executor-type: simple
|
||||
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
||||
map-underscore-to-camel-case: true
|
||||
@@ -0,0 +1,75 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
<!-- 日志存放路径 -->
|
||||
<property name="log.path" value="D:\\link56-log" />
|
||||
<!-- 日志输出格式 -->
|
||||
<property name="log.pattern" value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n" />
|
||||
|
||||
<!-- 控制台输出 -->
|
||||
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>${log.pattern}</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<!-- 系统日志输出 -->
|
||||
<appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${log.path}/sys-info.log</file>
|
||||
<!-- 循环政策:基于时间创建日志文件 -->
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<!-- 日志文件名格式 -->
|
||||
<fileNamePattern>${log.path}/sys-info.%d{yyyy-MM-dd}.log</fileNamePattern>
|
||||
<!-- 日志最大的历史 60天 -->
|
||||
<maxHistory>60</maxHistory>
|
||||
</rollingPolicy>
|
||||
<encoder>
|
||||
<pattern>${log.pattern}</pattern>
|
||||
</encoder>
|
||||
<filter class="ch.qos.logback.classic.filter.LevelFilter">
|
||||
<!-- 过滤的级别 -->
|
||||
<level>INFO</level>
|
||||
<!-- 匹配时的操作:接收(记录) -->
|
||||
<onMatch>ACCEPT</onMatch>
|
||||
<!-- 不匹配时的操作:拒绝(不记录) -->
|
||||
<onMismatch>DENY</onMismatch>
|
||||
</filter>
|
||||
</appender>
|
||||
|
||||
<appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${log.path}/sys-error.log</file>
|
||||
<!-- 循环政策:基于时间创建日志文件 -->
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<!-- 日志文件名格式 -->
|
||||
<fileNamePattern>${log.path}/sys-error.%d{yyyy-MM-dd}.log</fileNamePattern>
|
||||
<!-- 日志最大的历史 60天 -->
|
||||
<maxHistory>60</maxHistory>
|
||||
</rollingPolicy>
|
||||
<encoder>
|
||||
<pattern>${log.pattern}</pattern>
|
||||
</encoder>
|
||||
<filter class="ch.qos.logback.classic.filter.LevelFilter">
|
||||
<!-- 过滤的级别 -->
|
||||
<level>ERROR</level>
|
||||
<!-- 匹配时的操作:接收(记录) -->
|
||||
<onMatch>ACCEPT</onMatch>
|
||||
<!-- 不匹配时的操作:拒绝(不记录) -->
|
||||
<onMismatch>DENY</onMismatch>
|
||||
</filter>
|
||||
</appender>
|
||||
|
||||
<!-- 系统模块日志级别控制 -->
|
||||
<logger name="cn.link56" level="info" />
|
||||
<!-- Spring日志级别控制 -->
|
||||
<logger name="org.springframework" level="warn" />
|
||||
|
||||
<root level="info">
|
||||
<appender-ref ref="console" />
|
||||
</root>
|
||||
|
||||
<!--系统操作日志-->
|
||||
<root level="info">
|
||||
<appender-ref ref="file_info" />
|
||||
<appender-ref ref="file_error" />
|
||||
</root>
|
||||
|
||||
</configuration>
|
||||
@@ -0,0 +1,536 @@
|
||||
<!DOCTYPE html
|
||||
PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="x-apple-disable-message-reformatting" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
<meta name="supported-color-schemes" content="light dark" />
|
||||
<title></title>
|
||||
<style type="text/css" rel="stylesheet" media="all">
|
||||
/* Base ------------------------------ */
|
||||
|
||||
@import url("https://fonts.googleapis.com/css?family=Nunito+Sans:400,700&display=swap");
|
||||
|
||||
body {
|
||||
width: 100% !important;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
-webkit-text-size-adjust: none;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #3869D4;
|
||||
}
|
||||
|
||||
a img {
|
||||
border: none;
|
||||
}
|
||||
|
||||
td {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.preheader {
|
||||
display: none !important;
|
||||
visibility: hidden;
|
||||
mso-hide: all;
|
||||
font-size: 1px;
|
||||
line-height: 1px;
|
||||
max-height: 0;
|
||||
max-width: 0;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Type ------------------------------ */
|
||||
|
||||
body,
|
||||
td,
|
||||
th {
|
||||
font-family: "Nunito Sans", Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-top: 0;
|
||||
color: #333333;
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-top: 0;
|
||||
color: #333333;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-top: 0;
|
||||
color: #333333;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
p,
|
||||
ul,
|
||||
ol,
|
||||
blockquote {
|
||||
margin: .4em 0 1.1875em;
|
||||
font-size: 16px;
|
||||
line-height: 1.625;
|
||||
}
|
||||
|
||||
p.sub {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* Utilities ------------------------------ */
|
||||
|
||||
.align-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.align-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.align-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.u-margin-bottom-none {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Buttons ------------------------------ */
|
||||
|
||||
.button {
|
||||
background-color: #3869D4;
|
||||
border-top: 10px solid #3869D4;
|
||||
border-right: 18px solid #3869D4;
|
||||
border-bottom: 10px solid #3869D4;
|
||||
border-left: 18px solid #3869D4;
|
||||
display: inline-block;
|
||||
color: #FFF;
|
||||
text-decoration: none;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.16);
|
||||
-webkit-text-size-adjust: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.button--green {
|
||||
background-color: #22BC66;
|
||||
border-top: 10px solid #22BC66;
|
||||
border-right: 18px solid #22BC66;
|
||||
border-bottom: 10px solid #22BC66;
|
||||
border-left: 18px solid #22BC66;
|
||||
}
|
||||
|
||||
.button--red {
|
||||
background-color: #FF6136;
|
||||
border-top: 10px solid #FF6136;
|
||||
border-right: 18px solid #FF6136;
|
||||
border-bottom: 10px solid #FF6136;
|
||||
border-left: 18px solid #FF6136;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 500px) {
|
||||
.button {
|
||||
width: 100% !important;
|
||||
text-align: center !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Attribute list ------------------------------ */
|
||||
|
||||
.attributes {
|
||||
margin: 0 0 21px;
|
||||
}
|
||||
|
||||
.attributes_content {
|
||||
background-color: #F4F4F7;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.attributes_item {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Related Items ------------------------------ */
|
||||
|
||||
.related {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 25px 0 0 0;
|
||||
-premailer-width: 100%;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
}
|
||||
|
||||
.related_item {
|
||||
padding: 10px 0;
|
||||
color: #CBCCCF;
|
||||
font-size: 15px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.related_item-title {
|
||||
display: block;
|
||||
margin: .5em 0 0;
|
||||
}
|
||||
|
||||
.related_item-thumb {
|
||||
display: block;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.related_heading {
|
||||
border-top: 1px solid #CBCCCF;
|
||||
text-align: center;
|
||||
padding: 25px 0 10px;
|
||||
}
|
||||
|
||||
/* Discount Code ------------------------------ */
|
||||
|
||||
.discount {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 24px;
|
||||
-premailer-width: 100%;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
background-color: #F4F4F7;
|
||||
border: 2px dashed #CBCCCF;
|
||||
}
|
||||
|
||||
.discount_heading {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.discount_body {
|
||||
text-align: center;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
/* Social Icons ------------------------------ */
|
||||
|
||||
.social {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.social td {
|
||||
padding: 0;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.social_icon {
|
||||
height: 20px;
|
||||
margin: 0 8px 10px 8px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Data table ------------------------------ */
|
||||
|
||||
.purchase {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 35px 0;
|
||||
-premailer-width: 100%;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
}
|
||||
|
||||
.purchase_content {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 25px 0 0 0;
|
||||
-premailer-width: 100%;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
}
|
||||
|
||||
.purchase_item {
|
||||
padding: 10px 0;
|
||||
color: #51545E;
|
||||
font-size: 15px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.purchase_heading {
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #EAEAEC;
|
||||
}
|
||||
|
||||
.purchase_heading p {
|
||||
margin: 0;
|
||||
color: #85878E;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.purchase_footer {
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #EAEAEC;
|
||||
}
|
||||
|
||||
.purchase_total {
|
||||
margin: 0;
|
||||
text-align: right;
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.purchase_total--label {
|
||||
padding: 0 15px 0 0;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #F2F4F6;
|
||||
color: #51545E;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #51545E;
|
||||
}
|
||||
|
||||
.email-wrapper {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-premailer-width: 100%;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
background-color: #F2F4F6;
|
||||
}
|
||||
|
||||
.email-content {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-premailer-width: 100%;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
}
|
||||
|
||||
/* Masthead ----------------------- */
|
||||
|
||||
.email-masthead {
|
||||
padding: 25px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.email-masthead_logo {
|
||||
width: 94px;
|
||||
}
|
||||
|
||||
.email-masthead_name {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #A8AAAF;
|
||||
text-decoration: none;
|
||||
text-shadow: 0 1px 0 white;
|
||||
}
|
||||
|
||||
/* Body ------------------------------ */
|
||||
|
||||
.email-body {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-premailer-width: 100%;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
}
|
||||
|
||||
.email-body_inner {
|
||||
width: 570px;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
-premailer-width: 570px;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
background-color: #FFFFFF;
|
||||
}
|
||||
|
||||
.email-footer {
|
||||
width: 570px;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
-premailer-width: 570px;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.email-footer p {
|
||||
color: #A8AAAF;
|
||||
}
|
||||
|
||||
.body-action {
|
||||
width: 100%;
|
||||
margin: 30px auto;
|
||||
padding: 0;
|
||||
-premailer-width: 100%;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.body-sub {
|
||||
margin-top: 25px;
|
||||
padding-top: 25px;
|
||||
border-top: 1px solid #EAEAEC;
|
||||
}
|
||||
|
||||
.content-cell {
|
||||
padding: 45px;
|
||||
}
|
||||
|
||||
/*Media Queries ------------------------------ */
|
||||
|
||||
@media only screen and (max-width: 600px) {
|
||||
|
||||
.email-body_inner,
|
||||
.email-footer {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
|
||||
body,
|
||||
.email-body,
|
||||
.email-body_inner,
|
||||
.email-content,
|
||||
.email-wrapper,
|
||||
.email-masthead,
|
||||
.email-footer {
|
||||
background-color: #333333 !important;
|
||||
color: #FFF !important;
|
||||
}
|
||||
|
||||
p,
|
||||
ul,
|
||||
ol,
|
||||
blockquote,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
span,
|
||||
.purchase_item {
|
||||
color: #FFF !important;
|
||||
}
|
||||
|
||||
.attributes_content,
|
||||
.discount {
|
||||
background-color: #222 !important;
|
||||
}
|
||||
|
||||
.email-masthead_name {
|
||||
text-shadow: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
supported-color-schemes: light dark;
|
||||
}
|
||||
</style>
|
||||
<!--[if mso]>
|
||||
<style type="text/css">
|
||||
.f-fallback {
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
</style>
|
||||
<![endif]-->
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<span class="preheader">Use this link to reset your password. The link is only valid for 24 hours.</span>
|
||||
<table class="email-wrapper" width="100%" cellpadding="0" cellspacing="0" role="presentation">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table class="email-content" width="100%" cellpadding="0" cellspacing="0" role="presentation">
|
||||
<tr>
|
||||
<td class="email-masthead">
|
||||
<a href="https://example.com" class="f-fallback email-masthead_name">
|
||||
link56
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Email Body -->
|
||||
<tr>
|
||||
<td class="email-body" width="570" cellpadding="0" cellspacing="0">
|
||||
<table class="email-body_inner" align="center" width="570" cellpadding="0" cellspacing="0"
|
||||
role="presentation">
|
||||
<!-- Body content -->
|
||||
<tr>
|
||||
<td class="content-cell">
|
||||
<div class="f-fallback">
|
||||
<h1>您好 [[${Name}]],</h1>
|
||||
<p>您最近请求重置 link56 账户的密码。请点击下面的按钮进行密码重置。<strong>该重置链接仅在 10 分钟内有效。</strong></p>
|
||||
|
||||
<!-- Action -->
|
||||
<table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0"
|
||||
role="presentation">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<!-- Border based button
|
||||
https://litmus.com/blog/a-guide-to-bulletproof-buttons-in-email-design -->
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0" role="presentation">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="[[${ResetUrl}]]" class="f-fallback button button--green"
|
||||
target="_blank">重置您的密码</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<p>如果不是您本人操作,请忽略此邮件,或如有疑问,请 <a
|
||||
href="[[${SupportUrl}]]">联系客服</a>。</p>
|
||||
|
||||
<p>
|
||||
感谢,<br>
|
||||
link56 团队
|
||||
</p>
|
||||
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<table class="email-footer" align="center" width="570" cellpadding="0" cellspacing="0"
|
||||
role="presentation">
|
||||
<tr>
|
||||
<td class="content-cell" align="center">
|
||||
<p class="f-fallback sub align-center">
|
||||
宁波江东远通计算机有限公司
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,522 @@
|
||||
<!DOCTYPE html
|
||||
PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="x-apple-disable-message-reformatting" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
<meta name="supported-color-schemes" content="light dark" />
|
||||
<title></title>
|
||||
<style type="text/css" rel="stylesheet" media="all">
|
||||
/* Base ------------------------------ */
|
||||
|
||||
@import url("https://fonts.googleapis.com/css?family=Nunito+Sans:400,700&display=swap");
|
||||
|
||||
body {
|
||||
width: 100% !important;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
-webkit-text-size-adjust: none;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #3869D4;
|
||||
}
|
||||
|
||||
a img {
|
||||
border: none;
|
||||
}
|
||||
|
||||
td {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.preheader {
|
||||
display: none !important;
|
||||
visibility: hidden;
|
||||
mso-hide: all;
|
||||
font-size: 1px;
|
||||
line-height: 1px;
|
||||
max-height: 0;
|
||||
max-width: 0;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Type ------------------------------ */
|
||||
|
||||
body,
|
||||
td,
|
||||
th {
|
||||
font-family: "Nunito Sans", Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-top: 0;
|
||||
color: #333333;
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-top: 0;
|
||||
color: #333333;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-top: 0;
|
||||
color: #333333;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
p,
|
||||
ul,
|
||||
ol,
|
||||
blockquote {
|
||||
margin: .4em 0 1.1875em;
|
||||
font-size: 16px;
|
||||
line-height: 1.625;
|
||||
}
|
||||
|
||||
p.sub {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* Utilities ------------------------------ */
|
||||
|
||||
.align-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.align-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.align-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.u-margin-bottom-none {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Buttons ------------------------------ */
|
||||
|
||||
.button {
|
||||
background-color: #3869D4;
|
||||
border-top: 10px solid #3869D4;
|
||||
border-right: 18px solid #3869D4;
|
||||
border-bottom: 10px solid #3869D4;
|
||||
border-left: 18px solid #3869D4;
|
||||
display: inline-block;
|
||||
color: #FFF;
|
||||
text-decoration: none;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.16);
|
||||
-webkit-text-size-adjust: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.button--green {
|
||||
background-color: #22BC66;
|
||||
border-top: 10px solid #22BC66;
|
||||
border-right: 18px solid #22BC66;
|
||||
border-bottom: 10px solid #22BC66;
|
||||
border-left: 18px solid #22BC66;
|
||||
}
|
||||
|
||||
.button--red {
|
||||
background-color: #FF6136;
|
||||
border-top: 10px solid #FF6136;
|
||||
border-right: 18px solid #FF6136;
|
||||
border-bottom: 10px solid #FF6136;
|
||||
border-left: 18px solid #FF6136;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 500px) {
|
||||
.button {
|
||||
width: 100% !important;
|
||||
text-align: center !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Attribute list ------------------------------ */
|
||||
|
||||
.attributes {
|
||||
margin: 0 0 21px;
|
||||
}
|
||||
|
||||
.attributes_content {
|
||||
background-color: #F4F4F7;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.attributes_item {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Related Items ------------------------------ */
|
||||
|
||||
.related {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 25px 0 0 0;
|
||||
-premailer-width: 100%;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
}
|
||||
|
||||
.related_item {
|
||||
padding: 10px 0;
|
||||
color: #CBCCCF;
|
||||
font-size: 15px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.related_item-title {
|
||||
display: block;
|
||||
margin: .5em 0 0;
|
||||
}
|
||||
|
||||
.related_item-thumb {
|
||||
display: block;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.related_heading {
|
||||
border-top: 1px solid #CBCCCF;
|
||||
text-align: center;
|
||||
padding: 25px 0 10px;
|
||||
}
|
||||
|
||||
/* Discount Code ------------------------------ */
|
||||
|
||||
.discount {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 24px;
|
||||
-premailer-width: 100%;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
background-color: #F4F4F7;
|
||||
border: 2px dashed #CBCCCF;
|
||||
}
|
||||
|
||||
.discount_heading {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.discount_body {
|
||||
text-align: center;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
/* Social Icons ------------------------------ */
|
||||
|
||||
.social {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.social td {
|
||||
padding: 0;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.social_icon {
|
||||
height: 20px;
|
||||
margin: 0 8px 10px 8px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Data table ------------------------------ */
|
||||
|
||||
.purchase {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 35px 0;
|
||||
-premailer-width: 100%;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
}
|
||||
|
||||
.purchase_content {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 25px 0 0 0;
|
||||
-premailer-width: 100%;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
}
|
||||
|
||||
.purchase_item {
|
||||
padding: 10px 0;
|
||||
color: #51545E;
|
||||
font-size: 15px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.purchase_heading {
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #EAEAEC;
|
||||
}
|
||||
|
||||
.purchase_heading p {
|
||||
margin: 0;
|
||||
color: #85878E;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.purchase_footer {
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #EAEAEC;
|
||||
}
|
||||
|
||||
.purchase_total {
|
||||
margin: 0;
|
||||
text-align: right;
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.purchase_total--label {
|
||||
padding: 0 15px 0 0;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #F2F4F6;
|
||||
color: #51545E;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #51545E;
|
||||
}
|
||||
|
||||
.email-wrapper {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-premailer-width: 100%;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
background-color: #F2F4F6;
|
||||
}
|
||||
|
||||
.email-content {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-premailer-width: 100%;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
}
|
||||
|
||||
/* Masthead ----------------------- */
|
||||
|
||||
.email-masthead {
|
||||
padding: 25px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.email-masthead_logo {
|
||||
width: 94px;
|
||||
}
|
||||
|
||||
.email-masthead_name {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #A8AAAF;
|
||||
text-decoration: none;
|
||||
text-shadow: 0 1px 0 white;
|
||||
}
|
||||
|
||||
/* Body ------------------------------ */
|
||||
|
||||
.email-body {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-premailer-width: 100%;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
}
|
||||
|
||||
.email-body_inner {
|
||||
width: 570px;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
-premailer-width: 570px;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
background-color: #FFFFFF;
|
||||
}
|
||||
|
||||
.email-footer {
|
||||
width: 570px;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
-premailer-width: 570px;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.email-footer p {
|
||||
color: #A8AAAF;
|
||||
}
|
||||
|
||||
.body-action {
|
||||
width: 100%;
|
||||
margin: 30px auto;
|
||||
padding: 0;
|
||||
font-size: 42px;
|
||||
-premailer-width: 100%;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.body-sub {
|
||||
margin-top: 25px;
|
||||
padding-top: 25px;
|
||||
border-top: 1px solid #EAEAEC;
|
||||
}
|
||||
|
||||
.content-cell {
|
||||
padding: 45px;
|
||||
}
|
||||
|
||||
/*Media Queries ------------------------------ */
|
||||
|
||||
@media only screen and (max-width: 600px) {
|
||||
|
||||
.email-body_inner,
|
||||
.email-footer {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
|
||||
body,
|
||||
.email-body,
|
||||
.email-body_inner,
|
||||
.email-content,
|
||||
.email-wrapper,
|
||||
.email-masthead,
|
||||
.email-footer {
|
||||
background-color: #333333 !important;
|
||||
color: #FFF !important;
|
||||
}
|
||||
|
||||
p,
|
||||
ul,
|
||||
ol,
|
||||
blockquote,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
span,
|
||||
.purchase_item {
|
||||
color: #FFF !important;
|
||||
}
|
||||
|
||||
.attributes_content,
|
||||
.discount {
|
||||
background-color: #222 !important;
|
||||
}
|
||||
|
||||
.email-masthead_name {
|
||||
text-shadow: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
supported-color-schemes: light dark;
|
||||
}
|
||||
</style>
|
||||
<!--[if mso]>
|
||||
<style type="text/css">
|
||||
.f-fallback {
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
</style>
|
||||
<![endif]-->
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<span class="preheader">Use this link to reset your password. The link is only valid for 24 hours.</span>
|
||||
<table class="email-wrapper" width="100%" cellpadding="0" cellspacing="0" role="presentation">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table class="email-content" width="100%" cellpadding="0" cellspacing="0" role="presentation">
|
||||
<tr>
|
||||
<td class="email-masthead">
|
||||
<a href="https://example.com" class="f-fallback email-masthead_name">
|
||||
link56
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Email Body -->
|
||||
<tr>
|
||||
<td class="email-body" width="570" cellpadding="0" cellspacing="0">
|
||||
<table class="email-body_inner" align="center" width="570" cellpadding="0" cellspacing="0"
|
||||
role="presentation">
|
||||
<!-- Body content -->
|
||||
<tr>
|
||||
<td class="content-cell">
|
||||
<div class="f-fallback">
|
||||
<h1>您好</h1>
|
||||
<p>您正在注册我们的 link56 平台,系统已为您生成了注册验证码。<strong>该验证码仅在 10 分钟内有效。</strong></p>
|
||||
|
||||
<p class="body-action">
|
||||
[[${Code}]]
|
||||
</p>
|
||||
<p>如果不是您本人操作,请忽略此邮件,或如有疑问,请 <a
|
||||
href="{{SupportUrl}}">联系客服</a>。</p>
|
||||
|
||||
<p>
|
||||
感谢,<br>
|
||||
link56 团队
|
||||
</p>
|
||||
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<table class="email-footer" align="center" width="570" cellpadding="0" cellspacing="0"
|
||||
role="presentation">
|
||||
<tr>
|
||||
<td class="content-cell" align="center">
|
||||
<p class="f-fallback sub align-center">
|
||||
宁波江东远通计算机有限公司
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in new issue
Block a user