feat: handle off-shelf registration results

This commit is contained in:
Misaka_Company
2026-05-13 12:43:22 +08:00
parent b975b15ba7
commit b32c9c5aad
3 changed files with 145 additions and 18 deletions

View File

@@ -49,8 +49,7 @@ class _RegistrationPageState extends State<RegistrationPage> {
} }
void _onKeyEvent(KeyEvent event) { void _onKeyEvent(KeyEvent event) {
if (event is KeyDownEvent && if (event is KeyDownEvent && event.logicalKey == LogicalKeyboardKey.enter) {
event.logicalKey == LogicalKeyboardKey.enter) {
_submit(); _submit();
} }
} }
@@ -195,11 +194,24 @@ class _RegistrationPageState extends State<RegistrationPage> {
_locationType = null; _locationType = null;
} }
}); });
final msg = _isLocked ? '货位已锁定,请扫描下一张执行卡' : '上架成功'; final msg = result.isOffShelfSuccess
_showStatusOverride(msg, StatusDotColor.green, const Duration(milliseconds: 1500)); ? '下架成功'
: (_isLocked ? '货位已锁定,请扫描下一张执行卡' : '上架成功');
_showStatusOverride(
msg,
StatusDotColor.green,
const Duration(milliseconds: 1500),
);
} else if (result.isDuplicate) { } else if (result.isDuplicate) {
_feedbackService.trigger(FeedbackEvent.duplicateError); _feedbackService.trigger(FeedbackEvent.duplicateError);
_showDuplicateDialog(zongpaiNo, result.duplicateInfo); _showDuplicateDialog(zongpaiNo, result.duplicateInfo);
} else if (result.isAlreadyOffShelf) {
_feedbackService.trigger(FeedbackEvent.submitFailure);
_showStatusOverride(
result.errorMessage ?? '该总排号已下架至转运区域,不可重新上架',
StatusDotColor.red,
const Duration(seconds: 2),
);
} else { } else {
final isNetwork = result.errorMessage == '网络异常,请检查网络连接'; final isNetwork = result.errorMessage == '网络异常,请检查网络连接';
_feedbackService.trigger( _feedbackService.trigger(
@@ -215,6 +227,7 @@ class _RegistrationPageState extends State<RegistrationPage> {
Future<void> _submitBatch(String baseUrl) async { Future<void> _submitBatch(String baseUrl) async {
int successCount = 0; int successCount = 0;
int offShelfCount = 0;
final failed = <String>[]; final failed = <String>[];
final toRemove = <String>[]; final toRemove = <String>[];
@@ -226,6 +239,9 @@ class _RegistrationPageState extends State<RegistrationPage> {
); );
if (result.success) { if (result.success) {
successCount++; successCount++;
if (result.isOffShelfSuccess) {
offShelfCount++;
}
toRemove.add(zp); toRemove.add(zp);
} else { } else {
failed.add(zp); failed.add(zp);
@@ -238,6 +254,19 @@ class _RegistrationPageState extends State<RegistrationPage> {
_showDuplicateDialog(zp, result.duplicateInfo); _showDuplicateDialog(zp, result.duplicateInfo);
return; 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<RegistrationPage> {
if (failed.isEmpty) { if (failed.isEmpty) {
_feedbackService.trigger(FeedbackEvent.submitSuccess); _feedbackService.trigger(FeedbackEvent.submitSuccess);
final msg = offShelfCount == successCount
? '批量下架成功($successCount 条)'
: '批量上架成功($successCount 条)';
_showStatusOverride( _showStatusOverride(
'批量上架成功($successCount 条)', msg,
StatusDotColor.green, StatusDotColor.green,
const Duration(milliseconds: 1500), const Duration(milliseconds: 1500),
); );
@@ -492,7 +524,9 @@ class _RegistrationPageState extends State<RegistrationPage> {
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.all(14), padding: const EdgeInsets.all(14),
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade400), border: Border.all(
color: Colors.grey.shade400,
),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
child: const Text( child: const Text(
@@ -509,7 +543,9 @@ class _RegistrationPageState extends State<RegistrationPage> {
const SizedBox(height: 6), const SizedBox(height: 6),
itemBuilder: (context, index) { itemBuilder: (context, index) {
return Dismissible( return Dismissible(
key: ValueKey('${_zongpaiNos[index]}-$index'), key: ValueKey(
'${_zongpaiNos[index]}-$index',
),
direction: DismissDirection.endToStart, direction: DismissDirection.endToStart,
onDismissed: (_) => _removeZongpai(index), onDismissed: (_) => _removeZongpai(index),
background: Container( background: Container(

View File

@@ -5,23 +5,41 @@ import 'package:http/http.dart' as http;
class RegistrationResult { class RegistrationResult {
final bool success; final bool success;
final bool isDuplicate; final bool isDuplicate;
final bool isAlreadyOffShelf;
final bool isOffShelfSuccess;
final String? errorMessage; final String? errorMessage;
final Map<String, dynamic>? duplicateInfo; final Map<String, dynamic>? duplicateInfo;
final Map<String, dynamic>? conflictInfo;
RegistrationResult({ RegistrationResult({
required this.success, required this.success,
this.isDuplicate = false, this.isDuplicate = false,
this.isAlreadyOffShelf = false,
this.isOffShelfSuccess = false,
this.errorMessage, this.errorMessage,
this.duplicateInfo, this.duplicateInfo,
this.conflictInfo,
}); });
factory RegistrationResult.ok() => RegistrationResult(success: true); factory RegistrationResult.ok() => RegistrationResult(success: true);
factory RegistrationResult.offShelfSuccess() =>
RegistrationResult(success: true, isOffShelfSuccess: true);
factory RegistrationResult.duplicate(Map<String, dynamic> info) => factory RegistrationResult.duplicate(Map<String, dynamic> info) =>
RegistrationResult( RegistrationResult(
success: false, success: false,
isDuplicate: true, isDuplicate: true,
duplicateInfo: info, duplicateInfo: info,
conflictInfo: info,
);
factory RegistrationResult.alreadyOffShelf(Map<String, dynamic> info) =>
RegistrationResult(
success: false,
isAlreadyOffShelf: true,
errorMessage: info['message']?.toString() ?? '该总排号已下架至转运区域,不可重新上架',
conflictInfo: info,
); );
factory RegistrationResult.error(String message) => factory RegistrationResult.error(String message) =>
@@ -228,6 +246,10 @@ class ApiService {
switch (response.statusCode) { switch (response.statusCode) {
case 200: case 200:
final body = jsonDecode(response.body) as Map<String, dynamic>;
if (body['previous_location'] != null) {
return RegistrationResult.offShelfSuccess();
}
return RegistrationResult.ok(); return RegistrationResult.ok();
case 400: case 400:
final body = jsonDecode(response.body) as Map<String, dynamic>; final body = jsonDecode(response.body) as Map<String, dynamic>;
@@ -235,7 +257,15 @@ class ApiService {
return RegistrationResult.error(msg); return RegistrationResult.error(msg);
case 409: case 409:
final body = jsonDecode(response.body) as Map<String, dynamic>; final body = jsonDecode(response.body) as Map<String, dynamic>;
final errorCode = body['error_code']?.toString();
if (errorCode == 'DUPLICATE_LOCATION') {
return RegistrationResult.duplicate(body); return RegistrationResult.duplicate(body);
}
if (errorCode == 'ALREADY_OFF_SHELF') {
return RegistrationResult.alreadyOffShelf(body);
}
final msg = body['message']?.toString() ?? '提交失败';
return RegistrationResult.error(msg);
default: default:
final body = jsonDecode(response.body) as Map<String, dynamic>; final body = jsonDecode(response.body) as Map<String, dynamic>;
final msg = final msg =

View File

@@ -8,7 +8,11 @@ void main() {
test('returns ok on 200', () async { test('returns ok on 200', () async {
final mockClient = _MockClient((request) async { final mockClient = _MockClient((request) async {
return http.Response( 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, 200,
headers: {'content-type': 'application/json; charset=utf-8'}, headers: {'content-type': 'application/json; charset=utf-8'},
); );
@@ -21,9 +25,36 @@ void main() {
); );
expect(result.success, isTrue); expect(result.success, isTrue);
expect(result.isDuplicate, isFalse); 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({
'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 { final mockClient = _MockClient((request) async {
return http.Response( return http.Response(
jsonEncode({ jsonEncode({
@@ -46,12 +77,42 @@ void main() {
expect(result.isDuplicate, isTrue); expect(result.isDuplicate, isTrue);
expect(result.duplicateInfo?['location_code'], 'A01-02-03'); expect(result.duplicateInfo?['location_code'], 'A01-02-03');
expect(result.duplicateInfo?['registered_at'], '2026-05-11T10:00:00'); 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'},
);
});
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, isFalse);
expect(result.isAlreadyOffShelf, isTrue);
expect(result.errorMessage, '该总排号已下架至转运区域,不可重新上架');
expect(result.conflictInfo?['location_code'], 'TRANS-01');
}); });
test('returns error on 400 with message', () async { test('returns error on 400 with message', () async {
final mockClient = _MockClient((request) async { final mockClient = _MockClient((request) async {
return http.Response( return http.Response(
jsonEncode({'error_code': 'INVALID_ZONGPAI', 'message': 'INVALID_ZONGPAI'}), jsonEncode({
'error_code': 'INVALID_ZONGPAI',
'message': 'INVALID_ZONGPAI',
}),
400, 400,
headers: {'content-type': 'application/json; charset=utf-8'}, headers: {'content-type': 'application/json; charset=utf-8'},
); );