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
+73
View File
@@ -0,0 +1,73 @@
<?php
// +----------------------------------------------------------------------
// | YwxApp [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Author: ywxapp<admin@ywxapp.cn>
// +----------------------------------------------------------------------
namespace addon\wxchat\controller\api;
use think\facade\Db;
use think\facade\Request;
use ywxapp\controller\FrontendBase;
/**
* Feedback 类
*
* @author ywxapp <admin@ywxapp.cn>
*/
class Feedback extends FrontendBase
{
protected $noNeedLogin = [];
protected $noNeedVerify = ['*'];
protected function initialize()
{}
/**
* 提交意见反馈
* type: 1-功能建议 2-投诉举报 3-其他
*/
public function save()
{
$uid = $this->uid;
$content = trim((string) Request::post('content', ''));
$type = (int) Request::post('type', 1);
$contact = trim((string) Request::post('contact', ''));
if ($content === '') {
$this->error('反馈内容不能为空');
}
if (mb_strlen($content) > 500) {
$this->error('反馈内容过长(最多500字)');
}
Db::name('wxchat_feedback')->insert([
'uid' => $uid,
'type' => $type,
'content' => $content,
'contact' => $contact,
'status' => 0,
'create_at' => time(),
]);
$this->success([], '提交成功');
}
/**
* 我的反馈列表
*/
public function index()
{
$uid = $this->uid;
$list = Db::name('wxchat_feedback')
->where('uid', $uid)
->order('create_at', 'desc')
->limit(50)
->field('id,type,content,status,create_at')
->select();
$this->success(['list' => $list]);
}
}
+389
View File
@@ -0,0 +1,389 @@
<?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 as UserFollow;
use addon\wxchat\model\WxchatIntimacy as UserIntimacy;
use think\facade\Db;
use think\facade\Request;
use ywxapp\controller\FrontendBase;
use ywxapp\model\MemberUser as UserModel;
/**
* Follow 类
*
* @author ywxapp <admin@ywxapp.cn>
*/
class Follow extends FrontendBase
{
/**
* Summary of needLogin
* @var array
*/
protected $noNeedLogin = ['*'];
/**
* Summary of needRight
* @var array
*/
protected $noNeedVerify = ['*'];
/**
* 控制器初始化 _initialize
* @return void
*/
protected function initialize()
{
}
/**
* 关注/取消关注用户
* @return Json
*/
public function toggleFollow(): Json
{
$uid = $this->request->param('uid', 88888888);
$targetUid = $this->request->param('target_uid', 88888891);
$action = $this->request->param('action', 1); // 1-关注,0-取消关注
if (! $uid || ! $targetUid) {
$this->error('用户ID和目标用户ID不能为空', 400);
}
if ($uid == $targetUid) {
$this->error('不能关注自己', 400);
}
// 检查目标用户是否存在
$targetUser = UserModel::where('uid', $targetUid)->findOrEmpty();
if ($targetUser->isEmpty()) {
$this->error('目标用户不存在', 404);
}
// 检查是否已关注
$follow = UserFollow::where('uid', $uid)->where('fid', $targetUid)->findOrEmpty();
DB::startTrans();
try {
if ($follow->isEmpty()) {
$follow = new UserFollow();
$follow->uid = $uid;
$follow->fid = $targetUid;
$follow->status = 1;
$follow->save();
} else {
$follow->status = $follow->status == 1 ? 0 : 1;
$follow->save();
}
if ($follow->status == 1) {
// UserModel::where('id', $uid)->inc('following_count')->update();
// UserModel::where('id', $targetuid)->inc('follower_count')->update();
$mutual = UserFollow::where('fid', $targetUid)
->where('uid', $uid)
->where('status', 1)
->find();
$isMutual = $mutual ? 1 : 0;
// 增加亲密度
$this->updateIntimacy($uid, $targetUid, 5);
DB::commit();
$this->success(['is_mutual' => $isMutual, 'follow_status' => 1], '关注成功');
} else {
// 更新用户关注数
// UserModel::where('id', $uid)->dec('following_count')->update();
// UserModel::where('id', $targetUid)->dec('follower_count')->update();
// 减少亲密度
$this->updateIntimacy($uid, $targetUid, -5);
DB::commit();
$this->success(['follow_status' => 0], '取消关注成功');
}
} catch (\Exception $e) {
DB::rollback();
$this->error('操作失败: ' . $e->getMessage());
}
}
/**
* 获取关注列表
* @return Json
*/
public function getFollowingList(): Json
{
$uid = $this->request->param('uid', 0);
$page = $this->request->param('page', 1);
$size = $this->request->param('size', 20);
if (! $uid) {
$this->error('用户ID不能为空', 400);
}
// 我关注的人:uid=当前用户,取 fid 列表
$total = UserFollow::where('uid', $uid)
->where('status', 1)
->count();
$follows = UserFollow::where('uid', $uid)
->where('status', 1)
->order('id', 'desc')
->page($page, $size)
->select();
$targetIds = [];
foreach ($follows as $follow) {
$targetIds[] = $follow->fid;
}
// 批量取用户资料(按 uid 匹配,规避关联主键歧义)
$userMap = [];
if (! empty($targetIds)) {
$users = UserModel::whereIn('uid', $targetIds)
->field('uid,nickname,avatar')
->select();
foreach ($users as $u) {
$userMap[$u->uid] = $u;
}
}
// 对方是否也关注了我(互关判断)
$backIds = UserFollow::where('fid', $uid)
->where('status', 1)
->column('uid');
$backSet = array_flip($backIds);
$list = [];
foreach ($follows as $follow) {
$user = $userMap[$follow->fid] ?? null;
$list[] = [
'id' => $follow->fid,
'nickname' => $user ? $user->nickname : '',
'avatar' => $user ? $user->avatar : '',
'is_mutual' => isset($backSet[$follow->fid]) ? 1 : 0,
'follow_time' => date('Y-m-d H:i:s', (int) $follow->create_at),
];
}
$this->success([
'list' => $list,
'total' => $total,
'page' => $page,
'size' => $size,
], '获取成功');
}
/**
* 获取粉丝列表
* @return Json
*/
public function getFollowerList(): Json
{
$uid = $this->request->param('uid', 0);
$page = $this->request->param('page', 1);
$size = $this->request->param('size', 20);
if (! $uid) {
$this->error('用户ID不能为空', 400);
}
// 粉丝:谁关注了我,fid=当前用户,取 uid 列表
$follows = UserFollow::where('fid', $uid)
->where('status', 1)
->order('id', 'desc')
->page($page, $size)
->select();
$total = UserFollow::where('fid', $uid)
->where('status', 1)
->count();
$fanIds = [];
foreach ($follows as $follow) {
$fanIds[] = $follow->uid;
}
// 批量取粉丝资料(按 uid 匹配)
$userMap = [];
if (! empty($fanIds)) {
$users = UserModel::whereIn('uid', $fanIds)
->field('uid,nickname,avatar')
->select();
foreach ($users as $u) {
$userMap[$u->uid] = $u;
}
}
// 我是否也关注了该粉丝(互关判断)
$myFollowIds = UserFollow::where('uid', $uid)
->where('status', 1)
->column('fid');
$mySet = array_flip($myFollowIds);
$list = [];
foreach ($follows as $follow) {
$user = $userMap[$follow->uid] ?? null;
$list[] = [
'id' => $follow->uid,
'nickname' => $user ? $user->nickname : '',
'avatar' => $user ? $user->avatar : '',
'is_mutual' => isset($mySet[$follow->uid]) ? 1 : 0,
'follow_time' => date('Y-m-d H:i:s', (int) $follow->create_at),
];
}
$this->success([
'list' => $list,
'total' => $total,
'page' => $page,
'size' => $size,
], '获取成功');
}
/**
* 获取互关列表
* @return Json
*/
public function getMutualList(): Json
{
$uid = $this->request->param('uid', 0);
$page = $this->request->param('page', 1);
$size = $this->request->param('size', 20);
if (! $uid) {
$this->error('用户ID不能为空', 400);
}
// 获取互关用户ID
$mutualIds = UserFollow::alias('f1')
->join('user_follows f2', 'f1.uid = f2.fid AND f1.fid = f2.uid')
->where('f1.uid', $uid)
->where('f1.status', 1)
->where('f2.status', 1)
->column('f1.fid');
if (empty($mutualIds)) {
$this->success([
'list' => [],
'total' => 0,
'page' => $page,
'size' => $size,
], '获取成功');
}
$users = UserModel::whereIn('id', $mutualIds)
->field('id,username,nickname,avatar,following_count,follower_count')
->page($page, $size)
->select();
$total = count($mutualIds);
$list = [];
foreach ($users as $user) {
// 获取亲密度
$intimacy = $this->getIntimacy($uid, $user->id);
$list[] = [
'id' => $user->id,
'nickname' => $user->nickname,
'avatar' => $user->avatar,
'following_count' => $user->following_count,
'follower_count' => $user->follower_count,
'intimacy' => $intimacy['intimacy'],
'intimacy_level' => $intimacy['level'],
'free_message_count' => $this->getFreeMessageCount($intimacy['level']),
];
}
$this->success([
'list' => $list,
'total' => $total,
'page' => $page,
'size' => $size,
], '获取成功');
}
/**
* 更新亲密度
*/
private function updateIntimacy($uid, $targetuid, $value)
{
$record = UserIntimacy::where('uid', $uid)
->where('target_uid', $targetuid)
->find();
if ($record) {
$newIntimacy = $record->intimacy + $value;
$newIntimacy = max(0, min(100, $newIntimacy)); // 限制在0-100之间
$record->intimacy = $newIntimacy;
$record->level = $this->calculateIntimacyLevel($newIntimacy);
$record->last_interaction = date('Y-m-d H:i:s');
$record->save();
} else {
$intimacy = max(0, min(100, $value));
$record = new UserIntimacy();
$record->uid = $uid;
$record->target_uid = $targetuid;
$record->intimacy = $intimacy;
$record->level = $this->calculateIntimacyLevel($intimacy);
$record->save();
}
// 同时更新对方的亲密度
$this->updateIntimacy($targetuid, $uid, $value);
}
/**
* 获取亲密度
*/
private function getIntimacy($uid, $targetuid)
{
$record = UserIntimacy::where('uid', $uid)
->where('target_uid', $targetuid)
->find();
if ($record) {
return [
'intimacy' => $record->intimacy,
'level' => $record->level,
];
}
return ['intimacy' => 0, 'level' => 1];
}
/**
* 计算亲密度等级
*/
private function calculateIntimacyLevel($intimacy)
{
if ($intimacy >= 90) {
return 5;
}
if ($intimacy >= 80) {
return 4;
}
if ($intimacy >= 60) {
return 3;
}
if ($intimacy >= 40) {
return 2;
}
return 1;
}
/**
* 获取免费消息数量
*/
private function getFreeMessageCount($level)
{
$counts = [0, 0, 2, 4, 5, PHP_INT_MAX];
return $counts[$level] ?? 0;
}
}
+202
View File
@@ -0,0 +1,202 @@
<?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\WxchatFriend as FriendModel;
use think\facade\Db;
use think\facade\Request;
use ywxapp\controller\FrontendBase;
/**
* Friend 类
*
* @author ywxapp <admin@ywxapp.cn>
*/
class Friend extends FrontendBase
{
/**
* Summary of noNeedLogin
* @var array
*/
protected $noNeedLogin = [];
/**
* Summary of noNeedVerify
* @var array
*/
protected $noNeedVerify = ['*'];
/**
* 控制器初始化 initialize
* @return void
*/
protected function initialize()
{}
/**
* 当前用户好友列表(已通过)
*/
public function index()
{
$uid = $this->currentUid();
$friends = FriendModel::where('uid', $uid)
->where('status', 1)
->order('create_at', 'desc')
->select();
$list = [];
foreach ($friends as $f) {
$u = Db::name('member')->where('uid', $f->fid)
->field('uid,nickname,avatar,phone')->find();
if ($u) {
$u['friend_id'] = $f->id;
$u['remark'] = $f->remark;
$list[] = $u;
}
}
$this->success(['list' => $list, 'total' => count($list)]);
}
/**
* 待处理的好友请求(别人加我,status=0)
*/
public function requests()
{
$uid = $this->currentUid();
$rows = FriendModel::where('fid', $uid)
->where('status', 0)
->order('create_at', 'desc')
->select();
$list = [];
foreach ($rows as $r) {
$u = Db::name('member')->where('uid', $r->uid)
->field('uid,nickname,avatar')->find();
if ($u) {
$u['friend_id'] = $r->id;
$u['remark'] = $r->remark;
$list[] = $u;
}
}
$this->success(['list' => $list, 'total' => count($list)]);
}
/**
* 发送好友请求
*/
public function create()
{
$uid = $this->currentUid();
$fid = (int) Request::post('fid');
$remark = Request::post('remark', '');
if ($fid <= 0 || $fid == $uid) {
$this->error('无效的好友ID');
}
$exist = FriendModel::where('uid', $uid)->where('fid', $fid)->find();
if ($exist) {
if ($exist->status == 1) {
$this->error('你们已经是好友了');
}
// 重新发起请求
$exist->status = 0;
$exist->remark = $remark;
$exist->save();
$this->success([], '已重新发送好友请求');
}
FriendModel::create([
'uid' => $uid,
'fid' => $fid,
'remark' => $remark,
'status' => 0,
]);
$this->success([], '好友请求已发送');
}
/**
* 处理好友请求(接受/拒绝)
* post: { id, action } action=1 接受, action=2 拒绝
*/
public function save()
{
$uid = $this->currentUid();
$id = (int) Request::post('id');
$action = (int) Request::post('action', 1);
$row = FriendModel::where('id', $id)->where('fid', $uid)->where('status', 0)->find();
if (! $row) {
$this->error('请求不存在或已处理');
}
if ($action == 2) {
$row->delete();
$this->success([], '已拒绝');
}
$row->status = 1;
$row->save();
$this->success([], '已添加为好友');
}
/**
* 好友详情
*/
public function read()
{
$uid = $this->currentUid();
$id = (int) Request::post('id');
$row = FriendModel::where('id', $id)->where('uid', $uid)->find();
if (! $row) {
$this->error('好友不存在');
}
$u = Db::name('member')->where('uid', $row->fid)
->field('uid,nickname,avatar,phone')->find();
if ($u) {
$u['friend_id'] = $row->id;
$u['remark'] = $row->remark;
}
$this->success($u);
}
/**
* 修改备注
* post: { id, remark }
*/
public function update()
{
$uid = $this->currentUid();
$id = (int) Request::post('id');
$row = FriendModel::where('id', $id)->where('uid', $uid)->find();
if (! $row) {
$this->error('好友不存在');
}
$row->remark = Request::post('remark', '');
$row->save();
$this->success([], '备注已更新');
}
/**
* 删除好友
*/
public function delete()
{
$uid = $this->currentUid();
$id = (int) Request::post('id');
$row = FriendModel::where('id', $id)->where('uid', $uid)->find();
if (! $row) {
$this->error('好友不存在');
}
$row->delete();
$this->success([], '好友已删除');
}
/**
* 取当前登录用户 uid
*/
private function currentUid() : int
{
return (int) ($this->auth->model->uid ?? $this->auth->uid ?? 0);
}
}
+113
View File
@@ -0,0 +1,113 @@
<?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\WxchatGift;
use addon\wxchat\model\WxchatGiftRecord;
use think\facade\Db;
use think\facade\Request;
use ywxapp\controller\FrontendBase;
use ywxapp\service\WalletService;
/**
* Gift 类
*
* @author ywxapp <admin@ywxapp.cn>
*/
class Gift extends FrontendBase
{
protected $noNeedLogin = [];
protected $noNeedVerify = ['*'];
protected function initialize()
{
if ($this->auth->isLogin) {
$this->uid = $this->auth->model->uid;
}
}
private $uid = 0;
/**
* 礼物商城列表
*/
public function index()
{
$list = WxchatGift::where('status', 1)
->order('sort', 'asc')
->field('id,name,icon,price')
->select();
$this->success($list);
}
/**
* 赠送礼物(扣金币 + 记录)
*/
public function send()
{
$uid = $this->uid;
$giftId = (int) Request::post('gift_id', 0);
$toUid = (int) Request::post('to_uid', 0);
$count = max(1, (int) Request::post('count', 1));
$msg = (string) Request::post('message', '');
if (! $toUid || $toUid == $uid) {
$this->error('接收者无效');
}
$gift = WxchatGift::where('id', $giftId)->where('status', 1)->find();
if (! $gift) {
$this->error('礼物不存在');
}
$receiver = Db::name('member')->where('uid', $toUid)->value('uid');
if (! $receiver) {
$this->error('接收用户不存在');
}
$total = (int) $gift->price * $count;
try {
WalletService::decCoins($uid, $total, "赠送礼物:{$gift->name}x{$count}");
} catch (\Throwable $e) {
$this->error($e->getMessage());
}
$record = new WxchatGiftRecord();
$record->sender_uid = $uid;
$record->receiver_uid = $toUid;
$record->gift_id = $gift->id;
$record->gift_name = $gift->name;
$record->price = $gift->price;
$record->count = $count;
$record->total_price = $total;
$record->message = $msg;
$record->save();
$this->success([
'gift' => $gift->name,
'count' => $count,
'cost' => $total,
'coins_left'=> WalletService::getCoins($uid),
], '赠送成功');
}
/**
* 我收到的礼物
*/
public function received()
{
$uid = $this->uid;
$list = WxchatGiftRecord::where('receiver_uid', $uid)
->order('create_at', 'desc')
->limit(50)
->select();
$this->success($list);
}
}
+268
View File
@@ -0,0 +1,268 @@
<?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 think\facade\View;
use ywxapp\controller\FrontendBase as AddonController;
/**
* Index 类
*
* @author ywxapp <admin@ywxapp.cn>
*/
class Index extends AddonController
{
/**
* Summary of noNeedLogin
* @var array
*/
protected $noNeedLogin = ['*'];
/**
* Summary of noNeedVerify
* @var array
*/
protected $noNeedVerify = ['*'];
/**
* 控制器初始化 initialize
* @return void
*/
protected function initialize()
{}
public function index()
{
return View::fetch();
event('user.UserLogin', ['id' => 123]);
return '您好!这是一个[wxchat]示例应用';
}
/**
* 应用配置接口
* @return array
*/
public function appConf()
{
$defaults = [
'chatEnabled' => true, //聊天功能启用:布尔值
'videoCallEnabled' => true, //视频通话功能启用:布尔值
'voiceRoomEnabled' => true, //语音聊天室功能启用:布尔值
'giftShopEnabled' => true, //礼物商店功能启用:布尔值
'rechargeEnabled' => true, //充值功能启用:布尔值
'vipEnabled' => true, //VIP功能启用:布尔值
'matchEnabled' => true, //匹配功能启用:布尔值
'momentEnabled' => true, //动态功能启用:布尔值
// 业务限制配置
'dailyMatchLimit' => 10, //每日匹配次数限制:数字
'dailyMessageLimit' => 100, //每日消息发送次数限制:数字
'maxFriends' => 100, //好友数量上限:数字
'maxMomentImages' => 9, //动态图片数量上限:数字
'maxChatImageSize' => 1024, //聊天图片大小上限:数字
'maxVideoDuration' => 60, //视频时长上限:数字
'maxVoiceDuration' => 60, //音频时长上限:数字
// 广告配置
'enableAd' => true, //启用
'splashAdId' => 'splash_1', //启动广告ID:字符串
'bannerAdId' => 'banner_1', //横幅广告ID:字符串
'interstitialAdId' => 'interstitial_1', //间隙广告ID:字符串
'rewardAdId' => 'reward_1', //奖励广告ID:字符串
'adFrequency' => 10, //广告频率:数字
// 服务器配置
'socketUrl' => 'wss://dev.xixingwl.cn:2346',
'uploadUrl' => 'https://dev.xixingwl.cn:',
'cdnUrl' => 'https://dev.xixingwl.cn',
'apiUrl' => 'https://dev.xixingwl.cn',
'apiVersion' => 'v1',
// 第三方服务配置
'wechatAppId' => 'wechat_1', //微信AppId:字符串
'qqAppId' => 'qq_1', //qq AppId:字符串
'appleServiceId' => 'apple_1', //苹果 服务ID:字符串
'pushService' => 'default', //推送服务:字符串
'mapService' => 'amap', //地图服务
// 客服配置
//customerService ?: ICustomerService
// 版本兼容性
'minIosVersion' => '10.0', //最小iOS版本:字符串
'minAndroidVersion' => '5.0', //最小Android版本:字符串
'supportedPlatforms' => ['ios', 'android'], //支持平台:字符串数组(保持代码默认,不纳入后台配置)
];
// 统一从数据库配置表读取(后台"插件管理 → 配置"可改),覆盖默认值
$saved = \ywxapp\service\AddonService::config('wxchat');
$data = $defaults;
foreach ($defaults as $k => $dv) {
if (!array_key_exists($k, $saved)) {
continue;
}
$val = $saved[$k];
if (is_bool($dv)) {
$data[$k] = in_array($val, [true, 'true', '1', 1], true);
} elseif (is_int($dv)) {
$data[$k] = (int) $val;
} elseif (is_array($dv)) {
$data[$k] = is_array($val) ? $val : $dv;
} else {
$data[$k] = $val;
}
}
$this->success($data);
}
public function launchData()
{
$data = [
// 启动页广告
'splashAds' => [[
'id' => 1,
'image' => 'https://dev.xixingwl.cn:2346/splash.jpg',
'title' => '启动页广告',
'link' => 'https://dev.xixingwl.cn:2346',
'type' => 'url',
'order' => 1,
], [
'id' => 2,
'image' => 'https://dev.xixingwl.cn:2346/splash.jpg',
'title' => '启动页广告2',
'link' => 'https://dev.xixingwl.cn:2346',
'type' => 'url',
'order' => 2,
], [
'id' => 3,
'image' => 'https://dev.xixingwl.cn:2346/splash.jpg',
'title' => '启动页广告3',
'link' => 'https://dev.xixingwl.cn:2346',
'type' => 'url',
'order' => 3,
]],
// 首页轮播图
'banners' => [[
'id' => 1,
'image' => 'https://dev.xixingwl.cn:2346/banner.jpg',
'title' => '首页轮播图1',
'link' => 'https://dev.xixingwl.cn:2346',
'type' => 'url',
'order' => 1,
], [
'id' => 2,
'image' => 'https://dev.xixingwl.cn:2346/banner.jpg',
'title' => '首页轮播图2',
'link' => 'https://dev.xixingwl.cn:2346',
'type' => 'url',
'order' => 2,
], [
'id' => 3,
'image' => 'https://dev.xixingwl.cn:2346/banner.jpg',
'title' => '首页轮播图3',
'link' => 'https://dev.xixingwl.cn:2346',
'type' => 'url',
'order' => 3,
]],
// 系统公告
// 'announcements' => IAnnouncement[]
// 运营活动
'activities' => [[
'id' => 1,
'title' => '运营活动1',
'description' => '运营活动1描述',
'icon' => 'https://dev.xixingwl.cn:2346/activity1.png',
'badge' => 'new',
'link' => 'https://dev.xixingwl.cn:2346/activity1',
'startTime' => 1609459200,
'endTime' => 1609545600,
'status' => 'ongoing',
], [
'id' => 2,
'title' => '运营活动2',
'description' => '运营活动2描述',
'icon' => 'https://dev.xixingwl.cn:2346/activity2.png',
'badge' => 'hot',
'link' => 'https://dev.xixingwl.cn:2346/activity2',
'startTime' => 1609459200,
'endTime' => 1609545600,
'status' => 'upcoming',
], [
'id' => 3,
'title' => '运营活动3',
'description' => '运营活动3描述',
'icon' => 'https://dev.xixingwl.cn:2346/activity3.png',
'badge' => '',
'link' => 'https://dev.xixingwl.cn:2346/activity3',
'startTime' => 1609459200,
'endTime' => 1609545600,
'status' => 'ended',
]],
// 快捷入口
'quickActions' => [[
'id' => 1,
'name' => '快捷入口1',
'icon' => 'https://dev.xixingwl.cn:2346/quick_action1.png',
'color' => '#ff0000',
'route' => '/quick_action1',
'badge' => 5,
'visible' => true,
], [
'id' => 2,
'name' => '快捷入口2',
'icon' => 'https://dev.xixingwl.cn:2346/quick_action2.png',
'color' => '#00ff00',
'route' => '/quick_action2',
'badge' => 10,
'visible' => true,
], [
'id' => 3,
'name' => '快捷入口3',
'icon' => 'https://dev.xixingwl.cn:2346/quick_action3.png',
'color' => '#0000ff',
'route' => '/quick_action3',
'badge' => 15,
'visible' => true,
]],
// 新手引导
'newbieGuide' => [
'enabled' => true,
'steps' => [
[
'id' => 1,
'title' => '新手引导步骤1',
'content' => '这是新手引导步骤1的内容',
'image' => 'https://dev.xixingwl.cn:2346/newbie_guide1.png',
'action' => 'next',
], [
'id' => 2,
'title' => '新手引导步骤2',
'content' => '这是新手引导步骤2的内容',
'image' => 'https://dev.xixingwl.cn:2346/newbie_guide2.png',
'action' => 'prev',
], [
'id' => 3,
'title' => '新手引导步骤3',
'content' => '这是新手引导步骤3的内容',
'image' => 'https://dev.xixingwl.cn:2346/newbie_guide3.png',
'action' => 'finish',
],
],
]];
$this->success($data);
}
public function baseData()
{
$this->error();
$data = [];
$this->success($data);
}
}
+26
View File
@@ -0,0 +1,26 @@
<?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 ywxapp\controller\FrontendBase as AddonController;
/**
* Indexi 类
*
* @author ywxapp <admin@ywxapp.cn>
*/
class Indexi extends AddonController
{
public function index()
{
return $this->fetch() ;
}
}
+398
View File
@@ -0,0 +1,398 @@
<?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 think\facade\Request;
use ywxapp\controller\FrontendBase;
use ywxapp\utils\HttpClient;
use think\facade\Db;
use think\facade\Cache;
use ywxapp\service\JwtService;
/**
* Login 类
*
* @author ywxapp <admin@ywxapp.cn>
*/
class Login extends FrontendBase
{
/**
* Summary of noNeedLogin
* @var array
*/
protected $noNeedLogin = ['*'];
/**
* Summary of noNeedVerify
* @var array
*/
protected $noNeedVerify = ['*'];
/**
* 控制器初始化 initialize
* @return void
*/
protected function initialize()
{}
public function index()
{
echo "test";
}
// 短信验证码登录(验证码校验通过后,查找或自动注册用户,并签发 JWT)
public function smsLogin()
{
try {
$mobile = $this->request->post("mobile");
$event = $this->request->post("event", 'login');
$captcha = $this->request->post("captcha");
$data = ['mobile' => $mobile, 'captcha' => $captcha, 'event' => $event];
validate(\addon\wxchat\validate\Login::class)
->scene('sms')
->check($data);
$ret = app('sms')->check($mobile, $captcha, $event);
if (! $ret) {
$this->error("验证码不正确!");
}
$user = Db::name('member')->where('mobile', $mobile)->where('status', '>=', 0)->find();
if (empty($user)) {
$uid = Db::name('member')->insertGetId([
'account' => $mobile,
'mobile' => $mobile,
'nickname' => $this->generateRandomNickname(),
'password' => '',
'status' => 1,
'create_at' => time(),
'create_ip' => Request::ip(),
'update_at' => time(),
'update_ip' => Request::ip(),
]);
$user = Db::name('member')->find($uid);
} else {
Db::name('member')->where('uid', $user['uid'])->update([
'update_at' => time(),
'update_ip' => Request::ip(),
]);
}
$this->issueToken((int) $user['uid']);
$this->success([
'user_info' => $this->formatUserInfo($user),
'is_registered' => true,
], '登录成功');
} catch (\think\exception\ValidateException $e) {
$this->error($e->getMessage(), 400);
} catch (\Exception $e) {
$this->error('系统错误:' . $e->getMessage(), 500);
}
}
// 一键登录(本机号码):用预下发的一次性 pre_token 换取登录态
// 流程:客户端先调用 getOneClickPreToken(phone) 拿到 pre_token(模拟运营商 SDK 下发的本机号码凭证),
// 再携带 pre_token 调用本接口,服务端校验后签发 JWT。
public function oneClickLogin()
{
try {
$preToken = $this->request->post('pre_token');
if (empty($preToken)) {
return json(['code' => 400, 'msg' => '缺少一键登录凭证', 'data' => null]);
}
$phone = Cache::get('wxchat_oneclick_' . $preToken);
if (empty($phone)) {
return json(['code' => 400, 'msg' => '一键登录凭证已失效,请重试', 'data' => null]);
}
Cache::delete('wxchat_oneclick_' . $preToken);
$user = Db::name('member')->where('mobile', $phone)->where('status', '>=', 0)->find();
if (empty($user)) {
$uid = Db::name('member')->insertGetId([
'account' => $phone,
'mobile' => $phone,
'nickname' => $this->generateRandomNickname(),
'password' => '',
'status' => 1,
'create_at' => time(),
'create_ip' => Request::ip(),
'update_at' => time(),
'update_ip' => Request::ip(),
]);
$user = Db::name('member')->find($uid);
} else {
Db::name('member')->where('uid', $user['uid'])->update([
'update_at' => time(),
'update_ip' => Request::ip(),
]);
}
$this->issueToken((int) $user['uid']);
$this->success([
'user_info' => $this->formatUserInfo($user),
'is_registered' => true,
], '登录成功');
} catch (\think\exception\ValidateException $e) {
return json(['code' => 400, 'msg' => $e->getMessage(), 'data' => null]);
} catch (\Exception $e) {
return json(['code' => 500, 'msg' => '系统错误:' . $e->getMessage(), 'data' => null]);
}
}
// 完善个人信息(注册)。仅写入 user 表真实存在的字段。
public function completeUserInfo()
{
try {
$data = Request::only([
'temp_token', 'nickname', 'avatar',
], 'post');
// 验证临时token
$tempData = Cache::get('temp_login_' . ($data['temp_token'] ?? ''));
if (! $tempData || ($tempData['expire_time'] ?? 0) < time()) {
return json(['code' => 401, 'msg' => '临时凭证已过期,请重新登录', 'data' => null]);
}
if (empty($tempData['verified'])) {
return json(['code' => 403, 'msg' => '手机号未验证', 'data' => null]);
}
$phone = $tempData['phone'];
// 检查手机号是否已被注册
$exists = Db::name('member')
->where('mobile', $phone)
->where('status', 1)
->find();
if ($exists) {
return json(['code' => 400, 'msg' => '该手机号已注册', 'data' => null]);
}
// 处理头像上传
$avatarPath = '';
if (! empty($data['avatar'])) {
$avatarPath = $this->handleAvatarUpload($data['avatar'], $phone);
}
$nickname = ! empty($data['nickname']) ? $data['nickname'] : $this->generateRandomNickname();
$userData = [
'account' => $phone,
'mobile' => $phone,
'nickname' => $nickname,
'avatar' => $avatarPath,
'password' => '',
'status' => 1,
'create_at' => time(),
'create_ip' => Request::ip(),
'update_at' => time(),
'update_ip' => Request::ip(),
];
$userId = Db::name('member')->insertGetId($userData);
if (! $userId) {
throw new \Exception('用户注册失败');
}
Cache::delete('temp_login_' . ($data['temp_token'] ?? ''));
$this->issueToken($userId);
$this->logUserRegister($userId, $userData);
$user = Db::name('member')->find($userId);
$this->success([
'user_info' => $this->formatUserInfo($user),
'is_registered' => true,
], '注册成功');
} catch (\think\exception\ValidateException $e) {
return json(['code' => 400, 'msg' => $e->getMessage(), 'data' => null]);
} catch (\Exception $e) {
return json(['code' => 500, 'msg' => '系统错误:' . $e->getMessage(), 'data' => null]);
}
}
// 刷新 Access Token(无感续期):校验 refresh_token,重签 access 并轮转 refresh
public function refresh()
{
try {
$refreshToken = $this->request->post('refresh_token');
if (empty($refreshToken)) {
$this->error('缺少刷新令牌', 400);
}
$jwt = JwtService::instance();
$token = $jwt->parseRefreshToken($refreshToken);
$claims = $token->claims()->all();
$uid = (int) ($claims['uid'] ?? 0);
if ($uid <= 0) {
$this->error('刷新令牌无效', 400);
}
// 重新签发 access token,并轮转 refresh token
$jwt->createAccessToken(['uid' => $uid]);
$jwt->createRefreshToken(['uid' => $uid]);
$this->success([], '刷新成功');
} catch (\Exception $e) {
$this->error('刷新令牌已失效,请重新登录', 401);
}
}
// 下发一键登录预凭证(模拟运营商 SDK 下发的本机号码凭证)
public function getOneClickPreToken()
{
try {
$phone = $this->request->post('phone');
if (! preg_match('/^1[3-9]\d{9}$/', (string) $phone)) {
$this->error('手机号格式错误', 400);
}
$preToken = md5(uniqid((string) microtime(true), true) . $phone . time());
Cache::set('wxchat_oneclick_' . $preToken, $phone, 300);
$this->success(['pre_token' => $preToken]);
} catch (\Exception $e) {
$this->error('系统错误:' . $e->getMessage(), 500);
}
}
// 发送短信验证码
public function sendSmsCode()
{
try {
$phone = Request::param('phone');
// 验证手机号格式
if (! preg_match('/^1[3-9]\d{9}$/', $phone)) {
return json(['code' => 400, 'msg' => '手机号格式错误', 'data' => null]);
}
// 检查发送频率
$cacheKey = 'sms_limit_' . $phone;
$lastSendTime = Cache::get($cacheKey);
if ($lastSendTime && time() - $lastSendTime < 60) {
return json(['code' => 400, 'msg' => '请稍后再试', 'data' => null]);
}
// 生成6位随机验证码
$code = str_pad(random_int(0, 999999), 6, '0', STR_PAD_LEFT);
// 调用短信服务
$smsService = new SmsService();
$sendResult = $smsService->sendLoginCode($phone, $code);
if ($sendResult) {
// 记录发送时间
Cache::set($cacheKey, time(), 60);
return json([
'code' => 200,
'msg' => '验证码发送成功',
'data' => [
'expire_time' => 300, // 5分钟有效期
],
]);
} else {
return json(['code' => 500, 'msg' => '验证码发送失败', 'data' => null]);
}
} catch (\Exception $e) {
return json(['code' => 500, 'msg' => '系统错误:' . $e->getMessage(), 'data' => null]);
}
}
/**
* 签发 JWTaccess + refresh),借助 JwtService 内部写入 app()->result
* 最终由 $this->success() 在响应顶层输出 access_token/refresh_token/...
*/
private function issueToken(int $uid) : void
{
JwtService::instance()->createToken(['uid' => $uid]);
}
/**
* 统一输出用户信息(对齐客户端 IUserInfo 字段)
*/
private function formatUserInfo(array $user) : array
{
return [
'uid' => (int) ($user['uid'] ?? 0),
'account' => $user['account'] ?? '',
'nickname' => $user['nickname'] ?? '',
'avatar' => $user['avatar'] ?? '',
'mobile' => $user['mobile'] ?? '',
'status' => (int) ($user['status'] ?? 1),
'create_time' => $user['create_at'] ?? 0,
];
}
// 处理头像上传
private function handleAvatarUpload($base64Image, $phone)
{
try {
if (strpos($base64Image, 'data:image') === 0) {
// Base64图片
$imageData = base64_decode(preg_replace('#^data:image/\w+;base64,#i', '', $base64Image));
// 生成文件名
$fileName = 'avatar_' . $phone . '_' . time() . '.jpg';
$filePath = 'uploads/avatar/' . date('Ymd') . '/' . $fileName;
// 保存文件
$savePath = public_path() . $filePath;
$dir = dirname($savePath);
if (! is_dir($dir)) {
mkdir($dir, 0755, true);
}
file_put_contents($savePath, $imageData);
return $filePath;
} else {
// URL或其他格式
return $base64Image;
}
} catch (\Exception $e) {
// 头像上传失败不影响注册
return '';
}
}
// 生成随机昵称
private function generateRandomNickname()
{
$adjectives = ['快乐的', '聪明的', '勇敢的', '温柔的', '活泼的', '安静的', '热情的', '冷静的'];
$nouns = ['小猫', '小狗', '小鸟', '小鱼', '小虎', '小兔', '小熊', '小鹿'];
$numbers = ['123', '456', '789', '007', '888', '999'];
$adj = $adjectives[array_rand($adjectives)];
$noun = $nouns[array_rand($nouns)];
$num = $numbers[array_rand($numbers)];
return $adj . $noun . $num;
}
// 记录用户注册日志
private function logUserRegister($userId, $userData)
{
$logData = [
'user_id' => $userId,
'mobile' => $userData['mobile'] ?? '',
'register_time' => date('Y-m-d H:i:s'),
'register_ip' => Request::ip(),
'invite_user_id' => $userData['invite_user_id'] ?? 0,
'create_at' => time(),
];
try {
Db::name('user_register_log')->insert($logData);
} catch (\Exception $e) {
// 注册日志表可能未初始化,不影响主流程
}
}
}
+139
View File
@@ -0,0 +1,139 @@
<?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\WxchatLotteryPrize;
use addon\wxchat\model\WxchatLotteryRecord;
use think\facade\Db;
use think\facade\Request;
use ywxapp\controller\FrontendBase;
use ywxapp\service\WalletService;
/**
* Lottery 类
*
* @author ywxapp <admin@ywxapp.cn>
*/
class Lottery extends FrontendBase
{
protected $noNeedLogin = [];
protected $noNeedVerify = ['*'];
// 每次抽奖消耗金币
protected $cost = 10;
// 每日免费次数
protected $freeDaily = 1;
protected function initialize()
{
if ($this->auth->isLogin) {
$this->uid = $this->auth->model->uid;
}
}
private $uid = 0;
/**
* 抽奖配置(奖品列表 + 我的金币 + 剩余免费次数)
*/
public function config()
{
$uid = $this->uid;
$prizes = WxchatLotteryPrize::where('status', 1)
->order('sort', 'asc')
->field('id,name,type,value,probability')
->select();
$freeUsed = WxchatLotteryRecord::where('uid', $uid)
->where('is_free', 1)
->where('create_at', '>=', strtotime('today'))
->count();
$this->success([
'coins' => WalletService::getCoins($uid),
'cost' => $this->cost,
'free_daily' => $this->freeDaily,
'free_left' => max(0, $this->freeDaily - $freeUsed),
'prizes' => $prizes,
]);
}
/**
* 抽奖(金币或免费)
*/
public function draw()
{
$uid = $this->uid;
// 是否使用免费次数
$freeUsed = WxchatLotteryRecord::where('uid', $uid)
->where('is_free', 1)
->where('create_at', '>=', strtotime('today'))
->count();
$useFree = Request::post('use_free', 0) && $freeUsed < $this->freeDaily;
if (! $useFree) {
try {
WalletService::decCoins($uid, $this->cost, '抽奖消耗');
} catch (\Throwable $e) {
$this->error($e->getMessage());
}
}
$prize = $this->pickPrize();
$record = new WxchatLotteryRecord();
$record->uid = $uid;
$record->prize_id = $prize ? $prize->id : 0;
$record->prize_name = $prize ? $prize->name : '谢谢参与';
$record->prize_type = $prize ? $prize->type : 3;
$record->prize_value = $prize ? $prize->value : 0;
$record->is_free = $useFree ? 1 : 0;
$record->save();
// 金币奖品发放
if ($prize && $prize->type == 1 && $prize->value > 0) {
WalletService::incCoins($uid, (int) $prize->value, "抽奖中奖:{$prize->name}");
}
$this->success([
'prize' => $record->prize_name,
'prize_type' => $record->prize_type,
'prize_value'=> $record->prize_value,
'coins_left' => WalletService::getCoins($uid),
], '抽奖完成');
}
/**
* 按概率加权抽取奖品(无命中返回 null)
*/
private function pickPrize()
{
$prizes = WxchatLotteryPrize::where('status', 1)
->where('stock', '<>', 0)
->order('sort', 'asc')
->select();
if ($prizes->isEmpty()) {
return null;
}
$rand = mt_rand(1, 10000) / 10000; // 0-1
$acc = 0.0;
foreach ($prizes as $p) {
$acc += (float) $p->probability;
if ($rand <= $acc) {
// 扣库存
if ($p->stock > 0) {
Db::name('wxchat_lottery_prizes')->where('id', $p->id)->dec('stock')->update(['update_at' => time()]);
}
return $p;
}
}
return null;
}
}
+213
View File
@@ -0,0 +1,213 @@
<?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\WxchatMessage;
use addon\wxchat\model\WxchatUnreadCount;
use think\facade\Db;
use think\facade\Request;
use ywxapp\controller\FrontendBase;
use ywxapp\model\MemberUser as UserModel;
use ywxapp\model\MemberProfile as ProfileModel;
/**
* Message 类
*
* @author ywxapp <admin@ywxapp.cn>
*/
class Message extends FrontendBase
{
protected $noNeedLogin = [];
protected $noNeedVerify = ['*'];
protected function initialize()
{}
/**
* 发送消息(HTTP 落库,WebSocket 实时推送由 worker 处理)
*/
public function send()
{
$uid = $this->auth->model->uid;
$receiverId = (int) Request::post('receiver_id', 0);
$receiverType = (int) Request::post('receiver_type', 0); // 0-单聊 1-群聊
$content = trim((string) Request::post('content', ''));
$contentType = (int) Request::post('content_type', 0);
if (! $receiverId || $receiverId == $uid) {
$this->error('接收者无效');
}
if ($content === '' && $contentType == 0) {
$this->error('消息内容不能为空');
}
$msg = new WxchatMessage();
$msg->sender_id = $uid;
$msg->receiver_type = $receiverType;
$msg->receiver_id = $receiverId;
$msg->content = $content;
$msg->content_type = $contentType;
$msg->save();
// 更新对方未读计数(单聊)
if ($receiverType == 0) {
WxchatUnreadCount::increment($receiverId, 0, $uid);
}
$this->success($msg->toArray(), '发送成功');
}
/**
* 会话历史(单聊 / 群聊)
*/
public function history()
{
$uid = $this->auth->uid;
$contactId = (int) Request::get('contact_id', 0);
$contactType = (int) Request::get('contact_type', 0);
$limit = (int) Request::get('limit', 20);
$page = (int) Request::get('page', 1);
if (! $contactId) {
$this->error('会话对象无效');
}
$query = WxchatMessage::where('is_recalled', 0)
->where('receiver_type', $contactType);
if ($contactType == 0) {
// 单聊:取双方互发消息(修复原逻辑因外层 receiver_id 过滤而漏掉对方发来的消息)
$query->where(function ($q) use ($uid, $contactId) {
$q->where(function ($q) use ($uid, $contactId) {
$q->where('sender_id', $uid)->where('receiver_id', $contactId);
})->whereOr(function ($q) use ($uid, $contactId) {
$q->where('sender_id', $contactId)->where('receiver_id', $uid);
});
});
} else {
$query->where('receiver_id', $contactId);
}
$list = $query->order('create_at', 'desc')
->paginate($limit, false, ['page' => $page]);
// 单聊:读取后清零未读
if ($contactType == 0) {
WxchatUnreadCount::reset($uid, 0, $contactId);
}
$this->success([
'list' => $list->items(),
'total' => $list->total(),
'pages' => $list->lastPage(),
]);
}
/**
* 未读消息列表
*/
public function unread()
{
$uid = $this->auth->model->uid;
$list = WxchatUnreadCount::where('user_id', $uid)
->where('unread_count', '>', 0)
->order('update_at', 'desc')
->select();
$this->success($list);
}
/**
* 会话列表(聚合单聊最新一条消息 + 未读 + 对方资料/在线状态)
*/
public function conversations()
{
$uid = $this->auth->model->uid;
$msgs = WxchatMessage::where('receiver_type', 0)
->where('is_recalled', 0)
->where(function ($q) use ($uid) {
$q->where('sender_id', $uid)->whereOr('receiver_id', $uid);
})
->order('id', 'desc')
->limit(1000)
->select();
$convMap = [];
$peerIds = [];
foreach ($msgs as $m) {
$sid = (int) $m->sender_id;
$rid = (int) $m->receiver_id;
$peer = ($sid == $uid) ? $rid : $sid;
if (! isset($convMap[$peer])) {
$convMap[$peer] = [
'peer_id' => $peer,
'last_content' => $m->content,
'last_content_type' => (int) $m->content_type,
'last_time' => (int) $m->create_at,
'last_sender_id' => $sid,
];
$peerIds[] = $peer;
}
}
if (empty($peerIds)) {
$this->success(['list' => [], 'total' => 0]);
return;
}
$users = UserModel::where('uid', 'in', $peerIds)->column('*', 'uid');
$unread = WxchatUnreadCount::where('user_id', $uid)
->where('contact_type', 0)
->where('contact_id', 'in', $peerIds)
->column('unread_count', 'contact_id');
$onlineMap = MemberProfile::where('uid', 'in', $peerIds)->column('online_status', 'uid');
$list = [];
foreach ($convMap as $peer => $c) {
$u = $users[$peer] ?? [];
$list[] = [
'peer_id' => $peer,
'nickname' => $u['nickname'] ?? '',
'avatar' => $u['avatar'] ?? '',
'online' => ((int) ($onlineMap[$peer] ?? 0)) == 1,
'last_content' => $c['last_content'],
'last_content_type' => $c['last_content_type'],
'last_time' => $c['last_time'],
'unread_count' => (int) ($unread[$peer] ?? 0),
'is_self' => $c['last_sender_id'] == $uid,
];
}
usort($list, function ($a, $b) {
return $b['last_time'] - $a['last_time'];
});
$this->success(['list' => $list, 'total' => count($list)]);
}
/**
* 撤回消息
*/
public function recall()
{
$userId = $this->auth->model->uid;
$messageId = (int) Request::post('message_id', 0);
$message = WxchatMessage::find($messageId);
if (! $message || $message->sender_id != $userId) {
$this->error('无权撤回', 403);
}
$message->is_recalled = 1;
$message->recalled_at = date('Y-m-d H:i:s');
$message->save();
$this->success([], '撤回成功');
}
}
File diff suppressed because it is too large Load Diff
+55
View File
@@ -0,0 +1,55 @@
<?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 ywxapp\controller\FrontendBase;
/**
* Recommend 类
*
* @author ywxapp <admin@ywxapp.cn>
*/
class Recommend extends FrontendBase
{
/**
* Summary of noNeedLogin
* @var array
*/
protected $noNeedLogin = ['*'];
/**
* Summary of noNeedVerify
* @var array
*/
protected $noNeedVerify = ['*'];
/**
* 控制器初始化 initialize
* @return void
*/
protected function initialize()
{}
/**
* 显示资源列表
*
* @return \think\Response
*/
public function index()
{
if ($this->auth->isLogin) {
$this->success();
}else{
$this->error("用户未登录!");
}
}
}
+74
View File
@@ -0,0 +1,74 @@
<?php
// +----------------------------------------------------------------------
// | YwxApp [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Author: ywxapp<admin@ywxapp.cn>
// +----------------------------------------------------------------------
namespace addon\wxchat\controller\api;
// app/controller/Search.php
use app\model\Message;
use think\Request;
use ywxapp\controller\FrontendBase as AddonController;
/**
* Search 类
*
* @author ywxapp <admin@ywxapp.cn>
*/
class Search extends AddonController
{
/**
* Summary of noNeedLogin
* @var array
*/
protected $noNeedLogin = ['*'];
/**
* Summary of noNeedVerify
* @var array
*/
protected $noNeedVerify = ['*'];
/**
* 控制器初始化 initialize
* @return void
*/
protected function initialize()
{}
public function index(Request $request)
{
$userId = $request->user->id;
$keyword = $request->get('keyword');
$contactId = $request->get('contact_id');
$isGroup = $request->get('is_group', 0);
$query = Message::where(function($q) use ($userId, $contactId, $isGroup) {
if ($isGroup) {
$q->where('receiver_type', 1)->where('receiver_id', $contactId);
} else {
$q->where(function($q) use ($userId, $contactId) {
$q->where('sender_id', $contactId)->where('receiver_id', $userId);
})->whereOr(function($q) use ($userId, $contactId) {
$q->where('sender_id', $userId)->where('receiver_id', $contactId);
});
}
});
// 全文搜索(MySQL 5.7+
if (extension_loaded('mysqlnd')) {
$query->whereRaw("MATCH(content) AGAINST(?)", [$keyword]);
} else {
$query->where('content', 'like', "%{$keyword}%");
}
$messages = $query->order('create_at', 'desc')->limit(20)->select();
return json(['code' => 200, 'data' => $messages]);
}
}
+207
View File
@@ -0,0 +1,207 @@
<?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\WxchatTaskRecord;
use think\facade\Db;
use think\facade\Request;
use ywxapp\controller\FrontendBase;
use ywxapp\service\WalletService;
/**
* Task 类
*
* @author ywxapp <admin@ywxapp.cn>
*/
class Task extends FrontendBase
{
protected $noNeedLogin = [];
protected $noNeedVerify = ['*'];
protected function initialize()
{
if ($this->auth->isLogin) {
$this->uid = $this->auth->model->uid;
}
}
private $uid = 0;
/**
* 每日任务定义(硬编码;进度基于真实统计,领取记录防重复)
* status: 0 进行中 / 1 可领取 / 2 今日已领取
*/
private function definitions() : array
{
return [
['id' => 1, 'key' => 'login', 'title' => '每日登录', 'desc' => '每天登录 App 领取奖励', 'reward' => 5, 'target' => 1, 'icon' => 'calendar'],
['id' => 2, 'key' => 'publish', 'title' => '发布动态', 'desc' => '今日发布 1 条动态', 'reward' => 10, 'target' => 1, 'icon' => 'camera'],
['id' => 3, 'key' => 'like', 'title' => '点赞互动', 'desc' => '今日点赞 3 条动态', 'reward' => 8, 'target' => 3, 'icon' => 'heart'],
['id' => 4, 'key' => 'share', 'title' => '分享动态', 'desc' => '今日分享 1 条动态', 'reward' => 6, 'target' => 1, 'icon' => 'share'],
['id' => 5, 'key' => 'profile', 'title' => '完善资料', 'desc' => '完善个人交友资料', 'reward' => 20, 'target' => 1, 'icon' => 'person'],
];
}
/**
* 任务列表(含真实进度 + 领取状态)
*/
public function index()
{
$uid = $this->uid;
$today = strtotime('today');
$tasks = $this->definitions();
$result = [];
foreach ($tasks as $t) {
$current = $this->calcProgress($t['key'], $uid);
$done = $current >= $t['target'];
$received = WxchatTaskRecord::where('uid', $uid)
->where('task_id', $t['id'])
->where('status', 2)
->where('update_at', '>=', $today)
->count();
$status = $received > 0 ? 2 : ($done ? 1 : 0);
$result[] = [
'id' => $t['id'],
'title' => $t['title'],
'desc' => $t['desc'],
'icon' => $t['icon'],
'reward' => $t['reward'],
'target' => $t['target'],
'current' => min($current, $t['target']),
'status' => $status,
];
}
$this->success([
'list' => $result,
'coins' => WalletService::getCoins($uid),
]);
}
/**
* 领取任务奖励(金币发放,每日每任务一次)
*/
public function receive()
{
$uid = $this->uid;
$taskId = (int) Request::post('task_id');
$task = null;
foreach ($this->definitions() as $t) {
if ($t['id'] == $taskId) {
$task = $t;
break;
}
}
if (! $task) {
$this->error('任务不存在');
}
// 进度校验
$current = $this->calcProgress($task['key'], $uid);
if ($current < $task['target']) {
$this->error('任务未完成');
}
// 今日是否已领取
$today = strtotime('today');
$got = WxchatTaskRecord::where('uid', $uid)
->where('task_id', $taskId)
->where('status', 2)
->where('update_at', '>=', $today)
->count();
if ($got > 0) {
$this->error('今日已领取');
}
// 发放金币
WalletService::incCoins($uid, (int) $task['reward'], "任务奖励:{$task['title']}");
// 写领取记录;利用 UNIQUE(uid,task_id) 实现每日重置(次日触发冲突后更新)
try {
(new WxchatTaskRecord())->save([
'uid' => $uid,
'task_id' => $taskId,
'progress' => $task['target'],
'status' => 2,
'reward_coins' => $task['reward'],
'create_at' => time(),
'update_at' => time(),
]);
} catch (\Throwable $e) {
WxchatTaskRecord::where('uid', $uid)
->where('task_id', $taskId)
->update([
'progress' => $task['target'],
'status' => 2,
'reward_coins' => $task['reward'],
'update_at' => time(),
]);
}
$this->success([
'coins_left' => WalletService::getCoins($uid),
'reward' => $task['reward'],
], '领取成功');
}
/**
* 计算任务当前进度(基于真实业务统计)
*/
private function calcProgress(string $key, int $uid) : int
{
$today = strtotime('today');
try {
switch ($key) {
case 'login':
return 1;
case 'publish':
return (int) Db::name('wxchat_moments')
->where('user_id', $uid)
->where('create_at', '>=', $today)
->count();
case 'like':
return (int) Db::name('wxchat_moment_likes')
->where('user_id', $uid)
->where('create_at', '>=', $today)
->count();
case 'share':
return (int) Db::name('wxchat_moment_shares')
->where('user_id', $uid)
->where('create_at', '>=', $today)
->count();
case 'profile':
$user = Db::name('member')->where('uid', $uid)->field('nickname,avatar')->find();
$profile = Db::name('member_profile')->where('uid', $uid)
->field('bio,birthday,gender')->find();
$score = 0;
if (! empty($user['nickname'])) {
$score++;
}
if (! empty($user['avatar'])) {
$score++;
}
if (! empty($profile['bio'])) {
$score++;
}
if (! empty($profile['birthday'])) {
$score++;
}
if (! empty($profile['gender'])) {
$score++;
}
return $score >= 4 ? 1 : 0;
}
} catch (\Throwable $e) {
return 0;
}
return 0;
}
}
+123
View File
@@ -0,0 +1,123 @@
<?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\worker\Header;
use addon\wxchat\worker\Message;
use addon\wxchat\worker\Payload;
use think\Request;
use ywxapp\controller\FrontendBase as AddonController;
/**
* Test 类
*
* @author ywxapp <admin@ywxapp.cn>
*/
class Test extends AddonController
{
/**
* 显示资源列表
*
* @return \think\Response
*/
public function index()
{
// ===== 序列化:对象 → JSON =====
$message = new Message() /* 通过构造函数或 fromArray 创建 */;
$message->header->to = ('22');
dump($message);
$jsonStr = json_encode($message, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
// 或直接:$array = $message->toArray();
dump($jsonStr);
dump('===== 反序列化:JSON → 对象 ===== ');
// ===== 反序列化:JSON → 对象 =====
$message = Message::fromJson($jsonStr); // 推荐:带异常处理
// 或:$message = Message::fromArray(json_decode($jsonInput, true));
dump($message);
dump('===== 反序列化:设置 → 对象 ===== ');
// ===== 反序列化:设置 → 对象 =====
$message->payload = new Payload();
$message->payload->name = "傻子";
$message->header->to = ('11');
dump($message);
dump(json_encode($message));
}
/**
* 显示创建资源表单页.
*
* @return \think\Response
*/
public function create()
{
//
}
/**
* 保存新建的资源
*
* @param \think\Request $request
* @return \think\Response
*/
public function save(Request $request)
{
//
}
/**
* 显示指定的资源
*
* @param int $id
* @return \think\Response
*/
public function read($id)
{
//
}
/**
* 显示编辑资源表单页.
*
* @param int $id
* @return \think\Response
*/
public function edit($id = null)
{
//
}
/**
* 保存更新的资源
*
* @param \think\Request $request
* @param int $id
* @return \think\Response
*/
public function update(Request $request, $id)
{
//
}
/**
* 删除指定资源
*
* @param int $id
* @return \think\Response
*/
public function delete($id)
{
//
}
}
+88
View File
@@ -0,0 +1,88 @@
<?php
// +----------------------------------------------------------------------
// | YwxApp [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Author: ywxapp<admin@ywxapp.cn>
// +----------------------------------------------------------------------
// app/controller/Unread.php
namespace addon\wxchat\controller\api;
use think\Request;
use think\facade\Cache;
use GatewayClient\Gateway;
use ywxapp\controller\FrontendBase as AddonController;
/**
* Unread 类
*
* @author ywxapp <admin@ywxapp.cn>
*/
class Unread extends AddonController
{
/**
* Summary of noNeedLogin
* @var array
*/
protected $noNeedLogin = ['*'];
/**
* Summary of noNeedVerify
* @var array
*/
protected $noNeedVerify = ['*'];
/**
* 控制器初始化 initialize
* @return void
*/
protected function initialize()
{}
// 获取未读计数
public function count(Request $request)
{
$userId = $request->user->id;
// 从Redis读取
$redis = new \Redis();
$redis->connect('127.0.0.1', 6379);
$unreadData = $redis->hGetAll("unread:user:{$userId}");
// 格式化数据
$result = [];
foreach ($unreadData as $key => $count) {
list($type, $id) = explode(':', $key);
$result[] = [
'contact_type' => $type,
'contact_id' => $id,
'unread_count' => $count
];
}
return json(['code' => 200, 'data' => $result]);
}
// 标记已读
public function read(Request $request)
{
$userId = $request->user->id;
$contactType = $request->post('contact_type');
$contactId = $request->post('contact_id');
// 通知GatewayWorker
Gateway::$registerAddress = '127.0.0.1:1238';
Gateway::sendToUid($userId, json_encode([
'type' => 'read',
'contact_type' => $contactType,
'contact_id' => $contactId
]));
return json(['code' => 200, 'msg' => '已标记已读']);
}
}
+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,
]);
}
}