之前公告(QuotaConfig.announcement,v0.12.6)与 v0.20.1 消息中心 UX 重叠 —
两个铃铛 + 两套未读 + 两套入口让用户分不清。整合到统一 Notification 表:
后端:
- apps.notifications Notification.TYPE_CHOICES 加 'announcement'
- 新 endpoint POST /api/v1/admin/announcement/publish (IsSuperAdmin)
- body { content: HTML 字符串 }
- 空内容 400 "公告内容不能为空"
- User.objects.all() (含 is_active=False 封禁用户,解封后能看到历史)
- bulk_create(batch_size=500) 防大团队 OOM
- 同步把 content 存档到 QuotaConfig.announcement 作为下次编辑器初始值
- audit log: settings_update, target=announcement
- 重写 GET /announcement 内部查 Notification 表最新未读
- 重写 POST /announcement/read 标记所有未读公告已读
- endpoint 签名不变保持老前端兼容(返回结构相同)
前端:
- App.tsx 顶层挂 <GlobalAnnouncementGate /> — 任意路由有未读公告强弹 modal
必须看(关闭遮罩点击也算关闭→标已读),关闭后 60s 内不再弹
- AnnouncementModal 改成纯展示组件: props { content, onClose },不自己 fetch
HTML 内容用 DOMPurify.sanitize 防 XSS
- 删 VideoGenerationPage 右上角小喇叭 + 旧 AnnouncementModal 自动弹 + 重看路径
(用户重看走 sidebar 大铃铛 → 消息中心)
- SettingsPage 公告区:
- 删 announcement_enabled checkbox(不再有"启用/停用"概念,发了就强弹)
- "保存公告"按钮 → 改 "发送公告" 按钮
- 二次 confirm "确认发送给所有用户?发送后所有人打开页面会强制看到这条公告,无法撤回"
- 调 announcementApi.publish (POST /admin/announcement/publish)
- NotificationsPage 每条加 type chip([公告]/[安全]/[额度]/[系统]) 4 色
announcement type 用 DOMPurify + dangerouslySetInnerHTML 渲染(其他 type 纯文本)
- types/index.ts NotificationType 加 'announcement'
验证:
- 后端 curl 全过:发空 400 / 发 HTML 200 sent_to=21 / tudou GET 拿到未读 / read 后 GET 拿空
- typecheck 0 error
- v0.20.1-smoke 11/11 + modal-interaction 8/8 + v2-smoke 25/25
- vitest 71 fail / 162 pass 与基线 0 新增回归
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
88 lines
5.8 KiB
Python
88 lines
5.8 KiB
Python
from django.urls import path
|
|
from . import views
|
|
|
|
urlpatterns = [
|
|
# Media upload
|
|
path('media/upload', views.upload_media_view, name='media_upload'),
|
|
# Video generation
|
|
path('video/generate', views.video_generate_view, name='video_generate'),
|
|
path('video/tasks', views.video_tasks_list_view, name='video_tasks_list'),
|
|
path('video/tasks/<uuid:task_id>', views.video_task_detail_view, name='video_task_detail'),
|
|
path('video/tasks/<uuid:task_id>/favorite', views.video_task_toggle_favorite_view, name='video_task_toggle_favorite'),
|
|
# Public announcement
|
|
path('announcement', views.announcement_view, name='announcement'),
|
|
path('announcement/read', views.announcement_read_view, name='announcement_read'),
|
|
# Admin publish announcement (fan-out to all users)
|
|
path('admin/announcement/publish', views.admin_publish_announcement_view, name='admin_announcement_publish'),
|
|
|
|
# ── Super Admin: Dashboard ──
|
|
path('admin/stats', views.admin_stats_view, name='admin_stats'),
|
|
|
|
# ── Super Admin: Team management ──
|
|
path('admin/teams', views.admin_teams_list_view, name='admin_teams_list'),
|
|
path('admin/teams/create', views.admin_team_create_view, name='admin_team_create'),
|
|
path('admin/teams/<int:team_id>', views.admin_team_detail_view, name='admin_team_detail'),
|
|
path('admin/teams/<int:team_id>/topup', views.admin_team_topup_view, name='admin_team_topup'),
|
|
path('admin/teams/<int:team_id>/set-pool', views.admin_team_set_pool_view, name='admin_team_set_pool'),
|
|
path('admin/teams/<int:team_id>/admin', views.admin_team_create_admin_view, name='admin_team_create_admin'),
|
|
path('admin/teams/<int:team_id>/members/<int:member_id>/role', views.admin_team_member_role_view, name='admin_team_member_role'),
|
|
|
|
# ── Super Admin: User management ──
|
|
path('admin/users', views.admin_users_list_view, name='admin_users_list'),
|
|
path('admin/users/create', views.admin_create_user_view, name='admin_create_user'),
|
|
path('admin/users/<int:user_id>', views.admin_user_detail_view, name='admin_user_detail'),
|
|
path('admin/users/<int:user_id>/quota', views.admin_user_quota_view, name='admin_user_quota'),
|
|
path('admin/users/<int:user_id>/status', views.admin_user_status_view, name='admin_user_status'),
|
|
path('admin/users/<int:user_id>/reset-password', views.admin_reset_password_view, name='admin_reset_password'),
|
|
|
|
# ── Super Admin: Records, Settings & Audit Logs ──
|
|
path('admin/records', views.admin_records_view, name='admin_records'),
|
|
path('admin/settings', views.admin_settings_view, name='admin_settings'),
|
|
path('admin/logs', views.admin_audit_logs_view, name='admin_audit_logs'),
|
|
|
|
# ── Super Admin: Login Records ──
|
|
path('admin/login-records', views.admin_login_records_view, name='admin_login_records'),
|
|
|
|
# ── Super Admin: Anomaly Detection ──
|
|
path('admin/anomalies', views.admin_login_anomalies_view, name='admin_login_anomalies'),
|
|
path('admin/test-feishu', views.admin_test_feishu_view, name='admin_test_feishu'),
|
|
path('admin/test-sms', views.admin_test_sms_view, name='admin_test_sms'),
|
|
path('admin/teams/<int:team_id>/auto-learn', views.admin_team_auto_learn_view, name='admin_team_auto_learn'),
|
|
path('admin/teams/<int:team_id>/apply-learned-regions', views.admin_team_apply_learned_regions_view, name='admin_team_apply_learned_regions'),
|
|
|
|
# ── Super Admin: Content Assets ──
|
|
path('admin/assets/overview', views.admin_assets_overview, name='admin_assets_overview'),
|
|
path('admin/assets/team/<int:team_id>/members', views.admin_assets_team_members, name='admin_assets_team_members'),
|
|
path('admin/assets/user/<int:user_id>/videos', views.admin_assets_user_videos, name='admin_assets_user_videos'),
|
|
|
|
# ── Team Admin: Team management ──
|
|
path('team/info', views.team_info_view, name='team_info'),
|
|
path('team/stats', views.team_stats_view, name='team_stats'),
|
|
path('team/members', views.team_members_list_view, name='team_members_list'),
|
|
path('team/members/create', views.team_member_create_view, name='team_member_create'),
|
|
path('team/members/<int:member_id>', views.team_member_detail_view, name='team_member_detail'),
|
|
path('team/members/<int:member_id>/quota', views.team_member_quota_view, name='team_member_quota'),
|
|
path('team/members/<int:member_id>/status', views.team_member_status_view, name='team_member_status'),
|
|
path('team/members/<int:member_id>/role', views.team_member_role_view, name='team_member_role'),
|
|
path('team/members/<int:member_id>/reset-password', views.team_reset_member_password_view, name='team_reset_member_password'),
|
|
|
|
# ── Team Admin: Consumption Records ──
|
|
path('team/records', views.team_records_view, name='team_records'),
|
|
|
|
# ── Team Admin: Content Assets ──
|
|
path('team/assets/overview', views.team_assets_overview, name='team_assets_overview'),
|
|
path('team/assets/member/<int:member_id>/videos', views.team_assets_member_videos, name='team_assets_member_videos'),
|
|
|
|
# ── Profile: User's own data ──
|
|
path('profile/overview', views.profile_overview_view, name='profile_overview'),
|
|
path('profile/records', views.profile_records_view, name='profile_records'),
|
|
|
|
# ── Assets API (Virtual Avatar Library) ──
|
|
path('assets/groups', views.asset_groups_view, name='asset_groups'),
|
|
path('assets/groups/<int:group_id>', views.asset_group_detail_view, name='asset_group_detail'),
|
|
path('assets/groups/<int:group_id>/assets', views.asset_group_add_asset_view, name='asset_group_add_asset'),
|
|
path('assets/<int:asset_id>', views.asset_update_view, name='asset_update'),
|
|
path('assets/<int:asset_id>/status', views.asset_poll_status_view, name='asset_poll_status'),
|
|
path('assets/search', views.asset_search_view, name='asset_search'),
|
|
]
|