记录开发过程中遇到的各种问题及解决方案,供后续新模块开发参考
错误: Syntax error (-2007)
原因: 达梦(DM8)原生模式下不支持 MySQL 的行内 COMMENT 'xxx' 语法。
错误写法:
CREATE TABLE FOO (
ID VARCHAR(36) NOT NULL COMMENT '主键ID' -- ❌ DM不支持
);
正确写法:
CREATE TABLE FOO (
ID VARCHAR(36) NOT NULL
);
COMMENT ON TABLE FOO IS '表名';
COMMENT ON COLUMN FOO.ID IS '主键ID';
注意: .docs/sql/ 目录下的参考文件(如 重点关注人员信息.sql)使用的是 MySQL 语法,要在 DM 上执行需先转换。
错误: Table or view not found
原因: 在 DM 管理工具中连接时,SQL 可能执行到了错误的 schema 下,导致表和数据不在预期的 schema 中。
解决: 执行前确认当前 schema,或者使用 SET SCHEMA 模式名 切换到正确模式。
错误: Invalid column name [name]
原因: NAME、DESCRIPTION、STATUS 是达梦保留字,在 SQL 中直接使用会报错。
解决: 用双引号包围保留字,且需要注意大小写——达梦默认以大写存储对象名,所以双引号内也需大写:
-- ✅ 正确
INSERT INTO sys_permission (id, "NAME", "DESCRIPTION", "STATUS") VALUES (..., 'xxx', 'xxx', 1);
-- ❌ 错误(大小写不匹配)
INSERT INTO sys_permission (id, "name", "description", "status") VALUES (...);
错误: 视图创建成功后查询报错
原因: DM 原生模式下 TIMESTAMPDIFF + CURDATE() 是 MySQL 函数
解决: 使用 DM 原生函数:
-- ❌ MySQL 语法
TIMESTAMPDIFF(YEAR, birth_date, CURDATE()) AS age
-- ✅ DM 原生语法
FLOOR(MONTHS_BETWEEN(CURRENT_DATE, birth_date) / 12) AS age
现象: 数据库中有权限记录,角色授权也正确,但前端按钮不显示。
根因: 后端 SysPermissionController 查询权限时过滤条件为 status = 1。但插入 SQL 中 status 字段为 NULL,导致权限码不被返回给前端。
参考: V20260604_9__fix_permission_status.sql
解决: 插入按钮权限时 status 必须设为 1:
-- ✅ 正确
INSERT INTO sys_permission (..., status, internal_or_external)
VALUES (..., 1, 0); -- status = 1
-- ❌ 错误
INSERT INTO sys_permission (..., status, internal_or_external)
VALUES (..., NULL, 0); -- status = NULL
修复已有数据:
UPDATE sys_permission SET status = 1
WHERE perms LIKE '模块名:%' AND (status IS NULL OR status != 1);
现象: 权限已插入、角色已关联、status=1,但 v-auth 仍然隐藏按钮
排查步骤:
permissionStore.getPermCodeList 是否包含目标权限码status 是否为 1参考模块: FocusPersonnelList.vue
正确模式:
<template>
<div class="p-2">
<!-- 手动搜索表单 -->
<div class="jeecg-basic-table-form-container">
<a-form ref="formRef" ...>
<a-row :gutter="24">
<a-col :lg="6" :md="8" :sm="24">
<a-form-item label="姓名" name="fullName">
<a-input ... />
</a-form-item>
</a-col>
</a-row>
</a-form>
</div>
<!-- BasicTable -->
<BasicTable @register="registerTable" :rowSelection="rowSelection">
<template #tableTitle>
<!-- 顶部按钮 -->
</template>
<template #bodyCell="{ column, text }">
<!-- 字典翻译 -->
</template>
<template #action="{ record }">
<TableAction :actions="getTableAction(record)" />
</template>
</BasicTable>
<XxxModal ref="registerModal" @success="handleSuccess" />
</div>
</template>
<script lang="ts" setup>
import { useListPage } from '/@/hooks/system/useListPage';
const { tableContext, onExportXls } = useListPage({
tableProps: { api: list, columns, useSearchForm: false, ... },
exportConfig: { ... },
});
const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext;
</script>
❌ 错误模式(不要用):
// 不要用 formConfig.schemas 方式(前端的 useSearchForm: true)
const [registerTable] = useTable({
formConfig: { schemas: [...] }, // ❌
});
参考模块: FocusPersonnelModal.vue
正确模式:
<j-modal :visible="visible" :title="title" @cancel="handleCancel" @ok="handleOk">
<XxxDetail v-if="isDetailMode" :record="currentRecord" />
<XxxForm v-else ref="registerForm" @ok="submitCallback" />
</j-modal>
<script setup>
defineExpose({ add, edit, detail });
</script>
❌ 错误模式(不要用):
const [registerModal, { openModal }] = useModal(); // ❌
参考模块: FocusPersonnelDetail.vue
正确模式:
<template>
<a-spin :spinning="loading">
<div v-if="detailData">
<!-- 顶部头像+概要 -->
<div class="detail-header">
<a-avatar>{{ detailData.fullName?.substring(0, 1) }}</a-avatar>
<div class="header-info">
<span class="name">{{ detailData.fullName }}</span>
</div>
</div>
<!-- Descriptions详情 -->
<a-descriptions bordered :column="2" size="small">
<a-descriptions-item label="字段名">{{ detailData.field }}</a-descriptions-item>
</a-descriptions>
</div>
</a-spin>
</template>
<script setup>
import { queryDetailById } from '../api';
const props = defineProps({ record: { type: Object, default: () => ({}) } });
watch(() => props.record, () => { loadDetail(); }, { immediate: true, deep: true });
async function loadDetail() {
const id = props.record?.id;
if (!id) return;
const res = await queryDetailById({ id });
detailData.value = res;
}
</script>
现象: 列表显示数值而非文字(如性别显示 "1" 而非 "男性")
解决: 使用 useDict 钩子加载字典:
import { useDict } from '/@/hooks/dictionary/useDict';
const { getDictText } = useDict(['JobSeekerStatus', 'JobSeekerCategory', ...]);
模板中使用 getDictText 翻译:
<template v-else-if="column.dataIndex === 'jobSearchStatus'">
{{ getDictText('JobSeekerStatus', text) || text }}
</template>
性别用硬编码映射即可:
const genderMap: Record<string, string> = { '1': '男性', '2': '女性' };
错误: Cannot read properties of undefined (reading 'length')
原因: 表格未初始化时 selectedRowKeys 为 undefined,直接访问 .length 报错
解决:
:disabled="!selectedRowKeys || selectedRowKeys.length === 0"
| 功能 | MySQL 语法 | DM 语法 |
|---|---|---|
| 当前日期 | CURDATE() |
CURRENT_DATE |
| 年龄计算 | TIMESTAMPDIFF(YEAR, birth, CURDATE()) |
FLOOR(MONTHS_BETWEEN(CURRENT_DATE, birth) / 12) |
| 标识符引用 | `name` |
"NAME"(双引号,大写) |
| 行内注释 | COMMENT 'xxx' |
不支持,用 COMMENT ON |
| 删除视图 | DROP VIEW IF EXISTS v_name |
支持 |
| 删除表 | DROP TABLE IF EXISTS t_name |
支持 |
| 字符串拼接 | CONCAT('%', val, '%') |
CONCAT('%', val, '%') |
| 分页 | LIMIT ? OFFSET ? |
LIMIT ? OFFSET ? |
COMMENT,改用 COMMENT ONDROP TABLE IF EXISTS 支持重复执行FLOOR(MONTHS_BETWEEN(...)) 替代 TIMESTAMPDIFFDROP VIEW IF EXISTS 支持重复执行status 设为 1(非 NULL)NAME/DESCRIPTION/STATUS 用大写双引号FLOOR(MONTHS_BETWEEN(...)) 计算useListPage + 手动 <a-form> 搜索ref 方式 + <j-modal>,非 useModal<a-descriptions> + watch 加载useDict 加载字典翻译v-auth 权限控制!selectedRowKeys 判空