This commit is contained in:
oneao committed 2025-12-15 17:31:19 +08:00
1 parent 52112bd277
commit 1da52dc4f4
66 files changed
+3294 -721

No files matched your search

+39
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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>
+6
View File
@@ -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>
+130
View File
@@ -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);
}
}
@@ -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;
}
@@ -0,0 +1,6 @@
package cn.link56.auth.code.model;
public enum AuthCodeChannel {
EMAIL,
SMS
}
@@ -0,0 +1,10 @@
package cn.link56.auth.code.model;
/**
* 验证码用途 / 业务场景
*/
public enum AuthCodePurpose {
REGISTER, // 注册
LOGIN, // 登录
RESET_PASSWORD // 找回密码
}
@@ -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;
}
@@ -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;
}
@@ -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);
}
}
@@ -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;
}
}
@@ -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;
}
@@ -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;
}
@@ -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
}
@@ -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;
}
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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"
);
}
}
@@ -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();
}
}
@@ -0,0 +1,7 @@
package cn.link56.common.exception;
public class BusinessException extends RuntimeException {
public BusinessException(String message) {
super(message);
}
}
@@ -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());
}
}
@@ -0,0 +1,11 @@
package cn.link56.common.exception;
public class OperationTooFrequentException extends BusinessException {
public OperationTooFrequentException(String message) {
super(message);
}
public OperationTooFrequentException() {
super("操作过于频繁,请稍后再试");
}
}
@@ -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
}
}
}
@@ -0,0 +1,5 @@
package cn.link56.common.utils.snowflake.contract;
public interface IIdGenerator {
long newLong() throws IdGeneratorException;
}
@@ -0,0 +1,5 @@
package cn.link56.common.utils.snowflake.contract;
public interface ISnowWorker {
long nextId() throws IdGeneratorException;
}
@@ -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));
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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();
}
}
}
@@ -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;
}
}
}
@@ -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();
}
}
@@ -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>