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

257 lines
10 KiB
Markdown

# 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**
```bash
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**
```bash
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:
```bash
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:
```bash
cd backend
.venv/bin/python scripts/seed.py # add anything missing
.venv/bin/python scripts/seed.py --reset # empty the tables first
```