SceneSchemaService.java 9.34 KB
package com.onto.engine.meta;

import com.onto.engine.model.ModelRepository;
import com.onto.engine.security.MaskService;
import org.springframework.stereotype.Service;

import java.util.*;

/**
 * 场景渲染元数据合成服务:把 M8 页面模板与 M1 字段类型、M5 脱敏、M2 命令、M3 接口、MetaRule 规则
 * 合并为一份"前端通用渲染引擎"可直接消费的 Schema。前端据此动态生成"新增订单"页,无任何业务硬编码。
 */
@Service
public class SceneSchemaService {

    private final ModelRepository models;
    private final MaskService mask;

    public SceneSchemaService(ModelRepository models, MaskService mask) {
        this.models = models;
        this.mask = mask;
    }

    public Map<String, Object> buildSceneSchema(String sceneId) {
        Map<String, Object> scene = models.scene(sceneId);
        if (scene.isEmpty()) throw new NoSuchElementException("未找到场景: " + sceneId);
        Map<String, Object> template = models.templateByScene(sceneId);

        // 从 M4 flowSteps(含嵌套控制流)递归找到本场景绑定的命令
        String[] bound = findBoundCommand(asList(scene.get("flowSteps")), new String[]{null, null});
        String cmdAggregate = bound[0], cmdId = bound[1];

        Map<String, Object> out = new LinkedHashMap<>();
        out.put("sceneId", sceneId);
        out.put("sceneName", scene.get("sceneName"));
        out.put("aggregateScope", scene.get("aggregateScope"));
        out.put("permissionBind", scene.get("permissionBind"));

        // M2 命令(前端提交时据此把表单值映射为命令入参)
        if (cmdId != null) {
            Map<String, Object> cmd = models.command(cmdAggregate, cmdId);
            Map<String, Object> cmdOut = new LinkedHashMap<>();
            cmdOut.put("aggregateId", cmdAggregate);
            cmdOut.put("cmdId", cmdId);
            cmdOut.put("desc", cmd.get("desc"));
            cmdOut.put("inputParams", cmd.get("inputParams"));
            cmdOut.put("validations", cmd.get("validations"));
            cmdOut.put("bizSteps", cmd.get("bizSteps"));
            cmdOut.put("emitEvents", cmd.get("emitEvents"));
            out.put("command", cmdOut);
            // M3 命令 API 地址(前端无硬编码 URL,从模型取)
            Object api = asMap(models.deployOf(cmdAggregate).get("commandApi")).get(cmdId);
            out.put("commandApi", api);
        }

        // M8 模板(逐组件富化 M1 类型 / M5 脱敏 / 引用下拉数据源)
        if (!template.isEmpty()) {
            Map<String, Object> tOut = new LinkedHashMap<>();
            tOut.put("templateId", template.get("templateId"));
            tOut.put("templateName", template.get("templateName"));
            tOut.put("templateType", template.get("templateType"));
            tOut.put("renderMode", template.get("renderMode"));
            tOut.put("masterAggregate", template.get("masterAggregate"));
            tOut.put("detailAggregates", template.get("detailAggregates"));
            List<Object> enriched = new ArrayList<>();
            for (Object c : asList(template.get("rootComponents"))) {
                enriched.add(enrich(asMap(c)));
            }
            tOut.put("rootComponents", enriched);
            out.put("template", tOut);
        }

        // MetaRule 规则(前端实时校验用,与后端同源)
        List<Map<String, Object>> rules = new ArrayList<>();
        for (Map<String, Object> group : models.ruleGroupsByScene(sceneId)) {
            for (Object r : asList(group.get("rules"))) {
                Map<String, Object> rule = asMap(r);
                Map<String, Object> ro = new LinkedHashMap<>();
                ro.put("groupId", group.get("groupId"));
                ro.put("ruleId", rule.get("ruleId"));
                ro.put("ruleName", rule.get("ruleName"));
                ro.put("effect", rule.get("effect"));
                ro.put("message", rule.get("message"));
                ro.put("matchCondition", rule.get("matchCondition"));
                rules.add(ro);
            }
        }
        out.put("rules", rules);
        out.put("ruleGlobalParams", models.ruleGlobalParams());

        // 溯源信息(供前端"运行原理"面板展示每部分来自哪一层)
        Map<String, String> provenance = new LinkedHashMap<>();
        provenance.put("template", "M8-front-schema.yaml");
        provenance.put("fieldType", "M1-domain.yaml");
        provenance.put("command", "M2-command.yaml");
        provenance.put("commandApi", "M3-deploy.yaml");
        provenance.put("mask", "M5-security.yaml");
        provenance.put("rules", "MetaRule-business.yaml");
        provenance.put("permission", "M5-security.yaml (functionPermissions)");
        out.put("sourceLayers", provenance);

        return out;
    }

    /** 递归富化一个 M8 组件。 */
    private Map<String, Object> enrich(Map<String, Object> comp) {
        Map<String, Object> out = new LinkedHashMap<>();
        out.put("compId", comp.get("compId"));
        out.put("compType", comp.get("compType"));
        out.put("width", comp.get("width"));
        out.put("span", comp.get("span"));

        Map<String, Object> fb = asMap(comp.get("fieldBind"));
        if (!fb.isEmpty()) {
            Map<String, Object> fbOut = new LinkedHashMap<>(fb);
            String agg = String.valueOf(fb.get("bindAggregateId"));
            String sourceType = String.valueOf(fb.get("bindSourceType"));
            String entityName = fb.get("domainEntityName") == null ? null : String.valueOf(fb.get("domainEntityName"));
            String field = String.valueOf(fb.get("domainFieldName"));
            String entForType = "aggregate_root".equals(sourceType) ? null : entityName;

            fbOut.put("fieldType", models.fieldType(agg, entForType, field));
            boolean masked = Boolean.TRUE.equals(fb.get("maskField")) || mask.isMasked(field);
            fbOut.put("masked", masked);

            // 下拉引用数据源(通用,全部源自模型;无任何具体聚合/字段字面量):
            //  1) select 绑定到某聚合根【自身标识】字段 => 选项来自该聚合(如客户/商品/订单下拉);
            //  2) select 绑定到带 M1 refAggregate 的【引用】字段 => 选项来自被引用聚合,值=对方标识、回填本字段
            //     (如订单审核里 auditorId 引用 EmployeeAggregate.employeeId,渲染为员工下拉)。
            if ("select".equals(comp.get("compType"))) {
                if ("aggregate_root".equals(sourceType) && field.equals(models.rootIdentifier(agg))) {
                    fbOut.put("optionsSource", optionsSource(agg));
                } else {
                    Map<String, Object> ref = models.refInfo(agg, entForType, field);
                    if (!ref.isEmpty()) fbOut.put("optionsSource", optionsSource(String.valueOf(ref.get("refAggregate"))));
                }
            }
            out.put("fieldBind", fbOut);
        }

        List<Object> children = asList(comp.get("childComponents"));
        if (!children.isEmpty()) {
            List<Object> co = new ArrayList<>();
            for (Object ch : children) co.add(enrich(asMap(ch)));
            out.put("childComponents", co);
        }
        return out;
    }

    /** 某聚合根的下拉选项数据源描述(value=聚合根标识,label=展示字段,endpoint=/api/data/agg,通用)。 */
    private Map<String, Object> optionsSource(String aggregateId) {
        Map<String, Object> src = new LinkedHashMap<>();
        src.put("aggregateId", aggregateId);
        src.put("endpoint", "/api/data/" + aggregateId);
        src.put("valueField", models.rootIdentifier(aggregateId));
        src.put("labelField", displayField(aggregateId));
        return src;
    }

    /** 聚合根的展示字段:首个 string 类型属性(如 customerName/productName),否则用标识字段。 */
    private String displayField(String aggregateId) {
        for (Object at : models.rootAttributes(aggregateId)) {
            Map<String, Object> am = asMap(at);
            if ("string".equals(am.get("type"))) return String.valueOf(am.get("name"));
        }
        return models.rootIdentifier(aggregateId);
    }

    /** 递归定位场景绑定的主命令(含嵌套控制流),返回 {aggregate, cmdId}(后者为最后命中)。 */
    private static String[] findBoundCommand(List<Object> steps, String[] acc) {
        for (Object st : steps) {
            Map<String, Object> stp = asMap(st);
            if ("bindCommand".equals(stp.get("stepType"))) {
                acc[0] = String.valueOf(stp.get("bindAggregate"));
                acc[1] = String.valueOf(stp.get("bindCommand"));
            }
            for (String key : List.of("then", "else", "body", "default")) findBoundCommand(asList(stp.get(key)), acc);
            for (Object b : asList(stp.get("branches"))) findBoundCommand(asList(asMap(b).get("steps")), acc);
            for (Object c : asList(stp.get("cases"))) findBoundCommand(asList(asMap(c).get("steps")), acc);
        }
        return acc;
    }

    @SuppressWarnings("unchecked")
    private static Map<String, Object> asMap(Object o) {
        return o instanceof Map ? (Map<String, Object>) o : Collections.emptyMap();
    }

    @SuppressWarnings("unchecked")
    private static List<Object> asList(Object o) {
        return o instanceof List ? (List<Object>) o : Collections.emptyList();
    }
}