news 2026/10/7 16:46:53

Django后端开发实战:微信小程序档案管理服务搭建

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Django后端开发实战:微信小程序档案管理服务搭建

做了两年多小程序,我越来越觉得:微信小程序这层壳其实不难,难的是背后那套能支撑业务的数据服务。这次分享的“档案宝”正是这样一个项目——前端是原生微信小程序,后端用Django搭了一套完整的档案管理服务,涵盖档案录入、分类检索、借阅登记、附件上传这些核心流程。如果你正准备做类似的工具类小程序,或者刚学完Python想找个真实的Django项目练手,这篇应该能帮你少走不少弯路。

1. 为什么档案宝选了Django:前端之外,后端才是数据命脉

1.1 小程序天生需要一套API

刚开始接触小程序的人容易有一个错觉:小程序自己就能存数据,调用一下wx.setStorageSync不就完事了?确实,本地缓存对临时状态、用户偏好这类轻量数据没问题,但档案管理的场景完全不是这样。

档案数据的典型特征是结构化、强关联、需要跨设备访问。一份档案包含编号、标题、密级、保管期限、存放位置,还关联着借阅人、借出时间、归还时间。这类数据如果全塞在小程序本地缓存里,用户换个手机就全丢了,管理员也没法统一统计。更现实的问题是,档案宝往往不止一个用户在用,数据天然要汇总到一处——这必须是服务端。

这时Django的价值就出来了。它自带ORM、Admin后台、迁移机制和一套很成熟的请求处理链路,尤其适合“管理型”业务。我当时的选型逻辑很简单:团队对Python熟悉,档案管理的字段规则多、状态流转复杂,Django的ORM和Django Admin能让我在不写大量SQL的前提下,把数据模型和后台管理快速成型。换成Node或Java也能做,但用Django的成本最低。

1.2 新手最容易卡住的三个地方

搜“python安装”“django创建app”这类词的人很多,说明大家其实知道该学什么,只是卡在细节上。我在档案宝初始化阶段就踩过几个坑,提前说清楚:

第一个坑:Python版本和Django版本的搭配。如果直接pip install django,装到的最新版可能是5.x,某些第三方库还没跟上。档案宝用的是Django 4.2 LTS,搭配Python 3.10/3.11非常稳。建议用虚拟环境,别图省事装到全局:

python -m venv venv source venv/bin/activate # Windows下是 venv\Scripts\activate pip install django==4.2.9 djangorestframework django-cors-headers

第二个坑:项目名和应用名分不清。我见过有人建完项目直接在根目录里写业务代码,后面越写越乱。Django的最佳实践是一个项目(存放全局配置)对应多个应用(每个应用负责一块业务)。档案宝我是这么拆的:

django-admin startproject archive_project cd archive_project python manage.py startapp archives python manage.py startapp users python manage.py startapp borrows

archive_project管全局配置,archives管档案本身,users管小程序用户,borrows管借阅记录。后面加功能就加app,每个app内部自成一套model、views、urls,互不干扰。

第三个坑:第一次python manage.py runserver跑起来后,访问地址是http://127.0.0.1:8000,但小程序开发者工具里填的必须是局域网IP。真机调试时,手机和电脑要在同一WiFi下,还得关掉电脑防火墙或放行8000端口。这一步卡了很多人。

1.3 档案宝的整体架构

这个项目的架构不复杂,但层次分明:

微信小程序(原生) │ wx.request / wx.uploadFile(HTTPS+Token) ▼ Django REST API(DRF视图 + JWT认证) │ ├── MySQL(生产库)/ SQLite(开发库) ├── 本地文件存储(档案附件) └── Django Admin(后台管理)

小程序端负责展示和交互,服务端负责校验、鉴权、读写数据库和存储文件。所有敏感逻辑都放后端,前端只是一个“遥控器”。这个思路贯穿整个开发过程,后面每一步都是在给这条链路填细节。

2. 工程初始化与settings配置:决定你后面少掉多少头发

2.1 项目与app的切分要带着业务去思考

很多人建项目时不在乎app怎么切,觉得“能跑就行”。但档案宝这类业务,数据模型之间天然存在边界:档案本身的元数据、用户身份信息、借阅流转记录,三者互不干扰,却又需要关联查询。如果全部塞进一个app,初期没事,后面加“审批流”“统计报表”时就想哭了。

我建议按业务域切app,而不是按技术层切。档案宝就是三个核心app加一个全局项目:

  • archives:档案条目、档案分类、存放位置;
  • users:微信用户、管理员角色、手机号绑定;
  • borrows:借阅申请、审批状态、归还记录。

跨app的关联就用外键或逻辑外键,比如BorrowRecord里存archive和user的id,查询时通过Django ORM的select_related一次性把关联对象带出来,避免N+1查询。

2.2 settings里的三处关键配置

档案宝的settings.py里,有三处配置是我花时间最多的,其他都是默认就好。

第一处:ALLOWED_HOSTS。开发时无所谓的*就够,但一旦部署到服务器,Django会校验请求的Host头。只填自己的域名就够了:

ALLOWED_HOSTS = ["api.danganbao.example", "www.danganbao.example"]

第二处:CORS。小程序端wx.request其实不受浏览器同源策略限制,但如果你用了H5调试、或者有管理后台网页,跨域就绕不开。我当时直接上了django-cors-headers,配置比较简单:

INSTALLED_APPS = [ # ... "corsheaders", ] MIDDLEWARE = [ # 注意:放在最前面 "corsheaders.middleware.CorsMiddleware", # ... ] CORS_ALLOW_ALL_ORIGINS = False CORS_ALLOWED_ORIGINS = [ "https://servicewechat.com", # 小程序请求的Referer ]

有些人觉得配CORS麻烦,干脆全部放开。档案里有密级信息,我不建议这么干。明确允许来源比默认拒绝更安全。

第三处:数据库连接。开发期我在settings.py里用的是SQLite,零配置、单文件、方便拷贝。但档案数据量上来后,并发写是个问题。上线前切到MySQL时,我建了独立的settings_prod.py,用环境变量控制连接信息:

DATABASES = { "default": { "ENGINE": "django.db.backends.mysql", "NAME": os.environ.get("DB_NAME", "danganbao"), "USER": os.environ.get("DB_USER", "root"), "PASSWORD": os.environ.get("DB_PASSWORD", ""), "HOST": os.environ.get("DB_HOST", "127.0.0.1"), "PORT": os.environ.get("DB_PORT", "3306"), "OPTIONS": { "charset": "utf8mb4", }, } }

utf8mb4很重要,档案标题里如果用户填了生僻字或emoji,老版utf8会直接报错。

2.3 开发期和生产期的迁移策略

模型定义好之后,python manage.py makemigrations和python manage.py migrate这两步不能省。我的习惯是每次改完模型就立刻生成迁移文件,并且把迁移文件提交到git里。这样团队协作时,别人拉代码后直接migrate就能同步表结构,不用靠嘴传SQL。

切换数据库时,也别指望迁移文件能自动把数据搬过去。SQLite的数据迁移到MySQL,老老实实写脚本导出导入,别在migrate上较劲。

3. 档案数据模型与ORM实战:设计表和“删除对象”的教训

3.1 四张核心表的设计

档案宝的核心数据模型我设计得很克制,没有为了“全面”而堆字段。四张表就够用:

# archives/models.py from django.db import models from django.contrib.auth.models import AbstractUser class ArchiveCategory(models.Model): name = models.CharField(max_length=50, unique=True, verbose_name="分类名称") remark = models.CharField(max_length=200, blank=True, verbose_name="备注") class Meta: db_table = "archive_category" class Archive(models.Model): SECRET_LEVELS = [ ("public", "公开"), ("internal", "内部"), ("secret", "秘密"), ("confidential", "机密"), ] STATUS_CHOICES = [ ("stored", "在库"), ("borrowed", "借出"), ("archived", "归档"), ("destroyed", "销毁"), ] archive_no = models.CharField(max_length=32, unique=True, verbose_name="档案编号") title = models.CharField(max_length=200, verbose_name="档案标题") category = models.ForeignKey(ArchiveCategory, on_delete=models.PROTECT, verbose_name="分类") secret_level = models.CharField(max_length=20, choices=SECRET_LEVELS, default="internal", verbose_name="密级") status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="stored", verbose_name="状态") storage_location = models.CharField(max_length=100, blank=True, verbose_name="存放位置") created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间") class Meta: db_table = "archive" indexes = [ models.Index(fields=["archive_no"]), models.Index(fields=["status", "secret_level"]), ] class UserProfile(models.Model): openid = models.CharField(max_length=64, unique=True, verbose_name="微信openid") nickname = models.CharField(max_length=50, blank=True, verbose_name="昵称") mobile = models.CharField(max_length=20, blank=True, verbose_name="手机号") is_admin = models.BooleanField(default=False, verbose_name="是否管理员") class Meta: db_table = "user_profile" class BorrowRecord(models.Model): BORROW_STATUS = [ ("pending", "待审批"), ("approved", "已借出"), ("returned", "已归还"), ("rejected", "已拒绝"), ] archive = models.ForeignKey(Archive, on_delete=models.PROTECT, related_name="borrow_records", verbose_name="档案") user = models.ForeignKey(UserProfile, on_delete=models.PROTECT, related_name="borrow_records", verbose_name="借阅人") status = models.CharField(max_length=20, choices=BORROW_STATUS, default="pending", verbose_name="借阅状态") borrow_time = models.DateTimeField(null=True, blank=True, verbose_name="借出时间") return_time = models.DateTimeField(null=True, blank=True, verbose_name="归还时间") reason = models.CharField(max_length=255, blank=True, verbose_name="借阅理由") class Meta: db_table = "borrow_record"

这四个模型基本覆盖了档案宝的主要业务:谁在管档案(Category/Archive)、谁在用系统(UserProfile)、档案怎么流转(BorrowRecord)。

3.2 模型设计的几个关键点

外键的on_delete不要偷懒。Archive被删除时,其下的借阅记录怎么办?我选了PROTECT,宁可在删除时报错,也不能让借阅记录指向一个不存在的档案。CASCADE看起来很省事,但在这种业务里会引发连锁误删。

unique=True本身就是索引。很多人在archive_no上既加unique=True又加index=True,其实重复了。我建索引时会考虑实际查询:按编号精确查、按状态/密级过滤,这两个组合最常见,所以给status + secret_level建了联合索引。

不要用BooleanField表达多状态。档案状态绝对不是“可见/不可见”两种,而是“在库/借出/归档/销毁”这一组状态迁移。用choices配合CharField,后面加状态只改代码,不用改表结构。

3.3 django执行查询-删除对象:一次让我差点丢数据的教训

搜索热词里有一条“django执行查询-删除对象”,看到这个我就想起早期在档案宝上踩过的一次事故。

起因是我想清理一批测试产生的“销毁”状态的档案条目,写了这么一段:

Archive.objects.filter(status="destroyed").delete()

执行完以后,我发现“在库”状态的档案数量也不对了。排查原因时才发现,在Django ORM里,QuerySet的delete()是批量删除,它会先查出所有匹配对象,再级联删除关联数据。问题出在ForeignKey的默认行为:如果没有显式指定on_delete,默认是CASCADE。我当时在某个关联表上用了默认值,结果删档案时把借阅记录全带走了。

后来我改成软删除策略,给Archive加了一个is_active字段:

# 物理删除 archive = Archive.objects.get(pk=1) archive.delete() # 软删除:只标记,不真正删 archive.is_active = False archive.save()

现在档案宝里,所有重要表的删除操作默认都走软删除。真正物理删除只发生在管理员手动确认后,并且我会先用一条get()而不是filter()来定位单条记录,避免误伤一批:

try: archive = Archive.objects.get(pk=archive_id, is_active=True) except Archive.DoesNotExist: return error_response("档案不存在或已删除") archive.is_active = False archive.save()

4. 手机号登录、10002错误与多账号轮换

4.1 小程序登录链路

档案宝的用户体系完全基于微信生态,没有自建注册页。用户打开小程序,前端拿到wx.login返回的code,传给Django后端:

// 小程序端 wx.login({ success: (res) => { wx.request({ url: "https://api.danganbao.example/api/auth/login", method: "POST", data: { code: res.code }, success: (resp) => { const { token } = resp.data.data; wx.setStorageSync("token", token); }, }); }, });

Django后端拿到code后,调用微信的jscode2session接口,用appid + secret + code换openid和session_key:

import requests from django.conf import settings def code2session(code): resp = requests.get( "https://api.weixin.qq.com/sns/jscode2session", params={ "appid": settings.WX_APPID, "secret": settings.WX_SECRET, "js_code": code, "grant_type": "authorization_code", }, timeout=5, ) data = resp.json() openid = data.get("openid") session_key = data.get("session_key") return openid, session_key

拿到openid后,在UserProfile表里查或建用户,然后用PyJWT签发一个自己的token给小程序端。这里我特别说明一下:不要拿微信的session_key当业务token用,它只是配合解密数据的密钥,有效期短,不适合做长期登录态。我一般签发30天有效的JWT,过期后前端重新走一次wx.login即可。

4.2 获取手机号的完整过程

档案宝有“绑定手机号”的需求,方便管理员线下联系借阅人。2023年后的接口规则变了,不再需要用户手动输入手机号,而是用<button open-type="getPhoneNumber">弹窗授权:

<button open-type="getPhoneNumber" bindgetphonenumber="onGetPhoneNumber"> 绑定手机号 </button>

前端拿到code后传给后端:

onGetPhoneNumber(e) { if (e.detail.errMsg !== "getPhoneNumber:ok") return; wx.request({ url: "https://api.danganbao.example/api/auth/bind-mobile", method: "POST", data: { code: e.detail.code, }, header: { Authorization: "Bearer " + wx.getStorageSync("token"), }, }); }

后端用code调用微信的phonenumber.getPhoneNumber接口(注意这个接口需要在小程序后台开通权限,并且有调用次数限制)。回传数据里的phone_info就是用户的完整手机号,存库即可。

4.3 联调中的10002错误与多账号池轮换

开发档案宝时,我经常遇到一个报错:getPhoneNumber:fail,后端日志里显示微信返回错误码10002。

这个错误码在jscode2session和getPhoneNumber场景里经常出现,原因是客户端传入的code已经失效或者被重复使用。wx.login的code有效期只有5分钟,而且只能用一次。如果你在短时间内用同一个code调了两次接口,第二次必然报错。还有一种情况是开发者工具里的小程序appid和后端配置的appid不一致,微信直接不认。

排查这类问题,我一般按顺序检查:

  1. 确认后端WX_APPID和开发者工具里的appid一致;
  2. 确认code只使用一次,日志里看有没有重复提交;
  3. 确认服务器时间和实际时间差异不大,时钟偏差也会影响code校验;
  4. 确认小程序后台已配置“获取手机号”接口权限。

至于“多账号池轮换”,这是团队开发时的真实需求。微信小程序体验版和开发版对同一微信号的设备登录有限制,一个微信号反复切换容易触发风控。后来我维护了一个“测试微信账号池”,在开发者工具里通过“多账号调试”切换不同微信号,再配合Django后端按openid区分数据,测试借阅流程时就不用担心数据互相污染。

5. 档案列表接口与小程序分页:从API到“加载更多”

5.1 一个能用的列表接口长什么样

档案宝最核心的页面是档案检索列表。我没有直接用JsonResponse手写数据组装,而是用了DRF的GenericAPIView加PageNumberPagination:

# archives/views.py from rest_framework.pagination import PageNumberPagination from rest_framework.generics import ListAPIView from .models import Archive from .serializers import ArchiveListSerializer class ArchivePagination(PageNumberPagination): page_size = 10 page_size_query_param = "page_size" max_page_size = 50 class ArchiveListView(ListAPIView): queryset = Archive.objects.filter(is_active=True).select_related("category") serializer_class = ArchiveListSerializer pagination_class = ArchivePagination

对应序列化器:

# archives/serializers.py from rest_framework import serializers from .models import Archive class ArchiveListSerializer(serializers.ModelSerializer): category_name = serializers.CharField(source="category.name", read_only=True) class Meta: model = Archive fields = ["id", "archive_no", "title", "category_name", "secret_level", "status", "storage_location"]

DRF的核心价值不是少写代码,而是让分页、序列化、权限控制这些高频逻辑有标准做法。曾想过全部手写,但到搜索、排序、过滤都要自己处理时,你会发现DRF是一个成熟的选择。

5.2 小程序端的分页与加载更多

列表页我采用经典的“上拉加载更多”方案。小程序端的onReachBottom触发时,带上page参数请求下一页:

Page({ data: { archives: [], page: 1, hasMore: true, loading: false, }, loadArchives() { if (this.data.loading || !this.data.hasMore) return; this.setData({ loading: true }); wx.request({ url: "https://api.danganbao.example/api/archives/", data: { page: this.data.page, page_size: 10, }, header: { Authorization: "Bearer " + wx.getStorageSync("token"), }, success: (res) => { const list = res.data.data.results || []; const hasMore = this.data.page * 10 < res.data.data.count; this.setData({ archives: this.data.archives.concat(list), page: this.data.page + 1, hasMore, loading: false, }); }, fail: () => { this.setData({ loading: false }); }, }); }, onReachBottom() { this.loadArchives(); }, });

这里有几个容易出bug的点:

  • 合并数据用concat,不要直接setData覆盖,否则上一页内容会消失;
  • hasMore的判断用page * page_size < count,不要依赖“返回的条数是否等于page_size”,因为最后一页可能刚好等于10条,导致多请求一次空页;
  • loading标志要有,否则onReachBottom会连续触发多个请求,出现数据重复或顺序错乱。

5.3 顶部导航栏高度适配

很多人在小程序里做自定义导航栏时,卡在“刘海屏高度”问题上。档案宝的列表页用了自定义导航栏,标题要垂直居中,不同机型偏移严重。

后来我总结了标准做法:用胶囊按钮位置算导航栏高度,而不是写死64px:

const getNavBarHeight = () => { const windowInfo = wx.getWindowInfo(); const menuButton = wx.getMenuButtonBoundingClientRect(); const navBarHeight = (menuButton.top - windowInfo.statusBarHeight) * 2 + menuButton.height; return { statusBarHeight: windowInfo.statusBarHeight, navBarHeight, }; };

这样算出的高度,配合自定义导航栏组件,在iPhone和安卓上的表现基本一致。顶部导航栏高度这个高频热词,本质上就是“不同机型的胶囊位置不同,直接用固定值必翻车”。

5.4 查询性能:别让小数据量骗了你

开发时数据量小,列表怎么查都快。但档案数据到几万条以后,一个没建索引的filter就会让接口变慢。档案宝上线一段时间后,我统计过:列表接口慢的请求,SQL里往往都带着LIKE '%关键字%'这种全表扫描。

优化思路是分三步走:

  1. 给经常作为查询条件的字段建索引(archive_no、category_id、status);
  2. 全文搜索场景不要用LIKE,上MySQL FULLTEXT或者接入Meilisearch这类搜索引擎;
  3. 列表数据用select_related把外键一次取出来,避免每行数据触发一次数据库查询。

实测下来,加了索引和select_related之后,列表接口从800ms降到120ms左右,体感变化非常明显。

6. 文件上传与存储:档案附件不是塞进数据库就完事

6.1 media配置与文件路径设计

档案宝里有“上传扫描件/照片”的需求,保存到数据库的不应该是文件本体,而是Django帮你管理的FileField。相关配置如下:

# settings.py MEDIA_URL = "/media/" MEDIA_ROOT = os.path.join(BASE_DIR, "media")

上传的文件默认存在media/目录下。档案附件我按“档案编号/上传时间”分目录,防止同一编号下文件互相覆盖,也方便日后人工整理:

import os import time def archive_file_path(instance, filename): ext = os.path.splitext(filename)[1].lower() day = time.strftime("%Y%m%d") return f"archives/{instance.archive.archive_no}/{day}_{filename}"

FileField的upload_to支持传入可调用对象,这个.archive.archive_no的引用方式,能保证不同档案的文件物理隔离。

6.2 上传接口与文件校验

小程序端wx.uploadFile上传文件时,Django这边接收的是request.FILES。接口里必须做至少三层校验:

# archives/views.py from rest_framework.views import APIView from rest_framework.parsers import MultiPartParser, FormParser from django.core.exceptions import ValidationError class ArchiveFileUploadView(APIView): parser_classes = [MultiPartParser, FormParser] def post(self, request, archive_id): archive = Archive.objects.get(pk=archive_id, is_active=True) upload_file = request.FILES.get("file") if not upload_file: return error_response("缺少文件") # 1. 类型校验 allowed_ext = [".pdf", ".jpg", ".jpeg", ".png"] ext = os.path.splitext(upload_file.name)[1].lower() if ext not in allowed_ext: return error_response("不支持的文件类型") # 2. 大小校验 if upload_file.size > 20 * 1024 * 1024: return error_response("文件大小不能超过20MB") # 3. 内容头校验(防止伪造扩展名) head = upload_file.read(8) if ext in [".jpg", ".jpeg"] and not head.startswith(b"\xff\xd8"): return error_response("图片文件不完整") upload_file.seek(0) archive.file = upload_file archive.save() return success_response({"url": archive.file.url})

第一层校验扩展名,第二层限制大小,第三层读文件头。这三层都过了,才能基本确认这个文件是安全的。

6.3 小程序上传的注意事项

小程序端上传的核心代码不复杂,但有两个细节常被忽略:

wx.uploadFile({ url: "https://api.danganbao.example/api/archives/123/files/", filePath: filePath, name: "file", header: { Authorization: "Bearer " + wx.getStorageSync("token"), }, success: (res) => { const data = JSON.parse(res.data); // 处理上传结果 }, });

一是header里要带token,wx.uploadFile不会自动带上wx.request里的公共header;二是后端返回的res.data是字符串,需要JSON.parse。这两个问题我排查过不止一次。

6.4 文件安全:权限控制不能只靠“路径没人知道”

MEDIA_URL默认是对外暴露的,任何人猜到路径就能访问。档案宝里我做了两层防护:

第一层,文件名不用原始文件名,入库时重命名为随机字符串,同时把原始文件名存到数据库字段里,用户下载时再还原:

import uuid import os def secure_filename(instance, filename): ext = os.path.splitext(filename)[1].lower() return f"archives/{instance.archive.archive_no}/{uuid.uuid4().hex}{ext}"

第二层,敏感档案不做永久URL,而是提供一个带短期有效签名的下载接口,校验用户身份后临时返回文件流。这样即使路径泄露,没有身份也一样拿不到文件。

7. 联调、抓包与上线:把档案宝推到正式环境

7.1 Charles抓包小程序接口:联调必备

开发联调阶段,最头疼的是前端说“接口报错”,后端说“我这里没看到请求”。这时候Charles这类抓包工具能让你看到完整的请求/响应链路。

Charles配合小程序抓包的几个关键设置:

  • 手机和电脑连同一个WiFi,手机代理指向电脑IP的8888端口;
  • 在小程序开发者工具里勾选“不校验合法域名”,方便本地调试;
  • SSL代理要开启,否则看不到https明文请求;
  • 手机安装Charles的根证书,并信任证书。

用抓包工具能看到很多东西:小程序实际发出的请求头、后端返回的完整响应体、某个接口耗时到底在哪一段。有一次档案列表加载慢,前端怀疑是后端SQL问题,我用Charles看到响应时间确实很长,但进一步分析发现慢在服务器带宽上,图片附件加载占了大头,和SQL没关系。没有抓包数据,这种问题只能靠猜。

7.2 上线部署的清单

档案宝上线时,我踩了一遍所有新手都会踩的部署坑,整理出一份清单:

  1. Django关闭调试模式:DEBUG=False,ALLOWED_HOSTS必须配好;
  2. 用gunicorn或uwsgi启动应用,别用runserver扛生产流量;
  3. 静态文件和media文件交给Nginx托管,Django本身不擅长服务静态资源;
  4. 小程序要求所有请求域名必须https且已备案,Nginx上配置SSL证书;
  5. 小程序后台“服务器域名白名单”里添加你的API域名,不配置的话线上环境直接报fail url not in domain list;
  6. 数据库连接数优化,Django的连接池虽然有限,但CONN_MAX_AGE要设置,避免每次请求重建连接。

部署时的Nginx配置,核心片段如下:

server { listen 443 ssl; server_name api.danganbao.example; ssl_certificate /etc/nginx/ssl/danganbao.pem; ssl_certificate_key /etc/nginx/ssl/danganbao.key; client_max_body_size 25m; location /media/ { alias /var/www/danganbao/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-Proto $scheme; } }

client_max_body_size 25m不能省,否则超过1MB的文件上传会被Nginx直接拒绝,而后端日志里看不到任何报错。

7.3 必须做好的三件事:备份、日志、权限复核

上线不是终点。档案宝跑了几个月后,我最大的心得是运维基本功比功能开发更重要。

备份:数据库每天自动备份,文件附件每周做一次增量备份到独立磁盘。有一天服务器磁盘满了,备份任务静默失败,要不是我习惯性检查备份产物,可能连恢复点都没有。

日志:Django的日志配置明确区分了django.request和业务日志。接口报错时,看到的是具体的异常堆栈而不是一句Internal Server Error。小程序端传参不规范导致的4xx错误,也要能从日志里回溯到具体用户。

权限复核:每新增一个接口,我都会问自己:这个接口能不能被未登录用户调用?如果必须登录,JWT校验有没有漏?管理员接口有没有放在is_admin权限之下?做小程序项目,最大的风险不是写错功能,而是哪个接口忘了加权限,把档案数据裸奔在公网上。

最后说点实际的

档案宝从开发到上线,我最大的体会是:这类管理型小程序的难点不在技术炫技,而在数据模型设计得够不够稳、接口权限收得够不够紧、部署细节处理得够不够细。Django在其中扮演的角色更像一个“可靠的大后方”,把用户、档案、借阅、文件这些复杂关系打理清楚,小程序端只需要专注交互体验。

如果你正在做一个类似的小程序,我的建议是:先花时间把数据模型画清楚,再写一行代码;上线前把备份和日志配好,再谈功能迭代。这套做法帮我在档案宝项目上少熬了无数个深夜,希望也能帮到你。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/7 16:46:09

MODIS地表温度数据处理全流程:QC解析、坐标配准与不确定性量化

简介&#xff1a;本资源为2022年中国全域1km分辨率地表温度&#xff08;LST&#xff09;空间分布数据集&#xff0c;基于NASA MODIS MOD11A2产品加工生成&#xff0c;面向遥感、地理信息、生态与气候研究领域的科研人员及GIS初学者&#xff0c;支撑区域热环境分析、城市热岛评估…

作者头像 李华
网站建设 2026/10/7 16:45:38

Linux线程详解:从pthread创建到同步互斥与死锁排查

刚接触Linux线程时&#xff0c;我犯过一个特别低级的错误&#xff1a;在线程入口函数里直接操作了一个全局变量&#xff0c;两个线程同时跑&#xff0c;结果那个计数器忽大忽小&#xff0c;跟抽风一样。后来慢慢啃完概念、踩过死锁的坑&#xff0c;才算摸清这套东西的脾气。今天…

作者头像 李华
网站建设 2026/10/7 16:45:38

江苏土壤类型标准Shapefile:可计算、可配准、可建模的GIS生产级数据

简介&#xff1a;本资源为江苏省土壤类型空间分布标准GIS数据集&#xff0c;面向地理信息、农业遥感、环境科学等领域的科研人员与高校师生&#xff0c;支撑区域土壤属性分析、生态评估及空间建模等基础研究工作。数据基于1∶400万中国土壤图构建&#xff0c;采用三位数字编码体…

作者头像 李华
网站建设 2026/10/7 16:45:17

微服务架构稳定性实践:服务保护与分布式事务方案对比与选型

做微服务这几年&#xff0c;我收到最多的技术问题其实翻来覆去就两类&#xff1a;线上服务无缘无故被打垮&#xff0c;然后数据账目对不上。前者是 服务保护 没做好&#xff0c;后者是 分布式事务 没捋清。尤其当你把单体应用拆成十几个微服务之后&#xff0c;这两个问题会…

作者头像 李华
网站建设 2026/10/7 16:44:33

参数服务器架构详解:从同步异步到分布式训练实践

简介&#xff1a;基于参数服务器架构的分布式深度学习解决方案&#xff0c;面向需要处理海量数据与复杂模型的研究者、工程师以及高校学生&#xff0c;适用于毕业设计、课程设计、期末大作业和机器学习实战。方案以参数服务器统一维护全局参数&#xff0c;多个工作节点各自处理…

作者头像 李华