☰
Django高校固定资产管理系统:从零落地的开发实践与踩坑指南
2026/10/18 4:28:19 网站建设 项目流程

每年资产盘点季,高校资产管理员就要开始一场漫长的猫鼠游戏。打印出来的Excel表厚厚一摞,扫码枪扫出来的条码和台账对不上,问谁都说“东西在”,但到现场就是找不到。我做这套基于Django的高校固定资产管理系统,核心目标只有一个:让账实相符成为一件默认会发生的事。系统覆盖了资产入库、领用、调拨、维修、盘点、报废的全流程,也支持权限分级和台账导出。如果你正在为高校或类似的中大型机构规划资产管理系统,或者想用Django把一个典型的业务系统从零落地上线,这篇文章都能给你一条可以直接参考的路径和一堆真实踩坑记录。

1. 高校资产台账的现状:Excel、手工与“账实不符”的恶性循环

1.1 先搞清楚参与的人和他们手里的资产

做系统之前,我习惯先列角色和对象清单。高校固定资产管理系统和普通企业的资产系统看着像,但角色更复杂。高校里涉及资产的人大致有四类:归口管理部门的资产管理岗,负责全校资产的建账、处置审核和年度盘点组织;各二级单位的资产员,负责本部门的领用、报修、报废申请;普通教职工,是资产的实际使用人,但往往只在领用时出现过一次;校领导和财务部门,他们不看明细,只看总账、折旧和闲置情况。

再往下拆是资产对象。房屋和构筑物属于特殊资产,一般不进入日常的领用流程;通用设备包括电脑、打印机、空调;专用设备可能涉及实验仪器、实训设备;还有家具装具、图书文物、无形资产。不同资产的买入方式、折旧年限、处置流程都不一样。比如实验仪器可能需要单独管理维护记录,而家具和图书更新快、单价低,管理颗粒度可以粗一些。

如果一上来就按“统一资产表”建模,后期肯定要返工。我的建议是:先把资产类型和各自的管理规则列成一张矩阵,再抽象公共字段,而不是让数据库一开始就背上几十个专用字段。

1.2 Excel为什么撑不住这些需求

很多高校不是没有资产台账,恰恰相反,台账齐全到让人头皮发麻。问题在于维护方式。一份全校资产总表通常有上万行,每个人手里都有一份“最新修改版”,部门资产员更新了自己那一份,汇总到学校时又覆盖了一部分数据。资产编号靠手工规则编制,年份、部门代码、流水号一旦错位,重号断号很难发现。

盘点时的问题是台账更新和实物核对完全脱节。管理员拿着打印出来的Excel去各楼层扫码,回来后在电子表格里手动标记“已盘到”“盘亏”“待追溯”。这个过程中,任何一个字段被复制粘贴错了,都不会有人察觉。账实相符靠的是人肉责任心和运气,而不是机制。

我接手这个项目前,见过一个比较典型的案例:某个部门搬家后,资产员的Excel表里“使用部门”列没更新,但“存放地点”已经改了。年度盘点时按部门筛选,这批资产全部落在“旧部门”名下,形成了大批盘亏项,实际上资产就在新楼里。系统中的修改没有留痕,没有审批,也没有人知道是什么时候变的。

1.3 系统要解决的四个核心问题

这套Django系统从业务上要解决四个问题:

  • 账实相符:资产从入库开始就有唯一编号,后续每一次状态变化都有记录,盘点结果可以反向校正台账。
  • 过程留痕:领用、调拨、维修、报废都走线上申请和审批,每一步都要知道“谁在什么时候做了什么”。
  • 权责分明:资产管理岗、部门资产员、普通教职工的权限不同。教职工只能看到自己名下的资产,部门资产员只能操作本部门数据。
  • 决策支撑:系统能输出部门资产分布、闲置资产、超期未盘点资产等统计数据,而不是让领导继续看Excel透视图。

业务边界同样重要。我当时和用户明确说明,系统不做财务系统深度对接,不做自动折旧入账,因为高校折旧规则受财务制度约束,需要由财务部门提供参数化规则后,系统只负责按规则计算展示,不替代财务系统。边界划清楚,开发范围才能控制住。

2. 技术与架构选型:为什么是Django + PostgreSQL

2.1 Django在这个场景里赢在哪里

选Django不是因为它最潮,而是因为这类高校业务系统的几个硬指标,Django正好都能满足。

第一是ORM。高校资产系统的数据模型关系非常密集:资产表要关联类别、部门、使用人、供应商,还要跟审批、维修、盘点发生关系。如果用SQL手工堆,后期维护成本很高。Django的ORM在模型变更、迁移管理上都比较省心,配合makemigrations和migrate,团队里新来的开发也能很快把库表更新起来。

第二是自带Admin后台。开发早期我还没有做完整的运营管理界面时,直接用Django Admin就能让资产管理岗先录入测试数据、调整基础字段。它相当于给业务方提供了一个可配置的后台原型,帮我们省了一轮又一轮原型确认的时间。

第三是用户认证和权限模型。高校系统必须区分管理员、资产员、普通教职工的权限。Django自带的Group和Permission模型已经能覆盖多数场景,自定义权限也只需要扩展Meta.permissions。

第四是安全能力。CSRF保护、XSS转义、SQL注入防护都是框架内置策略,至少在合规层面给学校信息中心省了很多解释成本。

2.2 为什么数据库选了PostgreSQL,而不是继续用MySQL

项目数据库最终选了PostgreSQL。主要原因是资产系统里除了结构化表格,还有很多带可变属性的数据:资产附件、审批详情、扫码盘点的临时快照、导入原始行等。PostgreSQL的JSONField天然适合存这类半结构化数据,不用频繁改表。

另一个原因是资产盘点需要处理并发写入。多个盘点小组同时提交盘点结果,对同一批资产做状态更新,PostgreSQL的行锁机制和select_for_update配合得很顺手。MySQL的InnoDB也能做,但PostgreSQL在数据约束、索引方面给开发者的反馈更直观。如果学校已经有成熟的MySQL运维体系,继续用MySQL也没问题,但锁和隔离级别的测试一定要做足。

实际开发中,我用Django内置的django.contrib.postgres提供的数据库函数,排查重复资产编号时写起来很快。后面踩坑部分会专门讲一个和驱动版本有关的例子,这个选型直接影响了那个坑的定性。

2.3 项目目录结构:多应用拆分,而不是一个大而全的app

很多Django新手喜欢把所有模型写进一个models.py,把所有视图塞进一个views.py。这个系统上线下线后还要长期维护,我选择了按业务域拆分成多个应用:

asset_proj/ ├── config/ │ ├── settings/ │ ├── urls.py │ └── wsgi.py ├── apps/ │ ├── asset/ │ │ ├── models.py │ │ ├── services.py │ │ ├── views/ │ │ └── migrations/ │ ├── org/ │ │ ├── models.py # 部门、人员 │ │ └── services.py │ ├── approval/ │ │ ├── models.py # 审批单、审批记录 │ │ └── services.py │ └── report/ │ └── services.py # 统计报表 ├── utils/ │ ├── excel.py │ ├── asset_code.py │ └── qrcode_helper.py ├── templates/ ├── static/ └── requirements/

每个应用只负责自己的业务闭环。资产应用不直接操作审批表,而是调用approval.services.create_approval()。这样后面调整审批逻辑时,不需要动资产模块的代码。导入导出这种通用逻辑放到utils里,避免多个应用重复实现。

3. 数据模型是系统的地基:资产主表、部门树与审批模型的建模全过程

3.1 资产主表:字段宁缺毋滥,但关键字段一个都不能少

资产主表是整个系统的核心,建模时经常陷入一个误区:想把所有资产属性都塞进去。实验仪器要放规格型号,家具要放材质尺寸,软著要放登记证书编号。如果全做字段,表会失控。

我的处理方式是抽出公共字段,特殊属性放到关联的AssetExtra表里,用JSON存储。核心表模型大致是这个样子:

class AssetStatus(models.TextChoices): IN_STOCK = "IN_STOCK", "在库" IN_USE = "IN_USE", "使用中" REPAIRING = "REPAIRING", "维修中" SCRAPPED = "SCRAPPED", "已报废" class Asset(models.Model): asset_code = models.CharField("资产编号", max_length=64, unique=True) name = models.CharField("资产名称", max_length=255) category = models.ForeignKey( "asset.Category", on_delete=models.PROTECT, verbose_name="资产类别" ) department = models.ForeignKey( "org.Department", on_delete=models.PROTECT, verbose_name="使用部门" ) keeper = models.ForeignKey( settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.SET_NULL, verbose_name="使用人", ) status = models.CharField( "状态", max_length=20, choices=AssetStatus.choices, default=AssetStatus.IN_STOCK, ) original_value = models.DecimalField( "原值", max_digits=12, decimal_places=2 ) net_value = models.DecimalField( "净值", max_digits=12, decimal_places=2 ) buy_date = models.DateField("购置日期") location = models.CharField("存放地点", max_length=255, blank=True) source = models.CharField( "来源", max_length=20, choices=SourceType.choices, default=SourceType.PURCHASE, ) extra = models.JSONField("扩展属性", default=dict, blank=True) created_at = models.DateTimeField("建档时间", auto_now_add=True) updated_at = models.DateTimeField("更新时间", auto_now=True)

几个设计决策值得说明一下:

  • asset_code必须是全局唯一索引。高校资产编号跨部门不能重复,否则二维码标签、盘点扫码都会出问题。
  • original_value和net_value用DecimalField,绝对不用FloatField。资产金额可能到几十万,浮点数误差会导致对账差出几块钱,这种错误最不好解释。
  • extra字段存特殊属性。比如实验仪器的“出厂序列号”“功率”,家具的“材质”,软著的“证书编号”。查询需要精确匹配时,配合JSONField的索引和containment操作符使用。
  • keeper允许为空。很多资产是部门公共设备,没有固定使用人,但必须归属到一个部门。

3.2 部门树:用parent自关联加缓存,不上嵌套集合模型

高校部门层级不深,一般就是“学校-学院-系/中心-实验室”这种三到四层。网上很多文章推荐用django-mptt做树形结构。我评估后没有用,原因有两个:

一是mptt对写操作有额外维护成本,每次部门调整都要增量更新左右值,出错后整棵树的查询就废了。二是高校部门变动频繁,合并、更名每年都有,递归自关联配合应用层缓存就足够。

部门模型非常简单:

class Department(models.Model): parent = models.ForeignKey( "self", null=True, blank=True, on_delete=models.CASCADE, related_name="children", ) name = models.CharField("部门名称", max_length=255) code = models.CharField("部门代码", max_length=32, unique=True)

查询某个部门及其下级部门的资产时,我写了一个工具函数先把子树节点ID一次性取出来,放到缓存里,再拼到Django ORM的filter(department_id__in=...)里。部门表数据量小,读取完全够用,复杂度却比mptt低得多。

3.3 用一个审批模型承载所有业务单,而不是做三套流程表

领用、调拨、维修、报废,看起来是四种业务,但审批链路高度相似:申请人提交,部门资产员初审,归口管理岗终审,仓库或资产员执行。如果用四套流程表,光是“查询我发起的待办”“查询待我审批的任务”就要写四遍。

我最终用一个Approval模型承载所有业务单,配合biz_type和biz_id反查具体业务对象:

class Approval(models.Model): biz_type = models.CharField( "业务类型", max_length=20, choices=BizType.choices, ) biz_id = models.PositiveIntegerField("业务单ID") title = models.CharField("审批标题", max_length=255) submitter = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name="+", ) status = models.CharField( "审批状态", max_length=20, choices=ApprovalStatus.choices, default=ApprovalStatus.PENDING, ) current_node = models.CharField("当前审批节点", max_length=50) detail = models.JSONField("审批上下文", default=dict) created_at = models.DateTimeField(auto_now_add=True) class ApprovalRecord(models.Model): approval = models.ForeignKey(Approval, on_delete=models.CASCADE) user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT) action = models.CharField( "动作", max_length=20, choices=ActionType.choices, ) comment = models.TextField("审批意见", blank=True) created_at = models.DateTimeField("操作时间", auto_now_add=True)

Approval.detail里保存业务快照。比如报废审批,提交时就把“报废原因、资产原值、净值、照片附件路径”都写进JSON。这样即使资产后续发生调拨或维修,审批页展示的数据仍然是提交时的现场记录,不会跟着漂变。审批记录只负责存每一步操作,不重复存业务数据。

3.4 用Signal记录资产履历,避免业务逻辑散落各处

资产状态变更要写履历表,这是需求里明确写了“过程留痕”之后最先要落地的部分。如果每个视图里都手动写一段AssetLog.objects.create(...),后面加一个提醒功能很容易漏掉某个入口。我用Django Signal统一处理:

from django.db.models.signals import pre_save from django.dispatch import receiver @receiver(pre_save, sender=Asset) def asset_pre_save(sender, instance, **kwargs): if not instance.pk: return try: old = Asset.objects.get(pk=instance.pk) except Asset.DoesNotExist: return changes = [] for field in ["status", "department", "keeper", "location"]: new_value = getattr(instance, field) old_value = getattr(old, field) if new_value != old_value: changes.append({ "field": field, "old": str(old_value) if old_value is not None else None, "new": str(new_value) if new_value is not None else None, }) if changes: AssetLog.objects.create( asset=instance, changes=changes, )

这个方案虽然牺牲了一点性能,但逻辑集中,排查“资产状态是谁改的”特别方便。资产数量如果达到几十万,批量导入历史数据时我会临时断开这个Signal,导入结束后再重建日志。

4. 核心功能模块的Django实现细节:批量导入、领用、盘点、报废

4.1 Excel批量导入:第一步是清洗,不是insert

历史数据都在Excel里,系统上线必然要做批量导入。但资产管理员手里的Excel长期人工维护,格式问题非常多:资产编号带空格、金额列被格式化成文本、日期列有的显示成2019/1/1有的显示成2019-01-01、同一个部门有两种写法。我当时写了一个导入服务,第一遍先做格式清洗,第二遍才入库。

用openpyxl读取Excel,逐行校验,收集错误后一次性返回给前端:

from openpyxl import load_workbook from django.db import transaction def import_assets_from_excel(file): workbook = load_workbook(file) sheet = workbook.active errors = [] success_count = 0 for row_idx, row in enumerate( sheet.iter_rows(min_row=2, values_only=True), start=2 ): asset_code = str(row[0] or "").strip() name = str(row[1] or "").strip() category_name = str(row[2] or "").strip() ... if not asset_code: errors.append(f"第{row_idx}行:资产编号为空") continue if Asset.objects.filter(asset_code=asset_code).exists(): errors.append(f"第{row_idx}行:资产编号重复 {asset_code}") continue try: with transaction.atomic(): Asset.objects.create(...) success_count += 1 except Exception as exc: errors.append(f"第{row_idx}行:{exc}") return {"success": success_count, "errors": errors}

导入页面要支持下载模板,模板字段和系统模型一一对应。上传后先展示错误列表,而不是直接提示“导入完成”。这个体验细节很重要,资产管理员需要知道到底哪些行没有进来,以及为什么。

实战中还要处理几个隐形问题:Excel里的数字科学计数法,资产编号如果是18位以上,Excel默认会显示成科学计数法;身份证长度类似的问题会让编号精度丢失。所以我给导入模板加了说明,要求资产编号列必须设置为文本格式。服务端读取后统一做str(...).strip(),再判断是否满足编码正则。

4.2 领用与调拨:事务保护加状态机校验

领用和调拨的共同点是资产状态变化。领用是把在库资产变成使用中,调拨是把使用部门或使用人变更。这两类操作如果不加控制,就会出现“两个人同时申请领用同一台打印机”的尴尬。

状态机约束放在模型层的services.py里,而不是只靠视图里几行if:

from django.db import transaction from django.db.models import F def apply_for_asset(asset, applicant, target_department, target_keeper): with transaction.atomic(): locked_asset = Asset.objects.select_for_update().get(pk=asset.pk) if locked_asset.status != AssetStatus.IN_STOCK: raise ValueError(f"资产当前状态为{locked_asset.status},不可领用") locked_asset.status = AssetStatus.IN_USE locked_asset.department = target_department locked_asset.keeper = target_keeper locked_asset.save() AssetAssignment.objects.create( asset=asset, from_department=asset.department, to_department=target_department, operator=applicant, )

select_for_update()会把该行锁住,直到事务提交或回滚。两个并发请求同时进来,后一个会等待前一个释放锁,再重新读取状态,此时看到的已经是“使用中”,校验就会失败。这个机制比单纯依赖前端按钮禁用可靠得多。

调拨和领用的审批通过后要执行的就是这段服务函数。审批流和实际操作要分开:审批是流程,执行是服务。流程走完但执行失败时,审批记录里要能查到失败原因,系统要能支持作废重提。

4.3 盘点:任务、扫码与差异处理

盘点是资产系统里最容易写复杂的功能。我的方案分三层:

  • InventoryTask:一次盘点任务,包含盘点范围、负责人、开始时间、截止时间。
  • InventoryItem:任务下每个具体资产的一条盘点记录,保存扫码时间、盘点结果。
  • InventoryIssue:差异处理单,记录盘盈、盘亏、待追溯,以及后续的修正动作。

盘点任务生成时,可以从资产台账批量生成InventoryItem,状态为“待盘”。盘点员用手机扫码后,系统按资产编号查到这个盘点项,进入确认界面。资产实际编号和台账一致,勾选“已盘到”提交;不一致则进入差异流程。

差异处理是整个盘点的核心。实际做法是:

  • 实物有但台账没有,记为盘盈,走资产补录审核;
  • 台账有但实物没有,记为盘亏,进入调查确认流程;
  • 实物编号和台账不一致,记为待追溯,需要人工核对。

这些差异项不能直接修改资产表,必须由资产管理岗审核后,通过“盘点结果确认”按钮统一更新台账。这样一个盘点周期结束后,系统能输出差异报告,给管理层看哪些部门盘亏比例高,哪些资产长期处于失管状态。

手机上扫码我优先选了浏览器方案,用getUserMedia调用摄像头,识别条码后自动填充输入框并触发确认。没有做成原生App,因为高校很难统一要求教职工安装App,浏览器方案部署成本和培训成本都低。

4.4 维修与报废:业务闭环比页面复杂

维修工单不是简单记一条“坏了找人修”就行。从资产台账角度看,维修意味着资产状态要变成“维修中”,期间不能再被调拨或处置。维修完成后,状态回到可用的在库或使用中,同时把维修费用、维修厂商、故障原因写入记录。

报废处置要更谨慎。报废申请通过后,资产状态变为“已报废”,但资产本身还应该留在系统中,不能直接删除。很多高校资产在报废后还要走实物处置流程,包括拆解、回收、备案。所以我在资产表里保留SCRAPPED状态,而不是删记录,报废单里关联处置时间、处置方式、接收单位等信息。

维修和报废都涉及图片附件,我统一把附件目录按media/asset/{asset_code}/{yyyy}/{mm}/组织。上传时校验文件名,改名为时间戳加扩展名,避免中文文件名在部分浏览器下载时乱码。

4.5 二维码标签生成与打印中的细节

资产实物标签怎么打,直接决定盘点工作量。我选了二维码加资产编号文本双排版:中间一个二维码,下面一行资产编号。打印时用热敏标签纸,尺寸40mm×20mm,打印精度203dpi。

二维码内容不需要加密,直接存资产编号。扫描后系统后台查编号显示详情,没有网络连接时也能通过识别出的编号人工比对。这个方案比存URL更稳,因为系统域名可能调整,但资产编号不会变。

生成用qrcode库,关键是要保证二维码文本的编码正确:

import qrcode from qrcode.constants import ERROR_CORRECT_M def generate_asset_qrcode(asset_code): qr = qrcode.QRCode( version=2, error_correction=ERROR_CORRECT_M, box_size=4, border=1, ) qr.add_data(asset_code) qr.make(fit=True) img = qr.make_image(fill_color="black", back_color="white") return img

标签打印时要注意缩放。二维码图像直接用图片格式贴到打印模板里,不要从屏幕上截图再用,容易失真。系统里做批量打印时,按资产编号逐个生成图片,然后拼成一个A4 PDF或直接发到标签打印机。我前几个版本直接调浏览器打印,打印尺寸总差几毫米,后来改成服务器生成PDF后下发,问题才稳定解决。

5. 开发中的五个真实踩坑:从迁移失败到并发编号冲突

5.1 SCRAM认证报错:Django、psycopg2与PostgreSQL版本之间的暗坑

系统第一次在测试环境跑数据迁移时,报错信息很直接:

django.db.utils.OperationalError: SCRAM authentication is not supported

当时第一反应是密码问题。检查settings.py里的数据库用户名密码、HBA配置都没错。查日志发现底层报到了psycopg2,才意识到是Python驱动版本太老,不支持PostgreSQL服务端的SCRAM-SHA-256认证。

排查链路是这样的:

  1. 先看Django端的数据库配置,确认URL模式没问题。
  2. 看PostgreSQL日志里这条连接认证方式,发现服务端要求SCRAM,而驱动只支持旧版的MD5。
  3. 用pip show psycopg2-binary确认版本,确实是旧版本。
  4. 升级到新版后,迁移通过。

这类问题很难从Django层面发现,因为Django ORM只会把底层异常原样抛出来。遇到数据库报错时不要只盯着业务代码,先确认整条链路里各组件版本是否匹配。

5.2 IntegerField存金额:一分钱引发的迁移

第一版资产表里,金额字段我图省事用了IntegerField,单位是“元”。后续导入历史数据时,Excel里的金额是12345.67元,代码里int(12345.67)直接报错,有的数据带角分直接丢精度。更麻烦的是,已经录入的部分数据因为取整已经算错了。

排查过程不复杂,但教训很深:金额字段从建模第一天就该用DecimalField,不要为了省事用整数或浮点数。当时改正的办法是写了一个数据迁移脚本,把旧的整数字段备份成历史列,再新增DecimalField,按原值重新解析导入。

5.3 CSV编码乱码:盘点批次导入第一关

资产管理员喜欢用CSV从别的系统导出资产数据,再导入到新系统。第一次测试时,中文资产名称全部变成了乱码,比如“办公椅”变成“鍔炲叕妞呫”。我拿到文件先查编码,用chardet检测出来是GB18030,而Django视图按UTF-8去读,自然全乱。

处理办法是在导入接口里做一个编码检测兜底:

import chardet def read_csv_with_encoding(file): raw = file.read() detected = chardet.detect(raw) encoding = detected.get("encoding", "utf-8") if encoding.lower().replace("-", "") in ("gb2312", "gbk"): encoding = "gb18030" return raw.decode(encoding, errors="replace")

这里有个细节:GB2312和GBK都尽量统一转成GB18030读取,因为后者是前两者的超集,解析兼容性更好。导入模板要强制注明编码方式,同时服务端仍然要做自动检测,两种手段一起上,才能挡住历史数据这个最大的乱源。

5.4 LoginRequiredMiddleware把所有页面都变成了登录页

为了快速控制权限,我写了一个中间件,对所有请求做登录校验。上线自测时发现整个系统进了死循环:访问/admin/被重定向到登录页,访问登录页本身也被重定向到登录页,最后连静态文件都被拦了。

排查思路:

  1. 先看浏览器开发者工具里的网络请求,发现连续302,位置指向登录页。
  2. 停掉中间件,系统恢复,确认问题出在中间件。
  3. 检查中间件代码,发现没有放行/accounts/login/、/static/和/admin/的白名单。
  4. 修正后,把白名单配置放进settings.py,统一维护。

这件事让我后来对“全局中间件”非常警惕。权限控制如果只需要作用于一部分视图,优先用Django的LoginRequiredMixin或装饰器,而不是写一个影响所有路由的中间件。全局中间件看着省事,却很容易把登录页、静态文件、甚至回调URL一起卷进去。

5.5 get_or_create处理并发资产编号,差点造成重复资产

资产编号生成规则是“部门代码+年份+四位流水号”,例如CST-2024-0123。第一版生成流水号的逻辑是查当前最大值加一:

latest = Asset.objects.filter( department=dept, buy_date__year=year ).order_by("-asset_code").first() seq = int(latest.asset_code.split("-")[-1]) + 1 new_code = f"{dept.code}-{year}-{seq:04d}"

两个管理员同时操作时,可能查到的最大值一样,生成相同的编号。我想当然地用get_or_create兜底:

asset, created = Asset.objects.get_or_create(asset_code=new_code, defaults={...})

问题在于get_or_create在“已经存在相同编号”时会返回旧对象,但业务上这是两件资产,只是编码撞了,系统会把第二件资产申请直接夺回到第一件资产上,造成误登记。

正确处理分两步:

  1. 数据库层面保证asset_code唯一索引,这是必须的底线约束。
  2. 应用层捕获IntegrityError后重试,或者用一个单独的表预分配编号片段,避免每次都去查最大值。

我当时用了一个简单但不那么优雅的方案:在唯一约束触发冲突时,捕获异常并重新生成编号重试,重试次数上限5次。这个方案虽然没有预分配那么美观,但足够解决并发场景,而且代码容易理解。核心教训是:get_or_create适合主键明确、冲突即同一条数据的场景,不适合承载业务上需要新生成逻辑的记录。

6. 部署上线后的运维与优化:备份、nginx与标签打印的现场经验

6.1 部署当天最容易翻车的几个环节

部署到学校服务器时,我提前列了一张检查清单,逐项确认。

Gunicorn作为一个简单的WSGI服务器来跑Django应用:

gunicorn -w 3 -b 127.0.0.1:8000 config.wsgi:application

进程数量按CPU核数取两倍左右。如果服务器只有2核,起4个worker,同时配合超时配置,避免请求卡死整个进程。前端加nginx做反向代理:

server { listen 80; server_name asset.local; client_max_body_size 20M; location /static/ { alias /srv/asset/static/; } location /media/ { alias /srv/asset/media/; } location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }

client_max_body_size必须调大,因为盘点时要上传照片,默认1M根本不够用。/media/目录的权限也要确保Django进程能写入,很多部署问题都产生在这里。

静态文件收集同样容易漏:

python manage.py collectstatic --noinput

如果漏了这一步,nginx的/static/会全部404,页面样式消失。上线前一定要在服务器本地用curl验证静态资源路径可访问,而不是只在自己电脑上点了一遍。

6.2 定时任务:备份、折旧计算、超时提醒

系统稳定运行后,有几类定时任务必须保证可靠。

数据备份我用的是系统cron加pg_dump:

pg_dump -Fc assetdb -f /backup/asset_$(date +%Y%m%d).dump

备份文件保留最近30天,每天凌晨执行。这里额外提一个经验:备份不能只做不恢复。每季度要拿一份备份到测试环境做一次恢复演练,否则备份文件很可能在关键时候不可用。

资产折旧计算我放在了Celery beat定时任务里,每个月初运行。计算逻辑不直接写财务折旧结果,只是生成一份“本月折旧试算表”给资产管理岗核对。真正入账操作由财务系统处理,这里不越权。

领用审批超过7天未处理,系统要给发起人发提醒。这个任务也是每天执行一次,扫描待审批记录,超时的自动生成站内信通知。定时任务看起来简单,但监控不能少,任务执行失败的日志要能方便地查出来。

6.3 现场盘点对手机和条码枪的适配

盘点现场环境差异很大,有的部门用手机扫码,有的还用老式条码枪。条码枪本质上是模拟键盘输入,扫码后直接把字符送到当前焦点输入框。这就要求扫码框必须能够自动聚焦,并且扫完编码后要自动提交或跳到下一个框。

我在盘点页面里做了这样一个处理:资产编码输入框autofocus,监听回车事件,扫码枪扫完会自动带一个回车,正好触发查询。手机上如果用摄像头扫码,走浏览器API调用,识别成功后自动填充并触发同一个提交函数。

条码枪有一个经典坑:输入法。如果页面里有中文输入法被激活,扫进来的英文字母可能被输入法吞掉,导致查询失败。所以盘点页尽量锁死键盘或提醒用户切换到英文输入法,更省事的方式是扫码框用inputmode="none"配合readonly,避免移动设备弹出软键盘。

6.4 上线后数据质量的持续维护

系统上线不等于数据干净。历史数据导入第一批往往只能保证“数量对”,不能保证“每一条都对”。我建议上线后第一个盘点周期里,可以开一个“数据修正模式”:资产员扫描实物时,如果发现名称、型号、存放地点不符,可以直接在盘点界面提交修正建议。修正建议不直接入库,而是汇总给资产管理岗批量确认。

这个机制比让人直接在系统里改资产主数据安全得多。它保留了“现场实际是什么”的证据,也避免了资产员误操作覆盖正确数据。

上线三个月后,资产台账里那些批量导入产生的脏数据会逐步暴露:编码规则不一致的老资产、历史部门合并后残留的旧部门归属、无使用人但有状态“使用中”的异常记录。我专门写了一个“数据质量报告”页面,把这些异常规则列出来,定期检查。

最后分享两个系统改版时的真实体会

如果让我重新做一次这套系统,最想改动的地方是资产编号规则。编码方案上线后尽量不要改,因为所有标签、历史单据、导入模板都依赖它。设计规则时最好提前预留扩展位,而不是把“部门代码-年份-流水号”填得满满当当。

另外,别把资产管理员逼成数据录入员。系统能自动带出的字段,就不要让人重复填写。资产名称、类别、品牌型号,很多都能从采购订单或发票信息里带出来。能省一步就省一步,这样资产员才愿意持续维护数据,账实相符才不是一句空话。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询