|
|
@@ -0,0 +1,160 @@
|
|
|
+package com.guanlan.backend.web;
|
|
|
+
|
|
|
+import com.fasterxml.jackson.databind.JsonNode;
|
|
|
+import com.fasterxml.jackson.databind.ObjectMapper;
|
|
|
+import io.swagger.annotations.Api;
|
|
|
+import io.swagger.annotations.ApiOperation;
|
|
|
+import org.springframework.beans.factory.annotation.Value;
|
|
|
+import org.springframework.http.MediaType;
|
|
|
+import org.springframework.http.ResponseEntity;
|
|
|
+import org.springframework.web.bind.annotation.GetMapping;
|
|
|
+import org.springframework.web.bind.annotation.RestController;
|
|
|
+
|
|
|
+import java.io.IOException;
|
|
|
+import java.nio.file.Files;
|
|
|
+import java.nio.file.Path;
|
|
|
+import java.nio.file.Paths;
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.Comparator;
|
|
|
+import java.util.LinkedHashMap;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Map;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 数据可信度 findings(P12 第二批)——与 Python `dq_findings_view()`(serve.py:394-446)**逐字对齐**。
|
|
|
+ *
|
|
|
+ * 两侧读**同一份**对象库产物(`objects.json`),只做投影/排序,不重算。
|
|
|
+ */
|
|
|
+@Api(tags = "dq")
|
|
|
+@RestController
|
|
|
+public class DqController {
|
|
|
+
|
|
|
+ private static final String[] KEEP = {
|
|
|
+ "finding/data_quality/", "finding/caliber/", "finding/system_coverage/", "finding/tier_trend/",
|
|
|
+ };
|
|
|
+
|
|
|
+ private final ObjectMapper mapper = new ObjectMapper();
|
|
|
+
|
|
|
+ @Value("${guanlan.product-dir:outputs/rudong/guanlan}")
|
|
|
+ private String productDir;
|
|
|
+
|
|
|
+ @Value("${guanlan.objects-json:outputs/rudong/ontology/objects.json}")
|
|
|
+ private String objectsRel;
|
|
|
+
|
|
|
+ private Path objects() {
|
|
|
+ Path p = Paths.get(objectsRel);
|
|
|
+ if (p.isAbsolute() && Files.exists(p)) {
|
|
|
+ return p;
|
|
|
+ }
|
|
|
+ Path cwd = Paths.get("").toAbsolutePath();
|
|
|
+ for (Path base : new Path[]{cwd, cwd.resolve("..").resolve("..")}) {
|
|
|
+ Path f = base.resolve(objectsRel);
|
|
|
+ if (Files.exists(f)) {
|
|
|
+ return f;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return cwd.resolve(objectsRel);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static String s(JsonNode n) {
|
|
|
+ return (n == null || n.isNull() || n.isMissingNode()) ? null : n.asText();
|
|
|
+ }
|
|
|
+
|
|
|
+ private static List<String> list(JsonNode n) {
|
|
|
+ List<String> out = new ArrayList<>();
|
|
|
+ if (n != null && n.isArray()) {
|
|
|
+ for (JsonNode x : n) {
|
|
|
+ out.add(x.asText());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return out;
|
|
|
+ }
|
|
|
+
|
|
|
+ @ApiOperation("数据可信度 finding 行")
|
|
|
+ @GetMapping(value = "/api/dq_findings", produces = "application/json; charset=utf-8")
|
|
|
+ public ResponseEntity<Map<String, Object>> dqFindings() throws IOException {
|
|
|
+ Path f = objects();
|
|
|
+ JsonNode db = mapper.readTree(Files.readAllBytes(f));
|
|
|
+ List<Map<String, Object>> rows = new ArrayList<>();
|
|
|
+ java.util.Iterator<Map.Entry<String, JsonNode>> it = db.fields();
|
|
|
+ while (it.hasNext()) {
|
|
|
+ Map.Entry<String, JsonNode> e = it.next();
|
|
|
+ String k = e.getKey();
|
|
|
+ JsonNode o = e.getValue();
|
|
|
+ boolean keep = false;
|
|
|
+ for (String pre : KEEP) {
|
|
|
+ if (k.startsWith(pre)) {
|
|
|
+ keep = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (!keep || !o.isObject()) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ JsonNode p2 = o.path("props");
|
|
|
+ Map<String, Object> row = new LinkedHashMap<>();
|
|
|
+ row.put("id", k);
|
|
|
+ String[] parts = k.split("/");
|
|
|
+ row.put("kind", parts.length > 1 ? parts[1] : "");
|
|
|
+ row.put("name", s(p2.get("name_zh")) != null ? s(p2.get("name_zh")) : s(p2.get("name")));
|
|
|
+ row.put("name_zh", s(p2.get("name_zh")) != null ? s(p2.get("name_zh")) : s(p2.get("name")));
|
|
|
+ row.put("name_en", s(p2.get("name_en")));
|
|
|
+ row.put("grade", s(p2.get("grade")));
|
|
|
+ row.put("claim_window", s(p2.get("claim_window")));
|
|
|
+ row.put("evidence", firstOr(p2, "evidence_zh", "evidence"));
|
|
|
+ row.put("evidence_zh", list(p2.get("evidence_zh")));
|
|
|
+ row.put("evidence_en", list(p2.get("evidence_en")));
|
|
|
+ row.put("caveat", s(p2.get("caveat")));
|
|
|
+ row.put("caveat_en", s(p2.get("caveat_en")));
|
|
|
+ row.put("impact", firstOr(p2, "discriminator_zh", "disposition_zh", "implication_zh", "corrected_comparison_zh"));
|
|
|
+ row.put("impact_en", firstOr(p2, "discriminator_en", "disposition_en", "implication_en", "corrected_comparison_en"));
|
|
|
+ row.put("got_wrong", s(p2.get("what_i_got_wrong_zh")));
|
|
|
+ row.put("got_wrong_en", s(p2.get("what_i_got_wrong_en")));
|
|
|
+ row.put("quality_flag", s(p2.get("quality_flag_zh")));
|
|
|
+ row.put("quality_flag_en", s(p2.get("quality_flag_en")));
|
|
|
+ row.put("robust", s(p2.get("robust_zh")));
|
|
|
+ row.put("robust_en", s(p2.get("robust_en")));
|
|
|
+ row.put("basis", s(p2.get("basis_zh")));
|
|
|
+ row.put("basis_en", s(p2.get("basis_en")));
|
|
|
+ row.put("sensitivity", s(p2.path("sensitivity").get("note_zh")));
|
|
|
+ row.put("sensitivity_en", s(p2.path("sensitivity").get("note_en")));
|
|
|
+ row.put("age_basis", s(p2.get("age_basis_zh")));
|
|
|
+ row.put("age_basis_en", s(p2.get("age_basis_en")));
|
|
|
+ rows.add(row);
|
|
|
+ }
|
|
|
+ // 排序:先 data_quality 族,再按 id
|
|
|
+ rows.sort(Comparator.<Map<String, Object>, Integer>comparing(r -> "data_quality".equals(r.get("kind")) ? 0 : 1)
|
|
|
+ .thenComparing(r -> String.valueOf(r.get("id"))));
|
|
|
+ Map<String, Object> out = new LinkedHashMap<>();
|
|
|
+ out.put("rows", rows);
|
|
|
+ out.put("n", rows.size());
|
|
|
+ if (rows.isEmpty()) {
|
|
|
+ out.put("note", NOTE);
|
|
|
+ }
|
|
|
+ return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(out);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static final String NOTE =
|
|
|
+ "本体对象库里没有 finding/* 对象 (四族: data_quality / caliber / system_coverage / "
|
|
|
+ + "tier_trend) ⇒ 本页**如实为空**, 不是\"自查无发现\"。这些 finding 的生成端不在重算链上"
|
|
|
+ + "(唯一写方 scripts/ingest_ops_2025.py 的输入 data/raw/工作库 不在现场数据里), "
|
|
|
+ + "运行期不从交付包补齐(用户令 2026-09-17); 要出件须研发补生成端。";
|
|
|
+
|
|
|
+ /** Python 侧 `evidence` 字段的口径:`evidence_zh or evidence or []`;其余 `impact` 是四选一 */
|
|
|
+ private static Object firstOr(JsonNode p2, String... keys) {
|
|
|
+ for (String k : keys) {
|
|
|
+ JsonNode n = p2.get(k);
|
|
|
+ if (n == null || n.isNull()) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if (n.isArray()) {
|
|
|
+ return list(n);
|
|
|
+ }
|
|
|
+ String v = n.asText();
|
|
|
+ if (v != null && !v.isEmpty()) {
|
|
|
+ return v;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+}
|