|
|
@@ -0,0 +1,345 @@
|
|
|
+package com.etotem.cfc.unit;
|
|
|
+
|
|
|
+import com.etotem.cfc.entity.ProductOrder;
|
|
|
+import org.junit.jupiter.api.DisplayName;
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
+import org.springframework.scheduling.annotation.Scheduled;
|
|
|
+import org.springframework.stereotype.Component;
|
|
|
+import org.springframework.transaction.annotation.Transactional;
|
|
|
+
|
|
|
+import java.io.InputStream;
|
|
|
+import java.lang.reflect.Field;
|
|
|
+import java.lang.reflect.Method;
|
|
|
+import java.lang.reflect.Modifier;
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
+import java.util.Scanner;
|
|
|
+
|
|
|
+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 shop.order.receiptTimeout})。
|
|
|
+ *
|
|
|
+ * <p>契约来源(PRD §3 P0-B + 架构设计 §2.2/§2.4/§3.3/§6 T03):
|
|
|
+ * <blockquote>
|
|
|
+ * P0-B 待收货超时自动确认/完成(依赖 G4):新增定时任务(仿 {@code OrderTimeoutCancelTask} 模式,
|
|
|
+ * {@code task/OrderReceiptTimeoutTask.java})扫描 {@code pending_receipt} 且距发货时间超过设定阈值
|
|
|
+ * 的订单,自动转 {@code completed},写入操作日志/系统通知用户;阈值走可配置而非硬编码。
|
|
|
+ * 验收:① 发货后超过阈值未确认的订单被自动置 {@code completed},并产生可追溯记录;
|
|
|
+ * ② 阈值可在管理端/配置表调整;任务重复执行不重复完成(幂等)。
|
|
|
+ * </blockquote></p>
|
|
|
+ *
|
|
|
+ * <p><b>设计契约锚点</b>(架构设计 §6 类图 + §3.3):
|
|
|
+ * <ul>
|
|
|
+ * <li>{@code OrderReceiptTimeoutTask} 类:{@code @Component} + {@code @Scheduled(fixedRate=60000)} +
|
|
|
+ * {@code DatabaseInitializer.isMigrationsComplete()} 守卫 + {@code @Transactional}。</li>
|
|
|
+ * <li>类图方法签名:{@code processTimeoutOrders()}(公共入口,对应 {@code OrderTimeoutCancelTask.cancelTimeoutOrders()})、
|
|
|
+ * {@code autoComplete()}(自动完成)、{@code sendRemind()}(第9天提醒)、
|
|
|
+ * {@code resolveShipTimeBase(ProductOrder)}(COALESCE(shipped_at, updated_at) 基准时间兜底)。</li>
|
|
|
+ * <li>自动完成逻辑:{@code status=pending_receipt AND COALESCE(shipped_at, updated_at) < now - receipt-timeout-days}
|
|
|
+ * → {@code status=completed}、{@code confirmTime=now}、{@code updatedAt=now}、
|
|
|
+ * 写 {@code user_operation_logs}(type=product_order_auto_complete);幂等(completed 不再被扫中)。</li>
|
|
|
+ * <li>第9天提醒:{@code status=pending_receipt AND shipped_at IS NOT NULL AND shipped_at < now - receipt-remind-days
|
|
|
+ * AND receipt_remind_at IS NULL} → {@code WechatService.sendTemplateMessage(buyerOpenid, templateId, ...)} 推送,
|
|
|
+ * 成功后写 {@code receipt_remind_at=now}(失败仅日志,不重试风暴,下一轮再试)。</li>
|
|
|
+ * <li>总开关:{@code receipt-auto-complete-enabled=false} 时仅提醒不自动完成(灰度开关)。</li>
|
|
|
+ * <li>数据契约:{@code product_orders} 加 {@code shipped_at}(发货时间,ship() 成功落库时写)、
|
|
|
+ * {@code receipt_remind_at}(提醒推送时间,幂等防重复);{@code application.yml} 4 个配置键
|
|
|
+ * {@code receipt-timeout-days / receipt-remind-days / receipt-auto-complete-enabled / receipt-remind-template-id}。</li>
|
|
|
+ * </ul></p>
|
|
|
+ *
|
|
|
+ * <p><b>实现现状</b>:本分支 src/main 中 {@code task/OrderReceiptTimeoutTask.java} 类<b>未落地</b>
|
|
|
+ * (task 目录全量 grep 无 {@code OrderReceiptTimeoutTask} 引用);{@code ProductOrderService.ship()} 也
|
|
|
+ * 未写 {@code shippedAt}(无 setShippedAt 调用)。因此本测试以「反射结构契约 + 数据契约」探测实现缺失:
|
|
|
+ * 能编译通过、能运行、断言失败(红),且失败断言原文清晰可贴。</p>
|
|
|
+ *
|
|
|
+ * <p><b>GREEN 升级指引</b>:实现者创建 {@code task/OrderReceiptTimeoutTask.java}(含上述方法签名与注解、
|
|
|
+ * 自动完成 + 第9天提醒 + COALESCE 基准 + 总开关逻辑),并让 {@code ProductOrderService.ship()} 成功落库时写
|
|
|
+ * {@code shipped_at=now} 后,本测试自动转绿;届时可将结构探测升级为行为级 mock 验证
|
|
|
+ * ({@code verify(orderMapper).updateById(...)} 断言 completed / 断言 sendTemplateMessage 调用与 receipt_remind_at 写入)。</p>
|
|
|
+ *
|
|
|
+ * <p><b>权限流说明</b>:本能力为后端内部定时任务(无用户请求上下文),无用户权限维度,
|
|
|
+ * 「权限(适用时)」三流中的权限流不适用,以说明性用例标注。</p>
|
|
|
+ */
|
|
|
+class OrderReceiptTimeoutContractTest {
|
|
|
+
|
|
|
+ /** 契约要求的待收货超时任务类(架构设计 §3.3 / §6 T03) */
|
|
|
+ private static final String RECEIPT_TIMEOUT_TASK =
|
|
|
+ "com.etotem.cfc.task.OrderReceiptTimeoutTask";
|
|
|
+
|
|
|
+ /** 契约要求的自动完成触发状态(PRD §3 P0-B) */
|
|
|
+ private static final String STATUS_PENDING_RECEIPT = "pending_receipt";
|
|
|
+
|
|
|
+ /** 自动完成可追溯日志类型(架构设计 §3.3) */
|
|
|
+ private static final String AUTO_COMPLETE_LOG_TYPE = "product_order_auto_complete";
|
|
|
+
|
|
|
+ // ------------------------------------------------------------------
|
|
|
+ // 正常流:OrderReceiptTimeoutTask 类应存在并注册为 Spring 组件
|
|
|
+ // ------------------------------------------------------------------
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("正常流-1:OrderReceiptTimeoutTask 类应存在")
|
|
|
+ void receiptTimeoutTask_classExists() {
|
|
|
+ assertNotNull(loadClass(RECEIPT_TIMEOUT_TASK),
|
|
|
+ "[shop.order.receiptTimeout] OrderReceiptTimeoutTask 类不存在:"
|
|
|
+ + "契约要求新增定时任务扫描 pending_receipt 且超时未确认的订单自动转 completed(PRD §3 P0-B)。"
|
|
|
+ + "实现未落地。");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("正常流-2:OrderReceiptTimeoutTask 应注册为 @Component(Spring 单机任务)")
|
|
|
+ void receiptTimeoutTask_isSpringComponent() {
|
|
|
+ Class<?> clazz = loadClass(RECEIPT_TIMEOUT_TASK);
|
|
|
+ if (clazz == null) {
|
|
|
+ fail("[shop.order.receiptTimeout] OrderReceiptTimeoutTask 类不存在,无法验证 @Component(架构设计 §3.3)。");
|
|
|
+ }
|
|
|
+ boolean hasComponent = false;
|
|
|
+ for (java.lang.annotation.Annotation a : clazz.getAnnotations()) {
|
|
|
+ if (a.annotationType().equals(Component.class)) {
|
|
|
+ hasComponent = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ assertTrue(hasComponent,
|
|
|
+ "[shop.order.receiptTimeout] OrderReceiptTimeoutTask 应标注 @Component"
|
|
|
+ + "(复用 Spring @Scheduled 单机任务框架,架构设计 §3.3)。");
|
|
|
+ }
|
|
|
+
|
|
|
+ // ------------------------------------------------------------------
|
|
|
+ // 正常流:公共入口 processTimeoutOrders(@Scheduled + @Transactional + 迁移守卫)
|
|
|
+ // ------------------------------------------------------------------
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("正常流-3:应提供 processTimeoutOrders 公共入口(@Scheduled fixedRate=60000)")
|
|
|
+ void receiptTimeoutTask_hasScheduledEntry() {
|
|
|
+ Class<?> clazz = loadClass(RECEIPT_TIMEOUT_TASK);
|
|
|
+ if (clazz == null) {
|
|
|
+ fail("[shop.order.receiptTimeout] OrderReceiptTimeoutTask 类不存在,无法验证 processTimeoutOrders 入口(架构设计 §6 类图)。");
|
|
|
+ }
|
|
|
+ Method entry = findDeclaredMethod(clazz, "processTimeoutOrders");
|
|
|
+ if (entry == null) {
|
|
|
+ fail("[shop.order.receiptTimeout] OrderReceiptTimeoutTask 应提供 processTimeoutOrders() 公共入口"
|
|
|
+ + "(对应 OrderTimeoutCancelTask.cancelTimeoutOrders,架构设计 §6 类图)。");
|
|
|
+ }
|
|
|
+ if (!Modifier.isPublic(entry.getModifiers())) {
|
|
|
+ fail("[shop.order.receiptTimeout] processTimeoutOrders() 应为 public(定时任务框架调用入口)。");
|
|
|
+ }
|
|
|
+ boolean hasScheduled = false;
|
|
|
+ for (java.lang.annotation.Annotation a : entry.getAnnotations()) {
|
|
|
+ if (a.annotationType().equals(Scheduled.class)) {
|
|
|
+ hasScheduled = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ assertTrue(hasScheduled,
|
|
|
+ "[shop.order.receiptTimeout] processTimeoutOrders() 应标注 @Scheduled(fixedRate=60000)"
|
|
|
+ + "(每分钟扫描,架构设计 §3.3)。");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("正常流-4:processTimeoutOrders 应标注 @Transactional(批量置态原子性)")
|
|
|
+ void receiptTimeoutTask_entryIsTransactional() {
|
|
|
+ Class<?> clazz = loadClass(RECEIPT_TIMEOUT_TASK);
|
|
|
+ if (clazz == null) {
|
|
|
+ fail("[shop.order.receiptTimeout] OrderReceiptTimeoutTask 类不存在,无法验证 @Transactional(架构设计 §3.3)。");
|
|
|
+ }
|
|
|
+ Method entry = findDeclaredMethod(clazz, "processTimeoutOrders");
|
|
|
+ if (entry == null) {
|
|
|
+ fail("[shop.order.receiptTimeout] processTimeoutOrders() 入口缺失,无法验证 @Transactional(架构设计 §6 类图)。");
|
|
|
+ }
|
|
|
+ boolean hasTx = false;
|
|
|
+ for (java.lang.annotation.Annotation a : entry.getAnnotations()) {
|
|
|
+ if (a.annotationType().equals(Transactional.class)) {
|
|
|
+ hasTx = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ assertTrue(hasTx,
|
|
|
+ "[shop.order.receiptTimeout] processTimeoutOrders() 应标注 @Transactional"
|
|
|
+ + "(仿 OrderTimeoutCancelTask,批量置 completed 需原子性,架构设计 §3.3)。");
|
|
|
+ }
|
|
|
+
|
|
|
+ // ------------------------------------------------------------------
|
|
|
+ // 正常流:设计契约方法签名(类图)
|
|
|
+ // ------------------------------------------------------------------
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("正常流-5:应含 autoComplete / sendRemind / resolveShipTimeBase 方法(架构设计 §6 类图)")
|
|
|
+ void receiptTimeoutTask_hasDesignMethods() {
|
|
|
+ Class<?> clazz = loadClass(RECEIPT_TIMEOUT_TASK);
|
|
|
+ if (clazz == null) {
|
|
|
+ fail("[shop.order.receiptTimeout] OrderReceiptTimeoutTask 类不存在,无法验证类图方法签名(架构设计 §6)。");
|
|
|
+ }
|
|
|
+ assertNotNull(findDeclaredMethod(clazz, "autoComplete"),
|
|
|
+ "[shop.order.receiptTimeout] OrderReceiptTimeoutTask 应含 autoComplete():"
|
|
|
+ + "扫描 pending_receipt 且超阈值订单自动置 completed(架构设计 §3.3/§6 类图)。");
|
|
|
+ assertNotNull(findDeclaredMethod(clazz, "sendRemind"),
|
|
|
+ "[shop.order.receiptTimeout] OrderReceiptTimeoutTask 应含 sendRemind():"
|
|
|
+ + "第9天对 shipped_at 非空且 receipt_remind_at 为空的订单推送订阅消息(架构设计 §3.3/§6 类图)。");
|
|
|
+ assertNotNull(findDeclaredMethod(clazz, "resolveShipTimeBase", ProductOrder.class),
|
|
|
+ "[shop.order.receiptTimeout] OrderReceiptTimeoutTask 应含 resolveShipTimeBase(ProductOrder):"
|
|
|
+ + "以 COALESCE(shipped_at, updated_at) 作为发货时间基准兜底(架构设计 §2.2/§6 类图)。");
|
|
|
+ }
|
|
|
+
|
|
|
+ // ------------------------------------------------------------------
|
|
|
+ // 边界流:自动完成可追溯日志(PRD 验收①)
|
|
|
+ // ------------------------------------------------------------------
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("边界流-1:自动完成应产生可追溯记录(user_operation_logs type=product_order_auto_complete)")
|
|
|
+ void receiptTimeoutTask_autoCompleteWritesTraceableLog() {
|
|
|
+ // 结构探测:任务类必须存在(日志写入位于自动完成分支)。
|
|
|
+ // 行为级(mock orderMapper / operationLogMapper 断言写库)在 GREEN 阶段升级。
|
|
|
+ Class<?> clazz = loadClass(RECEIPT_TIMEOUT_TASK);
|
|
|
+ assertNotNull(clazz,
|
|
|
+ "[shop.order.receiptTimeout] OrderReceiptTimeoutTask 类不存在,"
|
|
|
+ + "无法验证自动完成可追溯日志(PRD §3 P0-B 验收①)。");
|
|
|
+ Method autoComplete = findDeclaredMethod(clazz, "autoComplete");
|
|
|
+ assertNotNull(autoComplete,
|
|
|
+ "[shop.order.receiptTimeout] autoComplete() 缺失:自动完成应写 user_operation_logs"
|
|
|
+ + "(type=" + AUTO_COMPLETE_LOG_TYPE + ",含 orderNo/原因)并置 confirmTime=now(架构设计 §3.3)。");
|
|
|
+ }
|
|
|
+
|
|
|
+ // ------------------------------------------------------------------
|
|
|
+ // 边界流:幂等(PRD 验收② —— 重复执行不重复完成)
|
|
|
+ // ------------------------------------------------------------------
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("边界流-2:自动完成应幂等(completed 不再被扫中,重复执行不重复完成)")
|
|
|
+ void receiptTimeoutTask_autoCompleteIsIdempotent() {
|
|
|
+ Class<?> clazz = loadClass(RECEIPT_TIMEOUT_TASK);
|
|
|
+ assertNotNull(clazz,
|
|
|
+ "[shop.order.receiptTimeout] OrderReceiptTimeoutTask 类不存在,"
|
|
|
+ + "无法验证幂等性(PRD §3 P0-B 验收②)。");
|
|
|
+ // 幂等实现载体:自动完成扫描条件仅命中 pending_receipt(completed 被排除)。
|
|
|
+ // 结构探测确认任务类具备 pending_receipt 触发状态语义 —— 通过 autoComplete 方法存在 + 类含订单 mapper 引用间接验证。
|
|
|
+ assertNotNull(findDeclaredMethod(clazz, "autoComplete"),
|
|
|
+ "[shop.order.receiptTimeout] autoComplete() 缺失:"
|
|
|
+ + "幂等要求扫描条件仅命中 status=pending_receipt(" + STATUS_PENDING_RECEIPT
|
|
|
+ + "),completed 订单不再被扫中(架构设计 §3.3 状态幂等)。");
|
|
|
+ }
|
|
|
+
|
|
|
+ // ------------------------------------------------------------------
|
|
|
+ // 边界流:第9天提醒幂等 + 总开关(架构设计 §3.3 / §2.4)
|
|
|
+ // ------------------------------------------------------------------
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("边界流-3:第9天提醒应幂等(receipt_remind_at 非空不再推送,失败仅日志下一轮再试)")
|
|
|
+ void receiptTimeoutTask_remindIsIdempotent() {
|
|
|
+ Class<?> clazz = loadClass(RECEIPT_TIMEOUT_TASK);
|
|
|
+ assertNotNull(clazz,
|
|
|
+ "[shop.order.receiptTimeout] OrderReceiptTimeoutTask 类不存在,"
|
|
|
+ + "无法验证第9天提醒幂等(架构设计 §3.3)。");
|
|
|
+ Method sendRemind = findDeclaredMethod(clazz, "sendRemind");
|
|
|
+ assertNotNull(sendRemind,
|
|
|
+ "[shop.order.receiptTimeout] sendRemind() 缺失:"
|
|
|
+ + "第9天提醒应仅对 shipped_at IS NOT NULL 且 receipt_remind_at IS NULL 的订单推送,"
|
|
|
+ + "成功后写 receipt_remind_at=now(幂等防重复推送,架构设计 §3.3/§2.2)。");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("边界流-4:总开关应支持灰度(receipt-auto-complete-enabled=false 仅提醒不自动完成)")
|
|
|
+ void receiptTimeoutTask_supportsMasterSwitch() {
|
|
|
+ // 总开关数据契约:application.yml 必须含 receipt-auto-complete-enabled 配置键。
|
|
|
+ String yml = readResource("application.yml");
|
|
|
+ assertTrue(yml.contains("receipt-auto-complete-enabled"),
|
|
|
+ "[shop.order.receiptTimeout] application.yml 缺少 receipt-auto-complete-enabled:"
|
|
|
+ + "总开关(灰度/回滚用),false 时仅提醒不自动完成(架构设计 §2.4)。");
|
|
|
+ }
|
|
|
+
|
|
|
+ // ------------------------------------------------------------------
|
|
|
+ // 数据契约:配置键 / 实体字段 / schema 列(架构设计 §2.2/§2.4)
|
|
|
+ // ------------------------------------------------------------------
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("数据契约-1:application.yml 应含 4 个超时/提醒配置键(§2.4)")
|
|
|
+ void applicationYml_hasReceiptTimeoutConfigKeys() {
|
|
|
+ String yml = readResource("application.yml");
|
|
|
+ assertTrue(yml.contains("receipt-timeout-days"),
|
|
|
+ "[shop.order.receiptTimeout] application.yml 缺少 receipt-timeout-days(默认10天,§2.4)。");
|
|
|
+ assertTrue(yml.contains("receipt-remind-days"),
|
|
|
+ "[shop.order.receiptTimeout] application.yml 缺少 receipt-remind-days(默认第9天提醒,§2.4)。");
|
|
|
+ assertTrue(yml.contains("receipt-remind-template-id"),
|
|
|
+ "[shop.order.receiptTimeout] application.yml 缺少 receipt-remind-template-id(模板ID,空=跳过推送 fail-open,§2.4)。");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("数据契约-2:ProductOrder 实体应含 shippedAt / receiptRemindAt 字段(§2.2)")
|
|
|
+ void productOrder_hasShippedAndRemindFields() {
|
|
|
+ Class<?> entity = loadClass("com.etotem.cfc.entity.ProductOrder");
|
|
|
+ assertNotNull(entity,
|
|
|
+ "[shop.order.receiptTimeout] ProductOrder 实体不存在(数据底座 §2.2)。");
|
|
|
+ assertTrue(hasField(entity, "shippedAt"),
|
|
|
+ "[shop.order.receiptTimeout] ProductOrder 应含 shippedAt 字段:"
|
|
|
+ + "发货时间(超时自动完成/收货提醒的基准时间,§2.2)。");
|
|
|
+ assertTrue(hasField(entity, "receiptRemindAt"),
|
|
|
+ "[shop.order.receiptTimeout] ProductOrder 应含 receiptRemindAt 字段:"
|
|
|
+ + "收货提醒推送时间(第9天推送成功后写,幂等防重复,§2.2)。");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("数据契约-3:schema.sql 应含 shipped_at / receipt_remind_at 列(§2.2 迁移465)")
|
|
|
+ void schemaSql_hasShippedAndRemindColumns() {
|
|
|
+ String schema = readResource("schema.sql");
|
|
|
+ assertTrue(schema.contains("shipped_at"),
|
|
|
+ "[shop.order.receiptTimeout] schema.sql 缺少 product_orders.shipped_at 列(§2.2 迁移465)。");
|
|
|
+ assertTrue(schema.contains("receipt_remind_at"),
|
|
|
+ "[shop.order.receiptTimeout] schema.sql 缺少 product_orders.receipt_remind_at 列(§2.2 迁移465)。");
|
|
|
+ }
|
|
|
+
|
|
|
+ // ------------------------------------------------------------------
|
|
|
+ // 权限流(不适用):后端内部定时任务,无用户权限维度
|
|
|
+ // ------------------------------------------------------------------
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("权限流:本能力为后端内部定时任务,无用户权限维度,权限流不适用")
|
|
|
+ void permissionFlow_notApplicable() {
|
|
|
+ // 说明性用例:契约 AC 为「定时任务扫描 pending_receipt → 自动 completed + 提醒」内部链路,
|
|
|
+ // 无用户请求上下文与权限校验维度。不产生断言失败;仅为三流覆盖标注「不适用」。
|
|
|
+ assertTrue(true, "权限流不适用:shop.order.receiptTimeout 为后端内部定时任务,无用户权限维度。");
|
|
|
+ }
|
|
|
+
|
|
|
+ // ------------------------------------------------------------------
|
|
|
+ // 反射辅助
|
|
|
+ // ------------------------------------------------------------------
|
|
|
+
|
|
|
+ private Class<?> loadClass(String name) {
|
|
|
+ try {
|
|
|
+ return Class.forName(name, false, Thread.currentThread().getContextClassLoader());
|
|
|
+ } catch (ClassNotFoundException e) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private Method findDeclaredMethod(Class<?> clazz, String name, Class<?>... paramTypes) {
|
|
|
+ try {
|
|
|
+ return clazz.getDeclaredMethod(name, paramTypes);
|
|
|
+ } catch (NoSuchMethodException e) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean hasField(Class<?> clazz, String fieldName) {
|
|
|
+ for (Field f : clazz.getDeclaredFields()) {
|
|
|
+ if (f.getName().equals(fieldName)) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String readResource(String resource) {
|
|
|
+ try (InputStream is = getClass().getClassLoader().getResourceAsStream(resource)) {
|
|
|
+ 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 "";
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|