docs: map existing codebase
This commit is contained in:
parent
2d82b2ef7f
commit
64a8cb8f7b
251
qy_lty/.planning/codebase/ARCHITECTURE.md
Normal file
251
qy_lty/.planning/codebase/ARCHITECTURE.md
Normal file
@ -0,0 +1,251 @@
|
||||
# Architecture
|
||||
|
||||
**Analysis Date:** 2026-05-07
|
||||
|
||||
## Pattern Overview
|
||||
|
||||
**Overall:** Multi-app Django with REST API + ASGI WebSocket support
|
||||
|
||||
**Key Characteristics:**
|
||||
- Each Django app is a bounded context with models, views, serializers, and URLs
|
||||
- HTTP API via Django REST Framework (DRF) ViewSets
|
||||
- Real-time WebSocket via Django Channels for device interactions
|
||||
- Custom token-based authentication with Redis backing
|
||||
- Standardized response middleware for all API responses
|
||||
- Message-driven device-to-device communication via WebSocket groups
|
||||
|
||||
## Layers
|
||||
|
||||
**API Layer (HTTP):**
|
||||
- Purpose: Expose business logic as REST endpoints
|
||||
- Location: Each app's `views.py` contains ViewSets and APIViews
|
||||
- Contains: DRF ViewSets, APIViews, action decorators
|
||||
- Depends on: Models, Serializers, Authentication (RedisTokenAuthentication)
|
||||
- Used by: Mobile app (Unity), Web admin (Next.js), External integrations
|
||||
|
||||
**WebSocket Layer (ASGI):**
|
||||
- Purpose: Real-time bidirectional communication for device control
|
||||
- Location: `device_interaction/consumers.py` (DeviceConsumer), `device_interaction/routing.py`
|
||||
- Contains: AsyncWebsocketConsumer, message handlers, channel layer group operations
|
||||
- Depends on: Models, Channels, Redis channel layer, Token authentication
|
||||
- Used by: Unity device terminal, Unity mobile app
|
||||
|
||||
**Serializer Layer:**
|
||||
- Purpose: Validate and transform data between API and models
|
||||
- Location: Each app's `serializers.py`
|
||||
- Contains: DRF ModelSerializer subclasses, custom validation
|
||||
- Depends on: Models, ValidationError exceptions
|
||||
- Used by: ViewSets for request/response serialization
|
||||
|
||||
**Model Layer:**
|
||||
- Purpose: Define data schema and business rules
|
||||
- Location: Each app's `models.py`
|
||||
- Contains: Django Models (ParadiseUser, Device, UserDevice, ChatMessage, etc.)
|
||||
- Depends on: Django ORM
|
||||
- Used by: Views, Serializers, Admin interface, WebSocket consumers
|
||||
|
||||
**Middleware Layer:**
|
||||
- Purpose: Cross-cutting concerns
|
||||
- Location: `common/middleware.py` (StandardResponseMiddleware), `device_interaction/auth.py` (TokenAuthMiddleware)
|
||||
- Contains: StandardResponseMiddleware wraps all responses; TokenAuthMiddleware authenticates WebSocket
|
||||
- Depends on: DRF Response, Channels scope
|
||||
- Used by: All API responses, all WebSocket connections
|
||||
|
||||
**Authentication Layer:**
|
||||
- Purpose: Validate and identify users
|
||||
- Location: `userapp/authentication.py` (RedisTokenAuthentication), `device_interaction/auth.py` (TokenAuthMiddleware)
|
||||
- Contains: Token lookup from Redis, user resolution
|
||||
- Depends on: Redis cache, ParadiseUser model
|
||||
- Used by: DRF (HTTP), WebSocket, Admin views
|
||||
|
||||
**Pagination Layer:**
|
||||
- Purpose: Standardize list pagination
|
||||
- Location: `common/pagination.py` (CustomPageNumberPagination)
|
||||
- Contains: Page size configuration, dynamic page_size parameter
|
||||
- Depends on: DRF pagination
|
||||
- Used by: All list ViewSet actions
|
||||
|
||||
**Audio Service Abstraction:**
|
||||
- Purpose: Provide vendor-agnostic audio synthesis/recognition
|
||||
- Location: `aiapp/audio/AudioService.py`
|
||||
- Contains: Multi-provider factory (Aliyun, Volcengine, Tencent)
|
||||
- Depends on: External vendor SDKs
|
||||
- Used by: `aiapp/views.py` for voice chat, speech-to-text
|
||||
|
||||
## Data Flow
|
||||
|
||||
**WebSocket Device Info Flow (Example):**
|
||||
|
||||
1. **Device Connection** → `device_interaction/consumers.py:DeviceConsumer.connect()`
|
||||
- Device sends WebSocket connect with token in URL: `ws://domain/ws/device/token/{token}/`
|
||||
- `device_interaction/auth.py:TokenAuthMiddleware` intercepts, validates token via `userapp/authentication.py:RedisTokenAuthentication`
|
||||
- Token key pattern: `token:{token_value}` → retrieves `user_id` from Redis (see `userapp/utils.py:generate_token`)
|
||||
- User resolved, scope set; Consumer creates group `device_{user_id}` and joins
|
||||
|
||||
2. **Device Reports Status** → Device sends `type: device_info` JSON message
|
||||
- Example payload:
|
||||
```json
|
||||
{
|
||||
"type": "device_info",
|
||||
"message": {
|
||||
"battery_level": 85,
|
||||
"firmware_version": "1.2.3",
|
||||
"wifi_name": "MyWiFi"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. **Consumer Processes** → `device_interaction/consumers.py:DeviceConsumer.receive()`
|
||||
- Line 196-198: Parses message type and content
|
||||
- Line 211-212: Refreshes heartbeat: `cache.set(f"device:last_seen:{mac_address}", time.time(), timeout=300)`
|
||||
- Device info not explicitly handled in receive() yet (TODO area)
|
||||
|
||||
4. **Database Update** → Would call `update_device_status()` (line 107-131)
|
||||
- Updates `Device` model fields: `battery_level`, `firmware_version`, `wifi_name`, status='connected'
|
||||
- Sets Redis heartbeat key `device:last_seen:{mac}` with 5-minute TTL
|
||||
|
||||
5. **Broadcast to Group** → Via `channel_layer.group_send()` (lines 221-227)
|
||||
- Routes message type specific handlers (e.g., `async def weather()`)
|
||||
- Broadcasts to all connections in group `device_{user_id}`
|
||||
- Any other user/device connection to same user_id receives forwarded message
|
||||
|
||||
6. **State in Redis:**
|
||||
- Token lookup: `token:{token}` → `user_id`
|
||||
- Admin token lookup: `admin_token:{token}` → `user_id` (alternative for admin routes)
|
||||
- Heartbeat tracking: `device:last_seen:{mac_address}` → unix timestamp (TTL 5 min)
|
||||
|
||||
**State Management:**
|
||||
- **HTTP Tokens**: Stored in Redis with 30-day TTL (line 36 in `userapp/utils.py`)
|
||||
- **WebSocket Groups**: In-memory Redis channel layer, groups scoped by `device_{user_id}`
|
||||
- **Heartbeats**: Redis cache, auto-expire after 5 minutes (line 128, 151 in consumers.py)
|
||||
- **Device Status**: PostgreSQL — `Device.status` set to 'connected' or 'disconnected' on connect/disconnect
|
||||
|
||||
## Device Binding & Control Semantics
|
||||
|
||||
**"Last-Bind-Wins":**
|
||||
- `UserDevice.Meta.ordering = ['-bound_at']` (line 131 in `device_interaction/models.py`)
|
||||
- When device MAC used to login, code fetches: `UserDevice.objects.filter(device=device).order_by('-bound_at').first()` (line 120 in `userapp/views.py`)
|
||||
- That user gets the token; older bindings exist in DB but are superseded
|
||||
|
||||
**Control Flow Consequence:**
|
||||
- WebSocket group is `device_{user_id}` where user_id is from latest binding
|
||||
- Same device, same time, only **one user_id** in the group
|
||||
- Old bindings' connections to same MAC get ignored — they can't authenticate to the new owner's user_id group
|
||||
- `UserDevice` records not auto-deleted; explicit deletion required for "unbind" semantics
|
||||
|
||||
**RTC Integration:**
|
||||
- Endpoint: `/api/device/rtc-token/get_by_mac/?mac_address=...` (no auth required)
|
||||
- Resolves MAC → latest `UserDevice.user_id` (via `.first()` with ordering)
|
||||
- Generates `room_id = f"room_{user_id}"` (consistent with WebSocket group)
|
||||
- Returns Volcengine RTC token cached as `rtc_room:{user_id}:{task_id}`
|
||||
|
||||
## Key Abstractions
|
||||
|
||||
**ParadiseUser:**
|
||||
- Purpose: Custom user model extending AbstractUser
|
||||
- Location: `userapp/models.py` (line 8)
|
||||
- Pattern: Single table inheritance; adds fields like `favorability`, `gender`, `mbti`, `interests`
|
||||
- Relations: M2M to Group/Permission (custom related_names), 1-M to Device via UserDevice
|
||||
|
||||
**Device & UserDevice:**
|
||||
- Purpose: Track physical devices and user-device bindings
|
||||
- Location: `device_interaction/models.py` (lines 43-136)
|
||||
- Pattern: Device is the hardware; UserDevice is the ownership/interaction record
|
||||
- Key: `UserDevice` ordering by `-bound_at` enforces "last-bind-wins"
|
||||
|
||||
**DeviceConsumer:**
|
||||
- Purpose: Handle WebSocket connections for real-time device messaging
|
||||
- Location: `device_interaction/consumers.py` (line 10)
|
||||
- Pattern: AsyncWebsocketConsumer subclass; manages connect/receive/disconnect lifecycle
|
||||
- Methods:
|
||||
- `connect()`: Auth, group add, accept
|
||||
- `receive()`: Parse message type, route to group via `group_send()`
|
||||
- `disconnect()`: Mark device offline, group discard
|
||||
|
||||
**RedisTokenAuthentication:**
|
||||
- Purpose: Validate tokens stored in Redis
|
||||
- Location: `userapp/authentication.py` (line 10)
|
||||
- Pattern: DRF BaseAuthentication subclass; used by HTTP views and WebSocket middleware
|
||||
- Flow: Extract token from Authorization header, call `get_user_id_from_token()` (utils.py:39), resolve user, return (user, None)
|
||||
|
||||
**StandardResponseMiddleware:**
|
||||
- Purpose: Wrap all API responses in standard format
|
||||
- Location: `common/middleware.py` (line 6)
|
||||
- Pattern: Django middleware; intercepts Response objects, adds `success`, `code`, `message`, `data` fields
|
||||
- Behavior: Line 61 sets `success = 200 <= status_code < 300`, extracts/transforms `response.data`
|
||||
|
||||
**AudioService:**
|
||||
- Purpose: Abstract audio provider differences
|
||||
- Location: `aiapp/audio/AudioService.py`
|
||||
- Pattern: Factory pattern with provider configuration
|
||||
- Providers: Aliyun NLS, Volcengine, Tencent (via `AUDIO_SERVICE_PROVIDER` setting)
|
||||
|
||||
## Entry Points
|
||||
|
||||
**HTTP Entry Points:**
|
||||
- Location: `qy_lty/urls.py` (line 49-60)
|
||||
- URL pattern: `/api/<app>/<endpoint>/`
|
||||
- Examples:
|
||||
- `/api/user/mac-login/` → `userapp.views.MacAddressLoginView.post()`
|
||||
- `/api/device/types/` → `device_interaction.views.DeviceTypeViewSet.list()`
|
||||
- `/api/ai/chat/` → `aiapp.views.ChatView` (for multi-turn chat)
|
||||
- Router: Standard DRF routing via `include('app.urls')`
|
||||
|
||||
**WebSocket Entry Points:**
|
||||
- Location: `qy_lty/asgi.py` (lines 14-26), `device_interaction/routing.py` (lines 4-7)
|
||||
- URL pattern: `ws://domain/ws/device/` (header auth) or `ws://domain/ws/device/token/{token}/` (URL auth)
|
||||
- Consumer: `device_interaction.consumers.DeviceConsumer.as_asgi()`
|
||||
- Middleware stack: TokenAuthMiddleware → URLRouter → DeviceConsumer
|
||||
|
||||
**Admin Entry Points:**
|
||||
- Location: `/admin/` (Django admin)
|
||||
- Config: `userapp/admin.py`, each app's `admin.py`
|
||||
- Customization: `simpleui` integration for enhanced UI
|
||||
|
||||
**API Documentation:**
|
||||
- Swagger UI: `/swagger/` (via drf-yasg)
|
||||
- ReDoc: `/redoc/` (via drf-yasg)
|
||||
- Config: Generated from openapi.Info (line 32-39 in qy_lty/urls.py)
|
||||
|
||||
## Error Handling
|
||||
|
||||
**Strategy:** Exception-aware middleware + standardized response format
|
||||
|
||||
**Patterns:**
|
||||
- **DRF Exceptions**: Caught by `StandardResponseMiddleware.process_exception()` (line 120), wrapped in standard format (line 133)
|
||||
- **Validation Errors**: Nested extraction of field errors (line 19-43), flattened into readable message
|
||||
- **HTTP Status Codes**: Mapped to `success` boolean (line 61: `200 <= code < 300`)
|
||||
- **Custom Error Codes**: HTTP response includes `code` field, e.g., `code=4010` for "device not bound" (line 127 in userapp/views.py)
|
||||
- **WebSocket Errors**: Consumer catches exceptions, logs, and closes connection with specific code (e.g., 4001 for auth failure, line 24)
|
||||
|
||||
## Cross-Cutting Concerns
|
||||
|
||||
**Logging:**
|
||||
- Framework: Python `logging` module
|
||||
- Configured in: `qy_lty/settings.py` (logging config)
|
||||
- Patterns: Each module instantiates logger: `logger = logging.getLogger(__name__)` (line 8 in consumers.py)
|
||||
- Aliyun integration: `common/aliyun_logging.py:setup_logging()` for production
|
||||
|
||||
**Validation:**
|
||||
- HTTP: DRF serializer validation via `is_valid()`, errors in response
|
||||
- WebSocket: Inline JSON parsing with try/except, error response via `send()`
|
||||
- Custom: Model `clean()` methods (if defined), field constraints
|
||||
|
||||
**Authentication:**
|
||||
- HTTP: `RedisTokenAuthentication` on ViewSet `authentication_classes` (line 79 in device_interaction/views.py)
|
||||
- WebSocket: `TokenAuthMiddleware` in ASGI app (line 21 in qy_lty/asgi.py)
|
||||
- Token generation: 30-day TTL, Redis-backed, pattern `token:{uuid}` or `admin_token:{uuid}`
|
||||
|
||||
**Authorization:**
|
||||
- HTTP: `permission_classes = [IsAuthenticated]` on ViewSets (line 80)
|
||||
- WebSocket: Implicit via token — anonymous users get 4001 close code (line 24)
|
||||
- Admin routes: `/api/v1/admin/` (separate URL namespace), likely checked via user.is_staff
|
||||
|
||||
**Pagination:**
|
||||
- Default: 10 items per page (settings.py line 283)
|
||||
- Override: Pass `page_size` query parameter (e.g., `?page_size=50`)
|
||||
- Response: Wrapped with `count`, `next`, `previous`, `results` (lines 93-95 in common/middleware.py)
|
||||
|
||||
---
|
||||
|
||||
*Architecture analysis: 2026-05-07*
|
||||
335
qy_lty/.planning/codebase/CONCERNS.md
Normal file
335
qy_lty/.planning/codebase/CONCERNS.md
Normal file
@ -0,0 +1,335 @@
|
||||
# Codebase Concerns
|
||||
|
||||
**Analysis Date:** 2026-05-07
|
||||
|
||||
## Tech Debt
|
||||
|
||||
### Hardcoded Test MAC Bypass (Security Risk)
|
||||
- **Issue:** Test MAC `AA:BB:CC:DD:EE:FF` is hardcoded to skip "device already bound" validation
|
||||
- **Files:**
|
||||
- `device_interaction/serializers.py:125`
|
||||
- `device_interaction/views.py:702`
|
||||
- **Code Pattern:**
|
||||
```python
|
||||
if value != 'AA:BB:CC:DD:EE:FF' and UserDevice.objects.filter(device=device).exists():
|
||||
raise serializers.ValidationError("设备已被其他用户绑定")
|
||||
```
|
||||
- **Impact:** In production, any request with this MAC bypasses binding validation entirely. If MAC is leaked or discovered, attacker can bypass device permission controls.
|
||||
- **Fix approach:** Remove hardcoded bypass before production scale-out. Replace with environment-based test mode flag (`ALLOW_TEST_MAC=false` in prod, check at startup). Document test procedures that don't require code branches.
|
||||
- **Severity:** HIGH — Production-blocking if scale-out planned
|
||||
|
||||
### Device Binding Semantics ("Last-Bind-Wins") Implicit in Ordering
|
||||
- **Issue:** CLAUDE.md (line 233) documents "后绑的挤掉先绑的" (last binder controls device) via `UserDevice.Meta.ordering = ['-bound_at']`. Control resolution in `views.py` uses `.first()` implicitly depending on this ordering, with no explicit comment at call sites.
|
||||
- **Files:**
|
||||
- `device_interaction/models.py` — `UserDevice` model ordering
|
||||
- `device_interaction/views.py:702` (bind_status), `views.py:1120` (rtc-token/get_by_mac) — implicit `.first()` usage
|
||||
- `userapp/views.py` — MAC login endpoint (explicit `.order_by('-bound_at').first()`)
|
||||
- **Impact:** New developers may not understand that multiple `UserDevice` rows for same device are intentional, and may try to auto-delete old records thinking they're bugs. Silent control hand-off between users without explicit notification.
|
||||
- **Fix approach:** Add explicit docstring to `UserDevice.Meta` explaining ordering rationale. Add comments at control-resolution sites: "Implicit: `.first()` depends on Meta.ordering = ['-bound_at']". Implement user notification when control is lost (older user's device suddenly stops responding).
|
||||
- **Severity:** MEDIUM — Operational surprise risk
|
||||
|
||||
### is_primary Ambiguity (Not "Primary Controller")
|
||||
- **Issue:** CLAUDE.md (line 237) notes `is_primary` is "user view of primary device" (each user has ≤1), NOT "device view of primary user". Same physical device can have multiple `is_primary=True` rows.
|
||||
- **Files:**
|
||||
- `device_interaction/models.py` — `UserDevice.is_primary` field
|
||||
- `device_interaction/views.py:746` — `set_primary` action
|
||||
- **Impact:** Code querying `UserDevice.objects.filter(device=device, is_primary=True)` will return multiple rows; calling `.first()` silently picks arbitrary one. Data migrations may assume primary uniqueness per device and fail.
|
||||
- **Fix approach:** Rename field to `is_user_primary` to clarify scope. Add unique constraint on `(user, is_primary=True)` at DB level (add migration). Audit all code touching is_primary to ensure correct semantics.
|
||||
- **Severity:** MEDIUM — Data consistency risk in multi-user scenarios
|
||||
|
||||
### Affinity System Field Backward Compatibility
|
||||
- **Issue:** `AffinityRule` and `AffinityLevel` have old fields (`points`, `daily_limit`, `is_active` for Rule; `required_points`, `rewards` for Level) marked "已弃用" (deprecated) in comments, pending deletion next version.
|
||||
- **Files:**
|
||||
- `userapp/models.py` — field definitions with deprecation comments
|
||||
- `userapp/migrations/0005_affinitysetting_affinitylevel_is_deleted_and_more.py` — schema
|
||||
- `docs/修改记录.md:47` — explicit note: "保留作为兼容字段,注释标记 '已弃用',下个版本删除"
|
||||
- **Impact:** Two parallel field sets in model creates confusion. Serializers/views must avoid referencing old fields. Data migrations targeting old fields will silently succeed but risk ignored values if migration rolls back.
|
||||
- **Fix approach:** Plan deprecation timeline: 1) mark version when old fields will be deleted (e.g., "v2.0"), 2) add migration warning log at deploy time listing affected rows still using old values, 3) drop fields in scheduled release with clear release notes.
|
||||
- **Severity:** MEDIUM — Code maintainability; low runtime risk if new field set is complete
|
||||
|
||||
### ParadiseUser.favorability Not Yet Deleted After Migration
|
||||
- **Issue:** Data migrated from `ParadiseUser.favorability` to `UserDevice.favorability` (migration 0006), but old field on ParadiseUser is NOT deleted. Docs (修改记录.md:116) explicitly state "旧 ParadiseUser.favorability 字段保留不删,由后续版本统一清理".
|
||||
- **Files:**
|
||||
- `userapp/models.py` — `ParadiseUser.favorability` field still exists
|
||||
- `userapp/migrations/0006_migrate_favorability_to_userdevice.py:11` — migration note
|
||||
- **Impact:** Source-of-truth split: old field may be modified by legacy code path or manual admin actions; new device-level field won't see updates. Risks silent inconsistency.
|
||||
- **Fix approach:** Create follow-up migration to drop `ParadiseUser.favorability` once P2 service layer is validated. Until then, add application-level guard: `AffinityService` should only read/write `UserDevice.favorability`, and log warning if `ParadiseUser.favorability > 0` and doesn't match primary device value.
|
||||
- **Severity:** LOW-MEDIUM — Handled by migration discipline; plan removal in next minor release
|
||||
|
||||
## Known Bugs
|
||||
|
||||
### Missing TODO in Achievement Conditions Check
|
||||
- **Issue:** `achievement_app/views.py:139` has explicit TODO comment: "# TODO: 根据成就的conditions字段和前端传递的check_data检查是否满足条件" (achievement condition validation not implemented)
|
||||
- **Files:** `achievement_app/views.py:139`
|
||||
- **Impact:** Achievement unlock endpoint does not validate if conditions are met; always grants achievement if user requests it. Any client can claim arbitrary achievements.
|
||||
- **Trigger:** Call POST `/api/achievement/check/` with any `check_data`
|
||||
- **Workaround:** None; feature is stub-only
|
||||
- **Fix approach:** Implement `AchievementService.check_conditions(achievement, check_data)` with per-condition validator registry. Add unit tests for each condition type.
|
||||
- **Severity:** HIGH — Functional gap in released feature
|
||||
|
||||
### Missing Conversation Source Field in WebSocket Callback
|
||||
- **Issue:** `device_interaction/views.py:1427` has TODO: "# TODO 决策点 #3 落定后填充 'phone' / 'device'" — callback routing doesn't tag which client sent conversation (phone app vs device endpoint).
|
||||
- **Files:** `device_interaction/views.py:1427` (in `conversation_status` action)
|
||||
- **Impact:** All WebSocket message routing broadcasts assume single source; can't distinguish if conversation came from phone client or device endpoint. Breaks analytics, replay, and client-side dedup logic that relies on source.
|
||||
- **Trigger:** Ongoing RTC chat with device and phone both active
|
||||
- **Workaround:** Clients must infer source from message timing/content patterns
|
||||
- **Fix approach:** Add `'source': 'phone' | 'device'` field in WebSocket payload. Update all consumers and message routers. Coordinate with client teams to unify source interpretation.
|
||||
- **Severity:** MEDIUM — Missing feature; blocks analytics correctness
|
||||
|
||||
### SMS Verification Code Rate Limiting Not Enforced
|
||||
- **Issue:** `userapp/views.py:297-318` (SendVerifyCodeView) has no rate limit, throttle, or per-phone backoff on SMS sending.
|
||||
- **Files:** `userapp/views.py:297-318`
|
||||
- **Code:**
|
||||
```python
|
||||
def post(self, request):
|
||||
phone_number = request.data.get('phone_number')
|
||||
# ... no rate limit check ...
|
||||
code = str(random.randint(100000, 999999))
|
||||
response = send_sms(phone_number, code) # Directly calls Aliyun SMS
|
||||
cache.set(phone_number, code, timeout=600) # Stores code in Redis
|
||||
```
|
||||
- **Impact:** SMS spam risk: attacker can hammer endpoint with same phone number to exhaust SMS quota or bankrupt via Aliyun charges (SMS is pay-per-send). No cooldown enforced.
|
||||
- **Trigger:** `while True: POST /api/user/send_verify_code/ {"phone_number": "13800000000"}`
|
||||
- **Workaround:** IP-level blocking or firewall; not app-level protection
|
||||
- **Fix approach:** Implement rate limiting: 1) Add Redis key `sms_sent:{phone}:{YYYYMMDD}` with counter, max 5 per day per phone, 2) Add `sms_cooldown:{phone}` with 60s TTL to block rapid retries, 3) Use DRF throttle class or custom decorator for IP-level limits (5 SMS per minute per IP).
|
||||
- **Severity:** HIGH — Production DoS vector in authentication endpoint
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### DEBUG Mode Hardcoded to True in Production Settings
|
||||
- **Issue:** `qy_lty/settings.py:31` has `DEBUG = True # 开发环境下设置为True`
|
||||
- **Files:** `qy_lty/settings.py:31`
|
||||
- **Impact:** DEBUG=True in production exposes full stack traces, local variables, SQL queries, and installed packages to any error page. Leaks internal URLs, file paths, and API keys from traceback context.
|
||||
- **Current mitigation:** Comment implies intent for dev-only, but no env var check
|
||||
- **Recommendations:** Change to `DEBUG = config('DEBUG', default=False, cast=bool)` so prod .env can override. Add pre-deploy check in CI to fail if DEBUG=True in prod build.
|
||||
- **Severity:** HIGH — Information disclosure
|
||||
|
||||
### CORS Allows All Origins in Production
|
||||
- **Issue:** `qy_lty/settings.py:98` sets `CORS_ALLOW_ALL_ORIGINS = True # 允许所有来源的跨域请求,适用于开发环境`
|
||||
- **Files:** `qy_lty/settings.py:98`
|
||||
- **Impact:** Any website can make authenticated requests to the API on behalf of users (e.g., attacker site makes POST requests using victim's stored token). Defeats CORS origin validation.
|
||||
- **Current mitigation:** Comment indicates dev-only intent; commented-out whitelist provided
|
||||
- **Recommendations:** Load whitelist from env var: `CORS_ALLOWED_ORIGINS = config('CORS_ALLOWED_ORIGINS', default='http://localhost:3000').split(',')`. Restrict to known Unity client domains + web admin domain + localhost in dev.
|
||||
- **Severity:** HIGH — CSRF-adjacent vulnerability if session auth used
|
||||
|
||||
### Token-in-URL WebSocket Auth Logged in Access Logs
|
||||
- **Issue:** CLAUDE.md (line 117) documents WebSocket URL-based auth: `ws://domain/ws/device/token/{token}/`
|
||||
- **Files:**
|
||||
- `device_interaction/consumers.py:54-71` — token extraction from URL
|
||||
- CLAUDE.md line 117 — design documentation
|
||||
- **Impact:** Token appears in nginx/reverse-proxy access logs, browser history, referrer headers, CloudFront logs. Any log aggregation breach exposes all active tokens.
|
||||
- **Current mitigation:** Header-based auth also supported (`ws://domain/ws/device/`)
|
||||
- **Recommendations:** Deprecate URL-based token auth (mark as legacy). Document that tokens should only be passed in Headers (`Authorization: Bearer {token}`). For legacy WebSocket clients that can't set custom headers, recommend binding device MAC early and using MAC-login instead.
|
||||
- **Severity:** HIGH — Token exposure in logs
|
||||
|
||||
### Credentials Handling in Audio Service Config
|
||||
- **Issue:** Audio service provider credentials (Aliyun, Tencent, VolcEngine) are stored in .env and loaded via `config()` calls throughout codebase. If .env is accidentally committed or exposed, all external API keys are compromised.
|
||||
- **Files:**
|
||||
- `qy_lty/settings.py` — loads `ALIYUN_*`, `VOLCENGINE_*` from env
|
||||
- `userapp/utils.py:8-30` — uses `ALIYUN_SMS_ACCESS_KEY_*` directly
|
||||
- Multiple other service files
|
||||
- **Current mitigation:** .env is in .gitignore (assumed)
|
||||
- **Recommendations:** 1) Add .env.example template with dummy values to repo, 2) Pre-deploy check to verify SECRET_KEY, DEBUG, and all *_KEY env vars are set (fail if default), 3) Rotate credentials if any .env branch is accidentally pushed, 4) Use cloud IAM (Aliyun RAM roles) for server-to-service auth instead of long-lived keys where possible.
|
||||
- **Severity:** HIGH — If .env leaked, all integrations compromised
|
||||
|
||||
### CSRF Middleware Disabled
|
||||
- **Issue:** `qy_lty/settings.py:88` has CSRF middleware commented out: `# 'django.middleware.csrf.CsrfViewMiddleware',`
|
||||
- **Files:** `qy_lty/settings.py:88`
|
||||
- **Impact:** POST/PUT/DELETE endpoints accept requests without CSRF tokens, enabling form-based CSRF attacks from third-party sites.
|
||||
- **Current mitigation:** Token-based REST API auth is used instead of session cookies
|
||||
- **Recommendations:** If using token auth (Redis tokens, not session cookies), CSRF can be relaxed, but should have: 1) explicit comment explaining trade-off, 2) CSRF exemption decorator `@csrf_exempt` only on REST endpoints, not HTML pages, 3) Consider re-enabling for browser-based admin endpoints (if any).
|
||||
- **Severity:** MEDIUM — Mitigated by token auth, but could surprise new developers
|
||||
|
||||
### File Upload Without Type/Size Validation
|
||||
- **Issue:** `common/views.py:66-114` (upload_file endpoint) accepts any file type and has no maximum size limit
|
||||
- **Files:** `common/views.py:66-114`
|
||||
- **Code:**
|
||||
```python
|
||||
file_obj = request.FILES['file']
|
||||
file_size = file_obj.size # Logged but not validated
|
||||
content_type = getattr(file_obj, 'content_type', 'unknown') # Logged but not checked
|
||||
result = uploader.upload_file(file_obj, is_permanent, filename) # No size/type check
|
||||
```
|
||||
- **Impact:** Users can upload arbitrary files (executables, archives, etc.) to OSS, consuming storage quota and potentially enabling supply-chain attacks (if files are later distributed). No DoS protection against huge uploads.
|
||||
- **Trigger:** `POST /api/upload/` with `multipart/form-data` containing 5GB file
|
||||
- **Workaround:** None at app level; depends on OSS bucket policy
|
||||
- **Recommendations:** Add validation: 1) Max file size (default 100MB, configurable), 2) Whitelist MIME types (audio, image, document only), 3) File extension check, 4) Virus scan if possible (ClamAV integration), 5) Add Content-Length header check before parsing.
|
||||
- **Severity:** MEDIUM-HIGH — Storage exhaustion + potential malware vector
|
||||
|
||||
## Performance Bottlenecks
|
||||
|
||||
### N+1 Query Risk in Device/Card List Endpoints
|
||||
- **Issue:** `device_interaction/views.py:77`, `device_interaction/views.py:193`, `card/views.py:262` define viewsets with plain `queryset = Model.objects.all()` without `select_related()` or `prefetch_related()`.
|
||||
- **Files:**
|
||||
- `device_interaction/views.py:77` — `DeviceTypeViewSet`
|
||||
- `device_interaction/views.py:135` — `DeviceBatchViewSet`
|
||||
- `device_interaction/views.py:193` — `DeviceViewSet`
|
||||
- `card/views.py:262` — `CardViewSet`
|
||||
- **Patterns:** Serializers reference related fields (`device_type_name = ReadOnlyField(source='device_type.name')`), so each list item triggers separate query for related object.
|
||||
- **Impact:** Listing 100 devices results in 101 queries (1 for list + 100 for device_type lookup). Scales linearly with result count. Noticeable on slow networks.
|
||||
- **Trigger:** `GET /api/device/` with large device list
|
||||
- **Fix approach:** Override `get_queryset()` in each ViewSet to add prefetch_related. Example:
|
||||
```python
|
||||
def get_queryset(self):
|
||||
qs = super().get_queryset()
|
||||
if self.action == 'list':
|
||||
qs = qs.select_related('device_type', 'batch')
|
||||
return qs
|
||||
```
|
||||
Add Django Debug Toolbar in dev to verify queries.
|
||||
- **Severity:** MEDIUM — Affects list performance as data grows
|
||||
|
||||
### WebSocket Consumer Synchronous DB Calls
|
||||
- **Issue:** `device_interaction/consumers.py:106-131` uses `@database_sync_to_async` decorator on `update_device_status()` and `mark_device_offline()`, which is correct, but the decorator wraps individual methods, not the whole consumer. If `receive()` calls multiple sync methods in sequence, they're not batched.
|
||||
- **Files:** `device_interaction/consumers.py:106-144`
|
||||
- **Pattern:**
|
||||
```python
|
||||
@database_sync_to_async
|
||||
def update_device_status(self, mac_address, device_data):
|
||||
device = Device.objects.filter(mac_address=mac_address).first()
|
||||
device.status = 'connected'
|
||||
device.save()
|
||||
cache.set(...)
|
||||
```
|
||||
- **Impact:** Each WebSocket message triggers separate DB transaction. If device sends 100 messages/sec, that's 100 DB round-trips/sec per device × device count = potential connection pool exhaustion.
|
||||
- **Trigger:** Device rapidly sending device_info messages
|
||||
- **Fix approach:** Batch writes using bulk_update() for multiple devices. Use Redis for frequent updates (heartbeat), sync to DB on schedule (5-min batch job). Consider using `select_for_update()` to prevent race conditions during concurrent updates.
|
||||
- **Severity:** MEDIUM — Affects high-frequency scenarios (concurrent devices)
|
||||
|
||||
### Redis Heartbeat TTL Race Condition
|
||||
- **Issue:** CLAUDE.md (line 230) documents: "设备心跳:每次收消息时刷新 `device:last_seen:{mac}`(TTL 5 分钟),断连时把 `Device.status` 标记为 `disconnected`"
|
||||
- **Files:**
|
||||
- `device_interaction/consumers.py:147-151` — `refresh_device_heartbeat()` refreshes Redis
|
||||
- `device_interaction/consumers.py:153-169` — `disconnect()` marks DB offline
|
||||
- **Pattern:** Heartbeat is Redis-only (fast); offline mark is DB-only. Gap between Redis expiry and disconnect handler creates brief window where Redis says offline but DB says online.
|
||||
- **Impact:** Race condition: if device loses connection and immediately reconnects within 5 minutes, Redis has fresh key but DB still marked disconnected. List endpoints show device as offline even though it's online.
|
||||
- **Trigger:** Device disconnects and reconnects within 5 minutes
|
||||
- **Fix approach:** On reconnect (in `connect()` method), check Redis and sync DB status: if Redis key exists, update Device.status to 'connected'. Add sync_task that periodically reconciles Redis heartbeat state with DB.
|
||||
- **Severity:** LOW-MEDIUM — Offline view is slightly stale, but eventually consistent
|
||||
|
||||
### Channel Layer Single Redis Instance
|
||||
- **Issue:** CLAUDE.md (line 116) mentions "基于 Redis 的通道层用于实时消息传递" but Docker compose and settings don't explicitly configure channel layer sharding/replication.
|
||||
- **Files:**
|
||||
- `docker-compose.yml` — no Redis cluster config
|
||||
- `qy_lty/settings.py` — CHANNEL_LAYERS likely not set (uses default)
|
||||
- **Impact:** Single Redis instance is single point of failure for WebSocket message routing. If Redis crashes, all device-to-phone communication drops until restart.
|
||||
- **Current mitigation:** Docker `restart: always` policy restarts container on Redis crash
|
||||
- **Recommendations:** For production: 1) Add Redis Sentinel or Redis Cluster for failover, 2) Explicitly configure CHANNEL_LAYERS in settings, 3) Add monitoring/alerting on Redis health, 4) Document failover procedure.
|
||||
- **Severity:** MEDIUM — High availability concern for production
|
||||
|
||||
## Test Coverage Gaps
|
||||
|
||||
### No Unit or Integration Tests Found
|
||||
- **Issue:** Codebase has 168 Python files but zero test files (`find . -name "test_*.py" -o -name "*.test.py"` returns nothing)
|
||||
- **Files:** Entire codebase
|
||||
- **Risk Areas (no test coverage):**
|
||||
- `device_interaction/consumers.py` (WebSocket consumer logic, token auth, message routing)
|
||||
- `device_interaction/views.py` (device binding, RTC token generation, message sending)
|
||||
- `aiapp/views.py` (chat API, audio service integration)
|
||||
- `card/views.py` (batch generation, QR code handling)
|
||||
- `userapp/views.py` (login endpoints, device MAC binding)
|
||||
- `userapp/utils.py` (token generation, SMS sending)
|
||||
- **Impact:** No regression detection for bug fixes. Refactoring is high-risk. New developers can't verify behavior. Critical paths (auth, device control, chat) have zero test coverage.
|
||||
- **Priority:** HIGH — This is the single largest gap. Recommend: 1) Set up pytest + pytest-django, 2) Implement tests for all auth endpoints (10-15 tests), 3) Test device binding logic (5-10 tests), 4) WebSocket consumer tests (unit, 10-15 tests), 5) Gradually expand to 70%+ coverage.
|
||||
- **Severity:** HIGH — Operational risk due to zero safety net
|
||||
|
||||
## Fragile Areas
|
||||
|
||||
### Device Interaction Views (1,867 lines)
|
||||
- **Files:** `device_interaction/views.py`
|
||||
- **Why fragile:** Single mega-file contains 30+ action methods for device management, message routing, RTC token generation, WebSocket callbacks, admin endpoints. Changes to one subsystem affect entire file's import dependencies.
|
||||
- **Safe modification:** Use code folding tools (IDE regions). Create feature branches. Add tests for each action before refactoring. Extract cohesive groups into separate views (e.g., `DeviceRTCViewSet`, `MessageDispatchViewSet`, `VolcEngineCallbackViewSet`).
|
||||
- **Test coverage:** Zero. Recommend starting with 10 happy-path + 5 error case tests.
|
||||
|
||||
### Card System Batch Generation (`card/views.py:262-364`)
|
||||
- **Files:** `card/views.py` batch_create action (~100 lines)
|
||||
- **Why fragile:** Batch generation calls external services (QR generation), creates multiple DB records in transaction, generates Excel exports. If any step fails mid-batch, partial state is left.
|
||||
- **Safe modification:** Use `@transaction.atomic` (already present) but verify rollback covers all side effects. Add dry-run mode. Test with batch size = 1000+.
|
||||
- **Test coverage:** Zero.
|
||||
|
||||
### WebSocket Token Authentication (`device_interaction/consumers.py:78-104`)
|
||||
- **Files:** `device_interaction/consumers.py`
|
||||
- **Why fragile:** Token extraction from URL (`get_token_from_url()`) has two fallback paths (url_route kwargs, manual path parsing). Order matters; if first path silently fails, second might not fire.
|
||||
- **Safe modification:** Add explicit logging at each path. Unit test both extraction methods independently.
|
||||
- **Test coverage:** Zero. Recommend: 2-3 token auth scenario tests.
|
||||
|
||||
## Migration & Data Debt
|
||||
|
||||
### Favorability Data Migration Dependency Chain
|
||||
- **Issue:** Affinity system (P1 phase, commit 2d82b2e) depends on three sequential migrations:
|
||||
1. `device_interaction/migrations/0003_userdevice_affinity_*.py` — schema changes
|
||||
2. `userapp/migrations/0005_affinitysetting_*.py` — schema + new tables
|
||||
3. `userapp/migrations/0006_migrate_favorability_to_userdevice.py` — data migration (RunPython)
|
||||
- **Files:** All three migration files
|
||||
- **Impact:** If migration #3 runs before #2, FK constraint fails. If rolled back out-of-order, data loss. Migration file 0006 is irreversible if both forward and backward have been applied (ParadiseUser.favorability loses history).
|
||||
- **Fix approach:** Document exact migration sequence in CLAUDE.md deploy section. Add migration dependency comments. Test migration path on staging before prod deploy.
|
||||
- **Severity:** MEDIUM — One-time risk during affinity system rollout
|
||||
|
||||
### Schema Churn Around UserDevice
|
||||
- **Issue:** Recent commits added 4 new columns to UserDevice (favorability, affinity_level, last_active_at, is_active). This is 5th major change to UserDevice in recent history (based on migrations count).
|
||||
- **Files:** `device_interaction/models.py`, `device_interaction/migrations/0003_*`
|
||||
- **Impact:** Existing deployments will have migration backlog. Large tables see downtime during migration if index creation locks table.
|
||||
- **Recommendations:** For future schema changes: 1) Use zero-downtime migration strategy (add column with default, backfill async, add constraint later), 2) Test migration time on prod-sized DB before deploy, 3) Plan maintenance window if >10GB table affected.
|
||||
- **Severity:** LOW — Manageable if migrations are small; communicate with ops team
|
||||
|
||||
## Internationalization Concerns
|
||||
|
||||
### i18n Setup Incomplete (en / zh_HAns Only)
|
||||
- **Issue:** CLAUDE.md (line 85) documents: `django-admin makemessages -l en` and `-l zh_HAns`, but only two language files present. No translation workflow defined (who translates, quality assurance, etc.).
|
||||
- **Files:** Locale directories (if present)
|
||||
- **Impact:** English and Simplified Chinese only. Any text from Chinese source code will show untranslated when language=en. Admin UI (SimpleUI) may have partial translations.
|
||||
- **Recommendations:** 1) Run makemessages and compilemessages at deploy time (add to Docker CMD), 2) Define translation workflow (assign translator, review process), 3) Consider using gettext_lazy for all user-facing strings, 4) Add i18n test to CI (verify no missing keys).
|
||||
- **Severity:** LOW — UI concern; functional impact minimal if default language is Chinese
|
||||
|
||||
## Known TODOs in Codebase
|
||||
|
||||
1. **`achievement_app/views.py:139`** — "TODO: 根据成就的conditions字段和前端传递的check_data检查是否满足条件" (achievement condition validation missing)
|
||||
2. **`device_interaction/views.py:1427`** — "TODO 决策点 #3 落定后填充 'phone' / 'device'" (conversation source field in WebSocket callback)
|
||||
3. **`subscription_app/apps.py:17`** — "TODO:: 测试环境不启动调度器" (scheduler disabled in test env, needs decision)
|
||||
|
||||
## Deployment Concerns
|
||||
|
||||
### Docker Image Uses Python 3.8 (EOL)
|
||||
- **Issue:** `Dockerfile:2` uses `python:3.8` which reached EOL in October 2024. No security patches available.
|
||||
- **Files:** `Dockerfile:2`
|
||||
- **Impact:** CVE fixes in Python runtime are no longer released. New dependencies may drop Python 3.8 support.
|
||||
- **Recommendations:** Upgrade to Python 3.11 or 3.12. Update requirements.txt with compatible versions (test in staging first).
|
||||
- **Severity:** MEDIUM — Security risk for production
|
||||
|
||||
### Single Daphne Process in Docker (No Workers)
|
||||
- **Issue:** `Dockerfile:25` and `run.sh:2` start single `daphne` instance with no worker scaling. No load balancing, no graceful shutdown.
|
||||
- **Files:**
|
||||
- `Dockerfile:20-26`
|
||||
- `run.sh:2`
|
||||
- **Impact:** Single process is bottleneck. Restart drops all active WebSocket connections. No way to scale without manual container replication.
|
||||
- **Recommendations:** 1) Use Docker Compose or Kubernetes to run multiple daphne replicas, 2) Add reverse proxy (nginx) to load balance, 3) Implement graceful shutdown handler (drain connections before exit), 4) Add health check endpoint (Django health_check app).
|
||||
- **Severity:** MEDIUM — High availability concern
|
||||
|
||||
### No Database Healthcheck
|
||||
- **Issue:** `docker-compose.yml` has no healthcheck for postgres or redis. If DB is down, container still shows "running".
|
||||
- **Files:** `docker-compose.yml:1-33`
|
||||
- **Impact:** Deployment tools can't detect DB failures. Restart policies won't trigger on DB outage.
|
||||
- **Recommendations:** Add healthcheck to docker-compose:
|
||||
```yaml
|
||||
healthcheck:
|
||||
test: ["CMD", "python", "-c", "import django; django.setup(); from django.db import connection; connection.ensure_connection()"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
```
|
||||
- **Severity:** LOW — Operational tool concern
|
||||
|
||||
### Missing Database Migrations in Dockerfile Startup
|
||||
- **Issue:** `Dockerfile:25` startup script (start.sh) runs daphne directly without calling `python manage.py migrate`. If migrations are pending, runtime errors occur.
|
||||
- **Files:** `Dockerfile:20-26`
|
||||
- **Impact:** Fresh deploy or rolling update fails if new migrations exist. Manual SSH to container required to fix.
|
||||
- **Recommendations:** Modify start.sh:
|
||||
```bash
|
||||
python manage.py migrate
|
||||
daphne -b 0.0.0.0 -p 8000 qy_lty.asgi:application
|
||||
```
|
||||
- **Severity:** MEDIUM — Deployment failure risk on schema changes
|
||||
|
||||
---
|
||||
|
||||
*Concerns audit: 2026-05-07*
|
||||
292
qy_lty/.planning/codebase/CONVENTIONS.md
Normal file
292
qy_lty/.planning/codebase/CONVENTIONS.md
Normal file
@ -0,0 +1,292 @@
|
||||
# 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*
|
||||
179
qy_lty/.planning/codebase/INTEGRATIONS.md
Normal file
179
qy_lty/.planning/codebase/INTEGRATIONS.md
Normal file
@ -0,0 +1,179 @@
|
||||
# External Integrations
|
||||
|
||||
**Analysis Date:** 2026-05-07
|
||||
|
||||
## APIs & External Services
|
||||
|
||||
**AI & Language Models:**
|
||||
- Kimi (OpenAI-compatible) - LLM for multi-turn AI conversations
|
||||
- SDK: `openai` Python package (in `requirements.txt`)
|
||||
- Implementation: `aiapp/kimi.py` (lines 1-29)
|
||||
- Config: `KIMI_API_KEY`, `KIMI_BASE_URL` (set at `qy_lty/settings.py:349-350`, loaded from `.env`)
|
||||
- Purpose: Single/multi-turn chat with users via POST `/api/ai/chat/` and `/api/ai/multi-chat/`
|
||||
|
||||
**Real-Time Communications (RTC):**
|
||||
- Volcengine (ByteDance) - RTC service for voice/video calls
|
||||
- SDK: Custom token generation in `common/VolcEngineAccessToken.py` (lines 1-204), HTTP API calls in `device_interaction/volcengine_api.py`
|
||||
- Config: `VOLCENGINE_ACCESS_KEY`, `VOLCENGINE_SECRET_KEY`, `VOLCENGINE_APP_ID`, `VOLCENGINE_APP_KEY` (at `qy_lty/settings.py:241-243`)
|
||||
- Purpose: Generate RTC tokens for device-to-device or device-to-app real-time communication
|
||||
- Endpoint: `POST /api/device/rtc-token/get_by_mac/?mac_address=...` (no auth required, returns RTC token for device's bound user)
|
||||
- Room structure: `room_id = f"room_{user_id}"` (shared with WebSocket group `device_{user_id}`)
|
||||
|
||||
**Audio/Speech Services (Multi-Provider):**
|
||||
- Configured via `AUDIO_SERVICE_PROVIDER` setting (default: `'huoshan'`) at `qy_lty/settings.py:415`
|
||||
|
||||
**Aliyun NLS (Natural Language Service):**
|
||||
- SDK: `aliyun-python-sdk-core` + custom integration
|
||||
- Implementation: `aiapp/audio/AliyunAudioService.py`
|
||||
- Config keys: `ALIYUN_NLS_ACCESS_KEY_ID`, `ALIYUN_NLS_ACCESS_KEY_SECRET`, `ALIYUN_NLS_APP_ID` (in `qy_lty/settings.py:419-421`)
|
||||
- Purpose: Speech synthesis (text→voice) and recognition (voice→text)
|
||||
- Storage: Audio uploaded to Aliyun OSS (see OSS section)
|
||||
|
||||
**Tencent Audio:**
|
||||
- Implementation: `aiapp/audio/TencentAudioService.py`
|
||||
- Config keys: `AUDIO_SERVICE_TENCENT_API_KEY`, `AUDIO_SERVICE_TENCENT_API_SECRET` (at `qy_lty/settings.py:430-431`)
|
||||
- Purpose: Text-to-speech and speech-to-text via Tencent APIs
|
||||
|
||||
**Volcengine/Huoshan (ByteDance):**
|
||||
- Implementation: `aiapp/audio/HuoshanAudioService.py`, `aiapp/audio/HuoshanAudioService_new.py`
|
||||
- Config keys: `AUDIO_SERVICE_HUOSHAN_APPID`, `AUDIO_SERVICE_HUOSHAN_ACCESS_TOKEN`, `AUDIO_SERVICE_HUOSHAN_CLUSTER`, `AUDIO_SERVICE_HUOSHAN_VOICE_TYPE`, `AUDIO_SERVICE_HUOSHAN_STORAGE_DIR`, `AUDIO_SERVICE_HUOSHAN_BASE_URL` (at `qy_lty/settings.py:434-439`)
|
||||
- Purpose: TTS (text→speech) and ASR (speech→text) with local storage fallback
|
||||
- Also includes nested Aliyun ASR config (lines 440-451) as fallback
|
||||
|
||||
**Audio Service Factory:**
|
||||
- Entry point: `aiapp/audio/AudioService.py:get_audio_service()` (lines 8-19)
|
||||
- Base class: `aiapp/audio/BaseAudioService.py` (abstract interface at lines 1-35)
|
||||
- Used by: `aiapp/views.py` (ChatBotAPIView, MultiChatAPIView) and tests
|
||||
|
||||
## Data Storage
|
||||
|
||||
**Databases:**
|
||||
- **PostgreSQL** (Primary)
|
||||
- Connection: `POSTGRESQL_DATABASE_*` env vars (configured at `qy_lty/settings.py:166-172`)
|
||||
- Client: psycopg2-binary
|
||||
- Contains: User models, AI conversations, device bindings, card data, achievement records, subscription info
|
||||
|
||||
- **Redis** (Cache + Message Queue)
|
||||
- Connection: `REDIS_LOCATION`, `REDIS_PASSWORD` (configured at `qy_lty/settings.py:252-260`)
|
||||
- Client: django-redis
|
||||
- Purpose: Token storage (key format: `token:{token}` or `admin_token:{token}` with 30-day TTL, see `CLAUDE.md` line 122), channel layer for WebSocket, caching
|
||||
- Channel layer config: `CHANNEL_LAYERS.default.BACKEND = 'channels_redis.core.RedisChannelLayer'` (at `qy_lty/settings.py:504-511`)
|
||||
|
||||
**File Storage:**
|
||||
- **Aliyun OSS (Object Storage Service)** - Production storage for audio, images, documents
|
||||
- SDK: `oss2` Python package (in `requirements.txt`)
|
||||
- Implementation: `common/oss.py` (OSSUploader class at lines 1-90)
|
||||
- Config: `AUDIO_SERVICE_CONFIG['aliyun']` with `oss_key_id`, `oss_key_secret`, `oss_bucket`, `oss_endpoint`, `oss_host` (at `qy_lty/settings.py:422-427`)
|
||||
- Used by: Audio service, image processing endpoints, media uploads
|
||||
- Path pattern: `{base_dir}/[permanent|temporary]/{YYYYMMDD}/{filename}`
|
||||
- Dev fallback: Local filesystem storage at `MEDIA_ROOT` (`qy_lty/settings.py:238`)
|
||||
|
||||
**Caching:**
|
||||
- Redis (see above)
|
||||
- TTL for RTC tokens: `rtc_room:{user_id}:{task_id}` (see `CLAUDE.md` line 172)
|
||||
- Device last-seen cache: `device:last_seen:{mac}` with 5-min TTL (see `CLAUDE.md` line 230)
|
||||
|
||||
## Authentication & Identity
|
||||
|
||||
**Auth Provider:**
|
||||
- Custom Redis-backed token authentication + django-allauth social auth
|
||||
|
||||
**Custom Token Auth:**
|
||||
- Implementation: `userapp/authentication.RedisTokenAuthentication` (at `userapp/authentication.py:10-34`)
|
||||
- Storage: Redis with keys `token:{token}` (users) and `admin_token:{token}` (admins)
|
||||
- TTL: 30 days
|
||||
- Used for: API requests via `Authorization: Bearer {token}` header
|
||||
- Generated by: `userapp/utils.py:generate_token()` (referenced in `CLAUDE.md` line 122)
|
||||
|
||||
**WeChat Social Auth:**
|
||||
- Provider: django-allauth with Weixin provider (configured at `qy_lty/settings.py:56`)
|
||||
- Backend: `allauth.account.auth_backends.AuthenticationBackend` (at `qy_lty/settings.py:324`)
|
||||
|
||||
**WebSocket Auth:**
|
||||
- Custom middleware: `device_interaction/auth.TokenAuthMiddleware` (lines 1-50)
|
||||
- Reuses `RedisTokenAuthentication` logic with mock request adapter
|
||||
- Routes: `ws://domain/ws/device/` (Header auth) and `ws://domain/ws/device/token/{token}/` (URL auth)
|
||||
- Group model: Messages routed to `device_{user_id}` (same user controls device and app)
|
||||
|
||||
**Device MAC Login:**
|
||||
- Endpoint: `POST /api/user/mac-login/` (referenced in `CLAUDE.md` line 125)
|
||||
- Returns: User-token for device's bound user (latest binding via `order_by('-bound_at').first()`)
|
||||
- Model: `UserDevice` relationship with `bound_at` ordering
|
||||
|
||||
## Monitoring & Observability
|
||||
|
||||
**Error Tracking:**
|
||||
- No dedicated external error tracking (Sentry, etc.) detected
|
||||
|
||||
**Logs:**
|
||||
- **Aliyun Log Service** - Production logging integration
|
||||
- SDK: `aliyun-log-python-sdk` (in `requirements.txt`)
|
||||
- Implementation: `common/aliyun_logging.py` (AliyunLogHandler at lines 16-36, setup_logging() at lines 32-36)
|
||||
- Config: `ALIYUN_LOG_PROJECT`, `ALIYUN_LOG_STORE`, `ALIYUN_LOG_ENDPOINT`, `ALIYUN_LOG_ACCESS_KEY_ID`, `ALIYUN_LOG_ACCESS_KEY_SECRET` (env vars, loaded at lines 7-11)
|
||||
- Logging config: `qy_lty/settings.py:372-411` configures handlers for `django`, `django.request`, `aiapp`, `common`, `userapp` loggers
|
||||
- Log levels: INFO for production, DEBUG for console in dev
|
||||
|
||||
## CI/CD & Deployment
|
||||
|
||||
**Hosting:**
|
||||
- Docker containers (self-managed or cloud-hosted)
|
||||
- Image base: Python 3.8 via docker.m.daocloud.io (China mirror)
|
||||
|
||||
**CI Pipeline:**
|
||||
- No dedicated CI/CD service detected in code (GitHub Actions, GitLab CI, etc. config not present)
|
||||
|
||||
**Deployment:**
|
||||
- Docker Compose for local/staging (docker-compose.yml:1-33)
|
||||
- Production via Docker Compose or Kubernetes (see Dockerfile for structure)
|
||||
- Port exposure: 12012 (maps to ASGI 8000)
|
||||
|
||||
## Environment Configuration
|
||||
|
||||
**Required env vars (from settings.py and CLAUDE.md):**
|
||||
- `SECRET_KEY` - Django secret
|
||||
- `DEBUG` - Debug mode
|
||||
- `POSTGRESQL_DATABASE_NAME`, `POSTGRESQL_DATABASE_USER`, `POSTGRESQL_DATABASE_PASSWORD`, `POSTGRESQL_DATABASE_HOST`, `POSTGRESQL_DATABASE_PORT`
|
||||
- `REDIS_LOCATION`, `REDIS_PASSWORD`
|
||||
- `KIMI_API_KEY`, `KIMI_BASE_URL` (default: https://api.moonshot.cn/v1)
|
||||
- `ALIYUN_SMS_ACCESS_KEY_ID`, `ALIYUN_SMS_ACCESS_KEY_SECRET`, `ALIYUN_SMS_SIGN_NAME`, `ALIYUN_SMS_TEMPLATE_CODE`
|
||||
- `ALIYUN_NLS_ACCESS_KEY_ID`, `ALIYUN_NLS_ACCESS_KEY_SECRET`, `ALIYUN_NLS_APP_ID`
|
||||
- `ALIYUN_OSS_ACCESS_KEY_ID`, `ALIYUN_OSS_ACCESS_KEY_SECRET`, `ALIYUN_OSS_BUCKET`, `ALIYUN_OSS_ENDPOINT`, `ALIYUN_OSS_HOST`, `ALIYUN_OSS_AUDIO_BASE_DIR`
|
||||
- `ALIYUN_VI_ACCESS_KEY_ID`, `ALIYUN_VI_ACCESS_KEY_SECRET`, `ALIYUN_VI_ENDPOINT`, `ALIYUN_VI_REGION`
|
||||
- `ALIYUN_LOG_PROJECT`, `ALIYUN_LOG_STORE`, `ALIYUN_LOG_ENDPOINT`, `ALIYUN_LOG_ACCESS_KEY_ID`, `ALIYUN_LOG_ACCESS_KEY_SECRET`
|
||||
- `VOLCENGINE_ACCESS_KEY`, `VOLCENGINE_SECRET_KEY`, `VOLCENGINE_APP_ID`, `VOLCENGINE_APP_KEY`
|
||||
- `AUDIO_SERVICE_PROVIDER` (values: 'aliyun', 'tencent', 'huoshan'; default: 'huoshan')
|
||||
- `AUDIO_SERVICE_HUOSHAN_APPID`, `AUDIO_SERVICE_HUOSHAN_ACCESS_TOKEN`, `AUDIO_SERVICE_HUOSHAN_CLUSTER`, `AUDIO_SERVICE_HUOSHAN_VOICE_TYPE`, `AUDIO_SERVICE_HUOSHAN_STORAGE_DIR`, `AUDIO_SERVICE_HUOSHAN_BASE_URL`
|
||||
- `AUDIO_SERVICE_TENCENT_API_KEY`, `AUDIO_SERVICE_TENCENT_API_SECRET` (if provider='tencent')
|
||||
|
||||
**Secrets location:**
|
||||
- `.env` file (git-ignored, copy from `.env.bak` template during setup per `CLAUDE.md` line 44)
|
||||
|
||||
## Webhooks & Callbacks
|
||||
|
||||
**Incoming:**
|
||||
- WebSocket device messages routed to `device_{user_id}` group (see `CLAUDE.md` line 118)
|
||||
- Volcengine RTC conversation callbacks: `conversation_status`, `conversation_subtitle` message types forwarded via WebSocket (see `CLAUDE.md` line 167)
|
||||
- Device heartbeat: `device:last_seen:{mac}` refreshed on message receipt (see `CLAUDE.md` line 230)
|
||||
|
||||
**Outgoing:**
|
||||
- No outbound webhooks detected (integrations are request/response based)
|
||||
|
||||
## API Documentation
|
||||
|
||||
**Documentation Endpoints:**
|
||||
- Swagger UI: `http://localhost:8000/swagger/`
|
||||
- ReDoc: `http://localhost:8000/redoc/`
|
||||
- Generated by: drf-yasg (configured at `qy_lty/settings.py:513-549`)
|
||||
|
||||
**Main API Routes:**
|
||||
- `/api/user/` - User auth & management (MAC login, token generation)
|
||||
- `/api/ai/` - AI chat (single-turn, multi-turn, voice synthesis/recognition)
|
||||
- `/api/device/` - Device interaction, RTC tokens, binding status
|
||||
- `/api/card/` - Card system (batch generation, QR codes, usage tracking)
|
||||
- `/api/achievement/` - Achievement tracking
|
||||
- `/api/v1/admin/` - Admin operations
|
||||
- `/ws/device/` - WebSocket for real-time device communication
|
||||
|
||||
---
|
||||
|
||||
*Integration audit: 2026-05-07*
|
||||
125
qy_lty/.planning/codebase/STACK.md
Normal file
125
qy_lty/.planning/codebase/STACK.md
Normal file
@ -0,0 +1,125 @@
|
||||
# Technology Stack
|
||||
|
||||
**Analysis Date:** 2026-05-07
|
||||
|
||||
## Languages
|
||||
|
||||
**Primary:**
|
||||
- Python 3.8 - Entire backend application (specified in `Dockerfile` line 2)
|
||||
|
||||
## Runtime
|
||||
|
||||
**Environment:**
|
||||
- Python 3.8 via Docker container (docker.m.daocloud.io/python:3.8)
|
||||
- ASGI server: Daphne for WebSocket + HTTP support (see `run.sh`)
|
||||
|
||||
**Package Manager:**
|
||||
- pip 3.8+ (Python package manager)
|
||||
- Lockfile: No pinned versions in `requirements.txt` (line 1-42) — dependencies are unpinned, allowing flexibility but risking compatibility issues
|
||||
|
||||
## Frameworks
|
||||
|
||||
**Core:**
|
||||
- Django 4.2.13 - Primary web framework (declared in `qy_lty/settings.py:4`)
|
||||
- Django REST Framework (DRF) - REST API implementation (in `requirements.txt`)
|
||||
- Django Channels - WebSocket support for real-time device communication (in `requirements.txt`, configured in `qy_lty/settings.py:501`)
|
||||
|
||||
**Real-Time & Async:**
|
||||
- Daphne - ASGI server to serve both HTTP and WebSocket (in `requirements.txt`, launched via `run.sh`)
|
||||
- channels-redis - Channel layer using Redis for inter-process messaging (in `requirements.txt`, configured at `qy_lty/settings.py:504-511`)
|
||||
- aioredis - Async Redis client (in `requirements.txt`)
|
||||
|
||||
**API Documentation:**
|
||||
- drf-yasg - Generates Swagger/OpenAPI documentation for DRF (in `requirements.txt`, configured at `qy_lty/settings.py:513-549`)
|
||||
|
||||
**Database & Caching:**
|
||||
- PostgreSQL - Primary relational database (configured at `qy_lty/settings.py:166-172`)
|
||||
- Redis - Cache backend and WebSocket channel layer (configured at `qy_lty/settings.py:252-260` for cache, `504-511` for channels)
|
||||
- Client: django-redis for Django integration (in `requirements.txt`)
|
||||
- Password-authenticated via environment variables
|
||||
|
||||
**Admin & UI:**
|
||||
- django-simpleui - Enhanced admin interface with Chinese localization (in `requirements.txt`, configured at `qy_lty/settings.py:475-498`)
|
||||
- django-rosetta - Translation management UI (in `requirements.txt`)
|
||||
|
||||
**Authentication & Authorization:**
|
||||
- django-allauth - Social authentication framework including WeChat provider (in `requirements.txt`, configured at `qy_lty/settings.py:52-56`, backend at `qy_lty/settings.py:321-325`)
|
||||
- dj_rest_auth - Token-based REST authentication bridge (in `requirements.txt`)
|
||||
- django-phone-verify - Phone verification support (in `requirements.txt`)
|
||||
- Custom: `userapp.authentication.RedisTokenAuthentication` - Token stored in Redis with 30-day TTL (see `CLAUDE.md` line 122)
|
||||
|
||||
**Development & Debugging:**
|
||||
- django-debug-toolbar - Development-time debugging (in `requirements.txt`)
|
||||
- django-cors-headers - CORS middleware for cross-origin requests (in `requirements.txt`, middleware at `qy_lty/settings.py:85`)
|
||||
|
||||
**Task Scheduling:**
|
||||
- APScheduler (apscheduler) - Background task scheduling (in `requirements.txt`)
|
||||
|
||||
**File Storage & Image Processing:**
|
||||
- Pillow - Image processing library (in `requirements.txt`)
|
||||
- oss2 - Aliyun OSS Python SDK for file uploads (in `requirements.txt`, used in `common/oss.py`)
|
||||
|
||||
**Data Export & Processing:**
|
||||
- openpyxl - Excel file generation (in `requirements.txt`)
|
||||
|
||||
**Utilities:**
|
||||
- requests - HTTP client for external API calls (in `requirements.txt`, used throughout for service integrations)
|
||||
- cryptography - Encryption/decryption utilities (in `requirements.txt`)
|
||||
- python-decouple - Environment variable loading (in `requirements.txt`, used at `qy_lty/settings.py:14`)
|
||||
- openai - OpenAI Python SDK for Kimi API (compatible with OpenAI spec, in `requirements.txt`)
|
||||
|
||||
## Key Dependencies
|
||||
|
||||
**Critical Infrastructure:**
|
||||
- mysqlclient - MySQL database adapter (in `requirements.txt`, though PostgreSQL is primary; MySQL config commented at `qy_lty/settings.py:159-164`)
|
||||
- psycopg2-binary - PostgreSQL database adapter (in `requirements.txt`, active at `qy_lty/settings.py:166`)
|
||||
|
||||
**Aliyun (Alibaba Cloud) SDKs:**
|
||||
- aliyun-python-sdk-core - Core SDK for Aliyun services (in `requirements.txt`)
|
||||
- aliyun-python-sdk-dysmsapi - SMS service SDK (in `requirements.txt`, configured at `qy_lty/settings.py:300-304`)
|
||||
- oss2 - OSS (Object Storage Service) SDK (in `requirements.txt`, integrated at `common/oss.py:1-90`)
|
||||
- alibabacloud_facebody20191230 - Face detection/recognition service (in `requirements.txt`)
|
||||
- alibabacloud_tea_console, alibabacloud_tea_util, alibabacloud_tea_openapi - Aliyun SDK utilities (in `requirements.txt`)
|
||||
- aliyun-log-python-sdk - Logging service SDK (in `requirements.txt`, used at `common/aliyun_logging.py:1-36`)
|
||||
|
||||
**AI & LLM:**
|
||||
- openai - OpenAI Python client, used for Kimi (OpenAI-compatible API) at `aiapp/kimi.py:2` (configured at `qy_lty/settings.py:349-350`)
|
||||
|
||||
## Configuration
|
||||
|
||||
**Environment:**
|
||||
- Loaded via `python-decouple.config()` from `.env` file (see `qy_lty/settings.py:14`)
|
||||
- Critical env vars required:
|
||||
- `SECRET_KEY` - Django secret key
|
||||
- `DEBUG` - Debug mode flag
|
||||
- `POSTGRESQL_DATABASE_NAME`, `POSTGRESQL_DATABASE_USER`, `POSTGRESQL_DATABASE_PASSWORD`, `POSTGRESQL_DATABASE_HOST`, `POSTGRESQL_DATABASE_PORT` - PostgreSQL connection
|
||||
- `REDIS_LOCATION`, `REDIS_PASSWORD` - Redis cache and channel layer
|
||||
- `KIMI_API_KEY`, `KIMI_BASE_URL` - Kimi AI configuration
|
||||
- `ALIYUN_*` family - SMS, OSS, NLS, VI, logging credentials
|
||||
- `VOLCENGINE_ACCESS_KEY`, `VOLCENGINE_SECRET_KEY` - RTC service credentials
|
||||
- Audio service provider env vars (AUDIO_SERVICE_HUOSHAN_*, AUDIO_SERVICE_TENCENT_*)
|
||||
|
||||
**Build:**
|
||||
- `Dockerfile` - Docker image definition (lines 1-33), uses Python 3.8 with China mirror for pip
|
||||
- `docker-compose.yml` - Service orchestration (lines 1-33), exposes port 12012 → 8000
|
||||
- `run.sh` - Local development entrypoint (lines 1-2), launches Daphne on 0.0.0.0:8000
|
||||
|
||||
## Platform Requirements
|
||||
|
||||
**Development:**
|
||||
- Python 3.8+
|
||||
- pip for dependency installation
|
||||
- PostgreSQL database server (or MySQL as fallback)
|
||||
- Redis server for cache + channel layer
|
||||
- Docker & docker-compose for containerized development
|
||||
|
||||
**Production:**
|
||||
- Docker container (see `Dockerfile` and `docker-compose.yml`)
|
||||
- External PostgreSQL service
|
||||
- External Redis service
|
||||
- HTTPS reverse proxy (e.g., Nginx) recommended for production
|
||||
- Deployment target: Linux/Docker (accessible via port 12012 in example)
|
||||
|
||||
---
|
||||
|
||||
*Stack analysis: 2026-05-07*
|
||||
421
qy_lty/.planning/codebase/STRUCTURE.md
Normal file
421
qy_lty/.planning/codebase/STRUCTURE.md
Normal file
@ -0,0 +1,421 @@
|
||||
# Codebase Structure
|
||||
|
||||
**Analysis Date:** 2026-05-07
|
||||
|
||||
## Directory Layout
|
||||
|
||||
```
|
||||
qy_lty/
|
||||
├── manage.py # Django management script
|
||||
├── run.sh # Start development server (daphne)
|
||||
├── requirements.txt # Python dependencies
|
||||
├── Dockerfile # Container image build
|
||||
├── docker-compose.yml # Docker compose for dev environment
|
||||
├── .env # Environment variables (not committed)
|
||||
│
|
||||
├── qy_lty/ # Project configuration package
|
||||
│ ├── __init__.py
|
||||
│ ├── settings.py # Django settings (INSTALLED_APPS, middleware, auth, etc.)
|
||||
│ ├── asgi.py # ASGI app entry (Channels + HTTP routing)
|
||||
│ ├── wsgi.py # WSGI app entry (for traditional servers)
|
||||
│ └── urls.py # Root URL router (includes all app URLs)
|
||||
│
|
||||
├── userapp/ # User authentication & profile management
|
||||
│ ├── models.py # ParadiseUser (custom user model), AffinityRule, AffinityLevel
|
||||
│ ├── views.py # MacAddressLoginView, PhoneLoginView, UserViewSet, ProfileView
|
||||
│ ├── serializers.py # ParadiseUserSerializer, ProfileUpdateSerializer
|
||||
│ ├── urls.py # User API routes: /api/user/
|
||||
│ ├── auth_urls.py # Auth-specific routes (deprecated/legacy)
|
||||
│ ├── admin_urls.py # Admin API routes: /api/v1/admin/
|
||||
│ ├── authentication.py # RedisTokenAuthentication class
|
||||
│ ├── utils.py # generate_token(), get_user_id_from_token(), send_sms()
|
||||
│ ├── admin.py # Django admin customization
|
||||
│ ├── apps.py # App configuration
|
||||
│ ├── migrations/ # Database migrations
|
||||
│ ├── management/ # Custom management commands
|
||||
│ └── tests.py # Unit tests
|
||||
│
|
||||
├── aiapp/ # AI chat & voice integration
|
||||
│ ├── models.py # ChatMessage, Bot, ConversationSubtitle
|
||||
│ ├── views.py # BotViewSet, ChatView (Kimi API integration, multi-turn chat)
|
||||
│ ├── serializers.py # ChatMessageSerializer
|
||||
│ ├── urls.py # AI API routes: /api/ai/
|
||||
│ ├── kimi.py # Kimi API client (OpenAI compatible)
|
||||
│ ├── audio/ # Audio service abstraction
|
||||
│ │ ├── AudioService.py # Multi-provider audio (Aliyun/Volcengine/Tencent)
|
||||
│ │ ├── aliyun_audio.py # Aliyun NLS implementation
|
||||
│ │ └── [other provider implementations]
|
||||
│ ├── admin.py # Admin interface for Chat models
|
||||
│ ├── apps.py # App configuration
|
||||
│ ├── migrations/ # Database migrations
|
||||
│ └── tests.py
|
||||
│
|
||||
├── device_interaction/ # WebSocket real-time device communication
|
||||
│ ├── models.py # Device, DeviceType, DeviceBatch, UserDevice
|
||||
│ ├── views.py # DeviceTypeViewSet, DeviceViewSet, UserDeviceViewSet, RTC endpoints
|
||||
│ ├── serializers.py # DeviceSerializer, UserDeviceSerializer, DeviceBindSerializer
|
||||
│ ├── urls.py # Device HTTP API routes: /api/device/
|
||||
│ ├── routing.py # WebSocket URL routing (ws://domain/ws/device/)
|
||||
│ ├── consumers.py # DeviceConsumer (AsyncWebsocketConsumer, message handlers)
|
||||
│ ├── auth.py # TokenAuthMiddleware (WebSocket auth)
|
||||
│ ├── amap_api.py # Amap (高德地图) location/nearby search
|
||||
│ ├── weather.py # QWeather API for weather data
|
||||
│ ├── volcengine_api.py # Volcengine RTC token generation
|
||||
│ ├── swagger.py # Swagger schema definitions
|
||||
│ ├── admin.py # Admin interface for Device models
|
||||
│ ├── apps.py # App configuration
|
||||
│ ├── middleware.py # Custom middleware (if any)
|
||||
│ ├── services.py # Business logic services
|
||||
│ ├── scheduler.py # Async task scheduling (e.g., device heartbeat monitoring)
|
||||
│ ├── management/ # Custom management commands
|
||||
│ ├── migrations/ # Database migrations
|
||||
│ └── tests.py
|
||||
│
|
||||
├── card/ # Card management system
|
||||
│ ├── models.py # CardCategory, CardBatch, Card, CardUsageLog
|
||||
│ ├── views.py # CardViewSet, batch generation, QR code endpoints
|
||||
│ ├── serializers.py # CardSerializer
|
||||
│ ├── urls.py # Card API routes: /api/card/
|
||||
│ ├── admin.py
|
||||
│ ├── apps.py
|
||||
│ ├── migrations/
|
||||
│ └── tests.py
|
||||
│
|
||||
├── achievement_app/ # User achievements & progress tracking
|
||||
│ ├── models.py # Achievement, AchievementProgress, AchievementBadge
|
||||
│ ├── views.py # AchievementViewSet, ProgressView
|
||||
│ ├── serializers.py # AchievementSerializer
|
||||
│ ├── urls.py # Achievement API routes: /api/achievement/
|
||||
│ ├── admin.py
|
||||
│ ├── apps.py
|
||||
│ ├── migrations/
|
||||
│ └── tests.py
|
||||
│
|
||||
├── subscription_app/ # Subscription & billing management
|
||||
│ ├── models.py # Subscription, SubscriptionPlan, Payment
|
||||
│ ├── views.py # SubscriptionViewSet
|
||||
│ ├── serializers.py # SubscriptionSerializer
|
||||
│ ├── urls.py # Subscription API routes (if exposed)
|
||||
│ ├── admin.py
|
||||
│ ├── apps.py
|
||||
│ ├── migrations/
|
||||
│ ├── templates/ # Email templates for subscription notifications
|
||||
│ └── tests.py
|
||||
│
|
||||
├── ali_vi_app/ # Aliyun Visual Intelligence integration
|
||||
│ ├── models.py # VIRequest, VIResult
|
||||
│ ├── views.py # Aliyun VI API wrappers
|
||||
│ ├── urls.py # VI API routes (currently disabled in root urls.py line 52)
|
||||
│ ├── admin.py
|
||||
│ ├── apps.py
|
||||
│ ├── migrations/
|
||||
│ └── tests.py
|
||||
│
|
||||
├── food_app/ # Food management system (new)
|
||||
│ ├── models.py # Food, FoodInventory
|
||||
│ ├── views.py # FoodViewSet
|
||||
│ ├── serializers.py
|
||||
│ ├── urls.py # Food API routes: /api/food/
|
||||
│ ├── admin.py
|
||||
│ ├── apps.py
|
||||
│ ├── migrations/
|
||||
│ └── tests.py
|
||||
│
|
||||
├── workflow_app/ # Multi-tenant workflow management (dev)
|
||||
│ ├── models.py # Workflow, WorkflowStep, WorkflowTask
|
||||
│ ├── views.py
|
||||
│ ├── serializers.py
|
||||
│ ├── urls.py
|
||||
│ ├── admin.py
|
||||
│ ├── apps.py
|
||||
│ ├── migrations/
|
||||
│ └── tests.py
|
||||
│
|
||||
├── common/ # Shared utilities & middleware
|
||||
│ ├── middleware.py # StandardResponseMiddleware (standardizes all responses)
|
||||
│ ├── pagination.py # CustomPageNumberPagination (DRF pagination)
|
||||
│ ├── responses.py # Helper functions: success_response(), error_response()
|
||||
│ ├── swagger_utils.py # Swagger/OpenAPI utilities
|
||||
│ ├── aliyun_logging.py # Aliyun log service setup
|
||||
│ ├── VolcEngineAccessToken.py # Volcengine RTC token generation
|
||||
│ ├── admin.py
|
||||
│ ├── apps.py
|
||||
│ └── [other shared modules]
|
||||
│
|
||||
├── templates/ # HTML templates for Django admin & error pages
|
||||
│ └── [admin templates, error pages]
|
||||
│
|
||||
├── locale/ # i18n translation catalogs
|
||||
│ ├── en/ # English translations
|
||||
│ │ └── LC_MESSAGES/
|
||||
│ │ ├── django.po # Compiled message file
|
||||
│ │ └── django.mo # Binary compiled messages
|
||||
│ └── zh_HAns/ # Simplified Chinese translations
|
||||
│ └── LC_MESSAGES/
|
||||
│
|
||||
├── static/ # Static files (JS, CSS, images) — dev only
|
||||
│ └── [served by STATIC_URL]
|
||||
│
|
||||
├── media/ # User-uploaded files — dev only
|
||||
│ └── [served by MEDIA_URL]
|
||||
│
|
||||
├── docs/ # Documentation
|
||||
│ ├── README.md # Project overview
|
||||
│ ├── 修改记录.md # Change log (Chinese, auto-maintained)
|
||||
│ ├── 设备动态绑定方案.md # Device dynamic binding design
|
||||
│ ├── 设备聊天记录_字幕落库方案.md # Chat transcript & subtitle storage design
|
||||
│ ├── device_api.md # WebSocket device API reference
|
||||
│ ├── device_commands.md # Device command reference
|
||||
│ ├── device_errors.md # Device error codes
|
||||
│ ├── device_module.md # Device module overview
|
||||
│ ├── user_authentication_api.md # User auth API reference
|
||||
│ ├── api/ # API endpoint documentation
|
||||
│ ├── development/ # Development guides
|
||||
│ ├── features/ # Feature documentation
|
||||
│ └── integrations/ # Integration guides
|
||||
│
|
||||
└── .planning/ # GSD codebase mapping (auto-generated)
|
||||
└── codebase/
|
||||
├── ARCHITECTURE.md # Architecture patterns & data flows
|
||||
└── STRUCTURE.md # This file
|
||||
```
|
||||
|
||||
## Directory Purposes
|
||||
|
||||
**qy_lty/ (Project Config):**
|
||||
- Purpose: Django project configuration and root URL routing
|
||||
- Contains: Settings, ASGI/WSGI entry points, URL dispatcher
|
||||
- Key files:
|
||||
- `settings.py` (line 1-400+): All Django configuration, app list, middleware, DB, cache, auth
|
||||
- `urls.py` (line 49-60): Centralizes all API routes via `include()`
|
||||
- `asgi.py` (line 19-26): ProtocolTypeRouter for HTTP + WebSocket
|
||||
|
||||
**userapp/:**
|
||||
- Purpose: User authentication, profile management, affinity system
|
||||
- Contains: ParadiseUser model, MAC/phone login, token generation, user info endpoints
|
||||
- Key files:
|
||||
- `models.py` (line 8-145): ParadiseUser, AffinityRule, AffinityLevel (favorability system)
|
||||
- `authentication.py` (line 10-34): RedisTokenAuthentication class
|
||||
- `utils.py` (line 33-37): `generate_token()` stores in Redis with 30-day TTL
|
||||
|
||||
**device_interaction/:**
|
||||
- Purpose: Real-time device communication via WebSocket, device management
|
||||
- Contains: WebSocket consumer, device CRUD, user-device binding, RTC token endpoints
|
||||
- Key files:
|
||||
- `consumers.py` (line 10-320+): DeviceConsumer handles connect/receive/disconnect, group messaging
|
||||
- `routing.py` (line 4-7): WebSocket URL patterns
|
||||
- `auth.py` (line 4-50): TokenAuthMiddleware authenticates WebSocket connections
|
||||
- `models.py` (line 43-145): Device, UserDevice with "last-bind-wins" ordering
|
||||
|
||||
**aiapp/:**
|
||||
- Purpose: AI chat (Kimi), multi-provider audio services
|
||||
- Contains: Chat message models, Kimi API client, audio synthesis/recognition
|
||||
- Key files:
|
||||
- `views.py` (line 1-60+): BotViewSet, ChatView for multi-turn conversations
|
||||
- `audio/` (submodule): AudioService factory + provider implementations
|
||||
|
||||
**card/:**
|
||||
- Purpose: Card management (categories, batches, QR codes, usage tracking)
|
||||
- Contains: Card CRUD, batch generation, QR code generation/scanning
|
||||
- Key files: models.py, views.py (batch endpoints), serializers.py
|
||||
|
||||
**achievement_app/:**
|
||||
- Purpose: User achievements, progress tracking, badges
|
||||
- Contains: Achievement definitions, user progress models
|
||||
- Key files: models.py, views.py (progress endpoints)
|
||||
|
||||
**subscription_app/:**
|
||||
- Purpose: Billing and subscription management
|
||||
- Contains: Subscription plans, payment records
|
||||
- Key files: models.py
|
||||
|
||||
**ali_vi_app/, food_app/, workflow_app/:**
|
||||
- Purpose: Specialized modules (Aliyun Visual Intelligence, Food management, Workflow automation)
|
||||
- Contains: Domain-specific models, views, serializers
|
||||
- Note: ali_vi_app routes disabled in qy_lty/urls.py line 52
|
||||
|
||||
**common/:**
|
||||
- Purpose: Shared utilities across all apps
|
||||
- Contains: Middleware, pagination, response helpers, external API clients
|
||||
- Key files:
|
||||
- `middleware.py` (line 6-145): StandardResponseMiddleware wraps all responses
|
||||
- `pagination.py` (line 1-9): CustomPageNumberPagination for DRF list views
|
||||
- `responses.py`: Helper functions for standardized responses
|
||||
|
||||
**locale/:**
|
||||
- Purpose: i18n translation catalogs (Chinese/English)
|
||||
- Contains: Django message files (.po/.mo)
|
||||
- Generated by: `django-admin makemessages -l en`, `compilemessages`
|
||||
|
||||
**docs/:**
|
||||
- Purpose: Human-readable documentation
|
||||
- Contains: Design docs, API references, development guides
|
||||
- Key files:
|
||||
- `修改记录.md`: Auto-maintained change log (new entries at top)
|
||||
- `设备动态绑定方案.md`: Device binding strategy & semantics
|
||||
- `device_api.md`: WebSocket message types & format
|
||||
|
||||
## Key File Locations
|
||||
|
||||
**Entry Points:**
|
||||
- `qy_lty/urls.py`: HTTP root router (line 63-73)
|
||||
- `qy_lty/asgi.py`: ASGI app with Channels (line 19-26)
|
||||
- `device_interaction/routing.py`: WebSocket URL patterns (line 4-7)
|
||||
- `device_interaction/consumers.py`: WebSocket message handler (line 10+)
|
||||
|
||||
**Configuration:**
|
||||
- `qy_lty/settings.py`: All Django configuration (INSTALLED_APPS, middleware, auth, databases)
|
||||
- `.env`: Environment variables (NEVER committed; copy from `.env.bak` if it exists)
|
||||
- `requirements.txt`: Python package dependencies
|
||||
|
||||
**Authentication & Authorization:**
|
||||
- `userapp/authentication.py`: RedisTokenAuthentication (HTTP)
|
||||
- `device_interaction/auth.py`: TokenAuthMiddleware (WebSocket)
|
||||
- `userapp/utils.py`: Token generation/lookup with Redis
|
||||
- `userapp/models.py`: ParadiseUser (custom user model)
|
||||
|
||||
**Core Models:**
|
||||
- `userapp/models.py`: ParadiseUser, AffinityRule, AffinityLevel
|
||||
- `device_interaction/models.py`: Device, DeviceType, DeviceBatch, UserDevice
|
||||
- `aiapp/models.py`: ChatMessage, Bot, ConversationSubtitle
|
||||
- `card/models.py`: CardCategory, CardBatch, Card, CardUsageLog
|
||||
- `achievement_app/models.py`: Achievement, AchievementProgress
|
||||
- `subscription_app/models.py`: Subscription, SubscriptionPlan
|
||||
|
||||
**Middleware & Cross-Cutting:**
|
||||
- `common/middleware.py`: StandardResponseMiddleware (response wrapper)
|
||||
- `common/pagination.py`: CustomPageNumberPagination (DRF pagination)
|
||||
- `common/responses.py`: `success_response()`, `error_response()` helpers
|
||||
- `qy_lty/settings.py` line 82-95: All middleware configured
|
||||
|
||||
**API Documentation:**
|
||||
- Swagger UI: Generated from code, served at `/swagger/`
|
||||
- ReDoc: Alternative documentation UI at `/redoc/`
|
||||
- drf-yasg integration: Configured in qy_lty/urls.py line 22-46
|
||||
|
||||
**Logging:**
|
||||
- Configuration: `qy_lty/settings.py` (logging config, or via `common/aliyun_logging.py`)
|
||||
- Entry point: `setup_logging()` in common/aliyun_logging.py (called from settings.py line 16)
|
||||
|
||||
**i18n Catalogs:**
|
||||
- English: `locale/en/LC_MESSAGES/django.{po,mo}`
|
||||
- Chinese: `locale/zh_HAns/LC_MESSAGES/django.{po,mo}`
|
||||
- Generation: `django-admin makemessages -l {lang}`
|
||||
- Compilation: `django-admin compilemessages`
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
**Files:**
|
||||
- Django app folders: lowercase_underscore (e.g., `device_interaction`, `user_app`)
|
||||
- Python modules: lowercase_underscore (e.g., `models.py`, `serializers.py`, `auth.py`)
|
||||
- Migrations: auto-generated format `000X_auto_YYYYMMDD_HHMM.py`
|
||||
- Documentation: Chinese filename or English snake_case
|
||||
|
||||
**Directories:**
|
||||
- Apps: snake_case (userapp, device_interaction, achievement_app)
|
||||
- Packages: lowercase without underscores (audio/, management/, migrations/)
|
||||
|
||||
**URL Prefixes:**
|
||||
- `/api/user/` → userapp endpoints
|
||||
- `/api/device/` → device_interaction endpoints
|
||||
- `/api/ai/` → aiapp endpoints
|
||||
- `/api/card/` → card endpoints
|
||||
- `/api/achievement/` → achievement_app endpoints
|
||||
- `/api/food/` → food_app endpoints
|
||||
- `/api/v1/admin/` → admin endpoints (via userapp.admin_urls)
|
||||
- `/ws/device/` → WebSocket endpoint
|
||||
|
||||
**Database Models:**
|
||||
- User model: `ParadiseUser` (extends AbstractUser)
|
||||
- Device-related: `Device`, `DeviceBatch`, `DeviceType`, `UserDevice`
|
||||
- Message models: `ChatMessage`, `ConversationSubtitle`
|
||||
- Achievement models: `Achievement`, `AchievementProgress`, `AchievementBadge`
|
||||
|
||||
## Where to Add New Code
|
||||
|
||||
**New Feature (e.g., "Affinity Decay Task"):**
|
||||
- Models: `device_interaction/models.py` (if device-related) or `userapp/models.py` (if user-related)
|
||||
- Views/API: `device_interaction/views.py` or `userapp/views.py` → new ViewSet or APIView
|
||||
- Serializers: `device_interaction/serializers.py` or `userapp/serializers.py` → new ModelSerializer
|
||||
- URLs: Edit `device_interaction/urls.py` or `userapp/urls.py` → add path/route
|
||||
- Async tasks: `device_interaction/scheduler.py` or new `tasks.py` file
|
||||
- Tests: `device_interaction/tests.py` or `userapp/tests.py`
|
||||
|
||||
**New Component/Module (e.g., "Chat Analytics"):**
|
||||
- Create new Django app: `python manage.py startapp analytics`
|
||||
- Standard files: `models.py`, `views.py`, `serializers.py`, `urls.py`, `admin.py`, `apps.py`, `tests.py`
|
||||
- Register in `qy_lty/settings.py` INSTALLED_APPS (line 43+)
|
||||
- Include URLs in `qy_lty/urls.py` (line 49-60)
|
||||
|
||||
**Shared Utilities:**
|
||||
- Common middleware/helpers: Add to `common/` directory
|
||||
- Examples: `common/responses.py`, `common/swagger_utils.py`
|
||||
- New integration (e.g., API client): Create in `common/` or within relevant app (e.g., `device_interaction/amap_api.py`)
|
||||
|
||||
**WebSocket Message Handler:**
|
||||
- Edit `device_interaction/consumers.py:DeviceConsumer.receive()` (line 171+)
|
||||
- Add new `elif message_type == 'new_type':` branch
|
||||
- Call appropriate handler or `group_send()` to broadcast
|
||||
|
||||
**Serializer Customization:**
|
||||
- Override `to_representation()` for custom output
|
||||
- Override `to_internal_value()` for custom input processing
|
||||
- Add `validate_<field>()` for field-specific validation
|
||||
- Add `validate()` for cross-field validation
|
||||
|
||||
## Special Directories
|
||||
|
||||
**migrations/:**
|
||||
- Purpose: Track database schema changes
|
||||
- Generated: `python manage.py makemigrations`
|
||||
- Applied: `python manage.py migrate`
|
||||
- Committed: Yes (part of version control)
|
||||
|
||||
**.planning/codebase/:**
|
||||
- Purpose: GSD (Generate Specification Documents) auto-generated codebase maps
|
||||
- Generated: By `/gsd-map-codebase` orchestrator
|
||||
- Committed: Yes (reference docs for other GSD commands)
|
||||
- Contents: ARCHITECTURE.md, STRUCTURE.md, CONVENTIONS.md, TESTING.md, CONCERNS.md, INTEGRATIONS.md, STACK.md
|
||||
|
||||
**docs/:**
|
||||
- Purpose: Human documentation
|
||||
- Committed: Yes
|
||||
- Auto-maintained: `修改记录.md` (change log, appended by developers after each commit)
|
||||
|
||||
**locale/:**
|
||||
- Purpose: i18n translation catalogs
|
||||
- Generated: `django-admin makemessages` (creates .po files)
|
||||
- Compiled: `django-admin compilemessages` (creates .mo files)
|
||||
- Committed: Yes (both .po and .mo)
|
||||
|
||||
**static/ & media/:**
|
||||
- Purpose: Served files (CSS, JS, images; user uploads)
|
||||
- Generated: No (user-managed)
|
||||
- Committed: No (in .gitignore)
|
||||
- Dev only: Yes (production uses CloudStorage/OSS)
|
||||
|
||||
## Management Commands
|
||||
|
||||
**Standard Django:**
|
||||
```bash
|
||||
python manage.py migrate # Apply database migrations
|
||||
python manage.py makemigrations # Create new migration files
|
||||
python manage.py createsuperuser # Create admin user
|
||||
python manage.py shell # Interactive Python shell
|
||||
python manage.py dbshell # Interactive database shell
|
||||
```
|
||||
|
||||
**i18n:**
|
||||
```bash
|
||||
django-admin makemessages -l en # Extract translatable strings (English)
|
||||
django-admin makemessages -l zh_HAns # Extract translatable strings (Simplified Chinese)
|
||||
django-admin compilemessages # Compile .po to .mo files
|
||||
```
|
||||
|
||||
**Custom Commands (if any):**
|
||||
- Location: `<app>/management/commands/<command_name>.py`
|
||||
- Run: `python manage.py <command_name>`
|
||||
|
||||
---
|
||||
|
||||
*Structure analysis: 2026-05-07*
|
||||
292
qy_lty/.planning/codebase/TESTING.md
Normal file
292
qy_lty/.planning/codebase/TESTING.md
Normal file
@ -0,0 +1,292 @@
|
||||
# Testing Patterns
|
||||
|
||||
**Analysis Date:** 2026-05-07
|
||||
|
||||
## Test Framework
|
||||
|
||||
**Runner:**
|
||||
- Django's built-in `TestCase` from `django.test`
|
||||
- NO pytest or pytest-django detected in `requirements.txt`
|
||||
- Tests run via: `python manage.py test`
|
||||
|
||||
**Assertion Library:**
|
||||
- Django's `TestCase` built-in assertions (inherited from `unittest.TestCase`)
|
||||
- `self.assertEqual()`, `self.assertTrue()`, `self.assertRaises()`, etc.
|
||||
|
||||
**Run Commands:**
|
||||
```bash
|
||||
python manage.py test # Run all tests
|
||||
python manage.py test app_name # Run tests for specific app
|
||||
python manage.py test app_name.tests.TestClassName # Run specific test class
|
||||
```
|
||||
|
||||
**Coverage:**
|
||||
- No `.coveragerc` or coverage configuration found
|
||||
- No coverage requirement enforced
|
||||
|
||||
## Test File Organization
|
||||
|
||||
**Location:**
|
||||
- Each Django app has a `tests.py` file in app root: `{app_name}/tests.py`
|
||||
- Pattern: co-located with models, serializers, views (one file per app)
|
||||
|
||||
**Naming:**
|
||||
- Single file: `tests.py`
|
||||
- Test classes inherit from `django.test.TestCase`
|
||||
- Test methods prefixed with `test_`: `test_audio()`, `test_model_creation()`, `test_view_response()`
|
||||
|
||||
**File Structure:**
|
||||
```
|
||||
app_name/
|
||||
├── models.py
|
||||
├── serializers.py
|
||||
├── views.py
|
||||
├── urls.py
|
||||
└── tests.py # All tests for this app
|
||||
```
|
||||
|
||||
## Current Test Coverage Status
|
||||
|
||||
**Honest Assessment: MINIMAL**
|
||||
|
||||
Test files examined show very sparse coverage:
|
||||
- `userapp/tests.py`: 4 lines (empty TestCase class)
|
||||
- `aiapp/tests.py`: 24 lines (only 1 actual test: `test_audio()`)
|
||||
- `card/tests.py`: 3 lines (empty)
|
||||
- `device_interaction/tests.py`: 3 lines (empty)
|
||||
- `achievement_app/tests.py`: 3 lines (empty)
|
||||
|
||||
**Total active test methods: 1** (audio synthesis in `aiapp/tests.py`)
|
||||
|
||||
This represents **critical test coverage gap** — almost all API endpoints, models, and serializers are untested.
|
||||
|
||||
## Test Structure
|
||||
|
||||
**Suite Organization from `aiapp/tests.py`:**
|
||||
```python
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from .audio.AudioService import get_audio_service
|
||||
|
||||
class YourModelTest(TestCase):
|
||||
|
||||
def test_audio(self):
|
||||
# Setup (minimal)
|
||||
audio_ser = get_audio_service()
|
||||
audio_ser.synthesize_speech('你好,你是谁啊')
|
||||
|
||||
# Commented-out template tests below (never implemented)
|
||||
# def test_model_creation(self):
|
||||
# def test_view_response(self):
|
||||
```
|
||||
|
||||
**Patterns:**
|
||||
- `setUp()` method not used; tests don't create fixtures
|
||||
- No tearDown() cleanup observed
|
||||
- No assertion calls in active test (test_audio just calls synthesize_speech without checking result)
|
||||
|
||||
## Mocking
|
||||
|
||||
**Framework:** No mocking library detected (no `unittest.mock`, `responses`, `pytest-mock` in requirements.txt)
|
||||
|
||||
**Current Practice:**
|
||||
- Tests that DO exist (test_audio) call real external services (AudioService.synthesize_speech)
|
||||
- No mocking observed for:
|
||||
- Aliyun API calls (SMS, OSS, NLS)
|
||||
- Volcengine RTC token generation
|
||||
- Tencent audio service
|
||||
- Kimi AI API
|
||||
- Redis cache operations
|
||||
- Database operations (use real test DB)
|
||||
|
||||
**Risk:** Tests that call production services (Aliyun, Kimi, Volcengine) require:
|
||||
- Valid credentials in .env
|
||||
- Network access to external services
|
||||
- Potential cost (SMS sends, API calls)
|
||||
|
||||
## Database Testing
|
||||
|
||||
**Test Database:**
|
||||
- Django uses a separate test database (by default: test_qy_lty or suffixed with `test_`)
|
||||
- No custom settings for test DB observed
|
||||
- No fixtures or data factories defined
|
||||
|
||||
**Patterns:**
|
||||
- Models have no test data factories (no factory_boy or similar)
|
||||
- No `setUp()` creating test objects
|
||||
- Manual creation would be required: `Model.objects.create(...)`
|
||||
|
||||
## Fixtures and Test Data
|
||||
|
||||
**Test Data:**
|
||||
- NO fixture files found (.json, .yaml, .fixture)
|
||||
- NO factory definitions (factory_boy not in requirements)
|
||||
- NO model-level test data builders
|
||||
|
||||
**How to Add:**
|
||||
If implementing tests, would need:
|
||||
```python
|
||||
def setUp(self):
|
||||
self.user = ParadiseUser.objects.create(
|
||||
username='testuser',
|
||||
email='test@example.com'
|
||||
)
|
||||
self.device = Device.objects.create(
|
||||
device_type=...,
|
||||
batch=...,
|
||||
mac_address='AA:BB:CC:DD:EE:FF'
|
||||
)
|
||||
```
|
||||
|
||||
## WebSocket Consumer Testing
|
||||
|
||||
**Current State:** NO WebSocket consumer tests
|
||||
|
||||
**Channels provides `channels.testing` package** for async testing, but:
|
||||
- No `conftest.py` or pytest configuration
|
||||
- No async test runner configured
|
||||
- Would require adding pytest + pytest-asyncio + pytest-django
|
||||
|
||||
**If implementing WebSocket tests, would require:**
|
||||
```python
|
||||
from channels.testing import WebsocketCommunicator
|
||||
from device_interaction.consumers import DeviceConsumer
|
||||
|
||||
async def test_device_consumer_connect():
|
||||
communicator = WebsocketCommunicator(DeviceConsumer.as_asgi(), '/ws/device/')
|
||||
connected, subprotocol = await communicator.connect()
|
||||
assert connected
|
||||
```
|
||||
|
||||
## External Service Testing
|
||||
|
||||
**Services integrated WITHOUT mocking in current codebase:**
|
||||
- `aiapp.audio.AudioService` — calls real Aliyun/Tencent/Volcengine
|
||||
- Aliyun SMS (send_sms in userapp/utils.py)
|
||||
- Aliyun OSS (file uploads)
|
||||
- Kimi API (AI chat)
|
||||
- Volcengine RTC token generation
|
||||
- WeChat OAuth (django-allauth)
|
||||
|
||||
**No test doubles, stubs, or mocks exist** — tests would hit production services.
|
||||
|
||||
## API Endpoint Testing
|
||||
|
||||
**Current State:** NO API tests
|
||||
|
||||
**Endpoints that should be tested but are NOT:**
|
||||
- User authentication: `/api/user/register/`, `/api/user/mac-login/`, `/api/user/phone-login/`
|
||||
- AI chat: `/api/ai/chat/{bot_id}/`, `/api/ai/multi-chat/`
|
||||
- Device binding: `/api/device/bind/`, `/api/device/bind-status/`
|
||||
- Card operations: `/api/card/scan/`, `/api/card/use/`, `/api/card/batches/generate/`
|
||||
- Achievement tracking: `/api/achievement/user-achievements/`
|
||||
- WebSocket: `/ws/device/` connection and message handling
|
||||
|
||||
**If implementing, would use DRF test client:**
|
||||
```python
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
class UserAuthTest(TestCase):
|
||||
def setUp(self):
|
||||
self.client = APIClient()
|
||||
|
||||
def test_mac_login(self):
|
||||
response = self.client.post('/api/user/mac-login/', {
|
||||
'mac_address': 'AA:BB:CC:DD:EE:FF'
|
||||
})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn('token', response.json()['data'])
|
||||
```
|
||||
|
||||
## Test Coverage Gaps
|
||||
|
||||
**CRITICAL UNTESTED AREAS:**
|
||||
|
||||
1. **Authentication layer** — RedisTokenAuthentication never tested
|
||||
- Token generation in userapp/utils.py:generate_token()
|
||||
- Token validation in userapp/authentication.py
|
||||
- Device MAC login flow
|
||||
- Phone verification login
|
||||
|
||||
2. **Device control semantics** — "last-bind-wins" ordering never verified
|
||||
- UserDevice.Meta.ordering = ['-bound_at'] not tested
|
||||
- Multiple users binding same device — control resolution untested
|
||||
- Hardcoded test MAC (AA:BB:CC:DD:EE:FF) skip logic untested
|
||||
|
||||
3. **WebSocket messaging** — All consumer methods untested
|
||||
- DeviceConsumer.connect() authentication
|
||||
- Message routing in receive()
|
||||
- Group messaging (device_{user_id})
|
||||
- Disconnect and heartbeat (device:last_seen:{mac})
|
||||
|
||||
4. **Serializer validation** — No validation tests
|
||||
- All custom validators in serializers.py files untested
|
||||
- Foreign key constraints untested
|
||||
- Unique field constraints untested
|
||||
|
||||
5. **External service integration** — No mocking
|
||||
- Aliyun SMS send (send_sms)
|
||||
- Aliyun OSS upload
|
||||
- Kimi AI API calls
|
||||
- Volcengine RTC token generation
|
||||
- Tencent audio service
|
||||
|
||||
6. **Async task processing** — No tests
|
||||
- Affinity rule evaluation (coming in P2)
|
||||
- Device heartbeat refresh (5-min TTL)
|
||||
- Daily affinity counter aggregation
|
||||
|
||||
7. **Model business logic** — No tests
|
||||
- AffinityRule.get_solo() singleton pattern
|
||||
- Device code generation (generate_device_code)
|
||||
- User achievement unlock logic
|
||||
- Card batch generation logic
|
||||
|
||||
## Recommendations for Test Implementation
|
||||
|
||||
**Phase 1 - Setup (High Priority):**
|
||||
- Add pytest + pytest-django + pytest-asyncio to requirements.txt
|
||||
- Create conftest.py for shared fixtures
|
||||
- Create factory definitions (factory_boy) for common models
|
||||
- Mock external services (responses, unittest.mock)
|
||||
|
||||
**Phase 2 - Unit Tests (High Priority):**
|
||||
- Test authentication (token generation, validation, expiry)
|
||||
- Test device binding semantics (last-bind-wins)
|
||||
- Test serializer validation
|
||||
- Test model methods (AffinitySetting.get_solo, generate_device_code)
|
||||
|
||||
**Phase 3 - Integration Tests (Medium Priority):**
|
||||
- Test full auth flows (MAC login, phone login, registration)
|
||||
- Test API endpoints with mocked external services
|
||||
- Test WebSocket consumer connection and messaging
|
||||
- Test card batch generation workflow
|
||||
|
||||
**Phase 4 - E2E Tests (Lower Priority):**
|
||||
- Test device binding → WebSocket connection → message exchange flow
|
||||
- Test achievement unlock on user action flow
|
||||
- Test affinity rule trigger and log creation
|
||||
|
||||
**Minimum Coverage Target for Production:**
|
||||
- Authentication/authorization: 80%+
|
||||
- Device control logic: 100% (critical for device binding semantics)
|
||||
- Serializer validation: 70%+
|
||||
- API endpoints: 60%+ (focus on public-facing, critical flows)
|
||||
|
||||
---
|
||||
|
||||
*Testing analysis: 2026-05-07*
|
||||
|
||||
## Test Execution Example
|
||||
|
||||
Current working test (from `aiapp/tests.py`):
|
||||
```bash
|
||||
$ python manage.py test aiapp.tests.YourModelTest.test_audio
|
||||
```
|
||||
|
||||
This test:
|
||||
1. Gets AudioService instance
|
||||
2. Calls synthesize_speech with Chinese text
|
||||
3. Returns (no assertions — test just checks for uncaught exceptions)
|
||||
|
||||
This represents the ONLY active test in the codebase and does not verify behavior, only that the call completes.
|
||||
Loading…
x
Reference in New Issue
Block a user