fix: import unpublished exam rooms and add opt-in school HTML feedback

This commit is contained in:
Codex
2026-10-16 16:38:00 +08:00
parent defe7fd50e
commit 437ed14dbe
31 changed files with 969 additions and 39 deletions
@@ -120,10 +120,11 @@ class HttpFeedbackSender implements FeedbackSender {
throw const FeedbackException('服务器不支持此附件的格式或内容,请移除附件,或换用正常的图片、视频后提交');
}
if (response.statusCode == 422 && errorCode == 'validation_error') {
if (submission.diagnosticLog != null &&
if ((submission.diagnosticLog != null ||
submission.diagnosticHtml != null) &&
result?['detail'] == 'Unexpected file field.') {
throw const FeedbackException(
'反馈服务器尚未更新日志附件协议,请先更新服务器,或取消附带日志后提交。填写内容已保留。',
'反馈服务器尚未更新日志或网页附件协议,请先更新服务器,或取消附带排查附件后提交。填写内容已保留。',
);
}
throw const FeedbackException('填写内容未通过服务器校验,请检查必填内容、长度及附件后重新提交');
@@ -206,6 +207,14 @@ class HttpFeedbackSender implements FeedbackSender {
bytes.add(submission.diagnosticLog!);
write('\r\n');
}
if (submission.diagnosticHtml != null) {
write(
'--$boundary\r\nContent-Disposition: form-data; name="diagnostic_html"; filename="TPSchedule-school-page.html"\r\n'
'Content-Type: text/html; charset=utf-8\r\n\r\n',
);
bytes.add(submission.diagnosticHtml!);
write('\r\n');
}
write('--$boundary--\r\n');
return bytes.takeBytes();
}
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import '../../app/app_theme.dart';
import '../../services/diagnostic_log.dart';
import '../../services/school_page_diagnostic.dart';
import 'feedback_submission.dart';
typedef FeedbackAttachmentPicker = Future<FeedbackAttachment?> Function();
@@ -34,6 +35,7 @@ class _FeedbackPageState extends State<FeedbackPage> {
var _success = false;
var _uncertain = false;
var _includeLogs = false;
var _includeHtml = false;
String? _error;
FeedbackSubmission? _pendingSubmission;
@@ -107,6 +109,20 @@ class _FeedbackPageState extends State<FeedbackPage> {
FocusScope.of(context).unfocus();
setState(() => _submitting = true);
try {
final html =
_pendingSubmission == null &&
_kind == FeedbackKind.problem &&
_includeHtml
? SchoolPageDiagnostic.instance.export()
: null;
if (_pendingSubmission == null &&
_kind == FeedbackKind.problem &&
_includeHtml &&
html == null) {
throw const FeedbackException(
'最近10分钟没有可用的本科导入页面。请返回学校页面再尝试导入,然后提交反馈;也可以取消附带HTML。',
);
}
_pendingSubmission ??= _kind == FeedbackKind.problem
? FeedbackSubmission.problem(
id: FeedbackSubmission.newId(),
@@ -117,6 +133,7 @@ class _FeedbackPageState extends State<FeedbackPage> {
diagnosticLog: _includeLogs
? await DiagnosticLog.instance.export()
: null,
diagnosticHtml: html,
)
: FeedbackSubmission.development(
id: FeedbackSubmission.newId(),
@@ -128,6 +145,9 @@ class _FeedbackPageState extends State<FeedbackPage> {
_error = null;
});
await widget.sender.send(_pendingSubmission!);
if (_pendingSubmission?.diagnosticHtml != null) {
SchoolPageDiagnostic.instance.clear();
}
if (mounted) setState(() => _success = true);
} on FeedbackDeliveryUncertain catch (error) {
if (mounted) {
@@ -275,6 +295,58 @@ class _FeedbackPageState extends State<FeedbackPage> {
: (value) {
setState(() {
_includeLogs = value ?? false;
if (!_includeLogs) _includeHtml = false;
_pendingSubmission = null;
_error = null;
});
},
),
CheckboxListTile(
key: const Key('feedback-html'),
contentPadding: EdgeInsets.zero,
title: const Text('附带最近一次本科导入页面HTML'),
subtitle: const Text(
'需先勾选日志。网页可能含姓名、学号、课程、教师和考场信息,仅保留最近10分钟,单独作为邮件附件发送。',
),
value: _includeHtml,
onChanged: _submitting || !_includeLogs
? null
: (value) async {
var accepted = value ?? false;
if (accepted) {
accepted =
await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('附带网页排查资料?'),
content: const Text(
'附件可能包含你的姓名、学号、课程及考试信息,将发送给开发者用于排查导入问题。密码、验证码、Cookie和认证参数不会包含在附件中。',
),
actions: [
TextButton(
onPressed: () =>
Navigator.pop(
context,
false,
),
child: const Text('取消'),
),
FilledButton(
onPressed: () =>
Navigator.pop(
context,
true,
),
child: const Text('同意附带'),
),
],
),
) ??
false;
}
if (!mounted) return;
setState(() {
_includeHtml = accepted;
_pendingSubmission = null;
_error = null;
});
@@ -103,6 +103,7 @@ class FeedbackSubmission {
this.school,
this.attachments = const [],
this.diagnosticLog,
this.diagnosticHtml,
});
factory FeedbackSubmission.problem({
@@ -112,6 +113,7 @@ class FeedbackSubmission {
List<FeedbackAttachment> attachments = const [],
FeedbackLimits limits = FeedbackLimits.defaults,
Uint8List? diagnosticLog,
Uint8List? diagnosticHtml,
}) {
final text = message.trim();
if (text.isEmpty) throw const FeedbackException('请填写问题或建议');
@@ -122,6 +124,10 @@ class FeedbackSubmission {
(diagnosticLog.isEmpty || diagnosticLog.length > 256 * 1024)) {
throw const FeedbackException('日志附件不能超过256 KB');
}
if (diagnosticHtml != null &&
(diagnosticHtml.isEmpty || diagnosticHtml.length > 2 * 1024 * 1024)) {
throw const FeedbackException('网页排查附件不能超过2 MB');
}
return FeedbackSubmission._(
id: id,
kind: FeedbackKind.problem,
@@ -131,6 +137,9 @@ class FeedbackSubmission {
diagnosticLog: diagnosticLog == null
? null
: Uint8List.fromList(diagnosticLog).asUnmodifiableView(),
diagnosticHtml: diagnosticHtml == null
? null
: Uint8List.fromList(diagnosticHtml).asUnmodifiableView(),
);
}
@@ -161,6 +170,7 @@ class FeedbackSubmission {
final String? school;
final List<FeedbackAttachment> attachments;
final Uint8List? diagnosticLog;
final Uint8List? diagnosticHtml;
static String newId() {
final random = Random.secure();