feat: 实现站内通知系统

- 后端: 新增 notifications/user_notifications 表迁移及 ORM 模型
- 后端: 实现 schema/repository/service/router 全套通知 API
  - GET /api/v1/notifications (列表+游标分页)
  - GET /api/v1/notifications/unread-count
  - PATCH /api/v1/notifications/{id}/read (幂等)
  - PATCH /api/v1/notifications/mark-all-read (幂等)
- 后端: payload 使用 Pydantic discriminated union (none/open_route/open_url)
- 后端: 19 个单元测试全部通过
- Flutter: 通知 feature 完整实现 (models/apis/repositories/bloc/UI)
- Flutter: Home 页通知按钮接入真实页面,显示未读 badge
- Flutter: 14 个测试全部通过
- 协议文档: notification-inbox-protocol.md 及错误码注册
This commit is contained in:
qzl
2026-04-10 18:50:08 +08:00
parent 17ef460391
commit 3f3d613d99
28 changed files with 3481 additions and 651 deletions
+15
View File
@@ -15,6 +15,9 @@ import '../features/auth/presentation/screens/login_screen.dart';
import '../features/divination/data/apis/divination_api.dart';
import '../features/divination/data/models/divination_result.dart';
import '../features/home/presentation/screens/home_screen.dart';
import '../features/notifications/data/apis/notification_api.dart';
import '../features/notifications/data/repositories/notification_repository.dart';
import '../features/notifications/presentation/bloc/notification_bloc.dart';
import '../features/settings/data/apis/profile_api.dart';
import '../features/settings/data/models/profile_settings.dart';
import '../l10n/app_localizations.dart';
@@ -35,6 +38,9 @@ class _EryaoAppState extends State<EryaoApp> {
late final AuthBloc _authBloc;
late final DivinationApi _divinationApi;
late final ProfileApi _profileApi;
late final NotificationApi _notificationApi;
late final NotificationRepository _notificationRepository;
late final NotificationBloc _notificationBloc;
Locale _locale = const Locale('zh');
ProfileSettingsV1 _profileSettings = ProfileSettingsV1.defaultsForLocale(
const Locale('zh'),
@@ -61,6 +67,11 @@ class _EryaoAppState extends State<EryaoApp> {
final authApi = AuthApi(apiClient: apiClient);
_divinationApi = DivinationApi(apiClient: apiClient);
_profileApi = ProfileApi(apiClient: apiClient);
_notificationApi = NotificationApi(apiClient: apiClient);
_notificationRepository = NotificationRepositoryImpl(
notificationApi: _notificationApi,
);
_notificationBloc = NotificationBloc(repository: _notificationRepository);
final authRepository = AuthRepositoryImpl(
authApi: authApi,
sessionStore: _sessionStore,
@@ -347,6 +358,7 @@ class _EryaoAppState extends State<EryaoApp> {
@override
void dispose() {
_authBloc.dispose();
_notificationBloc.dispose();
super.dispose();
}
@@ -415,6 +427,7 @@ class _EryaoAppState extends State<EryaoApp> {
_ensureCreditsLoaded(state.user!.email);
_ensureHistoryLoaded(state.user!.email);
_refreshProfile(userEmail: state.user!.email);
_notificationBloc.handleEvent(RefreshUnreadCount());
return HomeScreen(
account: state.user!.email,
sessionStore: _sessionStore,
@@ -423,6 +436,8 @@ class _EryaoAppState extends State<EryaoApp> {
historyRecords: _historyRecords,
coinBalance: _creditsBalance,
divinationApi: _divinationApi,
notificationBloc: _notificationBloc,
notificationRepository: _notificationRepository,
onLocaleChanged: _handleInterfaceLanguageChanged,
onProfileSettingsChanged: _saveProfileSettings,
onSaveProfile: _saveProfile,