diff --git a/src/main/java/com/hospital/front/api/admin/AdminApi.java b/src/main/java/com/hospital/front/api/admin/AdminApi.java index 59fd32e..407c85e 100644 --- a/src/main/java/com/hospital/front/api/admin/AdminApi.java +++ b/src/main/java/com/hospital/front/api/admin/AdminApi.java @@ -273,4 +273,20 @@ public class AdminApi { t.start(); return Result.success(); } + + // ================= 5. 接口清单(供 B 端接口测试工具加载) ================= + + /** + * 返回 /order 业务接口元数据清单。 + * + * 反射扫描 OrderApi 的 @ApiMeta 注解自动生成,无需手写维护。 + * B 端因前置机白名单无法直连,通过 Agent 通道拉取本接口清单, + * 再经 Agent HTTP_PROXY 代理调用具体接口。 + * 走 X-Admin-Token 鉴权(与其它管理接口一致)。 + */ + @Mapping("/apis") + public Result>> apis(Context ctx) { + checkToken(ctx); + return Result.success(com.hospital.front.infra.ApiScanner.scan(com.hospital.front.api.order.OrderApi.class)); + } } diff --git a/src/main/java/com/hospital/front/api/order/OrderApi.java b/src/main/java/com/hospital/front/api/order/OrderApi.java index 2f9ca2d..62a0cb1 100644 --- a/src/main/java/com/hospital/front/api/order/OrderApi.java +++ b/src/main/java/com/hospital/front/api/order/OrderApi.java @@ -17,6 +17,8 @@ import org.noear.solon.Solon; import org.noear.snack.ONode; import com.hospital.front.dto.Result; +import com.hospital.front.infra.annotation.ApiMeta; +import com.hospital.front.infra.annotation.ApiParam; import com.hospital.front.infra.exception.BusinessException; import com.hospital.front.service.OrderService; @@ -29,6 +31,8 @@ import com.hospital.front.service.OrderService; * 鉴权:对齐原程序 Sa-Token 的 SaBasicUtil.check(),对 /order/** 做 HTTP Basic 认证。 * 配置 order.basic(格式 "user:password",未配置时放行——与原程序 authFlag=true 且 * basic 为空时行为一致,仅建议生产环境必配)。 + * + * 每个业务方法标注 @ApiMeta,供 /api/admin/apis 反射扫描自动生成接口清单(B 端接口测试工具用)。 */ @Controller @Mapping("/order") @@ -83,6 +87,9 @@ public class OrderApi { // ================= 1. 预约提交 / 取消 ================= /** 4. 提交预约保存 */ + @ApiMeta(desc = "提交预约保存", params = { + @ApiParam(name = "body", desc = "预约 JSON 体") + }) @Mapping(value = "/saveOrder", method = {MethodType.POST}) public Result saveOrder(Context ctx, @Body String body) { checkBasic(ctx); @@ -93,6 +100,11 @@ public class OrderApi { } /** 5. 取消体检预约 */ + @ApiMeta(desc = "取消体检预约", params = { + @ApiParam(name = "peId", desc = "体检编号"), + @ApiParam(name = "name", desc = "姓名"), + @ApiParam(name = "idcard", desc = "身份证号") + }) @Mapping(value = "/cancelOrder/{peId}", method = {MethodType.POST}) public Result cancelOrder( Context ctx, @@ -107,6 +119,9 @@ public class OrderApi { // ================= 2. 体检状态 ================= /** 3. 体检状态查询(多个 peId 逗号分隔) */ + @ApiMeta(desc = "体检状态查询(多个 peId 逗号分隔)", params = { + @ApiParam(name = "peIds", desc = "体检编号逗号分隔") + }) @Mapping("/getPeStatus/{peIds}") public Result>> getPeStatus(Context ctx, @Path("peIds") String peIds) { checkBasic(ctx); @@ -114,6 +129,10 @@ public class OrderApi { } /** 13. 根据身份证号及年份获取当前体检状态及体检项状态 */ + @ApiMeta(desc = "体检状态及体检项状态", params = { + @ApiParam(name = "idNo", desc = "身份证号"), + @ApiParam(name = "year", desc = "年份") + }) @Mapping("/getPeStatusAndItemList/{idNo}/{year}") public Result> getPeStatusAndItemList(Context ctx, @Path("idNo") String idNo, @Path("year") String year) { checkBasic(ctx); @@ -123,6 +142,9 @@ public class OrderApi { // ================= 3. 体检项目 ================= /** 1. 查询员工体检项目 */ + @ApiMeta(desc = "员工体检项目", params = { + @ApiParam(name = "peId", desc = "体检编号") + }) @Mapping("/getEmpItem/{peId}") public Result>> getEmpItem(Context ctx, @Path("peId") String peId) { checkBasic(ctx); @@ -130,6 +152,7 @@ public class OrderApi { } /** 2. 体检项目查询 */ + @ApiMeta(desc = "体检项目查询") @Mapping("/getItemList") public Result>> getItemList(Context ctx) { checkBasic(ctx); @@ -137,6 +160,7 @@ public class OrderApi { } /** 6. 体检指标项数据获取 */ + @ApiMeta(desc = "体检指标项数据获取") @Mapping("/getPeItemList") public Result>> getPeItemList(Context ctx) { checkBasic(ctx); @@ -144,6 +168,7 @@ public class OrderApi { } /** 6.1 体检指标项及体检组合数据获取 */ + @ApiMeta(desc = "体检指标项及体检组合数据获取") @Mapping("/getAllItemList") public Result>> getAllItemList(Context ctx) { checkBasic(ctx); @@ -151,6 +176,9 @@ public class OrderApi { } /** 7. 根据体检指标项查询所属体检组合项 */ + @ApiMeta(desc = "按体检指标项查所属组合", params = { + @ApiParam(name = "peItemCode", desc = "体检指标项编码") + }) @Mapping("/getItemInfo/{peItemCode}") public Result>> getItemInfo(Context ctx, @Path("peItemCode") String peItemCode) { checkBasic(ctx); @@ -160,6 +188,9 @@ public class OrderApi { // ================= 4. 体检结果 / 结论 / 建议 ================= /** 8. 根据体检编号获取员工体检结果 */ + @ApiMeta(desc = "按体检编号查员工体检结果", params = { + @ApiParam(name = "peId", desc = "体检编号") + }) @Mapping("/getResult/{peId}") public Result>> getResult(Context ctx, @Path("peId") String peId) { checkBasic(ctx); @@ -167,6 +198,9 @@ public class OrderApi { } /** 9. 体检结论(汇总) */ + @ApiMeta(desc = "体检结论(汇总)", params = { + @ApiParam(name = "peId", desc = "体检编号") + }) @Mapping("/getTotalConclusion/{peId}") public Result> getTotalConclusion(Context ctx, @Path("peId") String peId) { checkBasic(ctx); @@ -174,6 +208,9 @@ public class OrderApi { } /** 10. 体检建议(汇总) */ + @ApiMeta(desc = "体检建议(汇总)", params = { + @ApiParam(name = "peId", desc = "体检编号") + }) @Mapping("/getTotalSuggest/{peId}") public Result> getTotalSuggest(Context ctx, @Path("peId") String peId) { checkBasic(ctx); @@ -181,6 +218,9 @@ public class OrderApi { } /** 11. 体检结论(列表) */ + @ApiMeta(desc = "体检结论(列表)", params = { + @ApiParam(name = "peId", desc = "体检编号") + }) @Mapping("/getConclusion/{peId}") public Result>> getConclusion(Context ctx, @Path("peId") String peId) { checkBasic(ctx); @@ -188,6 +228,9 @@ public class OrderApi { } /** 12. 体检建议(列表) */ + @ApiMeta(desc = "体检建议(列表)", params = { + @ApiParam(name = "peId", desc = "体检编号") + }) @Mapping("/getSuggest/{peId}") public Result>> getSuggest(Context ctx, @Path("peId") String peId) { checkBasic(ctx); @@ -197,6 +240,10 @@ public class OrderApi { // ================= 5. 按身份证 / 日期查询 ================= /** 14. 根据身份证号及年份获取体检结果 */ + @ApiMeta(desc = "按身份证及年份查体检结果", params = { + @ApiParam(name = "idNo", desc = "身份证号"), + @ApiParam(name = "year", desc = "年份") + }) @Mapping("/getResultByIdcardAndYear/{idNo}/{year}") public Result>> getResultByIdcardAndYear(Context ctx, @Path("idNo") String idNo, @Path("year") String year) { checkBasic(ctx); @@ -204,6 +251,10 @@ public class OrderApi { } /** 14.1 根据身份证号及年份获取体检结果(全量) */ + @ApiMeta(desc = "按身份证及年份查全量结果", params = { + @ApiParam(name = "idNo", desc = "身份证号"), + @ApiParam(name = "year", desc = "年份") + }) @Mapping("/getResultByIdcardAndYearAll/{idNo}/{year}") public Result>> getResultByIdcardAndYearAll(Context ctx, @Path("idNo") String idNo, @Path("year") String year) { checkBasic(ctx); @@ -211,6 +262,9 @@ public class OrderApi { } /** 15.1 根据体检编号获取体检结果 */ + @ApiMeta(desc = "按体检编号查体检结果", params = { + @ApiParam(name = "peId", desc = "体检编号") + }) @Mapping("/getResultByPeId/{peId}") public Result>> getResultByPeId(Context ctx, @Path("peId") String peId) { checkBasic(ctx); @@ -218,6 +272,9 @@ public class OrderApi { } /** 15.2 根据体检编号获取体检状态(简) */ + @ApiMeta(desc = "按体检编号查体检状态(简)", params = { + @ApiParam(name = "peId", desc = "体检编号") + }) @Mapping("/getResultStatus/{peId}") public Result>> getResultStatus(Context ctx, @Path("peId") String peId) { checkBasic(ctx); @@ -225,6 +282,9 @@ public class OrderApi { } /** 15.3 根据 audit_date 查询当天的体检报告 */ + @ApiMeta(desc = "按审查日期查体检报告", params = { + @ApiParam(name = "auditDate", desc = "审查日期 yyyy-MM-dd") + }) @Mapping("/getResultByAuditDate/{auditDate}") public Result>> getResultByAuditDate(Context ctx, @Path("auditDate") String auditDate) { checkBasic(ctx); @@ -232,6 +292,11 @@ public class OrderApi { } /** 16. 根据 id_code 或 pe_id 和审查年份查询体检报告 */ + @ApiMeta(desc = "按身份证或体检编号及年份查报告", params = { + @ApiParam(name = "idNo", desc = "身份证号(与 peId 二选一)"), + @ApiParam(name = "peId", desc = "体检编号(与 idNo 二选一)"), + @ApiParam(name = "auditYear", desc = "审查年份") + }) @Mapping(value = "/getResultByIdNoOrPeId", method = {MethodType.POST}) public Result>> getResultByIdNoOrPeId( Context ctx, @@ -246,6 +311,9 @@ public class OrderApi { } /** 17. 根据身份证号查询当前年的 peId 和预约日期 */ + @ApiMeta(desc = "按身份证查当年 peId 和预约日期", params = { + @ApiParam(name = "idNo", desc = "身份证号") + }) @Mapping("/getPeIdAndDateByIdcard/{idNo}") public Result> getPeIdAndDateByIdcard(Context ctx, @Path("idNo") String idNo) { checkBasic(ctx); @@ -253,6 +321,9 @@ public class OrderApi { } /** 17.1 同上(数组) */ + @ApiMeta(desc = "按身份证查 peId 和预约日期(数组)", params = { + @ApiParam(name = "idNo", desc = "身份证号") + }) @Mapping("/getPeIdAndDateByIdcardList/{idNo}") public Result>> getPeIdAndDateByIdcardList(Context ctx, @Path("idNo") String idNo) { checkBasic(ctx); @@ -262,6 +333,9 @@ public class OrderApi { // ================= 6. 单位 / 人员列表 ================= /** 18. 添加修改单位信息 */ + @ApiMeta(desc = "添加修改单位信息", params = { + @ApiParam(name = "body", desc = "单位 JSON 数组") + }) @Mapping(value = "/addUnit", method = {MethodType.POST}) public Result addUnit(Context ctx, @Body String body) { checkBasic(ctx); @@ -279,6 +353,9 @@ public class OrderApi { } /** 19. 根据日期查询当天体检人员列表(去重),日期为空则查当天 */ + @ApiMeta(desc = "按日期查当天体检人员列表(去重)", params = { + @ApiParam(name = "auditDate", desc = "审查日期(空则查当天)") + }) @Mapping("/getExamListByDate") public Result>> getExamListByDate(Context ctx, @Param("auditDate") String auditDate) { checkBasic(ctx); diff --git a/src/main/java/com/hospital/front/infra/ApiScanner.java b/src/main/java/com/hospital/front/infra/ApiScanner.java new file mode 100644 index 0000000..8e1d71a --- /dev/null +++ b/src/main/java/com/hospital/front/infra/ApiScanner.java @@ -0,0 +1,127 @@ +package com.hospital.front.infra; + +import com.hospital.front.infra.annotation.ApiMeta; +import com.hospital.front.infra.annotation.ApiParam; +import org.noear.solon.annotation.Body; +import org.noear.solon.annotation.Mapping; +import org.noear.solon.annotation.Param; +import org.noear.solon.annotation.Path; +import org.noear.solon.core.handle.Context; +import org.noear.solon.core.handle.MethodType; + +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * 接口元数据扫描器。 + * + * 反射扫描指定 Controller 类的所有 @ApiMeta 标注方法,自动提取: + * - method:从 @Mapping.method()(空数组视为 GET) + * - path:类 @Mapping 前缀 + 方法 @Mapping.value() + * - params:从方法签名 @Path/@Param/@Body 提取 name/in/required/type,@ApiMeta.params 补充 desc + * + * 替代手写接口清单,新增/调整接口时只需维护 @ApiMeta 注解即可自动同步。 + */ +public final class ApiScanner { + + private ApiScanner() { + } + + /** + * 扫描指定 Controller 类,返回带 @ApiMeta 的接口元数据清单。 + * + * @param controllerClass Controller 类(如 OrderApi.class) + * @return 接口元数据列表(不可变) + */ + public static List> scan(Class controllerClass) { + // 类级路径前缀(如 /order) + String prefix = ""; + Mapping classMapping = controllerClass.getAnnotation(Mapping.class); + if (classMapping != null && !classMapping.value().isEmpty()) { + prefix = classMapping.value(); + } + + List> list = new ArrayList<>(); + for (Method method : controllerClass.getDeclaredMethods()) { + ApiMeta meta = method.getAnnotation(ApiMeta.class); + Mapping methodMapping = method.getAnnotation(Mapping.class); + if (meta == null || methodMapping == null) { + continue; + } + + // 拼接完整路径 + String path = prefix + (methodMapping.value() == null ? "" : methodMapping.value()); + + // HTTP 方法:method() 空数组视为 GET + String httpMethod = "GET"; + MethodType[] mtypes = methodMapping.method(); + if (mtypes != null && mtypes.length > 0) { + httpMethod = mtypes[0].name(); + } + + // @ApiMeta.params 转 name→desc 映射,供参数补充描述 + Map descMap = new LinkedHashMap<>(); + for (ApiParam ap : meta.params()) { + descMap.put(ap.name(), ap.desc()); + } + + // 扫描方法参数(跳过 Context) + List> params = new ArrayList<>(); + for (Parameter p : method.getParameters()) { + if (p.getType() == Context.class) { + continue; + } + Map param = extractParam(p, descMap); + if (param != null) { + params.add(param); + } + } + + Map item = new LinkedHashMap<>(); + item.put("method", httpMethod); + item.put("path", path); + item.put("desc", meta.desc()); + item.put("params", params); + list.add(item); + } + return Collections.unmodifiableList(list); + } + + /** 从单个方法参数提取元数据 */ + private static Map extractParam(Parameter p, Map descMap) { + Map param = new LinkedHashMap<>(); + Path pathAnno = p.getAnnotation(Path.class); + Param paramAnno = p.getAnnotation(Param.class); + Body bodyAnno = p.getAnnotation(Body.class); + + if (pathAnno != null) { + String name = pathAnno.value().isEmpty() ? p.getName() : pathAnno.value(); + param.put("name", name); + param.put("in", "path"); + param.put("type", "string"); + param.put("required", true); + } else if (bodyAnno != null) { + param.put("name", "body"); + param.put("in", "body"); + param.put("type", "json"); + param.put("required", true); + } else if (paramAnno != null) { + String name = paramAnno.value().isEmpty() ? p.getName() : paramAnno.value(); + param.put("name", name); + param.put("in", "query"); + param.put("type", "string"); + param.put("required", paramAnno.required()); + } else { + // 无 Solon 参数注解,跳过(如注入的 bean 等) + return null; + } + param.put("desc", descMap.getOrDefault((String) param.get("name"), "")); + return param; + } +} diff --git a/src/main/java/com/hospital/front/infra/annotation/ApiMeta.java b/src/main/java/com/hospital/front/infra/annotation/ApiMeta.java new file mode 100644 index 0000000..deaf7a4 --- /dev/null +++ b/src/main/java/com/hospital/front/infra/annotation/ApiMeta.java @@ -0,0 +1,25 @@ +package com.hospital.front.infra.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 接口元数据注解(标注在 Controller 方法上)。 + * + * 用于 /api/admin/apis 反射扫描自动生成接口清单,供 B 端接口测试工具加载。 + * 标注后无需手写清单,新增/调整接口时自动同步。 + * + * 参数的 name/in/required/type 由方法签名(@Path/@Param/@Body)反射自动提取, + * 此处仅补充方法描述与参数描述(按 name 匹配)。 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface ApiMeta { + /** 接口描述 */ + String desc(); + + /** 参数描述(按 name 匹配方法参数补充 desc) */ + ApiParam[] params() default {}; +} diff --git a/src/main/java/com/hospital/front/infra/annotation/ApiParam.java b/src/main/java/com/hospital/front/infra/annotation/ApiParam.java new file mode 100644 index 0000000..d202062 --- /dev/null +++ b/src/main/java/com/hospital/front/infra/annotation/ApiParam.java @@ -0,0 +1,21 @@ +package com.hospital.front.infra.annotation; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.annotation.ElementType; + +/** + * 接口参数描述注解(仅作为 {@link ApiMeta#params()} 数组成员,不单独标注)。 + * + * 扫描时按 name 匹配方法参数,补充其 desc。 + */ +@Target(ElementType.ANNOTATION_TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface ApiParam { + /** 参数名(须与方法签名 @Path/@Param/@Body 的参数名一致) */ + String name(); + + /** 参数描述 */ + String desc() default ""; +}