第一章 国产开源数据库建模工具PDManer

目录

一、PDManer的故事

二、PDManer介绍 

三、主要功能

2.1. 数据表管理

2.2. 视图管理

2.3. ER关系图

​2.4. 数据字典

2.5. 数据类型

​2.6. 多数据库

​2.7. 代码生成

2.8. 生态对接

2.9. 版本管理

2.10. 数据库代码规范检查

四、软件下载/使用手册

五、PDManer多版本选择/对比

 六、PDManner源码地址

七、代码生成器扩展

7.1. api.ts

7.2. Table.vue

7.3. Form.vue

7.4. index.vue

7.5. TableButton.vue

7.6. layout.ts

7.7. axios.ts

7.8. common.ts


一、PDManer的故事

2018 年初,因 PowerDesigner 对 PDManer 创始人所在开发团队的需求不能满足,也因为购买商业授权的不便利,便创立了一个松散的组织,用一个半月时时间完成了 PDMan 的 1.0 版本发布,解决了从无到有的问题。

2018 年 5 月,推出了 PDMan 第一个开源公开版,中间持续阶段性更新,直到 2019 年 1 月,不再更新。

因前第一个版本时间仓促,设计考虑不充分,PDMan后续优化升级非常困难,因此于 2019 年 12 月,规划了另一个全新的版本。

2019 年底,PDMan创始团队不到三万块启动资金,启动创业,生存之难可以想像,当情怀遇上生存发展,饿着肚讲理想,真的很难。期间,团队几经折腾周转,数次濒临解散。幸得创始人好友关照,找了两个项目做,核心人员一分为二,一部分去杭州,另一部分在远走塞北,吃饭问题暂时解决。

在此期间,产品停止更新,但是对产品的思考一直在持续,同时也结识了更多志趣相投的朋友加入,团队利用业余时间,完成了技术架构设计,界面原型设计,以及关键核心模块的开发编码。

2021 年 7 月 17 日,终于推出全新的 3.0 版本,为区别于 PDMan2 版本,因为一些特殊的原因,不得已而使用新名称,使用了 [CHINER 元数建模] 作为新产品的名称,公开发行。

目前,每一天,有 50000 家以上的组织或个人在使用 CHINER 在设计自己的数据库。

CHINER 拼写虽然看上去比较协调,但是发音存在二义性,用户接受度不高,很多用户还是习惯叫他 PDMan,在构建 4.0 版本时,创始团队想延续用户对 PDMan 的习惯,同时,也希望能够保留 CHINER 的某些记忆,英文名 PDManer=PDMan+er (chiner 的 er 部分,ER 也表示关系图的意思),“元数建模” 的中文名称依然延续,名称需要精简,拿掉 chi 表示中国的前缀部分,使用中文能更加明确这是一个中国小团队的作品,因此 4.0 版本之后,产品名称:[PDManer 元数建模] 就此确定,承接了 PDMan 以及 CHINER 的所有功能,并且进行延续、精进。

2022 年 4 月 17 日,发布 “PDManer 元数建模 - v4.0” 版本,相对于 3 版本,增加了可定制的版本管理以及可定制的代码生成器(可生成 Java,C#,等相关程序代码),一直被用户吐槽的 mac 版本下图标过大的问题,也一并解决了。

PDManer 产品,主要针对单机用户会持续保持他的开源免费。面向团队的版本,后续会逐步推出。

二、PDManer介绍 

PDManer元数建模,是一款多操作系统开源免费的桌面版关系数据库模型建模工具,相对于PowerDesigner,他具备界面简洁美观,操作简单,上手容易等特点。支持Windows,Mac,Linux等操作系统,也能够支持国产操作系统,能够支持的数据库如下:

  • MySQL,PostgreSQL,Oracle,SQLServer等常见数据库

  • 支持达梦,GuassDB等国产数据库

  • 支持Hive,MaxCompute等大数据方向的数据库

  • 用户还可以自行添加更多的数据库扩展

PDManer 基于 ES6+React+Electron+Java 开发构建

三、主要功能

2.1. 数据表管理

数据表,字段,注释,索引等基本功能

2.2. 视图管理

实现选择多张表多个字段后,组合一个新的视图对象,视图可生成DDL以及相关程序代码,例如Java的DTO等

2.3. ER关系图

数据表可绘制ER关系图至画布,也支持概念模型等高阶抽像设计

ER 图

逻辑模型图

思维导图

流程图


2.4. 数据字典

代码映射表管理,例如1表示男,2表示女,并且实现数据字典与数据表字段的关联


2.5. 数据类型

系统实现了基础数据类型,基础数据类型在不同数据库下表现为不同数据库类型的方言,这是实现多数据**库支持的基础,为更贴近业务,引入了PowerDesigner的数据域这一概念,用于统一同一类具有同样业务属性字段的批量设置类型,长度等。基础数据类型以及数据域,用户均可自行添加,自行定义。


2.6. 多数据库

内置主流常见数据库,如MySQL,PostgreSQL,SQLServer,Oracle等,并且支持用户自行添加新的数据库。通过该功能,我们在日常开发中涉及到数据库SQL的转换时,将非常方便,规避了大量数据库间SQL语法转换的工作量。


2.7. 代码生成

内置Java,Mybatis,MyBatisPlus等常规情况下Controller,Service,Mapper的生成,也添加了C#语言支持,可自行扩展对其他语言的支持,如Python等

2.8. 生态对接

能够导入PowerDesigner的pdm文件,老版本的PDMan文件,也能导出为word文档,导出相关设置等

2.9. 版本管理

实现数据表的版本管理,可生成增量DDL脚本

2.10. 数据库代码规范检查

支持数据库表字段、表名、索引等的命名规范、长度校验等,且支持自定义规则。

四、软件下载/使用手册

PDManer数据库建模工具

PDManer元数建模-v4-操作手册 · 语雀

五、PDManer多版本选择/对比

 六、PDManner源码地址

chiner-java: 元数建模的Java功能部分,元数建模是一款丰富数据库生态,独立于具体数据库之外的,数据库关系模型设计平台。icon-default.png?t=O83Ahttps://gitee.com/robergroup/chiner-java

PDManer: PDManer元数建模,是一款多操作系统开源免费的桌面版关系数据库模型建模工具,相对于PowerDesigner,他具备界面简洁美观,操作简单,上手容易等特点。支持Windows,Mac,Linux等操作系统,也能够支持国产操作系统。PDMan-2 --> CHINER-3 --> PDManer-4,数据库建模产品的升级延续icon-default.png?t=O83Ahttps://gitee.com/robergroup/pdmaner

七、代码生成器扩展

Vue代码生成模板

7.1. api.ts

// api.ts
{{  var pkgName = it.entity.env.base.nameSpace;
    var appName = pkgName.split('.')[pkgName.split('.').length - 1];
    var beanName = it.func.camel(it.entity.defKey,false);
    var beanClass = it.func.camel(it.entity.defKey,true);
     
    var pkVarName = "undefinedId";
    var pkDataType = "string";
    it.entity.fields.forEach(function(field){
        if(field.primaryKey){
            pkVarName = it.func.camel(field.defKey,false);
            pkDataType = field.type;
            return;
        }
    });
    
}}import { createAxios } from '@/utils/axios'
$blankline

// 接口地址
export const uri = '/{{=appName}}/{{=it.func.camel(it.entity.defKey,false)}}'
$blankline

// {{=it.entity.defName}}{{=it.entity.comment}}类型定义
export interface {{=beanClass}} {
    {{~it.entity.fields:field:index}}
    {{=it.func.camel(field.defKey,false)}}?: {{=field.type}}, // {{=field.defName}}{{~}}
}
$blankline

// 根据主键获取{{=it.entity.defName}}
export const get{{=beanClass}} = ( {{=pkVarName}}: {{=pkDataType}} ) => createAxios({ data: { {{=pkVarName}} }, url: uri + '/get{{=beanClass}}' })
$blankline

// 分页获取{{=it.entity.defName}}
export const get{{=beanClass}}Page = (query: any = {}, page: any = {}) => createAxios({data:{ query, page }, url: uri + '/get{{=beanClass}}Page' })
$blankline

// 获取全部{{=it.entity.defName}}
export const get{{=beanClass}}List = () => createAxios({ url: uri + '/get{{=beanClass}}List' })
$blankline

// 新增{{=it.entity.defName}}
export const save{{=beanClass}} = (data: {{=beanClass}}) => createAxios({ data, url: uri + '/save{{=beanClass}}' }, { successMsg: '新增成功' })
$blankline

// 更新{{=it.entity.defName}}
export const update{{=beanClass}} = (data: {{=beanClass}}) => createAxios({ data, url: uri + '/update{{=beanClass}}' },{ successMsg: '修改成功!' })
$blankline

// 删除{{=it.entity.defName}}
export const delete{{=beanClass}} = (data: {{=beanClass}}) => createAxios({ data, url: uri + '/delete{{=beanClass}}' },{ successMsg: '删除成功!' })
$blankline

// 批量新增{{=it.entity.defName}}
export const save{{=beanClass}}Batch = (list: {{=beanClass}}[]) => createAxios({ data : { list }, url: uri + '/save{{=beanClass}}Batch' }, { successMsg: '新增成功' })
$blankline

// 批量更新{{=it.entity.defName}}
export const update{{=beanClass}}Batch = (list: {{=beanClass}}[]) => createAxios({ data : { list }, url: uri + '/update{{=beanClass}}Batch' },{ successMsg: '修改成功!' })
$blankline

// 批量删除{{=it.entity.defName}}
export const delete{{=beanClass}}Batch = (list: {{=beanClass}}[]) => createAxios({ data : { list }, url: uri + '/delete{{=beanClass}}Batch' },{ successMsg: '删除成功!' })
$blankline

7.2. Table.vue

// table.vue
{{  var pkgName = it.entity.env.base.nameSpace;
    var appName = pkgName.split('.')[pkgName.split('.').length - 1];
    var beanName = it.func.camel(it.entity.defKey,false);
    var beanClass = it.func.camel(it.entity.defKey,true);
    var exclude = ['uid','created_by','created_time','updated_by','updated_time','deleted','sort'];
    
    var pkVarName = "undefinedId";
    it.entity.fields.forEach(function(field){
        if(field.primaryKey){
            pkVarName = it.func.camel(field.defKey,false);
            return;
        }
    });
    
}}<script setup lang="ts">
import {
    {{=beanClass}},
    get{{=beanClass}},
    get{{=beanClass}}Page,
    save{{=beanClass}},
    update{{=beanClass}},
    delete{{=beanClass}}Batch,
} from '@/apis/{{=appName}}/{{=beanClass}}'
import {{=beanClass}}Form from './{{=beanClass}}Form.vue'
import { computed, onMounted, reactive, ref } from 'vue'
import { ElTable } from 'element-plus'
import pinyin from 'pinyin-match'
import { replaceKeyword } from '@/utils/common'
import { useLayout } from '@/stores/layout'
$blankline

// 主题样式
const layout = useLayout()
$blankline

const emits = defineEmits([''])
$blankline

const formRef = ref<InstanceType<typeof {{=beanClass}}Form>>()
const tableRef = ref<InstanceType<typeof ElTable>>()
// 加载状态
const loading = ref<boolean>(false)
// 表格数据定义
const data = ref({
    current: 1,
    size: 50,
    total: 0,
    records: [] as {{=beanClass}}[],
})
// 查询表单
const query = reactive({
    search: ''
})
// 弹出表单配置
const dialog = reactive({
    show: false,
    title: '',
    form: {} as {{=beanClass}},
})
$blankline

// 表格数据过滤
const tableData = computed(() =>
    data.value.records.filter(
        (e) =>
            !query.search ||
{{~it.entity.fields:field:index}}{{? exclude.indexOf(field.defKey) == -1}}
            {{? field.type == 'string'}}(e.{{=it.func.camel(field.defKey,false)}} && pinyin.match(e.{{=it.func.camel(field.defKey,false)}}, query.search)){{? index < it.entity.fields.length - 1}} ||{{?}}{{?}}{{?}}{{~}}
    )
)
$blankline

// 获取表格数据
const getData = () => {
    loading.value = true
    get{{=beanClass}}Page(query, { current: data.value.current, size: data.value.size }).then((res) => {
        setTimeout(() => { loading.value = false }, 500)
        data.value = res.data
    })
}
$blankline

// 新增按钮事件
const handleAdd = () => {
    dialog.show = true
    dialog.title = '新增{{=it.entity.defName}}'
    dialog.form = {
    {{~it.entity.fields:field:index}}{{? exclude.indexOf(field.defKey) == -1}}
    {{=it.func.camel(field.defKey,false)}} : {{=field.defaultValue || "''"}} ,{{?}}{{~}}
    }
}
$blankline

// 删除按钮事件
const handleDel = () => {
    const list = tableRef.value?.getSelectionRows()
    loading.value = true
    delete{{=beanClass}}Batch(list).then(() => {
        getData()
    })
}
$blankline

// 查询事件
const handleQuery = () => {
    getData()
}
$blankline

// 表格行双击事件
const handleTableDbClick = (row: {{=beanClass}}) => {
    dialog.title = '{{=it.entity.defName}}'
    if (row.{{=pkVarName}}) {
        loading.value = true
        get{{=beanClass}}(row.{{=pkVarName}}).then((res) => {
            dialog.form = res.data
            setTimeout(() => {
                loading.value = false
                dialog.show = true
            }, 500)
        })
    }
}
$blankline

// 弹出表单提交事件
const dialogConfirm = async () => {
    await formRef.value?.formRef?.validate((valid) => {
        if (valid) {
            loading.value = true
            const fun = dialog.form.id ? update{{=beanClass}} : save{{=beanClass}}
            fun(dialog.form).then(() => { getData() }).finally(() => { setTimeout(() => { dialog.show = false }, 500) })
        }
    })
}
$blankline

onMounted(() => {
    getData()
})
</script>
$blankline

<template>
    <el-card>
        <template #header>
            <el-row justify="space-between">
                <!-- 快捷按钮 -->
                <el-col :span="6">
                    <el-button-group>
                        <table-button :loading="loading" type="add" @confirm="handleAdd" />
                        <table-button :loading="loading" type="del" tip @confirm="handleDel" />
                        <table-button :loading="loading" type="refresh" mini @confirm="getData" />
                    </el-button-group>
                </el-col>
                <!-- 查询表单 -->
                <el-col :span="18">
                    <el-form inline :model="query" class="query-form">
                        <el-form-item>
                            <el-input v-model="query.search" placeholder="筛选" clearable prefix-icon="el-icon-Search" />
                        </el-form-item>
                        <el-form-item>
                            <el-button :loading="loading" icon="el-icon-Search" circle title="查询" @click="handleQuery" />
                        </el-form-item>
                    </el-form>
                </el-col>
            </el-row>
        </template>
$blankline

        <!-- 表格主体 -->
        <el-table ref="tableRef" v-loading="loading" class="app-table" row-key="{{=pkVarName}}" :data="tableData" :height="layout.getMainHeight(77 + 40 + 20 + 32).height" @row-dblclick="handleTableDbClick">
            <el-table-column type="selection" align="center" />
            {{~it.entity.fields:field:index}}{{? exclude.indexOf(field.defKey) == -1}}
            <el-table-column prop="{{=it.func.camel(field.defKey,false)}}" label="{{=field.defName}}" show-overflow-tooltip align="center">
                <template #default="scope">
                    <span :title="scope.row.{{=it.func.camel(field.defKey,false)}}" v-html="replaceKeyword(scope.row.{{=it.func.camel(field.defKey,false)}}!, query.search)" />
                </template>
            </el-table-column>{{?}}{{~}}
        </el-table>
$blankline

        <el-pagination v-model:page-size="data.size" v-model:current-page="data.current" :total="data.total" layout="sizes, prev, pager, next, jumper, ->, total, slot" @size-change="getData" @current-change="getData" />
    </el-card>
$blankline

    <!-- 表单弹出框 -->
    <el-dialog v-model="dialog.show" :title="dialog.title" width="30%" draggable align-center destroy-on-close @close="dialog.form = {}">
        <el-scrollbar max-height="calc(100vh - 180px - 120px)">
            <{{=beanClass}}Form ref="formRef" v-model="dialog.form" @confirm="dialogConfirm" />
        </el-scrollbar>
        <template #footer>
            <el-button @click="formRef?.formRef?.resetFields()">重置</el-button>
            <el-button v-if="dialog.form.{{=pkVarName}}" type="primary" @click="dialogConfirm">修改</el-button>
            <el-button v-else type="primary" @click="dialogConfirm">新增</el-button>
        </template>
    </el-dialog>
$blankline

</template>
$blankline

<style scoped lang="scss">
// 查询表单靠右
.query-form {
    display: flex;
    justify-content: flex-end;
    // 隐藏查询表单校验提示
    .el-form-item {
        margin-bottom: 0;
    }
}
</style>


7.3. Form.vue

// form.vue
{{  var pkgName = it.entity.env.base.nameSpace;
    var appName = pkgName.split('.')[pkgName.split('.').length - 1];
    var beanName = it.func.camel(it.entity.defKey,false);
    var beanClass = it.func.camel(it.entity.defKey,true);
    var exclude = ['uid','created_by','created_time','updated_by','updated_time','deleted','sort'];
    
}}<script setup lang="ts">
import { {{=beanClass}} } from '@/apis/{{=appName}}/{{=beanClass}}'
import { reactive, ref } from 'vue'
import { FormInstance, FormRules } from 'element-plus'
$blankline

// 接收传入的参数
interface Props {
    modelValue?: {{=beanClass}}
}
const props = defineProps<Props>()
const form = ref<{{=beanClass}}>(props.modelValue || {})
const formRef = ref<FormInstance>()
// 表单校验规则
const rules = reactive<FormRules>({
{{~it.entity.fields:field:index}}{{? exclude.indexOf(field.defKey) == -1}}
    {{=it.func.camel(field.defKey,false)}} : [{ required: true, message: '请输入{{=field.defName}}', trigger: 'change' }],{{?}}{{~}}
})
// 向父组件暴露
defineExpose({ form, formRef })
</script>
$blankline

<template>
    <!-- 表单主体 -->
    <el-form ref="formRef" :model="form" :rules="rules" label-width="120px" @keyup.enter="emits('confirm')">
    {{~it.entity.fields:field:index}}{{? exclude.indexOf(field.defKey) == -1}}
        <el-form-item label="{{=field.defName}}" prop="{{=it.func.camel(field.defKey,false)}}">
            <{{? field.type == 'boolean'}}el-switch{{?? field.type == 'Date'}}el-date-picker{{??}}el-input{{?}} v-model="form.{{=it.func.camel(field.defKey,false)}}" placeholder="请输入{{=field.defName}}" />
        </el-form-item>{{?}}{{~}}
    </el-form>
</template>
$blankline

<style scoped lang="scss"></style>

7.4. index.vue

// index.vue
{{  var pkgName = it.entity.env.base.nameSpace;
    var appName = pkgName.split('.')[pkgName.split('.').length - 1];
    var beanName = it.func.camel(it.entity.defKey,false);
    var beanClass = it.func.camel(it.entity.defKey,true);
    
}}<script setup lang="ts">
import {{=beanClass}}Table from './components/{{=beanClass}}Table.vue'
</script>
$blankline

<template>
    <{{=beanClass}}Table />
</template>
$blankline

<style scoped lang="scss"></style>

7.5. TableButton.vue

<script setup lang="ts">
import { ref } from 'vue'

const emits = defineEmits(['confirm'])

interface Props {
    type: 'add' | 'edit' | 'del' | 'refresh'
    loading?: boolean
    text?: string
    icon?: string
    disabled?: boolean
    mini?: boolean
    tip?: boolean
    tipTitle?: string
    tipIcon?: string
    tipIconColor?: string
}

const props = withDefaults(defineProps<Props>(), {
    type: 'add',
    loading: false,
    text: '',
    icon: '',
    disabled: false,
    mini: false,
    tip: false,
    tipTitle: '确认删除?',
    tipIcon: 'el-icon-WarningFilled',
    tipIconColor: 'var(--el-color-error)',
})

const buttonRef = ref()
const popoverRef = ref()

const btnClick = () => {
    if (!props.tip) {
        emits('confirm')
    }
}

const btn = {
    add: {
        type: 'primary',
        text: props.text || '新增',
        icon: props.icon || 'el-icon-Plus',
        loading: 'el-icon-Loading',
    },
    edit: {
        type: 'warning',
        text: props.text || '编辑',
        icon: props.icon || 'el-icon-EditPen',
        loading: 'el-icon-Loading',
    },
    del: {
        type: 'danger',
        text: props.text || '删除',
        icon: props.icon || 'el-icon-Delete',
        loading: 'el-icon-Loading',
    },
    refresh: {
        type: '' as any,
        text: props.text || '刷新',
        icon: props.icon || 'el-icon-Refresh',
        loading: 'el-icon-Refresh',
    },
    opt: {
        type: 'success',
        text: props.text || '操作',
        icon: props.icon || 'el-icon-Operation',
        loading: 'el-icon-Loading',
    },
}[props.type]
</script>

<template>
    <el-button
        ref="buttonRef"
        :type="btn.type"
        :title="btn.text"
        :icon="btn.icon"
        :loading="loading"
        :loading-icon="btn.loading"
        :disabled="disabled"
        auto-insert-space
        @click="btnClick"
    >
        {{ !mini ? btn.text : '' }}
    </el-button>

    <el-popconfirm
        ref="popoverRef"
        :virtual-ref="buttonRef"
        :title="tipTitle"
        :icon="tipIcon"
        :icon-color="tipIconColor"
        virtual-triggering
        :disabled="!tip"
        @confirm="emits('confirm')"
    />
</template>

<style lang="scss">
.el-button [class*='el-icon'] + span {
    margin-left: v-bind('mini ? 0 : "6px"');
}
</style>

7.6. layout.ts

import { defineStore } from 'pinia'
import { CSSProperties } from 'vue'

const name = 'layout'

export const useLayout = defineStore(name, {
    state: () => ({
        headerHeight: 0, // 顶部高度
    }),
    actions: {
        getMainHeight(extra = 0): CSSProperties {
            return {
                height:
                    'calc(100vh - ' +
                    this.headerHeight +
                    'px - ' +
                    extra.toString() +
                    'px)',
            }
        },
        initHeaderHeight() {
            this.headerHeight = 0
        },
        addHeaderHeight(height = 0) {
            this.headerHeight += height
        },
    }
})

7.7. axios.ts

import router from '@/router'
import { CONFIG } from '@/config'
import axios, { AxiosRequestConfig } from 'axios'
import { ElLoading, ElNotification } from 'element-plus'
import { isEmpty } from 'lodash'

let loadingInstance: any
const pendingMap = new Map()

export interface AxiosOptions {
    loading?: boolean // 是否开启loading层效果, 默认为false
    timeout?: number // 超时时间
    successMsg?: string // 请求成功提示信息
    showErrorMsg?: boolean // 是否提示异常信息
}

/*
 * 创建Axios
 */
export const createAxios = (
    axiosConfig: AxiosRequestConfig,
    options: AxiosOptions = {}
) => {
    const Axios = axios.create({
        baseURL: CONFIG.apiHost,
        timeout: options.timeout || 1000 * 10,
        headers: {
            'Content-Type': 'application/json',
        },
        responseType: 'json',
        method: 'POST',
    })
    options = Object.assign(
        {
            loading: false,
            successMsg: '',
            showErrorMsg: true,
        },
        options
    )
    // 请求拦截
    Axios.interceptors.request.use(
        (config) => {
            removePending(config)
            addPending(config)
            // 创建loading实例
            if (options.loading) {
                loadingInstance = ElLoading.service()
            }
            return config
        },
        (error) => {
            return Promise.reject(error)
        }
    )

    // 响应拦截
    Axios.interceptors.response.use(
        (response) => {
            removePending(response.config)
            options.loading && loadingInstance.close() // 关闭loading
            if (response.config.responseType == 'json') {
                if (response.data && response.data.code !== '0') {
                    if (response.data.code == '10000') {
                        // logout()
                    }
                    if (options.showErrorMsg) {
                        ElNotification({
                            offset: 55,
                            type: 'error',
                            message: response.data.msg,
                        })
                    }
                    // 自动跳转到路由name或path,仅限server端返回302的情况
                    if (response.data.code == '302') {
                        if (response.data.data.name) {
                            router.push({ name: response.data.data.name })
                        } else if (response.data.data.path) {
                            router.push({ path: response.data.data.path })
                        }
                    }
                    // code不等于0, 页面then内的具体逻辑就不执行了
                    return Promise.reject(response.data)
                } else if (
                    !isEmpty(options.successMsg) &&
                    response.data.code == '0'
                ) {
                    ElNotification({
                        offset: 55,
                        message: options.successMsg
                            ? options.successMsg
                            : response.data.msg,
                        type: 'success',
                    })
                }
            }
            return response.data
        },
        (error) => {
            error.config && removePending(error.config)
            options.loading && loadingInstance.close() // 关闭loading
            options.showErrorMsg && httpErrorStatusHandle(error) // 处理错误状态码
            return Promise.reject(error) // 错误继续返回给到具体页面
        }
    )
    return Axios(axiosConfig)
}

/**
 * 处理异常
 */
const httpErrorStatusHandle = (error: any) => {
    // 处理被取消的请求
    if (axios.isCancel(error))
        return console.error(`因为请求重复被自动取消:${error.message}`)
    let message = ''
    if (error && error.response) {
        switch (error.response.status) {
            case 302:
                message = '接口重定向了!'
                break
            case 400:
                message = '参数不正确!'
                break
            case 401:
                message = '您没有权限操作!'
                break
            case 403:
                message = '您没有权限操作!'
                break
            case 404:
                message = `请求地址出错:${error.response.config.url}`
                break
            case 408:
                message = '请求超时!'
                break
            case 409:
                message = '系统已存在相同数据!'
                break
            case 500:
                message = '服务器内部错误!'
                break
            case 501:
                message = '服务未实现!'
                break
            case 502:
                message = '网关错误!'
                break
            case 503:
                message = '服务不可用!'
                break
            case 504:
                message = '服务暂时无法访问,请稍后再试!'
                break
            case 505:
                message = 'HTTP版本不受支持!'
                break
            default:
                message = '异常问题,请联系网站管理员!'
                break
        }
    }
    if (error.message.includes('timeout')) {
        message = '网络请求超时!'
    }
    if (error.message.includes('Network')) {
        message = window.navigator.onLine ? '服务端异常!' : '您断网了!'
    }
    ElNotification({
        offset: 55,
        type: 'error',
        message,
    })
}

/**
 * 储存每个请求的唯一cancel回调, 以此为标识
 */
const addPending = (config: AxiosRequestConfig) => {
    const pendingKey = getPendingKey(config)
    config.cancelToken =
        config.cancelToken ||
        new axios.CancelToken((cancel) => {
            if (!pendingMap.has(pendingKey)) {
                pendingMap.set(pendingKey, cancel)
            }
        })
}

/**
 * 删除重复的请求
 */
const removePending = (config: AxiosRequestConfig) => {
    const pendingKey = getPendingKey(config)
    if (pendingMap.has(pendingKey)) {
        const cancelToken = pendingMap.get(pendingKey)
        cancelToken(pendingKey)
        pendingMap.delete(pendingKey)
    }
}

/**
 * 生成每个请求的唯一key
 */
const getPendingKey = (config: AxiosRequestConfig) => {
    let { data } = config
    const { url, method, params } = config
    if (typeof data === 'string') data = JSON.parse(data)
    return [url, method, JSON.stringify(params), JSON.stringify(data)].join('&')
}

7.8. common.ts


// 关键字高亮替换
export const replaceKeyword = (origin: any, target: string) => {
    if (origin && typeof origin == 'string') {
        const m = pinyin.match(origin, target)
        if (m) {
            target = origin.substring(
                (m as Array<number>)[0],
                (m as Array<number>)[1] + 1
            )
        }
        return origin.replace(
            target,
            `<span class="highlight">${target}</span>`
        )
    } else {
        return origin
    }
}

### PDManer 导出时 Java 空指针异常解决方案 在处理PDManer导出功能中的`java.lang.NullPointerException`(空指针异常)时,核心在于确保任何可能为空的对象都在被调用前进行了有效的检查和初始化。具体措施如下: 对于可能导致空指针异常的操作,在执行这些操作之前应加入必要的判空逻辑。例如,如果某个变量可能是`null`,那么应该先验证其是否不为`null`再继续后续流程[^1]。 考虑一个场景:假设有一个用于存储数据记录的列表,在准备将其内容写入文件或数据库表单之前,应当确认该列表不是`null`且至少含有一个元素。可以采用如下的方式来预防潜在的风险: ```java if (dataList != null && !dataList.isEmpty()) { // 安全地进行导出操作... } else { System.out.println("Data list is empty or not initialized."); } ``` 另外一种常见的情况是在构建复杂的数据结构比如JSON对象的时候,某些字段可能会因为业务逻辑原因未能成功赋值从而保持默认状态即`null`。此时可以在设置属性值之前增加额外的安全防护机制,像下面这样处理: ```java String description = getDescription(); JSONObject jsonItem = new JSONObject(); jsonItem.putOpt("description", description); // putOpt 方法会自动跳过 null 值 ``` 针对特定于PDManer工具本身的配置项或者其他依赖资源加载失败也可能引发此类错误。因此建议开发者仔细审查相关部分代码,确保所有外部输入都经过充分校验,并提供合理的缺省行为以应对意外情况的发生[^3]。 最后值得注意的是,当涉及到数值转换成更高精度类型的实例化过程时,务必提前做好参数合法性检测工作,防止因传递了非法甚至未定义的状态而导致程序崩溃。这里给出一段示范性的代码片段作为参考: ```java Double numValue = getSomeNumber(); BigDecimal bigDecimalValue = numValue != null ? new BigDecimal(numValue.toString()) : BigDecimal.ZERO; // 或者更简洁的方式 bigDecimalValue = Optional.ofNullable(numValue).map(BigDecimal::new).orElse(BigDecimal.ZERO); ``` 通过上述手段能够有效减少乃至消除由于不当使用`null`所引起的运行期问题,提高系统的稳定性和健壮性[^4]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值