用Django实战开发小区自动售水系统:从需求到部署

发布时间:2026/9/8 1:05:56
用Django实战开发小区自动售水系统:从需求到部署 小区里那台饮水机排队投币的场景大家应该都不陌生——口袋里翻不出硬币、纸币塞进去卡币、水都接完了才发现扣了两次钱。物业找了好几家公司询价成品设备贵的离谱定制方案更是不用想。后来就有人提议为什么不自己用 Python 和 Django 写一套刷卡/扫码的自动售水系统这个项目说到底并不复杂但麻雀虽小五脏俱全涉及用户账户、在线充值、订单流水、设备状态上报还有最核心的钱水对账。这篇文章就用我实际开发这套小区饮水机自动售水系统的经验把从需求拆解到表结构设计再到设备交互和部署上线的完整过程讲清楚。如果你正准备接类似的物联网支付项目或者是想用 Django 做点脱离 CRUD 的真实业务系统那这篇应该能帮你省不少时间。1. 为什么一套卖水的系统比想象中难需求拆解与核心模型设计1.1 售水系统的本质是交易系统不是库存系统接到这个需求的第一反应可能觉得这就是一个简单的用户付钱、设备出水的逻辑。但真正动手之后才意识到售水系统的难点不在出水的瞬间而在钱和水怎么对得上账。传统投币机器不存在这个问题硬币投进去控制器收到信号就放水一切都是物理层面闭环。但一旦接入互联网用户在线充值、扫码启动设备、按流量计费整个链路就变成了分布式交易用户在手机上支付平台记录扣款设备执行出水上报用量。这三者之间只要有一步出现延迟或者失败就一定会出现用户扣了钱但没出水或者水出了但没扣到钱的情况。所以整个系统的核心不是喝水功能而是交易一致性。这也是为什么我做完需求调研之后第一时间没有写代码而是花了三天时间把流程中每一个异常场景都列了出来。这套系统我拆成了五个核心子模块用户端注册、登录、充值、扫码取水、消费记录查询管理端设备管理、价格策略、订单查询、退款处理设备端状态上报、验证取水码、出水执行、用量回传支付模块微信支付/支付宝支付的接入与回调处理对账模块订单、流水、设备上报记录之间的核对机制1.2 表结构设计用四张核心表撑起整个业务闭环Django 开发最舒服的一点是 ORM 写起来很顺手但 ORM 顺手不代表表结构可以随便拍脑袋。尤其是这种涉及资金的项目表结构一旦设计失误后面改起来会非常痛苦因为存量数据不能随意删。这套系统的核心表我最终定了四张外加若干辅助表第一张是User 表Django 自带 auth.User 扩展。它不直接存余额因为用户的基本信息和资金数据放一起会让并发写锁问题非常集中。我通过 OneToOne 关系关联了一张 UserProfile里面放手机号、昵称、头像、注册时间、最后一次登录时间这些相对静态的信息。第二张是Wallet 表。这张表只干一件事就是存用户的钱包余额。字段极其精简user、balanceDecimalField、updated_at。注意这里有个关键点我故意不让它和其他表做外键级联更新就是为了独立控制资金变更的并发锁。第三张是Transaction 流水表。这张表是整个系统的账本每一笔钱的变化都要在这里留痕。字段包括流水号、用户、关联订单、交易类型充值/消费/退款/赠送、变动金额、变动前余额、变动后余额、状态、创建时间。这条表的价值在出问题的时候才会体现出来——运营人员要查某笔钱去了哪里只能靠流水表去追溯。第四张是Order 订单表。这是业务层面的操作记录一个订单可能对应多个流水比如一次消费扣款一笔、退款一笔。字段包含订单号、用户、设备、下单时间、开始时间、结束时间、预扣金额、实际金额、流量计量、订单状态。辅助表包括 Device 设备表、DeviceHeartbeat 心跳表、PriceConfig 价格策略表、RechargeRecord 充值记录表。1.3 为什么要区分订单和流水避免一笔账掰扯不清很多人会问订单表里直接加一个 amount 字段记录金额再放一个 status 字段记录状态不就行了为什么非要单独搞一张流水表我举个例子你就明白了。用户账户原来有 50 元充值 100 元然后去取水扣了 3.5 元后来发现那一次出水异常又退款 3.5 元。如果是单表记录最终余额是 150 元。看起来没错但这 150 元是由哪几笔操作构成的就说不清了。一旦用户投诉说我感觉余额不对运营人员根本没法回答这个问题。有了流水表之后所有金额变动都是一条条可审计的记录每个订单、每次充值、每笔退款都对应明确的时间戳和前后余额快照。这个设计思路在做资金类项目时一定要从一开始就落实项目上线再补账本数据早就烂了。2. 从零搭建 Django 项目版本选型、模块规划与配置要点2.1 为什么选 Python 3.10 Django 4.2 LTS技术选型阶段很多人会纠结是跟新版还是用稳的。我实测下来的结论是LTS 版本永远是最优解除非你有非用不可的新特性。我选的是 Django 4.2 LTS 搭配 Python 3.10这套组合的稳定性在社区经过大量项目验证网上能搜到的问题解决方案也最全。Python 3.12 虽然性能更好但第三方库的兼容性和踩坑成本不值得在这种业务系统上冒险。Django 4.2 相比之前的版本有几个对项目友好的变化支持CreateView等类视图里更精细的 post 处理流程、ORM 层对数据库约束的表达力更强、自带的Admin界面变得更现代。但对于本项目来说最核心的还是它作为一个成熟框架自带的认证、路由、ORM 和 Admin 体系能节约将近一半的开发时间。环境配置方面我在开发机上用的是虚拟环境管理的标准做法。创建项目和应用的命令如下python3.10 -m venv venv source venv/bin/activate pip install django4.2.* djangorestframework django-cors-headers requests django-admin startproject water_system cd water_system python manage.py startapp users python manage.py startapp wallet python manage.py startapp orders python manage.py startapp devices这里多说一句Django 3.2 之前默认用url写路由从 3.1 开始推荐path和re_path。在 4.2 里直接用path写就够了完全不需要正则适配简洁得多。2.2 应用模块划分的边界原则很多新手会把所有模型都扔进一个 app 里项目文件动辄上千行后续维护成本极高。我在这个项目里的划分原则很简单就是按业务域拆分不允许跨 app 直接操作对方的 models。五个 app 各司其职users负责注册、登录、个人信息wallet钱包余额和资金流水的读写orders下单、订单状态流转、取水码生成devices设备信息管理、心跳日志、设备指令下发payment对接第三方支付、处理回调app 之间通过服务层service.py通信。比如订单完成后要扣钱包余额orders 里的 service 会调用 wallet 里的一个deduct_balance(user_id, amount, order_id)函数而不是直接操作 Transaction 模型。这样才能保证资金变动逻辑集中在 wallet 内部不至于在多个地方各写一套扣款逻辑。2.3 settings 配置里最容易踩的坑Django 项目的 settings.py 是所有新手最容易犯错的地方而且错误通常不在写代码的时候暴露而是在部署的时候爆发。第一个坑是 DEBUG。上线前忘记把 DEBUG 设置为 False 会导致任何异常都直接把堆栈信息抛给浏览器等于把你的项目目录结构、Python 版本、数据库类型全暴露了。我在部署脚本里加了一条检测如果DEBUGTrue就拒绝启动 Gunicorn。第二个坑是 ALLOWED_HOSTS。配成[*]在某些安全要求高的环境下过不了审。实际项目中建议明确写入域名和服务器 IP。第三个坑是 CORS 配置。用户端如果做成 H5 页面或者小程序跨域请求是必然的。我用的是django-cors-headers需要在 INSTALLED_APPS 和 MIDDLEWARE 里各加一项INSTALLED_APPS [ # ... corsheaders, ] MIDDLEWARE [ corsheaders.middleware.CorsMiddleware, # 注意这个中间件要放在 CommonMiddleware 之前 ] CORS_ALLOWED_ORIGINS [ https://your-domain.com, ]第四个坑是时区问题。这几乎是所有涉及交易的项目都会踩的。系统默认TIME_ZONE UTC如果你不显式改成Asia/Shanghai那么所有带时间的功能——订单创建时间、流水时间、设备心跳时间——都会和用户实际感受到的时间差 8 个小时。这不是小事对账的时候差 8 小时可能就把一天的订单算到另一天去了。所以我在项目的配置里直接写死TIME_ZONE Asia/Shanghai USE_TZ FalseUSE_TZ False意味着数据库里存的时间就是本地时间不搞 UTC 转换。这个方案在纯国内项目里完全没问题查日志、对账的时候很直观不用每次都在脑子里做时区换算。3. 核心功能逐项落地从用户注册到扫码出水的完整链路3.1 用户注册登录用 DRF 写接口比函数视图高效太多用户端接口我用的是 Django REST FrameworkDRF不是 CBV 是 FBV 的问题而是 DRF 自带序列化器、认证类和限流组件省去大量重复工作。注册流程我设计的是手机号验证码模式考虑到小区居民的使用场景没必要上复杂的邮箱密码体系。逻辑链路是这样的用户输入手机号后端生成 6 位随机验证码存到缓存Redis设置 5 分钟过期调用短信服务商接口下发验证码用户收到后回填后端校验通过即注册成功并返回 token。这里有一个很关键的细节验证码接口一定要做频率限制否则被恶意脚本一刷短信账单直接爆掉。我用 DRF 自带的AnonRateThrottle做了简单限制每分钟同一个手机号最多请求一次每天最多 10 次。这个限制对正常用户完全无感知但能挡住绝大多数恶意行为。登录后 token 我用的是rest_framework_simplejwt的 JWT 方案。对比 Django 自带的 Session 方案JWT 对移动端和 H5 更友好服务端不用存储会话状态。不过 JWT 有一个天然缺陷服务端无法主动让 token 失效。所以我额外加了一层用户状态校验——如果用户被管理员禁用哪怕 token 仍然有效接口也会拒绝服务。3.2 充值流程与支付回调最容易出 bug 的资金入口充值流程是最需要小心的地方。用户点击充值 50 元前端向后端请求创建一个充值订单后端返回一个支付链接。用户在这个第三方支付页面完成付款后支付平台会向我们的后端发送一个异步通知webhook我们收到通知才能确认这笔钱到账了。这个异步通知的处理有几个铁律第一回调地址必须是外网可访问的 POST 接口不能是 GET。有些支付平台支持同步跳转return_url和异步通知notify_url两种但同步跳转的可靠性远低于异步通知绝不能把同步跳转作为入账依据。第二回调处理要幂等。支付平台可能因为网络抖动重复发送回调通知。如果每次收到回调都执行一次余额增加操作用户充 50 元可能到账 100 元甚至更多。我的处理方式是以一个唯一字段支付平台交易号作为幂等键处理前先查询这个交易号是否已经存在存在就直接返回成功不再重复入账。第三校验金额。回调里会带一个支付金额字段但这个字段不能完全信任必须和本地创建的充值订单金额做比对。如果回调金额小于订单金额直接拒绝入账并记录异常日志防止改金额攻击。充值成功之后的入账操作用的是事务from django.db import transaction def recharge_success(user_id, amount, pay_trade_no, order_id): with transaction.atomic(): wallet Wallet.objects.select_for_update().get(user_iduser_id) old_balance wallet.balance wallet.balance amount wallet.save() Transaction.objects.create( user_iduser_id, trade_nopay_trade_no, order_idorder_id, tx_typerecharge, amountamount, balance_beforeold_balance, balance_afterwallet.balance, )注意select_for_update()这个函数它会对这一行数据加锁防止并发情况下两个人同时充值、同时读写余额导致丢失更新。后面讲并发扣款的时候还会再提到它。3.3 扫码取水的核心流程一个订单三段状态用户扫码取水的完整流程是这个系统最核心的使用场景我把它拆成三个步骤第一步创建订单。用户扫设备上的二维码二维码里实际上就是一个设备编号或者 URL携带设备 ID。用户点击开始取水按钮后端创建一个状态为PENDING的订单同时预扣一部分金额比如预扣 2 元作为押金返回给前端一个取水码。第二步设备验证。设备端轮询平台或者接收平台指令后带着取水码来请求验证。平台验证取水码有效、订单状态为PENDING、设备编号匹配就返回一个允许出水的指令。第三步出水与结算。设备出水的过程中会实时计算流量。用户手动点击停止或者达到预设金额后设备自动停止出水设备把实际用水量上报平台。平台根据单价计算实际费用多退少补更新订单状态为FINISHED并把实际金额写入流水。这套流程看起来简单但每一个步骤都可能出问题。比如用户创建订单后没有去设备端操作这个预扣的金额什么时候退设备迟迟不验证取水码订单卡在 PENDING 状态怎么办超时机制是必须的。我设定的规则是订单创建后 15 分钟内设备未验证订单自动取消并退回预扣金额。这个超时任务放在 Django 的定时任务里处理每 10 分钟扫描一次。3.4 订单状态机把每个可能的状态转移都显式定义出来状态机是整个订单模块的灵魂。我最早写这个系统的时候图省事直接用 if else 判断当前状态结果出过不止一次从已结束的订单又被改成进行中这种刷新三观的事情。后来老老实实把状态转移表画出来用代码强制约束才彻底解决了这个问题。订单状态我定义为PENDING已创建等待设备验证取水码PROCESSING设备已验证正在出水FINISHED正常结束费用已结算CANCELLED超时未使用订单取消预扣金额已退回ABNORMAL设备上报异常订单挂起等待人工处理REFUNDED异常订单退款完成用代码进行转换约束的核心逻辑如下class Order(models.Model): STATUS_CHOICES [ (PENDING, 等待设备确认), (PROCESSING, 出水进行中), (FINISHED, 已完成), (CANCELLED, 已取消), (ABNORMAL, 异常挂起), (REFUNDED, 已退款), ] # 允许的状态转移表 TRANSITIONS { PENDING: {PROCESSING, CANCELLED}, PROCESSING: {FINISHED, ABNORMAL}, ABNORMAL: {REFUNDED, FINISHED}, } def transition_to(self, new_status): if new_status not in self.TRANSITIONS[self.status]: raise InvalidTransitionError(f{self.status} - {new_status} 是不允许的) self.status new_status self.save(update_fields[status, updated_at])这样有一个巨大好处所有修改订单状态的代码都要走transition_to方法非法跳转会在第一时间被拦截而不是让脏数据悄悄落库。3.5 管理后台Django Admin 在运营场景里的价值被低估了Django Admin 常被看作开发期调试工具但在这种内部运营系统里它其实可以直接充当管理后台省掉一整套管理端页面的开发成本。我配置了三个核心 list_display 页面第一个是订单管理页。展示订单号、用户手机号、设备编号、金额、状态、时间。加了几个自定义 filter按状态筛选、按日期范围筛选。运营人员查投诉的时候输入手机号就能快速看到该用户最近的所有订单效率比直接查数据库高很多。第二个是流水管理页。只读模式不允许编辑。因为流水是账本绝不能通过后台去改。Django Admin 里我把流水相关字段都设成readonly_fields并且没有给这个模型注册任何编辑权限。第三个是设备管理页。每个设备的基本信息、当前状态在线/离线/维修中、累计出水量、最近心跳时间都显示在列表页。运营人员在这里可以对设备进行启停操作方便维护时临时禁用某台机器。Django Admin 在这个项目里至少省了我两周开发时间而且它自带的搜索、筛选、分页功能都足够稳定。如果后期运营需求变多再补一个基于 Django Admin 深度定制的管理界面也完全来得及。4. 设备端交互设计心跳、指令与对账机制是项目的分水岭4.1 设备端与服务器通信轮询比长连接更适合售水机场景物联网设备和服务器的通信方式通常有两种长连接MQTT/WebSocket和短轮询HTTP。做售水机这个场景我最终选了短轮询理由很现实。售水机的运行环境是小区户外或者楼道间网络状况普遍一般而且设备端的 MCU 或者工控板性能不算强。MQTT 这类长连接需要维持一个常驻的连接会话一旦网络抖动就必须处理断线重连、心跳保活、消息 QoS 等一系列问题设备端编程成本直接上一个档次。HTTP 轮询就简单粗暴得多设备每 5 秒请求一次平台的接口获取当前有没有需要执行的指令。没有就返回空有就返回指令内容。这种方式网络开销略高但 5 秒一次也只有 0.2 次每秒对服务器压力完全可以忽略。设备接口我设计了四个最核心的POST /api/device/heartbeat设备上报心跳和运行状态GET /api/device/poll设备获取待执行指令POST /api/device/order/validate设备验证取水码POST /api/device/order/report设备上报出水用量4.2 心跳包协议设计一次上报的数据远不只是我还活着心跳包是设备端最频繁调用的接口它的信息含量被我做得远超一般认知中的 ping-pong。一次完整的心跳请求会携带设备编号device_no、运行状态1 正常2 故障、当前水价版本号、累计出水量用于防篡改对账。每次心跳到达服务器我会做三件事第一更新 DevDevice 表的last_heartbeat_at字段。这是一个很关键的运营指标通过定时扫描last_heartbeat_at超过 5 分钟的设备就能快速发现离线设备。第二对比设备上报的累计出水量与服务端记录的累计出水量如果差异超过阈值触发对账告警。这个设计能发现设备控制器被拆开篡改的场景虽然不多见但小区设备确实有人动过歪脑筋。第三检查指令池里是否有待下发的指令如果有就返回。所以这个心跳接口实际上同时承担了上行状态上报和下行指令获取两种职责。心跳接口返回的数据结构大致是这样{ code: 0, data: { cmd: order_start, order_no: WS202405121530001234, preset_amount: 2.00, expire_at: 2024-05-12 15:45:00 } }4.3 扣费与出水的时序问题先押金后结算还是先实扣前面提到取水前先预扣 2 元这里详细说一下为什么这么设计。如果完全不做预扣用户下单直接出水结束后再结算那用户余额为 0 甚至负数的时候怎么办如果先冻结足够一次出水的钱比如按最大出水量算预授权金额再允许设备出水就能保证钱一定够付。这和酒店入住刷预授权的逻辑一模一样。预扣的余额其实不是真正扣走而是从可用余额变成冻结余额。Wallet 表里我用两个字段区分available_balance和frozen_balance。用户下单时资金从 available 转入 frozen订单正常结束后按实际用量扣减 frozen 并解冻剩余部分订单超时取消时frozen 全额退回 available。这种冻结-实扣-解冻的模式比先全额扣款再退款体验好得多用户不用等退款到账后台也不用频繁发起退款操作。唯一要注意的是冻结余额的累计金额可能对运营资金造成一定占用但小区售水机单笔金额很小影响可以忽略。4.4 异常场景处理设备断网、断电、低水位这个系统最常出问题的时间段反而不是使用高峰而是设备维护和网络波动的时期。我整理一下我遇到过的异常场景以及各自的处理方式设备断网。用户在设备端已经验证了取水码正在出水突然设备断网。设备端本地记录当前用水量等网络恢复后补报用量。服务器端会把订单保持在 PROCESSING 状态超过一定时间后标记为 ABNORMAL等待补报结果。设备断电。这种情况最麻烦因为设备断电瞬间可能没来得及保存最新的用量数据。我在设备端存储模块上加了一个小容量掉电保持存储区每累计 50ml 就写一次断电恢复后从最近一次记录开始继续计算尽量减少误差。低水位。售水机的水箱或净水模块可能因为滤芯到期、原水不足等原因无法正常工作。设备上报状态为故障时平台会把对应设备的二维码标记为不可用用户扫码会直接提示该设备维护中请使用附近其他设备。争议订单的人工处理。即使做了这么多防护仍然会有个别订单两边对不上。对于这些订单我设计的原则是疑罪从无偏向用户——只要误差在合理范围比如 0.1 元以内按用户少扣款处理差额由运营方承担。为了几毛钱和用户争论品牌口碑的损失远大于那点水费。5. 部署上线与运维经验这套系统交付后真正考验才开始5.1 服务器部署方案Gunicorn Nginx Supervisor部署这块我用的是一套很务实也足够成熟的方案Nginx 负责静态文件和反向代理Gunicorn 负责运行 Django 应用Supervisor 负责守护进程。这套组合在实际项目中经受住了很大的流量考验完全够用。Gunicorn 的启动参数有几个值得注意的地方gunicorn water_system.wsgi:application \ --bind 127.0.0.1:8000 \ --workers 4 \ --threads 2 \ --timeout 60 \ --access-logfile /var/log/water_system/access.log \ --error-logfile /var/log/water_system/error.logworker 数量不是越多越好。Gunicorn 的官方建议是CPU核心数 * 2 1一台 2C4G 的服务器配 4 个 worker 是合理的选择。worker 太多反而会因为频繁上下文切换降低吞吐量实测 4 worker 能支撑大约 200 QPS对售水机这种低频请求的业务完全够。数据库我用的是 MySQL 8.0配在 Django 的 settings 里唯一需要注意的是把CONN_MAX_AGE设置为一个正数比如 60 秒避免每次请求都重新建立数据库连接否则在高并发下会连不上数据库。5.2 并发扣款的隐藏 Bug同一用户同时扫码两台设备这个坑我在测试阶段偶然踩出来的。设计的时候默认一个用户同一时间只会在一台设备上取水但实际用户完全可能扫码一台设备后又跑去另一台设备扫码。如果后端没做限制就会出现同一个人在两台设备上同时出水但余额只够付一台的情况。我加了一个很简单的规则创建订单前检查该用户是否有未完成订单状态为 PENDING 或 PROCESSING有的话直接拦截并提示您有正在进行中的取水订单请先完成后再继续。这个检查必须放在事务里做不然并发请求同时查没有未完成订单就会同时通过。我当时用了一个 Redis 锁来保证同一个 user_id 的请求只会有一个能进入创建订单的逻辑import redis r redis.Redis(...) lock_key fuser_order_lock:{user_id} if not r.set(lock_key, 1, nxTrue, ex5): return error(操作过于频繁请稍后重试) try: # 创建订单的逻辑 ... finally: r.delete(lock_key)Redis 的 SET NX 就是这个用途的——它保证同一时间只有一个请求能拿到锁其他请求直接返回不需要排队等待。5.3 定时任务超时取消、对账巡检、离线告警项目里有几个定时任务我用的方案是 Django-celery-beat但说实话这种体量的任务用 Celery 有点大炮打蚊子。如果只有几个轻量定时任务直接用 Linux 的 crontab 配合 Django manage.py command 就够了。我在项目里建了几个 command。check_pending_orders每 10 分钟扫描一次把超过 15 分钟仍未验证的 PENDING 订单自动取消并解冻预扣金额。reconcile_daily每天凌晨跑一次昨日对账任务拉取前一天的设备上报累计出水量和订单实际用水量核对差异并生成异常报表。offline_device_warning每 5 分钟检查一次设备心跳时间超过 5 分钟没有心跳的设备触发告警通知。通知方式用的是企业微信机器人比较简单直接维护人员收到消息就能及时处理。这些 command 用 crontab 起定时执行代码维护在 Django 项目里部署和迁移都方便*/10 * * * * cd /opt/water_system /opt/venv/bin/python manage.py check_pending_orders /var/log/water_system/cron.log 21 0 1 * * * cd /opt/water_system /opt/venv/bin/python manage.py reconcile_daily /var/log/water_system/cron.log 21 */5 * * * * cd /opt/water_system /opt/venv/bin/python manage.py offline_device_warning /var/log/water_system/cron.log 215.4 日志体系建设不要等出问题才去翻日志我之前做过一个项目日志只打了 print出问题的时候手忙脚乱。这次从第一天就把日志规范建立起来了。核心思路很简单每个关键业务节点都要有对应的结构化日志内容包括时间、用户 ID、订单号、设备号、操作类型、结果码、耗时。我在 Django 的 LOGGING 配置里做了输出格式的定制LOGGING { version: 1, formatters: { standard: { format: %(asctime)s [%(levelname)s] %(name)s: %(message)s }, }, handlers: { file: { level: INFO, class: logging.FileHandler, filename: /var/log/water_system/app.log, }, }, loggers: { water_system: { handlers: [file], level: INFO, }, }, }每个业务模块里都通过logging.getLogger(__name__)获取 logger然后打上对应级别的日志。支付回调这种关键节点必须打 INFO 级别的完整参数设备心跳这种高频调用打 DEBUG 日志只在排查问题时开启。一旦线上出了问题日志配合流水表基本能在十分钟内定位到是哪一环出了问题这比对着代码干瞪眼效率高太多了。5.5 上线后的几件小事给设备换二维码、备份策略、安全加固最后再说几个上线后立刻要处理的小事。设备二维码表面上只是一个链接但这个链接在系统上线后不能再随便换因为印在设备上的二维码是物理存在的换二维码就意味着要重新贴标。所以二维码里最好只放一个设备编号不要拼接时间戳之类的动态参数。具体形式是https://your-domain.com/s/DEV12345设备编号固定这样即使以后系统升级二维码也不用换。数据库备份我用的是 binlog 增量备份加每日全量备份的组合。每天凌晨用 mysqldump 做一次全量备份保留最近 7 天。同时开启 MySQL binlog这样万一需要恢复到某个时间点还能通过 binlog 做增量回放。安全方面重点做了三步所有后台管理页面强制开启两步验证、服务器防火墙只开放 80/443/22 端口、Django 的 SECRET_KEY 通过环境变量注入而不是写在代码仓库里。这三件事成本很低但能挡住绝大多数脚本小子的尝试。这个项目从需求调研到上线运行前后大概花了一个半月。期间最深的体会是任何看起来很简单的线上业务系统真正的复杂度都在异常处理和对账机制里。功能页面写得再花哨如果资金流转的账对不上系统上线就是事故的开始。如果你也在做类似的物联网支付项目建议从第一天就把表结构设计、状态机约束和日志规范这三件事刻在脑子里后面能少熬无数个查问题的夜。