20260717170047

This commit is contained in:
oneao committed 2026-07-17 17:00:48 +08:00
1 parent 49dafe524c
commit 35aa108a34
40 files changed
+6218 -413

No files matched your search

@@ -1,5 +1,7 @@
package cn.g3soft.fmsapi;
import cn.g3soft.fmsapi.utils.snowflake.contract.IdGeneratorOptions;
import cn.g3soft.fmsapi.utils.snowflake.idgen.IdGenerator;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
@@ -9,6 +11,10 @@ import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
public class FmsApiApplication {
public static void main(String[] args) {
IdGeneratorOptions options = new IdGeneratorOptions();
options.WorkerId = 1;
IdGenerator.setIdGenerator(options);
SpringApplication.run(FmsApiApplication.class, args);
}
@@ -1,6 +1,7 @@
package cn.g3soft.fmsapi.config;
import cn.g3soft.fmsapi.database.OrgContext;
import cn.g3soft.fmsapi.service.ActiveSessionRegistry;
import cn.g3soft.fmsapi.utils.ApiResponse;
import cn.g3soft.fmsapi.utils.JwtUtils;
import io.jsonwebtoken.JwtException;
@@ -24,6 +25,9 @@ public class JwtAuthFilter extends OncePerRequestFilter {
@Resource
private JwtUtils jwtUtils;
@Resource
private ActiveSessionRegistry activeSessionRegistry;
@Resource
private ObjectMapper objectMapper;
@@ -39,16 +43,42 @@ public class JwtAuthFilter extends OncePerRequestFilter {
HttpServletResponse response,
FilterChain filterChain
) throws ServletException, IOException {
String orgId;
JwtUtils.TokenClaims claims;
try {
String token = getToken(request);
orgId = jwtUtils.getOrgId(token);
OrgContext.setOrgId(orgId);
claims = jwtUtils.parseToken(token);
} catch (JwtException | IllegalArgumentException exception) {
writeUnauthorized(response);
writeUnauthorized(
response,
ApiResponse.AUTH_ERROR_CODE,
"未登录或登录已失效"
);
return;
}
if (!activeSessionRegistry.hasActiveSession(claims.orgId(), claims.userId())) {
writeUnauthorized(
response,
ApiResponse.AUTH_ERROR_CODE,
"未登录或登录已失效"
);
return;
}
if (!activeSessionRegistry.isActive(
claims.orgId(),
claims.userId(),
claims.sessionId()
)) {
writeUnauthorized(
response,
ApiResponse.SESSION_REPLACED_CODE,
"账号已在其他设备登录,请重新登录"
);
return;
}
OrgContext.setOrgId(claims.orgId());
try {
filterChain.doFilter(request, response);
} finally {
@@ -64,11 +94,12 @@ public class JwtAuthFilter extends OncePerRequestFilter {
return authorization.substring(BEARER_PREFIX.length()).trim();
}
private void writeUnauthorized(HttpServletResponse response) throws IOException {
ApiResponse<Void> body = ApiResponse.fail(
ApiResponse.AUTH_ERROR_CODE,
"未登录或登录已失效"
);
private void writeUnauthorized(
HttpServletResponse response,
int code,
String message
) throws IOException {
ApiResponse<Void> body = ApiResponse.fail(code, message);
response.setStatus(200);
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
@@ -1,8 +1,11 @@
package cn.g3soft.fmsapi.controller;
import cn.g3soft.fmsapi.service.DataSaveService;
import cn.g3soft.fmsapi.service.DataService;
import cn.g3soft.fmsapi.utils.ApiResponse;
import cn.g3soft.fmsapi.utils.snowflake.idgen.IdGenerator;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -19,6 +22,9 @@ public class DataController {
@Resource
private DataService dataService;
@Resource
private DataSaveService dataSaveService;
@PostMapping("/loaddata")
public ApiResponse<List<Map<String, Object>>> loadData(
@RequestBody(required = false) Map<String, Object> params
@@ -32,4 +38,17 @@ public class DataController {
) throws SQLException {
return ApiResponse.success(dataService.loadDataBySql(params));
}
@PostMapping("/saveobjt")
public ApiResponse<Void> saveObject(
@RequestBody(required = false) List<Map<String, Object>> requests
) throws SQLException {
dataSaveService.save(requests);
return ApiResponse.success("保存成功", null);
}
@GetMapping("/nextid")
public ApiResponse<Long> nextId() {
return ApiResponse.success(IdGenerator.nextId());
}
}
@@ -9,6 +9,8 @@ import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.sql.SQLException;
import java.util.LinkedHashMap;
import java.util.Map;
@RestControllerAdvice
public class GlobalExceptionHandler {
@@ -25,6 +27,24 @@ public class GlobalExceptionHandler {
return ApiResponse.fail(exception.getMessage());
}
@ExceptionHandler(SaveObjectException.class)
public ApiResponse<Map<String, Object>> handleSaveObjectException(
SaveObjectException exception
) {
LOGGER.error(
"保存失败,table={},action={},index={}",
exception.getTable(),
exception.getAction(),
exception.getIndex(),
exception
);
Map<String, Object> details = new LinkedHashMap<>();
details.put("table", exception.getTable());
details.put("action", exception.getAction());
details.put("index", exception.getIndex());
return new ApiResponse<>(ApiResponse.SAVE_ERROR_CODE, "保存失败", details);
}
@ExceptionHandler({SQLException.class, DataAccessException.class})
public ApiResponse<Void> handleDatabaseException(
Exception exception,
@@ -0,0 +1,32 @@
package cn.g3soft.fmsapi.exception;
public class SaveObjectException extends RuntimeException {
private final String table;
private final String action;
private final int index;
public SaveObjectException(
String table,
String action,
int index,
Throwable cause
) {
super("保存失败", cause);
this.table = table;
this.action = action;
this.index = index;
}
public String getTable() {
return table;
}
public String getAction() {
return action;
}
public int getIndex() {
return index;
}
}
@@ -0,0 +1,30 @@
package cn.g3soft.fmsapi.service;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Component
public class ActiveSessionRegistry {
private final Map<AccountKey, String> activeSessions = new ConcurrentHashMap<>();
public void activate(String orgId, String userId, String sessionId) {
activeSessions.put(new AccountKey(orgId, userId), sessionId);
}
public boolean isActive(String orgId, String userId, String sessionId) {
if (sessionId == null || sessionId.isBlank()) {
return false;
}
return sessionId.equals(activeSessions.get(new AccountKey(orgId, userId)));
}
public boolean hasActiveSession(String orgId, String userId) {
return activeSessions.containsKey(new AccountKey(orgId, userId));
}
private record AccountKey(String orgId, String userId) {
}
}
@@ -13,6 +13,7 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
@Service
public class AuthService {
@@ -23,6 +24,9 @@ public class AuthService {
@Resource
private JwtUtils jwtUtils;
@Resource
private ActiveSessionRegistry activeSessionRegistry;
public Optional<Map<String, Object>> login(
String orgId,
String userId,
@@ -33,7 +37,8 @@ public class AuthService {
List<Map<String, Object>> users = dbUtils.loadData(
normalizedOrgId,
"b_user",
"b_id = " + dbUtils.toSqlStringLiteral(normalizedUserId),
"b_id = " + dbUtils.toSqlStringLiteral(normalizedUserId)
+ " AND b_canuse = '1'",
null,
List.of("b_id", "b_name", "b_password")
);
@@ -49,7 +54,9 @@ public class AuthService {
}
String accountId = getString(account, "b_id");
String token = jwtUtils.generateToken(accountId, normalizedOrgId);
String sessionId = UUID.randomUUID().toString();
String token = jwtUtils.generateToken(accountId, normalizedOrgId, sessionId);
activeSessionRegistry.activate(normalizedOrgId, accountId, sessionId);
Map<String, Object> user = new LinkedHashMap<>();
user.put("id", accountId);
@@ -0,0 +1,165 @@
package cn.g3soft.fmsapi.service;
import cn.g3soft.fmsapi.exception.BusinessException;
import cn.g3soft.fmsapi.exception.SaveObjectException;
import cn.g3soft.fmsapi.utils.DbUtils;
import cn.g3soft.fmsapi.utils.ParamUtils;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@Service
public class DataSaveService {
@Resource
private DataSource dataSource;
@Resource
private DbUtils dbUtils;
public void save(List<Map<String, Object>> requests) throws SQLException {
if (requests == null || requests.isEmpty()) {
throw new BusinessException("保存数据不能为空");
}
try (Connection connection = dataSource.getConnection()) {
boolean originalAutoCommit = connection.getAutoCommit();
connection.setAutoCommit(false);
try {
Map<String, DbUtils.TableMetadata> metadataCache = new LinkedHashMap<>();
for (Map<String, Object> request : requests) {
saveTable(connection, request, metadataCache);
}
connection.commit();
} catch (SQLException | RuntimeException exception) {
rollback(connection, exception);
throw exception;
} finally {
if (originalAutoCommit) {
connection.setAutoCommit(true);
}
}
}
}
private void saveTable(
Connection connection,
Map<String, Object> request,
Map<String, DbUtils.TableMetadata> metadataCache
) {
if (request == null) {
throw new BusinessException("表保存配置不能为空");
}
String tableName = ParamUtils.getRequiredString(request, "table");
String keyField = ParamUtils.getRequiredString(request, "key_field");
DbUtils.TableMetadata table = loadMetadata(
connection,
tableName,
metadataCache
);
DbUtils.ColumnMetadata keyColumn = dbUtils.getColumn(table, keyField);
if (keyColumn == null) {
throw new BusinessException("主键字段不存在: " + tableName + "." + keyField);
}
executeRows(
connection,
table,
keyColumn,
"delete",
rows(request, "deletes")
);
executeRows(
connection,
table,
keyColumn,
"update",
rows(request, "updates")
);
executeRows(
connection,
table,
keyColumn,
"insert",
rows(request, "inserts")
);
}
private DbUtils.TableMetadata loadMetadata(
Connection connection,
String tableName,
Map<String, DbUtils.TableMetadata> metadataCache
) {
String cacheKey = tableName.toLowerCase(Locale.ROOT);
DbUtils.TableMetadata cached = metadataCache.get(cacheKey);
if (cached != null) {
return cached;
}
try {
DbUtils.TableMetadata loaded = dbUtils.loadTableMetadata(connection, tableName);
metadataCache.put(cacheKey, loaded);
return loaded;
} catch (SQLException exception) {
throw new SaveObjectException(tableName, "metadata", -1, exception);
}
}
private void executeRows(
Connection connection,
DbUtils.TableMetadata table,
DbUtils.ColumnMetadata keyColumn,
String action,
List<Map<String, Object>> rows
) {
for (int index = 0; index < rows.size(); index++) {
try {
switch (action) {
case "delete" -> dbUtils.delete(connection, table, keyColumn, rows.get(index));
case "update" -> dbUtils.update(connection, table, keyColumn, rows.get(index));
case "insert" -> dbUtils.insert(connection, table, rows.get(index));
default -> throw new IllegalArgumentException("不支持的保存操作: " + action);
}
} catch (SQLException exception) {
throw new SaveObjectException(table.requestedName(), action, index, exception);
}
}
}
private List<Map<String, Object>> rows(
Map<String, Object> request,
String fieldName
) {
List<?> values = ParamUtils.getList(request, fieldName);
if (values == null) {
return List.of();
}
for (Object value : values) {
if (!(value instanceof Map<?, ?>)) {
throw new BusinessException(fieldName + " 参数类型不正确");
}
}
@SuppressWarnings("unchecked")
List<Map<String, Object>> rows = (List<Map<String, Object>>) (List<?>) values;
return rows;
}
private void rollback(Connection connection, Throwable originalException) {
try {
connection.rollback();
} catch (SQLException rollbackException) {
originalException.addSuppressed(rollbackException);
}
}
}
@@ -8,6 +8,8 @@ public record ApiResponse<T>(
public static final int SUCCESS_CODE = 0;
public static final int AUTH_ERROR_CODE = 401;
public static final int SESSION_REPLACED_CODE = 40101;
public static final int SAVE_ERROR_CODE = 2001;
public static final int SYSTEM_ERROR_CODE = 500;
public static final int BUSINESS_ERROR_CODE = 1000;
@@ -15,6 +17,10 @@ public record ApiResponse<T>(
return new ApiResponse<>(SUCCESS_CODE, "操作成功", data);
}
public static <T> ApiResponse<T> success(String message, T data) {
return new ApiResponse<>(SUCCESS_CODE, message, data);
}
public static <T> ApiResponse<T> fail(String message) {
return fail(BUSINESS_ERROR_CODE, message);
}
@@ -8,14 +8,17 @@ import org.springframework.stereotype.Component;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@@ -115,6 +118,114 @@ public class DbUtils {
return executeQuery(connection, sql);
}
public TableMetadata loadTableMetadata(
Connection connection,
String tableName
) throws SQLException {
if (connection == null) {
throw new IllegalArgumentException("数据库连接不能为空");
}
String requestedName = requireText(tableName, "table");
String quotedName = quoteQualifiedIdentifier(requestedName);
String sql = "SELECT * FROM " + quotedName + " WHERE 1 = 0";
LinkedHashMap<String, ColumnMetadata> columns = new LinkedHashMap<>();
try (Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery(sql)) {
ResultSetMetaData metadata = resultSet.getMetaData();
for (int index = 1; index <= metadata.getColumnCount(); index++) {
String columnName = metadata.getColumnName(index);
validateIdentifier(columnName);
String normalizedName = normalizeName(columnName);
String typeName = metadata.getColumnTypeName(index);
boolean generated = metadata.isAutoIncrement(index)
|| metadata.isReadOnly(index)
|| "timestamp".equalsIgnoreCase(typeName)
|| "rowversion".equalsIgnoreCase(typeName);
columns.put(
normalizedName,
new ColumnMetadata(columnName, normalizedName, !generated)
);
}
}
if (columns.isEmpty()) {
throw new BusinessException("目标表没有可识别字段: " + requestedName);
}
return new TableMetadata(requestedName, quotedName, columns);
}
public ColumnMetadata getColumn(TableMetadata table, String fieldName) {
String requiredFieldName = requireText(fieldName, "字段名");
validateIdentifier(requiredFieldName);
return table.columnsByName().get(normalizeName(requiredFieldName));
}
public int insert(
Connection connection,
TableMetadata table,
Map<String, Object> row
) throws SQLException {
Map<String, Object> normalizedRow = normalizeRow(row);
LinkedHashMap<ColumnMetadata, Object> values = writableValues(
table,
normalizedRow,
null
);
if (values.isEmpty()) {
throw new BusinessException("没有可新增的字段: " + table.requestedName());
}
String columnsSql = joinColumns(values.keySet().stream().toList());
String placeholders = String.join(", ", Collections.nCopies(values.size(), "?"));
String sql = "INSERT INTO " + table.quotedName()
+ " (" + columnsSql + ") VALUES (" + placeholders + ")";
return executeWrite(connection, sql, new ArrayList<>(values.values()), "新增");
}
public int update(
Connection connection,
TableMetadata table,
ColumnMetadata keyColumn,
Map<String, Object> row
) throws SQLException {
Map<String, Object> normalizedRow = normalizeRow(row);
Object keyValue = requiredKeyValue(normalizedRow, keyColumn, table.requestedName());
LinkedHashMap<ColumnMetadata, Object> values = writableValues(
table,
normalizedRow,
keyColumn.normalizedName()
);
if (values.isEmpty()) {
throw new BusinessException("没有可修改的字段: " + table.requestedName());
}
String setSql = values.keySet().stream()
.map(column -> quoteColumn(column) + " = ?")
.collect(Collectors.joining(", "));
String sql = "UPDATE " + table.quotedName()
+ " SET " + setSql
+ " WHERE " + quoteColumn(keyColumn) + " = ?";
List<Object> parameters = new ArrayList<>(values.values());
parameters.add(keyValue);
return executeWrite(connection, sql, parameters, "修改");
}
public int delete(
Connection connection,
TableMetadata table,
ColumnMetadata keyColumn,
Map<String, Object> row
) throws SQLException {
Map<String, Object> normalizedRow = normalizeRow(row);
Object keyValue = requiredKeyValue(normalizedRow, keyColumn, table.requestedName());
String sql = "DELETE FROM " + table.quotedName()
+ " WHERE " + quoteColumn(keyColumn) + " = ?";
return executeWrite(connection, sql, List.of(keyValue), "删除");
}
private List<Map<String, Object>> executeQuery(
Connection connection,
String sql
@@ -126,6 +237,24 @@ public class DbUtils {
}
}
private int executeWrite(
Connection connection,
String sql,
List<Object> parameters,
String action
) throws SQLException {
try (PreparedStatement statement = connection.prepareStatement(sql)) {
for (int index = 0; index < parameters.size(); index++) {
statement.setObject(index + 1, parameters.get(index));
}
int affectedRows = statement.executeUpdate();
if (affectedRows != 1) {
throw new SQLException(action + "记录数异常: " + affectedRows);
}
return affectedRows;
}
}
public String toSqlStringLiteral(String value) {
if (value == null) {
return "NULL";
@@ -212,7 +341,7 @@ public class DbUtils {
return quoteQualifiedIdentifier(parts[0]) + direction;
}
private String quoteQualifiedIdentifier(String identifier) {
public String quoteQualifiedIdentifier(String identifier) {
if (!hasText(identifier)) {
throw new BusinessException("数据库标识符不能为空");
}
@@ -223,6 +352,75 @@ public class DbUtils {
.collect(Collectors.joining("."));
}
private LinkedHashMap<ColumnMetadata, Object> writableValues(
TableMetadata table,
Map<String, Object> normalizedRow,
String excludedColumn
) {
LinkedHashMap<ColumnMetadata, Object> values = new LinkedHashMap<>();
for (ColumnMetadata column : table.columnsByName().values()) {
if (!column.writable()
|| column.normalizedName().equals(excludedColumn)
|| !normalizedRow.containsKey(column.normalizedName())) {
continue;
}
values.put(column, normalizedRow.get(column.normalizedName()));
}
return values;
}
private Object requiredKeyValue(
Map<String, Object> normalizedRow,
ColumnMetadata keyColumn,
String tableName
) {
Object value = normalizedRow.get(keyColumn.normalizedName());
if (value == null || value instanceof String text && text.isBlank()) {
throw new BusinessException(
"缺少主键字段: " + tableName + "." + keyColumn.name()
);
}
return value;
}
private Map<String, Object> normalizeRow(Map<String, Object> row) {
if (row == null) {
throw new BusinessException("保存记录不能为空");
}
Map<String, Object> normalized = new LinkedHashMap<>();
for (Map.Entry<String, Object> entry : row.entrySet()) {
String fieldName = requireText(entry.getKey(), "字段名");
String normalizedName = normalizeName(fieldName);
if (normalized.containsKey(normalizedName)) {
throw new BusinessException("保存记录包含重复字段: " + fieldName);
}
normalized.put(normalizedName, entry.getValue());
}
return normalized;
}
private String joinColumns(List<ColumnMetadata> columns) {
return columns.stream()
.map(this::quoteColumn)
.collect(Collectors.joining(", "));
}
private String quoteColumn(ColumnMetadata column) {
return quoteQualifiedIdentifier(column.name());
}
private String requireText(String value, String fieldName) {
if (!hasText(value)) {
throw new BusinessException(fieldName + " 不能为空");
}
return value.trim();
}
private String normalizeName(String value) {
return value.trim().toLowerCase(Locale.ROOT);
}
private List<Map<String, Object>> readRows(ResultSet resultSet) throws SQLException {
ResultSetMetaData metadata = resultSet.getMetaData();
int columnCount = metadata.getColumnCount();
@@ -242,4 +440,18 @@ public class DbUtils {
private boolean hasText(String value) {
return value != null && !value.isBlank();
}
public record ColumnMetadata(
String name,
String normalizedName,
boolean writable
) {
}
public record TableMetadata(
String requestedName,
String quotedName,
LinkedHashMap<String, ColumnMetadata> columnsByName
) {
}
}
@@ -1,6 +1,8 @@
package cn.g3soft.fmsapi.utils;
import cn.g3soft.fmsapi.config.AuthProperties;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import jakarta.annotation.PostConstruct;
@@ -21,6 +23,7 @@ public class JwtUtils {
private SecretKey signingKey;
private Duration expiration;
private JwtParser jwtParser;
@PostConstruct
public void init() {
@@ -36,14 +39,16 @@ public class JwtUtils {
this.signingKey = Keys.hmacShaKeyFor(jwtSecret.getBytes(StandardCharsets.UTF_8));
this.expiration = properties.getJwtExpiration();
this.jwtParser = Jwts.parser().verifyWith(signingKey).build();
}
public String generateToken(String userId, String orgId) {
public String generateToken(String userId, String orgId, String sessionId) {
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt.plus(expiration);
return Jwts.builder()
.subject(userId)
.id(sessionId)
.claim("orgid", orgId)
.issuedAt(Date.from(issuedAt))
.expiration(Date.from(expiresAt))
@@ -51,12 +56,26 @@ public class JwtUtils {
.compact();
}
public TokenClaims parseToken(String token) {
Claims claims = jwtParser.parseSignedClaims(token).getPayload();
return new TokenClaims(
required(claims.getSubject(), "用户ID"),
required(claims.get("orgid", String.class), "机构码"),
required(claims.getId(), "会话ID")
);
}
public String getOrgId(String token) {
return Jwts.parser()
.verifyWith(signingKey)
.build()
.parseSignedClaims(token)
.getPayload()
.get("orgid", String.class);
return parseToken(token).orgId();
}
private String required(String value, String claimName) {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException("登录凭证缺少" + claimName);
}
return value;
}
public record TokenClaims(String userId, String orgId, String sessionId) {
}
}
@@ -0,0 +1,5 @@
package cn.g3soft.fmsapi.utils.snowflake.contract;
public interface IIdGenerator {
long newLong() throws IdGeneratorException;
}
@@ -0,0 +1,5 @@
package cn.g3soft.fmsapi.utils.snowflake.contract;
public interface ISnowWorker {
long nextId() throws IdGeneratorException;
}
@@ -0,0 +1,25 @@
package cn.g3soft.fmsapi.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,60 @@
package cn.g3soft.fmsapi.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.g3soft.fmsapi.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.g3soft.fmsapi.utils.snowflake.core;
import cn.g3soft.fmsapi.utils.snowflake.contract.ISnowWorker;
import cn.g3soft.fmsapi.utils.snowflake.contract.IdGeneratorException;
import cn.g3soft.fmsapi.utils.snowflake.contract.IdGeneratorOptions;
import cn.g3soft.fmsapi.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,37 @@
package cn.g3soft.fmsapi.utils.snowflake.core;
import cn.g3soft.fmsapi.utils.snowflake.contract.IdGeneratorException;
import cn.g3soft.fmsapi.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,90 @@
package cn.g3soft.fmsapi.utils.snowflake.idgen;
import cn.g3soft.fmsapi.utils.snowflake.contract.IIdGenerator;
import cn.g3soft.fmsapi.utils.snowflake.contract.ISnowWorker;
import cn.g3soft.fmsapi.utils.snowflake.contract.IdGeneratorException;
import cn.g3soft.fmsapi.utils.snowflake.contract.IdGeneratorOptions;
import cn.g3soft.fmsapi.utils.snowflake.core.SnowWorkerM1;
import cn.g3soft.fmsapi.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.g3soft.fmsapi.utils.snowflake.idgen;
import cn.g3soft.fmsapi.utils.snowflake.contract.IIdGenerator;
import cn.g3soft.fmsapi.utils.snowflake.contract.IdGeneratorException;
import cn.g3soft.fmsapi.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();
}
}
@@ -1,4 +1,5 @@
server:
port: 8088
servlet:
context-path: /api