1. 项目概述与整体设计思路
1.1 需求背景与痛点分析
接触这个项目之前,我先说个真实场景。你所在的公司、工作室或者机构,档案资料是不是还在用Excel表格甚至纸质文件夹管理?找一份合同要翻半天柜子,谁借走了哪份文件全靠一张手写登记表,月底统计归档数量还得人工数。这些看起来不大不小的问题,累积起来就是效率黑洞。我做“档案宝”这个项目,就是想用一套轻量方案解决这些问题。
档案宝是一套基于Python Django后端和微信小程序前端的档案管理系统。后端负责业务逻辑、数据存储和接口输出,前端以微信小程序形态呈现,用户不需要安装App,扫码或者搜索就能进入使用。系统覆盖了档案录入、关键词检索、借阅归还登记、借阅记录查询、管理员看板统计等核心功能,适合中小型企业、创业团队、学校院系、社区服务站这类档案数量在几千到几万份、没有专职IT人员维护的场景。
这个项目尤其适合正在学Django的开发者,或者准备做小程序方向毕设的同学参考。整套系统麻雀虽小五脏俱全,涉及用户登录、权限区分、增删改查、状态流转、分页加载、前后端联调这些日常开发绕不开的环节,跑通一遍基本等于把Web后端和小程序前端的主干知识串了一遍。
1.2 技术选型:为什么是Django配合微信小程序
选型这块我纠结过一阵子。当时考虑过Flask、Spring Boot、Node.js,最后定了Django,原因有三。
第一,Django自带ORM,不用自己拼SQL。档案系统的核心是数据操作,ORM能把模型定义和数据库表结构直接映射,开发期省事,后期改字段也方便,migrate一下就行。第二,Django内置Admin后台。虽然小程序端做了管理功能,但有Django Admin兜底,临时要查一条数据、批量改个状态,直接登录后台操作,不用专门写接口。第三,Django的认证体系和中间件机制成熟,做token鉴权、跨域配置都有现成方案。
微信小程序作为前端载体,最大优势是零安装。企业内部推广系统最怕什么?怕让大家装App,几百号人光下载注册就能折腾一周。小程序在微信里直接搜、直接打开,还能通过分享卡片传播,入口成本极低。再加上微信登录本身能拿到openid,天然解决用户身份识别问题,不需要单独做手机号验证码登录。
数据库我选了MySQL。开发阶段偷懒用SQLite也能跑,但考虑到档案数据需要长期保存、并发访问会逐渐增加,MySQL更稳。后面的表结构设计也是按MySQL来做的,字段类型、编码都按这个标准。
1.3 核心功能模块拆解
整个系统按使用角色分成两条线:普通用户线和管理员线。
普通用户能做的事:浏览档案列表、按关键词搜索档案、查看档案详情、发起借阅申请、查看自己的借阅记录和归还状态。管理员除了上面全部功能之外,还多出档案录入与编辑、销毁处理、借阅审核、归还确认、统计看板这几项。
我一开始差点把管理员功能直接做进同一套接口里,后来想想不对。档案系统最怕权限模糊,谁都能删记录等于没权限。所以前后端都必须做角色判断,后端接口用装饰器拦,前端根据登录返回的角色字段控制页面按钮显示。
按照这个思路,我把功能模块整理成下面几个核心表支撑:
| 数据表 | 核心字段 | 作用 |
|---|---|---|
| user_profile | openid、昵称、角色、手机号 | 小程序登录用户扩展信息,区分普通用户与管理员 |
| archive_category | 分类名、排序、创建时间 | 档案分类,比如合同、证件、报告 |
| archive | 档案编号、标题、分类、存放位置、状态、创建人 | 档案主表,状态区分在库/借出/已销毁 |
| borrow_record | 档案ID、借用人、借出时间、预计归还时间、实际归还时间、状态 | 借阅流转记录,支持多次借阅历史 |
2. 后端Django开发实操过程
2.1 工程初始化与基础配置
先强调一个容易踩坑的地方:Python虚拟环境一定要建。我见过太多人直接在全局环境pip install django,结果不同项目依赖冲突,升级一个库把另一个项目搞挂了。用venv建独立环境是习惯问题,不是额外负担。
mkdir archive_project cd archive_project python -m venv venv source venv/bin/activate # Windows环境用 venv\Scripts\activate pip install django mysqlclient django-cors-headers django-admin startproject archive_backend cd archive_backend python manage.py startapp archive python manage.py startapp users这里的mysqlclient在Windows上安装偶尔会报错缺Visual C++构建工具,如果实在装不上,退而求其次用pymysql,然后在项目__init__.py里写一句:
import pymysql pymysql.install_as_MySQLdb()当然这只是替代方案,能装mysqlclient还是优先mysqlclient,性能和兼容性更好。
创建好应用之后,记得去settings.py把两个app加进INSTALLED_APPS,然后配置数据库连接:
DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'archive_db', 'USER': 'root', 'PASSWORD': 'your_password', 'HOST': '127.0.0.1', 'PORT': '3306', 'OPTIONS': {'charset': 'utf8mb4'}, } }charset要特别留意,用utf8mb4而不是utf8。档案标题里如果有生僻字或者特殊符号,utf8mb4才能完整存储,utf8在个别生僻字上会报错。
2.2 模型设计与表关系梳理
Django模型设计是整个后端的地基。档案系统最大的特点是什么?是数据之间有明确归属和流转记录。我设计模型时重点处理了三个关系:用户与档案的创建归属关系、档案与分类的归属关系、档案与借阅记录的流转关系。
直接看代码更清楚:
from django.db import models from django.contrib.auth.models import User class ArchiveCategory(models.Model): name = models.CharField('分类名称', max_length=50, unique=True) sort_order = models.IntegerField('排序', default=0) class Meta: db_table = 'archive_category' verbose_name = '档案分类' def __str__(self): return self.name class Archive(models.Model): STATUS_CHOICES = ( ('in_stock', '在库'), ('borrowed', '借出'), ('destroyed', '已销毁'), ) archive_no = models.CharField('档案编号', max_length=50, unique=True) title = models.CharField('档案标题', max_length=200) category = models.ForeignKey(ArchiveCategory, on_delete=models.PROTECT, verbose_name='所属分类') location = models.CharField('存放位置', max_length=100, blank=True) status = models.CharField('状态', max_length=20, choices=STATUS_CHOICES, default='in_stock') remark = models.TextField('备注', blank=True) creator = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, verbose_name='创建人') created_at = models.DateTimeField('创建时间', auto_now_add=True) updated_at = models.DateTimeField('更新时间', auto_now=True) class Meta: db_table = 'archive' verbose_name = '档案' def __str__(self): return f'{self.archive_no}-{self.title}' class BorrowRecord(models.Model): STATUS_CHOICES = ( ('borrowing', '借阅中'), ('returned', '已归还'), ('overdue', '已逾期'), ) archive = models.ForeignKey(Archive, on_delete=models.CASCADE, related_name='borrow_records', verbose_name='档案') borrower = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='借用人') borrow_time = models.DateTimeField('借出时间', auto_now_add=True) expect_return_time = models.DateTimeField('预计归还时间') actual_return_time = models.DateTimeField('实际归还时间', null=True, blank=True) status = models.CharField('状态', max_length=20, choices=STATUS_CHOICES, default='borrowing') class Meta: db_table = 'borrow_record' verbose_name = '借阅记录'三个关键设计决定我解释一下。
一是archive_no做unique约束。档案编号好比人的身份证号,系统里数据一多,重号会导致两份档案互相覆盖,这是档案系统绝不允许的。数据库层面加唯一约束,比应用层判断更可靠。
二是Archive的外键用了on_delete=models.PROTECT。这是刻意选择,如果删除一个分类,但分类下还有档案,PROTECT会直接阻止删除并报错。分类下挂了几百份档案,一个误操作全没关联了,这种事故不能发生。宁可让管理员先把该分类下档案转移走,再允许删分类。
三是BorrowRecord用外键关联Archive,而不是在Archive表里堆借阅字段。这样做的好处是支持同一份档案多次借阅的历史记录,每次借阅都是新纪录,数据呈现完整闭环。借阅状态变化只需要update对应记录,不动档案主表。
模型写完之后执行数据库迁移:
python manage.py makemigrations python manage.py migrate执行完migrate后建议创建超级管理员,后面直接用Django Admin维护基础数据:
python manage.py createsuperuser2.3 接口设计与视图开发
接口设计我推荐一个原则:先列接口清单,再动手写代码。别想到哪写到哪,否则前端对接时你会被自己混乱的接口命名坑死。
我最终定的核心接口清单如下:
| 接口路径 | 方法 | 功能说明 | 是否需要登录 |
|---|---|---|---|
| /api/login | POST | 微信code换登录态,返回token和角色 | 否 |
| /api/archive/list | GET | 档案分页列表,支持关键词搜索 | 是 |
| /api/archive/detail | GET | 档案详情 | 是 |
| /api/archive/create | POST | 新增档案 | 是,仅管理员 |
| /api/archive/update | POST | 编辑档案 | 是,仅管理员 |
| /api/archive/delete | POST | 删除档案 | 是,仅管理员 |
| /api/borrow/create | POST | 发起借阅 | 是 |
| /api/borrow/return | POST | 归还档案 | 是,仅管理员 |
| /api/borrow/my | GET | 我的借阅记录 | 是 |
| /api/stats/overview | GET | 管理看板统计 | 是,仅管理员 |
视图层我选择用Django自带的JsonResponse,没有额外引入Django REST Framework。原因很简单,项目不是大型分布式系统,接口数量不到十个,JsonResponse配合@csrf_exempt足够用,少学一个框架,减少依赖复杂度。当然,如果你预期后续接口会大量膨胀,或者需要自动生成API文档,果断上DRF,它分分钟帮你把序列化、分页、认证都包好。
一个典型的列表接口长这样:
import json from django.http import JsonResponse from django.views.decorators.http import require_http_methods from django.views.decorators.csrf import csrf_exempt from django.core.paginator import Paginator from django.db.models import Q from archive.models import Archive @csrf_exempt @require_http_methods(["GET"]) def archive_list(request): page = int(request.GET.get('page', 1)) page_size = int(request.GET.get('pageSize', 10)) keyword = request.GET.get('keyword', '').strip() queryset = Archive.objects.all() if keyword: queryset = queryset.filter( Q(title__icontains=keyword) | Q(archive_no__icontains=keyword) | Q(location__icontains=keyword) ) paginator = Paginator(queryset, page_size) try: page_obj = paginator.page(page) except Exception: return JsonResponse({'code': 0, 'msg': '页码超出范围'}) data = [{ 'id': item.id, 'archive_no': item.archive_no, 'title': item.title, 'category': item.category.name, 'status': item.status, 'location': item.location, 'created_at': item.created_at.strftime('%Y-%m-%d %H:%M'), } for item in page_obj] return JsonResponse({ 'code': 1, 'data': data, 'total': paginator.count, 'page': page, 'pages': paginator.num_pages })接口里两个细节值得注意。一是keyword搜索我用Q对象把标题、编号、位置三个字段都查了一遍,用户不一定知道完整编号,可能就记得个标题词,这样查更人性化。二是分页直接用Django自带的Paginator,返回total和pages两个字段给前端,前端就知道一共多少页、当前页是多少,方便做上拉加载的判断。
删除接口我特别提醒一下。Django里删除对象最简单的方式是调用.delete(),但做档案系统建议不要物理删除。档案可能涉及合同、审批材料,删了就真没了,如果后面审计发现数据缺失,这锅你得背。我采用的方案是一条软删除逻辑,用状态字段控制:
@csrf_exempt @require_http_methods(["POST"]) def archive_delete(request): body = json.loads(request.body) archive_id = body.get('id') if not archive_id: return JsonResponse({'code': 0, 'msg': '缺少档案ID'}) archive = Archive.objects.filter(id=archive_id).first() if not archive: return JsonResponse({'code': 0, 'msg': '档案不存在'}) # 软删除:状态改为已销毁,而不是物理删除 archive.status = 'destroyed' archive.save() return JsonResponse({'code': 1, 'msg': '操作成功'})这样做的好处是数据永远在库里,只是状态变化。管理员后台想看还有多少销毁档案记录,也能统计出来。真遇到必须物理删除的场景,再单独去数据库操作,至少不会因前端误触导致数据不可恢复。
2.4 登录鉴权与管理员权限控制
微信小程序不像网页能直接用Cookie维持会话,它调用wx.login拿到一个临时code,后端拿这个code去找微信服务器换openid和session_key。这里有一个热词能对上:django执行查询-删除对象,但登录鉴权这个环节更多人关心的是code2session流程的稳定性。
我直接说后端实现:
import requests import hashlib import time from django.http import JsonResponse from django.views.decorators.http import require_http_methods from django.views.decorators.csrf import csrf_exempt from users.models import UserProfile from django.contrib.auth.models import User APPID = '你的appid' SECRET = '你的secret' @csrf_exempt @require_http_methods(["POST"]) def login(request): body = json.loads(request.body) code = body.get('code') if not code: return JsonResponse({'code': 0, 'msg': '缺少code'}) # 向微信服务器换取openid url = 'https://api.weixin.qq.com/sns/jscode2session' params = { 'appid': APPID, 'secret': SECRET, 'js_code': code, 'grant_type': 'authorization_code' } resp = requests.get(url, params=params, timeout=5).json() openid = resp.get('openid') if not openid: return JsonResponse({'code': 0, 'msg': '微信登录失败', 'detail': resp}) # 查用户,不存在则创建 user_profile = UserProfile.objects.filter(openid=openid).first() if not user_profile: new_user = User.objects.create_user(username=openid, password=None) user_profile = UserProfile.objects.create(user=new_user, openid=openid) # 生成简易token token_str = f'{openid}-{time.time()}' token = hashlib.md5(token_str.encode('utf-8')).hexdigest() user_profile.token = token user_profile.save() role = 'admin' if user_profile.role == 'admin' else 'user' return JsonResponse({'code': 1, 'token': token, 'role': role, 'nickname': user_profile.nickname})登录逻辑里有两个坑我必须提到。
第一,code只能用一次。微信的code是短时有效的,前端每次调用wx.login都会生成新code,后端处理完后这个code就作废了。前端千万不要把code存下来重复使用,否则第二次请求会直接报40029。第二,SECRET绝对不能暴露。这个值在后端请求微信接口时使用,一旦泄露到前端,任何人都可以冒充你的小程序跟微信服务器交互,后果很严重。
Token方案我做得比较朴素,MD5字符串加时间戳。正式上线建议换成更安全的token机制,比如用Django内置的signing模块或者JWT。但逻辑是一样的:前端后续每个请求都在请求头带Authorization字段,后端写一个装饰器统一校验。
装饰器实现如下:
from functools import wraps from django.http import JsonResponse from users.models import UserProfile def login_required(view_func): @wraps(view_func) def wrapper(request, *args, **kwargs): token = request.headers.get('Authorization', '').replace('Bearer ', '') if not token: return JsonResponse({'code': 0, 'msg': '未登录'}, status=401) profile = UserProfile.objects.filter(token=token).first() if not profile: return JsonResponse({'code': 0, 'msg': '登录已过期'}, status=401) request.user_profile = profile return view_func(request, *args, **kwargs) return wrapper def admin_required(view_func): @wraps(view_func) def wrapper(request, *args, **kwargs): profile = getattr(request, 'user_profile', None) if not profile or profile.role != 'admin': return JsonResponse({'code': 0, 'msg': '无权限'}, status=403) return view_func(request, *args, **kwargs) return wrapper有了这两个装饰器,视图函数上面一行注解,权限控制就完成了,不用每个接口里重复写判断。
3. 微信小程序前端的实现要点
3.1 页面结构与导航设计
前端我用的原生微信小程序框架,没有引入uni-app。原因是项目结构不复杂,原生框架代码量更少,真机调试也更直接。如果你想做多端复用,那另说,uniapp打包是另一条路线,适合跨iOS、Android、H5一起发的情况。
页面结构我分成三块共六个页面:
pages/ ├── login/ # 登录页 ├── index/ # 首页,展示统计概览和推荐档案 ├── archive/ │ ├── list/ # 档案列表页(含搜索) │ └── detail/ # 档案详情页 ├── borrow/ │ └── my/ # 我的借阅记录 └── mine/ # 个人中心/管理入口底部tabBar我配置了三个入口:首页、档案库、我的。登录页不进tabBar,用户未登录时跳转登录页,登录后通过switchTab进入主界面。
tabBar配置有一个细节,图标文件路径如果缺失会直接导致编译报错。如果你手头没有图标素材,可以用文字tab,也就是不配置iconPath字段,小程序是允许只用文字的。
3.2 列表页与分页加载更多
档案列表页是整个小程序最核心的页面,要做的事情有三件:拉取数据、渲染列表、上拉加载。
先看请求封装。小程序里wx.request是基础API,但每个请求都要带token、处理错误码,我习惯封装一个公共请求方法:
function request(url, method, data, header) { const token = wx.getStorageSync('token'); return new Promise((resolve, reject) => { wx.request({ url: baseUrl + url, method: method, data: data, header: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token }, success: (res) => { if (res.statusCode === 200 && res.data.code === 1) { resolve(res.data); } else if (res.statusCode === 401) { wx.navigateTo({ url: '/pages/login/login' }); reject(res); } else { wx.showToast({ title: res.data.msg || '请求失败', icon: 'none' }); reject(res); } }, fail: (err) => { wx.showToast({ title: '网络异常', icon: 'none' }); reject(err); } }); }); }列表页的加载更多逻辑,这里有一个高频热词就是“微信小程序页面列表加载更多”。实现思路不复杂:页面维护page和pageSize两个变量,加载下一页时page加1,然后把新数据concat到原有数组后面。
Page({ data: { list: [], page: 1, pageSize: 10, hasMore: true, keyword: '', loading: false }, onLoad() { this.loadList(true); }, loadList(reset) { if (this.data.loading) return; if (reset) { this.setData({ page: 1, list: [], hasMore: true }); } if (!this.data.hasMore) return; this.setData({ loading: true }); request('/api/archive/list', 'GET', { page: this.data.page, pageSize: this.data.pageSize, keyword: this.data.keyword }).then(res => { const newList = reset ? res.data : this.data.list.concat(res.data); this.setData({ list: newList, hasMore: this.data.page < res.pages, loading: false }); }).catch(() => { this.setData({ loading: false }); }); }, onReachBottom() { this.setData({ page: this.data.page + 1 }); this.loadList(false); }, onPullDownRefresh() { this.loadList(true); wx.stopPullDownRefresh(); }, onSearchInput(e) { this.setData({ keyword: e.detail.value }); }, onSearchConfirm() { this.loadList(true); } })代码里有几个细节经验。loadList里先加loading判断,防止重复请求。上拉加载时清空原有列表再重新加载还是追加?看场景,下拉刷新是重置,上拉加载是追加,逻辑分开。onReachBottom触发频率很高,一定记得loading保护,否则用户快速滑动时会连续发出多个重复请求。
3.3 单选框、表单与借阅流程
借阅流程是前端交互里比较完整的业务闭环。用户从档案详情页点“借阅”按钮,弹出借阅表单,选择预计归还日期,提交后生成借阅记录。
借阅时长选择我用单选框,在一个actionSheet或者表单里展示几个固定选项:一周、一个月、三个月、半年。在小程序里,radio-group的change事件会返回当前选中项的值,直接绑定到数据里。
<view class="borrow-form"> <view class="form-title">预计借阅时长</view> <radio-group bindchange="onDurationChange"> <label wx:for="{{durations}}" wx:key="*this"> <radio value="{{item.value}}" checked="{{item.checked}}" /> <text>{{item.label}}</text> </label> </radio-group> <button bindtap="submitBorrow" type="primary" loading="{{submitting}}">提交借阅申请</button> </view>这里有个交互细节我踩过坑:radio的checked要绑定对应的数据,不要在wxml里写死。如果多个radio的checked都指向同一个布尔值,会出现所有选项同时被选中的问题。正确做法是在数据里用一个选中项索引,每次change后更新它。
3.4 管理端页面与操作权限
管理端页面我做成一个独立的入口,只在个人中心显示给管理员角色。普通用户看不到新增档案、删除档案的按钮,从视觉上避免误操作。
新增档案表单有两个字段容易忽略。一个是存放位置,建议做成带提示的输入框,比如“A区-2排-3柜”,格式统一方便后期盘点。另一个是档案编号,前端不强制生成,由后端检查唯一性,重复时返回错误提示。前端做一层校验只是体验优化,后端校验才是真正的数据防线。
管理员提交新增接口时,注意要把创建人信息传给后端。我在登录接口已经把user_profile附在request对象上,后端视图里直接取就行,前端不需要额外传用户ID,也不应该传——用户ID这种信息前端传什么后端就信什么,这是典型的越权风险。
4. 联调、上线与常见问题排查
4.1 完整联调流程与试用反馈收集
联调阶段最容易被忽略的是环境配置。开发时小程序请求本地Django服务,必须先在开发者工具里做两件事:勾选“不校验合法域名、web-view、TLS版本以及HTTPS证书”,在详情-本地设置里打开。否则真机调试和模拟器会拒绝发请求。
如果你用模拟器调试,默认域名校验也会拦截http://127.0.0.1:8000,所以这个开关必须开。但注意,这个设置只在开发者工具里有效,真机预览时需要在同一个WiFi下,把请求地址改成电脑的局域网IP,比如192.168.1.100:8000。
我是这样串完整流程的:
- 启动Django服务:python manage.py runserver 0.0.0.0:8000
- 小程序开发者工具打开工程,确认“不校验合法域名”勾选
- 清空storage,重新进入小程序触发登录
- 登录后看控制台Network面板,确认登录接口返回token
- 打开档案列表页,确认列表加载正常
- 提交一个借阅申请,回Django Admin后台查borrow_record表是否有记录
- 管理员账号操作一遍档案新增和删除,同时用普通账号验证无权限提示
一轮联调下来,前端错误、后端报错基本都能暴露出来。
关于怎么发给其他人试用收集反馈,小程序开发完成后,在微信开发者工具点“上传”,把代码提交到微信公众平台,然后在版本管理-开发版本里把该版本设为体验版,生成体验版二维码。把二维码发给同事,对方扫码就能在真机上试用。体验版限制体验成员名单,需要在小程序后台“成员管理”里添加对方的微信账号,没加的话扫码进去会提示无权限。
收集反馈我建议准备一份试用清单,让试用者照着点一遍:搜索一个档案、借阅一份档案、查看借阅记录、测试超时后是否还能正常提示。试用反馈里最容易暴露的问题是页面卡顿和操作文案不明确,这两类问题收集一周基本能收敛。
4.2 常见问题速查表
整个项目跑下来,我把前端后端高概率遇到的问题整理成一张速查表:
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 登录返回40029 | code重复使用或code过期 | 确认每次登录都调wx.login获取新code |
| 登录返回40125 | appid与secret不匹配 | 去微信公众平台核对小程序AppSecret |
| 小程序请求后端跨域报错 | 后端未配置CORS | 安装django-cors-headers并配置白名单 |
| 中文查询返回乱码 | 数据库或表字符集不是utf8mb4 | 迁移前确认建库语句设utf8mb4 |
| 列表每次上拉加载数据重复 | 分页游标逻辑错误 | 检查page是否在加载成功后自增 |
| 新增档案提示字段过长 | MySQL默认字段长度不够 | 调整varchar长度或改为TextField |
| 真机调试请求失败 | 未关闭域名校验且使用未备案域名 | 开发阶段用局域网IP并关闭校验 |
| 管理员接口普通用户也能调用 | 后端缺权限校验 | 在接口上加admin_required装饰器 |
| 图片加载不出来 | MEDIA配置错误或nginx未代理 | 配置MEDIA_URL并在nginx加location |
| 部署后访问502 | gunicorn未启动或socket配置错误 | 检查gunicorn进程和nginx反向代理配置 |
这十条基本覆盖了从开发到上线的绝大部分故障。遇到问题排查时,先看后端日志,再看前端Network面板。小程序开发者工具的控制台会直接打印出请求失败的错误信息,按图索骥比瞎猜快很多。
4.3 上线前必做的检查项
小程序上线和普通网页不一样,多了一道平台审核流程。这里提醒一个硬性规定:小程序后台必须配置request合法域名,域名必须HTTPS,而且要在ICP备案完成后才能配置。如果暂时没有备案域名,可以先在开发阶段用IP地址跑通全流程,上线前一个月再准备域名和证书。
服务器部署我用的方案是nginx加gunicorn,配置示例:
pip install gunicorn gunicorn archive_backend.wsgi:application --bind 0.0.0.0:8000 --workers 3nginx配置里把/api路径转发到8000端口,静态文件用alias指到Django的media目录。HTTPS证书用免费版就行,小程序要求必须HTTPS,但没规定证书类型,免费的够用。
部署完记得把settings.py里的DEBUG改为False,ALLOWED_HOSTS改成你的域名或服务器IP。DEBUG=True上线是非常危险的,线上报错页面会把完整堆栈暴露给用户,等于主动送漏洞。还有一个容易疏忽的地方,Django的SECRET_KEY要换掉,不要用开发时写进代码里的值,可以用环境变量注入。
数据库备份我也多说一句。档案数据丢了不是小事,哪怕只是用户借阅记录。建议每天凌晨通过crontab执行mysqldump,保留最近30天备份,异地再存一份。备份命令非常简单:
mysqldump -u root -p archive_db > /backup/archive_db_$(date +\%Y\%m\%d).sql5. 项目迭代与后续扩展方向
项目做完第一版之后,我在实际使用中又发现很多可以优化的地方,这里分享三个我自己觉得最有价值的扩展方向。
第一,扫码借阅。给每份档案生成独立的二维码,贴在档案盒上,用户扫一下就能看到档案信息和状态,直接发起借阅。实现方式不复杂,利用小程序的扫码API扫到档案ID,跳转到详情页。这个功能对实体档案多的场景特别实用。
第二,档案到期提醒。合同、证照都有有效期,系统在档案表里加一个expire_date字段,后端用celery或者定时脚本每天扫描,把快过期的档案通过订阅消息推送给管理员。办过证照年检的人应该深有体会,错过续期日期是很麻烦的事情。
第三,操作审计日志。谁在什么时间对哪份档案做了什么操作,全部记录下来。这个功能做起来不难,在视图层加一个log函数,统一记录操作人和操作内容。审计日志的价值在于出问题时能回溯,这也是档案系统区别于普通管理系统的核心能力。
我自己实际跑了一段时间后的体会是,这类管理系统的开发难点不在技术,而在对业务的理解。一开始我只想着实现增删改查,结果真用起来才发现借阅状态流转的边界情况才是关键。一份档案借出后能不能再次被借?同一用户能不能重复借同一份档案?销毁的档案要不要保留历史记录?这些问题在写代码前想清楚,后面能少改很多轮。
最后分享一个小技巧:开发阶段在Django后端写一个test_view,直接调用系统里的真实数据打印JSON接口的返回结构,前端照着结构写页面,效率比前后端同时盲改高很多。等接口联调稳定了,再把test_view删掉就行。