ARTICLE DETAIL

资讯详情

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

基于Python+Django的电信资费管理系统设计与实践

基于Python+Django的电信资费管理系统设计与实践 1. 为什么这个项目值得做电信资费管理系统的真实业务场景先聊个比较扎心的现实很多接私活或者做课程设计的同学一听到资费管理系统就下意识觉得这是个老掉牙的CRUD工程——无非就是套餐表增删改查、用户表增删改查然后套个Django Admin就交付了。但真正接手过电信、运营商或者虚拟运营商MVNO相关业务的人心里都清楚这个领域最大的难点从来不是能不能增删改查而是资费计算模型的准确性和计费周期的严谨性。我之前接过一个类似的项目客户是一家做宽带融合套餐的小型运营商需求文档写得非常简略用户管理、套餐管理、订单管理、账单生成。结果一聊细节才发现光是一个用户在一个月内从A套餐改成B套餐账单怎么拆分计算这一个问题就涉及Django的日期时间处理、数据库事务边界、以及费率快照Rate Snapshot设计等一系列问题。所以当你拿到这套基于PythonDjango的电信资费管理系统源码时千万别只把它当成一个练手Demo真正值得研究的是它背后的业务设计思路和部署落地细节。这套系统适合谁来参考我认为有三类人第一类是正在做Django项目实战的学生缺一个业务逻辑稍微复杂一点的完整案例第二类是准备接类似外包项目的开发者需要一份能快速理解和二次开发的基础工程第三类是运维或者全栈工程师想研究 Django 项目从开发环境到生产环境部署的完整链路。下文我会从源码结构、核心数据模型、计费逻辑、部署文档和代码讲解几个维度展开全程结合我实操过的经验和踩过的坑来写。2. 源码结构拆解先看懂 Django 项目的骨骼再谈二次开发拿到源码第一件事别急着跑起来先花半小时把目录结构过一遍。这套电信资费管理系统的源码整体遵循 Django 的标准工程布局但在 app 划分上有它自己的业务逻辑我用实际项目经验帮你梳理一下。2.1 标准工程目录下的业务 app 划分逻辑一个典型的 Django 项目结构大致是这样的telecom_billing/ ├── manage.py ├── telecom_billing/ # 项目配置目录 │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ ├── wsgi.py │ └── asgi.py ├── apps/ │ ├── users/ # 用户管理模块 │ ├── packages/ # 套餐管理模块 │ ├── orders/ # 订单与业务受理模块 │ ├── billing/ # 计费与账单模块 │ └── dashboard/ # 后台数据概览模块 ├── static/ ├── media/ ├── templates/ ├── requirements.txt └── docs/ ├── 部署文档.md └── 数据库设计说明.md注意它把业务模块统一放进apps目录而不是直接放在项目根目录下。这一步看似简单实际对后期扩展影响很大。我之前见过很多 Django 初学者所有 app 都堆在根目录等业务模块多了以后settings 里的INSTALLED_APPS写一长串路由配置也乱成一团。把业务 app 收敛到统一目录本质上是在为后续的模块化扩展做铺垫。在这套系统里最核心的是billing这个 app。因为它不是普通的 CRUD而是承载了套餐匹配—费用计算—账单生成这条主线逻辑。其余users、packages、orders更多是在为主题业务做数据支撑。2.2 settings.py 中容易忽略但影响全局的配置项部署这套系统时settings.py里有几项配置我需要特别提醒AUTH_USER_MODEL电信系统通常需要扩展用户模型比如增加客户编号、账户余额、积分等字段。源码中如果继承了AbstractUser在数据库迁移前必须先配置好这一项否则后期改会非常痛苦。TIME_ZONE和USE_TZ计费系统最怕时区出错。国内项目建议设置TIME_ZONE Asia/ShanghaiUSE_TZ保持True。这里有个大坑Django 的DateTimeField如果USE_TZTrue存入数据库的是 UTC 时间模板渲染时如果不做转换显示的时间会和本地差 8 小时。后面部署章节我会专门讲这个坑。DATABASES的默认配置源码如果默认给的是 SQLite本地开发没问题但生产部署必须切 MySQL因为运营商级别的计费数据量级不是 SQLite 能扛住的。另外 MySQL 的配置里一定要加OPTIONS设置字符集和超时时间否则跑大批量账单生成时会抛OperationalError: MySQL server has gone away。2.3 关键业务代码的阅读顺序如果你拿到这套源码后感觉一头雾水我建议你按下面的顺序去阅读而不是从models.py从头看到尾先看billing/models.py——搞清楚账单、账单明细、套餐、用户的模型关系再看billing/views.py或对应的视图函数——理解生成账单这个动作在 HTTP 请求里是怎么触发的然后看services.py或utils.py如果源码有分层的话——计费核心逻辑一般都放在这里而不是视图里最后看orders/views.py——理解套餐变更、退订等操作如何影响后续账单。很多学员拿到代码后第一反应是跑起来然后在页面上点来点去这其实效率很低。先读模型关系再看业务动作的入口才能快速理解一套系统的运转逻辑。3. 数据模型设计资费系统的核心不是表多而是费率表怎么设计资费管理系统表面上管理的是用户—套餐—订单—账单四条线但真正的计费核心藏在费率表的设计里。费率表设计得好不好直接决定计费逻辑是清晰可维护还是一堆 if-else 硬编码。3.1 四大核心模型的关系拆解这套系统的数据模型我建议用用户中心、产品中心、订单中心、计费中心四个维度来理解模型核心字段示意关键约束User用户用户名、手机号、客户等级、账户余额、状态手机号唯一软删除标记Package套餐套餐名称、月租费、包含流量、包含语音、超出单价状态字段区分上架/下架Order订单用户外键、套餐外键、订购时间、生效时间、退订时间生效时间用于费率快照Bill账单用户外键、账单周期如2025-06、金额、状态用户周期唯一约束这里要注意一个细节订单表里生效时间和退订时间的作用远比你想象的重要。它们决定了该订单在某个账单周期内实际占用多少天从而影响按天分摊Pro-Rated Billing的计算。3.2 费率快照计费系统必知的设计防坑技巧在电信计费里有一个非常重要的概念叫费率快照。简单说就是用户的账单金额应该按照他订购套餐当时的资费标准来计算而不是按照账单生成时刻的最新资费标准来计算。比如用户在 5 月份订购了一个 59 元套餐到了 6 月份运营商把这个套餐调整到 69 元但用户 5 月的账单依然应该是 59 元。要实现这个效果费率表不能简单地冗余在订单里而是在Package模型里增加一个类似version或者effective_date的字段。或者更稳妥的做法是为每个订单保存下单时的套餐费用、包含量、超出单价等冗余字段。源码里如果只设计了简单的关联查询那你二次开发的时候一定要考虑这个场景——否则等用户投诉账单算错了你被动修改数据的成本会很高。3.3 数据库迁移的实操顺序不要一把梭我见过很多同学拿到源码后直接python manage.py makemigrations python manage.py migrate结果报错AUTH_USER_MODEL未定义、外键冲突等一堆问题。正确的操作顺序应该是# 1. 先修改 settings.py 中的 AUTH_USER_MODEL如果需要 # 2. 先创建核心业务 app 的迁移再创建关联 app 的迁移 python manage.py makemigrations users python manage.py makemigrations packages python manage.py makemigrations billing python manage.py makemigrations orders # 3. 最后统一执行 migrate python manage.py migrate之所以要先迁移users是因为订单、账单都外键关联到用户表如果用户表还没迁移成功后面的表根本建不起来。用make migrations按依赖顺序执行比一把梭 make 完再 migrate 更容易定位问题。4. 计费逻辑讲解看懂根据套餐算月租的完整业务流才能真正二次开发整个系统里最值得看的代码绝对不是网页列表页的 CRUD 逻辑而是账单生成那一段。这段逻辑几乎决定了系统的含金量到底是有业务深度还是只是个花架子。4.1 从订单到账单计费模块的核心处理流程一套标准的资费账单生成流程大致是这几步遍历当前需要出账的用户一般用 Django Celery 定时任务触发而不是请求时同步生成查出该用户在账单周期内的所有有效订单对每个订单判断是否覆盖整个账单周期还是只覆盖其中一部分根据订单的生效/退订时间按天数分摊月租费生成账单 账单明细汇总当月费用刷新用户账户余额标记账单状态为confirmed或pending_payment。源码里对应的关键代码很可能长这样这是我从同类项目里提炼的标准写法供你对照理解# billing/services.py from datetime import date from decimal import Decimal from .models import Bill, BillItem from orders.models import Order def generate_monthly_bill(user, bill_month: date): 生成单个用户某月账单 bill_month 为该月第一天例如 date(2025, 6, 1) month_start bill_month if month_start.month 12: month_end date(month_start.year 1, 1, 1) else: month_end date(month_start.year, month_start.month 1, 1) # 查询覆盖该账单周期的订单 active_orders Order.objects.filter( useruser, effective_date__ltmonth_end, # 退订时间为空或者退订时间大于账单周期开始时间 end_date__isnullTrue ) | Order.objects.filter( useruser, effective_date__ltmonth_end, end_date__gtmonth_start ) total_amount Decimal(0.00) bill_items [] for order in active_orders: # 计算该订单在账单周期内的有效天数 start_date max(order.effective_date, month_start) end_date min(order.end_date if order.end_date else month_end, month_end) valid_days (end_date - start_date).days # 当月完整月份天数这里简化取 30 天实际应取当月真实天数 days_in_month (month_end - month_start).days # 按天分摊月租 item_amount order.package.monthly_fee * (Decimal(valid_days) / Decimal(days_in_month)) total_amount item_amount bill_items.append( BillItem( orderorder, packageorder.package, amountitem_amount.quantize(Decimal(0.01)), start_datestart_date, end_dateend_date, ) ) # 创建账单和明细 bill Bill.objects.create(useruser, bill_monthmonth_start, total_amounttotal_amount) BillItem.objects.bulk_create(bill_items) return bill这段代码有几个关键点值得你仔细体会max和min的使用是为了处理跨周期订单的裁剪确保只计算本周期内的天数按天分摊用的是Decimal而不是float因为钱不能有浮点误差bulk_create批量插入明细是为了避免在大量用户出账时逐条 insert 造成数据库性能瓶颈。4.2 为什么计费要用 Decimal 而不是 float这个问题我几乎在每次代码评审里都会强调。float是 IEEE 754 的双精度浮点数0.1 0.2 这种基础运算都可能有精度误差。计费系统涉及金额一旦出现59.99被算成59.98999999用户看到账单立刻会投诉。Django 里对应的是DecimalFieldPython 里对应的是decimal.Decimal。源码里如果所有金额字段都用了DecimalField说明作者是有经验的如果用了FloatField那我劝你趁早改掉不然后面数据对账会对到你怀疑人生。4.3 异步任务生产环境必须引入 Celery而不是在视图里同步生成账单我在前面那段示例代码里提到遍历用户生成账单在实际生产环境中这个操作绝对不能放在某个视图函数里同步执行——几百上千个用户并发请求请求超时是必然的。正确做法是使用 Celery 分布式任务队列在后台异步执行。具体来说# 需要额外安装 pip install celery redis然后在settings.py配置CELERY_BROKER_URL redis://localhost:6379/0 CELERY_RESULT_BACKEND redis://localhost:6379/0在billing/tasks.py里写任务from celery import shared_task from datetime import date from .services import generate_monthly_bill shared_task def generate_all_user_bills(bill_month_str): bill_month date.fromisoformat(bill_month_str) # 遍历所有正常状态用户逐个出账 from users.models import User for user in User.objects.filter(statusactive): generate_monthly_bill(user, bill_month)再配合 Celery Beat 定时任务每个月 1 号凌晨自动触发出账。这一套流程跑通之后你的系统才算具备生产可用的雏形。源码里如果这一块没写二次开发时建议补上。5. 权限与配置后台Django Admin 的深度改造思路电信资费管理系统本质上是一个企业内部运营支撑系统BOSS 系统的最小化实现。它和普通博客网站的不同之处在于角色权限边界特别清晰甚至可以追溯到一句话——运营人员能看用户但不能改余额财务只能做对账管理员才能调费率。5.1 为什么默认的 Django Admin 不够用Django 自带的 Admin 后台确实高效但你直接拿默认 Admin 交给客户会遇到三个问题没有操作日志。运营人员误删了一个订单没留痕责任说不清字段暴露过散。Admin 默认把模型所有字段都渲染出来资费系统里很多字段比如余额、月租费对普通运营来说不该看见权限粒度不够细。Django 自带的is_staff、is_superuser只能粗粒度区分无法做到某个用户只能查看某几个套餐。这套源码里如果对 Admin 做了改造通常会有以下几个方向创建自定义UserAdmin在list_display中隐藏敏感字段重写get_queryset方法让不同角色的用户只看得到自己权限范围内的数据使用django-log-entry之类的方式记录操作日志或者自己重写admin.ModelAdmin的save_model、delete_model方法记录变更。5.2 多角色权限Group Permission 的正确打开方式我给这类系统做权限设计时一般会在初始数据迁移文件data_migrations里创建好三类角色而不是让管理员去后台手动勾选权限角色允许操作禁止操作客服专员新增用户、查询用户、办理套餐订购修改资费价格、删除订单财务人员查看账单、导出账单、核对余额修改套餐、删除用户系统管理员全部权限——具体实现上可以利用 Django 的Group和Permission机制再配合自定义装饰器。例如from django.contrib.auth.decorators import login_required, permission_required from django.shortcuts import render login_required permission_required(billing.change_bill, raise_exceptionTrue) def audit_bill(request, bill_id): # 财务审核账单 pass当然如果项目里用了 Django REST Framework 做前后端分离那权限就要用到permission_classes和IsAuthenticated、DjangoModelPermissions的组合。我建议你实践时先理清系统是服务端渲染用 Django templates还是 API 驱动的前后端分离这两种模式的权限控制写法和调试思路差异不小。6. 部署文档里的关键细节从本地 runserver 到 Nginx Gunicorn 的完整链路很多小伙伴说代码能跑就行但真正在工作里部署环节才是最容易翻车的地方。这个源码里如果带了部署文档我建议你不要只看步骤还要理解每一步在后面踩坑时怎么排查。我总结一下生产环境部署的核心链路和常见坑。6.1 生产环境技术栈选型一个可用的 Django 生产部署栈通常是Nginx静态文件与反向代理 → GunicornWSGI 应用服务器 → Django业务代码 → MySQL数据库 → Redis缓存 / Celery broker为什么不是直接用 Django 自带的runserver因为runserver是单进程、非线程安全的开发服务器扛不住并发也不适合处理 real-world 流量。Gunicorn 在这里扮演的是 Python Web 应用的进程管理角色Nginx 则负责接收用户请求、转发给 Gunicorn并托管静态文件比如 CSS、JS、图片。6.2 Django 4.x / 5.x 部署时最容易踩的三个坑第一个坑ALLOWED_HOSTS 没配好。本地开发时ALLOWED_HOSTS []就能跑但一部署到服务器浏览器访问你的域名会直接报DisallowedHost。部署文档里如果没强调这一点你一定会踩。正确写法ALLOWED_HOSTS [yourdomain.com, www.yourdomain.com, 127.0.0.1]第二个坑静态文件 404。Django 开发时静态文件直接由runserver提供但生产环境不会。必须在settings.py配好STATIC_URL /static/ STATIC_ROOT os.path.join(BASE_DIR, staticfiles)然后在服务器上执行python manage.py collectstatic否则你会看到页面排版完全错乱CSS 和 JS 全挂。我之前帮别人排查过一个问题用户说页面加载出图了但是样式没了排查半天发现就是 Nginx 的location /static/配错了目录。第三个坑时区导致单据时间错乱。前面我们提过TIME_ZONE和USE_TZ。生产环境最保险的配置是TIME_ZONE Asia/Shanghai USE_TZ True然后在模板展示层用 Django 自带的{{ value|date:Y-m-d H:i:s }}或者在前端做本地时区转换。如果直接用datetime.now()取当前时间它返回的是本地时间但数据库里存的 UTC 时间两次读取对不上账单的开始/结束日期就会变得非常诡异。6.3 Nginx Gunicorn 配置示例Gunicorn 启动 Django 项目的常用方式gunicorn telecom_billing.wsgi:application \ --bind 127.0.0.1:8001 \ --workers 4 \ --timeout 120 \ --name telecom_billingNginx 配置大致如下server { listen 80; server_name yourdomain.com; client_max_body_size 20M; location /static/ { alias /path/to/telecom_billing/staticfiles/; } location /media/ { alias /path/to/telecom_billing/media/; } location / { proxy_pass http://127.0.0.1:8001; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }这里有个细节proxy_set_header X-Forwarded-Proto $scheme;是为了让 Django 识别请求是 HTTP 还是 HTTPS。如果不配而你又上了 HTTPSDjango 的request.is_secure()永远返回 FalseCSRF 校验和重定向逻辑都容易出问题。6.4 MySQL 部署配置的注意事项数据库切 MySQL 之后务必在settings.py里加DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: telecom_billing, USER: your_user, PASSWORD: your_password, HOST: 127.0.0.1, PORT: 3306, OPTIONS: { charset: utf8mb4, init_command: SET sql_modeSTRICT_TRANS_TABLES, }, } }utf8mb4是为了支持手机号备注里可能出现的 emoji 字符以及冷僻汉字STRICT_TRANS_TABLES是为了让数据库在数据不合法时直接报错而不是静默截断——这一点对计费系统尤其重要因为一个被静默截断的金额字段会导致对账不平且极难追查。7. 代码讲解中的重点模块我建议你逐行看的几个文件这套系统的代码讲解如果只讲登录注册和列表页这些表面功能那等于没讲。真正值得逐行看的是下面这几个模块它们几乎决定了这个项目的水平。7.1 用户模块中关于手机号校验的细节电信系统里用户主键一般不是自增 ID而是手机号。别小看这个设计它直接影响后续所有业务代码里外键的查询效率。如果源码里用了PhoneField或者自定义模型字段来做手机号校验务必看一下它是否包含正则表达式校验比如中国大陆手机号^1[3-9]\d{9}$。如果没有建议补上否则13800000000和23800000000这种非法数据都能进入库后面对账全是麻烦。7.2 订购/退订事务处理为什么必须用transaction.atomic()办理套餐订购和退订是一组强一致性的操作。比如用户办完订购系统要同时做三件事写订单、更新用户当前套餐字段、记录日志。任何一个步骤失败前面的操作都应该回滚否则会出现订单创建了但用户状态没更新的脏数据。Django 里用transaction.atomic()包裹即可from django.db import transaction def subscribe_package(user, package): with transaction.atomic(): order Order.objects.create(useruser, packagepackage, effective_datenow) user.current_package package user.save(update_fields[current_package]) # 如果后面的代码抛异常前面的操作全部回滚源码讲解时如果作者在视图函数里加了transaction.atomic()说明他考虑过业务一致性如果完全没有那二次开发时你把这段加进去能少踩很多坑。7.3 账单查询列表如何避免 N1 查询Django ORM 写起来很方便但也很容易写出 N1 查询——先查列表再循环逐条查关联表。比如账单列表页如果代码是这样bills Bill.objects.filter(userrequest.user) # 循环中访问 bill.order.package.name 时每次都会发一条 SQL这种代码在数据量小的时候没感觉等账单数据积累到几万条页面就卡成 PPT 了。正确的写法是用select_related或prefetch_relatedbills Bill.objects.filter(userrequest.user).select_related(order__package)在代码讲解的章节里我会专门盯着这种细节讲因为这才是决定一个 Django 项目从能跑到能扛的分水岭。8. 账单生成结果的验证思路怎么判断系统算出的钱是准的最后这一点是我针对这类系统特别想说的。代码写完了、部署跑通了接下来你怎么验证它算出来的账单是对的靠肉眼看页面点几下不算数要有一套手工验证的思路。8.1 构造边界场景做测试我建议你在测试环境构造这几类场景用户整月使用套餐不退订账单金额应该等于套餐月租费用户在月中比如 6 月 15 日订购套餐账单金额应该是半月分摊用户在月末比如 6 月 30 日订购套餐账单金额应该极小甚至按 1 天计算用户月中退订套餐下月账单金额应为 0但退订当月应有分摊费用跨月退订7 月 1 日退订7 月账单应该完全不计费。逐条核对任何一个对不上都是计费逻辑的问题必须回到第 4 节的核心流程去调。8.2 用 Django 单元测试固化回归验证源码里如果带了tests.py那是很好的信号。如果没有我给你一个最小可用示例你把它加进billing/tests.pyfrom django.test import TestCase from datetime import date from decimal import Decimal from users.models import User from packages.models import Package from orders.models import Order from billing.services import generate_monthly_bill class BillingTests(TestCase): def setUp(self): self.user User.objects.create(usernamezhangsan, phone13800138000) self.package Package.objects.create(name59元套餐, monthly_feeDecimal(59.00)) def test_full_month_bill(self): order Order.objects.create( userself.user, packageself.package, effective_datedate(2025, 6, 1) ) bill generate_monthly_bill(self.user, date(2025, 6, 1)) self.assertEqual(bill.total_amount, Decimal(59.00)) def test_mid_month_bill(self): order Order.objects.create( userself.user, packageself.package, effective_datedate(2025, 6, 15) ) bill generate_monthly_bill(self.user, date(2025, 6, 1)) # 6月有30天有效天数16天 self.assertEqual(bill.total_amount, Decimal(59.00) * Decimal(16) / Decimal(30))这类测试跑起来以后每次改代码都能快速判断计费逻辑是否被破坏了。对于资费系统来说回归测试就是你的安全网没有这张网谁都不敢轻易改代码。我在实际项目里还会额外做一个数据对账脚本用独立的 SQL 语句按月汇总账单金额和系统生成的总账对比两边数字一致才算通过。这个习惯帮我抓住过好多次隐蔽的计费问题——尤其是跨年、闰年 2 月份天数差异导致的分摊误差。关于怎么把一套 Django 源码吃透、改好、部署稳核心思路基本上就是这些了。如果你也在折腾类似的系统不妨先从费率快照、按天分摊、事务一致性、Decimal 金额这四个关键词入手对照源码逐个确认它们是怎么实现的。这四个点弄明白了不管是做课程设计、毕业设计还是接企业的定制项目你都会比大多数人站得高一层。
返回列表