13
bin/domain/page_result.dart
Normal file
13
bin/domain/page_result.dart
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
class PageResult<T> {
|
||||||
|
final List<T> items;
|
||||||
|
final int total;
|
||||||
|
final int page;
|
||||||
|
final int size;
|
||||||
|
|
||||||
|
const PageResult({
|
||||||
|
required this.items,
|
||||||
|
required this.total,
|
||||||
|
required this.page,
|
||||||
|
required this.size,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import 'dart:typed_data';
|
|||||||
|
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
import '../entities/photo.dart';
|
import '../entities/photo.dart';
|
||||||
|
import '../page_result.dart';
|
||||||
|
|
||||||
class PhotoRepository {
|
class PhotoRepository {
|
||||||
final String basePath;
|
final String basePath;
|
||||||
@@ -12,7 +13,30 @@ class PhotoRepository {
|
|||||||
|
|
||||||
PhotoRepository(this.basePath);
|
PhotoRepository(this.basePath);
|
||||||
|
|
||||||
|
/// 获取相册中所有照片(不分页,保留兼容)
|
||||||
Future<List<Photo>> getPhotosByAlbumId(int id) async {
|
Future<List<Photo>> getPhotosByAlbumId(int id) async {
|
||||||
|
final all = await _loadPhotosForAlbum(id);
|
||||||
|
return all;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取相册中的照片(分页)
|
||||||
|
Future<PageResult<Photo>> 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)
|
||||||
|
: <Photo>[];
|
||||||
|
return PageResult(items: items, total: total, page: page, size: size);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 内部方法:加载相册照片(带缓存)
|
||||||
|
Future<List<Photo>> _loadPhotosForAlbum(int id) async {
|
||||||
if (_updatedMap.containsKey(id) &&
|
if (_updatedMap.containsKey(id) &&
|
||||||
DateTime.now().difference(_updatedMap[id]!).inSeconds <= 60) {
|
DateTime.now().difference(_updatedMap[id]!).inSeconds <= 60) {
|
||||||
return _map[id]!;
|
return _map[id]!;
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ Future<Response> _getPhotoHandler(Request req, PhotoRepository repo) async {
|
|||||||
if (photo == null) {
|
if (photo == null) {
|
||||||
return Response.notFound('Photo not found');
|
return Response.notFound('Photo not found');
|
||||||
}
|
}
|
||||||
return jsonResponse(photo);
|
return jsonResponse(photo.toJson());
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Response> _getPhotoFileHandler(Request req, PhotoRepository repo) async {
|
Future<Response> _getPhotoFileHandler(Request req, PhotoRepository repo) async {
|
||||||
@@ -199,8 +199,23 @@ Future<Response> _getPhotosOfAlbumHandler(
|
|||||||
if (albumId == null) {
|
if (albumId == null) {
|
||||||
return Response.badRequest(body: 'Invalid album id: $idParam');
|
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<Response> _getPhotoPreviewHandler(
|
Future<Response> _getPhotoPreviewHandler(
|
||||||
|
|||||||
@@ -254,12 +254,16 @@ void main() {
|
|||||||
|
|
||||||
final response = await server.get('/api/v1/album/$albumId/photo');
|
final response = await server.get('/api/v1/album/$albumId/photo');
|
||||||
expect(response.statusCode, 200);
|
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, isNotEmpty);
|
||||||
expect(photos.first['fileName'], 'image.png');
|
expect(photos.first['fileName'], 'image.png');
|
||||||
expect(photos.first['mimeType'], 'image/png');
|
expect(photos.first['mimeType'], 'image/png');
|
||||||
expect(photos.first['width'], 640);
|
expect(photos.first['width'], 640);
|
||||||
expect(photos.first['height'], 480);
|
expect(photos.first['height'], 480);
|
||||||
|
expect(body['total'], 1);
|
||||||
|
expect(body['page'], 1);
|
||||||
|
expect(body['size'], 20);
|
||||||
});
|
});
|
||||||
|
|
||||||
test(
|
test(
|
||||||
@@ -272,12 +276,135 @@ void main() {
|
|||||||
|
|
||||||
final response = await server.get('/api/v1/album/$albumId/photo');
|
final response = await server.get('/api/v1/album/$albumId/photo');
|
||||||
expect(response.statusCode, 200);
|
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(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', () {
|
group('Photo endpoints', () {
|
||||||
test('GET /api/v1/photo/<id> with invalid id returns 400', () async {
|
test('GET /api/v1/photo/<id> with invalid id returns 400', () async {
|
||||||
server = await TestServer.start();
|
server = await TestServer.start();
|
||||||
@@ -304,7 +431,8 @@ void main() {
|
|||||||
final albumId = albums.first['id'];
|
final albumId = albums.first['id'];
|
||||||
|
|
||||||
final photosResponse = await server.get('/api/v1/album/$albumId/photo');
|
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 photoId = photos.first['id'];
|
||||||
|
|
||||||
final response = await server.get('/api/v1/photo/$photoId');
|
final response = await server.get('/api/v1/photo/$photoId');
|
||||||
@@ -346,7 +474,8 @@ void main() {
|
|||||||
final albumId = albums.first['id'];
|
final albumId = albums.first['id'];
|
||||||
|
|
||||||
final photosResponse = await server.get('/api/v1/album/$albumId/photo');
|
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 photoId = photos.first['id'];
|
||||||
|
|
||||||
final response = await server.get('/api/v1/photo/$photoId/file');
|
final response = await server.get('/api/v1/photo/$photoId/file');
|
||||||
@@ -370,7 +499,8 @@ void main() {
|
|||||||
final photosResponse = await server.get(
|
final photosResponse = await server.get(
|
||||||
'/api/v1/album/$albumId/photo',
|
'/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 photoId = photos.first['id'];
|
||||||
|
|
||||||
final response = await server.get('/api/v1/photo/$photoId/file');
|
final response = await server.get('/api/v1/photo/$photoId/file');
|
||||||
@@ -415,7 +545,8 @@ void main() {
|
|||||||
final photosResponse = await server.get(
|
final photosResponse = await server.get(
|
||||||
'/api/v1/album/$albumId/photo',
|
'/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 photoId = photos.first['id'];
|
||||||
|
|
||||||
// This will fail because vips CLI is not available,
|
// This will fail because vips CLI is not available,
|
||||||
@@ -468,7 +599,8 @@ void main() {
|
|||||||
final albumId = albums.first['id'];
|
final albumId = albums.first['id'];
|
||||||
|
|
||||||
final photosResponse = await server.get('/api/v1/album/$albumId/photo');
|
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 photoId = photos.first['id'];
|
||||||
|
|
||||||
final response = await server.get('/api/v1/photo/$photoId');
|
final response = await server.get('/api/v1/photo/$photoId');
|
||||||
|
|||||||
Reference in New Issue
Block a user