Files
paper-doc/docs/OVERVIEW.md
T
govin 9ded3bd574 docs: document the template domain model, API, and shell layout
Records the decisions that are not visible from the code alone:

  - why section_field has no parent_id, and what reuse that buys;
  - that template_field.sort is the single source of display order, and
    that repeats and ties are deliberately legal;
  - that templates reference library fields rather than copying them, which
    is what makes a rename propagate;
  - that TiDB parses FOREIGN KEY and ignores it, so the integrity lives in
    the application layer.

Also adds the API table, the frontend route table, and the seed script.
2026-09-18 16:10:48 +08:00

10 KiB

paper-doc

A tool that supports the paper-writing process: it manages the materials, sections, figures, and references that go into a paper. The name reflects the document-centric core of the workflow — a paper is composed from structured documents rather than written in one pass.

The first feature to land is the template configuration: a reusable library of section fields plus named templates built from them, so that a paper is written by filling in a known structure instead of deciding one.

Tech Stack

Backend

  • FastAPI (Python) — REST API
  • SQLAlchemy + Alembic — ORM layer and schema migrations
  • TiDB v8.5.0 — MySQL-compatible distributed SQL database, deployed in k3s
  • Dependencies managed with venv + requirements.txt

Frontend

  • Vue 3 + Vite — single-page application
  • axios — HTTP client
  • vue-router — client-side routing
  • pinia + pinia-plugin-persistedstate — state management with persistence
  • Element Plus — UI component library
  • Dependencies managed with pnpm

Architecture

A decoupled SPA + REST API layout:

  • The Vue SPA runs in the browser and talks to the backend exclusively over HTTP/JSON via axios. It holds no direct database access.
  • The FastAPI backend owns all persistence. It connects to TiDB through SQLAlchemy sessions and owns the schema through Alembic migrations.
  • TiDB runs inside the existing k3s cluster. The backend reaches it either through the in-cluster Service (tidb-tidb.tidb:4000) or, during local development, through the NodePort 192.168.1.88:32738.

Development runs both parts locally: Vite serves the SPA with a dev proxy that forwards /api to the FastAPI process, so the browser sees a single origin and no CORS configuration is required.

Browser ──HTTP/JSON──▶ FastAPI ──SQLAlchemy──▶ TiDB (k3s)
   │                      ▲
   └── Vite dev server ───┘  (proxy /api)

Domain Model

Three tables, and one rule that everything else follows from.

section_field                     the reusable heading library
  id, name, level, font_size, font_color

paper_template                    a named outline
  id, name, abstract

template_field                    the join, and the only home of display order
  id, template_id → paper_template, field_id → section_field, sort

The field library is flat, not a tree

section_field has no parent_id. Hierarchy is expressed only by level (1 for 1., 2 for 1.1), which is a rendering hint: it drives indentation and numbering semantics in the UI and nothing else.

The reason is reuse. With a parent pointer a level-2 heading would belong to exactly one level-1 heading, so "Background" could not sit under both 1. Introduction and 2. Related Work. Flat fields are attached to templates, not to each other, so one field can appear in any number of templates, in a different position in each.

The numbering a reader sees is part of name and is written by the user ("1. Introduction", "0 Abstract"). Nothing derives or rewrites it.

Display order lives in the join table

template_field.sort is a plain ascending integer and is the only thing that decides what order a template's fields appear in. The order the user clicked fields in is never stored, so selecting 字段1, 字段2.1, 字段2, 字段1.1 and setting sorts 1, 4, 3, 2 renders as 字段1, 字段1.1, 字段2, 字段2.1.

Two consequences are deliberate:

  • There is no unique constraint on (template_id, field_id). Placing the same field twice in one template is a legitimate layout — the same level-2 heading under two different level-1 headings. The UI warns about a repeat; it does not forbid one.
  • Ties are legal. Equal sort values are broken by insertion order, so the ordering is always total and stable. The UI warns about ties too, and offers a button that renumbers the selection 1..N.

Templates hold references, not copies

template_field points at a library field; it does not snapshot its name or typography. Renaming or restyling a field therefore updates every template that places it, which is what makes "fix the template and the section names follow" work.

TiDB does not enforce foreign keys

TiDB parses FOREIGN KEY for compatibility and then ignores it. The constraints are declared to document the relationships, and the integrity they would provide is enforced in the application layer instead:

  • deleting a field still placed in a template is refused with 409, naming the field and how many templates use it;
  • creating a template that references a missing field is refused with 400;
  • PaperTemplate.items uses cascade="all, delete-orphan", so deleting a template removes its rows from the join table.

API

All routes are mounted under /api. Interactive docs at /docs.

Method Path Notes
GET /health liveness plus a TiDB probe
GET /section-fields keyword, level, page, page_size
POST /section-fields create
GET PATCH DELETE /section-fields/{id} read / partial update / delete
POST /section-fields/batch-delete body { "ids": [...] }
GET /templates keyword matches name or abstract
POST /templates name + abstract + ordered fields
GET PATCH DELETE /templates/{id} PATCH with fields replaces the selection
POST /templates/batch-delete body { "ids": [...] }

Conventions worth knowing:

  • font_color is stored and returned as canonical #RRGGBB. The API also accepts rgb(r, g, b), #rgb and bare aabbcc, and normalises them on write.
  • font_size is a JSON number, not a string — a client should not have to parse it before using it in a CSS rule.
  • List endpoints return { items, total, page, page_size, pages }.
  • A template read returns fields already ordered by sort; clients never sort.

Frontend

Shell

┌──────────────────────────────────────────────────────────┐
│ [book mark] paper-doc   论文  模板              [mark]   │  header
├──────────────────────────────────────┬───────────────────┤
│                                      │  模板配置         │  second-level
│  <RouterView>                        │   模板列表        │  menu, right
│                                      │   字段管理        │
└──────────────────────────────────────┴───────────────────┘

The mark appears at both ends of the header. The second-level menu sits on the right and is driven entirely by route.meta.section, so a route declares which menu it belongs to and deep links render correctly on first paint. Routes without a section (the welcome page) show no menu.

Routes

Path View Section
/ welcome
/papers 论文 (content TBD) papers
/templates redirects to /templates/list
/templates/list template table + CRUD templates
/templates/fields field library + CRUD templates

Directory Structure

paper-doc/
├── backend/                 # FastAPI application
│   ├── app/
│   │   ├── api/routes/      # route handlers (health, section_fields, templates)
│   │   ├── core/            # settings and configuration
│   │   ├── crud/            # data-access helpers
│   │   ├── db/              # engine, session, declarative base
│   │   ├── models/          # SQLAlchemy models
│   │   └── schemas/         # Pydantic request/response models
│   ├── alembic/             # migration environment and revisions
│   ├── scripts/seed.py      # idempotent seed for the field library + templates
│   ├── alembic.ini
│   ├── requirements.txt
│   └── .env.example
├── frontend/                # Vue 3 SPA
│   ├── src/
│   │   ├── api/             # axios instance and endpoint modules
│   │   ├── components/      # shell, field and template components
│   │   ├── router/          # vue-router configuration
│   │   ├── stores/          # pinia stores (persisted)
│   │   ├── styles/          # global reset and the shell viewport contract
│   │   ├── utils/           # formatting helpers
│   │   └── views/           # route-level components
│   ├── package.json
│   └── vite.config.ts
└── docs/                    # project documentation
    └── OVERVIEW.md

Getting Started

Prerequisites

  • Python 3.10+ with venv
  • Node.js 18+ with pnpm
  • Network access to the TiDB instance (local development uses the NodePort)

Backend

cd backend
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env          # then fill in the database credentials
uvicorn app.main:app --reload --port 8000

API docs are then served at http://127.0.0.1:8000/docs.

Frontend

cd frontend
pnpm install
pnpm dev

The SPA is served at http://127.0.0.1:5173 and proxies /api to the backend on port 8000.

Database

The paper_doc database is created once, out of band, against TiDB. Schema changes are applied through Alembic:

cd backend
alembic upgrade head

Seed data

scripts/seed.py fills the field library with a standard academic outline (20 fields, from 0 Abstract to 7 References) and creates three starter templates. It matches fields and templates by name, so running it twice adds nothing:

cd backend
.venv/bin/python scripts/seed.py            # add anything missing
.venv/bin/python scripts/seed.py --reset    # empty the tables first