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:
793
docs/superpowers/plans/2026-05-23-bugfix-enhancement-plan.md
Normal file
793
docs/superpowers/plans/2026-05-23-bugfix-enhancement-plan.md
Normal 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路径正确 |
|
||||
@@ -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 计算单个用户排名
|
||||
*/
|
||||
|
||||
377
docs/toolsapi/docs/DEEP_LINK_GUIDE.md
Normal file
377
docs/toolsapi/docs/DEEP_LINK_GUIDE.md
Normal 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: 使用 GoRouter(iOS/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`,深链接会在引导完成后生效
|
||||
@@ -1,175 +0,0 @@
|
||||
# 每日运势功能 — 进度跟踪与验收文档
|
||||
|
||||
- **创建时间**: 2026-05-13
|
||||
- **更新时间**: 2026-05-13
|
||||
- **作用**: 每日运势功能开发进度跟踪、验收审计、状态归档
|
||||
- **更新时间**: 2026-05-13
|
||||
- **版本**: v12.1.0
|
||||
|
||||
---
|
||||
|
||||
## 一、功能概述
|
||||
|
||||
在闲言APP足迹页面的会话流中新增"每日运势"系统会话,支持:
|
||||
- 每天自动生成运势卡片(类微信运动推送)
|
||||
- 三种卡片风格(古风签筒/微信运动/Apple Health)
|
||||
- 全维度运势(6维度+幸运指标+宜忌+签文)
|
||||
- 今日可换签,过去锁定
|
||||
- 历史运势本地保存,可编辑
|
||||
- 设置页面管理显示/推送/风格/排序
|
||||
- 聚合第三方API(60s新闻/黄历/星座运势)
|
||||
|
||||
---
|
||||
|
||||
## 二、开发任务清单
|
||||
|
||||
### 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®en=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` |
|
||||
182
docs/toolsapi/docs/SERVER_DEPLOYMENT_GUIDE.md
Normal file
182
docs/toolsapi/docs/SERVER_DEPLOYMENT_GUIDE.md
Normal 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开发团队*
|
||||
Reference in New Issue
Block a user