diff --git a/lib/core/network/api_client.dart b/lib/core/network/api_client.dart index 8960dac..891a409 100644 --- a/lib/core/network/api_client.dart +++ b/lib/core/network/api_client.dart @@ -113,6 +113,7 @@ class ApiClient { Future saveJudgeScoreDraft({ required String sessionEntryId, required List items, + double? deductionCoefficient, }) async { final data = await _requestMap( 'POST', @@ -120,6 +121,8 @@ class ApiClient { data: { 'sessionEntryId': sessionEntryId, 'items': items.map((item) => item.toJson()).toList(), + if (deductionCoefficient != null) + 'deductionCoefficient': deductionCoefficient, }, ); return JudgeScorePreview.fromJson(data); @@ -130,6 +133,7 @@ class ApiClient { required List items, required String idempotencyKey, String? disqualifyRemark, + double? deductionCoefficient, }) async { final data = await _requestMap( 'POST', @@ -138,6 +142,8 @@ class ApiClient { 'sessionEntryId': sessionEntryId, 'items': items.map((item) => item.toJson()).toList(), 'idempotencyKey': idempotencyKey, + if (deductionCoefficient != null) + 'deductionCoefficient': deductionCoefficient, if (disqualifyRemark?.trim().isNotEmpty == true) 'disqualifyRemark': disqualifyRemark!.trim(), }, diff --git a/lib/core/network/judge_api_repository.dart b/lib/core/network/judge_api_repository.dart index 195544a..588b323 100644 --- a/lib/core/network/judge_api_repository.dart +++ b/lib/core/network/judge_api_repository.dart @@ -70,19 +70,25 @@ class JudgeApiRepository { Future saveDraft({ required String sessionEntryId, required List items, - }) => - _client.saveJudgeScoreDraft(sessionEntryId: sessionEntryId, items: items); + double? deductionCoefficient, + }) => _client.saveJudgeScoreDraft( + sessionEntryId: sessionEntryId, + items: items, + deductionCoefficient: deductionCoefficient, + ); Future submitScore({ required String sessionEntryId, required List items, required String idempotencyKey, String? disqualifyRemark, + double? deductionCoefficient, }) => _client.submitJudgeScore( sessionEntryId: sessionEntryId, items: items, idempotencyKey: idempotencyKey, disqualifyRemark: disqualifyRemark, + deductionCoefficient: deductionCoefficient, ); Future>> loadResults(String sessionId) async { diff --git a/lib/core/network/score_sync_repository.dart b/lib/core/network/score_sync_repository.dart index ba96682..d73a257 100644 --- a/lib/core/network/score_sync_repository.dart +++ b/lib/core/network/score_sync_repository.dart @@ -43,6 +43,7 @@ class QueuedScoreRequest { required this.items, this.idempotencyKey, this.disqualifyRemark, + this.deductionCoefficient, }); final String sessionEntryId; @@ -50,26 +51,36 @@ class QueuedScoreRequest { final String? idempotencyKey; final String? disqualifyRemark; + /// 主裁花样赛各扣分项合计后的扣分系数,例如 0.125。 + /// + /// 页面展示的是得分系数 M = 1 - deductionCoefficient;服务端仍会 + /// 根据各项明细重算并校验该值,客户端不上传最终成绩。 + final double? deductionCoefficient; + Map toJson() => { 'sessionEntryId': sessionEntryId, 'items': items.map((item) => item.toJson()).toList(), 'idempotencyKey': idempotencyKey, 'disqualifyRemark': disqualifyRemark, + if (deductionCoefficient != null) + 'deductionCoefficient': deductionCoefficient, }; - factory QueuedScoreRequest.fromJson(Map json) => - QueuedScoreRequest( - sessionEntryId: '${json['sessionEntryId'] ?? ''}', - items: (json['items'] as List? ?? const []) - .map( - (item) => JudgeScoreItemInput.fromJson( - (item as Map).cast(), - ), - ) - .toList(growable: false), - idempotencyKey: json['idempotencyKey']?.toString(), - disqualifyRemark: json['disqualifyRemark']?.toString(), - ); + factory QueuedScoreRequest.fromJson( + Map json, + ) => QueuedScoreRequest( + sessionEntryId: '${json['sessionEntryId'] ?? ''}', + items: (json['items'] as List? ?? const []) + .map( + (item) => JudgeScoreItemInput.fromJson( + (item as Map).cast(), + ), + ) + .toList(growable: false), + idempotencyKey: json['idempotencyKey']?.toString(), + disqualifyRemark: json['disqualifyRemark']?.toString(), + deductionCoefficient: (json['deductionCoefficient'] as num?)?.toDouble(), + ); String encode() => jsonEncode(toJson()); diff --git a/lib/modules/scoring/controllers/scoring_controller.dart b/lib/modules/scoring/controllers/scoring_controller.dart index 5dd1e5d..a943897 100644 --- a/lib/modules/scoring/controllers/scoring_controller.dart +++ b/lib/modules/scoring/controllers/scoring_controller.dart @@ -1166,6 +1166,7 @@ class ScoringController extends GetxController { final preview = await _judgeApi.saveDraft( sessionEntryId: request.sessionEntryId, items: request.items, + deductionCoefficient: request.deductionCoefficient, ); serverPreview.value = preview; await _cacheDraftSnapshot(request, preview); @@ -1317,6 +1318,7 @@ class ScoringController extends GetxController { items: request.items, idempotencyKey: idempotencyKey, disqualifyRemark: request.disqualifyRemark, + deductionCoefficient: request.deductionCoefficient, ); await _completeRemoteSubmission(operation, output); return true; @@ -1489,9 +1491,45 @@ class ScoringController extends GetxController { items: items, idempotencyKey: sheet.idempotencyKey, disqualifyRemark: disqualifyRemark, + deductionCoefficient: + profile.role == ScoreRole.technical && + profile.competition != CompetitionKind.prescribed + ? _remoteDeductionCoefficient(items) + : null, ); } + /// 使用评分表下发的单位值和递进扣分序列计算上传系数,避免本地规则目录 + /// 与服务端规则版本不一致。服务端收到后会根据同一批明细再次计算并校验。 + double _remoteDeductionCoefficient(List inputs) { + final sheet = apiScoreSheet; + if (sheet == null) return _deductionValue(); + final values = {for (final input in inputs) input.itemCode: input}; + var coefficient = 0.0; + for (final item in sheet.items.where( + (item) => + !item.isAutoGenerated && + !item.isDisqualifyReason && + (item.itemType == 4 || item.scoreDimension == 4), + )) { + final input = values[item.code]; + final count = (input?.countValue ?? 0).round(); + if (item.stepDeductions.isNotEmpty) { + for (var index = 0; index < count; index++) { + final stepIndex = index < item.stepDeductions.length + ? index + : item.stepDeductions.length - 1; + coefficient += item.stepDeductions[stepIndex]; + } + } else if (item.inputMode == 'count') { + coefficient += count * item.unitValue; + } else { + coefficient += input?.inputValue ?? 0; + } + } + return coefficient.clamp(0, 1).toDouble(); + } + num _remoteValueFor(JudgeScoreItem item) { if (item.isDisqualifyReason) { return disqualified.value && selectedDisqualifyReasonCode == item.code diff --git a/lib/modules/scoring/views/scoring_page.dart b/lib/modules/scoring/views/scoring_page.dart index bdd65d3..21fc8fc 100644 --- a/lib/modules/scoring/views/scoring_page.dart +++ b/lib/modules/scoring/views/scoring_page.dart @@ -1585,7 +1585,7 @@ class _ChiefRequiredDeductionPanel extends StatelessWidget { children: [ _ChiefDeductionSummary( value: factor.toStringAsFixed(3), - detail: '当前扣分系数', + detail: '当前得分系数', compact: true, ), Positioned( @@ -1663,7 +1663,7 @@ class _ChiefDeductionPanel extends StatelessWidget { children: [ _ChiefDeductionSummary( value: summaryValue, - detail: prescribed ? '当前累计扣分(分)' : '当前扣分系数', + detail: prescribed ? '当前累计扣分(分)' : '当前得分系数', ), Positioned( right: 26, diff --git a/test/score_sync_request_test.dart b/test/score_sync_request_test.dart index 0251fc9..00697a8 100644 --- a/test/score_sync_request_test.dart +++ b/test/score_sync_request_test.dart @@ -8,6 +8,7 @@ void main() { sessionEntryId: '201', idempotencyKey: 'judge-201-v1', disqualifyRemark: '现场记录', + deductionCoefficient: .125, items: [ JudgeScoreItemInput(itemCode: 'MUSIC_PLUS', countValue: 2), JudgeScoreItemInput(itemCode: 'MUSIC_MINUS', countValue: 1), @@ -20,6 +21,8 @@ void main() { expect(restored.sessionEntryId, original.sessionEntryId); expect(restored.idempotencyKey, original.idempotencyKey); expect(restored.disqualifyRemark, original.disqualifyRemark); + expect(restored.deductionCoefficient, .125); + expect(restored.toJson()['deductionCoefficient'], .125); expect(restored.items.map((item) => item.toJson()), [ {'itemCode': 'MUSIC_PLUS', 'countValue': 2.0}, {'itemCode': 'MUSIC_MINUS', 'countValue': 1.0},