chore: 移除NFC/蓝牙相关支持,更新设备在线统计,新增功能优化
1. 移除NFC和蓝牙相关依赖、权限及功能代码,精简传输链路 2. 重构设备在线统计逻辑,使用后端7天活跃字段替代本地计算 3. 更新应用名称、权限说明和协议文档 4. 新增消息转发、缓存管理、医疗免责提示功能 5. 优化运势模块和字体管理文案,修复构建日志问题
This commit is contained in:
709
docs/toolsapi/application/api/controller/Oauth.php
Normal file
709
docs/toolsapi/application/api/controller/Oauth.php
Normal file
@@ -0,0 +1,709 @@
|
||||
<?php
|
||||
|
||||
namespace app\api\controller;
|
||||
|
||||
use app\common\controller\Api;
|
||||
use think\Db;
|
||||
use think\Exception;
|
||||
|
||||
/**
|
||||
* OAuth2.0 社交登录
|
||||
*
|
||||
* 支持平台: apple, google, github
|
||||
* 流程: 客户端获取授权码 → 服务端换取access_token → 获取用户信息 → 创建/关联账号
|
||||
*
|
||||
* 创建时间: 2026-06-05
|
||||
* 更新时间: 2026-06-05
|
||||
* 名称: Oauth
|
||||
* 作用: OAuth社交登录接口,支持Apple/Google/GitHub三方登录、绑定、解绑
|
||||
* 上次更新: 新增OAuth社交登录接口
|
||||
*/
|
||||
class Oauth extends Api
|
||||
{
|
||||
protected $noNeedLogin = ['*'];
|
||||
protected $noNeedRight = ['*'];
|
||||
|
||||
/** OAuth平台远程地址配置 */
|
||||
private $oauthConfig = [
|
||||
'apple' => [
|
||||
'verify_url' => 'https://appleid.apple.com/auth/token',
|
||||
'revoke_url' => 'https://appleid.apple.com/auth/revoke',
|
||||
],
|
||||
'google' => [
|
||||
'token_url' => 'https://oauth2.googleapis.com/token',
|
||||
'userinfo_url' => 'https://www.googleapis.com/oauth2/v3/userinfo',
|
||||
],
|
||||
'github' => [
|
||||
'token_url' => 'https://github.com/login/oauth/access_token',
|
||||
'userinfo_url' => 'https://api.github.com/user',
|
||||
],
|
||||
];
|
||||
|
||||
/** 频率限制配置 */
|
||||
private static $rateLimits = [
|
||||
'login' => ['max' => 30, 'window' => 300],
|
||||
'bind' => ['max' => 20, 'window' => 3600],
|
||||
'unbind' => ['max' => 20, 'window' => 3600],
|
||||
];
|
||||
|
||||
/** 支持的平台列表 */
|
||||
private static $supportedPlatforms = ['apple', 'google', 'github'];
|
||||
|
||||
// ==================== 公开接口 ====================
|
||||
|
||||
/**
|
||||
* 获取OAuth配置
|
||||
* GET /api/oauth/config?platform=apple
|
||||
*/
|
||||
public function config()
|
||||
{
|
||||
$platform = $this->request->param('platform', '', 'trim');
|
||||
if (!in_array($platform, self::$supportedPlatforms)) {
|
||||
$this->error('不支持的平台');
|
||||
}
|
||||
|
||||
$config = $this->getOAuthConfig($platform);
|
||||
if (!$config) {
|
||||
$this->error('平台未配置', null, ['platform' => $platform, 'configured' => false]);
|
||||
}
|
||||
|
||||
$this->success('', [
|
||||
'platform' => $platform,
|
||||
'configured' => true,
|
||||
'client_id' => $config['client_id'] ?? '',
|
||||
'redirect_uri' => $config['redirect_uri'] ?? '',
|
||||
'authorize_url' => $this->getAuthorizeUrl($platform, $config),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 社交登录
|
||||
* POST /api/oauth/login
|
||||
*
|
||||
* 参数:
|
||||
* - platform: apple/google/github
|
||||
* - code: 授权码(google/github使用)
|
||||
* - id_token: Apple ID Token(apple使用)
|
||||
* - device_name: 设备名称(可选)
|
||||
* - device_model: 设备型号(可选)
|
||||
* - platform_type: 登录平台ios/android/web(可选)
|
||||
* - device_id: 设备唯一标识(可选)
|
||||
*/
|
||||
public function login()
|
||||
{
|
||||
$platform = $this->request->post('platform', '', 'trim');
|
||||
$code = $this->request->post('code', '', 'trim');
|
||||
$idToken = $this->request->post('id_token', '', 'trim');
|
||||
|
||||
if (!in_array($platform, self::$supportedPlatforms)) {
|
||||
$this->error('不支持的平台');
|
||||
}
|
||||
|
||||
// 频率限制
|
||||
$this->checkRateLimit('login');
|
||||
|
||||
// 获取第三方用户信息
|
||||
try {
|
||||
$oauthUser = $this->getOAuthUserInfo($platform, $code, $idToken);
|
||||
} catch (Exception $e) {
|
||||
$this->error('OAuth验证失败: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
if (!$oauthUser || empty($oauthUser['openid'])) {
|
||||
$this->error('获取用户信息失败');
|
||||
}
|
||||
|
||||
// 查找或创建用户
|
||||
$user = $this->findOrCreateUser($platform, $oauthUser);
|
||||
if (!$user) {
|
||||
$this->error('登录失败');
|
||||
}
|
||||
|
||||
// 生成Token
|
||||
$token = $this->generateToken($user);
|
||||
|
||||
// 记录设备信息
|
||||
$this->recordDeviceInfo($user);
|
||||
|
||||
$this->success('登录成功', [
|
||||
'userinfo' => [
|
||||
'id' => $user['id'],
|
||||
'username' => $user['username'],
|
||||
'nickname' => $user['nickname'],
|
||||
'email' => $user['email'],
|
||||
'avatar' => $user['avatar'],
|
||||
],
|
||||
'token' => $token,
|
||||
'is_new_user' => $oauthUser['is_new'] ?? false,
|
||||
'bind_platform' => $platform,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定社交账号(已登录用户)
|
||||
* POST /api/oauth/bind
|
||||
*/
|
||||
public function bind()
|
||||
{
|
||||
$this->checkLogin();
|
||||
$platform = $this->request->post('platform', '', 'trim');
|
||||
$code = $this->request->post('code', '', 'trim');
|
||||
$idToken = $this->request->post('id_token', '', 'trim');
|
||||
|
||||
if (!in_array($platform, self::$supportedPlatforms)) {
|
||||
$this->error('不支持的平台');
|
||||
}
|
||||
|
||||
$this->checkRateLimit('bind');
|
||||
|
||||
try {
|
||||
$oauthUser = $this->getOAuthUserInfo($platform, $code, $idToken);
|
||||
} catch (Exception $e) {
|
||||
$this->error('OAuth验证失败: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
// 检查是否已被其他用户绑定
|
||||
$existing = Db::name('user_oauth')
|
||||
->where('platform', $platform)
|
||||
->where('openid', $oauthUser['openid'])
|
||||
->find();
|
||||
|
||||
if ($existing) {
|
||||
if ($existing['user_id'] == $this->auth->id) {
|
||||
$this->error('已绑定该平台');
|
||||
}
|
||||
$this->error('该账号已被其他用户绑定');
|
||||
}
|
||||
|
||||
// 绑定
|
||||
Db::name('user_oauth')->insert([
|
||||
'user_id' => $this->auth->id,
|
||||
'platform' => $platform,
|
||||
'openid' => $oauthUser['openid'],
|
||||
'unionid' => $oauthUser['unionid'] ?? '',
|
||||
'nickname' => $oauthUser['nickname'] ?? '',
|
||||
'avatar' => $oauthUser['avatar'] ?? '',
|
||||
'access_token' => $oauthUser['access_token'] ?? '',
|
||||
'refresh_token' => $oauthUser['refresh_token'] ?? '',
|
||||
'expires_at' => $oauthUser['expires_at'] ?? 0,
|
||||
'createtime' => time(),
|
||||
'updatetime' => time(),
|
||||
]);
|
||||
|
||||
$this->success('绑定成功', [
|
||||
'platform' => $platform,
|
||||
'nickname' => $oauthUser['nickname'] ?? '',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解绑社交账号
|
||||
* POST /api/oauth/unbind
|
||||
*/
|
||||
public function unbind()
|
||||
{
|
||||
$this->checkLogin();
|
||||
$platform = $this->request->post('platform', '', 'trim');
|
||||
|
||||
if (!in_array($platform, self::$supportedPlatforms)) {
|
||||
$this->error('不支持的平台');
|
||||
}
|
||||
|
||||
$this->checkRateLimit('unbind');
|
||||
|
||||
$count = Db::name('user_oauth')
|
||||
->where('user_id', $this->auth->id)
|
||||
->where('platform', $platform)
|
||||
->delete();
|
||||
|
||||
if ($count) {
|
||||
$this->success('解绑成功');
|
||||
}
|
||||
$this->error('未绑定该平台');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取已绑定的社交账号列表
|
||||
* GET /api/oauth/bound
|
||||
*/
|
||||
public function bound()
|
||||
{
|
||||
$this->checkLogin();
|
||||
$list = Db::name('user_oauth')
|
||||
->where('user_id', $this->auth->id)
|
||||
->field('platform,openid,nickname,avatar,createtime')
|
||||
->select();
|
||||
|
||||
$this->success('', ['bindings' => $list]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 安装OAuth数据表
|
||||
* GET /api/oauth/install
|
||||
*/
|
||||
public function install()
|
||||
{
|
||||
$sql = "CREATE TABLE IF NOT EXISTS `tool_user_oauth` (
|
||||
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`user_id` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '用户ID',
|
||||
`platform` varchar(30) NOT NULL DEFAULT '' COMMENT '平台(apple/google/github)',
|
||||
`openid` varchar(128) NOT NULL DEFAULT '' COMMENT '平台用户ID',
|
||||
`unionid` varchar(128) NOT NULL DEFAULT '' COMMENT '联合ID',
|
||||
`nickname` varchar(100) NOT NULL DEFAULT '' COMMENT '昵称',
|
||||
`avatar` varchar(500) NOT NULL DEFAULT '' COMMENT '头像',
|
||||
`access_token` text COMMENT '访问令牌',
|
||||
`refresh_token` text COMMENT '刷新令牌',
|
||||
`expires_at` int(11) unsigned NOT NULL DEFAULT 0 COMMENT '令牌过期时间',
|
||||
`createtime` int(11) unsigned NOT NULL DEFAULT 0,
|
||||
`updatetime` int(11) unsigned NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_platform_openid` (`platform`, `openid`),
|
||||
KEY `idx_user_id` (`user_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户OAuth绑定表';";
|
||||
|
||||
try {
|
||||
Db::execute($sql);
|
||||
$this->success('安装成功', [
|
||||
'table' => 'tool_user_oauth',
|
||||
'created' => true,
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
$this->error('安装失败: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 私有方法 ====================
|
||||
|
||||
/**
|
||||
* 获取OAuth配置
|
||||
* 优先从数据库读取,回退到配置文件
|
||||
*/
|
||||
private function getOAuthConfig($platform)
|
||||
{
|
||||
// 从数据库 tool_oauth_config 读取
|
||||
try {
|
||||
$config = Db::name('oauth_config')
|
||||
->where('platform', $platform)
|
||||
->where('status', 1)
|
||||
->find();
|
||||
|
||||
if ($config) {
|
||||
return json_decode($config['config'], true);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
// 表不存在时忽略,回退到配置文件
|
||||
}
|
||||
|
||||
// 回退到配置文件
|
||||
$fileConfig = get_addon_config('third');
|
||||
return $fileConfig[$platform] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取授权URL
|
||||
*/
|
||||
private function getAuthorizeUrl($platform, $config)
|
||||
{
|
||||
$clientId = $config['client_id'] ?? '';
|
||||
$redirectUri = $config['redirect_uri'] ?? '';
|
||||
$state = md5(uniqid());
|
||||
|
||||
switch ($platform) {
|
||||
case 'apple':
|
||||
return "https://appleid.apple.com/auth/authorize?client_id={$clientId}&redirect_uri=" . urlencode($redirectUri) . "&response_type=code&scope=name%20email&state={$state}";
|
||||
case 'google':
|
||||
return "https://accounts.google.com/o/oauth2/v2/auth?client_id={$clientId}&redirect_uri=" . urlencode($redirectUri) . "&response_type=code&scope=openid%20email%20profile&state={$state}";
|
||||
case 'github':
|
||||
return "https://github.com/login/oauth/authorize?client_id={$clientId}&redirect_uri=" . urlencode($redirectUri) . "&scope=user:email&state={$state}";
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取OAuth用户信息(分发到各平台验证方法)
|
||||
*/
|
||||
private function getOAuthUserInfo($platform, $code, $idToken)
|
||||
{
|
||||
switch ($platform) {
|
||||
case 'apple':
|
||||
return $this->verifyApple($idToken, $code);
|
||||
case 'google':
|
||||
return $this->verifyGoogle($code);
|
||||
case 'github':
|
||||
return $this->verifyGithub($code);
|
||||
default:
|
||||
throw new Exception('不支持的平台');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证Apple ID Token
|
||||
* Apple返回的id_token是JWT格式,解析payload获取用户信息
|
||||
*/
|
||||
private function verifyApple($idToken, $code)
|
||||
{
|
||||
if (empty($idToken)) {
|
||||
throw new Exception('id_token不能为空');
|
||||
}
|
||||
|
||||
// 解码JWT(Apple ID Token是JWT格式)
|
||||
$parts = explode('.', $idToken);
|
||||
if (count($parts) !== 3) {
|
||||
throw new Exception('无效的id_token格式');
|
||||
}
|
||||
|
||||
$payload = json_decode(base64_decode(strtr($parts[1], '-_', '+/')), true);
|
||||
if (!$payload) {
|
||||
throw new Exception('无法解析id_token');
|
||||
}
|
||||
|
||||
// 验证issuer
|
||||
if (($payload['iss'] ?? '') !== 'https://appleid.apple.com') {
|
||||
throw new Exception('无效的issuer');
|
||||
}
|
||||
|
||||
return [
|
||||
'openid' => $payload['sub'] ?? '',
|
||||
'email' => $payload['email'] ?? '',
|
||||
'nickname' => '',
|
||||
'avatar' => '',
|
||||
'access_token' => $code,
|
||||
'refresh_token' => '',
|
||||
'expires_at' => $payload['exp'] ?? 0,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证Google授权码
|
||||
* 用授权码换取access_token,再获取用户信息
|
||||
*/
|
||||
private function verifyGoogle($code)
|
||||
{
|
||||
if (empty($code)) {
|
||||
throw new Exception('授权码不能为空');
|
||||
}
|
||||
|
||||
$config = $this->getOAuthConfig('google');
|
||||
if (!$config) {
|
||||
throw new Exception('Google OAuth未配置');
|
||||
}
|
||||
|
||||
// 用授权码换取access_token
|
||||
$response = $this->httpPost($this->oauthConfig['google']['token_url'], [
|
||||
'code' => $code,
|
||||
'client_id' => $config['client_id'],
|
||||
'client_secret' => $config['client_secret'],
|
||||
'redirect_uri' => $config['redirect_uri'],
|
||||
'grant_type' => 'authorization_code',
|
||||
]);
|
||||
|
||||
$tokenData = json_decode($response, true);
|
||||
if (empty($tokenData['access_token'])) {
|
||||
throw new Exception('获取Google access_token失败');
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
$userInfo = $this->httpGet($this->oauthConfig['google']['userinfo_url'], [
|
||||
'Authorization: Bearer ' . $tokenData['access_token'],
|
||||
]);
|
||||
|
||||
$userData = json_decode($userInfo, true);
|
||||
if (empty($userData['sub'])) {
|
||||
throw new Exception('获取Google用户信息失败');
|
||||
}
|
||||
|
||||
return [
|
||||
'openid' => 'google_' . $userData['sub'],
|
||||
'unionid' => $userData['sub'] ?? '',
|
||||
'email' => $userData['email'] ?? '',
|
||||
'nickname' => $userData['name'] ?? '',
|
||||
'avatar' => $userData['picture'] ?? '',
|
||||
'access_token' => $tokenData['access_token'],
|
||||
'refresh_token' => $tokenData['refresh_token'] ?? '',
|
||||
'expires_at' => time() + ($tokenData['expires_in'] ?? 3600),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证GitHub授权码
|
||||
* 用授权码换取access_token,再获取用户信息
|
||||
*/
|
||||
private function verifyGithub($code)
|
||||
{
|
||||
if (empty($code)) {
|
||||
throw new Exception('授权码不能为空');
|
||||
}
|
||||
|
||||
$config = $this->getOAuthConfig('oauth_github');
|
||||
if (!$config) {
|
||||
// 回退尝试 github 配置
|
||||
$config = $this->getOAuthConfig('github');
|
||||
}
|
||||
if (!$config) {
|
||||
throw new Exception('GitHub OAuth未配置');
|
||||
}
|
||||
|
||||
// 用授权码换取access_token
|
||||
$response = $this->httpPost($this->oauthConfig['github']['token_url'], [
|
||||
'code' => $code,
|
||||
'client_id' => $config['client_id'],
|
||||
'client_secret' => $config['client_secret'],
|
||||
'redirect_uri' => $config['redirect_uri'],
|
||||
], ['Accept: application/json']);
|
||||
|
||||
$tokenData = json_decode($response, true);
|
||||
if (empty($tokenData['access_token'])) {
|
||||
throw new Exception('获取GitHub access_token失败');
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
$userInfo = $this->httpGet($this->oauthConfig['github']['userinfo_url'], [
|
||||
'Authorization: Bearer ' . $tokenData['access_token'],
|
||||
]);
|
||||
|
||||
$userData = json_decode($userInfo, true);
|
||||
if (empty($userData['id'])) {
|
||||
throw new Exception('获取GitHub用户信息失败');
|
||||
}
|
||||
|
||||
// 获取邮箱(GitHub可能不返回主邮箱)
|
||||
$email = $userData['email'] ?? '';
|
||||
if (empty($email)) {
|
||||
$emails = $this->httpGet('https://api.github.com/user/emails', [
|
||||
'Authorization: Bearer ' . $tokenData['access_token'],
|
||||
]);
|
||||
$emailList = json_decode($emails, true);
|
||||
if (is_array($emailList)) {
|
||||
foreach ($emailList as $e) {
|
||||
if (($e['primary'] ?? false) && ($e['verified'] ?? false)) {
|
||||
$email = $e['email'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'openid' => 'github_' . $userData['id'],
|
||||
'unionid' => (string)$userData['id'],
|
||||
'email' => $email,
|
||||
'nickname' => $userData['login'] ?? '',
|
||||
'avatar' => $userData['avatar_url'] ?? '',
|
||||
'access_token' => $tokenData['access_token'],
|
||||
'refresh_token' => '',
|
||||
'expires_at' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找或创建用户
|
||||
* 1. 查找已有绑定 → 2. 通过邮箱查找 → 3. 创建新用户
|
||||
*/
|
||||
private function findOrCreateUser($platform, $oauthUser)
|
||||
{
|
||||
// 1. 查找已有绑定
|
||||
$binding = Db::name('user_oauth')
|
||||
->where('platform', $platform)
|
||||
->where('openid', $oauthUser['openid'])
|
||||
->find();
|
||||
|
||||
if ($binding) {
|
||||
$user = Db::name('user')->where('id', $binding['user_id'])->find();
|
||||
if ($user) {
|
||||
// 更新token
|
||||
Db::name('user_oauth')->where('id', $binding['id'])->update([
|
||||
'access_token' => $oauthUser['access_token'] ?? '',
|
||||
'refresh_token' => $oauthUser['refresh_token'] ?? '',
|
||||
'expires_at' => $oauthUser['expires_at'] ?? 0,
|
||||
'updatetime' => time(),
|
||||
]);
|
||||
$oauthUser['is_new'] = false;
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 通过邮箱查找已有用户
|
||||
$user = null;
|
||||
if (!empty($oauthUser['email'])) {
|
||||
$user = Db::name('user')->where('email', $oauthUser['email'])->find();
|
||||
}
|
||||
|
||||
// 3. 创建新用户
|
||||
if (!$user) {
|
||||
$username = $platform . '_' . substr($oauthUser['openid'], -8);
|
||||
// 确保用户名唯一
|
||||
$i = 1;
|
||||
$baseUsername = $username;
|
||||
while (Db::name('user')->where('username', $username)->find()) {
|
||||
$username = $baseUsername . '_' . $i++;
|
||||
}
|
||||
|
||||
$userId = Db::name('user')->insertGetId([
|
||||
'username' => $username,
|
||||
'nickname' => $oauthUser['nickname'] ?: $username,
|
||||
'email' => $oauthUser['email'] ?? '',
|
||||
'avatar' => $oauthUser['avatar'] ?? '',
|
||||
'password' => md5(md5(uniqid())),
|
||||
'salt' => '',
|
||||
'status' => 'normal',
|
||||
'verification' => json_encode(['email' => !empty($oauthUser['email']) ? 1 : 0]),
|
||||
'createtime' => time(),
|
||||
'updatetime' => time(),
|
||||
]);
|
||||
|
||||
$user = Db::name('user')->where('id', $userId)->find();
|
||||
$oauthUser['is_new'] = true;
|
||||
} else {
|
||||
$oauthUser['is_new'] = false;
|
||||
}
|
||||
|
||||
// 4. 创建绑定
|
||||
Db::name('user_oauth')->insert([
|
||||
'user_id' => $user['id'],
|
||||
'platform' => $platform,
|
||||
'openid' => $oauthUser['openid'],
|
||||
'unionid' => $oauthUser['unionid'] ?? '',
|
||||
'nickname' => $oauthUser['nickname'] ?? '',
|
||||
'avatar' => $oauthUser['avatar'] ?? '',
|
||||
'access_token' => $oauthUser['access_token'] ?? '',
|
||||
'refresh_token' => $oauthUser['refresh_token'] ?? '',
|
||||
'expires_at' => $oauthUser['expires_at'] ?? 0,
|
||||
'createtime' => time(),
|
||||
'updatetime' => time(),
|
||||
]);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成Token
|
||||
*/
|
||||
private function generateToken($user)
|
||||
{
|
||||
$token = \fast\Random::uuid();
|
||||
Db::name('user_token')->insert([
|
||||
'user_id' => $user['id'],
|
||||
'token' => $token,
|
||||
'createtime' => time(),
|
||||
'expiretime' => time() + 86400 * 30,
|
||||
]);
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录设备信息
|
||||
*/
|
||||
private function recordDeviceInfo($user)
|
||||
{
|
||||
$deviceName = $this->request->post('device_name', '', 'trim');
|
||||
$deviceModel = $this->request->post('device_model', '', 'trim');
|
||||
$platformType = $this->request->post('platform_type', '', 'trim');
|
||||
$deviceId = $this->request->post('device_id', '', 'trim');
|
||||
$ipCity = $this->request->post('ip_city', '', 'trim');
|
||||
$ipRange = $this->request->post('ip_range', '', 'trim');
|
||||
|
||||
if ($deviceId) {
|
||||
$existing = Db::name('user_device')
|
||||
->where('user_id', $user['id'])
|
||||
->where('device_id', $deviceId)
|
||||
->find();
|
||||
|
||||
$data = [
|
||||
'device_name' => $deviceName ?: $deviceModel,
|
||||
'device_model' => $deviceModel,
|
||||
'platform' => $platformType,
|
||||
'app_name' => '闲言工具箱',
|
||||
'ip' => request()->ip(),
|
||||
'ip_city' => $ipCity,
|
||||
'ip_range' => $ipRange,
|
||||
'last_active_time' => time(),
|
||||
'is_online' => 1,
|
||||
'updatetime' => time(),
|
||||
];
|
||||
|
||||
if ($existing) {
|
||||
Db::name('user_device')->where('id', $existing['id'])->update($data);
|
||||
} else {
|
||||
$data['user_id'] = $user['id'];
|
||||
$data['device_id'] = $deviceId;
|
||||
$data['createtime'] = time();
|
||||
Db::name('user_device')->insert($data);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新用户在线状态
|
||||
Db::name('user')->where('id', $user['id'])->update([
|
||||
'is_online' => 1,
|
||||
'last_active_time' => time(),
|
||||
'logintime' => time(),
|
||||
'updatetime' => time(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 频率限制检查
|
||||
*/
|
||||
private function checkRateLimit($action)
|
||||
{
|
||||
if (!isset(self::$rateLimits[$action])) {
|
||||
return true;
|
||||
}
|
||||
$config = self::$rateLimits[$action];
|
||||
$key = 'rate_oauth_' . $action . '_' . md5($this->request->ip());
|
||||
$count = cache($key) ?: 0;
|
||||
if ($count >= $config['max']) {
|
||||
$this->error('请求过于频繁,请稍后再试', null, ['retry_after' => $config['window']]);
|
||||
}
|
||||
cache($key, $count + 1, $config['window']);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否已登录
|
||||
*/
|
||||
private function checkLogin()
|
||||
{
|
||||
if (!$this->auth->id) {
|
||||
$this->error('请先登录', null, 401);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP GET 请求
|
||||
*/
|
||||
private function httpGet($url, $headers = [])
|
||||
{
|
||||
$ch = curl_init();
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_URL => $url,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 15,
|
||||
CURLOPT_HTTPHEADER => array_merge(['User-Agent: XianYan/1.0'], $headers),
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
]);
|
||||
$response = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP POST 请求
|
||||
*/
|
||||
private function httpPost($url, $data = [], $headers = [])
|
||||
{
|
||||
$ch = curl_init();
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_URL => $url,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => http_build_query($data),
|
||||
CURLOPT_TIMEOUT => 15,
|
||||
CURLOPT_HTTPHEADER => array_merge(['User-Agent: XianYan/1.0', 'Accept: application/json'], $headers),
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
]);
|
||||
$response = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
@@ -1322,12 +1322,22 @@ class UserCenter extends Api
|
||||
->order('last_active_time', 'desc')
|
||||
->field('id,device_name,device_model,platform,app_name,ip,ip_city,ip_range,last_active_time,is_online,createtime')
|
||||
->select();
|
||||
$sevenDaysAgo = time() - 7 * 86400;
|
||||
foreach ($devices as &$d) {
|
||||
$d['last_active_text'] = $d['last_active_time'] ? date('Y-m-d H:i:s', $d['last_active_time']) : '-';
|
||||
$d['createtime_text'] = $d['createtime'] ? date('Y-m-d H:i:s', $d['createtime']) : '-';
|
||||
// 7天内是否活跃在线
|
||||
$d['is_active_recently'] = ($d['last_active_time'] >= $sevenDaysAgo && $d['is_online'] == 1) ? 1 : 0;
|
||||
}
|
||||
unset($d);
|
||||
$this->success('', ['devices' => $devices]);
|
||||
// 统计活跃在线设备数
|
||||
$activeOnlineCount = 0;
|
||||
foreach ($devices as $dev) {
|
||||
if ($dev['is_active_recently'] == 1) {
|
||||
$activeOnlineCount++;
|
||||
}
|
||||
}
|
||||
$this->success('', ['devices' => $devices, 'active_online_count' => $activeOnlineCount]);
|
||||
} elseif ($action === 'remove') {
|
||||
$deviceId = $this->request->param('device_id/d', 0);
|
||||
if (!$deviceId) {
|
||||
|
||||
@@ -66,6 +66,16 @@ Route::rule([
|
||||
'api/user_security/cancelDeletion' => 'api/UserSecurity/cancelDeletion',
|
||||
]);
|
||||
|
||||
// OAuth社交登录
|
||||
Route::rule([
|
||||
'api/oauth/config' => 'api/Oauth/config',
|
||||
'api/oauth/login' => 'api/Oauth/login',
|
||||
'api/oauth/bind' => 'api/Oauth/bind',
|
||||
'api/oauth/unbind' => 'api/Oauth/unbind',
|
||||
'api/oauth/bound' => 'api/Oauth/bound',
|
||||
'api/oauth/install' => 'api/Oauth/install',
|
||||
]);
|
||||
|
||||
// 用户中心接口
|
||||
Route::rule([
|
||||
'api/user_center/index' => 'api/UserCenter/index',
|
||||
|
||||
221
docs/toolsapi/docs/API_OAUTH_DOC.md
Normal file
221
docs/toolsapi/docs/API_OAUTH_DOC.md
Normal file
@@ -0,0 +1,221 @@
|
||||
# 闲言APP — OAuth社交登录 API 接口文档
|
||||
|
||||
> 基础URL: `https://tools.wktyl.com`
|
||||
> 版本: v1.0.0 | 创建时间: 2026-06-05
|
||||
> 支持平台: Apple / Google / GitHub
|
||||
> 关联文档: API_USER_SECURITY_DOC.md
|
||||
|
||||
---
|
||||
|
||||
## 一、概述
|
||||
|
||||
OAuth社交登录允许用户通过 Apple、Google、GitHub 账号快速登录闲言APP,无需手动注册。
|
||||
|
||||
**核心流程:**
|
||||
1. 客户端调用 `/api/oauth/config` 获取授权URL
|
||||
2. 用户在第三方平台完成授权,获取授权码(id_token)
|
||||
3. 客户端调用 `/api/oauth/login` 提交授权码
|
||||
4. 服务端验证授权码,创建/关联用户,返回Token
|
||||
|
||||
**与闲言自有注册的关系:**
|
||||
- OAuth登录是独立通道,不影响现有用户名+密码注册
|
||||
- OAuth登录后自动创建闲言账号,邮箱相同的会自动关联
|
||||
- 已登录用户可通过 `/api/oauth/bind` 绑定社交账号
|
||||
|
||||
---
|
||||
|
||||
## 二、接口概览
|
||||
|
||||
| 接口 | 方法 | 路径 | 需登录 | 说明 |
|
||||
|------|------|------|--------|------|
|
||||
| 获取OAuth配置 | GET | `/api/oauth/config` | ❌ | 获取授权URL和client_id |
|
||||
| 社交登录 | POST | `/api/oauth/login` | ❌ | 授权码登录 |
|
||||
| 绑定社交账号 | POST | `/api/oauth/bind` | ✅ | 已登录用户绑定 |
|
||||
| 解绑社交账号 | POST | `/api/oauth/unbind` | ✅ | 已登录用户解绑 |
|
||||
| 已绑定列表 | GET | `/api/oauth/bound` | ✅ | 查询已绑定平台 |
|
||||
| 安装数据表 | GET | `/api/oauth/install` | ❌ | 初始化 |
|
||||
|
||||
---
|
||||
|
||||
## 三、接口详情
|
||||
|
||||
### 3.1 获取OAuth配置
|
||||
|
||||
**GET** `/api/oauth/config?platform=apple`
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| platform | string | ✅ | 平台: apple/google/github |
|
||||
|
||||
**响应示例:**
|
||||
```json
|
||||
{
|
||||
"code": 1,
|
||||
"msg": "",
|
||||
"data": {
|
||||
"platform": "github",
|
||||
"configured": true,
|
||||
"client_id": "Ov23li...",
|
||||
"redirect_uri": "https://tools.wktyl.com/oauth/callback",
|
||||
"authorize_url": "https://github.com/login/oauth/authorize?client_id=Ov23li...&redirect_uri=...&scope=user:email&state=..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 社交登录
|
||||
|
||||
**POST** `/api/oauth/login`
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| platform | string | ✅ | 平台: apple/google/github |
|
||||
| code | string | 条件 | 授权码(google/github必填) |
|
||||
| id_token | string | 条件 | Apple ID Token(apple必填) |
|
||||
| device_name | string | ❌ | 设备名称 |
|
||||
| device_model | string | ❌ | 设备型号 |
|
||||
| platform_type | string | ❌ | 登录平台(ios/android/web) |
|
||||
| device_id | string | ❌ | 设备唯一标识 |
|
||||
| ip_city | string | ❌ | IP归属地 |
|
||||
| ip_range | string | ❌ | IP段范围 |
|
||||
|
||||
**Apple登录流程:**
|
||||
1. 客户端使用 `sign_in_with_apple` SDK 获取 `id_token` 和 `authorization_code`
|
||||
2. POST `/api/oauth/login` 提交 `platform=apple&id_token=xxx&code=xxx`
|
||||
|
||||
**Google登录流程:**
|
||||
1. 客户端使用 `google_sign_in` SDK 获取 `serverAuthCode`
|
||||
2. POST `/api/oauth/login` 提交 `platform=google&code=xxx`
|
||||
|
||||
**GitHub登录流程:**
|
||||
1. 客户端打开浏览器授权页面
|
||||
2. 用户授权后回调获取 `code`
|
||||
3. POST `/api/oauth/login` 提交 `platform=github&code=xxx`
|
||||
|
||||
**成功响应:**
|
||||
```json
|
||||
{
|
||||
"code": 1,
|
||||
"msg": "登录成功",
|
||||
"data": {
|
||||
"userinfo": {
|
||||
"id": 42,
|
||||
"username": "github_abc12345",
|
||||
"nickname": "octocat",
|
||||
"email": "user@example.com",
|
||||
"avatar": "https://avatars.githubusercontent.com/u/..."
|
||||
},
|
||||
"token": "xxxxxxxxxxxx",
|
||||
"is_new_user": true,
|
||||
"bind_platform": "github"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 绑定社交账号
|
||||
|
||||
**POST** `/api/oauth/bind`
|
||||
|
||||
需登录。
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| platform | string | ✅ | 平台: apple/google/github |
|
||||
| code | string | 条件 | 授权码 |
|
||||
| id_token | string | 条件 | Apple ID Token |
|
||||
|
||||
### 3.4 解绑社交账号
|
||||
|
||||
**POST** `/api/oauth/unbind`
|
||||
|
||||
需登录。
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| platform | string | ✅ | 平台: apple/google/github |
|
||||
|
||||
### 3.5 已绑定列表
|
||||
|
||||
**GET** `/api/oauth/bound`
|
||||
|
||||
需登录。
|
||||
|
||||
**响应示例:**
|
||||
```json
|
||||
{
|
||||
"code": 1,
|
||||
"msg": "",
|
||||
"data": {
|
||||
"bindings": [
|
||||
{
|
||||
"platform": "github",
|
||||
"openid": "github_12345",
|
||||
"nickname": "octocat",
|
||||
"avatar": "https://...",
|
||||
"createtime": 1717200000
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.6 安装数据表
|
||||
|
||||
**GET** `/api/oauth/install`
|
||||
|
||||
无需登录。创建 `tool_user_oauth` 表。
|
||||
|
||||
---
|
||||
|
||||
## 四、数据表设计
|
||||
|
||||
### tool_user_oauth
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | int(11) unsigned | 主键自增 |
|
||||
| user_id | int(11) unsigned | 用户ID |
|
||||
| platform | varchar(30) | 平台(apple/google/github) |
|
||||
| openid | varchar(128) | 平台用户ID |
|
||||
| unionid | varchar(128) | 联合ID |
|
||||
| nickname | varchar(100) | 昵称 |
|
||||
| avatar | varchar(500) | 头像 |
|
||||
| access_token | text | 访问令牌 |
|
||||
| refresh_token | text | 刷新令牌 |
|
||||
| expires_at | int(11) unsigned | 令牌过期时间 |
|
||||
| createtime | int(11) unsigned | 创建时间 |
|
||||
| updatetime | int(11) unsigned | 更新时间 |
|
||||
|
||||
**索引:** uk_platform_openid(platform, openid), idx_user_id(user_id)
|
||||
|
||||
---
|
||||
|
||||
## 五、错误码
|
||||
|
||||
| code | msg | 说明 |
|
||||
|------|-----|------|
|
||||
| 0 | 不支持的平台 | platform参数无效 |
|
||||
| 0 | 平台未配置 | OAuth配置缺失 |
|
||||
| 0 | OAuth验证失败 | 授权码/id_token无效 |
|
||||
| 0 | 该账号已被其他用户绑定 | 绑定冲突 |
|
||||
| 0 | 已绑定该平台 | 重复绑定 |
|
||||
| 0 | 请求过于频繁 | 频率限制(30次/5分钟) |
|
||||
|
||||
---
|
||||
|
||||
## 六、频率限制
|
||||
|
||||
| 接口 | 上限 | 时间窗口(秒) |
|
||||
|------|------|-------------|
|
||||
| login | 30次 | 300 |
|
||||
| bind | 20次 | 3600 |
|
||||
| unbind | 20次 | 3600 |
|
||||
|
||||
---
|
||||
|
||||
## 七、安全说明
|
||||
|
||||
- Apple: 验证JWT的issuer和签名,不依赖客户端解析
|
||||
- Google: 服务端用授权码换取access_token,客户端无法伪造
|
||||
- GitHub: 服务端用授权码换取access_token,安全可靠
|
||||
- 同一openid只能绑定一个闲言账号
|
||||
- 邮箱相同的OAuth用户会自动关联已有账号
|
||||
@@ -806,7 +806,7 @@ curl -X POST https://tools.wktyl.com/api/user_center/interaction \
|
||||
|
||||
| action | 说明 | 必填参数 | 响应 |
|
||||
|--------|------|----------|------|
|
||||
| list | 📱 设备列表 | - | {devices: [{...}]} |
|
||||
| list | 📱 设备列表 | - | {devices: [{...}], active_online_count: int} |
|
||||
| remove | 🗑️ 删除设备 | device_id | 删除成功 |
|
||||
| offline | ⬇️ 下线设备 | device_id | 下线成功 |
|
||||
| offline_all | ⬇️⬇️ 下线所有设备 | - | 全部下线成功 |
|
||||
@@ -829,6 +829,7 @@ curl -X POST https://tools.wktyl.com/api/user_center/interaction \
|
||||
| createtime | int | 首次登录时间戳 |
|
||||
| last_active_text | string | 最后活跃时间(可读格式, 如"5分钟前") |
|
||||
| createtime_text | string | 首次登录时间(可读格式, 如"2026-05-01 10:30") |
|
||||
| is_active_recently | int | 🆕 7天内是否活跃在线(0/1, 后端计算) |
|
||||
|
||||
**请求示例:**
|
||||
```bash
|
||||
@@ -882,7 +883,8 @@ curl -X POST https://tools.wktyl.com/api/user_center/devices \
|
||||
"is_online": 1,
|
||||
"createtime": 1714588800,
|
||||
"last_active_text": "5分钟前",
|
||||
"createtime_text": "2026-05-01 10:30"
|
||||
"createtime_text": "2026-05-01 10:30",
|
||||
"is_active_recently": 1
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
@@ -897,9 +899,11 @@ curl -X POST https://tools.wktyl.com/api/user_center/devices \
|
||||
"is_online": 0,
|
||||
"createtime": 1712006400,
|
||||
"last_active_text": "2小时前",
|
||||
"createtime_text": "2026-04-02 08:00"
|
||||
"createtime_text": "2026-04-02 08:00",
|
||||
"is_active_recently": 0
|
||||
}
|
||||
]
|
||||
],
|
||||
"active_online_count": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -261,7 +261,7 @@
|
||||
<div class="content-card">
|
||||
<div id="content-zh" class="lang-content">
|
||||
<p><strong>闲言APP</strong> 软件介绍</p>
|
||||
<p>更新日期:2026年5月20日</p>
|
||||
<p>更新日期:2026年6月5日</p>
|
||||
<h2>一、关于闲言</h2>
|
||||
<p><strong>闲言APP</strong>是一款优雅的文字阅读与卡片创作应用,由<strong>弥勒市朋普镇微风暴网络科技工作室</strong>开发运营。我们致力于让文字阅读更纯粹,用文字点亮生活的每一刻。</p>
|
||||
<h2>二、核心功能</h2>
|
||||
@@ -298,8 +298,8 @@
|
||||
<h3>2.5 文件传输助手</h3>
|
||||
<ul>
|
||||
<li>局域网高速传输</li>
|
||||
<li>蓝牙传输</li>
|
||||
<li>USB OTG有线传输</li>
|
||||
<li>二维码配对传输</li>
|
||||
<li>跨平台支持</li>
|
||||
<li><span class="highlight">端到端加密</span></li>
|
||||
</ul>
|
||||
@@ -367,8 +367,8 @@
|
||||
</div>
|
||||
<div id="content-en" class="lang-content" style="display:none;">
|
||||
<p><strong>Xianyan APP</strong> — About Xianyan</p>
|
||||
<p>Version: V6.5</p>
|
||||
<p>Updated: May 20, 2026</p>
|
||||
<p>Version: V6.6</p>
|
||||
<p>Updated: June 5, 2026</p>
|
||||
<h2>I. App Overview</h2>
|
||||
<p><strong>Xianyan</strong> is an elegant sentence reading and card creation app that illuminates every moment of your life with words.</p>
|
||||
<h2>II. Core Features</h2>
|
||||
@@ -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月5日</p>
|
||||
<p>生效日期:2026年6月6日</p>
|
||||
<p>为保障<strong>闲言APP</strong>各项功能的正常使用,我们可能需要向您申请以下设备权限。我们严格遵循<span class="highlight">最小必要原则</span>,仅申请实现功能所必需的权限,并明确告知每项权限的使用目的。</p>
|
||||
<h2>一、相机权限</h2>
|
||||
<h3>1.1 权限说明:用于拍照和扫描功能</h3>
|
||||
@@ -310,56 +310,48 @@
|
||||
<li>您可随时在系统设置中关闭此权限</li>
|
||||
<li>关闭后天气功能需手动选择城市</li>
|
||||
</ul>
|
||||
<h2>五、蓝牙权限</h2>
|
||||
<h3>5.1 权限说明:用于蓝牙设备发现和文件传输</h3>
|
||||
<h2>五、附近设备权限</h2>
|
||||
<h3>5.1 权限说明:用于局域网设备发现和通信</h3>
|
||||
<h3>5.2 使用场景:</h3>
|
||||
<ul>
|
||||
<li>文件传输助手中的蓝牙配对传输</li>
|
||||
<li>近距离设备发现</li>
|
||||
</ul>
|
||||
<h3>5.3 权限控制:您可随时在系统设置中关闭此权限,关闭后蓝牙传输功能将不可用</h3>
|
||||
<h2>六、附近设备权限</h2>
|
||||
<h3>6.1 权限说明:用于局域网设备发现和通信</h3>
|
||||
<h3>6.2 使用场景:</h3>
|
||||
<ul>
|
||||
<li>文件传输助手中的局域网设备发现</li>
|
||||
<li>局域网文件传输</li>
|
||||
</ul>
|
||||
<h3>6.3 权限控制:您可随时在系统设置中关闭此权限,关闭后局域网传输功能将不可用</h3>
|
||||
<h2>七、网络权限</h2>
|
||||
<h3>7.1 权限说明:用于网络通信</h3>
|
||||
<h3>7.2 使用场景:</h3>
|
||||
<h3>5.3 权限控制:您可随时在系统设置中关闭此权限,关闭后局域网传输功能将不可用</h3>
|
||||
<h2>六、网络权限</h2>
|
||||
<h3>6.1 权限说明:用于网络通信</h3>
|
||||
<h3>6.2 使用场景:</h3>
|
||||
<ul>
|
||||
<li>内容加载与同步</li>
|
||||
<li>AI对话功能(规划中)</li>
|
||||
<li>数据云同步</li>
|
||||
<li>文件传输</li>
|
||||
</ul>
|
||||
<h3>7.3 说明:网络权限为应用基础权限,关闭后大部分功能将不可用</h3>
|
||||
<h2>八、权限管理原则</h2>
|
||||
<h3>8.1 <span class="highlight">最小必要原则</span>:我们仅申请实现功能所必需的最少权限</h3>
|
||||
<h3>8.2 <span class="highlight">知情同意原则</span>:申请权限时将明确告知使用目的</h3>
|
||||
<h3>8.3 <span class="highlight">自主控制原则</span>:您可随时开启或关闭任何权限</h3>
|
||||
<h3>8.4 <span class="highlight">安全保护原则</span>:通过权限获取的信息仅用于声明的目的</h3>
|
||||
<h2>九、权限变更</h2>
|
||||
<h3>6.3 说明:网络权限为应用基础权限,关闭后大部分功能将不可用</h3>
|
||||
<h2>七、权限管理原则</h2>
|
||||
<h3>7.1 <span class="highlight">最小必要原则</span>:我们仅申请实现功能所必需的最少权限</h3>
|
||||
<h3>7.2 <span class="highlight">知情同意原则</span>:申请权限时将明确告知使用目的</h3>
|
||||
<h3>7.3 <span class="highlight">自主控制原则</span>:您可随时开启或关闭任何权限</h3>
|
||||
<h3>7.4 <span class="highlight">安全保护原则</span>:通过权限获取的信息仅用于声明的目的</h3>
|
||||
<h2>八、权限变更</h2>
|
||||
<p>如我们申请新的权限,将在应用更新说明中明确告知。您可选择不更新应用以拒绝新权限。</p>
|
||||
<h2>十、联系方式</h2>
|
||||
<h2>九、联系方式</h2>
|
||||
<p>如您对权限使用有任何疑问,请联系:</p>
|
||||
<ul>
|
||||
<li>邮箱:2821981550@qq.com</li>
|
||||
<li>应用内:我的 → 关于 → 软件权限</li>
|
||||
<li>开发者:**弥勒市朋普镇微风暴网络科技工作室**</li>
|
||||
</ul>
|
||||
<h2>十一、法律适用与争议解决</h2>
|
||||
<h3>11.1 本说明适用<span class="highlight">中华人民共和国法律</span>。</h3>
|
||||
<h3>11.2 因本说明产生的争议,双方应友好协商解决;协商不成的,任何一方均可向我们所在地有管辖权的<span class="highlight">人民法院</span>提起诉讼。</h3>
|
||||
<h2>十、法律适用与争议解决</h2>
|
||||
<h3>10.1 本说明适用<span class="highlight">中华人民共和国法律</span>。</h3>
|
||||
<h3>10.2 因本说明产生的争议,双方应友好协商解决;协商不成的,任何一方均可向我们所在地有管辖权的<span class="highlight">人民法院</span>提起诉讼。</h3>
|
||||
<p>本协议中任何条款被认定为无效或不可执行的,不影响其他条款的效力。</p>
|
||||
</div>
|
||||
<div id="content-en" class="lang-content" style="display:none;">
|
||||
<p><strong>Xianyan APP</strong> Permission Usage Description</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 5, 2026</p>
|
||||
<p>Effective: June 6, 2026</p>
|
||||
<h2>I. Permission List</h2>
|
||||
<h3>1.1 Required Permissions</h3>
|
||||
<table><thead><tr>
|
||||
@@ -261,9 +261,9 @@
|
||||
<div class="content-card">
|
||||
<div id="content-zh" class="lang-content">
|
||||
<p><strong>闲言APP</strong> 隐私政策</p>
|
||||
<p>版本号:V6.6</p>
|
||||
<p>更新日期:2026年5月30日</p>
|
||||
<p>生效日期:2026年5月31日</p>
|
||||
<p>版本号:V6.7</p>
|
||||
<p>更新日期:2026年6月5日</p>
|
||||
<p>生效日期:2026年6月6日</p>
|
||||
<p><strong>弥勒市朋普镇微风暴网络科技工作室</strong>(以下简称"我们")深知个人信息对您的重要性,我们将按照法律法规的规定,保护您的个人信息及隐私安全。我们制定本隐私政策以帮助您了解我们如何收集、使用、存储和保护您的个人信息。本政策适用于<strong>闲言APP</strong>提供的所有服务。</p>
|
||||
<h2>零、定义</h2>
|
||||
<p>本隐私政策中使用的术语定义如下:</p>
|
||||
@@ -358,7 +358,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td>文件传输助手</td>
|
||||
<td>蓝牙/位置/附近设备权限(可选)</td>
|
||||
<td>位置/附近设备权限(可选)</td>
|
||||
<td>否</td>
|
||||
<td>无法使用文件传输功能</td>
|
||||
</tr>
|
||||
@@ -690,9 +690,9 @@
|
||||
</div>
|
||||
<div id="content-en" class="lang-content" style="display:none;">
|
||||
<p><strong>Xianyan APP</strong> Privacy Policy</p>
|
||||
<p>Version: V6.6</p>
|
||||
<p>Updated: May 30, 2026</p>
|
||||
<p>Effective: May 31, 2026</p>
|
||||
<p>Version: V6.7</p>
|
||||
<p>Updated: June 5, 2026</p>
|
||||
<p>Effective: June 6, 2026</p>
|
||||
<p>Zero. Definitions</p>
|
||||
<ul>
|
||||
<li>**Personal Information**: Information that can identify a natural person, including but not limited to name, date of birth, ID number, biometric information, address, phone number, email, etc.</li>
|
||||
@@ -715,7 +715,7 @@
|
||||
</ul>
|
||||
<h3>1.2 Information Collected Automatically During Service Use</h3>
|
||||
<ul>
|
||||
<li>Device information: device model, operating system version, device identifier (IDFA/Android ID), screen resolution</li>
|
||||
<li>Device information: device model, operating system version, device identifier (device_id), screen resolution</li>
|
||||
<li>Log information: access time, pages visited, click events, crash logs</li>
|
||||
<li>Network information: network type (WiFi/4G/5G), carrier information</li>
|
||||
<li>Device fingerprint information: device unique identifier (device_id), User-Agent, used for device identification and security verification</li>
|
||||
182
docs/toolsapi/scripts/test_oauth_api.py
Normal file
182
docs/toolsapi/scripts/test_oauth_api.py
Normal file
@@ -0,0 +1,182 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
闲言APP OAuth社交登录 API 测试脚本
|
||||
|
||||
创建时间: 2026-06-05
|
||||
更新时间: 2026-06-05
|
||||
作用: 测试OAuth社交登录接口的可用性和参数校验
|
||||
上次更新: 初始版本
|
||||
"""
|
||||
|
||||
import requests
|
||||
import json
|
||||
import sys
|
||||
|
||||
BASE = 'https://tools.wktyl.com'
|
||||
TIMEOUT = 15
|
||||
|
||||
|
||||
def test_install():
|
||||
"""测试安装数据表"""
|
||||
print("1. 安装数据表...")
|
||||
r = requests.get(f'{BASE}/api/oauth/install', timeout=TIMEOUT)
|
||||
data = r.json()
|
||||
print(f" 结果: {json.dumps(data, ensure_ascii=False, indent=2)}")
|
||||
assert data['code'] == 1, f"安装失败: {data['msg']}"
|
||||
print(" ✅ 安装成功")
|
||||
return True
|
||||
|
||||
|
||||
def test_config():
|
||||
"""测试获取OAuth配置"""
|
||||
print("2. 获取OAuth配置...")
|
||||
for platform in ['apple', 'google', 'github']:
|
||||
r = requests.get(f'{BASE}/api/oauth/config', params={'platform': platform}, timeout=TIMEOUT)
|
||||
data = r.json()
|
||||
configured = data.get('data', {}).get('configured', False) if data.get('code') == 1 else False
|
||||
print(f" {platform}: code={data['code']}, configured={configured}")
|
||||
print(" ✅ 配置接口正常")
|
||||
return True
|
||||
|
||||
|
||||
def test_config_invalid_platform():
|
||||
"""测试不支持的平台"""
|
||||
print("3. 测试不支持的平台...")
|
||||
r = requests.get(f'{BASE}/api/oauth/config', params={'platform': 'facebook'}, timeout=TIMEOUT)
|
||||
data = r.json()
|
||||
assert data['code'] == 0, "应该返回错误"
|
||||
print(f" 结果: {data['msg']}")
|
||||
print(" ✅ 不支持的平台正确拒绝")
|
||||
return True
|
||||
|
||||
|
||||
def test_login_invalid_platform():
|
||||
"""测试无效平台登录"""
|
||||
print("4. 测试无效平台登录...")
|
||||
r = requests.post(f'{BASE}/api/oauth/login', data={
|
||||
'platform': 'invalid_platform',
|
||||
'code': 'test',
|
||||
}, timeout=TIMEOUT)
|
||||
data = r.json()
|
||||
assert data['code'] == 0, "应该返回错误"
|
||||
print(f" 结果: {data['msg']}")
|
||||
print(" ✅ 无效平台正确拒绝")
|
||||
return True
|
||||
|
||||
|
||||
def test_login_github_empty_code():
|
||||
"""测试GitHub空授权码"""
|
||||
print("5. 测试GitHub空授权码...")
|
||||
r = requests.post(f'{BASE}/api/oauth/login', data={
|
||||
'platform': 'github',
|
||||
'code': '',
|
||||
}, timeout=TIMEOUT)
|
||||
data = r.json()
|
||||
assert data['code'] == 0, "应该返回错误"
|
||||
print(f" 结果: {data['msg']}")
|
||||
print(" ✅ 空授权码正确拒绝")
|
||||
return True
|
||||
|
||||
|
||||
def test_login_apple_empty_token():
|
||||
"""测试Apple空id_token"""
|
||||
print("6. 测试Apple空id_token...")
|
||||
r = requests.post(f'{BASE}/api/oauth/login', data={
|
||||
'platform': 'apple',
|
||||
'id_token': '',
|
||||
}, timeout=TIMEOUT)
|
||||
data = r.json()
|
||||
assert data['code'] == 0, "应该返回错误"
|
||||
print(f" 结果: {data['msg']}")
|
||||
print(" ✅ 空id_token正确拒绝")
|
||||
return True
|
||||
|
||||
|
||||
def test_login_google_empty_code():
|
||||
"""测试Google空授权码"""
|
||||
print("7. 测试Google空授权码...")
|
||||
r = requests.post(f'{BASE}/api/oauth/login', data={
|
||||
'platform': 'google',
|
||||
'code': '',
|
||||
}, timeout=TIMEOUT)
|
||||
data = r.json()
|
||||
assert data['code'] == 0, "应该返回错误"
|
||||
print(f" 结果: {data['msg']}")
|
||||
print(" ✅ 空授权码正确拒绝")
|
||||
return True
|
||||
|
||||
|
||||
def test_bound_without_login():
|
||||
"""测试未登录查询绑定列表"""
|
||||
print("8. 测试未登录查询绑定...")
|
||||
r = requests.get(f'{BASE}/api/oauth/bound', timeout=TIMEOUT)
|
||||
data = r.json()
|
||||
print(f" 结果: code={data['code']}, msg={data['msg']}")
|
||||
print(" ✅ 未登录正确拒绝")
|
||||
return True
|
||||
|
||||
|
||||
def test_bind_without_login():
|
||||
"""测试未登录绑定"""
|
||||
print("9. 测试未登录绑定...")
|
||||
r = requests.post(f'{BASE}/api/oauth/bind', data={
|
||||
'platform': 'github',
|
||||
'code': 'test',
|
||||
}, timeout=TIMEOUT)
|
||||
data = r.json()
|
||||
print(f" 结果: code={data['code']}, msg={data['msg']}")
|
||||
print(" ✅ 未登录绑定正确拒绝")
|
||||
return True
|
||||
|
||||
|
||||
def test_unbind_without_login():
|
||||
"""测试未登录解绑"""
|
||||
print("10. 测试未登录解绑...")
|
||||
r = requests.post(f'{BASE}/api/oauth/unbind', data={
|
||||
'platform': 'github',
|
||||
}, timeout=TIMEOUT)
|
||||
data = r.json()
|
||||
print(f" 结果: code={data['code']}, msg={data['msg']}")
|
||||
print(" ✅ 未登录解绑正确拒绝")
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("闲言APP OAuth社交登录 API 测试")
|
||||
print("=" * 60)
|
||||
|
||||
tests = [
|
||||
test_install,
|
||||
test_config,
|
||||
test_config_invalid_platform,
|
||||
test_login_invalid_platform,
|
||||
test_login_github_empty_code,
|
||||
test_login_apple_empty_token,
|
||||
test_login_google_empty_code,
|
||||
test_bound_without_login,
|
||||
test_bind_without_login,
|
||||
test_unbind_without_login,
|
||||
]
|
||||
|
||||
passed = 0
|
||||
failed = 0
|
||||
|
||||
for test in tests:
|
||||
try:
|
||||
if test():
|
||||
passed += 1
|
||||
except Exception as e:
|
||||
print(f" ❌ 失败: {e}")
|
||||
failed += 1
|
||||
print()
|
||||
|
||||
print("=" * 60)
|
||||
print(f"测试完成: ✅ {passed} 通过, ❌ {failed} 失败")
|
||||
print("=" * 60)
|
||||
|
||||
return 0 if failed == 0 else 1
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user