“车间设备又趴了”这句话,在企业运维群里一出现,往往意味着接下来几天的沟通都会变成一团乱麻。我在总部信息中心干了这么多年,最深的体会是:设备故障本身不可怕,可怕的是故障之后的流程失控——报修的人不知道找谁,维修师傅跑空趟,备件领了没记录,月底算费用时账目完全对不上。为了把这条链路彻底理顺,我们内部基于Python从零设计了一套企业维修系统,覆盖报修、派单、维修、验收、备件领用和统计报表,从需求调研到上线约两个月。这篇文章把整个系统的设计思路、技术选型、核心实现和生产部署踩坑记录完整写出来,无论你正在做内部工单系统,还是准备做一个类似的毕业设计项目,都可以直接参考里面的模型设计和代码片段。
1. 不是工具不够用,是流程本身没被定义清
1.1 旧方式的三处痛点
在写代码之前,我先在内部调研了一圈,发现大家抱怨的其实不是没有系统,而是手里的工具各自为政。报修主要靠微信群“吼一嗓子”加 Excel 台账,维修单用纸质登记,备件出入库依赖库房管理员“凭记忆记账”,月底做维修成本统计时要把微信群聊记录翻出来逐条人工核对,耗时且容易漏。
这套模式下有三个结构性问题。第一,工单状态不可见。报修人发出消息后就只能等,不知道维修师傅是否已接单、是否到了现场、备件是否缺货;第二,数据和数据之间没有关联。设备台账、维修记录、备件库存彼此隔离,想回答“哪些设备最容易坏、单台设备一年修几次、平均维修成本是多少”这些问题基本做不到;第三,责任追溯难。一个工单经历了哪些环节、由谁处理、状态为什么卡住,全部没有结构化记录。
所以这个系统的首要目标并不是“做一个在线报修页面”,而是把维修全流程固化成可追踪、可量化、可回溯的数据流转。这也是整个项目最重要的需求拆解,后面所有模块的划分都以它为起点。
1.2 功能模块先画出来,再谈数据库
我不建议接到需求就直接建表。更稳妥的做法是先按业务角色和行为把功能地图画清楚。我们内部梳理之后,确认了五类角色和六个模块。
- 报修人:提交报修工单、查看处理进度、确认维修结果。
- 调度员:接收新工单、分配维修人员、处理加急和转派。
- 维修工:接单、填写处理过程和耗材、上传维修照片、申请验收。
- 部门经理/管理员:查看维修情况、审批特殊事项、管理基础数据和账号。
- 库房管理员:备件入库、维护库存、处理领用记录。
模块对应分为基础数据、工单管理、备件库存、权限体系、通知消息、统计报表六个部分。基础数据里最重要的是设备台账,使用部门、安装位置、负责人、维保厂家这些字段必须提前维护好,报修时才能通过下拉框直接选设备,避免每个人手动输入设备名,最终统计出十几种不规范的名称。
工单是系统核心,围绕它展开的是状态流转和操作记录。备件模块则要与工单紧密联动,维修工在填写处理过程时可以发起的领用申请,自动关联到当前工单编号,便于后续核算成本。统计报表不是额外装饰,它的数据来源就是工单和备件流水,直接回答管理层最关心的设备可靠性和维修成本问题。
1.3 被低估的“过程记录”功能
设计时我特意把操作日志放到高优先级。很多维修系统原型只关心工单最后是“已完工”还是“未完工”,忽略中间过程。可实际业务里,最容易被扯皮的就是“中途到底发生过什么”。
一个工从提交到关闭,至少要经历多次状态变化,例如调度员派给谁、维修工是否接了单、上门后发现缺备件暂停、再次开工、提交待验收、验收人驳回、重新处理、最终关闭。每一次状态变化都要记录操作人、操作时间和备注,用户点开工单详情页就能看到完整时间线。
这块成本并不高,无非是一张work_order_log表加一段信号或装饰器逻辑,但价值很大。某次叉车维修发生争议时,我们就是靠日志查出了维修工在工单里被错误延期了两天的事实。如果一开始不记录这些过程数据,后面再补几乎不可能补全,所以我把这条视为系统设计的默认原则。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术栈选型:选 Python 和 Django,是权衡后的结果
2.1 为什么是 Django 而不是 Flask 或 FastAPI
这个项目确定用 Python 开发后,内部确实讨论过 Flask 和 FastAPI。企业维修系统本质上是一个“套路化的数据管理系统”,核心是后端管理界面的增删改查、权限控制、状态机和报表导出。在评估时我重点掂量了几个点。
Django 让人放心的第一点是自带 Admin 后台。设备台账、备件类目、部门人员这些基础数据维护界面,如果全部自己写,至少要占三分之一的前端工作量;而 Django Admin 几分钟就能给非技术人员一个可以用的后台。第二是 Django ORM 的迁移机制做得成熟,模型变更后执行 makemigrations / migrate 就能平滑升级表结构,内部系统经常会加字段,这个特性非常实用。第三是用户认证、Session、CSRF 防护、消息框架这些开箱即用,省去重复造轮子。
Flask 的定位是轻量灵活,但灵活意味着所有组件都要自己挑自己组装。Flask-SQLAlchemy、Flask-Login、Flask-Admin、表单校验、迁移工具、权限扩展这一套装下来,和 Django 的默认全家桶相比并没有更轻,而且版本之间的兼容性时不时会踩坑。FastAPI 在异步和高并发上确实更强,但企业内部维修系统的并发量通常很低,几十个人同时报修已经算峰值,根本用不到异步红利;另一方面 FastAPI 对团队里 Python 水平一般的同事来说理解成本更高,系统以后需要维护时容易断层。
如果业务是做面向海量用户的开放 API,我会毫不犹豫换 FastAPI;但做企业内部事务型系统,Django+LTS 版本是能让人睡得最安稳的选择。这里我还想说一句,Python 版本也不要追新,企业内部服务器普遍安装稳定版 Python 3.8 或 3.10,配合 Django 4.2 LTS,至少能维持几年官方安全维护。
2.2 内部工具要不要做前后端分离
这个决策直接决定了整个研发效率。市面上很多模板把系统拆成 Vue + Django REST Framework + 部署两套前端项目,看上去更“现代”,但对内部系统来说维护成本明显偏高。你要考虑跨域问题、Token 过期刷新问题、前后端联调排期,以及多个版本接口不一致的风险。
我们这次采用的是 Django Templates 服务端渲染为主,结合少量页面对接 JSON 接口。报修页面、工单列表、详情页都由 Django 直接渲染,提交数据的表单仍然保持普通的 POST 提交,只在整个过程中用少量 Ajax 做局部刷新;真正的 JSON 接口只留给统计图表和移动端简单查询使用,接口用 Django REST Framework 提供,数量非常少。
这么做的好处显而易见。首先一个 Python 后端工程师就能独立搞定全部开发和上线,不需要专门的前端人员;其次页面出问题时的排查链路很短,直接看视图函数、模板和浏览器请求头就能定位问题;最后是代码部署简单,不涉及 Node.js 构建、nginx 独立托管静态前端这些额外环节。内部工具追求的不是技术表演,而是稳定的可用性和可维护性。
2.3 工程目录与项目结构
项目没有用默认的单 app 单目录结构,而是按业务拆分成多个应用,否则所有模型都放在 models.py 里会快速变成灾难,尤其到了后期新功能不断叠加时会很难维护。项目结构如下,供直接参考:
code复制repair_system/
├── manage.py
├── config/
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── apps/
│ ├── accounts/ # 用户、部门、角色权限
│ ├── assets/ # 设备台账
│ ├── workorders/ # 工单、日志、状态流转
│ ├── spareparts/ # 备件、出入库流水
│ └── reports/ # 统计报表与导出
├── templates/
├── static/
└── media/
把 accounts 放在 assets 和 workorders 之前,是因为用户和权限是其他模块的前置依赖。apps 目录下面是独立业务应用,每个应用保持相对独立,通过 Django 的 URL 反向解析互相跳转,而不是互相 import 视图。这样可以保证开发的时候多人并行互不干扰,后面做接口或重写某个子模块时也能单独替换。
settings 没有搞复杂的环境切换插件,只拆成 config/settings.py 和 config/settings_prod.py,本地开发读默认配置,生产服务器通过环境变量指定加载 production 配置。有时候越简单的配置管理反而越不容易出错,如果用多环境插件我没意见,但对团队默契度不够的情况来说,配置文件少一些更好沟通。
3. 核心功能实战:工单状态、备件库存和报表实现
3.1 工单模型建模与状态机的四种状态
工单模型是整个系统的中枢,它像一块记录板,串联了设备、报修人、维修人员和备件领用信息。我的代码里核心字段如下,建模时需要注意 choices 常量建议用英文标识,具体中文文案展示时再映射,因为数据库里面存英文值更方便后续扩展,如果存中文,将来想改文案就得写迁移脚本。
python复制class WorkOrder(models.Model):
class Status(models.TextChoices):
PENDING = "pending", "待派单"
PROCESSING = "processing", "处理中"
PENDING_ACCEPT = "pending_accept", "待验收"
DONE = "done", "已完成"
REJECTED = "rejected", "已驳回"
class Priority(models.TextChoices):
LOW = "low", "低"
MEDIUM = "medium", "中"
HIGH = "high", "高"
URGENT = "urgent", "紧急"
no = models.CharField("工单编号", max_length=32, unique=True, editable=False)
title = models.CharField("报修标题", max_length=120)
desc = models.TextField("故障描述")
device = models.ForeignKey(
"assets.Device", on_delete=models.PROTECT,
related_name="work_orders", verbose_name="设备"
)
reporter = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.PROTECT,
related_name="created_orders", verbose_name="报修人"
)
assignee = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.SET_NULL,
null=True, blank=True, related_name="assigned_orders",
verbose_name="维修人"
)
priority = models.CharField("优先级", max_length=20, choices=Priority.choices, default=Priority.MEDIUM)
status = models.CharField("状态", max_length=30, choices=Status.choices, default=Status.PENDING)
handling_result = models.TextField("处理结果", blank=True)
finish_time = models.DateTimeField("实际完成时间", null=True, blank=True)
created_at = models.DateTimeField("报修时间", auto_now_add=True)
updated_at = models.DateTimeField("更新时间", auto_now=True)
class Meta:
db_table = "work_order"
indexes = [
models.Index(fields=["status", "created_at"]),
models.Index(fields=["assignee", "status"]),
]
def save(self, *args, **kwargs):
if not self.no:
self.no = f"WO{timezone.now():%Y%m%d%H%M%S}{secrets.token_hex(2).upper()}"
return super().save(*args, **kwargs)
这里 on_delete=models.PROTECT 是有意的设计选择,而不是用 CASCADE。设备一旦被某个工单引用,就不允许直接删掉;报修人关联工单后同样不允许用户被直接删除。这样可以保证历史工单的引用完整性和审计追溯能力,避免删了用户导致工单查不出是谁报的。
状态流转的规则也刻意收敛得很严格,一共就五条路径:待派单可分配给维修工;被指派的维修工接单后状态改成处理中;处理完成需要等待验收;验收通过则完成;验收不通过则回退到处理中。工单一旦完成,任何编辑都不允许,如果设备再次故障只能新建工单,禁止去修改旧工单的结果。状态机不在视图函数里散落,而是封装在 WorkOrderService 的一个方法中。
python复制def transition(order, user, target_status, remark=""):
allowed_map = {
WorkOrder.Status.PENDING: {
"processing": lambda: order.assignee_id and order.assignee_id == user.id,
},
WorkOrder.Status.PROCESSING: {
"pending_accept": lambda: order.assignee_id == user.id,
"rejected": lambda: user.has_perm("workorders.can_reject"),
},
WorkOrder.Status.PENDING_ACCEPT: {
"done": lambda: request.user 通过验证,
"processing": lambda: 验收驳回需填写原因,
},
}
...
每条状态迁移都会同步写入一条 WorkOrderLog。后期无论管理层问“这个单为什么处理了三天”还是维修工解释“我早就修完了”,只要点开时间线,理由都会自动呈现在眼前。
3.2 并发场景下的状态更新漏洞
维修单流转系统有个经典坑:多人同时操作同一张工单时,状态会被前一个请求覆盖。举个实际例子:维修工 A 正在处理后提交验收,同时调度员误认为这张单没人处理,又把同一张单重新派给了维修工 B,两个请求同时到达服务器,各自在旧状态上做修改,后提交的请求可能把 A 的“待验收”覆盖回“待派单”。
解决这个问题的标准做法不是在 Python 代码里用锁,而是通过数据库条件更新来实现原子状态变更。update() 方法会生成一条 UPDATE ... WHERE id=某id AND status=期望状态 的 SQL,受影响行数返回 0 就说明当前状态已经不是预想值,此时抛出明确提示让用户刷新页面再看。
python复制from django.db.models import F
def assign_order(order_id, new_assignee_id):
order = WorkOrder.objects.get(pk=order_id)
updated = WorkOrder.objects.filter(
pk=order.pk,
status=WorkOrder.Status.PENDING
).update(
assignee_id=new_assignee_id,
status=WorkOrder.Status.PROCESSING,
updated_at=timezone.now()
)
if updated != 1:
raise ValidationError("工单已被其他人处理,请刷新页面后再操作")
我见过不少项目在视图层直接写 order.status = “x”; order.save(),这在单人演示时没问题,一旦推进到多人环境就会产生各种状态错乱。建议在项目一开始就约定所有更新走 service 层的原子方法。经验是:这类状态数据宁可更新失败让用户重新操作,也不要被静默覆盖,如果状态错乱,业务数据很容易彻底失控。
3.3 备件领用与库存扣减,事务和行锁缺一不可
维修工单里关联备件领用的环节也特别容易出问题。业务场景一般是维修工在系统里提交一张领用单,包括备件名称、型号和数量,库房管理员看到后按下“出库”按钮,库存数就从系统里减去。
最幼稚的写法是先查询库存是否够,然后做减法再保存,代码如下:
python复制part = SparePart.objects.get(pk=part_id)
if part.stock < qty:
raise ValidationError("库存不足")
part.stock -= qty
part.save()
这段代码在并发场景下存在严重的条件竞争:假如两个维修工同时各自领 3 个零件,备件只剩 5 个,两个请求同时读到 stock=5,都通过了 if 检查,最后都写回 2,实际库存却应该是 2,多扣了一个。对付这类业务,第一选择是数据库事务加行锁,让两个请求串行执行对库存行的修改。
python复制from django.db import transaction
def do_outbound(order_id, part_id, qty):
with transaction.atomic():
part = SparePart.objects.select_for_update().get(pk=part_id)
if part.stock < qty:
raise ValidationError(f"库存不足,当前仅剩 {part.stock}")
part.stock = F("stock") - qty
part.save(update_fields=["stock", "updated_at"])
SparePartLog.objects.create(
order_id=order_id,
part=part,
change_type="out",
qty=qty,
stock_after=part.stock - qty
)
select_for_update() 会给该行加一把数据库行锁,直到整个事务提交或回滚才释放。后面第二个请求再执行时,会等前面事务结束,然后读到最新的库存。配合 F 表达式,能进一步避免先读出旧值再写回的问题。这套机制在我们系统里一直很稳定,没有出现过一次库存金额对不上的情况。
库存预警也要用消息机制,而不是在页面上用 if 临时判断。我在每次出库后检查剩余库存是否低于预警阈值,如果低于阈值就自动生成一条待办消息和站内信,提醒库房管理员补货。这个逻辑放在模型层或事务里触发,避免不同页面各写一套。
3.4 报表统计:从无脑 ORM 到实用图表接口
做了工单和备件之后,管理层必然会想要数据报表。最核心的一张综合看板包括:本月报修数、待处理数、平均维修时长、按时完成率、备件库存预警数量。如果全部靠人肉统计,这种系统就失去了价值。
Django ORM 的聚合能力可以满足大部分场景。以下是按月统计数据:
python复制from django.db.models.functions import TruncMonth
from django.db.models import Count
def monthly_order_count(start_date, end_date):
return (
WorkOrder.objects
.filter(created_at__range=[start_date, end_date])
.annotate(month=TruncMonth("created_at"))
.values("month")
.annotate(total=Count("id"))
.order_by("month")
)
这个查询返回两种字段:month 是月份起点时间,total 是该月的工单量。前端拿到数组后喂给 ECharts 的折线图即可。为了避免原生统计接口异常难控,我只开了三个固定的 JSON 接口:月度工单趋势、部门报修排行、设备故障排行,这样前端不需要动态拼接 SQL 之类的拼接逻辑,接口又相对稳定。
导出 Excel 是每个人最后都想做但常常偷懒的需求。这里我选择用 pandas 直接处理查询结果,比手动用 openpyxl 逐行写入更省代码,pandas 再配合 to_excel 接口非常方便,虽然也会引来额外依赖,但对内部数据量来说完全可行。
python复制import pandas as pd
def export_workorders(request):
qs = WorkOrder.objects.select_related("device", "assignee").filter(
created_at__range=[start, end]
)
data = [
{
"工单编号": o.no,
"报修标题": o.title,
"设备名称": o.device.name,
"申报人": o.reporter.username,
"维修人": o.assignee.username if o.assignee else "",
"状态": o.get_status_display(),
"报修时间": o.created_at.strftime("%Y-%m-%d %H:%M"),
}
for o in qs.iterator()
]
df = pd.DataFrame(data)
response = HttpResponse(content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
response["Content-Disposition"] = "attachment; filename=workorders.xlsx"
df.to_excel(response, index=False)
return response
对于大量导出场景还应该在查询后做 limit 限制,比如“最多导出最近 6 个月、前 1 万条”,防止内部人员一次导出全量数据把数据库连接拖垮。导出操作也要写到操作日志里,尤其是涉及费用明细的报表,后续审计时需要知道谁会看过这些数据。
4. 部署与运营:从开发环境到生产,哪些代码之外的坑要躲开
4.1 Linux + Nginx + Gunicorn 的标准部署框架
企业内部系统部署尽量简单直接。服务器采用 Linux(Ubuntu 20.04),先安装 Python 3.10 和 venv,创建独立虚拟环境,避免影响系统 Python。项目所有依赖通过 pip freeze 输出到 requirements.txt,然后在新环境里执行 pip install -r 进行安装。用独立虚拟环境不会污染服务器上的系统库,这一点尤其重要,很多同事图省事直接把依赖装到系统全局 Python 里,之后很容易因为包版本冲突把系统工具搞坏。
生产 WSGI 服务器我们选的是 Gunicorn,配 4 个 worker,每个 worker 处理 2 个线程。这只是起步配置,理论上并发要求并不高。但 worker 数量不能一股脑设成“CPU 核数乘 2”,如果服务器只有 2 核 4G,worker 设置 8 个以上会直接吃满内存,随后出现被 OOM Killer 干掉的现象。务必要先观察内存占用再定 worker 数量。
Gunicorn 的启动命令建议配置到 supervisor 的配置文件里管理:
ini复制[program:repair]
command=/opt/repair/venv/bin/gunicorn config.wsgi:application -w 4 -b 127.0.0.1:8001 --timeout 120
directory=/opt/repair
user=www-data
autostart=true
autorestart=true
redirect_stderr=true
stdout_logfile=/var/log/repair/gunicorn.log
Nginx 反向代理将 80/443 端口接收到的请求转发到 8001 端口,同时承载静态文件 media 资源的访问,nginx 的关键配置如下:
nginx复制server {
listen 80;
server_name repair.example.internal;
client_max_body_size 20m;
location /static/ {
alias /opt/repair/staticfiles/;
}
location /media/ {
alias /opt/repair/media/;
}
location / {
proxy_pass http://127.0.0.1:8001;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
static 和 media 必须分清。static 是代码自带的 CSS、JS 文件,media 是用户上传的附件图片,如果媒体文件也放在应用目录里,重新发布代码可能就会把用户上传照片冲掉,这种事故我在其它项目里踩过,所以上线之前一定检查 media 路径是否独立在项目代码目录之外。
4.2 上线初期遇到的疑难问题排查表
以下问题是我们从开发环境切到生产环境后实际遇到的,每一条都很有代表性,新手可以直接把它当复查清单使用:
| 现象 | 原因 | 处理方法 |
|---|---|---|
| 访问站点提示 Bad Request (400) | 新域名没有加入 ALLOWED_HOSTS | settings_prod.py 中追加服务器访问域名或内网 IP 列表 |
| DEBUG=False 后所有 CSS/JS 全部 404 | 没执行 collectstatic 或 Nginx 没配 static | 执行 python manage.py collectstatic,并确认 staticfiles 已收集完成 |
| 上传设备照片后访问 404 或页面提示请求实体过大 | Nginx 默认 client_max_body_size 只有 1m | 在 nginx server 块中配置 client_max_body_size 20m |
| 表格中显示时间和实际相差 8 小时 | USE_TZ=True 且存储 UTC 时间 | 在模板里通过本地时区转换展示,或统一使用当地时区启动配置 |
| Gunicorn worker 突然持续重启 | 访问量峰值时 worker 内存不够 | 降低 worker 数量并启用系统 swap 或升级内存配置 |
| 生产后台修改密码后立即被登出 | Django 新版本默认会使密码修改后的所有 Session 失效 | 属正常安全机制,提醒用户重新登录即可,也可配置旧的 Session 清理方式 |
每个问题背后都对应一个运维原则。印象最深的还是 ALLOWED_HOSTS 和 collectstatic 这两个,Debug 环境下不会暴露,一旦切到生产,两者都是必现错误,这也是为什么发布前要准备一个完整部署 checklist。我后来把每次发布需要执行的操作都写成了 shell 脚本,从 git 拉代码、执行迁移、collectstatic 到 supervisorctl restart,全部自动化,人工手敲命令很容易漏步骤。
4.3 系统上线之后的体验迭代
上线并不代表项目终结。我们第一版只有网页端报修和后台处理,但运营一段时间后发现两个高频需求:一是维修工在设备现场时,不方便打开浏览器看工单;二是部分老旧设备没有打印二维码标签,维修工到现场后找不到对应的资产编号,工单没办法关联。
于是后续加入了两个迭代,一是手机端用简单响应式页面展示工单列表和状态流转,维修工不用带电脑去现场;二是为库房打印了一批设备二维码,扫码后自动跳转到该设备的报修页。这两处的改动并不大,前一个全靠 Bootstrap 栅格适配手机屏幕,后一个只是给设备详情页加一个 /device/
这个过程中也发现,不能把用户的使用反馈全盘照做。比如有人提“希望所有人都能直接改工单状态”,这显然会破坏流程;如果照做,那么后面所有的统计基线都会变乱。合理的情况是让每个角色都有自己的操作边界,需求可以听,功能是否加入要依据流程是否还成立来定。
5. 复盘与维护:一套内部系统真正值钱的地方在哪
5.1 如果重做一遍,哪些地方会不一样
复盘时我给自己列过一张再优化清单,最先想改的是消息通知模块。第一版只在系统内部做了站内信,少了主动推送,导致维修工如果不打开网站就不知道有新工单,必须依靠调度员额外电话催。后来重新规划时,把短信通知与邮件通知补上,报修和派单事件触发后能第一时间通知到责任人,处理效率提升一个明显的量级。
第二块会明显调整的是数据字典的治理。一开始设备类型、备件名称都只是自由文本,统计报表时做了很多字符串匹配,非常痛苦。某些备件有的叫“轴承 6205”,有的叫“6205 轴承”,统计结果自然失真。如果重新做,我会在一开始就固化备件型号字典、设备类型编码体系,让用户从下拉框选而不是自己填。
第三块是权限设计要再提前。我们用 Django 的 Group 和 Permission 基础能力搭建了一套角色体系,使用还算顺利。但后期出现“部门主管只能看到本部门设备工单”这种数据级权限需求时,就要加一批专门查询过滤逻辑,相当于改动一个较大的功能点;如果第一版设计时就考虑数据范围隔离,改造起来更从容。
5.2 给正在规划维修系统的同行几句实在话
Python 开发这套系统最大的红利是在快速迭代阶段。一个后端开发从零实现了完整的工单、备件、权限和报表,工程周期被限制在两个月内,离开 Django 全家桶几乎不可能。但代码能跑只是及格线,真正区分好坏的标准是边界是否清晰,状态流转是否可控,每次变更是否都能追踪。
另外也很想提醒各位:内部系统的成功并不仅仅取决于开发进度,更取决于前期流程共识,信息系统本质上只是把组织运作的规则翻译成代码。如果线下流程本身就是人为拍板,谁官大听谁的,那么再精致的系统上线后也会沦为一个“登记工具”。“先梳理业务规则,再设计软件”这句话看似废话,但它决定了一整套系统的上限。
每次遇到同事说“这个查询能不能直接导 Excel 我再自己处理”时,我都会多问一句:“你导出去之后要做什么?”这个问题往往能挖出一个新的统计需求。长期维护内部系统,需要的是不断从线下行为里提炼需求、补全功能,软件与业务才能越长越像。
