20260719215738

This commit is contained in:
oneao committed 2026-07-19 21:57:40 +08:00
1 parent 35aa108a34
commit cda7f16d3a
116 files changed
+11826 -3669

No files matched your search

-5
View File
@@ -43,11 +43,6 @@
<artifactId>druid</artifactId>
<version>1.2.18</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
@@ -0,0 +1,18 @@
package cn.g3soft.fmsapi.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import tools.jackson.databind.module.SimpleModule;
import tools.jackson.databind.ser.std.ToStringSerializer;
@Configuration
public class JacksonConfig {
@Bean
public SimpleModule longAsStringModule() {
SimpleModule module = new SimpleModule("long-as-string");
module.addSerializer(Long.class, ToStringSerializer.instance);
module.addSerializer(Long.TYPE, ToStringSerializer.instance);
return module;
}
}
@@ -39,6 +39,13 @@ public class DataController {
return ApiResponse.success(dataService.loadDataBySql(params));
}
@PostMapping("/page")
public ApiResponse<Map<String, Object>> page(
@RequestBody(required = false) Map<String, Object> params
) throws SQLException {
return ApiResponse.success(dataService.page(params));
}
@PostMapping("/saveobjt")
public ApiResponse<Void> saveObject(
@RequestBody(required = false) List<Map<String, Object>> requests
@@ -15,7 +15,7 @@ public class OrgDataSourceFactory {
public DruidDataSource create(String orgId, OrgDatabaseConfig config) {
OrgDatabaseProperties.Pool poolProperties = properties.getPool();
DruidDataSource dataSource = new DruidDataSource();
dataSource.setName("fms-" + orgId);
dataSource.setName(orgId);
dataSource.setUrl(config.url());
dataSource.setUsername(config.username());
dataSource.setPassword(config.password());
@@ -34,13 +34,9 @@ public class AuthService {
) throws SQLException {
String normalizedOrgId = orgId.trim().toUpperCase(Locale.ROOT);
String normalizedUserId = userId.trim();
List<Map<String, Object>> users = dbUtils.loadData(
List<Map<String, Object>> users = dbUtils.loadUserForAuthentication(
normalizedOrgId,
"b_user",
"b_id = " + dbUtils.toSqlStringLiteral(normalizedUserId)
+ " AND b_canuse = '1'",
null,
List.of("b_id", "b_name", "b_password")
normalizedUserId
);
if (users.isEmpty()) {
@@ -53,7 +49,7 @@ public class AuthService {
return Optional.empty();
}
String accountId = getString(account, "b_id");
String accountId = getString(account, "b_account");
String sessionId = UUID.randomUUID().toString();
String token = jwtUtils.generateToken(accountId, normalizedOrgId, sessionId);
activeSessionRegistry.activate(normalizedOrgId, accountId, sessionId);
@@ -70,10 +66,14 @@ public class AuthService {
}
private String getString(Map<String, Object> row, String column) {
Object value = getValue(row, column);
return value == null ? null : value.toString();
}
private Object getValue(Map<String, Object> row, String column) {
for (Map.Entry<String, Object> entry : row.entrySet()) {
if (entry.getKey().equalsIgnoreCase(column)) {
Object value = entry.getValue();
return value == null ? null : value.toString();
return entry.getValue();
}
}
return null;
@@ -1,11 +1,13 @@
package cn.g3soft.fmsapi.service;
import cn.g3soft.fmsapi.exception.BusinessException;
import cn.g3soft.fmsapi.utils.DbUtils;
import cn.g3soft.fmsapi.utils.ParamUtils;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import java.sql.SQLException;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -19,7 +21,7 @@ public class DataService {
return dbUtils.loadData(
ParamUtils.getRequiredString(params, "view_name"),
ParamUtils.getString(params, "search_condition"),
ParamUtils.getString(params, "order_field"),
ParamUtils.getString(params, "order_by"),
ParamUtils.getList(params, "search_columns")
);
}
@@ -29,4 +31,46 @@ public class DataService {
ParamUtils.getRequiredString(params, "sql")
);
}
public Map<String, Object> page(Map<String, Object> params) throws SQLException {
int pageNo = positiveInteger(params, "page_no", 1);
int pageSize = positiveInteger(params, "page_size", 20);
DbUtils.PageResult page = dbUtils.page(
ParamUtils.getRequiredString(params, "view_name"),
ParamUtils.getList(params, "search_columns"),
ParamUtils.getString(params, "search_condition"),
ParamUtils.getRequiredString(params, "order_by"),
pageNo,
pageSize
);
Map<String, Object> result = new LinkedHashMap<>();
result.put("rows", page.rows());
result.put("row_count", page.rows().size());
result.put("total", page.total());
result.put("page_no", page.pageNo());
result.put("page_size", page.pageSize());
return result;
}
private int positiveInteger(
Map<String, Object> params,
String field,
int defaultValue
) {
Number number = ParamUtils.get(params, field, Number.class);
if (number == null) {
return defaultValue;
}
long value = number.longValue();
if (value < 1
|| value > Integer.MAX_VALUE
|| Double.compare(number.doubleValue(), value) != 0) {
throw new BusinessException(
field + " 必须是大于 0 的整数"
);
}
return (int) value;
}
}
@@ -26,6 +26,7 @@ import java.util.stream.Collectors;
@Component
public class DbUtils {
private static final String PAGE_TOTAL_COLUMN = "__fms_page_total_count";
private static final Pattern SAFE_IDENTIFIER = Pattern.compile("[A-Za-z_][A-Za-z0-9_]*");
private static final Pattern READ_SQL_PATTERN = Pattern.compile("(?is)^\\s*(select|with)\\b");
private static final Pattern WRITE_SQL_PATTERN = Pattern.compile(
@@ -46,10 +47,10 @@ public class DbUtils {
public List<Map<String, Object>> loadData(
String viewName,
String searchCondition,
String orderField,
String orderBy,
List<String> searchColumns
) throws SQLException {
String sql = buildLoadDataSql(viewName, searchCondition, orderField, searchColumns);
String sql = buildLoadDataSql(viewName, searchCondition, orderBy, searchColumns);
Connection connection = DataSourceUtils.getConnection(dataSource);
try {
return executeQuery(connection, sql);
@@ -58,23 +59,10 @@ public class DbUtils {
}
}
public List<Map<String, Object>> loadData(
String orgId,
String viewName,
String searchCondition,
String orderField,
List<String> searchColumns
) throws SQLException {
String sql = buildLoadDataSql(viewName, searchCondition, orderField, searchColumns);
try (Connection connection = dataSourceManager.getConnection(orgId)) {
return executeQuery(connection, sql);
}
}
private String buildLoadDataSql(
String viewName,
String searchCondition,
String orderField,
String orderBy,
List<String> searchColumns
) {
String columnsSql = buildColumns(searchColumns);
@@ -86,8 +74,8 @@ public class DbUtils {
if (hasText(searchCondition)) {
sql.append(" WHERE ").append(searchCondition.trim());
}
if (hasText(orderField)) {
sql.append(" ORDER BY ").append(buildOrderBy(orderField));
if (hasText(orderBy)) {
sql.append(" ORDER BY ").append(buildOrderBy(orderBy));
}
String querySql = sql.toString();
@@ -118,6 +106,97 @@ public class DbUtils {
return executeQuery(connection, sql);
}
public PageResult page(
String viewName,
List<String> searchColumns,
String searchCondition,
String orderBy,
int pageNo,
int pageSize
) throws SQLException {
if (pageNo < 1) {
throw new BusinessException("page_no 必须大于 0");
}
if (pageSize < 1) {
throw new BusinessException("page_size 必须大于 0");
}
String querySql = buildLoadDataSql(
viewName,
searchCondition,
null,
searchColumns
);
String orderBySql = buildPageOrderBy(orderBy);
String pageSql = "SELECT [page_source].*, COUNT_BIG(1) OVER() AS ["
+ PAGE_TOTAL_COLUMN
+ "] FROM ("
+ querySql
+ ") AS [page_source] ORDER BY "
+ orderBySql
+ " OFFSET ? ROWS FETCH NEXT ? ROWS ONLY";
long offset = (long) (pageNo - 1) * pageSize;
Connection connection = DataSourceUtils.getConnection(dataSource);
try {
PageResult result = executePageQuery(
connection,
pageSql,
offset,
pageSize,
pageNo
);
if (!result.rows().isEmpty() || pageNo == 1) {
return result;
}
return new PageResult(
result.rows(),
countRows(connection, querySql),
pageNo,
pageSize
);
} finally {
DataSourceUtils.releaseConnection(connection, dataSource);
}
}
public List<Map<String, Object>> loadUserForAuthentication(
String orgId,
String account
) throws SQLException {
try (Connection connection = dataSourceManager.getConnection(orgId)) {
SQLException failure = null;
boolean privileged = false;
try {
setSystemUserVisibility(connection, 1);
privileged = true;
String sql = "SELECT [b_id], [b_account], [b_name], [b_password] "
+ "FROM [b_user] WHERE [b_account] = ? AND [b_canuse] = 1";
try (PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setString(1, account);
try (ResultSet resultSet = statement.executeQuery()) {
return readRows(resultSet);
}
}
} catch (SQLException exception) {
failure = exception;
throw exception;
} finally {
if (privileged) {
try {
setSystemUserVisibility(connection, null);
} catch (SQLException clearException) {
if (failure != null) {
failure.addSuppressed(clearException);
} else {
throw clearException;
}
}
}
}
}
}
public TableMetadata loadTableMetadata(
Connection connection,
String tableName
@@ -255,6 +334,59 @@ public class DbUtils {
}
}
private PageResult executePageQuery(
Connection connection,
String sql,
long offset,
int pageSize,
int pageNo
) throws SQLException {
try (PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setLong(1, offset);
statement.setInt(2, pageSize);
try (ResultSet resultSet = statement.executeQuery()) {
ResultSetMetaData metadata = resultSet.getMetaData();
int totalColumnIndex = -1;
for (int index = 1; index <= metadata.getColumnCount(); index++) {
if (PAGE_TOTAL_COLUMN.equalsIgnoreCase(metadata.getColumnLabel(index))) {
totalColumnIndex = index;
}
}
if (totalColumnIndex < 0) {
throw new SQLException("分页查询缺少总记录数字段");
}
List<Map<String, Object>> rows = new ArrayList<>();
long total = 0;
while (resultSet.next()) {
total = resultSet.getLong(totalColumnIndex);
Map<String, Object> row = new LinkedHashMap<>();
for (int index = 1; index <= metadata.getColumnCount(); index++) {
if (index != totalColumnIndex) {
row.put(
metadata.getColumnLabel(index),
resultSet.getObject(index)
);
}
}
rows.add(row);
}
return new PageResult(rows, total, pageNo, pageSize);
}
}
}
private long countRows(Connection connection, String sql) throws SQLException {
String countSql = "SELECT COUNT_BIG(1) FROM ("
+ sql
+ ") AS [count_source]";
try (Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery(countSql)) {
resultSet.next();
return resultSet.getLong(1);
}
}
public String toSqlStringLiteral(String value) {
if (value == null) {
return "NULL";
@@ -316,6 +448,22 @@ public class DbUtils {
return result.toString();
}
private void setSystemUserVisibility(
Connection connection,
Integer value
) throws SQLException {
String sql = "EXEC sys.sp_set_session_context @key = ?, @value = ?";
try (PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setString(1, "allow_g3soft");
if (value == null) {
statement.setNull(2, java.sql.Types.INTEGER);
} else {
statement.setInt(2, value);
}
statement.execute();
}
}
private String buildColumns(List<String> searchColumns) {
if (searchColumns == null || searchColumns.isEmpty()) {
return "*";
@@ -325,8 +473,8 @@ public class DbUtils {
.collect(Collectors.joining(", "));
}
private String buildOrderBy(String orderField) {
String[] parts = orderField.trim().split("\\s+");
private String buildOrderBy(String orderBy) {
String[] parts = orderBy.trim().split("\\s+");
if (parts.length > 2) {
throw new BusinessException("排序字段格式不正确");
}
@@ -341,6 +489,14 @@ public class DbUtils {
return quoteQualifiedIdentifier(parts[0]) + direction;
}
private String buildPageOrderBy(String orderBy) {
String requiredOrderBy = requireText(orderBy, "order_by");
return Arrays.stream(requiredOrderBy.split(","))
.map(String::trim)
.map(this::buildOrderBy)
.collect(Collectors.joining(", "));
}
public String quoteQualifiedIdentifier(String identifier) {
if (!hasText(identifier)) {
throw new BusinessException("数据库标识符不能为空");
@@ -454,4 +610,12 @@ public class DbUtils {
LinkedHashMap<String, ColumnMetadata> columnsByName
) {
}
public record PageResult(
List<Map<String, Object>> rows,
long total,
int pageNo,
int pageSize
) {
}
}
@@ -3,12 +3,14 @@ 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 IIdGenerator getIdGenInstance() {
return idGenInstance;
}
/**
* 设置参数,建议程序初始化时执行一次
*/
@@ -0,0 +1,67 @@
package cn.g3soft.fmsapi.service;
import cn.g3soft.fmsapi.utils.DbUtils;
import cn.g3soft.fmsapi.utils.JwtUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
class AuthServiceTests {
private DbUtils dbUtils;
private JwtUtils jwtUtils;
private ActiveSessionRegistry activeSessionRegistry;
private AuthService authService;
@BeforeEach
void setUp() {
dbUtils = mock(DbUtils.class);
jwtUtils = mock(JwtUtils.class);
activeSessionRegistry = new ActiveSessionRegistry();
authService = new AuthService();
ReflectionTestUtils.setField(authService, "dbUtils", dbUtils);
ReflectionTestUtils.setField(authService, "jwtUtils", jwtUtils);
ReflectionTestUtils.setField(
authService,
"activeSessionRegistry",
activeSessionRegistry
);
}
@Test
void hiddenSuperAdministratorCanLoginWithEmptyPassword() throws Exception {
when(dbUtils.loadUserForAuthentication(
"FMS",
"g3soft"
)).thenReturn(List.of(Map.of(
"b_id", 828084517703744L,
"b_account", "g3soft",
"b_name", "G3Soft",
"b_password", ""
)));
when(jwtUtils.generateToken(
eq("g3soft"),
eq("FMS"),
org.mockito.ArgumentMatchers.anyString()
))
.thenReturn("token");
Optional<Map<String, Object>> result = authService.login("fms", "g3soft", "");
assertThat(result).isPresent();
assertThat(result.orElseThrow()).containsEntry("token", "token");
Map<?, ?> user = (Map<?, ?>) result.orElseThrow().get("user");
assertThat(user.get("id")).isEqualTo("g3soft");
assertThat(user.get("name")).isEqualTo("G3Soft");
}
}