初始化本地仓库
This commit is contained in:
@@ -0,0 +1,252 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\service\api\member;
|
||||
|
||||
use app\api\CurrentMember;
|
||||
use app\dao\member\MemberThirdPartyDao;
|
||||
use app\enum\member\ThirdPartyPlatform;
|
||||
use core\base\BaseService;
|
||||
use support\Container;
|
||||
use support\Redis;
|
||||
|
||||
/**
|
||||
* 会员绑定服务
|
||||
*/
|
||||
class MemberBindService extends BaseService
|
||||
{
|
||||
/**
|
||||
* 构造方法
|
||||
*/
|
||||
public function __construct(MemberThirdPartyDao $dao)
|
||||
{
|
||||
$this->dao = $dao;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取第三方绑定列表
|
||||
*/
|
||||
public function getThirdPartyList(): array
|
||||
{
|
||||
$currentMember = Container::make(CurrentMember::class);
|
||||
$memberId = $currentMember->id();
|
||||
if (empty($memberId)) {
|
||||
throw new \Exception('用户未登录', 401);
|
||||
}
|
||||
$thirdParties = $this->dao->getMemberThirdParties($memberId);
|
||||
$platforms = [
|
||||
[
|
||||
'type' => 'qq',
|
||||
'name' => 'QQ',
|
||||
'platform' => ThirdPartyPlatform::QQ->value,
|
||||
'binded' => false,
|
||||
],
|
||||
[
|
||||
'type' => 'wechat',
|
||||
'name' => '微信',
|
||||
'platform' => ThirdPartyPlatform::WECHAT->value,
|
||||
'binded' => false,
|
||||
],
|
||||
[
|
||||
'type' => 'douyin',
|
||||
'name' => '抖音',
|
||||
'platform' => ThirdPartyPlatform::DOUYIN->value,
|
||||
'binded' => false,
|
||||
],
|
||||
[
|
||||
'type' => 'weibo',
|
||||
'name' => '微博',
|
||||
'platform' => ThirdPartyPlatform::WEIBO->value,
|
||||
'binded' => false,
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($thirdParties as $party) {
|
||||
foreach ($platforms as &$platform) {
|
||||
if ($platform['platform'] == $party['platform']) {
|
||||
$platform['binded'] = true;
|
||||
$platform['info'] = [
|
||||
'account' => $party['nickname'] ?? $party['openid'] ?? '',
|
||||
'avatar' => $party['avatar'] ?? '',
|
||||
'gender' => $party['gender'] ?? 0,
|
||||
'openid' => $party['openid'] ?? '',
|
||||
];
|
||||
|
||||
$bindTime = $party['updated_at'] ?? '';
|
||||
if ($bindTime) {
|
||||
$platform['bind_time'] = date('Y-m-d H:i:s', strtotime($bindTime));
|
||||
} else {
|
||||
$platform['bind_time'] = '';
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $platforms;
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定第三方账号
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function bindThirdParty(array $data): void
|
||||
{
|
||||
if (empty($data['platform']) || empty($data['openid'])) {
|
||||
throw new \Exception('平台和OpenID不能为空', 400);
|
||||
}
|
||||
|
||||
$platform = ThirdPartyPlatform::tryFrom((int)$data['platform']);
|
||||
if (!$platform) {
|
||||
throw new \Exception('无效的平台类型', 400);
|
||||
}
|
||||
|
||||
$currentMember = Container::make(CurrentMember::class);
|
||||
$memberId = $currentMember->id();
|
||||
|
||||
if (empty($memberId)) {
|
||||
throw new \Exception('用户未登录', 401);
|
||||
}
|
||||
|
||||
if ($this->dao->isBound($platform->value, $data['openid'])) {
|
||||
throw new \Exception('该第三方账号已被其他用户绑定', 400);
|
||||
}
|
||||
|
||||
$bindData = [
|
||||
'openid' => $data['openid'],
|
||||
'unionid' => $data['unionid'] ?? '',
|
||||
'nickname' => $data['nickname'] ?? '',
|
||||
'avatar' => $data['avatar'] ?? '',
|
||||
'gender' => $data['gender'] ?? 0,
|
||||
'country' => $data['country'] ?? '',
|
||||
'province' => $data['province'] ?? '',
|
||||
'city' => $data['city'] ?? '',
|
||||
'access_token' => $data['access_token'] ?? '',
|
||||
'refresh_token' => $data['refresh_token'] ?? '',
|
||||
];
|
||||
|
||||
$this->dao->bind($memberId, $platform->value, $data['openid'], $bindData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解绑第三方账号
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function unbindThirdParty(int $platform): void
|
||||
{
|
||||
$platformEnum = ThirdPartyPlatform::tryFrom($platform);
|
||||
if (!$platformEnum) {
|
||||
throw new \Exception('无效的平台类型', 400);
|
||||
}
|
||||
|
||||
$currentMember = Container::make(CurrentMember::class);
|
||||
$memberId = $currentMember->id();
|
||||
|
||||
if (empty($memberId)) {
|
||||
throw new \Exception('用户未登录', 401);
|
||||
}
|
||||
|
||||
$thirdParty = $this->dao->findByMemberAndPlatform($memberId, $platform);
|
||||
if (!$thirdParty) {
|
||||
throw new \Exception('未找到绑定记录', 404);
|
||||
}
|
||||
|
||||
$this->dao->unbind($thirdParty);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成第三方绑定二维码
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function generateBindQrCode(array $data): array
|
||||
{
|
||||
if (empty($data['platform'])) {
|
||||
throw new \Exception('平台不能为空');
|
||||
}
|
||||
|
||||
$platform = ThirdPartyPlatform::tryFrom((int)$data['platform']);
|
||||
if (!$platform) {
|
||||
throw new \Exception('无效的平台类型');
|
||||
}
|
||||
|
||||
$currentMember = Container::make(CurrentMember::class);
|
||||
$memberId = $currentMember->id();
|
||||
|
||||
if (empty($memberId)) {
|
||||
throw new \Exception('用户未登录', 401);
|
||||
}
|
||||
|
||||
$sceneId = 'bind_' . $platform->value . '_' . $memberId . '_' . time() . '_' . mt_rand(1000, 9999);
|
||||
|
||||
$bindUrl = $this->buildBindUrl($platform->value, $sceneId);
|
||||
|
||||
$bindInfo = [
|
||||
'scene_id' => $sceneId,
|
||||
'member_id' => $memberId,
|
||||
'platform' => $platform->value,
|
||||
'status' => 'pending',
|
||||
'created_at' => time(),
|
||||
];
|
||||
Redis::setex('third_party_bind:' . $sceneId, 300, json_encode($bindInfo));
|
||||
|
||||
return [
|
||||
'scene_id' => $sceneId,
|
||||
'qr_code' => $bindUrl,
|
||||
'expires_in' => 300,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查绑定二维码状态
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function checkBindQrStatus(string $sceneId): array
|
||||
{
|
||||
if (empty($sceneId)) {
|
||||
throw new \Exception('场景ID不能为空', 400);
|
||||
}
|
||||
|
||||
$cacheKey = 'third_party_bind:' . $sceneId;
|
||||
$bindInfo = Redis::get($cacheKey);
|
||||
|
||||
if (!$bindInfo) {
|
||||
throw new \Exception('二维码已过期或不存在', 400);
|
||||
}
|
||||
|
||||
$bindInfo = json_decode($bindInfo, true);
|
||||
|
||||
return [
|
||||
'scene_id' => $sceneId,
|
||||
'status' => $bindInfo['status'] ?? 'pending',
|
||||
'platform' => $bindInfo['platform'] ?? 0,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建绑定URL
|
||||
*/
|
||||
private function buildBindUrl(int $platform, string $sceneId): string
|
||||
{
|
||||
$baseUrl = config('app.url', 'http://localhost');
|
||||
|
||||
$bindUrls = [
|
||||
1 => 'https://graph.qq.com/oauth2.0/show?which=Login&display=pc&response_type=code&client_id=' . config('third_party.qq.app_id', '') . '&redirect_uri=' . urlencode($baseUrl . '/api/auth/qq/callback?scene=' . $sceneId),
|
||||
2 => 'https://open.weixin.qq.com/connect/qrconnect?appid=' . config('third_party.wechat.app_id', '') . '&redirect_uri=' . urlencode($baseUrl . '/api/auth/wechat/callback?scene=' . $sceneId) . '&response_type=code&scope=snsapi_login',
|
||||
3 => 'https://api.weibo.com/oauth2/authorize?client_id=' . config('third_party.weibo.app_id', '') . '&response_type=code&redirect_uri=' . urlencode($baseUrl . '/api/auth/weibo/callback?scene=' . $sceneId),
|
||||
4 => 'https://open.douyin.com/oauth2/authorize?client_key=' . config('third_party.douyin.app_id', '') . '&response_type=code&redirect_uri=' . urlencode($baseUrl . '/api/auth/douyin/callback?scene=' . $sceneId) . '&scope=user_info',
|
||||
];
|
||||
|
||||
$authUrl = $bindUrls[$platform] ?? '';
|
||||
|
||||
if (empty($authUrl)) {
|
||||
return '';
|
||||
}
|
||||
// 使用第三方二维码生成API
|
||||
return 'https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=' . urlencode($authUrl);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,423 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\service\api\member;
|
||||
|
||||
use app\dao\member\MemberDao;
|
||||
use app\dao\member\MemberPointsDao;
|
||||
use app\adminapi\event\PointsChangedEvent;
|
||||
use app\enum\member\PointSource;
|
||||
use app\enum\member\PointType;
|
||||
use core\base\BaseService;
|
||||
use support\Container;
|
||||
use support\Log;
|
||||
|
||||
class MemberPointsService extends BaseService
|
||||
{
|
||||
|
||||
public function __construct(
|
||||
private readonly MemberPointsDao $memberPointsDao,
|
||||
MemberDao $dao
|
||||
)
|
||||
{
|
||||
$this->dao = $dao;
|
||||
}
|
||||
|
||||
/**
|
||||
* 增加会员积分
|
||||
*
|
||||
* @param int|string $memberId 会员ID
|
||||
* @param int $points 积分数量
|
||||
* @param string $remark 备注
|
||||
* @param PointSource $source 积分来源
|
||||
* @param int|string|null $relatedId 关联ID
|
||||
*
|
||||
* @return float 新积分余额
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function addPoints(
|
||||
int|string $memberId,
|
||||
int $points,
|
||||
string $remark = '',
|
||||
PointSource $source = PointSource::OTHER,
|
||||
int|string|null $relatedId = null,
|
||||
?array $extra = null
|
||||
): float
|
||||
{
|
||||
$member = $this->dao->find($memberId);
|
||||
if (!$member) {
|
||||
throw new \Exception('会员不存在');
|
||||
}
|
||||
|
||||
$oldPoints = $member->points;
|
||||
$newPoints = $oldPoints + $points;
|
||||
|
||||
$event = new PointsChangedEvent(
|
||||
$memberId,
|
||||
$points,
|
||||
$source,
|
||||
PointType::INCREASE,
|
||||
$oldPoints,
|
||||
$newPoints,
|
||||
$remark,
|
||||
$relatedId,
|
||||
$extra
|
||||
);
|
||||
$event->dispatch();
|
||||
|
||||
return $this->getPointsBalance($memberId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 减少会员积分
|
||||
*
|
||||
* @param int|string $memberId 会员ID
|
||||
* @param int $points 积分数量
|
||||
* @param string $remark 备注
|
||||
* @param PointSource $source 积分来源
|
||||
* @param int|string|null $relatedId 关联ID
|
||||
*
|
||||
* @return float 新积分余额
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function deductPoints(
|
||||
int|string $memberId,
|
||||
int $points,
|
||||
string $remark = '',
|
||||
PointSource $source = PointSource::OTHER,
|
||||
int|string|null $relatedId = null,
|
||||
?array $extra = null
|
||||
): float
|
||||
{
|
||||
$member = $this->dao->find($memberId);
|
||||
if (!$member) {
|
||||
throw new \Exception('会员不存在');
|
||||
}
|
||||
|
||||
$oldPoints = $member->points;
|
||||
if ($oldPoints < $points) {
|
||||
throw new \Exception('积分不足');
|
||||
}
|
||||
$newPoints = $oldPoints - $points;
|
||||
|
||||
$event = new PointsChangedEvent(
|
||||
$memberId,
|
||||
$points,
|
||||
$source,
|
||||
PointType::DECREASE,
|
||||
$oldPoints,
|
||||
$newPoints,
|
||||
$remark,
|
||||
$relatedId,
|
||||
$extra
|
||||
);
|
||||
$event->dispatch();
|
||||
|
||||
return $this->getPointsBalance($memberId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会员积分记录
|
||||
*
|
||||
* @param int|string $memberId 会员ID
|
||||
* @param int $limit 记录数量
|
||||
*
|
||||
* @return array 积分记录列表
|
||||
*/
|
||||
public function getPointsRecords(int|string $memberId, int $limit = 20): array
|
||||
{
|
||||
return $this->memberPointsDao->getMemberPoints($memberId, $limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会员积分余额
|
||||
*
|
||||
* @param int|string $memberId 会员ID
|
||||
*
|
||||
* @return float 积分余额
|
||||
*/
|
||||
public function getPointsBalance(int|string $memberId): float
|
||||
{
|
||||
return $this->memberPointsDao->getMemberBalance($memberId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查会员积分是否足够
|
||||
*
|
||||
* @param int|string $memberId 会员ID
|
||||
* @param int $points 需要的积分数量
|
||||
*
|
||||
* @return bool 积分是否足够
|
||||
*/
|
||||
public function isPointsEnough(int|string $memberId, int $points): bool
|
||||
{
|
||||
$balance = $this->getPointsBalance($memberId);
|
||||
return $balance >= $points;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取积分流水记录(分页)
|
||||
*
|
||||
* @param array $params 查询参数
|
||||
*
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getPointsRecord(array $params): array
|
||||
{
|
||||
$memberId = $params['member_id'] ?? 0;
|
||||
$page = $params['page'] ?? 1;
|
||||
$limit = $params['limit'] ?? 20;
|
||||
$type = $params['type'] ?? null;
|
||||
|
||||
if (!$memberId) {
|
||||
throw new \Exception('会员ID不能为空');
|
||||
}
|
||||
|
||||
$offset = ($page - 1) * $limit;
|
||||
|
||||
$query = $this->memberPointsDao->query()
|
||||
->where('member_id', '=', $memberId);
|
||||
|
||||
if ($type) {
|
||||
$query->where('type', '=', $type);
|
||||
}
|
||||
|
||||
$total = $query->count();
|
||||
$items = $query->orderBy('created_at', 'desc')
|
||||
->offset($offset)
|
||||
->limit($limit)
|
||||
->get()
|
||||
->toArray();
|
||||
|
||||
return [
|
||||
'items' => $items,
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'limit' => $limit,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会员积分总额统计
|
||||
*
|
||||
* @param int|string $memberId 会员ID
|
||||
*
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getPointsTotal(int|string $memberId): array
|
||||
{
|
||||
if (!$memberId) {
|
||||
throw new \Exception('会员ID不能为空');
|
||||
}
|
||||
|
||||
$currentPoints = $this->getPointsBalance($memberId);
|
||||
|
||||
$monthStart = date('Y-m-01 00:00:00');
|
||||
$monthEnd = date('Y-m-t 23:59:59');
|
||||
|
||||
$monthlyPoints = $this->memberPointsDao->query()
|
||||
->where('member_id', '=', $memberId)
|
||||
->where('type', '=', PointType::INCREASE->value)
|
||||
->whereBetween('created_at', [$monthStart, $monthEnd])
|
||||
->sum('points');
|
||||
|
||||
$totalPoints = $this->memberPointsDao->query()
|
||||
->where('member_id', '=', $memberId)
|
||||
->where('type', '=', PointType::INCREASE->value)
|
||||
->sum('points');
|
||||
|
||||
return [
|
||||
'current_points' => (int)$currentPoints,
|
||||
'monthly_points' => (int)($monthlyPoints ?? 0),
|
||||
'total_points' => (int)($totalPoints ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会员等级信息
|
||||
*
|
||||
* @param int|string $memberId 会员ID
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getMemberLevel(int|string $memberId): array
|
||||
{
|
||||
if (!$memberId) {
|
||||
throw new \Exception('会员ID不能为空');
|
||||
}
|
||||
|
||||
$member = $this->dao->find($memberId);
|
||||
|
||||
if (!$member) {
|
||||
throw new \Exception('会员不存在');
|
||||
}
|
||||
|
||||
return [
|
||||
'level_id' => $member->level_id,
|
||||
'points' => $member->points,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 会员签到获取积分
|
||||
*
|
||||
* @param int|string $memberId 会员ID
|
||||
* @param int $continuousDays 连续签到天数
|
||||
*
|
||||
* @return int 获得的积分数量
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getSignPoints(int|string $memberId, int $continuousDays): int
|
||||
{
|
||||
// 每次签到固定+1积分
|
||||
$points = 1;
|
||||
|
||||
// 触发积分增加事件
|
||||
$this->addPoints(
|
||||
$memberId,
|
||||
$points,
|
||||
'每日签到',
|
||||
PointSource::SIGN_IN
|
||||
);
|
||||
|
||||
return $points;
|
||||
}
|
||||
|
||||
/**
|
||||
* 会员购物获取积分
|
||||
*
|
||||
* @param int|string $memberId 会员ID
|
||||
* @param float $amount 购物金额
|
||||
* @param int|string $orderId 订单ID
|
||||
*
|
||||
* @return int 获得的积分数量
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getShoppingPoints(int|string $memberId, float $amount, int|string $orderId): int
|
||||
{
|
||||
// 计算购物积分(例如:每消费10元获得1积分)
|
||||
$points = (int)($amount / 10);
|
||||
|
||||
if ($points > 0) {
|
||||
$this->addPoints(
|
||||
$memberId,
|
||||
$points,
|
||||
'购物获得积分',
|
||||
PointSource::SHOPPING,
|
||||
$orderId
|
||||
);
|
||||
}
|
||||
|
||||
return $points;
|
||||
}
|
||||
|
||||
/**
|
||||
* 会员邀请获取积分
|
||||
*
|
||||
* @param int|string $memberId 会员ID
|
||||
* @param int|string $invitedMemberId 被邀请的会员ID
|
||||
*
|
||||
* @return int 获得的积分数量
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getInvitePoints(int|string $memberId, int|string $invitedMemberId): int
|
||||
{
|
||||
// 邀请奖励积分
|
||||
$points = 50;
|
||||
|
||||
$this->addPoints(
|
||||
$memberId,
|
||||
$points,
|
||||
'邀请好友注册',
|
||||
PointSource::INVITE,
|
||||
$invitedMemberId
|
||||
);
|
||||
|
||||
return $points;
|
||||
}
|
||||
|
||||
/**
|
||||
* 会员注册获取积分
|
||||
*
|
||||
* @param int|string $memberId 会员ID
|
||||
*
|
||||
* @return int 获得的积分数量
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getRegisterPoints(int|string $memberId): int
|
||||
{
|
||||
// 注册奖励积分
|
||||
$points = 100;
|
||||
|
||||
$this->addPoints(
|
||||
$memberId,
|
||||
$points,
|
||||
'新会员注册奖励',
|
||||
PointSource::REGISTER
|
||||
);
|
||||
|
||||
return $points;
|
||||
}
|
||||
|
||||
/**
|
||||
* 会员兑换商品扣减积分
|
||||
*
|
||||
* @param int|string $memberId 会员ID
|
||||
* @param int $points 兑换需要的积分
|
||||
* @param int|string $productId 兑换商品ID
|
||||
*
|
||||
* @return float 扣减后的积分余额
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function deductExchangePoints(int|string $memberId, int $points, int|string $productId): float
|
||||
{
|
||||
return $this->deductPoints(
|
||||
$memberId,
|
||||
$points,
|
||||
'兑换商品',
|
||||
PointSource::EXCHANGE,
|
||||
$productId
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 积分过期扣减
|
||||
*
|
||||
* @param int|string $memberId 会员ID
|
||||
* @param int $points 过期的积分数量
|
||||
*
|
||||
* @return float 扣减后的积分余额
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function deductExpiredPoints(int|string $memberId, int $points): float
|
||||
{
|
||||
return $this->deductPoints(
|
||||
$memberId,
|
||||
$points,
|
||||
'积分过期',
|
||||
PointSource::EXPIRED
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 违规处罚扣减积分
|
||||
*
|
||||
* @param int|string $memberId 会员ID
|
||||
* @param int $points 处罚的积分数量
|
||||
* @param string $reason 处罚原因
|
||||
*
|
||||
* @return float 扣减后的积分余额
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function deductPenaltyPoints(int|string $memberId, int $points, string $reason): float
|
||||
{
|
||||
return $this->deductPoints(
|
||||
$memberId,
|
||||
$points,
|
||||
'违规处罚: ' . $reason,
|
||||
PointSource::PENALTY
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,363 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\service\api\member;
|
||||
|
||||
use app\api\CurrentMember;
|
||||
use app\dao\member\MemberDao;
|
||||
use app\dao\member\MemberThirdPartyDao;
|
||||
use app\model\member\Member;
|
||||
use app\model\member\MemberAddress;
|
||||
use app\enum\common\EnabledStatus;
|
||||
use core\base\BaseService;
|
||||
use support\Container;
|
||||
use support\Redis;
|
||||
|
||||
/**
|
||||
* 会员用户服务
|
||||
*/
|
||||
class MemberService extends BaseService
|
||||
{
|
||||
|
||||
/**
|
||||
* 构造方法
|
||||
*/
|
||||
public function __construct(MemberDao $dao)
|
||||
{
|
||||
$this->dao = $dao;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新会员信息
|
||||
*
|
||||
* @throws \Exception
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function updateInfo(array $data): void
|
||||
{
|
||||
$currentMember = Container::make(CurrentMember::class);
|
||||
$id = $currentMember->id();
|
||||
|
||||
if (empty($id)) {
|
||||
throw new \Exception('用户未登录', 401);
|
||||
}
|
||||
|
||||
$model = $this->dao->get($id);
|
||||
if (!$model) {
|
||||
throw new \Exception('用户不存在', 401);
|
||||
}
|
||||
|
||||
$allowedFields = ['nickname', 'avatar', 'gender', 'birthday','bio'];
|
||||
$updateData = [];
|
||||
foreach ($allowedFields as $field) {
|
||||
if (isset($data[$field])) {
|
||||
$updateData[$field] = $data[$field];
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($updateData)) {
|
||||
throw new \Exception('没有可更新的数据', 400);
|
||||
}
|
||||
|
||||
$model->fill($updateData);
|
||||
|
||||
if (!$model->isDirty()) {
|
||||
throw new \Exception('数据没有变化', 400);
|
||||
}
|
||||
|
||||
$model->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定会员手机号
|
||||
*/
|
||||
public function bindPhone(array $data): array
|
||||
{
|
||||
if (empty($data['phone']) || empty($data['verify_code'])) {
|
||||
throw new \Exception('手机号和验证码不能为空', 400);
|
||||
}
|
||||
|
||||
// 验证短信验证码
|
||||
$this->verifySmsCode($data['phone'], $data['verify_code']);
|
||||
|
||||
$memberId = $this->getCurrentMemberId();
|
||||
$member = Member::find($memberId);
|
||||
|
||||
if (!$member) {
|
||||
throw new \Exception('用户不存在', 401);
|
||||
}
|
||||
|
||||
// 检查手机号是否已被其他用户绑定
|
||||
$existingMember = Member::where('phone', $data['phone'])
|
||||
->where('id', '!=', $memberId)
|
||||
->first();
|
||||
|
||||
if ($existingMember) {
|
||||
throw new \Exception('手机号已被其他用户绑定', 400);
|
||||
}
|
||||
|
||||
// 更新手机号
|
||||
$member->phone = $data['phone'];
|
||||
$member->save();
|
||||
|
||||
return [
|
||||
'code' => 200,
|
||||
'msg' => '绑定成功',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改密码
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function changePassword(array $data): void
|
||||
{
|
||||
if (empty($data['old_password']) || empty($data['new_password'])) {
|
||||
throw new \Exception('旧密码和新密码不能为空', 400);
|
||||
}
|
||||
|
||||
$currentMember = Container::make(CurrentMember::class);
|
||||
$id = $currentMember->id();
|
||||
|
||||
if (empty($id)) {
|
||||
throw new \Exception('用户未登录', 401);
|
||||
}
|
||||
|
||||
$model = $this->dao->get($id);
|
||||
if (!$model) {
|
||||
throw new \Exception('用户不存在', 401);
|
||||
}
|
||||
|
||||
if (!$model->verifyPassword($data['old_password'])) {
|
||||
throw new \Exception('旧密码错误', 400);
|
||||
}
|
||||
|
||||
$model->password = $data['new_password'];
|
||||
$model->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取地址列表
|
||||
*/
|
||||
public function getAddressList(): array
|
||||
{
|
||||
$memberId = $this->getCurrentMemberId();
|
||||
$addresses = MemberAddress::where('member_id', $memberId)
|
||||
->where('enabled', EnabledStatus::ENABLED->value)
|
||||
->orderBy('is_default', 'desc')
|
||||
->orderBy('id', 'desc')
|
||||
->get();
|
||||
|
||||
return $addresses->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建地址
|
||||
*/
|
||||
public function createAddress(array $data): array
|
||||
{
|
||||
if (empty($data['name']) || empty($data['phone']) || empty($data['province']) ||
|
||||
empty($data['city']) || empty($data['district']) || empty($data['address'])) {
|
||||
throw new \Exception('请填写完整的地址信息', 400);
|
||||
}
|
||||
|
||||
$memberId = $this->getCurrentMemberId();
|
||||
|
||||
// 如果设置为默认地址,取消其他默认地址
|
||||
if ($data['is_default'] ?? false) {
|
||||
MemberAddress::where('member_id', $memberId)
|
||||
->where('is_default', true)
|
||||
->update(['is_default' => false]);
|
||||
}
|
||||
|
||||
$addressData = [
|
||||
'member_id' => $memberId,
|
||||
'name' => $data['name'],
|
||||
'phone' => $data['phone'],
|
||||
'province' => $data['province'],
|
||||
'city' => $data['city'],
|
||||
'district' => $data['district'],
|
||||
'address' => $data['address'],
|
||||
'is_default' => $data['is_default'] ?? false,
|
||||
'enabled' => EnabledStatus::ENABLED->value,
|
||||
];
|
||||
|
||||
$address = MemberAddress::create($addressData);
|
||||
|
||||
return ['id' => $address->id];
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新地址
|
||||
*/
|
||||
public function updateAddress(int $id, array $data): array
|
||||
{
|
||||
$memberId = $this->getCurrentMemberId();
|
||||
$address = MemberAddress::where('id', $id)
|
||||
->where('member_id', $memberId)
|
||||
->first();
|
||||
|
||||
if (!$address) {
|
||||
throw new \Exception('地址不存在', 404);
|
||||
}
|
||||
|
||||
// 如果设置为默认地址,取消其他默认地址
|
||||
if ($data['is_default'] ?? false) {
|
||||
MemberAddress::where('member_id', $memberId)
|
||||
->where('id', '!=', $id)
|
||||
->where('is_default', true)
|
||||
->update(['is_default' => false]);
|
||||
}
|
||||
|
||||
// 允许更新的字段
|
||||
$allowedFields = ['name', 'phone', 'province', 'city', 'district', 'address', 'is_default'];
|
||||
$updateData = [];
|
||||
|
||||
foreach ($allowedFields as $field) {
|
||||
if (isset($data[$field])) {
|
||||
$updateData[$field] = $data[$field];
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($updateData)) {
|
||||
throw new \Exception('没有可更新的数据', 400);
|
||||
}
|
||||
|
||||
$address->update($updateData);
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除地址
|
||||
*/
|
||||
public function deleteAddress(int $id): array
|
||||
{
|
||||
$memberId = $this->getCurrentMemberId();
|
||||
$address = MemberAddress::where('id', $id)
|
||||
->where('member_id', $memberId)
|
||||
->first();
|
||||
|
||||
if (!$address) {
|
||||
throw new \Exception('地址不存在', 404);
|
||||
}
|
||||
|
||||
// 软删除
|
||||
$address->enabled = EnabledStatus::DISABLED->value;
|
||||
$address->save();
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证短信验证码
|
||||
*/
|
||||
private function verifySmsCode(string $mobile, string $code): void
|
||||
{
|
||||
$cacheKey = 'sms_code:' . $mobile;
|
||||
$cachedCode = Redis::get($cacheKey);
|
||||
|
||||
if (!$cachedCode || $cachedCode !== $code) {
|
||||
throw new \Exception('验证码错误或已过期', 400);
|
||||
}
|
||||
|
||||
// 验证成功后删除验证码
|
||||
Redis::del($cacheKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户ID
|
||||
*/
|
||||
private function getCurrentMemberId(): int
|
||||
{
|
||||
$request = \request();
|
||||
$token = $request->header('Authorization');
|
||||
|
||||
if (!$token) {
|
||||
throw new \Exception('未登录', 401);
|
||||
}
|
||||
|
||||
$tokenData = Redis::get('user_token:' . md5($token));
|
||||
if (!$tokenData) {
|
||||
throw new \Exception('登录已过期', 401);
|
||||
}
|
||||
|
||||
$data = json_decode($tokenData, true);
|
||||
return $data['member_id'] ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传头像
|
||||
*/
|
||||
public function uploadAvatar(\Webman\Http\UploadFile $file): array
|
||||
{
|
||||
$memberId = $this->getCurrentMemberId();
|
||||
|
||||
// 验证文件类型
|
||||
$allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp'];
|
||||
if (!in_array($file->getUploadMimeType(), $allowedTypes)) {
|
||||
throw new \Exception('只支持jpg、png、gif、webp格式的图片', 400);
|
||||
}
|
||||
|
||||
// 验证文件大小(最大2MB)
|
||||
$maxSize = 2 * 1024 * 1024;
|
||||
if ($file->getSize() > $maxSize) {
|
||||
throw new \Exception('图片大小不能超过2MB', 400);
|
||||
}
|
||||
|
||||
// 生成文件名
|
||||
$extension = pathinfo($file->getUploadName(), PATHINFO_EXTENSION);
|
||||
$filename = 'avatar_' . $memberId . '_' . time() . '.' . $extension;
|
||||
|
||||
// 保存文件
|
||||
$uploadPath = public_path() . '/uploads/avatar/';
|
||||
if (!is_dir($uploadPath)) {
|
||||
mkdir($uploadPath, 0755, true);
|
||||
}
|
||||
|
||||
$file->moveTo($uploadPath . $filename);
|
||||
|
||||
// 生成访问URL
|
||||
$url = '/uploads/avatar/' . $filename;
|
||||
|
||||
// 更新会员头像
|
||||
$member = Member::find($memberId);
|
||||
if ($member) {
|
||||
$member->avatar = $url;
|
||||
$member->save();
|
||||
}
|
||||
|
||||
return [
|
||||
'url' => $url,
|
||||
'filename' => $filename
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取活跃用户
|
||||
*/
|
||||
public function getActiveUsers(int $limit = 10): array
|
||||
{
|
||||
$member = $this->dao->getActiveUsers($limit);
|
||||
return $member->map(function ($item) {
|
||||
return [
|
||||
'id' => $item->id,
|
||||
'username' => $item->username,
|
||||
'nickname' => $item->nickname,
|
||||
'avatar' => $item->avatar,
|
||||
'intro' => $item->intro ?? "",
|
||||
'last_active' => $item->last_time,
|
||||
];
|
||||
})->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否是管理员
|
||||
* 暂时返回 false,只有问题作者可以设置最佳答案
|
||||
*/
|
||||
public function isAdmin(int|string $userId): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
*+------------------
|
||||
* madong
|
||||
*+------------------
|
||||
* Copyright (c) https://gitee.com/motion-code All rights reserved.
|
||||
*+------------------
|
||||
* Author: Mr. April (405784684@qq.com)
|
||||
*+------------------
|
||||
* Official Website: https://madong.tech
|
||||
*/
|
||||
|
||||
namespace app\service\api\member;
|
||||
|
||||
use app\dao\member\MemberDao;
|
||||
use app\dao\member\MemberSignDao;
|
||||
use app\service\api\member\MemberPointsService;
|
||||
use app\model\member\MemberSign;
|
||||
use app\enum\member\PointSource;
|
||||
use core\base\BaseService;
|
||||
use core\exception\handler\BadRequestHttpException;
|
||||
use core\exception\handler\UnauthorizedHttpException;
|
||||
|
||||
/**
|
||||
* 会员签到服务
|
||||
*/
|
||||
class MemberSignService extends BaseService
|
||||
{
|
||||
|
||||
public function __construct(
|
||||
MemberDao $dao,
|
||||
private MemberSignDao $memberSignDao,
|
||||
private MemberPointsService $memberPointsService
|
||||
)
|
||||
{
|
||||
$this->dao = $dao;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行签到
|
||||
*
|
||||
* @throws \Exception
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function sign(int|string $memberId, array $deviceInfo = []): array
|
||||
{
|
||||
return $this->transaction(function () use ($memberId, $deviceInfo) {
|
||||
$today = date('Y-m-d');
|
||||
|
||||
// 检查今日是否已签到
|
||||
if ($this->memberSignDao->isSignedToday($memberId, $today)) {
|
||||
throw new BadRequestHttpException('今日已签到');
|
||||
}
|
||||
|
||||
// 获取最近一次签到记录
|
||||
$lastSign = $this->memberSignDao->getLastSign($memberId);
|
||||
|
||||
// 计算连续签到天数
|
||||
$continuousDays = 1;
|
||||
if ($lastSign) {
|
||||
$lastSignDate = $lastSign->sign_date;
|
||||
$yesterday = date('Y-m-d', strtotime('-1 day'));
|
||||
|
||||
if ($lastSignDate === $yesterday) {
|
||||
$continuousDays = $lastSign->continuous_days + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 创建签到记录
|
||||
$signRecord = $this->memberSignDao->created(
|
||||
$memberId,
|
||||
$today,
|
||||
0, // 暂时设为0,后续会通过积分服务更新
|
||||
$continuousDays,
|
||||
$deviceInfo
|
||||
);
|
||||
|
||||
// 增加会员积分(使用积分服务)
|
||||
$points = $this->memberPointsService->getSignPoints((int)$memberId, $continuousDays);
|
||||
|
||||
return [
|
||||
'points' => $points,
|
||||
'continuous_days' => $continuousDays,
|
||||
'sign_date' => $today,
|
||||
'sign_record' => $signRecord,
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取签到状态
|
||||
*/
|
||||
public function getStatus(int|string $memberId): array
|
||||
{
|
||||
$statistics = $this->memberSignDao->getSignStatistics($memberId);
|
||||
|
||||
return [
|
||||
'is_signed_today' => $statistics['today_signed'],
|
||||
'continuous_days' => $statistics['continuous_days'],
|
||||
'total_sign_days' => $statistics['total_sign_days'],
|
||||
'month_sign_days' => $statistics['month_sign_days'],
|
||||
'today' => date('Y-m-d'),
|
||||
'points' => $statistics['points'] ?? 0,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取签到日历
|
||||
*/
|
||||
public function getCalendar(int|string $memberId, int $year = null, int $month = null): array
|
||||
{
|
||||
$year = $year ?? (int)date('Y');
|
||||
$month = $month ?? (int)date('m');
|
||||
|
||||
$calendar = $this->memberSignDao->getSignCalendar($memberId, $year, $month);
|
||||
|
||||
return [
|
||||
'year' => $year,
|
||||
'month' => $month,
|
||||
'calendar' => $calendar,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取签到统计
|
||||
*/
|
||||
public function getStatistics(int|string $memberId, string $type = 'month'): array
|
||||
{
|
||||
$statistics = [];
|
||||
|
||||
switch ($type) {
|
||||
case 'week':
|
||||
$statistics = $this->memberSignDao->getWeekSignStatistics($memberId);
|
||||
break;
|
||||
case 'year':
|
||||
$statistics = $this->memberSignDao->getYearSignStatistics($memberId);
|
||||
break;
|
||||
case 'month':
|
||||
default:
|
||||
$basicStats = $this->memberSignDao->getSignStatistics($memberId);
|
||||
$statistics = [
|
||||
'total_sign_days' => $basicStats['total_sign_days'],
|
||||
'month_sign_days' => $basicStats['month_sign_days'],
|
||||
'continuous_days' => $basicStats['continuous_days'],
|
||||
'today_signed' => $basicStats['today_signed'],
|
||||
];
|
||||
break;
|
||||
}
|
||||
|
||||
return $statistics;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查今日是否已签到
|
||||
*/
|
||||
public function isSignedToday(int|string $memberId): bool
|
||||
{
|
||||
return $this->memberSignDao->isSignedToday($memberId, date('Y-m-d'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取连续签到天数
|
||||
*/
|
||||
public function getContinuousDays(int|string $memberId): int
|
||||
{
|
||||
$lastSign = $this->memberSignDao->getLastSign($memberId);
|
||||
|
||||
if (!$lastSign) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$lastSignDate = $lastSign->sign_date;
|
||||
$yesterday = date('Y-m-d', strtotime('-1 day'));
|
||||
|
||||
if ($lastSignDate === date('Y-m-d')) {
|
||||
// 今日已签到
|
||||
return $lastSign->continuous_days;
|
||||
} elseif ($lastSignDate === $yesterday) {
|
||||
// 昨日已签到
|
||||
return $lastSign->continuous_days;
|
||||
} else {
|
||||
// 连续签到中断
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取总签到天数
|
||||
*/
|
||||
public function getTotalSignDays(int|string $memberId): int
|
||||
{
|
||||
$statistics = $this->memberSignDao->getSignStatistics($memberId);
|
||||
return $statistics['total_sign_days'] ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取本月签到天数
|
||||
*/
|
||||
public function getMonthSignDays(int|string $memberId): int
|
||||
{
|
||||
return $this->memberSignDao->getMonthSignDays($memberId);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user