feat: 实现用户画像、占卜历史与后端用户管理模块

This commit is contained in:
ZL-Q
2026-04-06 01:28:10 +08:00
parent d87b2e1e3a
commit 8a18b3528b
77 changed files with 5850 additions and 2604 deletions
@@ -1,76 +0,0 @@
# 摇卦页面日期时间选择器优化设计
## 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. 原硬编码格式完全移除
@@ -1,341 +0,0 @@
# 日期时间选择器优化实现计划
> **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?**
@@ -1,505 +0,0 @@
# Eryao 用户档案/积分/会话数据模型设计
日期:2026-04-03
状态:已确认(待实现)
## 1. 目标与范围
本设计用于 Eryao 后端新增并对齐以下 5 张表:
1. 用户档案表:`profiles`
2. 用户积分表:`user_points`
3. 积分流水表:`points_ledger`
4. 会话表:`sessions`
5. 对话历史表:`messages`
来源原则:
- `profiles``sessions``messages` 参考并吸收 `social-app` 现有设计。
- 会话能力按“结构完整复制,但业务先停用 automation”执行。
- 本文档为设计方案,不包含迁移脚本与代码实现。
## 2. 关键确认项
### 2.1 profiles.username 不做唯一约束
已确认:`profiles.username` **不需要唯一**
设计落地:
- 不创建 `UNIQUE(username)` 约束。
- 可保留普通索引 `ix_profiles_username` 以支持检索。
- 若后续产品要支持“唯一用户名登录/提及”,另行引入唯一标识字段(例如 `handle`)。
### 2.2 settings 需要 JSONB 模板
`profiles.settings` 使用 `jsonb not null default '{}'::jsonb`,并约定版本化模板:
```json
{
"version": 1,
"preferences": {
"interface_language": "zh-CN",
"ai_language": "zh-CN",
"timezone": "Asia/Shanghai",
"country": "CN"
},
"privacy": {
"profile_visibility": "public",
},
"notification": {
"push_enabled": true,
},
}
```
说明:
- `version` 为配置结构版本,后续结构升级通过版本迁移处理。
- `timezone` 作为运行时时区回退来源之一。
- `default_runtime_mode` 当前仅允许 `chat` 生效。
## 3. 表结构设计
## 3.1 profiles(吸收 social-app
核心字段:
- `id uuid primary key`(外键指向 `auth.users(id)``on delete cascade`
- `username varchar(30) not null`(非唯一)
- `avatar_url text null`
- `bio varchar(200) null`
- `settings jsonb not null default '{}'::jsonb`
- `created_at timestamptz not null default now()`
- `updated_at timestamptz not null default now()`
- `deleted_at timestamptz null`
索引建议:
- `ix_profiles_username (username)`
- `ix_profiles_settings_gin using gin(settings)`
初始化建议:
-`auth.users` 建立注册触发器,自动插入 profile 默认记录。
- `settings` 初始化值应写入上述模板(而非空对象)。
## 3.2 user_points(用户积分账户)
职责:保存用户积分余额与累计统计,1 用户 1 行。
核心字段:
- `user_id uuid primary key`FK `auth.users(id)`
- `balance bigint not null default 0`
- `frozen_balance bigint not null default 0`
- `lifetime_earned bigint not null default 0`
- `lifetime_spent bigint not null default 0`
- `version int not null default 0`
- `updated_at timestamptz not null default now()`
约束建议:
- `check (balance >= 0)`
- `check (frozen_balance >= 0)`
- `check (lifetime_earned >= 0)`
- `check (lifetime_spent >= 0)`
## 3.3 points_ledger(积分流水)
职责:记录每次积分变更,支持审计、对账、幂等。
核心字段:
- `id uuid primary key`
- `user_id uuid not null`FK `auth.users(id)`
- `direction smallint not null`1 增加,-1 减少)
- `amount bigint not null`
- `balance_after bigint not null`
- `change_type varchar(16) not null`(约束:`register/consume/grant/adjust`
- `biz_type varchar(16) not null`(约束:当前仅 `chat`
- `biz_id uuid not null`(当前语义:指向 `sessions.id`
- `event_id varchar(64) not null`
- `operator_id uuid null`
- `metadata jsonb not null default '{}'::jsonb`
- `created_at timestamptz not null default now()`
约束与索引建议:
- `check (amount > 0)`
- `check (direction in (1, -1))`
- `check (change_type in ('register', 'consume', 'grant', 'adjust'))`
- `check (biz_type = 'chat')`
- `foreign key (biz_id) references sessions(id)`
- `unique (user_id, event_id)`(用户维度幂等)
- `index (user_id, created_at desc)`
- `index (biz_type, biz_id)`
## 3.4 sessions(完整复制结构,先停用 automation
来源:`social-app``sessions` 表结构。
核心字段:
- `id uuid primary key`
- `user_id uuid not null`
- `session_type varchar(20) not null`(结构保留 `chat/automation`
- `job_id uuid null`
- `title varchar(255) null`
- `status varchar(20) not null`
- `last_activity_at timestamptz not null default now()`
- `message_count int not null default 0`
- `total_tokens int not null default 0`
- `total_cost numeric(12,6) not null default 0`
- `state_snapshot jsonb null`
- `created_at/updated_at/deleted_at`
业务启用策略(当前阶段):
- 应用层仅允许 `session_type='chat'`
- 应用层要求 `job_id is null`
- 数据结构不删减,保留未来 automation 扩展能力。
## 3.5 messages(完整复制结构)
来源:`social-app``messages` 表结构。
核心字段:
- `id uuid primary key`
- `session_id uuid not null`FK `sessions(id)``on delete cascade`
- `seq int not null`
- `role varchar(20) not null``user/assistant/system/tool`
- `content text not null`
- `model_code varchar(50) null`
- `tool_name varchar(100) null`
- `input_tokens int not null default 0`
- `output_tokens int not null default 0`
- `cost numeric(12,6) not null default 0`
- `latency_ms int null`
- `visibility_mask bigint not null default 0`
- `metadata jsonb null`
- `created_at/updated_at/deleted_at`
约束与索引建议:
- `unique (session_id, seq)`
- `index (session_id)`
- `index (session_id, seq, visibility_mask)`
## 4. 一致性与事务约定
- 积分变更必须在单事务内同时更新:`user_points` + `points_ledger`
- 通过 `event_id` 做幂等写保护,避免重试导致重复扣发。
- `sessions.total_tokens/total_cost/message_count` 作为聚合字段,由写消息流程维护。
## 5. 安全与权限
- 所有业务写入走后端服务层,不信任客户端传入 `owner_id/user_id`
- 表级策略沿用项目约定(RLS + 服务端授权控制)。
- `metadata/settings` 禁止写入密钥类敏感信息。
## 6. 兼容与演进
- 本期兼容策略:新增表/字段为主,不做破坏式变更。
- automation 能力延后启用,仅在业务层放开,不需变更当前 DDL。
- 若后续需要唯一用户名,应新增独立唯一字段,不直接改造 `username` 历史数据。
## 7. 关于“用户实际成本核算表”的结论
结论:建议二期引入,不阻塞本期 5 张表上线。
理由:
- 本期已有 `messages.cost``sessions.total_cost`,可支持展示级统计。
- 若进入财务对账、补贴结算、重算审计场景,需要独立不可变成本流水表。
建议二期最小表:`user_cost_ledger`,记录 provider/model/tokens/raw_cost/billable_cost/event_id。
## 8. 字段释义(5 张表逐字段)
本节作为实施、联调、排障时的字段字典,避免同名字段被不同团队误读。
### 8.1 profiles
- `id`:用户主键,直接对应 `auth.users.id`,生命周期与认证用户绑定。
- `username`:展示名/昵称,不承担唯一身份语义。
- `avatar_url`:头像地址。
- `bio`:用户简介。
- `settings`:用户配置 JSON,承载语言、时区、隐私、通知等可扩展偏好。
- `created_at`:记录创建时间。
- `updated_at`:最近一次更新记录时间。
- `deleted_at`:软删除时间,`null` 表示有效。
### 8.2 user_points
- `user_id`:积分账户所属用户,1:1 对应 `auth.users.id`
- `balance`:当前可计入总余额的积分值(含可用与冻结)。
- `frozen_balance`:冻结中的积分,暂不可消费。
- `lifetime_earned`:历史累计获得积分(单调递增)。
- `lifetime_spent`:历史累计消费积分(单调递增)。
- `version`:乐观锁版本号,用于并发更新防冲突。
- `updated_at`:积分账户最近一次变更时间。
### 8.3 points_ledger
- `id`:流水主键。
- `user_id`:该条积分流水所属用户。
- `direction`:变更方向,`1` 表示加分,`-1` 表示扣分。
- `amount`:变更绝对值,始终为正数。
- `balance_after`:本次变更完成后的账户余额快照。
- `change_type`:变更分类,仅允许 `register/consume/grant/adjust`
- `biz_type`:业务域类型,当前固定 `chat`
- `biz_id`:业务侧引用 ID,当前固定引用 `sessions.id`
- `event_id`:幂等事件 ID,同一用户下不可重复。
- `operator_id`:操作人(系统/管理员/服务账号)用户 ID,可空。
- `metadata`:扩展信息 JSON(上下文参数、备注、来源等)。
- `created_at`:流水写入时间。
### 8.4 sessions
- `id`:会话主键。
- `user_id`:会话所属用户。
- `session_type`:会话类型,当前只启用 `chat`,结构保留 `automation`
- `job_id`:自动化任务 ID(当前阶段应为 `null`)。
- `title`:会话标题。
- `status`:会话状态(如 active/archived/closed)。
- `last_activity_at`:最近活动时间,用于排序与回收策略。
- `message_count`:消息总数聚合值。
- `total_tokens`:会话累计 token 聚合值。
- `total_cost`:会话累计成本聚合值。
- `state_snapshot`:会话状态快照(用于上下文恢复/调试)。
- `created_at`:创建时间。
- `updated_at`:更新时间。
- `deleted_at`:软删除时间。
### 8.5 messages
- `id`:消息主键。
- `session_id`:所属会话 ID,级联删除。
- `seq`:会话内消息序号(从小到大单调)。
- `role`:消息角色(`user/assistant/system/tool`)。
- `content`:消息主体文本。
- `model_code`:生成该消息的模型标识。
- `tool_name`:工具消息对应工具名。
- `input_tokens`:本条请求输入 token。
- `output_tokens`:本条响应输出 token。
- `cost`:本条消息成本。
- `latency_ms`:本条消息处理耗时(毫秒)。
- `visibility_mask`:可见性位掩码,用于多视图过滤。
- `metadata`:扩展信息 JSON。
- `created_at`:创建时间。
- `updated_at`:更新时间。
- `deleted_at`:软删除时间。
## 9. 审查结论(重点:user_points / points_ledger
结论:当前字段集可支撑一期上线,但若目标是“高并发 + 强审计 + 低误用”,建议在 DDL 层补 4 项硬约束、1 项审计字段,能显著降低后续事故概率。
### 9.1 user_points 审查
现状可用点:
- 账户余额、冻结、累计收支、版本号齐全,满足账户模型最小闭环。
- 非负约束已覆盖核心数值字段,能防止明显脏数据。
主要风险与建议:
1. 缺少 `frozen_balance <= balance` 约束。
- 风险:可能出现“冻结金额大于总余额”的不合法状态。
- 建议:新增 `check (frozen_balance <= balance)`
2. 缺少 `created_at`
- 风险:无法直接追溯账户初始化时间,审计链不完整。
- 建议:新增 `created_at timestamptz not null default now()`
3. 并发写依赖应用层版本控制,需明确 SQL 写法。
- 风险:若更新语句未携带 `version` 条件,可能发生覆盖写。
- 建议:约定更新模板 `... where user_id=? and version=?`,成功后 `version=version+1`
### 9.2 points_ledger 审查
现状可用点:
- `direction + amount + balance_after + event_id` 组合,已具备审计、幂等、对账基础能力。
- `(user_id, event_id)` 唯一约束符合“同一用户维度幂等”场景。
主要风险与建议:
1. 缺少 `balance_after >= 0` 约束。
- 风险:极端并发或逻辑 bug 时可能落负余额快照。
- 建议:新增 `check (balance_after >= 0)`
2. `operator_id` 未声明外键语义。
- 风险:排障时难确认操作者主体是否存在。
- 建议:若业务允许,增加 FK `operator_id -> auth.users(id)`(可 `on delete set null`)。
3. `change_type/biz_type` 为自由文本。
- 风险:枚举漂移(同义不同写)导致统计口径分裂。
- 建议:通过 `check in (...)` 或字典表约束可选值。
4. 缺少“业务发生时间”字段。
- 风险:`created_at` 仅表示入库时间,异步补偿场景下难对齐业务时序。
- 建议:二期可加 `occurred_at timestamptz`
### 9.3 一期最低增强清单(建议)
若只做最小改动,优先加以下 5 项:
1. `user_points`: `check (frozen_balance <= balance)`
2. `user_points`: `created_at timestamptz not null default now()`
3. `points_ledger`: `check (balance_after >= 0)`
4. `points_ledger`: 明确 `operator_id` 外键策略。
5. 统一 `change_type/biz_type` 枚举口径(约束或字典表)。
## 10. points_ledger 约束模型(定稿草案)
本节将 `change_type``biz_type``metadata` 固化为可执行约束,作为后续 DDL 实现依据。
### 10.1 change_type / biz_type / biz_id 约束
- `change_type``register | consume | grant | adjust`
- `biz_type`:当前仅允许 `chat`
- `biz_id``uuid not null`,并 `FK -> sessions(id)`
配套业务约束建议:
- `register/grant` 必须 `direction = 1`
- `consume` 必须 `direction = -1`
- `adjust` 允许 `direction in (1, -1)`
建议 SQL(可直接迁移化):
```sql
alter table points_ledger
add constraint ck_points_ledger_change_type
check (change_type in ('register', 'consume', 'grant', 'adjust')),
add constraint ck_points_ledger_biz_type
check (biz_type = 'chat'),
add constraint ck_points_ledger_direction_by_change_type
check (
(change_type in ('register', 'grant') and direction = 1)
or (change_type = 'consume' and direction = -1)
or (change_type = 'adjust' and direction in (1, -1))
),
add constraint fk_points_ledger_biz_session
foreign key (biz_id) references sessions(id);
```
### 10.2 metadata 结构(基于现有 chat 数据的定制模型)
设计依据(来自当前代码里的真实字段):
- `messages.metadata` 已稳定存在 `run_id`(见 `AgentChatMessageMetadata.run_id`)。
- `messages` 表已有计费上下文列:`id/seq/model_code/input_tokens/output_tokens/cost`
- chat 业务主键是 `session_id`,本设计里已对应 `points_ledger.biz_id`
因此,`points_ledger.metadata` 不再使用泛化字段,直接锚定现有运行时和消息数据:
```json
{
"schema_version": 1,
"reason_code": "REGISTER_WELCOME|CHAT_CONSUME|CHAT_GRANT|CHAT_ADJUST",
"operator_type": "user|system|admin",
"run_id": "string",
"request_id": "string|null",
"charge": {
"message_id": "uuid",
"message_seq": 1,
"model_code": "string",
"input_tokens": 0,
"output_tokens": 0,
"cost": "0.000000"
},
"ext": {}
}
```
字段说明(按现有数据来源):
- `schema_version`:固定 `1`
- `reason_code`:固定业务原因码,不允许自由文本。
- `operator_type`:与 `operator_id` 搭配使用,表达操作者身份类型。
- `run_id`:来自 agent 运行主键(`messages.metadata.run_id` 同源)。
- `request_id`:来自 `X-Request-ID`(可空,排障用)。
- `charge`:消费/赠金/调整时的“消息快照”,字段全部来自 `messages` 现有列。
- `ext`:仅允许对象,承载少量扩展审计信息(如工单号)。
`change_type` 的必填规则(不是通用模板,直接按你当前业务):
- `register`:必须有 `reason_code/operator_type/run_id``charge` 必须不存在。
- `consume`:必须有 `reason_code/operator_type/run_id/charge`,且 `charge.message_id/message_seq/model_code/input_tokens/output_tokens/cost` 全必填。
- `grant`:必须有 `reason_code/operator_type/run_id`;若是“按会话补偿赠金”,允许并建议带 `charge`
- `adjust`:必须有 `reason_code/operator_type/run_id``ext.ticket_id``charge` 可选。
建议 SQL(JSON 约束可执行最小集):
```sql
alter table points_ledger
add constraint ck_points_ledger_metadata_object
check (jsonb_typeof(metadata) = 'object'),
add constraint ck_points_ledger_metadata_common
check (
metadata->>'schema_version' = '1'
and metadata->>'reason_code' in ('REGISTER_WELCOME', 'CHAT_CONSUME', 'CHAT_GRANT', 'CHAT_ADJUST')
and metadata->>'operator_type' in ('user', 'system', 'admin')
and coalesce(metadata->>'run_id', '') <> ''
and (not (metadata ? 'ext') or jsonb_typeof(metadata->'ext') = 'object')
),
add constraint ck_points_ledger_metadata_register_shape
check (
change_type <> 'register'
or (
metadata->>'reason_code' = 'REGISTER_WELCOME'
and not (metadata ? 'charge')
)
),
add constraint ck_points_ledger_metadata_consume_shape
check (
change_type <> 'consume'
or (
metadata->>'reason_code' = 'CHAT_CONSUME'
and (metadata ? 'charge')
and jsonb_typeof(metadata->'charge') = 'object'
and (metadata->'charge' ? 'message_id')
and (metadata->'charge' ? 'message_seq')
and (metadata->'charge' ? 'model_code')
and (metadata->'charge' ? 'input_tokens')
and (metadata->'charge' ? 'output_tokens')
and (metadata->'charge' ? 'cost')
)
),
add constraint ck_points_ledger_metadata_adjust_shape
check (
change_type <> 'adjust'
or (
metadata->>'reason_code' = 'CHAT_ADJUST'
and (metadata ? 'ext')
and (metadata->'ext' ? 'ticket_id')
and coalesce(metadata #>> '{ext,ticket_id}', '') <> ''
)
);
```
可选强化(建议二期加触发器,而不是只靠 CHECK):
- 校验 `metadata.charge.message_id` 真正存在于 `messages.id`,且 `messages.session_id = points_ledger.biz_id`
- 校验 `metadata.charge.message_seq` 与该 `message_id` 的真实 `seq` 一致。
### 10.3 operator_id 与 created_by/updated_by 是否重复
不重复,语义不同:
- `operator_id`:业务操作者(“谁触发了积分变更”),是业务审计字段。
- `created_by/updated_by`:数据行审计字段(“谁写了这条数据库记录”)。
`points_ledger`(不可变流水)而言:
- `updated_by` 基本无意义(流水不应更新)。
- `created_by` 常等于服务账号,无法表达真实业务操作者。
- 因此保留 `operator_id` 是必要的,且建议允许空值(纯系统任务)。
推荐实践:
- `points_ledger`:保留 `operator_id`,不强制引入 `created_by/updated_by`
- `user_points`:如项目需要统一审计基类,可在账户表引入 `updated_by`,但不替代流水里的 `operator_id`
@@ -0,0 +1,241 @@
# Eryao 解卦历史与个人档案后端单一数据源改造计划
日期:2026-04-05
状态:评审中(未开始编码)
## 1. 背景与目标
当前移动端存在两类不符合目标架构的问题:
1. 个人档案(昵称、简介、头像)仍有前端本地状态路径,非后端权威数据源。
2. 首页历史解卦无法稳定由后端快照直接重建结果页,前端被迫做本地兜底。
本计划目标:
- 实现“后端为唯一数据源,前端仅缓存”。
-`DIVINATION_DERIVED` 的完整结构进入消息 `metadata.agent_output` 并持久化。
- 历史接口返回可被前端直接解析的结构化 assistant 输出(不再依赖 `ui_schema`)。
- 个人档案全链路后端化,头像使用 `avatars` bucket。
非目标:
- 本计划不直接提交代码实现。
- 本计划不包含 UI 视觉细节改稿。
## 2. 现状核对(基于仓库代码)
### 2.1 历史接口与消息转换
- 历史接口:`GET /api/v1/agent/history`,定义于 `backend/src/v1/agent/router.py`
- 当前转换逻辑在 `backend/src/v1/agent/utils.py`
- `user` 消息主要输出 `content``attachments`
- `assistant` 消息默认走 `ui_hints -> ui_schema` 编译路径。
- 历史响应结构 `HistoryMessage` 当前包含 `ui_schema`,不直接暴露结构化 `agent_output`
### 2.2 DIVINATION_DERIVED 与落库断点
- 运行时会发出 `DIVINATION_DERIVED`(见 `backend/src/core/agentscope/runtime/runner.py`)。
- 消息落库由 `backend/src/core/agentscope/events/store.py` 负责。
- 当前 `TEXT_MESSAGE_END` 持久化字段包含 `sign_level/summary/.../ui_hints`,未包含 `divination` 结构。
- 结果:历史快照难以完整重建结果页结构。
### 2.3 Profile 与头像
- 后端配置已有 `storage.avatar.bucket`,默认 `avatars``backend/src/core/config/settings.py`)。
- 当前 `v1` 仅挂载 `auth/agent/points` 路由(`backend/src/v1/router.py`),尚无 profile 专用路由。
## 3. 核心设计决策
### 决策 A:把 `divination_derived` 放入 `metadata.agent_output`
-`AgentOutput` 增加字段 `divination_derived`(强类型,禁止裸 `dict`)。
- 事件落库时把 `DIVINATION_DERIVED` 内容并入 assistant 的 `metadata.agent_output.divination_derived`
-`sign_level/summary/advice/...` 同时持久化,形成一条可回放的 assistant 结构化输出。
理由:
- 最小改动复用现有消息表,不新增历史结果表即可满足回放需求。
- 前端可直接从历史响应解析结果页,避免本地拼装。
### 决策 B:历史接口返回 `assistant.agent_output`,移除 `ui_schema`
- `HistoryMessage` 改为:
- `user`: `content + attachments`
- `assistant`: `content + agent_output`
- `ui_schema` 从接口协议中移除(迁移自通用模块的历史遗留,不在本项目范围)。
理由:
- 减少中间编译层,契约更稳定、语义更清晰。
- 前端直接消费业务数据,不依赖通用 UI 编译器。
### 决策 CProfile 全后端化 + 头像对象存储
- 新增 users/profile API,前端只保留缓存层。
- 头像上传走预签名 URL,bucket 固定 `avatars`,路径按用户隔离。
## 4. 协议与接口计划(先文档,后实现)
## 4.1 新增/修改协议文档
按“协议先行”更新以下文档:
1. `docs/protocols/divination/divination-run-protocol.md`
- 增补:历史回放时 assistant `agent_output.divination_derived` 的字段契约。
- 标记:`ui_schema` 已废弃并移除。
2. 新增:`docs/protocols/profile/profile-protocol.md`
- 定义 profile 读写与头像上传签名协议。
3. 如涉及错误码新增,更新:
- `docs/protocols/common/http-error-codes.md`
### 4.2 后端 API 契约(目标)
#### A. 历史快照(改造)
- `GET /api/v1/agent/history`
- 响应中 assistant 消息新增(或替换为)`agent_output`
- `sign_level`
- `summary`
- `conclusion`
- `focus_points`
- `advice`
- `keywords`
- `answer`
- `divination_derived`(完整卦象结构)
#### B. Profile(新增)
- `GET /api/v1/users/me/profile`
- `PATCH /api/v1/users/me/profile`
- `POST /api/v1/users/me/avatar/upload-url`
- (可选)`GET /api/v1/users/me/avatar/signed-url`
#### C. 头像上传约束
- bucket 固定:`config.storage.avatar.bucket`
- 路径前缀建议:`avatars/{user_id}/...`
- 文件类型:`image/png|image/jpeg|image/webp`
- 体积上限:`config.storage.avatar.max_size_mb`
## 5. 数据模型改造计划
### 5.1 Runtime 模型
- 文件:`backend/src/schemas/agent/runtime_models.py`
- 变更:`AgentOutput` 增加 `divination_derived` 字段(类型复用 `schemas/domain/divination.py`)。
- 规则:保持 `extra="forbid"`,禁止无类型漂移。
### 5.2 事件到落库链路
- 文件:`backend/src/core/agentscope/runtime/stage_emitter.py`
- `TEXT_MESSAGE_END` payload 带上 `divination_derived`
- 文件:`backend/src/core/agentscope/events/store.py`
- `worker_output_fields` 纳入 `divination_derived` 并写入 `metadata.agent_output`
### 5.3 历史响应转换
- 文件:`backend/src/v1/agent/utils.py`
- 删除 `ui_hints -> ui_schema` 编译路径。
- assistant 消息改为抽取并返回受控 `agent_output`
- 文件:`backend/src/v1/agent/schemas.py`
- `HistoryMessage` 改字段定义(去 `ui_schema`,加 `agent_output`)。
## 6. 前端消费与缓存策略
### 6.1 历史与结果页
- 历史列表数据源改为后端 `agent/history`
- 点开历史项时:
- 直接解析 `assistant.agent_output.divination_derived` + 解释文本字段。
- 本地仅做缓存,不做真源 fallback。
### 6.2 Profile
- 设置页资料读取改为 `GET /users/me/profile`
- 编辑资料写入 `PATCH /users/me/profile`
- 头像更新走 upload-url + 上传 + profile 更新引用路径。
### 6.3 点数
- 保持后端余额接口作为权威数据源(现有已接)。
- 前端只做短期缓存,解卦完成后强制 refresh。
## 7. 代码清理边界(你关心的“删除通用遗留”)
原则:先去引用,再删定义,最后删文件,避免误删。
分三步:
1. 第一阶段(本次改造内)
- 删除 `agent/history``ui_schema` 的输出与依赖。
- 删除前端对 `ui_schema` 的消费路径(若存在)。
2. 第二阶段(安全清理)
- 搜索 `schemas/domain``schemas/agent/ui_hints` 的实际引用。
- 对“零引用 + 非协议字段”进行清理。
3. 第三阶段(文档与测试补齐)
- 更新协议文档、错误码、回归测试。
备注:
- 不建议在同一 PR 里“功能改造 + 大规模 schema 删除”,建议拆成两个 PR,降低回归风险。
## 8. 测试计划(必须项)
### 8.1 后端单元/集成
1. `TEXT_MESSAGE_END` 持久化:`metadata.agent_output.divination_derived` 落库断言。
2. `GET /api/v1/agent/history`assistant 返回 `agent_output`,且不再返回 `ui_schema`
3. 历史分页与 owner 校验不回退。
4. profile API:读写、权限、字段约束、头像路径安全性。
5. 头像签名 URLbucket/path/mime/size 约束。
### 8.2 前端
1. 历史列表从后端数据渲染。
2. 点击历史项成功进入结果页,字段一致性校验。
3. profile 页面读写闭环(昵称/简介/头像)。
4. 点数刷新与缓存失效策略验证。
## 9. 风险与回滚
主要风险:
- 历史消息中旧数据可能没有 `divination_derived`,前端需兼容空值。
- `ui_schema` 下线后,若有隐藏调用方会断。
回滚策略:
- 协议层采用短期双读兼容窗口(仅过渡期):
- 新字段优先;旧字段仅用于读,不再写。
- 若线上异常,先回滚 history 响应变更,再保持落库新增字段不删。
## 10. 实施顺序(最小风险)
1. 协议文档更新并评审通过。
2. 后端:`AgentOutput` + 事件落库 + history 响应新增 `agent_output`(先加后切)。
3. 前端:改消费到 `agent_output`,移除本地真源。
4. 后端:移除 `ui_schema` 输出。
5. profile API + 前端接入头像上传。
6. 清理无用 schema(独立 PR)。
## 11. 验收标准(DoD
全部满足才算完成:
1. 解卦后写入的 assistant 消息在 DB 中可见 `metadata.agent_output.divination_derived`
2. 首页历史完全来自后端,清空本地缓存后仍可正确展示。
3. 历史详情可完整还原结果页,不依赖 `ui_schema`
4. profile 读写走后端,头像实际落 `avatars` bucket。
5. 前端不再把 profile/history 作为本地权威数据源。
6. 协议文档与实现一致,相关测试通过。
## 12. GSTACK REVIEW REPORT
| Review | Trigger | Why | Runs | Status | Findings |
|--------|---------|-----|------|--------|----------|
| Eng Review | `/plan-eng-review` | 锁定架构、契约、测试闭环 | 1 | Done | 确认后端单一数据源方向;建议分阶段移除 `ui_schema` 并将 schema 清理拆分独立 PR |
| CEO Review | `/plan-ceo-review` | 范围与优先级 | 0 | — | — |
| Design Review | `/plan-design-review` | UI/UX 风险 | 0 | — | — |
| DX Review | `/plan-devex-review` | 开发体验风险 | 0 | — | — |
VERDICT:可以进入实现阶段,但必须先完成协议文档更新并冻结字段契约。
@@ -0,0 +1,403 @@
# Eryao 工程计划:历史解卦与个人档案后端化(单一数据源)
日期:2026-04-05
状态:规划中(Planning Only
## 0. 约束与决策前提
本计划基于已确认前提:
1. 当前无生产兼容压力,旧字段可直接不兼容。
2. 前端只做缓存层,不做权威数据源。
3. `ui_schema` 属于通用迁移遗留,不在本项目范围,目标是移除。
4. 头像存储必须使用 `avatars` bucket`config.storage.avatar.bucket`)。
---
## 1. 目标
在不引入额外业务表的前提下,完成以下工程目标:
1. assistant 消息落库时,`metadata.agent_output` 持久化完整 `divination_derived`
2. `GET /api/v1/agent/history` 返回前端可直接消费的 `assistant.agent_output`(移除 `ui_schema`)。
3. 新增 profile 后端 API,前端设置页改为后端读写。
4. 头像上传改为预签名 + `avatars` bucket,后端校验路径和类型。
---
## 2. 系统边界与职责
### 2.1 边界图
```text
[Flutter App]
| Auth Token
v
[API Router v1]
|---- /agent/runs + /agent/history
|---- /users/me/profile + /users/me/avatar/upload-url
v
[Service Layer]
|---- AgentService: 会话、历史、消息转换
|---- UserProfileService: 档案读写、头像签名
v
[Repository Layer]
|---- sessions/messages/profiles CRUD
v
[Postgres + Supabase Storage]
|---- messages.metadata_json
|---- profiles
|---- bucket: avatars
```
### 2.2 分层职责
- Router:参数校验、鉴权入口、RFC7807 错误转换。
- Service:业务规则与信任边界控制。
- Repository:纯查询和写入,不做鉴权决策。
- Schema:协议强类型、禁止松散 dict 漂移。
---
## 3. 数据流设计
## 3.1 解卦写入链路(新增 `divination_derived`
```text
POST /agent/runs
-> Runner emit DIVINATION_DERIVED(divination)
-> StageEmitter merge into TEXT_MESSAGE_END payload
-> EventStore picks worker_output_fields
-> metadata.agent_output.divination_derived persisted
-> messages.metadata_json
```
### 关键点
1. `AgentOutput` 增加 `divination_derived` 强类型字段。
2. `EventStore` 字段白名单纳入 `divination_derived`
3. `extra="forbid"` 保留,防止脏字段入库。
## 3.2 历史读取链路(移除 `ui_schema`
```text
GET /agent/history
-> AgentService.get_history_snapshot
-> convert_message_to_history
user -> content + attachments
assistant -> content + agent_output
-> HistoryMessage response
```
### 关键点
1. 停止 `ui_hints -> ui_schema` 编译。
2. assistant 返回受控 `agent_output` 子集,不透传任意 metadata。
3. 前端结果页以 `agent_output.divination_derived` 为主数据源。
## 3.3 Profile 与头像链路
```text
GET /users/me/profile
-> read profiles
PATCH /users/me/profile
-> validate payload
-> update profiles
POST /users/me/avatar/upload-url
-> validate mime/size/path
-> create signed upload url (bucket=avatars)
```
---
## 4. API 契约(冻结版)
## 4.1 History 响应(目标结构)
```json
{
"scope": "history_day",
"threadId": "uuid",
"day": "2026-04-05",
"hasMore": false,
"messages": [
{
"id": "uuid",
"seq": 12,
"role": "assistant",
"content": "...",
"timestamp": "2026-04-05T12:34:56Z",
"agent_output": {
"sign_level": "中上签",
"summary": "...",
"conclusion": ["..."],
"focus_points": ["..."],
"advice": ["..."],
"keywords": ["..."],
"answer": "...",
"divination_derived": {
"binaryCode": "101001",
"changedBinaryCode": "100001",
"guaName": "...",
"targetGuaName": "...",
"ganzhi": {},
"yaoInfoList": []
}
}
}
]
}
```
说明:
- 本接口不再返回 `ui_schema`
- user 消息仍可返回 `attachments`
## 4.2 Profile API
### `GET /api/v1/users/me/profile`
```json
{
"user_id": "uuid",
"display_name": "string",
"bio": "string",
"avatar_path": "avatars/{user_id}/...",
"avatar_url": "https://...",
"updated_at": "..."
}
```
### `PATCH /api/v1/users/me/profile`
请求:
```json
{
"display_name": "string<=30",
"bio": "string<=200",
"avatar_path": "avatars/{user_id}/..."
}
```
### `POST /api/v1/users/me/avatar/upload-url`
请求:
```json
{
"mime_type": "image/png",
"file_size": 123456,
"ext": "png"
}
```
响应:
```json
{
"bucket": "avatars",
"path": "avatars/{user_id}/{uuid}.png",
"upload_url": "https://...",
"expires_in": 600
}
```
---
## 5. 信任边界与安全规则
1. `user_id` 只能取 JWT `sub`,禁止客户端传 owner。
2. 头像 path 必须前缀匹配:`avatars/{current_user.id}/`
3. bucket 必须等于 `config.storage.avatar.bucket`
4. mime 白名单:`image/png|image/jpeg|image/webp`
5. size 上限:`config.storage.avatar.max_size_mb`
6. history 读取严格校验 session owner。
7. 错误统一 RFC7807 + `code`
---
## 6. 失败模式与处理
## 6.1 消息落库阶段
1. `divination_derived` 校验失败
- 行为:拒绝写入该字段并记录结构化日志。
- 错误码:`AGENT_OUTPUT_DIVINATION_INVALID`(新)。
2. TEXT_MESSAGE_END 缺失关键字段
- 行为:整条 assistant 消息按失败路径处理,不写半残对象。
## 6.2 history 读取阶段
1. `agent_output` 缺失或损坏
- 行为:assistant 消息返回 `content`,并标记 `agent_output=null`
- 前端:展示“历史记录不完整”提示,不崩溃。
2. 非 owner 访问
- 行为:403`code=AGENT_SESSION_FORBIDDEN`
## 6.3 头像上传阶段
1. bucket/path 越权
- 422`AVATAR_PATH_SCOPE_INVALID`
2. mime/size 非法
- 422`AVATAR_FILE_INVALID`
3. storage 签名失败
- 502`AVATAR_SIGNED_URL_FAILED`
---
## 7. 关键边缘场景
1. 用户连续点击“保存资料”两次:
- 以后端最后一次写入为准,前端按钮防抖。
2. 上传头像成功但 profile 更新失败:
- 前端重试 profile PATCH,不重复上传。
3. history 返回空列表:
- 前端展示空态,不触发本地假数据。
4. 助手消息存在但缺 `divination_derived`
- 卡片可展示摘要,不允许进入完整结果页。
5. 解卦完成后 history 立即读取:
- 允许短暂读到旧快照,前端做一次重拉。
---
## 8. 技术取舍
### 方案 A(推荐):在现有 messages.metadata 扩展
- 优点:
- 最小变更,不新增表。
- 复用当前会话与历史体系。
- 缺点:
- metadata 体积增大,需要关注单条消息大小。
### 方案 B:新增 `divination_results` 独立表
- 优点:
- 结构更纯,查询更明确。
- 缺点:
- 迁移、回写、关联复杂度明显增加。
结论:
- 当前阶段选 A,满足速度与复杂度平衡。
---
## 9. 实施切片(按风险顺序)
### Slice 1:协议与 schema
1. 更新协议文档:history + profile + 错误码。
2. 更新 `AgentOutput` 模型字段。
### Slice 2:写链路改造
1. runner/emitter/store 打通 `divination_derived` 落库。
2. 增加单元测试与集成测试。
### Slice 3:读链路改造
1. history 转换改为返回 `agent_output`
2. 移除 `ui_schema` 响应字段。
### Slice 4profile API + 头像
1. users 路由、service、schema。
2. 头像 upload-url 接口。
### Slice 5:前端切换
1. 历史列表/详情改消费后端 `agent_output`
2. 设置页改 profile 接口。
3. 清理本地真源。
---
## 10. 测试覆盖计划
## 10.1 后端测试矩阵
### A. AgentOutput 落库
1. `divination_derived` 正常写入。
2. `divination_derived` 非法结构拒绝写入。
### B. history 接口
1. assistant 返回 `agent_output`
2. 响应不含 `ui_schema`
3. 非 owner 403。
4. 空历史返回空数组。
### C. profile 接口
1. GET 返回当前用户档案。
2. PATCH 字段边界(空、超长、非法字符)。
3. 并发 PATCH 最终一致性。
### D. avatar upload-url
1. 合法 mime/size/path 成功签名。
2. bucket/path 越权失败。
3. mime/size 超限失败。
4. storage 异常返回 502 问题体。
## 10.2 前端测试矩阵
1. history 列表从接口渲染。
2. 点击历史项进入结果页并解析 `divination_derived`
3. profile 读写回显。
4. 头像上传后刷新显示。
5. 异常提示(网络失败、数据缺失)不崩溃。
---
## 11. 可观测性
新增日志字段建议:
1. history 响应统计:`thread_id`, `message_count`, `assistant_with_agent_output_count`
2. profile 更新:`user_id`, `updated_fields`
3. avatar 签名:`user_id`, `mime_type`, `file_size`, `success/failure_code`
指标建议:
1. `history_agent_output_missing_rate`
2. `avatar_upload_url_failure_rate`
3. `profile_patch_error_rate`
---
## 12. 风险与回滚
### 风险
1. 单条 metadata 变大,可能影响查询性能。
2. 前端解析新结构时存在字段名误配风险。
### 回滚
1. 若读链路异常,先回滚 history 输出层(保持落库不回滚)。
2. profile 接口异常时,可临时只读禁写,保护账户信息。
---
## 13. 验收标准(Done
1. 新产生 assistant 消息均含 `metadata.agent_output.divination_derived`
2. history 接口返回 `agent_output`,且不再返回 `ui_schema`
3. 前端历史页与结果页不依赖本地真源。
4. profile 读写和头像上传全走后端。
5. 测试矩阵项全部落地并通过。
---
## 14. NOT in Scope
1. 大规模清理 `backend/src/schemas/domain/**`
2. 历史数据回填脚本。
3. 新增独立 `divination_results` 表。
+17
View File
@@ -25,6 +25,23 @@ This document is the source of truth for backend RFC7807 `code` values consumed
|---|---:|---|---|
| `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 |
| `AGENT_OUTPUT_DIVINATION_INVALID` | 422 | Worker output contains invalid `divination_derived` payload shape | Show generic history parse error and suggest retrying latest run |
## Profile
| code | status | meaning | frontend handling |
|---|---:|---|---|
| `PROFILE_PAYLOAD_INVALID` | 422 | Profile update payload invalid (length/type/empty constraints) | Highlight invalid fields and block submit |
| `PROFILE_NOT_FOUND` | 404 | User profile row missing | Show retry and optionally trigger profile bootstrap |
## Avatar
| code | status | meaning | frontend handling |
|---|---:|---|---|
| `AVATAR_FILE_INVALID` | 422 | Avatar mime type or size is invalid | Show file validation hint and ask user to pick another image |
| `AVATAR_PATH_SCOPE_INVALID` | 422 | Avatar path does not belong to current user scope | Show generic security error and force refresh |
| `AVATAR_SIGNED_URL_FAILED` | 502 | Backend failed to generate avatar signed upload URL | Show retry toast and keep previous avatar |
| `AVATAR_UPLOAD_FAILED` | 502 | Backend failed to upload avatar bytes to storage | Show retry toast and keep previous avatar |
Compatibility strategy:
@@ -18,6 +18,7 @@ Protocol verification status:
- Submit run: `POST /api/v1/agent/runs`
- Stream events: `GET /api/v1/agent/runs/{threadId}/events?runId=...`
- History snapshot: `GET /api/v1/agent/history`
## Run request contract
@@ -166,6 +167,73 @@ Frontend should combine:
- structural divination data from `DIVINATION_DERIVED`
- interpretation text from `TEXT_MESSAGE_END`
## History snapshot contract
`GET /api/v1/agent/history` is the canonical replay source for frontend history list and result reconstruction.
### Required response shape
```json
{
"scope": "history_day",
"threadId": "uuid|null",
"day": "2026-04-05|null",
"hasMore": false,
"messages": [
{
"id": "uuid",
"seq": 12,
"role": "assistant",
"content": "...",
"timestamp": "2026-04-05T12:34:56+00:00",
"agent_output": {
"status": "success",
"sign_level": "中上签",
"summary": "...",
"conclusion": ["..."],
"focus_points": ["..."],
"advice": ["..."],
"keywords": ["..."],
"answer": "...",
"key_points": ["..."],
"result_type": "structured_payload",
"suggested_actions": ["..."],
"divination_derived": {
"binaryCode": "101001",
"changedBinaryCode": "100001",
"guaName": "山火贲"
}
}
},
{
"id": "uuid",
"seq": 11,
"role": "user",
"content": "我最近换工作是否合适?",
"timestamp": "2026-04-05T12:34:12+00:00",
"attachments": [
{
"mimeType": "image/png",
"url": "https://...signed..."
}
]
}
]
}
```
Rules:
- `assistant` message MUST provide `agent_output` when backend has valid worker output metadata.
- `agent_output.divination_derived` uses the same shape as `DIVINATION_DERIVED.divination` payload.
- Frontend reconstructs divination result page from `agent_output` data, not from local mock data.
- `agent_output.sign_level` allowed values: `上上签` / `中上签` / `中下签` / `下下签`.
### Breaking change note
- `ui_schema` is removed from history response and is no longer part of this project protocol.
- This repository currently accepts non-backward-compatible protocol evolution (no production compatibility burden).
## Error contract linkage
- All errors use RFC7807 with extension `code` and optional `params`.
+143
View File
@@ -0,0 +1,143 @@
# Profile Protocol (Frontend <-> Backend)
This document defines the canonical backend contract for user profile read/write and avatar upload signing.
Protocol verification status:
- Backend model source: `backend/src/models/profile.py`
- Storage config source: `backend/src/core/config/settings.py`
- Current status: planned
## Compatibility strategy
- Current strategy: breaking changes allowed during implementation phase (no production compatibility burden).
- Once production compatibility is required, switch to additive-only evolution.
## Route overview
- Get profile: `GET /api/v1/users/me/profile`
- Update profile: `PATCH /api/v1/users/me/profile`
- Create avatar upload url: `POST /api/v1/users/me/avatar/upload-url`
- Upload avatar directly: `POST /api/v1/users/me/avatar` (multipart)
## Auth and trust boundary
- All routes require authenticated user context.
- `user_id` is derived from verified JWT `sub`; never accepted from client payload.
## Profile read contract
### `GET /api/v1/users/me/profile`
Response:
```json
{
"user_id": "uuid",
"display_name": "string",
"bio": "string|null",
"avatar_path": "avatars/{user_id}/{file}",
"avatar_url": "https://...signed-or-public...",
"settings": {
"version": 1,
"preferences": {
"interface_language": "zh-CN",
"ai_language": "zh-CN",
"timezone": "Asia/Shanghai",
"country": "CN"
},
"privacy": {},
"notification": {}
},
"updated_at": "2026-04-05T12:34:56+00:00"
}
```
Mapping note:
- `display_name` maps to `profiles.username`.
- `avatar_path` is stored in profile layer.
- `avatar_url` is render-ready URL generated from storage strategy.
## Profile update contract
### `PATCH /api/v1/users/me/profile`
Request:
```json
{
"display_name": "string(1..30)",
"bio": "string(0..200)",
"avatar_path": "avatars/{user_id}/{file}"
}
```
Rules:
- At least one field must be provided.
- `display_name` must be non-empty after trim.
- `bio` can be empty string and should be normalized to `null` only if agreed by API implementation.
- `avatar_path` must stay in current user prefix: `avatars/{current_user.id}/`.
Response:
- Returns the same shape as `GET /users/me/profile`.
## Avatar upload signing contract
### `POST /api/v1/users/me/avatar/upload-url`
Request:
```json
{
"mime_type": "image/png|image/jpeg|image/webp",
"file_size": 123456,
"ext": "png|jpg|jpeg|webp"
}
```
Response:
```json
{
"bucket": "avatars",
"path": "avatars/{user_id}/{uuid}.png",
"upload_url": "https://...signed...",
"expires_in": 600
}
```
Validation rules:
- `bucket` must equal `config.storage.avatar.bucket`.
- `file_size` must be `>0` and `<= config.storage.avatar.max_size_mb`.
- Only image mime types are allowed.
- Path must be server-generated and never trusted from client.
## Direct avatar upload contract
### `POST /api/v1/users/me/avatar`
Request:
- `multipart/form-data`
- field name: `file`
Validation rules:
- extension must be one of `png|jpg|jpeg|webp`
- mime must map to image type (`image/png|image/jpeg|image/webp`)
- payload size must be `<= config.storage.avatar.max_size_mb`
Behavior:
- backend writes avatar bytes to `bucket=config.storage.avatar.bucket`
- backend stores canonical path in profile
- response returns latest profile payload (`ProfileResponse`)
## Error contract linkage
- All errors must follow RFC7807 `application/problem+json`.
- `code` values must be registered in `docs/protocols/common/http-error-codes.md`.
-366
View File
@@ -1,366 +0,0 @@
# 后端服务功能模块
## 1. 用户认证模块 (`/auth`)
### 1.1 发送验证码
- **路径**: `POST /auth/send-code`
- **功能**: 向用户手机号发送短信验证码
- **参数**: `phoneNumber` (手机号)
- **依赖**: 阿里云短信服务
### 1.2 验证码登录
- **路径**: `POST /auth/login`
- **功能**: 使用手机号+验证码登录,返回用户信息和Token
- **参数**: `phoneNumber`, `code`
- **返回**: `userId`, `phoneNumber`, `token`
### 1.3 验证Token
- **路径**: `POST /auth/validate-token`
- **功能**: 验证Token有效性,自动刷新即将过期的Token
- **参数**: `token`
### 1.4 刷新Token
- **路径**: `POST /auth/refresh-token`
- **功能**: 刷新用户Token
- **参数**: `token`
### 1.5 注销登录
- **路径**: `POST /auth/logout`
- **功能**: 删除Token,注销登录
- **参数**: `token`
---
## 2. 用户资料模块 (`/user`)
### 2.1 获取用户资料
- **路径**: `GET /user/profile`
- **参数**: `id` (用户ID)
- **返回**: 昵称、性别、生日、个性签名
### 2.2 更新用户资料
- **路径**: `PUT /user/profile`
- **功能**: 更新用户资料(含敏感词检测)
- **参数**: `id`, `nickname`, `gender`, `birthday`, `signature`
### 2.3 单独更新昵称
- **路径**: `PUT /user/nickname`
- **功能**: 更新用户昵称(含敏感词检测)
- **参数**: `userId`, `nickname`
### 2.4 单独更新签名
- **路径**: `PUT /user/signature`
- **功能**: 更新用户个性签名(含敏感词检测)
- **参数**: `userId`, `signature`
---
## 3. 铜钱系统模块 (`/coin`)
### 3.1 查询余额
- **路径**: `GET /coin/balance`
- **参数**: `userId`
### 3.2 按手机号查询余额
- **路径**: `GET /coin/balance/phone`
- **参数**: `phoneNumber`
### 3.3 消费铜钱
- **路径**: `POST /coin/consume`
- **参数**: `userId`
### 3.4 按手机号消费铜钱
- **路径**: `POST /coin/consume/phone`
- **参数**: `phoneNumber`
### 3.5 重置余额
- **路径**: `POST /coin/reset`
- **功能**: 重置用户铜钱余额为0(用于注销)
- **参数**: `userId`
### 3.6 按手机号重置余额
- **路径**: `POST /coin/reset/phone`
- **参数**: `phoneNumber`
### 3.7 增加铜钱
- **路径**: `POST /coin/increase/phone`
- **参数**: `phoneNumber`, `coinCount`
### 3.8 同步用户铜钱
- **路径**: `POST /coin/sync`
- **功能**: 手动触发用户铜钱记录同步
---
## 4. 支付模块 (`/payment`)
### 4.1 获取支付宝订单
- **路径**: `GET /payment/alipay/order`
- **参数**: `userId`, `amount`, `coinCount`
- **返回**: 支付宝支付订单信息
### 4.2 支付宝异步通知
- **路径**: `POST /payment/notify`
- **功能**: 处理支付宝异步回调通知
- **返回**: `success``fail`
### 4.3 更新余额
- **路径**: `POST /payment/update-balance`
- **功能**: 处理支付结果,更新用户铜钱余额
- **参数**: `userId`, `orderNo`, `tradeNo`, `amount`, `coinCount`, `status`
---
## 5. 卦象历史模块 (`/divination-history`)
### 5.1 保存卦象记录
- **路径**: `POST /divination-history/save`
- **参数**: `userId`, `phoneNumber`, `localRecordId`, `jsonData`, `aiResult`, `questionType`, `question`, `timestamp`
### 5.2 获取卦象记录
- **路径**: `POST /divination-history/get`
- **参数**: `phoneNumber`, `questionType` (可选)
### 5.3 删除卦象记录
- **路径**: `POST /divination-history/delete`
- **参数**: `phoneNumber`, `localRecordId``localRecordIds`
### 5.4 统计记录数量
- **路径**: `GET /divination-history/count/{phoneNumber}`
### 5.5 批量软删除
- **路径**: `POST /divination-history/deactivate-all`
- **功能**: 用户注销时批量软删除卦象记录
- **参数**: `phoneNumber`, `userId`
---
## 6. 解卦溯源模块 (`/divination`)
### 6.1 增强解卦
- **路径**: `POST /divination/enhanced`
- **功能**: 调用DeepSeek API进行解卦,支持用户追踪
- **参数**: `userId`, `questionType`, `question`, `divinationData`
### 6.2 查询用户解卦记录
- **路径**: `GET /divination/records/user/{userId}`
- **参数**: `page`, `size`
### 6.3 按手机号查询记录
- **路径**: `GET /divination/records/phone/{phoneNumber}`
- **参数**: `page`, `size`
### 6.4 按追踪ID查询
- **路径**: `GET /divination/records/trace/{traceId}`
### 6.5 时间范围查询
- **路径**: `GET /divination/records/user/{userId}/daterange`
- **参数**: `startTime`, `endTime`, `page`, `size`
### 6.6 查询失败记录
- **路径**: `GET /divination/records/failed`
### 6.7 查询慢请求
- **路径**: `GET /divination/records/slow`
- **参数**: `durationMs` (默认10000ms)
### 6.8 统计用户解卦次数
- **路径**: `GET /divination/stats/user/{userId}/count`
### 6.9 统计时间范围内记录数
- **路径**: `GET /divination/stats/daterange/count`
- **参数**: `startTime`, `endTime`
---
## 7. DeepSeek代理模块 (`/deepseek`)
### 7.1 AI聊天代理
- **路径**: `POST /deepseek/chat`
- **功能**: 代理DeepSeek API,自动附加用户信息
- **参数**: 聊天请求体
---
## 8. 内容审核模块 (`/content-moderation`)
### 8.1 检测问题内容
- **路径**: `POST /content-moderation/check-question`
- **功能**: 检测用户问题是否包含敏感词
- **参数**: `userId`, `question`
---
## 9. 敏感词管理模块 (`/admin/sensitive-words`)
### 9.1 获取统计信息
- **路径**: `GET /admin/sensitive-words/statistics`
### 9.2 添加敏感词
- **路径**: `POST /admin/sensitive-words/add`
- **参数**: `word`, `type`
### 9.3 移除敏感词
- **路径**: `DELETE /admin/sensitive-words/remove`
- **参数**: `word`
### 9.4 测试检测
- **路径**: `POST /admin/sensitive-words/test/nickname`
- **路径**: `POST /admin/sensitive-words/test/signature`
- **路径**: `POST /admin/sensitive-words/test/question`
- **参数**: `content`
---
## 10. 违规记录管理模块 (`/admin/violations`)
### 10.1 获取违规记录列表
- **路径**: `GET /admin/violations/list`
- **参数**: `page`, `size`, `userId`, `contentType`, `violationType`, `startTime`, `endTime`
### 10.2 用户违规统计
- **路径**: `GET /admin/violations/user/{userId}/stats`
### 10.3 违规类型统计
- **路径**: `GET /admin/violations/stats/types`
### 10.4 高频违规用户
- **路径**: `GET /admin/violations/frequent-violators`
- **参数**: `days`, `threshold`
### 10.5 清理违规记录
- **路径**: `DELETE /admin/violations/cleanup`
- **参数**: `daysToKeep`
### 10.6 获取违规详情
- **路径**: `GET /admin/violations/{id}`
---
## 11. 敏感词迁移管理模块 (`/admin/sensitive-word-migration`)
### 11.1 获取配置状态
- **路径**: `GET /admin/sensitive-word-migration/status`
### 11.2 切换服务
- **路径**: `POST /admin/sensitive-word-migration/switch`
- **功能**: 切换本地词库/阿里云服务
- **参数**: `useAliyun`
### 11.3 设置降级策略
- **路径**: `POST /admin/sensitive-word-migration/fallback`
- **参数**: `enableFallback`
### 11.4 对比测试
- **路径**: `POST /admin/sensitive-word-migration/compare`
- **功能**: 对比本地和阿里云检测结果
- **参数**: `content`, `contentType`, `userId`
### 11.5 批量对比测试
- **路径**: `POST /admin/sensitive-word-migration/batch-compare`
### 11.6 健康检查
- **路径**: `GET /admin/sensitive-word-migration/health-check`
---
## 12. 通知模块 (`/notifications`)
### 12.1 获取最新通知
- **路径**: `GET /notifications/latest`
### 12.2 获取所有通知
- **路径**: `GET /notifications/all`
---
## 13. 用户反馈模块 (`/feedback`)
### 13.1 提交反馈
- **路径**: `POST /feedback`
- **参数**: `user_id`, `phone_number`, `content`
---
## 14. 版本管理模块 (`/version`)
### 14.1 检查版本更新
- **路径**: `POST /version/check`
- **参数**: `clientVersion`, `clientVersionCode`
### 14.2 获取最新版本
- **路径**: `GET /version/latest`
---
## 15. 网络访问日志模块 (`/admin/network-logs`)
### 15.1 按用户查询日志
- **路径**: `GET /admin/network-logs/user/{userId}`
### 15.2 按IP查询日志
- **路径**: `GET /admin/network-logs/ip/{clientIp}`
### 15.3 按时间范围查询
- **路径**: `GET /admin/network-logs/time-range`
### 15.4 查询失败记录
- **路径**: `GET /admin/network-logs/failed`
### 15.5 检测可疑IP
- **路径**: `GET /admin/network-logs/suspicious`
### 15.6 统计IP访问次数
- **路径**: `GET /admin/network-logs/count/ip`
### 15.7 清理过期日志
- **路径**: `DELETE /admin/network-logs/cleanup`
---
## 16. 用户数据管理模块 (`/admin/user-data`)
### 16.1 同步用户数据
- **路径**: `POST /admin/user-data/sync`
### 16.2 验证用户数据
- **路径**: `GET /admin/user-data/validate/user/{userId}`
- **路径**: `GET /admin/user-data/validate/phone/{phoneNumber}`
### 16.3 修复数据一致性
- **路径**: `POST /admin/user-data/fix/{phoneNumber}`
### 16.4 批量验证
- **路径**: `POST /admin/user-data/validate/batch`
### 16.5 测试用户信息
- **路径**: `GET /admin/user-data/test/user-info/{userId}`
---
## 17. 数据清理模块 (`/admin/data-cleanup`)
### 17.1 清理所有表
- **路径**: `DELETE /admin/data-cleanup/all`
### 17.2 清理验证码
- **路径**: `DELETE /admin/data-cleanup/verification-codes`
### 17.3 清理支付记录
- **路径**: `DELETE /admin/data-cleanup/payment-records`
### 17.4 清理反馈记录
- **路径**: `DELETE /admin/data-cleanup/feedback`
---
## 第三方服务集成
| 服务 | 用途 | 配置 |
|------|------|------|
| 阿里云短信 | 发送验证码 | `aliyun.sms.*` |
| 阿里云内容安全 | 敏感词检测 | `aliyun.content-security.*` |
| DeepSeek API | AI解卦/聊天 | `thirdparty.deepseek.api-key` |
| 支付宝 | 支付充值 | `alipay.*` |
| MySQL | 数据持久化 | `spring.datasource.*` |
| Redis | Token缓存/会话 | `spring.data.redis.*` |
@@ -1,202 +0,0 @@
# 算卦 Agent API Reference
## 1. API Endpoint
- **URL**: `POST https://meeyao.com.cn/api/deepseek/chat`
- **认证**: 需要通过 `AuthInterceptor` 注入用户 token
---
## 2. 请求结构
### 2.1 DeepSeekRequest (请求体)
```kotlin
data class DeepSeekRequest(
val model: String = "deepseek-chat",
val messages: List<DeepSeekMessage>,
val temperature: Double = 0.7,
val max_tokens: Int = 2048,
val stream: Boolean = false
)
```
### 2.2 DeepSeekMessage
```kotlin
data class DeepSeekMessage(
val role: String, // "system" 或 "user"
val content: String // 系统提示词或用户提示词(含卦象JSON)
)
```
### 2.3 DivinationInfo (卦象信息 JSON)
```kotlin
data class DivinationInfo(
// 用户信息
val question: String, // 用户问题
val questionType: String, // 问题类型 (如"事业"、"感情"、"健康")
// 起卦时间信息
val divinationTime: String, // 起卦时间 "2024年06月01日 12:00"
val yearGanZhi: String, // 年干支 "甲子"
val monthGanZhi: String, // 月干支
val dayGanZhi: String, // 日干支
val timeGanZhi: String, // 时干支
// 干支空亡信息
val yearKongWang: String, // 年空亡 "戌亥"
val monthKongWang: String, // 月空亡
val dayKongWang: String, // 日空亡
val timeKongWang: String, // 时空亡
// 月建日辰信息
val yueJian: String, // 月建 "寅木"
val riChen: String, // 日辰 "午火"
val yuePo: String, // 月破
val riChong: String, // 日冲
// 五行旺衰
val wuXingStatuses: Map<String, String>, // 五行旺相休囚死状态
// 本卦信息
val guaName: String, // 卦名 "坤为地"
val upperName: String, // 上卦名称
val lowerName: String, // 下卦名称
val worldPosition: Int, // 世爻位置 (1-6)
val responsePosition: Int, // 应爻位置 (1-6)
// 六爻信息
val yaoInfoList: List<YaoDetailInfo>,
// 变卦信息
val hasChangingYao: Boolean, // 是否有动爻
val targetGuaName: String, // 变卦名称
val targetYaoInfoList: List<YaoDetailInfo>
)
```
### 2.4 YaoDetailInfo (爻详细信息)
```kotlin
data class YaoDetailInfo(
val position: Int, // 爻位置 (1-6: 初爻到上爻)
val spiritName: String, // 神煞 (龙/雀/勾/蛇/虎/玄)
val relationName: String, // 六亲 (兄弟/父母/官鬼/妻财/子孙)
val tiganName: String, // 地支 (子/丑/寅...)
val elementName: String, // 五行 (金/木/水/火/土)
val isYang: Boolean, // 阴阳属性
val isChanging: Boolean, // 是否为动爻
val specialMark: String // 特殊标记 (世/应/"")
)
```
---
## 3. 响应结构
### 3.1 DeepSeekResponse
```kotlin
data class DeepSeekResponse(
val id: String,
val choices: List<DeepSeekChoice>
)
data class DeepSeekChoice(
val message: DeepSeekMessage?, // 包含 AI 回复内容
val finish_reason: String?
)
data class DeepSeekMessage(
val role: String,
val content: String // AI 返回的解卦结果
)
```
---
## 4. 系统提示词 (System Prompt)
```
## 角色
你是一个六爻解卦专家,熟悉六爻解卦步骤以及给出对应的解卦结果...
## 输出格式要求
- 单独在最开头输出一句话概括卦象的吉凶
- 输出顺序:解卦结论、卦象重点、卦象建议、关键词
- 格式:解卦结论:1、… 2、…;卦象重点:1、… 2、…;卦象建议:1、… 2、…;关键词:…
- 关键词:三个四字成语
```
### 4.1 吉凶等级
| 等级 | 描述 |
|------|------|
| 上上签 | 卦象结果较好,完成某事容易或最终结果好 |
| 中上签 | 卦象结果一般,需很努力才能完成或效果一般 |
| 中下签 | 卦象结果较差,即使很努力也无法完成或结果不好 |
---
## 5. 字段说明
### 5.1 字段名与含义对照表
| 字段名 | 含义 | 示例 |
|--------|------|------|
| `divinationTime` | 起卦时间 | "2024年06月01日 12:00" |
| `yearGanZhi` | 年柱天干地支 | "甲子" |
| `monthGanZhi` | 月柱天干地支 | "丙寅" |
| `dayGanZhi` | 日柱天干地支 | "戊午" |
| `timeGanZhi` | 时柱天干地支 | "庚子" |
| `yearKongWang` | 年柱空亡地支 | "戌亥" |
| `yueJian` | 月建 | "寅木" |
| `riChen` | 日辰 | "午火" |
| `yuePo` | 月破 | "申金" |
| `riChong` | 日冲 | "子水" |
| `guaName` | 本卦卦名 | "坤为地" |
| `upperName` | 上卦名称 | |
| `lowerName` | 下卦名称 | |
| `worldPosition` | 世爻位置 | 1-6 |
| `responsePosition` | 应爻位置 | 1-6 |
| `hasChangingYao` | 是否有动爻 | true/false |
| `targetGuaName` | 变卦卦名 | |
### 5.2 六神 (spiritName)
| 神煞 | 含义 |
|------|------|
| 龙 | 青龙 |
| 雀 | 朱雀 |
| 勾 | 勾陈 |
| 蛇 | 螣蛇 |
| 虎 | 白虎 |
| 玄 | 玄武 |
### 5.3 六亲 (relationName)
| 六亲 | 含义 |
|------|------|
| 兄弟 | |
| 父母 | |
| 官鬼 | |
| 妻财 | |
| 子孙 | |
### 5.4 地支 (tiganName)
子、丑、寅、卯、辰、巳、午、未、申、酉、戌、亥
### 5.5 五行 (elementName)
金、木、水、火、土
---
## 6. Source
- Android App: `old/app/src/main/java/com/example/eryaoapp/api/DivinationRepository.kt`
- Request Models: `old/app/src/main/java/com/example/eryaoapp/api/model/DivinationRequest.kt`
- API Service: `old/app/src/main/java/com/example/eryaoapp/api/DeepSeekApiService.kt`
-350
View File
@@ -1,350 +0,0 @@
# Old 项目数据库表结构参考
本文档记录 `old` 文件夹中历史项目的数据库表结构定义。
---
## 一、login-service (后端服务)
### 1. users - 用户表
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| id | BIGINT | PRIMARY KEY, AUTO_INCREMENT | 主键ID |
| phone_number | VARCHAR(20) | UNIQUE, NOT NULL | 手机号 |
| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 创建时间 |
| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP | 更新时间 |
**索引:**
- `idx_phone_number` ON `phone_number`
---
### 2. verification_codes - 验证码表
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| id | BIGINT | PRIMARY KEY, AUTO_INCREMENT | 主键ID |
| phone_number | VARCHAR(20) | NOT NULL | 手机号 |
| code | VARCHAR(6) | NOT NULL | 验证码 |
| expiration_time | TIMESTAMP | NOT NULL | 过期时间 |
| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 创建时间 |
**索引:**
- `idx_vc_phone_number` ON `phone_number`
- `idx_vc_expiration` ON `expiration_time`
---
### 3. user_profile - 用户资料表
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| id | BIGINT | PRIMARY KEY, AUTO_INCREMENT | 主键ID |
| phone_number | VARCHAR(20) | UNIQUE, NOT NULL | 手机号 |
| nickname | VARCHAR(50) | | 昵称 |
| avatar_url | VARCHAR(500) | | 头像URL |
| signature | VARCHAR(200) | | 个性签名 |
| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 创建时间 |
| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP | 更新时间 |
---
### 4. user_tokens - 用户令牌表
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| id | BIGINT | PRIMARY KEY, AUTO_INCREMENT | 主键ID |
| user_id | BIGINT | NOT NULL | 用户ID |
| token | VARCHAR(255) | NOT NULL | 访问令牌 |
| refresh_token | VARCHAR(255) | | 刷新令牌 |
| expires_at | TIMESTAMP | | 过期时间 |
| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 创建时间 |
---
### 5. user_feedback - 用户反馈表
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| id | BIGINT | PRIMARY KEY, AUTO_INCREMENT | 主键ID |
| user_id | BIGINT | NOT NULL | 用户ID |
| content | TEXT | NOT NULL | 反馈内容 |
| contact | VARCHAR(100) | | 联系方式 |
| status | VARCHAR(20) | DEFAULT 'PENDING' | 处理状态 |
| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 创建时间 |
| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP | 更新时间 |
---
### 6. user_coin - 用户金币表
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| id | BIGINT | PRIMARY KEY, AUTO_INCREMENT | 主键ID |
| user_id | BIGINT | UNIQUE, NOT NULL | 用户ID |
| coin_count | BIGINT | DEFAULT 0 | 金币数量 |
| total_charged | BIGINT | DEFAULT 0 | 累计充值金币 |
| total_consumed | BIGINT | DEFAULT 0 | 累计消费金币 |
| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 创建时间 |
| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP | 更新时间 |
---
### 7. notification - 通知表
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| id | BIGINT | PRIMARY KEY, AUTO_INCREMENT | 主键ID |
| user_id | BIGINT | NOT NULL | 用户ID |
| title | VARCHAR(100) | NOT NULL | 通知标题 |
| content | TEXT | | 通知内容 |
| type | VARCHAR(20) | | 通知类型 |
| is_read | BOOLEAN | DEFAULT FALSE | 是否已读 |
| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 创建时间 |
---
### 8. payment_record - 支付记录表
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| id | BIGINT | PRIMARY KEY, AUTO_INCREMENT | 主键ID |
| user_id | BIGINT | NOT NULL | 用户ID |
| order_id | VARCHAR(64) | NOT NULL | 订单ID |
| amount | DECIMAL(10,2) | NOT NULL | 支付金额 |
| coin_amount | BIGINT | NOT NULL | 购买金币数量 |
| payment_method | VARCHAR(20) | | 支付方式 |
| status | VARCHAR(20) | NOT NULL | 支付状态 |
| transaction_id | VARCHAR(100) | | 第三方交易号 |
| paid_at | TIMESTAMP | | 支付时间 |
| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 创建时间 |
---
### 9. payment_order - 支付订单表
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| id | BIGINT | PRIMARY KEY, AUTO_INCREMENT | 主键ID |
| order_no | VARCHAR(64) | UNIQUE, NOT NULL | 订单号 |
| user_id | BIGINT | NOT NULL | 用户ID |
| product_id | VARCHAR(50) | NOT NULL | 商品ID |
| product_name | VARCHAR(100) | NOT NULL | 商品名称 |
| amount | DECIMAL(10,2) | NOT NULL | 订单金额 |
| status | VARCHAR(20) | NOT NULL | 订单状态 |
| pay_url | TEXT | | 支付链接 |
| expire_time | TIMESTAMP | | 过期时间 |
| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP | 创建时间 |
| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP | 更新时间 |
---
### 10. sensitive_word_violations - 敏感词违规记录表
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| id | BIGINT | PRIMARY KEY, AUTO_INCREMENT | 主键ID |
| user_id | BIGINT | NOT NULL, FK | 用户ID |
| content_type | VARCHAR(20) | NOT NULL | 内容类型:NICKNAME, SIGNATURE |
| violation_type | VARCHAR(30) | NOT NULL | 违规类型:POLITICAL, ILLEGAL, VULGAR, ADVERTISING, PERSONAL_ATTACK |
| detection_service | VARCHAR(20) | DEFAULT 'LOCAL' | 检测服务类型:LOCAL, ALIYUN |
| risk_level | VARCHAR(50) | | 阿里云风险等级 |
| confidence | DOUBLE | | 阿里云置信度(0-1) |
| original_content | TEXT | NOT NULL | 原始内容 |
| matched_words | TEXT | NOT NULL | 匹配到的敏感词(JSON) |
| aliyun_response | TEXT | | 阿里云完整响应 |
| client_ip | VARCHAR(45) | | 客户端IP |
| user_agent | TEXT | | 用户代理 |
| violation_time | DATETIME | NOT NULL | 违规时间 |
| created_at | DATETIME | DEFAULT CURRENT_TIMESTAMP | 创建时间 |
**索引:**
- `idx_user_id` ON `user_id`
- `idx_content_type` ON `content_type`
- `idx_violation_type` ON `violation_type`
- `idx_violation_time` ON `violation_time`
- `idx_user_violation_time` ON `(user_id, violation_time)`
- `idx_client_ip` ON `client_ip`
- `idx_detection_service` ON `detection_service`
- `idx_risk_level` ON `risk_level`
- `idx_confidence` ON `confidence`
**外键:**
- `user_id` REFERENCES `user_profile(id)` ON DELETE CASCADE
---
### 11. user_divination_records - 用户解卦记录表
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| id | BIGINT | PRIMARY KEY, AUTO_INCREMENT | 主键ID |
| user_id | BIGINT | NOT NULL | 用户ID |
| trace_id | VARCHAR(64) | NOT NULL | 请求追踪ID |
| question | TEXT | NOT NULL | 用户问题 |
| question_type | VARCHAR(50) | NOT NULL | 问题类型 |
| divination_data | LONGTEXT | NOT NULL | 卦象详情JSON |
| deepseek_request | LONGTEXT | NOT NULL | 发送给DeepSeek的请求JSON |
| deepseek_response | LONGTEXT | | DeepSeek响应JSON |
| interpretation_result | LONGTEXT | | 解卦结果文本 |
| api_success | BOOLEAN | NOT NULL, DEFAULT FALSE | API调用是否成功 |
| error_message | TEXT | | 错误信息 |
| api_duration_ms | BIGINT | | API调用耗时(毫秒) |
| phone_number | VARCHAR(20) | | 用户手机号(冗余) |
| created_at | DATETIME | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 创建时间 |
**索引:**
- `idx_user_id` ON `user_id`
- `idx_trace_id` ON `trace_id`
- `idx_phone_number` ON `phone_number`
- `idx_created_at` ON `created_at`
- `idx_api_success` ON `api_success`
- `idx_question_type` ON `question_type`
- `idx_user_created` ON `(user_id, created_at)`
---
### 12. user_divination_history - 用户卦象历史同步表
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| id | BIGINT | PRIMARY KEY, AUTO_INCREMENT | 主键ID |
| user_id | BIGINT | NOT NULL, FK | 用户ID |
| phone_number | VARCHAR(20) | NOT NULL | 用户手机号 |
| local_record_id | BIGINT | | 本地记录ID |
| json_data | LONGTEXT | NOT NULL | 卦象详情JSON |
| ai_result | LONGTEXT | NOT NULL | AI解卦结果 |
| question_type | VARCHAR(50) | NOT NULL | 问题类型 |
| question | TEXT | NOT NULL | 用户问题 |
| timestamp | BIGINT | NOT NULL | 创建时间戳(毫秒) |
| is_active | BOOLEAN | NOT NULL, DEFAULT TRUE | 是否有效 |
| sync_time | DATETIME | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 同步时间 |
| updated_at | DATETIME | NOT NULL, DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP | 更新时间 |
**索引:**
- `idx_user_phone` ON `(user_id, phone_number)`
- `idx_phone_active` ON `(phone_number, is_active)`
- `idx_user_active_time` ON `(user_id, is_active, timestamp)`
- `idx_local_record` ON `local_record_id`
- `idx_sync_time` ON `sync_time`
- `idx_question_type` ON `question_type`
**外键:**
- `user_id` REFERENCES `user_profile(id)`
---
### 13. network_access_logs - 网络访问日志表
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| id | BIGINT | PRIMARY KEY, AUTO_INCREMENT | 主键ID |
| user_id | BIGINT | NULL | 用户ID |
| phone_number | VARCHAR(20) | NULL | 用户手机号 |
| client_ip | VARCHAR(45) | NOT NULL | 客户端IP |
| client_port | INT | NULL | 客户端端口 |
| server_ip | VARCHAR(45) | NOT NULL | 服务器IP |
| server_port | INT | NOT NULL | 服务器端口 |
| http_method | VARCHAR(10) | NOT NULL | 请求方法 |
| request_path | VARCHAR(500) | NOT NULL | 请求路径 |
| request_url | VARCHAR(1000) | NOT NULL | 完整请求URL |
| user_agent | VARCHAR(1000) | NULL | User-Agent |
| device_info | TEXT | NULL | 设备信息JSON |
| response_status | INT | NULL | HTTP响应状态码 |
| processing_time_ms | BIGINT | NULL | 处理耗时(毫秒) |
| request_size | BIGINT | NULL | 请求体大小(字节) |
| response_size | BIGINT | NULL | 响应体大小(字节) |
| x_forwarded_for | VARCHAR(500) | NULL | X-Forwarded-For |
| x_real_ip | VARCHAR(45) | NULL | X-Real-IP |
| referer | VARCHAR(1000) | NULL | Referer |
| operation_type | VARCHAR(50) | NULL | 操作类型 |
| operation_result | VARCHAR(20) | NULL | 操作结果 |
| error_message | TEXT | NULL | 错误信息 |
| session_id | VARCHAR(100) | NULL | 会话ID |
| access_time | DATETIME | NOT NULL | 访问时间 |
| created_at | DATETIME | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 创建时间 |
**索引:**
- `idx_user_id` ON `user_id`
- `idx_phone_number` ON `phone_number`
- `idx_client_ip` ON `client_ip`
- `idx_access_time` ON `access_time`
- `idx_operation_type` ON `operation_type`
- `idx_operation_result` ON `operation_result`
- `idx_client_ip_access_time` ON `(client_ip, access_time)`
- `idx_user_id_access_time` ON `(user_id, access_time)`
---
### 14. app_version - 应用版本管理表
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| id | BIGINT | PRIMARY KEY, AUTO_INCREMENT | 主键ID |
| version_name | VARCHAR(20) | UNIQUE, NOT NULL | 版本名称(如v1.06) |
| version_code | INT | UNIQUE, NOT NULL | 版本号(如106) |
| min_supported_version | VARCHAR(20) | NOT NULL | 最低支持版本 |
| min_supported_code | INT | NOT NULL | 最低支持版本号 |
| is_force_update | BOOLEAN | NOT NULL, DEFAULT FALSE | 是否强制更新 |
| update_message | TEXT | | 更新提示信息 |
| download_url | VARCHAR(500) | | 下载链接 |
| is_active | BOOLEAN | NOT NULL, DEFAULT TRUE | 是否启用 |
| created_at | DATETIME | NOT NULL, DEFAULT CURRENT_TIMESTAMP | 创建时间 |
| updated_at | DATETIME | NOT NULL, DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP | 更新时间 |
**索引:**
- `uk_version_name` UNIQUE ON `version_name`
- `uk_version_code` UNIQUE ON `version_code`
- `idx_is_active` ON `is_active`
- `idx_created_at` ON `created_at`
---
## 二、app (Android 客户端本地 Room 数据库)
### 1. divination_record - 解卦记录表
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| id | BIGINT | PRIMARY KEY, AUTO_INCREMENT | 主键ID |
| question_type | VARCHAR(50) | NOT NULL | 问题类型 |
| question | TEXT | NOT NULL | 用户问题 |
| hexagram_data | TEXT | NOT NULL | 卦象数据JSON |
| ai_result | TEXT | NOT NULL | AI解卦结果 |
| timestamp | BIGINT | NOT NULL | 创建时间戳 |
| is_synced | BOOLEAN | DEFAULT FALSE | 是否已同步到云端 |
| is_deleted | BOOLEAN | DEFAULT FALSE | 是否已删除 |
---
## 三、数据库初始化文件位置
| 文件 | 说明 |
|------|------|
| `login-service/src/main/resources/db/init.sql` | 初始化表结构 |
| `login-service/src/main/resources/db/migration.sql` | 迁移脚本 |
| `login-service/src/main/resources/db/migration/V1_4__Create_sensitive_word_violations_table.sql` | 敏感词表创建 |
| `login-service/src/main/resources/db/migration/V1_5__Enhance_sensitive_word_violations_table.sql` | 敏感词表增强 |
---
## 四、Entity 类位置
| Entity 类 | 表名 | 位置 |
|-----------|------|------|
| UsersEntity | users | `login-service/src/main/kotlin/com/eryao/login/entity/UsersEntity.kt` |
| VerificationCode | verification_codes | `login-service/src/main/kotlin/com/eryao/login/entity/VerificationCode.kt` |
| User | user_profile | `login-service/src/main/kotlin/com/eryao/login/entity/User.kt` |
| UserToken | user_tokens | `login-service/src/main/kotlin/com/eryao/login/entity/UserToken.kt` |
| UserFeedback | user_feedback | `login-service/src/main/kotlin/com/eryao/login/entity/UserFeedback.kt` |
| UserCoin | user_coin | `login-service/src/main/kotlin/com/eryao/login/entity/UserCoin.kt` |
| Notification | notification | `login-service/src/main/kotlin/com/eryao/login/entity/Notification.kt` |
| PaymentRecord | payment_record | `login-service/src/main/kotlin/com/eryao/login/entity/PaymentRecord.kt` |
| PaymentOrder | payment_order | `login-service/src/main/kotlin/com/eryao/login/entity/PaymentOrder.kt` |
| SensitiveWordViolation | sensitive_word_violations | `login-service/src/main/kotlin/com/eryao/login/entity/SensitiveWordViolation.kt` |
| DivinationRecord | user_divination_records | `login-service/src/main/kotlin/com/eryao/login/entity/DivinationRecord.kt` |
| DivinationHistory | user_divination_history | `login-service/src/main/kotlin/com/eryao/login/entity/DivinationRecord.kt` |
| NetworkAccessLog | network_access_logs | `login-service/src/main/kotlin/com/eryao/login/entity/NetworkAccessLog.kt` |
| AppVersion | app_version | `login-service/src/main/kotlin/com/eryao/login/entity/AppVersion.kt` |
| DivinationRecord (Room) | divination_record | `app/src/main/java/com/example/eryaoapp/database/DivinationRecord.kt` |