package jnpf.onlinedev.controller; import cn.dev33.satoken.annotation.SaCheckPermission; import cn.dev33.satoken.stp.StpUtil; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ObjectUtil; import com.google.common.collect.ImmutableList; import feign.Response; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jnpf.base.*; import jnpf.base.controller.SuperController; import jnpf.base.entity.*; import jnpf.base.model.ColumnDataModel; import jnpf.base.model.FormDeleteResult; import jnpf.base.model.VisualDevJsonModel; import jnpf.base.model.VisualWebTypeEnum; import jnpf.base.model.column.ColumnModel; import jnpf.base.model.export.VisualExportVo; import jnpf.base.model.flow.DataModel; import jnpf.base.model.flow.FlowLaunchModel; import jnpf.base.model.module.PropertyJsonModel; import jnpf.base.model.online.ExcelImportModel; import jnpf.base.model.online.ImportExcelFieldModel; import jnpf.base.model.online.VisualImportModel; import jnpf.base.model.online.VisualdevModelDataInfoVO; import jnpf.base.service.VisualAliasService; import jnpf.base.service.VisualdevReleaseService; import jnpf.base.service.VisualdevService; import jnpf.base.util.FormExecelUtils; import jnpf.base.util.ServiceBaseUtil; import jnpf.base.util.VisualUtil; import jnpf.base.util.VisualUtils; import jnpf.base.util.VisualBillUtil; import jnpf.base.vo.DownloadVO; import jnpf.base.vo.PageListVO; import jnpf.base.vo.PaginationVO; import jnpf.config.ConfigValueUtil; import jnpf.constant.FileTypeConstant; import jnpf.constant.JnpfConst; import jnpf.constant.MsgCode; import jnpf.constant.PermissionConst; import jnpf.emnus.ExportModelTypeEnum; import jnpf.emnus.ModuleTypeEnum; import jnpf.exception.DataException; import jnpf.exception.WorkFlowException; import jnpf.file.FileApi; import jnpf.file.FileUploadApi; import jnpf.flowable.TaskApi; import jnpf.flowable.WorkFlowApi; import jnpf.flowable.entity.TaskEntity; import jnpf.flowable.model.task.FlowModel; import jnpf.flowable.model.trigger.TriggerDataModel; import jnpf.model.ExcelModel; import jnpf.model.ExportModel; import jnpf.model.OnlineDevData; import jnpf.model.TransferModel; import jnpf.model.upload.UploadFileModel; import jnpf.model.visualjson.FieLdsModel; import jnpf.model.visualjson.FormDataModel; import jnpf.model.visualjson.UploaderTemplateModel; import jnpf.model.visualjson.config.HeaderModel; import jnpf.onlinedev.entity.VisualdevModelDataEntity; import jnpf.onlinedev.model.*; import jnpf.onlinedev.model.log.VisualLogForm; import jnpf.onlinedev.service.VisualDevInfoService; import jnpf.onlinedev.service.VisualDevListService; import jnpf.onlinedev.service.VisualLogService; import jnpf.onlinedev.service.VisualPersonalService; import jnpf.onlinedev.service.VisualdevModelDataService; import jnpf.onlinedev.util.*; import jnpf.permission.UserApi; import jnpf.permissions.PermissionInterfaceImpl; import jnpf.util.*; import jnpf.util.context.RequestContext; import jnpf.util.visiual.JnpfKeyConsts; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.dromara.x.file.storage.core.FileInfo; import org.jetbrains.annotations.NotNull; import org.springframework.http.MediaType; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.file.StandardCopyOption; import java.sql.SQLException; import java.text.ParseException; import java.util.*; import java.util.stream.Collectors; /** * 0代码无表开发 * * @author JNPF开发平台组 * @version V3.1.0 * @copyright 引迈信息技术有限公司 * @date 2019年9月27日 上午9:18 */ @Slf4j @Tag(name = "0代码无表开发", description = "OnlineDev") @RestController @RequiredArgsConstructor @RequestMapping("/OnlineDev") public class VisualdevModelDataController extends SuperController { private final VisualdevModelDataService visualdevModelDataService; private final VisualdevService visualdevService; private final FileApi fileApi; private final FileUploadApi fileUploadApi; private final ConfigValueUtil configValueUtil; private final VisualDevListService visualDevListService; private final VisualDevInfoService visualDevInfoService; private final VisualdevReleaseService visualdevReleaseService; // AUDIT 补丁(2026-08-01,登记 tasks/jnpf-platform-patches.md #6):批量删除需要发 VSLOG 事件供审计消费。 private final VisualLogService visualLogService; private final OnlineSwapDataUtils onlineSwapDataUtils; private final IntegrateUtil integrateUtil; private final ModuleApi moduleService; private final ModuleUseNumApi moduleUseNumService; private final VisualAliasService aliasService; private final WorkFlowApi workFlowApi; private final TaskApi taskApi; private final VisualPersonalService visualPersonalService; private final UserApi userService; private final SystemApi systemService; private final OnlineExcelUtil onlineExcelUtil; private final ServiceBaseUtil serviceBaseUtil; private final VisualBillUtil visualBillUtil; /** * AUDIT 补丁(2026-08-01):单次批量删除取快照的条数上限。 * *

对齐前端列表页分页上限(单页最多 500 条)——正常业务选不到第 501 条,这是安全阀 * 而非功能限制。超出部分**照样产生审计事件**(「这次删过」不能缺),只是不带内容快照, * 落在 {@link #auditDeleteSnapshot} 已定义的「取不到返回 null」语义上。 */ private static final int AUDIT_SNAPSHOT_LIMIT = 500; private boolean isOnlineUtilsOpen(Boolean onlineUtilsOpen) { return Boolean.TRUE.equals(onlineUtilsOpen); } /** 成功读取后发布一条审计专用事件;是否落库由 audit.layer0.record-read 决定。 */ private void publishReadAudit(String modelId, String dataId, String readScope) { try { ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); String requestMethod = attrs == null ? null : attrs.getRequest().getMethod(); // 树形删除会在同一 DELETE 请求内直接复用 listTree;这种内部查询不是用户 READ。 if ("LIST".equals(readScope) && !"POST".equalsIgnoreCase(requestMethod) || "DETAIL".equals(readScope) && !"GET".equalsIgnoreCase(requestMethod) && !"POST".equalsIgnoreCase(requestMethod)) { return; } visualLogService.createEventLog(VisualLogForm.builder() .modelId(modelId) .dataId(dataId) .type(3) .readScope(readScope) .auditOnly(true) .build()); } catch (Throwable t) { log.warn("[AUDIT] 读取事件发布失败,已跳过。modelId={}, dataId={}, scope={}, error={}", modelId, dataId, readScope, t.getMessage()); } } @Operation(summary = "获取数据列表") @Parameter(name = "modelId", description = "模板id") @PostMapping("/{modelId}/List") public ActionResult>> list(@PathVariable("modelId") String modelId, @RequestBody PaginationModel paginationModel) throws WorkFlowException { if (!isOnlineUtilsOpen(paginationModel.getOnlineUtilsOpen())) { StpUtil.checkPermission(modelId); } paginationModel.setSystemCode(RequestContext.getAppCode()); VisualdevReleaseEntity visualdevEntity = visualdevReleaseService.getById(modelId); VisualDevJsonModel visualJsonModel = OnlinePublicUtils.getVisualJsonModel(visualdevEntity); //判断请求客户端来源 if (!RequestContext.isOrignPc()) { visualJsonModel.setColumnData(visualJsonModel.getAppColumnData()); } ColumnDataModel columnDataModel = visualJsonModel.getColumnData(); List> realList; if (VisualWebTypeEnum.FORM.getType().equals(visualdevEntity.getWebType())) { realList = new ArrayList<>(); } else if (VisualWebTypeEnum.DATA_VIEW.getType().equals(visualdevEntity.getWebType())) {// //数据视图的接口数据获取、 realList = onlineSwapDataUtils.getInterfaceData(visualdevEntity, paginationModel, columnDataModel); } else { this.setFlowVersion(visualJsonModel, paginationModel); realList = visualDevListService.getDataList(visualJsonModel, paginationModel); } //判断数据是否分组 if (OnlineDevData.COLUMNTYPE_THREE.equals(columnDataModel.getType()) && StringUtil.isEmpty(paginationModel.getExtraQueryJson())) { realList = OnlineDevListUtils.groupData(realList, columnDataModel); } //树形列表 if (OnlineDevData.COLUMNTYPE_FIVE.equals(columnDataModel.getType()) && StringUtil.isEmpty(paginationModel.getExtraQueryJson())) { realList = OnlineDevListUtils.treeListData(realList, columnDataModel); } PaginationVO paginationVO = JsonUtil.getJsonToBean(paginationModel, PaginationVO.class); ActionResult>> result = ActionResult.page(realList, paginationVO); publishReadAudit(modelId, null, "LIST"); return result; } private void setFlowVersion(VisualDevJsonModel visualDevJsonModel, PaginationModel paginationModel) { ColumnDataModel columnDataModel = visualDevJsonModel.getColumnData(); assert columnDataModel != null; //是否数据管理(有菜单则不是数据管理) String flowTemplateId = null; boolean canResolveFlowByForm = true; if (ObjectUtil.isNotEmpty(paginationModel.getMenuId())) { //菜单id String menuId = paginationModel.getMenuId(); if (Boolean.TRUE.equals(columnDataModel.getUseColumnPermission())) { visualDevJsonModel.setNeedPermission(true); List pList = new ArrayList<>(); Map pMap = PermissionInterfaceImpl.getColumnMap(); if (pMap.get(menuId) != null) { pList = JsonUtil.getJsonToList(pMap.get(menuId), ColumnModel.class).stream() .map(ColumnModel::getEnCode).collect(Collectors.toList()); } visualDevJsonModel.setPermissionList(pList); } ModuleEntity info = moduleService.getModuleByList(menuId); if (info != null) { //流程菜单 boolean enableFlow = Objects.equals(info.getType(), 9); if (enableFlow) { PropertyJsonModel model = JsonUtil.getJsonToBean(info.getPropertyJson(), PropertyJsonModel.class); flowTemplateId = model.getModuleId(); } else { canResolveFlowByForm = Objects.equals(info.getType(), 1); } } } if (StringUtil.isEmpty(flowTemplateId) && isOnlineUtilsOpen(paginationModel.getOnlineUtilsOpen()) && StringUtil.isNotEmpty(paginationModel.getFlowId())) { flowTemplateId = paginationModel.getFlowId(); } if (StringUtil.isEmpty(flowTemplateId) && canResolveFlowByForm) { flowTemplateId = getFlowTemplateIdByFormId(visualDevJsonModel.getId()); } if (StringUtil.isNotEmpty(flowTemplateId)) { visualDevJsonModel.setEnableFlow(true); visualDevJsonModel.setFlowId(flowTemplateId); visualDevJsonModel.setFlowVersionIds(workFlowApi.getFlowIdsByTemplateId(flowTemplateId)); } } private String getFlowTemplateIdByFormId(String formId) { if (StringUtil.isEmpty(formId)) { return null; } Map flowFormMap = workFlowApi.getFlowFormMap(); if (flowFormMap == null || flowFormMap.isEmpty()) { return null; } return flowFormMap.entrySet().stream() .filter(item -> Objects.equals(formId, item.getValue())) .map(Map.Entry::getKey) .findFirst() .orElse(null); } @Operation(summary = "树形异步查询子列表接口") @Parameter(name = "modelId", description = "模板id") @Parameter(name = "id", description = "数据id") @PostMapping("/{modelId}/List/{id}") public ActionResult>> listTree(@PathVariable("modelId") String modelId, @RequestBody PaginationModel paginationModel, @PathVariable("id") String id) throws WorkFlowException { StpUtil.checkPermission(modelId); VisualdevReleaseEntity visualdevEntity = visualdevReleaseService.getById(modelId); VisualDevJsonModel visualJsonModel = OnlinePublicUtils.getVisualJsonModel(visualdevEntity); //判断请求客户端来源 if (!RequestContext.isOrignPc()) { visualJsonModel.setColumnData(visualJsonModel.getAppColumnData()); } this.setFlowVersion(visualJsonModel, paginationModel); List> realList = visualDevListService.getDataList(visualJsonModel, paginationModel); ColumnDataModel columnDataModel = visualJsonModel.getColumnData(); String parentField = columnDataModel.getParentField() + "_id"; List> collect = realList.stream().filter(item -> id.equals(item.get(parentField))).collect(Collectors.toList()); PaginationVO paginationVO = JsonUtil.getJsonToBean(paginationModel, PaginationVO.class); ActionResult>> result = ActionResult.page(collect, paginationVO); publishReadAudit(modelId, id, "LIST"); return result; } @Operation(summary = "获取列表表单配置JSON") @Parameter(name = "modelId", description = "模板id") @Parameter(name = "type", description = "类型0-草稿,1-发布") @GetMapping("/{modelId}/Config") public ActionResult getData(@PathVariable("modelId") String modelId, @RequestParam(value = "type", required = false) String type, @RequestParam(value = "personal", required = false) Integer personal, @RequestParam(value = "menuId", required = false) String menuId, @RequestParam(value = "onlineUtilsOpen", required = false) Boolean onlineUtilsOpen) { if (!isOnlineUtilsOpen(onlineUtilsOpen)) { StpUtil.checkPermissionOr(modelId, "onlineDev.formDesign", "onlineDev.flowEngine", "generator.webForm", "generator.flowForm"); } //app调用应用的在线开发功能,记录该菜单点击次数 if (!RequestContext.isOrignPc()) { moduleUseNumService.insertOrUpdateUseNum(menuId); } VisualdevEntity entity; //线上版本 if ("0".equals(type)) { entity = visualdevService.getInfo(modelId); } else { VisualdevReleaseEntity releaseEntity = visualdevReleaseService.getById(modelId); entity = JsonUtil.getJsonToBean(releaseEntity, VisualdevEntity.class); } if (entity == null) { return ActionResult.fail(MsgCode.VS412.get()); } String s = VisualUtil.checkPublishVisualModel(entity, MsgCode.VS005.get()); if (s != null) { return ActionResult.fail(s); } DataInfoVO vo = JsonUtil.getJsonToBean(entity, DataInfoVO.class); if (Objects.equals(entity.getType(), 1) && !VisualWebTypeEnum.DATA_VIEW.getType().equals(entity.getWebType())) { vo.setPropsValueList(visualdevService.storedFieldList(entity)); } //页面初始化获取个性化配置 if (Objects.equals(1, personal)) { visualPersonalService.setDataInfoVO(menuId, vo); } return ActionResult.success(vo); } @Operation(summary = "获取表单配置JSON") @Parameter(name = "modelId", description = "模板id") @GetMapping("/{modelId}/FormData") public ActionResult getFormData(@PathVariable("modelId") String modelId) { StpUtil.checkPermission(modelId); VisualdevEntity entity = visualdevService.getInfo(modelId); return ActionResult.success(entity.getFormData()); } @Operation(summary = "领取并锁定单据编号") @Parameter(name = "modelId", description = "模板id") @PostMapping("/{modelId}/BillNumber") public ActionResult lockBillNumber(@PathVariable("modelId") String modelId, @RequestBody Map request) { if (!isOnlineUtilsOpen(Boolean.TRUE.equals(request.get("onlineUtilsOpen")))) { StpUtil.checkPermission(modelId); } String field = ObjectUtil.toString(request.get("field")); if (StringUtil.isEmpty(field)) { return ActionResult.fail("单据字段不能为空"); } VisualdevEntity visualdevEntity = visualdevService.getReleaseInfo(modelId); if (visualdevEntity == null) { return ActionResult.fail(MsgCode.VS412.get()); } Map visualForm = JsonUtil.stringToMap(visualdevEntity.getFormData()); Object fields = visualForm.get("fields"); if (fields instanceof String) { fields = JsonUtil.getJsonToList((String) fields, Map.class); } Map component = findBillRuleComponent(fields, field); if (component == null || !Boolean.TRUE.equals(componentConfig(component).get("lockAndShow"))) { return ActionResult.fail("该单据组件未开启锁定并展示编号"); } FieLdsModel fieLdsModel = JsonUtil.getJsonToBean(component, FieLdsModel.class); Map formData = request.get("data") instanceof Map ? (Map) request.get("data") : new HashMap<>(); Object number; if (Objects.equals(fieLdsModel.getConfig().getRuleType(), 2)) { number = visualBillUtil.getBillNumber(modelId, fieLdsModel, formData, null); } else { number = serviceBaseUtil.getBillNumber(fieLdsModel.getConfig().getRule()); } if (ObjectUtil.isEmpty(number)) { return ActionResult.fail("单据编号领取失败"); } Map result = new HashMap<>(); result.put("field", field); result.put("number", number); return ActionResult.success(result); } @SuppressWarnings("unchecked") private Map findBillRuleComponent(Object nodes, String field) { if (nodes instanceof Collection) { for (Object node : (Collection) nodes) { Map found = findBillRuleComponent(node, field); if (found != null) { return found; } } return null; } if (!(nodes instanceof Map)) { return null; } Map component = (Map) nodes; Map config = componentConfig(component); if (Objects.equals(field, ObjectUtil.toString(component.get("__vModel__"))) && Objects.equals(JnpfKeyConsts.BILLRULE, ObjectUtil.toString(config.get("jnpfKey")))) { return component; } return findBillRuleComponent(config.get("children"), field); } @SuppressWarnings("unchecked") private Map componentConfig(Map component) { Object config = component.get("__config__"); return config instanceof Map ? (Map) config : Collections.emptyMap(); } @Operation(summary = "获取数据信息") @Parameter(name = "modelId", description = "模板id") @GetMapping("/{modelId}/{id}") public ActionResult info(@PathVariable("id") String id, @PathVariable("modelId") String modelId, @RequestParam(name = "menuId", required = false) String menuId, @RequestParam(value = "onlineUtilsOpen", required = false) Boolean onlineUtilsOpen) throws DataException { if (!isOnlineUtilsOpen(onlineUtilsOpen)) { StpUtil.checkPermission(modelId); } VisualdevEntity visualdevEntity = visualdevService.getReleaseInfo(modelId); VisualdevModelDataInfoVO editDataInfo = visualDevInfoService.getEditDataInfo(id, visualdevEntity, OnlineInfoModel.builder().menuId(menuId).build()); publishReadAudit(modelId, id, "DETAIL"); return ActionResult.success(editDataInfo); } @Operation(summary = "获取数据信息(带转换数据)") @Parameter(name = "modelId", description = "模板id") @Parameter(name = "id", description = "数据id") @Parameter(name = "propsValue", description = "存储字段(非必传)") @PostMapping("/{modelId}/DataChange") public ActionResult infoWithDataChange(@PathVariable("modelId") String modelId, @RequestBody VisualInfoParam visualInfoParam) { boolean onlineUtilsOpen = isOnlineUtilsOpen(visualInfoParam.getOnlineUtilsOpen()); if (!onlineUtilsOpen) { StpUtil.checkPermission(modelId); } String id = visualInfoParam.getId() instanceof String ? (String) visualInfoParam.getId() : JsonUtil.getObjectToString(visualInfoParam.getId()); String propsValue = StringUtil.isNotEmpty(visualInfoParam.getPropsValue()) && visualInfoParam.getPropsValue().contains(JnpfConst.FIELD_SUFFIX_JNPFID) ? visualInfoParam.getPropsValue().split(JnpfConst.FIELD_SUFFIX_JNPFID)[0] : visualInfoParam.getPropsValue(); VisualdevEntity visualdevEntity = visualdevService.getReleaseInfo(modelId); String columnData = RequestContext.isOrignPc() ? visualdevEntity.getColumnData() : visualdevEntity.getAppColumnData(); ColumnDataModel columnDataModel = JsonUtil.getJsonToBean(columnData, ColumnDataModel.class); assert columnDataModel != null; if (!onlineUtilsOpen && Boolean.TRUE.equals(columnDataModel.getUseBtnPermission())) { StpUtil.checkPermission(modelId + "::" + PermissionConst.BTN_DETAIL); } VisualdevModelDataInfoVO vo = visualDevInfoService.getDetailsDataInfo(id, visualdevEntity, OnlineInfoModel.builder().needRlationFiled(true).needSwap(true).propsValue(propsValue).menuId(visualInfoParam.getMenuId()).build()); publishReadAudit(modelId, id, "DETAIL"); return ActionResult.success(vo); } @Operation(summary = "添加数据") @Parameter(name = "modelId", description = "模板id") @Parameter(name = "visualdevModelDataCrForm", description = "功能数据创建表单") @PostMapping("/{modelId}") public ActionResult create(@PathVariable("modelId") String modelId, @RequestBody VisualdevModelDataCrForm visualdevModelDataCrForm) throws WorkFlowException { if (!isOnlineUtilsOpen(visualdevModelDataCrForm.getOnlineUtilsOpen())) { StpUtil.checkPermission(modelId); } String menuId = visualdevModelDataCrForm.getMenuId(); VisualdevEntity visualdevEntity = visualdevService.getReleaseInfo(modelId); String columnData = RequestContext.isOrignPc() ? visualdevEntity.getColumnData() : visualdevEntity.getAppColumnData(); ColumnDataModel columnDataModel = JsonUtil.getJsonToBean(columnData, ColumnDataModel.class); if (columnDataModel != null && Boolean.TRUE.equals(columnDataModel.getUseBtnPermission())) { StpUtil.checkPermission(modelId + "::" + PermissionConst.BTN_ADD); } Map map = JsonUtil.stringToMap(visualdevModelDataCrForm.getData()); DataModel dataModel = visualdevModelDataService.visualCreate(VisualParamModel.builder().visualdevEntity(visualdevEntity).data(map) .auditDisplayFields(visualdevModelDataCrForm.getAuditDisplayFields()).menuId(menuId).build()); AsyncExecuteModel model = new AsyncExecuteModel(); model.setModelId(modelId); model.setTrigger(1); model.setDataId(ImmutableList.of(dataModel.getMainId())); model.setUserInfo(UserProvider.getUser()); integrateUtil.asyncExecute(model); return ActionResult.success(MsgCode.SU001.get(), Collections.singletonMap("id", dataModel.getMainId())); } @Operation(summary = "修改数据") @Parameter(name = "modelId", description = "模板id") @Parameter(name = "id", description = "数据id") @Parameter(name = "visualdevModelDataUpForm", description = "功能数据修改表单") @PutMapping("/{modelId}/{id}") public ActionResult update(@PathVariable("id") String id, @PathVariable("modelId") String modelId, @RequestBody VisualdevModelDataUpForm visualdevModelDataUpForm) throws WorkFlowException { if (!isOnlineUtilsOpen(visualdevModelDataUpForm.getOnlineUtilsOpen())) { StpUtil.checkPermission(modelId); } String menuId = visualdevModelDataUpForm.getMenuId(); Map data = JsonUtil.stringToMap(visualdevModelDataUpForm.getData()); VisualdevEntity visualdevEntity = visualdevService.getReleaseInfo(modelId); String columnData = RequestContext.isOrignPc() ? visualdevEntity.getColumnData() : visualdevEntity.getAppColumnData(); ColumnDataModel columnDataModel = JsonUtil.getJsonToBean(columnData, ColumnDataModel.class); if (columnDataModel != null && Boolean.TRUE.equals(columnDataModel.getUseBtnPermission())) { StpUtil.checkPermission(modelId + "::" + PermissionConst.BTN_EDIT); } DataModel dataModel = visualdevModelDataService.visualUpdate(VisualParamModel.builder().visualdevEntity(visualdevEntity).data(data) .auditDisplayFields(visualdevModelDataUpForm.getAuditDisplayFields()).menuId(menuId).id(id).build()); AsyncExecuteModel model = new AsyncExecuteModel(); model.setModelId(modelId); model.setTrigger(2); model.setDataId(ImmutableList.of(id)); model.setDataModel(dataModel); model.setUserInfo(UserProvider.getUser()); integrateUtil.asyncExecute(model); return ActionResult.success(MsgCode.SU004.get()); } //接口废弃全部走批量接口 @Operation(summary = "删除数据") @Parameter(name = "modelId", description = "模板id") @Parameter(name = "id", description = "数据id") @DeleteMapping("/{modelId}/{id}") public ActionResult delete(@PathVariable("id") String id, @PathVariable("modelId") String modelId) throws Exception { // AUDIT 补丁(2026-08-05,登记 tasks/jnpf-platform-patches.md #7): // 树形表单删一个父节点会递归删掉全部子节点,这些删除属于**同一次操作**, // 用同一个 batchOpId 串起来,查询侧才折叠得成一行(与 batchDelete 的 BD- 同机制, // 前缀用 TD- 区分树形删除)。 String batchOpId = "TD-" + RandomUtil.uuId(); return deleteInternal(id, modelId, batchOpId); } /** * AUDIT 补丁(2026-08-05,#7):{@link #delete} 的真身,多带一个 batchOpId 供树形递归共享。 * *

拆出私有方法而不是给 public 端点加参数:端点签名是对外契约,不能因为审计需要就改。 */ private ActionResult deleteInternal(String id, String modelId, String batchOpId) throws Exception { StpUtil.checkPermission(modelId); VisualdevEntity visualdevEntity = visualdevService.getReleaseInfo(modelId); String columnData = RequestContext.isOrignPc() ? visualdevEntity.getColumnData() : visualdevEntity.getAppColumnData(); ColumnDataModel columnDataModel = JsonUtil.getJsonToBean(columnData, ColumnDataModel.class); assert columnDataModel != null; if (Boolean.TRUE.equals(columnDataModel.getUseBtnPermission())) { StpUtil.checkPermissionOr(modelId + "::" + PermissionConst.BTN_REMOVE, modelId + "::" + PermissionConst.BTN_BATCHREMOVE); } VisualDevJsonModel visualJsonModel = OnlinePublicUtils.getVisualJsonModel(visualdevEntity); //判断请求客户端来源 if (!RequestContext.isOrignPc()) { visualJsonModel.setColumnData(visualJsonModel.getAppColumnData()); } if (!StringUtil.isEmpty(visualdevEntity.getVisualTables()) && !OnlineDevData.TABLE_CONST.equals(visualdevEntity.getVisualTables())) { //树形递归删除 if (OnlineDevData.COLUMNTYPE_FIVE.equals(visualJsonModel.getColumnData().getType())) { try { ActionResult>> listTreeAction = listTree(modelId, new PaginationModel(), id); if (listTreeAction != null && listTreeAction.getCode() == 200 && listTreeAction.getData() instanceof Object) { Map map = JsonUtil.getJsonToBean(listTreeAction.getData(), Map.class); List> list = JsonUtil.getJsonToListMap(map.get("list").toString()); if (!list.isEmpty()) { for (Map item : list) { // AUDIT 补丁(#7):把 batchOpId 一路带进递归,父子节点归到同一次操作。 this.deleteInternal(item.get("id").toString(), modelId, batchOpId); } } } } catch (Exception e) { e.printStackTrace(); log.error("子数据删除异常:{}", e.getMessage()); } } visualDevInfoService.getEditDataInfo(id, visualdevEntity, OnlineInfoModel.builder().build()); // ── AUDIT 补丁(2026-08-05,登记 tasks/jnpf-platform-patches.md #7)第 1/2 段 ── // 🔴 顺序是硬约束:快照必须在 tableDelete **之前**取——删完再查什么都没有。 Map auditSnapshot = null; try { auditSnapshot = auditDeleteSnapshot(id, visualdevEntity); } catch (Throwable t) { log.warn("[AUDIT] 单条删除取快照失败,本次删除不带内容快照。modelId={}, id={}, error={}", modelId, id, t.getMessage()); } // ── 第 1/2 段结束 ──────────────────────────────────────────────────────── // AUDIT 补丁(2026-08-05,#7):改前 tableDelete 恒真,删一个不存在的 id 也会回「删除成功」。 FormDeleteResult delResult = visualdevModelDataService.tableDelete(id, visualJsonModel); if (delResult != null && delResult.isDeleted()) { // ── AUDIT 补丁 第 2/2 段 ─────────────────────────────────────────── // 不设 batchSize:递归过程中子节点数量未知,填错会让标题缀出错误的「共 N 条」。 try { visualLogService.createEventLog(VisualLogForm.builder() .modelId(modelId) .dataId(id) .newData(auditSnapshot) .type(2) .batchOpId(batchOpId) .build()); } catch (Throwable t) { log.warn("[AUDIT] 单条删除事件发布失败,已跳过。modelId={}, id={}, error={}", modelId, id, t.getMessage()); } // ── AUDIT 补丁结束 ──────────────────────────────────────────────── AsyncExecuteModel model = new AsyncExecuteModel(); model.setModelId(modelId); model.setTrigger(3); model.setDataId(ImmutableList.of(id)); model.setUserInfo(UserProvider.getUser()); integrateUtil.asyncExecute(model); return ActionResult.success(MsgCode.SU003.get()); } else { return ActionResult.fail(MsgCode.FA003.get()); } } return ActionResult.fail(MsgCode.FA003.get()); } @Operation(summary = "批量删除数据") @Parameter(name = "modelId", description = "模板id") @Parameter(name = "idsVo", description = "批量处理参数") @PostMapping("/batchDelete/{modelId}") public ActionResult batchDelete(@RequestBody BatchRemoveIdsVo idsVo, @PathVariable("modelId") String modelId) throws Exception { StpUtil.checkPermission(modelId); VisualdevEntity visualdevEntity = visualdevService.getReleaseInfo(modelId); String columnData = RequestContext.isOrignPc() ? visualdevEntity.getColumnData() : visualdevEntity.getAppColumnData(); ColumnDataModel columnDataModel = JsonUtil.getJsonToBean(columnData, ColumnDataModel.class); assert columnDataModel != null; if (Boolean.TRUE.equals(columnDataModel.getUseBtnPermission())) { StpUtil.checkPermissionOr(modelId + "::" + PermissionConst.BTN_REMOVE, modelId + "::" + PermissionConst.BTN_BATCHREMOVE); } VisualDevJsonModel visualJsonModel = OnlinePublicUtils.getVisualJsonModel(visualdevEntity); //判断请求客户端来源 if (!RequestContext.isOrignPc()) { visualJsonModel.setColumnData(visualJsonModel.getAppColumnData()); } List idsList = new ArrayList<>(); List idsVoList = Arrays.asList(idsVo.getIds()); String errMess = ""; List> dataMap = new ArrayList<>(); for (String id : idsVoList) { VisualdevModelDataInfoVO editDataInfo = visualDevInfoService.getEditDataInfo(id, visualdevEntity, OnlineInfoModel.builder().build()); Map map = JsonUtil.stringToMap(editDataInfo.getData()); dataMap.add(map); if (StringUtils.isNotBlank(idsVo.getFlowId())) { TaskEntity taskEntity = workFlowApi.getInfoSubmit(map.get(FlowFormConstant.FLOWTASKID).toString()); if (taskEntity != null) { ActionResult result = workFlowApi.delete(taskEntity); if (Objects.equals(result.getCode(), 200)) { idsList.add(id); } else { errMess = result.getMsg(); } } else { idsList.add(id); } } else { idsList.add(id); } } if (idsList.isEmpty()) { return ActionResult.fail(errMess); } if (!StringUtil.isEmpty(visualdevEntity.getVisualTables()) && !OnlineDevData.TABLE_CONST.equals(visualdevEntity.getVisualTables())) { AsyncExecuteModel model = new AsyncExecuteModel(); model.setModelId(modelId); model.setTrigger(3); model.setDataId(idsList); model.setUserInfo(UserProvider.getUser()); List triggerDataModels = integrateUtil.asyncDelExecute(model); // ── AUDIT 补丁(2026-08-01,登记 tasks/jnpf-platform-patches.md #6)第 1/2 段 ── // 🔴 顺序是硬约束:快照必须在 tableDeleteMore **之前**取——删完再查什么都没有。 // 只收集不发送,等删除确认成功后再发(删除失败就不该有审计事件)。 Map> auditSnapshots = new LinkedHashMap>(); try { int taken = 0; for (String id : idsList) { if (taken < AUDIT_SNAPSHOT_LIMIT) { auditSnapshots.put(id, auditDeleteSnapshot(id, visualdevEntity)); taken++; } else { // 超出上限:不查库,但 id 仍进 map —— 发送循环照样为它发事件 auditSnapshots.put(id, null); } } } catch (Throwable t) { log.warn("[AUDIT] 收集删除快照失败,本次删除不产出审计事件。modelId={}, error={}", modelId, t.getMessage()); auditSnapshots.clear(); } // ── 第 1/2 段结束 ──────────────────────────────────────────────────────────── ActionResult result = visualdevModelDataService.tableDeleteMore(idsList, visualJsonModel); // ── AUDIT 补丁 第 2/2 段 ───────────────────────────────────────────────────── // 删除是审计里最需要追溯的动作,而平台原先只在 create/update 发 VSLOG 事件。 // **逐条发**:审计按单条粒度记,target_id 才填得准;共享 batchOpId 让查询侧 // 折叠成一行。整段 try 兜底:审计是旁路,广播失败绝不能影响删除结果。 // Objects.equals 而非 ==:ActionResult#getCode() 是 Integer,== 会拆箱,result.getCode() // 为 null 时直接 NPE——那会逃出这个新增审计代码所在的 if,把已经成功的删除变成 500 响应。 // 同一文件里 workFlowApi.delete 那处判断(本方法上方 Objects.equals(result.getCode(), 200)) // 已经是这个写法,这里跟随既有风格。 if (result != null && Objects.equals(result.getCode(), 200) && !auditSnapshots.isEmpty()) { try { // AUDIT 补丁(2026-08-06,登记 tasks/jnpf-platform-patches.md #7): // 只为**真的删掉了**的 id 发事件。补丁 #6 这里是按 auditSnapshots 逐个发的, // 不看每条到底删没删到——tableDelete 当时恒返回 true,删一个根本不存在的 id // 也会产出一条「删除了它」的**假审计记录**(2026-08-06 端到端实测到)。 // 补丁 #7 之后 tableDeleteMore 把真删掉的 id 写进了 dataIdList,这里读回来过滤。 // 🔴 fail-open on 事实:拿不到列表(null)时**不过滤**——宁可多一条, // 也不能把「这次删过」的事实吞掉(与 auditDeleteSnapshot 的取不到返回 null 同精神)。 List deletedIds = visualJsonModel.getDataIdList(); Map> toEmit = auditSnapshots; if (deletedIds != null) { toEmit = new LinkedHashMap>(); for (Map.Entry> entry : auditSnapshots.entrySet()) { if (deletedIds.contains(entry.getKey())) { toEmit.put(entry.getKey(), entry.getValue()); } } } String batchOpId = "BD-" + RandomUtil.uuId(); int batchSize = toEmit.size(); for (Map.Entry> entry : toEmit.entrySet()) { visualLogService.createEventLog(VisualLogForm.builder() .modelId(modelId) .dataId(entry.getKey()) .newData(entry.getValue()) .type(2) .batchOpId(batchOpId) .batchSize(batchSize) .bizSign(idsVo.getBizSign()) .build()); } } catch (Throwable t) { log.warn("[AUDIT] 删除事件发布失败,已跳过。modelId={}, size={}, error={}", modelId, auditSnapshots.size(), t.getMessage()); } } // ── AUDIT 补丁结束 ────────────────────────────────────────────────────────── if (!triggerDataModels.isEmpty()) { model.setDataMap(dataMap); integrateUtil.asyncExecute(model); } return result; } return ActionResult.fail(MsgCode.FA003.get()); } @Operation(summary = "导入数据") @Parameter(name = "modelId", description = "模板id") @Parameter(name = "visualImportModel", description = "导入参数") @PostMapping("{modelId}/ImportData") public ActionResult imports(@PathVariable("modelId") String modelId, @RequestBody VisualImportModel visualImportModel) throws Exception { StpUtil.checkPermission(modelId); VisualdevEntity visualdevEntity = visualdevService.getReleaseInfo(modelId); ColumnDataModel columnDataModel = JsonUtil.getJsonToBean(visualdevEntity.getColumnData(), ColumnDataModel.class); assert columnDataModel != null; if (Boolean.TRUE.equals(columnDataModel.getUseBtnPermission())) { StpUtil.checkPermission(modelId + "::" + PermissionConst.BTN_DOWNLOAD); } VisualDevJsonModel visualJsonModel = OnlinePublicUtils.getVisualJsonModel(visualdevEntity); FormDataModel formData = visualJsonModel.getFormData(); List fieldsModelList = JsonUtil.getJsonToList(formData.getFields(), FieLdsModel.class); List allFieLds = new ArrayList<>(); VisualUtils.recursionFields(fieldsModelList, allFieLds); visualJsonModel.setFormListModels(allFieLds); visualJsonModel.setFlowId(visualImportModel.getFlowId()); //复杂表头数据 还原成普通数据 List> listData = new ArrayList<>(); List> headerRow = new ArrayList<>(); if (visualImportModel.isType()) { ActionResult> result = importPreview(modelId, visualImportModel.getFileName()); if (result == null) { throw new DataException(MsgCode.FA018.get()); } if (result.getCode() != 200) { return ActionResult.fail(result.getMsg()); } if (result.getData() instanceof Map) { Map data = result.getData(); listData = (List>) data.get("dataRow"); headerRow = (List>) data.get("headerRow"); } } else { listData = visualImportModel.getList(); } List> mapList = VisualUtils.complexImportsDataOnline(listData, visualdevEntity); ExcelImportModel excelData = onlineExcelUtil.createExcelData(mapList, visualJsonModel, visualdevEntity); //复杂表头-表头和数据处理 List complexHeaderList = columnDataModel.getComplexHeaderList(); if (!Objects.equals(columnDataModel.getType(), 3) && !Objects.equals(columnDataModel.getType(), 5)) { List> mapList1 = VisualUtils.complexHeaderDataHandel(excelData.getFailResult(), complexHeaderList, false); excelData.setFailResult(mapList1); } excelData.setHeaderRow(headerRow); return ActionResult.success(excelData); } @Operation(summary = "导出") @Parameter(name = "modelId", description = "模板id") @Parameter(name = "paginationModelExport", description = "导出参数") @PostMapping("/{modelId}/Actions/ExportData") public ActionResult export(@PathVariable("modelId") String modelId, @RequestBody PaginationModelExport paginationModelExport) throws ParseException, IOException, SQLException, DataException { StpUtil.checkPermission(modelId); paginationModelExport.setSystemCode(RequestContext.getAppCode()); ModuleEntity menuInfo = moduleService.getModuleByList(paginationModelExport.getMenuId()); VisualdevEntity visualdevEntity = visualdevService.getReleaseInfo(modelId); if (visualdevEntity == null) { return ActionResult.fail(MsgCode.FA001.get()); } String excelName = ""; if (menuInfo != null) { excelName = menuInfo.getFullName(); } else { excelName = visualdevEntity.getFullName(); } ColumnDataModel columnDataModel = JsonUtil.getJsonToBean(visualdevEntity.getColumnData(), ColumnDataModel.class); assert columnDataModel != null; if (Boolean.TRUE.equals(columnDataModel.getUseBtnPermission())) { StpUtil.checkPermission(modelId + "::" + PermissionConst.BTN_DOWNLOAD); } VisualDevJsonModel visualJsonModel = OnlinePublicUtils.getVisualJsonModel(visualdevEntity); //判断请求客户端来源 if (!RequestContext.isOrignPc()) { visualJsonModel.setColumnData(visualJsonModel.getAppColumnData()); } String[] keys = paginationModelExport.getSelectKey(); List selectKey = Arrays.asList(paginationModelExport.getSelectKey()); List selectIds = Arrays.asList(paginationModelExport.getSelectIds()); //关键字过滤 List> realList; DownloadVO vo; if (VisualWebTypeEnum.DATA_VIEW.getType().equals(visualdevEntity.getWebType())) {//视图查询数据 VisualdevReleaseEntity visualdevREntity = JsonUtil.getJsonToBean(visualdevEntity, VisualdevReleaseEntity.class); realList = onlineSwapDataUtils.getInterfaceData(visualdevREntity, paginationModelExport, visualJsonModel.getColumnData()); if ("2".equals(paginationModelExport.getDataType()) && StringUtils.isBlank(columnDataModel.getViewKey())) { ActionResult.fail(MsgCode.VS029.get()); } realList = "2".equals(paginationModelExport.getDataType()) ? realList.stream().filter(t -> selectIds.contains(t.get(columnDataModel.getViewKey()))).collect(Collectors.toList()) : realList; vo = VisualUtils.createModelExcelApiData(visualdevEntity.getColumnData(), realList, Arrays.asList(keys), "表单信息", excelName, new ExcelModel()); } else { ExcelModel excelModel = onlineSwapDataUtils.getDefaultValue(visualdevEntity.getFormData(), selectKey); realList = visualdevModelDataService.exportData(keys, paginationModelExport, visualJsonModel); realList = "2".equals(paginationModelExport.getDataType()) ? realList.stream().filter(t -> selectIds.contains(t.get("id"))).collect(Collectors.toList()) : realList; vo = VisualUtils.createModelExcel(visualdevEntity, realList, Arrays.asList(keys), "表单信息", excelName, excelModel); } return ActionResult.success(vo); } @Operation(summary = "功能导出") @Parameter(name = "modelId", description = "模板id") @PostMapping("/{modelId}/Actions/Export") @SaCheckPermission("onlineDev.formDesign") public ActionResult exportData(@PathVariable("modelId") String modelId) { VisualdevEntity visualdevEntity = visualdevService.getReleaseInfo(modelId); VisualExportVo vo = JsonUtil.getJsonToBean(visualdevEntity, VisualExportVo.class); List list = aliasService.getList(visualdevEntity.getId()); vo.setAliasListJson(JsonUtil.getObjectToString(list)); vo.setModelType(ExportModelTypeEnum.DESIGN.getMessage()); DownloadVO downloadVO = fileApi.exportFile(new ExportModel(vo, FileTypeConstant.TEMPORARY, UserProvider.getToken(), visualdevEntity.getFullName(), ModuleTypeEnum.VISUAL_DEV.getTableName())); return ActionResult.success(downloadVO); } @Operation(summary = "功能导入") @PostMapping(value = "/Actions/Import", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @SaCheckPermission("onlineDev.formDesign") public ActionResult importData(@RequestParam("type") Integer type, @RequestPart("file") MultipartFile multipartFile) { SystemEntity sysInfo = systemService.getInfoByEnCode(RequestContext.getAppCode()); if (sysInfo == null) { return ActionResult.fail(MsgCode.FA001.get()); } //判断是否为.json结尾 if (FileUtil.existsSuffix(multipartFile, ModuleTypeEnum.VISUAL_DEV.getTableName())) { return ActionResult.fail(MsgCode.IMP002.get()); } //获取文件内容 String fileContent = FileUtil.getFileContent(multipartFile); VisualExportVo vo = JsonUtil.getJsonToBean(fileContent, VisualExportVo.class); VisualdevEntity visualdevEntity = JsonUtil.getJsonToBean(vo, VisualdevEntity.class); if (!sysInfo.getId().equals(visualdevEntity.getSystemId())) { visualdevEntity.setId(RandomUtil.uuId()); visualdevService.setAutoEnCode(visualdevEntity); visualdevEntity.setSystemId(sysInfo.getId()); } StringJoiner errList = new StringJoiner("、"); String copyNum = UUID.randomUUID().toString().substring(0, 5); if (visualdevService.getInfo(visualdevEntity.getId()) != null) { if (Objects.equals(type, 0)) { errList.add("ID"); } else { visualdevEntity.setId(RandomUtil.uuId()); } } if (visualdevService.getObjByEncode(visualdevEntity.getEnCode(), visualdevEntity.getType())) { if (Objects.equals(type, 0)) { errList.add(MsgCode.IMP009.get()); } else { visualdevEntity.setEnCode(visualdevEntity.getEnCode() + copyNum); } } if (visualdevService.getCountByName(visualdevEntity.getFullName(), visualdevEntity.getType(), sysInfo.getId())) { if (Objects.equals(type, 0)) { errList.add(MsgCode.IMP008.get()); } else { visualdevEntity.setFullName(visualdevEntity.getFullName() + ".副本" + copyNum); } } if (Objects.equals(type, 0) && errList.length() > 0) { return ActionResult.fail(errList + MsgCode.IMP007.get()); } if (visualdevEntity.getId() != null) { visualdevService.setIgnoreLogicDelete().removeById(visualdevEntity.getId()); visualdevService.clearIgnoreLogicDelete(); } if (Objects.equals(visualdevEntity.getType(), 1)) { visualdevEntity.setDbLinkId("0"); } visualdevEntity.setCreatorTime(DateUtil.getNowDate()); visualdevEntity.setCreatorUserId(UserProvider.getUser().getUserId()); visualdevEntity.setLastModifyTime(null); visualdevEntity.setLastModifyUserId(null); visualdevEntity.setState(0); visualdevService.save(visualdevEntity); if (StringUtil.isNotEmpty(vo.getAliasListJson())) { List jsonToList = JsonUtil.getJsonToList(vo.getAliasListJson(), VisualAliasEntity.class); for (VisualAliasEntity aliasEntity : jsonToList) { aliasService.copyEntity(aliasEntity, visualdevEntity.getId()); } } return ActionResult.success(MsgCode.IMP001.get()); } @Operation(summary = "模板下载") @Parameter(name = "modelId", description = "模板id") @Parameter(name = "menuId", description = "菜单id") @GetMapping("/{modelId}/TemplateDownload") public ActionResult templateDownload(@PathVariable("modelId") String modelId, @RequestParam(value = "menuId", required = false) String menuId) { StpUtil.checkPermission(modelId); ModuleEntity menuInfo = moduleService.getModuleByList(menuId); VisualdevEntity visualdevEntity = visualdevService.getReleaseInfo(modelId); if (visualdevEntity == null) { return ActionResult.fail(MsgCode.FA001.get()); } String menuFullName = ""; if (menuInfo != null) { menuFullName = menuInfo.getFullName(); } else { menuFullName = visualdevEntity.getFullName(); } ColumnDataModel columnDataModel = JsonUtil.getJsonToBean(visualdevEntity.getColumnData(), ColumnDataModel.class); assert columnDataModel != null; if (Boolean.TRUE.equals(columnDataModel.getUseBtnPermission())) { StpUtil.checkPermission(modelId + "::" + PermissionConst.BTN_DOWNLOAD); } UploaderTemplateModel uploaderTemplateModel = JsonUtil.getJsonToBean(columnDataModel.getUploaderTemplateJson(), UploaderTemplateModel.class); List selectKey = uploaderTemplateModel.getSelectKey(); ExcelModel excelModel = onlineSwapDataUtils.getDefaultValue(visualdevEntity.getFormData(), selectKey); List> dataList = new ArrayList<>(); dataList.add(excelModel.getDataMap()); DownloadVO vo = VisualUtils.createModelExcel(visualdevEntity, dataList, selectKey, "导入模板", menuFullName + "导入模板", excelModel); return ActionResult.success(vo); } @Operation(summary = "上传文件") @PostMapping("/Uploader") public ActionResult uploader() { List list = UpUtil.getFileAll(); MultipartFile file = list.get(0); if (file != null) { String originalFilename = file.getOriginalFilename(); if (originalFilename != null && (originalFilename.endsWith(".xlsx") || originalFilename.endsWith(".xls"))) { String filePath = XSSEscape.escape(configValueUtil.getTemporaryFilePath()); String fileName = XSSEscape.escape(RandomUtil.uuId() + "." + UpUtil.getFileType(file)); //上传文件 FileInfo fileInfo = fileUploadApi.uploadFile(file, filePath, fileName); DownloadVO vo = DownloadVO.builder().build(); vo.setName(fileInfo.getFilename()); return ActionResult.success(vo); } } return ActionResult.fail(MsgCode.ETD110.get()); } @Operation(summary = "导入预览") @Parameter(name = "modelId", description = "模板id") @Parameter(name = "fileName", description = "文件名") @GetMapping("/{modelId}/ImportPreview") public ActionResult> importPreview(@PathVariable("modelId") String modelId, String fileName) { StpUtil.checkPermission(modelId); Map previewMap = null; try { VisualdevReleaseEntity entity = visualdevReleaseService.getById(modelId); ColumnDataModel columnDataModel = JsonUtil.getJsonToBean(entity.getColumnData(), ColumnDataModel.class); assert columnDataModel != null; if (Boolean.TRUE.equals(columnDataModel.getUseBtnPermission())) { StpUtil.checkPermission(modelId + "::" + PermissionConst.BTN_UPLOAD); } FormDataModel formDataModel = JsonUtil.getJsonToBean(entity.getFormData(), FormDataModel.class); UploaderTemplateModel uploaderTemplateModel = JsonUtil.getJsonToBean(columnDataModel.getUploaderTemplateJson(), UploaderTemplateModel.class); List fieLdsModels = JsonUtil.getJsonToList(formDataModel.getFields(), FieLdsModel.class); List allFields = new ArrayList<>(); OnlinePublicUtils.recursionFormFields(allFields, fieLdsModels); List selectKey = uploaderTemplateModel.getSelectKey(); String filePath = fileApi.getLocalBasePath() + configValueUtil.getTemporaryFilePath(); try (Response response = fileUploadApi.downFile(new UploadFileModel(configValueUtil.getTemporaryFilePath(), configValueUtil.getTemporaryFilePath(), fileName)); InputStream inputStream = response.body().asInputStream()) { cn.hutool.core.io.FileUtil.copyFile(inputStream, new File(filePath + fileName), StandardCopyOption.REPLACE_EXISTING); } catch (IOException e) { throw new DataException(e.getMessage()); } File temporary = new File(XSSEscape.escapePath(filePath + fileName));//判断有无子表 String tablefield = selectKey.stream().filter(s -> s.toLowerCase().startsWith(JnpfKeyConsts.CHILD_TABLE_PREFIX)).findFirst().orElse(null); //判断有没有复杂表头 boolean hasComplex = false; for (HeaderModel item : columnDataModel.getComplexHeaderList()) { if (item.getChildColumns() != null && !item.getChildColumns().isEmpty()) { List childColumns = new ArrayList<>(item.getChildColumns()); childColumns.retainAll(selectKey); if (!childColumns.isEmpty()) { hasComplex = true; } } } //有子表需要取第二行的表头 Integer i = tablefield != null || hasComplex ? 2 : 1; //读取excel中数据 InputStream inputStream = ExcelUtil.solveOrginTitle(temporary, i); List> excelDataList = ExcelUtil.getMapByInputStream(inputStream, 0, i); //数据超过100条 if (excelDataList != null && excelDataList.size() > 1000) { return ActionResult.fail(MsgCode.ETD117.get()); } ExcelUtil.imoportExcelToMap(temporary, i, excelDataList); //列表字段 List> columns = getColumns(selectKey, allFields); List> results = FormExecelUtils.dataMergeChildTable(excelDataList, selectKey); previewMap = new HashMap<>(); //复杂表头-表头和数据处理 List complexHeaderList = columnDataModel.getComplexHeaderList(); if (!Objects.equals(columnDataModel.getType(), 3) && !Objects.equals(columnDataModel.getType(), 5)) { columns = VisualUtils.complexHeaderHandelOnline(columns, complexHeaderList); } previewMap.put("dataRow", results); previewMap.put("headerRow", columns); } catch (Exception e) { e.printStackTrace(); return ActionResult.fail(MsgCode.VS407.get()); } return ActionResult.success(previewMap); } private static @NotNull List> getColumns(List selectKey, List allFields) { List> columns = new ArrayList<>(); List chiImList = new ArrayList<>(); List allImList = new ArrayList<>(); selectKey.stream().forEach(s -> { String requiredStr = ""; ImportExcelFieldModel importExcel = new ImportExcelFieldModel(); if (s.toLowerCase().startsWith(JnpfKeyConsts.CHILD_TABLE_PREFIX)) { String table = s.substring(0, s.indexOf("-")); String field = s.substring(s.indexOf("-") + 1); FieLdsModel fieLdsModel = allFields.stream().filter(t -> t.getVModel().equals(table)).findFirst().orElse(null); List children = fieLdsModel.getConfig().getChildren(); FieLdsModel fieLdsModel1 = children.stream().filter(t -> t.getVModel().equals(field)).findFirst().orElse(null); requiredStr = fieLdsModel1.getConfig().isRequired() ? "*" : ""; importExcel.setField(field); importExcel.setTableField(table); importExcel.setFullName(requiredStr + fieLdsModel1.getConfig().getLabel()); importExcel.setJnpfKey(fieLdsModel1.getConfig().getJnpfKey()); chiImList.add(importExcel); } else { FieLdsModel fieLdsModel = allFields.stream().filter(t -> t.getVModel().equals(s)).findFirst().orElse(null); requiredStr = fieLdsModel.getConfig().isRequired() ? "*" : ""; importExcel.setField(s); importExcel.setFullName(requiredStr + fieLdsModel.getConfig().getLabel()); importExcel.setJnpfKey(fieLdsModel.getConfig().getJnpfKey()); allImList.add(importExcel); } }); Map> groups = chiImList.stream().collect(Collectors.groupingBy(ImportExcelFieldModel::getTableField, LinkedHashMap::new, Collectors.toList())); for (Map.Entry> entry : groups.entrySet()) { ImportExcelFieldModel importExcel = new ImportExcelFieldModel(); List value = entry.getValue(); FieLdsModel fieLdsModel = allFields.stream().filter(f -> entry.getKey().equals(f.getVModel())).findFirst().orElse(null); if (fieLdsModel == null) { throw new DataException("字段不存在"); } String tableName = fieLdsModel.getConfig().getLabel(); importExcel.setField(entry.getKey()); importExcel.setFullName(tableName); importExcel.setJnpfKey("table"); importExcel.setChildren(value); allImList.add(importExcel); } for (ImportExcelFieldModel importExcel : allImList) { Map selectMap = new HashMap<>(16); selectMap.put("id", importExcel.getField()); selectMap.put("fullName", importExcel.getFullName()); selectMap.put("jnpfKey", importExcel.getJnpfKey()); if (importExcel.getChildren() != null) { List children = importExcel.getChildren(); List> childMapList = new ArrayList<>(); for (ImportExcelFieldModel childIm : children) { Map childMap = new HashMap<>(16); childMap.put("id", childIm.getField()); childMap.put("fullName", childIm.getFullName()); childMap.put("jnpfKey", childIm.getJnpfKey()); childMapList.add(childMap); } selectMap.put("children", childMapList); } columns.add(selectMap); } return columns; } @Operation(summary = "导出异常报告") @Parameter(name = "modelId", description = "模板id") @Parameter(name = "visualImportModel", description = "导出参数") @PostMapping("/{modelId}/ImportExceptionData") public ActionResult importExceptionData(@PathVariable("modelId") String modelId, @RequestBody VisualImportModel visualImportModel) { StpUtil.checkPermission(modelId); String menuFullName = ""; if (StringUtil.isNotEmpty(visualImportModel.getMenuId())) { ModuleEntity menuInfo = moduleService.getModuleByList(visualImportModel.getMenuId()); if (menuInfo != null && StringUtil.isNotEmpty(menuInfo.getFullName())) { menuFullName = menuInfo.getFullName(); } } VisualdevEntity visualdevEntity = visualdevService.getReleaseInfo(modelId); ColumnDataModel columnDataModel = JsonUtil.getJsonToBean(visualdevEntity.getColumnData(), ColumnDataModel.class); assert columnDataModel != null; if (Boolean.TRUE.equals(columnDataModel.getUseBtnPermission())) { StpUtil.checkPermission(modelId + "::" + PermissionConst.BTN_DOWNLOAD); } UploaderTemplateModel uploaderTemplateModel = JsonUtil.getJsonToBean(columnDataModel.getUploaderTemplateJson(), UploaderTemplateModel.class); List selectKey = uploaderTemplateModel.getSelectKey(); ExcelModel excelModel = onlineSwapDataUtils.getDefaultValue(visualdevEntity.getFormData(), selectKey); DownloadVO vo = VisualUtils.createModelExcel(visualdevEntity, visualImportModel.getList(), selectKey, "错误报告", menuFullName + "错误报告", excelModel); return ActionResult.success(vo); } @Operation(summary = "自定义按钮发起审批") @Parameter(name = "modelId", description = "模板id") @Parameter(name = "visualImportModel", description = "导出参数") @PostMapping("/{modelId}/actionLaunchFlow") public ActionResult actionLaunchFlow(@PathVariable("modelId") String modelId, @RequestBody FlowLaunchModel model) { StpUtil.checkPermission(modelId); VisualdevEntity visualdevEntity = visualdevService.getReleaseInfo(modelId); ColumnDataModel columnDataModel = JsonUtil.getJsonToBean(visualdevEntity.getColumnData(), ColumnDataModel.class); assert columnDataModel != null; if (Boolean.TRUE.equals(columnDataModel.getUseBtnPermission())) { StpUtil.checkPermission(modelId + "::" + model.getBtnCode()); } //用户列表 List userList = new ArrayList<>(); if (Objects.equals(1, model.getCurrentUser())) { userList.add(UserProvider.getUser().getUserId()); } if (Objects.equals(1, model.getCustomUser()) && CollUtil.isNotEmpty(model.getInitiator())) { userList.addAll(userService.getUserIdList(model.getInitiator())); userList = new ArrayList<>(new HashSet<>(userList)); } //数据列表 List> formDataList = new ArrayList<>(); List> dataList = model.getDataList(); if (CollUtil.isNotEmpty(dataList)) { transforData(dataList, formDataList); } FlowModel flowModel = new FlowModel(); flowModel.setTemplateId(model.getTemplate()); flowModel.setUserIds(userList); flowModel.setFormDataList(formDataList); flowModel.setHasPermission(model.getHasPermission()); ActionResult actionResult = taskApi.launchFlow(flowModel); if (actionResult == null || !ActionResultCode.SUCCESS.getCode().equals(actionResult.getCode())) { return ActionResult.fail(actionResult == null ? "" : actionResult.getMsg()); } return ActionResult.success(actionResult.getMsg()); } //转换数据 private static void transforData(List> dataList, List> formDataList) { for (List itemList : dataList) { if (CollUtil.isNotEmpty(itemList)) { Map map = new HashMap<>(); Map>> tableList = new HashMap<>(); for (TransferModel transferModel : itemList) { String[] table = transferModel.getTargetField().split("-"); String[] value = transferModel.getSourceValue().split("-"); List> list = tableList.get(table[0]) != null ? tableList.get(table[0]) : new ArrayList<>(); if (table.length > 1) { if (value.length > 1) { if (transferModel.getDefaultValue() instanceof List) { List data = (List) transferModel.getDefaultValue(); int num = data.size() - list.size(); for (int i = 0; i < num; i++) { list.add(new HashMap<>()); } for (int i = 0; i < data.size(); i++) { Map objectMap = list.get(i); objectMap.put(table[1], data.get(i)); } } } else { if (list.isEmpty()) { list.add(new HashMap<>()); } for (Map objectMap : list) { objectMap.put(table[1], transferModel.getDefaultValue()); } } tableList.put(table[0], list); } map.put(transferModel.getTargetField(), transferModel.getDefaultValue()); } map.putAll(tableList); formDataList.add(map); } } } @Operation(summary = "根据菜单获取功能配置(流程是直接通过菜单确定表单)") @Parameter(name = "menuId", description = "菜单id") @GetMapping("/Config") public ActionResult getConfigByMenu(@RequestParam(value = "menuId", required = false) String menuId, @RequestParam(value = "systemId", required = false) String systemId, @RequestParam(value = "onlineUtilsOpen", required = false) Boolean onlineUtilsOpen) { if (!isOnlineUtilsOpen(onlineUtilsOpen)) { StpUtil.checkPermissionOr(menuId, "onlineDev.formDesign", "generator.webForm", "generator.flowForm"); } //app调用应用的在线开发功能,记录该菜单点击次数 if (!RequestContext.isOrignPc()) { moduleUseNumService.insertOrUpdateUseNum(menuId); } VisualdevReleaseEntity releaseEntity = null; ModuleEntity info = moduleService.getModuleByList(menuId); if (info != null && StringUtil.isNotEmpty(info.getPropertyJson())) { PropertyJsonModel propertyJsonModel = JsonUtil.getJsonToBean(info.getPropertyJson(), PropertyJsonModel.class); String modelId = propertyJsonModel.getModuleId(); releaseEntity = visualdevReleaseService.getById(modelId); } if (releaseEntity == null || (StringUtil.isNotEmpty(systemId) && !Objects.equals(info.getSystemId(), systemId))) { return ActionResult.fail(MsgCode.FA001.get()); } VisualdevEntity entity = JsonUtil.getJsonToBean(releaseEntity, VisualdevEntity.class); String s = VisualUtil.checkPublishVisualModel(entity, MsgCode.VS005.get()); if (s != null) { return ActionResult.fail(s); } DataInfoVO vo = JsonUtil.getJsonToBean(entity, DataInfoVO.class); return ActionResult.success(vo); } @Operation(summary = "关联查询数据") @Parameter(name = "id", description = "功能id") @PostMapping("/RelationQuery") public ActionResult>> relationQuery(@RequestBody RelationQuery query) { VisualdevEntity entity = visualdevService.getReleaseInfo(query.getModelId()); List> realList = visualdevModelDataService.relationQuery(entity, query); PaginationVO paginationVO = JsonUtil.getJsonToBean(query, PaginationVO.class); return ActionResult.page(realList, paginationVO); } /** * AUDIT 补丁(2026-08-01):取被删数据的快照,供审计层 0 渲染标题与留存 data_snapshot。 * *

为什么不复用 batchDelete 里现成的 dataMap:那份是喂 integrateUtil 触发器的, * 用的是默认参数(needRlationFiled=false),关联表单字段取不到,标题会缺值; * 而改它的取数参数会连带改变触发器收到的数据——那是本次需求之外的行为变更。 * 这里按 visualUpdate 同款参数(needRlationFiled=true, needSwap=false)单独取一次, * 与 CREATE/UPDATE 的 newData 同构,审计侧整条渲染链路得以原样复用。 * *

取不到返回 null:事件照发(「这次删过」不能缺),只是没有快照可渲染。 */ private Map auditDeleteSnapshot(String id, VisualdevEntity visualdevEntity) { try { VisualdevModelDataInfoVO info = visualDevInfoService.getEditDataInfo(id, visualdevEntity, OnlineInfoModel.builder().needRlationFiled(true).needSwap(false).build()); return info == null ? null : JsonUtil.stringToMap(info.getData()); } catch (Throwable t) { log.warn("[AUDIT] 取删除快照失败 id={}, error={}", id, t.getMessage()); return null; } } }