# 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*
