本次更新包含多项功能优化与bug修复: 1. 新增flutter_keyboard_visibility依赖替代MediaQuery轮询获取键盘状态 2. 添加远程功能标志API支持与FeatureFlag服务 3. 重构壁纸背景渲染组件,统一全局壁纸展示逻辑 4. 延迟初始化壁纸源健康检测至用户同意协议后 5. 修复预测返回/长按预览锁定问题并移除相关配置项 6. 优化日志输出控制,release模式仅保留错误日志 7. 新增进度模块多语言翻译与相关UI字段 8. 优化稍后读功能,取消时同步删除聊天消息 9. 更新权限说明文档,移除冗余的存储写入权限配置 10. 重构部分UI组件减少参数传递,优化性能
211 lines
7.1 KiB
PHP
211 lines
7.1 KiB
PHP
<?php
|
||
|
||
namespace app\api\controller;
|
||
|
||
use app\common\controller\Api;
|
||
|
||
/**
|
||
* @name 功能标志接口
|
||
* @author AI Coder
|
||
* @date 2026-05-30
|
||
* @desc 提供Feature Flag远程配置,支持灰度发布和A/B测试
|
||
* @update v1.0 初始版本,支持列表查询和单个检查
|
||
*/
|
||
class FeatureFlag extends Api
|
||
{
|
||
protected $noNeedLogin = ['*'];
|
||
protected $noNeedRight = ['*'];
|
||
|
||
private static $flags = [
|
||
[
|
||
'key' => 'ai_summary',
|
||
'name' => 'AI智能摘要',
|
||
'description' => '自动为长文生成摘要',
|
||
'emoji' => '🤖',
|
||
'enabled' => true,
|
||
'status' => 'testing',
|
||
'progress' => 0.7,
|
||
'rollout_percentage' => 0.3,
|
||
'target_group' => null,
|
||
'expires_at' => null,
|
||
],
|
||
[
|
||
'key' => 'theme_store',
|
||
'name' => '主题商店',
|
||
'description' => '下载和分享自定义主题',
|
||
'emoji' => '🎨',
|
||
'enabled' => false,
|
||
'status' => 'developing',
|
||
'progress' => 0.4,
|
||
'rollout_percentage' => 0.0,
|
||
'target_group' => null,
|
||
'expires_at' => null,
|
||
],
|
||
[
|
||
'key' => 'reading_report',
|
||
'name' => '阅读报告',
|
||
'description' => '年度/月度阅读数据可视化',
|
||
'emoji' => '📊',
|
||
'enabled' => true,
|
||
'status' => 'preview',
|
||
'progress' => 0.9,
|
||
'rollout_percentage' => 0.5,
|
||
'target_group' => null,
|
||
'expires_at' => null,
|
||
],
|
||
[
|
||
'key' => 'cross_device_sync',
|
||
'name' => '跨设备同步',
|
||
'description' => '实时同步阅读进度和收藏',
|
||
'emoji' => '🔄',
|
||
'enabled' => false,
|
||
'status' => 'developing',
|
||
'progress' => 0.3,
|
||
'rollout_percentage' => 0.0,
|
||
'target_group' => null,
|
||
'expires_at' => null,
|
||
],
|
||
[
|
||
'key' => 'voice_reading',
|
||
'name' => '语音朗读',
|
||
'description' => 'AI语音朗读句子和文章',
|
||
'emoji' => '🎵',
|
||
'enabled' => true,
|
||
'status' => 'testing',
|
||
'progress' => 0.6,
|
||
'rollout_percentage' => 0.2,
|
||
'target_group' => 'ab_test_voice',
|
||
'expires_at' => null,
|
||
],
|
||
[
|
||
'key' => 'home_widget',
|
||
'name' => '桌面小组件',
|
||
'description' => 'iOS/Android桌面Widget',
|
||
'emoji' => '📱',
|
||
'enabled' => true,
|
||
'status' => 'preview',
|
||
'progress' => 0.85,
|
||
'rollout_percentage' => 0.8,
|
||
'target_group' => null,
|
||
'expires_at' => null,
|
||
],
|
||
[
|
||
'key' => 'nearby_discovery',
|
||
'name' => '近场发现',
|
||
'description' => '基于蓝牙/WiFi的近场内容发现',
|
||
'emoji' => '📡',
|
||
'enabled' => false,
|
||
'status' => 'developing',
|
||
'progress' => 0.15,
|
||
'rollout_percentage' => 0.0,
|
||
'target_group' => null,
|
||
'expires_at' => null,
|
||
],
|
||
[
|
||
'key' => 'shader_background',
|
||
'name' => '特效背景',
|
||
'description' => 'Metal/Vulkan着色器动态背景',
|
||
'emoji' => '✨',
|
||
'enabled' => false,
|
||
'status' => 'developing',
|
||
'progress' => 0.25,
|
||
'rollout_percentage' => 0.0,
|
||
'target_group' => null,
|
||
'expires_at' => null,
|
||
],
|
||
];
|
||
|
||
/**
|
||
* @name 获取所有Feature Flag列表
|
||
* @desc 返回所有功能标志配置,支持按status筛选
|
||
*/
|
||
public function list()
|
||
{
|
||
$status = $this->request->param('status', '');
|
||
|
||
$flags = self::$flags;
|
||
|
||
if (!empty($status)) {
|
||
$flags = array_values(array_filter($flags, function ($f) use ($status) {
|
||
return $f['status'] === $status;
|
||
}));
|
||
}
|
||
|
||
$summary = [
|
||
'total' => count($flags),
|
||
'developing' => count(array_filter($flags, function ($f) { return $f['status'] === 'developing'; })),
|
||
'testing' => count(array_filter($flags, function ($f) { return $f['status'] === 'testing'; })),
|
||
'preview' => count(array_filter($flags, function ($f) { return $f['status'] === 'preview'; })),
|
||
'released' => count(array_filter($flags, function ($f) { return $f['status'] === 'released'; })),
|
||
];
|
||
|
||
$this->success('成功', [
|
||
'flags' => $flags,
|
||
'summary' => $summary,
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* @name 检查单个Flag状态
|
||
* @desc 根据key查询单个功能标志的启用状态
|
||
*/
|
||
public function check()
|
||
{
|
||
$key = $this->request->param('key', '');
|
||
|
||
if (empty($key)) {
|
||
$this->error('缺少key参数');
|
||
}
|
||
|
||
$flag = null;
|
||
foreach (self::$flags as $f) {
|
||
if ($f['key'] === $key) {
|
||
$flag = $f;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if ($flag === null) {
|
||
$this->error('未找到该功能标志', null, 404);
|
||
}
|
||
|
||
$userId = $this->request->param('user_id', '');
|
||
$rolloutAllowed = true;
|
||
|
||
if ($flag['enabled'] && $flag['rollout_percentage'] < 1.0) {
|
||
$rolloutAllowed = $this->_isRolloutAllowed($key, $userId, $flag['rollout_percentage']);
|
||
}
|
||
|
||
$isAvailable = $flag['enabled'] && $rolloutAllowed;
|
||
|
||
if (!empty($flag['expires_at'])) {
|
||
$expiresAt = strtotime($flag['expires_at']);
|
||
if ($expiresAt !== false && time() > $expiresAt) {
|
||
$isAvailable = false;
|
||
}
|
||
}
|
||
|
||
$this->success('成功', [
|
||
'flag' => $flag,
|
||
'is_available' => $isAvailable,
|
||
'rollout_allowed' => $rolloutAllowed,
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* @name 灰度放量判断
|
||
* @desc 基于用户ID哈希判断是否在灰度范围内
|
||
*/
|
||
private function _isRolloutAllowed($flagKey, $userId, $percentage)
|
||
{
|
||
if ($percentage >= 1.0) return true;
|
||
if ($percentage <= 0.0) return false;
|
||
|
||
$combined = $flagKey . ':' . $userId;
|
||
$hash = crc32($combined) & 0x7FFFFFFF;
|
||
$bucket = $hash % 100;
|
||
|
||
return $bucket < ($percentage * 100);
|
||
}
|
||
}
|