做计算机毕业设计这几年,“进销存”三个字几乎快被写烂了,但真正能贴住某个行业场景做深做透的题目反而很少。这个标题乍一看很长,拆开其实很清晰:Django做后端、小程序做移动端入口、企业客户设备进销存是业务核心,“定制化”三个字是最大的加分点。它解决的不只是“有没有库存”的问题,而是设备类企业里从采购入库、销售出库、客户对账到售后配件追溯的一整条业务流程。很多同学看到“ERP”就发怵,觉得太复杂,其实落到毕业设计这个尺度,剪掉生产制造、财务总账这些重模块,把进销存主链路跑通,再叠加一两个行业定制特色,就已经是非常完整且容易出彩的作品。
这类项目适合谁?后端用Python、前端不打算自己写复杂管理后台、又想让作品有“移动办公”亮点的同学,尤其合适。Django自带Admin后台可以当运营管理界面用,小程序端负责高频查询和开单操作,两份界面都有了,工作量还不会爆炸。下面我从定题思路、数据库建模、后端逻辑、小程序联调、答辩亮点到常见坑位,完整过一遍。
1. 定题思路:为什么Django+进销存+小程序是稳妥又出彩的组合
1.1 从题目反推需求:评审老师到底想看什么
毕业设计评审不会真的像企业项目一样压性能、问并发,他们更看重的是“需求分析是否清楚、设计是否合理、有没有完整闭环”。进销存业务最大的优点就是流程清晰,天然自带“采购—库存—销售—统计”的闭环,所有模块之间数据联动明确,非常适合在答辩时讲逻辑。
但纯做进销存很容易掉进“普通商品管理”的俗套里,所以题目里特意加了“定制化ERP系统”“企业客户设备”两个限定。普通商品只需要名称、分类、价格、库存数量,而设备类商品不一样:每一台设备都有独立序列号,可能关联保修期、验收入库日期、当前存放位置、售后状态。这些字段只有设备行业才有,属于“定制化”的核心体现。把这两个点抓住,整篇论文和系统设计的差异化就出来了。
评审老师还喜欢看到“交付物完整”,这就是题目里“程序+文档+讲解+定制”对应的地方。程序是运行起来的系统,文档包含需求说明、数据库设计、接口文档和测试用例,讲解是能演示核心链路,定制是指能根据特定业务流程做调整。这四个维度分别对应了工作量证明、设计能力证明、表达能力和编码能力,比单交一份代码要有说服力得多。
1.2 技术选型背后的理由:为什么不是SpringBoot,也不是原生小程序
后端选Django而不是SpringBoot,核心原因有两个:开发效率和自带管理后台。Django的ORM对进销存这种强数据关联的业务非常友好,一对多、多对多关系在写查询时很顺手;Django Admin只需要简单注册模型就能得到一个可用的后台管理页面,采购单审核、客户管理这些低频操作完全可以在Admin里完成,不用额外开发一套后台。这意味着系统天然拥有了“管理端”和“移动端”两个入口,工作量却只多了一点点。
小程序端的选择也很实际。当客户需要查看设备库存或提交采购需求时,他们在办公室外的概率远高于坐在电脑前,工程师上门维护、销售跑客户现场时,用手机查一下某型号设备还有几台现货,是最自然的场景。小程序不需要安装、用完即走,对企业内部员工来说比原生App友好得多。前后端通过JSON格式的API通信,Django用Django REST Framework写接口,两边边界清晰,调试也比较方便。
这里要说明一下,基于常见实践,后端一般搭配MySQL或SQLite。本地演示可以先用SQLite保底,正式提交时切到MySQL,只需要改配置文件里的数据库连接即可。这种“从简到全”的过渡方式,也是实际项目里比较省时间的做法。
2. 系统模块设计与数据库建模
2.1 功能模块划分:五张主表三张流程表
进销存系统的功能可以归纳成五个模块:基础资料、采购管理、销售管理、库存管理、统计看板。
基础资料是整个系统运行的前提,包括设备档案、客户档案、供应商档案以及员工账号信息。设备档案是这五个模块里最特殊的一个,它对应了“设备进销存”而不是“日用品进销存”,决定了后续所有流程表单的字段设计。
采购管理处理的是向供应商订货这一条线:创建采购单、等待审核、到货后执行入库。销售管理是反方向:客户下单、创建销售单、审核通过、仓库出库。两套流程都围绕“单据”和“库存”两个核心概念展开。比较常见的做法是采购单和销售单只承担“业务凭证”的作用,真正影响库存数量的是入库单和出库单,这样做的好处是后续追账、查库存变动都有据可依。
库存管理本身包含实时库存、出入库流水、库存预警三个子功能。实时库存就是当前每个设备型号有多少可用数量;出入库流水记录每一台设备从入库到出库的完整生命周期;库存预警则根据预设的库存下限自动提醒补货。
统计看板是加分项,也基本是必选项。它把采购、销售、库存的数据汇总成直观的图表,比如近30天出入库趋势、热销设备排行、客户采购金额排行等。需求分析里加上这个模块,论文的“系统功能结构图”立刻变得有层次感。
2.2 定制化设备台账:与普通进销存拉开差距的关键
设备台账是整个项目最需要花心思设计的地方。普通电商类的库存表字段大概是:商品名称、SKU编码、分类、进价、售价、库存数量。但设备类企业会多出一批字段,这些字段直接来源于业务场景,我建议保留成独立字段而不是塞进一个“备注”里:
第一,设备序列号。每台设备出厂时都有唯一序列号,序列号在后续保修、维修、出库核验时都是关键标识,所以需要在设备档案里单独存储并设置为唯一约束。第二,设备型号与规格参数。设备不像日用品那样用SKU就能完全定义,CPU配置、内存大小、屏幕尺寸、接口类型都会影响设备的使用场景,直接决定报价,建议设计成JSON字段或拆分关键字段存储。第三,保修截止日期。设备销售后还需要跟踪保修状态,这在普通商品的库存模型里基本不会出现。第四,库存状态。设备可能存在“在库、已预约、已出库、返厂维修”等状态,状态机设计比单纯的库存数量更贴合实际。
这里需要补充一个细节:设备档案与库存数量是分离的。设备档案记录“有哪些设备”,库存记录“每个型号各有多少台可用数量”。一台设备从供应商到货后,先建立设备档案,然后通过入库单进入库存;销售出库时,通过出库单核销对应序列号并扣减库存数量。这种分离设计在后续统计设备生命周期时会非常方便。
2.3 核心数据表关系与Django模型骨架
从数据库层面看,主要表关系可以用语言描述清楚:客户和销售单是一对多,销售单和销售出库单是一对一,采购单和采购入库单是一对一,设备档案和库存流水是一对多。用户表用于登录和权限分配,角色区分管理员、销售员、仓管员三类。
下面给一个能在项目里直接改着用的模型骨架,这部分代码体现的是常规进销存建模方式:
from django.db import models class Customer(models.Model): """企业客户""" name = models.CharField(max_length=128, verbose_name='客户名称') contact = models.CharField(max_length=32, verbose_name='联系人') phone = models.CharField(max_length=16, verbose_name='联系电话') credit_limit = models.DecimalField(max_digits=10, decimal_places=2, default=0, verbose_name='信用额度') created_at = models.DateTimeField(auto_now_add=True) class Meta: db_table = 'erp_customer' class Device(models.Model): """设备基础档案,定制化的核心体现""" sn = models.CharField(max_length=64, unique=True, verbose_name='设备序列号') model_name = models.CharField(max_length=128, verbose_name='设备型号') brand = models.CharField(max_length=64, blank=True, verbose_name='品牌') specs = models.JSONField(default=dict, verbose_name='规格参数') warranty_end = models.DateField(null=True, blank=True, verbose_name='保修截止日') warehouse = models.CharField(max_length=64, verbose_name='存放仓库') location = models.CharField(max_length=128, blank=True, verbose_name='具体位置') status = models.CharField( max_length=16, choices=[('in_stock', '在库'), ('reserved', '已预约'), ('sold', '已出库'), ('repair', '返厂维修')], default='in_stock', verbose_name='设备状态' ) class StockRecord(models.Model): """出入库流水,与单据强关联""" device = models.ForeignKey(Device, on_delete=models.PROTECT, verbose_name='设备') record_type = models.CharField(max_length=16, choices=[('in', '入库'), ('out', '出库')], verbose_name='类型') related_order_no = models.CharField(max_length=64, verbose_name='关联单号') created_by = models.ForeignKey('auth.User', on_delete=models.SET_NULL, null=True, verbose_name='操作人') created_at = models.DateTimeField(auto_now_add=True)这里的Device.status字段用Choice约束取值,防止脏数据;StockRecord不直接修改库存数字,而是记录流水,库存数字则由统计函数从流水汇总得到,也可以额外维护库存数量字段。两种方式各有优劣,后者性能好,前者维护成本低、不易出错。实际项目里我见过多数人选择“库存表存数量+流水表存明细”的双写方案,这个我会在下一部分细讲。
3. Django后端核心逻辑落地
3.1 库存流水与库存数量的一致性设计
进销存系统最容易被问倒的一个问题是:库存数据到底存在哪。如果只存流水,没一单都现算SUM,数据肯定一致,但数据量大了以后查询速度会变慢;如果只存库存数字,每次出库入库更新一下,速度很快,但一旦出现并发操作或者事务回滚,数字就很容易对不上。
常规且稳健的做法是双写:库存表保存当前可用数量,流水表保存每次变动明细。写操作时先创建流水记录,再更新库存数量,两端放在同一个数据库事务里。这样做的好处是日常查询库存走数字表,秒开;出了问题可以拿流水表和库存表对账,看看数量差异到底出在哪一笔单子上。
Django里实现双写非常简单,在入库和出库的序列化器里直接用事务包裹即可:
from django.db import transaction from django.core.exceptions import ValidationError def execute_inbound(order, device_list, operator): with transaction.atomic(): for device in device_list: StockRecord.objects.create( device=device, record_type='in', related_order_no=order.order_no, created_by=operator, ) # 设备状态更新为在库或已预约 device.status = 'in_stock' if device.status != 'reserved' else device.status device.save() # 重新统计该仓库该型号的可用库存数量并更新库存表 refresh_stock_quantity(order.warehouse, order.model_name)这里的refresh_stock_quantity可以写成一个专用函数,按仓库+型号维度汇总流水表,然后把统计结果更新到库存总表。实际系统中,这个函数会在每次出入库后调用,虽然多了一次统计查询,但粒度足够低,性能完全够用。
3.2 并发扣减库存:如何避免“超卖”
库存系统最经典的坑就是超卖。两个仓管员同时给同一个型号设备开两笔销售出库单,系统里还剩10台库存,但两个人提交时都读到10台,各自扣减后就可能出现负库存。这在真实业务里会引发很严重的对账问题,在答辩中也很容易被老师抓住追问。
解决办法是在扣库存时加行锁。Django ORM里对应的方法是select_for_update(),配合事务使用。拿销售出库举例,锁定目标库存行后再做扣减,第二个事务就必须等第一个提交后才能继续执行:
from django.db import transaction @transaction.atomic def deduct_stock(warehouse, model_name, quantity): # 锁定库存行,防止并发超卖 stock_row = StockQuantity.objects.select_for_update().get( warehouse=warehouse, model_name=model_name, ) if stock_row.quantity < quantity: raise ValidationError(f'库存不足,当前可用库存为{stock_row.quantity}') stock_row.quantity -= quantity stock_row.save()这段代码的核心在于select_for_update()。没有这行时,两次操作会同时读到相同的库存数字,然后各自扣减并保存,最终结果错误;加上之后,同一时间只有一个操作能改这行数据。在答辩演示时,老师如果问“多个用户同时操作怎么办”,这就是标准答案。
3.3 基于DRF的接口权限设计
进销存系统的API不是所有角色都能调。销售员可以创建销售单和查库存,但不能审核采购单;仓管员可以做出入库操作,但不能修改客户价格;管理员什么都能看。这个在Django REST Framework里通过权限类控制,非常清晰。
比较常见的权限设计是自定义一个IsRoleOrReadOnly风格的权限类:
from rest_framework.permissions import BasePermission class IsSalesmanOrReadOnly(BasePermission): """ 销售员只允许创建销售单和查看统计; 仓管员允许出入库操作; 管理员允许一切操作 """ def has_permission(self, request, view): if request.method in ('GET', 'HEAD', 'OPTIONS'): return True user = request.user if not user.is_authenticated: return False if user.is_superuser: return True if request.path.startswith('/api/sales/') and user.profile.role == 'sales': return True if request.path.startswith('/api/inventory/') and user.profile.role == 'warehouse': return True return False权限逻辑看起来不复杂,但它是答辩时讲“系统安全性”的一手好牌。如果项目里完全没有权限控制,任何用户都能改库存,整个系统的可信度会大打折扣。把用户角色、API路径、HTTP动词三者结合起来谈,就显得你确实考虑过真实业务约束。
4. 小程序端功能开发与前后端联调
4.1 小程序端页面架构与操作流程
小程序端不用做得太庞大,核心页面控制在6到8个左右就非常合理。我见过不少做得好的设备进销存项目,小程序的页面基本围绕“查”“开”“审”“看”四个动作展开。
查:库存查询页。这是使用频率最高的页面,展示仓库、型号、可用库存、预警状态。可以直接用列表形式,也可以在列表上方加一个搜索框,按型号名称或序列号搜索。查库存还要支持扫码查询,设备出库时扫一下序列号就能定位设备信息和状态,这个功能非常适合现场作业。开:开单页。销售员见客户时可以直接在小程序里选择客户、添加设备型号、填写销售数量,生成草稿销售单。开单时还要联动库存余额,超出库存直接给出友好提示,避免录完单子入库审核时才被驳回。审:审核页。管理员在小程序端可以查看待审核的采购单和销售单,确认后进入出入库流程。这一步的作用是让“老板”不用打开后台也能移动审批。看:数据看板。小程序首页放当日销售金额、待审核单据数、低库存提醒三条关键信息,一眼掌握重点。
小程序的组件开发思路不需要在文中展开到能跑通的程度,但有一点要强调:页面状态设计要贴近流程。比如销售单从“草稿”到“待审核”,再到“已出库”“已完成”,每一步对应后端数据里一个明确的状态字段。前后端的状态值保持一致,就不会出现小程序显示“已完成”,而后台显示“待出库”这类低级问题。
4.2 API联调中常见的坑与处理
前后端联调是学生项目里耗时最多的环节,大部分问题不在业务代码,而在环境和协议。根据实际经历,下面这几个坑的频率最高:
第一个坑是小程序端请求本地Django服务时被拒绝。小程序平台普遍要求正式环境必须使用HTTPS域名,本地开发时可以勾选“不校验合法域名”选项,访问http://局域网IP:8000。这里要特别注意,不能写成http://localhost:8000或http://127.0.0.1:8000,因为手机上的小程序访问localhost时指向的是手机自己,而不是你的电脑。正确做法是查电脑局域网IP,比如http://192.168.1.101:8000,电脑和手机连同一个WiFi才能访问。
第二个坑是跨域问题。浏览器里用Axios请求Django接口时,如果没有配置CORS,浏览器会拦截响应。这个问题在Django里通过django-cors-headers这个库解决,安装后在settings.py中配置允许的来源即可:
INSTALLED_APPS = [ # ... 'corsheaders', ] MIDDLEWARE = [ # 注意放到最前面 'corsheaders.middleware.CorsMiddleware', # ... ] CORS_ALLOWED_ORIGINS = [ 'http://localhost:8080', 'http://127.0.0.1:8080', ]第三个坑是Token过期时间设得太短。很多同学把Token有效期设为30分钟,自己测试时觉得没问题,结果演示的时候隔一会儿就要重新登录,场面非常尴尬。演示环境下建议把过期时间调长一点,比如8小时,数据库里重新生成Token也很简单。真实项目当然不能这么干,但毕业设计讲清楚“演示环境放宽,生产环境收紧”这个思路,反而显得你懂安全配置。
第四个坑是时区导致日期统计对不上。设备入库销售记录如果按UTC时间存,国内看统计图会缺失8小时内数据。Django的settings.py里需要把TIME_ZONE设置为'Asia/Shanghai',同时将USE_TZ保持为True,接收前端传参时统一处理成本地时间,这样统计报表才是正常天数。
5. 答辩加分项:统计报表、库存预警与文档组织
5.1 近30天出入库趋势统计
统计报表不需要用太重型的工具,Django后台用annotate按天分组汇总数据就足够。根据常见实践,可以写一个视图接口,按日期维度返回过去30天每天的入库数和销售出库数,前端小程序用柱状图展示,视觉上比表格好看很多,答辩演示效果也好。
from django.db.models.functions import TruncDate from django.db.models import Count def inventory_trend(request): thirty_days_ago = timezone.now() - timezone.timedelta(days=30) inbound_data = ( StockRecord.objects .filter(record_type='in', created_at__gte=thirty_days_ago) .annotate(day=TruncDate('created_at')) .values('day') .annotate(total=Count('id')) .order_by('day') ) return JsonResponse({'inbound': list(inbound_data)})这个统计接口逻辑很简单,但答辩时一定要讲清楚“为什么按天聚合而不是按单聚合”。按单聚合会让趋势图出现毛刺,某一天有5笔单子就会暴增,按天聚合后数据更平滑,也更符合管理者观察趋势的视角。能在这种细节上说出理由,答辩老师会认为你真的做过数据分析。
5.2 库存预警与客户采购价值分析
库存预警功能在业务上很实用。给设备型号设置一个库存下限,比如某型号低配版低于5台就触发预警,当出库操作导致库存低于下限时,系统自动生成一条预警记录并显示在小程序首页。这个功能实现成本很低,在出库事务里加一个if判断即可,但效果非常好,属于一眼就能看懂的“智能功能”。
客户采购价值分析稍微进阶一些。按客户维度汇总历史销售金额,然后排序,就能得到贡献度最高的前10名客户。这个数据同时可以放进论文的结果展示和演示视频中,配合柱状图,放在“销售决策支持”这个小节里,比单纯展示CRUD界面有说服力得多。数据量不大时直接分组聚合即可,不需要引入复杂的分析框架。
5.3 交付文档怎么组织,演示路径怎么设计
交付文档是很多同学容易忽视的部分,但也是题目里明确列出的交付物。一套完整且逻辑清晰的文档可以分成四份:需求分析文档、数据库设计文档、接口文档、测试用例文档。需求分析文档里包含背景分析、业务流程、功能结构、非功能需求;数据库设计文档是ER图加表结构说明;接口文档用表格列出URL、请求参数、返回字段;测试用例文档覆盖核心业务流程,例如“创建销售单→审核→出库→库存扣减”这个完整链路。
演示路径的设置也很有讲究,不要拿着系统从头到尾点一遍,那样既没重点又容易暴露问题。我建议的演示顺序是:先展示小程序库存查询和低库存提醒,然后演示新增一个设备入库,再演示销售开单审核出库,最后打开统计看板看趋势图和Top10榜单。每一步都对应一个模块的核心价值,2到3分钟就能讲完整个故事。演示用的数据要提前造好,客户、供应商、设备档案都准备10条以上,图表才会有美感。数据太少时柱状图会显得稀疏,效果直接打折。
6. 常见问题与避坑实录
6.1 问题速查表
| 问题 | 通常原因 | 排查思路与处理 |
|---|---|---|
| 小程序连不上本地Django | 请求地址为localhost或未开启不校验域名 | 改用电脑局域网IP,同一WiFi下测试 |
| 接口报CORS跨域错误 | Django未配置跨域中间件 | 安装并启用django-cors-headers |
| 库存出现负数 | 扣库存未加锁 | 事务中使用select_for_update() |
| 统计报表日期少一天或错位 | Django时区为UTC | settings.py中设置TIME_ZONE为Asia/Shanghai |
| Admin后台无法触发库存流水 | 入库操作直接改了库存数字没写流水 | 统一通过Service层创建流水,不要直改库存字段 |
| 设备序列号重复入库 | 缺少唯一约束或入库时没校验 | Device.sn添加unique=True,入库时先查Exists |
| 小程序端状态值与后端不一致 | 状态枚举文字硬编码在前端 | 前后端共用同一枚举定义,状态改成英文标识存储 |
6.2 答辩时最容易被追问的三个问题
在答辩环节,老师如果看过进销存类项目,大概率会从三个点切入提问:第一个是库存数据一致性怎么保证,第二个是权限控制如何落地,第三个是如果业务量变大了怎么办。前两个问题对应上述的事务锁和权限类,提前准备好答案基本没问题。第三个问题常见思路是“增加缓存、读写分离、引入消息队列”,但这些方案在毕业设计阶段不一定落地,重点是表达清楚扩展方向,而不是实现细节。
还有一个经常被点出来的是“你的定制化到底体现在哪里”。这个问题必须讲出和普通进销存不一样的字段和流程:序列号追踪、设备状态流转、保修期管理、客户信用额度,这些点都是普通库存系统没有的。答辩前把这些业务差异列在一页PPT上,直接从业务角度切入,比空谈技术栈要有力得多。
7. 一些实际经验分享
这类项目我前后接触过不少,最大的心得是:进销存系统的难点从来不在增删改查,而在流程状态和一致性的设计上。很多同学一上手就急着写接口,结果发现做到一半,采购单、入库单、库存表之间的关系根本理不清,代码里到处是临时补丁。我建议动手前先把业务流程图画清楚,把每一步涉及的表、状态变化标注出来,再写代码就是按图索骥。
一个小细节:表名和字段建议全部用英文,Django模型里verbose_name用中文显示。很多同学喜欢直接用中文拼音作为字段名,图方便,但后期写接口、调前端、查错误时非常痛苦。规范命名虽然初期多花十分钟,后面省下的时间远超这十分钟。
最后再分享一个提升观感的做法:演示数据一定要“有故事”。不要造“设备A入库1台、出库1台”这种零散数据,而是造一条完整的采购销售链路,比如供应商发货10台设备,入库后分三笔卖给两家客户,其中一台刚好触发低库存预警,再配合看板上的趋势图,整个演示会非常流畅。这类作品在答辩时不是靠新技术赢的,而是靠业务完整度和细节打磨赢的。