数据库设计与表结构详解|信息化项目全流程管理系统源码逐行精讲(五)

数据库设计与表结构详解|信息化项目全流程管理系统源码逐行精讲(五)

本系列文章配套完整源码,已开源至 GitCode:https://gitcode.com/u010972645/IAS-ProjectArchiveSystem

读者可克隆或下载 ZIP 包,配合本系列文章逐行学习。每篇文章对应源码中的具体文件,跟着代码学开发,直接上手做二次开发。


前言

上一篇我们剖析了四层架构的数据流全景,知道了数据从 UI 到数据库再返回的完整路径。本文将把放大镜对准数据流的终点------数据库,逐表讲解 14 张表的设计细节。

数据库设计是整个系统的地基。地基没打好,上面盖再漂亮的楼也是危楼。很多开发者在写桌面应用时,习惯性地把所有数据塞进一张大表,或者随手建几张表不加任何约束,等到数据量上来或者需求变化时,才发现改不动了。

本系统基于 SQLite,采用"项目-分卷-附件"三级档案结构作为核心数据模型,配合 RBAC 权限体系(用户-角色-权限)和 AI 智能助手管理表,共 14 张表。本文将逐表解析字段定义、数据类型、约束条件、索引设计,并用 ER 图展示表间关系。

学完本文,你将能够:

  • 画出 14 张表的完整 ER 实体关系图
  • 理解"项目-分卷-附件"三级档案结构的设计思路
  • 掌握组合唯一约束的设计场景和实现方法
  • 理解 RBAC 五表模型(用户-角色-权限-部门-关联)的设计规范
  • 了解 AI 智能助手的技能和工作流如何通过数据库管理
  • 掌握 SQLite 种子数据初始化和数据库迁移的策略

源码对照 :本文讲解的代码全部位于 src/models/db_models.py,共 832 行。建议打开源码对照阅读。


一、数据库连接设计

1.1 为什么选择 SQLite

对比维度 SQLite MySQL PostgreSQL
部署方式 零部署,单文件 需安装服务端 需安装服务端
跨平台 原生跨平台 需额外适配 需额外适配
并发模型 文件级锁(单写多读) 行级锁 行级锁
适用场景 桌面应用、嵌入式 Web 应用、中大型项目 大型项目、复杂查询
Python 依赖 标准库内置 需 mysqlclient 需 psycopg2

政务桌面应用选择 SQLite 的核心理由:

  • 零部署:用户无需安装数据库服务,一个 .exe + 一个 .db 文件即可运行
  • 跨平台:Windows 和银河麒麟 V10 均原生支持,无需额外适配
  • 标准库内置import sqlite3 即可使用,不增加打包体积
  • 单文件存储:便于备份和迁移,复制 .db 文件即可

1.2 数据库路径管理

python 复制代码
# src/models/db_models.py(第 1-24 行)

import sqlite3
import os
import json
import logging
from dotenv import load_dotenv
import hashlib

from src.utils.app_paths import get_project_root

PROJECT_ROOT = get_project_root()

# 加载 .env:优先从 exe 同级目录加载,不存在则回退到源码目录
_env_path = os.path.join(PROJECT_ROOT, '.env')
if os.path.exists(_env_path):
    load_dotenv(_env_path)
else:
    # 源码运行时回退
    _src_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
    load_dotenv(os.path.join(_src_root, '.env'))

DB_PATH = os.getenv("DB_PATH", 'db/archive.db')
if not os.path.isabs(DB_PATH):
    DB_PATH = os.path.join(PROJECT_ROOT, DB_PATH)
DB_PATH = os.path.normpath(DB_PATH)

这段代码处理了两个关键问题:

问题 1:源码运行 vs 打包运行的路径差异

get_project_root() 在源码运行时返回项目根目录,打包运行时返回 exe 所在目录。这样 .env 文件和数据库文件都能正确定位。

问题 2:相对路径 vs 绝对路径

DB_PATH 从环境变量读取,默认值是相对路径 db/archive.db。通过 os.path.isabs() 检测后拼接为绝对路径,确保无论工作目录在哪里,数据库路径都能正确解析。

1.3 连接函数设计

python 复制代码
# src/models/db_models.py(第 26-31 行)

def get_db_connection():
    """获取数据库连接,并启用外键"""
    conn = sqlite3.connect(DB_PATH)
    conn.row_factory = sqlite3.Row  # 使返回结果支持列名访问
    conn.execute("PRAGMA foreign_keys = ON")  # 启用外键约束
    return conn

三个关键设置,缺一不可:

设置 作用 不设置的后果
row_factory = sqlite3.Row 支持列名访问 row['name'] 只能用索引 row[1],代码可读性差
PRAGMA foreign_keys = ON 启用外键级联删除 级联删除不生效,留下孤立数据
连接即用即关 防止文件锁 多连接并发时 database is locked

踩坑经验 :SQLite 默认不启用外键约束!如果不执行 PRAGMA foreign_keys = ONON DELETE CASCADE 完全不生效,删除项目后分卷和附件会变成孤立数据。这是 SQLite 的历史遗留设计,每次连接都必须手动启用。

1.4 init_db() 初始化流程

python 复制代码
# src/models/db_models.py(第 34-49 行)

def init_db():
    """不存在db文件夹则创建"""
    db_dir = os.path.join(PROJECT_ROOT, 'db')
    os.makedirs(db_dir, exist_ok=True)

    storage_dir = os.path.join(PROJECT_ROOT, 'storage')
    os.makedirs(storage_dir, exist_ok=True)

    assets_dir = os.path.join(PROJECT_ROOT, 'assets')
    os.makedirs(assets_dir, exist_ok=True)

    log_dir = os.path.join(PROJECT_ROOT, 'log')
    os.makedirs(log_dir, exist_ok=True)

    conn = get_db_connection()
    cursor = conn.cursor()

init_db() 在应用启动时由 main.py 调用,执行流程:

复制代码
init_db()
  ├── 创建目录结构(db/ storage/ assets/ log/)
  ├── 获取数据库连接
  ├── 建表(14 张表,CREATE TABLE IF NOT EXISTS)
  ├── 数据库迁移(ALTER TABLE / 重建表)
  ├── 种子数据初始化(仅表为空时插入)
  ├── 创建索引(15 个索引)
  └── conn.commit() + conn.close()

设计要点 :所有建表语句使用 CREATE TABLE IF NOT EXISTS,确保重复执行 init_db() 不会报错。种子数据插入前先 SELECT COUNT(*) 检查,只在表为空时插入,避免重复数据。


二、核心业务表:三级档案结构

2.1 项目-分卷-附件的业务背景

政务信息化项目档案管理遵循国家档案管理规范,采用"项目 → 分卷 → 附件"三级结构:

复制代码
项目(智慧政务平台)
  ├── 第1卷(立项阶段材料)
  │   ├── 附件001(项目建议书.pdf)
  │   ├── 附件002(可行性研究报告.docx)
  │   └── 附件003(审批批复.pdf)
  ├── 第2卷(采购阶段材料)
  │   ├── 附件001(招标文件.pdf)
  │   └── 附件002(中标通知书.pdf)
  └── 第3卷(验收阶段材料)
      ├── 附件001(验收报告.docx)
      └── 附件002(验收证书.pdf)

注意:分卷编号在项目内独立计数,附件编号在分卷内独立计数。不同项目的分卷编号可以相同(都从 001 开始),不同分卷的附件编号也可以相同。

2.2 projects 表------项目主表

python 复制代码
# src/models/db_models.py(第 52-66 行)

cursor.execute('''
    CREATE TABLE IF NOT EXISTS projects(
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        project_code TEXT UNIQUE NOT NULL,
        project_name TEXT NOT NULL,
        manager TEXT NOT NULL,
        department TEXT,
        budget REAL,
        actual_cost REAL,
        start_date TEXT,
        end_date TEXT,
        status TEXT, -- 立项/采购/建设/验收
        create_time TEXT DEFAULT(datetime('now','localtime'))
    )
''')
字段 类型 约束 说明
id INTEGER PRIMARY KEY AUTOINCREMENT 自增主键
project_code TEXT UNIQUE NOT NULL 项目编号,全局唯一(如 P001)
project_name TEXT NOT NULL 项目名称
manager TEXT NOT NULL 项目经理
department TEXT 所属部门
budget REAL 预算金额(单位:万元)
actual_cost REAL 实际成本
start_date TEXT 开始日期
end_date TEXT 结束日期
status TEXT 项目状态(INIT/PURCHASE/CONSTRUCTION/ACCEPTANCE)
create_time TEXT DEFAULT(datetime('now','localtime')) 创建时间

设计要点

  1. project_code 全局唯一:项目编号是业务主键,全系统不允许重复
  2. status 存储英文枚举码 :数据库存 INIT/PURCHASE/CONSTRUCTION/ACCEPTANCE,UI 层通过字典翻译为"立项"/"采购"/"建设"/"验收"
  3. 日期用 TEXT 而非 DATE :SQLite 没有原生的 DATE 类型,TEXT 存储 YYYY-MM-DD 格式字符串,排序时天然正确
  4. datetime('now','localtime'):获取本地时间(非 UTC),适合政务系统的时区需求

2.3 project_volume 表------分卷表

python 复制代码
# src/models/db_models.py(第 82-96 行)

cursor.execute('''
    CREATE TABLE IF NOT EXISTS project_volume(
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        project_id INTEGER NOT NULL,
        volume_no TEXT NOT NULL,
        volume_code TEXT UNIQUE NOT NULL,
        volume_name TEXT NOT NULL,
        total_pages INTEGER NOT NULL,
        retention_period TEXT NOT NULL,
        remarks TEXT,
        create_time TEXT DEFAULT(datetime('now','localtime')),
        FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
        UNIQUE(project_id, volume_no)
    )
''')
字段 类型 约束 说明
id INTEGER PRIMARY KEY AUTOINCREMENT 自增主键
project_id INTEGER NOT NULL, FOREIGN KEY 所属项目 ID
volume_no TEXT NOT NULL 分卷序号(如 001、002)
volume_code TEXT UNIQUE NOT NULL 分卷档号,全局唯一
volume_name TEXT NOT NULL 分卷名称
total_pages INTEGER NOT NULL 分卷总页数(自动计算)
retention_period TEXT NOT NULL 保管期限(10YEARS/30YEARS/PERMANENT)
remarks TEXT 备注
create_time TEXT DEFAULT(datetime('now','localtime')) 创建时间

核心约束UNIQUE(project_id, volume_no) ------组合唯一约束

组合唯一约束的设计原因 :档案管理规范中,分卷编号从 001 开始递增,不同项目的分卷编号独立计数。如果用全局唯一约束(volume_no TEXT UNIQUE),项目 A 的分卷 001 会阻止项目 B 创建分卷 001,不符合业务逻辑。组合唯一约束确保"同一项目内分卷编号不重复,不同项目可以重复"。

外键级联ON DELETE CASCADE ------删除项目时自动删除该项目下所有分卷,防止孤立数据。

2.4 project_file 表------附件表

python 复制代码
# src/models/db_models.py(第 108-128 行)

cursor.execute('''
    CREATE TABLE IF NOT EXISTS project_file(
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        project_id INTEGER NOT NULL,
        volume_id INTEGER,
        file_no TEXT NOT NULL,
        file_code TEXT,
        responsible_unit TEXT NOT NULL,
        file_name TEXT NOT NULL,
        file_path TEXT NOT NULL,
        file_date TEXT NOT NULL,
        pages INTEGER NOT NULL,
        remarks TEXT,
        file_type TEXT,
        create_time TEXT DEFAULT(datetime('now','localtime')),
        FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
        FOREIGN KEY (volume_id) REFERENCES project_volume(id) ON DELETE CASCADE,
        UNIQUE(file_no, volume_id)
    )
''')
字段 类型 约束 说明
id INTEGER PRIMARY KEY AUTOINCREMENT 自增主键
project_id INTEGER NOT NULL, FOREIGN KEY 所属项目 ID
volume_id INTEGER FOREIGN KEY 所属分卷 ID(允许 NULL,未归档附件)
file_no TEXT NOT NULL 附件序号(分卷内唯一)
file_code TEXT 文件档号(可选)
responsible_unit TEXT NOT NULL 责任单位
file_name TEXT NOT NULL 文件名称
file_path TEXT NOT NULL 文件存储路径
file_date TEXT NOT NULL 文件日期
pages INTEGER NOT NULL 页数(自动计算)
remarks TEXT 备注
file_type TEXT 文件类型(ORIGINAL/COPY/PRINT)
create_time TEXT DEFAULT(datetime('now','localtime')) 创建时间

核心约束UNIQUE(file_no, volume_id) ------附件序号在分卷内唯一

volume_id 允许 NULL 的设计:附件可以先上传到项目下但不归档到任何分卷,等整理后再分配到具体分卷。这是档案管理中常见的"暂存"需求。

双重外键级联 :附件同时关联项目和分卷,两个外键都设置 ON DELETE CASCADE。删除项目时级联删除附件,删除分卷时也级联删除附件。

2.5 三级结构的 ER 关系

复制代码
┌──────────┐     1:N     ┌────────────────┐     1:N     ┌──────────────┐
│ projects │────────────→│ project_volume │────────────→│ project_file │
│          │             │                │             │              │
│ id (PK)  │             │ id (PK)        │             │ id (PK)      │
│ code (U) │             │ project_id(FK) │             │ project_id(FK)│
│ name     │             │ volume_no      │             │ volume_id(FK)│
│ manager  │             │ volume_code(U) │             │ file_no      │
│ budget   │             │ total_pages    │             │ file_name    │
│ status   │             │ retention      │             │ pages        │
└──────────┘             └────────────────┘             └──────────────┘
      │                         │                            │
      │  ON DELETE CASCADE      │  ON DELETE CASCADE         │
      └─────────────────────────┴────────────────────────────┘
                删除项目 → 自动删除分卷 → 自动删除附件

组合唯一约束对比

约束 含义
project_volume UNIQUE(project_id, volume_no) 分卷编号在项目内唯一
project_file UNIQUE(file_no, volume_id) 附件编号在分卷内唯一
project_volume volume_code TEXT UNIQUE 分卷档号全局唯一
projects project_code TEXT UNIQUE 项目编号全局唯一

设计原则:业务编号(序号)用组合唯一约束,档号(全局标识)用全局唯一约束。序号是人工编排的,允许跨范围重复;档号是系统级唯一标识,不允许重复。


三、RBAC 权限体系表

3.1 RBAC 模型概述

系统采用经典的 RBAC(Role-Based Access Control)模型:

复制代码
用户(User) ──→ 角色(Role) ──→ 权限(Permission)
     │              │                │
     └── 部门(Dept) │                ├── menu 类型(菜单可见性)
                    │                └── action 类型(按钮操作权限)
                    │
                    └── role_permission 关联表(多对多)

用户登录后,系统查询其角色对应的权限编码列表(perm_codes),UI 层根据 hasattr() 判断按钮显隐。

3.2 dept 表------部门表

python 复制代码
# src/models/db_models.py(第 208-218 行)

cursor.execute('''
    CREATE TABLE IF NOT EXISTS dept(
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        dept_code TEXT UNIQUE NOT NULL,
        dept_name TEXT UNIQUE NOT NULL,
        parent_id INTEGER DEFAULT 0,
        sort_order INTEGER DEFAULT 0,
        enabled INTEGER DEFAULT 1,
        remark TEXT,
        create_time TEXT DEFAULT(datetime('now','localtime'))
    )
''')
字段 类型 约束 说明
id INTEGER PRIMARY KEY AUTOINCREMENT 自增主键
dept_code TEXT UNIQUE NOT NULL 部门编码(如 D001)
dept_name TEXT UNIQUE NOT NULL 部门名称
parent_id INTEGER DEFAULT 0 父部门 ID(0 表示顶级部门)
sort_order INTEGER DEFAULT 0 排序序号
enabled INTEGER DEFAULT 1 是否启用(1=启用,0=禁用)
remark TEXT 备注

种子数据

python 复制代码
default_depts = [
    ('D001', '系统管理部', 0, 1, 1, '系统管理部门'),
    ('D002', '信息化服务一部', 0, 2, 1, '信息化服务一部'),
    ('D003', '信息化服务二部', 0, 3, 1, '信息化服务二部'),
    ('D004', '信息化服务三部', 0, 4, 1, '信息化服务三部'),
]

parent_id 树形结构 :预留了父子部门关系字段,parent_id=0 表示顶级部门。当前系统使用扁平结构,但表结构已支持未来扩展为树形部门架构。

3.3 role 表------角色表

python 复制代码
# src/models/db_models.py(第 235-244 行)

cursor.execute('''
    CREATE TABLE IF NOT EXISTS role(
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        role_code TEXT UNIQUE NOT NULL,
        role_name TEXT UNIQUE NOT NULL,
        sort_order INTEGER DEFAULT 0,
        enabled INTEGER DEFAULT 1,
        remark TEXT,
        create_time TEXT DEFAULT(datetime('now','localtime'))
    )
''')
字段 类型 约束 说明
id INTEGER PRIMARY KEY AUTOINCREMENT 自增主键
role_code TEXT UNIQUE NOT NULL 角色编码(如 admin)
role_name TEXT UNIQUE NOT NULL 角色名称
sort_order INTEGER DEFAULT 0 排序序号
enabled INTEGER DEFAULT 1 是否启用

种子数据:三种预置角色

role_code role_name 说明
admin 管理员 拥有全部 20 项权限
manager 项目负责人 拥有项目/分卷/附件的 CRUD 权限(12 项)
user 普通用户 仅拥有查看权限(3 项)

3.4 permission 表------权限表

python 复制代码
# src/models/db_models.py(第 260-271 行)

cursor.execute('''
    CREATE TABLE IF NOT EXISTS permission(
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        perm_code TEXT UNIQUE NOT NULL,
        perm_name TEXT NOT NULL,
        perm_type TEXT DEFAULT 'menu',
        parent_id INTEGER DEFAULT 0,
        sort_order INTEGER DEFAULT 0,
        enabled INTEGER DEFAULT 1,
        remark TEXT,
        create_time TEXT DEFAULT(datetime('now','localtime'))
    )
''')
字段 类型 约束 说明
id INTEGER PRIMARY KEY AUTOINCREMENT 自增主键
perm_code TEXT UNIQUE NOT NULL 权限编码(如 project_add)
perm_name TEXT NOT NULL 权限名称(如"项目新增")
perm_type TEXT DEFAULT 'menu' 权限类型(menu/action)
parent_id INTEGER DEFAULT 0 父权限 ID
sort_order INTEGER DEFAULT 0 排序序号
enabled INTEGER DEFAULT 1 是否启用

权限编码命名规范

python 复制代码
default_permissions = [
    # 项目管理权限(4项)
    ('project_view', '项目查看', 'menu', ...),
    ('project_add', '项目新增', 'action', ...),
    ('project_edit', '项目编辑', 'action', ...),
    ('project_delete', '项目删除', 'action', ...),
    # 分卷管理权限(4项)
    ('volume_view', '分卷查看', 'menu', ...),
    ('volume_add', '分卷新增', 'action', ...),
    ('volume_edit', '分卷编辑', 'action', ...),
    ('volume_delete', '分卷删除', 'action', ...),
    # 附件管理权限(4项)
    ('file_view', '附件查看', 'menu', ...),
    ('file_add', '附件新增', 'action', ...),
    ('file_edit', '附件编辑', 'action', ...),
    ('file_delete', '附件删除', 'action', ...),
    # 系统管理权限(8项)
    ('sys_dict', '字典管理', 'menu', ...),
    ('sys_user', '用户管理', 'menu', ...),
    ('sys_dept', '部门管理', 'menu', ...),
    ('sys_role', '角色管理', 'menu', ...),
    ('sys_perm', '权限管理', 'menu', ...),
    ('sys_config', '配置管理', 'menu', ...),
    ('sys_log', '日志管理', 'menu', ...),
    ('sys_template', '模板管理', 'menu', ...),
]

命名规范

  • 查看权限{模块}_view,类型为 menu,控制菜单/Tab 页可见性
  • 操作权限{模块}_{动作},类型为 action,控制按钮显隐
  • 系统权限sys_{模块},统一前缀,类型为 menu

设计决策 :权限类型分为 menu(菜单级)和 action(按钮级)。UI 层根据权限类型决定是隐藏整个 Tab 页还是隐藏单个操作按钮。

3.5 role_permission 表------角色权限关联表

python 复制代码
# src/models/db_models.py(第 304-314 行)

cursor.execute('''
    CREATE TABLE IF NOT EXISTS role_permission(
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        role_id INTEGER NOT NULL,
        perm_id INTEGER NOT NULL,
        create_time TEXT DEFAULT(datetime('now','localtime')),
        FOREIGN KEY (role_id) REFERENCES role(id) ON DELETE CASCADE,
        FOREIGN KEY (perm_id) REFERENCES permission(id) ON DELETE CASCADE,
        UNIQUE(role_id, perm_id)
    )
''')

这是角色和权限的多对多关联表:

字段 类型 约束 说明
id INTEGER PRIMARY KEY AUTOINCREMENT 自增主键
role_id INTEGER NOT NULL, FOREIGN KEY 角色 ID
perm_id INTEGER NOT NULL, FOREIGN KEY 权限 ID

核心约束UNIQUE(role_id, perm_id) ------同一角色不能重复分配同一权限

种子数据------三级权限分配

python 复制代码
# admin 角色:分配全部 20 项权限
cursor.execute("SELECT id FROM permission")
perm_ids = [row[0] for row in cursor.fetchall()]
for perm_id in perm_ids:
    cursor.execute('INSERT INTO role_permission(role_id, perm_id) VALUES(?, ?)', 
                  (admin_role_id, perm_id))

# manager 角色:分配项目/分卷/附件的 CRUD 权限(12 项)
manager_perms = ['project_view', 'project_add', 'project_edit', 'project_delete',
                 'volume_view', 'volume_add', 'volume_edit', 'volume_delete',
                 'file_view', 'file_add', 'file_edit', 'file_delete']

# user 角色:仅分配查看权限(3 项)
user_perms = ['project_view', 'volume_view', 'file_view']
角色 权限数 权限范围
admin 20 全部权限(项目+分卷+附件+系统管理)
manager 12 项目/分卷/附件的增删改查
user 3 仅查看权限

3.6 users 表------用户表

python 复制代码
# src/models/db_models.py(第 347-358 行)

cursor.execute('''
    CREATE TABLE IF NOT EXISTS users(
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        username TEXT UNIQUE NOT NULL,
        password_hash TEXT NOT NULL,
        role_id INTEGER,
        dept_id INTEGER,
        create_time TEXT DEFAULT(datetime('now','localtime')),
        FOREIGN KEY (role_id) REFERENCES role(id) ON DELETE SET NULL,
        FOREIGN KEY (dept_id) REFERENCES dept(id) ON DELETE SET NULL
    )
''')
字段 类型 约束 说明
id INTEGER PRIMARY KEY AUTOINCREMENT 自增主键
username TEXT UNIQUE NOT NULL 用户名(全局唯一)
password_hash TEXT NOT NULL 密码哈希值(SHA-256)
role_id INTEGER FOREIGN KEY 角色 ID
dept_id INTEGER FOREIGN KEY 部门 ID
create_time TEXT DEFAULT(datetime('now','localtime')) 创建时间

外键策略差异

外键 策略 原因
role_id → role(id) ON DELETE SET NULL 删除角色后用户仍存在,只是失去角色
dept_id → dept(id) ON DELETE SET NULL 删除部门后用户仍存在,只是失去部门
project_id → projects(id) ON DELETE CASCADE 删除项目后分卷/附件无存在意义

设计决策 :用户表使用 ON DELETE SET NULL 而非 CASCADE,因为删除角色或部门不应导致用户被删除。用户只是失去角色/部门关联,管理员可以重新分配。

密码安全

python 复制代码
# 种子数据中的密码哈希
default_password = hashlib.sha256("admin123".encode()).hexdigest()
cursor.execute('INSERT INTO users(username, password_hash, role_id, dept_id) VALUES(?, ?, ?, ?)', 
               ('admin', default_password, admin_role_id, dept1_id))

密码使用 SHA-256 哈希存储,明文密码不入库。登录时对比哈希值,不解密。

种子用户

用户名 密码 角色 部门
admin admin123 管理员 系统管理部
张三 zhangsan123 普通用户 信息化服务一部
李四 lisi123 普通用户 信息化服务二部
王五 wangwu123 普通用户 信息化服务三部

3.7 RBAC 完整 ER 关系

复制代码
┌──────────┐      N:1      ┌──────────┐      N:1      ┌──────────┐
│  users   │──────────────→│   role   │──────────────→│permission│
│          │               │          │               │          │
│ id (PK)  │               │ id (PK)  │     ┌─────────│ id (PK)  │
│ username │               │ role_code│     │         │ perm_code│
│ password │               │ role_name│     │         │ perm_type│
│ role_id  │──FK           └──────────┘     │         └──────────┘
│ dept_id  │──FK                ↑           │              ↑
└──────────┘                    │           │              │
     │                          │           │              │
     │ N:1                      │     ┌─────┘              │
     ↓                          │     │                    │
┌──────────┐                    │     │                    │
│   dept   │                    │     │                    │
│          │               ┌──────────────────┐            │
│ id (PK)  │               │ role_permission  │            │
│ dept_code│               │                  │            │
│ dept_name│               │ role_id (FK)─────┘            │
│ parent_id│               │ perm_id (FK)─────────────────┘
└──────────┘               │ UNIQUE(role_id,  │
                           │        perm_id)  │
                           └──────────────────┘

四、系统管理表

4.1 sys_dict 表------系统字典表

python 复制代码
# src/models/db_models.py(第 161-172 行)

cursor.execute('''
    CREATE TABLE IF NOT EXISTS sys_dict(
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        dict_type TEXT NOT NULL,
        dict_code TEXT NOT NULL,
        dict_value TEXT NOT NULL,
        sort_order INTEGER DEFAULT 0,
        enabled INTEGER DEFAULT 1,
        remark TEXT,
        create_time TEXT DEFAULT(datetime('now','localtime'))
    )
''')

cursor.execute('CREATE INDEX IF NOT EXISTS idx_sys_dict_type ON sys_dict(dict_type)')
字段 类型 约束 说明
id INTEGER PRIMARY KEY AUTOINCREMENT 自增主键
dict_type TEXT NOT NULL 字典类型(project_status/retention_period/file_type)
dict_code TEXT NOT NULL 字典编码(INIT/10YEARS/ORIGINAL)
dict_value TEXT NOT NULL 字典显示值(立项/10年/原件)
sort_order INTEGER DEFAULT 0 排序序号
enabled INTEGER DEFAULT 1 是否启用

索引idx_sys_dict_type ON sys_dict(dict_type) ------按字典类型查询是最高频操作,建索引提升性能。

种子数据------三大字典类型

dict_type dict_code dict_value 说明
project_status INIT 立项 项目状态
project_status PURCHASE 采购
project_status CONSTRUCTION 建设
project_status ACCEPTANCE 验收
retention_period 10YEARS 10年 保管期限
retention_period 30YEARS 30年
retention_period PERMANENT 永久
file_type ORIGINAL 原件 文件类型
file_type COPY 复印件
file_type PRINT 打印件

设计要点 :字典表存储英文编码和中文显示值的映射。数据库中所有枚举字段存英文编码,UI 层通过 SysDictService.get_dict_value_by_code() 查询字典表翻译为中文显示。这样修改显示文案时只需改字典表,不需要改代码。

4.2 system_log 表------操作日志表

python 复制代码
# src/models/db_models.py(第 131-143 行)

cursor.execute('''
    CREATE TABLE IF NOT EXISTS system_log(
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        user_id INTEGER,
        user_name TEXT,
        operate_type TEXT NOT NULL,
        module TEXT,
        content TEXT,
        ip_address TEXT,
        operate_time TIMESTAMP DEFAULT(datetime('now','localtime')),
        FOREIGN KEY (user_id) REFERENCES users(id)
    )
''')
字段 类型 约束 说明
id INTEGER PRIMARY KEY AUTOINCREMENT 自增主键
user_id INTEGER FOREIGN KEY 操作用户 ID
user_name TEXT 操作用户名(冗余字段)
operate_type TEXT NOT NULL 操作类型(新增/编辑/删除/登录等)
module TEXT 操作模块(项目/分卷/附件等)
content TEXT 操作详情
ip_address TEXT IP 地址
operate_time TIMESTAMP DEFAULT(datetime('now','localtime')) 操作时间

冗余字段设计user_nameuser_id 的冗余字段。即使后续用户被删除或改名,日志中仍保留操作时的用户名,确保日志的不可篡改性。

日志迁移逻辑

python 复制代码
# 迁移:补齐 system_log 表缺失的列(兼容旧数据库)
cursor.execute("PRAGMA table_info(system_log)")
existing_cols = {row[1] for row in cursor.fetchall()}
missing_cols = []
if 'user_id' not in existing_cols:
    missing_cols.append("ADD COLUMN user_id INTEGER")
if 'user_name' not in existing_cols:
    missing_cols.append("ADD COLUMN user_name TEXT")
if 'module' not in existing_cols:
    missing_cols.append("ADD COLUMN module TEXT")
if 'ip_address' not in existing_cols:
    missing_cols.append("ADD COLUMN ip_address TEXT")
for col_sql in missing_cols:
    cursor.execute(f"ALTER TABLE system_log {col_sql}")

迁移设计 :通过 PRAGMA table_info() 检测现有列,动态补齐缺失列。这确保了从旧版本升级时日志表能自动适配新结构,不会丢失历史日志。

4.3 sys_template 表------系统模板表

python 复制代码
# src/models/db_models.py(第 558-568 行)

cursor.execute('''
    CREATE TABLE IF NOT EXISTS sys_template(
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        template_name TEXT NOT NULL,
        template_code TEXT UNIQUE NOT NULL,
        template_type TEXT NOT NULL,
        template_content TEXT,
        enabled INTEGER DEFAULT 1,
        remark TEXT,
        create_time TEXT DEFAULT(datetime('now','localtime'))
    )
''')
字段 类型 约束 说明
id INTEGER PRIMARY KEY AUTOINCREMENT 自增主键
template_name TEXT NOT NULL 模板名称
template_code TEXT UNIQUE NOT NULL 模板编码(全局唯一)
template_type TEXT NOT NULL 模板类型(archive_catalog/project_cycle)
template_content TEXT 模板内容(JSON 字符串)
enabled INTEGER DEFAULT 1 是否启用

模板内容结构template_content 存储完整的 JSON 模板,包含页面设置、样式、列定义等:

python 复制代码
default_template_content = {
    "page_settings": {
        "top_margin": 1.78,
        "bottom_margin": 0,
        "left_margin": 3.69,
        "right_margin": 2.05
    },
    "styles": {
        "normal": {"font_name": "宋体", "font_size": 12, "line_spacing": 1.5},
        "catalog": {
            "title": {"font_name": "黑体", "font_size": 22.5, "alignment": "center", "bold": True},
            "header": {"font_name": "宋体", "font_size": 12, "bold": True},
            "content": {"font_name": "宋体", "font_size": 10.5}
        },
        # ... file_catalog / note / box_side / transfer_list 样式
    },
    "catalog_columns": ["序号", "档号", "案卷题名", "总页数", "保管期限", "备注"],
    "file_catalog_columns": ["序号", "文件编号", "责任者", "文件题名", "日期", "页数", "备注"],
    "transfer_list": {
        "transfer_department": "移交处室:",
        "transfer_person": "移交人:",
        "receiver": "接收人:",
        "person_date": "日期:",
        "receiver_date": "日期:",
        "main_text": "今移交基建档案(项目名称:{project_name})共计{total_volumes}卷,{total_files}件。",
        "attachment_label": "附件:"
    }
}

JSON 存储设计:模板内容复杂且结构多变,用 JSON 存储在 TEXT 字段中比拆分为多张表更灵活。修改模板时只需更新一个字段,不需要跨表操作。
踩坑经验transfer_list 中的 person_datereceiver_date 字段必须显式定义。如果种子数据中缺少这两个字段,导出目录时会因 KeyError: 'person_date' 报错。

4.4 project_cycle 表------项目周期数据表

python 复制代码
# src/models/db_models.py(第 738-748 行)

cursor.execute('''
    CREATE TABLE IF NOT EXISTS project_cycle(
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        project_id INTEGER NOT NULL,
        template_id INTEGER,
        cycle_data TEXT,
        create_time TEXT DEFAULT(datetime('now','localtime')),
        update_time TEXT DEFAULT(datetime('now','localtime')),
        FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
    )
''')
字段 类型 约束 说明
id INTEGER PRIMARY KEY AUTOINCREMENT 自增主键
project_id INTEGER NOT NULL, FOREIGN KEY 所属项目 ID
template_id INTEGER 使用的模板 ID
cycle_data TEXT 周期数据(JSON 字符串)
create_time TEXT DEFAULT(datetime('now','localtime')) 创建时间
update_time TEXT DEFAULT(datetime('now','localtime')) 更新时间

设计要点cycle_data 存储项目周期的完整 JSON 数据(阶段、分阶段、要件、完成状态等),一个项目对应一条周期记录。update_time 在每次保存周期数据时更新,用于追踪最后修改时间。


五、AI 智能助手管理表

5.1 ai_skill 表------AI 技能管理表

python 复制代码
# src/models/db_models.py(第 752-768 行)

cursor.execute('''
    CREATE TABLE IF NOT EXISTS ai_skill(
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        skill_name TEXT UNIQUE NOT NULL,
        display_name TEXT NOT NULL,
        description TEXT,
        category TEXT,
        keywords TEXT,
        example_query TEXT,
        params_json TEXT,
        enabled INTEGER DEFAULT 1,
        is_builtin INTEGER DEFAULT 1,
        skill_source TEXT DEFAULT 'service',
        create_time TEXT DEFAULT(datetime('now','localtime')),
        update_time TEXT DEFAULT(datetime('now','localtime'))
    )
''')
字段 类型 约束 说明
id INTEGER PRIMARY KEY AUTOINCREMENT 自增主键
skill_name TEXT UNIQUE NOT NULL 技能名称(代码中的唯一标识)
display_name TEXT NOT NULL 显示名称
description TEXT 技能描述
category TEXT 技能分类(QUERY/CREATE/UPDATE/DELETE/EXPORT/CHART/STATISTICS/SYSTEM)
keywords TEXT 触发关键词(逗号分隔)
example_query TEXT 示例查询
params_json TEXT 参数定义(JSON 字符串)
enabled INTEGER DEFAULT 1 是否启用
is_builtin INTEGER DEFAULT 1 是否内置技能
skill_source TEXT DEFAULT 'service' 技能来源(service/tool)

设计理念

技能在代码中通过 BaseSkill 子类定义和注册,数据库表只存储启用状态管理员可编辑的元数据 (display_name/description/keywords/example_query)。每次应用启动时,assistant_init.py 会将代码注册的技能同步到数据库(upsert 语义),保留管理员的修改和 enabled 状态。

复制代码
代码中注册的 Skill(执行逻辑 + 元数据)
         ↓ 同步(upsert)
ai_skill 表(存储 enabled 状态 + 可编辑元数据)
         ↓ 生成摘要
大模型 System Prompt(仅含启用的技能)

为什么不全用数据库 :技能的执行逻辑(execute() 方法)是代码,不可能存入数据库。数据库只管理"是否启用"和"元数据可配置",执行逻辑由代码控制。这是"代码定义行为,数据库管理配置"的经典设计。

5.2 ai_workflow 表------AI 工作流管理表

python 复制代码
# src/models/db_models.py(第 771-786 行)

cursor.execute('''
    CREATE TABLE IF NOT EXISTS ai_workflow(
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        workflow_name TEXT NOT NULL,
        workflow_code TEXT UNIQUE NOT NULL,
        description TEXT,
        trigger_keywords TEXT,
        steps_json TEXT NOT NULL,
        template_key TEXT,
        template_data_keys TEXT,
        reply_hint TEXT,
        enabled INTEGER DEFAULT 1,
        create_time TEXT DEFAULT(datetime('now','localtime')),
        update_time TEXT DEFAULT(datetime('now','localtime'))
    )
''')
字段 类型 约束 说明
id INTEGER PRIMARY KEY AUTOINCREMENT 自增主键
workflow_name TEXT NOT NULL 工作流名称
workflow_code TEXT UNIQUE NOT NULL 工作流编码(全局唯一)
description TEXT 工作流描述
trigger_keywords TEXT 触发关键词(逗号分隔)
steps_json TEXT NOT NULL 工作流步骤(JSON 字符串)
template_key TEXT 结果模板键名
template_data_keys TEXT 模板数据键名列表(JSON 数组)
reply_hint TEXT 回复引导语
enabled INTEGER DEFAULT 1 是否启用

种子数据------预算筛选工作流

python 复制代码
sample_steps = json.dumps([
    {"action": "call_skill", "skill_name": "get_all_projects", "params": {}, "result_key": "projects"},
    {"action": "filter", "source": "${projects}", "condition": {"budget": {"lt": 200}}, "result_key": "filtered_projects"},
    {"action": "loop", "source": "${filtered_projects}", "item_key": "project",
     "call": {"skill_name": "get_files_by_project", "params": {"project_id": "${project.id}"}},
     "result_key": "all_files", "merge": True}
], ensure_ascii=False)

cursor.execute('''
    INSERT INTO ai_workflow(workflow_name, workflow_code, description, trigger_keywords,
                            steps_json, template_key, template_data_keys, reply_hint, enabled)
    VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)
''', ('预算筛选项目查附件', 'budget_filter_files',
      '按预算条件筛选项目并查询其全部附件(如:查询预算小于200的项目的全部附件)',
      '预算,筛选,附件,小于,大于',
      sample_steps, 'workflow_files_by_projects',
      json.dumps(["filtered_projects", "all_files"], ensure_ascii=False),
      '已按预算条件筛选项目并查询其附件:', 1))

这个预定义工作流实现了"查询预算小于 200 的项目的全部附件"------三个步骤:查询全部项目 → 按预算过滤 → 循环查询每个项目的附件。大模型识别到用户意图匹配此工作流时,直接调用 execute_predefined_workflow,无需动态编排,降低 token 消耗。

trigger_keywords 的一致性:关键词以逗号分隔存储在数据库中,读取时需统一处理为列表返回,避免序列化不一致导致工作流无法正确触发。


六、索引设计

6.1 索引清单

python 复制代码
# src/models/db_models.py(第 809-824 行)

# ========== 添加索引 ==========
cursor.execute('CREATE INDEX IF NOT EXISTS idx_projects_project_code ON projects(project_code)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_volume_project_volume_no ON project_volume(project_id, volume_no)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_file_file_no ON project_file(file_no)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_users_username ON users(username)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_ai_skill_name ON ai_skill(skill_name)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_ai_workflow_code ON ai_workflow(workflow_code)')
# 补充缺失的索引,提升常用查询性能
cursor.execute('CREATE INDEX IF NOT EXISTS idx_project_file_volume_id ON project_file(volume_id)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_project_file_project_id ON project_file(project_id)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_system_log_user_id ON system_log(user_id)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_system_log_operate_time ON system_log(operate_time)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_system_log_module ON system_log(module)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_role_permission_role_id ON role_permission(role_id)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_role_permission_perm_id ON role_permission(perm_id)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_project_cycle_project_id ON project_cycle(project_id)')

共 15 个索引(含 sys_dict 表的 1 个),按用途分类:

索引 字段 用途
idx_projects_project_code projects project_code 按项目编号查询
idx_volume_project_volume_no project_volume (project_id, volume_no) 组合查询分卷
idx_file_file_no project_file file_no 按附件编号查询
idx_project_file_volume_id project_file volume_id 按分卷查附件(高频)
idx_project_file_project_id project_file project_id 按项目查附件(高频)
idx_users_username users username 登录认证查询
idx_sys_dict_type sys_dict dict_type 按类型查字典(高频)
idx_system_log_user_id system_log user_id 按用户查日志
idx_system_log_operate_time system_log operate_time 按时间查日志
idx_system_log_module system_log module 按模块查日志
idx_role_permission_role_id role_permission role_id 查角色权限(高频)
idx_role_permission_perm_id role_permission perm_id 查权限被哪些角色使用
idx_ai_skill_name ai_skill skill_name 按技能名查询
idx_ai_workflow_code ai_workflow workflow_code 按编码查工作流
idx_project_cycle_project_id project_cycle project_id 按项目查周期

6.2 索引设计原则

原则 说明 示例
外键必建索引 外键关联查询是高频操作 volume_idproject_id
查询条件字段建索引 WHERE 子句中常用的字段 usernamedict_type
组合查询建复合索引 多字段组合查询 (project_id, volume_no)
唯一约束自带索引 UNIQUE 约束自动创建索引 project_code UNIQUE

为什么不建更多索引:SQLite 是单文件数据库,索引越多写入越慢(每次写入都要更新所有相关索引)。政务系统写入频率低、查询频率高,15 个索引覆盖了所有高频查询路径,是性能和写入开销的平衡点。


七、完整 ER 实体关系图

复制代码
┌─────────────────────────────────────────────────────────────────────┐
│                        数据库 ER 关系图                              │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  ┌─────────────┐         ┌─────────────────┐         ┌───────────┐ │
│  │  projects   │ 1─────N │ project_volume  │ 1─────N │project_file│ │
│  │             │         │                 │         │           │ │
│  │ id (PK)     │←──FK────│ project_id      │←──FK────│ project_id│ │
│  │ project_code│         │ volume_no       │         │ volume_id │ │
│  │ (UNIQUE)    │         │ volume_code(U)  │         │ file_no   │ │
│  │ project_name│         │ total_pages     │         │ file_name │ │
│  │ manager     │         │ retention_period│         │ pages     │ │
│  │ budget      │         │ UNIQUE(pid,vno) │         │ file_type │ │
│  │ status      │         └─────────────────┘         │ UNIQUE    │ │
│  └──────┬──────┘                              └──────┤ (fno,vid) │ │
│         │                                            └───────────┘ │
│         │ 1                                                    │
│         │                                                      │
│         ↓                                                      │
│  ┌──────────────┐                                             │
│  │project_cycle │          ┌──────────┐    ┌────────────┐     │
│  │              │          │  users   │    │   dept     │     │
│  │ id (PK)      │          │          │    │            │     │
│  │ project_id   │←──FK─────│ role_id  │──FK│ id (PK)    │     │
│  │ template_id  │          │ dept_id  │──FK│ dept_code  │     │
│  │ cycle_data   │          │ username │    │ dept_name  │     │
│  └──────────────┘          │ password │    │ parent_id  │     │
│                            └────┬─────┘    └────────────┘     │
│                                 │                              │
│                            ┌────┴─────┐                        │
│                            │   role   │                        │
│                            │          │                        │
│                            │ id (PK)  │                        │
│                            │ role_code│                        │
│                            │ role_name│                        │
│                            └────┬─────┘                        │
│                                 │                              │
│                    ┌────────────┴────────────┐                 │
│                    │  role_permission        │                 │
│                    │  (多对多关联表)          │                 │
│                    │  role_id (FK)           │                 │
│                    │  perm_id (FK)           │                 │
│                    │  UNIQUE(role_id,perm_id)│                 │
│                    └────────────┬────────────┘                 │
│                                 │                              │
│                            ┌────┴─────┐                        │
│                            │permission│                        │
│                            │          │                        │
│                            │ id (PK)  │                        │
│                            │perm_code │                        │
│                            │perm_type │                        │
│                            └──────────┘                        │
│                                                                │
│  ┌───────────┐  ┌──────────────┐  ┌──────────┐ ┌───────────┐ │
│  │ sys_dict  │  │ system_log   │  │ai_skill  │ │ai_workflow│ │
│  │           │  │              │  │          │ │           │ │
│  │ dict_type │  │ user_id (FK) │  │skill_name│ │workflow_  │ │
│  │ dict_code │  │ operate_type │  │ category │ │ code      │ │
│  │ dict_value│  │ module       │  │ enabled  │ │ steps_json│ │
│  └───────────┘  └──────────────┘  └──────────┘ └───────────┘ │
│                                                                │
│  ┌────────────────┐                                           │
│  │ sys_template   │                                           │
│  │                │                                           │
│  │ template_code  │                                           │
│  │ template_type  │                                           │
│  │ template_content│                                          │
│  └────────────────┘                                           │
│                                                                │
└────────────────────────────────────────────────────────────────┘

7.1 表间关系汇总

关系 类型 说明
projects → project_volume 1:N 一个项目有多个分卷,CASCADE 删除
projects → project_file 1:N 一个项目有多个附件,CASCADE 删除
project_volume → project_file 1:N 一个分卷有多个附件,CASCADE 删除
projects → project_cycle 1:N 一个项目有一条周期记录,CASCADE 删除
users → role N:1 多个用户属于一个角色,SET NULL 删除
users → dept N:1 多个用户属于一个部门,SET NULL 删除
role → role_permission 1:N 一个角色有多个权限关联,CASCADE 删除
permission → role_permission 1:N 一个权限被多个角色关联,CASCADE 删除
users → system_log 1:N 一个用户产生多条日志
sys_template → project_cycle 1:N 一个模板被多个项目周期引用

八、数据库迁移策略

8.1 SQLite 迁移的限制

SQLite 的 ALTER TABLE 语句支持非常有限:

操作 SQLite 支持 MySQL 支持
ADD COLUMN
RENAME COLUMN ✅(≥3.25.0)
DROP COLUMN ✅(≥3.35.0)
修改列类型
修改约束

对于不支持的迁移(如修改约束),需要使用"重建表"策略。

8.2 迁移实例:列重命名

python 复制代码
# src/models/db_models.py(第 98-106 行)

# 迁移:period 列重命名为 retention_period
try:
    cursor.execute('ALTER TABLE project_volume RENAME COLUMN period TO retention_period')
except sqlite3.OperationalError:
    pass  # 列不存在或已重命名,忽略

try:
    cursor.execute('ALTER TABLE project_volume ADD COLUMN retention_period TEXT')
except sqlite3.OperationalError:
    pass  # 列已存在,忽略

双重迁移 :先用 RENAME COLUMN 处理旧版本(period 列存在的情况),再用 ADD COLUMN 处理更旧版本(retention_period 列不存在的情况)。两个 try-except 保证无论从哪个版本升级都能成功。

8.3 迁移实例:users 表结构迁移

python 复制代码
# src/models/db_models.py(第 360-408 行)

# 迁移旧的users表结构(从role/department字段迁移到role_id/dept_id)
cursor.execute("PRAGMA table_info(users)")
columns = [col[1] for col in cursor.fetchall()]
if 'role' in columns and 'role_id' not in columns:
    cursor.execute("ALTER TABLE users ADD COLUMN role_id INTEGER")
if 'department' in columns and 'dept_id' not in columns:
    cursor.execute("ALTER TABLE users ADD COLUMN dept_id INTEGER")

# 如果存在旧数据(旧的role/department字段),需要关联新的role和dept表
if 'role' in columns and 'role_id' in columns:
    cursor.execute("SELECT COUNT(*) FROM users WHERE role_id IS NULL AND role IS NOT NULL")
    if cursor.fetchone()[0] > 0:
        cursor.execute("SELECT id, username, role, department FROM users WHERE role_id IS NULL")
        old_users = cursor.fetchall()
        for old_user in old_users:
            user_id, username, role_str, dept_str = old_user
            # 按旧 role 字符串查找新 role_id
            cursor.execute("SELECT id FROM role WHERE role_code=?", (role_str,))
            role_row = cursor.fetchone()
            role_id = role_row[0] if role_row else None
            # 按旧 department 字符串查找新 dept_id
            cursor.execute("SELECT id FROM dept WHERE dept_name=?", (dept_str,))
            dept_row = cursor.fetchone()
            dept_id = dept_row[0] if dept_row else None
            cursor.execute("UPDATE users SET role_id=?, dept_id=? WHERE id=?", (role_id, dept_id, user_id))

# 清理旧列(SQLite >= 3.35.0)
if 'role' in columns or 'department' in columns:
    sqlite_version = sqlite3.sqlite_version_info
    if sqlite_version >= (3, 35, 0):
        legacy_cols_to_drop = [c for c in ('role', 'department') if c in columns]
        for col_name in legacy_cols_to_drop:
            try:
                cursor.execute(f'ALTER TABLE users DROP COLUMN {col_name}')
            except sqlite3.OperationalError:
                pass

迁移流程:

  1. 检测旧列PRAGMA table_info(users) 查看现有列
  2. 添加新列ADD COLUMN role_id INTEGER
  3. 迁移数据 :按旧的 role 文本字段查找 role.id,更新 role_id
  4. 清理旧列 :SQLite ≥3.35.0 执行 DROP COLUMN

8.4 迁移实例:修改唯一约束(重建表)

SQLite 不支持直接修改约束,需要"创建临时表 → 复制数据 → 删除旧表 → 重命名":

python 复制代码
# src/models/db_models.py(第 455-490 行)

# 迁移 project_file 表,将 file_no 全局唯一约束改为 (file_no, volume_id) 组合唯一约束
cursor.execute("SELECT sql FROM sqlite_master WHERE type='table' AND name='project_file'")
result = cursor.fetchone()
has_file_no_unique = False
if result and result[0]:
    sql = result[0]
    if 'file_no TEXT UNIQUE' in sql or 'file_no UNIQUE' in sql:
        has_file_no_unique = True

if has_file_no_unique:
    # 1. 创建新表(带组合唯一约束)
    cursor.execute('''
        CREATE TABLE IF NOT EXISTS project_file_new(
            ...同结构...
            UNIQUE(file_no, volume_id)
        )
    ''')
    # 2. 复制数据
    cursor.execute('''
        INSERT INTO project_file_new SELECT id, project_id, volume_id, file_no, file_code, 
        responsible_unit, file_name, file_path, file_date, pages, remarks, file_type, create_time 
        FROM project_file
    ''')
    # 3. 删除旧表
    cursor.execute('DROP TABLE project_file')
    # 4. 重命名新表
    cursor.execute('ALTER TABLE project_file_new RENAME TO project_file')

检测旧约束的方法 :通过查询 sqlite_master 表的 sql 字段,检查建表语句中是否包含 file_no TEXT UNIQUE。这比解析 PRAGMA table_info() 更可靠,因为 table_info 不返回约束信息。
踩坑经验sqlite_master 表存储了所有表、索引、视图、触发器的定义。通过 SELECT sql FROM sqlite_master WHERE type='table' AND name='表名' 可以获取原始建表语句,用于检测约束是否需要迁移。


九、种子数据初始化策略

9.1 条件插入模式

所有种子数据都采用"先查后插"模式:

python 复制代码
# 检查表是否为空,只在空表时插入种子数据
cursor.execute("SELECT COUNT(*) FROM projects")
if cursor.fetchone()[0] == 0:
    # 插入种子数据
    ...

为什么不用 INSERT OR IGNOREINSERT OR IGNORE 需要在每条数据上检测唯一约束冲突,批量插入时性能差。"先查后插"只需一次 COUNT 查询,更高效。

9.2 种子数据清单

种子数据量 说明
projects 3 条 测试项目(智慧政务平台、数据中台建设、网络安全加固)
sys_dict 10 条 3 类字典(项目状态4条 + 保管期限3条 + 文件类型3条)
dept 4 条 默认部门
role 3 条 默认角色(admin/manager/user)
permission 20 条 全部权限定义
role_permission 35 条 admin 全部20项 + manager 12项 + user 3项
users 4 条 默认用户(admin + 张三 + 李四 + 王五)
sys_template 2 条 目录模板 + 周期模板
ai_workflow 1 条 预算筛选工作流示例

9.3 周期模板的优雅降级

python 复制代码
# src/models/db_models.py(第 713-735 行)

cursor.execute("SELECT COUNT(*) FROM sys_template WHERE template_code = 'project_cycle'")
if cursor.fetchone()[0] == 0:
    try:
        from src.services.cycle_default_template import DEFAULT_CYCLE_TEMPLATE
        cursor.execute('''
            INSERT INTO sys_template(...)
            VALUES(?, ?, ?, ?, ?, ?)
        ''', ('周期模板', 'project_cycle', 'project_cycle', 
              json.dumps(DEFAULT_CYCLE_TEMPLATE, ensure_ascii=False), 1, 
              '项目生命周期实施对照表模板(一比一复刻附件格式)'))
    except ImportError:
        # 周期模板文件不存在时,插入空模板占位
        empty_template = {
            "page_settings": {"top_margin": 1.78, "bottom_margin": 0, "left_margin": 2.54, "right_margin": 2.54},
            "styles": {},
            "header_groups": [],
            "columns": [],
            "requirements": []
        }
        cursor.execute('''
            INSERT INTO sys_template(...)
            VALUES(?, ?, ?, ?, ?, ?)
        ''', ('周期模板', 'project_cycle', 'project_cycle',
              json.dumps(empty_template, ensure_ascii=False), 1,
              '项目生命周期实施对照表模板(开源版本,请自行配置要件数据)'))

优雅降级设计cycle_default_template.py 包含单位内部要件数据,开源版本中可能不存在。通过 try-except ImportError 实现:本地开发时正常加载完整模板;开源版本运行时插入空模板占位,系统其余功能不受影响,仅项目周期管理缺少预置要件数据。


十、设计经验总结

10.1 值得借鉴的设计

设计 收益 实现方式
组合唯一约束 编号在范围内唯一,跨范围可重复 UNIQUE(project_id, volume_no)
外键级联策略分级 用户数据不丢失,档案数据自动清理 用户用 SET NULL,档案用 CASCADE
字典表翻译机制 修改显示文案不改代码 数据库存英文编码,UI 层查字典翻译
JSON 存储复杂模板 灵活可配,无需拆表 template_content TEXT 存 JSON
种子数据条件插入 重复初始化不产生重复数据 SELECT COUNT(*) 先查后插
索引覆盖高频查询 查询性能优化 外键 + 查询条件字段建索引
日志冗余字段 审计数据不可篡改 user_name 冗余存储
AI 技能代码注册+数据库管理 执行逻辑在代码,配置在数据库 upsert 同步 + enabled 控制

10.2 踩过的坑

问题 原因 解决方案
级联删除不生效 SQLite 默认不启用外键 每次连接执行 PRAGMA foreign_keys = ON
分卷编号冲突 volume_no 全局唯一 改为 UNIQUE(project_id, volume_no) 组合唯一
旧数据库升级失败 新增列在旧表中不存在 PRAGMA table_info() 检测后动态 ADD COLUMN
DROP COLUMN 报错 SQLite < 3.35.0 不支持 版本检测 + 降级处理
模板导出 KeyError 种子数据缺少 person_date 字段 显式定义所有模板字段
周期模板缺失 开源版本无内部要件文件 try-except ImportError 优雅降级
索引缺失查询慢 外键字段无索引 补充 15 个索引覆盖高频查询
数据库锁定 连接未及时关闭 连接即用即关,finally: conn.close()

10.3 SQLite 使用建议

建议 原因
每次连接都执行 PRAGMA foreign_keys = ON SQLite 默认不启用外键约束
连接即用即关,不保持长连接 SQLite 是文件级锁,长连接会导致锁定
使用 row_factory = sqlite3.Row 支持列名访问,代码可读性好
日期用 TEXT 存储 SQLite 无原生 DATE 类型,TEXT 排序天然正确
datetime('now','localtime') 获取本地时间 datetime('now') 返回 UTC 时间
JSON 存储在 TEXT 字段 SQLite 无原生 JSON 类型(3.9+ 有 JSON 函数但存储仍为 TEXT)
批量插入用 executemany() 比循环 execute() 快 10 倍以上

十一、小结

本文逐表解析了系统 14 张数据库表的设计:

核心业务表(3 张)

  • projects ------ 项目主表,全局唯一项目编号
  • project_volume ------ 分卷表,组合唯一约束 UNIQUE(project_id, volume_no)
  • project_file ------ 附件表,组合唯一约束 UNIQUE(file_no, volume_id)

RBAC 权限表(5 张)

  • users ------ 用户表,密码 SHA-256 哈希存储
  • role ------ 角色表,三种预置角色
  • permission ------ 权限表,20 项权限(menu/action 两种类型)
  • role_permission ------ 角色权限关联表,多对多关系
  • dept ------ 部门表,预留树形结构

系统管理表(4 张)

  • sys_dict ------ 系统字典表,枚举值翻译
  • system_log ------ 操作日志表,冗余用户名保证审计完整性
  • sys_template ------ 系统模板表,JSON 存储复杂模板
  • project_cycle ------ 项目周期数据表,JSON 存储周期数据

AI 管理表(2 张)

  • ai_skill ------ 技能管理表,代码注册+数据库管理配置
  • ai_workflow ------ 工作流管理表,预定义工作流存储

然后深入讲解了数据库迁移策略(列重命名、结构迁移、约束修改的重建表策略),种子数据初始化策略(条件插入、优雅降级),以及 15 个索引的设计原则。

最后总结了 SQLite 在桌面应用中的使用经验和踩坑教训。

源码获取 :完整源码已开源至 GitCode:https://gitcode.com/u010972645/IAS-ProjectArchiveSystem,建议对照 src/models/db_models.py 源码阅读本文,加深理解。


下一篇预告db_models.py(上):数据库连接与建表语句逐行解析 ------ 从第一行代码开始,逐行解析 get_db_connection() 函数的三个关键设置,以及前 7 张表的 CREATE TABLE IF NOT EXISTS 语句,包括字段类型选择、默认值设置、约束条件的完整解析。


本文是「PySide6+AI政务系统开发实战」系列第 5 篇。专栏持续更新中,点击关注不迷路。

相关推荐
罗西的思考2 小时前
【OpenClaw具身硬件】MiniClaw 阅读笔记---(1)基础
人工智能·算法·机器学习
DevUI团队2 小时前
从“即兴创作”到“规格先行”,华为云码道(CodeArts)代码智能体持续深耕企业级规范驱动开发能力
前端·人工智能·后端
论文避坑指南2 小时前
论文写作全流程AI合规边界:从选题到投稿的“红绿灯“清单
大数据·人工智能·深度学习
Dawson Zhu2 小时前
工业世界模型——基于AI Agent+数学仿真架构
人工智能·架构·agi
冬奇Lab2 小时前
开源项目第183期:Ontology Playground — 微软出品的本体论可视化学习工具,零后端、浏览器直接运行
人工智能·microsoft·开源
Wang's Blog2 小时前
AI Agent白手起家52: 从零搭建钉钉智能助手——资源准备与核心架构实现
人工智能·架构·钉钉
冬奇Lab2 小时前
代码库知识库系列(13):评测——怎么知道知识库够不够好
人工智能
字节跳动视频云技术团队2 小时前
把 AI 视频的钱花在刀刃上,不是每一刀上
人工智能·音视频开发
jufeng13072 小时前
【系列:手搓自主 AI Agent:Hermes 架构原理剖析 · 第 1 篇】
人工智能·python·架构·agent