- 清理大量废弃的 barrel 导出文件,移除冗余的中间导出层 - 修复所有相对路径导入错误,统一调整为扁平化模块引用 - 更新多平台 pubspec 版本号与依赖库版本 - 补充后端功能问题管理后台与脚本工具 - 调整部分页面的快捷方式文案适配新功能 - 更新部分翻译覆盖率与API文档
393 lines
12 KiB
Dart
393 lines
12 KiB
Dart
// ============================================================
|
||
// 闲言APP — 内容查重核心模块(模型 + 服务 + 状态管理)
|
||
// 创建时间: 2026-06-12
|
||
// 更新时间: 2026-06-12
|
||
// 作用: 合并 check_models / check_service / check_provider,
|
||
// 统一提供查重数据模型、API 服务、状态管理
|
||
// 上次更新: 由三个文件合并而来
|
||
// ============================================================
|
||
|
||
import 'package:dio/dio.dart';
|
||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||
|
||
import 'package:xianyan/core/network/api_client.dart';
|
||
import 'package:xianyan/core/network/api_exception.dart';
|
||
import 'package:xianyan/core/network/api_response.dart';
|
||
import 'package:xianyan/core/utils/logger.dart';
|
||
|
||
// ============================================================
|
||
// 一、数据模型 (CheckResult / CheckMatch / CheckSource / CheckMode)
|
||
// ============================================================
|
||
|
||
/// 查重结果模型
|
||
class CheckResult {
|
||
const CheckResult({
|
||
this.riskLevel = 'low',
|
||
this.riskScore = 0,
|
||
this.maxSimilarity = 0.0,
|
||
this.matches = const [],
|
||
this.sourceCount = 0,
|
||
this.checkedAt = '',
|
||
});
|
||
|
||
final String riskLevel;
|
||
final int riskScore;
|
||
final double maxSimilarity;
|
||
final List<CheckMatch> matches;
|
||
final int sourceCount;
|
||
final String checkedAt;
|
||
|
||
factory CheckResult.fromJson(Map<String, dynamic> json) => CheckResult(
|
||
riskLevel: json['risk_level'] as String? ?? 'low',
|
||
riskScore: json['risk_score'] as int? ?? 0,
|
||
maxSimilarity:
|
||
(json['max_similarity'] as num?)?.toDouble() ?? 0.0,
|
||
matches: (json['matches'] as List?)
|
||
?.map((e) =>
|
||
CheckMatch.fromJson(e as Map<String, dynamic>))
|
||
.toList() ??
|
||
[],
|
||
sourceCount: json['source_count'] as int? ?? 0,
|
||
checkedAt: json['checked_at'] as String? ?? '',
|
||
);
|
||
}
|
||
|
||
/// 查重匹配项模型
|
||
class CheckMatch {
|
||
const CheckMatch({
|
||
this.sourceType = '',
|
||
this.sourceTitle = '',
|
||
this.similarity = 0.0,
|
||
this.matchedText = '',
|
||
this.sourceId = 0,
|
||
});
|
||
|
||
final String sourceType;
|
||
final String sourceTitle;
|
||
final double similarity;
|
||
final String matchedText;
|
||
final int sourceId;
|
||
|
||
factory CheckMatch.fromJson(Map<String, dynamic> json) => CheckMatch(
|
||
sourceType: json['source_type'] as String? ?? '',
|
||
sourceTitle: json['source_title'] as String? ?? '',
|
||
similarity:
|
||
(json['similarity'] as num?)?.toDouble() ?? 0.0,
|
||
matchedText: json['matched_text'] as String? ?? '',
|
||
sourceId: json['source_id'] as int? ?? 0,
|
||
);
|
||
}
|
||
|
||
/// 查重数据源模型
|
||
class CheckSource {
|
||
const CheckSource({
|
||
required this.id,
|
||
required this.name,
|
||
required this.emoji,
|
||
this.desc = '',
|
||
this.enabled = true,
|
||
});
|
||
|
||
final String id;
|
||
final String name;
|
||
final String emoji;
|
||
final String desc;
|
||
final bool enabled;
|
||
|
||
static const List<CheckSource> sources = [
|
||
CheckSource(
|
||
id: 'poetry', name: '诗词库', emoji: '📜', desc: '古诗词数据库'),
|
||
CheckSource(
|
||
id: 'chengyu', name: '成语库', emoji: '🔗', desc: '成语词典'),
|
||
CheckSource(
|
||
id: 'story', name: '故事库', emoji: '📖', desc: '故事大全'),
|
||
CheckSource(
|
||
id: 'wisdom', name: '名言库', emoji: '💡', desc: '名言警句'),
|
||
CheckSource(
|
||
id: 'article', name: '文章库', emoji: '📝', desc: '用户文章'),
|
||
];
|
||
}
|
||
|
||
/// 查重模式枚举
|
||
enum CheckMode { exact, fuzzy, similar, report }
|
||
|
||
// ============================================================
|
||
// 二、查重服务 (CheckService)
|
||
// ============================================================
|
||
|
||
/// 内容查重 API 服务
|
||
class CheckService {
|
||
CheckService._();
|
||
static final ApiClient _api = ApiClient.instance;
|
||
static const String _basePath = '/api/check';
|
||
|
||
/// 获取查重数据源
|
||
static Future<List<Map<String, dynamic>>> getSources() async {
|
||
try {
|
||
final response = await _api.get<Map<String, dynamic>>(
|
||
'$_basePath/sources',
|
||
);
|
||
final apiResp = ApiResponse<Map<String, dynamic>>.fromJson(
|
||
response.data as Map<String, dynamic>,
|
||
);
|
||
if (!apiResp.isSuccess) {
|
||
throw ApiException(code: apiResp.code, message: apiResp.msg);
|
||
}
|
||
final list = apiResp.data?['list'] as List? ?? [];
|
||
return list.cast<Map<String, dynamic>>();
|
||
} on DioException catch (e) {
|
||
throw _handleDioError(e);
|
||
}
|
||
}
|
||
|
||
/// 精确查重
|
||
static Future<CheckResult> exactCheck({
|
||
required String text,
|
||
List<String>? sources,
|
||
}) async {
|
||
try {
|
||
final response = await _api.post<Map<String, dynamic>>(
|
||
'$_basePath/exact',
|
||
data: {
|
||
'text': text,
|
||
if (sources != null) 'sources': sources,
|
||
},
|
||
);
|
||
final respData = response.data as Map<String, dynamic>;
|
||
final code = respData['code'] as int? ?? 0;
|
||
if (code != 1) {
|
||
throw ApiException(
|
||
code: code,
|
||
message: respData['msg'] as String? ?? '查重失败',
|
||
);
|
||
}
|
||
return CheckResult.fromJson(
|
||
respData['data'] as Map<String, dynamic>? ?? {});
|
||
} on DioException catch (e) {
|
||
throw _handleDioError(e);
|
||
}
|
||
}
|
||
|
||
/// 模糊查重
|
||
static Future<CheckResult> fuzzyCheck({
|
||
required String text,
|
||
List<String>? sources,
|
||
double? threshold,
|
||
}) async {
|
||
try {
|
||
final response = await _api.post<Map<String, dynamic>>(
|
||
'$_basePath/fuzzy',
|
||
data: {
|
||
'text': text,
|
||
if (sources != null) 'sources': sources,
|
||
if (threshold != null) 'threshold': threshold,
|
||
},
|
||
);
|
||
final respData = response.data as Map<String, dynamic>;
|
||
final code = respData['code'] as int? ?? 0;
|
||
if (code != 1) {
|
||
throw ApiException(
|
||
code: code,
|
||
message: respData['msg'] as String? ?? '查重失败',
|
||
);
|
||
}
|
||
return CheckResult.fromJson(
|
||
respData['data'] as Map<String, dynamic>? ?? {});
|
||
} on DioException catch (e) {
|
||
throw _handleDioError(e);
|
||
}
|
||
}
|
||
|
||
/// 相似度查重
|
||
static Future<CheckResult> similarCheck({
|
||
required String text,
|
||
List<String>? sources,
|
||
}) async {
|
||
try {
|
||
final response = await _api.post<Map<String, dynamic>>(
|
||
'$_basePath/similar',
|
||
data: {
|
||
'text': text,
|
||
if (sources != null) 'sources': sources,
|
||
},
|
||
);
|
||
final respData = response.data as Map<String, dynamic>;
|
||
final code = respData['code'] as int? ?? 0;
|
||
if (code != 1) {
|
||
throw ApiException(
|
||
code: code,
|
||
message: respData['msg'] as String? ?? '查重失败',
|
||
);
|
||
}
|
||
return CheckResult.fromJson(
|
||
respData['data'] as Map<String, dynamic>? ?? {});
|
||
} on DioException catch (e) {
|
||
throw _handleDioError(e);
|
||
}
|
||
}
|
||
|
||
/// 综合报告(30秒超时)
|
||
static Future<CheckResult> reportCheck({
|
||
required String text,
|
||
List<String>? sources,
|
||
}) async {
|
||
try {
|
||
final response = await _api.post<Map<String, dynamic>>(
|
||
'$_basePath/report',
|
||
data: {
|
||
'text': text,
|
||
if (sources != null) 'sources': sources,
|
||
},
|
||
options: Options(receiveTimeout: const Duration(seconds: 30)),
|
||
);
|
||
final respData = response.data as Map<String, dynamic>;
|
||
final code = respData['code'] as int? ?? 0;
|
||
if (code != 1) {
|
||
throw ApiException(
|
||
code: code,
|
||
message: respData['msg'] as String? ?? '查重失败',
|
||
);
|
||
}
|
||
return CheckResult.fromJson(
|
||
respData['data'] as Map<String, dynamic>? ?? {});
|
||
} on DioException catch (e) {
|
||
throw _handleDioError(e);
|
||
}
|
||
}
|
||
|
||
/// Dio 错误统一处理
|
||
static ApiException _handleDioError(DioException e) {
|
||
switch (e.type) {
|
||
case DioExceptionType.connectionTimeout:
|
||
case DioExceptionType.sendTimeout:
|
||
case DioExceptionType.receiveTimeout:
|
||
return const ApiException(code: -1, message: '连接超时,请检查网络');
|
||
case DioExceptionType.connectionError:
|
||
return const ApiException(code: -2, message: '网络连接失败');
|
||
default:
|
||
if (e.response?.data != null) {
|
||
try {
|
||
final data = e.response!.data as Map<String, dynamic>;
|
||
return ApiException(
|
||
code: data['code'] as int? ?? e.response?.statusCode ?? -5,
|
||
message: data['msg'] as String? ?? '请求失败',
|
||
);
|
||
} catch (_) {}
|
||
}
|
||
return const ApiException(code: -5, message: '未知网络错误');
|
||
}
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// 三、状态管理 (CheckState / CheckNotifier / checkProvider)
|
||
// ============================================================
|
||
|
||
/// 查重状态
|
||
class CheckState {
|
||
const CheckState({
|
||
this.isChecking = false,
|
||
this.error,
|
||
this.result,
|
||
this.selectedMode = CheckMode.fuzzy,
|
||
this.selectedSources = const [],
|
||
});
|
||
|
||
final bool isChecking;
|
||
final String? error;
|
||
final CheckResult? result;
|
||
final CheckMode selectedMode;
|
||
final List<String> selectedSources;
|
||
|
||
CheckState copyWith({
|
||
bool? isChecking,
|
||
String? error,
|
||
bool clearError = false,
|
||
CheckResult? result,
|
||
bool clearResult = false,
|
||
CheckMode? selectedMode,
|
||
List<String>? selectedSources,
|
||
}) {
|
||
return CheckState(
|
||
isChecking: isChecking ?? this.isChecking,
|
||
error: clearError ? null : (error ?? this.error),
|
||
result: clearResult ? null : (result ?? this.result),
|
||
selectedMode: selectedMode ?? this.selectedMode,
|
||
selectedSources: selectedSources ?? this.selectedSources,
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 查重状态 Notifier
|
||
class CheckNotifier extends Notifier<CheckState> {
|
||
@override
|
||
CheckState build() => const CheckState();
|
||
CheckNotifier();
|
||
|
||
Future<void> check({required String text}) async {
|
||
if (text.trim().isEmpty) return;
|
||
state = state.copyWith(
|
||
isChecking: true, clearError: true, clearResult: true);
|
||
|
||
try {
|
||
final CheckResult result;
|
||
switch (state.selectedMode) {
|
||
case CheckMode.exact:
|
||
result = await CheckService.exactCheck(
|
||
text: text,
|
||
sources: state.selectedSources.isNotEmpty
|
||
? state.selectedSources
|
||
: null,
|
||
);
|
||
case CheckMode.fuzzy:
|
||
result = await CheckService.fuzzyCheck(
|
||
text: text,
|
||
sources: state.selectedSources.isNotEmpty
|
||
? state.selectedSources
|
||
: null,
|
||
);
|
||
case CheckMode.similar:
|
||
result = await CheckService.similarCheck(
|
||
text: text,
|
||
sources: state.selectedSources.isNotEmpty
|
||
? state.selectedSources
|
||
: null,
|
||
);
|
||
case CheckMode.report:
|
||
result = await CheckService.reportCheck(
|
||
text: text,
|
||
sources: state.selectedSources.isNotEmpty
|
||
? state.selectedSources
|
||
: null,
|
||
);
|
||
}
|
||
state = state.copyWith(isChecking: false, result: result);
|
||
} catch (e) {
|
||
Log.e('查重失败', e);
|
||
state = state.copyWith(isChecking: false, error: '查重失败');
|
||
}
|
||
}
|
||
|
||
void setMode(CheckMode mode) {
|
||
state = state.copyWith(selectedMode: mode);
|
||
}
|
||
|
||
void toggleSource(String sourceId) {
|
||
final sources = [...state.selectedSources];
|
||
if (sources.contains(sourceId)) {
|
||
sources.remove(sourceId);
|
||
} else {
|
||
sources.add(sourceId);
|
||
}
|
||
state = state.copyWith(selectedSources: sources);
|
||
}
|
||
|
||
void clearResult() {
|
||
state = state.copyWith(clearResult: true, clearError: true);
|
||
}
|
||
}
|
||
|
||
/// 查重 Provider
|
||
final checkProvider =
|
||
NotifierProvider<CheckNotifier, CheckState>(CheckNotifier.new);
|