|
|
@@ -0,0 +1,405 @@
|
|
|
+package com.etotem.cfc.unit;
|
|
|
+
|
|
|
+import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
|
|
+import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
|
|
+import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
|
|
+import com.etotem.cfc.common.Result;
|
|
|
+import com.etotem.cfc.entity.Activity;
|
|
|
+import com.etotem.cfc.entity.ActivityOrder;
|
|
|
+import com.etotem.cfc.entity.ActivityRegistration;
|
|
|
+import com.etotem.cfc.mapper.ActivityMapper;
|
|
|
+import com.etotem.cfc.mapper.ActivityOrderMapper;
|
|
|
+import com.etotem.cfc.mapper.ActivityRegistrationMapper;
|
|
|
+import com.etotem.cfc.service.ActivityRegistrationService;
|
|
|
+import com.etotem.cfc.service.PaymentService;
|
|
|
+import org.apache.ibatis.builder.MapperBuilderAssistant;
|
|
|
+import org.junit.jupiter.api.BeforeEach;
|
|
|
+import org.junit.jupiter.api.DisplayName;
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
+import org.mockito.InjectMocks;
|
|
|
+import org.mockito.Mock;
|
|
|
+import org.mockito.MockitoAnnotations;
|
|
|
+
|
|
|
+import java.io.InputStream;
|
|
|
+import java.lang.reflect.Field;
|
|
|
+import java.lang.reflect.Method;
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
+import java.util.Date;
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.Map;
|
|
|
+import java.util.Scanner;
|
|
|
+import java.util.Set;
|
|
|
+import java.util.concurrent.ConcurrentHashMap;
|
|
|
+
|
|
|
+import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertNotNull;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
|
+import static org.mockito.ArgumentMatchers.any;
|
|
|
+import static org.mockito.ArgumentMatchers.isNull;
|
|
|
+import static org.mockito.Mockito.never;
|
|
|
+import static org.mockito.Mockito.verify;
|
|
|
+import static org.mockito.Mockito.when;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 契约级单测:服务商营销活动报名(能力键 {@code supplier.activity.attend})。
|
|
|
+ *
|
|
|
+ * <p><b>契约溯源</b></p>
|
|
|
+ * <ul>
|
|
|
+ * <li>需求:{@code tests/user-scenarios/应用场景总览.md} SC-S-07「营销活动报名」——
|
|
|
+ * <b>主流程</b>:报名活动 → 商品打标;<b>边界</b>:活动失效不可报;模块:活动管理。</li>
|
|
|
+ * <li>用例:{@code tests/test-cases/vendor.md} TC-S-07(正常流:报名活动→商品打标,报名后商品出现活动标)/
|
|
|
+ * TC-S-07-E(异常流:① 活动已失效仍报名 → 失效活动报名拒;② 重复报名幂等;③ 报名参数非法 → 400)/
|
|
|
+ * TC-S-07-P(权限流:跨商户数据隔离;核销码校验防冒用)。</li>
|
|
|
+ * <li>设计:{@code docs/superpowers/api/API_REFERENCE.md} §4.13 管理员接口 ——
|
|
|
+ * {@code AdminActivityRegistrationController /api/admin/activity}(报名管理)+
|
|
|
+ * {@code AdminActivityCheckinController /api/admin/activity-checkins}(核销);
|
|
|
+ * §4.55.1 —— {@code POST /api/activity/register}(活动报名)。</li>
|
|
|
+ * </ul>
|
|
|
+ *
|
|
|
+ * <p><b>基线声明(2026-11-13 cfclub)</b>:SC-S-07 的「端点载体」已落地——服务商契约核对 spec
|
|
|
+ * ({@code docs/superpowers/specs/2026-11-07-service-provider-contract.md} §1.3,建议键
|
|
|
+ * {@code supplier.activity.signup})结论为「端点齐全:ActivityController 13 端点 +
|
|
|
+ * AdminActivityController 8 端点 + ActivityOrderController 5 端点」。本能力键
|
|
|
+ * {@code supplier.activity.attend} 为<b>契约新增行为</b>(超出端点核对层面),断言 SC-S-07
|
|
|
+ * 的行为契约,当前实现存在三处<b>契约缺口(本类 RED 断言)</b>:</p>
|
|
|
+ * <ol>
|
|
|
+ * <li><b>商品打标缺失</b>(TC-S-07 主流程):报名成功后不产生任何「活动↔商品」关联——
|
|
|
+ * schema.sql 无 {@code activity_products} 关联表(0 命中),{@code ActivityRegistrationService}
|
|
|
+ * 无打标位点(方法/字段 0 命中 tag/product),服务商报名活动后其商品不会出现活动标;</li>
|
|
|
+ * <li><b>活动失效(运行期)不可报缺失</b>(TC-S-07-E-①):{@code ActivityRegistrationService.register}
|
|
|
+ * 仅校验 {@code status=published} 与报名截止时间,<b>不校验活动运行期窗口</b>——
|
|
|
+ * published 活动 {@code end_time} 已过(活动已失效)仍可报名成功;</li>
|
|
|
+ * <li><b>服务商维度缺失</b>(TC-S-07-P):{@code activity_registrations} / {@code ActivityRegistration}
|
|
|
+ * 无 {@code vendorId} 等服务商维度字段,报名链路无跨商户归属校验位点,无法支撑跨商户数据隔离。</li>
|
|
|
+ * </ol>
|
|
|
+ *
|
|
|
+ * <p><b>测试策略</b>:与同批 RED 契约测试({@code TeamProfitHookContractTest} /
|
|
|
+ * {@code TeamStarMultiplierContractTest} / {@code FamilyJoinValidationRulesContractTest},纯 Mockito/
|
|
|
+ * 反射、无 Spring/DB/外部服务)同风格。绿锚点(已落地载体,防误伤)与红断言(契约缺口)并存:
|
|
|
+ * RED 断言在实现补齐前失败(红),GREEN 阶段实现使绿。</p>
|
|
|
+ *
|
|
|
+ * <p><b>GREEN 升级指引</b>:① 新增 {@code activity_products}(活动-商品关联表)并在报名成功链路写入
|
|
|
+ * 关联(商品打标载体);② {@code ActivityRegistrationService.register} 增加运行期校验:活动
|
|
|
+ * {@code end_time} 已过(运行期失效)拒绝报名;③ 报名/打标链路增加服务商 {@code vendorId} 维度与
|
|
|
+ * 跨商户归属校验。完成后本类应全绿。</p>
|
|
|
+ *
|
|
|
+ * <p><b>RED 预期</b>:正常流-红 2 例(商品打标载体/位点缺失)、边界流-红 1 例(运行期失效放行)、
|
|
|
+ * 权限流-红 2 例(服务商维度缺失)在实现落地前失败(红);绿锚 4 例锚定已落地载体
|
|
|
+ * (报名端点/报名管理/核销/状态维度失效拦截)。</p>
|
|
|
+ */
|
|
|
+class SupplierActivityAttendContractTest {
|
|
|
+
|
|
|
+ private static final String REGISTRATION_SERVICE = "com.etotem.cfc.service.ActivityRegistrationService";
|
|
|
+ private static final String ACTIVITY_REGISTRATION_ENTITY = "com.etotem.cfc.entity.ActivityRegistration";
|
|
|
+
|
|
|
+ /** 契约要求的「活动-商品」关联表(报名 → 商品打标载体,SC-S-07 主流程) */
|
|
|
+ private static final String ACTIVITY_PRODUCT_TABLE = "activity_products";
|
|
|
+
|
|
|
+ private static final Long ACTIVITY_ID = 1L;
|
|
|
+ private static final Long USER_ID = 200L;
|
|
|
+ private static final Long MEMBER_ID = 300L;
|
|
|
+
|
|
|
+ private static final Set<Class<?>> INITIALIZED = ConcurrentHashMap.newKeySet();
|
|
|
+
|
|
|
+ @Mock
|
|
|
+ private ActivityRegistrationMapper registrationMapper;
|
|
|
+ @Mock
|
|
|
+ private ActivityMapper activityMapper;
|
|
|
+ @Mock
|
|
|
+ private ActivityOrderMapper orderMapper;
|
|
|
+ @Mock
|
|
|
+ private PaymentService paymentService;
|
|
|
+
|
|
|
+ @InjectMocks
|
|
|
+ private ActivityRegistrationService registrationService;
|
|
|
+
|
|
|
+ @BeforeEach
|
|
|
+ void setUp() {
|
|
|
+ MockitoAnnotations.openMocks(this);
|
|
|
+ // 纯 Mockito 下 LambdaQueryWrapper 解析列名需要 MP TableInfo 预注册(同 ChildShopPointsExchangeContractTest 约定)
|
|
|
+ initTable(Activity.class);
|
|
|
+ initTable(ActivityRegistration.class);
|
|
|
+ initTable(ActivityOrder.class);
|
|
|
+ // 名额充足:occupyeQuota 的原子自增恒成功(活动失效校验若存在应发生在名额占用之前)
|
|
|
+ when(activityMapper.update(isNull(), any(Wrapper.class))).thenReturn(1);
|
|
|
+ }
|
|
|
+
|
|
|
+ // ======================================================================
|
|
|
+ // 正常流(绿锚):报名端点已落地 —— §4.55.1 POST /api/activity/register
|
|
|
+ // ======================================================================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("正常流锚-1:POST /api/activity/register 报名端点已落地(ActivityController)")
|
|
|
+ void activityController_registerEndpointExists() {
|
|
|
+ Class<?> ctrl = loadClass("com.etotem.cfc.controller.ActivityController");
|
|
|
+ assertNotNull(ctrl, "[supplier.activity.attend] ActivityController 不存在(§4.55.1)。");
|
|
|
+ assertEquals("/api/activity", resolveRequestMapping(ctrl),
|
|
|
+ "[supplier.activity.attend] ActivityController 类映射应为 /api/activity(§4.55.1)。");
|
|
|
+ boolean hasRegister = false;
|
|
|
+ for (Method m : ctrl.getDeclaredMethods()) {
|
|
|
+ Map<String, String> mapping = resolveMappingPath(m);
|
|
|
+ if ("/register".equals(mapping.get("path")) && "POST".equals(mapping.get("httpMethod"))) {
|
|
|
+ hasRegister = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ assertTrue(hasRegister,
|
|
|
+ "[supplier.activity.attend] ActivityController 应映射 POST /api/activity/register"
|
|
|
+ + "(活动报名,§4.55.1)。端点载体已落地,本例为绿锚。");
|
|
|
+ }
|
|
|
+
|
|
|
+ // ======================================================================
|
|
|
+ // 正常流(红):报名活动 → 商品打标 —— 打标载体/位点缺失
|
|
|
+ // ======================================================================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("正常流红-1:schema.sql 缺少 activity_products 关联表(报名→商品打标载体 → 红)")
|
|
|
+ void schemaSql_missingActivityProductLinkage() {
|
|
|
+ String schema = readSchemaSql();
|
|
|
+ assertTrue(schema.contains(ACTIVITY_PRODUCT_TABLE),
|
|
|
+ "[supplier.activity.attend] schema.sql 缺少 " + ACTIVITY_PRODUCT_TABLE
|
|
|
+ + " 关联表:SC-S-07 主流程「报名活动 → 商品打标」需要活动↔商品关联载体,"
|
|
|
+ + "否则服务商报名后其商品不会出现活动标(TC-S-07)。实现未落地。");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("正常流红-2:报名服务缺少商品打标位点(方法/字段无 tag/product → 红)")
|
|
|
+ void registrationService_missingProductTaggingSite() {
|
|
|
+ Class<?> clazz = loadClass(REGISTRATION_SERVICE);
|
|
|
+ assertNotNull(clazz, "[supplier.activity.attend] ActivityRegistrationService 不存在。");
|
|
|
+ boolean hasTaggingSite = false;
|
|
|
+ for (Method m : clazz.getDeclaredMethods()) {
|
|
|
+ String name = m.getName().toLowerCase();
|
|
|
+ if (name.contains("tag") || name.contains("product") || name.contains("attend")) {
|
|
|
+ hasTaggingSite = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ for (Field f : clazz.getDeclaredFields()) {
|
|
|
+ String name = f.getName().toLowerCase();
|
|
|
+ if (name.contains("tag") || name.contains("product")) {
|
|
|
+ hasTaggingSite = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ assertTrue(hasTaggingSite,
|
|
|
+ "[supplier.activity.attend] ActivityRegistrationService 应提供商品打标位点"
|
|
|
+ + "(方法/字段含 tag/product/attend):报名成功后应将活动标写入服务商商品"
|
|
|
+ + "(活动↔商品关联),支撑「报名活动 → 商品打标」(SC-S-07 主流程 / TC-S-07)。实现未落地。");
|
|
|
+ }
|
|
|
+
|
|
|
+ // ======================================================================
|
|
|
+ // 边界流(红):活动失效不可报 —— 运行期(end_time)失效校验缺失
|
|
|
+ // ======================================================================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("边界流红-1:published 活动 end_time 已过(活动已失效)仍报名成功 → 契约要求拒绝(红)")
|
|
|
+ void expiredActivity_registerShouldBeRejected() {
|
|
|
+ Activity expired = baseActivity();
|
|
|
+ // 运行期失效:活动已结束(end_time 早于当下),但状态仍 published、无报名截止限制
|
|
|
+ expired.setEndTime(new Date(System.currentTimeMillis() - 86_400_000L));
|
|
|
+ when(activityMapper.selectById(ACTIVITY_ID)).thenReturn(expired);
|
|
|
+
|
|
|
+ Result<Map<String, Object>> result = registrationService.register(ACTIVITY_ID, USER_ID, MEMBER_ID);
|
|
|
+
|
|
|
+ assertTrue(result.getCode() != 200 && result.getMessage() != null && result.getMessage().contains("失效"),
|
|
|
+ "[supplier.activity.attend] published 活动 end_time 已过(活动已失效)仍报名成功:"
|
|
|
+ + "code=" + result.getCode() + ", msg=" + result.getMessage()
|
|
|
+ + "。契约边界「活动失效不可报」(SC-S-07 / TC-S-07-E-①)"
|
|
|
+ + "要求运行期失效活动拒绝报名(当前仅校验 status 与报名截止,缺 end_time 运行期校验)。");
|
|
|
+ verify(registrationMapper, never()).insert(any(ActivityRegistration.class));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("边界锚-1:status=ended(状态维度失效)→ register 拒绝「活动未发布」(已拦截,绿锚)")
|
|
|
+ void statusEndedActivity_registerRejected() {
|
|
|
+ Activity ended = baseActivity();
|
|
|
+ ended.setStatus("ended");
|
|
|
+ when(activityMapper.selectById(ACTIVITY_ID)).thenReturn(ended);
|
|
|
+
|
|
|
+ Result<Map<String, Object>> result = registrationService.register(ACTIVITY_ID, USER_ID, MEMBER_ID);
|
|
|
+
|
|
|
+ assertEquals(500, result.getCode());
|
|
|
+ assertTrue(result.getMessage() != null && result.getMessage().contains("未发布"),
|
|
|
+ "[supplier.activity.attend] 状态维度失效(status=ended)应已拦截,实际 msg=" + result.getMessage());
|
|
|
+ verify(registrationMapper, never()).insert(any(ActivityRegistration.class));
|
|
|
+ }
|
|
|
+
|
|
|
+ // ======================================================================
|
|
|
+ // 权限流(红):TC-S-07-P 跨商户数据隔离 —— 服务商维度缺失
|
|
|
+ // ======================================================================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("权限流红-1:报名记录应承载服务商 vendorId 维度(当前缺失 → 红)")
|
|
|
+ void activityRegistration_missingVendorDimension() {
|
|
|
+ Class<?> clazz = loadClass(ACTIVITY_REGISTRATION_ENTITY);
|
|
|
+ assertNotNull(clazz, "[supplier.activity.attend] ActivityRegistration 实体不存在。");
|
|
|
+ boolean hasVendor = false;
|
|
|
+ for (Field f : clazz.getDeclaredFields()) {
|
|
|
+ String name = f.getName();
|
|
|
+ if ("vendorId".equals(name) || "supplierId".equals(name)) {
|
|
|
+ hasVendor = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ assertTrue(hasVendor,
|
|
|
+ "[supplier.activity.attend] ActivityRegistration 应含 vendorId/supplierId 字段:"
|
|
|
+ + "服务商报名营销活动须以服务商维度落库,跨商户数据隔离(TC-S-07-P)"
|
|
|
+ + "才有判定依据。实现未落地。");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("权限流红-2:报名链路缺少跨商户(服务商归属)校验位点(当前无 → 红)")
|
|
|
+ void registrationService_missingVendorIsolationCheck() {
|
|
|
+ Class<?> clazz = loadClass(REGISTRATION_SERVICE);
|
|
|
+ assertNotNull(clazz, "[supplier.activity.attend] ActivityRegistrationService 不存在。");
|
|
|
+ boolean hasIsolationSite = false;
|
|
|
+ for (Method m : clazz.getDeclaredMethods()) {
|
|
|
+ String name = m.getName().toLowerCase();
|
|
|
+ if (name.contains("vendor") || name.contains("supplier")
|
|
|
+ || name.contains("merchant") || name.contains("shop")) {
|
|
|
+ hasIsolationSite = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ for (Field f : clazz.getDeclaredFields()) {
|
|
|
+ String name = f.getName().toLowerCase();
|
|
|
+ if (name.contains("vendor") || name.contains("supplier")
|
|
|
+ || name.contains("merchant") || name.contains("shop")) {
|
|
|
+ hasIsolationSite = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ assertTrue(hasIsolationSite,
|
|
|
+ "[supplier.activity.attend] ActivityRegistrationService 应提供服务商归属校验位点"
|
|
|
+ + "(vendor/supplier/merchant):服务商报名/打标须限定本商户商品,"
|
|
|
+ + "跨商户数据隔离(TC-S-07-P)必须由服务端校验承载,不能依赖前端约束。实现未落地。");
|
|
|
+ }
|
|
|
+
|
|
|
+ // ======================================================================
|
|
|
+ // 权限流(绿锚):管理端报名管理 + 核销已落地 —— §4.13
|
|
|
+ // ======================================================================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("权限流锚-1:AdminActivityRegistrationController 报名管理三点 POST 已落地(§4.13)")
|
|
|
+ void adminActivityRegistration_adminManageExists() {
|
|
|
+ Class<?> ctrl = loadClass("com.etotem.cfc.controller.admin.AdminActivityRegistrationController");
|
|
|
+ assertNotNull(ctrl, "[supplier.activity.attend] AdminActivityRegistrationController 不存在(§4.13 报名管理)。");
|
|
|
+ assertEquals("/api/admin/activity", resolveRequestMapping(ctrl),
|
|
|
+ "[supplier.activity.attend] AdminActivityRegistrationController 类映射应为 /api/admin/activity(§4.13)。");
|
|
|
+ checkPostEndpoints(ctrl, "/registration/list", "/registration/approve", "/registration/reject");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("权限流锚-2:AdminActivityCheckinController 核销/补录/审核已落地(§4.13)")
|
|
|
+ void adminActivityCheckin_checkinManageExists() {
|
|
|
+ Class<?> ctrl = loadClass("com.etotem.cfc.controller.admin.AdminActivityCheckinController");
|
|
|
+ assertNotNull(ctrl, "[supplier.activity.attend] AdminActivityCheckinController 不存在(§4.13 核销)。");
|
|
|
+ assertEquals("/api/admin/activity-checkins", resolveRequestMapping(ctrl),
|
|
|
+ "[supplier.activity.attend] AdminActivityCheckinController 类映射应为 /api/admin/activity-checkins(§4.13)。");
|
|
|
+ checkPostEndpoints(ctrl, "/list", "/add", "/review");
|
|
|
+ }
|
|
|
+
|
|
|
+ // ======================================================================
|
|
|
+ // 辅助
|
|
|
+ // ======================================================================
|
|
|
+
|
|
|
+ /** 免审、无报名截止、免费活动的契约基线:聚焦「活动失效(运行期)校验」这一缺口 */
|
|
|
+ private Activity baseActivity() {
|
|
|
+ Activity a = new Activity();
|
|
|
+ a.setId(ACTIVITY_ID);
|
|
|
+ a.setTitle("契约测试营销活动");
|
|
|
+ a.setStatus("published");
|
|
|
+ a.setRequireRegistration(1);
|
|
|
+ a.setRequireReview(0);
|
|
|
+ a.setRegistrationDeadline(null);
|
|
|
+ return a;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void checkPostEndpoints(Class<?> ctrl, String... expectedPaths) {
|
|
|
+ Set<String> methodPaths = new java.util.HashSet<>();
|
|
|
+ for (Method m : ctrl.getDeclaredMethods()) {
|
|
|
+ Map<String, String> mapping = resolveMappingPath(m);
|
|
|
+ if ("POST".equals(mapping.get("httpMethod")) && mapping.get("path") != null) {
|
|
|
+ methodPaths.add(mapping.get("path"));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ for (String expected : expectedPaths) {
|
|
|
+ assertTrue(methodPaths.contains(expected),
|
|
|
+ "[supplier.activity.attend] " + ctrl.getSimpleName() + " 缺少 POST 端点 "
|
|
|
+ + expected + "(§4.13 报名管理/核销)。");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 为实体类预注册 MyBatis-Plus TableInfo(幂等,每类仅一次,仿 ChildShopPointsExchangeContractTest) */
|
|
|
+ private static void initTable(Class<?> clazz) {
|
|
|
+ if (INITIALIZED.add(clazz)) {
|
|
|
+ TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), clazz);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private Class<?> loadClass(String name) {
|
|
|
+ try {
|
|
|
+ return Class.forName(name, false, Thread.currentThread().getContextClassLoader());
|
|
|
+ } catch (ClassNotFoundException e) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 读取类级 @RequestMapping 的 path 值 */
|
|
|
+ private String resolveRequestMapping(Class<?> clazz) {
|
|
|
+ if (clazz.isAnnotationPresent(org.springframework.web.bind.annotation.RequestMapping.class)) {
|
|
|
+ org.springframework.web.bind.annotation.RequestMapping rm =
|
|
|
+ clazz.getAnnotation(org.springframework.web.bind.annotation.RequestMapping.class);
|
|
|
+ String[] value = rm.value();
|
|
|
+ if (value != null && value.length > 0) {
|
|
|
+ return value[0];
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 读取方法级映射 path + http 方法(@PostMapping/@GetMapping/@RequestMapping) */
|
|
|
+ private Map<String, String> resolveMappingPath(Method m) {
|
|
|
+ Map<String, String> result = new HashMap<>();
|
|
|
+ org.springframework.web.bind.annotation.PostMapping pm =
|
|
|
+ m.getAnnotation(org.springframework.web.bind.annotation.PostMapping.class);
|
|
|
+ if (pm != null) {
|
|
|
+ String[] value = pm.value();
|
|
|
+ result.put("path", (value != null && value.length > 0) ? value[0] : "");
|
|
|
+ result.put("httpMethod", "POST");
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+ org.springframework.web.bind.annotation.GetMapping gm =
|
|
|
+ m.getAnnotation(org.springframework.web.bind.annotation.GetMapping.class);
|
|
|
+ if (gm != null) {
|
|
|
+ String[] value = gm.value();
|
|
|
+ result.put("path", (value != null && value.length > 0) ? value[0] : "");
|
|
|
+ result.put("httpMethod", "GET");
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+ org.springframework.web.bind.annotation.RequestMapping rm =
|
|
|
+ m.getAnnotation(org.springframework.web.bind.annotation.RequestMapping.class);
|
|
|
+ if (rm != null) {
|
|
|
+ String[] value = rm.value();
|
|
|
+ result.put("path", (value != null && value.length > 0) ? value[0] : "");
|
|
|
+ result.put("httpMethod", "REQUEST");
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ 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 "";
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|