Files
xianyan/lib/features/check/check_core.dart
Developer f91be94e9c refactor: 完成项目架构重构,统一模块导入路径
- 清理大量废弃的 barrel 导出文件,移除冗余的中间导出层
- 修复所有相对路径导入错误,统一调整为扁平化模块引用
- 更新多平台 pubspec 版本号与依赖库版本
- 补充后端功能问题管理后台与脚本工具
- 调整部分页面的快捷方式文案适配新功能
- 更新部分翻译覆盖率与API文档
2026-06-12 08:53:57 +08:00

393 lines
12 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============================================================
// 闲言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);