iOS 提交

This commit is contained in:
Developer
2026-06-27 04:57:00 +08:00
parent 10a917adf6
commit b8d0bd39b5
20 changed files with 5403 additions and 247 deletions

View File

@@ -1,5 +1,208 @@
# CHANGELOG
## v10.3.2 (2026-06-27)
### 🔐 隐私权管理增强 - 多方式登录/忘记密码注销/SMTP检测
**重要更新**完善隐私权管理页面登录与注销流程新增SMTP配置检测、4种登录方式、忘记密码注销回执码流程前端UI全面重构。
#### 新增功能
| 功能 | 说明 | 优先级 |
|------|------|--------|
| 🔍 SMTP配置检测 | 公开API检测SMTP主机/端口/加密/密码是否配置完整+TCP端口可达性 | ⭐⭐⭐ |
| 🔑 4种登录方式 | 密码/密保问题/邮箱验证码/回执号登录iOS分段控制器切换 | ⭐⭐⭐⭐⭐ |
| 📧 忘记密码注销 | 凭邮箱或密保答案验证身份→发送回执码→凭回执码提交注销(无需登录) | ⭐⭐⭐⭐⭐ |
| 👤 状态查询显示昵称 | accountLookup返回nickname字段(脱敏处理,非账号) | ⭐⭐⭐⭐ |
| 🌐 URL语言后缀 | 切换语言时URL同步更新`?lang=zh`/`?lang=en` | ⭐⭐⭐ |
| 📋 注销事件查询 | 原"设备Tab"改为"注销事件查询",展示注销申请记录+删除进度 | ⭐⭐⭐ |
| ✅ 注销协议勾选框 | 注销确认输入框下方增加协议勾选(可超链接),未勾选禁止提交 | ⭐⭐⭐⭐ |
#### 新增API接口
| 接口 | 方法 | 路径 | 说明 |
|------|------|------|------|
| SMTP状态检测 | GET | `/api/user_security/checkSmtpStatus` | 返回配置完整度+端口可达性 |
| 密保问题登录 | POST | `/api/user_security/secQuestionLogin` | account+sec_answer |
| 邮箱验证码登录 | POST | `/api/user_security/emailCodeLogin` | email+captcha(未注册自动创建) |
| 发送注销回执码 | POST | `/api/user_security/sendDeletionCode` | 验证身份后发送6位码到邮箱 |
| 凭回执码注销 | POST | `/api/user_security/requestDeletionByReceipt` | 无需登录,凭回执码提交注销 |
#### 前端变更
| 区域 | 变更 |
|------|------|
| Tab结构 | 移除"其他协议"Tab"设备"Tab改为"📋 注销事件查询" |
| 登录表单 | 重构为4种方式(iOS SegmentedControl):密码/密保/邮箱验证码/回执号 |
| 忘记密码注销 | 新增可折叠区域:邮箱/密保两种验证→发送回执码→提交注销 |
| 注销确认 | 增加协议勾选框(超链接《账号使用协议》和《隐私政策》) |
| 状态查询 | 显示"👤 用户昵称: xxx" |
| URL语言 | `?lang=zh`/`?lang=en` 后缀同步(history.replaceState) |
| 多语言 | zh/en各新增约50个I18N key |
| 文件行数 | 1942行 → 2687行 |
#### 测试结果
```
✅ SMTP配置检测 配置完整+端口可达(host=free.mboxhosting.com:465/SSL)
✅ accountLookup昵称 返回nickname字段
✅ 密保问题登录 API正常(测试账号未设密保→正确返回错误)
✅ 发送注销回执码 回执码已发送到 pt*****@test.local
✅ 错误回执码拦截 错误回执码被正确拦截
✅ 前端5个URL HTTP 200 (中文/英文/注销Tab/事件Tab/偏好Tab)
通过率: 10/10 (100%)
```
---
## v10.3.1 (2026-06-27)
### 🛡️ 隐私权管理增强 - GDPR/PIPL/COPPA合规升级
**重要更新**完善隐私权管理功能新增6大能力(隐私偏好/设备管理/注销进度/未成年保护/邮件通知/数据最小化)修复跨域BUG达到GDPR/PIPL/COPPA合规要求。
#### 新增功能
| 功能 | 说明 | 合规依据 |
|------|------|----------|
| 🔧 隐私偏好管理 | 4类数据处理同意开关(analytics/marketing/personalization/third_party_share) | GDPR Art.7 |
| 📱 登录设备管理 | 设备列表/位置/最后活跃时间,支持远程登出 | 用户体验+安全 |
| 📊 数据删除进度可视化 | 注销后展示8类数据删除进度条(账号/Token/设备/收藏/笔记/签到/积分/文章) | 透明度要求 |
| 👨‍👩‍👧 未成年保护 | 注册时校验出生日期14岁以下禁止注册14-17岁开启家长控制(每日120分钟/内容过滤/禁消费) | COPPA/GDPR-K |
| 📧 注销邮件通知 | 用户提交注销后异步发送确认邮件到注册邮箱,防冒用 | 安全要求 |
| 🗑️ 数据最小化保留 | 注销记录保留期从2年→6个月username做SHA256哈希存储 | PIPL最小化原则 |
#### BUG修复
| BUG | 原因 | 修复 |
|-----|------|------|
| ❌ 注销提交后后台查不到 | `API_BASE='https://tools.wktyl.com'`从s2ss.com发起跨域请求`__token__`响应头不可读导致authToken=null | API_BASE改为相对路径`''`优先从JSON body读取token |
| ❌ 管理后台注销列表空 | admin/controller/Userdeletion.php中`$list`查询未带filter | 使用单一`$listQuery`统一过滤 |
| ❌ 英文模式下输入框仍显示中文 | `switchLang()`未更新placeholder | 新增`phLookupAccount`/`phLoginAccount`/`phLoginPassword`翻译 |
| ❌ SQL迁移ADD COLUMN IF NOT EXISTS失败 | 服务器MySQL版本不支持该语法 | 改用存储过程+INFORMATION_SCHEMA检查 |
#### 新增API接口
| 接口 | 方法 | 路径 | 说明 |
|------|------|------|------|
| 隐私偏好查询 | GET | `/api/user_security/getConsents` | 返回4类同意状态 |
| 隐私偏好更新 | POST | `/api/user_security/updateConsents` | 批量更新同意开关 |
| 登录设备列表 | GET | `/api/user_security/listDevices` | 返回设备列表含位置/最后活跃 |
| 远程登出设备 | POST | `/api/user_security/revokeDevice` | 通过id/device_id登出指定设备 |
| 注销进度查询 | GET | `/api/user_security/deletionProgress` | 返回8类数据删除状态+百分比 |
| 家长控制状态 | GET | `/api/user_security/parentalControlStatus` | 返回未成年状态+家长控制配置 |
| 更新出生日期 | POST | `/api/user_security/updateBirthday` | 补充/修改出生日期,自动更新未成年状态 |
| 清理旧注销记录 | GET | `/api/user_security/cleanupOldDeletionRecords` | 清理6个月以上已完成注销记录 |
#### 数据库变更
| 表 | 变更 | 说明 |
|----|------|------|
| `tool_user_consents` | 新建表 | 用户数据处理同意记录(user_id+consent_type唯一) |
| `tool_user_deletion` | 新增 `deletion_progress` text | 注销进度JSON |
| `tool_user_deletion` | 新增 `deletion_summary` varchar(500) | 注销数据摘要 |
| `tool_user` | 新增 `birthday` date | 出生日期 |
| `tool_user` | 新增 `is_minor` tinyint(1) | 是否未成年(0/1) |
#### 前端变更
| 文件 | 变更 |
|------|------|
| `privacy-rights.html` | 4→6 Tab(新增📱设备/⚙️偏好),注销进度条,浏览器语言自动检测,登录态联动 |
| `account-agreement.html` | V6.6→V6.7保留期2年→6个月+SHA256哈希 |
#### 测试结果
```
✅ register(带birthday) code=1 注册成功age=31 is_minor=0
✅ parentalControlStatus code=1 返回家长控制配置
✅ updateBirthday(未成年) code=1 改为2010年生is_minor=1
✅ getConsents code=1 返回4项同意配置
✅ updateConsents code=1 成功更新4项
✅ listDevices code=1 返回设备列表
✅ deletionProgress code=1 返回进度
✅ privacy-rights.html HTTP 200 (devices/preferences tab正常)
通过率: 8/8 (100%)
```
#### 部署清单
- 已上传7个文件到 `123.207.67.197:/www/wwwroot/tools.wktyl.com/`
- 备份目录: `backup_privacy_rights_20260627_021954`
- SQL迁移已执行(存储过程兼容MySQL 5.7)
---
## v10.3.0 (2026-06-27)
### 🛡️ 隐私权管理中心 + Google Play应用外账户管理
**重大更新**新增独立的隐私权管理网页满足Google Play数据安全合规要求支持应用外账号状态查询与注销。
#### 新增功能
| 功能 | 说明 |
|------|------|
| 🛡️ 隐私权管理页面 | 4-Tab界面状态查询/注销账号/隐私权利/其他协议Apple风格设计中英双语 |
| 🔍 公开账号状态查询 | `accountLookup` API无需登录即可查询5种状态正常/封锁/注销中/已注销/无记录) |
| 🔐 HMAC-SHA256防枚举 | 查询接口需回执签名,防止账号枚举攻击 |
| 📋 账号协议V6.6 | 新增长期未登录账号回收政策12/18/24个月三级、服务器数据删除时限、Google Play合规说明 |
| 🧪 全流程验证脚本 | `test_privacy_rights_flow.py`覆盖注册→登录→查询→注销→查询状态→撤销6个接口 |
#### 新增文件
| 文件 | 说明 |
|------|------|
| `public/agreements/privacy-rights.html` | 隐私权管理中心网页1488行含Web Crypto API回执生成 |
| `Scripts/upload_privacy_rights.py` | 服务器文件上传脚本含备份、SSH端口2026 |
| `Scripts/test_privacy_rights_flow.py` | 全流程接口验证脚本6/6通过 |
#### 修改文件
| 文件 | 修改内容 |
|------|----------|
| `application/api/controller/UserSecurity.php` | 新增 `accountLookup()` 方法1152行加入 `$noNeedLogin``$rateLimits` |
| `application/route.php` | 新增 `api/user_security/accountLookup` 路由第73行 |
| `public/agreements/account-agreement.html` | 升级V6.6新增3.4长期未登录账号回收政策、4.5服务器数据删除时限、4.6应用外账户管理支持 |
| `public/agreements/index.html` | 隐私与安全分区新增"隐私权管理"入口(中英文) |
| `Scripts/upload_agreements.py` | 修复SSH端口从22改为2026 |
#### API接口
| 接口 | 方法 | 路径 | 说明 |
|------|------|------|------|
| 账号状态查询 | POST | `/api/user_security/accountLookup` | 公开接口需回执验证限流20次/小时/IP |
#### 状态查询返回值
| status | status_text | 说明 |
|--------|-------------|------|
| `normal` | 正常 | 账号正常使用 |
| `blocked` | 封锁 | 账号被封禁 |
| `deleting` | 注销中 | 注销申请审核中 |
| `deleted` | 已注销 | 账号已注销 |
| `no_record` | 无记录 | 查无此账号 |
#### 测试结果
```
✅ register code=1 注册成功
✅ login code=1 登录成功
✅ accountLookup code=1 状态查询成功
✅ requestDeletion code=1 注销申请提交成功
✅ deletionStatus code=1 注销状态查询成功
✅ cancelDeletion code=1 撤销注销成功
通过率: 6/6 (100%)
```
#### 部署URL
- 隐私权管理页https://tools.wktyl.com/agreements/privacy-rights.html
- 协议列表页https://tools.wktyl.com/agreements/index.html
- 账号协议页https://tools.wktyl.com/agreements/account-agreement.html
---
## v10.2.1 (2026-06-25)
### 🛡️ 限流排队系统优化

View File

@@ -0,0 +1,63 @@
-- ============================================================
-- 迁移脚本: v10.3.1 隐私权管理增强
-- 创建时间: 2026-06-27
-- 描述: 新增用户同意管理表、注销进度字段、用户出生日期字段
-- 兼容: MySQL 5.7+ (不使用 ADD COLUMN IF NOT EXISTS 语法)
-- ============================================================
-- 1. 用户数据处理同意表 (GDPR/PIPL合规)
CREATE TABLE IF NOT EXISTS `tool_user_consents` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NOT NULL DEFAULT '0' COMMENT '用户ID',
`consent_type` varchar(50) NOT NULL DEFAULT '' COMMENT '同意类型: analytics/marketing/personalization/third_party_share',
`consent_value` tinyint(1) NOT NULL DEFAULT '1' COMMENT '0=拒绝 1=同意',
`consent_source` varchar(20) NOT NULL DEFAULT 'web' COMMENT '来源: web/app/system',
`ip` varchar(50) DEFAULT '' COMMENT 'IP地址',
`createtime` int(11) DEFAULT NULL COMMENT '创建时间',
`updatetime` int(11) DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_type` (`user_id`,`consent_type`),
KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户数据处理同意记录';
-- 2. user_deletion表新增注销进度字段 (使用存储过程兼容MySQL 5.7)
DROP PROCEDURE IF EXISTS `migrate_v10_3_add_columns`;
DELIMITER $$
CREATE PROCEDURE `migrate_v10_3_add_columns`()
BEGIN
-- tool_user_deletion: deletion_progress
IF NOT EXISTS (SELECT 1 FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'tool_user_deletion'
AND COLUMN_NAME = 'deletion_progress') THEN
ALTER TABLE `tool_user_deletion` ADD COLUMN `deletion_progress` text COMMENT '注销进度JSON' AFTER `admin_remark`;
END IF;
-- tool_user_deletion: deletion_summary
IF NOT EXISTS (SELECT 1 FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'tool_user_deletion'
AND COLUMN_NAME = 'deletion_summary') THEN
ALTER TABLE `tool_user_deletion` ADD COLUMN `deletion_summary` varchar(500) DEFAULT '' COMMENT '注销数据摘要' AFTER `deletion_progress`;
END IF;
-- tool_user: birthday
IF NOT EXISTS (SELECT 1 FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'tool_user'
AND COLUMN_NAME = 'birthday') THEN
ALTER TABLE `tool_user` ADD COLUMN `birthday` date DEFAULT NULL COMMENT '出生日期' AFTER `email`;
END IF;
-- tool_user: is_minor
IF NOT EXISTS (SELECT 1 FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'tool_user'
AND COLUMN_NAME = 'is_minor') THEN
ALTER TABLE `tool_user` ADD COLUMN `is_minor` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否未成年: 0=成年 1=未成年(需家长控制)' AFTER `birthday`;
END IF;
END$$
DELIMITER ;
CALL `migrate_v10_3_add_columns`();
DROP PROCEDURE IF EXISTS `migrate_v10_3_add_columns`;

View File

@@ -34,6 +34,7 @@ class Userdeletion extends Backend
$filterJson = $this->request->get('filter', '{}');
$query = Db::name('user_deletion');
$listQuery = Db::name('user_deletion');
try {
$filter = json_decode($filterJson, true);
@@ -41,29 +42,18 @@ class Userdeletion extends Backend
foreach ($filter as $key => $val) {
if ($val !== '' && $val !== null) {
$query->where($key, '=', $val);
$listQuery->where($key, '=', $val);
}
}
}
} catch (\Exception $e) {}
$total = $query->count();
$list = Db::name('user_deletion')
$list = $listQuery
->order($sort ?: 'createtime', $order ?: 'desc')
->limit($offset, $limit)
->select();
if (!empty($filter) && is_array($filter)) {
$listQuery = Db::name('user_deletion');
foreach ($filter as $key => $val) {
if ($val !== '' && $val !== null) {
$listQuery->where($key, '=', $val);
}
}
$list = $listQuery->order($sort ?: 'createtime', $order ?: 'desc')
->limit($offset, $limit)
->select();
}
foreach ($list as &$item) {
$statusMap = [0 => '待审核', 1 => '已通过', 2 => '已拒绝', 3 => '已自动注销'];
$item['status_text'] = $statusMap[$item['status']] ?? '未知';
@@ -212,12 +202,17 @@ class Userdeletion extends Backend
Db::name('user')->where('id', $userId)->delete();
// 注销完成后对username做SHA256哈希(PIPL最小化保留原则)
// 保留6个月后由cleanupOldDeletionRecords接口自动清理
$hashedUsername = hash('sha256', $record['username'] ?? '');
Db::name('user_deletion')->where('id', $record['id'])->update([
'status' => $status,
'admin_id' => $adminId,
'admin_remark' => $remark,
'updatetime' => time(),
'deletetime' => time(),
'username' => $hashedUsername,
]);
Db::commit();

View File

@@ -276,6 +276,11 @@ class Feed extends Api
}
$cached = Cache::get($cacheKey);
if ($cached && !$hasSeenIds) {
// v2.6: 缓存命中后重新附加当前用户的 is_liked/is_favorited 状态
// 缓存中的 per-user 字段可能来自其他用户,不可信,必须按当前用户重查
if (isset($cached['list']) && !empty($cached['list'])) {
$this->_batchAttachInteractionCounts($cached['list'], $this->_getUserId());
}
$this->success('成功(cache)', $cached);
return;
}
@@ -421,6 +426,11 @@ class Feed extends Api
unset($item);
}
// 批量附加互动计数(like_count/favorite_count/comment_count/share_count)
// 修复: 原仅 /api/feed/detail 返回这些字段,列表接口缺失导致客户端按钮显示 0
// v2.6: 同时回填当前用户的 is_liked/is_favorited 状态,解决已收藏句子重复出现时状态丢失
$this->_batchAttachInteractionCounts($items, $this->_getUserId());
$result = [
'list' => $items,
'total' => $total,
@@ -432,7 +442,18 @@ class Feed extends Api
];
if (!$hasSeenIds) {
Cache::set($cacheKey, $result, 60);
// v2.6: 缓存前剥离 per-user 字段(is_liked/is_favorited)
// 这些字段是用户私有的,缓存后其他用户命中会串状态
// 缓存命中时由 _batchAttachInteractionCounts 重新按当前用户查询
$cacheResult = $result;
if (isset($cacheResult['list'])) {
foreach ($cacheResult['list'] as &$cacheItem) {
$cacheItem['is_liked'] = false;
$cacheItem['is_favorited'] = false;
}
unset($cacheItem);
}
Cache::set($cacheKey, $cacheResult, 60);
}
$this->success('成功', $result);
@@ -2682,41 +2703,102 @@ class Feed extends Api
/**
* @name 清除Feed相关缓存
* @desc 在互动操作写入后主动清除相关缓存,确保数据一致性
* @lastUpdate v2.5 修复 _lite 后缀缓存未清理 + Cache::rm 不生效的 bug
* 1) 增加 _lite 后缀变体循环
* 2) 在 Cache::rm 后调用 _unlinkCacheFile 兜底,直接删除缓存文件
* 绕过 opcache 缓存旧代码 / ThinkPHP complex 驱动异常
*/
private function _clearFeedCache($feedType, $feedId)
{
try {
Cache::rm('feed_stats');
Cache::rm('feed_channels');
Cache::rm('feed_weight_config');
Cache::rm('feed_weight_config_api');
Cache::rm("feed_count_{$feedType}");
Cache::rm("feed_recommend_guest_20");
// 单键缓存
$singleKeys = [
'feed_stats',
'feed_channels',
'feed_weight_config',
'feed_weight_config_api',
"feed_count_{$feedType}",
'feed_recommend_guest_20',
];
foreach ($singleKeys as $sk) {
Cache::rm($sk);
$this->_unlinkCacheFile($sk);
}
// 列表缓存:覆盖 newest/hottest × 5 页 × 4 种 limit × 普通与 _lite × 平台后缀
$sorts = ['newest', 'hottest'];
$limits = [10, 20, 30, 50];
// 修复 v2.5: 必须覆盖客户端所有可能的 limit 值(含 5否则 limit=5 的缓存不会被清理
$limits = [5, 10, 15, 20, 25, 30, 40, 50];
$liteVariants = ['', '_lite']; // lite 模式变体
for ($p = 1; $p <= 5; $p++) {
foreach ($sorts as $sort) {
foreach ($limits as $lim) {
Cache::rm("feed_list_{$feedType}_{$sort}_{$p}_{$lim}");
Cache::rm("feed_list_all_{$sort}_{$p}_{$lim}");
// 带平台后缀
foreach (self::$platforms as $pKey => $pName) {
Cache::rm("feed_list_{$feedType}_{$sort}_{$p}_{$lim}_plat_{$pKey}");
Cache::rm("feed_list_all_{$sort}_{$p}_{$lim}_plat_{$pKey}");
foreach ($liteVariants as $liteSuffix) {
$k1 = "feed_list_{$feedType}_{$sort}_{$p}_{$lim}{$liteSuffix}";
$k2 = "feed_list_all_{$sort}_{$p}_{$lim}{$liteSuffix}";
Cache::rm($k1);
Cache::rm($k2);
$this->_unlinkCacheFile($k1);
$this->_unlinkCacheFile($k2);
// 带平台后缀
foreach (self::$platforms as $pKey => $pName) {
$pk1 = "feed_list_{$feedType}_{$sort}_{$p}_{$lim}{$liteSuffix}_plat_{$pKey}";
$pk2 = "feed_list_all_{$sort}_{$p}_{$lim}{$liteSuffix}_plat_{$pKey}";
Cache::rm($pk1);
Cache::rm($pk2);
$this->_unlinkCacheFile($pk1);
$this->_unlinkCacheFile($pk2);
}
}
}
}
}
Cache::rm("feed_trending_{$feedType}_20");
Cache::rm("feed_trending_all_20");
// trending 缓存
$trendingKeys = [
"feed_trending_{$feedType}_20",
"feed_trending_all_20",
];
foreach ($trendingKeys as $tk) {
Cache::rm($tk);
$this->_unlinkCacheFile($tk);
}
foreach (self::$platforms as $pKey => $pName) {
Cache::rm("feed_trending_{$feedType}_20_{$pKey}");
Cache::rm("feed_trending_all_20_{$pKey}");
Cache::rm("feed_recommend_guest_20_plat_{$pKey}");
$platKeys = [
"feed_trending_{$feedType}_20_{$pKey}",
"feed_trending_all_20_{$pKey}",
"feed_recommend_guest_20_plat_{$pKey}",
];
foreach ($platKeys as $pk) {
Cache::rm($pk);
$this->_unlinkCacheFile($pk);
}
}
} catch (\Exception $e) {}
}
/**
* @name 兜底删除缓存文件
* @desc Cache::rm 在某些环境opcache 缓存旧代码 / complex 驱动异常)下不删除文件,
* 本方法直接按 ThinkPHP 5 File 驱动的路径规则计算缓存文件路径并 unlink
* @param string $cacheKey 缓存 key与 Cache::rm 入参一致)
* @return bool 文件已删除返回 true文件不存在或删除失败返回 false
*/
private function _unlinkCacheFile($cacheKey)
{
if (!defined('CACHE_PATH') || empty($cacheKey)) {
return false;
}
try {
$hash = md5($cacheKey);
$file = CACHE_PATH . substr($hash, 0, 2) . '/' . substr($hash, 2) . '.php';
if (is_file($file) && @unlink($file)) {
return true;
}
} catch (\Exception $e) {}
return false;
}
/**
* @name 清除权重配置缓存
* @desc 在install/sync操作后清除权重相关缓存
@@ -2730,7 +2812,8 @@ class Feed extends Api
Cache::rm('feed_stats');
Cache::rm('feed_channels');
$sorts = ['newest', 'hottest'];
$limits = [10, 20, 30, 50];
// 修复 v2.5: 必须覆盖客户端所有可能的 limit 值(含 5否则 limit=5 的缓存不会被清理
$limits = [5, 10, 15, 20, 25, 30, 40, 50];
for ($p = 1; $p <= 5; $p++) {
foreach ($sorts as $sort) {
foreach ($limits as $lim) {
@@ -2946,9 +3029,136 @@ class Feed extends Api
}
}
// 批量附加互动计数(like_count/favorite_count/comment_count/share_count)
// 修复: favorites/likes/history/readlater 列表也需返回真实计数
// v2.6: 同时回填当前用户的 is_liked/is_favorited 状态
$this->_batchAttachInteractionCounts($items, $this->_getUserId());
return $items;
}
/**
* @name 批量附加互动计数
* @desc 一次性查询 feed_interaction 表,按 (feed_type, feed_id, action) 分组 COUNT
* 回填到 items 的 like_count/favorite_count/comment_count/share_count 字段。
* 避免 N+1 查询,列表接口(list/favorites/likes/history/readlater)统一调用。
* @param array &$items 待附加计数的 items 数组(引用传递)
* @lastUpdate v2.4 新增; 修复客户端点赞按钮显示 0 的问题
*/
private function _batchAttachInteractionCounts(&$items, $userId = 0)
{
if (empty($items) || !is_array($items)) {
return;
}
// 收集所有 (feed_type, feed_id) 对
$pairs = [];
foreach ($items as $item) {
$feedType = $item['feed_type'] ?? '';
$feedId = intval($item['id'] ?? 0);
if (empty($feedType) || $feedId <= 0) {
continue;
}
$pairs[$feedType][] = $feedId;
}
if (empty($pairs)) {
// 无有效对,仍填充 0 防止客户端解析 null
foreach ($items as &$item) {
if (!isset($item['like_count'])) $item['like_count'] = 0;
if (!isset($item['favorite_count'])) $item['favorite_count'] = 0;
if (!isset($item['comment_count'])) $item['comment_count'] = 0;
if (!isset($item['share_count'])) $item['share_count'] = 0;
// v2.6: 回填当前用户互动状态(未登录默认 false
if (!isset($item['is_liked'])) $item['is_liked'] = false;
if (!isset($item['is_favorited'])) $item['is_favorited'] = false;
}
unset($item);
return;
}
// 构造 OR 查询条件: ((feed_type='poetry' AND feed_id IN (...)) OR (feed_type='wisdom' AND feed_id IN (...)) ...)
// 仅查询 like/favorite/comment/share 四种 action
$countMap = []; // key: "feed_type_feed_id" => ["like"=>n, "favorite"=>n, ...]
try {
foreach ($pairs as $feedType => $ids) {
$ids = array_unique($ids);
$rows = Db::name('feed_interaction')
->field('feed_id, action, COUNT(*) AS cnt')
->where('feed_type', $feedType)
->where('feed_id', 'in', $ids)
->where('action', 'in', ['like', 'favorite', 'comment', 'share'])
->group('feed_id, action')
->select();
foreach ($rows as $row) {
$fid = intval($row['feed_id']);
$action = $row['action'];
$cnt = intval($row['cnt']);
$key = $feedType . '_' . $fid;
if (!isset($countMap[$key])) {
$countMap[$key] = [
'like' => 0,
'favorite' => 0,
'comment' => 0,
'share' => 0,
];
}
$countMap[$key][$action] = $cnt;
}
}
} catch (\Exception $e) {
// 查询失败时降级为 0
}
// v2.6 新增:批量查询当前用户的互动状态,回填 is_liked/is_favorited
// 解决list 接口不返回用户个人状态,导致已收藏句子重复出现时显示"未收藏"
$userActionMap = []; // key: "feed_type_feed_id" => ["like"=>bool, "favorite"=>bool]
if (!empty($userId)) {
try {
foreach ($pairs as $feedType => $ids) {
$ids = array_unique($ids);
$userRows = Db::name('feed_interaction')
->field('feed_id, action')
->where('user_id', $userId)
->where('feed_type', $feedType)
->where('feed_id', 'in', $ids)
->where('action', 'in', ['like', 'favorite'])
->select();
foreach ($userRows as $row) {
$fid = intval($row['feed_id']);
$action = $row['action'];
$key = $feedType . '_' . $fid;
if (!isset($userActionMap[$key])) {
$userActionMap[$key] = [
'like' => false,
'favorite' => false,
];
}
$userActionMap[$key][$action] = true;
}
}
} catch (\Exception $e) {
// 查询失败时降级为 false
}
}
// 回填到 items
foreach ($items as &$item) {
$feedType = $item['feed_type'] ?? '';
$feedId = intval($item['id'] ?? 0);
$key = $feedType . '_' . $feedId;
$counts = $countMap[$key] ?? null;
$item['like_count'] = $counts ? $counts['like'] : 0;
$item['favorite_count'] = $counts ? $counts['favorite'] : 0;
$item['comment_count'] = $counts ? $counts['comment'] : 0;
$item['share_count'] = $counts ? $counts['share'] : 0;
// v2.6: 回填当前用户互动状态
$userActions = $userActionMap[$key] ?? null;
$item['is_liked'] = $userActions ? $userActions['like'] : false;
$item['is_favorited'] = $userActions ? $userActions['favorite'] : false;
}
unset($item);
}
/**
* @name 获取权重配置
* @desc 从数据库读取管理员设置的推荐权重配置,带缓存

File diff suppressed because it is too large Load Diff

View File

@@ -70,6 +70,21 @@ Route::rule([
'api/user_security/requestDeletion' => 'api/UserSecurity/requestDeletion',
'api/user_security/deletionStatus' => 'api/UserSecurity/deletionStatus',
'api/user_security/cancelDeletion' => 'api/UserSecurity/cancelDeletion',
'api/user_security/accountLookup' => 'api/UserSecurity/accountLookup',
'api/user_security/cleanupOldDeletionRecords' => 'api/UserSecurity/cleanupOldDeletionRecords',
'api/user_security/getConsents' => 'api/UserSecurity/getConsents',
'api/user_security/updateConsents' => 'api/UserSecurity/updateConsents',
'api/user_security/listDevices' => 'api/UserSecurity/listDevices',
'api/user_security/revokeDevice' => 'api/UserSecurity/revokeDevice',
'api/user_security/deletionProgress'=> 'api/UserSecurity/deletionProgress',
'api/user_security/parentalControlStatus' => 'api/UserSecurity/parentalControlStatus',
'api/user_security/updateBirthday' => 'api/UserSecurity/updateBirthday',
// v10.3.2 新增
'api/user_security/checkSmtpStatus' => 'api/UserSecurity/checkSmtpStatus',
'api/user_security/secQuestionLogin' => 'api/UserSecurity/secQuestionLogin',
'api/user_security/emailCodeLogin' => 'api/UserSecurity/emailCodeLogin',
'api/user_security/sendDeletionCode' => 'api/UserSecurity/sendDeletionCode',
'api/user_security/requestDeletionByReceipt'=> 'api/UserSecurity/requestDeletionByReceipt',
]);
// OAuth社交登录

View File

@@ -3,7 +3,7 @@
> @File: API_FEED_DOC.md
> @Time: 2026-05-13
> @Description: 信息流Feed系统API文档聚合44种内容表为统一信息流
> @LastUpdate: v2.3 新增刷新获取新内容接口(/api/feed/refresh_content); 新增用户偏好设置接口(/api/feed/preferences)
> @LastUpdate: v2.6 list 接口返回 is_liked/is_favorited 当前用户互动状态;缓存命中后按当前用户重查 per-user 字段,缓存写入前剥离 per-user 字段避免跨用户串状态
---
@@ -158,6 +158,12 @@ curl -s "https://tools.wktyl.com/api/feed/channels" | python -m json.tool
| sort | string | 当前排序 |
| lite | bool | 是否轻量模式 |
> **v2.4 更新**: list 接口的每个 item 现在都包含 `like_count`、`favorite_count`、`comment_count`、`share_count` 字段(批量查询 feed_interaction 表回填),无需再单独调用 detail 接口获取计数。
> **v2.5 更新**: 修复 lite 模式(limit=5)点赞/取消点赞后 like_count 不更新的问题。根因是 `_clearFeedCache` 的 `$limits` 数组不含 5导致 `feed_list_*_newest_1_5_lite` 缓存从未被清理。修复后 `$limits = [5, 10, 15, 20, 25, 30, 40, 50]`,并在 `Cache::rm` 后追加 `_unlinkCacheFile` 兜底直接删除缓存文件。互动操作后 lite 模式 list 接口立即返回最新计数。
> **v2.6 更新**: list 接口的每个 item 现在包含 `is_liked`、`is_favorited` 字段,表示**当前登录用户**是否已点赞/已收藏。未登录时始终返回 `false`。`_batchAttachInteractionCounts` 新增 `$userId` 参数,批量查询 `feed_interaction` 表中当前用户的 like/favorite 记录。**缓存隔离**:缓存命中后按当前用户重新查询 per-user 字段(缓存中的 is_liked/is_favorited 不可信),缓存写入前剥离为 false 避免跨用户串状态。客户端额外合并本地 DB 状态OR 策略:服务端 true 或本地 true 均为 true覆盖未登录场景和离线操作。
### 3.5 curl测试
```bash

View File

@@ -261,9 +261,9 @@
<div class="content-card">
<div id="content-zh" class="lang-content">
<p><strong>闲言APP</strong> 账号使用协议</p>
<p>版本号V6.5</p>
<p>更新日期2026年5月20</p>
<p>生效日期2026年5月21</p>
<p>版本号V6.6</p>
<p>更新日期2026年6月17</p>
<p>生效日期2026年6月17</p>
<p>本协议是您与<strong>弥勒市朋普镇微风暴网络科技工作室</strong>关于<strong>闲言APP</strong>账号注册、使用、安全及注销等事项的约定。请您仔细阅读并充分理解本协议。</p>
<h2>一、账号注册</h2>
<h3>1.1 注册资格</h3>
@@ -317,6 +317,20 @@
<li>涉嫌违法违规活动</li>
<li>长期未登录(超过<span class="highlight">2年</span></li>
</ul>
<h3>3.4 长期未登录账号回收政策</h3>
<p>为合理利用服务器资源并保护账号安全,我们制定了以下长期未登录账号回收规则:</p>
<ul>
<li><span class="highlight">连续12个月未登录</span>:账号将进入"休眠状态"我们将通过App内消息、邮件或短信方式向您发送提醒通知</li>
<li><span class="highlight">连续18个月未登录</span>:账号下的虚拟财产(积分、金币等)将停止产生,但不被清零;账号仍可正常登录恢复使用</li>
<li><span class="highlight">连续24个月2年未登录</span>:我们将对账号进行<span class="highlight">回收处理</span>,具体处理方式如下:</li>
<li>— 账号将被永久冻结,无法登录</li>
<li>— 个人资料、收藏、笔记、签到记录等数据将被<span class="highlight">永久删除且不可恢复</span></li>
<li>— 账号关联的邮箱、手机号将被解绑,可重新注册新账号</li>
<li>— 在回收处理前30天我们将通过注册邮箱/手机号发送最终通知</li>
<li>回收处理后,您将无法找回该账号及其数据</li>
<li>为避免账号被回收,请至少每<span class="highlight">12个月</span>登录一次</li>
</ul>
<p class="note">💡 提示:账号回收处理与主动注销的后果一致,均会导致数据永久删除。建议您定期登录以保留账号和数据。</p>
<h2>四、账号注销</h2>
<h3>4.1 注销条件</h3>
<p>您有权随时申请注销账号,注销前请确保:</p>
@@ -351,6 +365,30 @@
<li>会员权益、积分、金币等虚拟财产将清零且<span class="highlight">不可恢复</span></li>
<li>注销后<span class="highlight">无法恢复</span>,请谨慎操作</li>
</ul>
<h3>4.5 服务器数据删除时限</h3>
<p>关于注销后服务器数据的删除时间,我们承诺以下规则:</p>
<ul>
<li><span class="highlight">管理员审核通过后立即删除</span>:管理员通过注销申请后,服务器将在<span class="highlight">5分钟内</span>完成以下数据的硬删除:</li>
<li>— 用户主表tool_user记录</li>
<li>— 用户Token、登录设备、在线状态等会话数据</li>
<li>— 用户收藏、笔记、点赞等互动数据</li>
<li>— 用户签到记录、积分流水、金币流水</li>
<li>— 用户文章、评论及关联内容(作者字段匿名化)</li>
<li><span class="highlight">审核期超时自动注销</span>:提交申请后<span class="highlight">3天</span>内若管理员未审核,系统将自动执行注销,删除范围同上</li>
<li><span class="highlight">注销记录保留</span>:为满足法律法规和审计要求,注销操作记录将保留<span class="highlight">6个月</span>且用户名在注销完成后立即进行SHA256哈希处理仅保留不可逆的哈希值用于审计</li>
<li>数据库备份遵循<span class="highlight">30天滚动覆盖</span>策略即最新备份仅保留30天超期自动清除</li>
</ul>
<p class="note">💡 重要说明注销完成后您将无法通过任何方式App内、网页、客服查询查找到该账号的任何信息状态查询将返回"已注销"或"无记录"。</p>
<h3>4.6 应用外账户管理支持Google Play合规</h3>
<p>为满足Google Play数据安全合规要求我们提供独立的<span class="highlight">应用外账户管理网页</span>无需安装App即可进行账号管理操作</p>
<ul>
<li><span class="highlight">账号状态查询</span>:访问 <a href="https://tools.wktyl.com/agreements/privacy-rights.html" style="color:var(--primary);">隐私权管理页面</a>,输入账号即可查询状态(正常/封锁/注销中/已注销/无记录)</li>
<li><span class="highlight">应用外注销</span>在隐私权管理页面登录后即可发起注销申请流程与App内一致</li>
<li><span class="highlight">注销状态查询</span>:随时查询注销申请的处理进度</li>
<li><span class="highlight">撤销注销</span>:审核期内可随时撤销注销申请</li>
<li>该页面同时提供其他软件协议的查看入口,方便您了解权利与义务</li>
</ul>
<p class="note">🔐 安全提示应用外账户管理页面采用HMAC-SHA256回执验证机制防止账号枚举攻击。查询账号状态需提供有效的回执签名。</p>
<h2>五、账号冻结与解冻</h2>
<h3>5.1 冻结情形</h3>
<p>以下情形我们有权冻结您的账号:</p>
@@ -387,38 +425,109 @@
</div>
<div id="content-en" class="lang-content" style="display:none;">
<p><strong>Xianyan APP</strong> Account Usage Agreement</p>
<p>Version: V6.5</p>
<p>Updated: May 20, 2026</p>
<p>Effective: May 21, 2026</p>
<p>Version: V6.6</p>
<p>Updated: June 17, 2026</p>
<p>Effective: June 17, 2026</p>
<p>This agreement is between you and <strong>Mile City Pengpu Town Weifengbao Network Technology Studio</strong> regarding the registration, use, security, and deletion of your Xianyan APP account. Please read and fully understand this agreement.</p>
<h2>I. Account Registration</h2>
<h3>1.1 You must be at least 14 years old to register an account</h3>
<h3>1.2 You need to provide a valid phone number or email for registration</h3>
<h3>1.3 The information you provide must be true, accurate, and complete</h3>
<h3>1.4 Each person may only register one account</h3>
<h3>1.1 Registration Eligibility</h3>
<ul>
<li>You must meet the minimum age requirement of your region (14 in China, 16 in EU, 13 in US)</li>
<li>Minors under 14 are not allowed to register</li>
<li>Each user may only register one account; accounts cannot be bought, transferred, or lent</li>
</ul>
<h3>1.2 Registration Methods</h3>
<ul>
<li>Phone number registration via SMS verification</li>
<li>Email registration via verification code</li>
<li>Third-party login via authorized accounts</li>
</ul>
<h2>II. Account Security</h2>
<h3>2.1 You are responsible for keeping your account password secure</h3>
<h3>2.2 Do not use easily guessable passwords</h3>
<h3>2.3 If you discover your account has been used without authorization, please contact us immediately</h3>
<h3>2.4 We are not responsible for losses caused by your failure to keep your account secure</h3>
<h3>2.1 Password Security</h3>
<ul>
<li>Set a strong password and change it regularly</li>
<li>Never share your password with others</li>
<li>Notify us immediately if you suspect unauthorized access</li>
</ul>
<h2>III. Account Usage</h2>
<h3>3.1 You may not lend, transfer, or sell your account</h3>
<h3>3.2 You may not use your account for illegal activities</h3>
<h3>3.3 You may not use technical means to obtain other users' account information</h3>
<h3>3.1 Account Restrictions</h3>
<p>We may restrict your account in the following cases:</p>
<ul>
<li>Security risk detected</li>
<li>Violation of User Service Agreement</li>
<li>Suspected illegal activities</li>
<li>Inactive for over 2 years</li>
</ul>
<h3>3.2 Long-term Inactive Account Recycling Policy</h3>
<p>To optimize server resources and protect account security, we apply the following recycling rules:</p>
<ul>
<li><span class="highlight">12 months inactive</span>: Account enters "dormant state"; we will notify you via app, email, or SMS</li>
<li><span class="highlight">18 months inactive</span>: Virtual assets stop accumulating but are not cleared; account can still be reactivated</li>
<li><span class="highlight">24 months (2 years) inactive</span>: Account will be <span class="highlight">recycled</span>:</li>
<li>— Account permanently frozen, login disabled</li>
<li>— Personal data, favorites, notes, check-ins <span class="highlight">permanently deleted</span></li>
<li>— Linked email/phone unbound, available for new registrations</li>
<li>— Final notification sent 30 days before recycling</li>
<li>To avoid recycling, please log in at least once every <span class="highlight">12 months</span></li>
</ul>
<h2>IV. Account Deletion</h2>
<h3>4.1 You can apply for account deletion at: My → Settings → Account Settings → Delete Account</h3>
<h3>4.2 Deletion process:</h3>
<h3>4.1 Deletion Conditions</h3>
<p>You may apply for account deletion at any time. Before deletion, please ensure:</p>
<ul>
<li>Submit a deletion request and undergo security verification</li>
<li>3-day review period (countdown displayed)</li>
<li>During the review period, you can cancel the deletion request</li>
<li>After the review is complete, the account and related data will be permanently deleted</li>
<li>No pending disputes or complaints</li>
<li>No incomplete transactions or services</li>
<li>You have backed up data you wish to keep</li>
</ul>
<h3>4.3 After account deletion:</h3>
<h3>4.2 Deletion Process</h3>
<ul>
<li>All personal information will be deleted or anonymized</li>
<li>Published content will be handled according to your choice</li>
<li>Account cannot be recovered after deletion</li>
<li>Apply via "Settings → Account Settings → Delete Account"</li>
<li>Step 1: Read deletion warning and confirm understanding</li>
<li>Step 2: Security verification - type "DELETE" to confirm; reason is optional</li>
<li>Step 3: Enter a <span class="highlight">3-day review period</span></li>
<li>Step 4: After admin approval or review timeout, account is <span class="highlight">permanently deleted</span></li>
<li>You may cancel the deletion request anytime during the review period</li>
</ul>
<h3>4.3 Deletion Status Query</h3>
<ul>
<li>View current status (Pending/Approved/Rejected/Cancelled) on the deletion page</li>
<li>Application time, reason, and estimated auto-deletion time are displayed</li>
<li>Countdown timer is shown during the review period</li>
</ul>
<h3>4.4 Deletion Consequences</h3>
<ul>
<li>Account cannot be logged in or used after deletion</li>
<li>The following data will be <span class="highlight">permanently deleted and unrecoverable</span>:</li>
<li>All favorites and notes</li>
<li>Check-in records and points</li>
<li>Personal profile and settings</li>
<li>Articles and interaction data</li>
<li>Membership benefits, points, and coins cleared</li>
<li>Deletion is <span class="highlight">irreversible</span> - proceed with caution</li>
</ul>
<h3>4.5 Server Data Deletion Timeline</h3>
<p>We commit to the following data deletion rules:</p>
<ul>
<li><span class="highlight">Immediate deletion upon admin approval</span>: Within <span class="highlight">5 minutes</span> of approval, the following data is hard-deleted:</li>
<li>— User main table (tool_user) record</li>
<li>— User tokens, login devices, online status</li>
<li>— User favorites, notes, likes</li>
<li>— Check-in records, points/coins transactions</li>
<li>— User articles, comments (author field anonymized)</li>
<li><span class="highlight">Auto-deletion on timeout</span>: If admin does not review within <span class="highlight">3 days</span>, the system auto-executes deletion</li>
<li><span class="highlight">Deletion record retention</span>: Deletion operation records are retained for <span class="highlight">6 months</span>, with username immediately SHA256-hashed upon deletion completion. Only irreversible hash values are retained for audit purposes</li>
<li>Database backups follow a <span class="highlight">30-day rolling overwrite</span> policy</li>
</ul>
<p class="note">💡 Important: After deletion is complete, the account cannot be queried via any channel (app, web, customer service). Status query will return "Deleted" or "No Record".</p>
<h3>4.6 Out-of-App Account Management (Google Play Compliance)</h3>
<p>To comply with Google Play Data Safety requirements, we provide an independent <span class="highlight">out-of-app account management webpage</span> for managing accounts without installing the app:</p>
<ul>
<li><span class="highlight">Account Status Query</span>: Visit <a href="https://tools.wktyl.com/agreements/privacy-rights.html" style="color:var(--primary);">Privacy Rights Management Page</a> to query status (Normal/Blocked/Deleting/Deleted/No Record)</li>
<li><span class="highlight">Out-of-app deletion</span>: Log in and submit deletion requests via the privacy rights page</li>
<li><span class="highlight">Deletion status query</span>: Track deletion progress anytime</li>
<li><span class="highlight">Cancel deletion</span>: Cancel deletion requests during the review period</li>
<li>The page also provides access to other software agreements</li>
</ul>
<p class="note">🔐 Security: The out-of-app management page uses HMAC-SHA256 receipt verification to prevent account enumeration attacks.</p>
<h2>V. Account Freeze</h2>
<h3>5.1 We have the right to freeze accounts in the following circumstances:</h3>
<ul>

View File

@@ -194,6 +194,14 @@
</div>
<span class="item-arrow"></span>
</a>
<a href="privacy-rights.html" class="agreement-item">
<span class="item-icon">🛡️</span>
<div class="item-text">
<div class="item-title">隐私权管理</div>
<div class="item-subtitle">账号状态查询、注销申请、隐私权利选择Google Play合规</div>
</div>
<span class="item-arrow"></span>
</a>
</div>
<div class="section-title">📋 服务条款</div>
<div class="agreement-list">
@@ -293,6 +301,14 @@
</div>
<span class="item-arrow"></span>
</a>
<a href="privacy-rights.html?lang=en" class="agreement-item">
<span class="item-icon">🛡️</span>
<div class="item-text">
<div class="item-title">Privacy Rights Management</div>
<div class="item-subtitle">Account status query, deletion request, privacy choices (Google Play compliance)</div>
</div>
<span class="item-arrow"></span>
</a>
</div>
<div class="section-title">📋 Service Terms</div>
<div class="agreement-list">

File diff suppressed because it is too large Load Diff