导入导出支持国际化语言分隔符

需求

  1. 导出 Excel 时,所有整数、小数字段需要加千分位分隔符显示
    例如:10000 导出成 10,000。
  2. 只影响“导出的显示效果”,不改变原始数据的语义。

实现思路

通过修改“ Excel 默认导出模版”的平台逻辑,将整型字段模版定义为文本类型,并自定义导出扩展,将所有整型字段的数据根据国际化配置进行分割。

代码示例

  1. 自定义导出扩展,分割整型字段
    注意 所有已有的导出扩展必须修改继承类ExcelExportSameQueryPageTemplate<Object> --> GlobalExportExt<Object>
    否则,已有的导出扩展生成的Excel将无法正常格式化整型字段。
package pro.shushi.pamirs.top.core.temp.exports;

import org.apache.commons.collections4.CollectionUtils;
import org.springframework.stereotype.Component;
import pro.shushi.pamirs.file.api.context.ExcelDefinitionContext;
import pro.shushi.pamirs.file.api.entity.EasyExcelBlockDefinition;
import pro.shushi.pamirs.file.api.entity.EasyExcelCellDefinition;
import pro.shushi.pamirs.file.api.entity.EasyExcelSheetDefinition;
import pro.shushi.pamirs.file.api.extpoint.ExcelExportFetchDataExtPoint;
import pro.shushi.pamirs.file.api.extpoint.impl.ExcelExportSameQueryPageTemplate;
import pro.shushi.pamirs.file.api.model.ExcelExportTask;
import pro.shushi.pamirs.meta.annotation.Ext;
import pro.shushi.pamirs.meta.annotation.ExtPoint;
import pro.shushi.pamirs.meta.api.dto.config.ModelFieldConfig;
import pro.shushi.pamirs.meta.api.session.PamirsSession;
import pro.shushi.pamirs.meta.enmu.TtypeEnum;
import pro.shushi.pamirs.meta.util.FieldUtils;
import pro.shushi.pamirs.resource.api.model.ResourceLang;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

@Component
@Ext(ExcelExportTask.class)
public class GlobalExportExt<T> extends ExcelExportSameQueryPageTemplate<T> implements ExcelExportFetchDataExtPoint {

    @ExtPoint.Implement(priority = 1)
    @Override
    public List<Object> fetchExportData(ExcelExportTask exportTask, ExcelDefinitionContext context) {
        List<Object> results = super.fetchExportData(exportTask, context);
        if (CollectionUtils.isEmpty(results)) {
            return results;
        }
        return dataFormat(context, results);
    }

    public static List<Object> dataFormat(ExcelDefinitionContext context, List<Object> results) {
        ResourceLang resourceLang = new ResourceLang().setCode(PamirsSession.getLang()).queryOne();
        if (resourceLang == null) {
            return results;
        }
        // 小数分隔符
        String decimalPoint = resourceLang.getDecimalPoint();
        // 整数分隔符
        String thousandsSep = resourceLang.getThousandsSep();
        // 数字分组规则(每组多少位,比如 "3")
        String groupingRule = resourceLang.getGroupingRule();

        // 解析 groupSize,只做一次
        int groupSize = 3;
        if (groupingRule != null && groupingRule.matches("\\d+")) {
            try {
                groupSize = Integer.parseInt(groupingRule);
            } catch (NumberFormatException ignore) {
            }
        }
        boolean needGroup = groupSize > 0 && thousandsSep != null && !thousandsSep.isEmpty();

        // 判断是否需要小数点替换
        String dp = (decimalPoint == null || decimalPoint.isEmpty()) ? "." : decimalPoint;
        boolean needDecimalReplace = !".".equals(dp);

        EasyExcelSheetDefinition sheetDefinition = context.getSheetList().get(0);
        EasyExcelBlockDefinition blockDefinition = sheetDefinition.getBlockDefinitions().get(0);
        String model = blockDefinition.getBindingModel();

        // 记录Excel中整数字段编码
        List<String> integerField = new ArrayList<>();
        // 记录Excel中小数字段编码
        List<String> decimalField = new ArrayList<>();

        Map<String, EasyExcelCellDefinition> fieldCells = blockDefinition.getFieldCells();
        for (String key : fieldCells.keySet()) {
            ModelFieldConfig modelField = PamirsSession.getContext().getModelField(model, key);
            if (modelField == null) {
                continue;
            }
            String ttype = modelField.getTtype();
            if (ttype == null) {
                continue;
            }
            if (TtypeEnum.INTEGER.value().equals(ttype)) {
                integerField.add(key);
            } else if (TtypeEnum.FLOAT.value().equals(ttype)) {
                decimalField.add(key);
            }
        }

        if (integerField.isEmpty() && decimalField.isEmpty()) {
            return results;
        }
        Object block = results.get(0);
        if (block instanceof List) {
            List<?> dataList = (List<?>) block;
            for (Object row : dataList) {
                // 处理整数字段:加千分位
                if (needGroup) {
                    for (String field : integerField) {
                        Object value = FieldUtils.getFieldValue(row, field);
                        if (value != null) {
                            String formatted = formatInteger(value, groupSize, thousandsSep);
                            FieldUtils.setFieldValue(row, field, formatted);
                        }
                    }
                }
                // 处理小数字段:只替换小数点
                if (needDecimalReplace) {
                    for (String field : decimalField) {
                        Object value = FieldUtils.getFieldValue(row, field);
                        if (value != null) {
                            String formatted = formatDecimal(value, dp);
                            FieldUtils.setFieldValue(row, field, formatted);
                        }
                    }
                }
            }
        }

        return results;
    }

    /**
     * 整数格式化:按照 groupSize 与 thousandsSep 插入分隔符
     */
    public static String formatInteger(Object value, int groupSize, String thousandsSep) {
        String numStr;
        if (value instanceof Integer || value instanceof Long || value instanceof Short) {
            numStr = String.valueOf(value);
        } else if (value instanceof BigDecimal || value instanceof Double || value instanceof Float) {
            numStr = new BigDecimal(value.toString()).toPlainString();
        } else {
            numStr = value.toString().trim();
        }
        boolean negative = false;
        if (numStr.startsWith("-")) {
            negative = true;
            numStr = numStr.substring(1);
        } else if (numStr.startsWith("+")) {
            numStr = numStr.substring(1);
        }

        // 去掉已有的分隔符(防止重复格式化)
        numStr = numStr.replace(",", "").replace(" ", "");

        // 非纯数字直接返回原始字符串
        if (!numStr.matches("\\d+")) {
            return (negative ? "-" : "") + numStr;
        }

        if (groupSize <= 0 || thousandsSep == null || thousandsSep.isEmpty()) {
            return (negative ? "-" : "") + numStr;
        }

        StringBuilder sb = new StringBuilder();
        int len = numStr.length();
        int firstGroupLen = len % groupSize;
        if (firstGroupLen == 0) {
            firstGroupLen = groupSize;
        }
        sb.append(numStr, 0, firstGroupLen);
        for (int i = firstGroupLen; i < len; i += groupSize) {
            sb.append(thousandsSep).append(numStr, i, Math.min(i + groupSize, len));
        }

        return (negative ? "-" : "") + sb;
    }

    /**
     * 小数格式化
     */
    public static String formatDecimal(Object value, String dp) {
        String numStr;

        if (value instanceof BigDecimal || value instanceof Double || value instanceof Float) {
            numStr = new BigDecimal(value.toString()).toPlainString();
        } else {
            numStr = value.toString().trim();
        }

        if (dp == null || ".".equals(dp)) {
            return numStr;
        }
        return numStr.replace(".", dp);
    }
}
  1. 修改平台逻辑,整型字段模版定义为文本类型
    启动工程同包同类名覆盖平台文件。
    pro.shushi.pamirs.file.api.init.FileLifecycleCompletedInit
package pro.shushi.pamirs.file.api.init;

import com.alibaba.fastjson.JSON;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import pro.shushi.pamirs.boot.base.model.View;
import pro.shushi.pamirs.boot.common.api.command.AppLifecycleCommand;
import pro.shushi.pamirs.boot.common.api.init.LifecycleCompletedInit;
import pro.shushi.pamirs.core.common.InitializationUtil;
import pro.shushi.pamirs.core.common.LifecycleExecutorHelper;
import pro.shushi.pamirs.core.common.cache.MemoryIterableSearchCache;
import pro.shushi.pamirs.core.common.xstream.TreeNodeXStream;
import pro.shushi.pamirs.core.common.xstream.XMLNodeContent;
import pro.shushi.pamirs.file.api.FileModule;
import pro.shushi.pamirs.file.api.builder.BlockDefinitionBuilder;
import pro.shushi.pamirs.file.api.builder.HeaderDefinitionBuilder;
import pro.shushi.pamirs.file.api.builder.WorkbookDefinitionBuilder;
import pro.shushi.pamirs.file.api.config.FileConstant;
import pro.shushi.pamirs.file.api.config.FileProperties;
import pro.shushi.pamirs.file.api.enmu.*;
import pro.shushi.pamirs.file.api.model.ExcelWorkbookDefinition;
import pro.shushi.pamirs.file.api.service.ExcelFileService;
import pro.shushi.pamirs.file.api.util.ExcelHelper;
import pro.shushi.pamirs.framework.common.config.AsyncTaskExecutorConfiguration;
import pro.shushi.pamirs.framework.common.entry.TreeNode;
import pro.shushi.pamirs.framework.connectors.data.sql.Pops;
import pro.shushi.pamirs.framework.gateways.util.BooleanHelper;
import pro.shushi.pamirs.meta.annotation.fun.extern.Slf4j;
import pro.shushi.pamirs.meta.api.Models;
import pro.shushi.pamirs.meta.api.dto.config.ModelConfig;
import pro.shushi.pamirs.meta.api.dto.config.ModelFieldConfig;
import pro.shushi.pamirs.meta.api.session.PamirsSession;
import pro.shushi.pamirs.meta.api.session.RequestContext;
import pro.shushi.pamirs.meta.common.constants.CharacterConstants;
import pro.shushi.pamirs.meta.constant.FieldConstants;
import pro.shushi.pamirs.meta.domain.model.DataDictionary;
import pro.shushi.pamirs.meta.domain.model.DataDictionaryItem;
import pro.shushi.pamirs.meta.domain.model.ModelDefinition;
import pro.shushi.pamirs.meta.domain.module.ModuleDefinition;
import pro.shushi.pamirs.meta.enmu.*;

import java.util.*;
import java.util.concurrent.Executor;
import java.util.stream.Collectors;

@Slf4j
@Component
@Order
public class FileLifecycleCompletedInit implements LifecycleCompletedInit {

    private static final String DEFAULT_EXPORT_TEMPLATE_DISPLAY_NAME = "默认导出模板";
    private static final String DEFAULT_IMPORT_TEMPLATE_DISPLAY_NAME = "默认导入模板";

    private static final String DEFAULT_EXPORT_TEMPLATE_SUFFIX = "导出";
    private static final String DEFAULT_IMPORT_TEMPLATE_SUFFIX = "导入";

    private static final String CONFIG_KEY = "config";

    private static final String CONFIG_EXCLUDED_ACTIONS_KEY = "excluded-actions";

    private static final String ACTION_KEY = "action";

    private static final String VIEW_KEY = "view";

    private static final String ACTION_REFS_KEY = "refs";

    private static final String FIELD_KEY = "field";

    private static final String FIELDS_KEY = "fields";

    private static final String FORM_KEY = "form";

    private static final String TEMPLATE_KEY = "template";

    private static final String SLOT_KEY = "slot";

    private static final String PACK_KEY = "pack";

    private static final String TABLE_KEY = "table";

    private static final String DATA_KEY = "data";

    private static final String TYPE_KEY = "type";

    private static final String WIDGET_KEY = "widget";

    private static final String NAME_KEY = "name";

    private static final String FIELD_LABEL_KEY = "label";

    private static final String FIELD_INVISIBLE_KEY = "invisible";

    private static final String EXPORT_DIALOG_KEY = "$$internal_GotoListExportDialog";

    private static final String EXPORT_DIALOG_KEY_V3 = "internal_GotoListExportDialog";

    private static final String DEFAULT_RELATION_LABEL_SPLIT = "-";

    @Autowired
    private ExcelFileService excelFileService;

    @Autowired
    private FileProperties fileProperties;

    @Autowired(required = false)
    @Qualifier(AsyncTaskExecutorConfiguration.FIXED_THREAD_POOL_EXECUTOR)
    private Executor globalFixedThreadPoolExecutor;

    @Override
    public void process(AppLifecycleCommand command, List<ModuleDefinition> installModules, List<ModuleDefinition> upgradeModules, List<ModuleDefinition> reloadModules) {
        log.info("Automatically create import/export system templates: {}", fileProperties.getAutoCreateTemplate());
        if (fileProperties.getAutoCreateTemplate()) {
            InitializationUtil.lifecycleCompletedInit(installModules, upgradeModules, Collections.emptyList(), (lifecycle, module) -> LifecycleExecutorHelper.execute(globalFixedThreadPoolExecutor, this::initDefaultTemplateByTableView), FileModule.MODULE_MODULE);
        }
    }

    private void initDefaultTemplateByTableView() {
        List<ExcelWorkbookDefinition> existImportExportWorkbookDefinitionList = getExistWorkbookDefinitionList(ExcelTemplateTypeEnum.IMPORT_EXPORT);
        // 默认导出模板(Table)
        List<ExcelWorkbookDefinition> existExportWorkbookDefinitionList = getExistWorkbookDefinitionList(ExcelTemplateTypeEnum.EXPORT);
        existExportWorkbookDefinitionList.addAll(existImportExportWorkbookDefinitionList);
        List<ExcelWorkbookDefinition> exportWorkbookDefinitionList = getEffectiveList(existExportWorkbookDefinitionList, ViewTypeEnum.TABLE);

        // 默认导入模板(Form)
        List<ExcelWorkbookDefinition> existImportWorkbookDefinitionList = getExistWorkbookDefinitionList(ExcelTemplateTypeEnum.IMPORT);
        existImportWorkbookDefinitionList.addAll(existImportExportWorkbookDefinitionList);
        List<ExcelWorkbookDefinition> importWorkbookDefinitionList = getEffectiveList(existImportWorkbookDefinitionList, ViewTypeEnum.FORM);

        log.info("Initialization system templates. import template: {}, export template: {}", exportWorkbookDefinitionList.size(), importWorkbookDefinitionList.size());
        refreshWorkbookDefinition(exportWorkbookDefinitionList);
        refreshWorkbookDefinition(importWorkbookDefinitionList);
    }

    private List<ExcelWorkbookDefinition> getExistWorkbookDefinitionList(ExcelTemplateTypeEnum type) {
        List<ExcelWorkbookDefinition> workbookDefinitions = Models.data().queryListByWrapper(Pops.<ExcelWorkbookDefinition>lambdaQuery().from(ExcelWorkbookDefinition.MODEL_MODEL).eq(ExcelWorkbookDefinition::getType, type).setBatchSize(200));
        if (CollectionUtils.isNotEmpty(workbookDefinitions)) {
            workbookDefinitions = Models.data().listFieldQuery(workbookDefinitions, ExcelWorkbookDefinition::getLocations);
        }
        return workbookDefinitions;
    }

    private List<ExcelWorkbookDefinition> getEffectiveList(List<ExcelWorkbookDefinition> existWorkbookDefinitionList, ViewTypeEnum viewType) {
        List<View> allViewList = Models.data().queryListByWrapper(Pops.<View>lambdaQuery().from(View.MODEL_MODEL).select(View::getModel, View::getName, View::getTitle, View::getTemplate).eq(View::getType, viewType).eq(View::getActive, ActiveEnum.ACTIVE.value()).orderByAsc(View::getPriority).setBatchSize(200));
        List<View> distinctViewList = allViewList.stream().collect(Collectors.collectingAndThen(Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(View::getModel))), ArrayList::new));
        TreeNodeXStream xs = new TreeNodeXStream();
        Set<String> repeatSet = new HashSet<>();
        Map<ExcelTemplateSourceEnum, List<ExcelWorkbookDefinition>> resultMap = new HashMap<>(existWorkbookDefinitionList.size());
        for (ExcelWorkbookDefinition item : existWorkbookDefinitionList) {
            resultMap.computeIfAbsent(item.getTemplateSource(), k -> new ArrayList<>()).add(item);
            if (!ExcelTemplateSourceEnum.SYSTEM.equals(item.getTemplateSource())) {
                repeatSet.add(item.getModel());
            }
        }
        List<ExcelWorkbookDefinition> createOrUpdateList = new ArrayList<>();
        MemoryIterableSearchCache<String, ExcelWorkbookDefinition> cache = new MemoryIterableSearchCache<>(resultMap.get(ExcelTemplateSourceEnum.SYSTEM), this::keyGenerator);
        for (View view : distinctViewList) {
            String model = view.getModel();
            if (!repeatSet.contains(model)) {
                ExcelWorkbookDefinition workbookDefinition;
                try {
                    workbookDefinition = createOrUpdateExcelTemplate(xs, view);
                } catch (Exception e) {
                    if (log.isErrorEnabled()) {
                        log.error("Excel模板生成失败 model: {}", model, e);
                    }
                    continue;
                }
                if (workbookDefinition != null) {
                    cache.compute(keyGenerator(workbookDefinition), (k, v) -> v);
                    createOrUpdateList.add(workbookDefinition);
                }
            }
        }
        cache.fill();
        List<ExcelWorkbookDefinition> deleteList = new ArrayList<>(cache.getNotComputedCache().values());
        if (!deleteList.isEmpty()) {
            Models.data().deleteByPks(deleteList);
            for (ExcelWorkbookDefinition item : deleteList) {
                String originKey = keyGenerator(item);
                for (int i = 0; i < existWorkbookDefinitionList.size(); i++) {
                    String targetKey = keyGenerator(existWorkbookDefinitionList.get(i));
                    if (originKey.equals(targetKey)) {
                        existWorkbookDefinitionList.remove(i);
                        break;
                    }
                }
            }
        }
        if (!createOrUpdateList.isEmpty()) {
            Models.data().createOrUpdateBatch(createOrUpdateList);
            List<ExcelWorkbookDefinition> needAddList = new ArrayList<>();
            MemoryIterableSearchCache<String, ExcelWorkbookDefinition> existWorkbookDefinitionCache = new MemoryIterableSearchCache<>(existWorkbookDefinitionList, this::keyGenerator);
            for (ExcelWorkbookDefinition item : createOrUpdateList) {
                if (existWorkbookDefinitionCache.get(keyGenerator(item)) == null) {
                    needAddList.add(item);
                }
            }
            existWorkbookDefinitionList.addAll(needAddList);
        }
        return existWorkbookDefinitionList;
    }

    private void refreshWorkbookDefinition(List<ExcelWorkbookDefinition> workbookDefinitionList) {
        try {
            excelFileService.refreshDefinitionContextBatch(workbookDefinitionList);
        } catch (Exception e) {
            log.error("Refresh excel workbook definition error.", e);
        }
    }

    private String keyGenerator(ExcelWorkbookDefinition item) {
        return item.getModel() + CharacterConstants.SEPARATOR_OCTOTHORPE + item.getName();
    }

    private ExcelWorkbookDefinition createOrUpdateExcelTemplate(TreeNodeXStream xs, View view) {
        String xmlTemplate = view.getTemplate();
        if (StringUtils.isBlank(xmlTemplate)) {
            return null;
        }
        String model = view.getModel();
        RequestContext requestContext = PamirsSession.getContext();
        if (requestContext == null) {
            return null;
        }
        ModelConfig modelConfig = requestContext.getModelConfig(model);
        if (modelConfig == null) {
            return null;
        }
        ModelTypeEnum modelType = modelConfig.getType();
        if (!ModelTypeEnum.STORE.equals(modelType) && !ModelTypeEnum.PROXY.equals(modelType)) {
            return null;
        }
        try {
            TreeNode<XMLNodeContent> viewRoot = xs.fromXML(xmlTemplate);
            XMLNodeContent rootContent = viewRoot.getValue();
            String type = rootContent.getAttribute(TYPE_KEY);
            if (StringUtils.isBlank(type)) {
                type = rootContent.getAttribute(WIDGET_KEY);
            }
            String viewType = type.trim().toUpperCase();
            if (StringUtils.isBlank(type) || (!ViewTypeEnum.TABLE.name().equals(viewType) && !ViewTypeEnum.FORM.name().equals(viewType))) {
                return null;
            }
            String templateDisplayName = generatorDefaultTemplateDisplayName(view, modelConfig, viewType);
            String sheetName = generatorDefaultSheetName(view, modelConfig, viewType);
            WorkbookDefinitionBuilder workbookDefinitionBuilder = WorkbookDefinitionBuilder.newInstance(model, FileConstant.DEFAULT_TEMPLATE_NAME + "_" + view.getName());
            workbookDefinitionBuilder.setBindingViewName(view.getName());
            BlockDefinitionBuilder blockDefinitionBuilder = workbookDefinitionBuilder.createSheet().setName(sheetName).createBlock(model, ExcelAnalysisTypeEnum.FIXED_HEADER, ExcelDirectionEnum.HORIZONTAL, 0, 1, 0, 12).setPresetNumber(10);
            HeaderDefinitionBuilder configHeaderDefinitionBuilder = blockDefinitionBuilder.createHeader().setStyleBuilder(ExcelHelper.createDefaultStyle()).setIsConfig(true);
            HeaderDefinitionBuilder headerDefinitionBuilder = blockDefinitionBuilder.createHeader().setStyleBuilder(ExcelHelper.createDefaultStyle(v -> v.setBold(true)));
            if (ViewTypeEnum.TABLE.name().equals(viewType)) {
                if (!createExportCell(configHeaderDefinitionBuilder, headerDefinitionBuilder, viewRoot, requestContext, model)) {
                    return null;
                }
                log.info("生成默认导出模板成功 [Model {}] [ViewName {}] [ViewType {}]", view.getModel(), view.getName(), view.getType());
                blockDefinitionBuilder.modifyDesignRange(0, 1, 0, configHeaderDefinitionBuilder.cellSize() - 1);
                return workbookDefinitionBuilder.setType(ExcelTemplateTypeEnum.EXPORT).setDisplayName(templateDisplayName).build().setTemplateSource(ExcelTemplateSourceEnum.SYSTEM);
            } else {
                if (!createImportCell(configHeaderDefinitionBuilder, headerDefinitionBuilder, viewRoot, requestContext, model)) {
                    return null;
                }
                log.info("生成默认导入模板成功 [Model {}] [ViewName {}] [ViewType {}]", view.getModel(), view.getName(), view.getType());
                blockDefinitionBuilder.modifyDesignRange(0, 1, 0, configHeaderDefinitionBuilder.cellSize() - 1);
                return workbookDefinitionBuilder.setType(ExcelTemplateTypeEnum.IMPORT).setDisplayName(templateDisplayName).build().setTemplateSource(ExcelTemplateSourceEnum.SYSTEM);
            }
        } catch (Exception e) {
            log.error("生成默认模板失败 [Model {}] [ViewName {}] [ViewType {}]", view.getModel(), view.getName(), view.getType(), e);
        }
        return null;
    }

    private String generatorDefaultTemplateDisplayName(View view, ModelConfig modelConfig, String viewType) {
        if (ViewTypeEnum.TABLE.name().equals(viewType.trim().toUpperCase())) {
            return generatorDefaultSheetName(view, modelConfig, viewType) + DEFAULT_EXPORT_TEMPLATE_SUFFIX;
        } else {
            return generatorDefaultSheetName(view, modelConfig, viewType) + DEFAULT_IMPORT_TEMPLATE_SUFFIX;
        }
    }

    private String generatorDefaultSheetName(View view, ModelConfig modelConfig, String viewType) {
        String name = view.getTitle();
        if (StringUtils.isBlank(name)) {
            name = modelConfig.getDisplayName();
            if (StringUtils.isBlank(name)) {
                if (ViewTypeEnum.TABLE.name().equals(viewType.trim().toUpperCase())) {
                    name = DEFAULT_EXPORT_TEMPLATE_DISPLAY_NAME;
                } else {
                    name = DEFAULT_IMPORT_TEMPLATE_DISPLAY_NAME;
                }
            }
        }
        return name;
    }

    private boolean createExportCell(HeaderDefinitionBuilder configHeaderDefinitionBuilder,
                                     HeaderDefinitionBuilder headerDefinitionBuilder,
                                     TreeNode<XMLNodeContent> viewRoot,
                                     RequestContext requestContext,
                                     String model) {
        boolean isCreate = false;
        for (TreeNode<XMLNodeContent> child : viewRoot.getChildren()) {
            XMLNodeContent childValue = child.getValue();
            String key = child.getKey();
            if (FIELD_KEY.equals(key)) {
                boolean invisible = childValue.getBooleanAttribute(FIELD_INVISIBLE_KEY);
                if (invisible) {
                    continue;
                }
                String field = childValue.getAttribute(DATA_KEY);
                if (StringUtils.isBlank(field)) {
                    continue;
                }
                String label = childValue.getAttribute(FIELD_LABEL_KEY);
                ModelFieldConfig modelFieldConfig = requestContext.getModelField(model, field);
                if (modelFieldConfig == null) {
                    continue;
                }
                if (Boolean.TRUE.equals(modelFieldConfig.getInvisible())) {
                    continue;
                }
                if (StringUtils.isBlank(label) || !BooleanHelper.isFalseWithoutException(label)) {
                    label = modelFieldConfig.getDisplayName();
                    if (StringUtils.isBlank(label)) {
                        label = field;
                    }
                }
                if (createCell(configHeaderDefinitionBuilder, headerDefinitionBuilder, requestContext, modelFieldConfig, field, label, true)) {
                    isCreate = true;
                }
            } else if (CONFIG_KEY.equals(key)) {
                String excludedActions = childValue.getAttribute(CONFIG_EXCLUDED_ACTIONS_KEY);
                if (excludedActions != null) {
                    if (excludedActions.contains(EXPORT_DIALOG_KEY)) {
                        return false;
                    }
                }
            } else if (TEMPLATE_KEY.equals(key)) {
                String slot = childValue.getAttribute(SLOT_KEY);
                if (StringUtils.isBlank(slot)) {
                    continue;
                }
                slot = slot.trim().toLowerCase();
                if (TABLE_KEY.equals(slot) || FIELDS_KEY.equals(slot)) {
                    isCreate = createExportCell(configHeaderDefinitionBuilder, headerDefinitionBuilder, child, requestContext, model);
                }
            }
        }
        return isCreate;
    }

    private boolean createImportCell(HeaderDefinitionBuilder configHeaderDefinitionBuilder,
                                     HeaderDefinitionBuilder headerDefinitionBuilder,
                                     TreeNode<XMLNodeContent> viewRoot,
                                     RequestContext requestContext,
                                     String model) {
        boolean isCreate = false;
        for (TreeNode<XMLNodeContent> child : viewRoot.getChildren()) {
            XMLNodeContent childValue = child.getValue();
            String key = child.getKey();
            if (FIELD_KEY.equals(key)) {
                boolean invisible = childValue.getBooleanAttribute(FIELD_INVISIBLE_KEY);
                if (invisible) {
                    continue;
                }
                String field = childValue.getAttribute(DATA_KEY);
                if (StringUtils.isBlank(field) || FieldConstants.CREATE_UID.equalsIgnoreCase(field) || FieldConstants.WRITE_UID.equalsIgnoreCase(field)) {
                    continue;
                }
                String label = childValue.getAttribute(FIELD_LABEL_KEY);
                ModelFieldConfig modelFieldConfig = requestContext.getModelField(model, field);
                if (modelFieldConfig == null) {
                    continue;
                }
                if (Boolean.TRUE.equals(modelFieldConfig.getInvisible())) {
                    continue;
                }
                if (StringUtils.isBlank(label) || !BooleanHelper.isFalseWithoutException(label)) {
                    label = modelFieldConfig.getDisplayName();
                    if (StringUtils.isBlank(label)) {
                        label = field;
                    }
                }
                if (createCell(configHeaderDefinitionBuilder, headerDefinitionBuilder, requestContext, modelFieldConfig, field, label, false)) {
                    isCreate = true;
                }
            } else if (CONFIG_KEY.equals(key)) {
                String excludedActions = childValue.getAttribute(CONFIG_EXCLUDED_ACTIONS_KEY);
                if (excludedActions != null) {
                    if (excludedActions.contains(EXPORT_DIALOG_KEY)) {
                        return false;
                    }
                }
            } else if (PACK_KEY.equals(key)) {
                isCreate = createImportCell(configHeaderDefinitionBuilder, headerDefinitionBuilder, child, requestContext, model);
            } else if (TEMPLATE_KEY.equals(key)) {
                String slot = childValue.getAttribute(SLOT_KEY);
                if (StringUtils.isBlank(slot)) {
                    continue;
                }
                slot = slot.trim().toLowerCase();
                if (TABLE_KEY.equals(slot) || FIELDS_KEY.equals(slot) || FORM_KEY.equals(slot)) {
                    isCreate = createImportCell(configHeaderDefinitionBuilder, headerDefinitionBuilder, child, requestContext, model);
                }
            }
        }
        return isCreate;
    }

    private boolean createCell(HeaderDefinitionBuilder configHeaderDefinitionBuilder,
                               HeaderDefinitionBuilder headerDefinitionBuilder,
                               RequestContext requestContext,
                               ModelFieldConfig modelFieldConfig,
                               String field,
                               String label,
                               Boolean isExport) {
        String ttype = modelFieldConfig.getTtype();
        if (TtypeEnum.RELATED.value().equals(ttype)) {
            if (Boolean.TRUE.equals(isExport) || Boolean.TRUE.equals(modelFieldConfig.getStore())) {
                ttype = modelFieldConfig.getRelatedTtype();
            } else {
                return false;
            }
        }
        boolean isMulti = Boolean.TRUE.equals(modelFieldConfig.getMulti());
        ExcelValueTypeEnum valueType = ExcelValueTypeEnum.STRING;
        String format = modelFieldConfig.getFormat();
        boolean isSampleField = true;
        if (TtypeEnum.STRING.value().equals(ttype) || TtypeEnum.INTEGER.value().equals(ttype)) {
            if (isMulti) {
                format = ExcelHelper.generatorMultiValueFormatExpression();
                valueType = ExcelValueTypeEnum.OBJECT;
            }
            else if (TtypeEnum.INTEGER.value().equals(ttype)) {
                format = ExcelValueTypeEnum.STRING.getDefaultFormat();
                valueType = ExcelValueTypeEnum.STRING;
            }
        } else if (TtypeEnum.ENUM.value().equals(ttype)) {
            Map<String, String> enumerationMapping = new HashMap<>();
            DataDictionary dictionary = requestContext.getDictionary(modelFieldConfig.getDictionary());
            for (DataDictionaryItem dictionaryItem : dictionary.getOptions()) {
                enumerationMapping.put(dictionaryItem.getValue(), dictionaryItem.getDisplayName());
            }
            format = JSON.toJSONString(enumerationMapping);
            valueType = ExcelValueTypeEnum.ENUMERATION;
        } else if (TtypeEnum.DATE.value().equals(ttype)) {
            if (StringUtils.isBlank(format)) {
                format = DateFormatEnum.DATE.value();
            }
            valueType = ExcelValueTypeEnum.DATETIME;
        } else if (TtypeEnum.TIME.value().equals(ttype)) {
            if (StringUtils.isBlank(format)) {
                format = DateFormatEnum.TIME.value();
            }
            valueType = ExcelValueTypeEnum.DATETIME;
        } else if (TtypeEnum.DATETIME.value().equals(ttype)) {
            if (StringUtils.isBlank(format)) {
                format = DateFormatEnum.DATETIME.value();
            }
            valueType = ExcelValueTypeEnum.DATETIME;
        } else if (TtypeEnum.BOOLEAN.value().equals(ttype)) {
            format = ExcelValueTypeEnum.BOOLEAN.getDefaultFormat();
            valueType = ExcelValueTypeEnum.BOOLEAN;
        } else if (TtypeEnum.MONEY.value().equals(ttype) || TtypeEnum.FLOAT.value().equals(ttype)) {
            if (isMulti) {
                format = ExcelHelper.generatorMultiValueFormatExpression();
                valueType = ExcelValueTypeEnum.OBJECT;
            } else {
                format = ExcelHelper.getNumberFormat(modelFieldConfig.getDecimal());
                valueType = ExcelValueTypeEnum.NUMBER;
            }
        } else if (TtypeEnum.M2O.value().equals(ttype) || TtypeEnum.O2O.value().equals(ttype)) {
            relationFieldProcess(configHeaderDefinitionBuilder, headerDefinitionBuilder, requestContext, modelFieldConfig, field, label, false, isExport);
            isSampleField = false;
        } else if (TtypeEnum.O2M.value().equals(ttype) || TtypeEnum.M2M.value().equals(ttype)) {
            relationFieldProcess(configHeaderDefinitionBuilder, headerDefinitionBuilder, requestContext, modelFieldConfig, field, label, true, isExport);
            isSampleField = false;
        }
        if (isSampleField) {
            configHeaderDefinitionBuilder.createCell().setField(field).setType(valueType).setFormat(format);
            headerDefinitionBuilder.createCell().setValue(label);
        }
        return true;
    }

    private void relationFieldProcess(HeaderDefinitionBuilder configHeaderDefinitionBuilder,
                                      HeaderDefinitionBuilder headerDefinitionBuilder,
                                      RequestContext requestContext,
                                      ModelFieldConfig modelFieldConfig,
                                      String field,
                                      String label,
                                      Boolean isMulti,
                                      Boolean isExport) {
        if (isExport) {
            exportRelationFieldProcess(configHeaderDefinitionBuilder, headerDefinitionBuilder, requestContext, modelFieldConfig, field, label, isMulti);
        } else {
            importRelationFieldProcess(configHeaderDefinitionBuilder, headerDefinitionBuilder, requestContext, modelFieldConfig, field, label, isMulti);
        }
    }

    private void exportRelationFieldProcess(HeaderDefinitionBuilder configHeaderDefinitionBuilder,
                                            HeaderDefinitionBuilder headerDefinitionBuilder,
                                            RequestContext requestContext,
                                            ModelFieldConfig modelFieldConfig,
                                            String field,
                                            String label,
                                            Boolean isMulti) {
        String referenceModel = modelFieldConfig.getReferences();
        ModelConfig referenceModelConfig = getReferenceModelConfig(requestContext, modelFieldConfig);
        if (referenceModelConfig == null) {
            return;
        }
        List<String> optionLabels = Optional.ofNullable(referenceModelConfig.getModelDefinition()).map(ModelDefinition::getLabelFields).filter(CollectionUtils::isNotEmpty).orElse(null);
        if (CollectionUtils.isEmpty(optionLabels)) {
            ModelFieldConfig labelFieldConfig = requestContext.getModelField(referenceModel, FieldConstants.NAME);
            if (labelFieldConfig == null) {
                labelFieldConfig = requestContext.getModelField(referenceModel, FieldConstants.CODE);
            }
            if (labelFieldConfig == null) {
                labelFieldConfig = requestContext.getModelField(referenceModel, FieldConstants.ID);
            }
            if (labelFieldConfig == null) {
                return;
            }
            optionLabels = Collections.singletonList(labelFieldConfig.getField());
        }
        String optionLabel = optionLabels.get(0);
        String formatExpression;
        if (isMulti) {
            formatExpression = ExcelHelper.generatorMultiObjectFormatExpression(referenceModel, optionLabel);
        } else {
            formatExpression = ExcelHelper.generatorSingleObjectFormatExpression(optionLabel);
        }
        configHeaderDefinitionBuilder.createCell().setField(field).setType(ExcelValueTypeEnum.OBJECT).setFormat(formatExpression);
        headerDefinitionBuilder.createCell().setValue(label);
    }

    /**
     * 导入关联关系处理仅处理包含唯一键或主键的多对一字段
     */
    private void importRelationFieldProcess(HeaderDefinitionBuilder configHeaderDefinitionBuilder,
                                            HeaderDefinitionBuilder headerDefinitionBuilder,
                                            RequestContext requestContext,
                                            ModelFieldConfig modelFieldConfig,
                                            String field,
                                            String label,
                                            Boolean isMulti) {
        if (isMulti) {
            // 过滤所有 o2m/m2m 类型字段
            return;
        }
        String referenceModel = modelFieldConfig.getReferences();
        ModelConfig referenceModelConfig = getReferenceModelConfig(requestContext, modelFieldConfig);
        if (referenceModelConfig == null) {
            return;
        }
        //此处仅处理指定唯一索引的关联对象
        List<String> uniqueList = referenceModelConfig.getUniques();
        if (CollectionUtils.isEmpty(uniqueList)) {
            return;
        }
        List<String> labelFields = Optional.ofNullable(referenceModelConfig.getModelDefinition()).map(ModelDefinition::getLabelFields).filter(CollectionUtils::isNotEmpty).orElse(null);
        if (CollectionUtils.isEmpty(labelFields)) {
            return;
        }
        String firstUnique = uniqueList.get(0);
        String[] uniques = firstUnique.split(CharacterConstants.SEPARATOR_COMMA);
        Map<String, String> referenceFieldMap = new LinkedHashMap<>();
        Map<String, ModelFieldConfig> referenceFieldConfigMap = new LinkedHashMap<>();
        List<String> relationFields = new ArrayList<>(Arrays.asList(uniques));
        relationFields.add(labelFields.get(0));
        for (String relationField : relationFields) {
            relationField = relationField.trim();
            ModelFieldConfig referenceModelFieldConfig = requestContext.getModelField(referenceModel, relationField);
            if (referenceModelFieldConfig == null) {
                return;
            }
            String referenceTtype = referenceModelFieldConfig.getTtype();
            if (TtypeEnum.isRelationType(referenceTtype)) {
                return;
            }
            String referenceLabel = referenceModelFieldConfig.getDisplayName();
            if (StringUtils.isBlank(referenceLabel)) {
                referenceLabel = relationField;
            }
            String newRelationField;
            if (isMulti) {
                newRelationField = field + FileConstant.LIST_FLAG_CHARACTER + FileConstant.POINT_CHARACTER + relationField;
            } else {
                newRelationField = field + FileConstant.POINT_CHARACTER + relationField;
            }
            referenceFieldMap.put(newRelationField, label + DEFAULT_RELATION_LABEL_SPLIT + referenceLabel);
            referenceFieldConfigMap.put(newRelationField, referenceModelFieldConfig);
        }
        for (Map.Entry<String, String> entry : referenceFieldMap.entrySet()) {
            String key = entry.getKey();
            createCell(configHeaderDefinitionBuilder, headerDefinitionBuilder, requestContext, referenceFieldConfigMap.get(key), key, entry.getValue(), null);
        }
    }

    private ModelConfig getReferenceModelConfig(RequestContext requestContext, ModelFieldConfig modelFieldConfig) {
        String referenceModel = modelFieldConfig.getReferences();
        ModelConfig referenceModelConfig = requestContext.getModelConfig(referenceModel);
        if (referenceModelConfig == null) {
            log.error("Invalid reference model config. model: {}, field: {}, referenceModel: {}",
                    modelFieldConfig.getModel(), modelFieldConfig.getField(), referenceModel);
            return null;
        }
        return referenceModelConfig;
    }
}

效果图

导入导出支持国际化语言分隔符

Oinone社区 作者:yexiu原创文章,如若转载,请注明出处:https://doc.oinone.top/other/25118.html

访问Oinone官网:https://www.oinone.top获取数式Oinone低代码应用平台体验

Like (0)
yexiu's avataryexiu数式员工
Previous 2025年11月17日 am9:57
Next 2025年12月17日 pm12:00

相关推荐

  • 如何实现业务表格跳转页面设计器设计器页面

    后端实现 代理继承界面设计器视图模型 @Model.model(MyView.MODEL_MODEL) @Model(displayName = "视图代理") @Model.Advanced(type = ModelTypeEnum.PROXY) public class MyView extends UiDesignerViewProxy { public static final String MODEL_MODEL = "hr.simple.MyView"; @Field.Integer @Field(displayName = "页面布局ID") private Long uiDesignerViewLayoutId; } 重写查询接口,返回页面布局ID,重写创建接口,实现创建页面逻辑。 package pro.shushi.pamirs.top.core.action; import org.apache.commons.collections4.CollectionUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import pro.shushi.pamirs.boot.base.constants.ViewConstants; import pro.shushi.pamirs.boot.base.enmu.ActionTargetEnum; import pro.shushi.pamirs.boot.base.ux.annotation.action.UxAction; import pro.shushi.pamirs.boot.base.ux.annotation.action.UxRoute; import pro.shushi.pamirs.boot.base.ux.annotation.button.UxRouteButton; import pro.shushi.pamirs.framework.connectors.data.sql.Pops; import pro.shushi.pamirs.framework.connectors.data.sql.query.LambdaQueryWrapper; import pro.shushi.pamirs.meta.annotation.Action; import pro.shushi.pamirs.meta.annotation.Function; import pro.shushi.pamirs.meta.annotation.Model; import pro.shushi.pamirs.meta.api.dto.condition.Pagination; import pro.shushi.pamirs.meta.api.dto.wrapper.IWrapper; import pro.shushi.pamirs.meta.api.session.PamirsSession; import pro.shushi.pamirs.meta.constant.FunctionConstants; import pro.shushi.pamirs.meta.enmu.*; import pro.shushi.pamirs.top.api.model.MyView; import pro.shushi.pamirs.ui.designer.api.designe.UiDesignerViewLayoutService; import pro.shushi.pamirs.ui.designer.model.UiDesignerViewLayout; import pro.shushi.pamirs.ui.designer.pmodel.UiDesignerViewLayoutProxy; import java.util.List; import java.util.Map; import java.util.stream.Collectors; /** * @author Yexiu at 20:39 on 2025/3/27 */ @Component @Model.model(MyView.MODEL_MODEL) public class MyViewAction { @Autowired private UiDesignerViewLayoutService uiDesignerViewLayoutService; @Action.Advanced(name = FunctionConstants.create, managed = true) @Action(displayName = "创建", summary = "添加", bindingType = ViewTypeEnum.FORM) @Function(name = FunctionConstants.create) @Function.fun(FunctionConstants.create) public MyView create(MyView data) { UiDesignerViewLayoutProxy uiDesignerViewLayoutProxy = new UiDesignerViewLayoutProxy(); uiDesignerViewLayoutProxy.setBizType(data.getBizType()); uiDesignerViewLayoutProxy.setDesignerActionBarType(data.getDesignerActionBarType()); uiDesignerViewLayoutProxy.setViewType(data.getType()); uiDesignerViewLayoutProxy.setModel(data.getModel()); uiDesignerViewLayoutProxy.setModule(PamirsSession.getServApp()); uiDesignerViewLayoutProxy.setViewTitle(data.getTitle()); uiDesignerViewLayoutProxy.setUsingDefaultView(data.getLoadLayout()); UiDesignerViewLayoutProxy saveUiDesigner = uiDesignerViewLayoutService.create(uiDesignerViewLayoutProxy); data.setDesignerViewId(saveUiDesigner.getId()); return data; } @Function.Advanced(type = FunctionTypeEnum.QUERY, displayName = "查询列表") @Function.fun(FunctionConstants.queryPage) @Function(openLevel = {FunctionOpenEnum.API, FunctionOpenEnum.LOCAL}) public Pagination<MyView> queryPage(Pagination<MyView> page, IWrapper<MyView> queryWrapper) { LambdaQueryWrapper<MyView> wrapper = Pops.<MyView>lambdaQuery().from(MyView.MODEL_MODEL) .eq(MyView::getSys, Boolean.FALSE) .eq(MyView::getSystemSource, SystemSourceEnum.UI); Pagination<MyView> myViewPagination = new MyView().queryPage(page, wrapper); List<MyView> content…

    2025年3月31日
    76000
  • 后端脚手架生成工程

    后端脚手架生成工程 脚手架版本 6.2.x(6.3.x/6.4.x)版本的脚手架,修改脚本(参考下节内容)中脚手架版本: # 新项目的pamirs platform version pamirsVersion=6.2.8 …… # mvn archetype:generate执行片段中 -DarchetypeVersion=6.2.8 7.0.x版本的脚手架,修改脚本(参考下节内容)中脚手架版本: # 新项目的pamirs platform version pamirsVersion=7.0.5 …… # mvn archetype:generate执行片段中 -DarchetypeVersion=7.0.5 后端脚手架生成工程 后端单工程脚手架 1、使用如下命令来用项目脚手架生成工程:新建archetype-project-generate.sh(bat) 脚本 archetype-project-generate.sh #!/bin/bash # 项目生成脚手架 # 用于新项目的构建 # 脚手架使用目录 # 本地 local # 本地脚手架信息存储路径 ~/.m2/repository/archetype-catalog.xml archetypeCatalog=local # 以下参数以pamirs-demo为例 # 新项目的groupId groupId=pro.shushi.pamirs.demo # 新项目的artifactId artifactId=pamirs-demo # 新项目的version version=1.0.0-SNAPSHOT # Java包名前缀 packagePrefix=pro.shushi # Java包名后缀 packageSuffix=pamirs.demo # 新项目的pamirs platform version;根据上个章节中的版本介绍进行修改 pamirsVersion=6.2.8 # Java类名称前缀 javaClassNamePrefix=Demo # 项目名称 module.displayName projectName=OinoneDemo # 模块 MODULE_MODULE 常量 moduleModule=demo_core # 模块 MODULE_NAME 常量 moduleName=DemoCore # spring.application.name applicationName=pamirs-demo # tomcat server address serverAddress=0.0.0.0 # tomcat server port serverPort=8090 # redis host redisHost=127.0.0.1 # redis port redisPort=6379 # 数据库名 db=demo # zookeeper connect string zkConnectString=127.0.0.1:2181 # zookeeper rootPath zkRootPath=/demo mvn archetype:generate \ -DinteractiveMode=false \ -DarchetypeCatalog=${archetypeCatalog} \ -DarchetypeGroupId=pro.shushi.pamirs.archetype \ -DarchetypeArtifactId=pamirs-project-archetype \ -DarchetypeVersion=6.2.8 \ #根据上个章节中的版本介绍进行修改 -DgroupId=${groupId} \ -DartifactId=${artifactId} \ -Dversion=${version} \ -DpamirsVersion=${pamirsVersion} \ -Dpackage=${packagePrefix}.${packageSuffix} \ -DpackagePrefix=${packagePrefix} \ -DpackageSuffix=${packageSuffix} \ -DjavaClassNamePrefix=${javaClassNamePrefix} \ -DprojectName="${projectName}" \ -DmoduleModule=${moduleModule} \ -DmoduleName=${moduleName} \ -DapplicationName=${applicationName} \ -DserverAddress=${serverAddress} \ -DserverPort=${serverPort} \ -DredisHost=${redisHost} \ -DredisPort=${redisPort} \ -Ddb=${db} \ -DzkConnectString=${zkConnectString} \ -DzkRootPath=${zkRootPath} archetype-project-generate.bat @echo off :: 项目生成脚手架 set archetypeCatalog=local set groupId=pro.shushi.pamirs.demo set artifactId=pamirs-demo set version=1.0.0-SNAPSHOT set…

    2025年8月22日
    1.1K00
  • 自定义流程设计器审批人公司列表

    打开弹窗时会默认选中返回的第一个公司。 依赖: <dependency> <groupId>pro.shushi.pamirs.workflow</groupId> <artifactId>pamirs-workflow-api</artifactId> </dependency> 只显示所有公司 ALL_COMPANY 表示所有公司。 @Order(10) @Component @SPI.Service public class CustomWorkflowCompanyQueryApi implements WorkflowCompanyQueryApi { @Override public Pagination<PamirsCompany> queryPage(Pagination<PamirsCompany> page, IWrapper<PamirsCompany> queryWrapper) { Pagination<PamirsCompany> pageResult = new Pagination<>(); pageResult.setContent(Lists.newArrayList(ALL_COMPANY)); pageResult.setTotalPages(1); return pageResult; } } 不显示所有公司 @Order(10) @Component @SPI.Service public class CustomWorkflowCompanyQueryApi implements WorkflowCompanyQueryApi { @Override public Pagination<PamirsCompany> queryPage(Pagination<PamirsCompany> page, IWrapper<PamirsCompany> queryWrapper) { return new PamirsCompany().queryPage(page, queryWrapper); } } 仅显示特定公司 @Order(10) @Component @SPI.Service public class CustomWorkflowCompanyQueryApi implements WorkflowCompanyQueryApi { private static final Set<String> COMPANY_CODES = Sets.newHashSet("CH0001", "CH0002"); @Override public Pagination<PamirsCompany> queryPage(Pagination<PamirsCompany> page, IWrapper<PamirsCompany> queryWrapper) { Set<String> companyCodes = Sets.newHashSet("CH0001", "CH0002"); LambdaQueryWrapper<PamirsCompany> query = ((QueryWrapper<PamirsCompany>) queryWrapper).lambda(); query.in(PamirsCompany::getCode, COMPANY_CODES); return new PamirsCompany().queryPage(page, queryWrapper); } }

    2025年9月4日
    1.2K00
  • 新人引导文档

    类型 文档链接 入门参考必看 7天入门到精通 Oinone 初级学习路径 Oinone 初级学习路径 平台部署启动 无代码docker启动说明 低代码启动说明 前端环境和启动前端工程 无代码设计器启动方式 后端无代码设计器Jar包启动方法 平台部署及依赖说明 Oinone平台部署及依赖说明(v5.0) 问题排查方法 问题排查工具使用手册 前后端研发帮助文档 前端文档 后端文档 项目开发实践要点 【前端】项目开发前端知识要点地图 【后端】项目开发后端知识要点地图 版本升级说明 版本更新日志 工具包 【附件一】下载说明 许可证使用说明 pamirs-license 许可证使用常见问题 更多文档新人引导建议,可以下方评论

    2024年7月15日
    2.0K00

Leave a Reply

Please Login to Comment