news 2026/9/17 3:24:13

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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_length=11, unique=True) deposit_status = models.BooleanField('押金状态', default=False) class Meta: verbose_name = '用户' verbose_name_plural = verbose_name class Vehicle(models.Model): VEHICLE_STATUS = [ ('idle', '待租'), ('riding', '骑行中'), ('fault', '故障'), ('repairing', '维修中'), ] bike_no = models.CharField('车辆编号', max_length=20, unique=True) latitude = models.DecimalField('纬度', max_digits=9, decimal_places=6) longitude = models.DecimalField('经度', max_digits=9, decimal_places=6) battery = models.IntegerField('电量', default=100) status = models.CharField('状态', max_length=10, choices=VEHICLE_STATUS, default='idle') update_time = models.DateTimeField('最后上报时间', auto_now=True) 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_delete=models.PROTECT, related_name='orders', db_index=True, verbose_name='用户') vehicle = models.ForeignKey(Vehicle, on_delete=models.PROTECT, related_name='orders', db_index=True, verbose_name='车辆') start_time = models.DateTimeField('开始时间', auto_now_add=True, db_index=True) end_time = models.DateTimeField('结束时间', null=True, blank=True) start_position = models.CharField('起点', max_length=100, blank=True) end_position = models.CharField('终点', max_length=100, blank=True) amount = models.DecimalField('实收金额', max_digits=8, decimal_places=2, default=0) status = models.CharField('状态', max_length=10, choices=ORDER_STATUS, default='ongoing', db_index=True) class Meta: verbose_name = '骑行订单' verbose_name_plural = verbose_name permissions = [ ('force_finish_order', '可以强制结束异常订单'), ('export_order', '可以导出订单数据'), ]

外键的 on_delete 用 PROTECT 而不是 CASCADE,因为车辆和用户不允许从库里硬删,只能改状态。PROTECT 会阻止删除被引用对象,避免「骑着骑着车没了」的脏数据。db_index=True 的三个字段是后台列表页最常做筛选和排序的,提前加索引能明显减少大数据量下的超时。权限定义放在 Meta 里是 Django 的标准做法,后面 Admin 编排角色时会用到。

2.2 状态机放在 Model 层,拦住误操作

车辆状态和订单状态不是互相独立的,它们之间是一组流转约束:车辆只能从 idle 变成 riding,订单从 ongoing 到 finished 时车辆才能回到 idle;出现故障时车辆进入 fault,对应订单标记为 abnormal 并走退款。这张流转约束表如果散落在各处视图代码里,半年后就是灾难。

当前车辆状态允许动作目标车辆状态对应订单状态
idle开锁ridingongoing
riding关锁结算idlefinished
riding上报故障faultabnormal
fault维修下线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(days=90) ids = list( TripOrder.objects .filter(end_time__lt=expired_time, status='refunded') .values_list('id', flat=True)[:200] ) TripOrder.objects.filter(id__in=ids).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(status='finished') .values('start_time__date') .annotate(income=Sum('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_time'
Admin 配置项作用这里的典型值
list_display列表页展示哪些列bike_no, status, battery
list_filter右侧过滤栏status, battery
search_fields顶部搜索框,生成 LIKE 查询bike_no
list_editable列表页可直接编辑的字段battery
list_per_page每页记录数50
date_hierarchy按时间逐级下钻筛选update_time

battery 是 IntegerField,Django 会自动把 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(status='ongoing'): 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(status='ongoing') 会在循环前做一次过滤,避免把已结束的订单再算进去。循环里逐条 save 单条记录,数据量大时性能不如 update,但订单量在共享单车后台通常一天几千条,完全够用。action 的 description 会直接显示在下拉框里,不用用户理解函数名。

如果以后要支持真正的批量改价,可以用 F 表达式配合 update 做原子更新,避免循环:

from django.db.models import F queryset.update(amount=F('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, obj=None): 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 是 bikes,codename 是 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, name='ops-dashboard'), path('dashboard/export', views.export_orders_csv, name='ops-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(status='fault').count(), 'order_today': TripOrder.objects.filter(start_time__date=today).count(), 'income_today': TripOrder.objects.filter( start_time__date=today, status='finished' ).aggregate(total=Sum('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(hour=0, minute=0, second=0, microsecond=0) active_vehicles = ( Vehicle.objects .annotate(today_orders=Count( 'orders', filter=Q(orders__start_time__gte=today_start) )) .order_by('-today_orders')[:20] )

注意这里 Count 里的 filter 是 Dango 2.0 起支持的条件聚合写法,它先对 orders 关联表做筛选,再按车辆分组计数,所以没有订单的车辆也会保留在结果里,today_orders 为 0。如果换成 Vehicle.objects.filter(orders__start_time__gte=today_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_type='text/csv') response['Content-Disposition'] = 'attachment; filename="orders.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 response

select_related 是这里的关键。for 循环里访问 order.user 和 order.vehicle 都会触发一条额外 SQL,不联表的话导出 1000 条订单会产生 2000 条附加查询,页面会卡到超时。select_related 把外键 JOIN 进一条 SQL,1000 条订单只剩一次查询。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(id=order_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 我会这样锁版本:

django>=4.2,<5.0 gunicorn==21.2.0 mysqlclient==2.2.0 django-simpleui==2024.3.0

mysqlclient 在部分内网机器上装不上,常见做法是把数据库驱动换成 pymysql,并在 settings.py 里打补丁:

import pymysql pymysql.install_as_MySQLdb()

pymysql 是纯 Python 实现,免编译,能避开宝塔默认 Python 环境缺少 libmysqlclient-dev 导致编译失败的问题。

5.2 上线前的项目配置

settings.py 里必须归位的三个点:DEBUG=False、ALLOWED_HOSTS 填上服务器 IP 或域名、STATIC_ROOT 指向收集目录。然后运行 collectstatic,把 Django Admin 和 simpleui 的静态资源收到正式目录,否则登录页会是裸 HTML。

python manage.py collectstatic --noinput python manage.py migrate python manage.py createsuperuser

5.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 进程上。

本文还有配套的精品资源,点击获取

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

群晖存储空间损毁修复续集:文件系统损坏的诊断与恢复

距离上次写《群晖“存储空间损毁”修复小记》才过去小半年&#xff0c;我这边又踩了一次雷。群晖玩了五六年&#xff0c;最让人血压飙升的弹窗&#xff0c;莫过于存储管理器里那行红色的“存储空间 2 已损毁”&#xff0c;后面还跟着一句“卷已卸载”或者“文件系统只读”。第一…

作者头像 李华
网站建设 2026/9/17 3:21:51

OPC一人公司不只是“一人一智能体”,更是企业组织OPC化

OPC一人公司不只是“一人一智能体”&#xff0c;更是企业组织OPC化一、一个需要被澄清的误解很多人第一次听到“OPC一人公司”&#xff0c;会下意识地理解为&#xff1a;“一个人&#xff0c;加一个智能体&#xff0c;就是一家公司。”这个理解没有错&#xff0c;但它只说对了一…

作者头像 李华
网站建设 2026/9/17 3:21:38

MyBatis自定义TypeHandler查询JSON字段返回null的排查与解决方案

如果你在 MyBatis 里自定义了一个 TypeHandler 来处理 MySQL 的 JSON 字段&#xff0c;结果查询返回 null&#xff0c;先别急着怀疑人生。这个问题我在项目里至少见过十几次&#xff0c;每次排查到最后原因都很简单&#xff0c;但没踩过坑的人就是看不出来。所谓“自定义 TypeH…

作者头像 李华