☰
企业管理软件后端项目结构复盘:FastAPI目录分层与模块划分实战
2026/10/1 4:28:58 网站建设 项目流程

《看潮企业管理软件》这个项目,我从项目开发的第一天就开始和项目结构较劲。作为一套典型的企业管理软件,它的业务范围覆盖组织、审批、采购、库存、销售和报表,代码一多,项目结构如果定得不好,改一个字段都能引发连锁反应。这篇就是看潮项目在“项目结构”环节的完整复盘,直接给你看最终的目录、模块边界和踩坑记录。

这次不聊具体业务功能怎么实现,专门讲一件事:一个真实的企业管理软件后端,目录到底怎么分层、模块怎么切、公共代码放哪里。如果你正准备做中小型管理系统,或者正在纠结 FastAPI、Spring Boot 项目的目录应该怎么分,这篇应该能给你一个可落地的参考,而不是网上那种只有理论意义的理想结构。

先说结论:我们最终采用的是“业务模块优先,模块内技术分层”的二维结构。听起来有点抽象,下面我用真实项目一步步拆开讲,包括每个文件是干什么的、为什么放在那里,以及在搭建过程中我们踩过哪些坑。

1. 为什么企业管理软件必须先定结构再写代码

1.1 看潮项目的真实背景与核心矛盾

“看潮”这个项目代号听起来挺文艺,实际上做的事情非常传统:给一家制造企业做一套经营管理平台,包含用户部门管理、审批流程、采购入库、库存盘点、销售开单和统计报表。这种系统最大的特点是业务单据多、角色权限密、报表需求频繁变,而且客户会不断推翻之前定的业务规则。

刚开始我们并没把结构当回事,觉得先用 FastAPI 把接口写出来,后面再慢慢整理。结果项目推进了两周,代码总量不到五千行,已经出现几个典型症状:一个业务逻辑既出现在订单模块又出现在库存模块,改一个字段要全局搜索;工具类里堆了各种业务函数;数据库模型之间互相引用,业务模块根本没法独立维护。这时候才意识到,企业管理软件的信息结构是交错复杂的,如果不先把项目结构划清楚,后续所有开发都会变成给旧代码打补丁。

这个阶段的教训很直接:业务复杂不是靠“灵活”解决的,而是靠“边界”解决的。项目结构的核心价值不是好看,而是让每一段业务代码都有一个明确的家,出了问题知道去哪里改,加了新需求知道往哪里放。

1.2 双维度结构的选型逻辑

业内常见的做法有两种。一种是纯技术分层,把所有 Controller 放在一起、所有 Service 放在一起、所有 Model 放在一起;另一种是纯业务模块化,每个模块自带 Controller、Service、Dao。在企业管理系统里,只按技术分层会导致“订单 Service 里塞了库存逻辑”,只按业务分层又会让公共的权限、日志、数据库基础组件被大量重复复制。

我们最终采用了“业务模块优先,模块内技术分层”的二维结构。顶层按照业务域拆成 user、org、auth、purchase、inventory、sales、report 等模块,每个模块内部再统一分成 controller(接口层)、service(业务层)、repository(数据访问层)和 schemas(数据校验层)。公共的东西比如数据库 session、redis 客户端、日志配置、异常处理,单独放在 core 目录里,由所有模块共用。

这样选的原因很简单:企业软件的需求变化通常会集中在某个业务域内,按业务模块拆分可以让一个需求改动尽量只影响一个模块;而模块内再做技术分层,能避免业务逻辑和数据库操作混在一起,也方便以后对某个模块单独做性能优化。后面所有目录设计、代码约束都是围绕这个核心思路展开的。

2. 项目结构长什么样:看潮后端目录逐层拆解

2.1 后端主目录(FastAPI 版)

先直接上我们当前这套结构的目录树:

app/ ├── core/ │ ├── config.py │ ├── database.py │ ├── dependencies.py │ ├── exceptions.py │ ├── logging.py │ └── security.py ├── modules/ │ ├── user/ │ │ ├── controller.py │ │ ├── service.py │ │ ├── repository.py │ │ ├── models.py │ │ └── schemas.py │ ├── org/ │ │ ├── controller.py │ │ ├── service.py │ │ └── ... │ ├── auth/ │ ├── purchase/ │ ├── inventory/ │ ├── sales/ │ └── report/ │ ├── controller.py │ ├── service.py │ ├── repository.py │ ├── models.py │ └── schemas.py ├── main.py ├── migrations/ └── tests/

每个业务模块下高度统一:controller 放路由和 HTTP 层相关代码,service 放业务用例和事务逻辑,repository 放数据库查询,models 放 SQLAlchemy ORM 模型,schemas 放 Pydantic 的请求响应模型。这样新同学看任何一个模块都能快速找到对应代码,不用靠猜。

core 目录是公共基础设施。config.py 负责读取配置并输出一个全局 settings 对象;database.py 创建 engine 和 session 工厂;dependencies.py 放通用的依赖函数,比如获取当前用户、分页参数;exceptions.py 定义统一的业务异常类;logging.py 负责初始化日志格式;security.py 放密码哈希、JWT 生成工具。这个目录相当于 Spring Boot 里各种全局配置和 starter 组件的集合。

migrations 放数据库迁移脚本,我们用的是 Alembic,每次表结构变更都生成一个新的迁移文件,而不是直接手改数据库。tests 目录按模块划分测试文件,比如 test_user.py、test_purchase.py,保证每个业务模块的测试互相独立。

2.2 和 Spring Boot/Django 目录的对应关系

很多同学是从 Spring Boot 或 Django 转过来的,第一次看到 FastAPI 的项目结构容易发懵。其实概念是互通的,我们做了一张对应表:

看潮(FastAPI)Spring BootDjango职责
controller.pyControllerviews.py接收 HTTP 请求,做参数绑定,不写业务逻辑
service.pyService业务层业务用例,事务边界,状态流转
repository.pyRepository/DAOmodels.Manager 自定义数据库查询,ORM 操作
models.pyEntity/Modelmodels.py数据表映射
schemas.pyDTOserializers.py请求/响应数据校验

企业管理软件不管用什么框架,核心都是把“对外暴露的接口”和“对数据库的访问”隔离开,否则后期加权限校验、加日志、加缓存都会无从下手。很多人搜“Spring Boot 项目文件目录结构”会看到一堆 controller/service/mapper 的包,搜“FastAPI 项目目录结构”看到的往往是纯技术分层,实际落地时一定要结合业务域调整。

2.3 配置文件的组织方式

企业软件的环境配置很关键,开发环境、测试环境、生产环境不能混着来。我们用 Pydantic-Settings 管理配置,所有环境变量集中在 .env 文件,config.py 统一加载。示例:

from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8") app_name: str = "看潮企业管理软件" database_url: str = "postgresql+psycopg2://postgres:postgres@localhost:5432/kanchao" jwt_secret: str = "change-me-in-prod" jwt_algorithm: str = "HS256" access_token_expire_minutes: int = 720 redis_url: str = "redis://localhost:6379/0" log_level: str = "INFO" settings = Settings()

这样所有配置都有一个统一入口,而不是散落在各个模块里。迁移到 Spring Boot 的同学可以理解成 application.yml 加 @ConfigurationProperties;Django 项目里对应 settings.py 加 python-dotenv。唯一需要注意的是,.env 文件不能提交到 git 仓库,尤其是 JWT 密钥、数据库密码这些敏感信息,要在部署环境里单独维护。

3. 核心模块如何塞进这个结构里

3.1 用户与权限模块的落地

企业管理软件里最核心的是权限。我们把 user 模块和 auth 模块分开:user 只管用户基本信息和组织关系,auth 管登录、token、角色权限判断。这样区分是为了避免“用户”这个概念膨胀成一个大杂烩。

先看用户模型的简化定义:

from sqlalchemy import Column, Integer, String, Boolean, ForeignKey, Table from sqlalchemy.orm import relationship class User(Base): __tablename__ = "sys_user" id = Column(Integer, primary_key=True, index=True) username = Column(String(50), unique=True, index=True, nullable=False) hashed_password = Column(String(255), nullable=False) disabled = Column(Boolean, default=False) org_id = Column(Integer, ForeignKey("sys_org.id")) roles = relationship("Role", secondary="sys_user_role", back_populates="users")

登录取到用户名密码后,在 auth/service.py 里校验密码,生成 JWT:

from datetime import datetime, timedelta from jose import jwt def create_access_token(user_id: int) -> str: expire = datetime.utcnow() + timedelta(minutes=settings.access_token_expire_minutes) payload = {"sub": str(user_id), "exp": expire} return jwt.encode(payload, settings.jwt_secret, algorithm=settings.jwt_algorithm)

然后在 auth/dependencies.py 写一个统一的 get_current_user 依赖,这样任何接口只要声明这个依赖,就能拿到当前登录用户:

from fastapi import Depends, HTTPException, status from fastapi.security import OAuth2PasswordBearer oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login") def get_current_user(token: str = Depends(oauth2_scheme)) -> User: # 解析 token,查数据库,返回 User # 如果 token 失效,抛出 401

真实项目里权限判断粒度要设计成“接口权限 + 数据权限”两层,不能只在菜单上藏按钮。接口权限控制“能不能调用这个接口”,数据权限控制“这个用户能看到哪些范围的数据”(比如部门管理员只能看本部门订单)。这些逻辑统一集中在 auth 模块里,不要在多个业务模块里各写一份 JWT 校验。

3.2 业务单据模块与状态流转

企业软件里订单、审批都有状态,以采购单为例,常见状态是草稿、待审批、已批准、已入库、已关闭。最容易写乱的地方是在 controller 里堆一堆 if status == ... 的判断,导致状态流转逻辑散落在各处。

我们约定状态流转放到 service 层,并且用一个配置字典约束合法流转:

import enum class PurchaseOrderStatus(str, enum.Enum): draft = "draft" pending_approval = "pending_approval" approved = "approved" received = "received" closed = "closed" ALLOWED_TRANSITIONS = { PurchaseOrderStatus.draft: {PurchaseOrderStatus.pending_approval, PurchaseOrderStatus.closed}, PurchaseOrderStatus.pending_approval: {PurchaseOrderStatus.approved, PurchaseOrderStatus.draft}, PurchaseOrderStatus.approved: {PurchaseOrderStatus.received, PurchaseOrderStatus.closed}, PurchaseOrderStatus.received: {PurchaseOrderStatus.closed}, } def transition(purchase_order, target_status: PurchaseOrderStatus): current = purchase_order.status if target_status not in ALLOWED_TRANSITIONS[current]: raise BusinessError(f"不允许从 {current} 变更为 {target_status}") purchase_order.status = target_status

controller 层只负责“收到请求 -> 调用 service”,不直接修改状态。这样以后如果状态变复杂,可以再抽独立状态机模块,但初期用约束字典完全够用。

3.3 公共层:数据库、日志、异常处理

数据库 session 的管理是项目结构里最容易被忽略但影响最大的部分。在 FastAPI 里,我们通过依赖注入来获取 session:

from typing import Generator from sqlalchemy.orm import Session def get_db() -> Generator[Session, None, None]: db = SessionLocal() try: yield db finally: db.close()

每个接口只需要在参数里声明db: Session = Depends(get_db),框架会自动创建、关闭 session,避免连接泄漏。

异常处理也要统一。我们定义了一个 BusinessError,然后再注册全局异常处理器:

class BusinessError(Exception): def __init__(self, message: str, code: int = 400): self.message = message self.code = code @app.exception_handler(BusinessError) async def business_error_handler(request, exc): return JSONResponse(status_code=exc.code, content={"message": exc.message})

这样业务代码里只需要raise BusinessError("库存不足"),外层会自动转成标准错误响应,不用在每个接口里写 try/except。

日志方面,每个模块的 logger 名称直接用“模块名.文件名”,比如 logger = logging.getLogger("app.modules.purchase.service"),这样查日志的时候能快速定位是哪个业务模块出的问题。日志格式、文件切割都放在 core/logging.py 里统一处理。

4. 搭建过程中踩过的坑与排查实录

4.1 循环依赖:结构设计最大的坑

我们踩的第一个大坑是循环依赖。当时图省事,把数据库 session 获取函数放在了 user 模块里,结果 auth 模块要调用 user 模块的 service,user 模块又要依赖 auth 模块的权限判断,一启动就报 ImportError。

循环依赖的本质是模块间互相引用,解决办法有三种:第一,把公共的 session、settings 提升到 core 层,从根上断绝依赖;第二,在函数内部延迟导入,把from xxx import yyy移到使用它的函数内部;第三,用 FastAPI 的 Depends 机制在接口处注入依赖,模块之间不做硬引用。

我们最终的约定是:任何业务模块都可以依赖 core,但业务模块之间最好不要直接跨模块 import。如果确实需要跨模块调用,比如 purchase 模块要查询用户资料,就通过 user 模块提供的 service 接口完成,而不是直接去操作 user 的表。这样把依赖方向控制成单向的,循环依赖就基本不可能发生了。

4.2 模块边界模糊:商品到底放基础资料还是库存

业务模块拆分时最容易吵架的是实体归属问题。比如“商品”这个实体,既和基础资料相关,又和库存、销售都有关系,到底放哪里?

我们定了一个判断原则:如果这个实体被多个业务流程用到且本身不依赖流程状态,就放基础资料模块;如果它主要随某个业务流程变化,就放在对应业务模块里。按照这个原则,商品基础资料放 basic_data,当前库存数量放 inventory,出入库单据也放 inventory,销售明细放 sales。这样拆分之后,改商品字段不会影响库存表结构,库存查询也不会被商品字段拖累。

实体/数据归属模块理由
商品基础资料(名称、规格、单位)basic_data被采购、库存、销售共用,不带流程状态
当前库存数量inventory由出入库单据计算得出
采购订单purchase属于采购流程
销售订单sales属于销售流程

如果拿不准,还有一个更简单的测试方法:你把这个实体放进某个模块后,如果其他模块访问它时要绕过模块边界并修改它的字段,那大概率放错了位置。

4.3 常量与配置散落各处

另一个让人抓狂的问题是常量和配置散落。新手很容易把状态值、错误码、权限标识直接写在 controller 里,导致后来要改一个状态名称,全局替换改到崩溃。

我们把每个模块的常量收进模块内独立的 constants.py 文件,比如 purchase/constants.py 放采购订单状态、审批类型;公共错误码放 core/constants.py;配置统一走 core/config.py。像“订单状态是否允许修改”“是否需要审批”这类规则,尽量收敛到 service 层,不要在 API 层判断。

这里给一个排查技巧:当你发现一个需求改动需要在 3 个以上文件里同时修改相似代码时,就说明常量或配置放置有问题。比如你改一个“草稿”状态的显示名称,结果要同时改 controller、service、前端接口文档,大概率是状态定义散落到了不该去的地方。

说到最后,我个人在实际操作中的体会是:项目结构这种事情,一开始多花三天整理,后面能省出三周时间。看潮项目能稳定推进,很大一部分原因是目录和模块边界足够清楚,每个人接手新需求都先问“这个功能属于哪个模块”,而不是“这个函数贴在哪个 Controller 里”。如果你也在做企业管理软件,我建议先别急着写代码,花半天把模块边界画出来,哪怕画一张纸都行,这比写代码本身更能决定项目的命运。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询