|
|
@@ -0,0 +1,425 @@
|
|
|
+package com.etotem.cfc.unit;
|
|
|
+
|
|
|
+import org.junit.jupiter.api.DisplayName;
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
+
|
|
|
+import java.io.InputStream;
|
|
|
+import java.lang.annotation.Annotation;
|
|
|
+import java.lang.reflect.Field;
|
|
|
+import java.lang.reflect.Method;
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
+import java.nio.file.Files;
|
|
|
+import java.nio.file.Paths;
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Scanner;
|
|
|
+
|
|
|
+import static org.junit.jupiter.api.Assertions.assertFalse;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertNotNull;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
|
+import static org.junit.jupiter.api.Assertions.fail;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 契约级单测:操作审计日志(能力键 {@code admin.audit.log},单元 admin / SC-A-15)。
|
|
|
+ *
|
|
|
+ * <p><b>需求/设计锚点</b></p>
|
|
|
+ * <ul>
|
|
|
+ * <li>需求:{@code tests/user-scenarios/05-admin.md} SC-A-15 —— 作为运营,查询审计日志并溯源,
|
|
|
+ * 以便可追溯;<b>边界:日志不可篡改</b>;<b>分页</b>。</li>
|
|
|
+ * <li>设计:{@code docs/superpowers/api/API_REFERENCE.md} §4.60.14 ——
|
|
|
+ * {@code OperationLogController /api/admin/logs}({@code operations} 分页查询 + {@code recent} 近期)
|
|
|
+ * + {@code AdminAuditController /api/admin/audit}({@code list} 审计列表);
|
|
|
+ * §4.13 管理员接口需 {@code role=admin}({@code AdminInterceptor})。</li>
|
|
|
+ * <li>写入链路:{@code AuditLogAspect} 自动落 {@code audit_log}(管理端写操作),
|
|
|
+ * {@code AuditLogService} 只增不删改(W0.3 审计日志统一)。</li>
|
|
|
+ * </ul>
|
|
|
+ *
|
|
|
+ * <p><b>被校验的契约行为</b></p>
|
|
|
+ * <ul>
|
|
|
+ * <li><b>正常流(溯源)</b>:{@code AdminAuditController /api/admin/audit/list} 分页查询 + 过滤
|
|
|
+ * (module / targetId / 时间范围);{@code OperationLogController /api/admin/logs}
|
|
|
+ * {@code operations} + {@code recent};{@code AuditLog} 实体承载溯源字段
|
|
|
+ * (operatorId / operatorName / module / action / targetType / targetId /
|
|
|
+ * beforeValue / afterValue / ip / createdAt)。</li>
|
|
|
+ * <li><b>边界流</b>:日志<b>不可篡改</b>——{@code AuditLogService} 只增不删改(不暴露
|
|
|
+ * update / remove / delete 公开方法);{@code audit_log} + {@code user_operation_logs}
|
|
|
+ * 建表落地 schema.sql,且 DatabaseInitializer 有对应建表迁移(schema.sql 与迁移一致)。</li>
|
|
|
+ * <li><b>权限流</b>:{@code AdminAuditController.list} 首参 {@code @RequestAttribute("role")},
|
|
|
+ * 仅 {@code role=admin} 可访问(§4.13)。</li>
|
|
|
+ * </ul>
|
|
|
+ *
|
|
|
+ * <p><b>测试范围</b>:纯契约级单测,反射锚定宿主类(避免编译期依赖不存在类)+ schema.sql
|
|
|
+ * 快照 + DatabaseInitializer 源码检查;<b>无 Spring / 无 DB / 无外部服务</b>。</p>
|
|
|
+ *
|
|
|
+ * <p><b>宿主契约类(实现方按此落地)</b></p>
|
|
|
+ * <ul>
|
|
|
+ * <li>{@code com.etotem.cfc.controller.admin.AdminAuditController} —— {@code @RequestMapping("/api/admin/audit")}
|
|
|
+ * + {@code @PostMapping("/list")} + {@code list(@RequestAttribute("role") String, Map)}</li>
|
|
|
+ * <li>{@code com.etotem.cfc.controller.admin.OperationLogController} —— {@code @RequestMapping("/api/admin/logs")}
|
|
|
+ * + {@code @PostMapping("/operations")} + {@code @PostMapping("/recent")}</li>
|
|
|
+ * <li>{@code com.etotem.cfc.service.AuditLogService} —— {@code page(module, targetId, startTime, endTime, pageNo, pageSize)}
|
|
|
+ * + {@code record(AuditLog)};<b>不暴露 update / remove / delete</b>(只增不删改)</li>
|
|
|
+ * <li>{@code com.etotem.cfc.entity.AuditLog} —— 溯源字段齐全</li>
|
|
|
+ * <li>{@code com.etotem.cfc.aspect.AuditLogAspect} —— 自动落审计切面</li>
|
|
|
+ * </ul>
|
|
|
+ *
|
|
|
+ * <p><b>RED 预期说明</b>:本批契约登记时该能力曾判定「实现未落地」;实测(2026-11-13)发现
|
|
|
+ * {@code origin/cfclub} 已包含上述宿主类与表,故本测试按契约行为断言如实验证——若契约行为缺失
|
|
|
+ * (如去掉了不可篡改约束 / 权限门禁 / 端点)即红,作为<b>契约回归测试</b>锁定行为。</p>
|
|
|
+ */
|
|
|
+class AdminAuditLogContractTest {
|
|
|
+
|
|
|
+ /** 审计查询控制器(AdminAuditController /api/admin/audit)。 */
|
|
|
+ private static final String ADMIN_AUDIT_CONTROLLER =
|
|
|
+ "com.etotem.cfc.controller.admin.AdminAuditController";
|
|
|
+
|
|
|
+ /** 操作日志控制器(OperationLogController /api/admin/logs)。 */
|
|
|
+ private static final String OPERATION_LOG_CONTROLLER =
|
|
|
+ "com.etotem.cfc.controller.admin.OperationLogController";
|
|
|
+
|
|
|
+ /** 审计日志服务(AuditLogService,只增不删改)。 */
|
|
|
+ private static final String AUDIT_LOG_SERVICE =
|
|
|
+ "com.etotem.cfc.service.AuditLogService";
|
|
|
+
|
|
|
+ /** 审计日志实体(AuditLog)。 */
|
|
|
+ private static final String AUDIT_LOG_ENTITY =
|
|
|
+ "com.etotem.cfc.entity.AuditLog";
|
|
|
+
|
|
|
+ /** 审计切面(AuditLogAspect,自动落审计)。 */
|
|
|
+ private static final String AUDIT_LOG_ASPECT =
|
|
|
+ "com.etotem.cfc.aspect.AuditLogAspect";
|
|
|
+
|
|
|
+ /** 用户操作日志服务(UserOperationLogService)。 */
|
|
|
+ private static final String USER_OPERATION_LOG_SERVICE =
|
|
|
+ "com.etotem.cfc.service.UserOperationLogService";
|
|
|
+
|
|
|
+ /** DatabaseInitializer 源码(迁移一致性检查)。 */
|
|
|
+ private static final String DATABASE_INITIALIZER_SOURCE =
|
|
|
+ "src/main/java/com/etotem/cfc/config/DatabaseInitializer.java";
|
|
|
+
|
|
|
+ // ------------------------------------------------------------------
|
|
|
+ // 正常流:查询审计日志 → 溯源(SC-A-15 主流程)
|
|
|
+ // ------------------------------------------------------------------
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("正常流-1:AdminAuditController 应挂载 /api/admin/audit 并提供 /list")
|
|
|
+ void adminAuditController_mountsAuditListEndpoint() {
|
|
|
+ Class<?> clazz = loadClass(ADMIN_AUDIT_CONTROLLER);
|
|
|
+ assertNotNull(clazz,
|
|
|
+ "[admin.audit.log] AdminAuditController 类不存在:契约要求 POST /api/admin/audit/list"
|
|
|
+ + " 审计列表(§4.60.14)。");
|
|
|
+ // 类级 @RequestMapping("/api/admin/audit")
|
|
|
+ assertTrue(hasRequestMapping(clazz, "/api/admin/audit"),
|
|
|
+ "[admin.audit.log] AdminAuditController 应 @RequestMapping(\"/api/admin/audit\")(§4.60.14)。");
|
|
|
+ // 方法级 @PostMapping("/list")
|
|
|
+ Method list = findMethod(clazz, "list");
|
|
|
+ assertNotNull(list,
|
|
|
+ "[admin.audit.log] AdminAuditController 应提供 list(...) 方法作为 /list 端点(§4.60.14)。");
|
|
|
+ assertTrue(hasPostMapping(list, "/list"),
|
|
|
+ "[admin.audit.log] AdminAuditController.list 应 @PostMapping(\"/list\")(统一 POST,§4.13)。");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("正常流-2:AuditLogService 应提供分页查询 page(module,targetId,start,end,page,size)")
|
|
|
+ void auditLogService_hasPageQuery() {
|
|
|
+ Class<?> clazz = loadClass(AUDIT_LOG_SERVICE);
|
|
|
+ assertNotNull(clazz,
|
|
|
+ "[admin.audit.log] AuditLogService 类不存在:契约要求分页查询审计日志(SC-A-15 主流程)。");
|
|
|
+ // page(String, String, Date, Date, int, int) —— 6 参分页查询(module/targetId/时间范围/分页)
|
|
|
+ Method page = findMethodByParamCount(clazz, "page", 6);
|
|
|
+ assertNotNull(page,
|
|
|
+ "[admin.audit.log] AuditLogService 应提供 page(module, targetId, startTime, endTime,"
|
|
|
+ + " pageNo, pageSize) 分页查询(SC-A-15 分页边界)。");
|
|
|
+ // record(AuditLog) 写入入口
|
|
|
+ Method record = findMethodByParamCount(clazz, "record", 1);
|
|
|
+ assertNotNull(record,
|
|
|
+ "[admin.audit.log] AuditLogService 应提供 record(AuditLog) 写入入口(W0.3 审计写入)。");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("正常流-3:AuditLog 实体应承载溯源字段(操作人/模块/动作/目标/前后值/IP/时间)")
|
|
|
+ void auditLogEntity_carriesTraceFields() {
|
|
|
+ Class<?> clazz = loadClass(AUDIT_LOG_ENTITY);
|
|
|
+ assertNotNull(clazz,
|
|
|
+ "[admin.audit.log] AuditLog 实体不存在:契约要求 audit_log 表实体承载溯源字段(SC-A-15)。");
|
|
|
+ String[] required = {
|
|
|
+ "operatorId", "operatorName", "module", "action", "targetType",
|
|
|
+ "targetId", "beforeValue", "afterValue", "ip", "createdAt"
|
|
|
+ };
|
|
|
+ List<String> missing = new ArrayList<>();
|
|
|
+ for (String name : required) {
|
|
|
+ if (!hasField(clazz, name)) {
|
|
|
+ missing.add(name);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ assertTrue(missing.isEmpty(),
|
|
|
+ "[admin.audit.log] AuditLog 实体缺失溯源字段:" + missing
|
|
|
+ + "(契约要求 operatorId/operatorName/module/action/targetType/"
|
|
|
+ + "targetId/beforeValue/afterValue/ip/createdAt,SC-A-15 溯源)。");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("正常流-4:OperationLogController 应挂载 /api/admin/logs 并提供 operations+recent")
|
|
|
+ void operationLogController_mountsOperationsAndRecent() {
|
|
|
+ Class<?> clazz = loadClass(OPERATION_LOG_CONTROLLER);
|
|
|
+ assertNotNull(clazz,
|
|
|
+ "[admin.audit.log] OperationLogController 类不存在:契约要求 POST /api/admin/logs"
|
|
|
+ + " operations + recent(§4.60.14)。");
|
|
|
+ assertTrue(hasRequestMapping(clazz, "/api/admin/logs"),
|
|
|
+ "[admin.audit.log] OperationLogController 应 @RequestMapping(\"/api/admin/logs\")(§4.60.14)。");
|
|
|
+ Method operations = findMethod(clazz, "queryLogs");
|
|
|
+ assertNotNull(operations,
|
|
|
+ "[admin.audit.log] OperationLogController 应提供 queryLogs(...) 作为 /operations 端点(§4.60.14)。");
|
|
|
+ assertTrue(hasPostMapping(operations, "/operations"),
|
|
|
+ "[admin.audit.log] OperationLogController.queryLogs 应 @PostMapping(\"/operations\")(§4.60.14)。");
|
|
|
+ Method recent = findMethod(clazz, "getRecentLogs");
|
|
|
+ assertNotNull(recent,
|
|
|
+ "[admin.audit.log] OperationLogController 应提供 getRecentLogs(...) 作为 /recent 端点(§4.60.14)。");
|
|
|
+ assertTrue(hasPostMapping(recent, "/recent"),
|
|
|
+ "[admin.audit.log] OperationLogController.getRecentLogs 应 @PostMapping(\"/recent\")(§4.60.14)。");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("正常流-5:UserOperationLogService 应提供 queryLogs + getRecentLogs 服务方法")
|
|
|
+ void userOperationLogService_hasQueryAndRecent() {
|
|
|
+ Class<?> clazz = loadClass(USER_OPERATION_LOG_SERVICE);
|
|
|
+ assertNotNull(clazz,
|
|
|
+ "[admin.audit.log] UserOperationLogService 类不存在:契约要求操作日志分页/近期查询(§4.60.14)。");
|
|
|
+ Method query = findMethod(clazz, "queryLogs");
|
|
|
+ assertNotNull(query,
|
|
|
+ "[admin.audit.log] UserOperationLogService 应提供 queryLogs(...)(/operations 分页查询)。");
|
|
|
+ Method recent = findMethod(clazz, "getRecentLogs");
|
|
|
+ assertNotNull(recent,
|
|
|
+ "[admin.audit.log] UserOperationLogService 应提供 getRecentLogs(...)(/recent 近期查询)。");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("正常流-6:AuditLogAspect 应存在(管理端写操作自动落审计)")
|
|
|
+ void auditLogAspect_existsForAutoAudit() {
|
|
|
+ Class<?> clazz = loadClass(AUDIT_LOG_ASPECT);
|
|
|
+ assertNotNull(clazz,
|
|
|
+ "[admin.audit.log] AuditLogAspect 类不存在:契约要求管理端写操作自动落 audit_log(W0.3)。");
|
|
|
+ // 切面应含 @Aspect 注解
|
|
|
+ assertTrue(hasAnnotation(clazz, "org.aspectj.lang.annotation.Aspect"),
|
|
|
+ "[admin.audit.log] AuditLogAspect 应标注 @Aspect(Spring AOP 自动审计切面)。");
|
|
|
+ }
|
|
|
+
|
|
|
+ // ------------------------------------------------------------------
|
|
|
+ // 边界流:日志不可篡改(只增不删改) + 建表落地
|
|
|
+ // ------------------------------------------------------------------
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("边界流-1:AuditLogService 不得暴露 update/remove/delete(日志不可篡改)")
|
|
|
+ void auditLogService_appendOnly_noUpdateDelete() {
|
|
|
+ Class<?> clazz = loadClass(AUDIT_LOG_SERVICE);
|
|
|
+ if (clazz == null) {
|
|
|
+ fail("[admin.audit.log] AuditLogService 类不存在,无法验证只增不删改(SC-A-15 边界)。");
|
|
|
+ }
|
|
|
+ List<String> exposed = new ArrayList<>();
|
|
|
+ for (Method m : clazz.getDeclaredMethods()) {
|
|
|
+ String n = m.getName().toLowerCase();
|
|
|
+ if (n.startsWith("update") || n.startsWith("remove") || n.startsWith("delete")) {
|
|
|
+ exposed.add(m.getName());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ assertTrue(exposed.isEmpty(),
|
|
|
+ "[admin.audit.log] 日志不可篡改(SC-A-15 边界):AuditLogService 不得暴露 "
|
|
|
+ + "update/remove/delete 方法(只增不删改),发现:" + exposed);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("边界流-2:audit_log 与 user_operation_logs 应落地 schema.sql")
|
|
|
+ void schemaSql_containsAuditTables() {
|
|
|
+ String schema = readSchemaSql();
|
|
|
+ assertTrue(schema.contains("audit_log"),
|
|
|
+ "[admin.audit.log] schema.sql 缺少 audit_log 建表(W0.3 审计日志统一)。");
|
|
|
+ assertTrue(schema.contains("user_operation_logs"),
|
|
|
+ "[admin.audit.log] schema.sql 缺少 user_operation_logs 建表(操作日志)。");
|
|
|
+ // 审计表应为只增不删改形态:含 created_at 时间列
|
|
|
+ assertTrue(schema.contains("created_at"),
|
|
|
+ "[admin.audit.log] audit_log 应含 created_at 时间列(溯源时间)。");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("边界流-3:DatabaseInitializer 应有 audit_log 建表迁移(schema.sql 与迁移一致)")
|
|
|
+ void databaseInitializer_hasAuditLogMigration() {
|
|
|
+ String source = readDatabaseInitializerSource();
|
|
|
+ assertTrue(source.contains("audit_log"),
|
|
|
+ "[admin.audit.log] DatabaseInitializer 缺少 audit_log 建表迁移(AGENTS 迁移一致原则:"
|
|
|
+ + "schema.sql 的 DDL 必须在 DatabaseInitializer 有对应迁移)。");
|
|
|
+ }
|
|
|
+
|
|
|
+ // ------------------------------------------------------------------
|
|
|
+ // 权限流:仅 admin 角色可访问(§4.13)
|
|
|
+ // ------------------------------------------------------------------
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("权限流:AdminAuditController.list 首参应带 @RequestAttribute(\"role\") 做 admin 门禁")
|
|
|
+ void adminAuditController_list_hasRoleGate() {
|
|
|
+ Class<?> clazz = loadClass(ADMIN_AUDIT_CONTROLLER);
|
|
|
+ if (clazz == null) {
|
|
|
+ fail("[admin.audit.log] AdminAuditController 类不存在,无法验证 role 门禁(§4.13)。");
|
|
|
+ }
|
|
|
+ Method list = findMethod(clazz, "list");
|
|
|
+ if (list == null) {
|
|
|
+ fail("[admin.audit.log] AdminAuditController.list 不存在,无法验证 role 门禁(§4.13)。");
|
|
|
+ }
|
|
|
+ Annotation[][] paramAnns = list.getParameterAnnotations();
|
|
|
+ boolean hasRoleAttr = false;
|
|
|
+ for (Annotation[] anns : paramAnns) {
|
|
|
+ for (Annotation a : anns) {
|
|
|
+ if (a.annotationType().getName().equals("org.springframework.web.bind.annotation.RequestAttribute")) {
|
|
|
+ hasRoleAttr = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (hasRoleAttr) {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ assertTrue(hasRoleAttr,
|
|
|
+ "[admin.audit.log] AdminAuditController.list 应通过 @RequestAttribute(\"role\") 校验"
|
|
|
+ + " role=admin(§4.13 管理员接口需 role=admin,AdminInterceptor 口径)。");
|
|
|
+ }
|
|
|
+
|
|
|
+ // ------------------------------------------------------------------
|
|
|
+ // 反射辅助(锚定宿主契约,避免编译期依赖不存在类)
|
|
|
+ // ------------------------------------------------------------------
|
|
|
+
|
|
|
+ private Class<?> loadClass(String name) {
|
|
|
+ try {
|
|
|
+ return Class.forName(name, false, Thread.currentThread().getContextClassLoader());
|
|
|
+ } catch (ClassNotFoundException e) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private Method findMethod(Class<?> clazz, String name) {
|
|
|
+ if (clazz == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ for (Method m : clazz.getDeclaredMethods()) {
|
|
|
+ if (m.getName().equals(name)) {
|
|
|
+ return m;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Method findMethodByParamCount(Class<?> clazz, String name, int paramCount) {
|
|
|
+ if (clazz == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ for (Method m : clazz.getDeclaredMethods()) {
|
|
|
+ if (m.getName().equals(name) && m.getParameterCount() == paramCount) {
|
|
|
+ return m;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean hasField(Class<?> clazz, String name) {
|
|
|
+ if (clazz == null) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ for (Field f : clazz.getDeclaredFields()) {
|
|
|
+ if (f.getName().equals(name)) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean hasAnnotation(Class<?> clazz, String annotationName) {
|
|
|
+ if (clazz == null) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ for (Annotation a : clazz.getDeclaredAnnotations()) {
|
|
|
+ if (a.annotationType().getName().equals(annotationName)) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 类级 @RequestMapping 是否含给定路径片段。 */
|
|
|
+ private boolean hasRequestMapping(Class<?> clazz, String pathPart) {
|
|
|
+ if (clazz == null) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ for (Annotation a : clazz.getDeclaredAnnotations()) {
|
|
|
+ if (a.annotationType().getName().equals("org.springframework.web.bind.annotation.RequestMapping")) {
|
|
|
+ try {
|
|
|
+ Method value = a.annotationType().getMethod("value");
|
|
|
+ String[] paths = (String[]) value.invoke(a);
|
|
|
+ for (String p : paths) {
|
|
|
+ if (p.contains(pathPart)) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (Exception ignored) {
|
|
|
+ // 注解反射失败,继续
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 方法级 @PostMapping 是否含给定路径。 */
|
|
|
+ private boolean hasPostMapping(Method method, String path) {
|
|
|
+ if (method == null) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ for (Annotation a : method.getDeclaredAnnotations()) {
|
|
|
+ if (a.annotationType().getName().equals("org.springframework.web.bind.annotation.PostMapping")) {
|
|
|
+ try {
|
|
|
+ Method value = a.annotationType().getMethod("value");
|
|
|
+ String[] paths = (String[]) value.invoke(a);
|
|
|
+ for (String p : paths) {
|
|
|
+ if (p.equals(path)) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (Exception ignored) {
|
|
|
+ // 注解反射失败,继续
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String readSchemaSql() {
|
|
|
+ try (InputStream is = getClass().getClassLoader().getResourceAsStream("schema.sql")) {
|
|
|
+ if (is == null) {
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+ try (Scanner scanner = new Scanner(is, StandardCharsets.UTF_8.name())) {
|
|
|
+ scanner.useDelimiter("\\A");
|
|
|
+ return scanner.hasNext() ? scanner.next() : "";
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private String readDatabaseInitializerSource() {
|
|
|
+ // mvn test 工作目录为模块根(cfc-backend),相对路径可直接读取;若失败再回退 classpath。
|
|
|
+ try {
|
|
|
+ byte[] bytes = Files.readAllBytes(Paths.get(DATABASE_INITIALIZER_SOURCE));
|
|
|
+ return new String(bytes, StandardCharsets.UTF_8);
|
|
|
+ } catch (Exception e) {
|
|
|
+ try (InputStream is = getClass().getClassLoader()
|
|
|
+ .getResourceAsStream("DatabaseInitializer.java")) {
|
|
|
+ if (is == null) {
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+ try (Scanner scanner = new Scanner(is, StandardCharsets.UTF_8.name())) {
|
|
|
+ scanner.useDelimiter("\\A");
|
|
|
+ return scanner.hasNext() ? scanner.next() : "";
|
|
|
+ }
|
|
|
+ } catch (Exception ignored) {
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|