mirror of
https://github.com/hpd840321/craftlabs-authorization-sdk.git
synced 2026-06-09 10:00:30 +08:00
feat(sdk): AuthConfigs, JSON Schema, examples, and release checksum CI
Add craftlabs-auth-config.schema.json, Java AuthConfigs model with tests, example configs aligned to BP-10, C/Java/auth-config documentation, native header notes, RELEASING guide, and workflow to verify SDK artifact checksums on release tags. Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
# CraftLabs 授权 SDK — 发布与完整性
|
||||
|
||||
对齐架构文档 [SYSTEM_ARCHITECTURE §9.8](../docs/engineering/SYSTEM_ARCHITECTURE.md):官方渠道、**SHA-256 清单**、**GPG 签名(建议)**、`java` 与 `native` **同 Git tag**。
|
||||
|
||||
## 1. 发布前检查
|
||||
|
||||
- [ ] `mvn -f java/pom.xml verify` 通过(JDK 17+)。
|
||||
- [ ] `native` 已在各目标平台完成构建,且与本次 **同一 tag** 一并交付。
|
||||
- [ ] `CHANGELOG`(或发布说明)写明 **比特 SDK / 运行时** 兼容版本。
|
||||
|
||||
## 2. 生成 SHA256SUMS(必做)
|
||||
|
||||
在仓库根目录执行(会先 `mvn -DskipTests package`):
|
||||
|
||||
```bash
|
||||
chmod +x scripts/sdk-release-checksums.sh
|
||||
./scripts/sdk-release-checksums.sh --output dist/sdk-release
|
||||
```
|
||||
|
||||
已构建过时跳过 Maven:
|
||||
|
||||
```bash
|
||||
./scripts/sdk-release-checksums.sh --no-mvn --output dist/sdk-release
|
||||
```
|
||||
|
||||
把 **本机构建出的 Native** 一并写入清单(路径相对于仓库根,便于客户校验):
|
||||
|
||||
```bash
|
||||
./scripts/sdk-release-checksums.sh --output dist/sdk-release --native-path "$(pwd)/native/build"
|
||||
```
|
||||
|
||||
含完整测试的构建后再生成清单:
|
||||
|
||||
```bash
|
||||
./scripts/sdk-release-checksums.sh --verify --output dist/sdk-release
|
||||
```
|
||||
|
||||
输出:
|
||||
|
||||
- `dist/sdk-release/SHA256SUMS` — 每行:`哈希 相对路径`
|
||||
- `dist/sdk-release/RELEASE-MANIFEST.txt` — 提交 SHA、UTC 时间
|
||||
|
||||
**客户校验**(在克隆/解压后的仓库根或同目录结构下):
|
||||
|
||||
```bash
|
||||
sha256sum -c dist/sdk-release/SHA256SUMS
|
||||
```
|
||||
|
||||
macOS:
|
||||
|
||||
```bash
|
||||
shasum -a 256 -c dist/sdk-release/SHA256SUMS
|
||||
```
|
||||
|
||||
对 `SHA256SUMS` 本身做 **分离签名**(本机已配置 GPG):
|
||||
|
||||
```bash
|
||||
SIGN=1 ./scripts/sdk-release-checksums.sh --no-mvn --output dist/sdk-release
|
||||
```
|
||||
|
||||
生成 `SHA256SUMS.asc`;客户使用公布的公钥:`gpg --verify SHA256SUMS.asc SHA256SUMS`。
|
||||
|
||||
## 3. Maven JAR 的 GPG 签名(强烈建议)
|
||||
|
||||
父 POM 已配置 `maven-gpg-plugin`,**默认跳过**(`gpg.skip=true`),不影响日常 `verify`。
|
||||
|
||||
发布前在已导入私钥的机器上:
|
||||
|
||||
```bash
|
||||
gpg --version # 确认可用
|
||||
mvn -f java/pom.xml -Prelease-sign verify
|
||||
```
|
||||
|
||||
各 **可发布模块**(`craftlabs-auth-core`、`craftlabs-auth-bitanswer`、`craftlabs-auth-selfhosted`)的 `target/*.jar` 旁会出现 **`.asc`**。`craftlabs-auth-tests` 模块固定 **不签名**。
|
||||
|
||||
若无私钥或未就绪,可只发 **SHA256SUMS**;待密钥就绪后再打开 `-Prelease-sign`。
|
||||
|
||||
### CI / 无人值守(可选)
|
||||
|
||||
在构建机配置 `MAVEN_GPG_PASSPHRASE` 等环境变量,或使用 `gpg-agent`;勿将私钥提交仓库。GitHub Actions 可用 `GPG_PRIVATE_KEY` secret + [crazy-max/ghaction-import-gpg](https://github.com/crazy-max/ghaction-import-gpg) 导入后再执行 `mvn -Prelease-sign verify`。
|
||||
|
||||
## 4. GitHub Release 建议资产
|
||||
|
||||
每个 **tag** 上传:
|
||||
|
||||
1. 三个 **release JAR**(及对应 **.asc**,若已签名)
|
||||
2. 各平台 **Native** 压缩包
|
||||
3. **`SHA256SUMS`** 与 **`SHA256SUMS.asc`**
|
||||
4. 固定页面公布 **GPG 公钥指纹** 与下载说明
|
||||
|
||||
---
|
||||
|
||||
版权所有 © 广州创飞人工智能技术有限公司(以项目实际声明为准)。
|
||||
@@ -13,4 +13,21 @@
|
||||
<artifactId>craftlabs-auth-core</artifactId>
|
||||
<name>CraftLabs Auth — core API</name>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.networknt</groupId>
|
||||
<artifactId>json-schema-validator</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
||||
@@ -3,6 +3,9 @@ package cn.craftlabs.auth;
|
||||
/**
|
||||
* 授权能力的统一契约:初始化、激活、校验许可、查询特性与释放等生命周期方法。
|
||||
*
|
||||
* <p>{@link #initialize(String)} 的 JSON 建议使用 {@link cn.craftlabs.auth.config.AuthConfigs#parse(String)}
|
||||
* 先校验后再传入,格式见仓库 {@code schemas/craftlabs-auth-config.schema.json} 与 {@code examples/config/}。
|
||||
*
|
||||
* <p>实现类负责加载对应 native 或远端适配器;调用方应在不再使用时调用 {@link #close()} 释放底层资源。
|
||||
*
|
||||
* <p>版权所有 © 广州创飞人工智能技术有限公司
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package cn.craftlabs.auth.config;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* {@link cn.craftlabs.auth.AuthProvider#initialize(String)} 所用 JSON 的配置模型,与 {@code
|
||||
* schemas/craftlabs-auth-config.schema.json} 对齐。
|
||||
*
|
||||
* <p>版权所有 © 广州创飞人工智能技术有限公司
|
||||
*
|
||||
* @author huangping@craftlabs.cn
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public record AuthConfig(
|
||||
@JsonProperty("schemaVersion") int schemaVersion,
|
||||
@JsonProperty("provider") String provider,
|
||||
@JsonProperty("scenario") String scenario,
|
||||
@JsonProperty("bitanswer") BitanswerConfigSection bitanswer,
|
||||
@JsonProperty("selfhosted") SelfhostedConfigSection selfhosted,
|
||||
@JsonProperty("features") Map<String, FeatureMapping> features,
|
||||
@JsonProperty("wharf") WharfScenarioSection wharf,
|
||||
@JsonProperty("school") SchoolScenarioSection school,
|
||||
@JsonProperty("floating") FloatingScenarioSection floating) {
|
||||
|
||||
public AuthConfig {
|
||||
features = features == null ? Map.of() : Collections.unmodifiableMap(new LinkedHashMap<>(features));
|
||||
provider = Objects.requireNonNullElse(provider, "");
|
||||
scenario = Objects.requireNonNullElse(scenario, "");
|
||||
}
|
||||
|
||||
/** 逻辑特性键对应的比特特征 id;未配置或非数值映射时返回 {@code null}。 */
|
||||
public Integer bitanswerFeatureId(String logicalFeatureKey) {
|
||||
FeatureMapping m = features.get(logicalFeatureKey);
|
||||
return m != null ? m.bitanswerFeatureId() : null;
|
||||
}
|
||||
|
||||
/** 逻辑特性键对应的比特特征名称;未配置时返回 {@code null}。 */
|
||||
public String bitanswerFeatureName(String logicalFeatureKey) {
|
||||
FeatureMapping m = features.get(logicalFeatureKey);
|
||||
return m != null ? m.bitanswerFeatureName() : null;
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package cn.craftlabs.auth.config;
|
||||
|
||||
/**
|
||||
* {@link AuthConfigs#parse(String)} 或校验失败时抛出。
|
||||
*
|
||||
* <p>版权所有 © 广州创飞人工智能技术有限公司
|
||||
*
|
||||
* @author huangping@craftlabs.cn
|
||||
*/
|
||||
public final class AuthConfigException extends Exception {
|
||||
|
||||
public AuthConfigException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public AuthConfigException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package cn.craftlabs.auth.config;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* 解析并校验 {@link cn.craftlabs.auth.AuthProvider#initialize(String)} 的 JSON 配置。
|
||||
*
|
||||
* <p>版权所有 © 广州创飞人工智能技术有限公司
|
||||
*
|
||||
* @author huangping@craftlabs.cn
|
||||
*/
|
||||
public final class AuthConfigs {
|
||||
|
||||
/** 与 {@code schemas/craftlabs-auth-config.schema.json} 中 {@code schemaVersion} 一致。 */
|
||||
public static final int SCHEMA_VERSION = 1;
|
||||
|
||||
private static final ObjectMapper MAPPER =
|
||||
new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
|
||||
private AuthConfigs() {}
|
||||
|
||||
/** 解析 JSON 并执行 {@link #validate(AuthConfig)}。 */
|
||||
public static AuthConfig parse(String json) throws AuthConfigException {
|
||||
if (json == null || json.isBlank()) {
|
||||
throw new AuthConfigException("config JSON is null or blank");
|
||||
}
|
||||
try {
|
||||
AuthConfig cfg = MAPPER.readValue(json, AuthConfig.class);
|
||||
validate(cfg);
|
||||
return cfg;
|
||||
} catch (JsonProcessingException e) {
|
||||
String msg = e.getOriginalMessage() != null ? e.getOriginalMessage() : e.getMessage();
|
||||
throw new AuthConfigException("Invalid config JSON: " + msg, e);
|
||||
}
|
||||
}
|
||||
|
||||
/** 将配置写回 JSON(便于日志脱敏后落盘或调试)。 */
|
||||
public static String toJson(AuthConfig config) throws AuthConfigException {
|
||||
try {
|
||||
return MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(config);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new AuthConfigException("Failed to serialize config", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验必填组合;通过后可安全交给 native / 供应商实现。
|
||||
*
|
||||
* @throws AuthConfigException 校验失败,消息为多条原因拼接
|
||||
*/
|
||||
public static void validate(AuthConfig c) throws AuthConfigException {
|
||||
List<String> err = new ArrayList<>();
|
||||
|
||||
if (c.schemaVersion() != SCHEMA_VERSION) {
|
||||
err.add("schemaVersion must be " + SCHEMA_VERSION);
|
||||
}
|
||||
|
||||
String provider = norm(c.provider());
|
||||
if (provider.isEmpty()) {
|
||||
err.add("provider is required");
|
||||
} else if (!provider.equals("bitanswer") && !provider.equals("selfhosted")) {
|
||||
err.add("provider must be bitanswer or selfhosted");
|
||||
}
|
||||
|
||||
String scenario = norm(c.scenario());
|
||||
if (scenario.isEmpty()) {
|
||||
err.add("scenario is required");
|
||||
} else if (!scenario.equals("wharf") && !scenario.equals("school") && !scenario.equals("floating")) {
|
||||
err.add("scenario must be wharf, school, or floating");
|
||||
}
|
||||
|
||||
if (provider.equals("bitanswer")) {
|
||||
if (c.bitanswer() == null) {
|
||||
err.add("bitanswer section is required when provider=bitanswer");
|
||||
} else if (isBlank(c.bitanswer().url())) {
|
||||
err.add("bitanswer.url is required and must be non-blank");
|
||||
}
|
||||
}
|
||||
|
||||
if (provider.equals("selfhosted")) {
|
||||
if (c.selfhosted() == null) {
|
||||
err.add("selfhosted section is required when provider=selfhosted");
|
||||
} else if (isBlank(c.selfhosted().baseUrl())) {
|
||||
err.add("selfhosted.baseUrl is required and must be non-blank");
|
||||
}
|
||||
}
|
||||
|
||||
if (scenario.equals("floating")) {
|
||||
if (c.floating() == null || isBlank(c.floating().projectId())) {
|
||||
err.add("floating.projectId is required when scenario=floating");
|
||||
}
|
||||
}
|
||||
|
||||
if (!err.isEmpty()) {
|
||||
throw new AuthConfigException(String.join("; ", err));
|
||||
}
|
||||
}
|
||||
|
||||
private static String norm(String s) {
|
||||
return s == null ? "" : s.trim().toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
private static boolean isBlank(String s) {
|
||||
return s == null || s.trim().isEmpty();
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package cn.craftlabs.auth.config;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* {@code config_json} 中与比特安索客户端相关的字段子集。
|
||||
*
|
||||
* @author huangping@craftlabs.cn
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public record BitanswerConfigSection(
|
||||
@JsonProperty("url") String url,
|
||||
@JsonProperty("loginMode") String loginMode,
|
||||
@JsonProperty("rootPath") String rootPath,
|
||||
@JsonProperty("sn") String sn,
|
||||
@JsonProperty("applicationData") String applicationData) {}
|
||||
@@ -0,0 +1,14 @@
|
||||
package cn.craftlabs.auth.config;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* 逻辑特性键(如 {@code face})到比特特征项的映射;至少填 id 或 name 之一即可在实现层选用。
|
||||
*
|
||||
* @author huangping@craftlabs.cn
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public record FeatureMapping(
|
||||
@JsonProperty("bitanswerFeatureId") Integer bitanswerFeatureId,
|
||||
@JsonProperty("bitanswerFeatureName") String bitanswerFeatureName) {}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package cn.craftlabs.auth.config;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/** 流动人口按项目授权场景补充字段。 */
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public record FloatingScenarioSection(
|
||||
@JsonProperty("projectId") String projectId,
|
||||
@JsonProperty("projectName") String projectName,
|
||||
@JsonProperty("contractRef") String contractRef) {}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package cn.craftlabs.auth.config;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/** 学校按边设备运营场景补充字段。 */
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public record SchoolScenarioSection(
|
||||
@JsonProperty("edgeDeviceId") String edgeDeviceId,
|
||||
@JsonProperty("tenantId") String tenantId) {}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package cn.craftlabs.auth.config;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* {@code provider=selfhosted} 时的 HTTP 后端参数。
|
||||
*
|
||||
* @author huangping@craftlabs.cn
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public record SelfhostedConfigSection(
|
||||
@JsonProperty("baseUrl") String baseUrl,
|
||||
@JsonProperty("tenantKey") String tenantKey) {}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package cn.craftlabs.auth.config;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/** 码头 / 集中授权场景补充字段。 */
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public record WharfScenarioSection(
|
||||
@JsonProperty("topology") String topology,
|
||||
@JsonProperty("groupServiceUrl") String groupServiceUrl,
|
||||
@JsonProperty("notes") String notes) {}
|
||||
@@ -0,0 +1,78 @@
|
||||
package cn.craftlabs.auth.config;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class AuthConfigsTest {
|
||||
|
||||
@Test
|
||||
void parseWharfExampleFromClasspath() throws Exception {
|
||||
String json = readClasspath("examples/config/wharf.bitanswer.json");
|
||||
AuthConfig c = AuthConfigs.parse(json);
|
||||
assertEquals(1, c.schemaVersion());
|
||||
assertEquals("bitanswer", c.provider());
|
||||
assertEquals("wharf", c.scenario());
|
||||
assertNotNull(c.bitanswer());
|
||||
assertTrue(c.bitanswer().url().startsWith("bit://"));
|
||||
assertEquals(101, c.bitanswerFeatureId("container_number_detect"));
|
||||
assertEquals("group", c.wharf().topology());
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseSchoolExample() throws Exception {
|
||||
String json = readClasspath("examples/config/school.bitanswer.json");
|
||||
AuthConfig c = AuthConfigs.parse(json);
|
||||
assertEquals("school", c.scenario());
|
||||
assertEquals(201, c.bitanswerFeatureId("face"));
|
||||
assertEquals("classroom-gate-01", c.school().edgeDeviceId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseFloatingExample_requiresProjectId() throws Exception {
|
||||
String json = readClasspath("examples/config/floating.bitanswer.json");
|
||||
AuthConfig c = AuthConfigs.parse(json);
|
||||
assertEquals("floating", c.scenario());
|
||||
assertEquals("migrant-flow-prj-2026-001", c.floating().projectId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void validateFails_whenFloatingWithoutProject() {
|
||||
String json =
|
||||
"""
|
||||
{"schemaVersion":1,"provider":"bitanswer","scenario":"floating",\
|
||||
"bitanswer":{"url":"http://x"},"floating":{}}\
|
||||
""";
|
||||
AuthConfigException ex = assertThrows(AuthConfigException.class, () -> AuthConfigs.parse(json));
|
||||
assertTrue(ex.getMessage().contains("projectId"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void validateFails_whenBitanswerWithoutUrl() {
|
||||
String json =
|
||||
"""
|
||||
{"schemaVersion":1,"provider":"bitanswer","scenario":"school",\
|
||||
"bitanswer":{"url":""}}\
|
||||
""";
|
||||
assertThrows(AuthConfigException.class, () -> AuthConfigs.parse(json));
|
||||
}
|
||||
|
||||
@Test
|
||||
void roundTrip_toJson() throws Exception {
|
||||
String json = readClasspath("examples/config/school.bitanswer.json");
|
||||
AuthConfig c = AuthConfigs.parse(json);
|
||||
String out = AuthConfigs.toJson(c);
|
||||
AuthConfig again = AuthConfigs.parse(out);
|
||||
assertEquals(c.scenario(), again.scenario());
|
||||
assertEquals(c.bitanswerFeatureId("expression"), again.bitanswerFeatureId("expression"));
|
||||
}
|
||||
|
||||
private static String readClasspath(String path) throws Exception {
|
||||
try (InputStream in = AuthConfigsTest.class.getClassLoader().getResourceAsStream(path)) {
|
||||
assertNotNull(in, "missing classpath resource: " + path);
|
||||
return new String(in.readAllBytes(), StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
package cn.craftlabs.auth.config;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.networknt.schema.JsonSchema;
|
||||
import com.networknt.schema.JsonSchemaFactory;
|
||||
import com.networknt.schema.SpecVersion;
|
||||
import com.networknt.schema.ValidationMessage;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* BP-10 / I5:{@code examples/config/*.json} 与仓库根 {@code schemas/craftlabs-auth-config.schema.json} 一致。
|
||||
*/
|
||||
class ExamplesConfigSchemaValidationTest {
|
||||
|
||||
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||
|
||||
@Test
|
||||
void allExampleConfigsValidateAgainstSchema() throws Exception {
|
||||
Path repoRoot = findRepoRoot();
|
||||
Path schemaPath = repoRoot.resolve("schemas/craftlabs-auth-config.schema.json");
|
||||
Path examplesDir = repoRoot.resolve("examples/config");
|
||||
assertTrue(Files.isRegularFile(schemaPath), "schema missing: " + schemaPath);
|
||||
assertTrue(Files.isDirectory(examplesDir), "examples dir missing: " + examplesDir);
|
||||
|
||||
JsonNode schemaNode = MAPPER.readTree(schemaPath.toFile());
|
||||
JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V202012);
|
||||
JsonSchema schema = factory.getSchema(schemaNode);
|
||||
|
||||
try (Stream<Path> stream = Files.list(examplesDir)) {
|
||||
stream.filter(p -> p.toString().endsWith(".json")).forEach(jsonFile -> {
|
||||
try {
|
||||
JsonNode instance = MAPPER.readTree(jsonFile.toFile());
|
||||
Set<ValidationMessage> errors = schema.validate(instance);
|
||||
assertTrue(
|
||||
errors.isEmpty(),
|
||||
() -> jsonFile.getFileName() + ": " + errors);
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError(jsonFile.toString(), e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 自 {@code user.dir} 向上查找含 {@code schemas/craftlabs-auth-config.schema.json} 的目录(兼容模块目录或仓库根执行)。
|
||||
*/
|
||||
static Path findRepoRoot() {
|
||||
Path p = Path.of(System.getProperty("user.dir", ".")).toAbsolutePath().normalize();
|
||||
for (int i = 0; i < 8 && p != null; i++) {
|
||||
Path candidate = p.resolve("schemas/craftlabs-auth-config.schema.json");
|
||||
if (Files.isRegularFile(candidate)) {
|
||||
return p;
|
||||
}
|
||||
p = p.getParent();
|
||||
}
|
||||
throw new IllegalStateException(
|
||||
"Could not find repo root (schemas/craftlabs-auth-config.schema.json) from user.dir="
|
||||
+ System.getProperty("user.dir"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"provider": "bitanswer",
|
||||
"scenario": "floating",
|
||||
"bitanswer": {
|
||||
"url": "https://cloud.bitanswer.example/e3",
|
||||
"loginMode": "REMOTE"
|
||||
},
|
||||
"features": {
|
||||
"face": { "bitanswerFeatureId": 301 }
|
||||
},
|
||||
"floating": {
|
||||
"projectId": "migrant-flow-prj-2026-001",
|
||||
"projectName": "某市流动人口人像核验",
|
||||
"contractRef": "PO-2026-8848"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"provider": "bitanswer",
|
||||
"scenario": "school",
|
||||
"bitanswer": {
|
||||
"url": "https://cloud.bitanswer.example/e3",
|
||||
"loginMode": "AUTO",
|
||||
"sn": ""
|
||||
},
|
||||
"features": {
|
||||
"face": { "bitanswerFeatureId": 201 },
|
||||
"expression": { "bitanswerFeatureId": 202 }
|
||||
},
|
||||
"school": {
|
||||
"edgeDeviceId": "classroom-gate-01",
|
||||
"tenantId": "school-district-demo"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"provider": "bitanswer",
|
||||
"scenario": "wharf",
|
||||
"bitanswer": {
|
||||
"url": "bit://license.example.com:8273",
|
||||
"loginMode": "AUTO",
|
||||
"rootPath": "/var/lib/craftlabs/bitanswer"
|
||||
},
|
||||
"features": {
|
||||
"container_number_detect": { "bitanswerFeatureId": 101 },
|
||||
"container_number_recognize": { "bitanswerFeatureId": 102 },
|
||||
"iso_detect": { "bitanswerFeatureId": 103 },
|
||||
"iso_recognize": { "bitanswerFeatureId": 104 }
|
||||
},
|
||||
"wharf": {
|
||||
"topology": "group",
|
||||
"groupServiceUrl": "bit://license.example.com:8273",
|
||||
"notes": "边设备集中连集团服务;特征项与控制台产品一致后替换 id。"
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,8 @@
|
||||
</dependencies>
|
||||
|
||||
<properties>
|
||||
<!-- 测试模块不发布;跳过 GPG 以免生成多余 .asc -->
|
||||
<gpg.skip>true</gpg.skip>
|
||||
<native.library.path>${project.basedir}/../../native/build</native.library.path>
|
||||
</properties>
|
||||
|
||||
|
||||
@@ -21,6 +21,10 @@
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<maven.compiler.release>17</maven.compiler.release>
|
||||
<junit.version>5.10.2</junit.version>
|
||||
<jackson.version>2.17.2</jackson.version>
|
||||
<json-schema-validator.version>1.5.7</json-schema-validator.version>
|
||||
<!-- 默认跳过 GPG;发布签名:mvn -Prelease-sign verify(见 RELEASING.md) -->
|
||||
<gpg.skip>true</gpg.skip>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
@@ -36,6 +40,17 @@
|
||||
<version>${junit.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
<version>${jackson.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.networknt</groupId>
|
||||
<artifactId>json-schema-validator</artifactId>
|
||||
<version>${json-schema-validator.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
@@ -52,7 +67,39 @@
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.2.5</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-gpg-plugin</artifactId>
|
||||
<version>3.2.7</version>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</pluginManagement>
|
||||
</build>
|
||||
|
||||
<!-- 对发布 JAR 做 GPG 分离签名(.asc);显式 -Prelease-sign 时启用 -->
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>release-sign</id>
|
||||
<properties>
|
||||
<gpg.skip>false</gpg.skip>
|
||||
</properties>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-gpg-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>sign-release-artifacts</id>
|
||||
<phase>verify</phase>
|
||||
<goals>
|
||||
<goal>sign</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</profile>
|
||||
</profiles>
|
||||
</project>
|
||||
|
||||
Reference in New Issue
Block a user