lty/qy_lty/.planning/codebase/CONVENTIONS.md
2026-05-07 10:37:16 +08:00

293 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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