Skip to content

Data Models (System)

System models live in app/system/models/admin.py and app/system/models/dictionary.py. All inherit BaseModel + AuditMixin; primary keys and foreign keys are auto-encoded as sqid at the HTTP boundary.

Business module models live in app/business/<name>/models.py. See Mixins and HR module for conventions.

User

Table: users

FieldTypeNotes
idint PK
user_namestr(20) uniquelogin name
passwordstr(128)Argon2 hash
nick_namestr(30) nulldisplay name
user_genderenum(GenderType)male / female / unknow
user_emailstr(255) unique nullemail
user_phonestr(20) nullphone
last_logindatetime nulllast login time
status_typeenum(StatusType)enable / disable / invalid
token_versionint default=0paired with Redis token_version:{uid} for session invalidation
must_change_passwordbool default=Falseforce change at first login
created_at / updated_atdatetimemaintained by AuditMixin
created_by / updated_bystr(64) nullCRUDBase writes from CTX_USER_ID

Relations:

  • by_user_roles — M2M → Role

token_version is the JWT invalidation primitive: INCR once after password change / forced logout / impersonation exit and all old tokens fail. See Auth.

Role

Table: roles

FieldTypeNotes
idint PK
role_namestr(20) uniquerole name
role_codestr(20) uniquerole code (e.g. R_HR_ADMIN)
role_descstr(500) nulldescription
data_scopeenum(DataScopeType) default=allrow-level scope (see data scope)
by_role_homeFK → Menudefault landing menu
status_typeenum(StatusType)status

Relations:

  • by_role_menus — M2M → Menu
  • by_role_apis — M2M → Api
  • by_role_buttons — M2M → Button
  • by_role_users — Reverse → User

data_scope default = all is a pitfall

The model defaults data_scope to all, so omitting it in ensure_role(...) makes the role "see everything". Always set it explicitly in business seeds or department managers will see the entire company.

Table: menus

FieldTypeNotes
idint PK
menu_namestr(100)menu label
menu_typeenum(MenuType)catalog / menu
route_namestr(100) uniquerouter name
route_pathstr(200) uniquerouter path
path_paramstr(200) nullpath params
route_paramJSON nullroute params (list[dict])
orderint default=0sibling order
componentstr(100) nullview.xxx / layout.base$view.xxx
parent_idint default=0parent menu (0 = top)
i18n_keystr(100) nulli18n key (overrides menu_name)
iconstr(100) nullicon name
icon_typeenum(IconType)iconify / local
hrefstr(200) nullexternal link
multi_tabboolallow multiple tabs for same route
keep_aliveboolcache page state
hide_in_menuboolhidden (e.g. detail pages)
active_menuFK self null"highlight parent" for hidden routes
fixed_index_in_tabint nullpinned tab index
status_typeenumstatus
redirectstr(200) nullredirect path
propsboolroot route flag
constantboolpublic route (login etc.)

Relations:

  • by_menu_buttons — M2M → Button
  • by_menu_roles — Reverse → Role

Api

Table: apis

FieldTypeNotes
idint PK
api_pathstr(500)path (with {item_id} placeholders)
api_methodenum(MethodType)get / post / put / patch / delete
summarystr(500) nullendpoint summary
tagsJSONtags
status_typeenumenable allows; disable rejects with 2200
is_systembool default=Falseauto-registered marker

refresh_api_list() reconciles the FastAPI route table with the Api table on every startup:

  • routes ↔ table set diff
  • extra → DELETE + Radar WARNING "API deleted"
  • missing → INSERT
  • existing → UPDATE summary / tags

Developers never maintain the Api table by hand. See RBAC / API auto-reconcile.

Button

Table: buttons

FieldTypeNotes
idint PK
button_codestr(200) indexedcode (e.g. B_HR_EMP_CREATE)
button_descstr(200)description
status_typeenumstatus

Relations:

  • by_button_menus — Reverse → Menu (button mounted on menu)
  • by_button_roles — Reverse → Role

Naming convention: B_<MODULE>_<RESOURCE>_<ACTION>. See RBAC / button naming.

Dictionary (system dictionary)

Table: sys_dictionary, unique constraint (dict_type, value).

FieldTypeNotes
idint PK
dict_typestr(100)type (e.g. tag_category / employee_position)
labelstr(100)label
valuestr(100)stored value
orderint default=0sort
statusenumstatus
remarkstr(500) nullnote

Purpose: turn "dropdown options" into back-office-configurable resources. Frontend hits GET /api/v1/system-manage/dictionaries/{dict_type}/options (5-min Redis cache).

Seed example (HR's Tag.category references dict_type="tag_category"):

python
DICTIONARY_SEEDS = [
    {"dict_type": "tag_category", "label": "Working style", "value": "working_style", "order": 1},
    ...
]

Database

  • Default: SQLite (app_system.sqlite3)
  • Switch to PostgreSQL / MySQL / SQL Server via .env DB_URL (no code change), see Switching DB
  • Business modules can declare a standalone DB_URL; autodiscover registers a separate Tortoise connection

Migrations

Tables are not auto-created at startup. After model changes:

bash
make mm     # = tortoise makemigrations + migrate

Migrations live in migrations/<app_name>/ (system + shared business in migrations/app_system/; standalone-DB modules in migrations/app_<biz>/).

See also

  • MixinsBaseModel / AuditMixin / TreeMixin / SoftDeleteMixin
  • Sqids — how PK / FK become sqid strings
  • RBAC — User / Role / Menu / Button / API together
  • HR module — business module model sample

基于 MIT 协议发布