feat(feedback): implement user feedback collection system with email reporting

Backend:
- Add user_feedback table with RLS policy
- Create feedback submission API (multipart/form-data)
- Implement xlsx report generation with embedded images
- Add scheduled email delivery via Feishu SMTP
- Create HTML email templates (daily_report, no_feedback)

Frontend:
- Add feedback screen with type selection and image picker
- Support anonymous submission via skipAuth flag
- Collect device info and app version

Protocol:
- Document feedback API contract and error codes
- Update http-error-codes.md with FEEDBACK_* codes
This commit is contained in:
qzl
2026-04-20 12:49:54 +08:00
parent 913ed26f8d
commit 6a2a9d2c87
46 changed files with 4768 additions and 9 deletions
@@ -73,16 +73,16 @@ class HomeScreen extends StatefulWidget {
class _HomeScreenState extends State<HomeScreen> {
MainTab _currentTab = MainTab.home;
late final InviteRepository _inviteRepository;
late final ApiClient _apiClient;
@override
void initState() {
super.initState();
final inviteApi = InviteApi(
apiClient: ApiClient(
baseUrl: appDependencies.backendUrl,
tokenProvider: widget.sessionStore.getToken,
),
_apiClient = ApiClient(
baseUrl: appDependencies.backendUrl,
tokenProvider: widget.sessionStore.getToken,
);
final inviteApi = InviteApi(apiClient: _apiClient);
_inviteRepository = InviteRepositoryImpl(inviteApi: inviteApi);
WidgetsBinding.instance.addPostFrameCallback((_) {
_tryShowWelcomeDialog();
@@ -135,6 +135,7 @@ class _HomeScreenState extends State<HomeScreen> {
settings: widget.profileSettings,
coinBalance: widget.coinBalance,
inviteRepository: _inviteRepository,
apiClient: _apiClient,
onLocaleChanged: widget.onLocaleChanged,
onSettingsChanged: widget.onProfileSettingsChanged,
onSaveProfile: widget.onSaveProfile,
@@ -563,6 +564,7 @@ class _ProfileTab extends StatelessWidget {
required this.settings,
required this.coinBalance,
required this.inviteRepository,
required this.apiClient,
required this.onLocaleChanged,
required this.onSettingsChanged,
required this.onSaveProfile,
@@ -575,6 +577,7 @@ class _ProfileTab extends StatelessWidget {
final ProfileSettingsV1 settings;
final int coinBalance;
final InviteRepository inviteRepository;
final ApiClient apiClient;
final Future<void> Function(String languageTag) onLocaleChanged;
final Future<void> Function(ProfileSettingsV1 settings) onSettingsChanged;
final Future<ProfileSettingsV1> Function(ProfileSettingsV1 updated)
@@ -590,6 +593,7 @@ class _ProfileTab extends StatelessWidget {
settings: settings,
coinBalance: coinBalance,
inviteRepository: inviteRepository,
apiClient: apiClient,
onInterfaceLanguageChanged: onLocaleChanged,
onSettingsChanged: onSettingsChanged,
onSaveProfile: onSaveProfile,