chore: 重写初始提交(清空历史,整理后全量提交)

This commit is contained in:
ywxapp
2026-08-16 16:54:14 +08:00
commit 6c1a106bc1
1808 changed files with 238144 additions and 0 deletions
+611
View File
@@ -0,0 +1,611 @@
<?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,
]);
}
}