ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Django共享单车后台管理系统开发实战:从数据模型到Admin定制

Django共享单车后台管理系统开发实战:从数据模型到Admin定制 简介面向Python Web开发者的Django实战项目基于Django框架与pymysql数据库驱动构建共享单车后台管理系统完整覆盖用户注册登录、单车状态管理、骑行订单记录、区域收入统计等核心业务模块。资源共64个文件以Python源码与编译文件为主搭配HTML模板、CSS/JS静态资源、项目配置文件、SQL脚本及SQLite数据库等压缩包约207KB目录结构清晰便于直接运行和二次开发。项目遵循Django的MTV设计模式通过模型、视图、模板分层实现业务逻辑与页面展示分离并借助pymysql连接MySQL完成数据持久化同时包含URL路由、settings配置、数据迁移等工程化细节。适合希望借助完整案例理解Django开发全流程的初中级开发者也适合作为课程设计或毕业设计的参考。已有525人学习下载资源内容紧凑、知识密度高能够快速上手并迁移到类似管理系统的开发中。1. 共享单车后台管理系统Django 把运维台搬进浏览器共享单车后台管理系统听起来是一张中规中矩的表单页面但实际跑起来就会碰到订单计费扣错了、地图上三十辆车没动静、用户押金异常退款这类运营问题。这个系统之所以选 Django不是因为它的 ORM 多聪明而是自带的 Admin 后台能覆盖「查、改、筛、批量操作」这些运维日常不必为内部工具单独搭一套前端。本文面向两类人拿它做毕业设计或课程项目的同学以及小团队里需要给运营和客服快速搭建后台的开发者。后面按「数据模型 → Admin 定制 → 统计视图 → 生产部署」的路径把共享单车管理后台的完整技术骨架讲清楚每一步都有可复现的代码和参数说明。真正耗时间的不在页面而在模型的状态设计。2. Django 数据模型设计从车辆状态机到订单计费2.1 先定实体关系用户、车辆、订单、计费规则项目创建好之后第一步是用python manage.py startapp bikes创建 bikes 应用。共享单车后台的核心实体其实就四张表用户、车辆、骑行订单、计费规则外加一张故障上报表做运维记录。用户表直接继承 Django 自带的 AbstractUser加上手机号和押金状态两个字段车辆表要注意经纬度用 DecimalField 而不是 FloatField否则地图上关锁位置会偏移。订单表是系统里数据增长最快的表它的外键和状态字段的索引直接影响后续 Admin 列表页的查询速度。# bikes/models.py from django.contrib.auth.models import AbstractUser from django.db import models class User(AbstractUser): mobile models.CharField(手机号, max_length11, uniqueTrue) deposit_status models.BooleanField(押金状态, defaultFalse) class Meta: verbose_name 用户 verbose_name_plural verbose_name class Vehicle(models.Model): VEHICLE_STATUS [ (idle, 待租), (riding, 骑行中), (fault, 故障), (repairing, 维修中), ] bike_no models.CharField(车辆编号, max_length20, uniqueTrue) latitude models.DecimalField(纬度, max_digits9, decimal_places6) longitude models.DecimalField(经度, max_digits9, decimal_places6) battery models.IntegerField(电量, default100) status models.CharField(状态, max_length10, choicesVEHICLE_STATUS, defaultidle) update_time models.DateTimeField(最后上报时间, auto_nowTrue) class Meta: verbose_name 车辆 verbose_name_plural verbose_name def __str__(self): return self.bike_no这里的 VEHICLE_STATUS 刻意用二维元组DB 里存英文短码Admin 列表和表单自动显示中文。状态字段用字符串而不是数字是为了过几个月新增状态时不用翻代码回忆 3 代表什么。电量存 0-100 的整数后台按百分比显示即可没必要带单位。经纬度用 DecimalField 是地图类系统最常踩的坑Float 在 Web 层做反序列化和地图聚合时会产生微小偏差轻则定位偏移重则地图打点糊成一团。订单表的关键不是字段多而是要把「结算金额」冗余在订单上。为什么不实时调计费规则算金额因为运营半年后调价是常事历史订单如果依赖当前计费规则到时候整张报表的数字全变了财务核对会非常痛苦。所以订单表存的是计价快照不是计价引用。class TripOrder(models.Model): ORDER_STATUS [ (ongoing, 进行中), (finished, 已完成), (abnormal, 异常), (refunded, 已退款), ] user models.ForeignKey(User, on_deletemodels.PROTECT, related_nameorders, db_indexTrue, verbose_name用户) vehicle models.ForeignKey(Vehicle, on_deletemodels.PROTECT, related_nameorders, db_indexTrue, verbose_name车辆) start_time models.DateTimeField(开始时间, auto_now_addTrue, db_indexTrue) end_time models.DateTimeField(结束时间, nullTrue, blankTrue) start_position models.CharField(起点, max_length100, blankTrue) end_position models.CharField(终点, max_length100, blankTrue) amount models.DecimalField(实收金额, max_digits8, decimal_places2, default0) status models.CharField(状态, max_length10, choicesORDER_STATUS, defaultongoing, db_indexTrue) class Meta: verbose_name 骑行订单 verbose_name_plural verbose_name permissions [ (force_finish_order, 可以强制结束异常订单), (export_order, 可以导出订单数据), ]外键的 on_delete 用 PROTECT 而不是 CASCADE因为车辆和用户不允许从库里硬删只能改状态。PROTECT 会阻止删除被引用对象避免「骑着骑着车没了」的脏数据。db_indexTrue 的三个字段是后台列表页最常做筛选和排序的提前加索引能明显减少大数据量下的超时。权限定义放在 Meta 里是 Django 的标准做法后面 Admin 编排角色时会用到。2.2 状态机放在 Model 层拦住误操作车辆状态和订单状态不是互相独立的它们之间是一组流转约束车辆只能从 idle 变成 riding订单从 ongoing 到 finished 时车辆才能回到 idle出现故障时车辆进入 fault对应订单标记为 abnormal 并走退款。这张流转约束表如果散落在各处视图代码里半年后就是灾难。当前车辆状态允许动作目标车辆状态对应订单状态idle开锁ridingongoingriding关锁结算idlefinishedriding上报故障faultabnormalfault维修下线repairing-repairing维修完成idle-常见做法是把状态变更方法收敛在 Model 层所有合法流转集中在一处校验。这样 Admin 的 action、自定义视图、甚至以后的接口层都只能走同一套规则。# bikes/models.py class Vehicle(models.Model): # ...字段省略... def start_ride(self): if self.status ! idle: raise ValueError(f车辆当前状态为{self.status}不能开锁) self.status riding self.save(update_fields[status, update_time]) def finish_ride(self): if self.status ! riding: raise ValueError(车辆不在骑行中不能闭合订单) self.status idle self.save(update_fields[status, update_time])start_ride 和 finish_ride 把非法状态流转直接拦在 Model 入口视图和 Admin action 只需要调用这两个方法。save 里的 update_fields 只更新 status 和 update_time 两个字段避免触发整行写入和并发下的覆盖问题。Admin 本身是个几乎无条件信任操作者的入口筛选框和 action 点错一次就影响线上状态状态机方法能把误操作挡在最底层。2.3 ORM 查询后台报表、批量删除对象的正确姿势后台系统里高频操作是根据条件批量删除或导出。Django 执行删除对象时有个常见坑对切片后的 QuerySet 调用 delete() 会直接抛 NotSupportedError因为分片后的 QuerySet 不能执行批量删除。清理三个月前的已退款订单时我一般先取出一批主键再按主键删除from datetime import timedelta from django.utils import timezone from bikes.models import TripOrder expired_time timezone.now() - timedelta(days90) ids list( TripOrder.objects .filter(end_time__ltexpired_time, statusrefunded) .values_list(id, flatTrue)[:200] ) TripOrder.objects.filter(id__inids).delete()先去 200 个主键再用 id__in 删除单次删除量控制在 200 条以内避免长事务锁表和 MySQL 大事务回滚问题。删除操作是不可逆的运营后台建议禁止直接从列表页删订单而是把订单标记为 refunded 或 abnormal保留审计痕迹。报表类的查询要用聚合函数。计算单日营收的前 7 天趋势是把values(start_time__date)当作 GROUP BY 的字段再用 Sum 汇总from django.db.models import Sum daily_report ( TripOrder.objects .filter(statusfinished) .values(start_time__date) .annotate(incomeSum(amount)) .order_by(-start_time__date)[:7] )这个查询返回的每个 dict 里多了一个 income 键就是当天完成的订单金额合计。要注意 annotate 产生的聚合字段默认按分组字段升序排列上面用 order_by 显式指定了倒序报表显示才符合直觉。假如运营要看每辆车今天的骑行次数记住一个边界条件跨表 filter 会先做 JOIN 再过滤把没有订单的车整体剔除要保留全量车辆需要把过滤条件挪进 Count 里。3. Django Admin 定制把「能用」的后台改成「好用」的运营台3.1 注册模型并配置列表页先让数据看得到Django Admin 最大的价值是零前端代码生成 CRUD但默认界面对运营来说称不上好用。注册模型后第一件要做的事是配置 list_display、list_filter、search_fields 这三个基础项。拿车辆管理来说运营每天看的是车辆状态、电量和最后上报时间运维可能还要按区域搜索车辆编号。# bikes/admin.py from django.contrib import admin from .models import Vehicle admin.register(Vehicle) class VehicleAdmin(admin.ModelAdmin): list_display (bike_no, status, battery, latitude, longitude, update_time) list_filter (status, battery) search_fields (bike_no,) list_editable (battery,) list_per_page 50 date_hierarchy update_timeAdmin 配置项作用这里的典型值list_display列表页展示哪些列bike_no, status, batterylist_filter右侧过滤栏status, batterysearch_fields顶部搜索框生成 LIKE 查询bike_nolist_editable列表页可直接编辑的字段batterylist_per_page每页记录数50date_hierarchy按时间逐级下钻筛选update_timebattery 是 IntegerFieldDjango 会自动把 list_filter 渲染成数值区间date_hierarchy 会在页面顶部生成年-月-日的下钻链路省去手写时间筛选表单。list_editable 让运维可以在列表页直接改电量不用点进详情页适合批量巡检车况的场景。注意 list_editable 的字段必须同时在 list_display 里出现这是 Django 的硬性要求。3.2 自定义 Action 批量处理订单与车辆状态运营后台最常做的事是「选中一批记录执行同一个操作」。比如巡街发现 20 辆车报故障让运维在列表页勾选后一键标记客服接到用户投诉骑行中锁坏了需要强制结束异常订单。Django Admin 的 action 就是干这个的。# bikes/admin.py from django.contrib import admin from django.utils import timezone from .models import TripOrder admin.register(TripOrder) class TripOrderAdmin(admin.ModelAdmin): list_display (id, user, vehicle, start_time, end_time, amount, status) list_filter (status, start_time) ordering (-start_time,) actions (force_finish,) admin.action(description强制结束选中的异常订单) def force_finish(self, request, queryset): count 0 for order in queryset.filter(statusongoing): order.status finished order.end_time timezone.now() order.amount 2.00 order.save() count 1 self.message_user(request, f已强制结束 {count} 个订单)这里刻意不用 queryset.update()是因为要同步写入 end_time 和 amount而 amount 按 2 元固定价结算逐条调 order.save() 才方便以后改成按时长计费。queryset.filter(statusongoing) 会在循环前做一次过滤避免把已结束的订单再算进去。循环里逐条 save 单条记录数据量大时性能不如 update但订单量在共享单车后台通常一天几千条完全够用。action 的 description 会直接显示在下拉框里不用用户理解函数名。如果以后要支持真正的批量改价可以用 F 表达式配合 update 做原子更新避免循环from django.db.models import F queryset.update(amountF(amount) 2.00)F 表达式把计算下推到数据库执行期间即使有并发订单写入也不会出现读改写之间的丢更新。3.3 权限分层客服看不到金额运营导不了订单共享单车后台的使用者有客服、运营、财务、运维四类人天然需要权限隔离。Django 自带的 User-Group-Permission 结构够用不用引第三方库。权限分两层第一层是 Django 默认的 add/change/delete/model 级别权限第二层是业务权限在 TripOrder.Meta.permissions 里已经定义了 force_finish_order 和 export_order。# bikes/admin.py class TripOrderAdmin(admin.ModelAdmin): # ...上面的配置省略... def get_readonly_fields(self, request, objNone): readonly super().get_readonly_fields(request, obj) if not request.user.has_perm(bikes.export_order): readonly (amount,) return readonly def has_export_order_permission(self, request): return request.user.has_perm(bikes.export_order)has_perm 的权限字符串格式必须是app_label.codename这里 app_label 是 bikescodename 是 export_order拼起来就是 bikes.export_order。客服组不勾选这个权限列表页和详情页都看不到金额字段导出订单的 action 也不显示。权限粒度到字段级别后财务和客服之间的人为矛盾少很多。3.4 Admin 界面美化simpleui 是最省事的方案共享单车后台是给运营天天盯着用的默认 Admin 的英文面包屑和生硬表格确实不好看。目前 Python 社区最省事的「admin 界面美化」方案是 django-simpleui安装后把 simpleui 放在 INSTALLED_APPS 的最前面就能自动替换登录页和侧边栏样式。# config/settings.py INSTALLED_APPS [ simpleui, django.contrib.admin, # ...其他应用... ]simpleui 会接管 Admin 的全部静态资源如果部署在内网环境且不能访问外网 CDN需要把 simpleui 依赖的资源文件一起收集到 STATIC_ROOT否则样式会丢失。不想引入第三方库时可以改模板里的 branding新建 templates/admin/base_site.html 覆盖标题即可。界面美化的意义是减少客服手滑点错不是给自己看的能保持默认布局、只改品牌名其实已经够用。4. 自定义业务视图从 Admin 走向可扩展的运营页面4.1 为什么还要写自定义视图Admin 适合快速查改但 Dashboard、趋势图、Excel 导出这些运营刚需它天生不行。项目的常见结构是 Admin 负责数据维护单独一组 URLs 和 Views 负责决策支持。如果将来要接小程序或独立 App就要走「django vue 前后端分离」的路线Admin 只做 JSON API前端用 vue3 搭一套运营台。共享单车系统的现实情况往往是两者都要内部后台用模板渲染面向用户的小程序和 App 走 REST 接口本章围绕内部后台展开。4.2 仪表盘视图与 URL 配置先在 bikes/urls.py 里把运营页面的路由挂上然后在项目根路由 include 进去。步骤是固定的先建 urls 文件、再写视图、再渲染模板。# bikes/urls.py from django.urls import path from . import views urlpatterns [ path(dashboard/, views.dashboard, nameops-dashboard), path(dashboard/export, views.export_orders_csv, nameops-export-orders), ]# bikes/views.py from django.shortcuts import render from django.db.models import Sum from django.utils import timezone from .models import TripOrder, Vehicle def dashboard(request): today timezone.localdate() context { vehicle_total: Vehicle.objects.count(), vehicle_fault: Vehicle.objects.filter(statusfault).count(), order_today: TripOrder.objects.filter(start_time__datetoday).count(), income_today: TripOrder.objects.filter( start_time__datetoday, statusfinished ).aggregate(totalSum(amount))[total] or 0, } return render(request, bikes/dashboard.html, context)aggregate 返回的是字典key 是 total也就是 Sum(amount) 的别名。当天还没有完成订单时聚合结果是 None所以后面接 or 0否则模板里会出现空白。timezone.localdate() 拿的是服务器当前时区的日期前提是 settings.py 的 TIME_ZONE 已经正确配置为 Asia/Shanghai否则统计口径会偏 8 小时。4.3 用 ORM 聚合分析车辆利用率判断一张地图上哪些车该调度核心指标是「今日骑行次数」和「单次平均时长」。用 annotate 给每辆车附加聚合值按次数倒序找出前 20 台活跃车。from django.db.models import Count, Q today_start timezone.now().replace(hour0, minute0, second0, microsecond0) active_vehicles ( Vehicle.objects .annotate(today_ordersCount( orders, filterQ(orders__start_time__gtetoday_start) )) .order_by(-today_orders)[:20] )注意这里 Count 里的 filter 是 Dango 2.0 起支持的条件聚合写法它先对 orders 关联表做筛选再按车辆分组计数所以没有订单的车辆也会保留在结果里today_orders 为 0。如果换成 Vehicle.objects.filter(orders__start_time__gtetoday_start).annotate(...)那么今天没有订单的车会被整体过滤掉运营看到的列表就不完整了。两种写法返回的数据范围不同做报表时优先用条件聚合。4.4 导出 CSV浏览器直接预览表格很多「后台管理系统在线预览 Excel 表格」需求内部实现其实就是导出 CSV。CSV 在浏览器里可以直接查看结构Excel 双击打开也无障碍如果需要真正的 .xlsx 预览常见做法是 openpyxl 生成后在前端用 SheetJS 渲染但企业内网通常不允许外链脚本所以 CSV 方案最稳。用 Python 标准库 csv 就能做不引入第三方依赖。import csv from django.http import HttpResponse from .models import TripOrder def export_orders_csv(request): response HttpResponse(content_typetext/csv) response[Content-Disposition] attachment; filenameorders.csv writer csv.writer(response) writer.writerow([订单号, 用户, 车辆, 开始时间, 结束时间, 金额]) orders TripOrder.objects.select_related(user, vehicle).all() for order in orders: writer.writerow([ order.id, order.user.username, order.vehicle.bike_no, order.start_time.strftime(%Y-%m-%d %H:%M:%S), order.end_time.strftime(%Y-%m-%d %H:%M:%S) if order.end_time else , order.amount, ]) return responseselect_related 是这里的关键。for 循环里访问 order.user 和 order.vehicle 都会触发一条额外 SQL不联表的话导出 1000 条订单会产生 2000 条附加查询页面会卡到超时。select_related 把外键 JOIN 进一条 SQL1000 条订单只剩一次查询。end_time 为空的订单还在骑行中导出时格式化成空字符串避免模板渲染报错。4.5 重定向、消息传递与 URL 反向解析自定义视图处理完数据后最标准的手法是重定向回 Admin 列表页同时通过 messages 框架往下一页携带操作结果这是 Web 后台避免重复提交的常规方案。from django.contrib import messages from django.shortcuts import redirect from django.urls import reverse def force_finish_view(request, order_id): order TripOrder.objects.get(idorder_id) order.amount 2.00 order.status finished order.save(update_fields[amount, status]) messages.success(request, f订单 {order.id} 已强制结束) return redirect(reverse(admin:bikes_triporder_changelist))用 reverse 而不是硬编码 /admin/bikes/triporder/以后改了 Admin URL 规则不用回头改视图代码。调试 routing 时可以在 Django shell 里用 django.urls.resolve 验证一个路径到底对应哪个视图和 urls 名称。from django.urls import resolve match resolve(/ops/dashboard/) print(match.func.__name__) # dashboard print(match.url_name) # ops-dashboard如果 resolve 抛 Resolver404说明 URLconf 里没有这个路径先检查 include 的挂载路径再检查 urls 里的 name 是否拼写正确。5. 生产部署清单宝塔部署 Django 时要手写的五个重点5.1 环境准备与依赖锁定在宝塔面板「网站」菜单里新建 Python 项目之前先把依赖装全。共享单车后台的 requirements.txt 我会这样锁版本django4.2,5.0 gunicorn21.2.0 mysqlclient2.2.0 django-simpleui2024.3.0mysqlclient 在部分内网机器上装不上常见做法是把数据库驱动换成 pymysql并在 settings.py 里打补丁import pymysql pymysql.install_as_MySQLdb()pymysql 是纯 Python 实现免编译能避开宝塔默认 Python 环境缺少 libmysqlclient-dev 导致编译失败的问题。5.2 上线前的项目配置settings.py 里必须归位的三个点DEBUGFalse、ALLOWED_HOSTS 填上服务器 IP 或域名、STATIC_ROOT 指向收集目录。然后运行 collectstatic把 Django Admin 和 simpleui 的静态资源收到正式目录否则登录页会是裸 HTML。python manage.py collectstatic --noinput python manage.py migrate python manage.py createsuperuser5.3 启动 gunicorn 与验证宝塔的 Python 项目管理器里启动方式选 gunicorn启动命令写gunicorn config.wsgi:application --bind 0.0.0.0:8000 --workers 3 --timeout 120运行目录指向 manage.py 所在目录Python 版本选 3.10 以上。验证分三步curl /admin 在终端里是否返回 200登录一次看后台是否正常再在宝塔监控里看内存是否稳定。如果 gunicorn 启动失败先去看日志里的 ModuleNotFoundError把缺的包装回来再重启。注意 --workers 3 不要盲目调大这个后台同时在线运维者一般不超过 20 人3 个 worker 配 2 线程是最稳的起步配置瓶颈通常出现在 MySQL 连接数而不是 Python 进程上。本文还有配套的精品资源点击获取
返回列表