新模块开发经验教训记录.md 9.0 KB

就业援助模块开发问题记录

记录开发过程中遇到的各种问题及解决方案,供后续新模块开发参考


1. SQL 执行问题

1.1 达梦不支持行内 COMMENT 语法

错误: 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 上执行需先转换。


1.2 SQL 执行到错误的模式(Schema)

错误: Table or view not found

原因: 在 DM 管理工具中连接时,SQL 可能执行到了错误的 schema 下,导致表和数据不在预期的 schema 中。

解决: 执行前确认当前 schema,或者使用 SET SCHEMA 模式名 切换到正确模式。


1.3 达梦保留字需用双引号包围

错误: Invalid column name [name]

原因: NAMEDESCRIPTIONSTATUS 是达梦保留字,在 SQL 中直接使用会报错。

解决: 用双引号包围保留字,且需要注意大小写——达梦默认以大写存储对象名,所以双引号内也需大写:

-- ✅ 正确
INSERT INTO sys_permission (id, "NAME", "DESCRIPTION", "STATUS") VALUES (..., 'xxx', 'xxx', 1);

-- ❌ 错误(大小写不匹配)
INSERT INTO sys_permission (id, "name", "description", "status") VALUES (...);

1.4 视图 SQL 需使用 DM 原生函数

错误: 视图创建成功后查询报错

原因: DM 原生模式下 TIMESTAMPDIFF + CURDATE() 是 MySQL 函数

解决: 使用 DM 原生函数:

-- ❌ MySQL 语法
TIMESTAMPDIFF(YEAR, birth_date, CURDATE()) AS age

-- ✅ DM 原生语法
FLOOR(MONTHS_BETWEEN(CURRENT_DATE, birth_date) / 12) AS age

2. 菜单权限问题

2.1 权限 status = NULL 导致按钮不显示

现象: 数据库中有权限记录,角色授权也正确,但前端按钮不显示。

根因: 后端 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);

2.2 v-auth 权限指令不生效

现象: 权限已插入、角色已关联、status=1,但 v-auth 仍然隐藏按钮

排查步骤:

  1. 在控制台查看 permissionStore.getPermCodeList 是否包含目标权限码
  2. 如果不在列表中,检查 status 是否为 1
  3. 重启后端服务(清除缓存)
  4. 重新登录(前端重新加载权限列表)

3. 前端组件模式

3.1 列表页必须使用 useListPage + 手动搜索表单

参考模块: 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: [...] },  // ❌
});

3.2 Modal 组件必须使用 ref 方式

参考模块: 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();  // ❌

3.3 详情页组件模式

参考模块: 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>

3.4 字典翻译必须使用 useDict

现象: 列表显示数值而非文字(如性别显示 "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': '女性' };

3.5 selectedRowKeys 判空保护

错误: Cannot read properties of undefined (reading 'length')

原因: 表格未初始化时 selectedRowKeysundefined,直接访问 .length 报错

解决:

:disabled="!selectedRowKeys || selectedRowKeys.length === 0"

4. 达梦函数兼容性速查表

功能 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 ?

5. 开发流程检查清单

新建模块时逐项检查:

  • 建表SQL:去掉行内 COMMENT,改用 COMMENT ON
  • 建表SQL:使用 DROP TABLE IF EXISTS 支持重复执行
  • 视图SQL:使用 FLOOR(MONTHS_BETWEEN(...)) 替代 TIMESTAMPDIFF
  • 视图SQL:使用 DROP VIEW IF EXISTS 支持重复执行
  • 菜单SQL:status 设为 1(非 NULL)
  • 菜单SQL:保留字 NAME/DESCRIPTION/STATUS 用大写双引号
  • 菜单SQL:先 DELETE 清理旧数据再 INSERT(兼容重复执行)
  • Mapper XML:年龄用 FLOOR(MONTHS_BETWEEN(...)) 计算
  • Mapper XML:不使用反引号
  • 前端列表页:使用 useListPage + 手动 <a-form> 搜索
  • 前端Modal:使用 ref 方式 + <j-modal>,非 useModal
  • 前端详情:使用 <a-descriptions> + watch 加载
  • 前端字典:使用 useDict 加载字典翻译
  • 导出按钮:保留 v-auth 权限控制
  • selectedRowKeys:添加 !selectedRowKeys 判空