diff --git a/bin/domain/page_result.dart b/bin/domain/page_result.dart new file mode 100644 index 0000000..acd9ada --- /dev/null +++ b/bin/domain/page_result.dart @@ -0,0 +1,13 @@ +class PageResult { + final List items; + final int total; + final int page; + final int size; + + const PageResult({ + required this.items, + required this.total, + required this.page, + required this.size, + }); +} diff --git a/bin/domain/repositories/photo_repository.dart b/bin/domain/repositories/photo_repository.dart index 976ca3a..674d272 100644 --- a/bin/domain/repositories/photo_repository.dart +++ b/bin/domain/repositories/photo_repository.dart @@ -4,6 +4,7 @@ import 'dart:typed_data'; import 'package:path/path.dart' as p; import '../entities/photo.dart'; +import '../page_result.dart'; class PhotoRepository { final String basePath; @@ -12,7 +13,30 @@ class PhotoRepository { PhotoRepository(this.basePath); + /// 获取相册中所有照片(不分页,保留兼容) Future> getPhotosByAlbumId(int id) async { + final all = await _loadPhotosForAlbum(id); + return all; + } + + /// 获取相册中的照片(分页) + Future> getPhotosByAlbumIdPaged( + int id, { + int page = 1, + int size = 20, + }) async { + final all = await _loadPhotosForAlbum(id); + final total = all.length; + final startIndex = (page - 1) * size; + final endIndex = startIndex + size; + final items = startIndex < total + ? all.sublist(startIndex, endIndex > total ? total : endIndex) + : []; + return PageResult(items: items, total: total, page: page, size: size); + } + + /// 内部方法:加载相册照片(带缓存) + Future> _loadPhotosForAlbum(int id) async { if (_updatedMap.containsKey(id) && DateTime.now().difference(_updatedMap[id]!).inSeconds <= 60) { return _map[id]!; diff --git a/bin/router.dart b/bin/router.dart index aa02449..add0974 100644 --- a/bin/router.dart +++ b/bin/router.dart @@ -135,7 +135,7 @@ Future _getPhotoHandler(Request req, PhotoRepository repo) async { if (photo == null) { return Response.notFound('Photo not found'); } - return jsonResponse(photo); + return jsonResponse(photo.toJson()); } Future _getPhotoFileHandler(Request req, PhotoRepository repo) async { @@ -199,8 +199,23 @@ Future _getPhotosOfAlbumHandler( if (albumId == null) { return Response.badRequest(body: 'Invalid album id: $idParam'); } - final photos = await repo.getPhotosByAlbumId(albumId); - return jsonResponse(photos); + + // 获取分页参数 + final page = int.tryParse(req.url.queryParameters['page'] ?? '1') ?? 1; + final size = int.tryParse(req.url.queryParameters['size'] ?? '20') ?? 20; + + final result = await repo.getPhotosByAlbumIdPaged( + albumId, + page: page, + size: size, + ); + + return jsonResponse({ + 'items': result.items.map((p) => p.toJson()).toList(), + 'total': result.total, + 'page': result.page, + 'size': result.size, + }); } Future _getPhotoPreviewHandler( diff --git a/test/integration/server_test.dart b/test/integration/server_test.dart index 580ae8c..429d0ec 100644 --- a/test/integration/server_test.dart +++ b/test/integration/server_test.dart @@ -254,12 +254,16 @@ void main() { final response = await server.get('/api/v1/album/$albumId/photo'); expect(response.statusCode, 200); - final photos = jsonDecode(response.body) as List; + final body = jsonDecode(response.body) as Map; + final photos = body['items'] as List; expect(photos, isNotEmpty); expect(photos.first['fileName'], 'image.png'); expect(photos.first['mimeType'], 'image/png'); expect(photos.first['width'], 640); expect(photos.first['height'], 480); + expect(body['total'], 1); + expect(body['page'], 1); + expect(body['size'], 20); }); test( @@ -272,12 +276,135 @@ void main() { final response = await server.get('/api/v1/album/$albumId/photo'); expect(response.statusCode, 200); - final photos = jsonDecode(response.body) as List; + final body = jsonDecode(response.body) as Map; + final photos = body['items'] as List; expect(photos, isEmpty); + expect(body['total'], 0); }, ); }); + group('Pagination', () { + test( + 'returns first page with default size when no query params', + () async { + final photos = List.generate( + 25, + (i) => ('photo_$i.png', _createTestPng()), + ); + server = await TestServer.start(testAlbums: {'paged_album': photos}); + final listResponse = await server.get('/api/v1/album'); + final albums = jsonDecode(listResponse.body) as List; + final albumId = albums.first['id']; + + final response = await server.get('/api/v1/album/$albumId/photo'); + final body = jsonDecode(response.body) as Map; + expect(body['total'], 25); + expect(body['page'], 1); + expect(body['size'], 20); + expect((body['items'] as List).length, 20); + }, + ); + + test('respects custom page and size parameters', () async { + final photos = List.generate( + 10, + (i) => ('photo_$i.png', _createTestPng()), + ); + server = await TestServer.start(testAlbums: {'small_album': photos}); + final listResponse = await server.get('/api/v1/album'); + final albums = jsonDecode(listResponse.body) as List; + final albumId = albums.first['id']; + + final response = await server.get( + '/api/v1/album/$albumId/photo?page=2&size=3', + ); + final body = jsonDecode(response.body) as Map; + expect(body['total'], 10); + expect(body['page'], 2); + expect(body['size'], 3); + expect((body['items'] as List).length, 3); + }); + + test('returns partial last page', () async { + final photos = List.generate( + 5, + (i) => ('photo_$i.png', _createTestPng()), + ); + server = await TestServer.start(testAlbums: {'five_photos': photos}); + final listResponse = await server.get('/api/v1/album'); + final albums = jsonDecode(listResponse.body) as List; + final albumId = albums.first['id']; + + final response = await server.get( + '/api/v1/album/$albumId/photo?page=2&size=3', + ); + final body = jsonDecode(response.body) as Map; + expect(body['total'], 5); + expect(body['page'], 2); + expect(body['size'], 3); + expect((body['items'] as List).length, 2); + }); + + test('returns empty items when page exceeds total', () async { + final photos = List.generate( + 3, + (i) => ('photo_$i.png', _createTestPng()), + ); + server = await TestServer.start(testAlbums: {'three_photos': photos}); + final listResponse = await server.get('/api/v1/album'); + final albums = jsonDecode(listResponse.body) as List; + final albumId = albums.first['id']; + + final response = await server.get( + '/api/v1/album/$albumId/photo?page=10&size=5', + ); + final body = jsonDecode(response.body) as Map; + expect(body['total'], 3); + expect(body['page'], 10); + expect(body['size'], 5); + expect((body['items'] as List), isEmpty); + }); + + test('page=1 returns items from the beginning', () async { + final photos = List.generate( + 5, + (i) => ('photo_$i.png', _createTestPng()), + ); + server = await TestServer.start(testAlbums: {'ordered_photos': photos}); + final listResponse = await server.get('/api/v1/album'); + final albums = jsonDecode(listResponse.body) as List; + final albumId = albums.first['id']; + + final response = await server.get( + '/api/v1/album/$albumId/photo?page=1&size=2', + ); + final body = jsonDecode(response.body) as Map; + final items = body['items'] as List; + expect(items.length, 2); + expect(items[0]['fileName'], 'photo_0.png'); + expect(items[1]['fileName'], 'photo_1.png'); + }); + + test('uses defaults when page/size are invalid strings', () async { + server = await TestServer.start( + testAlbums: { + 'defaults_album': [('image.png', _createTestPng())], + }, + ); + final listResponse = await server.get('/api/v1/album'); + final albums = jsonDecode(listResponse.body) as List; + final albumId = albums.first['id']; + + final response = await server.get( + '/api/v1/album/$albumId/photo?page=abc&size=xyz', + ); + final body = jsonDecode(response.body) as Map; + expect(body['page'], 1); + expect(body['size'], 20); + }); + }); + group('Photo endpoints', () { test('GET /api/v1/photo/ with invalid id returns 400', () async { server = await TestServer.start(); @@ -304,7 +431,8 @@ void main() { final albumId = albums.first['id']; final photosResponse = await server.get('/api/v1/album/$albumId/photo'); - final photos = jsonDecode(photosResponse.body) as List; + final photosBody = jsonDecode(photosResponse.body) as Map; + final photos = photosBody['items'] as List; final photoId = photos.first['id']; final response = await server.get('/api/v1/photo/$photoId'); @@ -346,7 +474,8 @@ void main() { final albumId = albums.first['id']; final photosResponse = await server.get('/api/v1/album/$albumId/photo'); - final photos = jsonDecode(photosResponse.body) as List; + final photosBody = jsonDecode(photosResponse.body) as Map; + final photos = photosBody['items'] as List; final photoId = photos.first['id']; final response = await server.get('/api/v1/photo/$photoId/file'); @@ -370,7 +499,8 @@ void main() { final photosResponse = await server.get( '/api/v1/album/$albumId/photo', ); - final photos = jsonDecode(photosResponse.body) as List; + final photosBody = jsonDecode(photosResponse.body) as Map; + final photos = photosBody['items'] as List; final photoId = photos.first['id']; final response = await server.get('/api/v1/photo/$photoId/file'); @@ -415,7 +545,8 @@ void main() { final photosResponse = await server.get( '/api/v1/album/$albumId/photo', ); - final photos = jsonDecode(photosResponse.body) as List; + final photosBody = jsonDecode(photosResponse.body) as Map; + final photos = photosBody['items'] as List; final photoId = photos.first['id']; // This will fail because vips CLI is not available, @@ -468,7 +599,8 @@ void main() { final albumId = albums.first['id']; final photosResponse = await server.get('/api/v1/album/$albumId/photo'); - final photos = jsonDecode(photosResponse.body) as List; + final photosBody = jsonDecode(photosResponse.body) as Map; + final photos = photosBody['items'] as List; final photoId = photos.first['id']; final response = await server.get('/api/v1/photo/$photoId');