Django与微信小程序档案管理系统开发实战

发布时间:2026/10/6 3:15:17
Django与微信小程序档案管理系统开发实战
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_profileopenid、昵称、角色、手机号小程序登录用户扩展信息区分普通用户与管理员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_length50, uniqueTrue) sort_order models.IntegerField(排序, default0) 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_length50, uniqueTrue) title models.CharField(档案标题, max_length200) category models.ForeignKey(ArchiveCategory, on_deletemodels.PROTECT, verbose_name所属分类) location models.CharField(存放位置, max_length100, blankTrue) status models.CharField(状态, max_length20, choicesSTATUS_CHOICES, defaultin_stock) remark models.TextField(备注, blankTrue) creator models.ForeignKey(User, on_deletemodels.SET_NULL, nullTrue, verbose_name创建人) created_at models.DateTimeField(创建时间, auto_now_addTrue) updated_at models.DateTimeField(更新时间, auto_nowTrue) 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_deletemodels.CASCADE, related_nameborrow_records, verbose_name档案) borrower models.ForeignKey(User, on_deletemodels.CASCADE, verbose_name借用人) borrow_time models.DateTimeField(借出时间, auto_now_addTrue) expect_return_time models.DateTimeField(预计归还时间) actual_return_time models.DateTimeField(实际归还时间, nullTrue, blankTrue) status models.CharField(状态, max_length20, choicesSTATUS_CHOICES, defaultborrowing) class Meta: db_table borrow_record verbose_name 借阅记录三个关键设计决定我解释一下。一是archive_no做unique约束。档案编号好比人的身份证号系统里数据一多重号会导致两份档案互相覆盖这是档案系统绝不允许的。数据库层面加唯一约束比应用层判断更可靠。二是Archive的外键用了on_deletemodels.PROTECT。这是刻意选择如果删除一个分类但分类下还有档案PROTECT会直接阻止删除并报错。分类下挂了几百份档案一个误操作全没关联了这种事故不能发生。宁可让管理员先把该分类下档案转移走再允许删分类。三是BorrowRecord用外键关联Archive而不是在Archive表里堆借阅字段。这样做的好处是支持同一份档案多次借阅的历史记录每次借阅都是新纪录数据呈现完整闭环。借阅状态变化只需要update对应记录不动档案主表。模型写完之后执行数据库迁移python manage.py makemigrations python manage.py migrate执行完migrate后建议创建超级管理员后面直接用Django Admin维护基础数据python manage.py createsuperuser2.3 接口设计与视图开发接口设计我推荐一个原则先列接口清单再动手写代码。别想到哪写到哪否则前端对接时你会被自己混乱的接口命名坑死。我最终定的核心接口清单如下接口路径方法功能说明是否需要登录/api/loginPOST微信code换登录态返回token和角色否/api/archive/listGET档案分页列表支持关键词搜索是/api/archive/detailGET档案详情是/api/archive/createPOST新增档案是仅管理员/api/archive/updatePOST编辑档案是仅管理员/api/archive/deletePOST删除档案是仅管理员/api/borrow/createPOST发起借阅是/api/borrow/returnPOST归还档案是仅管理员/api/borrow/myGET我的借阅记录是/api/stats/overviewGET管理看板统计是仅管理员视图层我选择用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__icontainskeyword) | Q(archive_no__icontainskeyword) | Q(location__icontainskeyword) ) 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(idarchive_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, paramsparams, timeout5).json() openid resp.get(openid) if not openid: return JsonResponse({code: 0, msg: 微信登录失败, detail: resp}) # 查用户不存在则创建 user_profile UserProfile.objects.filter(openidopenid).first() if not user_profile: new_user User.objects.create_user(usernameopenid, passwordNone) user_profile UserProfile.objects.create(usernew_user, openidopenid) # 生成简易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: 未登录}, status401) profile UserProfile.objects.filter(tokentoken).first() if not profile: return JsonResponse({code: 0, msg: 登录已过期}, status401) 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: 无权限}, status403) 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 classborrow-form view classform-title预计借阅时长/view radio-group bindchangeonDurationChange label wx:for{{durations}} wx:key*this radio value{{item.value}} checked{{item.checked}} / text{{item.label}}/text /label /radio-group button bindtapsubmitBorrow typeprimary 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 常见问题速查表整个项目跑下来我把前端后端高概率遇到的问题整理成一张速查表问题现象可能原因解决方案登录返回40029code重复使用或code过期确认每次登录都调wx.login获取新code登录返回40125appid与secret不匹配去微信公众平台核对小程序AppSecret小程序请求后端跨域报错后端未配置CORS安装django-cors-headers并配置白名单中文查询返回乱码数据库或表字符集不是utf8mb4迁移前确认建库语句设utf8mb4列表每次上拉加载数据重复分页游标逻辑错误检查page是否在加载成功后自增新增档案提示字段过长MySQL默认字段长度不够调整varchar长度或改为TextField真机调试请求失败未关闭域名校验且使用未备案域名开发阶段用局域网IP并关闭校验管理员接口普通用户也能调用后端缺权限校验在接口上加admin_required装饰器图片加载不出来MEDIA配置错误或nginx未代理配置MEDIA_URL并在nginx加location部署后访问502gunicorn未启动或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改为FalseALLOWED_HOSTS改成你的域名或服务器IP。DEBUGTrue上线是非常危险的线上报错页面会把完整堆栈暴露给用户等于主动送漏洞。还有一个容易疏忽的地方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删掉就行。