# 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`, `CardBatchSerializer` in `card/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`, or `setup.cfg` found - No explicit linter configured in `requirements.txt` - Code style appears self-enforced through code review rather than automated tools ## Import Organization **Order:** 1. Django imports (django, django.db, django.conf, etc.) 2. Django REST Framework (rest_framework, rest_framework.*) 3. Third-party (channels, aliyun, decouple, etc.) 4. Local app imports (relative or full path within project) **Example from `userapp/views.py`:** ```python 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 `Response` with 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):** ```python 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()` from `common.responses` ## Logging **Framework:** Python's built-in `logging` module (no custom wrapper library) **Logger setup pattern:** ```python import logging logger = logging.getLogger(__name__) ``` **Dedicated loggers configured in settings:** - `aiapp` - AI dialogue system logs - `userapp` - User authentication and management logs - `common` - Shared utility logs - `device_interaction` - Device connection logs - `card` - 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 activation - `logger.warning()` - Validation failures, auth attempts, device not found - `logger.error()` - Unexpected exceptions, external service failures **Example from `device_interaction/consumers.py`:** ```python 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()` in `device_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 in `userapp/models.py`) - ViewSet actions use `@action` decorator with docstrings describing endpoint behavior - Swagger schema classes defined inline with docstrings for API documentation **Example from `userapp/models.py`:** ```python 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) and `help_text` for 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 def` signature with minimal parameters **Return Values:** - View methods return DRF `Response` objects - 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__.py` exports) used - Direct imports from app modules instead ## Authentication & Authorization **Authentication:** - Custom `RedisTokenAuthentication` in `userapp/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 endpoints - `permission_classes = [IsAdminOrReadOnly]` custom class for admin write, others read (used in `BotViewSet`, `AchievementViewSet`) - Method-level permissions with `@permission_classes([...])` decorator **Example from `achievement_app/views.py`:** ```python class AchievementViewSet(viewsets.ModelViewSet): permission_classes = [IsAdminOrReadOnly] authentication_classes = [RedisTokenAuthentication] ``` ## Swagger/API Documentation **Pattern:** - Use `@swagger_auto_schema()` decorator from `drf_yasg` for 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()` from `common.swagger_utils` for consistent response wrapping **Example from `userapp/views.py`:** ```python @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 `UserDevice` records are NOT auto-deleted; they're just ignored in control resolution - `is_primary` field is "user's primary device" (per-user), NOT "device's controlling user" — same device can have multiple `is_primary=True` records **Test MAC affordance:** - `AA:BB:CC:DD:EE:FF` is hardcoded in `device_interaction/serializers.py` and `views.py` to 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):** ```markdown ### [日期] 修改简述 - **文件路径**: 相对于项目根目录的文件路径 - **修改类型**: 新增 / 修改 / 删除 / 重构 / 修复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/修改记录.md` records backend changes ONLY - `qy-lty-admin/docs/修改记录.md` records frontend changes ONLY - Cross-project changes: Write separate entries, cross-reference each other ## Settings Configuration **Environment variables via `decouple.config()`:** - Pattern: `config('VAR_NAME')` or `config('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 settings` then `settings.VAR_NAME` - Example from `userapp/views.py`: `if settings.DEBUG else 'https://...'` ## Async/WebSocket Patterns **Async consumers:** - Inherit from `AsyncWebsocketConsumer` (channels) - Use `async def` for all methods - Database calls wrapped with `@database_sync_to_async` decorator - 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*