# allbytaskid——一个接口扛六个DataStore

allbytaskid:一个接口扛六个DataStore

非科班野生程序员,深耕政务信息化20年,这套自研Java Web框架支撑过省级社保、跨省医保结算等核心民生系统。打开一个审批表单需要多少次网络请求?表单数据、字段权限、流程进度、下一步选项、会签意见、审批历史------如果每个单独请求,就是6次。政务系统的网络环境不比互联网,内网有时候很慢。这篇拆解一个接口怎么一次拿完6组数据。最后感谢豆包、智谱、OpenCode,决策是我做的,代码是我搓的,文字是他们总结的。


背景

审批表单加载时需要的数据:

数据 用途 不拿会怎样
表单数据 填到表单控件里 表单是空的
字段权限 某些字段只读 所有字段都能改
流程进度 顶部显示走到哪了 不知道当前环节
下一步选项 下拉框里选下一步 不知道能选什么
会签意见 如果有会签,显示意见表 看不到别人签了什么
审批历史 底部显示审批流 不知道前面谁签过

如果每个单独请求,浏览器到服务器6次往返。在内网环境下,每次往返可能100-300ms(包括网络延迟+服务器处理),6次就是600-1800ms。用户打开一个表单要等1-2秒。

能不能一次拿完?


核心思路

一个 allbytaskid 接口,接收 taskidinsidformid 三个参数,返回一个 DataCenter 对象,里面包含4个命名的 DataStore 和1个参数:

复制代码
DataCenter
  ├── DataStore "next"    ------ 下一步选项
  ├── Parameter "lcmx"    ------ 流程进度(字符串)
  ├── DataStore "frole"   ------ 字段权限
  ├── DataStore "common"  ------ 表单数据
  ├── DataStore "hqyj"    ------ 会签意见
  ├── DataStore "tasks"   ------ 审批历史
  └── code                ------ 0=会签完成/-1=会签未完成

后端实现

allbytaskid() 完整拆解

java 复制代码
public DataCenter allbytaskid(DataCenter center, ...) throws Exception {
    DBUtil.BeginTrans(false);
    DataCenter newdc = new DataCenter();
    String taskid = request.getParameter("taskid").trim();

1. 下一步选项(DataStore "next")

java 复制代码
    DataStore ds = processService.getNextBranches(taskid);
    String CODENAME = (String) ds.getRowset().getrow(0).getItemValue("CODENAME");
    String codeName = CODENAME.replaceAll("<br>", "")
        .replaceAll("\r", "").replaceAll("\n", "");
    ds.getRowset().getrow(0).setItemValue("CODENAME", codeName);
    ds.getRowset().resetUpdate();
    ds.setName("next");
    newdc.addStore(ds);

getNextBranches() 查Activiti的流程定义,找到当前节点后面可以走的所有分支。返回的 CODENAME 可能有HTML换行符,清理一下。

2. 流程进度(Parameter "lcmx")

java 复制代码
    String lc = "";
    HashMap<String, String> omap = processService.getTask(taskid);
    String processDefinitionId = omap.get("pid");
    DataStore detail = processService
        .getProcessDefinitionDetailByProcessDefinitionId(processDefinitionId);
    String currtaskkey = omap.get("taskdefid");
    String lcmx = "";
    for(int i = 0; i < detail.getRowset().getPrimary().size(); i++) {
        Row row = detail.getRowset().getrow(i);
        String name = row.getItemStringValue("name");
        String id = row.getItemStringValue("id");
        if(currtaskkey.equals(id.replace(omap.get("pid") + "_", "")))
            name = "[" + name + "]";  // 当前环节加方括号
        if(i != 0) lcmx = lcmx + "---→" + name;
        else lcmx = name;
    }
    lc = lcmx.replaceAll("<br>", "").replaceAll("\r", "").replaceAll("\n", "");
    newdc.setParameters("lcmx", lc);

遍历流程定义的所有活动节点,当前环节加方括号标红。比如:

复制代码
申请---→科长审批---→[处长审批]---→局长审批

3. 字段权限(DataStore "frole")

java 复制代码
    DataStore ds1 = new DataStore("frole");
    if(!"".equals(taskid) && !"null".equals(taskid) && taskid != null) {
        HashMap<String, String> tasks = processService.getTask(taskid);
        String pid = tasks.get("pid");
        String taskdefid = tasks.get("taskdefid");
        t_from_readonly dao = new t_from_readonly();
        dao.setPid(pid);
        dao.setTaskdefid(taskdefid);
        Object para[] = { dao };
        ds1 = DBUtil.Query(t_from_readonlyMapper.class,
            dao.getSearchMethod(), para, "frole");
    }
    newdc.addStore(ds1);

用流程定义ID + 步骤ID 查 t_from_readonly 表,拿到当前步骤应该只读的字段列表。

4. 表单数据(DataStore "common")

java 复制代码
    String insid = request.getParameter("insid").trim();
    String fromid = request.getParameter("formid").trim();
    if(!"".equals(insid) && !"null".equals(insid) && insid != null) {
        // 查表单定义获取SQL ID
        List<myform> list = (List<myform>) DBUtil.getDao(
            myformMapper.class, "selectByPrimaryKey", fromid);
        String sqlid = list.get(0).getSqlid();
        // 用 proc_inst_id_ 查业务数据
        DataStore pds = center.getSingleDataStore();
        pds.setParameter("proc_inst_id_", insid);
        pds.setParameter("fromid", fromid);
        pds.setParameter("sqlId", sqlid);
        CommonSQLcontrol sqlcl =
            (CommonSQLcontrol) BeanFactory.getBean("CommonSQL");
        DataCenter dc = sqlcl.CommonQuery(center, request, response);
        ds = dc.getSingleDataStore();
        ds.setName("common");
        newdc.addStore(ds);
    }

表单数据走通用SQL查询------表单定义里有 sqlid,框架的 CommonSQL 组件根据 sqlid 找到对应的SQL模板,用 proc_inst_id_ 作参数执行查询。

5. 会签意见(DataStore "hqyj")

java 复制代码
    hq dao = new hq();
    dao.setSearchMethod("selectC");
    dao.setPreProcInstId(insid);
    List<hq> list = (List<hq>) dao.search();
    int count = Integer.parseInt(list.get(0).getTaskId());
    boolean canNext = true;
    if(count > 0) canNext = false;

    dao.setSearchMethod("selectAll");
    dao.setPreProcInstId(insid);
    list = (List<hq>) dao.search();

    ds = new DataStore("hqyj");
    for(int i = 0; i < list.size(); i++) {
        Row row = ds.getRowset().add();
        User userdao = new User();
        userdao.setPsn_id(list.get(i).getUserId());
        List<User> userlist = (List<User>) DBUtil.getDao(
            userMapper.class, "getUserInfo_new", userdao);
        if(userlist != null && userlist.size() != 0) {
            User user = userlist.get(0);
            row.setItemValue("dept", user.getUnit_name());
            row.setItemValue("username", user.getPsn_name());
            String date1 = list.get(i).getEnddate();
            if(date1 == null || "".equals(date1) || "null".equals(date1)) {
                row.setItemValue("result", "未签批");
            } else if("null".equals(list.get(i).getProcInstId())) {
                row.setItemValue("result", "放弃");
            } else {
                String temp = "1".equals(list.get(i).getPass()) ? "通过" : "不通过";
                row.setItemValue("result", temp);
                row.setItemValue("cause", list.get(i).getCause());
                row.setItemValue("rtime", date1);
            }
        }
        if("0".equals(list.get(i).getPass())) canNext = false;
    }
    ds.setRecordCount(ds.getRowset().getPrimary().size());
    newdc.addStore(ds);

    if(canNext) newdc.setCode(0);
    else newdc.setCode(-1);

T_HJS_s 表------先查剩余未完成数(selectC),再查全部会签记录(selectAll)。每条记录填充:部门、姓名、结果(通过/不通过/未签批/放弃)、意见、时间。

canNext 是关键------有人没签完或有人不通过,code 设为 -1,前端据此禁用提交按钮。

6. 审批历史(DataStore "tasks")

java 复制代码
    List<HashMap<String, String>> tasks = processService.getTasks(insid);
    ds = new DataStore("tasks");
    if(tasks != null) {
        for(int i = 0; i < tasks.size(); i++) {
            Row row = ds.getRowset().add();
            HashMap<String, String> map = tasks.get(i);
            String userid = map.get("userid");
            // 查用户名、部门名
            User userdao = new User();
            userdao.setPsn_id(userid);
            List<User> userlist = (List<User>) DBUtil.getDao(
                userMapper.class, "getUserInfo_new", userdao);
            if(userlist != null && userlist.size() != 0) {
                row.setItemValue("usrename", userlist.get(0).getPsn_name());
                row.setItemValue("areano", userlist.get(0).getAreano());
                row.setItemValue("unitname", userlist.get(0).getUnitname());
            }
            row.setItemValue("taskname", map.get("taskname"));
            row.setItemValue("endTime", map.get("endTime"));
            row.setItemValue("suggestion", map.get("suggestion"));
        }
    }
    ds.setRecordCount(ds.getRowset().getPrimary().size());
    newdc.addStore(ds);

    DBUtil.EndTrans();
    return newdc;

getTasks() 查Activiti的历史任务表,拿到这个流程实例的所有历史任务。然后逐条查用户信息,填充姓名和部门。


前端消费

onlyFrom.jsp 中的 search()

javascript 复制代码
function search() {
    var URL = webRootDir
        + "/BusinessAction?Business=flow&Action=allbytaskid"
        + "&taskid=<%=taskid%>&insid=<%=insid%>&formid=<%=sqlId%>";
    var data = { url:URL, sync:false, load:function (dc1) {
        seachGrid();

        // 1. 表单数据
        var ds = dc1.getDataStore("common");
        if(!ds) ds = new browise.ds.DataStore();
        var row = ds.getRowSet().getRow(0);
        if(!row) row = ds.getRowSet().addRow();
        // 绑定到表单控件
        var from01 = browise.byId("...");
        from01.getBinding().bind(row);

        // 2. 流程进度
        setmx(dc1.getParameter("lcmx"));

        // 3. 字段权限
        setrolebydata(dc1.getDataStore("frole"));

        // 4. 会签意见
        sethqyj(dc1);

        // 5. 下一步选项
        setNext(dc1.getDataStore("next"));

        // 6. 审批历史
        var ds1 = dc1.getDataStore("tasks");
        browise.byId("grid_spyj").getBinding().setDataStore(ds1);

        // 7. 表单插件钩子
        try { if(searchothers) searchothers(); } catch(E) { }

        // 8. 工作时间校验
        try { QueryTime(); } catch(E) { }
    }};
    browise.Action.requestData(data, dc, true);
}

一次请求,8个步骤顺序处理,6组数据全部到位。


性能对比

方案 网络请求 预估耗时(内网)
6次独立请求 6次 600-1800ms
allbytaskid 1次 100-300ms

而且 allbytaskid 内部在一个数据库事务里执行6个查询(DBUtil.BeginTrans / DBUtil.EndTrans),避免了6次事务开启/提交的开销。


为什么这样做

为什么不用6个独立接口?

能做。但内网环境慢,6次网络往返的延迟用户能感觉到。而且6个接口的调用顺序有依赖------先拿表单数据才能处理字段权限,先拿会签状态才能决定是否禁用提交按钮。串行6次比并行1次慢得多。

为什么不用GraphQL?

框架没用任何现代前端框架。前端是Dojo的RIA组件库,后端是自研的Servlet+BeanFactory。没有Node.js、没有GraphQL Runtime。用一个Servlet接口返回一个包含多个DataStore的DataCenter对象,在框架自己的体系内就是最高效的做法。

这不就是BFF吗?

是的。后来在前端圈这叫BFF(Backend For Frontend)。我当时不知道这个词,只知道一次拿完比请求6次快。务实主义不需要先知道术语再做事。


决策原则

减少网络往返比优化单个查询更重要。

6个查询合并成1个请求,网络延迟降了一个量级。单个查询再怎么优化SQL,也省不出6次网络往返的时间。先把"胖"做对,再做"瘦"。


你的项目里表单加载是怎么做的?是多个接口还是一个聚合接口?欢迎评论区聊聊。


系列导航:


作者:许彰午 | 非科班野生程序员,深耕政务信息化20年

标签: #Java #Activiti #工作流 #BFF #DataStore #政务信息化

相关推荐
吠品1 小时前
Java byte数组与String互转:编码细节与踩坑记录
java·linux·服务器
牛奶咖啡131 小时前
AI助力运维——AIGC运维应用实践—Deepseek的介绍与本地部署选型
运维·人工智能·deepseek·deepseek能做什么·deepseek本地部署配置·本地部署选型避坑原则·本地部署的典型方案
木白CPP2 小时前
Linux 驱动UART子系统源码分析
linux·运维·服务器·驱动开发·嵌入式硬件·开源软件
-今昭-2 小时前
HAProxy 负载均衡部署
运维·负载均衡
RisunJan2 小时前
Linux命令-times(进程累计时间显示)
linux·运维·chrome
xiaoxiangsiyan3 小时前
RHCE2026完整版全知识点清单
运维·网络·学习·自动化·rhce
风控PM说3 小时前
自动化风控策略生成:基于决策树的方法
运维·决策树·自动化
Titan20244 小时前
Linux网络基础知识
linux·服务器·网络·c++·学习
Java&Develop4 小时前
QuantDinger 宝塔 Linux 部署完整指南
linux·运维·状态模式