feat: 接入起卦后端流程并完善积分扣减链路
This commit is contained in:
@@ -1,222 +0,0 @@
|
||||
import '../models/divination_params.dart';
|
||||
import '../models/divination_result.dart';
|
||||
|
||||
class DivinationResultBuilder {
|
||||
DivinationResultData build({
|
||||
required DivinationParams params,
|
||||
required List<YaoType> yaoStates,
|
||||
}) {
|
||||
final binaryCode = params.toBinary(yaoStates);
|
||||
final changedBinaryCode = params.toChangedBinary(yaoStates);
|
||||
final baseHexagram = _hexagramMap[binaryCode];
|
||||
final changedHexagram = _hexagramMap[changedBinaryCode];
|
||||
if (baseHexagram == null || changedHexagram == null) {
|
||||
throw StateError(
|
||||
'Unknown hexagram mapping for binary=$binaryCode changed=$changedBinaryCode',
|
||||
);
|
||||
}
|
||||
|
||||
final signType = _signByStates(yaoStates);
|
||||
final content = _mockContent(
|
||||
params.questionType,
|
||||
params.question,
|
||||
signType,
|
||||
);
|
||||
|
||||
final lineData = _buildYaoLines(yaoStates, false);
|
||||
final targetStates = _toChangedStates(yaoStates);
|
||||
final targetLineData = _buildYaoLines(targetStates, true);
|
||||
|
||||
return DivinationResultData(
|
||||
params: params,
|
||||
binaryCode: binaryCode,
|
||||
changedBinaryCode: changedBinaryCode,
|
||||
guaName: baseHexagram.name,
|
||||
targetGuaName: changedHexagram.name,
|
||||
upperName: baseHexagram.upper,
|
||||
lowerName: baseHexagram.lower,
|
||||
signType: signType,
|
||||
keywords: content.keywords,
|
||||
conclusion: content.conclusion,
|
||||
analysis: content.analysis,
|
||||
suggestion: content.suggestion,
|
||||
ganzhi: const GanzhiData(
|
||||
yearGanZhi: '丙午',
|
||||
monthGanZhi: '甲辰',
|
||||
dayGanZhi: '辛亥',
|
||||
timeGanZhi: '乙巳',
|
||||
yearKongWang: '子丑',
|
||||
monthKongWang: '申酉',
|
||||
dayKongWang: '寅卯',
|
||||
timeKongWang: '午未',
|
||||
yueJian: '辰',
|
||||
riChen: '亥',
|
||||
yuePo: '戌',
|
||||
riChong: '巳',
|
||||
),
|
||||
wuXingStatus: const {'木': '旺', '火': '相', '土': '休', '金': '囚', '水': '死'},
|
||||
yaoLines: lineData,
|
||||
targetYaoLines: targetLineData,
|
||||
);
|
||||
}
|
||||
|
||||
List<YaoLineData> _buildYaoLines(List<YaoType> states, bool target) {
|
||||
const spirits = ['龙', '雀', '勾', '蛇', '虎', '玄'];
|
||||
const relations = ['父母', '兄弟', '官鬼', '妻财', '子孙', '父母'];
|
||||
const branches = ['子', '寅', '辰', '午', '申', '戌'];
|
||||
const elements = ['水', '木', '土', '火', '金', '土'];
|
||||
return List<YaoLineData>.generate(6, (idx) {
|
||||
final mark = switch (idx) {
|
||||
1 => '应',
|
||||
4 => '世',
|
||||
_ => '',
|
||||
};
|
||||
return YaoLineData(
|
||||
index: idx,
|
||||
spirit: spirits[idx],
|
||||
relation: relations[idx],
|
||||
branch: branches[idx],
|
||||
element: elements[idx],
|
||||
type: states[idx],
|
||||
mark: target ? '' : mark,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
List<YaoType> _toChangedStates(List<YaoType> source) {
|
||||
return source.map((state) {
|
||||
return switch (state) {
|
||||
YaoType.oldYang => YaoType.youngYin,
|
||||
YaoType.oldYin => YaoType.youngYang,
|
||||
_ => state,
|
||||
};
|
||||
}).toList();
|
||||
}
|
||||
|
||||
String _signByStates(List<YaoType> states) {
|
||||
final dynamicCount = states
|
||||
.where((e) => e == YaoType.oldYang || e == YaoType.oldYin)
|
||||
.length;
|
||||
if (dynamicCount <= 1) {
|
||||
return '上上签';
|
||||
}
|
||||
if (dynamicCount <= 3) {
|
||||
return '中上签';
|
||||
}
|
||||
return '中下签';
|
||||
}
|
||||
|
||||
_MockContent _mockContent(
|
||||
QuestionType type,
|
||||
String question,
|
||||
String signType,
|
||||
) {
|
||||
final domain = switch (type) {
|
||||
QuestionType.career || QuestionType.study => '事业与成长',
|
||||
QuestionType.love => '关系与情感',
|
||||
QuestionType.wealth => '财富与资源',
|
||||
QuestionType.fortune => '阶段运势',
|
||||
QuestionType.dream => '潜意识信号',
|
||||
QuestionType.health => '身心节律',
|
||||
QuestionType.search => '寻物线索',
|
||||
QuestionType.other => '综合事项',
|
||||
};
|
||||
return _MockContent(
|
||||
keywords: '$signType · $domain',
|
||||
conclusion: '这个卦象的结果为$signType。你关注的“$question”处于可推进阶段,当前节奏重在稳步而行,不宜急进。',
|
||||
analysis:
|
||||
'本卦显示外在条件逐步成形,内在决心也在增强。若短期遇到反复,通常是资源重组与信息修正,并非方向错误。建议将目标拆分为可验证的小节点,持续复盘。',
|
||||
suggestion:
|
||||
'建议一:先定三周内可执行动作并按日推进。\n建议二:重要决定留有缓冲期,避免情绪化判断。\n建议三:遇到阻滞先调整节奏,再补关键资源。',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MockContent {
|
||||
const _MockContent({
|
||||
required this.keywords,
|
||||
required this.conclusion,
|
||||
required this.analysis,
|
||||
required this.suggestion,
|
||||
});
|
||||
|
||||
final String keywords;
|
||||
final String conclusion;
|
||||
final String analysis;
|
||||
final String suggestion;
|
||||
}
|
||||
|
||||
class _HexagramShort {
|
||||
const _HexagramShort(this.name, this.upper, this.lower);
|
||||
|
||||
final String name;
|
||||
final String upper;
|
||||
final String lower;
|
||||
}
|
||||
|
||||
const Map<String, _HexagramShort> _hexagramMap = {
|
||||
'111111': _HexagramShort('乾为天', '乾', '乾'),
|
||||
'011111': _HexagramShort('天风姤', '乾', '巽'),
|
||||
'001111': _HexagramShort('天山遁', '乾', '艮'),
|
||||
'000111': _HexagramShort('天地否', '乾', '坤'),
|
||||
'000011': _HexagramShort('风地观', '巽', '坤'),
|
||||
'000001': _HexagramShort('山地剥', '艮', '坤'),
|
||||
'000101': _HexagramShort('火地晋', '离', '坤'),
|
||||
'111101': _HexagramShort('火天大有', '离', '乾'),
|
||||
'010010': _HexagramShort('坎为水', '坎', '坎'),
|
||||
'110010': _HexagramShort('水泽节', '坎', '兑'),
|
||||
'100010': _HexagramShort('水雷屯', '坎', '震'),
|
||||
'101010': _HexagramShort('水火既济', '坎', '离'),
|
||||
'101110': _HexagramShort('泽火革', '兑', '离'),
|
||||
'101100': _HexagramShort('雷火丰', '震', '离'),
|
||||
'101000': _HexagramShort('地火明夷', '坤', '离'),
|
||||
'010000': _HexagramShort('地水师', '坤', '坎'),
|
||||
'001001': _HexagramShort('艮为山', '艮', '艮'),
|
||||
'101001': _HexagramShort('山火贲', '艮', '离'),
|
||||
'111001': _HexagramShort('山天大畜', '艮', '乾'),
|
||||
'110001': _HexagramShort('山泽损', '艮', '兑'),
|
||||
'110101': _HexagramShort('火泽睽', '离', '兑'),
|
||||
'110111': _HexagramShort('天泽履', '乾', '兑'),
|
||||
'110011': _HexagramShort('风泽中孚', '巽', '兑'),
|
||||
'001011': _HexagramShort('风山渐', '巽', '艮'),
|
||||
'100100': _HexagramShort('震为雷', '震', '震'),
|
||||
'000100': _HexagramShort('雷地豫', '震', '坤'),
|
||||
'010100': _HexagramShort('雷水解', '震', '坎'),
|
||||
'011100': _HexagramShort('雷风恒', '震', '巽'),
|
||||
'011000': _HexagramShort('地风升', '坤', '巽'),
|
||||
'011010': _HexagramShort('水风井', '坎', '巽'),
|
||||
'011110': _HexagramShort('泽风大过', '兑', '巽'),
|
||||
'100110': _HexagramShort('泽雷随', '兑', '震'),
|
||||
'011011': _HexagramShort('巽为风', '巽', '巽'),
|
||||
'111011': _HexagramShort('风天小畜', '巽', '乾'),
|
||||
'101011': _HexagramShort('风火家人', '巽', '离'),
|
||||
'100011': _HexagramShort('风雷益', '巽', '震'),
|
||||
'100111': _HexagramShort('天雷无妄', '乾', '震'),
|
||||
'100101': _HexagramShort('火雷噬嗑', '离', '震'),
|
||||
'100001': _HexagramShort('山雷颐', '艮', '震'),
|
||||
'011001': _HexagramShort('山风蛊', '艮', '巽'),
|
||||
'101101': _HexagramShort('离为火', '离', '离'),
|
||||
'001101': _HexagramShort('火山旅', '离', '艮'),
|
||||
'011101': _HexagramShort('火风鼎', '离', '巽'),
|
||||
'010101': _HexagramShort('火水未济', '离', '坎'),
|
||||
'010001': _HexagramShort('山水蒙', '艮', '坎'),
|
||||
'010011': _HexagramShort('风水涣', '巽', '坎'),
|
||||
'010111': _HexagramShort('天水讼', '乾', '坎'),
|
||||
'101111': _HexagramShort('天火同人', '乾', '离'),
|
||||
'000000': _HexagramShort('坤为地', '坤', '坤'),
|
||||
'100000': _HexagramShort('地雷复', '坤', '震'),
|
||||
'110000': _HexagramShort('地泽临', '坤', '兑'),
|
||||
'111000': _HexagramShort('地天泰', '坤', '乾'),
|
||||
'111100': _HexagramShort('雷天大壮', '震', '乾'),
|
||||
'111110': _HexagramShort('泽天夬', '兑', '乾'),
|
||||
'111010': _HexagramShort('水天需', '坎', '乾'),
|
||||
'000010': _HexagramShort('水地比', '坎', '坤'),
|
||||
'110110': _HexagramShort('兑为泽', '兑', '兑'),
|
||||
'010110': _HexagramShort('泽水困', '兑', '坎'),
|
||||
'000110': _HexagramShort('泽地萃', '兑', '坤'),
|
||||
'001110': _HexagramShort('泽山咸', '兑', '艮'),
|
||||
'001010': _HexagramShort('水山蹇', '坎', '艮'),
|
||||
'001000': _HexagramShort('地山谦', '坤', '艮'),
|
||||
'001100': _HexagramShort('雷山小过', '震', '艮'),
|
||||
'110100': _HexagramShort('雷泽归妹', '震', '兑'),
|
||||
};
|
||||
@@ -0,0 +1,147 @@
|
||||
import 'dart:math';
|
||||
|
||||
import '../../../../core/logging/logger.dart';
|
||||
import '../../../../core/network/api_problem.dart';
|
||||
import '../apis/divination_api.dart';
|
||||
import '../models/divination_backend_models.dart';
|
||||
import '../models/divination_params.dart';
|
||||
|
||||
class DivinationRunService {
|
||||
const DivinationRunService({required DivinationApi api}) : _api = api;
|
||||
|
||||
final DivinationApi _api;
|
||||
static final Logger _logger = getLogger('features.divination.run_service');
|
||||
|
||||
Future<DivinationRunAggregate> run({
|
||||
required DivinationParams params,
|
||||
required List<YaoType> yaoStates,
|
||||
void Function()? onDerived,
|
||||
void Function()? onTextMessageEnd,
|
||||
}) async {
|
||||
final threadId = _uuidV4();
|
||||
final runId = 'run_${DateTime.now().millisecondsSinceEpoch}';
|
||||
await _api.enqueueRun(
|
||||
params: params,
|
||||
yaoStates: yaoStates,
|
||||
threadId: threadId,
|
||||
runId: runId,
|
||||
);
|
||||
|
||||
DerivedDivinationData? derived;
|
||||
String signLevel = '';
|
||||
String summary = '';
|
||||
List<String> conclusion = const <String>[];
|
||||
List<String> focusPoints = const <String>[];
|
||||
List<String> advice = const <String>[];
|
||||
List<String> keywords = const <String>[];
|
||||
String answer = '';
|
||||
|
||||
await for (final event in _api.streamEvents(
|
||||
threadId: threadId,
|
||||
runId: runId,
|
||||
)) {
|
||||
final type = event['type'] as String? ?? '';
|
||||
_logger.debug(
|
||||
message: 'Received run SSE event',
|
||||
extra: <String, dynamic>{
|
||||
'threadId': threadId,
|
||||
'runId': runId,
|
||||
'type': type,
|
||||
},
|
||||
);
|
||||
if (type == 'DIVINATION_DERIVED') {
|
||||
final payload = event['divination'];
|
||||
if (payload is! Map<String, dynamic>) {
|
||||
throw const FormatException('DIVINATION_DERIVED 缺少 divination 结构');
|
||||
}
|
||||
derived = DerivedDivinationData.fromJson(payload);
|
||||
onDerived?.call();
|
||||
continue;
|
||||
}
|
||||
if (type == 'TEXT_MESSAGE_END') {
|
||||
signLevel = _requiredString(event, 'sign_level');
|
||||
summary = _requiredString(event, 'summary');
|
||||
conclusion = _requiredStringList(event, 'conclusion');
|
||||
focusPoints = _requiredStringList(event, 'focus_points');
|
||||
advice = _requiredStringList(event, 'advice');
|
||||
keywords = _requiredStringList(event, 'keywords');
|
||||
answer = _requiredString(event, 'answer');
|
||||
onTextMessageEnd?.call();
|
||||
continue;
|
||||
}
|
||||
if (type == 'RUN_ERROR') {
|
||||
_logger.warning(
|
||||
message: 'Run ended with RUN_ERROR event',
|
||||
extra: <String, dynamic>{
|
||||
'threadId': threadId,
|
||||
'runId': runId,
|
||||
'code': event['code'],
|
||||
'detail': event['detail'],
|
||||
'message': event['message'],
|
||||
},
|
||||
);
|
||||
throw ApiProblem(
|
||||
status: 500,
|
||||
title: 'Run failed',
|
||||
detail: event['detail'] as String? ?? '解卦失败',
|
||||
code: event['code'] as String?,
|
||||
);
|
||||
}
|
||||
if (type == 'RUN_FINISHED') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (derived == null) {
|
||||
_logger.warning(
|
||||
message: 'Missing DIVINATION_DERIVED before RUN_FINISHED',
|
||||
extra: <String, dynamic>{'threadId': threadId, 'runId': runId},
|
||||
);
|
||||
throw const FormatException('未收到 DIVINATION_DERIVED 事件');
|
||||
}
|
||||
|
||||
return DivinationRunAggregate(
|
||||
derived: derived,
|
||||
signLevel: signLevel,
|
||||
summary: summary,
|
||||
conclusion: conclusion,
|
||||
focusPoints: focusPoints,
|
||||
advice: advice,
|
||||
keywords: keywords,
|
||||
answer: answer,
|
||||
);
|
||||
}
|
||||
|
||||
String _requiredString(Map<String, dynamic> json, String key) {
|
||||
final value = json[key];
|
||||
if (value is! String || value.isEmpty) {
|
||||
throw FormatException('缺少字段: $key');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
List<String> _requiredStringList(Map<String, dynamic> json, String key) {
|
||||
final raw = json[key];
|
||||
if (raw is! List<dynamic>) {
|
||||
throw FormatException('缺少列表字段: $key');
|
||||
}
|
||||
return raw
|
||||
.map((item) {
|
||||
if (item is! String || item.isEmpty) {
|
||||
throw FormatException('字段 $key 存在非法项');
|
||||
}
|
||||
return item;
|
||||
})
|
||||
.toList(growable: false);
|
||||
}
|
||||
|
||||
String _uuidV4() {
|
||||
final random = Random.secure();
|
||||
final bytes = List<int>.generate(16, (_) => random.nextInt(256));
|
||||
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
||||
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
||||
String toHex(int b) => b.toRadixString(16).padLeft(2, '0');
|
||||
final b = bytes.map(toHex).toList(growable: false);
|
||||
return '${b[0]}${b[1]}${b[2]}${b[3]}-${b[4]}${b[5]}-${b[6]}${b[7]}-${b[8]}${b[9]}-${b[10]}${b[11]}${b[12]}${b[13]}${b[14]}${b[15]}';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user