ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

用FastAPI快速搭建办公室日常管理系统:轻量审批与数据建模实战

用FastAPI快速搭建办公室日常管理系统:轻量审批与数据建模实战 简介基于Maven的JavaWeb办公室日常管理信息系统完整项目涵盖文件管理、考勤管理、会议记录、日常事务处理等模块支持按条件查询与统计适合JavaWeb初学者、数据库课程设计或综合实训参考。资源共7个文件压缩包9.18MB主要包含5个SQL脚本、1个实践报告文档和相关项目压缩包。SQL脚本覆盖用户、文件、会议、事务等核心表结构docx报告可用于撰写设计说明项目源码则可直接导入IDE运行调试。目前已有770人学习下载。通过该资源可快速掌握基于Maven的JavaWeb分层开发、数据库表设计以及考勤查询、会议记录等典型业务编码思路同时附带完整文档便于对照完成实训报告能有效节省从零搭建系统的时间。1. 办公室日常管理信息系统先明确边界再动手搭很多人一听到“办公室日常管理信息系统”下意识会往大而全的 OA 方向想恨不得把审批、考勤、会议室、车辆、固定资产全部塞进去最后做成一个谁都不愿意用的“电子僵尸”。我经手过的几个内部系统项目里真正能活下来、每天有人打开的反而是那些只解决一两个痛点、表单不超过十张、审批流不超过三级的小系统。办公室日常管理信息系统本质上是把“纸质登记 口头沟通 Excel 台账”这三件套替换成一套可检索、可追溯、可统计的线上流程覆盖的对象通常是值班安排、公物领用、会议室预约、用印申请、访客登记这类高频且低风险的场景。这个标题里的“日常”二字决定了它的建设思路必须和传统 MIS 有所区别不要一开始就搞主数据治理、组织架构同步、复杂的角色矩阵而是先找到办公室里重复度最高、最容易被问“现在到哪一步了”的那几件事。适合读这篇内容的人是那些需要自己动手搭内部工具的后端工程师、运维同学或者说被行政部拉着做需求梳理的兼职开发。接下来我不讲产品经理那套需求分析模板直接讲我怎么把这张表拆开、怎么设计数据结构、怎么用 FastAPI 在三五天内把后端跑起来。2. 架构选型与核心实体像“标签化一张表”一样做扩展性2.1 为什么不用重型流程引擎日常管理系统的复杂度边界办公室日常管理里的审批比如“申请一支签字笔”“预约明天下午的会议室”本质上是一个状态机而不是一套业务流程。Activit 或 Camunda 这类流程引擎解决的是跨系统、多条件路由、会签、或签、超时自动提醒等复杂问题引入它们意味着要维护 BPMN 文件、流程版本、部署包。对于办公室场景90% 的申请只有三到四个状态草稿、待审批、已通过或已拒绝、已结束。引入流程引擎带来的部署复杂度和心智负担远超它节省的那点开发量。所以常见做法是用一张业务主表 一张状态流转表 一张操作日志表来完成轻量级审批。主表存业务字段状态流转表存“从哪个状态到哪个状态、谁操作的、什么时候”操作日志表记录所有查看和编辑行为。这种设计下加一个新审批类型只需要新增一张业务表复用的是同一条状态机逻辑。我一般会在项目初始化阶段就写一个approval_base的抽象模型后续所有业务表都继承它。2.2 核心数据模型设计从“办公室日常管理信息系统”的实体关系说起设计表的顺序应该从“谁在用”出发而不是从“有什么功能”出发。办公室日常管理信息系统里最核心的实体不是“申请单”而是“申请人”和“审批人”。任何时候一张申请单必须能回答三个问题谁提的、现在卡在谁那里、历史上有谁动过。基于这个原则我给出一个最小可用的核心表结构DDL 如下CREATE TABLE office_daily_applications ( id BIGSERIAL PRIMARY KEY, biz_type VARCHAR(32) NOT NULL, -- asset_apply / meeting_room / official_seal biz_no VARCHAR(64) NOT NULL UNIQUE, -- 业务编号如 OA20250711-001 applicant_id BIGINT NOT NULL, -- 申请人用户ID applicant_dept_id BIGINT NOT NULL, -- 申请人部门ID title VARCHAR(200) NOT NULL, current_status VARCHAR(20) NOT NULL DEFAULT draft, current_approver_id BIGINT, -- 当前待办人 form_data JSONB NOT NULL DEFAULT {}, -- 具体业务字段按类型扩展 created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), submitted_at TIMESTAMPTZ ); CREATE TABLE approval_flow_logs ( id BIGSERIAL PRIMARY KEY, application_id BIGINT NOT NULL REFERENCES office_daily_applications(id), from_status VARCHAR(20) NOT NULL, to_status VARCHAR(20) NOT NULL, operator_id BIGINT NOT NULL, comment TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now() );需要特别说明的是form_data这个 JSONB 字段。办公室场景的业务字段变化极快今天要加一个“预计归还日期”明天要加一个“是否涉及外带”如果每加一个字段都做一次 ALTER TABLE表结构会很快失控。JSONB 允许不同业务类型在同一张主表里共存查询时用form_data-field_name取字段值当某类表单的数据量真的涨上来、需要做统计报表时再把高频字段提升为独立列。这是一个常见的“先宽后严”建模策略适合内部系统这类需求不稳定的场景。2.3 查询视图与索引设计避免办公室系统变成慢查询重灾区办公室日常管理信息系统的数据量不会特别大一年撑死几万条但查询模式非常固定按申请人查、按审批人查待办、按时间段查记录、按部门查统计。下面这组索引基本覆盖了绝大部分检索路径CREATE INDEX idx_daily_apps_applicant_created ON office_daily_applications (applicant_id, created_at DESC); CREATE INDEX idx_daily_apps_approver_status ON office_daily_applications (current_approver_id, current_status); CREATE INDEX idx_daily_apps_biz_type_created ON office_daily_applications (biz_type, created_at DESC); CREATE INDEX idx_daily_apps_biz_no ON office_daily_applications (biz_no);注意idx_daily_apps_approver_status这个索引的顺序先把current_approver_id放前面因为“查我的待办”是最频繁的入口。如果把current_status放前面过滤掉已结束的申请后会得到大量行再按审批人过滤时效果就差了。还有一点容易被忽略biz_no虽然加了唯一索引但实际查询经常是用它做模糊搜索比如输入“OA20250711”想看当天所有单子。对于这种需求可以直接用biz_no LIKE OA20250711%走索引前缀匹配不用额外引入全文检索组件。3. 用 FastAPI 快速搭建可注册的业务接口从单路由到自动装载3.1 为什么选择 FastAPI 作为办公室 MIS 的后端骨架办公室日常管理信息系统的开发效率和迭代速度比极致性能更重要。FastAPI 的优点是自动生成 OpenAPI 文档前端可以对着/docs直接联调省去单独维护接口文档的精力Pydantic 模型同时承担请求体校验和序列化两个职责依赖注入系统让数据库会话、当前用户这些公共依赖变得非常干净。对于只有几个业务模块的内部系统这一套组合比 Spring Boot 轻很多又比 Flask 多了一层类型保障。这里需要坦白一点FastAPI 的异步特性在办公室 MIS 场景里其实用不太上因为绝大多数操作是同步的数据库读写没有高并发 IO 等待。选择它主要是看中代码组织方式和文档自动生成而不是性能。如果你所在团队的既有技术栈是 Java用 Spring Boot 写也完全没问题下面的设计思路是通用的只是代码示例用 Python 表达。3.2 实现一个按业务类型自动注册的路由工厂办公室日常管理信息系统的接口大多长得类似创建申请、提交审批、审批通过、拒绝、查询详情、列表分页。如果每个业务类型都复制一份 CRUD 代码维护成本会直线上升。我一般会写一个路由工厂把公共逻辑收敛到一个函数里业务模块只需要声明表单模型和审批流配置。# router_factory.py from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel from typing import Type, Optional, List class ApprovalFlowConfig(BaseModel): biz_type: str submit_to: str # 提交后进入哪一步如 dept_manager allow_cancel: bool True allow_reject: bool True def create_biz_router( biz_type: str, schema: Type[BaseModel], flow: ApprovalFlowConfig, ): router APIRouter(prefixf/api/{biz_type}, tags[biz_type]) router.post(/applications) def create_application( payload: schema, dbDepends(get_db), current_userDepends(get_current_user), ): # 写主表状态置为 draft生成业务编号 biz_no generate_biz_no(biz_type) app OfficeDailyApplication( biz_typebiz_type, biz_nobiz_no, applicant_idcurrent_user.id, applicant_dept_idcurrent_user.dept_id, titlepayload.title, form_datapayload.model_dump(), ) db.add(app) db.commit() return {biz_no: biz_no, id: app.id} router.post(/applications/{app_id}/submit) def submit_application(app_id: int, dbDepends(get_db)): app get_application_or_404(db, app_id) if app.current_status ! draft: raise HTTPException(400, 只有草稿状态才能提交) app.current_status pending app.current_approver_id find_approver(app.applicant_dept_id, flow.submit_to) app.submitted_at now() log_approval_flow(db, app, draft, pending, app.applicant_id) db.commit() return {message: submitted} router.get(/applications/mine) def my_applications( status: Optional[str] None, page: int 1, page_size: int 20, dbDepends(get_db), current_userDepends(get_current_user), ): query db.query(OfficeDailyApplication).filter( OfficeDailyApplication.applicant_id current_user.id ) if status: query query.filter(OfficeDailyApplication.current_status status) total query.count() items query.order_by(OfficeDailyApplication.created_at.desc()) \ .offset((page - 1) * page_size).limit(page_size).all() return {total: total, items: items} return router这段代码的逻辑很直白create_biz_router接收业务类型、Pydantic 模型、审批流配置返回一个独立的 APIRouter。创建申请时payload.model_dump()会把整个请求体挂到form_data字段里这样新增业务字段只需要改 Pydantic 模型不改表结构。submit时会自动寻找对应的审批人这个查找逻辑在find_approver里实现典型规则是查部门负责人或角色表。使用这个工厂时业务侧代码可以做到极简# main.py from router_factory import create_biz_router, ApprovalFlowConfig class AssetApplyForm(BaseModel): title: str asset_name: str quantity: int Field(gt0) reason: str expected_return_date: Optional[date] None app.include_router(create_biz_router( asset_apply, AssetApplyForm, ApprovalFlowConfig(biz_typeasset_apply, submit_todept_manager), )) class MeetingRoomApplyForm(BaseModel): title: str meeting_date: date start_time: time end_time: time attendee_count: int need_projector: bool False app.include_router(create_biz_router( meeting_room, MeetingRoomApplyForm, ApprovalFlowConfig(biz_typemeeting_room, submit_toadmin_office), ))新增一个业务模块只需要定义表单模型和审批流配置路由、创建、提交、查询逻辑全部复用。一个办公室日常管理信息系统里通常有四到六个这样的业务模块这套工厂方法能把重复代码压缩掉一半以上。要注意的是get_current_user依赖需要提前实现推荐用 JWT 或内部 SSO 的 header 透传。开发阶段最简单的方式是从X-User-Id头取用户 ID省去完整的登录流程等系统上线前再接正式的认证。3.3 分页与筛选参数的最佳实践办公室系统的列表页查询条件一般不会超过五个状态、申请人、时间范围、关键词、当前审批人。不要老老实实把这些参数全拼进 URL 然后逐字段判断而是统一封装一个QueryParams模型用model_dump(exclude_noneTrue)清掉空值后拼 where 条件。另一个更省事的方式是用 FastAPI 的Query参数直接在函数里声明代码更直观router.get(/applications) def list_applications( status: Optional[str] Query(None, pattern^(draft|pending|approved|rejected|closed)$), from_date: Optional[datetime] None, to_date: Optional[datetime] None, keyword: Optional[str] Query(None, max_length50), current_userDepends(get_current_user), ): query db.query(OfficeDailyApplication) if status: query query.filter(OfficeDailyApplication.current_status status) if from_date: query query.filter(OfficeDailyApplication.created_at from_date) if to_date: query query.filter(OfficeDailyApplication.created_at to_date) if keyword: # 不要用 contains or_ 拼多字段这里只查标题效率最高 query query.filter(OfficeDailyApplication.title.ilike(f%{keyword}%)) if not current_user.is_admin: query query.filter(OfficeDailyApplication.applicant_dept_id current_user.dept_id) return paginate(query, page, page_size)这里有个细小但影响体验的点分页的page和page_size最好设上限page_size最大 100避免有人写个脚本拉全量数据把内部系统的数据库打满。普通用户看到 20 条一页或 50 条一页没有实质差别但page_size从 20 改成 200 时数据库压力翻十倍办公室系统本身就部署在内网没必要为这种场景买单。4. 实战落地把资产管理做成一个低代码审批闭环4.1 需求边界办公室日常管理里最容易量化的模块办公室日常管理信息系统的第一个业务模块我建议从“固定资产与办公用品领用”入手。原因很简单它是所有模块里最容易被量化验收的。每一件资产都有“在用、闲置、维修中、已报废”的状态每一次领用都有“谁领的、什么时候领的、什么时候还的”月底能直接算出来每个部门的资产占用率。相比会议室预约这种用完即焚的场景资产管理的闭环更完整也更适合作为团队熟悉这套代码架构的练习项目。领用流程包含三个参与方申请人发起领用申请、部门负责人审批确认是不是真有需求、资产管理员审批确认库存够不够并完成出库。两级审批加一个出库动作这是办公室场景的中位数复杂度把它的状态流转理清楚其他模块基本可以复用同一套逻辑。4.2 领用审批状态机与关键接口实现先定义状态流转规则草稿draft→ 待部门审批pending_dept→ 待资产管理员出库pending_admin→ 已出库completed。任何一级拒绝都进入已拒绝rejected。申请人可以在 draft 状态下撤回提交后只要还在待审批阶段可以申请撤回管理员同意后回到 draft。这里有一个容易踩坑的设计决策不要为两级审批各建一张表而是复用current_status和current_approver_id两个字段。当状态是pending_dept时current_approver_id指向部门负责人当状态变成pending_admin这个字段直接改成资产管理员 ID。查询待办只需要一条 SQLWHERE current_approver_id :me AND current_status NOT IN (completed,rejected,draft)。办公室系统里所有“我的待办”本质都是在查这一张主表。# asset_flow.py def approve_asset_application(app_id: int, db: Session, current_user: User): app get_application_or_404(db, app_id) if app.current_approver_id ! current_user.id: raise HTTPException(403, 您不是当前审批人) if app.current_status pending_dept: app.current_status pending_admin app.current_approver_id find_asset_admin(db) log_approval_flow(db, app, pending_dept, pending_admin, current_user.id) db.commit() return {message: dept approved} if app.current_status pending_admin: # 出库时扣减库存 stock get_asset_stock(db, app.form_data[asset_name]) if stock app.form_data[quantity]: raise HTTPException(400, f库存不足当前剩余 {stock}) decrease_stock(db, app.form_data[asset_name], app.form_data[quantity]) app.current_status completed app.current_approver_id None log_approval_flow(db, app, pending_admin, completed, current_user.id) db.commit() return {message: asset delivered} raise HTTPException(400, f当前状态 {app.current_status} 不允许此操作)这段代码把两级审批放在同一个函数里处理逻辑顺序很清晰。要注意current_approver_id在完成后被置空这既是为了防止重复审批也是为了让“查我的待办”那条 SQL 不需要额外判断状态。审批日志写在approval_flow_logs表里每个动作都有记录出库时扣减库存的操作和状态更新放在同一个事务里避免“状态显示已出库但库存没扣”的脏数据。出库扣减库存这个小步骤建议加一行库存预检查。办公室场景虽然并发不高但到了月底集中领用的时候两个人同时申请同一件物品的情况会发生。扣减库存用 UPDATE 语句的条件写法能天然避免超卖UPDATE asset_stock SET quantity quantity - :qty WHERE asset_name :name AND quantity :qty RETURNING quantity;如果返回空结果说明库存不足直接抛异常。这条语句是原子操作不需要额外加锁比先 SELECT 再 UPDATE 安全得多。4.3 让普通员工也觉得好用的提交表单写法办公室日常管理信息系统最大的阻力不是开发而是用户习惯。行政同事习惯了微信发消息申请突然让他们打开网页填表如果没有明显的效率提升他们会用脚投票。所以表单设计要尽量贴近真实操作路径资产领用表单只需要六七个字段不要出现“资产编码”“财务归属”“折旧年限”这种财务视角的字段。真正需要这些数据时让资产管理员在出库阶段补录而不是让申请人填写。一个实用的做法是把常用物品做成下拉选项而不是自由输入。自由输入会让同一个物品产生“签字笔”“水笔”“圆珠笔”三个名字后面做统计时非常痛苦。在asset_apply表单里维护一个常用物品清单表前端下拉选择物品名称和数量后端校验时也检查这个物品是否在清单里。这种细节决定了系统上线后能不能留住用户。5. 权限与多部门边界让 RBAC 真正适配办公室角色5.1 部门隔离与数据可查范围的权衡办公室日常管理信息系统涉及的数据不是完全公开的。部门负责人的待办列表里不应该出现其他部门的申请普通员工能看到自己所在部门的申请列表吗我的默认策略是“部门内可见跨部门不可见”办公室和人事这样的管理部门具备全局查看权限。这种设计适配绝大多数中小型办公室的权责边界。数据权限的实现方式和角色强相关。权限模型用 RBAC但不要做角色继承或多级角色会让配置复杂化。办公室场景只需要四类角色普通员工employee、部门负责人dept_manager、资产管理员asset_admin、系统管理员admin。判断用户权限用 if/else 就足够了引入 Casbin 这类的权限框架反而成了过度设计。5.2 数据权限过滤的三个实用配置def apply_data_scope(query, current_user): if current_user.role in (admin, asset_admin): return query # 管理员看全局 if current_user.role dept_manager: return query.filter( OfficeDailyApplication.applicant_dept_id current_user.dept_id ) # 普通员工只能看自己的申请 return query.filter(OfficeDailyApplication.applicant_id current_user.id)这个函数放在分页查询之前所有列表接口都过它一遍。这里展示的是一套规则真实的系统中或许需要更细的维度但核心思路是一致的把数据范围过滤集中在一个函数里而不是散落在每个接口中。权限规则变动时只需改这里。部门负责人能看部门内所有申请包括其他人的申请这是为了让他们处理审批时能看到上下文历史记录。资产管理员看全局数据是因为他们需要跨部门做资产调动和盘点。角色和数据范围的对应关系如下角色可查看范围可操作行为普通员工仅本人申请创建、撤回部门负责人本部门全部申请一级审批、部门内查看资产管理员全部申请二审出库、库存维护系统管理员全部数据全部操作、配置管理这四行规则排序也很重要先判断管理员再判断部门负责人最后落到普通员工。如果用 ORM 的or_一次性接收这些条件有权限边界不清晰的风险。顺序判断的可读性更强。5.3 操作日志的安全审计视角办公室日常管理信息系统的数据敏感性高即使只是查看操作也建议留痕。但日志不用面面俱到记录更新、删除、导出三个危险动作就足够。办公室场景中查看他人申请属于敏感行为只要数据范围过滤做对了普通员工无法查看他人数据日志记录的优先级就降低了。class OperationLog(Base): __tablename__ operation_logs id Column(BigInteger, primary_keyTrue) user_id Column(BigInteger, nullableFalse) action Column(String(20), nullableFalse) # create / update / delete / export target_type Column(String(40), nullableFalse) target_id Column(BigInteger) detail Column(JSONB, default{}) ip_address Column(String(64)) created_at Column(DateTime, defaultdatetime.utcnow)写日志用装饰器包一层而不是在业务代码里手动埋点def log_action(action: str): def decorator(func): functools.wraps(func) def wrapper(*args, **kwargs): result func(*args, **kwargs) if db in kwargs: db kwargs[db] db.add(OperationLog( user_idkwargs.get(current_user).id, actionaction, target_typeresult.__class__.__name__, target_idresult.id, detail{args: str(kwargs.get(payload, ))[:200]}, )) db.commit() return result return wrapper return decorator日志表不需要建索引一年下来没多少行。需要排查问题时按 user_id 和时间段扫一遍就行这个量级全表扫描也在毫秒级。重点是把detail字段的 JSONB 截断到 200 字符避免把整个表单数据塞进去导致行膨胀。6. 从导出到对账验收上线前的五个收尾细节6.1 用 SQL 直接做月度统计不额外写报表接口办公室日常管理信息系统上线一个月后行政最常问的问题是“这个月各部门领了多少东西”“会议室使用率怎么样”。与其专门开发报表模块不如直接提供按月导出的接口统计用一条 SQL 在库里先聚合好SELECT form_data-asset_name AS asset_name, COUNT(*) AS apply_count, SUM((form_data-quantity)::int) AS total_quantity, applicant_dept_id FROM office_daily_applications WHERE biz_type asset_apply AND current_status completed AND created_at date_trunc(month, CURRENT_DATE) GROUP BY asset_name, applicant_dept_id ORDER BY total_quantity DESC;这条 SQL 利用了 JSONB 字段的-操作符直接在数据库层做统计避免把全量数据拉到内存再聚合。注意(form_data-quantity)::int这个类型转换JSONB 里存的是字符串聚合前必须转成数值。这一步漏掉的话数据库会报错对应到代码里就是 500。统计类查询在办公室系统里属于低频操作跑几十毫秒完全可以接受不用额外引入 OLAP 引擎。办公室系统验收时最容易被忽略的是“只有 Excel 导出没有对账”。建议在导出功能里加一个“统计口径说明”明确告诉你统计的是“已完成申请”而不是全部申请避免行政拿着包含草稿状态的数字去汇报。这种口径上的歧义往往比代码 bug 更容易引发信任危机。6.2 填报异常的四个高频坑与定位方法第一个坑是日期格式。会议室预约表单里时间字段传到后端变成字符串直接塞进 JSONB 没问题但排序时就错了。解决方案是在 Pydantic 模型里把字段声明为date或datetime类型序列化时统一格式化。第二个坑是数量字段允许填 0 或负数。Pydantic 的Field(gt0)能挡住大部分非法输入但前端如果把quantity传成字符串3Pydantic 会默认帮你强转这既是便利也是隐患需要定义好整型字段。第三个坑是审批人变量写法不一致。find_approver函数返回用户 ID但有些地方用字符串类型的工号等联调时两个类型碰撞数据库报invalid input syntax for type bigint。这种错误看日志立刻就能定位但提前在find_approver返回值上做类型标注更省事。第四个坑是时间范围查询的边界。from_date传2025-07-11数据库里只有2025-07-11 00:00:00这一条匹配当天的数据查不全。规范做法是为日期参数增加显式说明。6.3 上线前用一个脚本验证所有审批路径办公室日常管理信息系统的验收不能只点一遍页面至少要把所有审批路径用脚本跑一遍。我一般写一个 pytest 测试把每个业务类型的“创建→提交→一级通过→二级通过→完成”和“创建→提交→拒绝”两条主路径全部覆盖。审批系统的风险不在正常路径而在异常路径——重复提交、非审批人操作、越权查看。这几个用例优先级最高def test_asset_apply_full_approval_flow(client, normal_user, dept_manager, asset_admin): # 员工提交申请 resp client.post(/api/asset_apply/applications, json{title: 领用签字笔, asset_name: 签字笔, quantity: 2, reason: 日常办公}, headers{X-User-Id: str(normal_user.id)}) assert resp.status_code 200 app_id resp.json()[id] # 员工不能审批自己的申请 resp client.post(f/api/asset_apply/applications/{app_id}/submit, headers{X-User-Id: str(normal_user.id)}) # 这里先提交到草稿状态 assert resp.status_code 200 # 非当前审批人越权审批 resp client.post(f/api/asset_apply/applications/{app_id}/approve, headers{X-User-Id: str(asset_admin.id)}) assert resp.status_code 403测试覆盖了三个风险点。越权测试是关键——办公室系统的数据不敏感但不代表可以随便被看到越权审批验证不到位可能导致流程被绕过。每当新增一个业务类型建议把对应的审批路径测试用例加到 CI 里这样后面改公共逻辑时不会被无意的回归击穿。最后再提醒一个务实的小点上线第一周会把审批流日志表打开让开发随时能看操作记录。这样做不只是为了排查问题更重要的是让使用的人感到“系统在记录”这种行为约束对内部系统的规范和推广比写再多说明文档都有用。本文还有配套的精品资源点击获取
返回列表