293 lines
13 KiB
Markdown
293 lines
13 KiB
Markdown
# 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*
|