作為Blake Lab的文化科技融合實驗,第三天我們深入 Django 後端的技術核心,展示如何用優雅的架構設計解決阿美族社群複雜的財務分攤需求。
🎯 今日核心:從部落財務痛點到技術解決方案
昨天完成了前端 UX 設計,今天要處理真正的技術難題:如何設計一個既能處理複雜分帳邏輯,又能維持高效能的後端架構?
這不只是寫 CRUD 的問題,而是要處理:
時間敏感的分攤(後加入者不分攤之前的費用)
選擇性分攤(只分攤特定項目)
權限繼承(群組→活動→支出的權限傳遞)
即時同步(多人同時編輯不衝突)
🏗️ Django 權限系統:扁平化設計的智慧
為何選擇扁平化角色設計?
經過多次迭代,我發現複雜的權限樹反而讓使用者困惑。最終採用扁平化 + 關聯式的設計:
# 簡化但靈活的權限模型
class User(AbstractUser):
class UserRole(models.TextChoices):
ADMIN = 'ADMIN', '系統管理員'
USER = 'USER', '一般用戶'
role = models.CharField(
"角色", max_length=20,
choices=UserRole.choices,
default=UserRole.USER
)
@property
def managed_groups(self):
"""透過關聯獲取管理的群組"""
return self.managing_groups.all()
@property
def managed_events(self):
"""透過關聯獲取管理的活動"""
return Event.objects.filter(managers=self)
設計哲學:權限不是靜態的角色,而是動態的關係。
業務邏輯封裝的最佳實踐
class Event(models.Model):
def can_user_add_expense(self, user) -> bool:
"""
分層權限檢查邏輯
展現了阿美族 kapot(年齡階層)的權限概念
"""
if not user or not user.is_authenticated:
return False
# 層級 1:系統管理員 - 最高權限
if user.role == 'ADMIN':
return True
# 層級 2:活動已鎖定 - 只有管理者可操作
if self.is_locked:
return self.can_user_manage(user)
# 層級 3:活動進行中 - 參與者可新增
if self.participants.filter(
user=user,
is_active=True
).exists():
return True
# 層級 4:活動管理者 - 即使非參與者也可操作
if self.managers.filter(id=user.id).exists():
return True
return False
關鍵洞察:將權限邏輯直接嵌入模型,而不是分散在 View 或 Serializer 中,確保了業務規則的一致性。
🧮 智能分帳算法:處理現實世界的複雜性
四種分攤模式的統一架構
class ExpenseSplit(models.Model):
"""支出分攤記錄 - 核心算法載體"""
class SplitType(models.TextChoices):
AVERAGE = 'AVERAGE', '平均分攤' # 家族聚餐常用
RATIO = 'RATIO', '比例分攤' # 按收入比例
FIXED = 'FIXED', '固定金額' # 事先約定
SELECTIVE = 'SELECTIVE', '選擇性' # 靈活選擇
expense = models.ForeignKey(
Expense, on_delete=models.CASCADE,
related_name='splits'
)
participant = models.ForeignKey(
ActivityParticipant, on_delete=models.CASCADE
)
split_type = models.CharField(
max_length=20, choices=SplitType.choices
)
split_value = models.DecimalField(
max_digits=10, decimal_places=4,
help_text="比例時為小數,固定金額時為金額"
)
calculated_amount = models.DecimalField(
max_digits=10, decimal_places=2
)
# 審計欄位
is_adjusted = models.BooleanField(default=False)
adjusted_by = models.ForeignKey(
User, null=True, on_delete=models.SET_NULL
)
adjusted_at = models.DateTimeField(null=True)
時間敏感分攤的創新實現
這是 PAPA 最具特色的功能之一:
def calculate_expense_splits(expense: Expense) -> List[ExpenseSplit]:
"""
智能分攤計算引擎
處理時間敏感和選擇性分攤的複雜邏輯
"""
splits = []
participants = expense.event.participants.filter(is_active=True)
for participant in participants:
# 時間敏感檢查:晚加入者的特殊處理
if participant.split_option == 'NO_SPLIT':
# 只分攤加入後產生的支出
if expense.date < participant.joined_at:
continue
# 選擇性分攤:白名單機制
elif participant.split_option == 'PARTIAL_SPLIT':
# 檢查是否在分攤白名單中
if expense.id not in participant.partial_split_expenses:
continue
# 計算個人應付金額
amount = calculate_participant_amount(
expense.amount,
participant,
len(eligible_participants)
)
splits.append(ExpenseSplit(
expense=expense,
participant=participant,
split_type=participant.default_split_type,
calculated_amount=amount
))
return splits
創新點:將時間維度納入分攤邏輯,更符合真實世界的使用場景。
🗄️ PostgreSQL 查詢優化:從 N+1 到 1
問題:初版的 N+1 查詢地獄
# ❌ 錯誤示範:會產生大量資料庫查詢
expenses = Expense.objects.all()
for expense in expenses:
print(expense.user.name) # N 次查詢
print(expense.category.name) # N 次查詢
for split in expense.splits.all(): # N 次查詢
print(split.participant.user.name) # N*M 次查詢
解決方案:智能預載策略
# ✅ 優化後:只需要 3 次查詢
class ExpenseViewSet(viewsets.ModelViewSet):
def get_queryset(self):
return Expense.objects.select_related(
'user', # JOIN 使用者表
'category', # JOIN 分類表
'event', # JOIN 活動表
'event__group' # JOIN 群組表
).prefetch_related(
'splits__participant__user', # 預載分攤資料
Prefetch(
'event__participants',
queryset=ActivityParticipant.objects.select_related('user')
)
).annotate(
# 聚合計算,避免額外查詢
total_splits=Count('splits'),
split_sum=Sum('splits__calculated_amount')
)
效能提升:從平均 200ms 降至 15ms(資料量 1000 筆)。
複合索引的精準設計
class Meta:
indexes = [
# 常用查詢路徑的複合索引
models.Index(
fields=['event', 'is_active', '-date'],
name='event_active_date_idx'
),
# 分攤查詢優化
models.Index(
fields=['expense', 'participant'],
name='split_lookup_idx'
),
# 時間範圍查詢
models.Index(
fields=['date', 'event'],
condition=Q(is_active=True), # 部分索引
name='active_date_event_idx'
),
]
🔄 原子性操作:確保資料一致性
使用資料庫事務處理複雜操作
from django.db import transaction
class ExpenseViewSet(viewsets.ModelViewSet):
@action(detail=True, methods=['post'])
def adjust_splits(self, request, pk=None):
"""
調整分攤方式 - 需要原子性保證
"""
expense = self.get_object()
with transaction.atomic():
# 建立保存點
sid = transaction.savepoint()
try:
# 刪除舊的分攤記錄
expense.splits.all().delete()
# 創建新的分攤記錄
total_amount = Decimal('0')
for split_data in request.data['splits']:
split = ExpenseSplit.objects.create(
expense=expense,
**split_data
)
total_amount += split.calculated_amount
# 驗證總金額
if abs(total_amount - expense.amount) > Decimal('0.01'):
transaction.savepoint_rollback(sid)
return Response(
{'error': '分攤總金額與支出金額不符'},
status=400
)
# 記錄審計日誌
ActivityLog.objects.create(
activity=expense.event,
action_type='SPLIT_ADJUST',
description=f'調整支出分攤:{expense.description}',
operator=request.user,
metadata={
'expense_id': expense.id,
'old_count': old_count,
'new_count': len(request.data['splits'])
}
)
transaction.savepoint_commit(sid)
except Exception as e:
transaction.savepoint_rollback(sid)
raise e
return Response({'status': 'success'})
關鍵技術:
使用
savepoint實現細粒度的事務控制金額驗證防止分帳錯誤
完整的審計日誌追蹤
🚀 Railway 部署:從本地到雲端的無縫遷移
智能環境配置
# settings/railway.py
import dj_database_url
from decouple import config, Csv
# 環境感知配置
ENV = config('ENV', default='development')
DEBUG = config('DEBUG', default=False, cast=bool)
# 資料庫 URL 的智能解析
DATABASE_URL = config('DATABASE_URL', default='')
if DATABASE_URL:
# Railway 特殊字符處理
DATABASE_URL = DATABASE_URL.replace('{{', '').replace('}}', '')
DATABASES = {
'default': dj_database_url.parse(
DATABASE_URL,
conn_max_age=600, # 連接池優化
conn_health_checks=True, # 健康檢查
)
}
# Redis 配置與回退機制
REDIS_URL = config('REDIS_URL', default=None)
if REDIS_URL:
# 生產環境:使用 Redis
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels_redis.core.RedisChannelLayer',
'CONFIG': {
"hosts": [REDIS_URL],
"capacity": 1500, # 訊息容量
"expiry": 10, # 訊息過期時間
},
},
}
else:
# 開發環境:使用記憶體
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels.layers.InMemoryChannelLayer',
},
}
部署腳本的最佳實踐
#!/bin/bash
# railway-predeploy.sh - 部署前置作業
echo "🚀 Starting Railway deployment..."
# 資料庫遷移
python manage.py migrate --no-input
# 靜態檔案收集
python manage.py collectstatic --no-input
# 創建預設管理員(如果不存在)
python manage.py shell -c "
from django.contrib.auth import get_user_model
User = get_user_model()
if not User.objects.filter(username='admin').exists():
User.objects.create_superuser('admin', '[email protected]', 'admin')
print('✅ Default admin created')
"
# 初始化測試資料(僅開發環境)
if [ "$ENV" = "development" ]; then
python create_test_scenarios.py
echo "✅ Test data initialized"
fi
echo "✅ Railway deployment ready!"
🎯 今日成果與技術收穫
完成的技術亮點
✅ 扁平化權限系統的優雅實現
✅ 時間敏感分攤算法的創新設計
✅ PostgreSQL 查詢從 200ms 優化到 15ms
✅ 原子性操作確保資料一致性
✅ Railway 無縫部署配置
關鍵技術洞察
簡單勝過複雜:扁平化權限設計比複雜權限樹更易維護
業務邏輯內聚:將權限檢查放在 Model 層提升了程式碼品質
查詢優化至關重要:正確的預載策略可以提升 10 倍效能
原子性保證資料完整:金融相關功能必須使用事務
AI 輔助開發的實際效果
GitHub Copilot 協助生成了 60% 的序列化器程式碼
Claude 3.5 幫助設計了複雜的查詢優化策略
但分帳算法的核心邏輯仍需要人工設計(文化理解無法被 AI 取代)
🔮 明日預告
Day 4 將分享前後端整合的實戰經驗,包括:
React Query 與 Django REST 的完美配合
WebSocket 即時同步的實現細節
錯誤處理與用戶體驗優化
端到端測試策略
技術的價值在於解決真實問題,而不是炫技。PAPA 的後端架構證明了:用對的技術,解決對的問題,就是最好的設計。
🚀 Blake Lab - AI時代文化科技雙領先者
💡 20年政府專案 | 🤖 AI原住民應用 | 🎯 族語NLP開發 | 📈 文化敏感設計
🌐 wchung.tw | 📧 [email protected]
關注 Blake Lab,一起探索技術與文化的無限可能!