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

13 KiB
Raw Blame History

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:

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):

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:

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:

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:

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:

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:

@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):

### [日期] 修改简述

- **文件路径**: 相对于项目根目录的文件路径
- **修改类型**: 新增 / 修改 / 删除 / 重构 / 修复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