云计算百科
云计算领域专业知识百科平台

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

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

本系列文章配套完整源码,已开源至 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

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

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

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

1.2 数据库路径管理

# 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 连接函数设计

# 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 = ON,ON DELETE CASCADE 完全不生效,删除项目后分卷和附件会变成孤立数据。这是 SQLite 的历史遗留设计,每次连接都必须手动启用。

1.4 init_db() 初始化流程

# 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 表——项目主表

# 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’)) 创建时间

设计要点:

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

    # 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 表——附件表

    # 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 表——部门表

    # 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 备注

    种子数据:

    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 表——角色表

    # 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_coderole_name说明
    admin 管理员 拥有全部 20 项权限
    manager 项目负责人 拥有项目/分卷/附件的 CRUD 权限(12 项)
    user 普通用户 仅拥有查看权限(3 项)

    3.4 permission 表——权限表

    # 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 是否启用

    权限编码命名规范:

    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 表——角色权限关联表

    # 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) ——同一角色不能重复分配同一权限

    种子数据——三级权限分配:

    # 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 表——用户表

    # 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,因为删除角色或部门不应导致用户被删除。用户只是失去角色/部门关联,管理员可以重新分配。

    密码安全:

    # 种子数据中的密码哈希
    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 表——系统字典表

    # 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_typedict_codedict_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 表——操作日志表

    # 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_name 是 user_id 的冗余字段。即使后续用户被删除或改名,日志中仍保留操作时的用户名,确保日志的不可篡改性。

    日志迁移逻辑:

    # 迁移:补齐 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 表——系统模板表

    # 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 模板,包含页面设置、样式、列定义等:

    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_date 和 receiver_date 字段必须显式定义。如果种子数据中缺少这两个字段,导出目录时会因 KeyError: 'person_date' 报错。

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

    # 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 技能管理表

    # 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 工作流管理表

    # 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 是否启用

    种子数据——预算筛选工作流:

    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 索引清单

    # 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_id、project_id 等
    查询条件字段建索引 WHERE 子句中常用的字段 username、dict_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 迁移实例:列重命名

    # 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 表结构迁移

    # 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

    迁移流程:

  • 检测旧列:PRAGMA table_info(users) 查看现有列
  • 添加新列:ADD COLUMN role_id INTEGER
  • 迁移数据:按旧的 role 文本字段查找 role.id,更新 role_id
  • 清理旧列:SQLite ≥3.35.0 执行 DROP COLUMN
  • 8.4 迁移实例:修改唯一约束(重建表)

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

    # 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 条件插入模式

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

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

    为什么不用 INSERT OR IGNORE:INSERT 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 周期模板的优雅降级

    # 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 篇。专栏持续更新中,点击关注不迷路。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 数据库设计与表结构详解|信息化项目全流程管理系统源码逐行精讲(五)
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!