This commit is contained in:
tanglong 2026-06-02 18:57:04 +08:00
parent f6dbf915f3
commit b3880fe3d2
4 changed files with 163 additions and 0 deletions

View File

@ -16,6 +16,8 @@
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-webflux</artifactId></dependency> <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-webflux</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-validation</artifactId></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency> <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId></dependency>
<dependency><groupId>com.microsoft.sqlserver</groupId><artifactId>mssql-jdbc</artifactId><scope>runtime</scope></dependency>
<dependency><groupId>com.fasterxml.jackson.dataformat</groupId><artifactId>jackson-dataformat-xml</artifactId></dependency> <dependency><groupId>com.fasterxml.jackson.dataformat</groupId><artifactId>jackson-dataformat-xml</artifactId></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency> <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency>
</dependencies> </dependencies>

View File

@ -0,0 +1,69 @@
package com.company.agentfoundation.crm;
import com.company.agentfoundation.security.AgentAuthService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/v1/crm/db")
public class CrmDatabaseController {
private final AgentAuthService authService;
private final CrmDatabaseService crmDatabaseService;
public CrmDatabaseController(AgentAuthService authService, CrmDatabaseService crmDatabaseService) {
this.authService = authService;
this.crmDatabaseService = crmDatabaseService;
}
@GetMapping("/ping")
public ResponseEntity<Map<String, Object>> ping(
@RequestHeader("X-Agent-Id") String agentId,
@RequestHeader("X-Internal-Key") String internalKey
) {
authService.authenticate(agentId, internalKey);
return ResponseEntity.ok(crmDatabaseService.ping());
}
@GetMapping("/tables")
public ResponseEntity<List<Map<String, Object>>> tables(
@RequestHeader("X-Agent-Id") String agentId,
@RequestHeader("X-Internal-Key") String internalKey,
@RequestParam(value = "schema", defaultValue = "dbo") String schema,
@RequestParam(value = "keyword", defaultValue = "") String keyword,
@RequestParam(value = "limit", defaultValue = "100") int limit
) {
authService.authenticate(agentId, internalKey);
return ResponseEntity.ok(crmDatabaseService.listTables(schema, keyword, limit));
}
@GetMapping("/tables/{table}/columns")
public ResponseEntity<List<Map<String, Object>>> columns(
@RequestHeader("X-Agent-Id") String agentId,
@RequestHeader("X-Internal-Key") String internalKey,
@PathVariable String table,
@RequestParam(value = "schema", defaultValue = "dbo") String schema
) {
authService.authenticate(agentId, internalKey);
return ResponseEntity.ok(crmDatabaseService.listColumns(schema, table));
}
@GetMapping("/tables/{table}/sample")
public ResponseEntity<List<Map<String, Object>>> sample(
@RequestHeader("X-Agent-Id") String agentId,
@RequestHeader("X-Internal-Key") String internalKey,
@PathVariable String table,
@RequestParam(value = "schema", defaultValue = "dbo") String schema,
@RequestParam(value = "limit", defaultValue = "5") int limit
) {
authService.authenticate(agentId, internalKey);
return ResponseEntity.ok(crmDatabaseService.sampleRows(schema, table, limit));
}
}

View File

@ -0,0 +1,83 @@
package com.company.agentfoundation.crm;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
@Service
public class CrmDatabaseService {
private static final Logger log = LoggerFactory.getLogger(CrmDatabaseService.class);
private static final String IDENTIFIER_PATTERN = "^[A-Za-z0-9_]+$";
private final JdbcTemplate jdbcTemplate;
public CrmDatabaseService(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public Map<String, Object> ping() {
long start = System.currentTimeMillis();
Integer result = jdbcTemplate.queryForObject("SELECT 1", Integer.class);
long costMs = System.currentTimeMillis() - start;
log.info("crm db ping success costMs={}", costMs);
return Map.of("ok", result != null && result == 1, "costMs", costMs);
}
public List<Map<String, Object>> listTables(String schema, String keyword, int limit) {
int safeLimit = clamp(limit, 1, 200);
String safeSchema = blankToDefault(schema, "dbo");
String safeKeyword = keyword == null ? "" : keyword.trim();
log.info("crm db listTables schema={} keyword={} limit={}", safeSchema, safeKeyword, safeLimit);
return jdbcTemplate.queryForList("""
SELECT TOP %d TABLE_SCHEMA, TABLE_NAME, TABLE_TYPE
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_SCHEMA = ?
AND (? = '' OR TABLE_NAME LIKE ?)
ORDER BY TABLE_NAME
""".formatted(safeLimit), safeSchema, safeKeyword, "%" + safeKeyword + "%");
}
public List<Map<String, Object>> listColumns(String schema, String table) {
String safeSchema = blankToDefault(schema, "dbo");
validateIdentifier("schema", safeSchema);
validateIdentifier("table", table);
log.info("crm db listColumns schema={} table={}", safeSchema, table);
return jdbcTemplate.queryForList("""
SELECT COLUMN_NAME, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, IS_NULLABLE, ORDINAL_POSITION
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?
ORDER BY ORDINAL_POSITION
""", safeSchema, table);
}
public List<Map<String, Object>> sampleRows(String schema, String table, int limit) {
String safeSchema = blankToDefault(schema, "dbo");
validateIdentifier("schema", safeSchema);
validateIdentifier("table", table);
int safeLimit = clamp(limit, 1, 20);
log.info("crm db sampleRows schema={} table={} limit={}", safeSchema, table, safeLimit);
return jdbcTemplate.queryForList("SELECT TOP " + safeLimit + " * FROM " + quoted(safeSchema) + "." + quoted(table));
}
private String blankToDefault(String value, String defaultValue) {
return value == null || value.isBlank() ? defaultValue : value.trim();
}
private int clamp(int value, int min, int max) {
return Math.max(min, Math.min(max, value));
}
private void validateIdentifier(String name, String value) {
if (value == null || !value.matches(IDENTIFIER_PATTERN)) {
throw new IllegalArgumentException("Invalid " + name + " identifier");
}
}
private String quoted(String identifier) {
return "[" + identifier.replace("]", "]]") + "]";
}
}

View File

@ -3,6 +3,15 @@ server:
spring: spring:
application: application:
name: agent-foundation name: agent-foundation
datasource:
url: ${CRM_DB_URL:jdbc:sqlserver://rm-uf64b8oxdt19pfyxfao.sqlserver.rds.aliyuncs.com:1433;databaseName=SmartSportsServerDB_QA;encrypt=true;trustServerCertificate=true;loginTimeout=30}
username: ${CRM_DB_USERNAME:yd}
password: ${CRM_DB_PASSWORD:}
driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver
hikari:
maximum-pool-size: 5
minimum-idle: 1
connection-timeout: 30000
management: management:
endpoints: endpoints:
web: web: