13 KiB
Coding Conventions
Analysis Date: 2026-05-07
Naming Patterns
Files:
- Django apps follow structure:
{app_name}/models.py,{app_name}/serializers.py,{app_name}/views.py,{app_name}/urls.py,{app_name}/tests.py - Model files: lowercase underscore naming (e.g.,
device_interaction,ali_vi_app,achievement_app) - Serializer files: grouped with models in app directory, separate classes for different use cases (e.g.,
CardTemplateSerializer,CardDetailSerializer,CardBatchSerializerincard/serializers.py)
Functions:
- snake_case for all functions:
send_sms(),get_user_id_from_token(),generate_device_code(),authenticate_with_token() - Async functions use
async def:async def connect(),async def receive() - Helper/private functions prefixed with underscore:
_extract_validation_error_message(),_merge_subv_segments()
Variables:
- snake_case for local variables and instance attributes:
user_id,device_mac,error_message,phone_number - Boolean flags use is_/has_ prefixes:
is_active,is_primary,is_authenticated,has_achievement - Temporary/loop variables:
i,j,obj,result(minimal context only) - Constants: UPPERCASE with underscores:
MESSAGE_TYPE_TEXT,GENDER_CHOICES,TRIGGER_TYPE_CHOICES
Types:
- Model names: PascalCase (e.g.,
ParadiseUser,AffinityRule,UserDevice,DeviceBatch) - Serializer names: PascalCase with "Serializer" suffix (e.g.,
ParadiseUserSerializer,UserInfoSerializer,DeviceTypeSerializer) - ViewSet names: PascalCase with "ViewSet" suffix (e.g.,
AchievementViewSet,DeviceTypeViewSet,UserAchievementViewSet) - Permission classes: PascalCase (e.g.,
IsAdminOrReadOnly) - Middleware classes: PascalCase (e.g.,
StandardResponseMiddleware,TokenAuthMiddleware)
Code Style
Formatting:
- No explicit linter specified in configuration — project uses Django/DRF conventions implicitly
- Indentation: 4 spaces (Django standard)
- Line length: appears to follow PEP 8 (~79-99 chars), but not strictly enforced
- Imports organized: Django → third-party → local (visible in
userapp/views.py,device_interaction/views.py)
Linting:
- No
.flake8,pyproject.toml, orsetup.cfgfound - No explicit linter configured in
requirements.txt - Code style appears self-enforced through code review rather than automated tools
Import Organization
Order:
- Django imports (django, django.db, django.conf, etc.)
- Django REST Framework (rest_framework, rest_framework.*)
- Third-party (channels, aliyun, decouple, etc.)
- Local app imports (relative or full path within project)
Example from userapp/views.py:
from dj_rest_auth.registration.views import RegisterView
from rest_framework import viewsets, status
from .models import ParadiseUser, AffinityRule
from device_interaction.models import Device, UserDevice
from .serializers import ParadiseUserSerializer, CustomRegisterSerializer
from rest_framework.response import Response
from rest_framework.decorators import action, permission_classes
from common.responses import success_response, error_response
import logging
Path Aliases:
- No
@path aliases configured (e.g., no@/services) — full relative paths used - Common local patterns:
from common.responses import ...,from userapp.authentication import ...,from .models import ...
Error Handling
Patterns:
- Try-except blocks catch specific exceptions:
Device.DoesNotExist,ParadiseUser.DoesNotExist,ValidationError - Generic
except Exception as e:used for unexpected errors with logging - DRF exceptions converted to
Responsewith status codes:status.HTTP_400_BAD_REQUEST,status.HTTP_404_NOT_FOUND,status.HTTP_201_CREATED - Validation errors: use serializer validation + raise
serializers.ValidationError()in validators - WebSocket closure on auth failure:
await self.close(code=4001)for authentication failure
Example from userapp/views.py (MAC login):
try:
device = Device.objects.get(mac_address=mac_address)
user_device = UserDevice.objects.filter(device=device).order_by('-bound_at').first()
if user_device is None:
logger.warning(f"Device not bound to any user: {mac_address}")
return error_response(message="设备未绑定用户")
except Device.DoesNotExist:
logger.warning(f"Device not found: {mac_address}")
return error_response(message="设备不存在", status_code=404)
except Exception as e:
logger.error(f"MAC address login failed: {str(e)}")
return error_response(message="登录失败")
Standard Response Format:
- All API responses wrap through
common.middleware.StandardResponseMiddleware - Response format:
{"success": true/false, "code": status_code, "message": "...", "data": {...}} - Helper functions:
success_response(),error_response(),created_response()fromcommon.responses
Logging
Framework: Python's built-in logging module (no custom wrapper library)
Logger setup pattern:
import logging
logger = logging.getLogger(__name__)
Dedicated loggers configured in settings:
aiapp- AI dialogue system logsuserapp- User authentication and management logscommon- Shared utility logsdevice_interaction- Device connection logscard- Card system logs
Aliyun integration: common.aliyun_logging.AliyunLogHandler sends INFO+ level logs to Aliyun Log Service in production
Logging patterns observed:
logger.info()- Login success, token generation, device activationlogger.warning()- Validation failures, auth attempts, device not foundlogger.error()- Unexpected exceptions, external service failures
Example from device_interaction/consumers.py:
import logging
logger = logging.getLogger(__name__)
async def connect(self):
try:
logger.info(f'WebSocket connected for user {self.user_id}')
except Exception as e:
logger.error(f"Error in WebSocket connect: {str(e)}")
Comments
Language: Predominantly Chinese for inline comments and docstrings (business domain, user-facing logic)
When to Comment:
- Complex algorithm explanations (e.g.,
_merge_subv_segments()indevice_interaction/views.py— subtitle merging logic) - Non-obvious validation rules (e.g., device binding "last-bind-wins" semantics)
- WebSocket message type handlers and channel routing
- Async/await patterns and database sync-to-async wrappers
JSDoc/TSDoc:
- Models use Django docstrings in triple-quotes:
"""自定义用户模型"""(appears inuserapp/models.py) - ViewSet actions use
@actiondecorator with docstrings describing endpoint behavior - Swagger schema classes defined inline with docstrings for API documentation
Example from userapp/models.py:
class ParadiseUser(AbstractUser):
"""
自定义用户模型
"""
phone_number = models.CharField('手机号', max_length=20, unique=True, null=True, blank=True)
Field docstrings:
- Django model fields use
verbose_name(Chinese) andhelp_textfor field-level docs - Example:
models.CharField('手机号', max_length=20, unique=True, null=True, blank=True)
Function Design
Size: Functions stay reasonably focused (50-100 lines max observed, with WebSocket consumers being larger)
Parameters:
- Functions accept
self(methods),request(view methods),obj(serializer methods) - No heavy use of **kwargs; explicit named parameters preferred
- Async functions have
async defsignature with minimal parameters
Return Values:
- View methods return DRF
Responseobjects - Model methods return model instances or strings (
__str__) - Serializer methods return serialized data (dict/list)
- Async consumers return None (side effects via channel layer messaging)
Module Design
Exports:
- Apps explicitly import from
models.py,serializers.py,views.py— no star imports (from .models import *) - URLconf imports specific views/viewsets:
from .views import DeviceTypeViewSet - Serializers list all field serializers in their module, not hidden
Barrel Files:
- No barrel files (index.py/
__init__.pyexports) used - Direct imports from app modules instead
Authentication & Authorization
Authentication:
- Custom
RedisTokenAuthenticationinuserapp/authentication.py— token stored in Redis with 30-day TTL - Token format:
token:{token}for regular users,admin_token:{token}for admins - Used via
authentication_classes = [RedisTokenAuthentication]on views/viewsets - AllowAny permission for login/registration endpoints
Authorization:
permission_classes = [IsAuthenticated]for user-only endpointspermission_classes = [IsAdminOrReadOnly]custom class for admin write, others read (used inBotViewSet,AchievementViewSet)- Method-level permissions with
@permission_classes([...])decorator
Example from achievement_app/views.py:
class AchievementViewSet(viewsets.ModelViewSet):
permission_classes = [IsAdminOrReadOnly]
authentication_classes = [RedisTokenAuthentication]
Swagger/API Documentation
Pattern:
- Use
@swagger_auto_schema()decorator fromdrf_yasgfor endpoint docs - Define request/response schema classes inline (inherit from
serializers.Serializer) - Specify operation_summary, operation_description, request_body, responses parameters
- Use
get_standardized_response_schema()fromcommon.swagger_utilsfor consistent response wrapping
Example from userapp/views.py:
@swagger_auto_schema(
request_body=MacAddressLoginRequestSchema,
responses={
200: openapi.Response('登录成功', get_standardized_response_schema()),
404: openapi.Response('设备不存在或未绑定', get_standardized_response_schema())
},
operation_description="使用设备MAC地址进行登录,返回认证令牌"
)
def post(self, request):
Device Binding Convention (Critical)
"Last-bind-wins" semantics:
UserDevice.Meta.ordering = ['-bound_at']— orders by most recent binding first- When a device is bound by multiple users, the most recently bound user controls it
- MAC login in
userapp/views.py:UserDevice.objects.filter(device=device).order_by('-bound_at').first()explicitly gets latest binder - Old
UserDevicerecords are NOT auto-deleted; they're just ignored in control resolution is_primaryfield is "user's primary device" (per-user), NOT "device's controlling user" — same device can have multipleis_primary=Truerecords
Test MAC affordance:
AA:BB:CC:DD:EE:FFis hardcoded indevice_interaction/serializers.pyandviews.pyto skip "device already bound" validation — test-only
Project Modification Record Rule (CRITICAL)
Every meaningful code change MUST be recorded in docs/修改记录.md at session time, appended to the top (newest first). This is a load-bearing convention.
Format (from docs/修改记录.md header):
### [日期] 修改简述
- **文件路径**: 相对于项目根目录的文件路径
- **修改类型**: 新增 / 修改 / 删除 / 重构 / 修复Bug
- **修改内容**: 具体修改了什么
- **修改原因**: 为什么要做这个修改
Applies to:
- Business code, configuration, migration files, CI/k8s/Dockerfile, structural doc changes → MUST record
- Typo fixes, comment tweaks → Can omit
- Temp debug scripts, .gitignore files, local experiments → Do not record
Cross-project coordination:
qy_lty/docs/修改记录.mdrecords backend changes ONLYqy-lty-admin/docs/修改记录.mdrecords frontend changes ONLY- Cross-project changes: Write separate entries, cross-reference each other
Settings Configuration
Environment variables via decouple.config():
- Pattern:
config('VAR_NAME')orconfig('VAR_NAME', default=value, cast=type) - No .env reading in code — all via settings.py config layer
- Critical vars:
SECRET_KEY,DEBUG,POSTGRESQL_DATABASE_*,REDIS_LOCATION,REDIS_PASSWORD,KIMI_API_KEY,ALIYUN_*,VOLCENGINE_*
Settings access in views:
from django.conf import settingsthensettings.VAR_NAME- Example from
userapp/views.py:if settings.DEBUG else 'https://...'
Async/WebSocket Patterns
Async consumers:
- Inherit from
AsyncWebsocketConsumer(channels) - Use
async deffor all methods - Database calls wrapped with
@database_sync_to_asyncdecorator - Channel layer messaging:
await self.channel_layer.group_add(),await self.channel_layer.group_send() - Connection groups by user ID:
device_{user_id}(fixed convention per CLAUDE.md)
Token authentication in WebSocket:
- Extract token from URL path:
ws://domain/ws/device/token/{token}/ - Token validated via mock request object calling
RedisTokenAuthentication.authenticate() - Close on auth failure:
await self.close(code=4001)
Convention analysis: 2026-05-07