社区物业管理系统听起来平平无奇,但真正上手做过的人才知道,要把报修、缴费、公告、访客这些看似简单的场景理清楚,背后要趟的坑一点不比做一个电商平台少。这个“城市生活e家平台”的项目,我前后花了差不多三个月时间,从需求梳理到上线部署,踩了不少坑,也积累了一些实战经验。今天就把整个项目的设计思路、核心模块的实现细节、以及过程中遇到过的典型问题,全部摊开来讲,希望能给正在做类似系统的朋友一些参考。
先说下项目背景。这是一个面向城市社区居民的物业服务平台,核心目标是让业主不再需要跑到物业办公室排队,报修、缴费、投诉、查公告这些日常事务,全部可以在线上完成。物业管理端则负责处理工单、发布公告、管理收费项目。技术选型上,我采用的是Django 4.x + SQLite(开发环境)/ PostgreSQL(生产环境),前端用了Bootstrap 5加少量原生JavaScript,整体架构就是经典的服务端渲染模式。当时也纠结过要不要用前后端分离,后来想明白了:这类系统强依赖SEO的场景不多,服务端渲染开发效率高,维护成本低,对物业这种体量的项目来说绰绰有余。
1. 项目整体设计与思路拆解
1.1 需求分析里的几个关键判断
在动工之前,我把需求反复过了好几遍。表面上看,社区物业系统的核心就是“报修”、“缴费”、“公告”三件套,但实际操作中你会发现,很多细节需求才是真正决定开发量的地方。比如报修,业主端需要上传照片吗?需要选择故障类型吗?物业端需要派单给具体维修师傅吗?师傅接单后要不要在手机端操作?再比如缴费,物业费是按面积算还是按户算?停车费是月缴还是年缴?逾期要不要生成滞纳金?这些如果不提前跟业主方和物业方反复确认,后期返工成本会非常高。
我当时把需求拆成了三个角色维度:业主端(小程序/网页)、物业管理员端(后台)、系统管理员端(平台运营)。前两者是业务核心,第三个主要负责系统配置、用户管理、数据统计这块。每个角色的功能清单我都用表格列出来,逐条跟需求方确认。
| 角色 | 核心功能 | 备注 |
|---|---|---|
| 业主 | 在线报修、维修进度查看、物业缴费、缴费记录查询、公告查看、投诉建议、访客预约 | 需要微信登录和手机号绑定 |
| 物业管理员 | 报修受理与派单、维修工单管理、缴费账单生成与核销、公告发布、投诉处理 | 需要工单看板,按状态和紧急程度排序 |
| 系统管理员 | 楼栋/房屋数据维护、物业人员账号管理、收费项目配置、数据统计报表 | 负责初始化数据和日常维护 |
这里有个容易忽略的点:房屋数据模型。物业系统的底座是“楼栋-单元-房屋”这样的层级结构,业主必须绑定到具体的房屋上,才能产生对应的缴费账单和报修记录。如果一开始没有把房屋数据结构设计好,后面所有跟“房屋”相关的业务逻辑都会变得很别扭。我采用的做法是设计了一张building表(楼栋)、一张unit表(单元)、一张house表(房屋),house表通过外键关联unit,再通过一个owner字段去关联业主用户账号,做到“一房一主”的业务约束。
1.2 为什么用Django而不是其他框架
做这个项目之前,我也考虑过用Flask或者FastAPI。但在对比之后,还是坚定地选了Django,原因有这么几个。
第一,Django自带的Admin后台非常强大。物业管理员要维护楼栋、房屋、收费项目这些数据,如果全部自己写管理界面,工作量会多出一大截。Django Admin可以直接生成基于模型的CRUD界面,加上自定义list_display、list_filter、search_fields这些配置,几分钟就能搞定一个功能完整的数据维护后台。这个优势在做内部管理系统时尤其明显。
第二,Django的ORM非常好用,特别是在做复杂查询的时候。物业系统里有很多关联关系,一个缴费订单要关联房屋、关联账单类型、关联支付记录;一个报修工单要关联业主、关联房屋、关联维修师傅。如果用原生SQL,光是拼接这些JOIN就很头疼,但用Django的ORM,用select_related和prefetch_related就能高效地解决查询性能问题。
第三,Django自带认证体系。用户登录、会话管理、权限控制这些安全相关的功能,Django本身就有成熟的解决方案。虽然实际开发中我还是要做一些定制(比如业主用手机号验证码登录),但有Django的认证框架做基础,省掉了很多底层工作。
阶段选型切忌跟风。如果项目核心是业务CRUD和数据管理,Django这种“全家桶”式框架反而是效率最高的选择;如果一个项目需要处理高并发的I/O任务,那异步性能更强的框架会更合适。社区物业系统这种场景,性能和并发都不是瓶颈,开发效率和可维护性才是。
1.3 项目的整体架构规划
整个项目我分了三个主要服务端应用:accounts(用户认证与档案)、property(物业核心业务:楼栋房屋、工单、投诉)、payments(缴费与账单)。每个应用各自管理自己的模型、视图和模板,通过Django的URL路由串联起来。
为什么这么拆分?因为这三个子域的变更频率和迭代节奏不一样。用户认证这块一般稳定,缴费模块因为涉及支付回调、账单逻辑,改动最多;物业核心业务的工单流则是系统最复杂的地方。拆开后,每个应用可以独立维护,互不干扰,这对后期的排错和演进都很有帮助。
整个项目的工程结构是这样的:
city_life/ ├── accounts/ # 用户登录、注册、业主认证 │ ├── models.py # UserProfile、OwnerProfile │ ├── views.py # 登录、注册、验证码 │ ├── urls.py │ └── migrations/ ├── property/ # 物业核心业务 │ ├── models.py # Building、Unit、House、RepairOrder │ ├── views.py # 报修、派单、工单处理 │ ├── templatetags/ # 自定义模板标签 │ ├── urls.py │ └── migrations/ ├── payments/ # 缴费业务 │ ├── models.py # BillType、Bill、PaymentRecord │ ├── views.py # 账单查询、在线支付 │ ├── urls.py │ └── migrations/ ├── templates/ # 全站模板 ├── static/ # 静态资源 ├── manage.py └── config/ # 项目配置 ├── settings.py ├── urls.py └── wsgi.py这个划分方式不是我拍脑袋想出来的,而是基于之前做过的一个失败的“大应用”项目总结出来的。最初那个项目把所有模型都堆在一个app里,半年后代码就乱到不敢动了。Django的app本质上就是一个模块边界,按业务域划分边界是一条值得这条严格遵守的红线。
2. 核心细节解析与实操要点
2.1 报修工单的状态机设计
报修,是物业系统里最好体现业务逻辑功底的一个模块。表面上看是“业主提交、物业处理”这么简单,实际跑起来,状态流转稍微没设计好,就会出现“业主报了修不知道什么进度、物业处理完忘了闭环、财务核销对不上账”这些乱七八糟的事。
我设计的报修工单状态机是这样的:
待受理 → 已派单 → 维修中 → 待验收 → 已完成 │ │ └──────────┴→ 已取消- 业主提交报修后,工单进入“待受理”状态,此时物业管理员可以看到待处理列表。
- 物业管理员点击“派单”,工单变成“已派单”,并会指定一个维修师傅(这里可以用DTO字段记录师傅的姓名和电话)。
- 维修师傅在物业端小程序里操作“开始维修”,工单变为“维修中”。
- 维修完成,师傅点“完工”,工单进入“待验收”状态。
- 业主要在业主端确认,如果没问题点“验收通过”,工单才真正变成“已完成”。
这里有个细节值得注意:验收环节不能省略。如果物业方修完就直接标记完成,业主有异议没地方反馈,那就变成“修了等于没修”。加一个验收环节,就是把“服务完成”的评判权交还给业主本人,体验也好,后续的投诉也少。
另一个容易踩坑的地方是:取消状态。如果维修师傅不够、或者业主自己不想修了,工单需要能取消,但取消时的原因必须记录。我加了一个cancel_reason字段,就是为了运营后期做数据复盘用的。
在设计这个状态机时,为了不让change_state的逻辑散落得到处都是,我把状态变更的方法统一收敛在模型里:
class RepairOrder(models.Model): STATUS_CHOICES = [ ('pending', '待受理'), ('assigned', '已派单'), ('processing', '维修中'), ('review', '待验收'), ('completed', '已完成'), ('cancelled', '已取消'), ] status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending') cancel_reason = models.CharField(max_length=255, blank=True) def assign(self, worker_name, worker_phone): self.status = 'assigned' self.worker_name = worker_name self.worker_phone = worker_phone self.save(update_fields=['status', 'worker_name', 'worker_phone', 'updated_at']) def complete_review(self): if self.status != 'processing': raise ValueError("只有维修中的工单才能标记完工") self.status = 'review' self.save(update_fields=['status', 'updated_at'])这样把状态转移的规则收口在模型层,视图里只负责调用方法,就不会出现“controller里到处都在改status”的乱象。另一个好处是,如果后面要接入Webhook通知、或者加状态变更记录,只需在模型方法里统一加就行。
2.2 物业缴费模块的账单生成逻辑
缴费模块看似简单——查账单、支付、更新状态——但真正做得好的缴费模块,难点在于账单生成这一步。
账单不能随便手动一笔一笔加,否则数据就乱了。我的做法是设计了一套月度自动生成账单的机制:每个月的固定日期,系统自动扫描所有已入住的房屋,为每个房屋生成当月物业费账单。物业费的算法是“单价 × 建筑面积”,单价在收费项目配置里维护,建筑面积在房屋数据里维护。
在这个基础上,还要支持临时的费用项,比如停车管理费、垃圾清运费、维修材料费。这种临时费用项标记为manual类型,由物业管理员手动生成,关联到具体房屋。
账单的核心状态是:未支付、已支付、已作废。
pending → paid pending → void生成账单的Python代码用Celery定时任务调用:
# 假设这是celery任务 @app.task def generate_monthly_bills(): houses = House.objects.filter(is_active=True, owner__isnull=False) bill_type = BillType.objects.get(code='property_fee') month = datetime.now().strftime('%Y-%m') for house in houses: amount = Decimal(house.building.unit_price) * house.area Bill.objects.get_or_create( house=house, bill_type=bill_type, period=month, defaults={'amount': amount, 'status': 'pending'} )这里用get_or_create来保证幂等性,避免了重复生成账单的问题。定时任务每天跑一次,但因为有周期字段的唯一约束,所以只会生成一次。
实际开发中,关于金额字段必须用DecimalField这一点需要特别注意。刚开始做的时候我用的是FloatField,结果有一次算物业费的时候发现0.1 + 0.2的精度问题,直接在账单里显示出一个多出几毛钱的费用,被业主找上门来。后来Django社区里翻了下资料才确认,金额类型一律用DecimalField,并且要设置max_digits和decimal_places,千万不能用Float。
2.3 业主认领房屋的流程设计
一个很容易被忽略的痛点就是“业主和房屋怎么绑定”。如果随便开放绑定,就会出现有人把自己绑到别人家房子上的情况。
C端用了两种验证方式:
手机号验证:业主注册时绑定手机号,物业管理员在后台录入业主名单时也维护手机号。业主用手机号登录后,系统会自动匹配手机号对应的房屋信息,弹窗让业主确认认领。这个方式效率高,但依赖物业管理员准确录入手机号。
手动申请:业主提交绑定申请,选择楼栋、单元、门牌号,上传一些基础的认证材料(比如购房合同号),由物业管理员在后台审核。这种方式麻烦一点,但严谨。
实际场景里两种并存最合适。自动匹配覆盖多数情况,手动申请兜底。在设置owner字段时,我还加了unique约束和软删除逻辑,避免“一房多主”造成后续账单归属混乱。
2.4 公告与投诉模块的设计细节
公告模块比较简单,就是物业管理员发布、业主查看,核心点在已读标记和置顶排序。我做了已读状态表来记录每个用户的已读情况,这样业主看到的公告列表能有个醒目的小红点提示。
投诉模块反而比想象中复杂,主要原因是投诉的内容往往不是“标准工单”,无法直接派给维修师傅,需要物业客服先判断。所以投诉模块我加了一个“评论备注”功能,物业管理员可以在投诉单里追加工单备注,最后将投诉单标记为已办结。这里报表统计时要区分“投诉”和“报修”,不然月度统计时数据会混在一块。
3. 实操过程与核心环节实现
3.1 数据库模型设计与约束
Django的ORM模型是整个系统的地基。我的数据库表主要有:用户表、房屋表、账单表、报修表、公告表、投诉表。几个关键模型的字段和约束如下。
House(房屋表):
class House(models.Model): building = models.ForeignKey(Building, on_delete=models.PROTECT, verbose_name='所属楼栋') unit = models.CharField(max_length=20, verbose_name='单元号') room_number = models.CharField(max_length=20, verbose_name='房号') area = models.DecimalField(max_digits=8, decimal_places=2, verbose_name='建筑面积') owner = models.OneToOneField( settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.SET_NULL, related_name='owned_house', verbose_name='业主' ) is_active = models.BooleanField(default=True) class Meta: unique_together = ('building', 'unit', 'room_number')这里重点说一下unique_together,这个字段约束能直接解决“同一栋楼的同一个单元同一个门牌号怎么能有两个房号记录”的问题。算是建表时就锁住了脏数据的入口。
RepairOrder(报修表):
class RepairOrder(models.Model): house = models.ForeignKey(House, on_delete=models.CASCADE, verbose_name='关联房屋') reporter = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, verbose_name='报修人') description = models.TextField(verbose_name='问题描述') images = models.JSONField(default=list, blank=True, verbose_name='图片列表') status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending') priority = models.CharField(max_length=10, choices=[('low', '低'), ('medium', '中'), ('high', '高')], default='medium') worker_name = models.CharField(max_length=50, blank=True) worker_phone = models.CharField(max_length=20, blank=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True)报修的照片上传,我直接用了Django的ImageField。但在生产环境要注意,如果直接用本地存储,早晚会遇到磁盘被塞满的问题。我当时是配置了自定义storage指向对象存储服务(某云OSS),这样图片都存到云端,本地只保留数据库记录里的URL路径。
Bill(账单表):
class Bill(models.Model): house = models.ForeignKey(House, on_delete=models.CASCADE) bill_type = models.ForeignKey(BillType, on_delete=models.CASCADE) period = models.CharField(max_length=7, verbose_name='账期') # 如 2024-06 amount = models.DecimalField(max_digits=10, decimal_places=2) status = models.CharField(max_length=10, choices=[('pending', '未支付'), ('paid', '已支付'), ('void', '已作废')], default='pending') paid_at = models.DateTimeField(null=True, blank=True) class Meta: unique_together = ('house', 'bill_type', 'period')这个unique_together也是关键。没有这个约束,定时任务跑重复了就会生成两条相同金额的账单,业主付了一次钱,发现另一条账单还在挂着,投诉就来了。
3.2 视图逻辑与URL设计:从页面到接口
整个项目的视图我按“页面渲染”和“数据交互”两类来设计。凡是GET请求且需要整页渲染的,走Django的TemplateView;凡是POST提交数据或返回JSON的,走APIView风格的自定义视图函数。
拿报修工单来说,业主端的提交请求是这样的:
@login_required def create_repair_order(request): if request.method == 'POST': form = RepairOrderForm(request.POST, request.FILES) if form.is_valid(): order = form.save(commit=False) order.reporter = request.user order.house = request.user.owned_house order.save() return redirect('property:repair_detail', pk=order.pk) else: form = RepairOrderForm() return render(request, 'property/repair_form.html', {'form': form})这里有几个关键点:
- 用login_required保证只有登录用户才能提交工单。
- 房屋信息从request.user.owned_house获取,而不是让前端传。为什么?因为前端传的话,用户可以伪造自己不是业主的房屋ID。所有“当前操作用户是谁”的信息,一律从后端Session或认证上下文中取,这是安全底线。
- 上传的图片通过request.FILES传入处罚,Django会自动处理临时文件和表单解析。
物业端派单的视图逻辑更直接:
@staff_member_required def dispatch_order(request, pk): order = get_object_or_404(RepairOrder, pk=pk, status='pending') if request.method == 'POST': worker_name = request.POST.get('worker_name') worker_phone = request.POST.get('worker_phone') if worker_name and worker_phone: order.assign(worker_name, worker_phone) return redirect('property:repair_list') return render(request, 'property/dispatch_form.html', {'order': order})这里用staff_member_required做了权限控制,只有物业员工能进入派单页面。在派单时就把师傅的姓名电话记录下来,便于后续业主直接联系师傅,也便于物业端工作人员追踪进度。
URL配置上,为了后续接口的稳定,我尽量用语义化路径:
urlpatterns = [ path('repairs/', repair_list, name='repair_list'), path('repairs/create/', create_repair_order, name='repair_create'), path('repairs/<int:pk>/', repair_detail, name='repair_detail'), path('repairs/<int:pk>/dispatch/', dispatch_order, name='repair_dispatch'), path('repairs/<int:pk>/complete/', complete_repair, name='repair_complete'), path('bills/', bill_list, name='bill_list'), ]3.3 支付模块的对接实现
支付环节可能是整个项目里最让人头大的模块。我当时对接的是一种常见的H5聚合支付服务,流程大概是:后端生成订单号,调支付服务接口拿到支付凭证,前端拿到凭证跳转到支付页,支付成功之后支付服务异步回调通知后端。
首先看支付接口的封装。这一步我建议可以配置在单独的service.py文件里,不要让视图直接满天飞HTTP请求:
import requests import hashlib import time class PaymentService: def __init__(self, config): self.app_id = config['app_id'] self.api_key = config['api_key'] self.gateway_url = config['gateway_url'] def create_payment(self, order_no, amount, notify_url, return_url): params = { 'app_id': self.app_id, 'order_no': order_no, 'amount': str(amount), 'notify_url': notify_url, 'return_url': return_url, 'timestamp': int(time.time()) } params['sign'] = self._sign(params) resp = requests.post(f'{self.gateway_url}/pay', data=params, timeout=10) resp.raise_for_status() return resp.json() ...这里最需要注意的是签名机制。所有调用支付服务的请求参数必须按指定规则拼接后加签,同步回调验签,异步回调也要验签。验签的同时还要校验订单金额是否一致。金额校验这个坑我踩过:有次线上回调只校验了订单号没校验金额,结果一个测试环境的1分钱订单回调,把正式环境的同号订单改了状态。还好只发生在测试阶段,但已经足够吓出冷汗。
支付成功后的回调处理:
@csrf_exempt def payment_notify(request): data = request.POST.dict() sign = data.pop('sign', '') if not verify_sign(data, sign): return HttpResponse('bad sign', status=400) order_no = data.get('order_no') amount = Decimal(data.get('amount')) bill = get_object_or_404(Bill, order_no=order_no) if bill.amount != amount: return HttpResponse('amount mismatch', status=400) bill.status = 'paid' bill.paid_at = timezone.now() bill.payment_method = data.get('channel', '') bill.save(update_fields=['status', 'paid_at', 'payment_method']) return HttpResponse('success')这种第三方支付服务特别在意回调接口的响应。如果返回不是它规定的“success”字眼,它会认为回调失败,然后持续重试。所以回调处理里不管业务成功还是失败,协议层面都要确保返回“success”,业务失败就靠前端对账补救。
3.4 模板渲染与前端交互的一点心得
Django的模板系统功能有限,不过用来做这类内部管理系统完全够用。我的做法是用Bootstrap 5写一套基础布局模板,然后每个功能页继承基础模板。前端交互主要靠少量JavaScript做局部更新。
比如报修列表页,我用了一个简单的fetch请求来更新工单状态,而不需要刷新整个页面:
async function changeStatus(repairId, targetStatus) { const resp = await fetch(`/repairs/${repairId}/status/`, { method: 'POST', headers: { 'X-CSRFToken': getCookie('csrftoken'), 'Content-Type': 'application/json' }, body: JSON.stringify({ status: targetStatus }) }); const result = await resp.json(); if (result.ok) { location.reload(); } else { alert(result.error); } }这种处理方式的好处是用户体验提升明显,坏处是后端要额外写一个JSON接口。所有POST接口里,CSRF token的处理都必须特别留意,Django默认开启CSRF防护,直接用fetch提交时会报403,必须在headers里带上正确的Token。
3.5 部署要点:从开发机到云服务器
开发完成后的部署环节,我踩的坑也不比开发少。一开始图省事直接用python manage.py runserver跑在服务器上,结果监控告警直接就来了。后来规规矩矩用Gunicorn + Nginx部署才稳定下来。
完整的部署流程大概是这几步:
- 在服务器上创建虚拟环境,把requirements.txt装完。
- 收集静态文件:python manage.py collectstatic。
- 迁移数据库:python manage.py migrate。
- 用Gunicorn启动Django服务。
- 用Nginx把80端口转发到Gunicorn的socket接口。
- 用Supervisor守护Gunicorn进程。
Gunicorn的配置参数值得说一下。worker数量不是越多越好,多了反而占内存,太少又撑不住请求。当时配的是4个worker,每worker 2线程,跑下来压力测试完全没有问题。
Nginx配置里有个关键静态文件路径:
location /static/ { alias /var/www/city_life/static/; expires 30d; }静态文件用expires做浏览器缓存,能明显减少服务端压力。每次发布上线后,建议去服务器上手动重新collectstatic一下,如果静态文件有缓存洁癖,干脆在文件名上加版本号避免缓存污染。
数据库方面,生产环境建议切到PostgreSQL而不是SQLite。SQLite在开发阶段很舒服,但它不支持并发写,物业管理员和业主同时操作时,偶尔会有“database is locked”的错误。迁移到PostgreSQL之后,这个报错就再也没出现过。
4. 常见问题与排查技巧实录
这个模块实操性最强,我把开发三个月里遇到过的典型问题和解决思路整理成了一张表,给后来人一个速查的参考。
| 问题现场 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 报修图片上传后400错误 | 上传体积超出后端限制 | 查看Django报错日志 | 配置DATA_UPLOAD_MAX_MEMORY_SIZE,前端限制5MB |
| 业主支付成功但账单还是未支付 | 回调接口验签或金额检验失败 | 查看支付服务后台日志对应的回调记录 | 验证回调数据处理流程,确认异步回调能正确落库 |
| 同一房屋被两个业主绑定 | 缺少唯一约束/认证逻辑漏洞 | 查数据库house表的owner字段 | 加OneToOneField唯一约束,前端提示“该房屋已被绑定” |
| collectstatic后静态文件404 | 路径配置错误 | curl静态文件看返回状态码 | Nginx配置alias指向实际静态文件目录 |
| 定时任务生成了重复账单 | Celery任务重复执行 | 查任务日志 | 用get_or_create + unique_together字段兜底 |
| 页面报错“table already exists” | 迁移顺序不统一 | 查迁移记录文件 | 统一用makemigrations生成迁移文件,并从版本库维护迁移顺序 |
再补充几个隐性的坑。
第一是时区问题。Django的settings里如果设置TIME_ZONE = 'Asia/Shanghai',但USE_TZ = True,那么存入PostgreSQL的时间会是UTC时区,而模板渲染时会自动转成上海时间。这个机制本身没毛病,但如果你在代码里用了datetime.now()而不是timezone.now(),就会混入本地时间,导致查询范围时数据边界错乱。所以代码里统一用timezone.now(),这个问题连资深开发者也会偶尔中招。
第二是跨域问题。虽然主服务是SSR,但某个页面我加了从JSON接口里去取数据的逻辑,于是出现了本地调试时正常、部署到服务器后接口请求失败的情况。原因是我没有使用JS去调自己域名的接口还去访问了不同的IP端口。解决方式:要么所有API都在同一个域名下,要么后端配置CORS。
第三是图片存储越占越满的问题。本地存储虽然方便,但维护成本极高。前期如果不存对象存储,后面迁移数据时要把本地图片全部搬到云端,而且还要处理URL改写的问题,非常痛苦。所以再次强调:图片或附件类数据,从第一天开始就应该存云端,不要贪图本地存储的一时方便。
第四是权限控制的安全隐患。物业系统的角色比较多,一不小心就会漏掉权限判断。我写了一个装饰器统一处理权限,凡是staff_member_required装饰的视图函数,都只能被物业/管理员访问;普通业主访问时直接403。每一个视图函数都必须明确它的访问角色,这算是我写这个系统时守住的一条铁律。
5. 一些值得复盘的经验体会
整个项目从零到一跑下来,有几点心得特别想分享。
第一,Django的Admin后台虽然好用,但别指望它能完全取代定制页面。Admin后台适合物业管理员进行数据维护,但如果要处理复杂的工单流程,还是得写定制页面。Admin可以加速原型开发,中期必须逐步迁移到真正的业务界面上。
第二,数据模型设计上有几个今天看来特别值得的“坚持”:把所有金额字段设计成DecimalField、把所有关键的联合唯一约束都写在Meta里、把所有状态机的变更逻辑收敛在模型层。这些坚持让后期维护时少走了很多弯路。
第三,测试环节的重要性。我前两个月光顾着写功能,到后面开始加单元测试的时候才发现,没有测试的代码就像没有安全网的高空作业。回归测试至少覆盖了最核心的几段逻辑:状态流转、账单生成、回调验签。后续再有二期的迭代,这种底气和安全感,真金白银都换不来。
最后再分享一个踩坑后的改变:部署流程里,发布脚本的顺序一定要固定,数据库迁移必须在代码更新前执行。如果代码已经更新了,而迁移文件还没跑,认证逻辑里新增的那些字段就会因为数据库里没有对应列而报500,这算得上是最常见的部署事故了。
这个项目的二期现在已经在规划中了。社区团购、智能门禁对接、物业公告的定向推送这些新需求,都有了依托于这次架构清晰的table结构。后端因为app拆分合理,接新功能都不用伤筋动骨,这应该算得上是对当初“先规划、再动手”的一个回报吧。