feat: 接入起卦后端流程并完善积分扣减链路
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
# 摇卦页面日期时间选择器优化设计
|
||||
|
||||
## 1. 现状问题
|
||||
|
||||
1. **硬编码日期格式**:`DateFormat('yyyy年MM月dd日 HH:mm')` 在3处硬编码,未做 l10n
|
||||
- `auto_divination_screen.dart:353`
|
||||
- `manual_divination_screen.dart:271`
|
||||
- `divination_result_screen.dart:455`
|
||||
|
||||
2. **原生 picker 样式简陋**:使用 Material `showDatePicker` + `showTimePicker`,交互体验差
|
||||
|
||||
## 2. 优化方案
|
||||
|
||||
### 2.1 自定义底部弹层时间选择器
|
||||
|
||||
- 使用 `CupertinoDatePicker`(iOS 滚轮样式)替代原生 Material picker
|
||||
- 底部弹层,带半透明遮罩和圆角动画
|
||||
- 日期/时间在同一个 picker 内通过 SegmentedControl 切换
|
||||
|
||||
### 2.2 Locale-aware 日期格式化
|
||||
|
||||
使用 `intl` 包实现:
|
||||
- 中文 locale:`DateFormat.yMd('zh_CN').add_Hm()` → `2026年4月3日 14:30`
|
||||
- 英文 locale:`DateFormat.yMd('en').add_Hm()` → `4/3/2026 14:30`
|
||||
|
||||
### 2.3 新增 l10n 键值
|
||||
|
||||
已有键值:
|
||||
- `autoSelectTime`: "选择起卦时间" / "Select time"
|
||||
- `manualSelectTime`: "选择起卦时间" / "Select time"
|
||||
- `divinationModify`: "修改" / "Modify"
|
||||
|
||||
无需新增键值,日期格式完全由 `intl` 包根据 locale 自动处理。
|
||||
|
||||
## 3. 组件结构
|
||||
|
||||
```
|
||||
apps/lib/shared/widgets/
|
||||
└── date_time_picker/
|
||||
└── date_time_picker_bottom_sheet.dart # 弹层容器
|
||||
```
|
||||
|
||||
**DateTimePickerBottomSheet** 接口:
|
||||
```dart
|
||||
Future<DateTime?> showDateTimePickerBottomSheet({
|
||||
required BuildContext context,
|
||||
required DateTime initialDateTime,
|
||||
DateTime? minDateTime,
|
||||
DateTime? maxDateTime,
|
||||
});
|
||||
```
|
||||
|
||||
## 4. 交互流程
|
||||
|
||||
1. 用户点击"修改"按钮
|
||||
2. 底部弹出 `DateTimePickerBottomSheet`
|
||||
3. SegmentedControl 切换"日期"/"时间"tab
|
||||
4. Cupertino 滚轮选择值
|
||||
5. 点击"确认"关闭弹层并更新状态
|
||||
|
||||
## 5. 涉及的改动文件
|
||||
|
||||
### 新建
|
||||
- `apps/lib/shared/widgets/date_time_picker/date_time_picker_bottom_sheet.dart`
|
||||
|
||||
### 修改
|
||||
- `apps/lib/features/divination/presentation/screens/auto_divination_screen.dart`
|
||||
- `apps/lib/features/divination/presentation/screens/manual_divination_screen.dart`
|
||||
- `apps/lib/features/divination/presentation/screens/divination_result_screen.dart`
|
||||
|
||||
## 6. 验收标准
|
||||
|
||||
1. 日期格式跟随系统语言:中文环境显示中文格式,英文环境显示英文格式
|
||||
2. 选择器使用 iOS 滚轮样式
|
||||
3. 底部弹层带遮罩动画
|
||||
4. 原硬编码格式完全移除
|
||||
@@ -0,0 +1,341 @@
|
||||
# 日期时间选择器优化实现计划
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** 将摇卦页面的日期时间选择器改为 iOS 滚轮样式,并实现 locale-aware 格式化
|
||||
|
||||
**Architecture:** 创建共享的 `DateTimePickerBottomSheet` 组件,封装 `CupertinoDatePicker` 和底部弹层交互,替换现有的 `showDatePicker` + `showTimePicker` 调用
|
||||
|
||||
**Tech Stack:** Flutter, Cupertino widgets, intl package
|
||||
|
||||
---
|
||||
|
||||
## Task 1: 创建 DateTimePickerBottomSheet 组件
|
||||
|
||||
**Files:**
|
||||
- Create: `apps/lib/shared/widgets/date_time_picker/date_time_picker_bottom_sheet.dart`
|
||||
|
||||
**Step 1: 创建文件结构和基础代码**
|
||||
|
||||
```dart
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class DateTimePickerBottomSheet extends StatefulWidget {
|
||||
const DateTimePickerBottomSheet({
|
||||
super.key,
|
||||
required this.initialDateTime,
|
||||
this.minDateTime,
|
||||
this.maxDateTime,
|
||||
});
|
||||
|
||||
final DateTime initialDateTime;
|
||||
final DateTime? minDateTime;
|
||||
final DateTime? maxDateTime;
|
||||
|
||||
@override
|
||||
State<DateTimePickerBottomSheet> createState() => _DateTimePickerBottomSheetState();
|
||||
}
|
||||
|
||||
class _DateTimePickerBottomSheetState extends State<DateTimePickerBottomSheet> {
|
||||
late DateTime _selectedDateTime;
|
||||
int _selectedTab = 0; // 0=日期, 1=时间
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_selectedDateTime = widget.initialDateTime;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final locale = Localizations.localeOf(context);
|
||||
|
||||
return Container(
|
||||
height: 400,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// 顶部栏
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(l10n.cancel),
|
||||
),
|
||||
Text(
|
||||
l10n.autoSelectTime,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, _selectedDateTime),
|
||||
child: Text(l10n.confirm),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// SegmentedControl 切换日期/时间
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: CupertinoSlidingSegmentedControl<int>(
|
||||
groupValue: _selectedTab,
|
||||
children: {
|
||||
0: Text(l10n.dateTab),
|
||||
1: Text(l10n.timeTab),
|
||||
},
|
||||
onValueChanged: (value) => setState(() => _selectedTab = value ?? 0),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// CupertinoDatePicker
|
||||
Expanded(
|
||||
child: CupertinoDatePicker(
|
||||
mode: _selectedTab == 0
|
||||
? CupertinoDatePickerMode.date
|
||||
: CupertinoDatePickerMode.time,
|
||||
initialDateTime: _selectedDateTime,
|
||||
minimumDate: widget.minDateTime,
|
||||
maximumDate: widget.maxDateTime,
|
||||
onDateTimeChanged: (DateTime newDateTime) {
|
||||
setState(() => _selectedDateTime = newDateTime);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<DateTime?> showDateTimePickerBottomSheet({
|
||||
required BuildContext context,
|
||||
required DateTime initialDateTime,
|
||||
DateTime? minDateTime,
|
||||
DateTime? maxDateTime,
|
||||
}) {
|
||||
return showModalBottomSheet<DateTime>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) => DateTimePickerBottomSheet(
|
||||
initialDateTime: initialDateTime,
|
||||
minDateTime: minDateTime,
|
||||
maxDateTime: maxDateTime,
|
||||
),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: 添加 l10n 键值**
|
||||
|
||||
在 `apps/lib/l10n/app_zh.arb` 添加:
|
||||
```json
|
||||
"dateTab": "日期",
|
||||
"timeTab": "时间",
|
||||
"confirm": "确认",
|
||||
"cancel": "取消"
|
||||
```
|
||||
|
||||
在 `apps/lib/l10n/app_en.arb` 添加:
|
||||
```json
|
||||
"dateTab": "Date",
|
||||
"timeTab": "Time",
|
||||
"confirm": "Confirm",
|
||||
"cancel": "Cancel"
|
||||
```
|
||||
|
||||
运行 `flutter gen-l10n` 生成代码
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add apps/lib/shared/widgets/date_time_picker/ apps/lib/l10n/
|
||||
git commit -m "feat(divination): add DateTimePickerBottomSheet with iOS wheel style"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: 修改 auto_divination_screen.dart 使用新选择器
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/lib/features/divination/presentation/screens/auto_divination_screen.dart:208-230`
|
||||
- Modify: `apps/lib/features/divination/presentation/screens/auto_divination_screen.dart:353`
|
||||
|
||||
**Step 1: 添加 import**
|
||||
|
||||
在文件顶部添加:
|
||||
```dart
|
||||
import 'package:shared/widgets/date_time_picker/date_time_picker_bottom_sheet.dart';
|
||||
```
|
||||
|
||||
**Step 2: 修改 _pickTime 方法**
|
||||
|
||||
将:
|
||||
```dart
|
||||
Future<void> _pickTime() async {
|
||||
final date = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: _selectedTime,
|
||||
firstDate: DateTime(2000),
|
||||
lastDate: DateTime(2100),
|
||||
);
|
||||
if (date == null || !mounted) return;
|
||||
final time = await showTimePicker(
|
||||
context: context,
|
||||
initialTime: TimeOfDay.fromDateTime(_selectedTime),
|
||||
);
|
||||
if (time == null || !mounted) return;
|
||||
setState(() {
|
||||
_selectedTime = DateTime(
|
||||
date.year,
|
||||
date.month,
|
||||
date.day,
|
||||
time.hour,
|
||||
time.minute,
|
||||
);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
替换为:
|
||||
```dart
|
||||
Future<void> _pickTime() async {
|
||||
final result = await showDateTimePickerBottomSheet(
|
||||
context: context,
|
||||
initialDateTime: _selectedTime,
|
||||
minDateTime: DateTime(2000),
|
||||
maxDateTime: DateTime(2100),
|
||||
);
|
||||
if (result == null || !mounted) return;
|
||||
setState(() {
|
||||
_selectedTime = result;
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**Step 3: 修改日期显示格式**
|
||||
|
||||
将:
|
||||
```dart
|
||||
DateFormat('yyyy年MM月dd日 HH:mm').format(selectedTime)
|
||||
```
|
||||
|
||||
替换为:
|
||||
```dart
|
||||
DateFormat.yMd(Localizations.localeOf(context).toString()).add_Hm().format(selectedTime)
|
||||
```
|
||||
|
||||
需要添加 import:
|
||||
```dart
|
||||
import 'package:intl/intl.dart';
|
||||
```
|
||||
|
||||
**Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add apps/lib/features/divination/presentation/screens/auto_divination_screen.dart
|
||||
git commit -m "feat(divination): use DateTimePickerBottomSheet in auto_divination_screen"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: 修改 manual_divination_screen.dart 使用新选择器
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/lib/features/divination/presentation/screens/manual_divination_screen.dart:142-168`
|
||||
- Modify: `apps/lib/features/divination/presentation/screens/manual_divination_screen.dart:271`
|
||||
|
||||
**Step 1: 添加 import**
|
||||
|
||||
```dart
|
||||
import 'package:shared/widgets/date_time_picker/date_time_picker_bottom_sheet.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
```
|
||||
|
||||
**Step 2: 修改 _pickTime 方法和日期显示格式**
|
||||
|
||||
同 Task 2 的修改方式
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add apps/lib/features/divination/presentation/screens/manual_divination_screen.dart
|
||||
git commit -m "feat(divination): use DateTimePickerBottomSheet in manual_divination_screen"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: 修改 divination_result_screen.dart 的日期格式
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/lib/features/divination/presentation/screens/divination_result_screen.dart:455-457`
|
||||
|
||||
**Step 1: 添加 import**
|
||||
|
||||
```dart
|
||||
import 'package:intl/intl.dart';
|
||||
```
|
||||
|
||||
**Step 2: 修改日期格式**
|
||||
|
||||
将:
|
||||
```dart
|
||||
DateFormat(
|
||||
'yyyy年MM月dd日 HH:mm',
|
||||
).format(data.params.divinationTime),
|
||||
```
|
||||
|
||||
替换为:
|
||||
```dart
|
||||
DateFormat.yMd(Localizations.localeOf(context).toString()).add_Hm().format(data.params.divinationTime),
|
||||
```
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add apps/lib/features/divination/presentation/screens/divination_result_screen.dart
|
||||
git commit -m "refactor(divination): use locale-aware date format in divination_result_screen"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: 运行验证
|
||||
|
||||
**Step 1: 生成 l10n**
|
||||
|
||||
```bash
|
||||
cd apps && flutter gen-l10n
|
||||
```
|
||||
|
||||
**Step 2: 运行静态分析**
|
||||
|
||||
```bash
|
||||
cd apps && flutter analyze
|
||||
```
|
||||
|
||||
预期: 无错误
|
||||
|
||||
**Step 3: 运行相关测试**
|
||||
|
||||
```bash
|
||||
cd apps && flutter test test/features/divination/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Plan complete.** Two execution options:
|
||||
|
||||
**1. Subagent-Driven (this session)** - I dispatch fresh subagent per task, review between tasks, fast iteration
|
||||
|
||||
**2. Parallel Session (separate)** - Open new session with executing-plans, batch execution with checkpoints
|
||||
|
||||
**Which approach?**
|
||||
@@ -24,6 +24,7 @@ This document is the source of truth for backend RFC7807 `code` values consumed
|
||||
| code | status | meaning | frontend handling |
|
||||
|---|---:|---|---|
|
||||
| `AGENT_SESSION_RUN_LIMIT_EXCEEDED` | 409 | Session already reached max run count (start + 3 follow-ups) | Show run-limit message and require starting a new session |
|
||||
| `AGENT_DIVINATION_PAYLOAD_REQUIRED` | 422 | Missing required `forwardedProps.divinationPayload` in run request | Prompt user to restart casting flow and resubmit |
|
||||
|
||||
Compatibility strategy:
|
||||
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
# Divination Run Protocol (Frontend <-> Backend)
|
||||
|
||||
This document defines the structured contract for divination run input, backend hexagram derivation, and run event output.
|
||||
|
||||
Protocol verification status:
|
||||
|
||||
- Backend route source: `backend/src/v1/agent/router.py`
|
||||
- Backend derivation source: `backend/src/core/divination/derivation.py`
|
||||
- Runtime payload schema source: `backend/src/schemas/domain/divination.py`
|
||||
|
||||
## Compatibility strategy
|
||||
|
||||
- Current strategy: additive evolution only.
|
||||
- Existing required fields cannot be removed or renamed without migration notes.
|
||||
- Canonical divination terminology values must remain Chinese.
|
||||
|
||||
## Route overview
|
||||
|
||||
- Submit run: `POST /api/v1/agent/runs`
|
||||
- Stream events: `GET /api/v1/agent/runs/{threadId}/events?runId=...`
|
||||
|
||||
## Run request contract
|
||||
|
||||
`RunAgentInput` uses AG-UI shape. This protocol constrains two sections:
|
||||
|
||||
1) `messages[0].content` (question text)
|
||||
2) `forwardedProps.divinationPayload` (structured divination input)
|
||||
|
||||
### Required request shape
|
||||
|
||||
```json
|
||||
{
|
||||
"threadId": "uuid",
|
||||
"runId": "run_20260403_xxx",
|
||||
"state": {},
|
||||
"messages": [
|
||||
{
|
||||
"id": "msg_run_20260403_xxx_user_0",
|
||||
"role": "user",
|
||||
"content": "我最近换工作是否合适?"
|
||||
}
|
||||
],
|
||||
"tools": [],
|
||||
"context": [],
|
||||
"forwardedProps": {
|
||||
"runtime_mode": "chat",
|
||||
"client_time": {
|
||||
"device_timezone": "Asia/Shanghai",
|
||||
"client_now_iso": "2026-04-03T20:30:00+08:00",
|
||||
"client_epoch_ms": 1775219400000
|
||||
},
|
||||
"divinationPayload": {
|
||||
"divinationMethod": "手动起卦",
|
||||
"questionType": "事业",
|
||||
"question": "我最近换工作是否合适?",
|
||||
"divinationTimeIso": "2026-04-03T20:30:00+08:00",
|
||||
"yaoLines": [
|
||||
"少阳",
|
||||
"少阴",
|
||||
"老阳",
|
||||
"少阴",
|
||||
"少阳",
|
||||
"老阴"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### AG-UI required base fields
|
||||
|
||||
- `state`: required object, frontend sends `{}` by default.
|
||||
- `messages[0].id`: required string id for user message.
|
||||
- `tools`: required array, frontend sends empty array when no tools requested.
|
||||
- `context`: required array, frontend sends empty array when no extra context.
|
||||
|
||||
### `divinationPayload` strict rules
|
||||
|
||||
- `divinationMethod`: enum, allowed values `手动起卦 | 自动起卦`
|
||||
- `questionType`: non-empty string, recommended Chinese category labels
|
||||
- `question`: non-empty string
|
||||
- `divinationTimeIso`: RFC3339 datetime with timezone offset
|
||||
- `yaoLines`: exactly 6 items, order is `初爻 -> 上爻`
|
||||
- `yaoLines` item enum: `少阳 | 少阴 | 老阳 | 老阴`
|
||||
- Additional fields are forbidden.
|
||||
|
||||
## Event output contract
|
||||
|
||||
During run streaming, backend emits standard AG-UI lifecycle events and two divination-relevant payload events:
|
||||
|
||||
### 1) `DIVINATION_DERIVED`
|
||||
|
||||
- Emitted once after backend derives hexagram data.
|
||||
- Payload field: `divination` (strict object).
|
||||
|
||||
`divination` object:
|
||||
|
||||
```json
|
||||
{
|
||||
"question": "我最近换工作是否合适?",
|
||||
"questionType": "事业",
|
||||
"divinationMethod": "手动起卦",
|
||||
"divinationTime": "2026年04月03日 20:30",
|
||||
"binaryCode": "101001",
|
||||
"changedBinaryCode": "100001",
|
||||
"guaName": "山火贲",
|
||||
"upperName": "艮",
|
||||
"lowerName": "离",
|
||||
"targetGuaName": "山雷颐",
|
||||
"worldPosition": 1,
|
||||
"responsePosition": 4,
|
||||
"hasChangingYao": true,
|
||||
"ganzhi": {
|
||||
"yearGanZhi": "丙午",
|
||||
"monthGanZhi": "壬辰",
|
||||
"dayGanZhi": "辛亥",
|
||||
"timeGanZhi": "乙巳",
|
||||
"yearKongWang": "子丑",
|
||||
"monthKongWang": "午未",
|
||||
"dayKongWang": "寅卯",
|
||||
"timeKongWang": "戌亥",
|
||||
"yueJian": "辰土",
|
||||
"riChen": "亥水",
|
||||
"yuePo": "戌土",
|
||||
"riChong": "巳火"
|
||||
},
|
||||
"wuXingStatuses": {
|
||||
"木": "囚",
|
||||
"火": "休",
|
||||
"土": "旺",
|
||||
"金": "相",
|
||||
"水": "死"
|
||||
},
|
||||
"yaoInfoList": [
|
||||
{
|
||||
"position": 1,
|
||||
"spiritName": "虎",
|
||||
"relationName": "官鬼",
|
||||
"tiganName": "卯",
|
||||
"elementName": "木",
|
||||
"isYang": true,
|
||||
"isChanging": false,
|
||||
"specialMark": "世"
|
||||
}
|
||||
],
|
||||
"targetYaoInfoList": [],
|
||||
"fushenPositions": [2],
|
||||
"fushenInfoList": [
|
||||
{
|
||||
"position": 2,
|
||||
"relationName": "父母",
|
||||
"tiganName": "午",
|
||||
"elementName": "火"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 2) `TEXT_MESSAGE_END`
|
||||
|
||||
- Standard final answer event.
|
||||
- Existing fields remain canonical: `sign_level`, `summary`, `conclusion`, `focus_points`, `advice`, `keywords`, `answer`.
|
||||
|
||||
Frontend should combine:
|
||||
|
||||
- structural divination data from `DIVINATION_DERIVED`
|
||||
- interpretation text from `TEXT_MESSAGE_END`
|
||||
|
||||
## Error contract linkage
|
||||
|
||||
- All errors use RFC7807 with extension `code` and optional `params`.
|
||||
- Error code registry source: `docs/protocols/common/http-error-codes.md`.
|
||||
@@ -0,0 +1,48 @@
|
||||
# Points Balance Protocol (Frontend <-> Backend)
|
||||
|
||||
This document defines the read-only points balance contract for authenticated users.
|
||||
|
||||
Protocol verification status:
|
||||
|
||||
- Backend route source: `backend/src/v1/points/router.py`
|
||||
- Backend service source: `backend/src/v1/points/service.py`
|
||||
- Response schema source: `backend/src/v1/points/schemas.py`
|
||||
|
||||
## Compatibility strategy
|
||||
|
||||
- Additive evolution only.
|
||||
- Existing response fields are stable and must remain backward-compatible.
|
||||
|
||||
## Route
|
||||
|
||||
- `GET /api/v1/points/balance`
|
||||
|
||||
## Authorization
|
||||
|
||||
- Requires authenticated session.
|
||||
- User identity is derived from verified backend auth context.
|
||||
|
||||
## Response (200)
|
||||
|
||||
```json
|
||||
{
|
||||
"balance": 120,
|
||||
"frozenBalance": 20,
|
||||
"availableBalance": 100,
|
||||
"runCost": 20,
|
||||
"canRun": true
|
||||
}
|
||||
```
|
||||
|
||||
Field rules:
|
||||
|
||||
- `balance`: integer `>= 0`
|
||||
- `frozenBalance`: integer `>= 0`
|
||||
- `availableBalance`: integer `>= 0`, computed as `max(balance - frozenBalance, 0)`
|
||||
- `runCost`: integer `> 0`, current value `20`
|
||||
- `canRun`: boolean, equivalent to `availableBalance >= runCost`
|
||||
|
||||
## Error contract linkage
|
||||
|
||||
- RFC7807 + extension `code`, optional `params`.
|
||||
- Shared registry: `docs/protocols/common/http-error-codes.md`.
|
||||
Reference in New Issue
Block a user