Просмотр исходного кода

feat:用户端-微信登录(邀请码)

刘学玺 4 месяцев назад
Родитель
Сommit
6eac65db64

+ 13 - 0
app/Exceptions/BusinessException.php

@@ -0,0 +1,13 @@
+<?php
+
+namespace App\Exceptions;
+
+use Exception;
+
+class BusinessException extends Exception
+{
+    public function __construct(string $message = '', int $code = 400)
+    {
+        parent::__construct($message, $code);
+    }
+}

+ 145 - 23
app/Services/Client/AccountService.php

@@ -3,11 +3,16 @@
 namespace App\Services\Client;
 
 use App\Enums\UserStatus;
+use App\Exceptions\BusinessException;
+use App\Models\CoachUser;
+use App\Models\MarketDistTeam;
 use App\Models\MemberSocialAccount;
 use App\Models\MemberUser;
 use App\Services\SmsService;
 use Illuminate\Support\Facades\Auth;
 use Illuminate\Support\Facades\Cache;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Log;
 
 class AccountService
 {
@@ -20,6 +25,11 @@ class AccountService
 
     /**
      * 发送验证码
+     * 业务逻辑:
+     * 1. 生成6位随机数字验证码
+     * 2. 将验证码保存到缓存中,有效期5分钟
+     * 3. 调用短信服务发送验证码
+     * 4. 返回发送成功消息和验证码
      */
     public function sendVerifyCode(string $mobile)
     {
@@ -37,10 +47,15 @@ class AccountService
 
     /**
      * 用户登录
+     * 业务逻辑:
+     * 1. 验证用户输入的验证码是否与缓存中的一致
+     * 2. 根据手机号查找用户,不存在则创建新用户
+     * 3. 新用户默认状态为开启,记录注册区域
+     * 4. 生成用户认证token
+     * 5. 返回token和用户信息
      */
     public function login(string $mobile, string $code)
     {
-
         // 验证验证码
         $cacheCode = Cache::get("verify_code:{$mobile}");
 
@@ -68,37 +83,137 @@ class AccountService
 
     /**
      * 微信登录
+     * 业务逻辑:
+     * 1. 根据openid查找或创建社交账号记录
+     * 2. 检查社交账号是否已关联用户
+     * 3. 未关联则创建新用户并建立关联
+     * 4. 更新用户微信相关信息
+     * 5. 生成用户认证token
+     * 6. 返回token和用户信息
+     *
+     * @throws BusinessException 业务异常
      */
-    public function wxLogin(string $openid)
+    public function wxLogin(string $openid, array $userInfo): array
     {
-        // 查找或创建微信用户
-        $socialAccount = MemberSocialAccount::firstOrCreate(
-            [
-                'platform' => 'WECHAT',
-                'social_id' => $openid,
-            ]
-        );
+        return DB::transaction(function () use ($openid, $userInfo) {
+            // 查找或创建社交账号
+            $socialAccount = MemberSocialAccount::firstOrCreate(
+                [
+                    'platform' => 'WECHAT',
+                    'social_id' => $openid,
+                ]
+            );
+
+            $user = $socialAccount->user;
+            $isNewUser = false;
+
+            if (! $user) {
+                // 创建新用户
+                $user = MemberUser::create([
+                    'state' => UserStatus::OPEN->value,
+                    'register_area' => request()->header('area_code'),
+                    'nickname' => $userInfo['nickname'] ?? null,
+                    'avatar' => $userInfo['avatar'] ?? null,
+                    'gender' => $userInfo['gender'] ?? null,
+                ]);
+
+                $socialAccount->update(['user_id' => $user->id]);
+                $isNewUser = true;
+            } else {
+                // 更新现有用户信息
+                $user->update([
+                    'nickname' => $userInfo['nickname'] ?? $user->nickname,
+                    'avatar' => $userInfo['avatar'] ?? $user->avatar,
+                    'gender' => $userInfo['gender'] ?? $user->gender,
+                ]);
+            }
+
+            // 处理邀请关系
+            if (isset($userInfo['invite_code']) && $isNewUser) {
+                $this->handleInviteRelation($user, $userInfo['invite_code']);
+            }
+
+            // 生成token
+            $token = $user->createToken('auth-token')->plainTextToken;
+
+            return [
+                'token' => $token,
+                'user' => $user->fresh(),
+            ];
+        });
+    }
 
-        $user = $socialAccount->user;
-        if (! $user) {
-            $user = MemberUser::create([
-                'state' => 'enable',
-                'register_area' => request()->header('area_code'),
+    /**
+     * 处理邀请关系
+     *
+     * @param  MemberUser  $user  新用户
+     * @param  string  $inviteCode  邀请码 (格式: type_id, 如 user_1, coach_1)
+     */
+    protected function handleInviteRelation(MemberUser $user, string $inviteCode): void
+    {
+        try {
+            // 解析邀请码
+            $parts = explode('_', $inviteCode);
+            if (count($parts) !== 2) {
+                Log::warning('Invalid invite code format', ['invite_code' => $inviteCode]);
+
+                return;
+            }
+
+            [$type, $id] = $parts;
+
+            // 根据类型查找邀请人
+            $inviter = match ($type) {
+                'user' => MemberUser::find($id),
+                'coach' => CoachUser::find($id),
+                default => null
+            };
+
+            if (! $inviter) {
+                Log::warning('Inviter not found', [
+                    'type' => $type,
+                    'id' => $id,
+                    'invite_code' => $inviteCode,
+                ]);
+
+                return;
+            }
+
+            // 检查用户是否已在营销团队中
+            $existingTeam = MarketDistTeam::where('user_id', $user->id)->exists();
+            if ($existingTeam) {
+                Log::info('User already in marketing team', ['user_id' => $user->id]);
+
+                return;
+            }
+
+            // 创建团队关系
+            DB::transaction(function () use ($user, $inviter) {
+                MarketDistTeam::create([
+                    'user_id' => $user->id,
+                    'owner_id' => $inviter->id,
+                    'owner_type' => $inviter::class,
+                    'level' => 1,
+                    'status' => 1,
+                ]);
+            });
+
+        } catch (\Exception $e) {
+            Log::error('Failed to handle invite relation', [
+                'user_id' => $user->id,
+                'invite_code' => $inviteCode,
+                'error' => $e->getMessage(),
             ]);
-            $socialAccount->update(['user_id' => $user->id]);
         }
-
-        // 生成token
-        $token = $user->createToken('auth-token')->plainTextToken;
-
-        return [
-            'token' => $token,
-            'user' => $user,
-        ];
     }
 
     /**
      * 用户退出
+     * 业务逻辑:
+     * 1. 根据用户ID查找用户
+     * 2. 验证用户是否存在
+     * 3. 删除用户所有token
+     * 4. 返回退出成功消息
      */
     public function logout(int $userId)
     {
@@ -112,6 +227,13 @@ class AccountService
 
     /**
      * 用户注销
+     * 业务逻辑:
+     * 1. 获取当前认证用户
+     * 2. 验证用户存在且状态为启用
+     * 3. 更新用户状态为禁用
+     * 4. 软删除用户记录
+     * 5. 删除用户所有token
+     * 6. 返回注销成功消息
      */
     public function deleteAccount()
     {

+ 46 - 24
app/Services/Client/OrderService.php

@@ -14,13 +14,11 @@ use App\Enums\TechnicianStatus;
 use App\Enums\TransactionType;
 use App\Enums\UserStatus;
 use App\Models\AgentInfo;
-use App\Models\CoachUser;
 use App\Models\CoachSchedule;
-use App\Models\EvaluationTag;
+use App\Models\CoachUser;
 use App\Models\MemberAddress;
 use App\Models\MemberUser;
 use App\Models\Order;
-use App\Models\OrderEvaluation;
 use App\Models\OrderGrabRecord;
 use App\Models\OrderRecord;
 use App\Models\Project;
@@ -59,6 +57,15 @@ readonly class OrderService
      * @return array 初始化的订单信息
      *
      * @throws \Exception
+     *
+     * 逻辑描述:
+     * 1. 验证初始化参数(project_id、coach_id)
+     * 2. 获取用户信息(包含钱包和地址)
+     * 3. 获取区域编码(从地址或参数)
+     * 4. 验证技师状态并获取可用时间
+     * 5. 获取项目详情(含价格)
+     * 6. 计算订单金额(含路费)
+     * 7. 返回初始化数据(钱包、技师、项目、地址、金额、可用时间)
      */
     public function initialize(int $userId, array $data): array
     {
@@ -113,6 +120,10 @@ readonly class OrderService
 
     /**
      * 验证初始化数据
+     *
+     * 逻辑描述:
+     * 1. 验证项目ID不能为空
+     * 2. 验证技师ID不能为空
      */
     private function validateInitializeData(array $data): void
     {
@@ -122,6 +133,11 @@ readonly class OrderService
 
     /**
      * 获取用户信息(包含钱包和地址)
+     *
+     * 逻辑描述:
+     * 1. 查询用户信息(关联钱包和地址)
+     * 2. 验证用户状态是否正常
+     * 3. 返回用户信息
      */
     private function getUserWithWalletAndAddress(int $userId): MemberUser
     {
@@ -137,6 +153,11 @@ readonly class OrderService
 
     /**
      * 获取区域编码
+     *
+     * 逻辑描述:
+     * 1. 优先使用地址中的区域编码
+     * 2. 其次使用参数中的区域编码
+     * 3. 验证区域编码不能为空
      */
     private function getAreaCode(?object $address, array $data): string
     {
@@ -148,6 +169,11 @@ readonly class OrderService
 
     /**
      * 获取项目详情
+     *
+     * 逻辑描述:
+     * 1. 调用项目服务获取详情
+     * 2. 验证项目是否存在
+     * 3. 返回项目信息
      */
     private function getProjectDetail(int $projectId, string $areaCode): Project
     {
@@ -159,6 +185,11 @@ readonly class OrderService
 
     /**
      * 记录错误日志
+     *
+     * 逻辑描述:
+     * 1. 记录错误信息
+     * 2. 记录上下文数据
+     * 3. 记录错误堆栈
      */
     private function logError(string $message, Exception $e, array $context = []): void
     {
@@ -184,7 +215,6 @@ readonly class OrderService
      *     distance: float,
      *     payment_type: int,
      *     type: int,
-     *
      * } $data 订单数据
      * @return array{
      *     order_id: int,
@@ -195,6 +225,17 @@ readonly class OrderService
      * }
      *
      * @throws \Exception
+     *
+     * 逻辑描述:
+     * 1. 验证基础数据(用户状态、项目状态、服务时间、技师状态等)
+     * 2. 计算订单金额(项目价格、路费、优惠等)
+     * 3. 处理加钟订单特殊逻辑(使用原订单地址和技师)
+     * 4. 获取服务地址信息
+     * 5. 创建订单数据
+     * 6. 创建订单相关记录
+     * 7. 处理余额支付(如果是余额支付)
+     * 8. 发送订单创建通知
+     * 9. 返回订单信息
      */
     public function createOrder(int $userId, array $data): array
     {
@@ -460,25 +501,6 @@ readonly class OrderService
         }
     }
 
-    /**
-     * 准备订单数据
-     */
-    private function prepareOrderData(array $data, Project $project, object $address, array $amounts): array
-    {
-        return [
-            'project_id' => $data['project_id'],
-            'coach_id' => $data['coach_id'] ?? null,
-            'duration' => $project->duration,
-            'address_id' => $address->id,
-            'use_balance' => $data['use_balance'] ?? false,
-            'service_time' => $data['service_time'],
-            ...array_filter($data, fn ($key) => in_array($key, [
-                'order_id',
-                'shop_id',
-            ]),
-        ];
-    }
-
     /**
      * 判断是否需要处理余额支付
      */
@@ -1667,7 +1689,7 @@ readonly class OrderService
             // 1. 验证用户状态和权限
             $user = $this->validateUserForCalculation($userId);
 
-            // 2. 获取项目信息和价格(包含代理价格)
+            // 2. 获取项目信息和价格(包含代理���价格)
             $project = $this->getProjectWithPrice($projectId, $agentId);
 
             // 3. 计算路费(如果有技师)

+ 187 - 0
app/Services/Client/WechatService.php

@@ -0,0 +1,187 @@
+<?php
+
+namespace App\Services\Client;
+
+use App\Exceptions\BusinessException;
+use EasyWeChat\OfficialAccount\Application;
+use Illuminate\Support\Facades\Cache;
+use Illuminate\Support\Facades\Log;
+use Overtrue\Socialite\Contracts\ProviderInterface;
+use Overtrue\Socialite\Contracts\UserInterface;
+
+class WechatService
+{
+    protected Application $app;
+
+    protected AccountService $accountService;
+
+    protected ProviderInterface $socialite;
+
+    public function __construct(AccountService $accountService)
+    {
+        $this->app = new Application(config('wechat.official_account.default'));
+        $this->accountService = $accountService;
+        $this->socialite = $this->app->getOAuth();
+    }
+
+    /**
+     * 获取微信授权URL
+     *
+     * @param  string  $redirectUrl  授权后重定向地址
+     * @param  string  $scope  授权范围 snsapi_base或snsapi_userinfo
+     * @return array{auth_url: string, state: string}
+     *
+     * @throws BusinessException
+     */
+    public function getAuthUrl(string $redirectUrl, ?string $scope = null): array
+    {
+        $scope = $scope ?? config('wechat.auth.default_scope');
+
+        try {
+            $state = $this->generateState();
+            $this->cacheAuthState($state);
+
+            $url = $this->socialite
+                ->scopes([$scope])
+                ->withState($state)
+                ->redirect($redirectUrl);
+
+            return [
+                'auth_url' => $url,
+                'state' => $state,
+            ];
+        } catch (\Exception $e) {
+            $this->logError('生成微信授权URL失败', $e);
+            throw new BusinessException('生成授权链接失败,请稍后重试');
+        }
+    }
+
+    /**
+     * 处理微信授权回调
+     *
+     * @param  string  $code  授权码
+     * @param  string  $state  状态码
+     * @param  string|null  $inviteCode  邀请码
+     * @return array{token: string, user: array}
+     *
+     * @throws BusinessException
+     */
+    public function handleAuthCallback(string $code, string $state, ?string $inviteCode = null): array
+    {
+        try {
+            // 验证state
+            $this->validateState($state);
+
+            // 获取用户信息
+            $user = $this->socialite->userFromCode($code);
+
+            // 整理用户信息
+            $userInfo = $this->formatUserInfo($user);
+
+            // 添加邀请人信息
+            if ($inviteCode) {
+                $userInfo['invite_code'] = $inviteCode;
+            }
+
+            // 执行登录
+            return $this->accountService->wxLogin($user->getId(), $userInfo);
+
+        } catch (BusinessException $e) {
+            throw $e;
+        } catch (\Exception $e) {
+            $this->logError('微信授权回调处理失败', $e, [
+                'code' => $code,
+                'state' => $state,
+                'invite_code' => $inviteCode,
+            ]);
+            throw new BusinessException('微信授权失败,请稍后重试');
+        }
+    }
+
+    /**
+     * 生成随机state
+     */
+    protected function generateState(): string
+    {
+        return md5(uniqid(microtime(true), true));
+    }
+
+    /**
+     * 缓存授权state
+     */
+    protected function cacheAuthState(string $state): void
+    {
+        Cache::put(
+            $this->getAuthStateKey($state),
+            true,
+            config('wechat.auth.cache_ttl')
+        );
+    }
+
+    /**
+     * 验证state
+     *
+     * @throws BusinessException
+     */
+    protected function validateState(string $state): void
+    {
+        if (! Cache::pull($this->getAuthStateKey($state))) {
+            throw new BusinessException('无效的授权请求');
+        }
+    }
+
+    /**
+     * 格式化用户信息
+     *
+     * @return array{
+     *     nickname: string,
+     *     avatar: string,
+     *     gender: string|null,
+     *     country: string|null,
+     *     province: string|null,
+     *     city: string|null
+     * }
+     */
+    protected function formatUserInfo(UserInterface $user): array
+    {
+        return [
+            'nickname' => $user->getName(),
+            'avatar' => $user->getAvatar(),
+            'gender' => $this->formatGender($user->getRaw()['sex'] ?? null),
+            'country' => $user->getRaw()['country'] ?? null,
+            'province' => $user->getRaw()['province'] ?? null,
+            'city' => $user->getRaw()['city'] ?? null,
+        ];
+    }
+
+    /**
+     * 格式化性别信息
+     *
+     * @param  int|null  $gender  微信返回的性别值:1为男性,2为女性,0为未知
+     * @return string|null male/female/null
+     */
+    protected function formatGender(?int $gender): ?string
+    {
+        return match ($gender) {
+            1 => 'male',
+            2 => 'female',
+            default => null,
+        };
+    }
+
+    /**
+     * 记录错误日志
+     */
+    protected function logError(string $message, \Exception $e, array $context = []): void
+    {
+        Log::error($message, array_merge($context, [
+            'error' => $e->getMessage(),
+            'trace' => $e->getTraceAsString(),
+        ]));
+    }
+
+    protected function getAuthStateKey(string $state): string
+    {
+        return config('wechat.auth.cache_prefix').$state;
+    }
+}

+ 1 - 1
app/Traits/ResponseTrait.php

@@ -7,7 +7,7 @@ trait ResponseTrait
     protected function success($data = [], $message = 'success')
     {
         return [
-            'code' => 0,
+            'code' => 200,
             'message' => $message,
             'data' => $data,
         ];