refactor: 重构项目目录结构与路径引用

1.  调整工具类、平台相关代码的目录组织,将原有根目录下的工具类迁移到`data/`和`platform/`子目录
2.  统一修复全项目的文件导入路径,匹配新的目录结构
3.  新增Web端平台适配的Stub实现,包括Isolate、path_provider、platform_io等
4.  删除旧的单文件平台适配实现,替换为分平台的目录结构实现
5.  移除旧的iOS Widget入口文件,新增Widget Extension的权限配置
6.  调整部分组件的目录位置,统一widget的分类组织
7.  修复部分硬编码文本和废弃的正则表达式逻辑
This commit is contained in:
Developer
2026-05-23 05:16:31 +08:00
parent 85d856f0ed
commit a9499d7219
357 changed files with 6505 additions and 2532 deletions

View File

@@ -0,0 +1,793 @@
# 闲言APP Bug修复与功能增强 实施计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 修复5个关键Bug + 实现9项功能增强 + 2个目录重构
**Architecture:** 按优先级分4批执行P0紧急Bug修复 → P1功能增强 → P2交互优化 → P3目录重构
**Tech Stack:** Flutter/Dart, Riverpod, Cupertino, confetti, flutter_animate, fl_chart, flutter_quill
---
## 批次一P0 紧急Bug修复Task 1-5
### Task 1: 修复国学经典频道ID映射错误
**Files:**
- Modify: `lib/features/classics/services/classics_service.dart:16-28`
- Modify: `lib/features/tool_center/health/services/health_service.dart:16-28`
**问题:** 客户端硬编码的频道ID与后端 `$feedMap` key 不匹配导致API报"不支持的频道"错误。
**后端正确的key映射表:**
| 分类名 | 客户端当前ID | 后端正确ID |
|--------|------------|-----------|
| 黄帝内经 | `huangdi` | `hdnj` |
| 史记 | `shiji` | `sj` |
| 孙子兵法 | `sunzi` | `sbbf` |
| 墨子 | `mozi` | `mz` |
| 庄子 | `zhuangzi` | `zz` |
| 三国志 | `sanguozhi` | `sgz` |
| 金匮要略 | `jingui` | `jgj` |
| 战国策 | `zhanguoce` | `warring` |
| 万历野获编 | `wanli` | 无对应(需后端新增或移除) |
| 药茶养生 | `tea` | `tisana` |
| 疾病自查 | `disease` | `illness` |
- [ ] **Step 1: 修复 classics_service.dart 中的ID映射**
`ClassicsCategory``id` 字段改为与后端一致:
```dart
static const List<ClassicsCategory> categories = [
ClassicsCategory(id: 'lunyu', name: '论语', emoji: '📖', desc: '孔子言行录'),
ClassicsCategory(id: 'hdnj', name: '黄帝内经', emoji: '🏥', desc: '中医经典'),
ClassicsCategory(id: 'sj', name: '史记', emoji: '📜', desc: '司马迁著'),
ClassicsCategory(id: 'sbbf', name: '孙子兵法', emoji: '⚔️', desc: '兵家圣典'),
ClassicsCategory(id: 'mz', name: '墨子', emoji: '🖤', desc: '墨家经典'),
ClassicsCategory(id: 'zz', name: '庄子', emoji: '🦋', desc: '道家经典'),
ClassicsCategory(id: 'zuozhuan', name: '左传', emoji: '📚', desc: '春秋左氏传'),
ClassicsCategory(id: 'sgz', name: '三国志', emoji: '🏯', desc: '陈寿著'),
ClassicsCategory(id: 'jgj', name: '金匮要略', emoji: '💊', desc: '张仲景著'),
ClassicsCategory(id: 'warring', name: '战国策', emoji: '🗡️', desc: '纵横家书'),
];
```
移除 `wanli`(万历野获编),因为后端无对应频道。
- [ ] **Step 2: 修复 health_service.dart 中的ID映射**
```dart
static const List<HealthCategory> categories = [
HealthCategory(id: 'drug', name: '药品查询', emoji: '💊', desc: '药品信息·用法用量'),
HealthCategory(id: 'herbal', name: '中草药', emoji: '🌿', desc: '中草药百科'),
HealthCategory(id: 'food', name: '食物相克', emoji: '🍎', desc: '食物搭配禁忌'),
HealthCategory(id: 'prescription', name: '偏方大全', emoji: '📋', desc: '民间偏方验方'),
HealthCategory(id: 'tisana', name: '药茶养生', emoji: '🍵', desc: '养生药茶配方'),
HealthCategory(id: 'illness', name: '疾病自查', emoji: '🏥', desc: '症状·疾病查询'),
];
```
- [ ] **Step 3: 搜索所有引用旧ID的代码并更新**
搜索 `huangdi|shiji|sunzi|mozi|zhuangzi|sanguozhi|jingui|zhanguoce|wanli``tea|disease` 在整个 `lib/` 目录中的引用确保没有硬编码的旧ID。
- [ ] **Step 4: 运行 flutter analyze 验证**
Run: `flutter analyze lib/features/classics/ lib/features/tool_center/health/`
Expected: No issues found
---
### Task 2: 修复离线模式预加载频道使用中文名
**Files:**
- Modify: `lib/core/storage/cache_config.dart:28,71-74`
**问题:** `preloadChannels` 默认值 `{'推荐', '经典', '诗词'}` 是中文,但后端 Feed API 的 channel 参数需要英文 key。
- [ ] **Step 1: 修改 CacheConfig 默认值**
```dart
// 将中文频道名改为英文key
this.preloadChannels = const {'all', 'poetry', 'wisdom'},
```
同步修改 fromJson 中的默认值:
```dart
preloadChannels: (json['preloadChannels'] as List<dynamic>?)
?.map((e) => e.toString())
.toSet() ??
const {'all', 'poetry', 'wisdom'},
```
- [ ] **Step 2: 修改 offline_page.dart 中频道显示**
在离线模式页面的"预加载频道"行中将英文key映射为中文名显示
```dart
_SettingRow(
icon: CupertinoIcons.cube_box,
title: '预加载频道',
trailing: Text(config.preloadChannels.map(_channelDisplayName).join('')),
),
```
添加映射方法:
```dart
String _channelDisplayName(String key) {
const map = {
'all': '推荐',
'poetry': '诗词',
'wisdom': '哲理',
'story': '故事',
'hitokoto': '一言',
'lyric': '歌词',
'saying': '名言',
};
return map[key] ?? key;
}
```
- [ ] **Step 3: 运行 flutter analyze 验证**
---
### Task 3: 合并两套独立的预加载开关
**Files:**
- Modify: `lib/mine/settings/providers/sub/performance_settings_provider.dart`
- Modify: `lib/mine/settings/providers/general_settings_provider.dart`
- Modify: `lib/mine/settings/presentation/general/general_settings_sections.dart`
- Modify: `lib/features/home/presentation/providers/offline_page.dart`
- Modify: `lib/features/home/services/offline_manager.dart`
**问题:** 通用设置中的 `preloadEnabled`KV key: `general_preload`)和离线模式中的 `preloadOnWifi`CacheConfig互不关联。
**方案:** 统一使用 `CacheConfig.preloadOnWifi` 作为唯一预加载开关,移除 `PerformanceSettingsState.preloadEnabled`
- [ ] **Step 1: 从 PerformanceSettingsState 移除 preloadEnabled**
移除 `preloadEnabled` 字段、`fromStorage` 中的读取、`saveToStorage` 中的保存、`setPreloadEnabled` 方法。
- [ ] **Step 2: 从 GeneralSettingsState 移除 preloadEnabled 透传**
移除 `bool get preloadEnabled``setPreloadEnabled` 方法。
- [ ] **Step 3: 修改通用设置页面**
将"预加载"开关改为读取/写入 `CacheConfig.preloadOnWifi`(通过 `offlineProvider`),而非 `generalSettingsProvider`
- [ ] **Step 4: 修改 OfflineManager.preloadNow()**
`preloadNow()` 开头检查 `CacheConfig.preloadOnWifi`,如果关闭则返回 `PreloadResult(loaded: 0, skipped: 0)` 并在调用方提示"预加载已关闭"。
- [ ] **Step 5: 运行 flutter analyze 验证**
---
### Task 4: 增强阅读报告错误提示
**Files:**
- Modify: `lib/features/reading_report/presentation/reading_report_page.dart`
- Modify: `lib/features/reading_report/providers/reading_report_provider.dart`
**问题:** 当 dashboard API 返回空或格式变化时,仅显示"报告加载失败",用户无法理解原因。
- [ ] **Step 1: 修改 ReadingReportState 增加部分加载状态**
```dart
class ReadingReportState {
final ReadingReport? report;
final ReportPeriod currentPeriod;
final bool isLoading;
final String? error;
final Set<String> failedSources; // 新增:记录哪些数据源加载失败
// copyWith 也需更新
}
```
- [ ] **Step 2: 修改 ReadingReportService.generateReport 返回失败源信息**
`generateReport`catch 块记录失败的数据源名称:
```dart
final failedSources = <String>{};
try { dashboard = await UserCenterService.getDashboard(); } catch (e) { failedSources.add('仪表盘'); }
try { statsData = await UserCenterService.getStats(...); } catch (e) { failedSources.add('趋势数据'); }
try { heatmapData = await UserCenterService.getHeatmap(); } catch (e) { failedSources.add('热力图'); }
```
- [ ] **Step 3: 修改阅读报告页面错误/部分加载提示**
`failedSources` 非空但 report 不为空时,在页面顶部显示警告条:
```dart
if (state.failedSources.isNotEmpty && state.report != null)
Container(
padding: const EdgeInsets.all(AppSpacing.sm),
decoration: BoxDecoration(color: CupertinoColors.systemYellow.withValues(alpha: 0.1), ...),
child: Row(children: [
Icon(CupertinoIcons.exclamationmark_triangle, color: CupertinoColors.systemYellow, size: 16),
Text('部分数据加载失败:${state.failedSources.join('')}', ...),
]),
),
```
当全部失败时,显示更友好的错误信息:
```dart
Widget _buildError(String error, AppThemeExtension ext) {
return Center(child: Column(children: [
Icon(CupertinoIcons.cloud_slash, size: 48, color: ext.textHint),
Text('数据源暂时不可用', ...),
Text('请检查网络连接后重试', ...),
CupertinoButton(onPressed: retry, child: Text('重新加载')),
]));
}
```
- [ ] **Step 4: 运行 flutter analyze 验证**
---
### Task 5: 排行榜赛季自动创建(后端定时任务)
**Files:**
- Create: `Scripts/create_rank_season.py` — Python脚本通过API创建赛季
- Modify: `docs/toolsapi/application/api/controller/Rank.php` — 添加自动创建当前赛季逻辑
**问题:** `rank_season` 表中没有 `status=active` 的记录,排行榜为空。
- [ ] **Step 1: 修改 Rank.php leaderboard 方法,自动创建当前周赛**
`leaderboard()` 方法中,当找不到活跃赛季时,自动创建:
```php
if (!$currentSeason) {
// 自动创建当前周赛
$now = time();
$weekStart = strtotime('monday this week', $now);
$weekEnd = $weekStart + 7 * 86400;
$weekNum = date('W', $now);
$year = date('Y', $now);
Db::name('rank_season')->insert([
'name' => "{$year}年第{$weekNum}周赛",
'type' => 'weekly',
'start_time' => $weekStart,
'end_time' => $weekEnd,
'status' => 'active',
'rewards' => json_encode([
['rank_start' => 1, 'rank_end' => 3, 'exp' => 100, 'score' => 50],
['rank_start' => 4, 'rank_end' => 10, 'exp' => 50, 'score' => 20],
['rank_start' => 11, 'rank_end' => 50, 'exp' => 20, 'score' => 10],
]),
'createtime' => $now,
'updatetime' => $now,
]);
$currentSeason = Db::name('rank_season')
->where('status', 'active')
->where('start_time', '<=', $now)
->where('end_time', '>', $now)
->order('start_time desc')
->find();
}
```
同样在 `myRank()` 方法中添加相同的自动创建逻辑。
- [ ] **Step 2: 上传修改后的 Rank.php 到服务器**
使用 `Scripts/upload_server_code.py` 上传。
- [ ] **Step 3: 运行测试脚本验证**
Run: `python Scripts/test_rank_api.py`
Expected: 排行榜API返回 `season != null`
---
## 批次二P1 功能增强Task 6-8
### Task 6: 排行榜前3名动画效果
**Files:**
- Modify: `lib/shared/widgets/rank_item_card.dart`
- Modify: `lib/features/rank/presentation/rank_page.dart`
- [ ] **Step 1: 为 RankItemCard 添加 flutter_animate 动画**
前三名添加缩放+光晕入场动画:
```dart
Widget build(BuildContext context) {
final widget = Container(/* 现有卡片内容 */);
if (item.rank <= 3) {
return widget
.animate()
.fadeIn(duration: 400.ms, delay: (item.rank * 100).ms)
.scale(begin: const Offset(0.9, 0.9), end: const Offset(1, 1), duration: 400.ms)
.shimmer(duration: 1200.ms, color: _rankColor.withValues(alpha: 0.15));
}
return widget.animate().fadeIn(duration: 300.ms, delay: (item.rank * 50).ms);
}
```
- [ ] **Step 2: 为第1名添加持续光晕效果**
```dart
if (item.rank == 1) {
return Container(
decoration: BoxDecoration(
borderRadius: AppRadius.mdBorder,
boxShadow: [
BoxShadow(
color: CupertinoColors.systemYellow.withValues(alpha: 0.3),
blurRadius: 12,
spreadRadius: 2,
),
],
),
child: widget,
).animate(onPlay: (c) => c.repeat()).shimmer(duration: 2000.ms, color: CupertinoColors.systemYellow.withValues(alpha: 0.1));
}
```
- [ ] **Step 3: 运行 flutter analyze 验证**
---
### Task 7: 阅读报告趋势图交互
**Files:**
- Modify: `lib/features/reading_report/presentation/widgets/trend_chart.dart`
- [ ] **Step 1: 为 fl_chart LineChart 添加 touchCallback**
```dart
LineChartData(
lineTouchData: LineTouchData(
touchTooltipData: LineTouchTooltipData(
tooltipBgColor: ext.bgSecondary,
tooltipRoundedRadius: 8,
getTooltipItems: (spots) => spots.map((spot) {
final date = trendData.length > spot.x.toInt()
? trendData[spot.x.toInt()].date
: '';
return LineTooltipItem(
'$date\n${spot.y.toInt()}',
AppTypography.caption1.copyWith(color: ext.textPrimary),
);
}).toList(),
),
handleBuiltInTouches: true,
),
// ... 现有配置
);
```
- [ ] **Step 2: 运行 flutter analyze 验证**
---
### Task 8: 排行榜领奖/成就解锁庆祝动画
**Files:**
- Modify: `lib/features/rank/presentation/rank_page.dart`
- [ ] **Step 1: 为 RankPage 添加 ConfettiController**
```dart
class _RankPageState extends ConsumerState<RankPage> {
late ConfettiController _confettiController;
@override
void initState() {
super.initState();
_confettiController = ConfettiController(duration: const Duration(seconds: 2));
}
@override
void dispose() {
_confettiController.dispose();
super.dispose();
}
}
```
- [ ] **Step 2: 在 Stack 中添加 ConfettiWidget**
```dart
Stack(children: [
// 现有页面内容
Align(
alignment: Alignment.topCenter,
child: ConfettiWidget(
confettiController: _confettiController,
blastDirectionality: BlastDirectionality.explosive,
emissionFrequency: 0.05,
numberOfParticles: 20,
colors: [ext.accent, CupertinoColors.systemYellow, CupertinoColors.systemGreen],
),
),
]);
```
- [ ] **Step 3: 领取奖励成功时触发动画**
`claimReward` 成功回调中调用 `_confettiController.play()`
- [ ] **Step 4: 运行 flutter analyze 验证**
---
## 批次三P2 交互优化Task 9-12
### Task 9: 笔记编辑器Markdown工具栏
**Files:**
- Modify: `lib/features/note/presentation/note_edit_page.dart`
- [ ] **Step 1: 在编辑区域上方添加Markdown快捷工具栏**
在标题输入框和内容输入框之间,添加一行工具栏:
```dart
Widget _buildMarkdownToolbar(AppThemeExtension ext) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.sm, vertical: 4),
decoration: BoxDecoration(color: ext.bgSecondary, borderRadius: AppRadius.smBorder),
child: Wrap(
spacing: 2,
runSpacing: 2,
children: [
_toolbarBtn(ext, CupertinoIcons.bold, '粗体', () => _insertMarkdown('**', '**')),
_toolbarBtn(ext, CupertinoIcons.italic, '斜体', () => _insertMarkdown('*', '*')),
_toolbarBtn(ext, CupertinoIcons.list_bullet, '列表', () => _insertMarkdown('\n- ', '')),
_toolbarBtn(ext, CupertinoIcons.number, '编号', () => _insertMarkdown('\n1. ', '')),
_toolbarBtn(ext, CupertinoIcons.quote_closing, '引用', () => _insertMarkdown('\n> ', '')),
_toolbarBtn(ext, CupertinoIcons.link, '链接', () => _insertMarkdown('[', '](url)')),
_toolbarBtn(ext, CupertinoIcons.code, '代码', () => _insertMarkdown('`', '`')),
_toolbarBtn(ext, CupertinoIcons.text_justify, '分割线', () => _insertMarkdown('\n---\n', '')),
],
),
);
}
Widget _toolbarBtn(AppThemeExtension ext, IconData icon, String tooltip, VoidCallback onTap) {
return CupertinoButton(
padding: const EdgeInsets.all(6),
minSize: 32,
onPressed: onTap,
child: Icon(icon, size: 18, color: ext.textSecondary),
);
}
```
- [ ] **Step 2: 实现 _insertMarkdown 方法**
在光标位置前后插入 Markdown 标记:
```dart
void _insertMarkdown(String before, String after) {
final controller = _contentController;
final sel = controller.selection;
final text = controller.text;
final selected = sel.textInside(text);
final newText = text.replaceRange(sel.start, sel.end, '$before$selected$after');
controller.text = newText;
controller.selection = TextSelection.collapsed(
offset: sel.start + before.length + selected.length,
);
setState(() => _hasUnsavedChanges = true);
_debounceSave();
}
```
- [ ] **Step 3: 运行 flutter analyze 验证**
---
### Task 10: SafeCachedImage 增加预加载和渐进式加载
**Files:**
- Modify: `lib/shared/widgets/safe_cached_image.dart`
- [ ] **Step 1: 添加预加载静态方法**
```dart
static Future<void> preload(String url) async {
if (!_isValidUrl(url)) return;
try {
await CachedNetworkImage.evictFromCache(url);
} catch (_) {}
}
```
- [ ] **Step 2: 添加渐进式加载占位符**
`CachedNetworkImage``progressIndicatorBuilder` 中显示下载进度:
```dart
progressIndicatorBuilder: (context, url, progress) {
if (progress.downloaded != null && progress.totalSize != null) {
final percent = (progress.downloaded! / progress.totalSize! * 100).toInt();
return CupertinoActivityIndicator.partiallyRevealed(progress: percent / 100);
}
return const CupertinoActivityIndicator();
},
```
- [ ] **Step 3: 运行 flutter analyze 验证**
---
### Task 11: 离线模式操作队列持久化
**Files:**
- Modify: `lib/features/home/services/offline_manager.dart`
- Modify: `lib/features/home/providers/offline_provider.dart`
- [ ] **Step 1: 在 OfflineManager.init() 中从 Hive 加载待执行操作**
```dart
static Future<void> init() async {
// ... 现有逻辑
await _loadPendingActions();
}
static Future<void> _loadPendingActions() async {
final box = await Hive.openBox('offlineQueue');
final saved = box.get('pendingActions', defaultValue: []) as List;
for (final item in saved) {
if (item is Map) {
_pendingActions.add(OfflineAction.fromMap(Map<String, dynamic>.from(item)));
}
}
}
```
- [ ] **Step 2: 在操作入队时持久化到 Hive**
```dart
static Future<void> enqueueAction(OfflineAction action) async {
_pendingActions.add(action);
final box = await Hive.openBox('offlineQueue');
await box.put('pendingActions', _pendingActions.map((a) => a.toMap()).toList());
}
```
- [ ] **Step 3: 在操作完成后从 Hive 移除**
```dart
static Future<void> _removeAction(int index) async {
_pendingActions.removeAt(index);
final box = await Hive.openBox('offlineQueue');
await box.put('pendingActions', _pendingActions.map((a) => a.toMap()).toList());
}
```
- [ ] **Step 4: 定义 OfflineAction 模型**
```dart
class OfflineAction {
final String type; // 'like', 'favorite', 'comment', etc.
final Map<String, dynamic> data;
final int createdAt;
const OfflineAction({required this.type, required this.data, required this.createdAt});
Map<String, dynamic> toMap() => {'type': type, 'data': data, 'createdAt': createdAt};
factory OfflineAction.fromMap(Map<String, dynamic> m) =>
OfflineAction(type: m['type'], data: Map<String, dynamic>.from(m['data']), createdAt: m['createdAt']);
}
```
- [ ] **Step 5: 运行 flutter analyze 验证**
---
### Task 12: 深度链接支持 + 鸿蒙端路由同步
**Files:**
- Modify: `lib/core/router/app_routes.dart`
- Modify: `lib/core/router/content_routes.dart`
- Modify: `lib/core/router/ohos_nav_bridge.dart`
- Modify: `lib/core/router/settings_routes.dart`
- [ ] **Step 1: 在 app_routes.dart 中添加深度链接路由常量**
```dart
static const String deepArticle = '/article/:id';
static const String deepFortune = '/fortune';
static const String deepNote = '/notes';
```
- [ ] **Step 2: 在 GoRouter 中注册深度链接处理**
`content_routes.dart``app_router.dart` 中,添加 `redirect` 逻辑处理通用链接:
```dart
redirect: (context, state) {
// 处理通用链接: xianyan://article/123 → /article/123
final uri = state.uri;
if (uri.scheme == 'xianyan') {
return uri.path;
}
return null;
},
```
- [ ] **Step 3: 在 ohos_nav_bridge.dart 中同步新增路由**
确保所有新增路由都在 `_routes` 列表中有对应的 `OhosRouteEntry`
```dart
OhosRouteEntry(pattern: '/other-settings', builder: (_) => const OtherSettingsPage()),
OhosRouteEntry(pattern: '/article/:id', builder: (ctx) => ArticleDetailPage(articleId: ctx.params.getInt('id'))),
```
- [ ] **Step 4: 运行 flutter analyze 验证**
---
## 批次四P3 目录重构Task 13-14
### Task 13: 重构 lib/shared/widgets/ 目录
**当前问题:** 36个文件平铺在一个目录下难以维护。
**目标结构:**
```
lib/shared/widgets/
├── widgets.dart # 统一导出(保持不变)
├── adaptive/ # 自适应组件
│ ├── adaptive_back_button.dart
│ └── responsive_layout.dart
├── animation/ # 动画组件
│ ├── animated_widgets.dart
│ └── sprite_loading_indicator.dart
├── containers/ # 容器组件
│ ├── glass_container.dart
│ ├── glass_bottom_nav_bar.dart
│ └── shader_card_background.dart
├── feedback/ # 反馈组件
│ ├── app_toast.dart
│ ├── empty_state.dart
│ ├── skeleton.dart
│ ├── app_error_boundary.dart
│ └── offline_banner.dart
├── input/ # 输入组件
│ ├── bottom_sheet.dart
│ ├── keyboard_back_handler.dart
│ ├── keyboard_safe_sheet.dart
│ └── app_popup_menu.dart
├── media/ # 媒体组件
│ ├── safe_cached_image.dart
│ ├── tts_player_bar.dart
│ └── wallpaper_gallery/ # 保持子目录不变
├── navigation/ # 导航组件
│ ├── appbar_character_sprite.dart
│ ├── appbar_date_display.dart
│ ├── tab_icon_sprite.dart
│ └── app_page_transitions.dart
├── content/ # 内容展示组件
│ ├── app_markdown.dart
│ ├── app_slidable.dart
│ ├── app_sticky_header.dart
│ ├── category_icon.dart
│ ├── level_card.dart
│ ├── rank_item_card.dart
│ ├── task_card.dart
│ ├── character_tip_bubble.dart
│ ├── share_sheet.dart
│ └── app_icon.dart
└── layout/ # 布局组件
└── (reserved for future)
```
- [ ] **Step 1: 创建子目录并移动文件**
按上述结构移动文件。每个文件移动后,更新内部 import 路径。
- [ ] **Step 2: 更新 widgets.dart 统一导出文件**
```dart
export 'adaptive/adaptive_back_button.dart';
export 'adaptive/responsive_layout.dart';
export 'animation/animated_widgets.dart';
export 'animation/sprite_loading_indicator.dart';
export 'containers/glass_container.dart';
export 'containers/glass_bottom_nav_bar.dart';
export 'containers/shader_card_background.dart';
export 'feedback/app_toast.dart';
export 'feedback/empty_state.dart';
export 'feedback/skeleton.dart';
export 'feedback/app_error_boundary.dart';
export 'feedback/offline_banner.dart';
export 'input/bottom_sheet.dart';
export 'input/keyboard_back_handler.dart';
export 'input/keyboard_safe_sheet.dart';
export 'input/app_popup_menu.dart';
export 'media/safe_cached_image.dart';
export 'media/tts_player_bar.dart';
export 'navigation/appbar_character_sprite.dart';
export 'navigation/appbar_date_display.dart';
export 'navigation/tab_icon_sprite.dart';
export 'navigation/app_page_transitions.dart';
export 'content/app_markdown.dart';
export 'content/app_slidable.dart';
export 'content/app_sticky_header.dart';
export 'content/category_icon.dart';
export 'content/level_card.dart';
export 'content/rank_item_card.dart';
export 'content/task_card.dart';
export 'content/character_tip_bubble.dart';
export 'content/share_sheet.dart';
export 'content/app_icon.dart';
```
- [ ] **Step 3: 全局搜索并更新所有 import 路径**
搜索 `package:xianyan/shared/widgets/` 开头的 import确保通过 `widgets.dart` 统一导出的路径仍然有效(因为 widgets.dart 重新导出了所有组件,大部分 import 不需要改)。
对于直接引用子文件的 import`package:xianyan/shared/widgets/glass_container.dart`),更新为新路径。
- [ ] **Step 4: 运行 flutter analyze 验证**
---
### Task 14: 重构 lib/core/utils/ 目录
**当前问题:** 18个文件平铺在一个目录下。
**目标结构:**
```
lib/core/utils/
├── logger.dart # 日志(保持不变,被广泛引用)
├── extensions.dart # 扩展方法(保持不变)
├── pattern_utils.dart # 正则工具(保持不变)
├── platform/ # 平台相关
│ ├── platform_utils.dart
│ ├── platform_helper.dart
│ ├── platform_feature_guard.dart
│ ├── platform_io_native.dart
│ ├── platform_io_stub.dart
│ ├── device_detection.dart
│ └── path_provider_native.dart
│ └── path_provider_stub.dart
├── ui/ # UI工具
│ ├── page_transitions.dart
│ ├── interaction_animations.dart
│ ├── sheet_animation_notifier.dart
│ └── clipboard_bridge.dart
├── data/ # 数据工具
│ ├── level_utils.dart
│ └── receipt_helper.dart
└── isolate_stub.dart # isolate保持不变
```
- [ ] **Step 1: 创建子目录并移动文件**
- [ ] **Step 2: 全局搜索并更新所有 import 路径**
- [ ] **Step 3: 运行 flutter analyze 验证**
---
## 验收标准
| 批次 | 验收项 |
|------|--------|
| P0 | `flutter analyze lib/` 零 error国学经典/健康模块所有分类可正常加载预加载使用英文key只有一套预加载开关阅读报告有友好错误提示排行榜自动创建赛季 |
| P1 | 排行榜前3名有缩放+光晕动画;趋势图可点击查看数据详情;领奖有庆祝动画 |
| P2 | 笔记编辑器有Markdown工具栏图片有渐进式加载离线操作重启后不丢失深度链接可跳转鸿蒙端路由同步 |
| P3 | widgets目录按职责分子目录utils目录按职责分子目录所有import路径正确 |

View File

@@ -4,7 +4,7 @@
* @author AI Coder
* @date 2026-05-14
* @desc 赛季排行榜: 赛季列表/排行榜/我的排名/领取奖励
* @update v13.0.0 初始版本
* @update v14.103.0 新增_ensureActiveSeason自动创建周赛季,leaderboard和myRank无活跃赛季时自动创建
*/
namespace app\api\controller;
@@ -95,12 +95,7 @@ class Rank extends Api
$startTime = intval($season['start_time']);
$endTime = intval($season['end_time']);
} else {
$currentSeason = Db::name('rank_season')
->where('status', 'active')
->where('start_time', '<=', time())
->where('end_time', '>', time())
->order('start_time desc')
->find();
$currentSeason = $this->_ensureActiveSeason();
if (!$currentSeason) {
$this->success('', ['type' => $rankType, 'season' => null, 'list' => [], 'is_realtime' => true]);
}
@@ -148,12 +143,7 @@ class Rank extends Api
$startTime = intval($season['start_time']);
$endTime = intval($season['end_time']);
} else {
$currentSeason = Db::name('rank_season')
->where('status', 'active')
->where('start_time', '<=', time())
->where('end_time', '>', time())
->order('start_time desc')
->find();
$currentSeason = $this->_ensureActiveSeason();
if (!$currentSeason) {
$this->success('', ['rank' => 0, 'value' => 0, 'claimed' => 0]);
}
@@ -241,9 +231,9 @@ class Rank extends Api
->join('user u', 'e.user_id = u.id', 'LEFT')
->where('e.createtime', 'between', [$startTime, $endTime])
->group('e.user_id')
->order('total_exp desc')
->order('value desc')
->limit($limit)
->field('e.user_id, SUM(e.amount) as total_exp as value, u.username, u.avatar, u.level')
->field('e.user_id, SUM(e.amount) as value, u.username, u.avatar, u.level')
->select();
break;
case 'signin':
@@ -252,9 +242,9 @@ class Rank extends Api
->join('user u', 's.user_id = u.id', 'LEFT')
->where('s.createtime', 'between', [$startTime, $endTime])
->group('s.user_id')
->order('signin_count desc')
->order('value desc')
->limit($limit)
->field('s.user_id, COUNT(*) as signin_count as value, u.username, u.avatar, u.level')
->field('s.user_id, COUNT(*) as value, u.username, u.avatar, u.level')
->select();
break;
case 'badge':
@@ -263,9 +253,9 @@ class Rank extends Api
->join('user u', 'b.user_id = u.id', 'LEFT')
->where('b.createtime', 'between', [$startTime, $endTime])
->group('b.user_id')
->order('badge_count desc')
->order('value desc')
->limit($limit)
->field('b.user_id, COUNT(*) as badge_count as value, u.username, u.avatar, u.level')
->field('b.user_id, COUNT(*) as value, u.username, u.avatar, u.level')
->select();
break;
case 'score':
@@ -275,9 +265,9 @@ class Rank extends Api
->where('s.createtime', 'between', [$startTime, $endTime])
->where('s.score', '>', 0)
->group('s.user_id')
->order('total_score desc')
->order('value desc')
->limit($limit)
->field('s.user_id, SUM(s.score) as total_score as value, u.username, u.avatar, u.level')
->field('s.user_id, SUM(s.score) as value, u.username, u.avatar, u.level')
->select();
break;
default:
@@ -299,6 +289,56 @@ class Rank extends Api
return $result;
}
/**
* @name 确保存在活跃赛季
* @desc 查询当前活跃赛季,若不存在则自动创建本周赛季
* @return array|null 当前赛季记录
*/
private function _ensureActiveSeason()
{
$now = time();
$currentSeason = Db::name('rank_season')
->where('status', 'active')
->where('start_time', '<=', $now)
->where('end_time', '>', $now)
->order('start_time desc')
->find();
if (!$currentSeason) {
$weekStart = strtotime('monday this week', $now);
if ($weekStart === false || $weekStart > $now) {
$weekStart = strtotime('last monday', $now);
}
$weekEnd = $weekStart + 7 * 86400;
$weekNum = (int)date('W', $now);
$year = date('Y', $now);
Db::name('rank_season')->insert([
'name' => "{$year}年第{$weekNum}周赛",
'type' => 'weekly',
'start_time' => $weekStart,
'end_time' => $weekEnd,
'status' => 'active',
'rewards' => json_encode([
['rank_start' => 1, 'rank_end' => 3, 'exp' => 100, 'score' => 50],
['rank_start' => 4, 'rank_end' => 10, 'exp' => 50, 'score' => 20],
['rank_start' => 11, 'rank_end' => 50, 'exp' => 20, 'score' => 10],
]),
'createtime' => $now,
'updatetime' => $now,
]);
$currentSeason = Db::name('rank_season')
->where('status', 'active')
->where('start_time', '<=', $now)
->where('end_time', '>', $now)
->order('start_time desc')
->find();
}
return $currentSeason;
}
/**
* @name 计算单个用户排名
*/

View File

@@ -0,0 +1,377 @@
# 闲言APP — 深度链接Deep Link技术文档
> 创建时间: 2026-05-23
> 更新时间: 2026-05-23
> 版本: v1.0
---
## 一、概述
闲言APP支持三种深度链接跳转方式可从推送通知、外部浏览器、分享链接等场景直接跳转到应用内指定页面
| 协议 | 格式 | 适用场景 |
|------|------|----------|
| Custom URL Scheme | `xianyan://path` | iOS/Android 通用,应用间跳转 |
| Universal Link | `https://xianyan.app/path` | iOS 无缝跳转(不经过浏览器确认) |
| App Link | `https://xianyan.app/path` | Android 无缝跳转(需验证域名) |
---
## 二、URL 格式与参数说明
### 2.1 基础格式
```
xianyan://<path>[?<query_params>]
https://xianyan.app/<path>[?<query_params>]
```
### 2.2 支持的路径映射
以下为 `_resolveDeepLinkPath()` 中注册的所有深链接路径:
| 深链接路径 | 应用内路由 | 说明 |
|-----------|-----------|------|
| `/` 或空 | `/home` | 首页 |
| `/home` | `/home` | 首页 |
| `/inspiration` | `/inspiration` | 灵感页 |
| `/profile` | `/profile` | 个人中心 |
| `/discover` | `/discover` | 发现页 |
| `/search` | `/search` | 搜索页 |
| `/favorites` | `/favorites` | 收藏页 |
| `/history` | `/history` | 历史页 |
| `/fortune` | `/daily-fortune` | 每日运势 |
| `/notes` | `/notes` | 笔记列表(需登录) |
| `/article/:id` | `/article/:id` | 文章详情 |
| `/settings/*` | `/settings/*` | 设置子页面 |
| `/weather` | `/weather` | 天气页 |
| `/weather/settings` | `/weather/settings` | 天气设置 |
| `/poetry` | `/poetry` | 诗词页 |
| `/poetry/settings` | `/poetry/settings` | 诗词设置 |
| `/achievement` | `/achievement` | 成就页(需登录) |
| `/rank` | `/rank` | 排行榜 |
| `/learning` | `/learning` | 学习中心 |
| `/checkin` | `/achievement` | 签到(跳转成就页) |
### 2.3 路径参数Path Parameters
| 路由模式 | 参数 | 类型 | 示例 |
|---------|------|------|------|
| `/article/:id` | `id` | int | `xianyan://article/42` |
| `/category/:type` | `type` | String | `xianyan://category/poetry` |
| `/user/:uid` | `uid` | int | `xianyan://user/10086` |
| `/chat-settings/:conversationId` | `conversationId` | String | `xianyan://chat-settings/abc123` |
| `/canvas/:id` | `id` | String | `xianyan://canvas/draft-001` |
| `/screen-share/:id` | `id` | String | `xianyan://screen-share/meeting-42` |
| `/agreement/:type` | `type` | String | `xianyan://agreement/privacy` |
### 2.4 查询参数Query Parameters
| 路由 | 参数 | 类型 | 示例 |
|------|------|------|------|
| `/editor` | `text` | String | `xianyan://editor?text=Hello` |
| `/chat-flow` | `sessionId` | String | `xianyan://chat-flow?sessionId=abc` |
---
## 三、使用方法
### 3.1 推送通知跳转
在推送通知的 payload 中携带深链接 URL
```json
{
"aps": {
"alert": {
"title": "今日运势",
"body": "点击查看今日运势详情"
}
},
"deep_link": "xianyan://fortune"
}
```
客户端收到通知后,从 payload 中提取 `deep_link` 字段,调用:
```dart
// 在通知回调中
final deepLink = payload['deep_link'] as String?;
if (deepLink != null) {
context.go(deepLink);
// 或使用 GoRouter:
// appRouter.go(deepLink);
}
```
### 3.2 代码中手动跳转
```dart
// 方式1: 使用 GoRouteriOS/Android
context.go('/daily-fortune');
// 方式2: 使用 OhosNavBridge鸿蒙端
OhosNavBridge.push(context, '/daily-fortune');
// 方式3: 带参数跳转
context.go('/article/42');
OhosNavBridge.push(context, '/article/42');
// 方式4: 带查询参数
context.go('/editor?text=Hello%20World');
OhosNavBridge.push(context, '/editor?text=Hello%20World');
```
### 3.3 外部浏览器跳转
用户在浏览器中访问 `https://xianyan.app/fortune`,如果已安装应用:
- **iOS**: Universal Link 自动打开应用并跳转到运势页
- **Android**: App Link 自动打开应用并跳转到运势页
- **未安装**: 显示网页版引导下载
---
## 四、平台配置
### 4.1 iOS — Universal Links
`ios/Runner/Info.plist` 中配置:
```xml
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:xianyan.app</string>
<string>applinks:www.xianyan.app</string>
</array>
```
在服务端 `https://xianyan.app/.well-known/apple-app-site-association` 放置配置文件。
### 4.2 Android — App Links
`android/app/src/main/AndroidManifest.xml` 中配置:
```xml
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="xianyan" />
<data android:scheme="https" android:host="xianyan.app" />
</intent-filter>
```
### 4.3 鸿蒙端 — Deep Link 处理
鸿蒙端通过 `OhosNavBridge` 处理深链接,路由注册表已包含所有路径:
```dart
// 鸿蒙端收到深链接后
final uri = Uri.parse('xianyan://fortune');
final path = uri.path; // '/fortune'
OhosNavBridge.push(context, path);
```
鸿蒙端路由中间件自动处理:
- **AuthMiddleware**: 需要登录的页面(如 `/notes``/achievement`)自动拦截并跳转登录页
- **LogMiddleware**: 调试模式下记录所有导航信息
---
## 五、路由重定向流程
```
外部 URL
├─ xianyan://fortune
│ └─ _handleDeepLinkRedirect()
│ └─ scheme == 'xianyan' → _resolveDeepLinkPath('/fortune')
│ └─ 返回 '/daily-fortune'
│ └─ GoRouter 导航到 DailyFortunePage
├─ https://xianyan.app/article/42
│ └─ _handleDeepLinkRedirect()
│ └─ host == 'xianyan.app' → _resolveDeepLinkPath('/article/42')
│ └─ 返回 '/article/42'
│ └─ GoRouter 导航到 ArticleDetailPage(id: 42)
└─ https://other.com/path
└─ host 不匹配 → 不处理(正常网页浏览)
```
---
## 六、完整路由常量参考
所有路由常量定义在 `lib/core/router/app_routes.dart``AppRoutes` 类中:
| 常量名 | 路径 | 用途 |
|--------|------|------|
| `home` | `/home` | 首页 |
| `inspiration` | `/inspiration` | 灵感 |
| `profile` | `/profile` | 个人中心 |
| `search` | `/search` | 搜索 |
| `favorites` | `/favorites` | 收藏 |
| `history` | `/history` | 历史 |
| `discover` | `/discover` | 发现 |
| `login` | `/login` | 登录 |
| `signin` | `/signin` | 签到 |
| `notes` | `/notes` | 笔记列表 |
| `noteEdit` | `/notes/edit` | 笔记编辑 |
| `classics` | `/classics` | 国学经典 |
| `health` | `/health` | 健康生活 |
| `game` | `/game` | 游戏中心 |
| `achievement` | `/achievement` | 成就 |
| `checkin` | `/achievement/checkin` | 签到 |
| `badgeWall` | `/badge-wall` | 勋章墙 |
| `rank` | `/rank` | 排行榜 |
| `dailyFortune` | `/daily-fortune` | 每日运势 |
| `weather` | `/weather` | 天气 |
| `poetry` | `/poetry` | 诗词 |
| `readingReport` | `/reading-report` | 阅读报告 |
| `articleDetail` | `/article/:id` | 文章详情 |
| `categoryDetail` | `/category/:type` | 分类详情 |
| `publicProfile` | `/user/:uid` | 用户主页 |
| `editor` | `/editor` | 编辑器 |
| `chatFlow` | `/chat-flow` | 对话流 |
| `chatSettings` | `/chat-settings` | 对话设置 |
| `deepFortune` | `/fortune` | 运势深链接入口 |
| `offline` | `/offline` | 离线模式 |
| `about` | `/about` | 关于 |
| `onboarding` | `/onboarding` | 引导页 |
> 完整列表请参考源码 `AppRoutes` 类约115个路由常量
---
## 七、可扩展功能
### 7.1 新增深链接路径
`_resolveDeepLinkPath()` 中添加新的 case 分支:
```dart
String? _resolveDeepLinkPath(String path) {
// ...
return switch (segments.first) {
'fortune' => AppRoutes.dailyFortune,
'article' => path,
// ↓ 新增深链接
'pomodoro' => AppRoutes.pomodoro,
'countdown' => AppRoutes.countdown,
_ => path,
};
}
```
同时在 `AppRoutes` 中添加常量:
```dart
static const String deepPomodoro = '/pomodoro';
```
### 7.2 新增鸿蒙端路由
`OhosNavBridge._routes` 中添加注册条目:
```dart
OhosRouteEntry(
pattern: '/pomodoro',
builder: (_) => const PomodoroPage(),
),
```
> ⚠️ **同步提醒**:新增路由时,必须同时更新以下文件:
> 1. `app_routes.dart` — 路由路径常量
> 2. 对应的模块路由文件settings_routes / tool_routes / editor_router 等)
> 3. `ohos_nav_bridge.dart` — 鸿蒙端路由注册表
> 4. `CHANGELOG.md` — 变更日志
### 7.3 自定义中间件
可扩展 `OhosNavMiddleware` 创建新的路由守卫:
```dart
class FeatureFlagMiddleware extends OhosNavMiddleware {
final String featureKey;
FeatureFlagMiddleware(this.featureKey);
@override
OhosMiddlewareResult handle(OhosRouteContext context) {
final enabled = KvStorage.getBool('feature_$featureKey') ?? false;
if (!enabled) {
return const OhosMiddlewareReject('功能未开放');
}
return const OhosMiddlewareNext();
}
}
```
### 7.4 延迟加载路由
对于大型页面,可使用懒加载减少启动时间:
```dart
OhosRouteEntry(
pattern: '/knowledge-graph',
builder: (_) => const KnowledgeGraphPage(),
// 可扩展为异步加载
),
```
### 7.5 深链接统计分析
可在 `_handleDeepLinkRedirect` 中添加埋点:
```dart
if (scheme == 'xianyan') {
AnalyticsService.track('deep_link_received', {
'url': uri.toString(),
'path': uri.path,
'source': 'custom_scheme',
});
// ...
}
```
---
## 八、测试验证
### 8.1 命令行测试iOS 模拟器)
```bash
xcrun simctl openurl booted "xianyan://fortune"
xcrun simctl openurl booted "xianyan://article/42"
xcrun simctl openurl booted "https://xianyan.app/rank"
```
### 8.2 命令行测试Android
```bash
adb shell am start -a android.intent.action.VIEW -d "xianyan://fortune"
adb shell am start -a android.intent.action.VIEW -d "https://xianyan.app/rank"
```
### 8.3 Flutter 测试代码
```dart
testWidgets('deep link redirect', (tester) async {
final router = appRouter;
router.go('/fortune');
await tester.pumpAndSettle();
expect(router.routerDelegate.currentConfiguration.uri.path,
equals('/daily-fortune'));
});
```
---
## 九、注意事项
1. **登录态路由**`/notes``/achievement``/chat-settings/:id` 等路由需要登录态,未登录时鸿蒙端通过 `AuthMiddleware` 自动重定向到 `/login`
2. **参数类型**:路径参数 `:id``:uid` 为整数类型,传入非数字会导致页面异常
3. **URL编码**:查询参数中的中文和特殊字符需要 URL 编码(如 `text=%E4%BD%A0%E5%A5%BD`
4. **鸿蒙端限制**:鸿蒙端不使用 GoRouter通过 `OhosNavBridge.push()` 跳转,所有路由必须在 `_routes` 注册表中注册
5. **首次安装**:首次安装用户会先进入引导页 `/onboarding`,深链接会在引导完成后生效

View File

@@ -1,175 +0,0 @@
# 每日运势功能 — 进度跟踪与验收文档
- **创建时间**: 2026-05-13
- **更新时间**: 2026-05-13
- **作用**: 每日运势功能开发进度跟踪、验收审计、状态归档
- **更新时间**: 2026-05-13
- **版本**: v12.1.0
---
## 一、功能概述
在闲言APP足迹页面的会话流中新增"每日运势"系统会话,支持:
- 每天自动生成运势卡片(类微信运动推送)
- 三种卡片风格(古风签筒/微信运动/Apple Health
- 全维度运势6维度+幸运指标+宜忌+签文)
- 今日可换签,过去锁定
- 历史运势本地保存,可编辑
- 设置页面管理显示/推送/风格/排序
- 聚合第三方API60s新闻/黄历/星座运势)
---
## 二、开发任务清单
### 2.1 服务端开发
| # | 任务 | 状态 | 验收标准 | 备注 |
|---|------|------|---------|------|
| S1 | Fortune.php 控制器 | ✅ | 所有接口正常响应 | 核心文件 |
| S2 | 运势生成算法(hash+seed) | ✅ | 同人同日结果确定 | mt_srand(crc32($seed)) |
| S3 | 签文数据导入(tool_fortune_data) | ✅ | 至少100条签文 | seedFortuneDataDb() |
| S4 | 数据库建表(install接口) | ✅ | 3张表创建成功 | fortune_data/record/config |
| S5 | 路由注册(route.php) | ✅ | 所有路由可访问 | 12个接口 |
| S6 | 60秒新闻聚合 | ✅ | viki源可用 | viki.moe/v2/60s |
| S7 | 黄历数据聚合 | ✅ | timor源可用 | timor.tech |
| S8 | 星座运势聚合 | ✅ | 降级可用 | 自建数据+第三方 |
| S9 | 运势图片生成(GD库) | ✅ | 可生成PNG | 多字体路径回退 |
| S10 | 部署脚本(deploy_fortune.py) | ✅ | 一键部署到服务器 | paramiko SSH |
| S11 | 全流程接口测试 | ✅ | 核心接口code=1 | curl测试 |
### 2.2 客户端开发Flutter
| # | 任务 | 状态 | 验收标准 | 备注 |
|---|------|------|---------|------|
| C1 | 会话流新增"每日运势"条目 | ✅ | 灵感页面可见 | ChatSession注册 |
| C2 | 运势时间线页面 | ✅ | 点击展开运势卡片 | DailyFortunePage |
| C3 | 运势卡片组件(3种风格) | ✅ | 动态主题+动态样式 | FortuneCardWidget |
| C4 | 换一签功能 | ✅ | 今日可换,过去锁定 | regen=1 |
| C5 | 运势设置页面 | ✅ | 所有设置项可操作 | FortuneSettingsPage |
| C6 | 排序设置(最新/最早在上面) | ✅ | 切换后时间线重排 | sort_order |
| C7 | 本地数据保存(Drift) | ✅ | 历史运势持久化 | FortuneRecords表+CRUD+v14迁移 |
| C8 | 推送通知集成 | ✅ | 每日定时推送 | 通知设置页+id:1003+运势推送路由 |
| C9 | 运势卡片编辑功能 | ✅ | 过去的运势可编辑备注 | CupertinoAlertDialog+本地保存 |
| C10 | SVG图标系统 | ✅ | 维度图标用emoji | ❤️💼💰💪📚🤝 |
### 2.3 第三方API对接
| # | API | 状态 | 测试结果 | 备注 |
|---|-----|------|---------|------|
| A1 | 60s.viki.moe/v2/60s | ✅ | code:1, 数据正常 | 主源可用 |
| A2 | api.vvhan.com/api/60s | ❌ | DNS解析失败 | 服务器不可达 |
| A3 | tenapi.cn/v2/60s | ⬜ | - | 待测试 |
| A4 | api.vvhan.com/api/horoscope | ❌ | DNS解析失败 | 服务器不可达 |
| A5 | tenapi.cn/v2/star | ⬜ | - | 待测试 |
| A6 | api.03c3.cn/api/constellation | ⬜ | - | 待测试 |
| A7 | api.vvhan.com/api/laohuangli | ❌ | DNS解析失败 | 服务器不可达 |
| A8 | timor.tech/api/holiday/info | ✅ | 黄历数据正常 | 备源可用 |
| A9 | api.vvhan.com/api/fortune | ❌ | DNS解析失败 | 服务器不可达 |
| A10 | api.vvhan.com/api/lucky | ❌ | DNS解析失败 | 服务器不可达 |
| A11 | api.vvhan.com/api/daily | ❌ | DNS解析失败 | 服务器不可达 |
| A12 | api.vvhan.com/api/nongli | ❌ | DNS解析失败 | 服务器不可达 |
| A13 | 60s.viki.moe/history | ⬜ | - | 待测试 |
| A14 | api.vvhan.com/api/dujitang | ❌ | DNS解析失败 | 服务器不可达 |
---
## 三、客户端文件清单
| 文件 | 路径 | 状态 | 说明 |
|------|------|------|------|
| fortune_models.dart | lib/features/daily_fortune/models/ | ✅ | 数据模型(FortuneRecord/Config/Level/Dimension/Lucky/CardStyle) |
| fortune_service.dart | lib/features/daily_fortune/services/ | ✅ | API服务(8个接口方法) |
| fortune_provider.dart | lib/features/daily_fortune/providers/ | ✅ | 状态管理(FortuneNotifier/State/Provider) |
| daily_fortune_page.dart | lib/features/daily_fortune/presentation/ | ✅ | 主页面(时间线+展开卡片+换签) |
| fortune_card_widget.dart | lib/features/daily_fortune/presentation/widgets/ | ✅ | 卡片组件(3风格:古风/微信/Apple) |
| fortune_settings_page.dart | lib/features/daily_fortune/presentation/ | ✅ | 设置页面(风格/显示/推送/排序/扩展) |
---
## 四、验收检查单
### 4.1 服务端验收
- [x] Fortune.php 文件已上传到服务器
- [x] route.php 已添加运势路由并上传
- [x] 数据库表已创建install接口返回成功
- [x] 签文数据已导入
- [x] `/api/fortune/daily?uid=test` 返回正确运势数据
- [x] `/api/fortune/daily?uid=test&regen=1` 返回不同运势(换签)
- [x] `/api/fortune/history?uid=test` 返回历史列表
- [x] `/api/fortune/config?uid=test` 返回用户配置
- [x] `/api/fortune/themes` 返回3种风格
- [x] `/api/fortune/sixtySeconds` 返回今日新闻
- [x] `/api/fortune/huangli` 返回黄历数据
- [x] `/api/fortune/horoscope?sign=白羊` 返回星座运势
- [x] `/api/fortune/image?uid=test` 返回图片URL
- [x] 第三方API降级策略正常
### 4.2 客户端验收
- [x] 灵感页面会话列表可见"每日运势"条目
- [x] 点击进入运势时间线页面
- [x] 时间线显示今日+历史运势
- [x] 点击摘要展开完整运势卡片
- [x] 3种卡片风格可切换且跟随主题
- [x] Dark/Light主题切换正常
- [x] 今日运势"换一签"按钮可用
- [x] 过去运势显示"已锁定"
- [x] 编辑功能可用(过去的运势)
- [x] 设置页面所有开关可操作
- [x] 排序设置切换后时间线重排
- [x] 推送通知按时触发
### 4.3 API接口测试
- [x] 所有核心接口HTTP状态码200
- [x] 所有核心接口返回JSON格式正确
- [x] 所有核心接口code=1成功
- [x] 运势数据维度完整6维度+幸运+宜忌+签文)
- [x] 同人同日运势确定性(多次请求结果一致)
- [x] 换签后运势变化regen=1结果不同
- [x] 历史运势分页正常
- [x] 第三方API聚合降级正常
---
## 五、部署记录
| # | 日期 | 操作 | 结果 | 操作人 |
|---|------|------|------|--------|
| 1 | 2026-05-13 | 初始部署Fortune.php+route.php | ✅ 成功 | deploy_fortune.py |
| 2 | 2026-05-13 | 全流程接口测试 | ✅ 核心接口通过 | curl |
| 3 | 2026-05-13 | Flutter客户端开发 | ✅ 6个文件创建 | - |
---
## 六、问题与风险归档
| # | 问题描述 | 严重程度 | 状态 | 解决方案 | 归档日期 |
|---|---------|---------|------|---------|---------|
| 1 | vvhan.com DNS在服务器无法解析 | 中 | ✅已解决 | 使用viki.moe和timor.tech替代 | 2026-05-13 |
| 2 | 60s路由路径`60s`在ThinkPHP中异常 | 中 | ✅已解决 | 改为`sixtySeconds` | 2026-05-13 |
| 3 | install接口PDO报错 | 高 | ✅已解决 | 改用Db::execute() | 2026-05-13 |
| 4 | 图片生成字体路径问题 | 低 | ✅已解决 | 多路径回退+imagestring兜底 | 2026-05-13 |
---
## 七、版本变更
| 版本 | 日期 | 变更内容 |
|------|------|---------|
| v12.0.0 | 2026-05-13 | 新增每日运势功能含服务端API+客户端会话流+3种卡片风格+第三方API聚合 |
---
## 八、参考资源
| 资源 | 链接 |
|------|------|
| nonebot_plugin_fortune | https://github.com/MinatoAquaCrews/nonebot_plugin_fortune |
| 60s (vikiboss) | https://github.com/vikiboss/60s |
| 60s-web (dogxii) | https://github.com/dogxii/60s-web |
| API接口文档 | `docs/toolsapi/docs/API_FORTUNE_DOC.md` |
| 设计预览 | `docs/toolsapi/preview/fortune_design.html` |

View File

@@ -0,0 +1,182 @@
# 闲言APP 服务端部署与维护指南
## 一、服务器信息
- 服务器IP: 123.207.67.197
- SSH端口: 22
- 项目路径: /www/wwwroot/tools.wktyl.com/
- 运行目录: tools.wktyl.com/public → tools.wktyl.com
- 数据库: MySQL (tools库, 前缀 tool_)
- Web服务器: Nginx + PHP (ThinkPHP5框架)
- **python**: C:\Users\无书\AppData\Local\Python\pythoncore-3.14-64\python.exe
## 二、代码上传方式
### 2.1 使用上传脚本(推荐)
项目提供了 Python 上传脚本 `Scripts/upload_server_code.py`,基于 paramiko SFTP。
使用方法:
```bash
# 安装依赖
pip install paramiko
# 执行上传
python Scripts/upload_server_code.py
```
### 2.2 手动SFTP上传
```bash
sftp root@123.207.67.197
# 密码: 520Kiss123
# 上传控制器文件
put docs/toolsapi/application/api/controller/Rank.php /www/wwwroot/tools.wktyl.com/application/api/controller/Rank.php
```
### 2.3 手动SSH操作
```bash
ssh root@123.207.67.197
# 密码: 520Kiss123
# 查看项目结构
ls -la /www/wwwroot/tools.wktyl.com/
# 查看PHP错误日志
tail -f /www/wwwroot/tools.wktyl.com/runtime/log/
# 重启PHP-FPM
systemctl restart php-fpm
```
## 三、数据库操作
### 3.1 数据库连接信息
- 数据库名: tools
- 用户名: tools
- 表前缀: tool_
- 字符集: utf8mb4
### 3.2 执行SQL
```bash
# SSH登录服务器后
mysql -u tools -p tools
# 或通过宝塔面板 > 数据库 > phpMyAdmin
```
### 3.3 常用SQL操作
```sql
-- 查看所有表
SHOW TABLES;
-- 创建排行榜赛季表(如不存在)
CREATE TABLE IF NOT EXISTS `tool_rank_season` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL DEFAULT '',
`type` enum('weekly','monthly') NOT NULL DEFAULT 'weekly',
`start_time` int(11) unsigned NOT NULL DEFAULT 0,
`end_time` int(11) unsigned NOT NULL DEFAULT 0,
`status` enum('active','settled','cancelled') NOT NULL DEFAULT 'active',
`rewards` text DEFAULT NULL,
`createtime` int(11) unsigned NOT NULL DEFAULT 0,
`updatetime` int(11) unsigned NOT NULL DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 创建排行记录表
CREATE TABLE IF NOT EXISTS `tool_rank_record` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`season_id` int(11) unsigned NOT NULL DEFAULT 0,
`rank_type` enum('exp','signin','badge','score') NOT NULL DEFAULT 'exp',
`user_id` int(11) unsigned NOT NULL DEFAULT 0,
`rank` int(11) unsigned NOT NULL DEFAULT 0,
`value` int(11) NOT NULL DEFAULT 0,
`reward_claimed` tinyint(1) NOT NULL DEFAULT 0,
`createtime` int(11) unsigned NOT NULL DEFAULT 0,
`updatetime` int(11) unsigned NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
KEY `idx_season_type` (`season_id`, `rank_type`),
KEY `idx_user` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 创建活跃赛季(用于测试)
INSERT INTO `tool_rank_season` (`name`, `type`, `start_time`, `end_time`, `status`, `createtime`, `updatetime`)
VALUES ('2026年第21周赛', 'weekly', UNIX_TIMESTAMP(), UNIX_TIMESTAMP() + 7*86400, 'active', UNIX_TIMESTAMP(), UNIX_TIMESTAMP());
-- 查看当前赛季
SELECT * FROM `tool_rank_season` WHERE `status` = 'active';
-- 查看排行榜数据
SELECT * FROM `tool_rank_record` WHERE `season_id` = 1 ORDER BY `rank` ASC LIMIT 20;
```
### 3.4 API安装接口
部分模块提供了自动建表接口:
- 排行榜: GET /api/rank/install (如已实现)
- 每日运势: GET /api/fortune/install
- 云端暂存: POST /api/cloud_cache/install
## 四、接口更改流程
### 4.1 修改接口步骤
1. 在本地 `docs/toolsapi/application/` 目录修改PHP代码
2. 更新对应的API文档 `docs/toolsapi/docs/API_*.md`
3. 运行上传脚本或手动SFTP上传到服务器
4. 运行测试脚本验证接口
5. 提交Git
### 4.2 新增接口步骤
1.`docs/toolsapi/application/api/controller/` 下创建新控制器
2.`docs/toolsapi/application/api/config/route.php` 中注册路由
3. 编写API文档
4. 上传到服务器
5. 运行测试脚本
## 五、API测试
### 5.1 使用测试脚本
```bash
# 排行榜API测试
python Scripts/test_rank_api.py
# 完整API测试如已有
python Scripts/account_insights_full_test.py
```
### 5.2 手动curl测试
```bash
# 测试排行榜
curl -s "https://tools.wktyl.com/api/rank/seasons" | python -m json.tool
curl -s "https://tools.wktyl.com/api/rank/leaderboard?type=exp" | python -m json.tool
# 测试每日运势
curl -s "https://tools.wktyl.com/api/fortune/daily?uid=test_user" | python -m json.tool
# 测试IP查询
curl -X POST "https://tools.wktyl.com/api/webapi/ip" -d "ip=8.8.8.8" | python -m json.tool
```
## 六、文档更新规范
### 6.1 API文档格式
所有API文档存放在 `docs/toolsapi/docs/` 目录使用Markdown格式包含
- 接口列表方法、URL、说明
- 请求参数(字段、类型、必填、说明)
- 响应示例JSON
- 错误码
- 变更日志
### 6.2 文档命名规范
`API_{模块名}_DOC.md`,如 `API_RANK_DOC.md``API_FORTUNE_DOC.md`
## 七、常见问题
| 问题 | 原因 | 解决方案 |
|------|------|----------|
| 接口返回 code=0 | 参数错误或服务端异常 | 检查PHP错误日志 |
| 排行榜数据为空 | 无活跃赛季 | 创建赛季或等待自动创建 |
| 上传脚本连接失败 | SSH密码变更 | 更新脚本中的PASS字段 |
| SQL执行报错 | 表已存在或字段冲突 | 先DROP TABLE再重建 |
| API 404 | 路由未注册 | 检查route.php配置 |
---
*文档创建时间: 2026-05-23 | 维护者: 闲言APP开发团队*