From b32c9c5aad349708c391c3f42157b8f150ab106b Mon Sep 17 00:00:00 2001 From: Misaka_Company Date: Wed, 13 May 2026 12:43:22 +0800 Subject: [PATCH] feat: handle off-shelf registration results --- lib/pages/registration_page.dart | 50 +++++++++++++++--- lib/services/api_service.dart | 32 +++++++++++- test/services/api_service_test.dart | 81 +++++++++++++++++++++++++---- 3 files changed, 145 insertions(+), 18 deletions(-) diff --git a/lib/pages/registration_page.dart b/lib/pages/registration_page.dart index bf98b02..20e351f 100644 --- a/lib/pages/registration_page.dart +++ b/lib/pages/registration_page.dart @@ -49,8 +49,7 @@ class _RegistrationPageState extends State { } void _onKeyEvent(KeyEvent event) { - if (event is KeyDownEvent && - event.logicalKey == LogicalKeyboardKey.enter) { + if (event is KeyDownEvent && event.logicalKey == LogicalKeyboardKey.enter) { _submit(); } } @@ -195,11 +194,24 @@ class _RegistrationPageState extends State { _locationType = null; } }); - final msg = _isLocked ? '货位已锁定,请扫描下一张执行卡' : '上架成功'; - _showStatusOverride(msg, StatusDotColor.green, const Duration(milliseconds: 1500)); + final msg = result.isOffShelfSuccess + ? '下架成功' + : (_isLocked ? '货位已锁定,请扫描下一张执行卡' : '上架成功'); + _showStatusOverride( + msg, + StatusDotColor.green, + const Duration(milliseconds: 1500), + ); } else if (result.isDuplicate) { _feedbackService.trigger(FeedbackEvent.duplicateError); _showDuplicateDialog(zongpaiNo, result.duplicateInfo); + } else if (result.isAlreadyOffShelf) { + _feedbackService.trigger(FeedbackEvent.submitFailure); + _showStatusOverride( + result.errorMessage ?? '该总排号已下架至转运区域,不可重新上架', + StatusDotColor.red, + const Duration(seconds: 2), + ); } else { final isNetwork = result.errorMessage == '网络异常,请检查网络连接'; _feedbackService.trigger( @@ -215,6 +227,7 @@ class _RegistrationPageState extends State { Future _submitBatch(String baseUrl) async { int successCount = 0; + int offShelfCount = 0; final failed = []; final toRemove = []; @@ -226,6 +239,9 @@ class _RegistrationPageState extends State { ); if (result.success) { successCount++; + if (result.isOffShelfSuccess) { + offShelfCount++; + } toRemove.add(zp); } else { failed.add(zp); @@ -238,6 +254,19 @@ class _RegistrationPageState extends State { _showDuplicateDialog(zp, result.duplicateInfo); return; } + if (result.isAlreadyOffShelf) { + setState(() { + _zongpaiNos.removeWhere((e) => toRemove.contains(e)); + _isSubmitting = false; + }); + _feedbackService.trigger(FeedbackEvent.submitFailure); + _showStatusOverride( + result.errorMessage ?? '该总排号已下架至转运区域,不可重新上架', + StatusDotColor.red, + const Duration(seconds: 2), + ); + return; + } } } @@ -250,8 +279,11 @@ class _RegistrationPageState extends State { if (failed.isEmpty) { _feedbackService.trigger(FeedbackEvent.submitSuccess); + final msg = offShelfCount == successCount + ? '批量下架成功($successCount 条)' + : '批量上架成功($successCount 条)'; _showStatusOverride( - '批量上架成功($successCount 条)', + msg, StatusDotColor.green, const Duration(milliseconds: 1500), ); @@ -492,7 +524,9 @@ class _RegistrationPageState extends State { width: double.infinity, padding: const EdgeInsets.all(14), decoration: BoxDecoration( - border: Border.all(color: Colors.grey.shade400), + border: Border.all( + color: Colors.grey.shade400, + ), borderRadius: BorderRadius.circular(8), ), child: const Text( @@ -509,7 +543,9 @@ class _RegistrationPageState extends State { const SizedBox(height: 6), itemBuilder: (context, index) { return Dismissible( - key: ValueKey('${_zongpaiNos[index]}-$index'), + key: ValueKey( + '${_zongpaiNos[index]}-$index', + ), direction: DismissDirection.endToStart, onDismissed: (_) => _removeZongpai(index), background: Container( diff --git a/lib/services/api_service.dart b/lib/services/api_service.dart index ee3760b..fdfd5e6 100644 --- a/lib/services/api_service.dart +++ b/lib/services/api_service.dart @@ -5,23 +5,41 @@ import 'package:http/http.dart' as http; class RegistrationResult { final bool success; final bool isDuplicate; + final bool isAlreadyOffShelf; + final bool isOffShelfSuccess; final String? errorMessage; final Map? duplicateInfo; + final Map? conflictInfo; RegistrationResult({ required this.success, this.isDuplicate = false, + this.isAlreadyOffShelf = false, + this.isOffShelfSuccess = false, this.errorMessage, this.duplicateInfo, + this.conflictInfo, }); factory RegistrationResult.ok() => RegistrationResult(success: true); + factory RegistrationResult.offShelfSuccess() => + RegistrationResult(success: true, isOffShelfSuccess: true); + factory RegistrationResult.duplicate(Map info) => RegistrationResult( success: false, isDuplicate: true, duplicateInfo: info, + conflictInfo: info, + ); + + factory RegistrationResult.alreadyOffShelf(Map info) => + RegistrationResult( + success: false, + isAlreadyOffShelf: true, + errorMessage: info['message']?.toString() ?? '该总排号已下架至转运区域,不可重新上架', + conflictInfo: info, ); factory RegistrationResult.error(String message) => @@ -228,6 +246,10 @@ class ApiService { switch (response.statusCode) { case 200: + final body = jsonDecode(response.body) as Map; + if (body['previous_location'] != null) { + return RegistrationResult.offShelfSuccess(); + } return RegistrationResult.ok(); case 400: final body = jsonDecode(response.body) as Map; @@ -235,7 +257,15 @@ class ApiService { return RegistrationResult.error(msg); case 409: final body = jsonDecode(response.body) as Map; - return RegistrationResult.duplicate(body); + final errorCode = body['error_code']?.toString(); + if (errorCode == 'DUPLICATE_LOCATION') { + return RegistrationResult.duplicate(body); + } + if (errorCode == 'ALREADY_OFF_SHELF') { + return RegistrationResult.alreadyOffShelf(body); + } + final msg = body['message']?.toString() ?? '提交失败'; + return RegistrationResult.error(msg); default: final body = jsonDecode(response.body) as Map; final msg = diff --git a/test/services/api_service_test.dart b/test/services/api_service_test.dart index 1f7a745..6e22123 100644 --- a/test/services/api_service_test.dart +++ b/test/services/api_service_test.dart @@ -8,7 +8,11 @@ void main() { test('returns ok on 200', () async { final mockClient = _MockClient((request) async { return http.Response( - jsonEncode({'zongpai_no': '26B1', 'location_code': 'A01-02-03', 'created_at': '2026-05-11T10:00:00'}), + jsonEncode({ + 'zongpai_no': '26B1', + 'location_code': 'A01-02-03', + 'created_at': '2026-05-11T10:00:00', + }), 200, headers: {'content-type': 'application/json; charset=utf-8'}, ); @@ -21,16 +25,69 @@ void main() { ); expect(result.success, isTrue); expect(result.isDuplicate, isFalse); + expect(result.isOffShelfSuccess, isFalse); }); - test('returns duplicate on 409 with location_code and registered_at', () async { + test('returns off-shelf success on 200 with previous_location', () async { final mockClient = _MockClient((request) async { return http.Response( jsonEncode({ - 'error_code': 'DUPLICATE_LOCATION', - 'message': '该总排号已存在货位记录', - 'location_code': 'A01-02-03', - 'registered_at': '2026-05-11T10:00:00', + 'zongpai_no': '26B1', + 'location_code': 'TRANS-01', + 'previous_location': 'A01-02-03', + 'created_at': '2026-05-13T10:00:00', + }), + 200, + headers: {'content-type': 'application/json; charset=utf-8'}, + ); + }); + final svc = ApiService(client: mockClient); + final result = await svc.registerLocation( + baseUrl: 'http://localhost', + zongpaiNo: '26B1', + locationCode: 'TRANS-01', + ); + expect(result.success, isTrue); + expect(result.isOffShelfSuccess, isTrue); + expect(result.isDuplicate, isFalse); + }); + + test( + 'returns duplicate on 409 with location_code and registered_at', + () async { + final mockClient = _MockClient((request) async { + return http.Response( + jsonEncode({ + 'error_code': 'DUPLICATE_LOCATION', + 'message': '该总排号已存在货位记录', + 'location_code': 'A01-02-03', + 'registered_at': '2026-05-11T10:00:00', + }), + 409, + headers: {'content-type': 'application/json; charset=utf-8'}, + ); + }); + final svc = ApiService(client: mockClient); + final result = await svc.registerLocation( + baseUrl: 'http://localhost', + zongpaiNo: '26B1', + locationCode: 'A02-01-01', + ); + expect(result.success, isFalse); + expect(result.isDuplicate, isTrue); + expect(result.duplicateInfo?['location_code'], 'A01-02-03'); + expect(result.duplicateInfo?['registered_at'], '2026-05-11T10:00:00'); + }, + ); + + test('returns already off shelf on 409 ALREADY_OFF_SHELF', () async { + final mockClient = _MockClient((request) async { + return http.Response( + jsonEncode({ + 'error_code': 'ALREADY_OFF_SHELF', + 'message': '该总排号已下架至转运区域,不可重新上架', + 'location_code': 'TRANS-01', + 'registered_at': '2026-05-13T10:00:00', }), 409, headers: {'content-type': 'application/json; charset=utf-8'}, @@ -43,15 +100,19 @@ void main() { locationCode: 'A02-01-01', ); expect(result.success, isFalse); - expect(result.isDuplicate, isTrue); - expect(result.duplicateInfo?['location_code'], 'A01-02-03'); - expect(result.duplicateInfo?['registered_at'], '2026-05-11T10:00:00'); + expect(result.isDuplicate, isFalse); + expect(result.isAlreadyOffShelf, isTrue); + expect(result.errorMessage, '该总排号已下架至转运区域,不可重新上架'); + expect(result.conflictInfo?['location_code'], 'TRANS-01'); }); test('returns error on 400 with message', () async { final mockClient = _MockClient((request) async { return http.Response( - jsonEncode({'error_code': 'INVALID_ZONGPAI', 'message': 'INVALID_ZONGPAI'}), + jsonEncode({ + 'error_code': 'INVALID_ZONGPAI', + 'message': 'INVALID_ZONGPAI', + }), 400, headers: {'content-type': 'application/json; charset=utf-8'}, );