Files

612 lines
21 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
// +----------------------------------------------------------------------
// | YwxApp [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Author: ywxapp<admin@ywxapp.cn>
// +----------------------------------------------------------------------
declare (strict_types = 1);
namespace addon\wxchat\controller\api;
use addon\wxchat\model\WxchatFollow;
use think\facade\Db;
use think\facade\Request;
use ywxapp\controller\FrontendBase;
use ywxapp\model\MemberUser as UserModel;
use ywxapp\model\MemberProfile as ProfileModel;
/**
* Member 类
*
* @author ywxapp <admin@ywxapp.cn>
*/
class Member extends FrontendBase
{
protected $noNeedLogin = [];
protected $noNeedVerify = ['*'];
protected function initialize()
{}
/**
* 当前用户交友档案
*/
public function profile()
{
$uid = $this->auth->model->uid;
$profile = ProfileModel::where('uid', $uid)->find();
if (! $profile) {
$this->error('资料不存在');
}
$this->success($profile);
}
/**
* 查看他人公开资料(资料卡页用)
* 返回:档案字段 + nickname/avatar/account + 计算后的 age + 是否关注 + 动态数 + 相册
*/
public function detail()
{
$targetUid = (int) Request::get('uid');
if ($targetUid <= 0) {
$this->error('参数错误');
}
$profile = ProfileModel::where('uid', $targetUid)->find();
if (! $profile) {
$this->error('用户不存在');
}
$user = Db::name('member')->where('uid', $targetUid)
->field('uid,nickname,avatar,account,status')->find();
$pc = ! empty($profile->privacy_config) ? json_decode($profile->privacy_config, true) : [];
$pc = is_array($pc) ? $pc : $this->defaultPrivacy();
$data = $profile->toArray();
$data['nickname'] = $user['nickname'] ?? '';
$data['avatar'] = $user['avatar'] ?? '';
$data['account'] = $user['account'] ?? '';
$data['age'] = $profile->birthday
? (int) floor((time() - strtotime($profile->birthday)) / 31557600) : 0;
if (! empty($pc['hide_age'])) {
unset($data['age']);
}
// 关注状态(基于 wxchat_follows
$meUid = $this->auth->model->uid;
$followed = WxchatFollow::where('uid', $meUid)
->where('fid', $targetUid)
->where('status', 1)
->count();
$data['is_followed'] = $followed > 0 ? 1 : 0;
$data['is_online'] = ($profile->online_status == 1)
|| (! empty($profile->last_active_at) && time() - (int) $profile->last_active_at < 300);
// 动态数(表可能未初始化,容错)
try {
$momentCount = Db::name('wxchat_moments')->where('user_id', $targetUid)->count();
} catch (\Throwable $e) {
$momentCount = 0;
}
$data['moment_count'] = $momentCount;
// 照片墙:优先用 user.avatarprofile 表无头像列)
$data['photos'] = ! empty($user['avatar']) ? [$user['avatar']] : [];
$this->success($data);
}
/**
* 更新交友档案(定位 / 同城 / 择偶条件)
*/
public function updateProfile()
{
$uid = $this->auth->model->uid;
$allow = [
'gender', 'birthday', 'latitude', 'longitude', 'geo_hash',
'resideprovince', 'residecity', 'residedist',
'height', 'weight', 'lookingfor', 'affectivestatus',
'education', 'occupation', 'revenue', 'bloodtype',
'signature', 'bio', 'address',
];
$data = Request::post();
$update = array_intersect_key($data, array_flip($allow));
if (! empty($update['birthday'])) {
$update['constellation'] = $this->calcConstellation($update['birthday']);
$update['zodiac'] = $this->calcZodiac($update['birthday']);
}
if (! empty($update['latitude']) && ! empty($update['longitude'])) {
$update['geo_hash'] = $this->geoHash((float) $update['latitude'], (float) $update['longitude']);
}
ProfileModel::where('uid', $uid)->update($update);
$this->success([], '更新成功');
}
/**
* 同城智能匹配(基于经纬度距离排序)
*/
public function nearby()
{
$uid = $this->auth->model->uid;
$me = ProfileModel::where('uid', $uid)->field('latitude,longitude,residecity')->find();
$lat = (float) Request::get('lat', $me['latitude'] ?? 0);
$lng = (float) Request::get('lng', $me['longitude'] ?? 0);
$radius = (float) Request::get('radius', 10);
$gender = (int) Request::get('gender', 0);
$ageMin = (int) Request::get('age_min', 0);
$ageMax = (int) Request::get('age_max', 0);
$limit = (int) Request::get('limit', 20);
$page = (int) Request::get('page', 1);
if (! $lat || ! $lng) {
$this->error('请先更新你的定位');
}
$dist = "(6371 * acos(cos(radians({$lat})) * cos(radians(p.latitude)) "
. "* cos(radians(p.longitude) - radians({$lng})) "
. "+ sin(radians({$lat})) * sin(radians(p.latitude))))";
$query = Db::name('member_profile')->alias('p')
->join('user u', 'u.uid = p.uid')
->where('p.uid', '<>', $uid)
->whereNotNull('p.latitude')
->whereNotNull('p.longitude')
->where('p.latitude', '<>', 0)
->where('p.longitude', '<>', 0)
->field([
'p.uid', 'u.nickname', 'u.avatar', 'p.gender', 'p.birthday',
'p.resideprovince', 'p.residecity', 'p.residedist',
'p.height', 'p.weight', 'p.bio', 'p.online_status', 'p.last_active_at',
'p.privacy_config',
Db::raw("{$dist} AS distance"),
]);
if ($gender) {
$query->where('p.gender', $gender);
}
if ($ageMin || $ageMax) {
$lo = max(1, $ageMin);
$hi = max($lo, $ageMax ?: 100);
$query->whereRaw('TIMESTAMPDIFF(YEAR, p.birthday, CURDATE()) BETWEEN ? AND ?', [$lo, $hi]);
}
$query->whereRaw("{$dist} <= ?", [$radius]);
$query->order('distance', 'asc');
$list = $query->paginate($limit, false, ['page' => $page]);
$this->success($this->formatUsers($list));
}
/**
* 本地同城(按居住城市匹配,无定位也能用)
*/
public function local()
{
$uid = $this->auth->model->uid;
$me = ProfileModel::where('uid', $uid)->field('residecity,geo_hash')->find();
$city = Request::get('city', $me['residecity'] ?? '');
$gender = (int) Request::get('gender', 0);
$ageMin = (int) Request::get('age_min', 0);
$ageMax = (int) Request::get('age_max', 0);
$limit = (int) Request::get('limit', 20);
$page = (int) Request::get('page', 1);
$query = Db::name('member_profile')->alias('p')
->join('user u', 'u.uid = p.uid')
->where('p.uid', '<>', $uid)
->field([
'p.uid', 'u.nickname', 'u.avatar', 'p.gender', 'p.birthday',
'p.resideprovince', 'p.residecity', 'p.residedist',
'p.height', 'p.weight', 'p.bio', 'p.online_status', 'p.last_active_at',
'p.privacy_config',
]);
if ($city) {
$query->where('p.residecity', $city);
} elseif (! empty($me['geo_hash'])) {
$query->where('p.geo_hash', 'like', substr($me['geo_hash'], 0, 4) . '%');
} else {
$this->success(['list' => [], 'total' => 0, 'pages' => 0]);
return;
}
if ($gender) {
$query->where('p.gender', $gender);
}
if ($ageMin || $ageMax) {
$lo = max(1, $ageMin);
$hi = max($lo, $ageMax ?: 100);
$query->whereRaw('TIMESTAMPDIFF(YEAR, p.birthday, CURDATE()) BETWEEN ? AND ?', [$lo, $hi]);
}
$query->order('p.last_active_at', 'desc');
$list = $query->paginate($limit, false, ['page' => $page]);
$this->success($this->formatUsers($list));
}
/**
* 24 小时在线列表(实时在线活跃用户)
*/
public function online()
{
$uid = $this->auth->model->uid;
$gender = (int) Request::get('gender', 0);
$limit = (int) Request::get('limit', 50);
$page = (int) Request::get('page', 1);
$query = Db::name('member_profile')->alias('p')
->join('user u', 'u.uid = p.uid')
->where('p.uid', '<>', $uid)
->where(function ($q) {
$q->where('p.online_status', 1)
->whereOr('p.last_active_at', '>', time() - 300);
})
->field([
'p.uid', 'u.nickname', 'u.avatar', 'p.gender', 'p.birthday',
'p.resideprovince', 'p.residecity', 'p.height', 'p.weight',
'p.bio', 'p.online_status', 'p.last_active_at',
'p.privacy_config',
]);
if ($gender) {
$query->where('p.gender', $gender);
}
$query->order('p.last_active_at', 'desc');
$list = $query->paginate($limit, false, ['page' => $page]);
$this->success($this->formatUsers($list));
}
/**
* 心跳:上报在线状态
*/
public function heartbeat()
{
$uid = $this->auth->model->uid;
ProfileModel::where('uid', $uid)->update([
'last_active_at' => time(),
'online_status' => 1,
]);
$this->success(['online' => 1], 'ok');
}
/**
* 统一格式化用户列表(附带距离/年龄/在线)
*/
private function formatUsers($paginate)
{
$items = $paginate->items();
foreach ($items as &$it) {
$it['age'] = $it['birthday'] ? (int) floor((time() - strtotime($it['birthday'])) / 31557600) : 0;
$it['is_online'] = $it['online_status'] == 1
|| (! empty($it['last_active_at']) && time() - (int) $it['last_active_at'] < 300);
if (isset($it['distance'])) {
$it['distance'] = round((float) $it['distance'], 2);
}
// 应用对方隐私设置:隐藏其年龄/在线/距离
$pc = ! empty($it['privacy_config']) ? json_decode($it['privacy_config'], true) : [];
$pc = is_array($pc) ? $pc : [];
if (! empty($pc['hide_age'])) {
unset($it['age']);
}
if (! empty($pc['hide_online'])) {
$it['is_online'] = false;
}
if (! empty($pc['hide_distance']) && isset($it['distance'])) {
unset($it['distance']);
}
unset($it['online_status'], $it['last_active_at'], $it['birthday'], $it['privacy_config']);
}
return [
'list' => $items,
'total' => $paginate->total(),
'pages' => $paginate->lastPage(),
];
}
/**
* 计算星座
*/
private function calcConstellation($date)
{
$ts = strtotime($date);
if (! $ts) {
return '';
}
$m = (int) date('m', $ts);
$d = (int) date('d', $ts);
$map = [
[1, 20, '摩羯座'], [2, 19, '水瓶座'], [3, 21, '双鱼座'], [4, 20, '白羊座'],
[5, 21, '金牛座'], [6, 22, '双子座'], [7, 23, '巨蟹座'], [8, 23, '狮子座'],
[9, 23, '处女座'], [10, 24, '天秤座'], [11, 23, '天蝎座'], [12, 22, '射手座'], [13, 0, '摩羯座'],
];
foreach ($map as [$mm, $dd, $name]) {
if ($m < $mm || ($m == $mm && $d < $dd)) {
return $name;
}
}
return '摩羯座';
}
/**
* 计算生肖
*/
private function calcZodiac($date)
{
$ts = strtotime($date);
if (! $ts) {
return '';
}
$zodiacs = ['鼠', '牛', '虎', '兔', '龙', '蛇', '马', '羊', '猴', '鸡', '狗', '猪'];
$year = (int) date('Y', $ts);
return $zodiacs[($year - 4) % 12];
}
/**
* 简易 geohash 编码(用于同城粗匹配前缀)
*/
private function geoHash($lat, $lng, $precision = 8)
{
$base32 = '0123456789bcdefghjkmnpqrstuvwxyz';
$latRange = [-90.0, 90.0];
$lngRange = [-180.0, 180.0];
$hash = '';
$bits = 0;
$bit = 0;
$even = true;
$i = 0;
while (strlen($hash) < $precision) {
if ($even) {
$mid = ($lngRange[0] + $lngRange[1]) / 2;
if ($lng > $mid) {
$bit = 1;
$lngRange[0] = $mid;
} else {
$bit = 0;
$lngRange[1] = $mid;
}
} else {
$mid = ($latRange[0] + $latRange[1]) / 2;
if ($lat > $mid) {
$bit = 1;
$latRange[0] = $mid;
} else {
$bit = 0;
$latRange[1] = $mid;
}
}
$even = ! $even;
$bits = ($bits << 1) + $bit;
if (++$i == 5) {
$hash .= $base32[$bits];
$bits = 0;
$i = 0;
}
}
return $hash;
}
/**
* 默认隐私 / 设置项
*/
private function defaultPrivacy(): array
{
return [
'hide_age' => 0,
'hide_online' => 0,
'hide_distance' => 0,
'hide_album' => 0,
'danmaku_global' => 1, // 弹幕总开关
];
}
/**
* 读取我的隐私 / 设置
*/
public function privacy()
{
$uid = $this->auth->uid;
$profile = ProfileModel::where('uid', $uid)->field('privacy_config')->find();
$pc = $profile && $profile->privacy_config ? json_decode($profile->privacy_config, true) : [];
$pc = is_array($pc) ? $pc : [];
$this->success(array_merge($this->defaultPrivacy(), $pc));
}
/**
* 更新隐私 / 设置(隐藏年龄/在线/距离/相册 + 弹幕总开关)
*/
public function updatePrivacy()
{
$uid = $this->auth->uid;
$allow = ['hide_age', 'hide_online', 'hide_distance', 'hide_album', 'danmaku_global'];
$data = Request::post();
$update = array_intersect_key($data, array_flip($allow));
if (empty($update)) {
$this->error('无有效字段');
}
$profile = ProfileModel::where('uid', $uid)->field('privacy_config')->find();
$pc = $profile && $profile->privacy_config ? json_decode($profile->privacy_config, true) : [];
$pc = is_array($pc) ? $pc : [];
foreach ($update as $k => $v) {
$pc[$k] = $v ? 1 : 0;
}
ProfileModel::where('uid', $uid)->update(['privacy_config' => json_encode($pc, JSON_UNESCAPED_UNICODE)]);
$this->success($pc, '已更新');
}
/**
* 账号注销(逻辑注销:匿名化资料 + 标记注销时间)
* 注:JWT 为无状态令牌,注销后需客户端清除本地令牌;服务端标记后拒绝以该账号发起的新业务
*/
public function cancelAccount()
{
$uid = $this->auth->uid;
// 匿名化资料
ProfileModel::where('uid', $uid)->update([
'bio' => '',
'address' => '',
'latitude' => null,
'longitude' => null,
'geo_hash' => '',
'canceled_at' => time(),
'online_status'=> 0,
]);
// 匿名化账号(昵称/头像)
try {
UserModel::where('uid', $uid)->update([
'nickname' => '已注销用户',
'avatar' => '',
'status' => 0,
]);
} catch (\Throwable $e) {
// 个别环境 user 表无 status 字段时忽略
}
$this->success([], '账号已注销');
}
/**
* 我的聚合统计(缘友/粉丝/关注/获赞/动态数)
* 供「我的」中心页展示使用
*/
public function summary()
{
$uid = $this->auth->model->uid;
$stat = function (callable $cb, $default = 0) {
try {
return $cb();
} catch (\Throwable $e) {
return $default;
}
};
// 关注:我关注别人(uid 为关注者)
$following = $stat(function () use ($uid) {
return (int) Db::name('wxchat_follow')
->where('uid', $uid)
->where('status', 1)
->count();
});
// 粉丝:别人关注我(fid 为被关注者)
$followers = $stat(function () use ($uid) {
return (int) Db::name('wxchat_follow')
->where('fid', $uid)
->where('status', 1)
->count();
});
// 缘友:互为好友数
$friends = $stat(function () use ($uid) {
return (int) Db::name('wxchat_friend')
->where(function ($q) use ($uid) {
$q->where('uid', $uid)->whereOr('fid', $uid);
})
->where('status', 1)
->count();
});
// 获赞:我的动态被点赞总数
$likes = $stat(function () use ($uid) {
return (int) Db::name('wxchat_moments')
->where('uid', $uid)
->where('status', 1)
->sum('like_count');
});
// 我的动态数
$moments = $stat(function () use ($uid) {
return (int) Db::name('wxchat_moments')
->where('uid', $uid)
->where('status', 1)
->count();
});
$this->success([
'following' => $following,
'followers' => $followers,
'friends' => $friends,
'likes' => $likes,
'moments' => $moments,
]);
}
/**
* 排行榜(财富榜/魅力榜)
* type = rich 按金币,type = charm 按粉丝数
*/
public function ranking()
{
$type = Request::get('type', 'charm');
$limit = 50;
if ($type == 'rich') {
$list = Db::name('member_wallets')->alias('w')
->join('user u', 'u.uid = w.uid')
->order('w.coins', 'desc')
->limit($limit)
->field('u.uid,u.nickname,u.avatar,w.coins as value')
->select();
$metric = 'coins';
} else {
$list = Db::name('wxchat_follow')->alias('f')
->join('user u', 'u.uid = f.fid')
->where('f.status', 1)
->group('f.fid')
->order('count(f.fid) desc')
->limit($limit)
->field('f.fid as uid,u.nickname,u.avatar,count(f.fid) as value')
->select();
$metric = 'fans';
}
$rank = 1;
$result = [];
foreach ($list as $it) {
$result[] = [
'rank' => $rank++,
'uid' => $it['uid'],
'nickname' => $it['nickname'],
'avatar' => $it['avatar'],
'value' => (int) $it['value'],
'coins' => isset($it['coins']) ? (int) $it['coins'] : 0,
];
}
$this->success(['type' => $type, 'metric' => $metric, 'list' => $result]);
}
/**
* 我的钱包(余额 / 金币 / 汇总 / 明细)
*/
public function wallet()
{
$uid = $this->uid;
$row = Db::name('member_wallets')->where('uid', $uid)->find();
if (empty($row)) {
$row = [
'balance' => '0.00',
'coins' => 0,
'total_recharge' => '0.00',
'total_consume' => '0.00',
];
}
// 金币/余额变动明细(表不存在时返回空)
try {
$logs = Db::name('wxchat_coin_logs')
->where('uid', $uid)
->order('create_at', 'desc')
->limit(50)
->field('id,type,change_num,balance_after,remark,create_at')
->select();
} catch (\Exception $e) {
$logs = [];
}
$this->success([
'balance' => (float) ($row['balance'] ?? 0),
'coins' => (int) ($row['coins'] ?? 0),
'total_recharge' => (float) ($row['total_recharge'] ?? 0),
'total_consume' => (float) ($row['total_consume'] ?? 0),
'logs' => $logs,
]);
}
}