Skip to content

Architecture

Overview

FastAPI App
|-- Middleware: CORS / RequestID / BackgroundTask / Guard / Radar
|-- /api/v1/auth                 (system)
|-- /api/v1/route                (system)
|-- /api/v1/system-manage/*      (system)
|-- /api/v1/business/<module>/*  (business)
`-- api -> services -> controllers -> models

Runtime dependencies
|-- Tortoise ORM  (SQLite / PostgreSQL / MySQL)
|-- Redis         (cache + startup lock)
`-- Sqids / JWT   (ID encoding + token signing)

Module boundaries

PackageResponsibilityAllowed dependencies
app/core/framework infrastructure (no business)doesn't depend on system / business
app/system/built-in modules (auth, RBAC, users, menus, APIs, dictionary)only app/core/
app/business/<x>/business modules (HR / CRM / Inventory ...)app/utils (transitively core/system); never sibling business modules
app/utils/stable public facade for business codere-exports app/core/* and a few app/system/security symbols
app/cli/code generators (init/gen/gen-web/initdb)offline-only, no runtime impact

Business code should never from app.system.xxx import ... (except services system explicitly exposes — ensure_menu / ensure_role). For cross-business communication use the event bus.

Request lifecycle

  1. Inbound middleware (app/core/middlewares.py + make_middlewares())
    • CORSMiddleware
    • PrettyErrorsMiddleware — pretty exception output
    • BackgroundTaskMiddleware — injects FastAPI's BackgroundTasks into CTX_BG_TASKS
    • RequestIDMiddleware — injects X-Request-ID to response headers and CTX_X_REQUEST_ID
    • RadarMiddleware (conditional) — captures request / SQL / exception to Radar
    • fastapi-guard (conditional) — rate limit / auto-ban
  2. Routing — business routes uniformly under /api/v1/business/<name>; system routes under /api/v1/{auth,route,system-manage}
  3. Dependency injection
    • DependAuth — JWT decode → check token version → load user + role/button permissions into ContextVars
    • DependPermission — on top of DependAuth, exact (method, path) match against role.apis
    • require_buttons(...) / require_roles(...) — factory dependencies, attach as needed
  4. Business logic
    • api/ only wires; rules live in services/ and controllers/
  5. Response
    • Always return Success / SuccessExtra / Fail (JSONResponse subclasses, auto camelCase)

Startup lifecycle

create_app()
  |-- register_db(app)                      # Tortoise.init(config=TORTOISE_ORM)
  |-- register_exceptions(app)              # BizError / DoesNotExist / IntegrityError / ValidationError handlers
  |-- register_routers(app, prefix="/api")  # system /api/v1/...
  |-- discover_business_routers()           # /api/v1/business/<name>/...
  `-- setup_radar(app)                      # optional

lifespan(app)
  |-- init_redis() -> app.state.redis
  |-- FastAPICache.init(RedisBackend(redis), prefix="fastapi-cache")
  |-- delete _INIT_LOCK_KEY / _INIT_DONE_KEY
  |-- _run_init_data(app)                   # leader-only with multi-worker
  |   |-- init_menus()                      # system menu seeds (only when Menu table is empty)
  |   |-- refresh_api_list()                # FastAPI routes <-> Api table reconciliation
  |   |-- init_users()                      # system roles + default users + dictionary
  |   |-- for each business init():         # business modules' init_data.init()
  |   `-- refresh_all_cache()               # role permissions / constant routes -> Redis
  |-- startup_radar()                       # optional
  |-- yield
  `-- close_redis()                         # shutdown

For semantics see Startup init & reconciliation.

RBAC data model

User
`-- M2M --> Role
    |-- M2M --> Menu        frontend-visible routes
    |-- M2M --> Button      in-page actionable buttons
    |-- M2M --> Api         callable backend endpoints
    |-- FK  --> Menu        role's default home page; by_role_home
    `-- field: data_scope   row-level scope: all / department / self / custom
  • The super-admin role R_SUPER (app.core.constants.SUPER_ADMIN_ROLE) bypasses every check
  • API permissions are auto-managed by refresh_api_list() (full reconciliation by (method, path))
  • Menus / buttons are declared per module via ensure_menu(), optionally with reconcile_menu_subtree() for IaC
  • Button code convention: B_<MODULE>_<RESOURCE>_<ACTION> (e.g. B_HR_EMP_CREATE)
  • See Auth / Data scope

Multi-worker startup

Production typically runs 4 granian workers. They coordinate via Redis lock app:init_lock:

  • The leader (SET app:init_lock 1 NX EX 120 winner) runs the full init, then SET app:init_done 1 EX 120
  • Other workers poll app:init_done (max wait 150s)
  • Before each process start, the leader DELs both keys, so init really runs on every restart

Multi-database connections

  • By default all models live on conn_system
  • A business module can declare its own DB_URL in config.py, which autodiscover registers as conn_<biz> with a separate Tortoise app
  • Use get_db_conn(Model) for cross-model transactions; never hard-code the connection name
  • See Database / standalone DB

Cache model

DataRedis KeyTTLWriter
Constant routesconstant_routesforeverrefresh_all_cache
Role menu IDsrole:{code}:menusforeverload_role_permissions
Role APIsrole:{code}:apisforeversame
Role buttonsrole:{code}:buttonsforeversame
Role data scoperole:{code}:data_scopeforeversame
User rolesuser:{uid}:rolesforeverload_user_roles
User homeuser:{uid}:role_homeforeversame
Token versiontoken_version:{uid}foreverpassword change / impersonate
Business-localper moduleper moduleper module

See Cache.

Where to next

基于 MIT 协议发布