资讯动态

基于Django的共享单车后台管理系统:从模型设计到权限与计费落地

发布时间:2026/9/17 3:09:07 来源:尧图企业网站定制
简介这是一个基于Django框架实现的共享单车后台管理系统源码包适合正在学习Python Web开发、或准备做课程设计与毕业设计的开发者参考项目使用pymysql连接MySQL数据库围绕用户管理、单车管理、订单记录和统计分析等业务展开能直观展示Django的MTV架构和实际开发流程。资源共64个文件约207KB主要包含Python源码、HTML模板、CSS/JS前端文件、XML配置文件、SQL数据库转储及SQLite数据库其中16个py文件对应模型、视图、路由等核心逻辑14个pyc为编译缓存10个html构成后台管理页面目录按Django项目规范划分便于按模块阅读和二次开发。目前已有525人学习下载除了完整可运行的Django工程外附带的bicycle_management.sql脚本可直接导入MySQL完成建表与初始数据填充同时后台页面模板展示了表单提交、列表展示、登录鉴权等前后端交互方式能帮助理解Django项目从模型设计到页面渲染的完整链路。1. 共享单车后台管理系统为什么值得用django重写一版线下运营手里攥着一大堆Excel车辆分布、故障上报、骑行订单各对不上账这是共享单车后台管理系统最典型的立项场景另一类场景是车辆与车锁接口已经能上报数据缺一个给运营、客服、财务共同使用的管理后台。django自带ORM和迁移admin界面和权限框架都是现成的覆盖共享单车后台的车辆档案、订单结算、计费规则、运维工单绰绰有余。用django做这个后台不是因为它擅长写炫酷的前端而是因为admin加自定义视图能在三天内把可用的版本跑起来后续再按需做前后端分离。适合django熟、以管理端为主、上线时间紧的团队C端小程序需要实时骑行轨迹的话接口单独设计别和后台塞同一个工程。2. django模型设计车辆、订单、计费规则怎么落库先在环境里装好django与数据库驱动常规操作是pip install django mysqlclient然后创建bikes应用因为车辆、订单、费率这些核心表都从车辆上下文出发python manage.py startapp bikesbikes会生成models.py、admin.py等骨架文件。这里注意startapp只是生成目录真正的模型、迁移、注册admin都要自己补下面逐张表拆。2.1 车辆状态与车锁编号用choices锁死状态集合共享单车后台管理系统里车辆是最核心的资源。车辆表不只要存车牌号和GPS坐标更要存锁的状态。锁的状态决定了用户能不能借也决定了后台能不能远程下发指令。先看bikes/models.py里的Bike模型from django.db import models class Bike(models.Model): STATUS_AVAILABLE available STATUS_IN_USE in_use STATUS_MAINTENANCE maintenance STATUS_LOST lost STATUS_CHOICES [ (STATUS_AVAILABLE, 可借), (STATUS_IN_USE, 骑行中), (STATUS_MAINTENANCE, 维修中), (STATUS_LOST, 丢失), ] bike_no models.CharField(车辆编号, max_length32, uniqueTrue) lock_no models.CharField(车锁编号, max_length32, uniqueTrue) region models.CharField(辖区, max_length32, default北京) status models.CharField(状态, max_length20, choicesSTATUS_CHOICES, defaultSTATUS_AVAILABLE) longitude models.DecimalField(经度, max_digits9, decimal_places6, nullTrue, blankTrue) latitude models.DecimalField(纬度, max_digits9, decimal_places6, nullTrue, blankTrue) battery models.IntegerField(电量%, default100) updated_at models.DateTimeField(最后上报时间, auto_nowTrue) def __str__(self): return f{self.bike_no}({self.status})bike_no和lock_no都加了uniqueTrue。车辆编号对用户可见车锁编号对硬件平台可见两套编号在对接车锁厂商时经常对不上如果数据库不约束唯一排查问题时会多出很多无效沟通。status用choices约束后admin下拉框只有四种状态可填业务代码里也只引用Bike.STATUS_IN_USE这样的常量少一层魔法字符串。region字段先当字符串用后面有跨城运营再拆成城市表。四种状态的含义直接决定业务流程列一下状态值中文业务含义available可借空闲用户扫码可以开锁in_use骑行中已被订单占用不允许再借maintenance维修中运维提交工单后置位lost丢失超时未还或运维上报找回把bikes应用加进INSTALLED_APPS并迁移python manage.py makemigrations bikes python manage.py migrate迁移文件会记录choices、unique这些约束执行migrate前看一眼生成的迁移文件确认新增字段和索引没有误伤线上数据。新手常犯的错是改了模型就一路yes结果生产环境里加了非空字段却没有默认值迁移直接中断。2.2 订单表与计费规则django里存单价还是存规则订单表要回答三个问题谁借的、哪辆车、花了多少钱。骑行的费用不是单价乘时长那么简单共享单车行业常见起步价、免费时长、超出单价、每日封顶。把单价直接存在订单表里计费规则一变老订单就成了历史账单财务对账时没法解释。常见做法是订单表存金额快照另建一张费率表维护规则版本class RateRule(models.Model): name models.CharField(规则名, max_length64) start_time models.DateTimeField(生效时间) end_time models.DateTimeField(失效时间, nullTrue, blankTrue) base_fee models.DecimalField(起步价, max_digits6, decimal_places2) base_minutes models.IntegerField(起步时长(分钟)) extra_fee_per_minute models.DecimalField(超出后每分钟单价, max_digits6, decimal_places2) daily_cap models.DecimalField(每日封顶, max_digits6, decimal_places2, nullTrue, blankTrue) def is_active(self, dt): return self.start_time dt and ( self.end_time is None or dt self.end_time )订单表只存费率规则的外键和计费结果from django.conf import settings class RideOrder(models.Model): STATUS_UNPAID unpaid STATUS_PAID paid STATUS_EXCEPTION exception order_no models.CharField(订单号, max_length32, uniqueTrue) user models.ForeignKey(settings.AUTH_USER_MODEL, on_deletemodels.PROTECT) bike models.ForeignKey(Bike, on_deletemodels.PROTECT) rate_rule models.ForeignKey(RateRule, on_deletemodels.PROTECT, nullTrue, blankTrue) start_time models.DateTimeField(借车时间) end_time models.DateTimeField(还车时间, nullTrue, blankTrue) fee models.DecimalField(应收金额, max_digits8, decimal_places2, default0) status models.CharField(状态, max_length20, choices[ (STATUS_UNPAID, 待支付), (STATUS_PAID, 已支付), (STATUS_EXCEPTION, 异常), ], defaultSTATUS_UNPAID)两个外键都用了on_deletemodels.PROTECT。车辆或用户被删除时只要还有关联订单删除就会抛ProtectedError强迫你先处理历史订单的归档而不是让账单悬空。rate_rule在订单结算那一刻被引用算出fee后写入订单之后改费率规则不影响历史订单对账。is_active方法按生效时间判断规则可用适合运营后台管理系统里「现在生效的是哪条规则」这种高频查询。2.3 数据导入与初始化用管理命令而不是loaddata初始化几百辆车的台账很多人第一反应是django fixturesloaddata但车辆数据隔三差五要批量更新JSON文件维护起来不如CSV顺手。常见做法是写一个management command直接读CSV后续导入运维排班表、导出台账都能复用mkdir -p bikes/management/commands touch bikes/management/commands/__init__.py touch bikes/management/commands/import_bikes.py在import_bikes.py里import csv from django.core.management.base import BaseCommand from bikes.models import Bike class Command(BaseCommand): help 从CSV导入车辆 def add_arguments(self, parser): parser.add_argument(csv_path) def handle(self, *args, **options): with open(options[csv_path], encodingutf-8-sig) as f: for row in csv.DictReader(f): Bike.objects.update_or_create( bike_norow[bike_no], defaults{ lock_no: row[lock_no], region: row[region], longitude: row[longitude], latitude: row[latitude], } ) self.stdout.write(self.style.SUCCESS(车辆导入完成))执行python manage.py import_bikes bikes_2025.csv这里核心是encodingutf-8-sig兼容Excel导出CSV时常见的BOM头不然第一行字段名会变成bike_no前面带不可见字符。update_or_create的查找键是bike_no同一份CSV重复执行不会产生重复车辆适合每天夜间同步一次。管理命令里可以继续加--dry-run参数先打印要变更的行数确认无误再真正写库。3. 后台管理系统的权限与admin定制3.1 自定义用户模型运营、客服、财务的角色区分项目从零开始就换掉django默认的auth.User这是共享单车后台管理系统最值得做的决定。默认的is_staff和is_superuser只能区分「能不能进后台」和「是不是超管」表达不了运营、客服、财务之间细粒度的权限差异。运营要能查订单但不能改金额客服要能远程开关锁但不能导入车辆财务要能看账单和退款但不能动车辆档案。在第一次migrate前创建accounts应用并定义用户模型python manage.py startapp accountsaccounts/models.pyfrom django.contrib.auth.models import AbstractUser from django.db import models class Role(models.TextChoices): OPERATOR operator, 运营 SUPPORT support, 客服 FINANCE finance, 财务 ADMIN admin, 管理员 class User(AbstractUser): role models.CharField(角色, max_length20, choicesRole.choices, defaultRole.OPERATOR) phone models.CharField(手机号, max_length20, uniqueTrue) region models.CharField(负责辖区, max_length32, blankTrue)改settings.pyAUTH_USER_MODEL accounts.User这个切换必须在第一次migrate之前完成。项目一旦执行过初始迁移默认auth_user表已经存在再改AUTH_USER_MODEL要清理相关迁移和表代价很大。如果线上项目已经跑了大半年不要硬改用OneToOne扩展Profile表更稳妥。roles只给了四个后面加「调度员」这类新角色直接在TextChoices里加一项再配admin的Group权限。角色与权限的分配落到django admin上要分清两个层级模型级权限用admin自带的add/change/delete/view行级数据范围靠重写get_queryset实现。参考下表角色数据范围可执行动作禁止动作运营本辖区车辆、订单导入车辆、编辑车辆状态退款、改价客服全部用户、订单查询订单、登记工单修改金额财务全部订单、账单导出报表、发起退款车辆档案操作管理员全部一切无3.2 重写admin视图让运营只看自己辖区的车辆自定义用户模型只是第一步。django admin变成运营真正愿意用的后台管理系统关键在get_queryset和save_model。先给Bike注册adminfrom django.contrib import admin from bikes.models import Bike admin.register(Bike) class BikeAdmin(admin.ModelAdmin): list_display [bike_no, lock_no, region, status, battery, updated_at] list_filter [status, region] search_fields [bike_no, lock_no] def get_queryset(self, request): qs super().get_queryset(request) if request.user.is_superuser: return qs return qs.filter(regionrequest.user.region) def save_model(self, request, obj, form, change): if not request.user.is_superuser: obj.region request.user.region super().save_model(request, obj, form, change)get_queryset同时作用于列表、批量操作和删除运营永远只能看到本辖区的车。save_model的补充是防止运营手动创建车辆时把region选到别处导致自己创建的记录自己看不到。这里的边界是如果运营需要跨区调配车辆就不能用save_model强制覆盖region而应该走一个专门的调拨action并在调拨里记录操作人和时间。admin还可以提供批量action比如把选中车辆集体标记为维修admin.action(description批量标记为维修) def mark_maintenance(modeladmin, request, queryset): queryset.update(statusBike.STATUS_MAINTENANCE) actions [mark_maintenance]queryset.update不会调用save也就不会触发save_model的region覆盖逻辑适合做批量状态流转。批量操作对审计有要求所以在3.3节还要补一层通用日志。3.3 操作审计记录谁在后台管理系统改了什么共享单车后台被多个角色共用出问题时要能回答「谁在什么时候改了什么」。django admin自带的LogEntry只记录管理后台的增删改记录颗粒度是模型对象不区分具体字段运营和客服都改状态时看不出来差异。常见做法是单独建审计表class AdminLog(models.Model): user models.ForeignKey(settings.AUTH_USER_MODEL, on_deletemodels.SET_NULL, nullTrue) model_name models.CharField(模型, max_length64) object_id models.CharField(对象ID, max_length64) action models.CharField(动作, max_length16) detail models.JSONField(变更详情, defaultdict) created_at models.DateTimeField(时间, auto_now_addTrue)再把这个表挂进admin的变更流程。ModelAdmin自带log_change方法覆盖它把所有变更都落一份到AdminLogclass AuditMixin: def log_change(self, request, obj, message): AdminLog.objects.create( userrequest.user, model_nameobj._meta.label, object_idstr(obj.pk), actionchange, detail{message: message}, ) super().log_change(request, obj, message) class BikeAdmin(AuditMixin, admin.ModelAdmin): ...detail用JSONField可以记录前后值比如status从maintenance改成available审计里就能看到操作人和时间。前后值对比不能依赖obj的当前值要在save_model里把form.initial和form.cleaned_data做diff再传给log_change。注意AuditMixin会影响所有继承它的ModelAdmin不要把敏感字段直接塞进message日志表本身也要按天归档。admin界面美化不是这一章的重点但有一个性价比很高的动作改站点头和site title。运营每天打开后台管理系统看到的是django默认「Django administration」观感上不像给自家业务做的系统admin.site.site_header 共享单车运营后台 admin.site.site_title 单车后台 admin.site.index_title 车辆与订单管理4. 借还车与计费共享单车后台的核心业务实现4.1 借车接口的状态校验与原子更新用户扫码借车时后台管理系统要做的事是根据车锁上报的状态判断车能不能借然后调车锁接口开锁同时生成一条待支付订单并把车辆置为骑行中。这个过程最容易出并发问题同一辆车两个请求同时查到statusavailable都往下走最后两笔订单对应同一辆车车锁开了两次。django ORM的get或filter都不带锁需要用select_for_updateimport uuid from django.db import transaction from django.db.models import Q from django.utils import timezone from bikes.models import Bike, RideOrder, RateRule class BikeNotAvailable(Exception): pass def gen_order_no(): return B timezone.now().strftime(%Y%m%d%H%M%S) uuid.uuid4().hex[:6] transaction.atomic def borrow_bike(bike_id, user): bike Bike.objects.select_for_update().filter( idbike_id, statusBike.STATUS_AVAILABLE ).first() if bike is None: raise BikeNotAvailable(车辆不可用或已被借走) now timezone.now() rule RateRule.objects.filter( start_time__ltenow, ).filter( Q(end_time__isnullTrue) | Q(end_time__gtenow) ).order_by(-start_time).first() ride RideOrder.objects.create( order_nogen_order_no(), useruser, bikebike, rate_rulerule, start_timenow, statusRideOrder.STATUS_UNPAID, ) updated Bike.objects.filter( idbike_id, statusBike.STATUS_AVAILABLE ).update(statusBike.STATUS_IN_USE) if updated ! 1: raise BikeNotAvailable(车辆状态已变化) return rideselect_for_update在事务内锁住这一行第二个请求会阻塞而不是读到旧状态。update语句把filter条件再带上statusavailable这是双保险一旦锁没有生效update受影响行数也能暴露竞态。异常会让整个事务回滚订单不会留下半截脏数据。注意select_for_update必须包在transaction.atomic里django会直接抛TransactionManagementError。这个接口要不要做成django REST framework的ViewSet取决于前端是谁。后台管理系统里远程开锁通常是给客服用的从admin后台点一个按钮更顺手如果未来要做小程序扫码借车再把borrow_bike包一层API。业务逻辑独立成函数不要直接写在View里方便Django的manage.py shell直接调用测试。4.2 还车计费用策略模式处理起步价、超出单价与封顶还车时后台管理系统要做三件事校验订单状态、确认车锁已落锁、计算费用。计费逻辑最忌讳用if-else堆起步价、超出单价、每日封顶是个组合规则一多视图函数就变成一坨。把计算抽成独立函数只依赖RateRule和骑行分钟数不读数据库不碰requestfrom decimal import Decimal def calc_fee(rule, minutes): minutes int(minutes) if minutes rule.base_minutes: fee Decimal(rule.base_fee) else: extra minutes - rule.base_minutes fee Decimal(rule.base_fee) Decimal(rule.extra_fee_per_minute) * extra if rule.daily_cap is not None and fee Decimal(rule.daily_cap): fee Decimal(rule.daily_cap) return fee.quantize(Decimal(0.01))几个典型费率放在一起便于后面写单测规则名起步价起步时长超出单价每日封顶普通车1.50元15分钟0.10元/分钟20.00元节假日活动1.00元10分钟0.08元/分钟15.00元学生优惠0.50元30分钟0.06元/分钟10.00元骑行15分钟内只要1.5元第16分钟开始按分钟计费超过20元按20元收。边界情况是minutes等于base_minutes走起步价分支extra为0时不会额外计费daily_cap为空时不做封顶。把这个函数放在bikes/services.py里用pytest或unittest直接构造RateRule对象测不需要创建数据库记录。还车动作的数据库操作在事务里完成transaction.atomic def finish_ride(ride_id): ride RideOrder.objects.select_for_update().filter( idride_id, statusRideOrder.STATUS_UNPAID ).first() if ride is None: raise ValueError(订单不存在或已结算) ride.end_time timezone.now() minutes (ride.end_time - ride.start_time).total_seconds() / 60 ride.fee calc_fee(ride.rate_rule, minutes) ride.status RideOrder.STATUS_PAID ride.save(update_fields[end_time, fee, status]) Bike.objects.filter(pkride.bike_id).update(statusBike.STATUS_AVAILABLE)select_for_update锁的是订单行防止同一笔订单被两次还车请求重复结算。update_fields限定只写end_time、fee、status三个字段避免把start_time等字段在并发下覆盖。车辆状态用update而不是save是因为这里不关心车辆的其他字段直接条件更新更干净。如果运营要求「还车后车辆必须立刻出现在可借列表」这个顺序是对的先结算订单再放车。4.3 超时未还车的清理任务django命令加定时调度骑行超过24小时没还车正常流程是系统自动标记异常并把车辆置为丢失转入人工排查。这个任务不适合在请求线程里跑应该做成management command由定时任务调度mkdir -p bikes/management/commands touch bikes/management/commands/__init__.py touch bikes/management/commands/handle_stale_rides.pyfrom django.core.management.base import BaseCommand from django.utils import timezone from bikes.models import Bike, RideOrder class Command(BaseCommand): help 标记超时未还的订单和车辆 def handle(self, *args, **options): cutoff timezone.now() - timezone.timedelta(hours24) stale_rides RideOrder.objects.filter( statusRideOrder.STATUS_UNPAID, start_time__ltcutoff, ).select_related(bike) for ride in stale_rides: ride.status RideOrder.STATUS_EXCEPTION ride.save(update_fields[status]) Bike.objects.filter(pkride.bike_id).update( statusBike.STATUS_LOST, updated_attimezone.now(), ) self.stdout.write(self.style.SUCCESS(f处理 {stale_rides.count()} 条超时订单))这个命令幂等吗由于filter限定statusRideOrder.STATUS_UNPAID已经标记过exception的订单不会再次处理。注意count()会额外执行一次查询如果要看实际处理数量用列表收集ride对象再计数更准确。定时调度用系统crontab最省事*/30 * * * * cd /opt/bike_admin venv/bin/python manage.py handle_stale_rides logs/stale.log 21每半小时跑一次。如果公司内部有通用任务平台把这个命令包装成shell调用同样能接。共享单车后台管理系统里订单增长很快历史数据不要物理删除用状态标记再加归档任务这也是后台管理系统处理「删除对象」的通用做法。5. 后台管理系统的搜索、Excel导出与部署排错5.1 让订单搜索不卡覆盖admin搜索与查询优化订单量到百万级后直接在admin的search_fields里同时放order_no、user_name、bike_no多个字段页面响应会明显变慢。django的search_fields对CharField默认生成带前后通配符的LIKE查询索引用不上。订单号短且唯一保留在search_fields里用户名、手机号这类字段交给list_filter再加时间范围筛选。给订单表建复合索引class Meta: indexes [ models.Index(fields[status, start_time], nameidx_status_start), models.Index(fields[order_no], nameidx_order_no), ]5.2 用openpyxl生成订单报表运营要的「后台管理系统在线预览excel表格」本质是让列表能筛、能导出、能打开。django admin里加一个导出action用openpyxl直接生成xlsxfrom openpyxl import Workbook from django.http import HttpResponse def export_orders(modeladmin, request, queryset): wb Workbook() ws wb.active ws.title 订单 ws.append([订单号, 车辆编号, 用户, 借车时间, 还车时间, 金额]) for order in queryset.select_related(bike, user).iterator(chunk_size2000): ws.append([ order.order_no, order.bike.bike_no, order.user.username, 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 , str(order.fee), ]) resp HttpResponse( content_typeapplication/vnd.openxmlformats-officedocument.spreadsheetml.sheet ) resp[Content-Disposition] attachment; filenameorders.xlsx wb.save(resp) return resp export_orders.short_description 导出选中订单为Excel注册的时候在ModelAdmin里加actions [export_orders]。大导出用iterator分批取避免一次查几万行把内存打满。如果运营还想在页面上直接预览就把导出的xlsx放到nginx的静态目录再加一个签名下载地址但先保证导出本身的数据准确预览只是展示层的事。5.3 部署前要检查的三个配置用宝塔或systemd跑django后台管理系统上线前查三处STATIC_ROOT没配admin样式全丢ALLOWED_HOSTS漏了域名访问直接400DEBUG开着运营点到异常页能看到堆栈和路径信息。部署时顺序是pip install gunicorn python manage.py collectstatic --noinput gunicorn bike_admin.wsgi:application -w 4 -b 0.0.0.0:8080nginx里把/static/代理到STATIC_ROOT。gunicorn的worker数不要照抄推荐值后台管理系统有Excel导出这类高内存操作先压一遍再定。最后做一次数据自检确认车辆状态和订单没有脏数据python manage.py shell -c from bikes.models import Bike; print(Bike.objects.filter(statusin_use, rideorder__isnullTrue).count())输出为0再发布。这条查询找的是「骑行中但没有订单」的车辆只要有查一下是不是哪个借车或还车流程少了一笔事务提交。之后的每次发布把迁移执行、静态文件收集、缓存清理三步串进发布脚本这个自检命令也放进同一段脚本里人工少一步漏配置的概率就小一步。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价