fix: harden school imports and add optional feedback diagnostics

This commit is contained in:
Codex
2026-10-15 10:37:32 +08:00
parent 674df1ceb3
commit 7299f4d8b6
25 changed files with 722 additions and 42 deletions
@@ -192,6 +192,14 @@ class HttpFeedbackSender implements FeedbackSender {
bytes.add(attachment.bytes);
write('\r\n');
}
if (submission.diagnosticLog != null) {
write(
'--$boundary\r\nContent-Disposition: form-data; name="diagnostics"; filename="TPSchedule-diagnostics.txt"\r\n'
'Content-Type: text/plain; charset=utf-8\r\n\r\n',
);
bytes.add(submission.diagnosticLog!);
write('\r\n');
}
write('--$boundary--\r\n');
return bytes.takeBytes();
}
@@ -2,6 +2,7 @@ import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import '../../app/app_theme.dart';
import '../../services/diagnostic_log.dart';
import 'feedback_submission.dart';
typedef FeedbackAttachmentPicker = Future<FeedbackAttachment?> Function();
@@ -32,6 +33,7 @@ class _FeedbackPageState extends State<FeedbackPage> {
var _picking = false;
var _success = false;
var _uncertain = false;
var _includeLogs = false;
String? _error;
FeedbackSubmission? _pendingSubmission;
@@ -103,6 +105,7 @@ class _FeedbackPageState extends State<FeedbackPage> {
if (_submitting || _picking) return;
if (!_uncertain && !(_form.currentState?.validate() ?? false)) return;
FocusScope.of(context).unfocus();
setState(() => _submitting = true);
try {
_pendingSubmission ??= _kind == FeedbackKind.problem
? FeedbackSubmission.problem(
@@ -111,6 +114,9 @@ class _FeedbackPageState extends State<FeedbackPage> {
contact: _contact.text,
attachments: _attachments,
limits: widget.limits,
diagnosticLog: _includeLogs
? await DiagnosticLog.instance.export()
: null,
)
: FeedbackSubmission.development(
id: FeedbackSubmission.newId(),
@@ -256,6 +262,24 @@ class _FeedbackPageState extends State<FeedbackPage> {
: null,
),
if (_kind == FeedbackKind.problem) ...[
CheckboxListTile(
key: const Key('feedback-logs'),
contentPadding: EdgeInsets.zero,
title: const Text('附带近10分钟诊断日志'),
subtitle: const Text(
'包含登录跳转、课表读取及错误状态,帮助排查问题;不包含密码、验证码或课表内容。',
),
value: _includeLogs,
onChanged: _submitting
? null
: (value) {
setState(() {
_includeLogs = value ?? false;
_pendingSubmission = null;
_error = null;
});
},
),
const SizedBox(height: 10),
const Text(
'图片或视频(选填)',
@@ -102,6 +102,7 @@ class FeedbackSubmission {
this.contact,
this.school,
this.attachments = const [],
this.diagnosticLog,
});
factory FeedbackSubmission.problem({
@@ -110,18 +111,26 @@ class FeedbackSubmission {
String? contact,
List<FeedbackAttachment> attachments = const [],
FeedbackLimits limits = FeedbackLimits.defaults,
Uint8List? diagnosticLog,
}) {
final text = message.trim();
if (text.isEmpty) throw const FeedbackException('请填写问题或建议');
if (text.length > 5000) throw const FeedbackException('问题或建议不能超过 5000 个字符');
_validateContact(contact);
validateAttachments(attachments, limits);
if (diagnosticLog != null &&
(diagnosticLog.isEmpty || diagnosticLog.length > 256 * 1024)) {
throw const FeedbackException('日志附件不能超过256 KB');
}
return FeedbackSubmission._(
id: id,
kind: FeedbackKind.problem,
message: text,
contact: _optional(contact),
attachments: List.unmodifiable(attachments),
diagnosticLog: diagnosticLog == null
? null
: Uint8List.fromList(diagnosticLog).asUnmodifiableView(),
);
}
@@ -151,6 +160,7 @@ class FeedbackSubmission {
final String? contact;
final String? school;
final List<FeedbackAttachment> attachments;
final Uint8List? diagnosticLog;
static String newId() {
final random = Random.secure();