SPMS项目实现过程博客

从零构建「智造中枢 SPMS」—— 一个销售生产一体化管理系统的诞生

这不是一篇"完美架构"的复盘,而是一份踩坑记录与技术决策的流水账。
项目源码基于 Flask + SQLite + 飞书机器人,覆盖销售、生产、物料、发货全链路。


目录

  1. 项目缘起:为什么会做这个系统
  2. 需求梳理:5 大模块 18 张表
  3. 技术选型:为什么是 Flask + SQLite
  4. 数据库设计:状态机驱动的闭环
  5. 后端架构:三层分离 + 双路由注册
  6. 前端实现:一个 Bootstrap 打天下
  7. 飞书集成:从回调到双机器人的演进
  8. 进程管理:一键启动的调度器
  9. 踩坑实录:5 个值得记录的教训
  10. 总结与展望

1. 项目缘起:为什么会做这个系统

制造业中小型工厂的常见痛点:销售接了单,生产不知道;生产做完了,仓库不知道;仓库发货了,财务不知道。各部门靠 Excel 和微信群沟通,信息断层导致了大量的沟通成本和延期交付。

「智造中枢 SPMS(Sales Production Management System)」就是为解决这个问题而生的。核心目标很简单:

销-产-存-发,每个环节的状态对上下游透明可见。

系统定位为内部管理系统,而非 SaaS 产品。交互方式兼顾 Web 页面和飞书群聊机器人,让车间主任不用打开网页也能完成排产、报工操作。


2. 需求梳理:5 大模块 18 张表

需求来自制造业内部流程——从订单录入到发货的全链路。拆解为以下模块:

┌─────────────────────────────────────────────────┐
│                    仪表盘                         │
│          (订单/工单/库存/发货数据汇总)              │
├──────────┬──────────┬───────────┬───────────────┤
│  销售订货  │  生产计划  │  物料管控   │   销售发货      │
│  ───────  │  ───────  │  ────────  │   ───────      │
│  订单管理  │  工单管理  │  发料/退料  │   发货单管理     │
│  订单评审  │  工序排程  │  完工入库   │   财务审核      │
│  订单变更  │  日计划    │  BOM管理   │   OQC检验      │
│  预测订单  │  进度跟踪  │  库存管理   │   发货确认      │
│           │           │  在线仓管理  │               │
├──────────┴──────────┴───────────┴───────────────┤
│                    系统管理                       │
│          (用户/角色/工作流审批记录)                  │
└─────────────────────────────────────────────────┘

经过梳理,最终产出 18 张数据表

模块 表名 用途
销售 t_sales_order 销售订单主表
销售 t_sales_order_detail 订单明细(行项目)
销售 t_order_review 多部门评审记录
销售 t_order_change 订单变更历史
销售 t_forecast_order 月度预测订单
生产 t_work_order 生产工单主表
生产 t_production_schedule 工序排程
生产 t_daily_plan 日生产计划
生产 t_plan_tracking 产量跟踪
物料 t_material_issue 发料记录
物料 t_material_return 退料记录
物料 t_material_supplement 补料记录
物料 t_online_warehouse 在线仓(产线物料)
物料 t_finish_inbound 完工入库
物料 t_bom 物料清单
物料 t_bom_detail BOM 明细
物料 t_inventory 库存主数据
发货 t_delivery_order 发货单
系统 t_user 用户
系统 t_role 角色
系统 t_workflow 审批流记录

3. 技术选型:为什么是 Flask + SQLite

选型原则

  1. 快速开发:原型到可用版本越快越好
  2. 单机部署:内部使用,不需要分布式
  3. 运维零成本:不需要装数据库服务器、不需要配 Nginx

最终技术栈

层级 技术 理由
Web 框架 Flask 3.1 轻量、路由清晰、Jinja2 模板顺手
ORM Flask-SQLAlchemy 3.1 模型即文档,关系映射简洁
数据库 SQLite 3 零配置、单文件、备份=复制文件
前端 Bootstrap 5.3 CDN 引入、组件丰富、响应式
图表 Chart.js 4.4 仪表盘柱状图+环形图
飞书 SDK lark-oapi ≥1.4.0 Python 原生支持 WebSocket 长连接

为什么不用 Django?

Django 的 Admin 和 ORM 很好,但太重。这个项目的核心是业务流程而非 CRUD,Flask 的路由灵活度更适合快速迭代。

为什么不用 MySQL?

内部使用场景,单机 SQLite 完全扛得住。并发写入少(订单工单操作频率低),SQLite 在 WAL 模式下的读写性能足够。备份只需复制的 spms.db 文件。


4. 数据库设计:状态机驱动的闭环

核心设计思路

不是简单的 CRUD,而是状态流转。 订单、工单、发货单三套各自独立的状态机,通过外键关联形成闭环。

订单状态流转

草稿(0) → 已确认(1) → 生产中(2) → 已完工(3) → 已发货(4)
  ↓
评审中(1) → 通过(2) / 拒绝(3)

工单状态流转

待排产(0) → 已排产(1) → 生产中(2) → 已完工(3) → 已结案(4)
                ↑                          ↑
          (排产时记录)              (报工自动计算完工率)

发货状态流转

待审核(0) → 已审核(1) → 已备货(2) → 已发货(3)
              ↑                       ↑
         (财务审批)            (自动更新关联订单状态)

外键关系设计

SalesOrder ──1:N── SalesOrderDetail     (订单明细)
SalesOrder ──1:N── OrderReview           (订单评审)
SalesOrder ──1:N── OrderChange           (订单变更)
SalesOrder ──1:N── WorkOrder             (一个订单可拆多个工单)
SalesOrder ──1:N── DeliveryOrder         (一个订单可分批发货)
WorkOrder  ──1:N── ProductionSchedule    (工序排程)
WorkOrder  ──1:N── DailyPlan             (日计划)
WorkOrder  ──1:N── PlanTracking          (进度跟踪)
WorkOrder  ──1:N── MaterialIssue         (发料)
WorkOrder  ──1:N── MaterialReturn        (退料)
WorkOrder  ──1:N── MaterialSupplement    (补料)
WorkOrder  ──1:N── FinishInbound         (完工入库)
Bom        ──1:N── BomDetail             (BOM 明细)
DeliveryOrder ──1:N── DeliveryDetail     (发货明细)

关键决策:级联删除

所有 detail 子表设置了 cascade='all, delete-orphan',删除主记录自动清理明细。 MaterialIssue/Return/Supplement/FinishInbound 不设置 cascade,因为它们是业务凭证,不能因工单删除而消失——在飞书机器人的 delete_mo 函数中增加了前置校验:

# 如果工单有物料流转记录,拒绝删除
if has_material or has_inbound:
    return '⚠️ 该工单已有物料/入库记录,无法直接删除'

5. 后端架构:三层分离 + 双路由注册

目录结构

spms/
├── app.py           # 应用工厂 create_app()
├── config.py        # 配置:密钥、数据库URI、飞书凭证
├── models/          # 数据模型层(18个模型类)
├── blueprints/      # 路由控制器层(7个Blueprint, 58条路由)
├── services/        # 服务层(飞书API客户端)
├── templates/       # Jinja2模板(33个HTML文件)
└── static/          # 静态资源(CDN为主)

应用工厂模式

app.py 不直接创建 Flask 实例,而是通过 create_app() 工厂函数:

def create_app():
    app = Flask(__name__)
    app.config.from_object(Config)    # 注入配置
    db.init_app(app)                  # 绑定数据库

    # 注册7个Blueprint,每个注册两次
    app.register_blueprint(sales_bp, url_prefix='/sales')
    app.register_blueprint(sales_bp, url_prefix='/api/sales', name='sales_api')

    # 上下文处理器:注入侧边栏导航到所有模板
    @app.context_processor
    def inject_modules():
        return {'modules': [...]}

    return app

为什么要工厂模式? 因为在飞书机器人进程中也要手动创建 app context 来操作数据库:

from app import create_app
app = create_app()
with app.app_context():
    orders = SalesOrder.query.all()

双路由注册

每个 Blueprint 注册两次——直接的路径前缀和 /api 前缀。这是为了兼容隧道代理部署场景(如 ngrok),外部 URL 可能是 https://xxx.ngrok.io/api/sales/,而内部直接访问是 /sales/

模型层组织方式

models/__init__.py 创建 db = SQLAlchemy() 实例,然后导入所有模型确保注册。这是 SQLAlchemy 的经典模式——模型需要在导入时被 SQLAlchemy 元数据"发现"。


6. 前端实现:一个 Bootstrap 打天下

技术策略

零构建工具、零静态文件。 所有 CSS/JS 通过 CDN 引入,页面样式内联在模板中。

  • Bootstrap 5.3.3 CDN:布局 + 组件
  • Bootstrap Icons 1.11.3:图标
  • Chart.js 4.4.0:仪表盘图表
  • 内联 <style>:自定义侧边栏、状态徽章、统计卡片

布局设计

┌──────────┬──────────────────────────┐
│          │   Topbar (面包屑 + 用户)    │
│ Sidebar  ├──────────────────────────┤
│ (240px)  │                          │
│          │   Page Content           │
│  仪表盘   │   (Bootstrap Grid + Card) │
│  销售订货  │                          │
│  生产计划  │                          │
│  物料管控  │                          │
│  销售发货  │                          │
│  系统管理  │                          │
│  飞书集成  │                          │
└──────────┴──────────────────────────┘

侧边栏使用 position: fixed 实现,深蓝色渐变背景。通过 context_processor 注入 modules 列表,模板中循环渲染导航项,根据 request.path 判断高亮。

仪表盘实现

用四个彩色统计卡片 + 两个 Chart.js 图表构成首屏:

  • 统计卡片:订单总数 / 在产工单 / 待发货单 / 库存预警
  • 柱状图:订单状态分布(订单用不同颜色区分状态)
  • 环形图:工单状态分布

模板继承

base.html 定义了完整的骨架(sidebar + topbar + page-content),每个模块的页面只需:

{% extends "base.html" %}
{% block page_title %}销售订单列表{% endblock %}
{% block breadcrumb %}<li class="breadcrumb-item active">销售订货</li>{% endblock %}
{% block content %}
  <!-- 页面内容 -->
{% endblock %}

7. 飞书集成:从回调到双机器人的演进

这部分是整个项目技术复杂度最高的模块。详细实现可参考飞书机器人接入博客,这里聚焦于演进过程。

阶段一:Webhook 回调

最初按飞书文档实现标准的事件回调——在飞书后台配置回调 URL,Flask 运行在公网可访问的地址上。写了一整套:

  • /feishu/event 端点:URL 验证 + 消息事件处理
  • /feishu/login + /feishu/callback:OAuth 登录
  • /feishu/settings:管理页面

问题:内网环境无法被飞书服务器回调。虽然有 ngrok 等隧道方案,但不稳定。

阶段二:WebSocket 长连接

发现 lark-oapi SDK 支持 WebSocket 长连接模式——不需要公网 URL,客户端主动连接飞书 WS 服务器订阅事件。于是写了 feishu_ws.py

def start_ws_client():
    handler = EventDispatcherHandlerBuilder(
        FEISHU_APP_ID, FEISHU_APP_SECRET
    )
    handler.register_event('im.message.receive_v1', handle_message)
    ws_client = handler.build()
    ws_client.start()  # 阻塞方法,在新线程中运行

SDK 自动管理 token 刷新、重连、心跳,开发者只需关注 handle_message 函数。

阶段三:双机器人分离

单一机器人承载了销售和生产两套命令,消息路由日益复杂。最终拆分为两个独立飞书应用:

机器人 App ID 文件 职责
销售通用 cli_aaba20... feishu_ws.py 订单查询/创建/修改/删除、库存预警
生产专用 cli_aab3fbe... feishu_ws_production.py 工单 CRUD、排产/报工/完工/结案、日报/预警

两个机器人各跑独立进程,互不干扰。

消息路由

消息处理器(核心是文本匹配 + 正则提取):

def handle_message(client, event):
    text = event.get('text', '')
    text_clean = text.replace('@_all', '').strip()

    if '新建工单' in text_clean:
        reply = create_mo(text)           # 解析多行键值对
    elif '报工' in text_clean:
        reply = report_progress(text)     # 正则提取工单号+数量
    elif '生产日报' in text_clean:
        reply = daily_report()
    # ... 更多路由

多行命令解析 是生产机器人的亮点——支持这样的输入:

新建工单
编码: P005
名称: 测试外壳
部门: 第一车间
数量: 500
开工: 2025-07-01
完工: 2025-07-15
订单: SO202406001

用正则按行提取字段名和值,支持中英文冒号、多种日期格式:

pattern = re.compile(r'(编码|名称|部门|数量|开工|完工|状态|...)[::]\s*(.+)')

8. 进程管理:一键启动的调度器

问题

系统由 3 个独立进程组成,手动启停非常繁琐:

  1. Flask Web 服务(TCP 5000)
  2. 飞书销售机器人(WebSocket 长连接)
  3. 飞书生产机器人(WebSocket 长连接)

解决方案:start.py

一个 70 行的进程管理器,使用 subprocess.Popen 启动所有子进程:

PROCESSES = [
    ('Flask',              ['python', 'run_flask.py']),
    ('飞书-销售机器人',    ['python', 'feishu_ws.py']),
    ('飞书-生产机器人',    ['python', 'feishu_ws_production.py']),
]

for name, cmd in PROCESSES:
    proc = subprocess.Popen(cmd, stdout=PIPE, stderr=STDOUT)
    processes.append((name, proc))

关键特性

  • 命令行参数控制python start.py --no-flask 只启飞书机器人
  • 输出转发:每个子进程的 stdout 由独立线程转发到主控制台
  • 进程监控:守护线程每 3 秒检查子进程是否存活
  • 优雅关闭:注册 SIGINT/SIGTERM,先 terminate 后 kill
signal.signal(signal.SIGINT, graceful_shutdown)
signal.signal(signal.SIGTERM, graceful_shutdown)

9. 踩坑实录:5 个值得记录的教训

坑一:飞书长连接阻塞 Flask 主线程

ws_client.start() 是阻塞方法。如果在 app.py__main__ 中直接调用,Flask 永远不会开始监听端口。

解决:扔进 daemon=True 的后台线程。但后来发现线程中的异常难以追踪,最终改用独立进程 start.py 管理。

坑二:card.action.trigger 不支持 WebSocket

想给生产机器人加卡片按钮交互(点击「开始生产」),但在飞书控制台搜不到事件。查文档才发现 card.action.trigger 只支持 Webhook 推送,不支持 WebSocket 长连接模式。

解决:放弃卡片交互,用纯文本命令覆盖所有场景。

坑三:@机器人消息包含 @_all 标记

飞书群聊中 @机器人,消息文本会被插入 @_all@_user_1 等标记,干扰文本匹配。

解决:消息处理的第一步就是正则清洗:

text_clean = re.sub(r'@\S+', '', text).strip()

坑四:Windows 控制台 emoji 编码

飞书机器人回复大量使用了 emoji(🔧 ✅ ⚠️ 等),在 Windows CMD 下全部乱码。但这不是功能 bug——emoji 在飞书消息和网页中正常显示,只是控制台日志乱码。

解决:不修复。日志改用文字标识即可,emoji 只在飞书消息中使用。

坑五:数据库上下文隔离

两个飞书机器人进程各自用 from app import create_app 创建独立的 Flask 实例和 app context。如果不在 with app.app_context(): 中操作数据库,会报 RuntimeError: No application found

最佳实践:所有数据库操作封装在函数中,在 feishu_ws.py 的入口创建一次 app context,内部函数直接用 db.session


10. 总结与展望

成果

  • 18 张数据表,58 条 Web 路由:覆盖销-产-存-发全链路
  • 双飞书机器人:支持 20+ 条自然语言命令(订单 CRUD、工单 CRUD、排产、报工、日报、预警)
  • 一键启动python start.py 拉起全部服务
  • 代码量:约 3500 行 Python + 3000 行 HTML

可改进的方向

  1. 用户认证:目前硬编码 created_by='sales01',缺乏真正的登录鉴权
  2. 飞书卡片交互:如果条件允许部署公网服务,启用 card.action.trigger 的 Webhook 模式
  3. 数据可视化增强:更多维度的生产看板(OEE、产线负载、良品率)
  4. 消息通知:订单状态变更、工单延期时主动推送飞书消息
  5. 移动端适配:车间操作人员在手机上访问 Web 界面的体验优化

这个项目的 GitHub 仓库暂未公开,但如果你正在做类似的制造管理系统,希望这篇记录能给你一些参考。从 SQLite 文件出发,到飞书群聊里说一句"工单"就能看到排产情况——这个过程比想象中简单,也比想象中更折腾。

posted on 2026-06-30 23:06  不耻  阅读(28)  评论(0)    收藏  举报

导航