简介:这份OA系统设计方案文档面向企事业单位、政府机构及非营利组织的信息化建设人员、系统架构师与项目管理者,针对传统纸质办公效率低、审批流程繁琐、信息难以集中共享等问题,提供一套完整的办公自动化平台建设思路。文档围绕编写目的、项目背景与适用范围展开,重点阐述运行环境(硬件与软件选型)、功能模块设计(文档管理、工作流引擎、协同办公、信息公告、人力资源、资产管理、客户服务等)、三层系统架构、实施策略以及安全性与稳定性保障措施,并附有术语缩略语与参考资料说明。资源包共1个doc文件,大小约11.61MB,内容为结构完整的概要设计说明书,目录层级清晰,便于按章节查阅与二次编辑。目前已有81人学习下载,适合需要撰写OA系统方案、进行技术选型或搭建办公自动化平台的技术人员参考借鉴。
1. 一份 OA 系统设计方案文档,到底该先写什么
很多团队接到“OA 系统设计方案”这个任务时,第一反应是打开 Word 先写目录:系统概述、功能模块、技术架构、数据库设计、部署方案。写完几十页,评审会上被问三个问题就卡住了——审批流节点怎么动态增减?组织架构调整后历史单据怎么追溯?并发审批时数据锁在哪一层?这三个问题答不上来,方案文档就是一堆排版整齐的废话。
OA 系统设计方案的核心不是“写一份文档”,而是把一套可落地的协同办公系统在纸面上推演一遍:谁用、用什么功能、数据怎么流、权限怎么控、出问题怎么查。它适合两类人——一类是接到任务要输出方案文档的开发者或技术负责人,另一类是准备自研或二次开发 OA 的团队,需要先想清楚边界再动手。这篇笔记按“先立骨架、再填血肉、最后留后路”的顺序,把一份能通过评审、能指导开发的设计方案拆开讲。
2. 先定边界:OA 系统设计方案的功能域与角色划分
2.1 功能域怎么切才不重不漏
OA 的功能域划分有个常见误区:按部门切。行政部管公告、人事部管考勤、财务部管报销——听起来清晰,实际开发时公告要关联组织架构,考勤要关联审批流,报销要关联预算科目,模块之间全是交叉引用。我一般按“数据对象 + 动作”来切,把 OA 拆成六个基础域:
| 功能域 | 核心数据对象 | 典型动作 | 依赖关系 |
|---|---|---|---|
| 组织与人员 | 部门、岗位、用户 | 增删改查、调岗 | 被所有域引用 |
| 权限与角色 | 角色、资源、策略 | 授权、鉴权 | 依赖组织域 |
| 审批流 | 流程模板、实例、任务 | 发起、审批、转办 | 依赖组织与权限 |
| 文档与知识 | 文件夹、文档、版本 | 上传、检索、分享 | 依赖权限 |
| 日程与会议 | 日程、会议室、参会人 | 预约、冲突检测 | 依赖组织 |
| 日志与审计 | 操作日志、登录日志 | 记录、查询、导出 | 独立写入 |
这个切法的好处是:每个域的数据对象边界清晰,域之间只通过 ID 引用,不直接持有对方实体。比如审批流实例只存发起人 ID 和审批人 ID,不存姓名和部门名——姓名和部门变了,历史单据显示的是当前值还是快照值,这是设计方案里必须明确的一个决策点。
提示:组织与人员域建议保留“历史快照”能力。用户调岗后,历史审批单据应显示当时的部门,而不是当前部门。实现方式可以是在流程实例表里冗余一份发起时的组织快照 JSON。
2.2 角色划分要落到权限模型上
角色划分不是画几个框写“管理员、普通用户、领导”就完了。设计方案里必须落到权限模型:是 RBAC 还是 ABAC,还是两者结合。OA 场景下我推荐 RBAC 打底、ABAC 补位。
RBAC 解决“谁能访问哪个资源”,ABAC 解决“在什么条件下能访问”。比如“部门经理可以审批本部门的报销单”——“部门经理”是角色,“本部门”是属性条件,“报销单”是资源。纯 RBAC 要把每个部门经理和每个部门组合成角色,角色数量爆炸;纯 ABAC 策略写起来灵活但排查困难。混合模型的做法是:
# 权限判定伪代码:RBAC 先过资源级,ABAC 再过数据级 def check_permission(user, resource, action): # 第一层:RBAC 资源级鉴权 if not rbac.has_permission(user.roles, resource.type, action): return False # 第二层:ABAC 数据级过滤 if resource.type == "expense": # 只有同部门或上级部门可审批 if action == "approve": return user.dept_id in get_sub_dept_ids(resource.owner_dept_id) return True逻辑说明:第一层用角色判断“有没有资格碰这类资源”,第二层用属性判断“能不能碰这条数据”。参数上,user.dept_id和resource.owner_dept_id都来自组织域,get_sub_dept_ids需要缓存部门树,避免每次递归查库。
2.3 用表格把角色与功能域的权限矩阵定下来
设计方案里必须有一张权限矩阵表,否则开发时每个人理解不一样。矩阵的行是角色,列是功能域,单元格是权限级别(无权限/只读/编辑/审批/管理)。这张表不用写进代码,但评审时必须逐格确认。
| 角色 | 组织人员 | 权限角色 | 审批流 | 文档知识 | 日程会议 | 日志审计 |
|---|---|---|---|---|---|---|
| 普通员工 | 只读 | 无 | 发起/查看 | 编辑自己 | 编辑自己 | 无 |
| 部门主管 | 只读 | 无 | 审批本部门 | 编辑本部门 | 编辑本部门 | 查看本部门 |
| 行政管理员 | 编辑 | 无 | 管理模板 | 管理全局 | 管理全局 | 查看全局 |
| 系统管理员 | 管理 | 管理 | 管理 | 管理 | 管理 | 管理 |
这张表定下来之后,后面所有接口的鉴权注解都从这里推导,不会出现“这个接口到底谁能调”的扯皮。
3. 审批流引擎选型与数据表设计:自研还是用现成
3.1 自研轻量引擎 vs 集成工作流框架
审批流是 OA 的心脏,也是设计方案里最容易写虚的部分。常见做法有两种:自研一套轻量级状态机,或者集成成熟的工作流框架。我一般建议:如果审批场景不超过“串行、并行、条件分支、会签、或签”这五种,自研更可控;如果涉及子流程、回退到任意节点、动态加签、超时升级,直接上框架。
自研轻量引擎的核心是两张表:流程模板表和流程实例表。模板表定义节点和连线,实例表记录当前走到哪、谁在审。下面是一个最小可用的表结构:
-- 流程模板表:一个模板对应一条审批链 CREATE TABLE flow_template ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(128) NOT NULL COMMENT '模板名称,如报销审批', nodes JSON NOT NULL COMMENT '节点定义数组', created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 流程实例表:一次审批的运行时状态 CREATE TABLE flow_instance ( id BIGINT PRIMARY KEY AUTO_INCREMENT, template_id BIGINT NOT NULL, biz_type VARCHAR(64) NOT NULL COMMENT '业务类型,如 expense', biz_id BIGINT NOT NULL COMMENT '业务单据 ID', current_node VARCHAR(64) NOT NULL COMMENT '当前节点标识', status TINYINT NOT NULL DEFAULT 0 COMMENT '0进行中 1通过 2驳回 3撤销', org_snapshot JSON COMMENT '发起时组织快照', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_biz (biz_type, biz_id), INDEX idx_status (status) );逻辑说明:nodes字段用 JSON 存节点数组,每个节点包含id、name、type(approve/condition/cc)、assignee_rule(审批人规则)。org_snapshot存发起时的部门和岗位,解决前面说的历史追溯问题。参数上,biz_type和biz_id联合索引保证业务单据能反查流程实例,status索引用于待办列表查询。
3.2 节点审批人规则的四种写法
审批人怎么定,是设计方案里必须写死的。常见规则有四种:指定人、角色、部门主管、发起人自选。每种规则对应一个解析函数,运行时根据规则算出审批人列表。
def resolve_assignees(node, instance, biz_data): rule = node["assignee_rule"] if rule["type"] == "fixed": return rule["user_ids"] elif rule["type"] == "role": # 查角色下所有用户,再按部门过滤 users = get_users_by_role(rule["role_code"]) return filter_by_dept(users, biz_data["dept_id"]) elif rule["type"] == "dept_leader": # 逐级向上找主管,直到找到有审批权限的人 return find_dept_leader(biz_data["dept_id"], rule.get("level", 1)) elif rule["type"] == "self_select": return instance["starter_choice"]逻辑说明:fixed直接返回用户 ID 列表;role先查角色再按部门过滤,避免跨部门审批;dept_leader需要处理“主管空缺”的情况,向上递归找;self_select依赖发起时用户选择。参数上,level控制向上找几级,默认 1 级。
注意:
dept_leader规则必须处理循环引用。如果部门树配置错误导致 A 的上级是 B、B 的上级是 A,递归会死循环。加一个最大深度限制,比如 10 级。
3.3 会签与或签的计数逻辑
会签是所有审批人都通过才进入下一节点,或签是任意一人通过即可。这两种模式在实例表里需要记录每个审批人的状态。
CREATE TABLE flow_task ( id BIGINT PRIMARY KEY AUTO_INCREMENT, instance_id BIGINT NOT NULL, node_id VARCHAR(64) NOT NULL, assignee_id BIGINT NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT '0待处理 1通过 2驳回 3转办', comment VARCHAR(512), created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_instance_node (instance_id, node_id), INDEX idx_assignee_status (assignee_id, status) );会签判定:SELECT COUNT(*) FROM flow_task WHERE instance_id=? AND node_id=? AND status=0,如果为 0 且没有驳回记录,则节点通过。或签判定:任意一条status=1即通过,同时把同节点其他待处理任务置为“自动通过”。参数上,idx_assignee_status支撑待办列表查询,idx_instance_node支撑节点状态聚合。
4. 数据库与接口设计:把并发和审计写进方案里
4.1 审批并发时的锁策略
同一节点多个审批人同时操作,或者同一审批人重复提交,是 OA 里最常见的并发问题。设计方案里必须明确锁的粒度。我一般用“乐观锁 + 唯一约束”组合:flow_task表加version字段,更新时带版本号;同时给(instance_id, node_id, assignee_id)加唯一索引,防止重复插入任务。
ALTER TABLE flow_task ADD COLUMN version INT NOT NULL DEFAULT 0; ALTER TABLE flow_task ADD UNIQUE KEY uk_task (instance_id, node_id, assignee_id); -- 审批时更新 UPDATE flow_task SET status = 1, comment = ?, version = version + 1 WHERE id = ? AND version = ? AND status = 0;如果affected_rows为 0,说明任务已被处理或版本冲突,直接返回“该任务已处理”。参数上,version由前端提交时带上,后端比对。这个方案不需要分布式锁,适合中小规模 OA;如果审批量极大,再考虑 Redis 分布式锁。
4.2 审计日志表的设计要点
审计日志不是简单记一条“谁在什么时候做了什么”。设计方案里要明确:记哪些操作、存哪些字段、保留多久、怎么查询。我一般分两张表:操作日志和登录日志。操作日志记录业务变更,登录日志记录认证事件。
CREATE TABLE audit_operation ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, action VARCHAR(64) NOT NULL COMMENT '如 approve, reject, create', resource_type VARCHAR(64) NOT NULL, resource_id BIGINT, before_data JSON COMMENT '变更前快照', after_data JSON COMMENT '变更后快照', ip VARCHAR(45), created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_user_time (user_id, created_at), INDEX idx_resource (resource_type, resource_id) );逻辑说明:before_data和after_data存 JSON 快照,用于追溯“改了什么”。参数上,ip字段长度 45 兼容 IPv6;索引idx_user_time支撑“某人某段时间的操作”查询,idx_resource支撑“某条单据的变更历史”查询。保留策略建议按时间分区,超过 12 个月的数据归档到冷存储。
4.3 接口幂等与防重提交
OA 里发起审批、提交报销这类接口必须幂等。常见做法是前端生成一个request_id,后端用 Redis 或数据库唯一约束去重。
def submit_expense(request_id, user_id, amount, items): # 幂等检查:同一个 request_id 只处理一次 if redis.setnx(f"idem:{request_id}", "1") == 0: return get_existing_result(request_id) redis.expire(f"idem:{request_id}", 3600) # 正常业务逻辑 expense_id = create_expense(user_id, amount, items) return {"expense_id": expense_id}逻辑说明:setnx原子操作,返回 0 表示已存在,直接返回上次结果。expire设 1 小时过期,防止 Redis 膨胀。参数上,request_id由前端用 UUID 生成,每次提交新生成一个;重试时复用同一个。
5. 避坑与排查:OA 设计方案里最容易翻车的五个点
5.1 组织架构调整后历史单据显示错乱
现象:用户调岗后,之前发起的审批单在待办列表里显示的是新部门,审批人按新部门规则解析,导致原审批人看不到单子。原因:流程实例没有存组织快照,运行时动态查当前组织。解决:在flow_instance表加org_snapshot字段,发起时写入部门、岗位、上级链;审批人解析优先用快照,除非模板明确要求“按当前组织”。
5.2 会签节点有人转办后计数错误
现象:会签节点 3 个审批人,其中 1 人转办给他人,原任务状态置为“转办”,新任务插入后,节点通过条件判断status=0的数量变成 3,永远无法通过。原因:转办逻辑没有把原任务排除在计数之外。解决:计数时只统计status IN (0,1)且assignee_id在当前节点有效审批人列表中的任务;转办任务单独标记status=3,不参与计数。
5.3 条件分支的表达式注入
现象:流程模板里条件分支用字符串表达式,如amount > 1000,用户输入金额时带特殊字符导致表达式解析异常或越权。原因:表达式直接拼接执行。解决:用白名单解析器,只允许数字、比较运算符和预定义字段名;或者把条件配置成结构化 JSON,如{"field":"amount","op":"gt","value":1000},后端逐字段解析。
5.4 待办列表分页查询慢
现象:用户待办列表加载超过 3 秒。原因:flow_task表数据量大,assignee_id和status没有联合索引,或者查询时关联了多张表。解决:加idx_assignee_status (assignee_id, status)联合索引;待办列表只查任务表,业务单据信息通过biz_type和biz_id批量查,避免 N+1。
5.5 审批意见的 XSS 与敏感词
现象:审批意见输入框提交<script>标签,在详情页渲染时执行。原因:前端直接渲染富文本,后端没有过滤。解决:后端存储时做 HTML 转义或白名单过滤;前端渲染用textContent而非innerHTML。敏感词过滤按业务需要加,但不要阻塞主流程,异步审核即可。
6. 从方案文档到可运行 Demo:一个最小验证路径
方案写得再漂亮,不跑一遍心里没底。我一般会在方案评审通过后,用两天时间搭一个最小 Demo,只验证三件事:审批流能不能走通、权限能不能控住、并发会不会出错。技术栈选最熟的,不追求完整。
第一步,用 SQLite 代替 MySQL 建表,把flow_template、flow_instance、flow_task三张表建起来。第二步,写一个命令行脚本模拟发起和审批:
# demo_flow.py:最小审批流验证 import sqlite3, json, uuid conn = sqlite3.connect(":memory:") conn.execute("""CREATE TABLE flow_instance ( id TEXT PRIMARY KEY, template_id TEXT, biz_id TEXT, current_node TEXT, status INT, org_snapshot TEXT)""") conn.execute("""CREATE TABLE flow_task ( id TEXT PRIMARY KEY, instance_id TEXT, node_id TEXT, assignee_id TEXT, status INT)""") def start_flow(template, biz_id, starter, dept): inst_id = str(uuid.uuid4()) first_node = template["nodes"][0] conn.execute("INSERT INTO flow_instance VALUES (?,?,?,?,?,?)", (inst_id, template["id"], biz_id, first_node["id"], 0, json.dumps({"dept": dept, "starter": starter}))) for uid in first_node["assignees"]: conn.execute("INSERT INTO flow_task VALUES (?,?,?,?,?)", (str(uuid.uuid4()), inst_id, first_node["id"], uid, 0)) conn.commit() return inst_id def approve(task_id): cur = conn.execute("UPDATE flow_task SET status=1 WHERE id=? AND status=0", (task_id,)) if cur.rowcount == 0: return "already handled" # 检查同节点是否还有待处理 row = conn.execute("SELECT instance_id, node_id FROM flow_task WHERE id=?", (task_id,)).fetchone() pending = conn.execute("SELECT COUNT(*) FROM flow_task WHERE instance_id=? AND node_id=? AND status=0", (row[0], row[1])).fetchone()[0] if pending == 0: conn.execute("UPDATE flow_instance SET status=1 WHERE id=?", (row[0],)) conn.commit() return "ok"逻辑说明:start_flow创建实例和首个节点的任务;approve用UPDATE ... WHERE status=0保证幂等,rowcount=0说明已被处理;然后检查同节点待处理数,为 0 则实例通过。参数上,template是内存里的字典,assignees是用户 ID 列表。
第三步,用多线程模拟并发审批同一个任务,验证只有一个线程成功。第四步,把 SQLite 换成 MySQL,加唯一索引和版本号,再跑一遍。这个 Demo 不超过 200 行代码,但能把方案里最核心的流程、权限、并发三个问题验证掉。
我自己的习惯是:方案文档里每写一个“必须”,就在 Demo 里找一个对应的验证点。写“审批人解析必须支持部门主管”,Demo 里就造一个三级部门树,测主管空缺时向上递归;写“并发必须幂等”,Demo 里就开 10 个线程抢同一个任务。跑通了,方案才敢签字;跑不通,回去改设计,比开发到一半发现流程走不通强得多。希望帮到你。
本文还有配套的精品资源,点击获取