feat: 接入起卦后端流程并完善积分扣减链路

This commit is contained in:
qzl
2026-04-03 19:04:46 +08:00
parent a136e42290
commit d87b2e1e3a
56 changed files with 3310 additions and 809 deletions
@@ -0,0 +1,108 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:meeyao_qianwen/features/divination/data/apis/divination_api.dart';
import 'package:meeyao_qianwen/features/divination/data/models/divination_params.dart';
void main() {
test('buildDivinationRunPayload contains required AG-UI fields', () {
final params = DivinationParams(
method: DivinationMethod.auto,
questionType: QuestionType.career,
question: '什么时候找到工作',
divinationTime: DateTime(2026, 4, 3, 18, 0, 2),
coinBalance: 0,
userId: 'u_test',
);
final payload = buildDivinationRunPayload(
params: params,
yaoStates: const <YaoType>[
YaoType.youngYang,
YaoType.youngYang,
YaoType.youngYang,
YaoType.oldYang,
YaoType.oldYin,
YaoType.youngYang,
],
threadId: 'de44f2fb-de0a-46b9-bbf2-e99ee36f2a2d',
runId: 'run_1775210431957',
clientNow: DateTime(2026, 4, 3, 18, 0, 31, 958),
);
expect(payload['state'], isA<Map<String, dynamic>>());
expect(payload['tools'], isA<List<dynamic>>());
expect(payload['context'], isA<List<dynamic>>());
final forwardedProps = payload['forwardedProps'] as Map<String, dynamic>;
expect(forwardedProps['runtime_mode'], 'chat');
final clientTime = forwardedProps['client_time'] as Map<String, dynamic>;
expect((clientTime['client_now_iso'] as String).endsWith('Z'), isTrue);
final divinationPayload =
forwardedProps['divinationPayload'] as Map<String, dynamic>;
expect(
(divinationPayload['divinationTimeIso'] as String).endsWith('Z'),
isTrue,
);
expect((divinationPayload['yaoLines'] as List<dynamic>).length, 6);
final messages = payload['messages'] as List<dynamic>;
expect(messages.length, 1);
final userMessage = messages.first as Map<String, dynamic>;
expect(userMessage['id'], isNotEmpty);
expect(userMessage['role'], 'user');
expect(userMessage['content'], '什么时候找到工作');
});
test('buildDivinationRunPayload throws when yaoStates length is not 6', () {
final params = DivinationParams(
method: DivinationMethod.auto,
questionType: QuestionType.career,
question: '测试',
divinationTime: DateTime(2026, 4, 3, 18, 0, 2),
coinBalance: 0,
userId: 'u_test',
);
expect(
() => buildDivinationRunPayload(
params: params,
yaoStates: const <YaoType>[YaoType.youngYang],
threadId: 'de44f2fb-de0a-46b9-bbf2-e99ee36f2a2d',
runId: 'run_1775210431957',
clientNow: DateTime(2026, 4, 3, 18, 0, 31, 958),
),
throwsArgumentError,
);
});
test(
'buildDivinationRunPayload throws when yaoStates contains undetermined',
() {
final params = DivinationParams(
method: DivinationMethod.auto,
questionType: QuestionType.career,
question: '测试',
divinationTime: DateTime(2026, 4, 3, 18, 0, 2),
coinBalance: 0,
userId: 'u_test',
);
expect(
() => buildDivinationRunPayload(
params: params,
yaoStates: const <YaoType>[
YaoType.youngYang,
YaoType.youngYang,
YaoType.youngYang,
YaoType.oldYang,
YaoType.oldYin,
YaoType.undetermined,
],
threadId: 'de44f2fb-de0a-46b9-bbf2-e99ee36f2a2d',
runId: 'run_1775210431957',
clientNow: DateTime(2026, 4, 3, 18, 0, 31, 958),
),
throwsArgumentError,
);
},
);
}
@@ -0,0 +1,19 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:meeyao_qianwen/features/divination/data/models/divination_backend_models.dart';
void main() {
test('YaoBackendLine accepts empty specialMark', () {
final line = YaoBackendLine.fromJson(<String, dynamic>{
'position': 1,
'spiritName': '',
'relationName': '父母',
'tiganName': '',
'elementName': '',
'isYang': true,
'isChanging': false,
'specialMark': '',
});
expect(line.specialMark, '');
});
}
@@ -2,8 +2,15 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:meeyao_qianwen/features/divination/data/models/divination_params.dart';
void main() {
test('mock data contains valid defaults', () {
final params = DivinationMockData.initial();
test('params contains valid fields', () {
final params = DivinationParams(
method: DivinationMethod.manual,
questionType: QuestionType.career,
question: '测试问题',
divinationTime: DateTime(2026, 4, 3, 10, 30),
coinBalance: 8,
userId: 'u_test',
);
expect(params.method, DivinationMethod.manual);
expect(params.questionType, QuestionType.career);
@@ -30,7 +37,14 @@ void main() {
});
test('toBinary and toChangedBinary mappings are correct', () {
final params = DivinationMockData.initial();
final params = DivinationParams(
method: DivinationMethod.manual,
questionType: QuestionType.career,
question: '测试问题',
divinationTime: DateTime(2026, 4, 3, 10, 30),
coinBalance: 8,
userId: 'u_test',
);
final states = <YaoType>[
YaoType.oldYin,
YaoType.youngYang,
@@ -1,36 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:meeyao_qianwen/features/divination/data/models/divination_params.dart';
import 'package:meeyao_qianwen/features/divination/data/services/divination_result_builder.dart';
void main() {
final builder = DivinationResultBuilder();
test('build returns result with hexagram names and section text', () {
final params = DivinationMockData.initial().copyWith(
method: DivinationMethod.auto,
question: '近期工作是否会有突破',
questionType: QuestionType.career,
);
final result = builder.build(
params: params,
yaoStates: const [
YaoType.youngYang,
YaoType.youngYin,
YaoType.oldYang,
YaoType.youngYin,
YaoType.oldYin,
YaoType.youngYang,
],
);
expect(result.guaName, isNotEmpty);
expect(result.targetGuaName, isNotEmpty);
expect(result.binaryCode, hasLength(6));
expect(result.changedBinaryCode, hasLength(6));
expect(result.keywords, contains(''));
expect(result.conclusion, contains('这个卦象的结果为'));
expect(result.yaoLines.length, 6);
expect(result.targetYaoLines.length, 6);
});
}
@@ -1,43 +1,180 @@
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:meeyao_qianwen/app/app_theme.dart';
import 'package:meeyao_qianwen/features/divination/data/models/divination_params.dart';
import 'package:meeyao_qianwen/features/divination/data/services/divination_result_builder.dart';
import 'package:meeyao_qianwen/features/divination/data/models/divination_result.dart';
import 'package:meeyao_qianwen/features/divination/presentation/screens/divination_result_screen.dart';
import 'package:meeyao_qianwen/l10n/app_localizations.dart';
void main() {
testWidgets('result screen shows key sections', (tester) async {
final params = DivinationMockData.initial().copyWith(
final params = DivinationParams(
method: DivinationMethod.auto,
questionType: QuestionType.health,
question: '近期状态是否平稳',
divinationTime: DateTime(2026, 4, 3, 20, 30),
coinBalance: 10,
userId: 'u_test',
);
final data = DivinationResultBuilder().build(
final data = DivinationResultData(
params: params,
yaoStates: const [
YaoType.oldYin,
YaoType.youngYang,
YaoType.youngYin,
YaoType.oldYang,
YaoType.youngYang,
YaoType.oldYin,
binaryCode: '101001',
changedBinaryCode: '100001',
guaName: '山火贲',
targetGuaName: '山雷颐',
upperName: '',
lowerName: '',
signType: '中上签',
keywords: '稳中求进、审时度势、蓄势待发',
conclusion: '1. 方向可行\n2. 节奏宜稳',
analysis: '当前阶段需先稳住节奏,再做关键推进。',
suggestion: '1. 控节奏\n2. 重复盘',
ganzhi: GanzhiData(
yearGanZhi: '丙午',
monthGanZhi: '辛卯',
dayGanZhi: '丁未',
timeGanZhi: '庚戌',
yearKongWang: '子丑',
monthKongWang: '戌亥',
dayKongWang: '寅卯',
timeKongWang: '寅卯',
yueJian: '卯木',
riChen: '未土',
yuePo: '酉金',
riChong: '丑土',
),
wuXingStatus: {'': '', '': '', '': '', '': '', '': ''},
yaoLines: [
YaoLineData(
index: 0,
spirit: '',
relation: '父母',
branch: '',
element: '',
type: YaoType.oldYin,
mark: '',
),
YaoLineData(
index: 1,
spirit: '',
relation: '兄弟',
branch: '',
element: '',
type: YaoType.youngYang,
mark: '',
),
YaoLineData(
index: 2,
spirit: '',
relation: '妻财',
branch: '',
element: '',
type: YaoType.youngYin,
mark: '',
),
YaoLineData(
index: 3,
spirit: '',
relation: '妻财',
branch: '',
element: '',
type: YaoType.oldYang,
mark: '',
),
YaoLineData(
index: 4,
spirit: '',
relation: '子孙',
branch: '',
element: '',
type: YaoType.youngYang,
mark: '',
),
YaoLineData(
index: 5,
spirit: '',
relation: '兄弟',
branch: '',
element: '',
type: YaoType.oldYin,
mark: '',
),
],
targetYaoLines: [
YaoLineData(
index: 0,
spirit: '',
relation: '父母',
branch: '',
element: '',
type: YaoType.youngYang,
mark: '',
),
YaoLineData(
index: 1,
spirit: '',
relation: '兄弟',
branch: '',
element: '',
type: YaoType.youngYang,
mark: '',
),
YaoLineData(
index: 2,
spirit: '',
relation: '妻财',
branch: '',
element: '',
type: YaoType.youngYin,
mark: '',
),
YaoLineData(
index: 3,
spirit: '',
relation: '妻财',
branch: '',
element: '',
type: YaoType.youngYin,
mark: '',
),
YaoLineData(
index: 4,
spirit: '',
relation: '子孙',
branch: '',
element: '',
type: YaoType.youngYang,
mark: '',
),
YaoLineData(
index: 5,
spirit: '',
relation: '兄弟',
branch: '',
element: '',
type: YaoType.youngYang,
mark: '',
),
],
);
await tester.pumpWidget(
MaterialApp(
theme: AppTheme.light(),
locale: const Locale('zh'),
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: AppLocalizations.supportedLocales,
home: DivinationResultScreen(data: data),
),
);
await tester.pump();
expect(find.text('天机推演中'), findsOneWidget);
await tester.pump(const Duration(milliseconds: 450));
expect(find.text('正在解卦'), findsOneWidget);
await tester.pump(const Duration(milliseconds: 850));
expect(find.text('解卦完成\n点击查看'), findsOneWidget);
await tester.pump(const Duration(milliseconds: 1000));
await tester.pumpAndSettle();
expect(find.text('解卦结果'), findsOneWidget);
expect(find.text('AI解卦'), findsOneWidget);
@@ -1,15 +1,42 @@
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:meeyao_qianwen/app/app_theme.dart';
import 'package:meeyao_qianwen/core/auth/session_store.dart';
import 'package:meeyao_qianwen/data/network/api_client.dart';
import 'package:meeyao_qianwen/data/storage/local_kv_store.dart';
import 'package:meeyao_qianwen/features/divination/data/apis/divination_api.dart';
import 'package:meeyao_qianwen/features/divination/data/models/divination_params.dart';
import 'package:meeyao_qianwen/features/divination/data/services/divination_run_service.dart';
import 'package:meeyao_qianwen/features/divination/presentation/screens/auto_divination_screen.dart';
import 'package:meeyao_qianwen/features/divination/presentation/screens/divination_screen.dart';
import 'package:meeyao_qianwen/features/divination/presentation/screens/manual_divination_screen.dart';
import 'package:meeyao_qianwen/l10n/app_localizations.dart';
void main() {
final runService = DivinationRunService(
api: DivinationApi(apiClient: ApiClient(baseUrl: 'http://localhost:5775')),
);
final sessionStore = SessionStore(LocalKvStore());
testWidgets('divination screen navigates to auto screen', (tester) async {
await tester.pumpWidget(
MaterialApp(theme: AppTheme.light(), home: const DivinationScreen()),
MaterialApp(
theme: AppTheme.light(),
locale: const Locale('zh'),
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: AppLocalizations.supportedLocales,
home: DivinationScreen(
sessionStore: sessionStore,
userId: 'user_test',
runServiceOverride: runService,
),
),
);
await tester.tap(find.text('自动起卦'));
@@ -25,15 +52,27 @@ void main() {
testWidgets('auto screen keeps resolve button disabled initially', (
tester,
) async {
final params = DivinationMockData.initial().copyWith(
final params = DivinationParams(
method: DivinationMethod.auto,
questionType: QuestionType.career,
question: '测试问题',
divinationTime: DateTime(2026, 4, 3, 20, 30),
coinBalance: 9,
userId: 'user_test',
);
await tester.pumpWidget(
MaterialApp(
theme: AppTheme.light(),
home: AutoDivinationScreen(params: params),
locale: const Locale('zh'),
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: AppLocalizations.supportedLocales,
home: AutoDivinationScreen(params: params, runService: runService),
),
);
@@ -49,7 +88,22 @@ void main() {
tester,
) async {
await tester.pumpWidget(
MaterialApp(theme: AppTheme.light(), home: const DivinationScreen()),
MaterialApp(
theme: AppTheme.light(),
locale: const Locale('zh'),
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: AppLocalizations.supportedLocales,
home: DivinationScreen(
sessionStore: sessionStore,
userId: 'user_test',
runServiceOverride: runService,
),
),
);
await tester.enterText(find.byType(TextField), '近期感情是否稳定');
@@ -1,20 +1,41 @@
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:meeyao_qianwen/app/app_theme.dart';
import 'package:meeyao_qianwen/data/network/api_client.dart';
import 'package:meeyao_qianwen/features/divination/data/apis/divination_api.dart';
import 'package:meeyao_qianwen/features/divination/data/models/divination_params.dart';
import 'package:meeyao_qianwen/features/divination/data/services/divination_run_service.dart';
import 'package:meeyao_qianwen/features/divination/presentation/screens/manual_divination_screen.dart';
import 'package:meeyao_qianwen/l10n/app_localizations.dart';
void main() {
testWidgets('manual screen shows yao legend', (tester) async {
final params = DivinationMockData.initial().copyWith(
final params = DivinationParams(
method: DivinationMethod.manual,
questionType: QuestionType.career,
question: '测试问题',
divinationTime: DateTime(2026, 4, 3, 20, 30),
coinBalance: 9,
userId: 'user_test',
);
final runService = DivinationRunService(
api: DivinationApi(
apiClient: ApiClient(baseUrl: 'http://localhost:5775'),
),
);
await tester.pumpWidget(
MaterialApp(
theme: AppTheme.light(),
home: ManualDivinationScreen(params: params),
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: AppLocalizations.supportedLocales,
home: ManualDivinationScreen(params: params, runService: runService),
),
);